diff --git a/packages/runtime/src/client/crud/dialects/base.ts b/packages/runtime/src/client/crud/dialects/base.ts index 98574b86..daa154e9 100644 --- a/packages/runtime/src/client/crud/dialects/base.ts +++ b/packages/runtime/src/client/crud/dialects/base.ts @@ -37,7 +37,7 @@ export abstract class BaseCrudDialect { abstract get provider(): DataSourceProviderType; - transformPrimitive(value: unknown, _type: BuiltinType) { + transformPrimitive(value: unknown, _type: BuiltinType, _forArrayField: boolean) { return value; } @@ -363,7 +363,7 @@ export abstract class BaseCrudDialect { continue; } - const value = this.transformPrimitive(_value, fieldType); + const value = this.transformPrimitive(_value, fieldType, !!fieldDef.array); switch (key) { case 'equals': { @@ -437,7 +437,7 @@ export abstract class BaseCrudDialect { } private buildLiteralFilter(eb: ExpressionBuilder, lhs: Expression, type: BuiltinType, rhs: unknown) { - return eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformPrimitive(rhs, type) : rhs); + return eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformPrimitive(rhs, type, false) : rhs); } private buildStandardFilter( @@ -588,7 +588,7 @@ export abstract class BaseCrudDialect { type, payload, buildFieldRef(this.schema, model, field, this.options, eb), - (value) => this.transformPrimitive(value, type), + (value) => this.transformPrimitive(value, type, false), (value) => this.buildNumberFilter(eb, model, table, field, type, value), ); return this.and(eb, ...conditions); @@ -605,7 +605,7 @@ export abstract class BaseCrudDialect { 'Boolean', payload, sql.ref(`${table}.${field}`), - (value) => this.transformPrimitive(value, 'Boolean'), + (value) => this.transformPrimitive(value, 'Boolean', false), (value) => this.buildBooleanFilter(eb, table, field, value as BooleanFilter), true, ['equals', 'not'], @@ -624,7 +624,7 @@ export abstract class BaseCrudDialect { 'DateTime', payload, sql.ref(`${table}.${field}`), - (value) => this.transformPrimitive(value, 'DateTime'), + (value) => this.transformPrimitive(value, 'DateTime', false), (value) => this.buildDateTimeFilter(eb, table, field, value as DateTimeFilter), true, ); @@ -642,7 +642,7 @@ export abstract class BaseCrudDialect { 'Bytes', payload, sql.ref(`${table}.${field}`), - (value) => this.transformPrimitive(value, 'Bytes'), + (value) => this.transformPrimitive(value, 'Bytes', false), (value) => this.buildBytesFilter(eb, table, field, value as BytesFilter), true, ['equals', 'in', 'notIn', 'not'], @@ -793,11 +793,11 @@ export abstract class BaseCrudDialect { } public true(eb: ExpressionBuilder): Expression { - return eb.lit(this.transformPrimitive(true, 'Boolean') as boolean); + return eb.lit(this.transformPrimitive(true, 'Boolean', false) as boolean); } public false(eb: ExpressionBuilder): Expression { - return eb.lit(this.transformPrimitive(false, 'Boolean') as boolean); + return eb.lit(this.transformPrimitive(false, 'Boolean', false) as boolean); } public isTrue(expression: Expression) { diff --git a/packages/runtime/src/client/crud/dialects/postgresql.ts b/packages/runtime/src/client/crud/dialects/postgresql.ts index efe09a35..44869b51 100644 --- a/packages/runtime/src/client/crud/dialects/postgresql.ts +++ b/packages/runtime/src/client/crud/dialects/postgresql.ts @@ -25,13 +25,20 @@ export class PostgresCrudDialect extends BaseCrudDiale return 'postgresql' as const; } - override transformPrimitive(value: unknown, type: BuiltinType): unknown { + override transformPrimitive(value: unknown, type: BuiltinType, forArrayField: boolean): unknown { if (value === undefined) { return value; } if (Array.isArray(value)) { - return value.map((v) => this.transformPrimitive(v, type)); + if (type === 'Json' && !forArrayField) { + // node-pg incorrectly handles array values passed to non-array JSON fields, + // the workaround is to JSON stringify the value + // https://github.com/brianc/node-postgres/issues/374 + return JSON.stringify(value); + } else { + return value.map((v) => this.transformPrimitive(v, type, false)); + } } else { return match(type) .with('DateTime', () => diff --git a/packages/runtime/src/client/crud/dialects/sqlite.ts b/packages/runtime/src/client/crud/dialects/sqlite.ts index 2d919745..7fa67905 100644 --- a/packages/runtime/src/client/crud/dialects/sqlite.ts +++ b/packages/runtime/src/client/crud/dialects/sqlite.ts @@ -26,13 +26,13 @@ export class SqliteCrudDialect extends BaseCrudDialect return 'sqlite' as const; } - override transformPrimitive(value: unknown, type: BuiltinType): unknown { + override transformPrimitive(value: unknown, type: BuiltinType, _forArrayField: boolean): unknown { if (value === undefined) { return value; } if (Array.isArray(value)) { - return value.map((v) => this.transformPrimitive(v, type)); + return value.map((v) => this.transformPrimitive(v, type, false)); } else { return match(type) .with('Boolean', () => (value ? 1 : 0)) diff --git a/packages/runtime/src/client/crud/operations/base.ts b/packages/runtime/src/client/crud/operations/base.ts index 7dfbd43c..26a75376 100644 --- a/packages/runtime/src/client/crud/operations/base.ts +++ b/packages/runtime/src/client/crud/operations/base.ts @@ -464,9 +464,17 @@ export abstract class BaseOperationHandler { Array.isArray(value.set) ) { // deal with nested "set" for scalar lists - createFields[field] = this.dialect.transformPrimitive(value.set, fieldDef.type as BuiltinType); + createFields[field] = this.dialect.transformPrimitive( + value.set, + fieldDef.type as BuiltinType, + true, + ); } else { - createFields[field] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType); + createFields[field] = this.dialect.transformPrimitive( + value, + fieldDef.type as BuiltinType, + !!fieldDef.array, + ); } } else { const subM2M = getManyToManyRelation(this.schema, model, field); @@ -788,7 +796,7 @@ export abstract class BaseOperationHandler { for (const [name, value] of Object.entries(item)) { const fieldDef = this.requireField(model, name); invariant(!fieldDef.relation, 'createMany does not support relations'); - newItem[name] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType); + newItem[name] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType, !!fieldDef.array); } if (fromRelation) { for (const { fk, pk } of relationKeyPairs) { @@ -831,7 +839,7 @@ export abstract class BaseOperationHandler { } } else if (fields[field]?.updatedAt) { // TODO: should this work at kysely level instead? - values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime'); + values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); } } } @@ -934,7 +942,7 @@ export abstract class BaseOperationHandler { if (finalData === data) { finalData = clone(data); } - finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime'); + finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false); } } @@ -972,7 +980,11 @@ export abstract class BaseOperationHandler { continue; } - updateFields[field] = this.dialect.transformPrimitive(finalData[field], fieldDef.type as BuiltinType); + updateFields[field] = this.dialect.transformPrimitive( + finalData[field], + fieldDef.type as BuiltinType, + !!fieldDef.array, + ); } else { if (!allowRelationUpdate) { throw new QueryError(`Relation update not allowed for field "${field}"`); @@ -1054,7 +1066,7 @@ export abstract class BaseOperationHandler { ); const key = Object.keys(payload)[0]; - const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType); + const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType, false); const eb = expressionBuilder(); const fieldRef = buildFieldRef(this.schema, model, field, this.options, eb); @@ -1077,7 +1089,7 @@ export abstract class BaseOperationHandler { ) { invariant(Object.keys(payload).length === 1, 'Only one of "set", "push" can be provided'); const key = Object.keys(payload)[0]; - const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType); + const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType, true); const eb = expressionBuilder(); const fieldRef = buildFieldRef(this.schema, model, field, this.options, eb); @@ -1125,7 +1137,11 @@ export abstract class BaseOperationHandler { if (isRelationField(this.schema, model, field)) { continue; } - updateFields[field] = this.dialect.transformPrimitive(data[field], fieldDef.type as BuiltinType); + updateFields[field] = this.dialect.transformPrimitive( + data[field], + fieldDef.type as BuiltinType, + !!fieldDef.array, + ); } let query = kysely.updateTable(model).set(updateFields); diff --git a/packages/runtime/src/plugins/policy/expression-transformer.ts b/packages/runtime/src/plugins/policy/expression-transformer.ts index 23725791..bbc98881 100644 --- a/packages/runtime/src/plugins/policy/expression-transformer.ts +++ b/packages/runtime/src/plugins/policy/expression-transformer.ts @@ -275,7 +275,7 @@ export class ExpressionTransformer { } private transformValue(value: unknown, type: BuiltinType) { - return ValueNode.create(this.dialect.transformPrimitive(value, type) ?? null); + return ValueNode.create(this.dialect.transformPrimitive(value, type, false) ?? null); } @expr('unary') diff --git a/packages/runtime/src/plugins/policy/policy-handler.ts b/packages/runtime/src/plugins/policy/policy-handler.ts index 31b7174c..9423bd5a 100644 --- a/packages/runtime/src/plugins/policy/policy-handler.ts +++ b/packages/runtime/src/plugins/policy/policy-handler.ts @@ -185,12 +185,16 @@ export class PolicyHandler extends OperationNodeTransf invariant(item.kind === 'ValueNode', 'expecting a ValueNode'); result.push({ node: ValueNode.create( - this.dialect.transformPrimitive((item as ValueNode).value, fieldDef.type as BuiltinType), + this.dialect.transformPrimitive( + (item as ValueNode).value, + fieldDef.type as BuiltinType, + !!fieldDef.array, + ), ), raw: (item as ValueNode).value, }); } else { - const value = this.dialect.transformPrimitive(item, fieldDef.type as BuiltinType); + const value = this.dialect.transformPrimitive(item, fieldDef.type as BuiltinType, !!fieldDef.array); if (Array.isArray(value)) { result.push({ node: RawNode.createWithSql(this.dialect.buildArrayLiteralSQL(value)), diff --git a/packages/runtime/src/plugins/policy/utils.ts b/packages/runtime/src/plugins/policy/utils.ts index 01960533..3c2e641d 100644 --- a/packages/runtime/src/plugins/policy/utils.ts +++ b/packages/runtime/src/plugins/policy/utils.ts @@ -19,14 +19,14 @@ import type { SchemaDef } from '../../schema'; * Creates a `true` value node. */ export function trueNode(dialect: BaseCrudDialect) { - return ValueNode.createImmediate(dialect.transformPrimitive(true, 'Boolean')); + return ValueNode.createImmediate(dialect.transformPrimitive(true, 'Boolean', false)); } /** * Creates a `false` value node. */ export function falseNode(dialect: BaseCrudDialect) { - return ValueNode.createImmediate(dialect.transformPrimitive(false, 'Boolean')); + return ValueNode.createImmediate(dialect.transformPrimitive(false, 'Boolean', false)); } /** diff --git a/packages/runtime/test/client-api/type-coverage.test.ts b/packages/runtime/test/client-api/type-coverage.test.ts index 81aa706d..50f3e2bb 100644 --- a/packages/runtime/test/client-api/type-coverage.test.ts +++ b/packages/runtime/test/client-api/type-coverage.test.ts @@ -26,7 +26,6 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', ( ` model Foo { id String @id @default(cuid()) - String String Int Int BigInt BigInt @@ -36,8 +35,6 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', ( Boolean Boolean Bytes Bytes Json Json - - @@allow('all', true) } `, { provider, dbName: PG_DB_NAME }, @@ -50,6 +47,42 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', ( } }); + it('supports all types - default values', async () => { + let db: any; + try { + db = await createTestClient( + ` + model Foo { + id String @id @default(cuid()) + String String @default("default") + Int Int @default(100) + BigInt BigInt @default(9007199254740991) + DateTime DateTime @default("2021-01-01T00:00:00.000Z") + Float Float @default(1.23) + Decimal Decimal @default(1.2345) + Boolean Boolean @default(true) + Json Json @default("{\\"foo\\":\\"bar\\"}") + } + `, + { provider, dbName: PG_DB_NAME }, + ); + + await db.foo.create({ data: { id: '1' } }); + await expect(db.foo.findUnique({ where: { id: '1' } })).resolves.toMatchObject({ + String: 'default', + Int: 100, + BigInt: BigInt(9007199254740991), + DateTime: expect.any(Date), + Float: 1.23, + Decimal: new Decimal(1.2345), + Boolean: true, + Json: { foo: 'bar' }, + }); + } finally { + await db?.$disconnect(); + } + }); + it('supports all types - array', async () => { if (provider === 'sqlite') { return; @@ -66,7 +99,7 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', ( Decimal: [new Decimal(1.2345)], Boolean: [true], Bytes: [new Uint8Array([1, 2, 3, 4])], - Json: [{ foo: 'bar' }], + Json: [{ hello: 'world' }], }; let db: any; @@ -85,8 +118,35 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', ( Boolean Boolean[] Bytes Bytes[] Json Json[] + } + `, + { provider, dbName: PG_DB_NAME }, + ); - @@allow('all', true) + 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 for plain json field', async () => { + if (provider === 'sqlite') { + return; + } + + const data = { + id: '1', + Json: [{ hello: 'world' }], + }; + + let db: any; + try { + db = await createTestClient( + ` + model Foo { + id String @id @default(cuid()) + Json Json } `, { provider, dbName: PG_DB_NAME },