feat: add application productivity foundations
Quality / quality (ubuntu-latest) (push) Failing after 22s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-23 11:13:03 +05:30
parent 46195462c3
commit 64ab20cc95
42 changed files with 1533 additions and 40 deletions
+100
View File
@@ -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;
}