feat: add application productivity foundations
This commit is contained in:
@@ -7,6 +7,11 @@ export interface AddSeedOptions {
|
||||
conflict?: "error" | "ignore" | "replace";
|
||||
}
|
||||
|
||||
export interface UpsertSeedOptions {
|
||||
key: string | readonly string[];
|
||||
update?: readonly string[];
|
||||
}
|
||||
|
||||
function identifier(value: string, label: string): string {
|
||||
if (!/^[A-Za-z_][A-Za-z0-9_]*$/.test(value)) {
|
||||
throw new TypeError(`${label} must be a safe SQL identifier`);
|
||||
@@ -81,3 +86,98 @@ export function runSeedQuery(
|
||||
if (!query.trim()) throw new TypeError("seed query cannot be empty");
|
||||
return db.exec(query, [...data]);
|
||||
}
|
||||
|
||||
/** Portable object upsert for SQLite, PostgreSQL and MySQL. */
|
||||
export async function upsertSeedData(
|
||||
source: SeedDatabase,
|
||||
data: SeedRow | readonly SeedRow[],
|
||||
table: string,
|
||||
options: UpsertSeedOptions,
|
||||
): Promise<ExecResult[]> {
|
||||
const db = database(source);
|
||||
const target = identifier(table, "seed table");
|
||||
const keys = (Array.isArray(options.key) ? options.key : [options.key]).map((key) =>
|
||||
identifier(key, "seed key"),
|
||||
);
|
||||
if (!keys.length) throw new TypeError("upsertSeedData requires at least one key");
|
||||
const results: ExecResult[] = [];
|
||||
for (const row of Array.isArray(data) ? data : [data]) {
|
||||
const entries = Object.entries(row);
|
||||
if (!entries.length) throw new TypeError("seed row cannot be empty");
|
||||
const columns = entries.map(([column]) => identifier(column, "seed column"));
|
||||
for (const key of keys)
|
||||
if (!columns.includes(key)) throw new TypeError(`seed row is missing key '${key}'`);
|
||||
const updates = (options.update ?? columns.filter((column) => !keys.includes(column))).map(
|
||||
(column) => identifier(column, "seed update column"),
|
||||
);
|
||||
const prefix = db.driver.dialect === "mysql" ? "INSERT INTO" : "INSERT INTO";
|
||||
const suffix =
|
||||
db.driver.dialect === "mysql"
|
||||
? ` ON DUPLICATE KEY UPDATE ${updates.map((column) => `${column} = VALUES(${column})`).join(", ")}`
|
||||
: ` ON CONFLICT (${keys.join(", ")}) DO ${
|
||||
updates.length
|
||||
? `UPDATE SET ${updates.map((column) => `${column} = excluded.${column}`).join(", ")}`
|
||||
: "NOTHING"
|
||||
}`;
|
||||
results.push(
|
||||
await db.exec(
|
||||
`${prefix} ${target} (${columns.join(", ")}) VALUES (${columns.map(() => "?").join(", ")})${suffix}`,
|
||||
entries.map(([, value]) => value),
|
||||
),
|
||||
);
|
||||
}
|
||||
return results;
|
||||
}
|
||||
|
||||
export async function seedIfMissing(
|
||||
source: SeedDatabase,
|
||||
table: string,
|
||||
match: SeedRow,
|
||||
data: SeedRow,
|
||||
): Promise<boolean> {
|
||||
const db = database(source);
|
||||
const target = identifier(table, "seed table");
|
||||
const where = Object.entries(match);
|
||||
if (!where.length) throw new TypeError("seedIfMissing requires match fields");
|
||||
const existing = await db.one(
|
||||
`SELECT 1 AS found FROM ${target} WHERE ${where
|
||||
.map(([column]) => `${identifier(column, "seed column")} = ?`)
|
||||
.join(" AND ")} LIMIT 1`,
|
||||
where.map(([, value]) => value),
|
||||
);
|
||||
if (existing) return false;
|
||||
await addSeedData(db, data, target);
|
||||
return true;
|
||||
}
|
||||
|
||||
export type SeedStep = (db: Db) => void | Promise<void>;
|
||||
|
||||
export function defineSeed(...steps: readonly SeedStep[]): SeedStep {
|
||||
return async (db) =>
|
||||
db.tx(async (transaction) => {
|
||||
for (const step of steps) await step(transaction);
|
||||
});
|
||||
}
|
||||
|
||||
export interface SeedUserAccount {
|
||||
identifier: string;
|
||||
password?: string;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
export async function seedUsers<T extends SeedUserAccount, U extends { id: string }>(
|
||||
accounts: readonly T[],
|
||||
options: {
|
||||
find(identifier: string): Promise<U | null>;
|
||||
create(account: T): Promise<U>;
|
||||
configure?(user: U, account: T): void | Promise<void>;
|
||||
},
|
||||
): Promise<U[]> {
|
||||
const users: U[] = [];
|
||||
for (const account of accounts) {
|
||||
const user = (await options.find(account.identifier)) ?? (await options.create(account));
|
||||
await options.configure?.(user, account);
|
||||
users.push(user);
|
||||
}
|
||||
return users;
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user