All checks were successful
Docker Deploy / build-and-push (push) Successful in 3m49s
323 lines
9.0 KiB
TypeScript
323 lines
9.0 KiB
TypeScript
import {
|
|
sqliteTable,
|
|
text,
|
|
integer,
|
|
real,
|
|
primaryKey,
|
|
foreignKey,
|
|
index,
|
|
} from "drizzle-orm/sqlite-core";
|
|
import { nanoid } from "nanoid";
|
|
|
|
export const users = sqliteTable("users", {
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
email: text("email").notNull().unique(),
|
|
passwordHash: text("password_hash").notNull(),
|
|
name: text("name").notNull(),
|
|
isSiteAdmin: integer("is_site_admin", { mode: "boolean" }).default(false),
|
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
});
|
|
|
|
export const organizations = sqliteTable("organizations", {
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
name: text("name").notNull(),
|
|
logoUrl: text("logo_url"),
|
|
street: text("street"),
|
|
city: text("city"),
|
|
state: text("state"),
|
|
zip: text("zip"),
|
|
country: text("country"),
|
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
});
|
|
|
|
export const members = sqliteTable(
|
|
"members",
|
|
{
|
|
userId: text("user_id").notNull(),
|
|
organizationId: text("organization_id").notNull(),
|
|
role: text("role").notNull().default("member"), // 'owner', 'admin', 'member'
|
|
joinedAt: integer("joined_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
},
|
|
(table: any) => ({
|
|
pk: primaryKey({ columns: [table.userId, table.organizationId] }),
|
|
userFk: foreignKey({
|
|
columns: [table.userId],
|
|
foreignColumns: [users.id],
|
|
}),
|
|
orgFk: foreignKey({
|
|
columns: [table.organizationId],
|
|
foreignColumns: [organizations.id],
|
|
}),
|
|
userIdIdx: index("members_user_id_idx").on(table.userId),
|
|
organizationIdIdx: index("members_organization_id_idx").on(
|
|
table.organizationId,
|
|
),
|
|
}),
|
|
);
|
|
|
|
export const clients = sqliteTable(
|
|
"clients",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
organizationId: text("organization_id").notNull(),
|
|
name: text("name").notNull(),
|
|
email: text("email"),
|
|
phone: text("phone"),
|
|
street: text("street"),
|
|
city: text("city"),
|
|
state: text("state"),
|
|
zip: text("zip"),
|
|
country: text("country"),
|
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
},
|
|
(table: any) => ({
|
|
orgFk: foreignKey({
|
|
columns: [table.organizationId],
|
|
foreignColumns: [organizations.id],
|
|
}),
|
|
organizationIdIdx: index("clients_organization_id_idx").on(
|
|
table.organizationId,
|
|
),
|
|
}),
|
|
);
|
|
|
|
export const categories = sqliteTable(
|
|
"categories",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
organizationId: text("organization_id").notNull(),
|
|
name: text("name").notNull(),
|
|
color: text("color"),
|
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
},
|
|
(table: any) => ({
|
|
orgFk: foreignKey({
|
|
columns: [table.organizationId],
|
|
foreignColumns: [organizations.id],
|
|
}),
|
|
organizationIdIdx: index("categories_organization_id_idx").on(
|
|
table.organizationId,
|
|
),
|
|
}),
|
|
);
|
|
|
|
export const timeEntries = sqliteTable(
|
|
"time_entries",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
userId: text("user_id").notNull(),
|
|
organizationId: text("organization_id").notNull(),
|
|
clientId: text("client_id").notNull(),
|
|
categoryId: text("category_id").notNull(),
|
|
startTime: integer("start_time", { mode: "timestamp" }).notNull(),
|
|
endTime: integer("end_time", { mode: "timestamp" }),
|
|
description: text("description"),
|
|
isManual: integer("is_manual", { mode: "boolean" }).default(false),
|
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
},
|
|
(table: any) => ({
|
|
userFk: foreignKey({
|
|
columns: [table.userId],
|
|
foreignColumns: [users.id],
|
|
}),
|
|
orgFk: foreignKey({
|
|
columns: [table.organizationId],
|
|
foreignColumns: [organizations.id],
|
|
}),
|
|
clientFk: foreignKey({
|
|
columns: [table.clientId],
|
|
foreignColumns: [clients.id],
|
|
}),
|
|
categoryFk: foreignKey({
|
|
columns: [table.categoryId],
|
|
foreignColumns: [categories.id],
|
|
}),
|
|
userIdIdx: index("time_entries_user_id_idx").on(table.userId),
|
|
organizationIdIdx: index("time_entries_organization_id_idx").on(
|
|
table.organizationId,
|
|
),
|
|
clientIdIdx: index("time_entries_client_id_idx").on(table.clientId),
|
|
startTimeIdx: index("time_entries_start_time_idx").on(table.startTime),
|
|
}),
|
|
);
|
|
|
|
export const tags = sqliteTable(
|
|
"tags",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
organizationId: text("organization_id").notNull(),
|
|
name: text("name").notNull(),
|
|
color: text("color"),
|
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
},
|
|
(table: any) => ({
|
|
orgFk: foreignKey({
|
|
columns: [table.organizationId],
|
|
foreignColumns: [organizations.id],
|
|
}),
|
|
organizationIdIdx: index("tags_organization_id_idx").on(
|
|
table.organizationId,
|
|
),
|
|
}),
|
|
);
|
|
|
|
export const timeEntryTags = sqliteTable(
|
|
"time_entry_tags",
|
|
{
|
|
timeEntryId: text("time_entry_id").notNull(),
|
|
tagId: text("tag_id").notNull(),
|
|
},
|
|
(table: any) => ({
|
|
pk: primaryKey({ columns: [table.timeEntryId, table.tagId] }),
|
|
timeEntryFk: foreignKey({
|
|
columns: [table.timeEntryId],
|
|
foreignColumns: [timeEntries.id],
|
|
}),
|
|
tagFk: foreignKey({
|
|
columns: [table.tagId],
|
|
foreignColumns: [tags.id],
|
|
}),
|
|
timeEntryIdIdx: index("time_entry_tags_time_entry_id_idx").on(
|
|
table.timeEntryId,
|
|
),
|
|
tagIdIdx: index("time_entry_tags_tag_id_idx").on(table.tagId),
|
|
}),
|
|
);
|
|
|
|
export const sessions = sqliteTable(
|
|
"sessions",
|
|
{
|
|
id: text("id").primaryKey(),
|
|
userId: text("user_id").notNull(),
|
|
expiresAt: integer("expires_at", { mode: "timestamp" }).notNull(),
|
|
},
|
|
(table: any) => ({
|
|
userFk: foreignKey({
|
|
columns: [table.userId],
|
|
foreignColumns: [users.id],
|
|
}),
|
|
userIdIdx: index("sessions_user_id_idx").on(table.userId),
|
|
}),
|
|
);
|
|
|
|
export const siteSettings = sqliteTable("site_settings", {
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
key: text("key").notNull().unique(),
|
|
value: text("value").notNull(),
|
|
updatedAt: integer("updated_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
});
|
|
|
|
export const apiTokens = sqliteTable(
|
|
"api_tokens",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
userId: text("user_id").notNull(),
|
|
name: text("name").notNull(),
|
|
token: text("token").notNull().unique(),
|
|
scopes: text("scopes").notNull().default("*"),
|
|
lastUsedAt: integer("last_used_at", { mode: "timestamp" }),
|
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
},
|
|
(table: any) => ({
|
|
userFk: foreignKey({
|
|
columns: [table.userId],
|
|
foreignColumns: [users.id],
|
|
}),
|
|
userIdIdx: index("api_tokens_user_id_idx").on(table.userId),
|
|
}),
|
|
);
|
|
|
|
export const invoices = sqliteTable(
|
|
"invoices",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
organizationId: text("organization_id").notNull(),
|
|
clientId: text("client_id").notNull(),
|
|
number: text("number").notNull(),
|
|
type: text("type").notNull().default("invoice"), // 'invoice' or 'quote'
|
|
status: text("status").notNull().default("draft"), // 'draft', 'sent', 'paid', 'void', 'accepted', 'declined'
|
|
issueDate: integer("issue_date", { mode: "timestamp" }).notNull(),
|
|
dueDate: integer("due_date", { mode: "timestamp" }).notNull(),
|
|
notes: text("notes"),
|
|
currency: text("currency").default("USD").notNull(),
|
|
subtotal: integer("subtotal").notNull().default(0), // in cents
|
|
taxRate: real("tax_rate").default(0), // percentage
|
|
taxAmount: integer("tax_amount").notNull().default(0), // in cents
|
|
total: integer("total").notNull().default(0), // in cents
|
|
createdAt: integer("created_at", { mode: "timestamp" }).$defaultFn(
|
|
() => new Date(),
|
|
),
|
|
},
|
|
(table: any) => ({
|
|
orgFk: foreignKey({
|
|
columns: [table.organizationId],
|
|
foreignColumns: [organizations.id],
|
|
}),
|
|
clientFk: foreignKey({
|
|
columns: [table.clientId],
|
|
foreignColumns: [clients.id],
|
|
}),
|
|
organizationIdIdx: index("invoices_organization_id_idx").on(
|
|
table.organizationId,
|
|
),
|
|
clientIdIdx: index("invoices_client_id_idx").on(table.clientId),
|
|
}),
|
|
);
|
|
|
|
export const invoiceItems = sqliteTable(
|
|
"invoice_items",
|
|
{
|
|
id: text("id")
|
|
.primaryKey()
|
|
.$defaultFn(() => nanoid()),
|
|
invoiceId: text("invoice_id").notNull(),
|
|
description: text("description").notNull(),
|
|
quantity: real("quantity").notNull().default(1),
|
|
unitPrice: integer("unit_price").notNull().default(0), // in cents
|
|
amount: integer("amount").notNull().default(0), // in cents
|
|
},
|
|
(table: any) => ({
|
|
invoiceFk: foreignKey({
|
|
columns: [table.invoiceId],
|
|
foreignColumns: [invoices.id],
|
|
}),
|
|
invoiceIdIdx: index("invoice_items_invoice_id_idx").on(table.invoiceId),
|
|
}),
|
|
);
|