init
This commit is contained in:
@@ -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;
|
||||
}
|
||||
@@ -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
|
||||
);
|
||||
`);
|
||||
}
|
||||
@@ -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 });
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
import { createDatabaseContext } from "./database";
|
||||
|
||||
const context = await createDatabaseContext();
|
||||
|
||||
console.log(`Seeded ${context.databasePath}`);
|
||||
context.sqlite.close();
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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";
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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()
|
||||
});
|
||||
@@ -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" }
|
||||
]);
|
||||
}
|
||||
Reference in New Issue
Block a user