mirror of
https://github.com/soconnor0919/beenvoice.git
synced 2026-05-08 09:38:55 -04:00
494 lines
16 KiB
TypeScript
494 lines
16 KiB
TypeScript
import { relations, sql } from "drizzle-orm";
|
|
import { index, pgTableCreator } from "drizzle-orm/pg-core";
|
|
|
|
/**
|
|
* This is an example of how to use the multi-project schema feature of Drizzle ORM. Use the same
|
|
* database instance for multiple projects.
|
|
*
|
|
* @see https://orm.drizzle.team/docs/goodies#multi-project-schema
|
|
*/
|
|
export const createTable = pgTableCreator((name) => `beenvoice_${name}`);
|
|
|
|
// Auth-related tables (keeping existing)
|
|
export const users = createTable("user", (d) => ({
|
|
id: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
name: d.varchar({ length: 255 }).notNull(),
|
|
email: d.varchar({ length: 255 }).notNull().unique(),
|
|
emailVerified: d.boolean().default(false).notNull(),
|
|
image: d.varchar({ length: 255 }),
|
|
createdAt: d.timestamp().notNull().defaultNow(),
|
|
updatedAt: d
|
|
.timestamp()
|
|
.notNull()
|
|
.defaultNow()
|
|
.$onUpdate(() => new Date()),
|
|
password: d.varchar({ length: 255 }), // Matched DB: varchar(255)
|
|
resetToken: d.varchar({ length: 255 }), // Matched DB: varchar(255)
|
|
resetTokenExpiry: d.timestamp(),
|
|
// Custom fields
|
|
prefersReducedMotion: d.boolean().default(false).notNull(),
|
|
animationSpeedMultiplier: d.real().default(1).notNull(),
|
|
colorTheme: d.varchar({ length: 50 }).default("slate").notNull(),
|
|
customColor: d.varchar({ length: 50 }),
|
|
theme: d.varchar({ length: 20 }).default("system").notNull(),
|
|
interfaceTheme: d.varchar({ length: 50 }).default("beenvoice").notNull(),
|
|
fontPreference: d.varchar({ length: 50 }).default("brand").notNull(),
|
|
bodyFontPreference: d.varchar({ length: 50 }).default("brand").notNull(),
|
|
headingFontPreference: d.varchar({ length: 50 }).default("brand").notNull(),
|
|
radiusPreference: d.varchar({ length: 20 }).default("xl").notNull(),
|
|
sidebarStyle: d.varchar({ length: 20 }).default("floating").notNull(),
|
|
role: d.varchar({ length: 20 }).default("user").notNull(),
|
|
}));
|
|
|
|
export const platformSettings = createTable("platform_setting", (d) => ({
|
|
id: d.varchar({ length: 50 }).notNull().primaryKey().default("global"),
|
|
brandName: d.varchar({ length: 100 }).default("beenvoice").notNull(),
|
|
brandTagline: d
|
|
.varchar({ length: 255 })
|
|
.default(
|
|
"Simple and efficient invoicing for freelancers and small businesses",
|
|
)
|
|
.notNull(),
|
|
brandLogoText: d.varchar({ length: 100 }).default("beenvoice").notNull(),
|
|
brandIcon: d.varchar({ length: 20 }).default("$").notNull(),
|
|
colorTheme: d.varchar({ length: 50 }).default("slate").notNull(),
|
|
customColor: d.varchar({ length: 50 }),
|
|
theme: d.varchar({ length: 20 }).default("system").notNull(),
|
|
interfaceTheme: d.varchar({ length: 50 }).default("beenvoice").notNull(),
|
|
bodyFontPreference: d.varchar({ length: 50 }).default("brand").notNull(),
|
|
headingFontPreference: d.varchar({ length: 50 }).default("brand").notNull(),
|
|
radiusPreference: d.varchar({ length: 20 }).default("xl").notNull(),
|
|
sidebarStyle: d.varchar({ length: 20 }).default("floating").notNull(),
|
|
pdfTemplate: d.varchar({ length: 20 }).default("classic").notNull(),
|
|
pdfAccentColor: d.varchar({ length: 50 }).default("#111827").notNull(),
|
|
pdfFooterText: d
|
|
.varchar({ length: 120 })
|
|
.default("Professional Invoicing")
|
|
.notNull(),
|
|
pdfShowLogo: d.boolean().default(true).notNull(),
|
|
pdfShowPageNumbers: d.boolean().default(true).notNull(),
|
|
createdAt: d.timestamp().notNull().defaultNow(),
|
|
updatedAt: d
|
|
.timestamp()
|
|
.notNull()
|
|
.defaultNow()
|
|
.$onUpdate(() => new Date()),
|
|
}));
|
|
|
|
export const usersRelations = relations(users, ({ many }) => ({
|
|
accounts: many(accounts),
|
|
clients: many(clients),
|
|
businesses: many(businesses),
|
|
invoices: many(invoices),
|
|
sessions: many(sessions),
|
|
expenses: many(expenses),
|
|
invoiceTemplates: many(invoiceTemplates),
|
|
}));
|
|
|
|
export const accounts = createTable(
|
|
"account",
|
|
(d) => ({
|
|
id: d
|
|
.text()
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()), // Matched DB: text
|
|
userId: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => users.id),
|
|
accountId: d.varchar({ length: 255 }).notNull(),
|
|
providerId: d.varchar({ length: 255 }).notNull(),
|
|
accessToken: d.text(),
|
|
refreshToken: d.text(),
|
|
accessTokenExpiresAt: d.timestamp(),
|
|
refreshTokenExpiresAt: d.timestamp(),
|
|
scope: d.varchar({ length: 255 }),
|
|
idToken: d.text(),
|
|
password: d.text(), // Matched DB: text
|
|
createdAt: d.timestamp().notNull().defaultNow(),
|
|
updatedAt: d
|
|
.timestamp()
|
|
.notNull()
|
|
.defaultNow()
|
|
.$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [index("account_userId_idx").on(t.userId)],
|
|
);
|
|
|
|
export const accountsRelations = relations(accounts, ({ one }) => ({
|
|
user: one(users, { fields: [accounts.userId], references: [users.id] }),
|
|
}));
|
|
|
|
export const sessions = createTable(
|
|
"session",
|
|
(d) => ({
|
|
id: d
|
|
.text()
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()), // Matched DB: text
|
|
userId: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => users.id),
|
|
token: d.varchar({ length: 255 }).notNull().unique(),
|
|
expiresAt: d.timestamp().notNull(),
|
|
ipAddress: d.text(), // Matched DB: text
|
|
userAgent: d.text(), // Matched DB: text
|
|
createdAt: d.timestamp().notNull().defaultNow(),
|
|
updatedAt: d
|
|
.timestamp()
|
|
.notNull()
|
|
.defaultNow()
|
|
.$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [index("session_userId_idx").on(t.userId)],
|
|
);
|
|
|
|
export const sessionsRelations = relations(sessions, ({ one }) => ({
|
|
user: one(users, { fields: [sessions.userId], references: [users.id] }),
|
|
}));
|
|
|
|
export const verificationTokens = createTable(
|
|
"verification_token",
|
|
(d) => ({
|
|
id: d
|
|
.text()
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()), // Matched DB: text
|
|
identifier: d.varchar({ length: 255 }).notNull(),
|
|
value: d.varchar({ length: 255 }).notNull(),
|
|
expiresAt: d.timestamp().notNull(),
|
|
createdAt: d.timestamp().notNull().defaultNow(),
|
|
updatedAt: d
|
|
.timestamp()
|
|
.notNull()
|
|
.defaultNow()
|
|
.$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [index("verification_token_identifier_idx").on(t.identifier)],
|
|
);
|
|
|
|
export const ssoProviders = createTable(
|
|
"sso_provider",
|
|
(d) => ({
|
|
id: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
providerId: d.varchar({ length: 255 }).notNull().unique(),
|
|
userId: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => users.id),
|
|
redirectURI: d.varchar({ length: 255 }).notNull().default(""), // Added detailed fields
|
|
oidcConfig: d.text(),
|
|
samlConfig: d.text(),
|
|
createdAt: d.timestamp().notNull().defaultNow(),
|
|
updatedAt: d
|
|
.timestamp()
|
|
.notNull()
|
|
.defaultNow()
|
|
.$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [index("sso_provider_user_id_idx").on(t.userId)],
|
|
);
|
|
|
|
// Invoicing app tables
|
|
export const clients = createTable(
|
|
"client",
|
|
(d) => ({
|
|
id: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
name: d.varchar({ length: 255 }).notNull(),
|
|
email: d.varchar({ length: 255 }),
|
|
phone: d.varchar({ length: 50 }),
|
|
addressLine1: d.varchar({ length: 255 }),
|
|
addressLine2: d.varchar({ length: 255 }),
|
|
city: d.varchar({ length: 100 }),
|
|
state: d.varchar({ length: 50 }),
|
|
postalCode: d.varchar({ length: 20 }),
|
|
country: d.varchar({ length: 100 }),
|
|
defaultHourlyRate: d.real(),
|
|
currency: d.varchar({ length: 3 }).default("USD").notNull(),
|
|
createdById: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => users.id),
|
|
createdAt: d
|
|
.timestamp()
|
|
.default(sql`CURRENT_TIMESTAMP`)
|
|
.notNull(),
|
|
updatedAt: d.timestamp().$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [
|
|
index("client_created_by_idx").on(t.createdById),
|
|
index("client_name_idx").on(t.name),
|
|
index("client_email_idx").on(t.email),
|
|
],
|
|
);
|
|
|
|
export const clientsRelations = relations(clients, ({ one, many }) => ({
|
|
createdBy: one(users, {
|
|
fields: [clients.createdById],
|
|
references: [users.id],
|
|
}),
|
|
invoices: many(invoices),
|
|
}));
|
|
|
|
export const businesses = createTable(
|
|
"business",
|
|
(d) => ({
|
|
id: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
name: d.varchar({ length: 255 }).notNull(),
|
|
nickname: d.varchar({ length: 255 }),
|
|
email: d.varchar({ length: 255 }),
|
|
phone: d.varchar({ length: 50 }),
|
|
addressLine1: d.varchar({ length: 255 }),
|
|
addressLine2: d.varchar({ length: 255 }),
|
|
city: d.varchar({ length: 100 }),
|
|
state: d.varchar({ length: 50 }),
|
|
postalCode: d.varchar({ length: 20 }),
|
|
country: d.varchar({ length: 100 }),
|
|
website: d.varchar({ length: 255 }),
|
|
taxId: d.varchar({ length: 100 }),
|
|
logoUrl: d.varchar({ length: 500 }),
|
|
isDefault: d.boolean().default(false),
|
|
// Email configuration for custom Resend setup
|
|
resendApiKey: d.varchar({ length: 255 }),
|
|
resendDomain: d.varchar({ length: 255 }),
|
|
emailFromName: d.varchar({ length: 255 }),
|
|
createdById: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => users.id),
|
|
createdAt: d
|
|
.timestamp()
|
|
.default(sql`CURRENT_TIMESTAMP`)
|
|
.notNull(),
|
|
updatedAt: d.timestamp().$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [
|
|
index("business_created_by_idx").on(t.createdById),
|
|
index("business_name_idx").on(t.name),
|
|
index("business_nickname_idx").on(t.nickname),
|
|
index("business_email_idx").on(t.email),
|
|
index("business_is_default_idx").on(t.isDefault),
|
|
],
|
|
);
|
|
|
|
export const businessesRelations = relations(businesses, ({ one, many }) => ({
|
|
createdBy: one(users, {
|
|
fields: [businesses.createdById],
|
|
references: [users.id],
|
|
}),
|
|
invoices: many(invoices),
|
|
}));
|
|
|
|
export const invoices = createTable(
|
|
"invoice",
|
|
(d) => ({
|
|
id: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
invoiceNumber: d.varchar({ length: 100 }).notNull(),
|
|
invoicePrefix: d.varchar({ length: 20 }).default("#"),
|
|
businessId: d.varchar({ length: 255 }).references(() => businesses.id),
|
|
clientId: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => clients.id),
|
|
issueDate: d.timestamp().notNull(),
|
|
dueDate: d.timestamp().notNull(),
|
|
status: d.varchar({ length: 50 }).notNull().default("draft"), // draft, sent, paid (overdue computed)
|
|
totalAmount: d.real().notNull().default(0),
|
|
taxRate: d.real().notNull().default(0.0),
|
|
notes: d.varchar({ length: 1000 }),
|
|
emailMessage: d.varchar({ length: 2000 }),
|
|
currency: d.varchar({ length: 3 }).default("USD").notNull(),
|
|
createdById: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => users.id),
|
|
createdAt: d
|
|
.timestamp()
|
|
.default(sql`CURRENT_TIMESTAMP`)
|
|
.notNull(),
|
|
updatedAt: d.timestamp().$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [
|
|
index("invoice_business_id_idx").on(t.businessId),
|
|
index("invoice_client_id_idx").on(t.clientId),
|
|
index("invoice_created_by_idx").on(t.createdById),
|
|
index("invoice_number_idx").on(t.invoiceNumber),
|
|
index("invoice_status_idx").on(t.status),
|
|
],
|
|
);
|
|
|
|
export const invoicesRelations = relations(invoices, ({ one, many }) => ({
|
|
business: one(businesses, {
|
|
fields: [invoices.businessId],
|
|
references: [businesses.id],
|
|
}),
|
|
client: one(clients, {
|
|
fields: [invoices.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
createdBy: one(users, {
|
|
fields: [invoices.createdById],
|
|
references: [users.id],
|
|
}),
|
|
items: many(invoiceItems),
|
|
}));
|
|
|
|
export const invoiceItems = createTable(
|
|
"invoice_item",
|
|
(d) => ({
|
|
id: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
invoiceId: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => invoices.id, { onDelete: "cascade" }),
|
|
date: d.timestamp().notNull(),
|
|
description: d.varchar({ length: 500 }).notNull(),
|
|
hours: d.real().notNull(),
|
|
rate: d.real().notNull(),
|
|
amount: d.real().notNull(),
|
|
position: d.integer().notNull().default(0), // NEW: position for ordering
|
|
createdAt: d
|
|
.timestamp()
|
|
.default(sql`CURRENT_TIMESTAMP`)
|
|
.notNull(),
|
|
}),
|
|
(t) => [
|
|
index("invoice_item_invoice_id_idx").on(t.invoiceId),
|
|
index("invoice_item_date_idx").on(t.date),
|
|
index("invoice_item_position_idx").on(t.position), // NEW: index for position
|
|
],
|
|
);
|
|
|
|
export const invoiceItemsRelations = relations(invoiceItems, ({ one }) => ({
|
|
invoice: one(invoices, {
|
|
fields: [invoiceItems.invoiceId],
|
|
references: [invoices.id],
|
|
}),
|
|
}));
|
|
|
|
export const expenses = createTable(
|
|
"expense",
|
|
(d) => ({
|
|
id: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
businessId: d.varchar({ length: 255 }).references(() => businesses.id),
|
|
clientId: d.varchar({ length: 255 }).references(() => clients.id),
|
|
invoiceId: d
|
|
.varchar({ length: 255 })
|
|
.references(() => invoices.id, { onDelete: "set null" }),
|
|
date: d.timestamp().notNull(),
|
|
description: d.varchar({ length: 500 }).notNull(),
|
|
amount: d.real().notNull(),
|
|
currency: d.varchar({ length: 3 }).default("USD").notNull(),
|
|
category: d.varchar({ length: 100 }),
|
|
billable: d.boolean().default(false).notNull(),
|
|
reimbursable: d.boolean().default(false).notNull(),
|
|
taxDeductible: d.boolean().default(false).notNull(),
|
|
notes: d.varchar({ length: 500 }),
|
|
createdById: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => users.id),
|
|
createdAt: d
|
|
.timestamp()
|
|
.default(sql`CURRENT_TIMESTAMP`)
|
|
.notNull(),
|
|
updatedAt: d.timestamp().$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [
|
|
index("expense_created_by_idx").on(t.createdById),
|
|
index("expense_client_id_idx").on(t.clientId),
|
|
index("expense_invoice_id_idx").on(t.invoiceId),
|
|
index("expense_date_idx").on(t.date),
|
|
index("expense_billable_idx").on(t.billable),
|
|
],
|
|
);
|
|
|
|
export const expensesRelations = relations(expenses, ({ one }) => ({
|
|
business: one(businesses, {
|
|
fields: [expenses.businessId],
|
|
references: [businesses.id],
|
|
}),
|
|
client: one(clients, {
|
|
fields: [expenses.clientId],
|
|
references: [clients.id],
|
|
}),
|
|
invoice: one(invoices, {
|
|
fields: [expenses.invoiceId],
|
|
references: [invoices.id],
|
|
}),
|
|
createdBy: one(users, {
|
|
fields: [expenses.createdById],
|
|
references: [users.id],
|
|
}),
|
|
}));
|
|
|
|
export const invoiceTemplates = createTable(
|
|
"invoice_template",
|
|
(d) => ({
|
|
id: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.primaryKey()
|
|
.$defaultFn(() => crypto.randomUUID()),
|
|
name: d.varchar({ length: 255 }).notNull(),
|
|
type: d.varchar({ length: 50 }).notNull().default("notes"), // "notes" | "terms"
|
|
content: d.text().notNull(),
|
|
isDefault: d.boolean().default(false).notNull(),
|
|
createdById: d
|
|
.varchar({ length: 255 })
|
|
.notNull()
|
|
.references(() => users.id),
|
|
createdAt: d
|
|
.timestamp()
|
|
.default(sql`CURRENT_TIMESTAMP`)
|
|
.notNull(),
|
|
updatedAt: d.timestamp().$onUpdate(() => new Date()),
|
|
}),
|
|
(t) => [
|
|
index("invoice_template_created_by_idx").on(t.createdById),
|
|
index("invoice_template_type_idx").on(t.type),
|
|
],
|
|
);
|
|
|
|
export const invoiceTemplatesRelations = relations(
|
|
invoiceTemplates,
|
|
({ one }) => ({
|
|
createdBy: one(users, {
|
|
fields: [invoiceTemplates.createdById],
|
|
references: [users.id],
|
|
}),
|
|
}),
|
|
);
|