0
Fork 0
mirror of https://github.com/withastro/astro.git synced 2025-02-24 22:46:02 -05:00

chore: tidy up collection -> table error states

This commit is contained in:
bholmesdev 2024-02-29 16:26:12 -05:00
parent 0b9d9636e9
commit dc0af089e6
3 changed files with 67 additions and 41 deletions

View file

@ -30,3 +30,27 @@ export const SEED_EMPTY_ARRAY_ERROR = (tableName: string) => {
// This is specific to db.insert(). Prettify for seed().
return SEED_ERROR(tableName, `Empty array was passed. seed() must receive at least one value.`);
};
export const REFERENCE_DNE_ERROR = (columnName: string) => {
return `Column ${bold(
columnName
)} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`;
};
export const FOREIGN_KEY_DNE_ERROR = (tableName: string) => {
return `Table ${bold(
tableName
)} references a table that does not exist. Did you apply the referenced table to the \`tables\` object in your db config?`;
};
export const FOREIGN_KEY_REFERENCES_LENGTH_ERROR = (tableName: string) => {
return `Foreign key on ${bold(
tableName
)} is misconfigured. \`columns\` and \`references\` must be the same length.`;
};
export const FOREIGN_KEY_REFERENCES_EMPTY_ERROR = (tableName: string) => {
return `Foreign key on ${bold(
tableName
)} is misconfigured. \`references\` must be a function that returns a column or array of columns.`;
};

View file

