Skip to content

Commit 2bd55ac

Browse files
committed
fix: workaround node-pg's issue with passing array to non-array JSON fields
1 parent a4a5cba commit 2bd55ac

File tree

8 files changed

+119
-32
lines changed

8 files changed

+119
-32
lines changed

packages/runtime/src/client/crud/dialects/base.ts

Lines changed: 9 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
3737

3838
abstract get provider(): DataSourceProviderType;
3939

40-
transformPrimitive(value: unknown, _type: BuiltinType) {
40+
transformPrimitive(value: unknown, _type: BuiltinType, _forArrayField: boolean) {
4141
return value;
4242
}
4343

@@ -363,7 +363,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
363363
continue;
364364
}
365365

366-
const value = this.transformPrimitive(_value, fieldType);
366+
const value = this.transformPrimitive(_value, fieldType, !!fieldDef.array);
367367

368368
switch (key) {
369369
case 'equals': {
@@ -437,7 +437,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
437437
}
438438

439439
private buildLiteralFilter(eb: ExpressionBuilder<any, any>, lhs: Expression<any>, type: BuiltinType, rhs: unknown) {
440-
return eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformPrimitive(rhs, type) : rhs);
440+
return eb(lhs, '=', rhs !== null && rhs !== undefined ? this.transformPrimitive(rhs, type, false) : rhs);
441441
}
442442

443443
private buildStandardFilter(
@@ -588,7 +588,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
588588
type,
589589
payload,
590590
buildFieldRef(this.schema, model, field, this.options, eb),
591-
(value) => this.transformPrimitive(value, type),
591+
(value) => this.transformPrimitive(value, type, false),
592592
(value) => this.buildNumberFilter(eb, model, table, field, type, value),
593593
);
594594
return this.and(eb, ...conditions);
@@ -605,7 +605,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
605605
'Boolean',
606606
payload,
607607
sql.ref(`${table}.${field}`),
608-
(value) => this.transformPrimitive(value, 'Boolean'),
608+
(value) => this.transformPrimitive(value, 'Boolean', false),
609609
(value) => this.buildBooleanFilter(eb, table, field, value as BooleanFilter<true>),
610610
true,
611611
['equals', 'not'],
@@ -624,7 +624,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
624624
'DateTime',
625625
payload,
626626
sql.ref(`${table}.${field}`),
627-
(value) => this.transformPrimitive(value, 'DateTime'),
627+
(value) => this.transformPrimitive(value, 'DateTime', false),
628628
(value) => this.buildDateTimeFilter(eb, table, field, value as DateTimeFilter<true>),
629629
true,
630630
);
@@ -642,7 +642,7 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
642642
'Bytes',
643643
payload,
644644
sql.ref(`${table}.${field}`),
645-
(value) => this.transformPrimitive(value, 'Bytes'),
645+
(value) => this.transformPrimitive(value, 'Bytes', false),
646646
(value) => this.buildBytesFilter(eb, table, field, value as BytesFilter<true>),
647647
true,
648648
['equals', 'in', 'notIn', 'not'],
@@ -793,11 +793,11 @@ export abstract class BaseCrudDialect<Schema extends SchemaDef> {
793793
}
794794

795795
public true(eb: ExpressionBuilder<any, any>): Expression<SqlBool> {
796-
return eb.lit<SqlBool>(this.transformPrimitive(true, 'Boolean') as boolean);
796+
return eb.lit<SqlBool>(this.transformPrimitive(true, 'Boolean', false) as boolean);
797797
}
798798

799799
public false(eb: ExpressionBuilder<any, any>): Expression<SqlBool> {
800-
return eb.lit<SqlBool>(this.transformPrimitive(false, 'Boolean') as boolean);
800+
return eb.lit<SqlBool>(this.transformPrimitive(false, 'Boolean', false) as boolean);
801801
}
802802

803803
public isTrue(expression: Expression<SqlBool>) {

packages/runtime/src/client/crud/dialects/postgresql.ts

Lines changed: 9 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,13 +25,20 @@ export class PostgresCrudDialect<Schema extends SchemaDef> extends BaseCrudDiale
2525
return 'postgresql' as const;
2626
}
2727

28-
override transformPrimitive(value: unknown, type: BuiltinType): unknown {
28+
override transformPrimitive(value: unknown, type: BuiltinType, forArrayField: boolean): unknown {
2929
if (value === undefined) {
3030
return value;
3131
}
3232

3333
if (Array.isArray(value)) {
34-
return value.map((v) => this.transformPrimitive(v, type));
34+
if (type === 'Json' && !forArrayField) {
35+
// node-pg incorrectly handles array values passed to non-array JSON fields,
36+
// the workaround is to JSON stringify the value
37+
// https://github.com/brianc/node-postgres/issues/374
38+
return JSON.stringify(value);
39+
} else {
40+
return value.map((v) => this.transformPrimitive(v, type, false));
41+
}
3542
} else {
3643
return match(type)
3744
.with('DateTime', () =>

packages/runtime/src/client/crud/dialects/sqlite.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -26,13 +26,13 @@ export class SqliteCrudDialect<Schema extends SchemaDef> extends BaseCrudDialect
2626
return 'sqlite' as const;
2727
}
2828

29-
override transformPrimitive(value: unknown, type: BuiltinType): unknown {
29+
override transformPrimitive(value: unknown, type: BuiltinType, _forArrayField: boolean): unknown {
3030
if (value === undefined) {
3131
return value;
3232
}
3333

3434
if (Array.isArray(value)) {
35-
return value.map((v) => this.transformPrimitive(v, type));
35+
return value.map((v) => this.transformPrimitive(v, type, false));
3636
} else {
3737
return match(type)
3838
.with('Boolean', () => (value ? 1 : 0))

packages/runtime/src/client/crud/operations/base.ts

Lines changed: 25 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -464,9 +464,17 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
464464
Array.isArray(value.set)
465465
) {
466466
// deal with nested "set" for scalar lists
467-
createFields[field] = this.dialect.transformPrimitive(value.set, fieldDef.type as BuiltinType);
467+
createFields[field] = this.dialect.transformPrimitive(
468+
value.set,
469+
fieldDef.type as BuiltinType,
470+
true,
471+
);
468472
} else {
469-
createFields[field] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType);
473+
createFields[field] = this.dialect.transformPrimitive(
474+
value,
475+
fieldDef.type as BuiltinType,
476+
!!fieldDef.array,
477+
);
470478
}
471479
} else {
472480
const subM2M = getManyToManyRelation(this.schema, model, field);
@@ -788,7 +796,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
788796
for (const [name, value] of Object.entries(item)) {
789797
const fieldDef = this.requireField(model, name);
790798
invariant(!fieldDef.relation, 'createMany does not support relations');
791-
newItem[name] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType);
799+
newItem[name] = this.dialect.transformPrimitive(value, fieldDef.type as BuiltinType, !!fieldDef.array);
792800
}
793801
if (fromRelation) {
794802
for (const { fk, pk } of relationKeyPairs) {
@@ -831,7 +839,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
831839
}
832840
} else if (fields[field]?.updatedAt) {
833841
// TODO: should this work at kysely level instead?
834-
values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime');
842+
values[field] = this.dialect.transformPrimitive(new Date(), 'DateTime', false);
835843
}
836844
}
837845
}
@@ -934,7 +942,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
934942
if (finalData === data) {
935943
finalData = clone(data);
936944
}
937-
finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime');
945+
finalData[fieldName] = this.dialect.transformPrimitive(new Date(), 'DateTime', false);
938946
}
939947
}
940948

@@ -972,7 +980,11 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
972980
continue;
973981
}
974982

