From ad70ca403af3143a3083aed0adfb9508c0de74a2 Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Wed, 7 Feb 2024 17:35:58 -0500 Subject: [PATCH] fix: transform collection config to be JSON serializable --- packages/db/src/core/cli/migrations.ts | 10 +++- packages/db/src/core/queries.ts | 6 +- packages/db/src/core/types.ts | 80 ++++++++++++++++++-------- 3 files changed, 68 insertions(+), 28 deletions(-) diff --git a/packages/db/src/core/cli/migrations.ts b/packages/db/src/core/cli/migrations.ts index f6a87dd897..0356a1ec88 100644 --- a/packages/db/src/core/cli/migrations.ts +++ b/packages/db/src/core/cli/migrations.ts @@ -1,6 +1,6 @@ import deepDiff from 'deep-diff'; import { mkdir, readFile, readdir, writeFile } from 'fs/promises'; -import type { DBSnapshot } from '../types.js'; +import { collectionsSchema, type DBSnapshot } from '../types.js'; import type { AstroConfig } from 'astro'; import { cyan, green, yellow } from 'kleur/colors'; const { applyChange, diff: generateDiff } = deepDiff; @@ -82,7 +82,9 @@ export async function getMigrations(): Promise { return migrationFiles; } -export async function loadMigration(migration: string): Promise<{ diff: any[]; db: string[], confirm?: string[] }> { +export async function loadMigration( + migration: string +): Promise<{ diff: any[]; db: string[]; confirm?: string[] }> { return JSON.parse(await readFile(`./migrations/${migration}`, 'utf-8')); } @@ -117,7 +119,9 @@ export async function initializeFromMigrations(allMigrationFiles: string[]): Pro } export function createCurrentSnapshot(config: AstroConfig): DBSnapshot { - const schema = JSON.parse(JSON.stringify(config.db?.collections ?? {})); + // Parse to resolve non-serializable types like () => references + const collectionsConfig = collectionsSchema.parse(config.db?.collections ?? {}); + const schema = JSON.parse(JSON.stringify(collectionsConfig)); return { experimentalVersion: 1, schema }; } export function createEmptySnapshot(): DBSnapshot { diff --git a/packages/db/src/core/queries.ts b/packages/db/src/core/queries.ts index 5a80da2dd6..52e800216e 100644 --- a/packages/db/src/core/queries.ts +++ b/packages/db/src/core/queries.ts @@ -114,7 +114,9 @@ export function getCreateForeignKeyQueries(collectionName: string, collection: D let queries: string[] = []; for (const foreignKey of collection.foreignKeys ?? []) { const fields = asArray(foreignKey.fields); - const references = asArray(foreignKey.references()); + const references = asArray(foreignKey.references); + + console.log(references[0]); if (fields.length !== references.length) { throw new Error( @@ -184,7 +186,7 @@ export function getModifiers(fieldName: string, field: DBField) { export function getReferencesConfig(field: DBField) { const canHaveReferences = field.type === 'number' || field.type === 'text'; if (!canHaveReferences) return undefined; - return field.references?.(); + return field.references; } // Using `DBField` will not narrow `default` based on the column `type` diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts index ddb792fe62..fc144fb229 100644 --- a/packages/db/src/core/types.ts +++ b/packages/db/src/core/types.ts @@ -1,8 +1,9 @@ import type { SQLiteInsertValue } from 'drizzle-orm/sqlite-core'; import type { InferSelectModel } from 'drizzle-orm'; import type { SqliteDB, Table } from '../runtime/index.js'; -import { z } from 'zod'; -import { getTableName, SQL } from 'drizzle-orm'; +import { z, type ZodTypeDef } from 'zod'; +import { SQL } from 'drizzle-orm'; +import { errorMap } from './integration/error-map.js'; export type MaybePromise = T | Promise; export type MaybeArray = T | T[]; @@ -43,14 +44,19 @@ const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and( const numberFieldOptsSchema: z.ZodType< z.infer & { // ReferenceableField creates a circular type. Define ZodType to resolve. - references?: () => NumberField; + references?: NumberField; + }, + ZodTypeDef, + z.input & { + references?: () => NumberFieldInput; } > = numberFieldBaseSchema.and( z.object({ references: z .function() .returns(z.lazy(() => numberFieldSchema)) - .optional(), + .optional() + .transform((fn) => fn?.()), }) ); @@ -86,14 +92,19 @@ const textFieldBaseSchema = baseFieldSchema const textFieldOptsSchema: z.ZodType< z.infer & { // ReferenceableField creates a circular type. Define ZodType to resolve. - references?: () => TextField; + references?: TextField; + }, + ZodTypeDef, + z.input & { + references?: () => TextFieldInput; } > = textFieldBaseSchema.and( z.object({ references: z .function() .returns(z.lazy(() => textFieldSchema)) - .optional(), + .optional() + .transform((fn) => fn?.()), }) ); @@ -136,14 +147,22 @@ export const indexSchema = z.object({ unique: z.boolean().optional(), }); -const foreignKeysSchema: z.ZodType<{ +type ForeignKeysInput = { fields: MaybeArray; - references: () => MaybeArray; -}> = z.object({ + references: () => MaybeArray>; +}; + +type ForeignKeysOutput = Omit & { + // reference fn called in `transform`. Ensures output is JSON serializable. + references: MaybeArray>; +}; + +const foreignKeysSchema: z.ZodType = z.object({ fields: z.string().or(z.array(z.string())), references: z .function() - .returns(z.lazy(() => referenceableFieldSchema.or(z.array(referenceableFieldSchema)))), + .returns(z.lazy(() => referenceableFieldSchema.or(z.array(referenceableFieldSchema)))) + .transform((fn) => fn()), }); export type Indexes = Record>; @@ -165,11 +184,32 @@ export const writableCollectionSchema = baseCollectionSchema.extend({ }); export const collectionSchema = z.union([readableCollectionSchema, writableCollectionSchema]); -export const collectionsSchema = z.record(collectionSchema); +export const collectionsSchema = z.preprocess((rawCollections) => { + // Preprocess collections to append collection and field names to fields. + // Used to track collection info for references. + + // Use minimum parsing to ensure collection has a `fields` record. + const collections = z + .record( + z.object({ + fields: z.record(z.any()), + }) + ) + .parse(rawCollections, { errorMap }); + for (const [collectionName, collection] of Object.entries(collections)) { + for (const [fieldName, field] of Object.entries(collection.fields)) { + field.name = fieldName; + field.collection = collectionName; + } + } + return rawCollections; +}, z.record(collectionSchema)); export type BooleanField = z.infer; export type NumberField = z.infer; +export type NumberFieldInput = z.input; export type TextField = z.infer; +export type TextFieldInput = z.input; export type DateField = z.infer; // Type `Date` is the config input, `string` is the output for D1 storage export type DateFieldInput = z.input; @@ -183,7 +223,12 @@ export type FieldType = | JsonField['type']; export type DBField = z.infer; -export type DBFieldInput = DateFieldInput | BooleanField | NumberField | TextField | JsonField; +export type DBFieldInput = + | DateFieldInput + | BooleanField + | NumberFieldInput + | TextFieldInput + | JsonField; export type DBFields = z.infer; export type DBCollection = z.infer< typeof readableCollectionSchema | typeof writableCollectionSchema @@ -268,11 +313,6 @@ function baseDefineCollection, writable: TWritable ): ResolvedCollectionConfig { - for (const fieldName in userConfig.fields) { - const field = userConfig.fields[fieldName]; - // Store field name within the field itself to track references - field.name = fieldName; - } const meta: { table: Table } = { table: null! }; /** * We need to attach the Drizzle `table` at runtime using `_setMeta`. @@ -281,12 +321,6 @@ function baseDefineCollection }) => { Object.assign(meta, values); - - const tableName = getTableName(meta.table); - for (const fieldName in userConfig.fields) { - const field = userConfig.fields[fieldName]; - field.collection = tableName; - } }; return {