diff --git a/packages/orm/src/client/crud/dialects/postgresql.ts b/packages/orm/src/client/crud/dialects/postgresql.ts index 733af920c..92e570fe8 100644 --- a/packages/orm/src/client/crud/dialects/postgresql.ts +++ b/packages/orm/src/client/crud/dialects/postgresql.ts @@ -22,6 +22,7 @@ import { getDelegateDescendantModels, getManyToManyRelation, isRelationField, + isTypeDef, requireField, requireIdFields, requireModel, @@ -52,13 +53,25 @@ export class PostgresCrudDialect extends BaseCrudDiale invariant(false, 'should not reach here: AnyNull is not a valid input value'); } - if (Array.isArray(value)) { + // 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 + + if (isTypeDef(this.schema, type)) { + // type-def fields (regardless array or scalar) are stored as scalar `Json` and + // their input values need to be stringified if not already (i.e., provided in + // default values) + if (typeof value !== 'string') { + return JSON.stringify(value); + } else { + return value; + } + } else if (Array.isArray(value)) { 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 + // scalar `Json` fields need their input stringified return JSON.stringify(value); } else { + // `Json[]` fields need their input as array (not stringified) return value.map((v) => this.transformPrimitive(v, type, false)); } } else { diff --git a/tests/regression/test/issue-493.test.ts b/tests/regression/test/issue-493.test.ts new file mode 100644 index 000000000..269a68f33 --- /dev/null +++ b/tests/regression/test/issue-493.test.ts @@ -0,0 +1,94 @@ +import { createTestClient } from '@zenstackhq/testtools'; +import { describe, expect, it } from 'vitest'; + +describe('Issue 493 regression tests', () => { + it('should correctly handle JSON and typed-JSON array fields for PostgreSQL', async () => { + const schema = ` +type InlineButton { + id String + text String + callback_data String? + url String? + message String? + type String? +} + +type BotButton { + id String + label String + action String + enabled Boolean + order_index Int + message String + inline_buttons InlineButton[]? // Nested custom type +} + +model bot_settings { + id Int @id @default(autoincrement()) + setting_key String @unique + menu_buttons BotButton[] @json // Array of custom type + meta Meta @json +} + +type Meta { + info String +} + +model Foo { + id Int @id @default(autoincrement()) + data Json +} +`; + + const db = await createTestClient(schema, { provider: 'postgresql', debug: true }); + + // plain JSON non-array + await expect( + db.foo.create({ + data: { + data: { hello: 'world' }, + }, + }), + ).resolves.toMatchObject({ + data: { hello: 'world' }, + }); + + // plain JSON array + await expect( + db.foo.create({ + data: { + data: [{ hello: 'world' }], + }, + }), + ).resolves.toMatchObject({ + data: [{ hello: 'world' }], + }); + + // typed-JSON array & non-array + const input = { + setting_key: 'abc', + menu_buttons: [ + { + id: '1', + label: 'Button 1', + action: 'action_1', + enabled: true, + order_index: 1, + message: 'msg', + inline_buttons: [ + { + id: 'ib1', + text: 'Inline 1', + }, + ], + }, + ], + meta: { info: 'some info' }, + }; + await expect( + db.bot_settings.create({ + data: input, + }), + ).resolves.toMatchObject(input); + }); +});