975-
updateFields[field] = this.dialect.transformPrimitive(finalData[field], fieldDef.type as BuiltinType);
983+
updateFields[field] = this.dialect.transformPrimitive(
984+
finalData[field],
985+
fieldDef.type as BuiltinType,
986+
!!fieldDef.array,
987+
);
976988
} else {
977989
if (!allowRelationUpdate) {
978990
throw new QueryError(`Relation update not allowed for field "${field}"`);
@@ -1054,7 +1066,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
10541066
);
10551067

10561068
const key = Object.keys(payload)[0];
1057-
const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType);
1069+
const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType, false);
10581070
const eb = expressionBuilder<any, any>();
10591071
const fieldRef = buildFieldRef(this.schema, model, field, this.options, eb);
10601072

@@ -1077,7 +1089,7 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
10771089
) {
10781090
invariant(Object.keys(payload).length === 1, 'Only one of "set", "push" can be provided');
10791091
const key = Object.keys(payload)[0];
1080-
const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType);
1092+
const value = this.dialect.transformPrimitive(payload[key!], fieldDef.type as BuiltinType, true);
10811093
const eb = expressionBuilder<any, any>();
10821094
const fieldRef = buildFieldRef(this.schema, model, field, this.options, eb);
10831095

@@ -1125,7 +1137,11 @@ export abstract class BaseOperationHandler<Schema extends SchemaDef> {
11251137
if (isRelationField(this.schema, model, field)) {
11261138
continue;
11271139
}
1128-
updateFields[field] = this.dialect.transformPrimitive(data[field], fieldDef.type as BuiltinType);
1140+
updateFields[field] = this.dialect.transformPrimitive(
1141+
data[field],
1142+
fieldDef.type as BuiltinType,
1143+
!!fieldDef.array,
1144+
);
11291145
}
11301146

11311147
let query = kysely.updateTable(model).set(updateFields);

packages/runtime/src/plugins/policy/expression-transformer.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -275,7 +275,7 @@ export class ExpressionTransformer<Schema extends SchemaDef> {
275275
}
276276

277277
private transformValue(value: unknown, type: BuiltinType) {
278-
return ValueNode.create(this.dialect.transformPrimitive(value, type) ?? null);
278+
return ValueNode.create(this.dialect.transformPrimitive(value, type, false) ?? null);
279279
}
280280

281281
@expr('unary')

