diff --git a/packages/db/src/core/cli/migration-queries.ts b/packages/db/src/core/cli/migration-queries.ts index 9aca12a9ea..7806b4939e 100644 --- a/packages/db/src/core/cli/migration-queries.ts +++ b/packages/db/src/core/cli/migration-queries.ts @@ -1,19 +1,19 @@ import * as color from 'kleur/colors'; import deepDiff from 'deep-diff'; import { - fieldSchema, - type BooleanField, + columnSchema, + type BooleanColumn, type DBCollection, type DBCollections, - type DBField, - type DBFields, + type DBColumn, + type DBColumns, type DBSnapshot, - type DateField, - type FieldType, + type DateColumn, + type ColumnType, type Indexes, - type JsonField, - type NumberField, - type TextField, + type JsonColumn, + type NumberColumn, + type TextColumn, } from '../types.js'; import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core'; import { customAlphabet } from 'nanoid'; @@ -35,7 +35,7 @@ const genTempTableName = customAlphabet('abcdefghijklmnopqrstuvwxyz', 10); /** Dependency injected for unit testing */ type AmbiguityResponses = { collectionRenames: Record; - fieldRenames: { + columnRenames: { [collectionName: string]: Record; }; }; @@ -102,9 +102,9 @@ export async function getCollectionChangeQueries({ }): Promise<{ queries: string[]; confirmations: string[] }> { const queries: string[] = []; const confirmations: string[] = []; - const updated = getUpdatedFields(oldCollection.fields, newCollection.fields); - let added = getAdded(oldCollection.fields, newCollection.fields); - let dropped = getDropped(oldCollection.fields, newCollection.fields); + const updated = getUpdatedColumns(oldCollection.columns, newCollection.columns); + let added = getAdded(oldCollection.columns, newCollection.columns); + let dropped = getDropped(oldCollection.columns, newCollection.columns); /** Any foreign key changes require a full table recreate */ const hasForeignKeyChanges = Boolean( deepDiff(oldCollection.foreignKeys, newCollection.foreignKeys) @@ -121,10 +121,10 @@ export async function getCollectionChangeQueries({ }; } if (!hasForeignKeyChanges && !isEmpty(added) && !isEmpty(dropped)) { - const resolved = await resolveFieldRenames(collectionName, added, dropped, ambiguityResponses); + const resolved = await resolveColumnRenames(collectionName, added, dropped, ambiguityResponses); added = resolved.added; dropped = resolved.dropped; - queries.push(...getFieldRenameQueries(collectionName, resolved.renamed)); + queries.push(...getColumnRenameQueries(collectionName, resolved.renamed)); } if ( !hasForeignKeyChanges && @@ -145,31 +145,31 @@ export async function getCollectionChangeQueries({ const dataLossCheck = canRecreateTableWithoutDataLoss(added, updated); if (dataLossCheck.dataLoss) { - const { reason, fieldName } = dataLossCheck; + const { reason, columnName } = dataLossCheck; const reasonMsgs: Record = { - 'added-required': `New field ${color.bold( - collectionName + '.' + fieldName + 'added-required': `New column ${color.bold( + collectionName + '.' + columnName )} is required with no default value.\nThis requires deleting existing data in the ${color.bold( collectionName )} collection.`, - 'added-unique': `New field ${color.bold( - collectionName + '.' + fieldName + 'added-unique': `New column ${color.bold( + collectionName + '.' + columnName )} is marked as unique.\nThis requires deleting existing data in the ${color.bold( collectionName )} collection.`, - 'updated-type': `Updated field ${color.bold( - collectionName + '.' + fieldName - )} cannot convert data to new field data type.\nThis requires deleting existing data in the ${color.bold( + 'updated-type': `Updated column ${color.bold( + collectionName + '.' + columnName + )} cannot convert data to new column data type.\nThis requires deleting existing data in the ${color.bold( collectionName )} collection.`, }; confirmations.push(reasonMsgs[reason]); } - const primaryKeyExists = Object.entries(newCollection.fields).find(([, field]) => - hasPrimaryKey(field) + const primaryKeyExists = Object.entries(newCollection.columns).find(([, column]) => + hasPrimaryKey(column) ); - const droppedPrimaryKey = Object.entries(dropped).find(([, field]) => hasPrimaryKey(field)); + const droppedPrimaryKey = Object.entries(dropped).find(([, column]) => hasPrimaryKey(column)); const recreateTableQueries = getRecreateTableQueries({ collectionName, @@ -209,31 +209,31 @@ function getChangeIndexQueries({ type Renamed = Array<{ from: string; to: string }>; -async function resolveFieldRenames( +async function resolveColumnRenames( collectionName: string, - mightAdd: DBFields, - mightDrop: DBFields, + mightAdd: DBColumns, + mightDrop: DBColumns, ambiguityResponses?: AmbiguityResponses -): Promise<{ added: DBFields; dropped: DBFields; renamed: Renamed }> { - const added: DBFields = {}; - const dropped: DBFields = {}; +): Promise<{ added: DBColumns; dropped: DBColumns; renamed: Renamed }> { + const added: DBColumns = {}; + const dropped: DBColumns = {}; const renamed: Renamed = []; - for (const [fieldName, field] of Object.entries(mightAdd)) { - let oldFieldName = ambiguityResponses - ? ambiguityResponses.fieldRenames[collectionName]?.[fieldName] ?? '__NEW__' + for (const [columnName, column] of Object.entries(mightAdd)) { + let oldColumnName = ambiguityResponses + ? ambiguityResponses.columnRenames[collectionName]?.[columnName] ?? '__NEW__' : undefined; - if (!oldFieldName) { + if (!oldColumnName) { const res = await prompts( { type: 'select', - name: 'fieldName', + name: 'columnName', message: - 'New field ' + - color.blue(color.bold(`${collectionName}.${fieldName}`)) + - ' detected. Was this renamed from an existing field?', + 'New column ' + + color.blue(color.bold(`${collectionName}.${columnName}`)) + + ' detected. Was this renamed from an existing column?', choices: [ - { title: 'New field (not renamed from existing)', value: '__NEW__' }, + { title: 'New column (not renamed from existing)', value: '__NEW__' }, ...Object.keys(mightDrop) .filter((key) => !(key in renamed)) .map((key) => ({ title: key, value: key })), @@ -245,18 +245,18 @@ async function resolveFieldRenames( }, } ); - oldFieldName = res.fieldName as string; + oldColumnName = res.columnName as string; } - if (oldFieldName === '__NEW__') { - added[fieldName] = field; + if (oldColumnName === '__NEW__') { + added[columnName] = column; } else { - renamed.push({ from: oldFieldName, to: fieldName }); + renamed.push({ from: oldColumnName, to: columnName }); } } - for (const [droppedFieldName, droppedField] of Object.entries(mightDrop)) { - if (!renamed.find((r) => r.from === droppedFieldName)) { - dropped[droppedFieldName] = droppedField; + for (const [droppedColumnName, droppedColumn] of Object.entries(mightDrop)) { + if (!renamed.find((r) => r.from === droppedColumnName)) { + dropped[droppedColumnName] = droppedColumn; } } @@ -339,7 +339,7 @@ function getDroppedCollections( return dropped; } -function getFieldRenameQueries(unescapedCollectionName: string, renamed: Renamed): string[] { +function getColumnRenameQueries(unescapedCollectionName: string, renamed: Renamed): string[] { const queries: string[] = []; const collectionName = sqlite.escapeName(unescapedCollectionName); @@ -359,25 +359,25 @@ function getFieldRenameQueries(unescapedCollectionName: string, renamed: Renamed */ function getAlterTableQueries( unescapedCollectionName: string, - added: DBFields, - dropped: DBFields + added: DBColumns, + dropped: DBColumns ): string[] { const queries: string[] = []; const collectionName = sqlite.escapeName(unescapedCollectionName); - for (const [unescFieldName, field] of Object.entries(added)) { - const fieldName = sqlite.escapeName(unescFieldName); - const type = schemaTypeToSqlType(field.type); - const q = `ALTER TABLE ${collectionName} ADD COLUMN ${fieldName} ${type}${getModifiers( - fieldName, - field + for (const [unescColumnName, column] of Object.entries(added)) { + const columnName = sqlite.escapeName(unescColumnName); + const type = schemaTypeToSqlType(column.type); + const q = `ALTER TABLE ${collectionName} ADD COLUMN ${columnName} ${type}${getModifiers( + columnName, + column )}`; queries.push(q); } - for (const unescFieldName of Object.keys(dropped)) { - const fieldName = sqlite.escapeName(unescFieldName); - const q = `ALTER TABLE ${collectionName} DROP COLUMN ${fieldName}`; + for (const unescColumnName of Object.keys(dropped)) { + const columnName = sqlite.escapeName(unescColumnName); + const q = `ALTER TABLE ${collectionName} DROP COLUMN ${columnName}`; queries.push(q); } @@ -393,7 +393,7 @@ function getRecreateTableQueries({ }: { collectionName: string; newCollection: DBCollection; - added: Record; + added: Record; hasDataLoss: boolean; migrateHiddenPrimaryKey: boolean; }): string[] { @@ -407,7 +407,7 @@ function getRecreateTableQueries({ getCreateTableQuery(unescCollectionName, newCollection), ]; } - const newColumns = [...Object.keys(newCollection.fields)]; + const newColumns = [...Object.keys(newCollection.columns)]; if (migrateHiddenPrimaryKey) { newColumns.unshift('_id'); } @@ -434,44 +434,44 @@ function isEmpty(obj: Record) { * * @see https://www.sqlite.org/lang_altertable.html#alter_table_add_column */ -function canAlterTableAddColumn(field: DBField) { - if (field.schema.unique) return false; - if (hasRuntimeDefault(field)) return false; - if (!field.schema.optional && !hasDefault(field)) return false; - if (hasPrimaryKey(field)) return false; - if (getReferencesConfig(field)) return false; +function canAlterTableAddColumn(column: DBColumn) { + if (column.schema.unique) return false; + if (hasRuntimeDefault(column)) return false; + if (!column.schema.optional && !hasDefault(column)) return false; + if (hasPrimaryKey(column)) return false; + if (getReferencesConfig(column)) return false; return true; } -function canAlterTableDropColumn(field: DBField) { - if (field.schema.unique) return false; - if (hasPrimaryKey(field)) return false; +function canAlterTableDropColumn(column: DBColumn) { + if (column.schema.unique) return false; + if (hasPrimaryKey(column)) return false; return true; } type DataLossReason = 'added-required' | 'added-unique' | 'updated-type'; type DataLossResponse = | { dataLoss: false } - | { dataLoss: true; fieldName: string; reason: DataLossReason }; + | { dataLoss: true; columnName: string; reason: DataLossReason }; function canRecreateTableWithoutDataLoss( - added: DBFields, - updated: UpdatedFields + added: DBColumns, + updated: UpdatedColumns ): DataLossResponse { - for (const [fieldName, a] of Object.entries(added)) { + for (const [columnName, a] of Object.entries(added)) { if (hasPrimaryKey(a) && a.type !== 'number' && !hasDefault(a)) { - return { dataLoss: true, fieldName, reason: 'added-required' }; + return { dataLoss: true, columnName, reason: 'added-required' }; } if (!a.schema.optional && !hasDefault(a)) { - return { dataLoss: true, fieldName, reason: 'added-required' }; + return { dataLoss: true, columnName, reason: 'added-required' }; } if (!a.schema.optional && a.schema.unique) { - return { dataLoss: true, fieldName, reason: 'added-unique' }; + return { dataLoss: true, columnName, reason: 'added-unique' }; } } - for (const [fieldName, u] of Object.entries(updated)) { + for (const [columnName, u] of Object.entries(updated)) { if (u.old.type !== u.new.type && !canChangeTypeWithoutQuery(u.old, u.new)) { - return { dataLoss: true, fieldName, reason: 'updated-type' }; + return { dataLoss: true, columnName, reason: 'updated-type' }; } } return { dataLoss: false }; @@ -503,58 +503,58 @@ function getUpdated(oldObj: Record, newObj: Record) { return updated; } -type UpdatedFields = Record; +type UpdatedColumns = Record; -function getUpdatedFields(oldFields: DBFields, newFields: DBFields): UpdatedFields { - const updated: UpdatedFields = {}; - for (const [key, newField] of Object.entries(newFields)) { - let oldField = oldFields[key]; - if (!oldField) continue; +function getUpdatedColumns(oldColumns: DBColumns, newColumns: DBColumns): UpdatedColumns { + const updated: UpdatedColumns = {}; + for (const [key, newColumn] of Object.entries(newColumns)) { + let oldColumn = oldColumns[key]; + if (!oldColumn) continue; - if (oldField.type !== newField.type && canChangeTypeWithoutQuery(oldField, newField)) { + if (oldColumn.type !== newColumn.type && canChangeTypeWithoutQuery(oldColumn, newColumn)) { // If we can safely change the type without a query, // try parsing the old schema as the new schema. - // This lets us diff the fields as if they were the same type. - const asNewField = fieldSchema.safeParse({ - type: newField.type, - schema: oldField.schema, + // This lets us diff the columns as if they were the same type. + const asNewColumn = columnSchema.safeParse({ + type: newColumn.type, + schema: oldColumn.schema, }); - if (asNewField.success) { - oldField = asNewField.data; + if (asNewColumn.success) { + oldColumn = asNewColumn.data; } // If parsing fails, move on to the standard diff. } - const diff = deepDiff(oldField, newField); + const diff = deepDiff(oldColumn, newColumn); if (diff) { - updated[key] = { old: oldField, new: newField }; + updated[key] = { old: oldColumn, new: newColumn }; } } return updated; } -const typeChangesWithoutQuery: Array<{ from: FieldType; to: FieldType }> = [ +const typeChangesWithoutQuery: Array<{ from: ColumnType; to: ColumnType }> = [ { from: 'boolean', to: 'number' }, { from: 'date', to: 'text' }, { from: 'json', to: 'text' }, ]; -function canChangeTypeWithoutQuery(oldField: DBField, newField: DBField) { +function canChangeTypeWithoutQuery(oldColumn: DBColumn, newColumn: DBColumn) { return typeChangesWithoutQuery.some( - ({ from, to }) => oldField.type === from && newField.type === to + ({ from, to }) => oldColumn.type === from && newColumn.type === to ); } -// Using `DBField` will not narrow `default` based on the column `type` -// Handle each field separately -type WithDefaultDefined = T & Required>; -type DBFieldWithDefault = - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined; +// Using `DBColumn` will not narrow `default` based on the column `type` +// Handle each column separately +type WithDefaultDefined = T & Required>; +type DBColumnWithDefault = + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined; -function hasRuntimeDefault(field: DBField): field is DBFieldWithDefault { - return !!(field.schema.default && isSerializedSQL(field.schema.default)); +function hasRuntimeDefault(column: DBColumn): column is DBColumnWithDefault { + return !!(column.schema.default && isSerializedSQL(column.schema.default)); } diff --git a/packages/db/src/core/cli/migrations.ts b/packages/db/src/core/cli/migrations.ts index 977816ecf1..9fe0041b06 100644 --- a/packages/db/src/core/cli/migrations.ts +++ b/packages/db/src/core/cli/migrations.ts @@ -91,7 +91,7 @@ export async function loadMigration( export async function loadInitialSnapshot(): Promise { const snapshot = JSON.parse(await readFile('./migrations/0000_snapshot.json', 'utf-8')); - // `experimentalVersion: 1` -- added the version field + // `experimentalVersion: 1` -- added the version column if (snapshot.experimentalVersion === 1) { return snapshot; } diff --git a/packages/db/src/core/integration/typegen.ts b/packages/db/src/core/integration/typegen.ts index 42af4bed6c..66c241b082 100644 --- a/packages/db/src/core/integration/typegen.ts +++ b/packages/db/src/core/integration/typegen.ts @@ -30,13 +30,13 @@ function generateTableType(name: string, collection: DBCollection): string { ${JSON.stringify(name)}, ${JSON.stringify( Object.fromEntries( - Object.entries(collection.fields).map(([fieldName, field]) => [ - fieldName, + Object.entries(collection.columns).map(([columnName, column]) => [ + columnName, { - // Only select fields Drizzle needs for inference - type: field.type, - optional: field.schema.optional, - default: field.schema.default, + // Only select columns Drizzle needs for inference + type: column.type, + optional: column.schema.optional, + default: column.schema.default, }, ]) ) diff --git a/packages/db/src/core/queries.ts b/packages/db/src/core/queries.ts index b7bf81c051..7429a13221 100644 --- a/packages/db/src/core/queries.ts +++ b/packages/db/src/core/queries.ts @@ -1,14 +1,14 @@ import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; import { - type BooleanField, + type BooleanColumn, type DBCollection, type DBCollections, - type DBField, - type DateField, - type FieldType, - type JsonField, - type NumberField, - type TextField, + type DBColumn, + type DateColumn, + type ColumnType, + type JsonColumn, + type NumberColumn, + type TextColumn, } from '../core/types.js'; import { bold, red } from 'kleur/colors'; import { type SQL, sql, getTableName } from 'drizzle-orm'; @@ -89,13 +89,13 @@ export function getCreateTableQuery(collectionName: string, collection: DBCollec let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`; const colQueries = []; - const colHasPrimaryKey = Object.entries(collection.fields).find(([, field]) => - hasPrimaryKey(field) + const colHasPrimaryKey = Object.entries(collection.columns).find(([, column]) => + hasPrimaryKey(column) ); if (!colHasPrimaryKey) { colQueries.push('_id INTEGER PRIMARY KEY'); } - for (const [columnName, column] of Object.entries(collection.fields)) { + for (const [columnName, column] of Object.entries(collection.columns)) { const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType( column.type )}${getModifiers(columnName, column)}`; @@ -129,12 +129,12 @@ export function getCreateIndexQueries( export function getCreateForeignKeyQueries(collectionName: string, collection: DBCollection) { let queries: string[] = []; for (const foreignKey of collection.foreignKeys ?? []) { - const fields = asArray(foreignKey.fields); + const columns = asArray(foreignKey.columns); const references = asArray(foreignKey.references); - if (fields.length !== references.length) { + if (columns.length !== references.length) { throw new Error( - `Foreign key on ${collectionName} is misconfigured. \`fields\` and \`references\` must be the same length.` + `Foreign key on ${collectionName} is misconfigured. \`columns\` and \`references\` must be the same length.` ); } const referencedCollection = references[0]?.schema.collection; @@ -143,7 +143,7 @@ export function getCreateForeignKeyQueries(collectionName: string, collection: D `Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.` ); } - const query = `FOREIGN KEY (${fields + const query = `FOREIGN KEY (${columns .map((f) => sqlite.escapeName(f)) .join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references .map((r) => sqlite.escapeName(r.schema.name!)) @@ -157,7 +157,7 @@ function asArray(value: T | T[]) { return Array.isArray(value) ? value : [value]; } -export function schemaTypeToSqlType(type: FieldType): 'text' | 'integer' { +export function schemaTypeToSqlType(type: ColumnType): 'text' | 'integer' { switch (type) { case 'date': case 'text': @@ -169,26 +169,26 @@ export function schemaTypeToSqlType(type: FieldType): 'text' | 'integer' { } } -export function getModifiers(fieldName: string, field: DBField) { +export function getModifiers(columnName: string, column: DBColumn) { let modifiers = ''; - if (hasPrimaryKey(field)) { + if (hasPrimaryKey(column)) { return ' PRIMARY KEY'; } - if (!field.schema.optional) { + if (!column.schema.optional) { modifiers += ' NOT NULL'; } - if (field.schema.unique) { + if (column.schema.unique) { modifiers += ' UNIQUE'; } - if (hasDefault(field)) { - modifiers += ` DEFAULT ${getDefaultValueSql(fieldName, field)}`; + if (hasDefault(column)) { + modifiers += ` DEFAULT ${getDefaultValueSql(columnName, column)}`; } - const references = getReferencesConfig(field); + const references = getReferencesConfig(column); if (references) { const { collection, name } = references.schema; if (!collection || !name) { throw new Error( - `Field ${collection}.${name} references a collection that does not exist. Did you apply the referenced collection to the \`collections\` object in your Astro config?` + `Column ${collection}.${name} references a collection that does not exist. Did you apply the referenced collection to the \`collections\` object in your Astro config?` ); } @@ -197,30 +197,30 @@ export function getModifiers(fieldName: string, field: DBField) { return modifiers; } -export function getReferencesConfig(field: DBField) { - const canHaveReferences = field.type === 'number' || field.type === 'text'; +export function getReferencesConfig(column: DBColumn) { + const canHaveReferences = column.type === 'number' || column.type === 'text'; if (!canHaveReferences) return undefined; - return field.schema.references; + return column.schema.references; } -// Using `DBField` will not narrow `default` based on the column `type` -// Handle each field separately -type WithDefaultDefined = T & { +// Using `DBColumn` will not narrow `default` based on the column `type` +// Handle each column separately +type WithDefaultDefined = T & { schema: Required>; }; -type DBFieldWithDefault = - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined - | WithDefaultDefined; +type DBColumnWithDefault = + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined + | WithDefaultDefined; // Type narrowing the default fails on union types, so use a type guard -export function hasDefault(field: DBField): field is DBFieldWithDefault { - if (field.schema.default !== undefined) { +export function hasDefault(column: DBColumn): column is DBColumnWithDefault { + if (column.schema.default !== undefined) { return true; } - if (hasPrimaryKey(field) && field.type === 'number') { + if (hasPrimaryKey(column) && column.type === 'number') { return true; } return false; @@ -237,7 +237,7 @@ function toDefault(def: T | SQL): string { } } -function getDefaultValueSql(columnName: string, column: DBFieldWithDefault): string { +function getDefaultValueSql(columnName: string, column: DBColumnWithDefault): string { if (isSerializedSQL(column.schema.default)) { return column.schema.default.sql; } diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts index 398373d121..8289e0e175 100644 --- a/packages/db/src/core/types.ts +++ b/packages/db/src/core/types.ts @@ -19,7 +19,7 @@ const sqlSchema = z.instanceof(SQL).transform( }) ); -const baseFieldSchema = z.object({ +const baseColumnSchema = z.object({ label: z.string().optional(), optional: z.boolean().optional().default(false), unique: z.boolean().optional().default(false), @@ -29,18 +29,18 @@ const baseFieldSchema = z.object({ collection: z.string().optional(), }); -const booleanFieldSchema = z.object({ +const booleanColumnSchema = z.object({ type: z.literal('boolean'), - schema: baseFieldSchema.extend({ + schema: baseColumnSchema.extend({ default: z.union([z.boolean(), sqlSchema]).optional(), }), }); -const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and( +const numberColumnBaseSchema = baseColumnSchema.omit({ optional: true }).and( z.union([ z.object({ primaryKey: z.literal(false).optional().default(false), - optional: baseFieldSchema.shape.optional, + optional: baseColumnSchema.shape.optional, default: z.union([z.number(), sqlSchema]).optional(), }), z.object({ @@ -54,31 +54,31 @@ const numberFieldBaseSchema = baseFieldSchema.omit({ optional: true }).and( ]) ); -const numberFieldOptsSchema: z.ZodType< - z.infer & { - // ReferenceableField creates a circular type. Define ZodType to resolve. - references?: NumberField; +const numberColumnOptsSchema: z.ZodType< + z.infer & { + // ReferenceableColumn creates a circular type. Define ZodType to resolve. + references?: NumberColumn; }, ZodTypeDef, - z.input & { - references?: () => z.input; + z.input & { + references?: () => z.input; } -> = numberFieldBaseSchema.and( +> = numberColumnBaseSchema.and( z.object({ references: z .function() - .returns(z.lazy(() => numberFieldSchema)) + .returns(z.lazy(() => numberColumnSchema)) .optional() .transform((fn) => fn?.()), }) ); -const numberFieldSchema = z.object({ +const numberColumnSchema = z.object({ type: z.literal('number'), - schema: numberFieldOptsSchema, + schema: numberColumnOptsSchema, }); -const textFieldBaseSchema = baseFieldSchema +const textColumnBaseSchema = baseColumnSchema .omit({ optional: true }) .extend({ default: z.union([z.string(), sqlSchema]).optional(), @@ -88,7 +88,7 @@ const textFieldBaseSchema = baseFieldSchema z.union([ z.object({ primaryKey: z.literal(false).optional().default(false), - optional: baseFieldSchema.shape.optional, + optional: baseColumnSchema.shape.optional, }), z.object({ // text primary key allows NULL values. @@ -101,33 +101,33 @@ const textFieldBaseSchema = baseFieldSchema ]) ); -const textFieldOptsSchema: z.ZodType< - z.infer & { - // ReferenceableField creates a circular type. Define ZodType to resolve. - references?: TextField; +const textColumnOptsSchema: z.ZodType< + z.infer & { + // ReferenceableColumn creates a circular type. Define ZodType to resolve. + references?: TextColumn; }, ZodTypeDef, - z.input & { - references?: () => z.input; + z.input & { + references?: () => z.input; } -> = textFieldBaseSchema.and( +> = textColumnBaseSchema.and( z.object({ references: z .function() - .returns(z.lazy(() => textFieldSchema)) + .returns(z.lazy(() => textColumnSchema)) .optional() .transform((fn) => fn?.()), }) ); -const textFieldSchema = z.object({ +const textColumnSchema = z.object({ type: z.literal('text'), - schema: textFieldOptsSchema, + schema: textColumnOptsSchema, }); -const dateFieldSchema = z.object({ +const dateColumnSchema = z.object({ type: z.literal('date'), - schema: baseFieldSchema.extend({ + schema: baseColumnSchema.extend({ default: z .union([ sqlSchema, @@ -138,23 +138,23 @@ const dateFieldSchema = z.object({ }), }); -const jsonFieldSchema = z.object({ +const jsonColumnSchema = z.object({ type: z.literal('json'), - schema: baseFieldSchema.extend({ + schema: baseColumnSchema.extend({ default: z.unknown().optional(), }), }); -export const fieldSchema = z.union([ - booleanFieldSchema, - numberFieldSchema, - textFieldSchema, - dateFieldSchema, - jsonFieldSchema, +export const columnSchema = z.union([ + booleanColumnSchema, + numberColumnSchema, + textColumnSchema, + dateColumnSchema, + jsonColumnSchema, ]); -export const referenceableFieldSchema = z.union([textFieldSchema, numberFieldSchema]); +export const referenceableColumnSchema = z.union([textColumnSchema, numberColumnSchema]); -const fieldsSchema = z.record(fieldSchema); +const columnsSchema = z.record(columnSchema); export const indexSchema = z.object({ on: z.string().or(z.array(z.string())), @@ -162,27 +162,27 @@ export const indexSchema = z.object({ }); type ForeignKeysInput = { - fields: MaybeArray; - references: () => MaybeArray, 'references'>>; + columns: MaybeArray; + references: () => MaybeArray, 'references'>>; }; type ForeignKeysOutput = Omit & { // reference fn called in `transform`. Ensures output is JSON serializable. - references: MaybeArray, 'references'>>; + references: MaybeArray, 'references'>>; }; const foreignKeysSchema: z.ZodType = z.object({ - fields: z.string().or(z.array(z.string())), + columns: z.string().or(z.array(z.string())), references: z .function() - .returns(z.lazy(() => referenceableFieldSchema.or(z.array(referenceableFieldSchema)))) + .returns(z.lazy(() => referenceableColumnSchema.or(z.array(referenceableColumnSchema)))) .transform((fn) => fn()), }); export type Indexes = Record>; const baseCollectionSchema = z.object({ - fields: fieldsSchema, + columns: columnsSchema, indexes: z.record(indexSchema).optional(), foreignKeys: z.array(foreignKeysSchema).optional(), }); @@ -206,43 +206,43 @@ export const collectionsSchema = z.preprocess((rawCollections) => { collectionName, collectionSchema.parse(collection, { errorMap }) ); - // Append collection and field names to fields. + // Append collection and column names to columns. // Used to track collection info for references. - const { fields } = z.object({ fields: z.record(z.any()) }).parse(collection, { errorMap }); - for (const [fieldName, field] of Object.entries(fields)) { - field.schema.name = fieldName; - field.schema.collection = collectionName; + const { columns } = z.object({ columns: z.record(z.any()) }).parse(collection, { errorMap }); + for (const [columnName, column] of Object.entries(columns)) { + column.schema.name = columnName; + column.schema.collection = collectionName; } } return 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; -export type DateFieldInput = z.input; -export type JsonField = z.infer; -export type JsonFieldInput = z.input; +export type BooleanColumn = z.infer; +export type BooleanColumnInput = z.input; +export type NumberColumn = z.infer; +export type NumberColumnInput = z.input; +export type TextColumn = z.infer; +export type TextColumnInput = z.input; +export type DateColumn = z.infer; +export type DateColumnInput = z.input; +export type JsonColumn = z.infer; +export type JsonColumnInput = z.input; -export type FieldType = - | BooleanField['type'] - | NumberField['type'] - | TextField['type'] - | DateField['type'] - | JsonField['type']; +export type ColumnType = + | BooleanColumn['type'] + | NumberColumn['type'] + | TextColumn['type'] + | DateColumn['type'] + | JsonColumn['type']; -export type DBField = z.infer; -export type DBFieldInput = - | DateFieldInput - | BooleanFieldInput - | NumberFieldInput - | TextFieldInput - | JsonFieldInput; -export type DBFields = z.infer; +export type DBColumn = z.infer; +export type DBColumnInput = + | DateColumnInput + | BooleanColumnInput + | NumberColumnInput + | TextColumnInput + | JsonColumnInput; +export type DBColumns = z.infer; export type DBCollection = z.infer< typeof readableCollectionSchema | typeof writableCollectionSchema >; @@ -260,20 +260,20 @@ export type WritableDBCollection = z.infer; export type DBDataContext = { db: SqliteDB; - seed: ( - collection: ResolvedCollectionConfig, - data: MaybeArray>> + seed: ( + collection: ResolvedCollectionConfig, + data: MaybeArray>> ) => Promise; seedReturning: < - TFields extends FieldsConfig, - TData extends MaybeArray>>, + TColumns extends ColumnsConfig, + TData extends MaybeArray>>, >( - collection: ResolvedCollectionConfig, + collection: ResolvedCollectionConfig, data: TData ) => Promise< - TData extends Array>> - ? InferSelectModel>[] - : InferSelectModel> + TData extends Array>> + ? InferSelectModel>[] + : InferSelectModel> >; mode: 'dev' | 'build'; }; @@ -300,37 +300,37 @@ export const astroConfigWithDbSchema = z.object({ db: dbConfigSchema.optional(), }); -export type FieldsConfig = z.input['fields']; +export type ColumnsConfig = z.input['columns']; -interface CollectionConfig +interface CollectionConfig // use `extends` to ensure types line up with zod, // only adding generics for type completions. - extends Pick, 'fields' | 'indexes' | 'foreignKeys'> { - fields: TFields; + extends Pick, 'columns' | 'indexes' | 'foreignKeys'> { + columns: TColumns; foreignKeys?: Array<{ - fields: MaybeArray>; - // TODO: runtime error if parent collection doesn't match for all fields. Can't put a generic here... - references: () => MaybeArray>; + columns: MaybeArray>; + // TODO: runtime error if parent collection doesn't match for all columns. Can't put a generic here... + references: () => MaybeArray>; }>; - indexes?: Record>; + indexes?: Record>; } -interface IndexConfig extends z.input { - on: MaybeArray>; +interface IndexConfig extends z.input { + on: MaybeArray>; } export type ResolvedCollectionConfig< - TFields extends FieldsConfig = FieldsConfig, + TColumns extends ColumnsConfig = ColumnsConfig, Writable extends boolean = boolean, -> = CollectionConfig & { +> = CollectionConfig & { writable: Writable; - table: Table; + table: Table; }; -function baseDefineCollection( - userConfig: CollectionConfig, +function baseDefineCollection( + userConfig: CollectionConfig, writable: TWritable -): ResolvedCollectionConfig { +): ResolvedCollectionConfig { return { ...userConfig, writable, @@ -339,26 +339,26 @@ function baseDefineCollection( - userConfig: CollectionConfig -): ResolvedCollectionConfig { +export function defineCollection( + userConfig: CollectionConfig +): ResolvedCollectionConfig { return baseDefineCollection(userConfig, false); } -export function defineWritableCollection( - userConfig: CollectionConfig -): ResolvedCollectionConfig { +export function defineWritableCollection( + userConfig: CollectionConfig +): ResolvedCollectionConfig { return baseDefineCollection(userConfig, true); } export type AstroConfigWithDB = z.input; -// We cannot use `Omit`, +// We cannot use `Omit`, // since Omit collapses our union type on primary key. -type NumberFieldOpts = z.input; -type TextFieldOpts = z.input; +type NumberColumnOpts = z.input; +type TextColumnOpts = z.input; -function createField>(type: S, schema: T) { +function createColumn>(type: S, schema: T) { return { type, /** @@ -368,20 +368,20 @@ function createField>(type: }; } -export const field = { - number: (opts: T = {} as T) => { - return createField('number', opts) satisfies { type: 'number' }; +export const column = { + number: (opts: T = {} as T) => { + return createColumn('number', opts) satisfies { type: 'number' }; }, - boolean: (opts: T = {} as T) => { - return createField('boolean', opts) satisfies { type: 'boolean' }; + boolean: (opts: T = {} as T) => { + return createColumn('boolean', opts) satisfies { type: 'boolean' }; }, - text: (opts: T = {} as T) => { - return createField('text', opts) satisfies { type: 'text' }; + text: (opts: T = {} as T) => { + return createColumn('text', opts) satisfies { type: 'text' }; }, - date(opts: T = {} as T) { - return createField('date', opts) satisfies { type: 'date' }; + date(opts: T = {} as T) { + return createColumn('date', opts) satisfies { type: 'date' }; }, - json(opts: T = {} as T) { - return createField('json', opts) satisfies { type: 'json' }; + json(opts: T = {} as T) { + return createColumn('json', opts) satisfies { type: 'json' }; }, }; diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts index 7e46c6c709..b2e5c7c28b 100644 --- a/packages/db/src/index.ts +++ b/packages/db/src/index.ts @@ -1,4 +1,4 @@ -export { defineCollection, defineWritableCollection, defineData, field } from './core/types.js'; +export { defineCollection, defineWritableCollection, defineData, column } from './core/types.js'; export type { ResolvedCollectionConfig, DBDataContext } from './core/types.js'; export { cli } from './core/cli/index.js'; export { integration as default } from './core/integration/index.js'; diff --git a/packages/db/src/runtime/index.ts b/packages/db/src/runtime/index.ts index 9677f1d735..eae509b282 100644 --- a/packages/db/src/runtime/index.ts +++ b/packages/db/src/runtime/index.ts @@ -1,5 +1,5 @@ import type { SqliteRemoteDatabase } from 'drizzle-orm/sqlite-proxy'; -import { type DBCollection, type DBField } from '../core/types.js'; +import { type DBCollection, type DBColumn } from '../core/types.js'; import { type ColumnBuilderBaseConfig, type ColumnDataType, sql, SQL } from 'drizzle-orm'; import { customType, @@ -18,8 +18,8 @@ export type SqliteDB = SqliteRemoteDatabase; export type { Table } from './types.js'; export { createRemoteDatabaseClient, createLocalDatabaseClient } from './db-client.js'; -export function hasPrimaryKey(field: DBField) { - return 'primaryKey' in field.schema && !!field.schema.primaryKey; +export function hasPrimaryKey(column: DBColumn) { + return 'primaryKey' in column.schema && !!column.schema.primaryKey; } // Exports a few common expressions @@ -58,11 +58,11 @@ type D1ColumnBuilder = SQLiteColumnBuilderBase< export function collectionToTable(name: string, collection: DBCollection) { const columns: Record = {}; - if (!Object.entries(collection.fields).some(([, field]) => hasPrimaryKey(field))) { + if (!Object.entries(collection.columns).some(([, column]) => hasPrimaryKey(column))) { columns['_id'] = integer('_id').primaryKey(); } - for (const [fieldName, field] of Object.entries(collection.fields)) { - columns[fieldName] = columnMapper(fieldName, field); + for (const [columnName, column] of Object.entries(collection.columns)) { + columns[columnName] = columnMapper(columnName, column); } const table = sqliteTable(name, columns, (ormTable) => { const indexes: Record = {}; @@ -82,7 +82,7 @@ function atLeastOne(arr: T[]): arr is [T, ...T[]] { return arr.length > 0; } -function columnMapper(fieldName: string, field: DBField) { +function columnMapper(columnName: string, column: DBColumn) { let c: ReturnType< | typeof text | typeof integer @@ -91,45 +91,45 @@ function columnMapper(fieldName: string, field: DBField) { | typeof integer >; - switch (field.type) { + switch (column.type) { case 'text': { - c = text(fieldName); + c = text(columnName); // Duplicate default logic across cases to preserve type inference. // No clean generic for every column builder. - if (field.schema.default !== undefined) - c = c.default(handleSerializedSQL(field.schema.default)); - if (field.schema.primaryKey === true) c = c.primaryKey(); + if (column.schema.default !== undefined) + c = c.default(handleSerializedSQL(column.schema.default)); + if (column.schema.primaryKey === true) c = c.primaryKey(); break; } case 'number': { - c = integer(fieldName); - if (field.schema.default !== undefined) - c = c.default(handleSerializedSQL(field.schema.default)); - if (field.schema.primaryKey === true) c = c.primaryKey(); + c = integer(columnName); + if (column.schema.default !== undefined) + c = c.default(handleSerializedSQL(column.schema.default)); + if (column.schema.primaryKey === true) c = c.primaryKey(); break; } case 'boolean': { - c = integer(fieldName, { mode: 'boolean' }); - if (field.schema.default !== undefined) - c = c.default(handleSerializedSQL(field.schema.default)); + c = integer(columnName, { mode: 'boolean' }); + if (column.schema.default !== undefined) + c = c.default(handleSerializedSQL(column.schema.default)); break; } case 'json': - c = jsonType(fieldName); - if (field.schema.default !== undefined) c = c.default(field.schema.default); + c = jsonType(columnName); + if (column.schema.default !== undefined) c = c.default(column.schema.default); break; case 'date': { - c = dateType(fieldName); - if (field.schema.default !== undefined) { - const def = handleSerializedSQL(field.schema.default); + c = dateType(columnName); + if (column.schema.default !== undefined) { + const def = handleSerializedSQL(column.schema.default); c = c.default(typeof def === 'string' ? new Date(def) : def); } break; } } - if (!field.schema.optional) c = c.notNull(); - if (field.schema.unique) c = c.unique(); + if (!column.schema.optional) c = c.notNull(); + if (column.schema.unique) c = c.unique(); return c; } diff --git a/packages/db/src/runtime/types.ts b/packages/db/src/runtime/types.ts index 5bff0ad1d1..32ee6153b8 100644 --- a/packages/db/src/runtime/types.ts +++ b/packages/db/src/runtime/types.ts @@ -1,6 +1,6 @@ import type { ColumnDataType, ColumnBaseConfig } from 'drizzle-orm'; import type { SQLiteColumn, SQLiteTableWithColumns } from 'drizzle-orm/sqlite-core'; -import type { DBField, FieldsConfig } from '../core/types.js'; +import type { DBColumn, ColumnsConfig } from '../core/types.js'; type GeneratedConfig = Pick< ColumnBaseConfig, @@ -62,7 +62,7 @@ export type AstroJson> = SQLiteColumn< } >; -export type Column = T extends 'boolean' +export type Column = T extends 'boolean' ? AstroBoolean : T extends 'number' ? AstroNumber @@ -76,23 +76,23 @@ export type Column = T ext export type Table< TTableName extends string, - TFields extends FieldsConfig, + TColumns extends ColumnsConfig, > = SQLiteTableWithColumns<{ name: TTableName; schema: undefined; dialect: 'sqlite'; columns: { - [K in Extract]: Column< - TFields[K]['type'], + [K in Extract]: Column< + TColumns[K]['type'], { tableName: TTableName; name: K; - hasDefault: TFields[K]['schema'] extends { default: NonNullable } + hasDefault: TColumns[K]['schema'] extends { default: NonNullable } ? true - : TFields[K]['schema'] extends { primaryKey: true } + : TColumns[K]['schema'] extends { primaryKey: true } ? true : false; - notNull: TFields[K]['schema']['optional'] extends true ? false : true; + notNull: TColumns[K]['schema']['optional'] extends true ? false : true; } >; }; diff --git a/packages/db/test/basics.test.js b/packages/db/test/basics.test.js index db5dbf0ab5..1fd2083350 100644 --- a/packages/db/test/basics.test.js +++ b/packages/db/test/basics.test.js @@ -60,7 +60,7 @@ describe('astro:db', () => { app = await fixture.loadTestAdapterApp(); }); - it('Allows expression defaults for date fields', async () => { + it('Allows expression defaults for date columns', async () => { const request = new Request('http://example.com/'); const res = await app.render(request); const html = await res.text(); @@ -80,7 +80,7 @@ describe('astro:db', () => { expect(new Date(themeAdded).getTime()).to.not.be.NaN; }); - it('Allows expression defaults for text fields', async () => { + it('Allows expression defaults for text columns', async () => { const request = new Request('http://example.com/'); const res = await app.render(request); const html = await res.text(); @@ -90,7 +90,7 @@ describe('astro:db', () => { expect(themeOwner).to.equal(''); }); - it('Allows expression defaults for boolean fields', async () => { + it('Allows expression defaults for boolean columns', async () => { const request = new Request('http://example.com/'); const res = await app.render(request); const html = await res.text(); diff --git a/packages/db/test/fixtures/basics/astro.config.ts b/packages/db/test/fixtures/basics/astro.config.ts index 5b64a9921d..7ffc64e28a 100644 --- a/packages/db/test/fixtures/basics/astro.config.ts +++ b/packages/db/test/fixtures/basics/astro.config.ts @@ -1,23 +1,23 @@ import { defineConfig } from 'astro/config'; -import db, { defineCollection, defineWritableCollection, field, sql, NOW } from '@astrojs/db'; +import db, { defineCollection, defineWritableCollection, column, sql, NOW } from '@astrojs/db'; const Author = defineCollection({ - fields: { - name: field.text(), + columns: { + name: column.text(), }, }); const Themes = defineWritableCollection({ - fields: { - name: field.text(), - added: field.date({ + columns: { + name: column.text(), + added: column.date({ default: sql`CURRENT_TIMESTAMP` }), - updated: field.date({ + updated: column.date({ default: NOW }), - isDark: field.boolean({ default: sql`TRUE` }), - owner: field.text({ optional: true, default: sql`NULL` }), + isDark: column.boolean({ default: sql`TRUE` }), + owner: column.text({ optional: true, default: sql`NULL` }), }, }); diff --git a/packages/db/test/fixtures/glob/astro.config.ts b/packages/db/test/fixtures/glob/astro.config.ts index e8516a4637..f2385cd243 100644 --- a/packages/db/test/fixtures/glob/astro.config.ts +++ b/packages/db/test/fixtures/glob/astro.config.ts @@ -1,12 +1,12 @@ import { defineConfig } from 'astro/config'; -import db, { defineCollection, field } from '@astrojs/db'; +import db, { defineCollection, column } from '@astrojs/db'; import { asJson, createGlob } from './utils'; const Quote = defineCollection({ - fields: { - author: field.text(), - body: field.text(), - file: field.text({ unique: true }), + columns: { + author: column.text(), + body: column.text(), + file: column.text({ unique: true }), }, }); diff --git a/packages/db/test/fixtures/glob/utils.ts b/packages/db/test/fixtures/glob/utils.ts index ae3b7c5e5c..cbc29a88dd 100644 --- a/packages/db/test/fixtures/glob/utils.ts +++ b/packages/db/test/fixtures/glob/utils.ts @@ -14,9 +14,9 @@ export function createGlob({ db, mode }: Pick) { ) { // TODO: expose `table` const { table } = opts.into as any; - const fileField = table.file; - if (!fileField) { - throw new Error('`file` field is required for glob collections.'); + const fileColumn = table.file; + if (!fileColumn) { + throw new Error('`file` column is required for glob collections.'); } if (mode === 'dev') { chokidar @@ -33,12 +33,12 @@ export function createGlob({ db, mode }: Pick) { .insert(table) .values({ ...parsed, file }) .onConflictDoUpdate({ - target: fileField, + target: fileColumn, set: parsed, }); }) .on('unlink', async (file) => { - await db.delete(table).where(eq(fileField, file)); + await db.delete(table).where(eq(fileColumn, file)); }); } else { const files = await fastGlob(pattern); diff --git a/packages/db/test/fixtures/recipes/astro.config.ts b/packages/db/test/fixtures/recipes/astro.config.ts index 4a61f7c2c6..28a27588e9 100644 --- a/packages/db/test/fixtures/recipes/astro.config.ts +++ b/packages/db/test/fixtures/recipes/astro.config.ts @@ -1,25 +1,25 @@ import { defineConfig } from 'astro/config'; -import astroDb, { defineCollection, field } from '@astrojs/db'; +import astroDb, { defineCollection, column } from '@astrojs/db'; const Recipe = defineCollection({ - fields: { - id: field.number({ primaryKey: true }), - title: field.text(), - description: field.text(), + columns: { + id: column.number({ primaryKey: true }), + title: column.text(), + description: column.text(), }, }); const Ingredient = defineCollection({ - fields: { - id: field.number({ primaryKey: true }), - name: field.text(), - quantity: field.number(), - recipeId: field.number(), + columns: { + id: column.number({ primaryKey: true }), + name: column.text(), + quantity: column.number(), + recipeId: column.number(), }, indexes: { recipeIdx: { on: 'recipeId' }, }, - foreignKeys: [{ fields: 'recipeId', references: () => [Recipe.fields.id] }], + foreignKeys: [{ columns: 'recipeId', references: () => [Recipe.columns.id] }], }); export default defineConfig({ diff --git a/packages/db/test/fixtures/ticketing-example/astro.config.ts b/packages/db/test/fixtures/ticketing-example/astro.config.ts index 09db7b6bd6..9cba1abf45 100644 --- a/packages/db/test/fixtures/ticketing-example/astro.config.ts +++ b/packages/db/test/fixtures/ticketing-example/astro.config.ts @@ -1,25 +1,25 @@ import { defineConfig } from 'astro/config'; import preact from '@astrojs/preact'; import simpleStackForm from 'simple-stack-form'; -import db, { defineCollection, defineWritableCollection, field } from '@astrojs/db'; +import db, { defineCollection, defineWritableCollection, column } from '@astrojs/db'; import node from '@astrojs/node'; const Event = defineCollection({ - fields: { - id: field.number({ primaryKey: true }), - name: field.text(), - description: field.text(), - ticketPrice: field.number(), - date: field.date(), - location: field.text(), + columns: { + id: column.number({ primaryKey: true }), + name: column.text(), + description: column.text(), + ticketPrice: column.number(), + date: column.date(), + location: column.text(), }, }); const Ticket = defineWritableCollection({ - fields: { - eventId: field.text(), - email: field.text(), - quantity: field.number(), - newsletter: field.boolean({ + columns: { + eventId: column.text(), + email: column.text(), + quantity: column.number(), + newsletter: column.boolean({ default: false, }), }, diff --git a/packages/db/test/fixtures/ticketing-example/src/components/Form.tsx b/packages/db/test/fixtures/ticketing-example/src/components/Form.tsx index c7d4dcd79e..d78fecff8d 100644 --- a/packages/db/test/fixtures/ticketing-example/src/components/Form.tsx +++ b/packages/db/test/fixtures/ticketing-example/src/components/Form.tsx @@ -4,25 +4,25 @@ import { type ComponentProps, createContext } from 'preact'; import { useContext, useState } from 'preact/hooks'; import { navigate } from 'astro:transitions/client'; import { - type FieldErrors, + type ColumnErrors, type FormState, type FormValidator, formNameInputProps, getInitialFormState, toSetValidationErrors, toTrackAstroSubmitStatus, - toValidateField, + toValidateColumn, validateForm, } from 'simple:form'; -export function useCreateFormContext(validator: FormValidator, fieldErrors?: FieldErrors) { - const initial = getInitialFormState({ validator, fieldErrors }); +export function useCreateFormContext(validator: FormValidator, columnErrors?: ColumnErrors) { + const initial = getInitialFormState({ validator, columnErrors }); const [formState, setFormState] = useState(initial); return { value: formState, set: setFormState, setValidationErrors: toSetValidationErrors(setFormState), - validateField: toValidateField(setFormState), + validateColumn: toValidateColumn(setFormState), trackAstroSubmitStatus: toTrackAstroSubmitStatus(setFormState), }; } @@ -45,15 +45,15 @@ export function Form({ children, validator, context, - fieldErrors, + columnErrors, name, ...formProps }: { validator: FormValidator; context?: FormContextType; - fieldErrors?: FieldErrors; + columnErrors?: ColumnErrors; } & Omit, 'method' | 'onSubmit'>) { - const formContext = context ?? useCreateFormContext(validator, fieldErrors); + const formContext = context ?? useCreateFormContext(validator, columnErrors); return ( @@ -80,7 +80,7 @@ export function Form({ return formContext.trackAstroSubmitStatus(); } - formContext.setValidationErrors(parsed.fieldErrors); + formContext.setValidationErrors(parsed.columnErrors); }} > {name ? : null} @@ -92,28 +92,28 @@ export function Form({ export function Input({ onInput, ...inputProps }: ComponentProps<'input'> & { name: string }) { const formContext = useFormContext(); - const fieldState = formContext.value.fields[inputProps.name]; - if (!fieldState) { + const columnState = formContext.value.columns[inputProps.name]; + if (!columnState) { throw new Error( `Input "${inputProps.name}" not found in form. Did you use the
component?` ); } - const { hasErroredOnce, validationErrors, validator } = fieldState; + const { hasErroredOnce, validationErrors, validator } = columnState; return ( <> { const value = e.currentTarget.value; if (value === '') return; - formContext.validateField(inputProps.name, value, validator); + formContext.validateColumn(inputProps.name, value, validator); }} onInput={async (e) => { onInput?.(e); if (!hasErroredOnce) return; const value = e.currentTarget.value; - formContext.validateField(inputProps.name, value, validator); + formContext.validateColumn(inputProps.name, value, validator); }} {...inputProps} /> diff --git a/packages/db/test/unit/field-queries.test.js b/packages/db/test/unit/field-queries.test.js index cddfdd68a4..f11080587b 100644 --- a/packages/db/test/unit/field-queries.test.js +++ b/packages/db/test/unit/field-queries.test.js @@ -5,27 +5,27 @@ import { getMigrationQueries, } from '../../dist/core/cli/migration-queries.js'; import { getCreateTableQuery } from '../../dist/core/queries.js'; -import { field, defineCollection, collectionSchema } from '../../dist/core/types.js'; +import { column, defineCollection, collectionSchema } from '../../dist/core/types.js'; import { NOW, sql } from '../../dist/runtime/index.js'; const COLLECTION_NAME = 'Users'; // `parse` to resolve schema transformations -// ex. convert field.date() to ISO strings +// ex. convert column.date() to ISO strings const userInitial = collectionSchema.parse( defineCollection({ - fields: { - name: field.text(), - age: field.number(), - email: field.text({ unique: true }), - mi: field.text({ optional: true }), + columns: { + name: column.text(), + age: column.number(), + email: column.text({ unique: true }), + mi: column.text({ optional: true }), }, }) ); const defaultAmbiguityResponses = { collectionRenames: {}, - fieldRenames: {}, + columnRenames: {}, }; function userChangeQueries( @@ -53,7 +53,7 @@ function configChangeQueries( }); } -describe('field queries', () => { +describe('column queries', () => { describe('getMigrationQueries', () => { it('should be empty when collections are the same', async () => { const oldCollections = { [COLLECTION_NAME]: userInitial }; @@ -97,16 +97,16 @@ describe('field queries', () => { it('should be empty when type updated to same underlying SQL type', async () => { const blogInitial = collectionSchema.parse({ ...userInitial, - fields: { - title: field.text(), - draft: field.boolean(), + columns: { + title: column.text(), + draft: column.boolean(), }, }); const blogFinal = collectionSchema.parse({ ...userInitial, - fields: { - ...blogInitial.fields, - draft: field.number(), + columns: { + ...blogInitial.columns, + draft: column.number(), }, }); const { queries } = await userChangeQueries(blogInitial, blogFinal); @@ -116,17 +116,17 @@ describe('field queries', () => { it('should respect user primary key without adding a hidden id', async () => { const user = collectionSchema.parse({ ...userInitial, - fields: { - ...userInitial.fields, - id: field.number({ primaryKey: true }), + columns: { + ...userInitial.columns, + id: column.number({ primaryKey: true }), }, }); const userFinal = collectionSchema.parse({ ...user, - fields: { - ...user.fields, - name: field.text({ unique: true, optional: true }), + columns: { + ...user.columns, + name: column.text({ unique: true, optional: true }), }, }); @@ -143,19 +143,19 @@ describe('field queries', () => { }); describe('ALTER RENAME COLUMN', () => { - it('when renaming a field', async () => { + it('when renaming a column', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, + columns: { + ...userInitial.columns, }, }; - userFinal.fields.middleInitial = userFinal.fields.mi; - delete userFinal.fields.mi; + userFinal.columns.middleInitial = userFinal.columns.mi; + delete userFinal.columns.mi; const { queries } = await userChangeQueries(userInitial, userFinal, { collectionRenames: {}, - fieldRenames: { [COLLECTION_NAME]: { middleInitial: 'mi' } }, + columnRenames: { [COLLECTION_NAME]: { middleInitial: 'mi' } }, }); expect(queries).to.deep.equal([ `ALTER TABLE "${COLLECTION_NAME}" RENAME COLUMN "mi" TO "middleInitial"`, @@ -164,12 +164,12 @@ describe('field queries', () => { }); describe('Lossy table recreate', () => { - it('when changing a field type', async () => { + it('when changing a column type', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, - age: field.text(), + columns: { + ...userInitial.columns, + age: column.text(), }, }; @@ -181,12 +181,12 @@ describe('field queries', () => { ]); }); - it('when adding a required field without a default', async () => { + it('when adding a required column without a default', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, - phoneNumber: field.text(), + columns: { + ...userInitial.columns, + phoneNumber: column.text(), }, }; @@ -203,9 +203,9 @@ describe('field queries', () => { it('when adding a primary key', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, - id: field.number({ primaryKey: true }), + columns: { + ...userInitial.columns, + id: column.number({ primaryKey: true }), }, }; @@ -224,9 +224,9 @@ describe('field queries', () => { it('when dropping a primary key', async () => { const user = { ...userInitial, - fields: { - ...userInitial.fields, - id: field.number({ primaryKey: true }), + columns: { + ...userInitial.columns, + id: column.number({ primaryKey: true }), }, }; @@ -242,12 +242,12 @@ describe('field queries', () => { ]); }); - it('when adding an optional unique field', async () => { + it('when adding an optional unique column', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, - phoneNumber: field.text({ unique: true, optional: true }), + columns: { + ...userInitial.columns, + phoneNumber: column.text({ unique: true, optional: true }), }, }; @@ -267,11 +267,11 @@ describe('field queries', () => { it('when dropping unique column', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, + columns: { + ...userInitial.columns, }, }; - delete userFinal.fields.email; + delete userFinal.columns.email; const { queries } = await userChangeQueries(userInitial, userFinal); expect(queries).to.have.lengthOf(4); @@ -289,17 +289,17 @@ describe('field queries', () => { it('when updating to a runtime default', async () => { const initial = collectionSchema.parse({ ...userInitial, - fields: { - ...userInitial.fields, - age: field.date(), + columns: { + ...userInitial.columns, + age: column.date(), }, }); const userFinal = collectionSchema.parse({ ...initial, - fields: { - ...initial.fields, - age: field.date({ default: NOW }), + columns: { + ...initial.columns, + age: column.date({ default: NOW }), }, }); @@ -316,12 +316,12 @@ describe('field queries', () => { ]); }); - it('when adding a field with a runtime default', async () => { + it('when adding a column with a runtime default', async () => { const userFinal = collectionSchema.parse({ ...userInitial, - fields: { - ...userInitial.fields, - birthday: field.date({ default: NOW }), + columns: { + ...userInitial.columns, + birthday: column.date({ default: NOW }), }, }); @@ -345,12 +345,12 @@ describe('field queries', () => { * * @see https://planetscale.com/blog/safely-making-database-schema-changes#backwards-compatible-changes */ - it('when changing a field to required', async () => { + it('when changing a column to required', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, - mi: field.text(), + columns: { + ...userInitial.columns, + mi: column.text(), }, }; @@ -368,12 +368,12 @@ describe('field queries', () => { ]); }); - it('when changing a field to unique', async () => { + it('when changing a column to unique', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, - age: field.number({ unique: true }), + columns: { + ...userInitial.columns, + age: column.number({ unique: true }), }, }; @@ -392,12 +392,12 @@ describe('field queries', () => { }); describe('ALTER ADD COLUMN', () => { - it('when adding an optional field', async () => { + it('when adding an optional column', async () => { const userFinal = { ...userInitial, - fields: { - ...userInitial.fields, - birthday: field.date({ optional: true }), + columns: { + ...userInitial.columns, + birthday: column.date({ optional: true }), }, }; @@ -405,13 +405,13 @@ describe('field queries', () => { expect(queries).to.deep.equal(['ALTER TABLE "Users" ADD COLUMN "birthday" text']); }); - it('when adding a required field with default', async () => { + it('when adding a required column with default', async () => { const defaultDate = new Date('2023-01-01'); const userFinal = collectionSchema.parse({ ...userInitial, - fields: { - ...userInitial.fields, - birthday: field.date({ default: new Date('2023-01-01') }), + columns: { + ...userInitial.columns, + birthday: column.date({ default: new Date('2023-01-01') }), }, }); @@ -423,12 +423,12 @@ describe('field queries', () => { }); describe('ALTER DROP COLUMN', () => { - it('when removing optional or required fields', async () => { + it('when removing optional or required columns', async () => { const userFinal = { ...userInitial, - fields: { - name: userInitial.fields.name, - email: userInitial.fields.email, + columns: { + name: userInitial.columns.name, + email: userInitial.columns.email, }, }; diff --git a/packages/db/test/unit/index-queries.test.js b/packages/db/test/unit/index-queries.test.js index d391249d1c..7153261dbd 100644 --- a/packages/db/test/unit/index-queries.test.js +++ b/packages/db/test/unit/index-queries.test.js @@ -1,14 +1,14 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; -import { field, collectionSchema } from '../../dist/core/types.js'; +import { column, collectionSchema } from '../../dist/core/types.js'; const userInitial = collectionSchema.parse({ - fields: { - name: field.text(), - age: field.number(), - email: field.text({ unique: true }), - mi: field.text({ optional: true }), + columns: { + name: column.text(), + age: column.number(), + email: column.text({ unique: true }), + mi: column.text({ optional: true }), }, indexes: {}, writable: false, diff --git a/packages/db/test/unit/reference-queries.test.js b/packages/db/test/unit/reference-queries.test.js index dac71a5571..5d644f7a97 100644 --- a/packages/db/test/unit/reference-queries.test.js +++ b/packages/db/test/unit/reference-queries.test.js @@ -1,30 +1,30 @@ import { expect } from 'chai'; import { describe, it } from 'mocha'; import { getCollectionChangeQueries } from '../../dist/core/cli/migration-queries.js'; -import { field, defineCollection, collectionsSchema } from '../../dist/core/types.js'; +import { column, defineCollection, collectionsSchema } from '../../dist/core/types.js'; const BaseUser = defineCollection({ - fields: { - id: field.number({ primaryKey: true }), - name: field.text(), - age: field.number(), - email: field.text({ unique: true }), - mi: field.text({ optional: true }), + columns: { + id: column.number({ primaryKey: true }), + name: column.text(), + age: column.number(), + email: column.text({ unique: true }), + mi: column.text({ optional: true }), }, }); const BaseSentBox = defineCollection({ - fields: { - to: field.number(), - toName: field.text(), - subject: field.text(), - body: field.text(), + columns: { + to: column.number(), + toName: column.text(), + subject: column.text(), + body: column.text(), }, }); const defaultAmbiguityResponses = { collectionRenames: {}, - fieldRenames: {}, + columnRenames: {}, }; /** @@ -59,9 +59,9 @@ describe('reference queries', () => { const { SentBox: Initial } = resolveReferences(); const { SentBox: Final } = resolveReferences({ SentBox: defineCollection({ - fields: { - ...BaseSentBox.fields, - to: field.number({ references: () => BaseUser.fields.id }), + columns: { + ...BaseSentBox.columns, + to: column.number({ references: () => BaseUser.columns.id }), }, }), }); @@ -83,9 +83,9 @@ describe('reference queries', () => { it('removes references with lossless table recreate', async () => { const { SentBox: Initial } = resolveReferences({ SentBox: defineCollection({ - fields: { - ...BaseSentBox.fields, - to: field.number({ references: () => BaseUser.fields.id }), + columns: { + ...BaseSentBox.columns, + to: column.number({ references: () => BaseUser.columns.id }), }, }), }); @@ -109,9 +109,9 @@ describe('reference queries', () => { const { SentBox: Initial } = resolveReferences(); const { SentBox: Final } = resolveReferences({ SentBox: defineCollection({ - fields: { - ...BaseSentBox.fields, - from: field.number({ references: () => BaseUser.fields.id, optional: true }), + columns: { + ...BaseSentBox.columns, + from: column.number({ references: () => BaseUser.columns.id, optional: true }), }, }), }); @@ -133,7 +133,7 @@ describe('reference queries', () => { const { SentBox: InitialWithDifferentFK } = resolveReferences({ SentBox: defineCollection({ ...BaseSentBox, - foreignKeys: [{ fields: ['to'], references: () => [BaseUser.fields.id] }], + foreignKeys: [{ columns: ['to'], references: () => [BaseUser.columns.id] }], }), }); const { SentBox: Final } = resolveReferences({ @@ -141,8 +141,8 @@ describe('reference queries', () => { ...BaseSentBox, foreignKeys: [ { - fields: ['to', 'toName'], - references: () => [BaseUser.fields.id, BaseUser.fields.name], + columns: ['to', 'toName'], + references: () => [BaseUser.columns.id, BaseUser.columns.name], }, ], }),