diff --git a/.changeset/nine-bulldogs-learn.md b/.changeset/nine-bulldogs-learn.md new file mode 100644 index 0000000000..b09b11e98d --- /dev/null +++ b/.changeset/nine-bulldogs-learn.md @@ -0,0 +1,6 @@ +--- +"electric-sql": patch +"@electric-sql/prisma-generator": patch +--- + +[VAX-825] Add client-side support for JSON type. diff --git a/.changeset/strange-bottles-carry.md b/.changeset/strange-bottles-carry.md new file mode 100644 index 0000000000..6a7a1b790c --- /dev/null +++ b/.changeset/strange-bottles-carry.md @@ -0,0 +1,5 @@ +--- +"@core/electric": patch +--- + +[VAX-825] Add support for the JSONB column type in electrified tables. diff --git a/clients/typescript/src/cli/migrations/migrate.ts b/clients/typescript/src/cli/migrations/migrate.ts index ada54edc23..932d4a55b1 100644 --- a/clients/typescript/src/cli/migrations/migrate.ts +++ b/clients/typescript/src/cli/migrations/migrate.ts @@ -8,6 +8,7 @@ import decompress from 'decompress' import { buildMigrations, getMigrationNames } from './builder' import { exec } from 'child_process' import { dedent } from 'ts-dedent' +import { findAndReplaceInFile } from '../util' const appRoot = path.resolve() // path where the user ran `npx electric migrate` @@ -182,6 +183,12 @@ async function _generate(opts: Omit) { console.log('Generating Electric client...') await generateElectricClient(prismaSchema) const relativePath = path.relative(appRoot, opts.out) + // Modify the type of JSON input values in the generated Prisma client + // because we deviate from Prisma's typing for JSON values + const outDir = opts.out + await extendJsonType(outDir) + // Delete all files generated for the Prisma client, except the typings + await keepOnlyPrismaTypings(outDir) console.log(`Successfully generated Electric client at: ./${relativePath}`) // Build the migrations @@ -227,16 +234,17 @@ async function createPrismaSchema( ) const output = path.resolve(out) const schema = dedent` - generator client { - provider = "prisma-client-js" - } - generator electric { provider = "${escapePathForString(provider)}" output = "${escapePathForString(output)}" relationModel = "false" } + generator client { + provider = "prisma-client-js" + output = "${output}" + } + datasource db { provider = "postgresql" url = "${proxy}" @@ -568,3 +576,29 @@ function parseAttributes(attributes: string): Array { } }) } + +/* + * Modifies Prisma's `InputJsonValue` type to include `null` + */ +function extendJsonType(prismaDir: string): Promise { + const prismaTypings = path.join(prismaDir, 'index.d.ts') + const inputJsonValueRegex = /^\s*export\s*type\s*InputJsonValue\s*(=)\s*/gm + const replacement = 'export type InputJsonValue = null | ' + return findAndReplaceInFile(inputJsonValueRegex, replacement, prismaTypings) +} + +async function keepOnlyPrismaTypings(prismaDir: string): Promise { + const contents = await fs.readdir(prismaDir) + // Delete all files except the generated Electric client and the Prisma typings + const proms = contents.map(async (fileOrDir) => { + const filePath = path.join(prismaDir, fileOrDir) + if (fileOrDir === 'index.d.ts') { + // rename this file to `prismaClient.d.ts` + return fs.rename(filePath, path.join(prismaDir, 'prismaClient.d.ts')) + } else if (fileOrDir !== 'index.ts') { + // delete the file or folder + return fs.rm(filePath, { recursive: true }) + } + }) + await Promise.all(proms) +} diff --git a/clients/typescript/src/cli/util/index.ts b/clients/typescript/src/cli/util/index.ts new file mode 100644 index 0000000000..6fc6005874 --- /dev/null +++ b/clients/typescript/src/cli/util/index.ts @@ -0,0 +1 @@ +export * from './io' diff --git a/clients/typescript/src/cli/util/io.ts b/clients/typescript/src/cli/util/io.ts new file mode 100644 index 0000000000..31ef602f64 --- /dev/null +++ b/clients/typescript/src/cli/util/io.ts @@ -0,0 +1,15 @@ +import { readFile, writeFile } from 'fs/promises' + +/* + * Replaces the first occurence of `find` by `replace` in the file `file`. + * If `find` is a regular expression that sets the `g` flag, then it replaces all occurences. + */ +export async function findAndReplaceInFile( + find: string | RegExp, + replace: string, + file: string +) { + const content = await readFile(file, 'utf8') + const replacedContent = content.replace(find, replace) + await writeFile(file, replacedContent) +} diff --git a/clients/typescript/src/client/conversions/datatypes/json.ts b/clients/typescript/src/client/conversions/datatypes/json.ts new file mode 100644 index 0000000000..6e965eafff --- /dev/null +++ b/clients/typescript/src/client/conversions/datatypes/json.ts @@ -0,0 +1,27 @@ +// not the most precise JSON type +// but good enough for serialising/deserialising +type JSON = string | number | boolean | Array | Record + +export function serialiseJSON(v: JSON): string { + if (isJsonNull(v)) { + // user provided the special `JsonNull` value + // to indicate a JSON null value rather than a DB NULL + return JSON.stringify(null) + } + return JSON.stringify(v) +} + +export function deserialiseJSON(v: string): JSON { + if (v === JSON.stringify(null)) return { __is_electric_json_null__: true } + return JSON.parse(v) +} + +function isJsonNull(v: JSON): boolean { + return ( + typeof v === 'object' && + !Array.isArray(v) && + v !== null && + Object.hasOwn(v, '__is_electric_json_null__') && + v['__is_electric_json_null__'] + ) +} diff --git a/clients/typescript/src/client/conversions/sqlite.ts b/clients/typescript/src/client/conversions/sqlite.ts index b5a7e7339c..5e7e0e1273 100644 --- a/clients/typescript/src/client/conversions/sqlite.ts +++ b/clients/typescript/src/client/conversions/sqlite.ts @@ -1,6 +1,7 @@ import { InvalidArgumentError } from '../validation/errors/invalidArgumentError' import { deserialiseBoolean, serialiseBoolean } from './datatypes/boolean' import { deserialiseDate, serialiseDate } from './datatypes/date' +import { deserialiseJSON, serialiseJSON } from './datatypes/json' import { PgBasicType, PgDateType, PgType } from './types' /** @@ -34,6 +35,11 @@ export function toSqlite(v: any, pgType: PgType): any { pgType === PgBasicType.PG_REAL ) { return Math.fround(v) + } else if ( + pgType === PgBasicType.PG_JSON || + pgType === PgBasicType.PG_JSONB + ) { + return serialiseJSON(v) } else { return v } @@ -68,6 +74,12 @@ export function fromSqlite(v: any, pgType: PgType): any { // because some drivers (e.g. wa-sqlite) return a regular JS number if the value fits into a JS number // but we know that it should be a BigInt based on the column type return BigInt(v) + } else if ( + pgType === PgBasicType.PG_JSON || + pgType === PgBasicType.PG_JSONB + ) { + // it's serialised JSON + return deserialiseJSON(v) } else { return v } diff --git a/clients/typescript/src/client/conversions/types.ts b/clients/typescript/src/client/conversions/types.ts index fe21577aee..ed448fc229 100644 --- a/clients/typescript/src/client/conversions/types.ts +++ b/clients/typescript/src/client/conversions/types.ts @@ -12,6 +12,8 @@ export enum PgBasicType { PG_VARCHAR = 'VARCHAR', PG_CHAR = 'CHAR', PG_UUID = 'UUID', + PG_JSON = 'JSON', + PG_JSONB = 'JSONB', } /** diff --git a/clients/typescript/src/client/model/builder.ts b/clients/typescript/src/client/model/builder.ts index acdd247727..bc19fe1a1b 100644 --- a/clients/typescript/src/client/model/builder.ts +++ b/clients/typescript/src/client/model/builder.ts @@ -321,6 +321,7 @@ function makeFilter( // an object containing filters is provided // e.g. users.findMany({ where: { id: { in: [1, 2, 3] } } }) const fs = { + equals: z.any(), in: z.any().array().optional(), not: z.any().optional(), notIn: z.any().optional(), @@ -334,6 +335,7 @@ function makeFilter( } const fsHandlers = { + equals: makeEqualsFilter.bind(null), in: makeInFilter.bind(null), not: makeNotFilter.bind(null), notIn: makeNotInFilter.bind(null), @@ -421,6 +423,13 @@ function makeBooleanFilter( } } +function makeEqualsFilter( + fieldName: string, + value: unknown | undefined +): { sql: string; args?: unknown[] } { + return { sql: `${fieldName} = ?`, args: [value] } +} + function makeInFilter( fieldName: string, values: unknown[] | undefined diff --git a/clients/typescript/src/satellite/client.ts b/clients/typescript/src/satellite/client.ts index 45598e4445..a6f0c6feb8 100644 --- a/clients/typescript/src/satellite/client.ts +++ b/clients/typescript/src/satellite/client.ts @@ -1137,16 +1137,6 @@ function deserializeColumnData( columnType: PgType ): string | number { switch (columnType) { - case PgBasicType.PG_CHAR: - case PgDateType.PG_DATE: - case PgBasicType.PG_INT8: - case PgBasicType.PG_TEXT: - case PgDateType.PG_TIME: - case PgDateType.PG_TIMESTAMP: - case PgDateType.PG_TIMESTAMPTZ: - case PgBasicType.PG_UUID: - case PgBasicType.PG_VARCHAR: - return typeDecoder.text(column) case PgBasicType.PG_BOOL: return typeDecoder.bool(column) case PgBasicType.PG_INT: @@ -1161,17 +1151,13 @@ function deserializeColumnData( case PgDateType.PG_TIMETZ: return typeDecoder.timetz(column) default: - // should not occur - throw new SatelliteError( - SatelliteErrorCode.UNKNOWN_DATA_TYPE, - `can't deserialize ${columnType}` - ) + return typeDecoder.text(column) } } // All values serialized as textual representation function serializeColumnData( - columnValue: string | number, + columnValue: string | number | object, columnType: PgType ): Uint8Array { switch (columnType) { diff --git a/clients/typescript/test/client/conversions/input.test.ts b/clients/typescript/test/client/conversions/input.test.ts index 319103e9eb..5c76b9afdd 100644 --- a/clients/typescript/test/client/conversions/input.test.ts +++ b/clients/typescript/test/client/conversions/input.test.ts @@ -8,7 +8,7 @@ import { _NOT_UNIQUE_, _RECORD_NOT_FOUND_, } from '../../../src/client/validation/errors/messages' -import { schema } from '../generated' +import { JsonNull, schema } from '../generated' import { DataTypes, Dummy } from '../generated/client' const db = new Database(':memory:') @@ -31,7 +31,7 @@ await tbl.sync() function setupDB() { db.exec('DROP TABLE IF EXISTS DataTypes') db.exec( - "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'relatedId' int);" + "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'json' varchar, 'relatedId' int);" ) db.exec('DROP TABLE IF EXISTS Dummy') @@ -91,6 +91,26 @@ test.serial('findFirst transforms booleans to integer in SQLite', async (t) => { t.is(res?.bool, true) }) +test.serial( + 'findFirst transforms json values to strings in SQLite', + async (t) => { + await electric.adapter.run({ + sql: `INSERT INTO DataTypes('id', 'json') VALUES (1, NULL), (2, '{ "a": 5 }'), (3, 'null')`, + }) + + const res = await tbl.findFirst({ + where: { + json: { + equals: JsonNull, + }, + }, + }) + + t.is(res?.id, 3) + t.deepEqual(res?.json, JsonNull) + } +) + test.serial( 'findFirst transforms JS objects in equals filter to SQLite', async (t) => { @@ -236,6 +256,7 @@ const dateNulls = { float4: null, float8: null, uuid: null, + json: null, } const nulls = { diff --git a/clients/typescript/test/client/conversions/sqlite.test.ts b/clients/typescript/test/client/conversions/sqlite.test.ts index b2d422bd7d..9d8d1294d7 100644 --- a/clients/typescript/test/client/conversions/sqlite.test.ts +++ b/clients/typescript/test/client/conversions/sqlite.test.ts @@ -8,7 +8,7 @@ import { _NOT_UNIQUE_, _RECORD_NOT_FOUND_, } from '../../../src/client/validation/errors/messages' -import { schema } from '../generated' +import { schema, JsonNull } from '../generated' const db = new Database(':memory:') const electric = await electrify( @@ -30,7 +30,7 @@ await tbl.sync() function setupDB() { db.exec('DROP TABLE IF EXISTS DataTypes') db.exec( - "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'relatedId' int);" + "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'json' varchar, 'relatedId' int);" ) } @@ -243,3 +243,79 @@ test.serial('BigInts are converted correctly to SQLite', async (t) => { t.deepEqual(rawRes, [{ id: 1, int8: bigInt.toString() }]) //db.defaultSafeIntegers(false) // disables BigInt support }) + +test.serial('json is converted correctly to SQLite', async (t) => { + const json = { a: 1, b: true, c: { d: 'nested' }, e: [1, 2, 3], f: null } + await tbl.create({ + data: { + id: 1, + json, + }, + }) + + const rawRes = await electric.db.raw({ + sql: 'SELECT json FROM DataTypes WHERE id = ?', + args: [1], + }) + t.is(rawRes[0].json, JSON.stringify(json)) + + // Also test null values + // this null value is not a JSON null + // but a DB NULL that indicates absence of a value + await tbl.create({ + data: { + id: 2, + json: null, + }, + }) + + const rawRes2 = await electric.db.raw({ + sql: 'SELECT json FROM DataTypes WHERE id = ?', + args: [2], + }) + t.is(rawRes2[0].json, null) + + // Also test JSON null value + await tbl.create({ + data: { + id: 3, + json: JsonNull, + }, + }) + + const rawRes3 = await electric.db.raw({ + sql: 'SELECT json FROM DataTypes WHERE id = ?', + args: [3], + }) + t.is(rawRes3[0].json, JSON.stringify(null)) + + // also test regular values + await tbl.create({ + data: { + id: 4, + json: 'foo', + }, + }) + + const rawRes4 = await electric.db.raw({ + sql: 'SELECT json FROM DataTypes WHERE id = ?', + args: [4], + }) + + t.is(rawRes4[0].json, JSON.stringify('foo')) + + // also test arrays + await tbl.create({ + data: { + id: 5, + json: [1, 2, 3], + }, + }) + + const rawRes5 = await electric.db.raw({ + sql: 'SELECT json FROM DataTypes WHERE id = ?', + args: [5], + }) + + t.is(rawRes5[0].json, JSON.stringify([1, 2, 3])) +}) diff --git a/clients/typescript/test/client/generated/client/index.d.ts b/clients/typescript/test/client/generated/client/index.d.ts index c8dde2627a..1f16b819e1 100644 --- a/clients/typescript/test/client/generated/client/index.d.ts +++ b/clients/typescript/test/client/generated/client/index.d.ts @@ -85,6 +85,7 @@ export type DataTypes = { * @zod.custom.use(z.number().or(z.nan())) */ float8: number | null + json: Prisma.JsonValue | null relatedId: number | null } @@ -374,7 +375,7 @@ export namespace Prisma { * * @see https://www.prisma.io/docs/concepts/components/prisma-client/working-with-fields/working-with-json-fields#filtering-by-null-values */ - export type InputJsonValue = string | number | boolean | InputJsonObject | InputJsonArray + export type InputJsonValue = null | string | number | boolean | InputJsonObject | InputJsonArray /** * Types of the values used to represent different kinds of `null` values when working with JSON fields. @@ -4870,6 +4871,7 @@ export namespace Prisma { int8: number float4: number float8: number + json: number relatedId: number _all: number } @@ -4943,6 +4945,7 @@ export namespace Prisma { int8?: true float4?: true float8?: true + json?: true relatedId?: true _all?: true } @@ -5048,6 +5051,7 @@ export namespace Prisma { int8: bigint | null float4: number | null float8: number | null + json: JsonValue | null relatedId: number | null _count: DataTypesCountAggregateOutputType | null _avg: DataTypesAvgAggregateOutputType | null @@ -5084,6 +5088,7 @@ export namespace Prisma { int8?: boolean float4?: boolean float8?: boolean + json?: boolean relatedId?: boolean related?: boolean | DummyArgs } @@ -6834,6 +6839,7 @@ export namespace Prisma { int8: 'int8', float4: 'float4', float8: 'float8', + json: 'json', relatedId: 'relatedId' }; @@ -6856,6 +6862,23 @@ export namespace Prisma { export type ItemsScalarFieldEnum = (typeof ItemsScalarFieldEnum)[keyof typeof ItemsScalarFieldEnum] + export const JsonNullValueFilter: { + DbNull: typeof DbNull, + JsonNull: typeof JsonNull, + AnyNull: typeof AnyNull + }; + + export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter] + + + export const NullableJsonNullValueInput: { + DbNull: typeof DbNull, + JsonNull: typeof JsonNull + }; + + export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput] + + export const PostScalarFieldEnum: { id: 'id', title: 'title', @@ -7098,6 +7121,7 @@ export namespace Prisma { int8?: BigIntNullableFilter | bigint | number | null float4?: FloatNullableFilter | number | null float8?: FloatNullableFilter | number | null + json?: JsonNullableFilter relatedId?: IntNullableFilter | number | null related?: XOR | null } @@ -7116,6 +7140,7 @@ export namespace Prisma { int8?: SortOrder float4?: SortOrder float8?: SortOrder + json?: SortOrder relatedId?: SortOrder related?: DummyOrderByWithRelationInput } @@ -7139,6 +7164,7 @@ export namespace Prisma { int8?: SortOrder float4?: SortOrder float8?: SortOrder + json?: SortOrder relatedId?: SortOrder _count?: DataTypesCountOrderByAggregateInput _avg?: DataTypesAvgOrderByAggregateInput @@ -7164,6 +7190,7 @@ export namespace Prisma { int8?: BigIntNullableWithAggregatesFilter | bigint | number | null float4?: FloatNullableWithAggregatesFilter | number | null float8?: FloatNullableWithAggregatesFilter | number | null + json?: JsonNullableWithAggregatesFilter relatedId?: IntNullableWithAggregatesFilter | number | null } @@ -7392,6 +7419,7 @@ export namespace Prisma { int8?: bigint | number | null float4?: number | null float8?: number | null + json?: NullableJsonNullValueInput | InputJsonValue related?: DummyCreateNestedOneWithoutDatatypeInput } @@ -7409,6 +7437,7 @@ export namespace Prisma { int8?: bigint | number | null float4?: number | null float8?: number | null + json?: NullableJsonNullValueInput | InputJsonValue relatedId?: number | null } @@ -7426,6 +7455,7 @@ export namespace Prisma { int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null + json?: NullableJsonNullValueInput | InputJsonValue related?: DummyUpdateOneWithoutDatatypeNestedInput } @@ -7443,6 +7473,7 @@ export namespace Prisma { int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null + json?: NullableJsonNullValueInput | InputJsonValue relatedId?: NullableIntFieldUpdateOperationsInput | number | null } @@ -7460,6 +7491,7 @@ export namespace Prisma { int8?: bigint | number | null float4?: number | null float8?: number | null + json?: NullableJsonNullValueInput | InputJsonValue relatedId?: number | null } @@ -7477,6 +7509,7 @@ export namespace Prisma { int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null + json?: NullableJsonNullValueInput | InputJsonValue } export type DataTypesUncheckedUpdateManyInput = { @@ -7493,6 +7526,7 @@ export namespace Prisma { int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null + json?: NullableJsonNullValueInput | InputJsonValue relatedId?: NullableIntFieldUpdateOperationsInput | number | null } @@ -7834,6 +7868,28 @@ export namespace Prisma { gte?: number not?: NestedFloatNullableFilter | number | null } + export type JsonNullableFilter = + | PatchUndefined< + Either, Exclude, 'path'>>, + Required + > + | OptionalFlat, 'path'>> + + export type JsonNullableFilterBase = { + equals?: InputJsonValue | JsonNullValueFilter + path?: string[] + string_contains?: string + string_starts_with?: string + string_ends_with?: string + array_contains?: InputJsonValue | null + array_starts_with?: InputJsonValue | null + array_ends_with?: InputJsonValue | null + lt?: InputJsonValue + lte?: InputJsonValue + gt?: InputJsonValue + gte?: InputJsonValue + not?: InputJsonValue | JsonNullValueFilter + } export type DummyRelationFilter = { is?: DummyWhereInput | null @@ -7854,6 +7910,7 @@ export namespace Prisma { int8?: SortOrder float4?: SortOrder float8?: SortOrder + json?: SortOrder relatedId?: SortOrder } @@ -7979,6 +8036,31 @@ export namespace Prisma { _min?: NestedFloatNullableFilter _max?: NestedFloatNullableFilter } + export type JsonNullableWithAggregatesFilter = + | PatchUndefined< + Either, Exclude, 'path'>>, + Required + > + | OptionalFlat, 'path'>> + + export type JsonNullableWithAggregatesFilterBase = { + equals?: InputJsonValue | JsonNullValueFilter + path?: string[] + string_contains?: string + string_starts_with?: string + string_ends_with?: string + array_contains?: InputJsonValue | null + array_starts_with?: InputJsonValue | null + array_ends_with?: InputJsonValue | null + lt?: InputJsonValue + lte?: InputJsonValue + gt?: InputJsonValue + gte?: InputJsonValue + not?: InputJsonValue | JsonNullValueFilter + _count?: NestedIntNullableFilter + _min?: NestedJsonNullableFilter + _max?: NestedJsonNullableFilter + } export type DataTypesListRelationFilter = { every?: DataTypesWhereInput @@ -8468,6 +8550,28 @@ export namespace Prisma { _min?: NestedFloatNullableFilter _max?: NestedFloatNullableFilter } + export type NestedJsonNullableFilter = + | PatchUndefined< + Either, Exclude, 'path'>>, + Required + > + | OptionalFlat, 'path'>> + + export type NestedJsonNullableFilterBase = { + equals?: InputJsonValue | JsonNullValueFilter + path?: string[] + string_contains?: string + string_starts_with?: string + string_ends_with?: string + array_contains?: InputJsonValue | null + array_starts_with?: InputJsonValue | null + array_ends_with?: InputJsonValue | null + lt?: InputJsonValue + lte?: InputJsonValue + gt?: InputJsonValue + gte?: InputJsonValue + not?: InputJsonValue | JsonNullValueFilter + } export type PostCreateWithoutAuthorInput = { id: number @@ -8662,6 +8766,7 @@ export namespace Prisma { int8?: bigint | number | null float4?: number | null float8?: number | null + json?: NullableJsonNullValueInput | InputJsonValue } export type DataTypesUncheckedCreateWithoutRelatedInput = { @@ -8678,6 +8783,7 @@ export namespace Prisma { int8?: bigint | number | null float4?: number | null float8?: number | null + json?: NullableJsonNullValueInput | InputJsonValue } export type DataTypesCreateOrConnectWithoutRelatedInput = { @@ -8723,6 +8829,7 @@ export namespace Prisma { int8?: BigIntNullableFilter | bigint | number | null float4?: FloatNullableFilter | number | null float8?: FloatNullableFilter | number | null + json?: JsonNullableFilter relatedId?: IntNullableFilter | number | null } @@ -8768,6 +8875,7 @@ export namespace Prisma { int8?: bigint | number | null float4?: number | null float8?: number | null + json?: NullableJsonNullValueInput | InputJsonValue } export type DataTypesUpdateWithoutRelatedInput = { @@ -8784,6 +8892,7 @@ export namespace Prisma { int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null + json?: NullableJsonNullValueInput | InputJsonValue } export type DataTypesUncheckedUpdateWithoutRelatedInput = { @@ -8800,6 +8909,7 @@ export namespace Prisma { int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null + json?: NullableJsonNullValueInput | InputJsonValue } export type DataTypesUncheckedUpdateManyWithoutDatatypeInput = { @@ -8816,6 +8926,7 @@ export namespace Prisma { int8?: NullableBigIntFieldUpdateOperationsInput | bigint | number | null float4?: NullableFloatFieldUpdateOperationsInput | number | null float8?: NullableFloatFieldUpdateOperationsInput | number | null + json?: NullableJsonNullValueInput | InputJsonValue } diff --git a/clients/typescript/test/client/generated/index.ts b/clients/typescript/test/client/generated/index.ts index c6b0dfb848..35b997dc81 100644 --- a/clients/typescript/test/client/generated/index.ts +++ b/clients/typescript/test/client/generated/index.ts @@ -12,17 +12,54 @@ import { // HELPER FUNCTIONS ///////////////////////////////////////// +// JSON +//------------------------------------------------------ + +export type NullableJsonInput = Prisma.JsonValue | null | Prisma.NullTypes.JsonNull; + + +export const JsonValue: z.ZodType = z.union([ + z.null(), + z.string(), + z.number(), + z.boolean(), + z.lazy(() => z.array(JsonValue)), + z.lazy(() => z.record(JsonValue)), +]); + +export type JsonValueType = z.infer; + +export const NullableJsonValue = JsonValue + .nullable(); + +export type NullableJsonValueType = z.infer; + +export const InputJsonValue: z.ZodType = z.union([ + z.null(), + z.string(), + z.number(), + z.boolean(), + z.lazy(() => z.array(InputJsonValue.nullable())), + z.lazy(() => z.record(InputJsonValue.nullable())), +]); + +export type InputJsonValueType = z.infer; + ///////////////////////////////////////// // ENUMS ///////////////////////////////////////// -export const DataTypesScalarFieldEnumSchema = z.enum(['id','date','time','timetz','timestamp','timestamptz','bool','uuid','int2','int4','int8','float4','float8','relatedId']); +export const DataTypesScalarFieldEnumSchema = z.enum(['id','date','time','timetz','timestamp','timestamptz','bool','uuid','int2','int4','int8','float4','float8','json','relatedId']); export const DummyScalarFieldEnumSchema = z.enum(['id','timestamp']); export const ItemsScalarFieldEnumSchema = z.enum(['value','nbr']); +export const JsonNullValueFilterSchema = z.enum(['DbNull','JsonNull','AnyNull',]); + +export const NullableJsonNullValueInputSchema = z.enum(['DbNull','JsonNull',]) + export const PostScalarFieldEnumSchema = z.enum(['id','title','contents','nbr','authorId']); export const ProfileScalarFieldEnumSchema = z.enum(['id','bio','userId']); @@ -104,6 +141,7 @@ export const DataTypesSchema = z.object({ int8: z.bigint().nullish(), float4: z.number().or(z.nan()).nullish(), float8: z.number().or(z.nan()).nullish(), + json: NullableJsonValue.optional(), relatedId: z.number().int().nullish(), }) @@ -228,6 +266,7 @@ export const DataTypesSelectSchema: z.ZodType = z.object int8: z.boolean().optional(), float4: z.boolean().optional(), float8: z.boolean().optional(), + json: z.boolean().optional(), relatedId: z.boolean().optional(), related: z.union([z.boolean(),z.lazy(() => DummyArgsSchema)]).optional(), }).strict() @@ -448,6 +487,7 @@ export const DataTypesWhereInputSchema: z.ZodType = int8: z.union([ z.lazy(() => BigIntNullableFilterSchema),z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]) ]).optional().nullable(), float4: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), float8: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), + json: z.lazy(() => JsonNullableFilterSchema).optional(), relatedId: z.union([ z.lazy(() => IntNullableFilterSchema),z.number() ]).optional().nullable(), related: z.union([ z.lazy(() => DummyRelationFilterSchema),z.lazy(() => DummyWhereInputSchema) ]).optional().nullable(), }).strict(); @@ -466,6 +506,7 @@ export const DataTypesOrderByWithRelationInputSchema: z.ZodType SortOrderSchema).optional(), float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), + json: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional(), related: z.lazy(() => DummyOrderByWithRelationInputSchema).optional() }).strict(); @@ -489,6 +530,7 @@ export const DataTypesOrderByWithAggregationInputSchema: z.ZodType SortOrderSchema).optional(), float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), + json: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional(), _count: z.lazy(() => DataTypesCountOrderByAggregateInputSchema).optional(), _avg: z.lazy(() => DataTypesAvgOrderByAggregateInputSchema).optional(), @@ -514,6 +556,7 @@ export const DataTypesScalarWhereWithAggregatesInputSchema: z.ZodType BigIntNullableWithAggregatesFilterSchema),z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]) ]).optional().nullable(), float4: z.union([ z.lazy(() => FloatNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), float8: z.union([ z.lazy(() => FloatNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), + json: z.lazy(() => JsonNullableWithAggregatesFilterSchema).optional(), relatedId: z.union([ z.lazy(() => IntNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), }).strict(); @@ -742,6 +785,7 @@ export const DataTypesCreateInputSchema: z.ZodType int8: z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]).optional().nullable(), float4: z.number().or(z.nan()).optional().nullable(), float8: z.number().or(z.nan()).optional().nullable(), + json: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), related: z.lazy(() => DummyCreateNestedOneWithoutDatatypeInputSchema).optional() }).strict(); @@ -759,6 +803,7 @@ export const DataTypesUncheckedCreateInputSchema: z.ZodType NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), relatedId: z.number().int().optional().nullable() }).strict(); @@ -776,6 +821,7 @@ export const DataTypesUpdateInputSchema: z.ZodType int8: z.union([ z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]),z.lazy(() => NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), + json: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), related: z.lazy(() => DummyUpdateOneWithoutDatatypeNestedInputSchema).optional() }).strict(); @@ -793,6 +839,7 @@ export const DataTypesUncheckedUpdateInputSchema: z.ZodType NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), + json: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), relatedId: z.union([ z.number().int(),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -810,6 +857,7 @@ export const DataTypesCreateManyInputSchema: z.ZodType NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), relatedId: z.number().int().optional().nullable() }).strict(); @@ -827,6 +875,7 @@ export const DataTypesUpdateManyMutationInputSchema: z.ZodType NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), + json: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), }).strict(); export const DataTypesUncheckedUpdateManyInputSchema: z.ZodType = z.object({ @@ -843,6 +892,7 @@ export const DataTypesUncheckedUpdateManyInputSchema: z.ZodType NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), + json: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), relatedId: z.union([ z.number().int(),z.lazy(() => NullableIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); @@ -1185,6 +1235,22 @@ export const FloatNullableFilterSchema: z.ZodType = not: z.union([ z.number(),z.lazy(() => NestedFloatNullableFilterSchema) ]).optional().nullable(), }).strict(); +export const JsonNullableFilterSchema: z.ZodType = z.object({ + equals: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), + path: z.string().array().optional(), + string_contains: z.string().optional(), + string_starts_with: z.string().optional(), + string_ends_with: z.string().optional(), + array_contains: InputJsonValue.optional().nullable(), + array_starts_with: InputJsonValue.optional().nullable(), + array_ends_with: InputJsonValue.optional().nullable(), + lt: InputJsonValue.optional(), + lte: InputJsonValue.optional(), + gt: InputJsonValue.optional(), + gte: InputJsonValue.optional(), + not: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), +}).strict(); + export const DummyRelationFilterSchema: z.ZodType = z.object({ is: z.lazy(() => DummyWhereInputSchema).optional().nullable(), isNot: z.lazy(() => DummyWhereInputSchema).optional().nullable() @@ -1204,6 +1270,7 @@ export const DataTypesCountOrderByAggregateInputSchema: z.ZodType SortOrderSchema).optional(), float4: z.lazy(() => SortOrderSchema).optional(), float8: z.lazy(() => SortOrderSchema).optional(), + json: z.lazy(() => SortOrderSchema).optional(), relatedId: z.lazy(() => SortOrderSchema).optional() }).strict(); @@ -1330,6 +1397,25 @@ export const FloatNullableWithAggregatesFilterSchema: z.ZodType NestedFloatNullableFilterSchema).optional() }).strict(); +export const JsonNullableWithAggregatesFilterSchema: z.ZodType = z.object({ + equals: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), + path: z.string().array().optional(), + string_contains: z.string().optional(), + string_starts_with: z.string().optional(), + string_ends_with: z.string().optional(), + array_contains: InputJsonValue.optional().nullable(), + array_starts_with: InputJsonValue.optional().nullable(), + array_ends_with: InputJsonValue.optional().nullable(), + lt: InputJsonValue.optional(), + lte: InputJsonValue.optional(), + gt: InputJsonValue.optional(), + gte: InputJsonValue.optional(), + not: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), + _count: z.lazy(() => NestedIntNullableFilterSchema).optional(), + _min: z.lazy(() => NestedJsonNullableFilterSchema).optional(), + _max: z.lazy(() => NestedJsonNullableFilterSchema).optional() +}).strict(); + export const DataTypesListRelationFilterSchema: z.ZodType = z.object({ every: z.lazy(() => DataTypesWhereInputSchema).optional(), some: z.lazy(() => DataTypesWhereInputSchema).optional(), @@ -1819,6 +1905,22 @@ export const NestedFloatNullableWithAggregatesFilterSchema: z.ZodType NestedFloatNullableFilterSchema).optional() }).strict(); +export const NestedJsonNullableFilterSchema: z.ZodType = z.object({ + equals: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), + path: z.string().array().optional(), + string_contains: z.string().optional(), + string_starts_with: z.string().optional(), + string_ends_with: z.string().optional(), + array_contains: InputJsonValue.optional().nullable(), + array_starts_with: InputJsonValue.optional().nullable(), + array_ends_with: InputJsonValue.optional().nullable(), + lt: InputJsonValue.optional(), + lte: InputJsonValue.optional(), + gt: InputJsonValue.optional(), + gte: InputJsonValue.optional(), + not: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), +}).strict(); + export const PostCreateWithoutAuthorInputSchema: z.ZodType = z.object({ id: z.number(), title: z.string(), @@ -2011,7 +2113,8 @@ export const DataTypesCreateWithoutRelatedInputSchema: z.ZodType NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), }).strict(); export const DataTypesUncheckedCreateWithoutRelatedInputSchema: z.ZodType = z.object({ @@ -2027,7 +2130,8 @@ export const DataTypesUncheckedCreateWithoutRelatedInputSchema: z.ZodType NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), }).strict(); export const DataTypesCreateOrConnectWithoutRelatedInputSchema: z.ZodType = z.object({ @@ -2073,6 +2177,7 @@ export const DataTypesScalarWhereInputSchema: z.ZodType BigIntNullableFilterSchema),z.union([ z.bigint().gte(-9223372036854775808n).lte(9223372036854775807n), z.number().int().gte(Number.MIN_SAFE_INTEGER).lte(Number.MAX_SAFE_INTEGER).transform(BigInt) ]) ]).optional().nullable(), float4: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), float8: z.union([ z.lazy(() => FloatNullableFilterSchema),z.number() ]).optional().nullable(), + json: z.lazy(() => JsonNullableFilterSchema).optional(), relatedId: z.union([ z.lazy(() => IntNullableFilterSchema),z.number() ]).optional().nullable(), }).strict(); @@ -2117,7 +2222,8 @@ export const DataTypesCreateManyRelatedInputSchema: z.ZodType NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), }).strict(); export const DataTypesUpdateWithoutRelatedInputSchema: z.ZodType = z.object({ @@ -2134,6 +2240,7 @@ export const DataTypesUpdateWithoutRelatedInputSchema: z.ZodType NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), float4: z.union([ z.number(),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number(),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), + json: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), }).strict(); export const DataTypesUncheckedUpdateWithoutRelatedInputSchema: z.ZodType = z.object({ @@ -2150,6 +2257,7 @@ export const DataTypesUncheckedUpdateWithoutRelatedInputSchema: z.ZodType NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), float4: z.union([ z.number(),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number(),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), + json: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), }).strict(); export const DataTypesUncheckedUpdateManyWithoutDatatypeInputSchema: z.ZodType = z.object({ @@ -2166,6 +2274,7 @@ export const DataTypesUncheckedUpdateManyWithoutDatatypeInputSchema: z.ZodType

