Files
beenvoice-web/src/server/db/schema.ts
T
soconnorandCursor 31151e7f39 Sync time entries with invoice lines and harden auth for mobile.
Link clocked time to invoice items with bidirectional sync, add entry editing on web, broaden session cookie detection for Expo clients, and handle API rate limits without signing users out.

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-06-28 21:44:19 -04:00

798 lines
24 KiB
TypeScript

import { relations, sql } from "drizzle-orm";
import { index, pgTableCreator, uniqueIndex } 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(),
theme: d.varchar({ length: 20 }).default("system").notNull(),
role: d.varchar({ length: 20 }).default("user").notNull(),
onboardingCompletedAt: d.timestamp(),
}));
export const platformSettings = createTable("platform_setting", (d) => ({
id: d.varchar({ length: 50 }).notNull().primaryKey().default("global"),
pdfTemplate: d.varchar({ length: 20 }).default("classic").notNull(),
pdfAccentColor: d.varchar({ length: 50 }).default("#111827").notNull(),
pdfFontFamily: d.varchar({ length: 20 }).default("sans").notNull(),
pdfNumericFontFamily: d.varchar({ length: 20 }).default("mono").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),
apiKeys: many(apiKeys),
clients: many(clients),
businesses: many(businesses),
invoices: many(invoices),
sessions: many(sessions),
expenses: many(expenses),
invoiceTemplates: many(invoiceTemplates),
recurringInvoices: many(recurringInvoices),
timeEntries: many(timeEntries),
auditLogsAsActor: many(auditLog),
}));
export const auditLog = createTable(
"audit_log",
(d) => ({
id: d
.varchar({ length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
actorUserId: d
.varchar({ length: 255 })
.notNull()
.references(() => users.id),
action: d.varchar({ length: 100 }).notNull(),
targetType: d.varchar({ length: 50 }).notNull(),
targetId: d.varchar({ length: 255 }),
metadata: d.jsonb().$type<Record<string, unknown>>(),
createdAt: d.timestamp().notNull().defaultNow(),
}),
(t) => [
index("audit_log_actor_user_id_idx").on(t.actorUserId),
index("audit_log_action_idx").on(t.action),
index("audit_log_created_at_idx").on(t.createdAt),
],
);
export const auditLogRelations = relations(auditLog, ({ one }) => ({
actor: one(users, {
fields: [auditLog.actorUserId],
references: [users.id],
}),
}));
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 apiKeys = createTable(
"api_key",
(d) => ({
id: d
.varchar({ length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: d.varchar({ length: 100 }).notNull(),
keyHash: d.varchar({ length: 64 }).notNull().unique(),
keyPrefix: d.varchar({ length: 16 }).notNull(),
userId: d
.varchar({ length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
lastUsedAt: d.timestamp(),
expiresAt: d.timestamp(),
revokedAt: d.timestamp(),
createdAt: d.timestamp().notNull().defaultNow(),
updatedAt: d
.timestamp()
.notNull()
.defaultNow()
.$onUpdate(() => new Date()),
}),
(t) => [
index("api_key_hash_idx").on(t.keyHash),
index("api_key_user_id_idx").on(t.userId),
index("api_key_revoked_at_idx").on(t.revokedAt),
],
);
export const apiKeysRelations = relations(apiKeys, ({ one }) => ({
user: one(users, { fields: [apiKeys.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.text().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),
timeEntries: many(timeEntries),
}));
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),
publicToken: d.varchar({ length: 255 }).unique(),
publicTokenExpiresAt: d.timestamp(),
lastReminderSentAt: d.timestamp(),
sendReminderAt: d.timestamp(),
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),
index("invoice_public_token_idx").on(t.publicToken),
],
);
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),
payments: many(invoicePayments),
}));
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
timeEntryId: d
.varchar({ length: 255 })
.references(() => timeEntries.id, { onDelete: "set null" }),
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],
}),
timeEntry: one(timeEntries, {
fields: [invoiceItems.timeEntryId],
references: [timeEntries.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_business_id_idx").on(t.businessId),
index("expense_date_idx").on(t.date),
index("expense_billable_idx").on(t.billable),
],
);
export const expenseReceipts = createTable(
"expense_receipt",
(d) => ({
id: d
.varchar({ length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
expenseId: d
.varchar({ length: 255 })
.notNull()
.references(() => expenses.id, { onDelete: "cascade" }),
storageKey: d.varchar({ length: 500 }).notNull(),
originalFilename: d.varchar({ length: 255 }).notNull(),
mimeType: d.varchar({ length: 100 }).notNull(),
sizeBytes: d.integer().notNull(),
createdAt: d
.timestamp()
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
}),
(t) => [index("expense_receipt_expense_id_idx").on(t.expenseId)],
);
export const expensesRelations = relations(expenses, ({ one, many }) => ({
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],
}),
receipts: many(expenseReceipts),
}));
export const expenseReceiptsRelations = relations(
expenseReceipts,
({ one }) => ({
expense: one(expenses, {
fields: [expenseReceipts.expenseId],
references: [expenses.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],
}),
}),
);
// ─── Invoice Payments ────────────────────────────────────────────────────────
export const invoicePayments = createTable(
"invoice_payment",
(d) => ({
id: d
.varchar({ length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
invoiceId: d
.varchar({ length: 255 })
.notNull()
.references(() => invoices.id, { onDelete: "cascade" }),
amount: d.real().notNull(),
currency: d.varchar({ length: 3 }).default("USD").notNull(),
date: d.timestamp().notNull(),
method: d
.varchar({ length: 50 })
.notNull()
.default("other"), // cash | check | bank_transfer | credit_card | paypal | other
notes: d.varchar({ length: 500 }),
createdById: d
.varchar({ length: 255 })
.notNull()
.references(() => users.id),
createdAt: d
.timestamp()
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
}),
(t) => [
index("invoice_payment_invoice_id_idx").on(t.invoiceId),
index("invoice_payment_created_by_idx").on(t.createdById),
],
);
export const invoicePaymentsRelations = relations(invoicePayments, ({ one }) => ({
invoice: one(invoices, {
fields: [invoicePayments.invoiceId],
references: [invoices.id],
}),
createdBy: one(users, {
fields: [invoicePayments.createdById],
references: [users.id],
}),
}));
// ─── Recurring Invoices ───────────────────────────────────────────────────────
export const recurringInvoices = createTable(
"recurring_invoice",
(d) => ({
id: d
.varchar({ length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
name: d.varchar({ length: 255 }).notNull(),
clientId: d
.varchar({ length: 255 })
.notNull()
.references(() => clients.id),
businessId: d.varchar({ length: 255 }).references(() => businesses.id),
schedule: d.varchar({ length: 20 }).notNull().default("monthly"), // weekly | biweekly | monthly | quarterly | yearly
status: d.varchar({ length: 20 }).notNull().default("active"), // active | paused
invoicePrefix: d.varchar({ length: 20 }).default("#"),
taxRate: d.real().notNull().default(0),
currency: d.varchar({ length: 3 }).default("USD").notNull(),
notes: d.varchar({ length: 1000 }),
emailMessage: d.varchar({ length: 2000 }),
nextDueAt: d.timestamp().notNull(),
lastGeneratedAt: d.timestamp(),
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("recurring_invoice_created_by_idx").on(t.createdById),
index("recurring_invoice_client_id_idx").on(t.clientId),
index("recurring_invoice_status_idx").on(t.status),
index("recurring_invoice_next_due_idx").on(t.nextDueAt),
],
);
export const recurringInvoicesRelations = relations(
recurringInvoices,
({ one, many }) => ({
client: one(clients, {
fields: [recurringInvoices.clientId],
references: [clients.id],
}),
business: one(businesses, {
fields: [recurringInvoices.businessId],
references: [businesses.id],
}),
createdBy: one(users, {
fields: [recurringInvoices.createdById],
references: [users.id],
}),
items: many(recurringInvoiceItems),
}),
);
export const recurringInvoiceItems = createTable(
"recurring_invoice_item",
(d) => ({
id: d
.varchar({ length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
recurringInvoiceId: d
.varchar({ length: 255 })
.notNull()
.references(() => recurringInvoices.id, { onDelete: "cascade" }),
description: d.varchar({ length: 500 }).notNull(),
hours: d.real().notNull(),
rate: d.real().notNull(),
position: d.integer().notNull().default(0),
createdAt: d
.timestamp()
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
}),
(t) => [
index("recurring_invoice_item_recurring_id_idx").on(t.recurringInvoiceId),
],
);
export const recurringInvoiceItemsRelations = relations(
recurringInvoiceItems,
({ one }) => ({
recurringInvoice: one(recurringInvoices, {
fields: [recurringInvoiceItems.recurringInvoiceId],
references: [recurringInvoices.id],
}),
}),
);
// ─── Time Entries ─────────────────────────────────────────────────────────────
export const timeEntries = createTable(
"time_entry",
(d) => ({
id: d
.varchar({ length: 255 })
.notNull()
.primaryKey()
.$defaultFn(() => crypto.randomUUID()),
description: d.varchar({ length: 500 }).notNull().default(""),
clientId: d
.varchar({ length: 255 })
.references(() => clients.id, { onDelete: "set null" }),
invoiceId: d
.varchar({ length: 255 })
.references(() => invoices.id, { onDelete: "set null" }),
startedAt: d.timestamp().notNull(),
endedAt: d.timestamp(), // null = currently running
hours: d.real(), // stored when stopped
rate: d.real(),
notes: d.varchar({ length: 500 }),
createdById: d
.varchar({ length: 255 })
.notNull()
.references(() => users.id, { onDelete: "cascade" }),
createdAt: d
.timestamp()
.default(sql`CURRENT_TIMESTAMP`)
.notNull(),
updatedAt: d.timestamp().$onUpdate(() => new Date()),
}),
(t) => [
index("time_entry_created_by_idx").on(t.createdById),
index("time_entry_client_id_idx").on(t.clientId),
index("time_entry_started_at_idx").on(t.startedAt),
index("time_entry_ended_at_idx").on(t.endedAt),
uniqueIndex("time_entry_one_running_per_user_idx")
.on(t.createdById)
.where(sql`${t.endedAt} is null`),
],
);
export const timeEntriesRelations = relations(timeEntries, ({ one }) => ({
client: one(clients, {
fields: [timeEntries.clientId],
references: [clients.id],
}),
invoice: one(invoices, {
fields: [timeEntries.invoiceId],
references: [invoices.id],
}),
createdBy: one(users, {
fields: [timeEntries.createdById],
references: [users.id],
}),
}));