This commit is contained in:
admin
2026-09-03 22:02:52 +09:00
commit 659725de15
90 changed files with 2840 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
import { Database } from "bun:sqlite";
import { drizzle, type BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
import * as schema from "./schema";
import { initializeDatabase } from "./init-db";
import { ensureDatabaseDirectory, getDefaultDatabasePath } from "./paths";
import { seedDatabase } from "./seed-db";
export interface DatabaseContext {
db: BunSQLiteDatabase<typeof schema>;
sqlite: Database;
databasePath: string;
}
export async function createDatabaseContext(databasePath = getDefaultDatabasePath()) {
ensureDatabaseDirectory(databasePath);
const sqlite = new Database(databasePath, { create: true });
initializeDatabase(sqlite);
const db = drizzle(sqlite, { schema });
await seedDatabase(db);
return {
db,
sqlite,
databasePath
} satisfies DatabaseContext;
}
+122
View File
@@ -0,0 +1,122 @@
import type { Database } from "bun:sqlite";
function tableExists(sqlite: Database, tableName: string) {
return Boolean(sqlite.query("SELECT name FROM sqlite_master WHERE type = 'table' AND name = ?").get(tableName));
}
function columnExists(sqlite: Database, tableName: string, columnName: string) {
if (!tableExists(sqlite, tableName)) {
return false;
}
return sqlite
.query(`PRAGMA table_info(${tableName})`)
.all()
.some((column) => typeof column === "object" && column !== null && "name" in column && column.name === columnName);
}
function resetLegacySchema(sqlite: Database) {
sqlite.exec(`
PRAGMA foreign_keys = OFF;
DROP TABLE IF EXISTS sessions;
DROP TABLE IF EXISTS audit_events;
DROP TABLE IF EXISTS refunds;
DROP TABLE IF EXISTS order_items;
DROP TABLE IF EXISTS orders;
DROP TABLE IF EXISTS inventory;
DROP TABLE IF EXISTS products;
DROP TABLE IF EXISTS addresses;
DROP TABLE IF EXISTS users;
DROP TABLE IF EXISTS customers;
DROP TABLE IF EXISTS employees;
PRAGMA foreign_keys = ON;
`);
}
function needsLegacyReset(sqlite: Database) {
return tableExists(sqlite, "customers") || tableExists(sqlite, "employees") || columnExists(sqlite, "audit_events", "employee_id");
}
export function initializeDatabase(sqlite: Database) {
if (needsLegacyReset(sqlite)) {
resetLegacySchema(sqlite);
}
sqlite.exec(`
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS users (
id TEXT PRIMARY KEY,
username TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
email TEXT NOT NULL UNIQUE,
role TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS sessions (
id TEXT PRIMARY KEY,
user_id TEXT NOT NULL REFERENCES users(id),
created_at TEXT NOT NULL,
expires_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS addresses (
id TEXT PRIMARY KEY,
full_name TEXT NOT NULL,
line_1 TEXT NOT NULL,
line_2 TEXT,
city TEXT NOT NULL,
state TEXT NOT NULL,
postal_code TEXT NOT NULL,
country TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS products (
id TEXT PRIMARY KEY,
sku TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
price_cents INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS inventory (
product_id TEXT PRIMARY KEY REFERENCES products(id),
available_quantity INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS orders (
id TEXT PRIMARY KEY,
order_number TEXT NOT NULL UNIQUE,
customer_id TEXT NOT NULL REFERENCES users(id),
shipping_address_id TEXT NOT NULL REFERENCES addresses(id),
status TEXT NOT NULL,
created_at TEXT NOT NULL,
updated_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS order_items (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL REFERENCES orders(id),
product_id TEXT NOT NULL REFERENCES products(id),
quantity INTEGER NOT NULL,
unit_price_cents INTEGER NOT NULL
);
CREATE TABLE IF NOT EXISTS refunds (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL REFERENCES orders(id),
amount_cents INTEGER NOT NULL,
reason TEXT NOT NULL,
created_at TEXT NOT NULL
);
CREATE TABLE IF NOT EXISTS audit_events (
id TEXT PRIMARY KEY,
order_id TEXT NOT NULL REFERENCES orders(id),
actor TEXT NOT NULL,
type TEXT NOT NULL,
message TEXT NOT NULL,
created_at TEXT NOT NULL
);
`);
}
+15
View File
@@ -0,0 +1,15 @@
import { mkdirSync } from "node:fs";
import { dirname } from "node:path";
import { fileURLToPath } from "node:url";
export function getDefaultDatabasePath() {
return fileURLToPath(new URL("../../data/orderops.sqlite", import.meta.url));
}
export function ensureDatabaseDirectory(databasePath: string) {
if (databasePath === ":memory:") {
return;
}
mkdirSync(dirname(databasePath), { recursive: true });
}
+6
View File
@@ -0,0 +1,6 @@
import { createDatabaseContext } from "./database";
const context = await createDatabaseContext();
console.log(`Seeded ${context.databasePath}`);
context.sqlite.close();
+12
View File
@@ -0,0 +1,12 @@
import { text, sqliteTable } from "drizzle-orm/sqlite-core";
export const addresses = sqliteTable("addresses", {
id: text("id").primaryKey(),
fullName: text("full_name").notNull(),
line1: text("line_1").notNull(),
line2: text("line_2"),
city: text("city").notNull(),
state: text("state").notNull(),
postalCode: text("postal_code").notNull(),
country: text("country").notNull()
});
+11
View File
@@ -0,0 +1,11 @@
import { text, sqliteTable } from "drizzle-orm/sqlite-core";
import { orders } from "./orders";
export const auditEvents = sqliteTable("audit_events", {
id: text("id").primaryKey(),
orderId: text("order_id").notNull().references(() => orders.id),
actor: text("actor").notNull(),
type: text("type").notNull(),
message: text("message").notNull(),
createdAt: text("created_at").notNull()
});
+7
View File
@@ -0,0 +1,7 @@
import { text, sqliteTable } from "drizzle-orm/sqlite-core";
export const customers = sqliteTable("customers", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique()
});
+7
View File
@@ -0,0 +1,7 @@
import { text, sqliteTable } from "drizzle-orm/sqlite-core";
export const employees = sqliteTable("employees", {
id: text("id").primaryKey(),
name: text("name").notNull(),
email: text("email").notNull().unique()
});
+8
View File
@@ -0,0 +1,8 @@
export * from "./addresses";
export * from "./audit";
export * from "./inventory";
export * from "./orders";
export * from "./products";
export * from "./refunds";
export * from "./sessions";
export * from "./users";
+7
View File
@@ -0,0 +1,7 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { products } from "./products";
export const inventory = sqliteTable("inventory", {
productId: text("product_id").primaryKey().references(() => products.id),
availableQuantity: integer("available_quantity").notNull()
});
+22
View File
@@ -0,0 +1,22 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { addresses } from "./addresses";
import { products } from "./products";
import { users } from "./users";
export const orders = sqliteTable("orders", {
id: text("id").primaryKey(),
orderNumber: text("order_number").notNull().unique(),
customerId: text("customer_id").notNull().references(() => users.id),
shippingAddressId: text("shipping_address_id").notNull().references(() => addresses.id),
status: text("status").notNull(),
createdAt: text("created_at").notNull(),
updatedAt: text("updated_at").notNull()
});
export const orderItems = sqliteTable("order_items", {
id: text("id").primaryKey(),
orderId: text("order_id").notNull().references(() => orders.id),
productId: text("product_id").notNull().references(() => products.id),
quantity: integer("quantity").notNull(),
unitPriceCents: integer("unit_price_cents").notNull()
});
+8
View File
@@ -0,0 +1,8 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
export const products = sqliteTable("products", {
id: text("id").primaryKey(),
sku: text("sku").notNull().unique(),
name: text("name").notNull(),
priceCents: integer("price_cents").notNull()
});
+10
View File
@@ -0,0 +1,10 @@
import { integer, sqliteTable, text } from "drizzle-orm/sqlite-core";
import { orders } from "./orders";
export const refunds = sqliteTable("refunds", {
id: text("id").primaryKey(),
orderId: text("order_id").notNull().references(() => orders.id),
amountCents: integer("amount_cents").notNull(),
reason: text("reason").notNull(),
createdAt: text("created_at").notNull()
});
+9
View File
@@ -0,0 +1,9 @@
import { text, sqliteTable } from "drizzle-orm/sqlite-core";
import { users } from "./users";
export const sessions = sqliteTable("sessions", {
id: text("id").primaryKey(),
userId: text("user_id").notNull().references(() => users.id),
createdAt: text("created_at").notNull(),
expiresAt: text("expires_at").notNull()
});
+10
View File
@@ -0,0 +1,10 @@
import { text, sqliteTable } from "drizzle-orm/sqlite-core";
export const users = sqliteTable("users", {
id: text("id").primaryKey(),
username: text("username").notNull().unique(),
name: text("name").notNull(),
email: text("email").notNull().unique(),
role: text("role").notNull(),
createdAt: text("created_at").notNull()
});
+81
View File
@@ -0,0 +1,81 @@
import { count } from "drizzle-orm";
import type { BunSQLiteDatabase } from "drizzle-orm/bun-sqlite";
import * as schema from "./schema";
import { addresses, auditEvents, inventory, orderItems, orders, products, refunds, users } from "./schema";
const now = "2025-01-15T10:00:00.000Z";
export async function seedDatabase(db: BunSQLiteDatabase<typeof schema>) {
const [row] = await db.select({ value: count() }).from(orders);
if ((row?.value ?? 0) > 0) {
return;
}
await db.insert(users).values([
{ id: "user_cust_1", username: "john-smith", name: "John Smith", email: "john.smith@example.com", role: "customer", createdAt: now },
{ id: "user_cust_2", username: "jane-doe", name: "Jane Doe", email: "jane.doe@example.com", role: "customer", createdAt: now },
{ id: "user_cust_3", username: "priya-patel", name: "Priya Patel", email: "priya.patel@example.com", role: "customer", createdAt: now },
{ id: "user_cust_4", username: "marco-ruiz", name: "Marco Ruiz", email: "marco.ruiz@example.com", role: "customer", createdAt: now },
{ id: "user_cust_5", username: "evelyn-brooks", name: "Evelyn Brooks", email: "evelyn.brooks@example.com", role: "customer", createdAt: now },
{ id: "user_cust_6", username: "sana-khan", name: "Sana Khan", email: "sana.khan@example.com", role: "customer", createdAt: now }
]);
await db.insert(addresses).values([
{ id: "addr_1", fullName: "John Smith", line1: "123 Market Street", line2: null, city: "San Francisco", state: "CA", postalCode: "94105", country: "US" },
{ id: "addr_2", fullName: "Jane Doe", line1: "500 State Street", line2: "Suite 8", city: "Chicago", state: "IL", postalCode: "60601", country: "US" },
{ id: "addr_3", fullName: "Priya Patel", line1: "18 Lake Avenue", line2: null, city: "Seattle", state: "WA", postalCode: "98101", country: "US" },
{ id: "addr_4", fullName: "Marco Ruiz", line1: "240 Harbor Road", line2: null, city: "Miami", state: "FL", postalCode: "33101", country: "US" },
{ id: "addr_5", fullName: "Evelyn Brooks", line1: "88 Elm Street", line2: "Apt 4B", city: "Boston", state: "MA", postalCode: "02108", country: "US" },
{ id: "addr_6", fullName: "Sana Khan", line1: "12 Cedar Lane", line2: null, city: "Austin", state: "TX", postalCode: "73301", country: "US" }
]);
await db.insert(products).values([
{ id: "prod_1", sku: "TSHIRT-RED-M", name: "Red T-Shirt / M", priceCents: 2500 },
{ id: "prod_2", sku: "MUG-BLACK", name: "Black Mug", priceCents: 1800 },
{ id: "prod_3", sku: "CAP-NAVY", name: "Navy Cap", priceCents: 2200 },
{ id: "prod_4", sku: "NOTEBOOK-GRID", name: "Grid Notebook", priceCents: 1200 }
]);
await db.insert(inventory).values([
{ productId: "prod_1", availableQuantity: 6 },
{ productId: "prod_2", availableQuantity: 10 },
{ productId: "prod_3", availableQuantity: 3 },
{ productId: "prod_4", availableQuantity: 14 }
]);
await db.insert(orders).values([
{ id: "order_1", orderNumber: "ORD-1001", customerId: "user_cust_1", shippingAddressId: "addr_1", status: "pending", createdAt: now, updatedAt: now },
{ id: "order_2", orderNumber: "ORD-1002", customerId: "user_cust_2", shippingAddressId: "addr_2", status: "processing", createdAt: "2025-01-12T09:30:00.000Z", updatedAt: "2025-01-13T08:15:00.000Z" },
{ id: "order_3", orderNumber: "ORD-1003", customerId: "user_cust_1", shippingAddressId: "addr_1", status: "shipped", createdAt: "2025-01-10T07:00:00.000Z", updatedAt: "2025-01-11T12:00:00.000Z" },
{ id: "order_4", orderNumber: "ORD-1004", customerId: "user_cust_3", shippingAddressId: "addr_3", status: "pending", createdAt: "2025-01-09T14:20:00.000Z", updatedAt: "2025-01-09T14:20:00.000Z" },
{ id: "order_5", orderNumber: "ORD-1005", customerId: "user_cust_4", shippingAddressId: "addr_4", status: "processing", createdAt: "2025-01-08T11:45:00.000Z", updatedAt: "2025-01-09T08:10:00.000Z" },
{ id: "order_6", orderNumber: "ORD-1006", customerId: "user_cust_5", shippingAddressId: "addr_5", status: "shipped", createdAt: "2025-01-06T18:00:00.000Z", updatedAt: "2025-01-07T16:30:00.000Z" },
{ id: "order_7", orderNumber: "ORD-1007", customerId: "user_cust_6", shippingAddressId: "addr_6", status: "pending", createdAt: "2025-01-05T10:10:00.000Z", updatedAt: "2025-01-05T10:10:00.000Z" }
]);
await db.insert(orderItems).values([
{ id: "item_1", orderId: "order_1", productId: "prod_1", quantity: 2, unitPriceCents: 2500 },
{ id: "item_2", orderId: "order_1", productId: "prod_2", quantity: 1, unitPriceCents: 1800 },
{ id: "item_3", orderId: "order_2", productId: "prod_3", quantity: 1, unitPriceCents: 2200 },
{ id: "item_4", orderId: "order_3", productId: "prod_2", quantity: 2, unitPriceCents: 1800 },
{ id: "item_5", orderId: "order_4", productId: "prod_4", quantity: 3, unitPriceCents: 1200 },
{ id: "item_6", orderId: "order_5", productId: "prod_1", quantity: 1, unitPriceCents: 2500 },
{ id: "item_7", orderId: "order_6", productId: "prod_4", quantity: 2, unitPriceCents: 1200 },
{ id: "item_8", orderId: "order_7", productId: "prod_3", quantity: 1, unitPriceCents: 2200 }
]);
await db.insert(refunds).values([
{ id: "refund_1", orderId: "order_6", amountCents: 1200, reason: "Customer reported damaged item.", createdAt: "2025-01-14T15:45:00.000Z" }
]);
await db.insert(auditEvents).values([
{ id: "audit_1", orderId: "order_1", actor: "System Import", type: "order.created", message: "Order created from storefront checkout.", createdAt: now },
{ id: "audit_2", orderId: "order_2", actor: "Warehouse Sync", type: "order.processing", message: "Warehouse started packing order.", createdAt: "2025-01-13T08:15:00.000Z" },
{ id: "audit_3", orderId: "order_3", actor: "Carrier Sync", type: "order.shipped", message: "Order handed off to carrier.", createdAt: "2025-01-11T12:00:00.000Z" },
{ id: "audit_4", orderId: "order_4", actor: "System Import", type: "order.created", message: "Order created from storefront checkout.", createdAt: "2025-01-09T14:20:00.000Z" },
{ id: "audit_5", orderId: "order_5", actor: "Warehouse Sync", type: "order.processing", message: "Pick list sent to fulfillment.", createdAt: "2025-01-09T08:10:00.000Z" },
{ id: "audit_6", orderId: "order_6", actor: "Refund Batch", type: "refund.created", message: "Partial refund issued for damaged notebook.", createdAt: "2025-01-14T15:45:00.000Z" },
{ id: "audit_7", orderId: "order_7", actor: "System Import", type: "order.created", message: "Order created from storefront checkout.", createdAt: "2025-01-05T10:10:00.000Z" }
]);
}