NullableBigIntFieldUpdateOperationsInputSchema) ]).optional().nullable(), float4: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), float8: z.union([ z.number().or(z.nan()),z.lazy(() => NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), + json: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), }).strict(); ///////////////////////////////////////// @@ -3052,6 +3161,10 @@ export const tableSchemas = { "float8", "FLOAT8" ], + [ + "json", + "JSON" + ], [ "relatedId", "INT4" @@ -3126,3 +3239,4 @@ export const tableSchemas = { export const schema = new DbSchema(tableSchemas, []) export type Electric = ElectricClient +export const JsonNull = { __is_electric_json_null__: true } diff --git a/clients/typescript/test/client/model/datatype.test.ts b/clients/typescript/test/client/model/datatype.test.ts index af72f0006a..8110c80893 100644 --- a/clients/typescript/test/client/model/datatype.test.ts +++ b/clients/typescript/test/client/model/datatype.test.ts @@ -8,7 +8,7 @@ import { _NOT_UNIQUE_, _RECORD_NOT_FOUND_, } from '../../../src/client/validation/errors/messages' -import { schema } from '../generated' +import { schema, JsonNull } from '../generated' import { ZodError } from 'zod' const db = new Database(':memory:') @@ -31,7 +31,7 @@ await tbl.sync() function setupDB() { db.exec('DROP TABLE IF EXISTS DataTypes') db.exec( - "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'relatedId' int);" + "CREATE TABLE DataTypes('id' int PRIMARY KEY, 'date' varchar, 'time' varchar, 'timetz' varchar, 'timestamp' varchar, 'timestamptz' varchar, 'bool' int, 'uuid' varchar, 'int2' int2, 'int4' int4, 'int8' int8, 'float4' real, 'float8' real, 'json' varchar, 'relatedId' int);" ) } @@ -861,3 +861,73 @@ test.serial( ) } ) + +test.serial('support JSON type', async (t) => { + const json = { a: 1, b: true, c: { d: 'nested' }, e: [1, 2, 3], f: null } + const res = await tbl.create({ + data: { + id: 1, + json, + }, + }) + + t.deepEqual(res.json, json) + + const fetchRes = await tbl.findUnique({ + where: { + id: 1, + }, + }) + + t.deepEqual(fetchRes?.json, json) + + // Also test that we can write the special JsonNull value + const res2 = await tbl.create({ + data: { + id: 2, + json: JsonNull, + }, + }) + + t.deepEqual(res2.json, JsonNull) + + const fetchRes2 = await tbl.findUnique({ + where: { + id: 2, + }, + }) + + t.deepEqual(fetchRes2?.json, JsonNull) +}) + +test.serial('support null values for JSON type', async (t) => { + const expectedRes = { + id: 1, + json: null, + } + + const res = await tbl.create({ + data: { + id: 1, + json: null, + }, + select: { + id: true, + json: true, + }, + }) + + t.deepEqual(res, expectedRes) + + const fetchRes = await tbl.findUnique({ + where: { + id: 1, + }, + select: { + id: true, + json: true, + }, + }) + + t.deepEqual(fetchRes, expectedRes) +}) diff --git a/clients/typescript/test/client/prisma/schema.prisma b/clients/typescript/test/client/prisma/schema.prisma index 39f04374a9..e4526b774a 100644 --- a/clients/typescript/test/client/prisma/schema.prisma +++ b/clients/typescript/test/client/prisma/schema.prisma @@ -57,6 +57,7 @@ model DataTypes { int8 BigInt? float4 Float? @db.Real /// @zod.custom.use(z.number().or(z.nan())) float8 Float? @db.DoublePrecision /// @zod.custom.use(z.number().or(z.nan())) + json Json? relatedId Int? related Dummy? @relation(fields: [relatedId], references: [id]) } diff --git a/components/electric/lib/electric/postgres.ex b/components/electric/lib/electric/postgres.ex index ddeb7083e0..d5c5de8229 100644 --- a/components/electric/lib/electric/postgres.ex +++ b/components/electric/lib/electric/postgres.ex @@ -86,6 +86,7 @@ defmodule Electric.Postgres do date float4 float8 int2 int4 int8 + jsonb text time timestamp timestamptz diff --git a/components/electric/lib/electric/satellite/serialization.ex b/components/electric/lib/electric/satellite/serialization.ex index 9757960019..47df8b9f4f 100644 --- a/components/electric/lib/electric/satellite/serialization.ex +++ b/components/electric/lib/electric/satellite/serialization.ex @@ -516,6 +516,11 @@ defmodule Electric.Satellite.Serialization do val end + def decode_column_value!(val, type) when type in [:json, :jsonb] do + _ = Jason.decode!(val) + val + end + def decode_column_value!(val, :time) do <> <> frac = val diff --git a/components/electric/test/electric/postgres/extension_test.exs b/components/electric/test/electric/postgres/extension_test.exs index 9acbf02607..7a48b62612 100644 --- a/components/electric/test/electric/postgres/extension_test.exs +++ b/components/electric/test/electric/postgres/extension_test.exs @@ -444,7 +444,8 @@ defmodule Electric.Postgres.ExtensionTest do tstz TIMESTAMPTZ, d DATE, t TIME, - flag BOOLEAN + flag BOOLEAN, + jb JSONB ); CALL electric.electrify('public.t1'); """) diff --git a/components/electric/test/electric/satellite/serialization_test.exs b/components/electric/test/electric/satellite/serialization_test.exs index a66b0cb4b3..3f0f8b07a2 100644 --- a/components/electric/test/electric/satellite/serialization_test.exs +++ b/components/electric/test/electric/satellite/serialization_test.exs @@ -208,7 +208,9 @@ defmodule Electric.Satellite.SerializationTest do {"false", :bool}, {"yes", :bool}, {"no", :bool}, - {"-1", :bool} + {"-1", :bool}, + {"...", :jsonb}, + {"{[}]", :jsonb} ] Enum.each(test_data, fn {val, type} -> diff --git a/components/electric/test/electric/satellite/ws_validations_test.exs b/components/electric/test/electric/satellite/ws_validations_test.exs index 58b2333a97..dad01cc7f7 100644 --- a/components/electric/test/electric/satellite/ws_validations_test.exs +++ b/components/electric/test/electric/satellite/ws_validations_test.exs @@ -447,6 +447,57 @@ defmodule Electric.Satellite.WsValidationsTest do end) end + test "validates json values", ctx do + vsn = "2023110701" + + :ok = + migrate( + ctx.db, + vsn, + "CREATE TABLE public.foo (id TEXT PRIMARY KEY, jb JSONB)", + electrify: "public.foo" + ) + + valid_records = [ + %{"id" => "1", "jb" => "null"}, + %{"id" => "2", "jb" => "{}"}, + %{"id" => "3", "jb" => "[]"}, + %{"id" => "4", "jb" => "\"hello\""}, + %{"id" => "5", "jb" => "-123"}, + %{"id" => "6", "jb" => ~s'{"foo": {"bar": ["baz", "quux"]}, "x": "I 👀 you"}'}, + %{"id" => "7", "jb" => ~s'[1, 2.0, 3e5, true, false, null, "", ["It\'s \u26a1"]]'} + ] + + within_replication_context(ctx, vsn, fn conn -> + Enum.each(valid_records, fn record -> + tx_op_log = serialize_trans(record) + MockClient.send_data(conn, tx_op_log) + end) + + refute_receive {^conn, %SatErrorResp{error_type: :INVALID_REQUEST}}, @receive_timeout + end) + + invalid_records = [ + %{"id" => "10", "jb" => "now"}, + %{"id" => "11", "jb" => ".123"}, + %{"id" => "12", "jb" => ".."}, + %{"id" => "13", "jb" => "{]"}, + %{"id" => "13", "jb" => "[}"}, + %{"id" => "14", "jb" => "\"hello"}, + %{"id" => "15", "jb" => "+"}, + %{"id" => "16", "jb" => "-"}, + %{"id" => "16", "jb" => "0.0.0"} + ] + + Enum.each(invalid_records, fn record -> + within_replication_context(ctx, vsn, fn conn -> + tx_op_log = serialize_trans(record) + MockClient.send_data(conn, tx_op_log) + assert_receive {^conn, %SatErrorResp{error_type: :INVALID_REQUEST}}, @receive_timeout + end) + end) + end + defp within_replication_context(ctx, vsn, expectation_fn) do with_connect(ctx.conn_opts, fn conn -> # Replication start ceremony diff --git a/e2e/satellite_client/src/client.ts b/e2e/satellite_client/src/client.ts index 26ff1f1709..1814dfff81 100644 --- a/e2e/satellite_client/src/client.ts +++ b/e2e/satellite_client/src/client.ts @@ -6,6 +6,7 @@ import { setLogLevel } from 'electric-sql/debug' import { electrify } from 'electric-sql/node' import { v4 as uuidv4 } from 'uuid' import { schema, Electric } from './generated/client' +export { JsonNull } from './generated/client' import { globalRegistry } from 'electric-sql/satellite' setLogLevel('DEBUG') @@ -96,7 +97,7 @@ export const write_datetime = (electric: Electric, datetime: Datetime) => { }) } -export const get_timestamp = (electric: Electric, id: string): Promise => { +export const get_timestamp = (electric: Electric, id: string) => { return electric.db.timestamps.findUnique({ where: { id: id @@ -104,7 +105,7 @@ export const get_timestamp = (electric: Electric, id: string): Promise => { +export const get_datetime = async (electric: Electric, id: string) => { const datetime = await electric.db.datetimes.findUnique({ where: { id: id @@ -124,13 +125,13 @@ export const assert_datetime = async (electric: Electric, id: string, expectedDa return check_datetime(datetime, expectedDate, expectedTime) } -export const check_timestamp = (timestamp: Timestamp | undefined, expectedCreatedAt: string, expectedUpdatedAt: string) => { +export const check_timestamp = (timestamp: Timestamp | null, expectedCreatedAt: string, expectedUpdatedAt: string) => { return (timestamp ?? false) && timestamp!.created_at.getTime() === new Date(expectedCreatedAt).getTime() && timestamp!.updated_at.getTime() === new Date(expectedUpdatedAt).getTime() } -export const check_datetime = (datetime: Datetime | undefined, expectedDate: string, expectedTime: string) => { +export const check_datetime = (datetime: Datetime | null, expectedDate: string, expectedTime: string) => { return (datetime ?? false) && datetime!.d.getTime() === new Date(expectedDate).getTime() && datetime!.t.getTime() === new Date(expectedTime).getTime() @@ -145,13 +146,13 @@ export const write_bool = (electric: Electric, id: string, b: boolean) => { }) } -export const get_bool = async (electric: Electric, id: string): Promise => { +export const get_bool = async (electric: Electric, id: string) => { const row = await electric.db.bools.findUnique({ where: { id: id }, }) - return row.b + return row?.b } export const get_datetimes = (electric: Electric) => { @@ -222,6 +223,58 @@ export const write_float = (electric: Electric, id: string, f4: number, f8: numb }) } +export const get_json_raw = async (electric: Electric, id: string) => { + const res = await electric.db.raw({ + sql: `SELECT js FROM jsons WHERE id = ?;`, + args: [id] + }) as unknown as Array<{ js: string }> + return res[0]?.js +} + +export const get_jsonb_raw = async (electric: Electric, id: string) => { + const res = await electric.db.raw({ + sql: `SELECT jsb FROM jsons WHERE id = ?;`, + args: [id] + }) as unknown as Array<{ jsb: string }> + return res[0]?.jsb +} + +export const get_json = async (electric: Electric, id: string) => { + const res = await electric.db.jsons.findUnique({ + where: { + id: id + }, + select: { + id: true, + js: true, + } + }) + return res +} + +export const get_jsonb = async (electric: Electric, id: string) => { + const res = await electric.db.jsons.findUnique({ + where: { + id: id + }, + select: { + id: true, + jsb: true, + } + }) + return res +} + +export const write_json = async (electric: Electric, id: string, js: any, jsb: any) => { + return electric.db.jsons.create({ + data: { + id, + //js, + jsb, + } + }) +} + export const get_item_columns = (electric: Electric, table: string, column: string) => { return electric.db.raw({ sql: `SELECT ${column} FROM ${table};` }) } diff --git a/e2e/satellite_client/src/generated/client/index.ts b/e2e/satellite_client/src/generated/client/index.ts index 83d027ab1a..43608010e0 100644 --- a/e2e/satellite_client/src/generated/client/index.ts +++ b/e2e/satellite_client/src/generated/client/index.ts @@ -7,6 +7,39 @@ import migrations from './migrations'; // HELPER FUNCTIONS ///////////////////////////////////////// +// JSON +//------------------------------------------------------ + +export type NullableJsonInput = Prisma.JsonValue | null; + + +export const JsonValue: z.ZodType = z.union([ + z.null(), + z.string(), + z.number(), + z.boolean(), + z.lazy(() => z.array(JsonValue)), + z.lazy(() => z.record(JsonValue)), +]); + +export type JsonValueType = z.infer; + +export const NullableJsonValue = JsonValue + .nullable(); + +export type NullableJsonValueType = z.infer; + +export const InputJsonValue: z.ZodType = z.union([ + z.null(), + z.string(), + z.number(), + z.boolean(), + z.lazy(() => z.array(InputJsonValue.nullable())), + z.lazy(() => z.record(InputJsonValue.nullable())), +]); + +export type InputJsonValueType = z.infer; + ///////////////////////////////////////// // ENUMS @@ -22,6 +55,12 @@ export const IntsScalarFieldEnumSchema = z.enum(['id','i2','i4','i8']); export const ItemsScalarFieldEnumSchema = z.enum(['id','content','content_text_null','content_text_null_default','intvalue_null','intvalue_null_default']); +export const JsonNullValueFilterSchema = z.enum(['DbNull','JsonNull','AnyNull',]); + +export const JsonsScalarFieldEnumSchema = z.enum(['id','js','jsb']); + +export const NullableJsonNullValueInputSchema = z.enum(['DbNull','JsonNull',]) + export const OtherItemsScalarFieldEnumSchema = z.enum(['id','content','item_id']); export const QueryModeSchema = z.enum(['default','insensitive']); @@ -134,6 +173,18 @@ export const FloatsSchema = z.object({ export type Floats = z.infer +///////////////////////////////////////// +// JSONS SCHEMA +///////////////////////////////////////// + +export const JsonsSchema = z.object({ + id: z.string(), + js: NullableJsonValue.optional(), + jsb: NullableJsonValue.optional(), +}) + +export type Jsons = z.infer + ///////////////////////////////////////// // SELECT & INCLUDE ///////////////////////////////////////// @@ -231,6 +282,15 @@ export const FloatsSelectSchema: z.ZodType = z.object({ f8: z.boolean().optional(), }).strict() +// JSONS +//------------------------------------------------------ + +export const JsonsSelectSchema: z.ZodType = z.object({ + id: z.boolean().optional(), + js: z.boolean().optional(), + jsb: z.boolean().optional(), +}).strict() + ///////////////////////////////////////// // INPUT TYPES @@ -547,6 +607,43 @@ export const FloatsScalarWhereWithAggregatesInputSchema: z.ZodType FloatNullableWithAggregatesFilterSchema),z.number() ]).optional().nullable(), }).strict(); +export const JsonsWhereInputSchema: z.ZodType = z.object({ + AND: z.union([ z.lazy(() => JsonsWhereInputSchema),z.lazy(() => JsonsWhereInputSchema).array() ]).optional(), + OR: z.lazy(() => JsonsWhereInputSchema).array().optional(), + NOT: z.union([ z.lazy(() => JsonsWhereInputSchema),z.lazy(() => JsonsWhereInputSchema).array() ]).optional(), + id: z.union([ z.lazy(() => StringFilterSchema),z.string() ]).optional(), + js: z.lazy(() => JsonNullableFilterSchema).optional(), + jsb: z.lazy(() => JsonNullableFilterSchema).optional() +}).strict(); + +export const JsonsOrderByWithRelationInputSchema: z.ZodType = z.object({ + id: z.lazy(() => SortOrderSchema).optional(), + js: z.lazy(() => SortOrderSchema).optional(), + jsb: z.lazy(() => SortOrderSchema).optional() +}).strict(); + +export const JsonsWhereUniqueInputSchema: z.ZodType = z.object({ + id: z.string().optional() +}).strict(); + +export const JsonsOrderByWithAggregationInputSchema: z.ZodType = z.object({ + id: z.lazy(() => SortOrderSchema).optional(), + js: z.lazy(() => SortOrderSchema).optional(), + jsb: z.lazy(() => SortOrderSchema).optional(), + _count: z.lazy(() => JsonsCountOrderByAggregateInputSchema).optional(), + _max: z.lazy(() => JsonsMaxOrderByAggregateInputSchema).optional(), + _min: z.lazy(() => JsonsMinOrderByAggregateInputSchema).optional() +}).strict(); + +export const JsonsScalarWhereWithAggregatesInputSchema: z.ZodType = z.object({ + AND: z.union([ z.lazy(() => JsonsScalarWhereWithAggregatesInputSchema),z.lazy(() => JsonsScalarWhereWithAggregatesInputSchema).array() ]).optional(), + OR: z.lazy(() => JsonsScalarWhereWithAggregatesInputSchema).array().optional(), + NOT: z.union([ z.lazy(() => JsonsScalarWhereWithAggregatesInputSchema),z.lazy(() => JsonsScalarWhereWithAggregatesInputSchema).array() ]).optional(), + id: z.union([ z.lazy(() => StringWithAggregatesFilterSchema),z.string() ]).optional(), + js: z.lazy(() => JsonNullableWithAggregatesFilterSchema).optional(), + jsb: z.lazy(() => JsonNullableWithAggregatesFilterSchema).optional() +}).strict(); + export const ItemsCreateInputSchema: z.ZodType = z.object({ id: z.string(), content: z.string(), @@ -893,6 +990,48 @@ export const FloatsUncheckedUpdateManyInputSchema: z.ZodType NullableFloatFieldUpdateOperationsInputSchema) ]).optional().nullable(), }).strict(); +export const JsonsCreateInputSchema: z.ZodType = z.object({ + id: z.string(), + js: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), + jsb: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), +}).strict(); + +export const JsonsUncheckedCreateInputSchema: z.ZodType = z.object({ + id: z.string(), + js: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), + jsb: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), +}).strict(); + +export const JsonsUpdateInputSchema: z.ZodType = z.object({ + id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), + js: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), + jsb: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), +}).strict(); + +export const JsonsUncheckedUpdateInputSchema: z.ZodType = z.object({ + id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), + js: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), + jsb: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), +}).strict(); + +export const JsonsCreateManyInputSchema: z.ZodType = z.object({ + id: z.string(), + js: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), + jsb: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), +}).strict(); + +export const JsonsUpdateManyMutationInputSchema: z.ZodType = z.object({ + id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), + js: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), + jsb: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), +}).strict(); + +export const JsonsUncheckedUpdateManyInputSchema: z.ZodType = z.object({ + id: z.union([ z.string(),z.lazy(() => StringFieldUpdateOperationsInputSchema) ]).optional(), + js: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), + jsb: z.union([ z.lazy(() => NullableJsonNullValueInputSchema),InputJsonValue ]).optional(), +}).strict(); + export const StringFilterSchema: z.ZodType = z.object({ equals: z.string().optional(), in: z.union([ z.string().array(),z.string() ]).optional(), @@ -1294,6 +1433,55 @@ export const FloatNullableWithAggregatesFilterSchema: z.ZodType NestedFloatNullableFilterSchema).optional() }).strict(); +export const JsonNullableFilterSchema: z.ZodType = z.object({ + equals: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), + path: z.string().array().optional(), + string_contains: z.string().optional(), + string_starts_with: z.string().optional(), + string_ends_with: z.string().optional(), + array_contains: InputJsonValue.optional().nullable(), + array_starts_with: InputJsonValue.optional().nullable(), + array_ends_with: InputJsonValue.optional().nullable(), + lt: InputJsonValue.optional(), + lte: InputJsonValue.optional(), + gt: InputJsonValue.optional(), + gte: InputJsonValue.optional(), + not: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), +}).strict(); + +export const JsonsCountOrderByAggregateInputSchema: z.ZodType = z.object({ + id: z.lazy(() => SortOrderSchema).optional(), + js: z.lazy(() => SortOrderSchema).optional(), + jsb: z.lazy(() => SortOrderSchema).optional() +}).strict(); + +export const JsonsMaxOrderByAggregateInputSchema: z.ZodType = z.object({ + id: z.lazy(() => SortOrderSchema).optional() +}).strict(); + +export const JsonsMinOrderByAggregateInputSchema: z.ZodType = z.object({ + id: z.lazy(() => SortOrderSchema).optional() +}).strict(); + +export const JsonNullableWithAggregatesFilterSchema: z.ZodType = z.object({ + equals: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), + path: z.string().array().optional(), + string_contains: z.string().optional(), + string_starts_with: z.string().optional(), + string_ends_with: z.string().optional(), + array_contains: InputJsonValue.optional().nullable(), + array_starts_with: InputJsonValue.optional().nullable(), + array_ends_with: InputJsonValue.optional().nullable(), + lt: InputJsonValue.optional(), + lte: InputJsonValue.optional(), + gt: InputJsonValue.optional(), + gte: InputJsonValue.optional(), + not: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), + _count: z.lazy(() => NestedIntNullableFilterSchema).optional(), + _min: z.lazy(() => NestedJsonNullableFilterSchema).optional(), + _max: z.lazy(() => NestedJsonNullableFilterSchema).optional() +}).strict(); + export const OtherItemsCreateNestedOneWithoutItemsInputSchema: z.ZodType = z.object({ create: z.union([ z.lazy(() => OtherItemsCreateWithoutItemsInputSchema),z.lazy(() => OtherItemsUncheckedCreateWithoutItemsInputSchema) ]).optional(), connectOrCreate: z.lazy(() => OtherItemsCreateOrConnectWithoutItemsInputSchema).optional(), @@ -1599,6 +1787,22 @@ export const NestedFloatNullableWithAggregatesFilterSchema: z.ZodType NestedFloatNullableFilterSchema).optional() }).strict(); +export const NestedJsonNullableFilterSchema: z.ZodType = z.object({ + equals: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), + path: z.string().array().optional(), + string_contains: z.string().optional(), + string_starts_with: z.string().optional(), + string_ends_with: z.string().optional(), + array_contains: InputJsonValue.optional().nullable(), + array_starts_with: InputJsonValue.optional().nullable(), + array_ends_with: InputJsonValue.optional().nullable(), + lt: InputJsonValue.optional(), + lte: InputJsonValue.optional(), + gt: InputJsonValue.optional(), + gte: InputJsonValue.optional(), + not: z.union([ InputJsonValue,z.lazy(() => JsonNullValueFilterSchema) ]).optional(), +}).strict(); + export const OtherItemsCreateWithoutItemsInputSchema: z.ZodType = z.object({ id: z.string(), content: z.string() @@ -2145,6 +2349,63 @@ export const FloatsFindUniqueOrThrowArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + where: JsonsWhereInputSchema.optional(), + orderBy: z.union([ JsonsOrderByWithRelationInputSchema.array(),JsonsOrderByWithRelationInputSchema ]).optional(), + cursor: JsonsWhereUniqueInputSchema.optional(), + take: z.number().optional(), + skip: z.number().optional(), + distinct: JsonsScalarFieldEnumSchema.array().optional(), +}).strict() + +export const JsonsFindFirstOrThrowArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + where: JsonsWhereInputSchema.optional(), + orderBy: z.union([ JsonsOrderByWithRelationInputSchema.array(),JsonsOrderByWithRelationInputSchema ]).optional(), + cursor: JsonsWhereUniqueInputSchema.optional(), + take: z.number().optional(), + skip: z.number().optional(), + distinct: JsonsScalarFieldEnumSchema.array().optional(), +}).strict() + +export const JsonsFindManyArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + where: JsonsWhereInputSchema.optional(), + orderBy: z.union([ JsonsOrderByWithRelationInputSchema.array(),JsonsOrderByWithRelationInputSchema ]).optional(), + cursor: JsonsWhereUniqueInputSchema.optional(), + take: z.number().optional(), + skip: z.number().optional(), + distinct: JsonsScalarFieldEnumSchema.array().optional(), +}).strict() + +export const JsonsAggregateArgsSchema: z.ZodType = z.object({ + where: JsonsWhereInputSchema.optional(), + orderBy: z.union([ JsonsOrderByWithRelationInputSchema.array(),JsonsOrderByWithRelationInputSchema ]).optional(), + cursor: JsonsWhereUniqueInputSchema.optional(), + take: z.number().optional(), + skip: z.number().optional(), +}).strict() + +export const JsonsGroupByArgsSchema: z.ZodType = z.object({ + where: JsonsWhereInputSchema.optional(), + orderBy: z.union([ JsonsOrderByWithAggregationInputSchema.array(),JsonsOrderByWithAggregationInputSchema ]).optional(), + by: JsonsScalarFieldEnumSchema.array(), + having: JsonsScalarWhereWithAggregatesInputSchema.optional(), + take: z.number().optional(), + skip: z.number().optional(), +}).strict() + +export const JsonsFindUniqueArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + where: JsonsWhereUniqueInputSchema, +}).strict() + +export const JsonsFindUniqueOrThrowArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + where: JsonsWhereUniqueInputSchema, +}).strict() + export const ItemsCreateArgsSchema: z.ZodType = z.object({ select: ItemsSelectSchema.optional(), include: ItemsIncludeSchema.optional(), @@ -2449,6 +2710,43 @@ export const FloatsDeleteManyArgsSchema: z.ZodType where: FloatsWhereInputSchema.optional(), }).strict() +export const JsonsCreateArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + data: z.union([ JsonsCreateInputSchema,JsonsUncheckedCreateInputSchema ]), +}).strict() + +export const JsonsUpsertArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + where: JsonsWhereUniqueInputSchema, + create: z.union([ JsonsCreateInputSchema,JsonsUncheckedCreateInputSchema ]), + update: z.union([ JsonsUpdateInputSchema,JsonsUncheckedUpdateInputSchema ]), +}).strict() + +export const JsonsCreateManyArgsSchema: z.ZodType = z.object({ + data: z.union([ JsonsCreateManyInputSchema,JsonsCreateManyInputSchema.array() ]), + skipDuplicates: z.boolean().optional(), +}).strict() + +export const JsonsDeleteArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + where: JsonsWhereUniqueInputSchema, +}).strict() + +export const JsonsUpdateArgsSchema: z.ZodType = z.object({ + select: JsonsSelectSchema.optional(), + data: z.union([ JsonsUpdateInputSchema,JsonsUncheckedUpdateInputSchema ]), + where: JsonsWhereUniqueInputSchema, +}).strict() + +export const JsonsUpdateManyArgsSchema: z.ZodType = z.object({ + data: z.union([ JsonsUpdateManyMutationInputSchema,JsonsUncheckedUpdateManyInputSchema ]), + where: JsonsWhereInputSchema.optional(), +}).strict() + +export const JsonsDeleteManyArgsSchema: z.ZodType = z.object({ + where: JsonsWhereInputSchema.optional(), +}).strict() + interface ItemsGetPayload extends HKT { readonly _A?: boolean | null | undefined | Prisma.ItemsArgs readonly type: Prisma.ItemsGetPayload @@ -2489,6 +2787,11 @@ interface FloatsGetPayload extends HKT { readonly type: Prisma.FloatsGetPayload } +interface JsonsGetPayload extends HKT { + readonly _A?: boolean | null | undefined | Prisma.JsonsArgs + readonly type: Prisma.JsonsGetPayload +} + export const tableSchemas = { items: { fields: new Map([ @@ -2824,7 +3127,49 @@ export const tableSchemas = { Prisma.FloatsScalarFieldEnum, FloatsGetPayload >, + jsons: { + fields: new Map([ + [ + "id", + "TEXT" + ], + /*[ + "js", + "JSON" + ],*/ + [ + "jsb", + "JSON" + ] + ]), + relations: [ + ], + modelSchema: (JsonsCreateInputSchema as any) + .partial() + .or((JsonsUncheckedCreateInputSchema as any).partial()), + createSchema: JsonsCreateArgsSchema, + createManySchema: JsonsCreateManyArgsSchema, + findUniqueSchema: JsonsFindUniqueArgsSchema, + findSchema: JsonsFindFirstArgsSchema, + updateSchema: JsonsUpdateArgsSchema, + updateManySchema: JsonsUpdateManyArgsSchema, + upsertSchema: JsonsUpsertArgsSchema, + deleteSchema: JsonsDeleteArgsSchema, + deleteManySchema: JsonsDeleteManyArgsSchema + } as TableSchema< + z.infer, + Prisma.JsonsCreateArgs['data'], + Prisma.JsonsUpdateArgs['data'], + Prisma.JsonsFindFirstArgs['select'], + Prisma.JsonsFindFirstArgs['where'], + Prisma.JsonsFindUniqueArgs['where'], + never, + Prisma.JsonsFindFirstArgs['orderBy'], + Prisma.JsonsScalarFieldEnum, + JsonsGetPayload + >, } export const schema = new DbSchema(tableSchemas, migrations) export type Electric = ElectricClient +export const JsonNull = { __is_electric_json_null__: true } diff --git a/e2e/satellite_client/src/generated/client/prismaClient.d.ts b/e2e/satellite_client/src/generated/client/prismaClient.d.ts index 469396e5d1..794c305d0f 100644 --- a/e2e/satellite_client/src/generated/client/prismaClient.d.ts +++ b/e2e/satellite_client/src/generated/client/prismaClient.d.ts @@ -108,6 +108,16 @@ export type Floats = { f8: number | null } +/** + * Model Jsons + * + */ +export type Jsons = { + id: string + js: Prisma.JsonValue | null + jsb: Prisma.JsonValue | null +} + /** * ## Prisma Client ʲˢ @@ -305,6 +315,16 @@ export class PrismaClient< * ``` */ get floats(): Prisma.FloatsDelegate; + + /** + * `prisma.jsons`: Exposes CRUD operations for the **Jsons** model. + * Example usage: + * ```ts + * // Fetch zero or more Jsons + * const jsons = await prisma.jsons.findMany() + * ``` + */ + get jsons(): Prisma.JsonsDelegate; } export namespace Prisma { @@ -781,7 +801,8 @@ export namespace Prisma { Bools: 'Bools', Uuids: 'Uuids', Ints: 'Ints', - Floats: 'Floats' + Floats: 'Floats', + Jsons: 'Jsons' }; export type ModelName = (typeof ModelName)[keyof typeof ModelName] @@ -8100,127 +8121,1011 @@ export namespace Prisma { /** - * Enums + * Model Jsons */ - export const BoolsScalarFieldEnum: { - id: 'id', - b: 'b' - }; - - export type BoolsScalarFieldEnum = (typeof BoolsScalarFieldEnum)[keyof typeof BoolsScalarFieldEnum] + export type AggregateJsons = { + _count: JsonsCountAggregateOutputType | null + _min: JsonsMinAggregateOutputType | null + _max: JsonsMaxAggregateOutputType | null + } - export const DatetimesScalarFieldEnum: { - id: 'id', - d: 'd', - t: 't' - }; + export type JsonsMinAggregateOutputType = { + id: string | null + } - export type DatetimesScalarFieldEnum = (typeof DatetimesScalarFieldEnum)[keyof typeof DatetimesScalarFieldEnum] + export type JsonsMaxAggregateOutputType = { + id: string | null + } + export type JsonsCountAggregateOutputType = { + id: number + js: number + jsb: number + _all: number + } - export const FloatsScalarFieldEnum: { - id: 'id', - f4: 'f4', - f8: 'f8' - }; - export type FloatsScalarFieldEnum = (typeof FloatsScalarFieldEnum)[keyof typeof FloatsScalarFieldEnum] + export type JsonsMinAggregateInputType = { + id?: true + } + export type JsonsMaxAggregateInputType = { + id?: true + } - export const IntsScalarFieldEnum: { - id: 'id', - i2: 'i2', - i4: 'i4', - i8: 'i8' - }; + export type JsonsCountAggregateInputType = { + id?: true + js?: true + jsb?: true + _all?: true + } - export type IntsScalarFieldEnum = (typeof IntsScalarFieldEnum)[keyof typeof IntsScalarFieldEnum] + export type JsonsAggregateArgs = { + /** + * Filter which Jsons to aggregate. + */ + where?: JsonsWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Jsons to fetch. + */ + orderBy?: Enumerable + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the start position + */ + cursor?: JsonsWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Jsons from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Jsons. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Count returned Jsons + **/ + _count?: true | JsonsCountAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the minimum value + **/ + _min?: JsonsMinAggregateInputType + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/aggregations Aggregation Docs} + * + * Select which fields to find the maximum value + **/ + _max?: JsonsMaxAggregateInputType + } + export type GetJsonsAggregateType = { + [P in keyof T & keyof AggregateJsons]: P extends '_count' | 'count' + ? T[P] extends true + ? number + : GetScalarType + : GetScalarType + } - export const ItemsScalarFieldEnum: { - id: 'id', - content: 'content', - content_text_null: 'content_text_null', - content_text_null_default: 'content_text_null_default', - intvalue_null: 'intvalue_null', - intvalue_null_default: 'intvalue_null_default' - }; - export type ItemsScalarFieldEnum = (typeof ItemsScalarFieldEnum)[keyof typeof ItemsScalarFieldEnum] - export const OtherItemsScalarFieldEnum: { - id: 'id', - content: 'content', - item_id: 'item_id' - }; + export type JsonsGroupByArgs = { + where?: JsonsWhereInput + orderBy?: Enumerable + by: JsonsScalarFieldEnum[] + having?: JsonsScalarWhereWithAggregatesInput + take?: number + skip?: number + _count?: JsonsCountAggregateInputType | true + _min?: JsonsMinAggregateInputType + _max?: JsonsMaxAggregateInputType + } - export type OtherItemsScalarFieldEnum = (typeof OtherItemsScalarFieldEnum)[keyof typeof OtherItemsScalarFieldEnum] + export type JsonsGroupByOutputType = { + id: string + js: JsonValue | null + jsb: JsonValue | null + _count: JsonsCountAggregateOutputType | null + _min: JsonsMinAggregateOutputType | null + _max: JsonsMaxAggregateOutputType | null + } - export const QueryMode: { - default: 'default', - insensitive: 'insensitive' - }; + type GetJsonsGroupByPayload = Prisma.PrismaPromise< + Array< + PickArray & + { + [P in ((keyof T) & (keyof JsonsGroupByOutputType))]: P extends '_count' + ? T[P] extends boolean + ? number + : GetScalarType + : GetScalarType + } + > + > - export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] + export type JsonsSelect = { + id?: boolean + js?: boolean + jsb?: boolean + } - export const SortOrder: { - asc: 'asc', - desc: 'desc' - }; - export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder] + export type JsonsGetPayload = + S extends { select: any, include: any } ? 'Please either choose `select` or `include`' : + S extends true ? Jsons : + S extends undefined ? never : + S extends { include: any } & (JsonsArgs | JsonsFindManyArgs) + ? Jsons + : S extends { select: any } & (JsonsArgs | JsonsFindManyArgs) + ? { + [P in TruthyKeys]: + P extends keyof Jsons ? Jsons[P] : never + } + : Jsons - export const TimestampsScalarFieldEnum: { - id: 'id', - created_at: 'created_at', - updated_at: 'updated_at' - }; + type JsonsCountArgs = + Omit & { + select?: JsonsCountAggregateInputType | true + } - export type TimestampsScalarFieldEnum = (typeof TimestampsScalarFieldEnum)[keyof typeof TimestampsScalarFieldEnum] + export interface JsonsDelegate { + /** + * Find zero or one Jsons that matches the filter. + * @param {JsonsFindUniqueArgs} args - Arguments to find a Jsons + * @example + * // Get one Jsons + * const jsons = await prisma.jsons.findUnique({ + * where: { + * // ... provide filter here + * } + * }) + **/ + findUnique( + args: SelectSubset + ): HasReject extends True ? Prisma__JsonsClient> : Prisma__JsonsClient | null, null> - export const TransactionIsolationLevel: { - ReadUncommitted: 'ReadUncommitted', - ReadCommitted: 'ReadCommitted', - RepeatableRead: 'RepeatableRead', - Serializable: 'Serializable' - }; + /** + * Find one Jsons that matches the filter or throw an error with `error.code='P2025'` + * if no matches were found. + * @param {JsonsFindUniqueOrThrowArgs} args - Arguments to find a Jsons + * @example + * // Get one Jsons + * const jsons = await prisma.jsons.findUniqueOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + **/ + findUniqueOrThrow( + args?: SelectSubset + ): Prisma__JsonsClient> - export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] + /** + * Find the first Jsons that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {JsonsFindFirstArgs} args - Arguments to find a Jsons + * @example + * // Get one Jsons + * const jsons = await prisma.jsons.findFirst({ + * where: { + * // ... provide filter here + * } + * }) + **/ + findFirst( + args?: SelectSubset + ): HasReject extends True ? Prisma__JsonsClient> : Prisma__JsonsClient | null, null> + /** + * Find the first Jsons that matches the filter or + * throw `NotFoundError` if no matches were found. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {JsonsFindFirstOrThrowArgs} args - Arguments to find a Jsons + * @example + * // Get one Jsons + * const jsons = await prisma.jsons.findFirstOrThrow({ + * where: { + * // ... provide filter here + * } + * }) + **/ + findFirstOrThrow( + args?: SelectSubset + ): Prisma__JsonsClient> - export const UuidsScalarFieldEnum: { - id: 'id' - }; + /** + * Find zero or more Jsons that matches the filter. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {JsonsFindManyArgs=} args - Arguments to filter and select certain fields only. + * @example + * // Get all Jsons + * const jsons = await prisma.jsons.findMany() + * + * // Get first 10 Jsons + * const jsons = await prisma.jsons.findMany({ take: 10 }) + * + * // Only select the `id` + * const jsonsWithIdOnly = await prisma.jsons.findMany({ select: { id: true } }) + * + **/ + findMany( + args?: SelectSubset + ): Prisma.PrismaPromise>> - export type UuidsScalarFieldEnum = (typeof UuidsScalarFieldEnum)[keyof typeof UuidsScalarFieldEnum] + /** + * Create a Jsons. + * @param {JsonsCreateArgs} args - Arguments to create a Jsons. + * @example + * // Create one Jsons + * const Jsons = await prisma.jsons.create({ + * data: { + * // ... data to create a Jsons + * } + * }) + * + **/ + create( + args: SelectSubset + ): Prisma__JsonsClient> + /** + * Create many Jsons. + * @param {JsonsCreateManyArgs} args - Arguments to create many Jsons. + * @example + * // Create many Jsons + * const jsons = await prisma.jsons.createMany({ + * data: { + * // ... provide data here + * } + * }) + * + **/ + createMany( + args?: SelectSubset + ): Prisma.PrismaPromise - /** - * Deep Input Types - */ + /** + * Delete a Jsons. + * @param {JsonsDeleteArgs} args - Arguments to delete one Jsons. + * @example + * // Delete one Jsons + * const Jsons = await prisma.jsons.delete({ + * where: { + * // ... filter to delete one Jsons + * } + * }) + * + **/ + delete( + args: SelectSubset + ): Prisma__JsonsClient> + /** + * Update one Jsons. + * @param {JsonsUpdateArgs} args - Arguments to update one Jsons. + * @example + * // Update one Jsons + * const jsons = await prisma.jsons.update({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + **/ + update( + args: SelectSubset + ): Prisma__JsonsClient> - export type ItemsWhereInput = { - AND?: Enumerable - OR?: Enumerable - NOT?: Enumerable - id?: StringFilter | string - content?: StringFilter | string - content_text_null?: StringNullableFilter | string | null - content_text_null_default?: StringNullableFilter | string | null - intvalue_null?: IntNullableFilter | number | null - intvalue_null_default?: IntNullableFilter | number | null - other_items?: XOR | null - } + /** + * Delete zero or more Jsons. + * @param {JsonsDeleteManyArgs} args - Arguments to filter Jsons to delete. + * @example + * // Delete a few Jsons + * const { count } = await prisma.jsons.deleteMany({ + * where: { + * // ... provide filter here + * } + * }) + * + **/ + deleteMany( + args?: SelectSubset + ): Prisma.PrismaPromise - export type ItemsOrderByWithRelationInput = { + /** + * Update zero or more Jsons. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {JsonsUpdateManyArgs} args - Arguments to update one or more rows. + * @example + * // Update many Jsons + * const jsons = await prisma.jsons.updateMany({ + * where: { + * // ... provide filter here + * }, + * data: { + * // ... provide data here + * } + * }) + * + **/ + updateMany( + args: SelectSubset + ): Prisma.PrismaPromise + + /** + * Create or update one Jsons. + * @param {JsonsUpsertArgs} args - Arguments to update or create a Jsons. + * @example + * // Update or create a Jsons + * const jsons = await prisma.jsons.upsert({ + * create: { + * // ... data to create a Jsons + * }, + * update: { + * // ... in case it already exists, update + * }, + * where: { + * // ... the filter for the Jsons we want to update + * } + * }) + **/ + upsert( + args: SelectSubset + ): Prisma__JsonsClient> + + /** + * Count the number of Jsons. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {JsonsCountArgs} args - Arguments to filter Jsons to count. + * @example + * // Count the number of Jsons + * const count = await prisma.jsons.count({ + * where: { + * // ... the filter for the Jsons we want to count + * } + * }) + **/ + count( + args?: Subset, + ): Prisma.PrismaPromise< + T extends _Record<'select', any> + ? T['select'] extends true + ? number + : GetScalarType + : number + > + + /** + * Allows you to perform aggregations operations on a Jsons. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {JsonsAggregateArgs} args - Select which aggregations you would like to apply and on what fields. + * @example + * // Ordered by age ascending + * // Where email contains prisma.io + * // Limited to the 10 users + * const aggregations = await prisma.user.aggregate({ + * _avg: { + * age: true, + * }, + * where: { + * email: { + * contains: "prisma.io", + * }, + * }, + * orderBy: { + * age: "asc", + * }, + * take: 10, + * }) + **/ + aggregate(args: Subset): Prisma.PrismaPromise> + + /** + * Group by Jsons. + * Note, that providing `undefined` is treated as the value not being there. + * Read more here: https://pris.ly/d/null-undefined + * @param {JsonsGroupByArgs} args - Group by arguments. + * @example + * // Group by city, order by createdAt, get count + * const result = await prisma.user.groupBy({ + * by: ['city', 'createdAt'], + * orderBy: { + * createdAt: true + * }, + * _count: { + * _all: true + * }, + * }) + * + **/ + groupBy< + T extends JsonsGroupByArgs, + HasSelectOrTake extends Or< + Extends<'skip', Keys>, + Extends<'take', Keys> + >, + OrderByArg extends True extends HasSelectOrTake + ? { orderBy: JsonsGroupByArgs['orderBy'] } + : { orderBy?: JsonsGroupByArgs['orderBy'] }, + OrderFields extends ExcludeUnderscoreKeys>>, + ByFields extends TupleToUnion, + ByValid extends Has, + HavingFields extends GetHavingFields, + HavingValid extends Has, + ByEmpty extends T['by'] extends never[] ? True : False, + InputErrors extends ByEmpty extends True + ? `Error: "by" must not be empty.` + : HavingValid extends 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 Keys + ? 'orderBy' extends Keys + ? ByValid extends 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 Keys + ? 'orderBy' extends Keys + ? ByValid extends 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 True + ? {} + : { + [P in OrderFields]: P extends ByFields + ? never + : `Error: Field "${P}" in "orderBy" needs to be provided in "by"` + }[OrderFields] + >(args: SubsetIntersection & InputErrors): {} extends InputErrors ? GetJsonsGroupByPayload : Prisma.PrismaPromise + + } + + /** + * The delegate class that acts as a "Promise-like" for Jsons. + * Why is this prefixed with `Prisma__`? + * Because we want to prevent naming conflicts as mentioned in + * https://github.com/prisma/prisma-client-js/issues/707 + */ + export class Prisma__JsonsClient implements Prisma.PrismaPromise { + private readonly _dmmf; + private readonly _queryType; + private readonly _rootField; + private readonly _clientMethod; + private readonly _args; + private readonly _dataPath; + private readonly _errorFormat; + private readonly _measurePerformance?; + private _isList; + private _callsite; + private _requestPromise?; + readonly [Symbol.toStringTag]: 'PrismaPromise'; + constructor(_dmmf: runtime.DMMFClass, _queryType: 'query' | 'mutation', _rootField: string, _clientMethod: string, _args: any, _dataPath: string[], _errorFormat: ErrorFormat, _measurePerformance?: boolean | undefined, _isList?: boolean); + + + private get _document(); + /** + * Attaches callbacks for the resolution and/or rejection of the Promise. + * @param onfulfilled The callback to execute when the Promise is resolved. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of which ever callback is executed. + */ + then(onfulfilled?: ((value: T) => TResult1 | PromiseLike) | undefined | null, onrejected?: ((reason: any) => TResult2 | PromiseLike) | undefined | null): Promise; + /** + * Attaches a callback for only the rejection of the Promise. + * @param onrejected The callback to execute when the Promise is rejected. + * @returns A Promise for the completion of the callback. + */ + catch(onrejected?: ((reason: any) => TResult | PromiseLike) | undefined | null): Promise; + /** + * Attaches a callback that is invoked when the Promise is settled (fulfilled or rejected). The + * resolved value cannot be modified from the callback. + * @param onfinally The callback to execute when the Promise is settled (fulfilled or rejected). + * @returns A Promise for the completion of the callback. + */ + finally(onfinally?: (() => void) | undefined | null): Promise; + } + + + + // Custom InputTypes + + /** + * Jsons base type for findUnique actions + */ + export type JsonsFindUniqueArgsBase = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * Filter, which Jsons to fetch. + */ + where: JsonsWhereUniqueInput + } + + /** + * Jsons findUnique + */ + export interface JsonsFindUniqueArgs extends JsonsFindUniqueArgsBase { + /** + * Throw an Error if query returns no results + * @deprecated since 4.0.0: use `findUniqueOrThrow` method instead + */ + rejectOnNotFound?: RejectOnNotFound + } + + + /** + * Jsons findUniqueOrThrow + */ + export type JsonsFindUniqueOrThrowArgs = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * Filter, which Jsons to fetch. + */ + where: JsonsWhereUniqueInput + } + + + /** + * Jsons base type for findFirst actions + */ + export type JsonsFindFirstArgsBase = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * Filter, which Jsons to fetch. + */ + where?: JsonsWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Jsons to fetch. + */ + orderBy?: Enumerable + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Jsons. + */ + cursor?: JsonsWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Jsons from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Jsons. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Jsons. + */ + distinct?: Enumerable + } + + /** + * Jsons findFirst + */ + export interface JsonsFindFirstArgs extends JsonsFindFirstArgsBase { + /** + * Throw an Error if query returns no results + * @deprecated since 4.0.0: use `findFirstOrThrow` method instead + */ + rejectOnNotFound?: RejectOnNotFound + } + + + /** + * Jsons findFirstOrThrow + */ + export type JsonsFindFirstOrThrowArgs = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * Filter, which Jsons to fetch. + */ + where?: JsonsWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Jsons to fetch. + */ + orderBy?: Enumerable + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for searching for Jsons. + */ + cursor?: JsonsWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Jsons from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Jsons. + */ + skip?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/distinct Distinct Docs} + * + * Filter by unique combinations of Jsons. + */ + distinct?: Enumerable + } + + + /** + * Jsons findMany + */ + export type JsonsFindManyArgs = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * Filter, which Jsons to fetch. + */ + where?: JsonsWhereInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/sorting Sorting Docs} + * + * Determine the order of Jsons to fetch. + */ + orderBy?: Enumerable + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination#cursor-based-pagination Cursor Docs} + * + * Sets the position for listing Jsons. + */ + cursor?: JsonsWhereUniqueInput + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Take `±n` Jsons from the position of the cursor. + */ + take?: number + /** + * {@link https://www.prisma.io/docs/concepts/components/prisma-client/pagination Pagination Docs} + * + * Skip the first `n` Jsons. + */ + skip?: number + distinct?: Enumerable + } + + + /** + * Jsons create + */ + export type JsonsCreateArgs = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * The data needed to create a Jsons. + */ + data: XOR + } + + + /** + * Jsons createMany + */ + export type JsonsCreateManyArgs = { + /** + * The data used to create many Jsons. + */ + data: Enumerable + skipDuplicates?: boolean + } + + + /** + * Jsons update + */ + export type JsonsUpdateArgs = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * The data needed to update a Jsons. + */ + data: XOR + /** + * Choose, which Jsons to update. + */ + where: JsonsWhereUniqueInput + } + + + /** + * Jsons updateMany + */ + export type JsonsUpdateManyArgs = { + /** + * The data used to update Jsons. + */ + data: XOR + /** + * Filter which Jsons to update + */ + where?: JsonsWhereInput + } + + + /** + * Jsons upsert + */ + export type JsonsUpsertArgs = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * The filter to search for the Jsons to update in case it exists. + */ + where: JsonsWhereUniqueInput + /** + * In case the Jsons found by the `where` argument doesn't exist, create a new Jsons with this data. + */ + create: XOR + /** + * In case the Jsons was found with the provided `where` argument, update it with this data. + */ + update: XOR + } + + + /** + * Jsons delete + */ + export type JsonsDeleteArgs = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + /** + * Filter which Jsons to delete. + */ + where: JsonsWhereUniqueInput + } + + + /** + * Jsons deleteMany + */ + export type JsonsDeleteManyArgs = { + /** + * Filter which Jsons to delete + */ + where?: JsonsWhereInput + } + + + /** + * Jsons without action + */ + export type JsonsArgs = { + /** + * Select specific fields to fetch from the Jsons + */ + select?: JsonsSelect | null + } + + + + /** + * Enums + */ + + export const BoolsScalarFieldEnum: { + id: 'id', + b: 'b' + }; + + export type BoolsScalarFieldEnum = (typeof BoolsScalarFieldEnum)[keyof typeof BoolsScalarFieldEnum] + + + export const DatetimesScalarFieldEnum: { + id: 'id', + d: 'd', + t: 't' + }; + + export type DatetimesScalarFieldEnum = (typeof DatetimesScalarFieldEnum)[keyof typeof DatetimesScalarFieldEnum] + + + export const FloatsScalarFieldEnum: { + id: 'id', + f4: 'f4', + f8: 'f8' + }; + + export type FloatsScalarFieldEnum = (typeof FloatsScalarFieldEnum)[keyof typeof FloatsScalarFieldEnum] + + + export const IntsScalarFieldEnum: { + id: 'id', + i2: 'i2', + i4: 'i4', + i8: 'i8' + }; + + export type IntsScalarFieldEnum = (typeof IntsScalarFieldEnum)[keyof typeof IntsScalarFieldEnum] + + + export const ItemsScalarFieldEnum: { + id: 'id', + content: 'content', + content_text_null: 'content_text_null', + content_text_null_default: 'content_text_null_default', + intvalue_null: 'intvalue_null', + intvalue_null_default: 'intvalue_null_default' + }; + + export type ItemsScalarFieldEnum = (typeof ItemsScalarFieldEnum)[keyof typeof ItemsScalarFieldEnum] + + + export const JsonNullValueFilter: { + DbNull: typeof DbNull, + JsonNull: typeof JsonNull, + AnyNull: typeof AnyNull + }; + + export type JsonNullValueFilter = (typeof JsonNullValueFilter)[keyof typeof JsonNullValueFilter] + + + export const JsonsScalarFieldEnum: { + id: 'id', + js: 'js', + jsb: 'jsb' + }; + + export type JsonsScalarFieldEnum = (typeof JsonsScalarFieldEnum)[keyof typeof JsonsScalarFieldEnum] + + + export const NullableJsonNullValueInput: { + DbNull: typeof DbNull, + JsonNull: typeof JsonNull + }; + + export type NullableJsonNullValueInput = (typeof NullableJsonNullValueInput)[keyof typeof NullableJsonNullValueInput] + + + export const OtherItemsScalarFieldEnum: { + id: 'id', + content: 'content', + item_id: 'item_id' + }; + + export type OtherItemsScalarFieldEnum = (typeof OtherItemsScalarFieldEnum)[keyof typeof OtherItemsScalarFieldEnum] + + + export const QueryMode: { + default: 'default', + insensitive: 'insensitive' + }; + + export type QueryMode = (typeof QueryMode)[keyof typeof QueryMode] + + + export const SortOrder: { + asc: 'asc', + desc: 'desc' + }; + + export type SortOrder = (typeof SortOrder)[keyof typeof SortOrder] + + + export const TimestampsScalarFieldEnum: { + id: 'id', + created_at: 'created_at', + updated_at: 'updated_at' + }; + + export type TimestampsScalarFieldEnum = (typeof TimestampsScalarFieldEnum)[keyof typeof TimestampsScalarFieldEnum] + + + export const TransactionIsolationLevel: { + ReadUncommitted: 'ReadUncommitted', + ReadCommitted: 'ReadCommitted', + RepeatableRead: 'RepeatableRead', + Serializable: 'Serializable' + }; + + export type TransactionIsolationLevel = (typeof TransactionIsolationLevel)[keyof typeof TransactionIsolationLevel] + + + export const UuidsScalarFieldEnum: { + id: 'id' + }; + + export type UuidsScalarFieldEnum = (typeof UuidsScalarFieldEnum)[keyof typeof UuidsScalarFieldEnum] + + + /** + * Deep Input Types + */ + + + export type ItemsWhereInput = { + AND?: Enumerable + OR?: Enumerable + NOT?: Enumerable + id?: StringFilter | string + content?: StringFilter | string + content_text_null?: StringNullableFilter | string | null + content_text_null_default?: StringNullableFilter | string | null + intvalue_null?: IntNullableFilter | number | null + intvalue_null_default?: IntNullableFilter | number | null + other_items?: XOR | null + } + + export type ItemsOrderByWithRelationInput = { id?: SortOrder content?: SortOrder content_text_null?: SortOrder @@ -8518,6 +9423,43 @@ export namespace Prisma { f8?: FloatNullableWithAggregatesFilter | number | null } + export type JsonsWhereInput = { + AND?: Enumerable + OR?: Enumerable + NOT?: Enumerable + id?: StringFilter | string + js?: JsonNullableFilter + jsb?: JsonNullableFilter + } + + export type JsonsOrderByWithRelationInput = { + id?: SortOrder + js?: SortOrder + jsb?: SortOrder + } + + export type JsonsWhereUniqueInput = { + id?: string + } + + export type JsonsOrderByWithAggregationInput = { + id?: SortOrder + js?: SortOrder + jsb?: SortOrder + _count?: JsonsCountOrderByAggregateInput + _max?: JsonsMaxOrderByAggregateInput + _min?: JsonsMinOrderByAggregateInput + } + + export type JsonsScalarWhereWithAggregatesInput = { + AND?: Enumerable + OR?: Enumerable + NOT?: Enumerable + id?: StringWithAggregatesFilter | string + js?: JsonNullableWithAggregatesFilter + jsb?: JsonNullableWithAggregatesFilter + } + export type ItemsCreateInput = { id: string content: string @@ -8864,6 +9806,48 @@ export namespace Prisma { f8?: NullableFloatFieldUpdateOperationsInput | number | null } + export type JsonsCreateInput = { + id: string + js?: NullableJsonNullValueInput | InputJsonValue + jsb?: NullableJsonNullValueInput | InputJsonValue + } + + export type JsonsUncheckedCreateInput = { + id: string + js?: NullableJsonNullValueInput | InputJsonValue + jsb?: NullableJsonNullValueInput | InputJsonValue + } + + export type JsonsUpdateInput = { + id?: StringFieldUpdateOperationsInput | string + js?: NullableJsonNullValueInput | InputJsonValue + jsb?: NullableJsonNullValueInput | InputJsonValue + } + + export type JsonsUncheckedUpdateInput = { + id?: StringFieldUpdateOperationsInput | string + js?: NullableJsonNullValueInput | InputJsonValue + jsb?: NullableJsonNullValueInput | InputJsonValue + } + + export type JsonsCreateManyInput = { + id: string + js?: NullableJsonNullValueInput | InputJsonValue + jsb?: NullableJsonNullValueInput | InputJsonValue + } + + export type JsonsUpdateManyMutationInput = { + id?: StringFieldUpdateOperationsInput | string + js?: NullableJsonNullValueInput | InputJsonValue + jsb?: NullableJsonNullValueInput | InputJsonValue + } + + export type JsonsUncheckedUpdateManyInput = { + id?: StringFieldUpdateOperationsInput | string + js?: NullableJsonNullValueInput | InputJsonValue + jsb?: NullableJsonNullValueInput | InputJsonValue + } + export type StringFilter = { equals?: string in?: Enumerable | string @@ -9264,6 +10248,67 @@ export namespace Prisma { _min?: NestedFloatNullableFilter _max?: NestedFloatNullableFilter } + export type JsonNullableFilter = + | PatchUndefined< + Either, Exclude, 'path'>>, + Required + > + | OptionalFlat, 'path'>> + + export type JsonNullableFilterBase = { + equals?: InputJsonValue | JsonNullValueFilter + path?: string[] + string_contains?: string + string_starts_with?: string + string_ends_with?: string + array_contains?: InputJsonValue | null + array_starts_with?: InputJsonValue | null + array_ends_with?: InputJsonValue | null + lt?: InputJsonValue + lte?: InputJsonValue + gt?: InputJsonValue + gte?: InputJsonValue + not?: InputJsonValue | JsonNullValueFilter + } + + export type JsonsCountOrderByAggregateInput = { + id?: SortOrder + js?: SortOrder + jsb?: SortOrder + } + + export type JsonsMaxOrderByAggregateInput = { + id?: SortOrder + } + + export type JsonsMinOrderByAggregateInput = { + id?: SortOrder + } + export type JsonNullableWithAggregatesFilter = + | PatchUndefined< + Either, Exclude, 'path'>>, + Required + > + | OptionalFlat, 'path'>> + + export type JsonNullableWithAggregatesFilterBase = { + equals?: InputJsonValue | JsonNullValueFilter + path?: string[] + string_contains?: string + string_starts_with?: string + string_ends_with?: string + array_contains?: InputJsonValue | null + array_starts_with?: InputJsonValue | null + array_ends_with?: InputJsonValue | null + lt?: InputJsonValue + lte?: InputJsonValue + gt?: InputJsonValue + gte?: InputJsonValue + not?: InputJsonValue | JsonNullValueFilter + _count?: NestedIntNullableFilter + _min?: NestedJsonNullableFilter + _max?: NestedJsonNullableFilter + } export type OtherItemsCreateNestedOneWithoutItemsInput = { create?: XOR @@ -9569,6 +10614,28 @@ export namespace Prisma { _min?: NestedFloatNullableFilter _max?: NestedFloatNullableFilter } + export type NestedJsonNullableFilter = + | PatchUndefined< + Either, Exclude, 'path'>>, + Required + > + | OptionalFlat, 'path'>> + + export type NestedJsonNullableFilterBase = { + equals?: InputJsonValue | JsonNullValueFilter + path?: string[] + string_contains?: string + string_starts_with?: string + string_ends_with?: string + array_contains?: InputJsonValue | null + array_starts_with?: InputJsonValue | null + array_ends_with?: InputJsonValue | null + lt?: InputJsonValue + lte?: InputJsonValue + gt?: InputJsonValue + gte?: InputJsonValue + not?: InputJsonValue | JsonNullValueFilter + } export type OtherItemsCreateWithoutItemsInput = { id: string diff --git a/e2e/satellite_client/src/prisma/schema.prisma b/e2e/satellite_client/src/prisma/schema.prisma index e051113be1..83aa9c4a5a 100644 --- a/e2e/satellite_client/src/prisma/schema.prisma +++ b/e2e/satellite_client/src/prisma/schema.prisma @@ -72,3 +72,10 @@ model Floats { f8 Float? @db.DoublePrecision /// @zod.custom.use(z.number().or(z.nan())) @@map("floats") } + +model Jsons { + id String @id + js Json? @db.Json + jsb Json? + @@map("jsons") +} diff --git a/e2e/tests/03.19_node_satellite_can_sync_json.lux b/e2e/tests/03.19_node_satellite_can_sync_json.lux new file mode 100644 index 0000000000..45ad8c412f --- /dev/null +++ b/e2e/tests/03.19_node_satellite_can_sync_json.lux @@ -0,0 +1,134 @@ +[doc NodeJS Satellite correctly syncs json values from and to Electric] +[include _shared.luxinc] +[include _satellite_macros.luxinc] + +[invoke setup] + +[shell proxy_1] + [local sql= + """ + CREATE TABLE public.jsons ( + id TEXT PRIMARY KEY, + -- js JSON, + jsb JSONB + ); + ALTER TABLE public.jsons ENABLE ELECTRIC; + """] + [invoke migrate_pg 20230908 $sql] + +[invoke setup_client 1 electric_1 5133] + +[shell satellite_1] + [invoke node_await_table "jsons"] + [invoke node_sync_table "jsons"] + +[shell pg_1] + #!INSERT INTO public.jsons (id, js, jsb) VALUES ('row1', '{ "a": 1, "c": true, "b": "foo", "d": null, "e": [1,2,3] }', '[ { "a": 1 }, { "d": false, "b": 5 } ]'); + !INSERT INTO public.jsons (id, jsb) VALUES ('row1', '[ { "a": 1 }, { "d": false, "b": 5 } ]'); + ??INSERT 0 1 + +[shell satellite_1] + # Wait for the rows to arrive + [invoke node_await_get_json "row1"] + + # read raw JSON that is stored in the DB + # json must preserve whitespace and key ordering + #[invoke node_get_json_raw "row1" "{ a: 1, c: true, b: \"foo\", d: null, e: [1,2,3] }"] + # when parsed as JSON, whitespace is trimmed but key order is kept + #[invoke node_get_json "row1" "{ a: 1, c: true, b: \"foo\", d: null, e: [1,2,3] }"] + # jsonb trims white space and sorts keys + [invoke node_get_jsonb_raw "row1" "[{\"a\": 1}, {\"b\": 5, \"d\": false}]"] + [invoke node_get_jsonb "row1" "[ { a: 1 }, { b: 5, d: false } ]"] + + # write JSON null value and DB NULL value + [invoke node_write_json "row2" "client.JsonNull" "null"] + # read JsonNull value + #[invoke node_get_json "row2" "{ __is_electric_json_null__: true }"] + [invoke node_get_jsonb "row2" "null"] + + [invoke node_write_json "row3" "null" "client.JsonNull"] + # read JsonNull value + #[invoke node_get_json "row3" "null"] + [invoke node_get_jsonb "row3" "{ __is_electric_json_null__: true }"] + + # write regular JSON values + [invoke node_write_json "row4" 500 "{ a: true, b: [ 1, 2 ] }"] + #[invoke node_get_json "row4" 500] + [invoke node_get_jsonb "row4" "{ a: true, b: [ 1, 2 ] }"] + + [invoke node_write_json "row5" "'bar'" "[ 1, { a: 'foo' }, true ]"] + # [invoke node_get_json "row5" "'bar'"] + [invoke node_get_jsonb "row5" "[ 1, { a: 'foo' }, true ]"] + + [invoke node_write_json "row6" "null" "[\"it's ⚡\", {}, \"\\u2603 under \\u2602\"]"] + [invoke node_get_jsonb "row6" "[ \"it's ⚡\", {}, '☃ under ☂' ]"] + + # Even though JSON can encode the NUL code point and unpaired surrogates, those will fail Postgres' jsonb validation. + # Per the builtin JSON.stringify() function: + # + # > JSON.stringify("hello\x00NUL") + # '"hello\\u0000NUL"' + # > JSON.stringify("\ud83d\ude43") + # '"🙃"' + # > JSON.stringify("\ud83d") + # '"\\ud83d"' + # > JSON.stringify("\ude43") + # '"\\ude43"' + # + # See VAX-1365. + # + # NOTE: this currently causes Electric's validation to fail because the chosen JSON library + # does not support Unicode escape sequences. + #[invoke node_write_json "row7" "null" "['\x00', '\ud83d\ude43', '\ud83d', '\ude43']"] + #[invoke node_get_jsonb "row7" "[ '\"\\u0000\"', '\"🙃\"', '\"\\ud83d\"', '\"\\ude43\"' ]"] + +[shell pg_1] + [invoke wait-for "SELECT * FROM public.jsons;" "row4" 10 $psql] + + !SELECT * FROM public.jsons; + #??row1 | {"a": 1, "c": true, "b": "foo", "d": null, "e": [1,2,3] } | [{"a": 1}, {"b": {"c": true}, "d": false}] + #??row2 | { __is_electric_json_null__: true } | null + #??row3 | 500 | {"a": true, "b": [1, 2]} + #??row4 | [ 1, { a: "foo" }, true ] | "bar" + ??row1 | [{"a": 1}, {"b": 5, "d": false}] + ??row2 | + ??row3 | null + ??row4 | {"a": true, "b": [1, 2]} + ??row5 | [1, {"a": "foo"}, true] + ??row6 | ["it's ⚡", {}, "☃ under ☂"] + +# Start a new Satellite client and verify that it receives all rows +[invoke setup_client 2 electric_1 5133] + +[shell satellite_2] + [invoke node_await_table "jsons"] + [invoke node_sync_table "jsons"] + + # Wait for the rows to arrive + [invoke node_await_get_json "row6"] + + # read raw JSON that is stored in the DB + # json must preserve whitespace and key ordering + #[invoke node_get_json_raw "row1" "{ a: 1, c: true, b: \"foo\", d: null, e: [1,2,3] }"] + # when parsed as JSON, whitespace is trimmed but key order is kept + #[invoke node_get_json "row1" "{ a: 1, c: true, b: \"foo\", d: null, e: [1,2,3] }"] + # jsonb trims white space and sorts keys + [invoke node_get_jsonb_raw "row1" "[{\"a\": 1}, {\"b\": 5, \"d\": false}]"] + [invoke node_get_jsonb "row1" "[ { a: 1 }, { b: 5, d: false } ]"] + + #[invoke node_get_json "row2" "{ __is_electric_json_null__: true }"] + [invoke node_get_jsonb "row2" "null"] + + #[invoke node_get_json "row3" "null"] + [invoke node_get_jsonb "row3" "{ __is_electric_json_null__: true }"] + + #[invoke node_get_json "row4" 500] + [invoke node_get_jsonb "row4" "{ a: true, b: [ 1, 2 ] }"] + + #[invoke node_get_json "row5" "'bar'"] + [invoke node_get_jsonb "row5" "[ 1, { a: 'foo' }, true ]"] + + [invoke node_get_jsonb "row6" "[ \"it's ⚡\", {}, '☃ under ☂' ]"] + +[cleanup] + [invoke teardown] diff --git a/e2e/tests/_satellite_macros.luxinc b/e2e/tests/_satellite_macros.luxinc index b124e479a9..556be0d966 100644 --- a/e2e/tests/_satellite_macros.luxinc +++ b/e2e/tests/_satellite_macros.luxinc @@ -55,6 +55,10 @@ [invoke wait-for "await client.get_float(db, '${id}')" "${id}" 10 $node] [endmacro] +[macro node_await_get_json id] + [invoke wait-for "await client.get_jsonb(db, '${id}')" "${id}" 10 $node] +[endmacro] + [macro node_write_float id f4_value f8_value] # Can write valid floats to the DB !await client.write_float(db, '${id}', ${f4_value}, ${f8_value}) @@ -80,6 +84,38 @@ ??$node [endmacro] +[macro node_get_json_raw id expected_json] + !await client.get_json_raw(db, '${id}') + ??${expected_json} + ??$node +[endmacro] + +[macro node_get_jsonb_raw id expected_jsonb] + !await client.get_jsonb_raw(db, '${id}') + ??${expected_jsonb} + ??$node +[endmacro] + +[macro node_get_json id expected_json] + !await client.get_json(db, '${id}') + ??{ id: '${id}', js: ${expected_json} } + ??$node +[endmacro] + +[macro node_get_jsonb id expected_jsonb] + !await client.get_jsonb(db, '${id}') + ??{ id: '${id}', jsb: ${expected_jsonb} } + ??$node +[endmacro] + +[macro node_write_json id json_value jsonb_value] + # Can write valid JSON to the DB + !await client.write_json(db, '${id}', ${json_value}, ${jsonb_value}) + #??{ id: '${id}', js: ${json_value}, jsb: ${jsonb_value} } + # ??{ id: '${id}', jsb: ${jsonb_value} } + ??$node +[endmacro] + [macro node_await_get_timestamps match] [invoke wait-for "await client.get_timestamps(db)" "${match}" 10 $node] [endmacro] diff --git a/generator/src/functions/contentWriters/writeInputJsonValue.ts b/generator/src/functions/contentWriters/writeInputJsonValue.ts index e7cecb55d9..9e0382b741 100644 --- a/generator/src/functions/contentWriters/writeInputJsonValue.ts +++ b/generator/src/functions/contentWriters/writeInputJsonValue.ts @@ -19,6 +19,7 @@ export const writeInputJsonValue = ({ ) .withIndentationLevel(1, () => { writer + .writeLine(`z.null(),`) .writeLine(`z.string(),`) .writeLine(`z.number(),`) .writeLine(`z.boolean(),`) diff --git a/generator/src/functions/contentWriters/writeJsonValue.ts b/generator/src/functions/contentWriters/writeJsonValue.ts index b677b730ae..17ac51b7ef 100644 --- a/generator/src/functions/contentWriters/writeJsonValue.ts +++ b/generator/src/functions/contentWriters/writeJsonValue.ts @@ -19,6 +19,7 @@ export const writeJsonValue = ({ ) .withIndentationLevel(1, () => { writer + .writeLine(`z.null(),`) .writeLine(`z.string(),`) .writeLine(`z.number(),`) .writeLine(`z.boolean(),`) diff --git a/generator/src/functions/contentWriters/writeNullableJsonValue.ts b/generator/src/functions/contentWriters/writeNullableJsonValue.ts index e6d232c656..30a68c6f6d 100644 --- a/generator/src/functions/contentWriters/writeNullableJsonValue.ts +++ b/generator/src/functions/contentWriters/writeNullableJsonValue.ts @@ -9,20 +9,14 @@ export const writeNullableJsonValue = ({ if (useMultipleFiles && !getSingleFileContent) { writeImport('{ z }', 'zod') - writeImport('transformJsonNull', './transformJsonNull') writeImport('JsonValue', './JsonValue') } writer .blankLine() - .writeLine(`export const NullableJsonValue = z`) + .writeLine(`export const NullableJsonValue = JsonValue`) .withIndentationLevel(1, () => { - writer - .writeLine( - `.union([JsonValue, z.literal('DbNull'), z.literal('JsonNull')])` - ) - .writeLine('.nullable()') - .writeLine(`.transform((v) => transformJsonNull(v));`) + writer.writeLine('.nullable();') }) .blankLine() .writeLine( diff --git a/generator/src/functions/contentWriters/writePrismaEnum.ts b/generator/src/functions/contentWriters/writePrismaEnum.ts index 05062252d7..c5cc35392c 100644 --- a/generator/src/functions/contentWriters/writePrismaEnum.ts +++ b/generator/src/functions/contentWriters/writePrismaEnum.ts @@ -24,23 +24,11 @@ export const writePrismaEnum = ( }) writer.write(`]);`) } else { - writer - .conditionalWrite( - useMultipleFiles && name.includes('NullableJson'), - `import transformJsonNull from './transformJsonNull'` - ) - .blankLine() - .write(`export const ${name}Schema = z.enum([`) + writer.blankLine().write(`export const ${name}Schema = z.enum([`) values.forEach((value) => { writer.write(`'${value}',`) }) - writer - .write(`])`) - .conditionalWrite(!name.includes('Nullable'), `;`) - .conditionalWrite( - name.includes('Nullable'), - `.transform((v) => transformJsonNull(v));` - ) + writer.write(`])`).conditionalWrite(!name.includes('Nullable'), `;`) } if (useMultipleFiles && !getSingleFileContent) { diff --git a/generator/src/functions/contentWriters/writeTransformJsonNull.ts b/generator/src/functions/contentWriters/writeTransformJsonNull.ts index 4c23a67a33..d53517e37a 100644 --- a/generator/src/functions/contentWriters/writeTransformJsonNull.ts +++ b/generator/src/functions/contentWriters/writeTransformJsonNull.ts @@ -17,24 +17,6 @@ export const writeTransformJsonNull = ({ .newLine() .write(`export type NullableJsonInput = `) .write(`Prisma.JsonValue | `) - .write(`null | `) - .write(`'JsonNull' | `) - .write(`'DbNull' | `) - .write(`Prisma.NullTypes.DbNull | `) - .write(`Prisma.NullTypes.JsonNull;`) + .write(`null;`) .blankLine() - - writer - .write(`export const transformJsonNull = (v?: NullableJsonInput) => `) - .inlineBlock(() => { - writer - .writeLine(`if (!v || v === 'DbNull') return Prisma.DbNull;`) - .writeLine(`if (v === 'JsonNull') return Prisma.JsonNull;`) - .writeLine(`return v;`) - }) - .write(`;`) - - if (useMultipleFiles && !getSingleFileContent) { - writer.blankLine().writeLine(`export default transformJsonNull;`) - } } diff --git a/generator/src/functions/tableDescriptionWriters/writeTableSchemas.ts b/generator/src/functions/tableDescriptionWriters/writeTableSchemas.ts index f5fac010da..e92042bc6b 100644 --- a/generator/src/functions/tableDescriptionWriters/writeTableSchemas.ts +++ b/generator/src/functions/tableDescriptionWriters/writeTableSchemas.ts @@ -79,6 +79,10 @@ export function writeTableSchemas( writer .writeLine('export const schema = new DbSchema(tableSchemas, migrations)') .writeLine('export type Electric = ElectricClient') + .conditionalWriteLine( + dmmf.schema.hasJsonTypes, + 'export const JsonNull = { __is_electric_json_null__: true }' + ) } export function writeFieldsMap( @@ -105,7 +109,6 @@ function pgType(field: ExtendedDMMFField, modelName: string): string { const getTypeAttribute = () => attributes.find((a) => a.type.startsWith('@db')) switch (prismaType) { - // BigInt, Boolean, Bytes, DateTime, Decimal, Float, Int, JSON, String case 'String': return stringToPg(getTypeAttribute()) case 'Int': @@ -122,8 +125,8 @@ function pgType(field: ExtendedDMMFField, modelName: string): string { return 'DECIMAL' case 'Float': return floatToPg(getTypeAttribute()) - case 'JSON': - return 'JSON' + case 'Json': + return jsonToPg(attributes) default: return 'UNRECOGNIZED PRISMA TYPE' } @@ -138,6 +141,16 @@ function floatToPg(pgTypeAttribute: Attribute | undefined): string { } } +function jsonToPg(attributes: Array) { + const pgTypeAttribute = attributes.find((a) => a.type.startsWith('@db')) + if (pgTypeAttribute && pgTypeAttribute.type === '@db.Json') { + return 'JSON' + } else { + // default mapping for Prisma's `Json` type is PG's JSONB + return 'JSONB' + } +} + function dateTimeToPg( a: Attribute | undefined, field: string, diff --git a/generator/src/functions/writeMultiFileInputTypeFiles.ts b/generator/src/functions/writeMultiFileInputTypeFiles.ts index e142235035..f5d1c03240 100644 --- a/generator/src/functions/writeMultiFileInputTypeFiles.ts +++ b/generator/src/functions/writeMultiFileInputTypeFiles.ts @@ -48,7 +48,6 @@ export const writeInputTypeFiles: CreateFiles = ({ }) if (dmmf.schema.hasJsonTypes) { - writeExport(`{ transformJsonNull }`, `./transformJsonNull`) writeExport(`{ NullableJsonValue }`, `./NullableJsonValue`) writeExport(`{ InputJsonValue }`, `./InputJsonValue`) writeExport(`{ JsonValue }`, `./JsonValue`) diff --git a/generator/src/functions/writeSingleFileImportStatements.ts b/generator/src/functions/writeSingleFileImportStatements.ts index 87bdde0225..fb054681ac 100644 --- a/generator/src/functions/writeSingleFileImportStatements.ts +++ b/generator/src/functions/writeSingleFileImportStatements.ts @@ -8,18 +8,9 @@ export const writeSingleFileImportStatements: WriteStatements = ( dmmf, { writer, writeImport } ) => { - const { prismaClientPath } = dmmf.generatorConfig writeImport('{ z }', 'zod') - // Prisma should primarily be imported as a type, but if there are json fields, - // we need to import the whole namespace because the null transformation - // relies on the Prisma.JsonNull and Prisma.DbNull objects - - if (dmmf.schema.hasJsonTypes) { - writeImport(`{ Prisma }`, `${prismaClientPath}`) - } else { - writeImport(`type { Prisma }`, `${prismaClientPath}`) - } + writeImport(`type { Prisma }`, `./prismaClient`) if (dmmf.customImports) { dmmf.customImports.forEach((statement) => {