diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index 22cc44d6d4..21ab364a81 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -434,16 +434,16 @@ function isEmpty(obj: Record) { * @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column */ function canAlterTableAddColumn(field: DBField) { - if (field.unique) return false; + if (field.schema.unique) return false; if (hasRuntimeDefault(field)) return false; - if (!field.optional && !hasDefault(field)) return false; + if (!field.schema.optional && !hasDefault(field)) return false; if (hasPrimaryKey(field)) return false; if (getReferencesConfig(field)) return false; return true; } function canAlterTableDropColumn(field: DBField) { - if (field.unique) return false; + if (field.schema.unique) return false; if (hasPrimaryKey(field)) return false; return true; } @@ -461,10 +461,10 @@ function canRecreateTableWithoutDataLoss( if (hasPrimaryKey(a) && a.type !== 'number' && !hasDefault(a)) { return { dataLoss: true, fieldName, reason: 'added-required' }; } - if (!a.optional && !hasDefault(a)) { + if (!a.schema.optional && !hasDefault(a)) { return { dataLoss: true, fieldName, reason: 'added-required' }; } - if (!a.optional && a.unique) { + if (!a.schema.optional && a.schema.unique) { return { dataLoss: true, fieldName, reason: 'added-unique' }; } } @@ -546,7 +546,7 @@ function canChangeTypeWithoutQuery(oldField: DBField, newField: DBField) { // Using `DBField` will not narrow `default` based on the column `type` // Handle each field separately -type WithDefaultDefined = T & Required>; +type WithDefaultDefined = T & Required>; type DBFieldWithDefault = | WithDefaultDefined | WithDefaultDefined @@ -555,5 +555,5 @@ type DBFieldWithDefault = | WithDefaultDefined; function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault { - return !!(field.default && isSerializedSQL(field.default)); + return !!(field.schema.default && isSerializedSQL(field.schema.default)); } diff --git a/packages/db/src/core/integration/typegen.ts b/packages/db/src/core/integration/typegen.ts index bab881bf15..42af4bed6c 100644 --- a/packages/db/src/core/integration/typegen.ts +++ b/packages/db/src/core/integration/typegen.ts @@ -35,8 +35,8 @@ function generateTableType(name: string, collection: DBCollection): string { { // Only select fields Drizzle needs for inference type: field.type, - optional: field.optional, - default: field.default, + optional: field.schema.optional, + default: field.schema.default, }, ]) ) diff --git a/packages/db/src/core/queries.ts b/packages/db/src/core/queries.ts index da6dc2d5a5..7ae2eb2b47 100644 --- a/packages/db/src/core/queries.ts +++ b/packages/db/src/core/queries.ts @@ -123,7 +123,7 @@ export function getCreateForeignKeyQueries(collectionName: string, collection: D `Foreign key on ${collectionName} is misconfigured. \`fields\` and \`references\` must be the same length.` ); } - const referencedCollection = references[0]?.collection; + const referencedCollection = references[0]?.schema.collection; if (!referencedCollection) { throw new Error( `Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.` @@ -132,7 +132,7 @@ export function getCreateForeignKeyQueries(collectionName: string, collection: D const query = `FOREIGN KEY (${fields .map((f) => sqlite.escapeName(f)) .join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references - .map((r) => sqlite.escapeName(r.name!)) + .map((r) => sqlite.escapeName(r.schema.name!)) .join(', ')})`; queries.push(query); } @@ -160,10 +160,10 @@ export function getModifiers(fieldName: string, field: DBField) { if (hasPrimaryKey(field)) { return ' PRIMARY KEY'; } - if (!field.optional) { + if (!field.schema.optional) { modifiers += ' NOT NULL'; } - if (field.unique) { + if (field.schema.unique) { modifiers += ' UNIQUE'; } if (hasDefault(field)) { @@ -171,7 +171,7 @@ export function getModifiers(fieldName: string, field: DBField) { } const references = getReferencesConfig(field); if (references) { - const { collection, name } = references; + const { collection, name } = references.schema; if (!collection || !name) { throw new Error( `Invalid reference for field ${fieldName}. This is an unexpected error that should be reported to the Astro team.` @@ -186,12 +186,14 @@ 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.schema.references; } // Using `DBField` will not narrow `default` based on the column `type` // Handle each field separately -type WithDefaultDefined = T & Required>; +type WithDefaultDefined = T & { + schema: Required> +}; type DBFieldWithDefault = | WithDefaultDefined | WithDefaultDefined @@ -201,7 +203,7 @@ type DBFieldWithDefault = // Type narrowing the default fails on union types, so use a type guard export function hasDefault(field: DBField): field is DBFieldWithDefault { - if (field.default !== undefined) { + if (field.schema.default !== undefined) { return true; } if (hasPrimaryKey(field) && field.type === 'number') { @@ -222,8 +224,8 @@ function toDefault(def: T | SQL): string { } function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): string { - if (isSerializedSQL(column.default)) { - return column.default.sql; + if (isSerializedSQL(column.schema.default)) { + return column.schema.default.sql; } switch (column.type) { @@ -231,11 +233,11 @@ function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): str case 'number': case 'text': case 'date': - return toDefault(column.default); + return toDefault(column.schema.default); case 'json': { let stringified = ''; try { - stringified = JSON.stringify(column.default); + stringified = JSON.stringify(column.schema.default); } catch (e) { // eslint-disable-next-line no-console console.log( diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts index c1d76ff39e..6ddf50f739 100644 --- a/packages/db/src/core/types.ts +++ b/packages/db/src/core/types.ts @@ -29,9 +29,11 @@ const baseFieldSchema = z.object({ collection: z.string().optional(), }); -const booleanFieldSchema = baseFieldSchema.extend({ +const booleanFieldSchema = z.object({ type: z.literal('boolean'), - default: z.union([z.boolean(), sqlSchema]).optional(), + schema: baseFieldSchema.extend({ + default: z.union([z.boolean(), sqlSchema]).optional(), + }), }); const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and( @@ -71,11 +73,10 @@ const numberFieldOptsSchema: z.ZodType< }) ); -const numberFieldSchema = numberFieldOptsSchema.and( - z.object({ - type: z.literal('number'), - }) -); +const numberFieldSchema = z.object({ + type: z.literal('number'), + schema: numberFieldOptsSchema +}); const textFieldBaseSchema = baseFieldSchema .omit({ optional: true }) @@ -119,27 +120,30 @@ const textFieldOptsSchema: z.ZodType< }) ); -const textFieldSchema = textFieldOptsSchema.and( - z.object({ - type: z.literal('text'), - }) -); - -const dateFieldSchema = baseFieldSchema.extend({ - type: z.literal('date'), - default: z - .union([ - sqlSchema, - // allow date-like defaults in user config, - // transform to ISO string for D1 storage - z.coerce.date().transform((d) => d.toISOString()), - ]) - .optional(), +const textFieldSchema = z.object({ + type: z.literal('text'), + schema: textFieldOptsSchema }); -const jsonFieldSchema = baseFieldSchema.extend({ +const dateFieldSchema = z.object({ + type: z.literal('date'), + schema: baseFieldSchema.extend({ + default: z + .union([ + sqlSchema, + // allow date-like defaults in user config, + // transform to ISO string for D1 storage + z.coerce.date().transform((d) => d.toISOString()), + ]) + .optional(), + }) +}); + +const jsonFieldSchema = z.object({ type: z.literal('json'), - default: z.unknown().optional(), + schema: baseFieldSchema.extend({ + default: z.unknown().optional(), + }) }); const fieldSchema = z.union([ @@ -150,6 +154,7 @@ const fieldSchema = z.union([ jsonFieldSchema, ]); export const referenceableFieldSchema = z.union([textFieldSchema, numberFieldSchema]); + const fieldsSchema = z.record(fieldSchema); export const indexSchema = z.object({ @@ -350,20 +355,30 @@ type FieldOpts = Omit; type NumberFieldOpts = z.input; type TextFieldOpts = z.input; +function createField>(type: S, schema: T) { + return { + type, + /** + * @internal + */ + schema + }; +} + export const field = { number: (opts: T = {} as T) => { - return { type: 'number', ...opts } satisfies T & { type: 'number' }; + return createField('number', opts) satisfies { type: 'number' }; }, boolean: >(opts: T = {} as T) => { - return { type: 'boolean', ...opts } satisfies T & { type: 'boolean' }; + return createField('boolean', opts) satisfies { type: 'boolean' } }, text: (opts: T = {} as T) => { - return { type: 'text', ...opts } satisfies T & { type: 'text' }; + return createField('text', opts) satisfies { type: 'text' }; }, - date>(opts: T) { - return { type: 'date', ...opts } satisfies T & { type: 'date' }; + date>(opts: T = {} as T) { + return createField('date', opts) satisfies { type: 'date' }; }, - json>(opts: T) { - return { type: 'json', ...opts } satisfies T & { type: 'json' }; + json>(opts: T = {} as T) { + return createField('json', opts) satisfies { type: 'json' }; }, }; diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index c763cb7727..d9408a673a 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -19,7 +19,7 @@ export type { Table } from './types.js'; export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js'; export function hasPrimaryKey(field: DBField) { - return 'primaryKey' in field && !!field.primaryKey; + return 'primaryKey' in field.schema && !!field.schema.primaryKey; } // Exports a few common expressions @@ -99,42 +99,42 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo c = text(fieldName); // Duplicate default logic across cases to preserve type inference. // No clean generic for every column builder. - if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); - if (field.primaryKey === true) c = c.primaryKey(); + if (field.schema.default !== undefined) c = c.default(handleSerializedSQL(field.schema.default)); + if (field.schema.primaryKey === true) c = c.primaryKey(); break; } case 'number': { c = integer(fieldName); - if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); - if (field.primaryKey === true) c = c.primaryKey(); + if (field.schema.default !== undefined) c = c.default(handleSerializedSQL(field.schema.default)); + if (field.schema.primaryKey === true) c = c.primaryKey(); break; } case 'boolean': { c = integer(fieldName, { mode: 'boolean' }); - if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); + if (field.schema.default !== undefined) c = c.default(handleSerializedSQL(field.schema.default)); break; } case 'json': c = jsonType(fieldName); - if (field.default !== undefined) c = c.default(field.default); + if (field.schema.default !== undefined) c = c.default(field.schema.default); break; case 'date': { // Parse dates as strings when in JSON serializable mode if (isJsonSerializable) { c = text(fieldName); - if (field.default !== undefined) { - c = c.default(handleSerializedSQL(field.default)); + if (field.schema.default !== undefined) { + c = c.default(handleSerializedSQL(field.schema.default)); } } else { c = dateType(fieldName); - if (field.default !== undefined) { - const def = handleSerializedSQL(field.default); + if (field.schema.default !== undefined) { + const def = handleSerializedSQL(field.schema.default); c = c.default( def instanceof SQL ? def : // default comes pre-transformed to an ISO string for D1 storage. // parse back to a Date for Drizzle. - z.coerce.date().parse(field.default) + z.coerce.date().parse(field.schema.default) ); } } @@ -142,8 +142,8 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo } } - if (!field.optional) c = c.notNull(); - if (field.unique) c = c.unique(); + if (!field.schema.optional) c = c.notNull(); + if (field.schema.unique) c = c.unique(); return c; } diff --git a/packages/db/src/runtime/types.ts b/packages/db/src/runtime/types.ts index e0d20fd1cc..77de3657f9 100644 --- a/packages/db/src/runtime/types.ts +++ b/packages/db/src/runtime/types.ts @@ -76,7 +76,7 @@ export type Column = T ext export type Table< TTableName extends string, - TFields extends Record>, + TFields extends Record>, > = SQLiteTableWithColumns<{ name: TTableName; schema: undefined; @@ -92,7 +92,7 @@ export type Table< : TFields[K] extends { primaryKey: true } ? true : false; - notNull: TFields[K]['optional'] extends true ? false : true; + notNull: TFields[K]['schema']['optional'] extends true ? false : true; } >; };