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