@ -25,6 +25,7 @@ const baseColumnSchema = z.object({
// Defined when `defineReadableTable()` is called
name: z.string().optional(),
// TODO: rename to `tableName`. Breaking schema change
collection: z.string().optional(),
});
@ -186,19 +187,19 @@ export const tableSchema = z.object({
foreignKeys: z.array(foreignKeysSchema).optional(),
});
export const tablesSchema = z.preprocess((rawCollections) => {
export const tablesSchema = z.preprocess((rawTables) => {
// Use `z.any()` to avoid breaking object references
const tables = z.record(z.any()).parse(rawCollections, { errorMap });
for (const [collectionName, collection] of Object.entries(tables)) {
// Append collection and column names to columns.
// Used to track collection info for references.
const { columns } = z.object({ columns: z.record(z.any()) }).parse(collection, { errorMap });
const tables = z.record(z.any()).parse(rawTables, { errorMap });
for (const [tableName, table] of Object.entries(tables)) {
// Append table and column names to columns.
// Used to track table info for references.
const { columns } = z.object({ columns: z.record(z.any()) }).parse(table, { errorMap });
for (const [columnName, column] of Object.entries(columns)) {
column.schema.name = columnName;
column.schema.collection = collectionName;
column.schema.collection = tableName;
}
}
return rawCollections;
return rawTables;
}, z.record(tableSchema));
export type BooleanColumn = z.infer<typeof booleanColumnSchema>;
@ -255,7 +256,7 @@ export interface TableConfig<TColumns extends ColumnsConfig = ColumnsConfig>
columns: TColumns;
foreignKeys?: Array<{
columns: MaybeArray<Extract<keyof TColumns, string>>;
// TODO: runtime error if parent collection doesn't match for all columns. Can't put a generic here...
// TODO: runtime error if parent table doesn't match for all columns. Can't put a generic here...
references: () => MaybeArray<z.input<typeof referenceableColumnSchema>>;
}>;
indexes?: Record<string, IndexConfig<TColumns>>;

View file

@ -15,7 +15,13 @@ import { type SQL, sql } from 'drizzle-orm';
import { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
import { hasPrimaryKey, type SqliteDB } from './index.js';
import { isSerializedSQL } from './types.js';
import { SEED_EMPTY_ARRAY_ERROR } from '../core/errors.js';
import {
FOREIGN_KEY_REFERENCES_LENGTH_ERROR,
FOREIGN_KEY_REFERENCES_EMPTY_ERROR,
REFERENCE_DNE_ERROR,
SEED_EMPTY_ARRAY_ERROR,
FOREIGN_KEY_DNE_ERROR,
} from '../core/errors.js';
const sqlite = new SQLiteAsyncDialect();
@ -61,10 +67,10 @@ export async function seedDev({
export async function recreateTables({ db, tables }: { db: SqliteDB; tables: DBTables }) {
const setupQueries: SQL[] = [];
for (const [name, collection] of Object.entries(tables)) {
for (const [name, table] of Object.entries(tables)) {
const dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`);
const createQuery = sql.raw(getCreateTableQuery(name, collection));
const indexQueries = getCreateIndexQueries(name, collection);
const createQuery = sql.raw(getCreateTableQuery(name, table));
const indexQueries = getCreateIndexQueries(name, table);
setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s)));
}
await db.batch([
@ -80,67 +86,64 @@ function seedErrorChecks(mode: 'dev' | 'build', tableName: string, values: Maybe
}
}
export function getCreateTableQuery(collectionName: string, collection: DBTable) {
let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`;
export function getCreateTableQuery(tableName: string, table: DBTable) {
let query = `CREATE TABLE ${sqlite.escapeName(tableName)} (`;
const colQueries = [];
const colHasPrimaryKey = Object.entries(collection.columns).find(([, column]) =>
const colHasPrimaryKey = Object.entries(table.columns).find(([, column]) =>
hasPrimaryKey(column)
);
if (!colHasPrimaryKey) {
colQueries.push('_id INTEGER PRIMARY KEY');
}
for (const [columnName, column] of Object.entries(collection.columns)) {
for (const [columnName, column] of Object.entries(table.columns)) {
const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
column.type
)}${getModifiers(columnName, column)}`;
colQueries.push(colQuery);
}
colQueries.push(...getCreateForeignKeyQueries(collectionName, collection));
colQueries.push(...getCreateForeignKeyQueries(tableName, table));
query += colQueries.join(', ') + ')';
return query;
}
export function getCreateIndexQueries(
collectionName: string,
collection: Pick<DBTable, 'indexes'>
) {
export function getCreateIndexQueries(tableName: string, table: Pick<DBTable, 'indexes'>) {
let queries: string[] = [];
for (const [indexName, indexProps] of Object.entries(collection.indexes ?? {})) {
for (const [indexName, indexProps] of Object.entries(table.indexes ?? {})) {
const onColNames = asArray(indexProps.on);
const onCols = onColNames.map((colName) => sqlite.escapeName(colName));
const unique = indexProps.unique ? 'UNIQUE ' : '';
const indexQuery = `CREATE ${unique}INDEX ${sqlite.escapeName(
indexName
)} ON ${sqlite.escapeName(collectionName)} (${onCols.join(', ')})`;
)} ON ${sqlite.escapeName(tableName)} (${onCols.join(', ')})`;
queries.push(indexQuery);
}
return queries;
}
export function getCreateForeignKeyQueries(collectionName: string, collection: DBTable) {
export function getCreateForeignKeyQueries(tableName: string, table: DBTable) {
let queries: string[] = [];
for (const foreignKey of collection.foreignKeys ?? []) {
for (const foreignKey of table.foreignKeys ?? []) {
const columns = asArray(foreignKey.columns);
const references = asArray(foreignKey.references);
if (columns.length !== references.length) {
throw new Error(
`Foreign key on ${collectionName} is misconfigured. \`columns\` and \`references\` must be the same length.`
);
throw new Error(FOREIGN_KEY_REFERENCES_LENGTH_ERROR(tableName));
}
const referencedCollection = references[0]?.schema.collection;
if (!referencedCollection) {
throw new Error(
`Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.`
);
const firstReference = references[0];
if (!firstReference) {
throw new Error(FOREIGN_KEY_REFERENCES_EMPTY_ERROR(tableName));
}
const referencedTable = firstReference.schema.collection;
if (!referencedTable) {
throw new Error(FOREIGN_KEY_DNE_ERROR(tableName));
}
const query = `FOREIGN KEY (${columns
.map((f) => sqlite.escapeName(f))
.join(', ')}) REFERENCES ${sqlite.escapeName(referencedCollection)}(${references
.join(', ')}) REFERENCES ${sqlite.escapeName(referencedTable)}(${references
.map((r) => sqlite.escapeName(r.schema.name!))
.join(', ')})`;
queries.push(query);
@ -180,14 +183,12 @@ export function getModifiers(columnName: string, column: DBColumn) {
}
const references = getReferencesConfig(column);
if (references) {
const { collection, name } = references.schema;
if (!collection || !name) {
throw new Error(
`Column ${collection}.${name} references a collection that does not exist. Did you apply the referenced collection to the \`tables\` object in your Astro config?`
);
const { collection: tableName, name } = references.schema;
if (!tableName || !name) {
throw new Error(REFERENCE_DNE_ERROR(columnName));
}
modifiers += ` REFERENCES ${sqlite.escapeName(collection)} (${sqlite.escapeName(name)})`;
modifiers += ` REFERENCES ${sqlite.escapeName(tableName)} (${sqlite.escapeName(name)})`;
}
return modifiers;
}