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:
parent
0b9d9636e9
commit
dc0af089e6
3 changed files with 67 additions and 41 deletions
|
@ -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.`;
|
||||||
|
};
|
||||||
|
|
|
@ -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>>;
|
||||||
|
|
|
@ -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;
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue