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(). // This is specific to db.insert(). Prettify for seed().
return SEED_ERROR(tableName, `Empty array was passed. seed() must receive at least one value.`); 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 // Defined when `defineReadableTable()` is called
name: z.string().optional(), name: z.string().optional(),
// TODO: rename to `tableName`. Breaking schema change
collection: z.string().optional(), collection: z.string().optional(),
}); });
@ -186,19 +187,19 @@ export const tableSchema = z.object({
foreignKeys: z.array(foreignKeysSchema).optional(), 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 // Use `z.any()` to avoid breaking object references
const tables = z.record(z.any()).parse(rawCollections, { errorMap }); const tables = z.record(z.any()).parse(rawTables, { errorMap });
for (const [collectionName, collection] of Object.entries(tables)) { for (const [tableName, table] of Object.entries(tables)) {
// Append collection and column names to columns. // Append table and column names to columns.
// Used to track collection info for references. // Used to track table info for references.
const { columns } = z.object({ columns: z.record(z.any()) }).parse(collection, { errorMap }); const { columns } = z.object({ columns: z.record(z.any()) }).parse(table, { errorMap });
for (const [columnName, column] of Object.entries(columns)) { for (const [columnName, column] of Object.entries(columns)) {
column.schema.name = columnName; column.schema.name = columnName;
column.schema.collection = collectionName; column.schema.collection = tableName;
} }
} }
return rawCollections; return rawTables;
}, z.record(tableSchema)); }, z.record(tableSchema));
export type BooleanColumn = z.infer<typeof booleanColumnSchema>; export type BooleanColumn = z.infer<typeof booleanColumnSchema>;
@ -255,7 +256,7 @@ export interface TableConfig<TColumns extends ColumnsConfig = ColumnsConfig>
columns: TColumns; columns: TColumns;
foreignKeys?: Array<{ foreignKeys?: Array<{
columns: MaybeArray<Extract<keyof TColumns, string>>; 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>>; references: () => MaybeArray<z.input<typeof referenceableColumnSchema>>;
}>; }>;
indexes?: Record<string, IndexConfig<TColumns>>; 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 { SQLiteAsyncDialect } from 'drizzle-orm/sqlite-core';
import { hasPrimaryKey, type SqliteDB } from './index.js'; import { hasPrimaryKey, type SqliteDB } from './index.js';
import { isSerializedSQL } from './types.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(); const sqlite = new SQLiteAsyncDialect();
@ -61,10 +67,10 @@ export async function seedDev({
export async function recreateTables({ db, tables }: { db: SqliteDB; tables: DBTables }) { export async function recreateTables({ db, tables }: { db: SqliteDB; tables: DBTables }) {
const setupQueries: SQL[] = []; 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 dropQuery = sql.raw(`DROP TABLE IF EXISTS ${sqlite.escapeName(name)}`);
const createQuery = sql.raw(getCreateTableQuery(name, collection)); const createQuery = sql.raw(getCreateTableQuery(name, table));
const indexQueries = getCreateIndexQueries(name, collection); const indexQueries = getCreateIndexQueries(name, table);
setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s))); setupQueries.push(dropQuery, createQuery, ...indexQueries.map((s) => sql.raw(s)));
} }
await db.batch([ await db.batch([
@ -80,67 +86,64 @@ function seedErrorChecks(mode: 'dev' | 'build', tableName: string, values: Maybe
} }
} }
export function getCreateTableQuery(collectionName: string, collection: DBTable) { export function getCreateTableQuery(tableName: string, table: DBTable) {
let query = `CREATE TABLE ${sqlite.escapeName(collectionName)} (`; let query = `CREATE TABLE ${sqlite.escapeName(tableName)} (`;
const colQueries = []; const colQueries = [];
const colHasPrimaryKey = Object.entries(collection.columns).find(([, column]) => const colHasPrimaryKey = Object.entries(table.columns).find(([, column]) =>
hasPrimaryKey(column) 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.columns)) { for (const [columnName, column] of Object.entries(table.columns)) {
const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType( const colQuery = `${sqlite.escapeName(columnName)} ${schemaTypeToSqlType(
column.type column.type
)}${getModifiers(columnName, column)}`; )}${getModifiers(columnName, column)}`;
colQueries.push(colQuery); colQueries.push(colQuery);
} }
colQueries.push(...getCreateForeignKeyQueries(collectionName, collection)); colQueries.push(...getCreateForeignKeyQueries(tableName, table));
query += colQueries.join(', ') + ')'; query += colQueries.join(', ') + ')';
return query; return query;
} }
export function getCreateIndexQueries( export function getCreateIndexQueries(tableName: string, table: Pick<DBTable, 'indexes'>) {
collectionName: string,
collection: Pick<DBTable, 'indexes'>
) {
let queries: string[] = []; 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 onColNames = asArray(indexProps.on);
const onCols = onColNames.map((colName) => sqlite.escapeName(colName)); const onCols = onColNames.map((colName) => sqlite.escapeName(colName));
const unique = indexProps.unique ? 'UNIQUE ' : ''; const unique = indexProps.unique ? 'UNIQUE ' : '';
const indexQuery = `CREATE ${unique}INDEX ${sqlite.escapeName( const indexQuery = `CREATE ${unique}INDEX ${sqlite.escapeName(
indexName indexName
)} ON ${sqlite.escapeName(collectionName)} (${onCols.join(', ')})`; )} ON ${sqlite.escapeName(tableName)} (${onCols.join(', ')})`;
queries.push(indexQuery); queries.push(indexQuery);
} }
return queries; return queries;
} }
export function getCreateForeignKeyQueries(collectionName: string, collection: DBTable) { export function getCreateForeignKeyQueries(tableName: string, table: DBTable) {
let queries: string[] = []; let queries: string[] = [];
for (const foreignKey of collection.foreignKeys ?? []) { for (const foreignKey of table.foreignKeys ?? []) {
const columns = asArray(foreignKey.columns); const columns = asArray(foreignKey.columns);
const references = asArray(foreignKey.references); const references = asArray(foreignKey.references);
if (columns.length !== references.length) { if (columns.length !== references.length) {
throw new Error( throw new Error(FOREIGN_KEY_REFERENCES_LENGTH_ERROR(tableName));
`Foreign key on ${collectionName} is misconfigured. \`columns\` and \`references\` must be the same length.`
);
} }
const referencedCollection = references[0]?.schema.collection; const firstReference = references[0];
if (!referencedCollection) { if (!firstReference) {
throw new Error( throw new Error(FOREIGN_KEY_REFERENCES_EMPTY_ERROR(tableName));
`Foreign key on ${collectionName} is misconfigured. \`references\` cannot be empty.` }
); const referencedTable = firstReference.schema.collection;
if (!referencedTable) {
throw new Error(FOREIGN_KEY_DNE_ERROR(tableName));
} }
const query = `FOREIGN KEY (${columns 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(referencedTable)}(${references
.map((r) => sqlite.escapeName(r.schema.name!)) .map((r) => sqlite.escapeName(r.schema.name!))
.join(', ')})`; .join(', ')})`;
queries.push(query); queries.push(query);
@ -180,14 +183,12 @@ export function getModifiers(columnName: string, column: DBColumn) {
} }
const references = getReferencesConfig(column); const references = getReferencesConfig(column);
if (references) { if (references) {
const { collection, name } = references.schema; const { collection: tableName, name } = references.schema;
if (!collection || !name) { if (!tableName || !name) {
throw new Error( throw new Error(REFERENCE_DNE_ERROR(columnName));
`Column ${collection}.${name} references a collection that does not exist. Did you apply the referenced collection to the \`tables\` object in your Astro config?`
);
} }
modifiers += ` REFERENCES ${sqlite.escapeName(collection)} (${sqlite.escapeName(name)})`; modifiers += ` REFERENCES ${sqlite.escapeName(tableName)} (${sqlite.escapeName(name)})`;
} }
return modifiers; return modifiers;
} }