From dc0af089e6122ce1b9c71252cb6e7be7a2280d7f Mon Sep 17 00:00:00 2001 From: bholmesdev Date: Thu, 29 Feb 2024 16:26:12 -0500 Subject: [PATCH] chore: tidy up collection -> table error states --- packages/db/src/core/errors.ts | 24 +++++++++++ packages/db/src/core/types.ts | 19 ++++----- packages/db/src/runtime/queries.ts | 65 +++++++++++++++--------------- 3 files changed, 67 insertions(+), 41 deletions(-) diff --git a/packages/db/src/core/errors.ts b/packages/db/src/core/errors.ts index 3f97969274..4224cc72cf 100644 --- a/packages/db/src/core/errors.ts +++ b/packages/db/src/core/errors.ts @@ -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.`; +}; diff --git a/packages/db/src/core/types.ts b/packages/db/src/core/types.ts index e7cf047dcf..25aa06087c 100644 --- a/packages/db/src/core/types.ts +++ b/packages/db/src/core/types.ts @@ -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; @@ -255,7 +256,7 @@ export interface TableConfig columns: TColumns; foreignKeys?: Array<{ columns: MaybeArray>; - // 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>; }>; indexes?: Record>; diff --git a/packages/db/src/runtime/queries.ts b/packages/db/src/runtime/queries.ts index ed46cf4de7..381e41c368 100644 --- a/packages/db/src/runtime/queries.ts +++ b/packages/db/src/runtime/queries.ts @@ -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 -) { +export function getCreateIndexQueries(tableName: string, table: Pick) { 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; }