packages/runtime/src/plugins/policy/policy-handler.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,12 +185,16 @@ export class PolicyHandler<Schema extends SchemaDef> extends OperationNodeTransf
185185
invariant(item.kind === 'ValueNode', 'expecting a ValueNode');
186186
result.push({
187187
node: ValueNode.create(
188-
this.dialect.transformPrimitive((item as ValueNode).value, fieldDef.type as BuiltinType),
188+
this.dialect.transformPrimitive(
189+
(item as ValueNode).value,
190+
fieldDef.type as BuiltinType,
191+
!!fieldDef.array,
192+
),
189193
),
190194
raw: (item as ValueNode).value,
191195
});
192196
} else {
193-
const value = this.dialect.transformPrimitive(item, fieldDef.type as BuiltinType);
197+
const value = this.dialect.transformPrimitive(item, fieldDef.type as BuiltinType, !!fieldDef.array);
194198
if (Array.isArray(value)) {
195199
result.push({
196200
node: RawNode.createWithSql(this.dialect.buildArrayLiteralSQL(value)),

packages/runtime/src/plugins/policy/utils.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,14 +19,14 @@ import type { SchemaDef } from '../../schema';
1919
* Creates a `true` value node.
2020
*/
2121
export function trueNode<Schema extends SchemaDef>(dialect: BaseCrudDialect<Schema>) {
22-
return ValueNode.createImmediate(dialect.transformPrimitive(true, 'Boolean'));
22+
return ValueNode.createImmediate(dialect.transformPrimitive(true, 'Boolean', false));
2323
}
2424

2525
/**
2626
* Creates a `false` value node.
2727
*/
2828
export function falseNode<Schema extends SchemaDef>(dialect: BaseCrudDialect<Schema>) {
29-
return ValueNode.createImmediate(dialect.transformPrimitive(false, 'Boolean'));
29+
return ValueNode.createImmediate(dialect.transformPrimitive(false, 'Boolean', false));
3030
}
3131

3232
/**

packages/runtime/test/client-api/type-coverage.test.ts

Lines changed: 65 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,6 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', (
2626
`
2727
model Foo {
2828
id String @id @default(cuid())
29-
3029
String String
3130
Int Int
3231
BigInt BigInt
@@ -36,8 +35,6 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', (
3635
Boolean Boolean
3736
Bytes Bytes
3837
Json Json
39-
40-
@@allow('all', true)
4138
}
4239
`,
4340
{ provider, dbName: PG_DB_NAME },
@@ -50,6 +47,42 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', (
5047
}
5148
});
5249

50+
it('supports all types - default values', async () => {
51+
let db: any;
52+
try {
53+
db = await createTestClient(
54+
`
55+
model Foo {
56+
id String @id @default(cuid())
57+
String String @default("default")
58+
Int Int @default(100)
59+
BigInt BigInt @default(9007199254740991)
60+
DateTime DateTime @default("2021-01-01T00:00:00.000Z")
61+
Float Float @default(1.23)
62+
Decimal Decimal @default(1.2345)
63+
Boolean Boolean @default(true)
64+
Json Json @default("{\\"foo\\":\\"bar\\"}")
65+
}
66+
`,
67+
{ provider, dbName: PG_DB_NAME },
68+
);
69+
70+
await db.foo.create({ data: {} });
71+
await expect(db.foo.findUnique({ where: { id: '1' } })).resolves.toMatchObject({
72+
String: 'default',
73+
Int: 100,
74+
BigInt: BigInt(9007199254740991),
75+
DateTime: new Date('2021-01-01T00:00:00.000Z'),
76+
Float: 1.23,
77+
Decimal: new Decimal(1.2345),
78+
Boolean: true,
79+
Json: { foo: 'bar' },
80+
});
81+
} finally {
82+
await db?.$disconnect();
83+
}
84+
});
85+
5386
it('supports all types - array', async () => {
5487
if (provider === 'sqlite') {
5588
return;
@@ -66,7 +99,7 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', (
6699
Decimal: [new Decimal(1.2345)],
67100
Boolean: [true],
68101
Bytes: [new Uint8Array([1, 2, 3, 4])],
69-
Json: [{ foo: 'bar' }],
102+
Json: [{ hello: 'world' }],
70103
};
71104

72105
let db: any;
@@ -85,8 +118,35 @@ describe.each(['sqlite', 'postgresql'] as const)('zmodel type coverage tests', (
85118
Boolean Boolean[]
86119
Bytes Bytes[]
87120
Json Json[]
121+
}
122+
`,
123+
{ provider, dbName: PG_DB_NAME },
124+
);
88125

89-
@@allow('all', true)
126+
await db.foo.create({ data });
127+
await expect(db.foo.findUnique({ where: { id: '1' } })).resolves.toMatchObject(data);
128+
} finally {
129+
await db?.$disconnect();
130+
}
131+
});
132+
133+
it('supports all types - array for plain json field', async () => {
134+
if (provider === 'sqlite') {
135+
return;
136+
}
137+
138+
const data = {
139+
id: '1',
140+
Json: [{ hello: 'world' }],
141+
};
142+
143+
let db: any;
144+
try {
145+
db = await createTestClient(
146+
`
147+
model Foo {
148+
id String @id @default(cuid())
149+
Json Json
90150
}
91151
`,
92152
{ provider, dbName: PG_DB_NAME },

0 commit comments

Comments
 (0)