diff --git a/packages/db/src/core/cli/commands/push/index.ts b/packages/db/src/core/cli/commands/push/index.ts index abd2031663..437576afad 100644 --- a/packages/db/src/core/cli/commands/push/index.ts +++ b/packages/db/src/core/cli/commands/push/index.ts @@ -6,7 +6,7 @@ import prompts from 'prompts'; import type { Arguments } from 'yargs-parser'; import { setupDbTables } from '../../../queries.js'; import { getManagedAppTokenOrExit } from '../../../tokens.js'; -import type { AstroConfigWithDB, DBSnapshot } from '../../../types.js'; +import { collectionsSchema, type AstroConfigWithDB, type DBSnapshot } from '../../../types.js'; import { getRemoteDatabaseUrl } from '../../../utils.js'; import { getMigrationQueries } from '../../migration-queries.js'; import { @@ -28,7 +28,7 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum const migration = await getMigrationStatus(config); if (migration.state === 'no-migrations-found') { - console.log(MIGRATIONS_NOT_INITIALIZED) + console.log(MIGRATIONS_NOT_INITIALIZED); process.exit(1); } else if (migration.state === 'ahead') { console.log(MIGRATION_NEEDED); @@ -42,12 +42,12 @@ export async function cmd({ config, flags }: { config: AstroConfig; flags: Argum const { data } = await prepareMigrateQuery({ migrations: allLocalMigrations, appToken: appToken.token, - }) + }); missingMigrations = data; } catch (e) { if (e instanceof Error) { if (e.message.startsWith('{')) { - const { error: { code } = { code: "" } } = JSON.parse(e.message); + const { error: { code } = { code: '' } } = JSON.parse(e.message); if (code === 'TOKEN_UNAUTHORIZED') { console.error(MISSING_SESSION_ID_ERROR); } @@ -169,7 +169,7 @@ async function pushData({ await setupDbTables({ db, mode: 'build', - collections: config.db.collections ?? {}, + collections: collectionsSchema.parse(config.db.collections ?? {}), data: config.db.data, }); } diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index a121c0f4a0..22cc44d6d4 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -14,7 +14,6 @@ import type { NumberField, TextField, } from '../types.js'; -import { SQL } from 'drizzle-orm'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { customAlphabet } from 'nanoid'; import prompts from 'prompts'; @@ -27,6 +26,7 @@ import { schemaTypeToSqlType, } from '../queries.js'; import { hasPrimaryKey } from '../../runtime/index.js'; +import { isSerializedSQL } from '../../runtime/types.js'; const sqlite = new SQLiteAsyncDialect(); const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); @@ -555,5 +555,5 @@ type DBFieldWithDefault = | WithDefaultDefined; function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault { - return !!(field.default && field.default instanceof SQL); + return !!(field.default && isSerializedSQL(field.default)); } diff --git a/packages/db/src/core/queries.ts b/packages/db/src/core/queries.ts index c3e47c7ce0..5c6a355b11 100644 --- a/packages/db/src/core/queries.ts +++ b/packages/db/src/core/queries.ts @@ -16,6 +16,7 @@ import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import type { AstroIntegrationLogger } from 'astro'; import type { DBUserConfig } from '../core/types.js'; import { hasPrimaryKey } from '../runtime/index.js'; +import { isSerializedSQL } from '../runtime/types.js'; const sqlite = new SQLiteAsyncDialect(); @@ -209,11 +210,9 @@ export function hasDefault(field: DBField): field is DBFieldWithDefault { return false; } -function toStringDefault(def: T | SQL): string { +function toDefault(def: T | SQL): string { const type = typeof def; - if (def instanceof SQL) { - return sqlite.sqlToQuery(def).sql; - } else if (type === 'string') { + if (type === 'string') { return sqlite.escapeString(def as string); } else if (type === 'boolean') { return def ? 'TRUE' : 'FALSE'; @@ -223,12 +222,16 @@ function toStringDefault(def: T | SQL): string { } function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): string { + if (isSerializedSQL(column.default)) { + return sqlite.sqlToQuery(new SQL(column.default.queryChunks)).sql; + } + switch (column.type) { case 'boolean': case 'number': case 'text': case 'date': - return toStringDefault(column.default); + return toDefault(column.default); case 'json': { let stringified = ''; try { diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts index 4bac36a7f6..c8aea5fe69 100644 --- a/packages/db/src/core/types.ts +++ b/packages/db/src/core/types.ts @@ -1,13 +1,22 @@ -import type { SQLiteInsertValue } from 'drizzle-orm/sqlite-core'; +import { type SQLiteInsertValue } from 'drizzle-orm/sqlite-core'; import type { InferSelectModel } from 'drizzle-orm'; import { collectionToTable, type SqliteDB, type Table } from '../runtime/index.js'; import { z, type ZodTypeDef } from 'zod'; import { SQL } from 'drizzle-orm'; import { errorMap } from './integration/error-map.js'; +import { SERIALIZED_SQL_KEY, type SerializedSQL } from '../runtime/types.js'; export type MaybePromise = T | Promise; export type MaybeArray = T | T[]; +// Transform to serializable object for migration files +const sqlSchema = z.instanceof(SQL).transform( + (sqlObj): SerializedSQL => ({ + [SERIALIZED_SQL_KEY]: true, + queryChunks: sqlObj.queryChunks, + }) +); + const baseFieldSchema = z.object({ label: z.string().optional(), optional: z.boolean().optional(), @@ -20,7 +29,7 @@ const baseFieldSchema = z.object({ const booleanFieldSchema = baseFieldSchema.extend({ type: z.literal('boolean'), - default: z.union([z.boolean(), z.instanceof(SQL)]).optional(), + default: z.union([z.boolean(), sqlSchema]).optional(), }); const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and( @@ -28,7 +37,7 @@ const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and( z.object({ primaryKey: z.literal(false).optional(), optional: z.boolean().optional(), - default: z.union([z.number(), z.instanceof(SQL)]).optional(), + default: z.union([z.number(), sqlSchema]).optional(), }), z.object({ // `integer primary key` uses ROWID as the default value. @@ -69,7 +78,7 @@ const numberFieldSchema = numberFieldOptsSchema.and( const textFieldBaseSchema = baseFieldSchema .omit({ optional: true }) .extend({ - default: z.union([z.string(), z.instanceof(SQL)]).optional(), + default: z.union([z.string(), sqlSchema]).optional(), multiline: z.boolean().optional(), }) .and( @@ -118,7 +127,7 @@ const dateFieldSchema = baseFieldSchema.extend({ type: z.literal('date'), default: z .union([ - z.instanceof(SQL), + sqlSchema, // allow date-like defaults in user config, // transform to ISO string for D1 storage z.coerce.date().transform((d) => d.toISOString()), @@ -138,8 +147,7 @@ const fieldSchema = z.union([ dateFieldSchema, jsonFieldSchema, ]); -export const referenceableFieldSchema = z.union([textFieldBaseSchema, numberFieldSchema]); -export type ReferenceableField = z.input; +export const referenceableFieldSchema = z.union([textFieldSchema, numberFieldSchema]); const fieldsSchema = z.record(fieldSchema); export const indexSchema = z.object({ @@ -149,12 +157,12 @@ export const indexSchema = z.object({ type ForeignKeysInput = { fields: MaybeArray; - references: () => MaybeArray>; + references: () => MaybeArray, 'references'>>; }; type ForeignKeysOutput = Omit & { // reference fn called in `transform`. Ensures output is JSON serializable. - references: MaybeArray>; + references: MaybeArray, 'references'>>; }; const foreignKeysSchema: z.ZodType = z.object({ @@ -204,14 +212,15 @@ export const collectionsSchema = z.preprocess((rawCollections) => { }, z.record(collectionSchema)); export type BooleanField = z.infer; +export type BooleanFieldInput = z.input; 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; export type JsonField = z.infer; +export type JsonFieldInput = z.input; export type FieldType = | BooleanField['type'] @@ -223,10 +232,10 @@ export type FieldType = export type DBField = z.infer; export type DBFieldInput = | DateFieldInput - | BooleanField + | BooleanFieldInput | NumberFieldInput | TextFieldInput - | JsonField; + | JsonFieldInput; export type DBFields = z.infer; export type DBCollection = z.infer< typeof readableCollectionSchema | typeof writableCollectionSchema @@ -290,7 +299,7 @@ interface CollectionConfig foreignKeys?: Array<{ fields: MaybeArray>; // TODO: runtime error if parent collection doesn't match for all fields. Can't put a generic here... - references: () => MaybeArray; + references: () => MaybeArray>; }>; indexes?: Record>; } @@ -343,7 +352,7 @@ export const field = { number: (opts: T = {} as T) => { return { type: 'number', ...opts } satisfies T & { type: 'number' }; }, - boolean: >(opts: T = {} as T) => { + boolean: >(opts: T = {} as T) => { return { type: 'boolean', ...opts } satisfies T & { type: 'boolean' }; }, text: (opts: T = {} as T) => { @@ -352,7 +361,7 @@ export const field = { date>(opts: T) { return { type: 'date', ...opts } satisfies T & { type: 'date' }; }, - json>(opts: T) { + json>(opts: T) { return { type: 'json', ...opts } satisfies T & { type: 'json' }; }, }; diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 67331573d2..2160477232 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -11,6 +11,7 @@ import { type IndexBuilder, } from 'drizzle-orm/sqlite-core'; import { z } from 'zod'; +import { isSerializedSQL, type SerializedSQL } from './types.js'; export { sql }; export type SqliteDB = SqliteRemoteDatabase; @@ -98,19 +99,19 @@ 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(field.default); + if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); if (field.primaryKey === true) c = c.primaryKey(); break; } case 'number': { c = integer(fieldName); - if (field.default !== undefined) c = c.default(field.default); + if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); if (field.primaryKey === true) c = c.primaryKey(); break; } case 'boolean': { c = integer(fieldName, { mode: 'boolean' }); - if (field.default !== undefined) c = c.default(field.default); + if (field.default !== undefined) c = c.default(handleSerializedSQL(field.default)); break; } case 'json': @@ -122,12 +123,12 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo if (isJsonSerializable) { c = text(fieldName); if (field.default !== undefined) { - c = c.default(field.default); + c = c.default(handleSerializedSQL(field.default)); } } else { c = dateType(fieldName); if (field.default !== undefined) { - const def = convertSerializedSQL(field.default); + const def = handleSerializedSQL(field.default); c = c.default( def instanceof SQL ? def @@ -146,14 +147,9 @@ function columnMapper(fieldName: string, field: DBField, isJsonSerializable: boo return c; } -function isSerializedSQL(obj: unknown): boolean { - return typeof obj === 'object' && !!(obj as any).queryChunks; -} - -function convertSerializedSQL(obj: T): SQL | T { - if (isSerializedSQL(obj)) { - return new SQL((obj as any).queryChunks); - } else { - return obj; +function handleSerializedSQL(def: T | SerializedSQL) { + if (isSerializedSQL(def)) { + return new SQL(def.queryChunks); } + return def; } diff --git a/packages/db/src/runtime/types.ts b/packages/db/src/runtime/types.ts index 59c053a6db..17136e37bc 100644 --- a/packages/db/src/runtime/types.ts +++ b/packages/db/src/runtime/types.ts @@ -1,4 +1,4 @@ -import type { ColumnDataType, ColumnBaseConfig } from 'drizzle-orm'; +import type { ColumnDataType, ColumnBaseConfig, SQLChunk } from 'drizzle-orm'; import type { SQLiteColumn, SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'; import type { DBField } from '../core/types.js'; @@ -97,3 +97,13 @@ export type Table< >; }; }>; + +export const SERIALIZED_SQL_KEY = '__serializedSQL'; +export type SerializedSQL = { + [SERIALIZED_SQL_KEY]: true; + queryChunks: SQLChunk[]; +}; + +export function isSerializedSQL(value: any): value is SerializedSQL { + return typeof value === 'object' && value !== null && SERIALIZED_SQL_KEY in value; +}