mirror of
https://github.com/withastro/astro.git
synced 2024-12-30 22:03:56 -05:00
Rename field->column
This commit is contained in:
parent
841e24ebb9
commit
9693d19801
18 changed files with 479 additions and 479 deletions
|
@ -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<string, string>;
|
||||
fieldRenames: {
|
||||
columnRenames: {
|
||||
[collectionName: string]: Record<string, string>;
|
||||
};
|
||||
};
|
||||
|
@ -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<DataLossReason, string> = {
|
||||
'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<string, DBField>;
|
||||
added: Record<string, DBColumn>;
|
||||
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<string, unknown>) {
|
|||
*
|
||||
* @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<T>(oldObj: Record<string, T>, newObj: Record<string, T>) {
|
|||
return updated;
|
||||
}
|
||||
|
||||
type UpdatedFields = Record<string, { old: DBField; new: DBField }>;
|
||||
type UpdatedColumns = Record<string, { old: DBColumn; new: DBColumn }>;
|
||||
|
||||
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 extends DBField> = T & Required<Pick<T['schema'], 'default'>>;
|
||||
type DBFieldWithDefault =
|
||||
| WithDefaultDefined<TextField>
|
||||
| WithDefaultDefined<DateField>
|
||||
| WithDefaultDefined<NumberField>
|
||||
| WithDefaultDefined<BooleanField>
|
||||
| WithDefaultDefined<JsonField>;
|
||||
// Using `DBColumn` will not narrow `default` based on the column `type`
|
||||
// Handle each column separately
|
||||
type WithDefaultDefined<T extends DBColumn> = T & Required<Pick<T['schema'], 'default'>>;
|
||||
type DBColumnWithDefault =
|
||||
| WithDefaultDefined<TextColumn>
|
||||
| WithDefaultDefined<DateColumn>
|
||||
| WithDefaultDefined<NumberColumn>
|
||||
| WithDefaultDefined<BooleanColumn>
|
||||
| WithDefaultDefined<JsonColumn>;
|
||||
|
||||
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));
|
||||
}
|
||||
|
|
|
@ -91,7 +91,7 @@ export async function loadMigration(
|
|||
|
||||
export async function loadInitialSnapshot(): Promise<DBSnapshot> {
|
||||
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;
|
||||
}
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
])
|
||||
)
|
||||
|
|
|
@ -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<T>(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 extends DBField> = T & {
|
||||
// Using `DBColumn` will not narrow `default` based on the column `type`
|
||||
// Handle each column separately
|
||||
type WithDefaultDefined<T extends DBColumn> = T & {
|
||||
schema: Required<Pick<T['schema'], 'default'>>;
|
||||
};
|
||||
type DBFieldWithDefault =
|
||||
| WithDefaultDefined<TextField>
|
||||
| WithDefaultDefined<DateField>
|
||||
| WithDefaultDefined<NumberField>
|
||||
| WithDefaultDefined<BooleanField>
|
||||
| WithDefaultDefined<JsonField>;
|
||||
type DBColumnWithDefault =
|
||||
| WithDefaultDefined<TextColumn>
|
||||
| WithDefaultDefined<DateColumn>
|
||||
| WithDefaultDefined<NumberColumn>
|
||||
| WithDefaultDefined<BooleanColumn>
|
||||
| WithDefaultDefined<JsonColumn>;
|
||||
|
||||
// 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<T>(def: T | SQL<any>): 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;
|
||||
}
|
||||
|
|
|
@ -19,7 +19,7 @@ const sqlSchema = z.instanceof(SQL<any>).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<typeof numberFieldBaseSchema> & {
|
||||
// ReferenceableField creates a circular type. Define ZodType to resolve.
|
||||
references?: NumberField;
|
||||
const numberColumnOptsSchema: z.ZodType<
|
||||
z.infer<typeof numberColumnBaseSchema> & {
|
||||
// ReferenceableColumn creates a circular type. Define ZodType to resolve.
|
||||
references?: NumberColumn;
|
||||
},
|
||||
ZodTypeDef,
|
||||
z.input<typeof numberFieldBaseSchema> & {
|
||||
references?: () => z.input<typeof numberFieldSchema>;
|
||||
z.input<typeof numberColumnBaseSchema> & {
|
||||
references?: () => z.input<typeof numberColumnSchema>;
|
||||
}
|
||||
> = 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<typeof textFieldBaseSchema> & {
|
||||
// ReferenceableField creates a circular type. Define ZodType to resolve.
|
||||
references?: TextField;
|
||||
const textColumnOptsSchema: z.ZodType<
|
||||
z.infer<typeof textColumnBaseSchema> & {
|
||||
// ReferenceableColumn creates a circular type. Define ZodType to resolve.
|
||||
references?: TextColumn;
|
||||
},
|
||||
ZodTypeDef,
|
||||
z.input<typeof textFieldBaseSchema> & {
|
||||
references?: () => z.input<typeof textFieldSchema>;
|
||||
z.input<typeof textColumnBaseSchema> & {
|
||||
references?: () => z.input<typeof textColumnSchema>;
|
||||
}
|
||||
> = 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<string>;
|
||||
references: () => MaybeArray<Omit<z.input<typeof referenceableFieldSchema>, 'references'>>;
|
||||
columns: MaybeArray<string>;
|
||||
references: () => MaybeArray<Omit<z.input<typeof referenceableColumnSchema>, 'references'>>;
|
||||
};
|
||||
|
||||
type ForeignKeysOutput = Omit<ForeignKeysInput, 'references'> & {
|
||||
// reference fn called in `transform`. Ensures output is JSON serializable.
|
||||
references: MaybeArray<Omit<z.output<typeof referenceableFieldSchema>, 'references'>>;
|
||||
references: MaybeArray<Omit<z.output<typeof referenceableColumnSchema>, 'references'>>;
|
||||
};
|
||||
|
||||
const foreignKeysSchema: z.ZodType<ForeignKeysOutput, ZodTypeDef, ForeignKeysInput> = 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<string, z.infer<typeof indexSchema>>;
|
||||
|
||||
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<typeof booleanFieldSchema>;
|
||||
export type BooleanFieldInput = z.input<typeof booleanFieldSchema>;
|
||||
export type NumberField = z.infer<typeof numberFieldSchema>;
|
||||
export type NumberFieldInput = z.input<typeof numberFieldSchema>;
|
||||
export type TextField = z.infer<typeof textFieldSchema>;
|
||||
export type TextFieldInput = z.input<typeof textFieldSchema>;
|
||||
export type DateField = z.infer<typeof dateFieldSchema>;
|
||||
export type DateFieldInput = z.input<typeof dateFieldSchema>;
|
||||
export type JsonField = z.infer<typeof jsonFieldSchema>;
|
||||
export type JsonFieldInput = z.input<typeof jsonFieldSchema>;
|
||||
export type BooleanColumn = z.infer<typeof booleanColumnSchema>;
|
||||
export type BooleanColumnInput = z.input<typeof booleanColumnSchema>;
|
||||
export type NumberColumn = z.infer<typeof numberColumnSchema>;
|
||||
export type NumberColumnInput = z.input<typeof numberColumnSchema>;
|
||||
export type TextColumn = z.infer<typeof textColumnSchema>;
|
||||
export type TextColumnInput = z.input<typeof textColumnSchema>;
|
||||
export type DateColumn = z.infer<typeof dateColumnSchema>;
|
||||
export type DateColumnInput = z.input<typeof dateColumnSchema>;
|
||||
export type JsonColumn = z.infer<typeof jsonColumnSchema>;
|
||||
export type JsonColumnInput = z.input<typeof jsonColumnSchema>;
|
||||
|
||||
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<typeof fieldSchema>;
|
||||
export type DBFieldInput =
|
||||
| DateFieldInput
|
||||
| BooleanFieldInput
|
||||
| NumberFieldInput
|
||||
| TextFieldInput
|
||||
| JsonFieldInput;
|
||||
export type DBFields = z.infer<typeof fieldsSchema>;
|
||||
export type DBColumn = z.infer<typeof columnSchema>;
|
||||
export type DBColumnInput =
|
||||
| DateColumnInput
|
||||
| BooleanColumnInput
|
||||
| NumberColumnInput
|
||||
| TextColumnInput
|
||||
| JsonColumnInput;
|
||||
export type DBColumns = z.infer<typeof columnsSchema>;
|
||||
export type DBCollection = z.infer<
|
||||
typeof readableCollectionSchema | typeof writableCollectionSchema
|
||||
>;
|
||||
|
@ -260,20 +260,20 @@ export type WritableDBCollection = z.infer<typeof writableCollectionSchema>;
|
|||
|
||||
export type DBDataContext = {
|
||||
db: SqliteDB;
|
||||
seed: <TFields extends FieldsConfig>(
|
||||
collection: ResolvedCollectionConfig<TFields>,
|
||||
data: MaybeArray<SQLiteInsertValue<Table<string, TFields>>>
|
||||
seed: <TColumns extends ColumnsConfig>(
|
||||
collection: ResolvedCollectionConfig<TColumns>,
|
||||
data: MaybeArray<SQLiteInsertValue<Table<string, TColumns>>>
|
||||
) => Promise<void>;
|
||||
seedReturning: <
|
||||
TFields extends FieldsConfig,
|
||||
TData extends MaybeArray<SQLiteInsertValue<Table<string, TFields>>>,
|
||||
TColumns extends ColumnsConfig,
|
||||
TData extends MaybeArray<SQLiteInsertValue<Table<string, TColumns>>>,
|
||||
>(
|
||||
collection: ResolvedCollectionConfig<TFields>,
|
||||
collection: ResolvedCollectionConfig<TColumns>,
|
||||
data: TData
|
||||
) => Promise<
|
||||
TData extends Array<SQLiteInsertValue<Table<string, TFields>>>
|
||||
? InferSelectModel<Table<string, TFields>>[]
|
||||
: InferSelectModel<Table<string, TFields>>
|
||||
TData extends Array<SQLiteInsertValue<Table<string, TColumns>>>
|
||||
? InferSelectModel<Table<string, TColumns>>[]
|
||||
: InferSelectModel<Table<string, TColumns>>
|
||||
>;
|
||||
mode: 'dev' | 'build';
|
||||
};
|
||||
|
@ -300,37 +300,37 @@ export const astroConfigWithDbSchema = z.object({
|
|||
db: dbConfigSchema.optional(),
|
||||
});
|
||||
|
||||
export type FieldsConfig = z.input<typeof collectionSchema>['fields'];
|
||||
export type ColumnsConfig = z.input<typeof collectionSchema>['columns'];
|
||||
|
||||
interface CollectionConfig<TFields extends FieldsConfig = FieldsConfig>
|
||||
interface CollectionConfig<TColumns extends ColumnsConfig = ColumnsConfig>
|
||||
// use `extends` to ensure types line up with zod,
|
||||
// only adding generics for type completions.
|
||||
extends Pick<z.input<typeof collectionSchema>, 'fields' | 'indexes' | 'foreignKeys'> {
|
||||
fields: TFields;
|
||||
extends Pick<z.input<typeof collectionSchema>, 'columns' | 'indexes' | 'foreignKeys'> {
|
||||
columns: TColumns;
|
||||
foreignKeys?: Array<{
|
||||
fields: MaybeArray<Extract<keyof TFields, string>>;
|
||||
// TODO: runtime error if parent collection doesn't match for all fields. Can't put a generic here...
|
||||
references: () => MaybeArray<z.input<typeof referenceableFieldSchema>>;
|
||||
columns: MaybeArray<Extract<keyof TColumns, string>>;
|
||||
// TODO: runtime error if parent collection doesn't match for all columns. Can't put a generic here...
|
||||
references: () => MaybeArray<z.input<typeof referenceableColumnSchema>>;
|
||||
}>;
|
||||
indexes?: Record<string, IndexConfig<TFields>>;
|
||||
indexes?: Record<string, IndexConfig<TColumns>>;
|
||||
}
|
||||
|
||||
interface IndexConfig<TFields extends FieldsConfig> extends z.input<typeof indexSchema> {
|
||||
on: MaybeArray<Extract<keyof TFields, string>>;
|
||||
interface IndexConfig<TColumns extends ColumnsConfig> extends z.input<typeof indexSchema> {
|
||||
on: MaybeArray<Extract<keyof TColumns, string>>;
|
||||
}
|
||||
|
||||
export type ResolvedCollectionConfig<
|
||||
TFields extends FieldsConfig = FieldsConfig,
|
||||
TColumns extends ColumnsConfig = ColumnsConfig,
|
||||
Writable extends boolean = boolean,
|
||||
> = CollectionConfig<TFields> & {
|
||||
> = CollectionConfig<TColumns> & {
|
||||
writable: Writable;
|
||||
table: Table<string, TFields>;
|
||||
table: Table<string, TColumns>;
|
||||
};
|
||||
|
||||
function baseDefineCollection<TFields extends FieldsConfig, TWritable extends boolean>(
|
||||
userConfig: CollectionConfig<TFields>,
|
||||
function baseDefineCollection<TColumns extends ColumnsConfig, TWritable extends boolean>(
|
||||
userConfig: CollectionConfig<TColumns>,
|
||||
writable: TWritable
|
||||
): ResolvedCollectionConfig<TFields, TWritable> {
|
||||
): ResolvedCollectionConfig<TColumns, TWritable> {
|
||||
return {
|
||||
...userConfig,
|
||||
writable,
|
||||
|
@ -339,26 +339,26 @@ function baseDefineCollection<TFields extends FieldsConfig, TWritable extends bo
|
|||
};
|
||||
}
|
||||
|
||||
export function defineCollection<TFields extends FieldsConfig>(
|
||||
userConfig: CollectionConfig<TFields>
|
||||
): ResolvedCollectionConfig<TFields, false> {
|
||||
export function defineCollection<TColumns extends ColumnsConfig>(
|
||||
userConfig: CollectionConfig<TColumns>
|
||||
): ResolvedCollectionConfig<TColumns, false> {
|
||||
return baseDefineCollection(userConfig, false);
|
||||
}
|
||||
|
||||
export function defineWritableCollection<TFields extends FieldsConfig>(
|
||||
userConfig: CollectionConfig<TFields>
|
||||
): ResolvedCollectionConfig<TFields, true> {
|
||||
export function defineWritableCollection<TColumns extends ColumnsConfig>(
|
||||
userConfig: CollectionConfig<TColumns>
|
||||
): ResolvedCollectionConfig<TColumns, true> {
|
||||
return baseDefineCollection(userConfig, true);
|
||||
}
|
||||
|
||||
export type AstroConfigWithDB = z.input<typeof astroConfigWithDbSchema>;
|
||||
|
||||
// We cannot use `Omit<NumberField | TextField, 'type'>`,
|
||||
// We cannot use `Omit<NumberColumn | TextColumn, 'type'>`,
|
||||
// since Omit collapses our union type on primary key.
|
||||
type NumberFieldOpts = z.input<typeof numberFieldOptsSchema>;
|
||||
type TextFieldOpts = z.input<typeof textFieldOptsSchema>;
|
||||
type NumberColumnOpts = z.input<typeof numberColumnOptsSchema>;
|
||||
type TextColumnOpts = z.input<typeof textColumnOptsSchema>;
|
||||
|
||||
function createField<S extends string, T extends Record<string, unknown>>(type: S, schema: T) {
|
||||
function createColumn<S extends string, T extends Record<string, unknown>>(type: S, schema: T) {
|
||||
return {
|
||||
type,
|
||||
/**
|
||||
|
@ -368,20 +368,20 @@ function createField<S extends string, T extends Record<string, unknown>>(type:
|
|||
};
|
||||
}
|
||||
|
||||
export const field = {
|
||||
number: <T extends NumberFieldOpts>(opts: T = {} as T) => {
|
||||
return createField('number', opts) satisfies { type: 'number' };
|
||||
export const column = {
|
||||
number: <T extends NumberColumnOpts>(opts: T = {} as T) => {
|
||||
return createColumn('number', opts) satisfies { type: 'number' };
|
||||
},
|
||||
boolean: <T extends BooleanFieldInput['schema']>(opts: T = {} as T) => {
|
||||
return createField('boolean', opts) satisfies { type: 'boolean' };
|
||||
boolean: <T extends BooleanColumnInput['schema']>(opts: T = {} as T) => {
|
||||
return createColumn('boolean', opts) satisfies { type: 'boolean' };
|
||||
},
|
||||
text: <T extends TextFieldOpts>(opts: T = {} as T) => {
|
||||
return createField('text', opts) satisfies { type: 'text' };
|
||||
text: <T extends TextColumnOpts>(opts: T = {} as T) => {
|
||||
return createColumn('text', opts) satisfies { type: 'text' };
|
||||
},
|
||||
date<T extends DateFieldInput['schema']>(opts: T = {} as T) {
|
||||
return createField('date', opts) satisfies { type: 'date' };
|
||||
date<T extends DateColumnInput['schema']>(opts: T = {} as T) {
|
||||
return createColumn('date', opts) satisfies { type: 'date' };
|
||||
},
|
||||
json<T extends JsonFieldInput['schema']>(opts: T = {} as T) {
|
||||
return createField('json', opts) satisfies { type: 'json' };
|
||||
json<T extends JsonColumnInput['schema']>(opts: T = {} as T) {
|
||||
return createColumn('json', opts) satisfies { type: 'json' };
|
||||
},
|
||||
};
|
||||
|
|
|
@ -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';
|
||||
|
|
|
@ -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<string, D1ColumnBuilder> = {};
|
||||
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<string, IndexBuilder> = {};
|
||||
|
@ -82,7 +82,7 @@ function atLeastOne<T>(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<string, 'boolean'>
|
||||
>;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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<T extends ColumnDataType = ColumnDataType> = Pick<
|
||||
ColumnBaseConfig<T, string>,
|
||||
|
@ -62,7 +62,7 @@ export type AstroJson<T extends GeneratedConfig<'custom'>> = SQLiteColumn<
|
|||
}
|
||||
>;
|
||||
|
||||
export type Column<T extends DBField['type'], S extends GeneratedConfig> = T extends 'boolean'
|
||||
export type Column<T extends DBColumn['type'], S extends GeneratedConfig> = T extends 'boolean'
|
||||
? AstroBoolean<S>
|
||||
: T extends 'number'
|
||||
? AstroNumber<S>
|
||||
|
@ -76,23 +76,23 @@ export type Column<T extends DBField['type'], S extends GeneratedConfig> = 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<keyof TFields, string>]: Column<
|
||||
TFields[K]['type'],
|
||||
[K in Extract<keyof TColumns, string>]: Column<
|
||||
TColumns[K]['type'],
|
||||
{
|
||||
tableName: TTableName;
|
||||
name: K;
|
||||
hasDefault: TFields[K]['schema'] extends { default: NonNullable<unknown> }
|
||||
hasDefault: TColumns[K]['schema'] extends { default: NonNullable<unknown> }
|
||||
? 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;
|
||||
}
|
||||
>;
|
||||
};
|
||||
|
|
|
@ -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();
|
||||
|
|
18
packages/db/test/fixtures/basics/astro.config.ts
vendored
18
packages/db/test/fixtures/basics/astro.config.ts
vendored
|
@ -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` }),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
10
packages/db/test/fixtures/glob/astro.config.ts
vendored
10
packages/db/test/fixtures/glob/astro.config.ts
vendored
|
@ -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 }),
|
||||
},
|
||||
});
|
||||
|
||||
|
|
10
packages/db/test/fixtures/glob/utils.ts
vendored
10
packages/db/test/fixtures/glob/utils.ts
vendored
|
@ -14,9 +14,9 @@ export function createGlob({ db, mode }: Pick<DBDataContext, 'db' | 'mode'>) {
|
|||
) {
|
||||
// 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<DBDataContext, 'db' | 'mode'>) {
|
|||
.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);
|
||||
|
|
|
@ -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({
|
||||
|
|
|
@ -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,
|
||||
}),
|
||||
},
|
||||
|
|
|
@ -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<FormState>(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<ComponentProps<'form'>, 'method' | 'onSubmit'>) {
|
||||
const formContext = context ?? useCreateFormContext(validator, fieldErrors);
|
||||
const formContext = context ?? useCreateFormContext(validator, columnErrors);
|
||||
|
||||
return (
|
||||
<FormContext.Provider value={formContext}>
|
||||
|
@ -80,7 +80,7 @@ export function Form({
|
|||
return formContext.trackAstroSubmitStatus();
|
||||
}
|
||||
|
||||
formContext.setValidationErrors(parsed.fieldErrors);
|
||||
formContext.setValidationErrors(parsed.columnErrors);
|
||||
}}
|
||||
>
|
||||
{name ? <input {...formNameInputProps} value={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 <Form> component?`
|
||||
);
|
||||
}
|
||||
|
||||
const { hasErroredOnce, validationErrors, validator } = fieldState;
|
||||
const { hasErroredOnce, validationErrors, validator } = columnState;
|
||||
return (
|
||||
<>
|
||||
<input
|
||||
onBlur={async (e) => {
|
||||
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}
|
||||
/>
|
||||
|
|
|
@ -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,
|
||||
},
|
||||
};
|
||||
|
||||
|
|
|
@ -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,
|
||||
|
|
|
@ -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],
|
||||
},
|
||||
],
|
||||
}),
|
||||
|
|
Loading…
Reference in a new issue