From b8d03e9b9fc0d7cc5753f76a64c7fb1911bb81a4 Mon Sep 17 00:00:00 2001 From: Emma Hamilton Date: Tue, 11 Mar 2025 14:57:59 +1000 Subject: [PATCH 1/2] remove jsonFieldTypePolyfilledForSQLite as Prisma now supports the Json scalar for SQLite --- .changeset/tired-tires-look.md | 7 + examples/cloudinary/schema.prisma | 8 +- packages/cloudinary/src/index.ts | 124 ++++++------- packages/core/src/fields/types/json/index.ts | 58 +++--- .../src/fields/types/multiselect/index.ts | 77 ++++---- packages/core/src/types/index.ts | 1 - .../json-field-type-polyfill-for-sqlite.ts | 161 ----------------- packages/fields-document/src/index.ts | 110 ++++++------ packages/fields-document/src/structure.ts | 167 +++++++++--------- 9 files changed, 271 insertions(+), 442 deletions(-) create mode 100644 .changeset/tired-tires-look.md delete mode 100644 packages/core/src/types/json-field-type-polyfill-for-sqlite.ts diff --git a/.changeset/tired-tires-look.md b/.changeset/tired-tires-look.md new file mode 100644 index 00000000000..acad50f5e00 --- /dev/null +++ b/.changeset/tired-tires-look.md @@ -0,0 +1,7 @@ +--- +"@keystone-6/core": major +"@keystone-6/fields-document": patch +"@keystone-6/cloudinary": patch +--- + +Removes `jsonFieldTypePolyfilledForSQLite` since Prisma now supports the Json scalar for SQLite diff --git a/examples/cloudinary/schema.prisma b/examples/cloudinary/schema.prisma index d48f2c78e4a..305b1f07544 100644 --- a/examples/cloudinary/schema.prisma +++ b/examples/cloudinary/schema.prisma @@ -13,8 +13,8 @@ generator client { } model Post { - id String @id @default(cuid()) - title String @default("") - content String @default("") - banner String? + id String @id @default(cuid()) + title String @default("") + content String @default("") + banner Json? } diff --git a/packages/cloudinary/src/index.ts b/packages/cloudinary/src/index.ts index 256982205b6..887208f368f 100644 --- a/packages/cloudinary/src/index.ts +++ b/packages/cloudinary/src/index.ts @@ -1,15 +1,16 @@ -import { randomBytes } from 'node:crypto' +import { g } from '@keystone-6/core' +import type { GArg, InferValueFromArg } from '@keystone-6/core/graphql-ts' import type { - CommonFieldConfig, + BaseFieldTypeInfo, BaseListTypeInfo, + CommonFieldConfig, FieldTypeFunc, - BaseFieldTypeInfo, } from '@keystone-6/core/types' -import { jsonFieldTypePolyfilledForSQLite } from '@keystone-6/core/types' -import { g } from '@keystone-6/core' +import { fieldType } from '@keystone-6/core/types' + import type Cloudinary from 'cloudinary' import { v2 as cloudinary } from 'cloudinary' -import type { GArg, InferValueFromArg } from '@keystone-6/core/graphql-ts' +import { randomBytes } from 'node:crypto' type StoredFile = { id: string @@ -167,62 +168,61 @@ export function cloudinaryImage({ } } - return jsonFieldTypePolyfilledForSQLite( - meta.provider, - { - ...config, - __ksTelemetryFieldTypeName: '@keystone-6/cloudinary', - input: { - create: { arg: inputArg, resolve: resolveInput }, - update: { arg: inputArg, resolve: resolveInput }, - }, - output: g.field({ - type: outputType, - resolve({ value }) { - if (value === null) return null - const val = value as any - return { - width: val?._meta.width, - height: val?._meta.width, - filesize: val?._meta.bytes, - publicUrl: val?._meta?.secure_url ?? null, - publicUrlTransformed: ({ - transformation, - }: { - transformation: InferValueFromArg> - }) => { - if (!val._meta) return null - - const { prettyName, ...rest } = transformation ?? {} - - // no formatting options provided, return the publicUrl field - if (!Object.keys(rest).length) return val?._meta?.secure_url ?? null - - const { public_id, format } = val._meta - - // ref https://github.com/cloudinary/cloudinary_npm/blob/439586eac73cee7f2803cf19f885e98f237183b3/src/utils.coffee#L472 - return cloudinary.url(public_id, { - type: 'upload', - format, - secure: true, // the default as of version 2 - url_suffix: prettyName, - transformation, - cloud_name: cloudinaryConfig.cloudName, - - // SDK analytics defaults to true in version 2 (ref https://github.com/cloudinary/cloudinary_npm/commit/d2510eb677e553a45bc7e363b35d2c20b4c4b144#diff-9aa82f0ed674e050695a7422b1cd56d43ce47e6953688a16a003bf49c3481622) - // we default to false for the least surprise, keeping this upgrade as a patch - urlAnalytics: false, - }) - }, - ...val, - } - }, - }), - views: '@keystone-6/cloudinary/views', + return fieldType({ + kind: 'scalar', + mode: 'optional', + scalar: 'Json', + map: config.db?.map, + })({ + ...config, + __ksTelemetryFieldTypeName: '@keystone-6/cloudinary', + input: { + create: { arg: inputArg, resolve: resolveInput }, + update: { arg: inputArg, resolve: resolveInput }, }, - { - map: config.db?.map, - } - ) + output: g.field({ + type: outputType, + resolve({ value }) { + if (value === null) return null + const val = value as any + return { + width: val?._meta.width, + height: val?._meta.width, + filesize: val?._meta.bytes, + publicUrl: val?._meta?.secure_url ?? null, + publicUrlTransformed: ({ + transformation, + }: { + transformation: InferValueFromArg> + }) => { + if (!val._meta) return null + + const { prettyName, ...rest } = transformation ?? {} + + // no formatting options provided, return the publicUrl field + if (!Object.keys(rest).length) return val?._meta?.secure_url ?? null + + const { public_id, format } = val._meta + + // ref https://github.com/cloudinary/cloudinary_npm/blob/439586eac73cee7f2803cf19f885e98f237183b3/src/utils.coffee#L472 + return cloudinary.url(public_id, { + type: 'upload', + format, + secure: true, // the default as of version 2 + url_suffix: prettyName, + transformation, + cloud_name: cloudinaryConfig.cloudName, + + // SDK analytics defaults to true in version 2 (ref https://github.com/cloudinary/cloudinary_npm/commit/d2510eb677e553a45bc7e363b35d2c20b4c4b144#diff-9aa82f0ed674e050695a7422b1cd56d43ce47e6953688a16a003bf49c3481622) + // we default to false for the least surprise, keeping this upgrade as a patch + urlAnalytics: false, + }) + }, + ...val, + } + }, + }), + views: '@keystone-6/cloudinary/views', + }) } } diff --git a/packages/core/src/fields/types/json/index.ts b/packages/core/src/fields/types/json/index.ts index 10b4fadd7b0..079f7fdbdd6 100644 --- a/packages/core/src/fields/types/json/index.ts +++ b/packages/core/src/fields/types/json/index.ts @@ -1,12 +1,11 @@ +import { g } from '../../..' import { type BaseListTypeInfo, - type JSONValue, - type FieldTypeFunc, type CommonFieldConfig, - jsonFieldTypePolyfilledForSQLite, + fieldType, + type FieldTypeFunc, + type JSONValue, } from '../../../types' -import { g } from '../../..' -import type { controller } from './views' type FieldTypeInfo = { item: JSONValue | null @@ -41,33 +40,30 @@ export const json = throw Error("isIndexed: 'unique' is not a supported option for field type json") } - return jsonFieldTypePolyfilledForSQLite( - meta.provider, - { - ...config, - __ksTelemetryFieldTypeName: '@keystone-6/json', - input: { - create: { - arg: g.arg({ type: g.JSON }), - resolve(val) { - return val === undefined ? defaultValue : val - }, + return fieldType({ + kind: 'scalar', + mode: 'optional', + scalar: 'Json', + default: + defaultValue === null + ? undefined + : { kind: 'literal', value: JSON.stringify(defaultValue) }, + map: config.db?.map, + extendPrismaSchema: config.db?.extendPrismaSchema, + })({ + ...config, + __ksTelemetryFieldTypeName: '@keystone-6/json', + input: { + create: { + arg: g.arg({ type: g.JSON }), + resolve(val) { + return val === undefined ? defaultValue : val }, - update: { arg: g.arg({ type: g.JSON }) }, }, - output: g.field({ type: g.JSON }), - views: '@keystone-6/core/fields/types/json/views', - getAdminMeta: (): Parameters[0]['fieldMeta'] => ({ - defaultValue, - }), + update: { arg: g.arg({ type: g.JSON }) }, }, - { - default: - defaultValue === null - ? undefined - : { kind: 'literal', value: JSON.stringify(defaultValue) }, - map: config.db?.map, - extendPrismaSchema: config.db?.extendPrismaSchema, - } - ) + output: g.field({ type: g.JSON }), + views: '@keystone-6/core/fields/types/json/views', + getAdminMeta: () => ({ defaultValue }), + }) } diff --git a/packages/core/src/fields/types/multiselect/index.ts b/packages/core/src/fields/types/multiselect/index.ts index 7ca19ad865e..86a71064a98 100644 --- a/packages/core/src/fields/types/multiselect/index.ts +++ b/packages/core/src/fields/types/multiselect/index.ts @@ -1,14 +1,15 @@ import { classify } from 'inflection' + +import { g } from '../../..' import { humanize } from '../../../lib/utils' import type { JSONValue } from '../../../types' import { type BaseListTypeInfo, - type FieldTypeFunc, type CommonFieldConfig, type FieldData, - jsonFieldTypePolyfilledForSQLite, + fieldType, + type FieldTypeFunc, } from '../../../types' -import { g } from '../../..' import { makeValidateHook } from '../../non-null-graphql' import type { controller } from './views' @@ -121,44 +122,42 @@ export function multiselect( } ) - return jsonFieldTypePolyfilledForSQLite( - meta.provider, - { - ...config, - ui, - __ksTelemetryFieldTypeName: '@keystone-6/multiselect', - hooks: { - ...config.hooks, - validate, - }, - views: '@keystone-6/core/fields/types/multiselect/views', - getAdminMeta: (): Parameters[0]['fieldMeta'] => ({ - options: transformedConfig.options, - type: config.type ?? 'string', - displayMode: displayMode, - defaultValue: [], - }), - input: { - create: { arg: g.arg({ type }), resolve: resolveCreate }, - update: { arg: g.arg({ type }), resolve: resolveUpdate }, - }, - output: g.field({ - type: type, - resolve({ value }) { - return value as any - }, - }), + return fieldType({ + kind: 'scalar', + scalar: 'Json', + mode, + map: config?.db?.map, + extendPrismaSchema: config.db?.extendPrismaSchema, + default: { + kind: 'literal', + value: JSON.stringify(defaultValue ?? null), + }, + })({ + ...config, + ui, + __ksTelemetryFieldTypeName: '@keystone-6/multiselect', + hooks: { + ...config.hooks, + validate, + }, + views: '@keystone-6/core/fields/types/multiselect/views', + getAdminMeta: (): Parameters[0]['fieldMeta'] => ({ + options: transformedConfig.options, + type: config.type ?? 'string', + displayMode: displayMode, + defaultValue: [], + }), + input: { + create: { arg: g.arg({ type }), resolve: resolveCreate }, + update: { arg: g.arg({ type }), resolve: resolveUpdate }, }, - { - mode, - map: config?.db?.map, - extendPrismaSchema: config.db?.extendPrismaSchema, - default: { - kind: 'literal', - value: JSON.stringify(defaultValue ?? null), + output: g.field({ + type: type, + resolve({ value }) { + return value as any }, - } - ) + }), + }) } } diff --git a/packages/core/src/types/index.ts b/packages/core/src/types/index.ts index 43f8722a5bf..99992bf0e8a 100644 --- a/packages/core/src/types/index.ts +++ b/packages/core/src/types/index.ts @@ -5,5 +5,4 @@ export * from './session' export * from './admin-meta' export * from './context' export * from './next-fields' -export { jsonFieldTypePolyfilledForSQLite } from './json-field-type-polyfill-for-sqlite' export * from './type-info' diff --git a/packages/core/src/types/json-field-type-polyfill-for-sqlite.ts b/packages/core/src/types/json-field-type-polyfill-for-sqlite.ts deleted file mode 100644 index 5a4e495dfbb..00000000000 --- a/packages/core/src/types/json-field-type-polyfill-for-sqlite.ts +++ /dev/null @@ -1,161 +0,0 @@ -import { g } from './schema' -import { - type BaseItem, - type CreateFieldInputArg, - type DatabaseProvider, - type FieldTypeWithoutDBField, - type JSONValue, - type KeystoneContext, - type ScalarDBField, - type UpdateFieldInputArg, - fieldType, -} from '.' -import type { - GArg, - GField, - GInputType, - GNullableInputType, - InferValueFromArg, -} from '@graphql-ts/schema' - -function mapOutputFieldToSQLite( - field: GField< - { value: JSONValue; item: BaseItem }, - Record>, - any, - JSONValue, - KeystoneContext - > -) { - const innerResolver = field.resolve || (({ value }) => value) - return g.field({ - type: field.type, - args: field.args, - deprecationReason: field.deprecationReason, - description: field.description, - extensions: field.extensions as any, - resolve( - source: { - value: string | null - item: BaseItem - }, - ...extra - ) { - if (source.value === null) { - return innerResolver(source, ...extra) - } - let value: JSONValue = null - try { - value = JSON.parse(source.value) - } catch (err) {} - return innerResolver({ item: source.item, value }, ...extra) - }, - }) -} - -function mapUpdateInputArgToSQLite>( - arg: UpdateFieldInputArg, Arg> | undefined -): UpdateFieldInputArg, Arg> | undefined { - if (arg === undefined) { - return undefined - } - return { - arg: arg.arg, - async resolve( - input: InferValueFromArg, - context: KeystoneContext, - relationshipInputResolver: any - ) { - const resolvedInput = - arg.resolve === undefined - ? input - : await arg.resolve(input, context, relationshipInputResolver) - if (resolvedInput === undefined || resolvedInput === null) { - return resolvedInput - } - return JSON.stringify(resolvedInput) - }, - } as any -} - -function mapCreateInputArgToSQLite>( - arg: CreateFieldInputArg, Arg> | undefined -): CreateFieldInputArg, Arg> | undefined { - if (arg === undefined) { - return undefined - } - return { - arg: arg.arg, - async resolve( - input: InferValueFromArg, - context: KeystoneContext, - relationshipInputResolver: any - ) { - const resolvedInput = - arg.resolve === undefined - ? input - : await arg.resolve(input as any, context, relationshipInputResolver) - if (resolvedInput === undefined || resolvedInput === null) { - return resolvedInput - } - return JSON.stringify(resolvedInput) - }, - } as any -} - -export function jsonFieldTypePolyfilledForSQLite< - CreateArg extends GArg, - UpdateArg extends GArg, ->( - provider: DatabaseProvider, - config: FieldTypeWithoutDBField< - ScalarDBField<'Json', 'optional'>, - CreateArg, - UpdateArg, - GArg, - GArg - > & { - input?: { - uniqueWhere?: undefined - orderBy?: undefined - } - }, - dbFieldConfig?: { - map?: string - mode?: 'required' | 'optional' - default?: ScalarDBField<'Json', 'optional'>['default'] - extendPrismaSchema?: (field: string) => string - } -) { - if (provider === 'sqlite') { - return fieldType({ - kind: 'scalar', - mode: dbFieldConfig?.mode ?? 'optional', - scalar: 'String', - default: dbFieldConfig?.default, - map: dbFieldConfig?.map, - extendPrismaSchema: dbFieldConfig?.extendPrismaSchema, - })({ - ...config, - input: { - create: mapCreateInputArgToSQLite(config.input?.create) as any, - update: mapUpdateInputArgToSQLite(config.input?.update), - }, - output: mapOutputFieldToSQLite(config.output), - extraOutputFields: Object.fromEntries( - Object.entries(config.extraOutputFields || {}).map(([key, field]) => [ - key, - mapOutputFieldToSQLite(field), - ]) - ), - }) - } - return fieldType({ - kind: 'scalar', - mode: (dbFieldConfig?.mode ?? 'optional') as 'optional', - scalar: 'Json', - default: dbFieldConfig?.default, - map: dbFieldConfig?.map, - extendPrismaSchema: dbFieldConfig?.extendPrismaSchema, - })(config) -} diff --git a/packages/fields-document/src/index.ts b/packages/fields-document/src/index.ts index a05e4178b7d..700e14b23e3 100644 --- a/packages/fields-document/src/index.ts +++ b/packages/fields-document/src/index.ts @@ -6,7 +6,7 @@ import { type FieldData, type FieldTypeFunc, type JSONValue, - jsonFieldTypePolyfilledForSQLite, + fieldType, } from '@keystone-6/core/types' import { GraphQLError } from 'graphql' import type { ComponentBlock } from './DocumentEditor/component-blocks/api-shared' @@ -115,67 +115,65 @@ export function document({ } } - return jsonFieldTypePolyfilledForSQLite( - meta.provider, - { - ...config, - __ksTelemetryFieldTypeName: '@keystone-6/document', - input: { - create: { - arg: g.arg({ type: g.JSON }), - resolve(val) { - if (val === undefined) { - val = [{ type: 'paragraph', children: [{ text: '' }] }] - } - return inputResolver(val) - }, + return fieldType({ + kind: 'scalar', + scalar: 'Json', + mode: 'required', + default: { + kind: 'literal', + value: JSON.stringify([{ type: 'paragraph', children: [{ text: '' }] }]), + }, + map: config.db?.map, + extendPrismaSchema: config.db?.extendPrismaSchema, + })({ + ...config, + __ksTelemetryFieldTypeName: '@keystone-6/document', + input: { + create: { + arg: g.arg({ type: g.JSON }), + resolve(val) { + if (val === undefined) { + val = [{ type: 'paragraph', children: [{ text: '' }] }] + } + return inputResolver(val) }, - update: { arg: g.arg({ type: g.JSON }), resolve: inputResolver }, }, - output: g.field({ - type: g.object<{ document: JSONValue }>()({ - name: `${meta.listKey}_${meta.fieldKey}_Document`, - fields: { - document: g.field({ - args: { - hydrateRelationships: g.arg({ - type: g.nonNull(g.Boolean), - defaultValue: false, - }), - }, - type: g.nonNull(g.JSON), - resolve({ document }, { hydrateRelationships }, context) { - return hydrateRelationships - ? addRelationshipData(document as any, context, relationships, componentBlocks) - : (document as any) - }, - }), - }, - }), - resolve({ value }) { - if (value === null) return null - return { document: value } + update: { arg: g.arg({ type: g.JSON }), resolve: inputResolver }, + }, + output: g.field({ + type: g.object<{ document: JSONValue }>()({ + name: `${meta.listKey}_${meta.fieldKey}_Document`, + fields: { + document: g.field({ + args: { + hydrateRelationships: g.arg({ + type: g.nonNull(g.Boolean), + defaultValue: false, + }), + }, + type: g.nonNull(g.JSON), + resolve({ document }, { hydrateRelationships }, context) { + return hydrateRelationships + ? addRelationshipData(document as any, context, relationships, componentBlocks) + : (document as any) + }, + }), }, }), - views: '@keystone-6/fields-document/views', - getAdminMeta(): Parameters[0]['fieldMeta'] { - return { - relationships, - documentFeatures, - componentBlocksPassedOnServer: Object.keys(componentBlocks), - } + resolve({ value }) { + if (value === null) return null + return { document: value } }, + }), + views: '@keystone-6/fields-document/views', + getAdminMeta(): Parameters[0]['fieldMeta'] { + return { + relationships, + documentFeatures, + componentBlocksPassedOnServer: Object.keys(componentBlocks), + } }, - { - mode: 'required', - default: { - kind: 'literal', - value: JSON.stringify([{ type: 'paragraph', children: [{ text: '' }] }]), - }, - map: config.db?.map, - extendPrismaSchema: config.db?.extendPrismaSchema, - } - ) + }) } } diff --git a/packages/fields-document/src/structure.ts b/packages/fields-document/src/structure.ts index dddbe82ee2d..d4c23a11ab7 100644 --- a/packages/fields-document/src/structure.ts +++ b/packages/fields-document/src/structure.ts @@ -5,7 +5,7 @@ import { type CommonFieldConfig, type FieldTypeFunc, type JSONValue, - jsonFieldTypePolyfilledForSQLite, + fieldType, } from '@keystone-6/core/types' import type { ComponentSchema } from './DocumentEditor/component-blocks/api' import { assertValidComponentSchema } from './DocumentEditor/component-blocks/field-assertions' @@ -49,100 +49,91 @@ export function structure({ typeof config.hooks?.resolveInput === 'function' ? config.hooks.resolveInput : config.hooks?.resolveInput?.update - return jsonFieldTypePolyfilledForSQLite( - meta.provider, - { - ...config, - hooks: { - ...config.hooks, - resolveInput: { - create: - typeof config.hooks?.resolveInput === 'function' - ? config.hooks.resolveInput - : config.hooks?.resolveInput?.create, - update: async args => { - let val = args.resolvedData[meta.fieldKey] - let prevVal = args.item[meta.fieldKey] - if (meta.provider === 'sqlite') { - prevVal = JSON.parse(prevVal as any) - val = args.inputData[meta.fieldKey] - } - val = await getValueForUpdate(schema, val, prevVal, args.context, []) - if (meta.provider === 'sqlite') { - val = JSON.stringify(val) - } - return innerUpdate - ? innerUpdate({ - ...args, - resolvedData: { ...args.resolvedData, [meta.fieldKey]: val }, - }) - : val - }, + return fieldType({ + kind: 'scalar', + scalar: 'Json', + default: { + kind: 'literal', + value: JSON.stringify(defaultValue), + }, + map: config.db?.map, + mode: 'required', + })({ + ...config, + hooks: { + ...config.hooks, + resolveInput: { + create: + typeof config.hooks?.resolveInput === 'function' + ? config.hooks.resolveInput + : config.hooks?.resolveInput?.create, + update: async args => { + let val = args.resolvedData[meta.fieldKey] + let prevVal = args.item[meta.fieldKey] + val = await getValueForUpdate(schema, val, prevVal, args.context, []) + return innerUpdate + ? innerUpdate({ + ...args, + resolvedData: { ...args.resolvedData, [meta.fieldKey]: val }, + }) + : val }, }, - input: { - create: { - arg: g.arg({ - type: getGraphQLInputType(name, schema, 'create', new Map(), meta), - }), - async resolve(val, context) { - return await getValueForCreate(schema, val, context, []) - }, - }, - update: { - arg: g.arg({ - type: getGraphQLInputType(name, schema, 'update', new Map(), meta), - }), - resolve(val) { - return val as any - }, + }, + input: { + create: { + arg: g.arg({ + type: getGraphQLInputType(name, schema, 'create', new Map(), meta), + }), + async resolve(val, context) { + return await getValueForCreate(schema, val, context, []) }, }, - output: g.field({ - type: g.object<{ value: JSONValue }>()({ - name: `${name}Output`, - fields: { - structure: getOutputGraphQLField( - name, - schema, - unreferencedConcreteInterfaceImplementations, - new Map(), - meta - ), - json: g.field({ - type: g.JSON, - args: { - hydrateRelationships: g.arg({ - type: g.nonNull(g.Boolean), - defaultValue: false, - }), - }, - resolve({ value }, args, context) { - if (!args.hydrateRelationships) return value - return addRelationshipDataToComponentProps(schema, value, (schema, value) => { - return fetchRelationshipData(context, schema, value) - }) - }, - }), - }, + update: { + arg: g.arg({ + type: getGraphQLInputType(name, schema, 'update', new Map(), meta), }), - resolve(source) { - return source + resolve(val) { + return val as any }, - }), - __ksTelemetryFieldTypeName: '@keystone-6/structure', - views: '@keystone-6/fields-document/structure-views', - getAdminMeta: () => ({}), - unreferencedConcreteInterfaceImplementations, + }, }, - { - default: { - kind: 'literal', - value: JSON.stringify(defaultValue), + output: g.field({ + type: g.object<{ value: JSONValue }>()({ + name: `${name}Output`, + fields: { + structure: getOutputGraphQLField( + name, + schema, + unreferencedConcreteInterfaceImplementations, + new Map(), + meta + ), + json: g.field({ + type: g.JSON, + args: { + hydrateRelationships: g.arg({ + type: g.nonNull(g.Boolean), + defaultValue: false, + }), + }, + resolve({ value }, args, context) { + if (!args.hydrateRelationships) return value + return addRelationshipDataToComponentProps(schema, value, (schema, value) => { + return fetchRelationshipData(context, schema, value) + }) + }, + }), + }, + }), + resolve(source) { + return source }, - map: config.db?.map, - mode: 'required', - } - ) + }), + __ksTelemetryFieldTypeName: '@keystone-6/structure', + views: '@keystone-6/fields-document/structure-views', + getAdminMeta: () => ({}), + unreferencedConcreteInterfaceImplementations, + }) } } From 79f88ccf856a6a7641070ec713da53be0c19d3ef Mon Sep 17 00:00:00 2001 From: Daniel Cousens Date: Tue, 15 Jul 2025 17:28:06 +1000 Subject: [PATCH 2/2] add example usage, and dont default for SQLite for now --- examples/default-values/schema.graphql | 3 +++ examples/default-values/schema.prisma | 17 +++++++------- examples/default-values/schema.ts | 22 ++++++++++++++++--- .../keystone-server/schema.prisma | 2 +- examples/document-field/schema.prisma | 4 ++-- .../schema.prisma | 2 +- .../keystone-server/schema.prisma | 2 +- examples/relationships/schema.prisma | 4 ++-- examples/structure-field/schema.prisma | 4 ++-- examples/usecase-blog/schema.prisma | 2 +- packages/core/src/fields/types/json/index.ts | 22 +++++++++++++------ .../src/fields/types/multiselect/index.ts | 13 +++++++---- packages/fields-document/src/index.ts | 16 +++++++++----- packages/fields-document/src/structure.ts | 13 +++++++---- 14 files changed, 85 insertions(+), 41 deletions(-) diff --git a/examples/default-values/schema.graphql b/examples/default-values/schema.graphql index 90120928777..62ee1344dc5 100644 --- a/examples/default-values/schema.graphql +++ b/examples/default-values/schema.graphql @@ -6,6 +6,7 @@ type Task { label: String priority: TaskPriorityType isComplete: Boolean + additionalData: JSON assignedTo: Person finishBy: DateTime viewCount: BigInt @@ -129,6 +130,7 @@ input TaskUpdateInput { label: String priority: TaskPriorityType isComplete: Boolean + additionalData: JSON assignedTo: PersonRelateToOneForUpdateInput finishBy: DateTime viewCount: BigInt @@ -149,6 +151,7 @@ input TaskCreateInput { label: String priority: TaskPriorityType isComplete: Boolean + additionalData: JSON assignedTo: PersonRelateToOneForCreateInput finishBy: DateTime viewCount: BigInt diff --git a/examples/default-values/schema.prisma b/examples/default-values/schema.prisma index 1ff4406b190..ab06f6ffe0a 100644 --- a/examples/default-values/schema.prisma +++ b/examples/default-values/schema.prisma @@ -13,14 +13,15 @@ generator client { } model Task { - id String @id @default(cuid()) - label String @default("") - priority String? - isComplete Boolean @default(false) - assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) - assignedToId String? @map("assignedTo") - finishBy DateTime? - viewCount BigInt? @default(0) + id String @id @default(cuid()) + label String @default("") + priority String? + isComplete Boolean @default(false) + additionalData Json? + assignedTo Person? @relation("Task_assignedTo", fields: [assignedToId], references: [id]) + assignedToId String? @map("assignedTo") + finishBy DateTime? + viewCount BigInt? @default(0) @@index([assignedToId]) } diff --git a/examples/default-values/schema.ts b/examples/default-values/schema.ts index face1749ba8..6c695c0b5be 100644 --- a/examples/default-values/schema.ts +++ b/examples/default-values/schema.ts @@ -1,8 +1,15 @@ +import type { Lists } from '.keystone/types' import { list } from '@keystone-6/core' -import { bigInt, checkbox, relationship, text, timestamp } from '@keystone-6/core/fields' -import { select } from '@keystone-6/core/fields' import { allowAll } from '@keystone-6/core/access' -import type { Lists } from '.keystone/types' +import { + bigInt, + checkbox, + json, + relationship, + select, + text, + timestamp, +} from '@keystone-6/core/fields' export const lists = { Task: list({ @@ -34,6 +41,15 @@ export const lists = { // static default: when a task is first created, it is incomplete isComplete: checkbox({ defaultValue: false }), + // static default: may stringify in Prisma schema + additionalData: json({ + defaultValue: { + conditions: [], + location: null, + notification: 'bongo', + }, + }), + assignedTo: relationship({ ref: 'Person.tasks', many: false, diff --git a/examples/document-field-customisation/keystone-server/schema.prisma b/examples/document-field-customisation/keystone-server/schema.prisma index b24e20b62aa..bb455ff31d4 100644 --- a/examples/document-field-customisation/keystone-server/schema.prisma +++ b/examples/document-field-customisation/keystone-server/schema.prisma @@ -23,7 +23,7 @@ model Post { id String @id @default(cuid()) title String @default("") slug String @unique @default("") - content String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") + content Json publishDate DateTime? @default(now()) author User? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") diff --git a/examples/document-field/schema.prisma b/examples/document-field/schema.prisma index 4081058267d..d61be13151d 100644 --- a/examples/document-field/schema.prisma +++ b/examples/document-field/schema.prisma @@ -17,7 +17,7 @@ model Post { title String @default("") slug String @unique @default("") status String? - content String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") + content Json publishDate DateTime? author Author? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") @@ -30,5 +30,5 @@ model Author { name String @default("") email String @unique @default("") posts Post[] @relation("Post_author") - bio String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") + bio Json } diff --git a/examples/framework-nextjs-app-directory/schema.prisma b/examples/framework-nextjs-app-directory/schema.prisma index fc6131d94ef..4a1df607ced 100644 --- a/examples/framework-nextjs-app-directory/schema.prisma +++ b/examples/framework-nextjs-app-directory/schema.prisma @@ -15,6 +15,6 @@ generator client { model User { id String @id @default(cuid()) name String @default("") - about String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") + about Json createdAt DateTime? @default(now()) } diff --git a/examples/framework-nextjs-two-servers/keystone-server/schema.prisma b/examples/framework-nextjs-two-servers/keystone-server/schema.prisma index b24e20b62aa..bb455ff31d4 100644 --- a/examples/framework-nextjs-two-servers/keystone-server/schema.prisma +++ b/examples/framework-nextjs-two-servers/keystone-server/schema.prisma @@ -23,7 +23,7 @@ model Post { id String @id @default(cuid()) title String @default("") slug String @unique @default("") - content String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") + content Json publishDate DateTime? @default(now()) author User? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") diff --git a/examples/relationships/schema.prisma b/examples/relationships/schema.prisma index 1afa4028e79..ca8779d0b14 100644 --- a/examples/relationships/schema.prisma +++ b/examples/relationships/schema.prisma @@ -19,8 +19,8 @@ model Post { categoryId String? @map("category") tags Tag[] @relation("Post_tags") related Post[] @relation("Post_related") - recommendations String @default("[]") - bundles String @default("[]") + recommendations Json + bundles Json from_Post_related Post[] @relation("Post_related") @@index([categoryId]) diff --git a/examples/structure-field/schema.prisma b/examples/structure-field/schema.prisma index dfeb6bd108e..423e1bd0485 100644 --- a/examples/structure-field/schema.prisma +++ b/examples/structure-field/schema.prisma @@ -13,8 +13,8 @@ generator client { } model Homepage { - id Int @id - metadata String @default("{\"featuredPosts\":[]}") + id Int @id + metadata Json } model Post { diff --git a/examples/usecase-blog/schema.prisma b/examples/usecase-blog/schema.prisma index d4c96a2390e..f7966a8c983 100644 --- a/examples/usecase-blog/schema.prisma +++ b/examples/usecase-blog/schema.prisma @@ -24,7 +24,7 @@ model Author { model Post { id String @id @default(cuid()) title String @default("") - content String @default("[{\"type\":\"paragraph\",\"children\":[{\"text\":\"\"}]}]") + content Json author Author? @relation("Post_author", fields: [authorId], references: [id]) authorId String? @map("author") tags Tag[] @relation("Post_tags") diff --git a/packages/core/src/fields/types/json/index.ts b/packages/core/src/fields/types/json/index.ts index 079f7fdbdd6..123cd3aac3d 100644 --- a/packages/core/src/fields/types/json/index.ts +++ b/packages/core/src/fields/types/json/index.ts @@ -30,12 +30,11 @@ export type JsonFieldConfig = CommonField db?: { map?: string; extendPrismaSchema?: (field: string) => string } } -export const json = - ({ - defaultValue = null, - ...config - }: JsonFieldConfig = {}): FieldTypeFunc => - meta => { +export function json({ + defaultValue = null, + ...config +}: JsonFieldConfig = {}): FieldTypeFunc { + return meta => { if ((config as any).isIndexed === 'unique') { throw Error("isIndexed: 'unique' is not a supported option for field type json") } @@ -47,7 +46,14 @@ export const json = default: defaultValue === null ? undefined - : { kind: 'literal', value: JSON.stringify(defaultValue) }, + : meta.provider === 'sqlite' + ? undefined + : { + kind: 'literal', + // TODO: waiting on https://github.com/prisma/prisma/issues/26571 + // input.create manages defaultValues anyway + value: JSON.stringify(defaultValue ?? null), + }, map: config.db?.map, extendPrismaSchema: config.db?.extendPrismaSchema, })({ @@ -57,6 +63,7 @@ export const json = create: { arg: g.arg({ type: g.JSON }), resolve(val) { + // TODO: redundant when https://github.com/prisma/prisma/issues/26571 is resolved return val === undefined ? defaultValue : val }, }, @@ -67,3 +74,4 @@ export const json = getAdminMeta: () => ({ defaultValue }), }) } +} diff --git a/packages/core/src/fields/types/multiselect/index.ts b/packages/core/src/fields/types/multiselect/index.ts index 86a71064a98..d8e8471bf8f 100644 --- a/packages/core/src/fields/types/multiselect/index.ts +++ b/packages/core/src/fields/types/multiselect/index.ts @@ -128,10 +128,15 @@ export function multiselect( mode, map: config?.db?.map, extendPrismaSchema: config.db?.extendPrismaSchema, - default: { - kind: 'literal', - value: JSON.stringify(defaultValue ?? null), - }, + default: + meta.provider === 'sqlite' + ? undefined + : { + kind: 'literal', + // TODO: waiting on https://github.com/prisma/prisma/issues/26571 + // input.create manages defaultValues anyway + value: JSON.stringify(defaultValue ?? null), + }, })({ ...config, ui, diff --git a/packages/fields-document/src/index.ts b/packages/fields-document/src/index.ts index 700e14b23e3..1d6601b20e8 100644 --- a/packages/fields-document/src/index.ts +++ b/packages/fields-document/src/index.ts @@ -115,14 +115,20 @@ export function document({ } } + const defaultValue = [{ type: 'paragraph', children: [{ text: '' }] }] return fieldType({ kind: 'scalar', scalar: 'Json', mode: 'required', - default: { - kind: 'literal', - value: JSON.stringify([{ type: 'paragraph', children: [{ text: '' }] }]), - }, + default: + meta.provider === 'sqlite' + ? undefined + : { + kind: 'literal', + // TODO: waiting on https://github.com/prisma/prisma/issues/26571 + // input.create manages defaultValues anyway + value: JSON.stringify(defaultValue ?? null), + }, map: config.db?.map, extendPrismaSchema: config.db?.extendPrismaSchema, })({ @@ -133,7 +139,7 @@ export function document({ arg: g.arg({ type: g.JSON }), resolve(val) { if (val === undefined) { - val = [{ type: 'paragraph', children: [{ text: '' }] }] + val = defaultValue } return inputResolver(val) }, diff --git a/packages/fields-document/src/structure.ts b/packages/fields-document/src/structure.ts index d4c23a11ab7..b7dbaf4973f 100644 --- a/packages/fields-document/src/structure.ts +++ b/packages/fields-document/src/structure.ts @@ -52,10 +52,15 @@ export function structure({ return fieldType({ kind: 'scalar', scalar: 'Json', - default: { - kind: 'literal', - value: JSON.stringify(defaultValue), - }, + default: + meta.provider === 'sqlite' + ? undefined + : { + kind: 'literal', + // TODO: waiting on https://github.com/prisma/prisma/issues/26571 + // input.create manages defaultValues anyway + value: JSON.stringify(defaultValue ?? null), + }, map: config.db?.map, mode: 'required', })({