Introduced New DB

This commit is contained in:
2025-12-06 10:34:58 +01:00
parent 407592680a
commit 63af22b671
80 changed files with 4509 additions and 154 deletions

10
db/index.ts Normal file
View File

@@ -0,0 +1,10 @@
import { drizzle } from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import {secrets} from "../src/utils/secrets";
const pool = new Pool({
connectionString: secrets.DATABASE_URL,
max: 10, // je nach Last
})
export const db = drizzle(pool)

24
db/schema/accounts.ts Normal file
View File

@@ -0,0 +1,24 @@
import {
pgTable,
bigint,
timestamp,
text,
} from "drizzle-orm/pg-core"
export const accounts = pgTable("accounts", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
number: text("number").notNull(),
label: text("label").notNull(),
description: text("description"),
})
export type Account = typeof accounts.$inferSelect
export type NewAccount = typeof accounts.$inferInsert

View File

@@ -0,0 +1,83 @@
import {
pgTable,
uuid,
text,
timestamp,
date,
boolean,
bigint,
doublePrecision,
jsonb,
} from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
export const authProfiles = pgTable("auth_profiles", {
id: uuid("id").primaryKey().defaultRandom(),
userId: uuid("user_id").references(() => authUsers.id),
tenantId: bigint("tenant_id", { mode: "number" }).notNull(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
firstName: text("first_name").notNull(),
lastName: text("last_name").notNull(),
fullName: text("full_name").generatedAlwaysAs(
`((first_name || ' ') || last_name)`
),
mobileTel: text("mobile_tel"),
fixedTel: text("fixed_tel"),
salutation: text("salutation"),
employeeNumber: text("employee_number"),
weeklyWorkingHours: doublePrecision("weekly_working_hours").default(0),
annualPaidLeaveDays: bigint("annual_paid_leave_days", { mode: "number" }),
weeklyRegularWorkingHours: jsonb("weekly_regular_working_hours").default("{}"),
clothingSizeTop: text("clothing_size_top"),
clothingSizeBottom: text("clothing_size_bottom"),
clothingSizeShoe: text("clothing_size_shoe"),
emailSignature: text("email_signature").default("<p>Mit freundlichen Grüßen</p>"),
birthday: date("birthday"),
entryDate: date("entry_date").defaultNow(),
automaticHourCorrections: jsonb("automatic_hour_corrections").default("[]"),
recreationDaysCompensation: boolean("recreation_days_compensation")
.notNull()
.default(true),
customerForPortal: bigint("customer_for_portal", { mode: "number" }),
pinnedOnNavigation: jsonb("pinned_on_navigation").notNull().default("[]"),
email: text("email"),
tokenId: text("token_id"),
weeklyWorkingDays: doublePrecision("weekly_working_days"),
oldProfileId: uuid("old_profile_id"),
tempConfig: jsonb("temp_config"),
stateCode: text("state_code").default("DE-NI"),
contractType: text("contract_type"),
position: text("position"),
qualification: text("qualification"),
addressStreet: text("address_street"),
addressZip: text("address_zip"),
addressCity: text("address_city"),
active: boolean("active").notNull().default(true),
})
export type AuthProfile = typeof authProfiles.$inferSelect
export type NewAuthProfile = typeof authProfiles.$inferInsert

View File

@@ -0,0 +1,23 @@
import { pgTable, uuid, text, timestamp } from "drizzle-orm/pg-core"
import { authRoles } from "./auth_roles"
export const authRolePermissions = pgTable(
"auth_role_permissions",
{
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
roleId: uuid("role_id")
.notNull()
.references(() => authRoles.id),
permission: text("permission").notNull(),
},
(table) => ({
primaryKey: [table.roleId, table.permission],
})
)
export type AuthRolePermission = typeof authRolePermissions.$inferSelect
export type NewAuthRolePermission = typeof authRolePermissions.$inferInsert

19
db/schema/auth_roles.ts Normal file
View File

@@ -0,0 +1,19 @@
import { pgTable, uuid, text, timestamp, bigint } from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
export const authRoles = pgTable("auth_roles", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
description: text("description"),
createdBy: uuid("created_by").references(() => authUsers.id),
tenantId: bigint("tenant_id", {mode: "number"}),
})
export type AuthRole = typeof authRoles.$inferSelect
export type NewAuthRole = typeof authRoles.$inferInsert

View File

@@ -0,0 +1,22 @@
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
export const authTenantUsers = pgTable(
"auth_tenant_users",
{
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenantId: bigint("tenant_id", { mode: "number" }).notNull(),
userId: uuid("user_id").notNull(),
createdBy: uuid("created_by").references(() => authUsers.id),
},
(table) => ({
primaryKey: [table.tenantId, table.userId],
})
)
export type AuthTenantUser = typeof authTenantUsers.$inferSelect
export type NewAuthTenantUser = typeof authTenantUsers.$inferInsert

View File

@@ -0,0 +1,30 @@
import { pgTable, uuid, bigint, timestamp } from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
import { authRoles } from "./auth_roles"
export const authUserRoles = pgTable(
"auth_user_roles",
{
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id),
roleId: uuid("role_id")
.notNull()
.references(() => authRoles.id),
tenantId: bigint("tenant_id", { mode: "number" }).notNull(),
createdBy: uuid("created_by").references(() => authUsers.id),
},
(table) => ({
primaryKey: [table.userId, table.roleId, table.tenantId],
})
)
export type AuthUserRole = typeof authUserRoles.$inferSelect
export type NewAuthUserRole = typeof authUserRoles.$inferInsert

22
db/schema/auth_users.ts Normal file
View File

@@ -0,0 +1,22 @@
import { pgTable, uuid, text, boolean, timestamp } from "drizzle-orm/pg-core"
export const authUsers = pgTable("auth_users", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
email: text("email").notNull(),
passwordHash: text("password_hash").notNull(),
multiTenant: boolean("multi_tenant").notNull().default(true),
mustChangePassword: boolean("must_change_password").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
ported: boolean("ported").notNull().default(true),
})
export type AuthUser = typeof authUsers.$inferSelect
export type NewAuthUser = typeof authUsers.$inferInsert

52
db/schema/bankaccounts.ts Normal file
View File

@@ -0,0 +1,52 @@
import {
pgTable,
bigint,
timestamp,
text,
doublePrecision,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const bankaccounts = pgTable("bankaccounts", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name"),
iban: text("iban").notNull(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
bankId: text("bankId").notNull(),
ownerName: text("ownerName"),
accountId: text("accountId").notNull(),
balance: doublePrecision("balance"),
expired: boolean("expired").notNull().default(false),
datevNumber: text("datevNumber"),
syncedAt: timestamp("synced_at", { withTimezone: true })
.notNull()
.defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
archived: boolean("archived").notNull().default(false),
})
export type BankAccount = typeof bankaccounts.$inferSelect
export type NewBankAccount = typeof bankaccounts.$inferInsert

View File

@@ -0,0 +1,30 @@
import {
pgTable,
uuid,
timestamp,
text,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const bankrequisitions = pgTable("bankrequisitions", {
id: uuid("id").primaryKey(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
institutionId: text("institutionId"),
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
status: text("status"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type BankRequisition = typeof bankrequisitions.$inferSelect
export type NewBankRequisition = typeof bankrequisitions.$inferInsert

View File

@@ -0,0 +1,70 @@
import {
pgTable,
bigint,
timestamp,
text,
doublePrecision,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { bankaccounts } from "./bankaccounts"
import { createddocuments } from "./createddocuments"
import { tenants } from "./tenants"
import { incominginvoices } from "./incominginvoices"
import { contracts } from "./contracts"
import { authUsers } from "./auth_users"
export const bankstatements = pgTable("bankstatements", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
account: bigint("account", { mode: "number" })
.notNull()
.references(() => bankaccounts.id),
date: text("date").notNull(),
credIban: text("credIban"),
credName: text("credName"),
text: text("text"),
amount: doublePrecision("amount").notNull(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
debIban: text("debIban"),
debName: text("debName"),
gocardlessId: text("gocardlessId"),
currency: text("currency"),
valueDate: text("valueDate"),
incomingInvoice: bigint("incomingInvoice", { mode: "number" }).references(
() => incominginvoices.id
),
mandateId: text("mandateId"),
contract: bigint("contract", { mode: "number" }).references(
() => contracts.id
),
createdDocument: bigint("createdDocument", { mode: "number" }).references(
() => createddocuments.id
),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type BankStatement = typeof bankstatements.$inferSelect
export type NewBankStatement = typeof bankstatements.$inferInsert

View File

@@ -0,0 +1,27 @@
import {
pgTable,
uuid,
timestamp,
text,
} from "drizzle-orm/pg-core"
import { checks } from "./checks"
export const checkexecutions = pgTable("checkexecutions", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
check: uuid("check").references(() => checks.id),
executedAt: timestamp("executed_at"),
// ❌ executed_by removed (was 0_profiles)
description: text("description"),
})
export type CheckExecution = typeof checkexecutions.$inferSelect
export type NewCheckExecution = typeof checkexecutions.$inferInsert

52
db/schema/checks.ts Normal file
View File

@@ -0,0 +1,52 @@
import {
pgTable,
uuid,
timestamp,
text,
bigint,
boolean,
jsonb,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { vehicles } from "./vehicles"
import { inventoryItems } from "./inventoryitems"
import { authUsers } from "./auth_users"
export const checks = pgTable("checks", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
vehicle: bigint("vehicle", { mode: "number" })
.references(() => vehicles.id),
// ❌ profile removed (old 0_profiles reference)
inventoryItem: bigint("inventoryitem", { mode: "number" })
.references(() => inventoryItems.id),
tenant: bigint("tenant", { mode: "number" })
.references(() => tenants.id),
name: text("name"),
type: text("type"),
distance: bigint("distance", { mode: "number" }).default(1),
distanceUnit: text("distanceUnit").default("days"),
description: text("description"),
profiles: jsonb("profiles").notNull().default([]),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Check = typeof checks.$inferSelect
export type NewCheck = typeof checks.$inferInsert

32
db/schema/citys.ts Normal file
View File

@@ -0,0 +1,32 @@
import {
pgTable,
bigint,
text,
jsonb,
} from "drizzle-orm/pg-core"
export const citys = pgTable("citys", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
name: text("name"),
short: text("short"),
long: text("long"),
geometry: jsonb("geometry"),
zip: bigint("zip", { mode: "number" }),
districtCode: bigint("districtCode", { mode: "number" }),
countryName: text("countryName"),
countryCode: bigint("countryCode", { mode: "number" }),
districtName: text("districtName"),
geopoint: text("geopoint"),
})
export type City = typeof citys.$inferSelect
export type NewCity = typeof citys.$inferInsert

66
db/schema/contacts.ts Normal file
View File

@@ -0,0 +1,66 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
date,
uuid,
} from "drizzle-orm/pg-core"
import { customers } from "./customers"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const contacts = pgTable(
"contacts",
{
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
firstName: text("firstName"),
lastName: text("lastName"),
email: text("email"),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
tenant: bigint("tenant", { mode: "number" }).notNull(),
phoneMobile: text("phoneMobile"),
phoneHome: text("phoneHome"),
heroId: text("heroId"),
role: text("role"),
fullName: text("fullName"),
salutation: text("salutation"),
vendor: bigint("vendor", { mode: "number" }), // vendors folgt separat
active: boolean("active").notNull().default(true),
birthday: date("birthday"),
notes: text("notes"),
profiles: jsonb("profiles").notNull().default([]),
archived: boolean("archived").notNull().default(false),
title: text("title"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
}
)
export type Contact = typeof contacts.$inferSelect
export type NewContact = typeof contacts.$inferInsert

76
db/schema/contracts.ts Normal file
View File

@@ -0,0 +1,76 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { contacts } from "./contacts"
import { authUsers } from "./auth_users"
export const contracts = pgTable(
"contracts",
{
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" }).notNull(),
name: text("name").notNull(),
customer: bigint("customer", { mode: "number" })
.notNull()
.references(() => customers.id),
notes: text("notes"),
active: boolean("active").notNull().default(true),
recurring: boolean("recurring").notNull().default(false),
rhythm: jsonb("rhythm"),
startDate: timestamp("startDate", { withTimezone: true }),
endDate: timestamp("endDate", { withTimezone: true }),
signDate: timestamp("signDate", { withTimezone: true }),
duration: text("duration"),
contact: bigint("contact", { mode: "number" }).references(
() => contacts.id
),
bankingIban: text("bankingIban"),
bankingBIC: text("bankingBIC"),
bankingName: text("bankingName"),
bankingOwner: text("bankingOwner"),
sepaRef: text("sepaRef"),
sepaDate: timestamp("sepaDate", { withTimezone: true }),
paymentType: text("paymentType"),
invoiceDispatch: text("invoiceDispatch"),
ownFields: jsonb("ownFields").notNull().default({}),
profiles: jsonb("profiles").notNull().default([]),
archived: boolean("archived").notNull().default(false),
contractNumber: text("contractNumber"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
}
)
export type Contract = typeof contracts.$inferSelect
export type NewContract = typeof contracts.$inferInsert

50
db/schema/costcentres.ts Normal file
View File

@@ -0,0 +1,50 @@
import {
pgTable,
uuid,
timestamp,
text,
boolean,
jsonb,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { inventoryItems } from "./inventoryitems"
import { projects } from "./projects"
import { vehicles } from "./vehicles"
import { authUsers } from "./auth_users"
export const costcentres = pgTable("costcentres", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
number: text("number").notNull(),
name: text("name").notNull(),
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
project: bigint("project", { mode: "number" }).references(() => projects.id),
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(
() => inventoryItems.id
),
description: text("description"),
archived: boolean("archived").notNull().default(false),
profiles: jsonb("profiles").notNull().default([]),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type CostCentre = typeof costcentres.$inferSelect
export type NewCostCentre = typeof costcentres.$inferInsert

21
db/schema/countrys.ts Normal file
View File

@@ -0,0 +1,21 @@
import {
pgTable,
bigint,
timestamp,
text,
} from "drizzle-orm/pg-core"
export const countrys = pgTable("countrys", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
})
export type Country = typeof countrys.$inferSelect
export type NewCountry = typeof countrys.$inferInsert

View File

@@ -0,0 +1,121 @@
import {
pgTable,
bigint,
timestamp,
text,
jsonb,
boolean,
smallint,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { contacts } from "./contacts"
import { contracts } from "./contracts"
import { letterheads } from "./letterheads"
import { projects } from "./projects"
import { plants } from "./plants"
import { authUsers } from "./auth_users"
export const createddocuments = pgTable("createddocuments", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
type: text("type").notNull().default("INVOICE"),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
contact: bigint("contact", { mode: "number" }).references(
() => contacts.id
),
address: jsonb("address"),
project: bigint("project", { mode: "number" }).references(
() => projects.id
),
documentNumber: text("documentNumber"),
documentDate: text("documentDate"),
state: text("state").notNull().default("Entwurf"),
info: jsonb("info"),
createdBy: uuid("createdBy").references(() => authUsers.id),
title: text("title"),
description: text("description"),
startText: text("startText"),
endText: text("endText"),
rows: jsonb("rows").default([]),
deliveryDateType: text("deliveryDateType"),
paymentDays: smallint("paymentDays"),
deliveryDate: text("deliveryDate"),
contactPerson: uuid("contactPerson"),
serialConfig: jsonb("serialConfig").default({}),
linkedDocument: bigint("linkedDocument", { mode: "number" }).references(
() => createddocuments.id
),
agriculture: jsonb("agriculture"),
letterhead: bigint("letterhead", { mode: "number" }).references(
() => letterheads.id
),
advanceInvoiceResolved: boolean("advanceInvoiceResolved")
.notNull()
.default(false),
usedAdvanceInvoices: jsonb("usedAdvanceInvoices").notNull().default([]),
archived: boolean("archived").notNull().default(false),
deliveryDateEnd: text("deliveryDateEnd"),
plant: bigint("plant", { mode: "number" }).references(() => plants.id),
taxType: text("taxType"),
customSurchargePercentage: smallint("customSurchargePercentage")
.notNull()
.default(0),
report: jsonb("report").notNull().default({}),
availableInPortal: boolean("availableInPortal")
.notNull()
.default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
created_by: uuid("created_by").references(() => authUsers.id),
payment_type: text("payment_type").default("transfer"),
contract: bigint("contract", { mode: "number" }).references(
() => contracts.id
),
})
export type CreatedDocument = typeof createddocuments.$inferSelect
export type NewCreatedDocument = typeof createddocuments.$inferInsert

View File

@@ -0,0 +1,43 @@
import {
pgTable,
uuid,
timestamp,
bigint,
text,
jsonb,
boolean,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { vendors } from "./vendors"
import { authUsers } from "./auth_users"
export const createdletters = pgTable("createdletters", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
contentJson: jsonb("content_json").default([]),
contentText: text("content_text"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
archived: boolean("archived").notNull().default(false),
})
export type CreatedLetter = typeof createdletters.$inferSelect
export type NewCreatedLetter = typeof createdletters.$inferInsert

69
db/schema/customers.ts Normal file
View File

@@ -0,0 +1,69 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
smallint,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const customers = pgTable(
"customers",
{
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
customerNumber: text("customerNumber").notNull(),
name: text("name").notNull(),
tenant: bigint("tenant", { mode: "number" }).notNull(),
infoData: jsonb("infoData").default({}),
active: boolean("active").notNull().default(true),
notes: text("notes"),
type: text("type").default("Privat"),
heroId: text("heroId"),
isCompany: boolean("isCompany").notNull().default(false),
profiles: jsonb("profiles").notNull().default([]),
customPaymentDays: smallint("customPaymentDays"),
firstname: text("firstname"),
lastname: text("lastname"),
archived: boolean("archived").notNull().default(false),
customSurchargePercentage: smallint("customSurchargePercentage")
.notNull()
.default(0),
salutation: text("salutation"),
title: text("title"),
nameAddition: text("nameAddition"),
availableInPortal: boolean("availableInPortal")
.notNull()
.default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
customPaymentType: text("custom_payment_type"), // ENUM payment_types separat?
}
)
export type Customer = typeof customers.$inferSelect
export type NewCustomer = typeof customers.$inferInsert

29
db/schema/devices.ts Normal file
View File

@@ -0,0 +1,29 @@
import {
pgTable,
uuid,
timestamp,
text,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
export const devices = pgTable("devices", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
type: text("type").notNull(),
tenant: bigint("tenant", { mode: "number" }).references(() => tenants.id),
password: text("password"),
externalId: text("externalId"),
})
export type Device = typeof devices.$inferSelect
export type NewDevice = typeof devices.$inferInsert

View File

@@ -0,0 +1,28 @@
import { pgTable, uuid, timestamp, text, boolean, bigint } from "drizzle-orm/pg-core"
import { spaces } from "./spaces"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const documentboxes = pgTable("documentboxes", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
space: bigint("space", { mode: "number" }).references(() => spaces.id),
key: text("key").notNull(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type DocumentBox = typeof documentboxes.$inferSelect
export type NewDocumentBox = typeof documentboxes.$inferInsert

97
db/schema/enums.ts Normal file
View File

@@ -0,0 +1,97 @@
import { pgEnum } from "drizzle-orm/pg-core"
// public.textTemplatePositions
export const textTemplatePositionsEnum = pgEnum("texttemplatepositions", [
"startText",
"endText",
])
// public.folderFunctions
export const folderFunctionsEnum = pgEnum("folderfunctions", [
"none",
"yearSubCategory",
"incomingInvoices",
"invoices",
"quotes",
"confirmationOrders",
"deliveryNotes",
"vehicleData",
"reminders",
"taxData",
"deposit",
"timeEvaluations",
])
// public.locked_tenant
export const lockedTenantEnum = pgEnum("locked_tenant", [
"maintenance_tenant",
"maintenance",
"general",
"no_subscription",
])
// public.credential_types
export const credentialTypesEnum = pgEnum("credential_types", [
"mail",
"m365",
])
// public.payment_types
export const paymentTypesEnum = pgEnum("payment_types", [
"transfer",
"direct_debit",
])
// public.notification_status
export const notificationStatusEnum = pgEnum("notification_status", [
"queued",
"sent",
"failed",
"read",
])
// public.notification_channel
export const notificationChannelEnum = pgEnum("notification_channel", [
"email",
"inapp",
"sms",
"push",
"webhook",
])
// public.notification_severity
export const notificationSeverityEnum = pgEnum("notification_severity", [
"info",
"success",
"warning",
"error",
])
// public.times_state
export const timesStateEnum = pgEnum("times_state", [
"submitted",
"approved",
"draft",
])
export const helpdeskStatusEnum = [
"open",
"in_progress",
"waiting_for_customer",
"answered",
"closed",
] as const
export const helpdeskPriorityEnum = [
"low",
"normal",
"high",
] as const
export const helpdeskDirectionEnum = [
"incoming",
"outgoing",
"internal",
"system",
] as const

60
db/schema/events.ts Normal file
View File

@@ -0,0 +1,60 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { authUsers } from "./auth_users"
export const events = pgTable(
"events",
{
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" }).notNull(),
name: text("name").notNull(),
startDate: timestamp("startDate", { withTimezone: true }).notNull(),
endDate: timestamp("endDate", { withTimezone: true }),
eventtype: text("eventtype").default("Umsetzung"),
project: bigint("project", { mode: "number" }), // FK follows when projects.ts exists
resources: jsonb("resources").default([]),
notes: text("notes"),
link: text("link"),
profiles: jsonb("profiles").notNull().default([]),
archived: boolean("archived").notNull().default(false),
vehicles: jsonb("vehicles").notNull().default([]),
inventoryitems: jsonb("inventoryitems").notNull().default([]),
inventoryitemgroups: jsonb("inventoryitemgroups").notNull().default([]),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
vendor: bigint("vendor", { mode: "number" }), // will link once vendors.ts is created
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
}
)
export type Event = typeof events.$inferSelect
export type NewEvent = typeof events.$inferInsert

79
db/schema/files.ts Normal file
View File

@@ -0,0 +1,79 @@
import {
pgTable,
uuid,
timestamp,
text,
boolean,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { projects } from "./projects"
import { customers } from "./customers"
import { contracts } from "./contracts"
import { vendors } from "./vendors"
import { incominginvoices } from "./incominginvoices"
import { plants } from "./plants"
import { createddocuments } from "./createddocuments"
import { vehicles } from "./vehicles"
import { products } from "./products"
import { inventoryItems } from "./inventoryitems"
import { folders } from "./folders"
import { filetags } from "./filetags"
import { authUsers } from "./auth_users"
import { authProfiles } from "./auth_profiles"
import { spaces } from "./spaces"
import { documentboxes } from "./documentboxes"
import { checks } from "./checks"
export const files = pgTable("files", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
path: text("path"),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
project: bigint("project", { mode: "number" }).references(() => projects.id),
customer: bigint("customer", { mode: "number" }).references(() => customers.id),
contract: bigint("contract", { mode: "number" }).references(() => contracts.id),
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
incominginvoice: bigint("incominginvoice", { mode: "number" }).references(() => incominginvoices.id),
plant: bigint("plant", { mode: "number" }).references(() => plants.id),
createddocument: bigint("createddocument", { mode: "number" }).references(() => createddocuments.id),
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
product: bigint("product", { mode: "number" }).references(() => products.id),
check: uuid("check").references(() => checks.id),
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(() => inventoryItems.id),
folder: uuid("folder").references(() => folders.id),
mimeType: text("mimeType"),
archived: boolean("archived").notNull().default(false),
space: bigint("space", { mode: "number" }).references(() => spaces.id),
type: uuid("type").references(() => filetags.id),
documentbox: uuid("documentbox").references(() => documentboxes.id),
name: text("name"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
createdBy: uuid("created_by").references(() => authUsers.id),
authProfile: uuid("auth_profile").references(() => authProfiles.id),
})
export type File = typeof files.$inferSelect
export type NewFile = typeof files.$inferInsert

33
db/schema/filetags.ts Normal file
View File

@@ -0,0 +1,33 @@
import {
pgTable,
uuid,
timestamp,
text,
boolean,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
export const filetags = pgTable("filetags", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
color: text("color"),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
createdDocumentType: text("createddocumenttype").default(""),
incomingDocumentType: text("incomingDocumentType"),
archived: boolean("archived").notNull().default(false),
})
export type FileTag = typeof filetags.$inferSelect
export type NewFileTag = typeof filetags.$inferInsert

51
db/schema/folders.ts Normal file
View File

@@ -0,0 +1,51 @@
import {
pgTable,
uuid,
timestamp,
text,
boolean,
integer,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { filetags } from "./filetags"
import { folderFunctionsEnum } from "./enums"
export const folders = pgTable("folders", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
icon: text("icon"),
parent: uuid("parent").references(() => folders.id),
isSystemUsed: boolean("isSystemUsed").notNull().default(false),
function: folderFunctionsEnum("function"),
year: integer("year"),
standardFiletype: uuid("standardFiletype").references(() => filetags.id),
standardFiletypeIsOptional: boolean("standardFiletypeIsOptional")
.notNull()
.default(true),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
archived: boolean("archived").notNull().default(false),
})
export type Folder = typeof folders.$inferSelect
export type NewFolder = typeof folders.$inferInsert

View File

@@ -0,0 +1,35 @@
import {
pgTable,
bigint,
timestamp,
text,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
export const generatedexports = pgTable("exports", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id),
startDate: timestamp("start_date", { withTimezone: true }).notNull(),
endDate: timestamp("end_date", { withTimezone: true }).notNull(),
validUntil: timestamp("valid_until", { withTimezone: true }),
type: text("type").notNull().default("datev"),
url: text("url").notNull(),
filePath: text("file_path"),
})
export type Export = typeof generatedexports.$inferSelect
export type NewExport = typeof generatedexports.$inferInsert

View File

@@ -0,0 +1,22 @@
import {
pgTable,
bigint,
timestamp,
text,
} from "drizzle-orm/pg-core"
export const globalmessages = pgTable("globalmessages", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
title: text("title"),
description: text("description"),
})
export type GlobalMessage = typeof globalmessages.$inferSelect
export type NewGlobalMessage = typeof globalmessages.$inferInsert

View File

@@ -0,0 +1,17 @@
import {
pgTable,
timestamp,
bigint,
} from "drizzle-orm/pg-core"
import { globalmessages } from "./globalmessages"
export const globalmessagesseen = pgTable("globalmessagesseen", {
message: bigint("message", { mode: "number" })
.notNull()
.references(() => globalmessages.id),
seenAt: timestamp("seen_at", { withTimezone: true })
.notNull()
.defaultNow(),
})

View File

@@ -0,0 +1,44 @@
import {
pgTable,
uuid,
timestamp,
text,
boolean,
jsonb,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { helpdesk_channel_types } from "./helpdesk_channel_types"
export const helpdesk_channel_instances = pgTable("helpdesk_channel_instances", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
typeId: text("type_id")
.notNull()
.references(() => helpdesk_channel_types.id),
name: text("name").notNull(),
isActive: boolean("is_active").notNull().default(true),
config: jsonb("config").notNull(),
publicConfig: jsonb("public_config").notNull().default({}),
publicToken: text("public_token").unique(),
secretToken: text("secret_token"),
createdBy: uuid("created_by").references(() => authUsers.id),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
})
export type HelpdeskChannelInstance =
typeof helpdesk_channel_instances.$inferSelect
export type NewHelpdeskChannelInstance =
typeof helpdesk_channel_instances.$inferInsert

View File

@@ -0,0 +1,9 @@
import { pgTable, text } from "drizzle-orm/pg-core"
export const helpdesk_channel_types = pgTable("helpdesk_channel_types", {
id: text("id").primaryKey(),
description: text("description").notNull(),
})
export type HelpdeskChannelType = typeof helpdesk_channel_types.$inferSelect
export type NewHelpdeskChannelType = typeof helpdesk_channel_types.$inferInsert

View File

@@ -0,0 +1,45 @@
import {
pgTable,
uuid,
timestamp,
text,
jsonb,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { contacts } from "./contacts"
import { helpdesk_channel_instances } from "./helpdesk_channel_instances" // placeholder
export const helpdesk_contacts = pgTable("helpdesk_contacts", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
customerId: bigint("customer_id", { mode: "number" })
.references(() => customers.id, { onDelete: "set null" }),
email: text("email"),
phone: text("phone"),
externalRef: jsonb("external_ref"),
displayName: text("display_name"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
sourceChannelId: uuid("source_channel_id").references(
() => helpdesk_channel_instances.id,
{ onDelete: "set null" }
),
contactId: bigint("contact_id", { mode: "number" }).references(
() => contacts.id,
{ onDelete: "set null" }
),
})
export type HelpdeskContact = typeof helpdesk_contacts.$inferSelect
export type NewHelpdeskContact = typeof helpdesk_contacts.$inferInsert

View File

@@ -0,0 +1,34 @@
import {
pgTable,
uuid,
text,
} from "drizzle-orm/pg-core"
import { helpdesk_conversations } from "./helpdesk_conversations"
import { authUsers } from "./auth_users"
export const helpdesk_conversation_participants = pgTable(
"helpdesk_conversation_participants",
{
conversationId: uuid("conversation_id")
.notNull()
.references(() => helpdesk_conversations.id, { onDelete: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id, { onDelete: "cascade" }),
role: text("role"),
},
(table) => ({
pk: {
name: "helpdesk_conversation_participants_pkey",
columns: [table.conversationId, table.userId],
},
})
)
export type HelpdeskConversationParticipant =
typeof helpdesk_conversation_participants.$inferSelect
export type NewHelpdeskConversationParticipant =
typeof helpdesk_conversation_participants.$inferInsert

View File

@@ -0,0 +1,59 @@
import {
pgTable,
uuid,
timestamp,
text,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { helpdesk_contacts } from "./helpdesk_contacts"
import { contacts } from "./contacts"
import { customers } from "./customers"
import { authUsers } from "./auth_users"
import { helpdesk_channel_instances } from "./helpdesk_channel_instances"
export const helpdesk_conversations = pgTable("helpdesk_conversations", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
channelInstanceId: uuid("channel_instance_id")
.notNull()
.references(() => helpdesk_channel_instances.id, { onDelete: "cascade" }),
contactId: uuid("contact_id").references(() => helpdesk_contacts.id, {
onDelete: "set null",
}),
subject: text("subject"),
status: text("status").notNull().default("open"),
priority: text("priority").default("normal"),
assigneeUserId: uuid("assignee_user_id").references(() => authUsers.id),
lastMessageAt: timestamp("last_message_at", { withTimezone: true }),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
customerId: bigint("customer_id", { mode: "number" }).references(
() => customers.id,
{ onDelete: "set null" }
),
contactPersonId: bigint("contact_person_id", { mode: "number" }).references(
() => contacts.id,
{ onDelete: "set null" }
),
ticketNumber: text("ticket_number"),
})
export type HelpdeskConversation =
typeof helpdesk_conversations.$inferSelect
export type NewHelpdeskConversation =
typeof helpdesk_conversations.$inferInsert

View File

@@ -0,0 +1,46 @@
import {
pgTable,
uuid,
timestamp,
text,
jsonb,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { helpdesk_contacts } from "./helpdesk_contacts"
import { helpdesk_conversations } from "./helpdesk_conversations"
import { authUsers } from "./auth_users"
export const helpdesk_messages = pgTable("helpdesk_messages", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
conversationId: uuid("conversation_id")
.notNull()
.references(() => helpdesk_conversations.id, { onDelete: "cascade" }),
direction: text("direction").notNull(),
authorUserId: uuid("author_user_id").references(() => authUsers.id),
payload: jsonb("payload").notNull(),
rawMeta: jsonb("raw_meta"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
contactId: uuid("contact_id").references(() => helpdesk_contacts.id, {
onDelete: "set null",
}),
externalMessageId: text("external_message_id").unique(),
receivedAt: timestamp("received_at", { withTimezone: true }).defaultNow(),
})
export type HelpdeskMessage = typeof helpdesk_messages.$inferSelect
export type NewHelpdeskMessage = typeof helpdesk_messages.$inferInsert

View File

@@ -0,0 +1,33 @@
import {
pgTable,
uuid,
timestamp,
text,
jsonb,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const helpdesk_routing_rules = pgTable("helpdesk_routing_rules", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade" }),
name: text("name").notNull(),
condition: jsonb("condition").notNull(),
action: jsonb("action").notNull(),
createdBy: uuid("created_by").references(() => authUsers.id),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
})
export type HelpdeskRoutingRule =
typeof helpdesk_routing_rules.$inferSelect
export type NewHelpdeskRoutingRule =
typeof helpdesk_routing_rules.$inferInsert

140
db/schema/historyitems.ts Normal file
View File

@@ -0,0 +1,140 @@
import {
pgTable,
bigint,
uuid,
timestamp,
text,
jsonb,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { vendors } from "./vendors"
import { projects } from "./projects"
import { plants } from "./plants"
import { incominginvoices } from "./incominginvoices"
import { contacts } from "./contacts"
import { inventoryItems } from "./inventoryitems"
import { products } from "./products"
import { tasks } from "./tasks"
import { vehicles } from "./vehicles"
import { bankstatements } from "./bankstatements"
import { spaces } from "./spaces"
import { costcentres } from "./costcentres"
import { ownaccounts } from "./ownaccounts"
import { createddocuments } from "./createddocuments"
import { documentboxes } from "./documentboxes"
import { hourrates } from "./hourrates"
import { projecttypes } from "./projecttypes"
import { checks } from "./checks"
import { services } from "./services"
import { events } from "./events"
import { inventoryitemgroups } from "./inventoryitemgroups"
import { authUsers } from "./auth_users"
import {files} from "./files";
export const historyitems = pgTable("historyitems", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
text: text("text").notNull(),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id,
{ onDelete: "cascade" }
),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
project: bigint("project", { mode: "number" }).references(
() => projects.id,
{ onDelete: "cascade" }
),
plant: bigint("plant", { mode: "number" }).references(
() => plants.id,
{ onDelete: "cascade" }
),
incomingInvoice: bigint("incomingInvoice", { mode: "number" }).references(
() => incominginvoices.id,
{ onDelete: "cascade" }
),
contact: bigint("contact", { mode: "number" }).references(() => contacts.id, {
onDelete: "cascade",
}),
inventoryitem: bigint("inventoryitem", { mode: "number" }).references(
() => inventoryItems.id,
{ onDelete: "cascade" }
),
product: bigint("product", { mode: "number" }).references(
() => products.id,
{ onDelete: "cascade" }
),
event: bigint("event", { mode: "number" }).references(() => events.id),
newVal: text("newVal"),
oldVal: text("oldVal"),
task: bigint("task", { mode: "number" }).references(() => tasks.id),
vehicle: bigint("vehicle", { mode: "number" }).references(() => vehicles.id),
bankstatement: bigint("bankstatement", { mode: "number" }).references(
() => bankstatements.id
),
space: bigint("space", { mode: "number" }).references(() => spaces.id),
config: jsonb("config"),
projecttype: bigint("projecttype", { mode: "number" }).references(
() => projecttypes.id
),
check: uuid("check").references(() => checks.id),
service: bigint("service", { mode: "number" }).references(
() => services.id
),
createddocument: bigint("createddocument", { mode: "number" }).references(
() => createddocuments.id
),
file: uuid("file").references(() => files.id),
inventoryitemgroup: uuid("inventoryitemgroup").references(
() => inventoryitemgroups.id
),
source: text("source").default("Software"),
costcentre: uuid("costcentre").references(() => costcentres.id),
ownaccount: uuid("ownaccount").references(() => ownaccounts.id),
documentbox: uuid("documentbox").references(() => documentboxes.id),
hourrate: uuid("hourrate").references(() => hourrates.id),
createdBy: uuid("created_by").references(() => authUsers.id),
action: text("action"),
})
export type HistoryItem = typeof historyitems.$inferSelect
export type NewHistoryItem = typeof historyitems.$inferInsert

18
db/schema/holidays.ts Normal file
View File

@@ -0,0 +1,18 @@
import { pgTable, bigint, date, text, timestamp } from "drizzle-orm/pg-core"
export const holidays = pgTable("holidays", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedAlwaysAsIdentity(),
date: date("date").notNull(),
name: text("name").notNull(),
stateCode: text("state_code").notNull(),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
})
export type Holiday = typeof holidays.$inferSelect
export type NewHoliday = typeof holidays.$inferInsert

27
db/schema/hourrates.ts Normal file
View File

@@ -0,0 +1,27 @@
import { pgTable, uuid, timestamp, text, boolean, bigint, doublePrecision } from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const hourrates = pgTable("hourrates", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true }).notNull().defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
purchasePrice: doublePrecision("purchasePrice").notNull(),
sellingPrice: doublePrecision("sellingPrice").notNull(),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type HourRate = typeof hourrates.$inferSelect
export type NewHourRate = typeof hourrates.$inferInsert

View File

@@ -0,0 +1,63 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { vendors } from "./vendors"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const incominginvoices = pgTable("incominginvoices", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
state: text("state").notNull().default("Entwurf"),
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
reference: text("reference"),
date: text("date"),
document: bigint("document", { mode: "number" }),
dueDate: text("dueDate"),
description: text("description"),
paymentType: text("paymentType"),
accounts: jsonb("accounts").notNull().default([
{
account: null,
taxType: null,
amountNet: null,
amountTax: 19,
costCentre: null,
},
]),
paid: boolean("paid").notNull().default(false),
expense: boolean("expense").notNull().default(true),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
archived: boolean("archived").notNull().default(false),
})
export type IncomingInvoice = typeof incominginvoices.$inferSelect
export type NewIncomingInvoice = typeof incominginvoices.$inferInsert

70
db/schema/index.ts Normal file
View File

@@ -0,0 +1,70 @@
export * from "./accounts"
export * from "./auth_profiles"
export * from "./auth_role_permisssions"
export * from "./auth_roles"
export * from "./auth_tenant_users"
export * from "./auth_user_roles"
export * from "./auth_users"
export * from "./bankaccounts"
export * from "./bankrequisitions"
export * from "./bankstatements"
export * from "./checkexecutions"
export * from "./checks"
export * from "./citys"
export * from "./contacts"
export * from "./contracts"
export * from "./costcentres"
export * from "./countrys"
export * from "./createddocuments"
export * from "./createdletters"
export * from "./customers"
export * from "./devices"
export * from "./documentboxes"
export * from "./enums"
export * from "./events"
export * from "./files"
export * from "./filetags"
export * from "./folders"
export * from "./generatedexports"
export * from "./globalmessages"
export * from "./globalmessagesseen"
export * from "./helpdesk_channel_instances"
export * from "./helpdesk_channel_types"
export * from "./helpdesk_contacts"
export * from "./helpdesk_conversation_participants"
export * from "./helpdesk_conversations"
export * from "./helpdesk_messages"
export * from "./helpdesk_routing_rules"
export * from "./historyitems"
export * from "./holidays"
export * from "./hourrates"
export * from "./incominginvoices"
export * from "./inventoryitemgroups"
export * from "./inventoryitems"
export * from "./letterheads"
export * from "./movements"
export * from "./notifications_event_types"
export * from "./notifications_items"
export * from "./notifications_preferences"
export * from "./notifications_preferences_defaults"
export * from "./ownaccounts"
export * from "./plants"
export * from "./productcategories"
export * from "./products"
export * from "./projects"
export * from "./projecttypes"
export * from "./servicecategories"
export * from "./services"
export * from "./spaces"
export * from "./staff_time_entries"
export * from "./staff_time_entry_connects"
export * from "./staff_zeitstromtimestamps"
export * from "./statementallocations"
export * from "./tasks"
export * from "./taxtypes"
export * from "./tenants"
export * from "./texttemplates"
export * from "./units"
export * from "./user_credentials"
export * from "./vehicles"
export * from "./vendors"

View File

@@ -0,0 +1,39 @@
import {
pgTable,
uuid,
timestamp,
text,
boolean,
jsonb, bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const inventoryitemgroups = pgTable("inventoryitemgroups", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" }).notNull().references(() => tenants.id),
name: text("name").notNull(),
inventoryitems: jsonb("inventoryitems").notNull().default([]),
description: text("description"),
archived: boolean("archived").notNull().default(false),
profiles: jsonb("profiles").notNull().default([]),
usePlanning: boolean("usePlanning").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type InventoryItemGroup = typeof inventoryitemgroups.$inferSelect
export type NewInventoryItemGroup = typeof inventoryitemgroups.$inferInsert

View File

@@ -0,0 +1,68 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
doublePrecision,
uuid,
jsonb,
date,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { vendors } from "./vendors"
import { spaces } from "./spaces"
import { authUsers } from "./auth_users"
export const inventoryItems = pgTable("inventoryitems", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
usePlanning: boolean("usePlanning").notNull().default(false),
description: text("description"),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
currentSpace: bigint("currentSpace", { mode: "number" }).references(
() => spaces.id
),
articleNumber: text("articleNumber"),
serialNumber: text("serialNumber"),
purchaseDate: date("purchaseDate"),
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
quantity: bigint("quantity", { mode: "number" }).notNull().default(0),
purchasePrice: doublePrecision("purchasePrice").default(0),
manufacturer: text("manufacturer"),
manufacturerNumber: text("manufacturerNumber"),
currentValue: doublePrecision("currentValue"),
archived: boolean("archived").notNull().default(false),
profiles: jsonb("profiles").notNull().default([]),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() =>
authUsers.id
),
})
export type InventoryItem = typeof inventoryItems.$inferSelect
export type NewInventoryItem = typeof inventoryItems.$inferInsert

39
db/schema/letterheads.ts Normal file
View File

@@ -0,0 +1,39 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const letterheads = pgTable("letterheads", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").default("Standard"),
path: text("path").notNull(),
documentTypes: text("documentTypes").array().notNull().default([]),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
archived: boolean("archived").notNull().default(false),
})
export type Letterhead = typeof letterheads.$inferSelect
export type NewLetterhead = typeof letterheads.$inferInsert

49
db/schema/movements.ts Normal file
View File

@@ -0,0 +1,49 @@
import {
pgTable,
bigint,
timestamp,
text,
uuid,
} from "drizzle-orm/pg-core"
import { products } from "./products"
import { spaces } from "./spaces"
import { tenants } from "./tenants"
import { projects } from "./projects"
import { authUsers } from "./auth_users"
export const movements = pgTable("movements", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
quantity: bigint("quantity", { mode: "number" }).notNull(),
productId: bigint("productId", { mode: "number" })
.notNull()
.references(() => products.id),
spaceId: bigint("spaceId", { mode: "number" }).references(() => spaces.id),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
projectId: bigint("projectId", { mode: "number" }).references(
() => projects.id
),
notes: text("notes"),
serials: text("serials").array(),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Movement = typeof movements.$inferSelect
export type NewMovement = typeof movements.$inferInsert

View File

@@ -0,0 +1,34 @@
import {
pgTable,
text,
jsonb,
boolean,
timestamp,
} from "drizzle-orm/pg-core"
import {notificationSeverityEnum} from "./enums";
export const notificationsEventTypes = pgTable("notifications_event_types", {
eventKey: text("event_key").primaryKey(),
displayName: text("display_name").notNull(),
description: text("description"),
category: text("category"),
severity: notificationSeverityEnum("severity").notNull().default("info"),
allowedChannels: jsonb("allowed_channels").notNull().default(["inapp", "email"]),
payloadSchema: jsonb("payload_schema"),
isActive: boolean("is_active").notNull().default(true),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
})
export type NotificationsEventType =
typeof notificationsEventTypes.$inferSelect
export type NewNotificationsEventType =
typeof notificationsEventTypes.$inferInsert

View File

@@ -0,0 +1,54 @@
import {
pgTable,
uuid,
bigint,
text,
jsonb,
timestamp,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { notificationsEventTypes } from "./notifications_event_types"
import {notificationChannelEnum, notificationStatusEnum} from "./enums";
export const notificationsItems = pgTable("notifications_items", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, { onDelete: "cascade", onUpdate: "cascade" }),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id, { onDelete: "cascade", onUpdate: "cascade" }),
eventType: text("event_type")
.notNull()
.references(() => notificationsEventTypes.eventKey, {
onUpdate: "cascade",
onDelete: "restrict",
}),
title: text("title").notNull(),
message: text("message").notNull(),
payload: jsonb("payload"),
channel: notificationChannelEnum("channel").notNull(),
status: notificationStatusEnum("status").notNull().default("queued"),
error: text("error"),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
sentAt: timestamp("sent_at", { withTimezone: true }),
readAt: timestamp("read_at", { withTimezone: true }),
})
export type NotificationItem = typeof notificationsItems.$inferSelect
export type NewNotificationItem = typeof notificationsItems.$inferInsert

View File

@@ -0,0 +1,60 @@
import {
pgTable,
uuid,
bigint,
text,
boolean,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { notificationsEventTypes } from "./notifications_event_types"
import {notificationChannelEnum} from "./enums";
export const notificationsPreferences = pgTable(
"notifications_preferences",
{
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
eventType: text("event_type")
.notNull()
.references(() => notificationsEventTypes.eventKey, {
onDelete: "restrict",
onUpdate: "cascade",
}),
channel: notificationChannelEnum("channel").notNull(),
enabled: boolean("enabled").notNull().default(true),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
uniquePrefs: uniqueIndex(
"notifications_preferences_tenant_id_user_id_event_type_chan_key",
).on(table.tenantId, table.userId, table.eventType, table.channel),
}),
)
export type NotificationPreference =
typeof notificationsPreferences.$inferSelect
export type NewNotificationPreference =
typeof notificationsPreferences.$inferInsert

View File

@@ -0,0 +1,52 @@
import {
pgTable,
uuid,
bigint,
text,
boolean,
timestamp,
uniqueIndex,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { notificationsEventTypes } from "./notifications_event_types"
import {notificationChannelEnum} from "./enums";
export const notificationsPreferencesDefaults = pgTable(
"notifications_preferences_defaults",
{
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id, {
onDelete: "cascade",
onUpdate: "cascade",
}),
eventKey: text("event_key")
.notNull()
.references(() => notificationsEventTypes.eventKey, {
onDelete: "restrict",
onUpdate: "cascade",
}),
channel: notificationChannelEnum("channel").notNull(),
enabled: boolean("enabled").notNull().default(true),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
},
(table) => ({
uniqueDefaults: uniqueIndex(
"notifications_preferences_defau_tenant_id_event_key_channel_key",
).on(table.tenantId, table.eventKey, table.channel),
}),
)
export type NotificationPreferenceDefault =
typeof notificationsPreferencesDefaults.$inferSelect
export type NewNotificationPreferenceDefault =
typeof notificationsPreferencesDefaults.$inferInsert

39
db/schema/ownaccounts.ts Normal file
View File

@@ -0,0 +1,39 @@
import {
pgTable,
uuid,
timestamp,
text,
boolean,
jsonb,
bigint,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const ownaccounts = pgTable("ownaccounts", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
number: text("number").notNull(),
name: text("name").notNull(),
description: text("description"),
archived: boolean("archived").notNull().default(false),
profiles: jsonb("profiles").notNull().default([]),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type OwnAccount = typeof ownaccounts.$inferSelect
export type NewOwnAccount = typeof ownaccounts.$inferInsert

56
db/schema/plants.ts Normal file
View File

@@ -0,0 +1,56 @@
import {
pgTable,
bigint,
timestamp,
text,
jsonb,
boolean,
uuid,
date,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { contracts } from "./contracts"
import { authUsers } from "./auth_users"
export const plants = pgTable("plants", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
infoData: jsonb("infoData"),
contract: bigint("contract", { mode: "number" }).references(
() => contracts.id
),
description: jsonb("description").default({
html: "",
json: [],
text: "",
}),
archived: boolean("archived").notNull().default(false),
profiles: jsonb("profiles").notNull().default([]),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Plant = typeof plants.$inferSelect
export type NewPlant = typeof plants.$inferInsert

View File

@@ -0,0 +1,37 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const productCategories = pgTable("productcategories", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
description: text("description"),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type ProductCategory = typeof productCategories.$inferSelect
export type NewProductCategory = typeof productCategories.$inferInsert

69
db/schema/products.ts Normal file
View File

@@ -0,0 +1,69 @@
import {
pgTable,
bigint,
timestamp,
text,
doublePrecision,
boolean,
smallint,
uuid,
jsonb,
json,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { units } from "./units"
import { authUsers } from "./auth_users"
export const products = pgTable("products", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
manufacturer: text("manufacturer"),
unit: bigint("unit", { mode: "number" })
.notNull()
.references(() => units.id),
tags: json("tags").notNull().default([]),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
ean: text("ean"),
barcode: text("barcode"),
purchasePrice: doublePrecision("purchasePrice"),
sellingPrice: doublePrecision("sellingPrice"),
description: text("description"),
manufacturerNumber: text("manufacturerNumber"),
vendorAllocation: jsonb("vendorAllocation").default([]),
articleNumber: text("articleNumber"),
barcodes: text("barcodes").array().notNull().default([]),
productcategories: jsonb("productcategories").default([]),
archived: boolean("archived").notNull().default(false),
taxPercentage: smallint("taxPercentage").notNull().default(19),
markupPercentage: doublePrecision("markupPercentage"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Product = typeof products.$inferSelect
export type NewProduct = typeof products.$inferInsert

78
db/schema/projects.ts Normal file
View File

@@ -0,0 +1,78 @@
import {
pgTable,
bigint,
timestamp,
text,
jsonb,
json,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { customers } from "./customers"
import { contracts } from "./contracts"
import { projecttypes } from "./projecttypes"
import { authUsers } from "./auth_users"
export const projects = pgTable("projects", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
notes: text("notes"),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
phases: jsonb("phases").default([]),
description: json("description"),
forms: jsonb("forms").default([]),
heroId: text("heroId"),
measure: text("measure"),
material: jsonb("material"),
plant: bigint("plant", { mode: "number" }),
profiles: uuid("profiles").array().notNull().default([]),
projectNumber: text("projectNumber"),
contract: bigint("contract", { mode: "number" }).references(
() => contracts.id
),
projectType: text("projectType").default("Projekt"),
projecttype: bigint("projecttype", { mode: "number" }).references(
() => projecttypes.id
),
archived: boolean("archived").notNull().default(false),
customerRef: text("customerRef"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
activePhase: text("active_phase"),
})
export type Project = typeof projects.$inferSelect
export type NewProject = typeof projects.$inferInsert

41
db/schema/projecttypes.ts Normal file
View File

@@ -0,0 +1,41 @@
import {
pgTable,
bigint,
timestamp,
text,
jsonb,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const projecttypes = pgTable("projecttypes", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
initialPhases: jsonb("initialPhases"),
addablePhases: jsonb("addablePhases"),
icon: text("icon"),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type ProjectType = typeof projecttypes.$inferSelect
export type NewProjectType = typeof projecttypes.$inferInsert

View File

@@ -0,0 +1,39 @@
import {
pgTable,
bigint,
timestamp,
text,
doublePrecision,
boolean,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const serviceCategories = pgTable("servicecategories", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
description: text("description"),
discount: doublePrecision("discount").default(0),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type ServiceCategory = typeof serviceCategories.$inferSelect
export type NewServiceCategory = typeof serviceCategories.$inferInsert

63
db/schema/services.ts Normal file
View File

@@ -0,0 +1,63 @@
import {
pgTable,
bigint,
timestamp,
text,
doublePrecision,
jsonb,
boolean,
smallint,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { units } from "./units"
import { authUsers } from "./auth_users"
export const services = pgTable("services", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
sellingPrice: doublePrecision("sellingPrice"),
description: text("description"),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
unit: bigint("unit", { mode: "number" }).references(() => units.id),
serviceNumber: bigint("serviceNumber", { mode: "number" }),
tags: jsonb("tags").default([]),
servicecategories: jsonb("servicecategories").notNull().default([]),
archived: boolean("archived").notNull().default(false),
purchasePriceComposed: jsonb("purchasePriceComposed")
.notNull()
.default({ total: 0 }),
sellingPriceComposed: jsonb("sellingPriceComposed")
.notNull()
.default({ total: 0 }),
taxPercentage: smallint("taxPercentage").notNull().default(19),
materialComposition: jsonb("materialComposition").notNull().default([]),
personalComposition: jsonb("personalComposition").notNull().default([]),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Service = typeof services.$inferSelect
export type NewService = typeof services.$inferInsert

49
db/schema/spaces.ts Normal file
View File

@@ -0,0 +1,49 @@
import {
pgTable,
bigint,
timestamp,
text,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const spaces = pgTable("spaces", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name"),
type: text("type").notNull(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
spaceNumber: text("spaceNumber").notNull(),
parentSpace: bigint("parentSpace", { mode: "number" }).references(
() => spaces.id
),
infoData: jsonb("infoData")
.notNull()
.default({ zip: "", city: "", streetNumber: "" }),
description: text("description"),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Space = typeof spaces.$inferSelect
export type NewSpace = typeof spaces.$inferInsert

View File

@@ -0,0 +1,68 @@
import {
pgTable,
uuid,
bigint,
timestamp,
integer,
text,
boolean,
numeric,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { timesStateEnum } from "./enums"
import {sql} from "drizzle-orm";
export const staffTimeEntries = pgTable("staff_time_entries", {
id: uuid("id").primaryKey().defaultRandom(),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id, { onDelete: "cascade" }),
startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
stoppedAt: timestamp("stopped_at", { withTimezone: true }),
durationMinutes: integer("duration_minutes").generatedAlwaysAs(
sql`CASE
WHEN stopped_at IS NOT NULL
THEN (EXTRACT(epoch FROM (stopped_at - started_at)) / 60)
ELSE NULL
END`
),
type: text("type").default("work"),
description: text("description"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
archived: boolean("archived").notNull().default(false),
updatedBy: uuid("updated_by").references(() => authUsers.id),
source: text("source"),
state: timesStateEnum("state").notNull().default("draft"),
device: uuid("device"),
internalNote: text("internal_note"),
vacationReason: text("vacation_reason"),
vacationDays: numeric("vacation_days", { precision: 5, scale: 2 }),
approvedBy: uuid("approved_by").references(() => authUsers.id),
approvedAt: timestamp("approved_at", { withTimezone: true }),
sickReason: text("sick_reason"),
})
export type StaffTimeEntry = typeof staffTimeEntries.$inferSelect
export type NewStaffTimeEntry = typeof staffTimeEntries.$inferInsert

View File

@@ -0,0 +1,38 @@
import {
pgTable,
uuid,
bigint,
timestamp,
integer,
text,
} from "drizzle-orm/pg-core"
import { staffTimeEntries } from "./staff_time_entries"
import {sql} from "drizzle-orm";
export const staffTimeEntryConnects = pgTable("staff_time_entry_connects", {
id: uuid("id").primaryKey().defaultRandom(),
timeEntryId: uuid("time_entry_id")
.notNull()
.references(() => staffTimeEntries.id, { onDelete: "cascade" }),
projectId: bigint("project_id", { mode: "number" }), // referenziert später projects.id
startedAt: timestamp("started_at", { withTimezone: true }).notNull(),
stoppedAt: timestamp("stopped_at", { withTimezone: true }).notNull(),
durationMinutes: integer("duration_minutes").generatedAlwaysAs(
sql`(EXTRACT(epoch FROM (stopped_at - started_at)) / 60)`
),
notes: text("notes"),
createdAt: timestamp("created_at", { withTimezone: true }).defaultNow(),
updatedAt: timestamp("updated_at", { withTimezone: true }).defaultNow(),
})
export type StaffTimeEntryConnect =
typeof staffTimeEntryConnects.$inferSelect
export type NewStaffTimeEntryConnect =
typeof staffTimeEntryConnects.$inferInsert

View File

@@ -0,0 +1,44 @@
import {
pgTable,
uuid,
timestamp,
bigint,
text,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authProfiles } from "./auth_profiles"
import { staffTimeEntries } from "./staff_time_entries"
export const staffZeitstromTimestamps = pgTable("staff_zeitstromtimestamps", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
profile: uuid("profile")
.notNull()
.references(() => authProfiles.id),
key: text("key").notNull(),
intent: text("intent").notNull(),
time: timestamp("time", { withTimezone: true }).notNull(),
staffTimeEntry: uuid("staff_time_entry").references(
() => staffTimeEntries.id
),
internalNote: text("internal_note"),
})
export type StaffZeitstromTimestamp =
typeof staffZeitstromTimestamps.$inferSelect
export type NewStaffZeitstromTimestamp =
typeof staffZeitstromTimestamps.$inferInsert

View File

@@ -0,0 +1,69 @@
import {
pgTable,
uuid,
bigint,
integer,
text,
timestamp,
boolean,
doublePrecision,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { customers } from "./customers"
import { vendors } from "./vendors"
import { ownaccounts } from "./ownaccounts"
import { incominginvoices } from "./incominginvoices"
import { createddocuments } from "./createddocuments"
import { bankstatements } from "./bankstatements"
import { accounts } from "./accounts" // Falls noch nicht erstellt → bitte melden!
export const statementAllocations = pgTable("statementallocations", {
id: uuid("id").primaryKey().defaultRandom(),
// foreign keys
bsId: integer("bs_id")
.notNull()
.references(() => bankstatements.id),
cdId: integer("cd_id").references(() => createddocuments.id),
amount: doublePrecision("amount").notNull().default(0),
iiId: bigint("ii_id", { mode: "number" }).references(
() => incominginvoices.id
),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
account: bigint("account", { mode: "number" }).references(
() => accounts.id
),
createdAt: timestamp("created_at", {
withTimezone: false,
}).defaultNow(),
ownaccount: uuid("ownaccount").references(() => ownaccounts.id),
description: text("description"),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
vendor: bigint("vendor", { mode: "number" }).references(() => vendors.id),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
archived: boolean("archived").notNull().default(false),
})
export type StatementAllocation = typeof statementAllocations.$inferSelect
export type NewStatementAllocation =
typeof statementAllocations.$inferInsert

51
db/schema/tasks.ts Normal file
View File

@@ -0,0 +1,51 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { customers } from "./customers"
export const tasks = pgTable("tasks", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
description: text("description"),
categorie: text("categorie"),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
// FIXED: user_id statt profile, verweist auf auth_users.id
userId: uuid("user_id").references(() => authUsers.id),
project: bigint("project", { mode: "number" }),
plant: bigint("plant", { mode: "number" }),
customer: bigint("customer", { mode: "number" }).references(
() => customers.id
),
profiles: jsonb("profiles").notNull().default([]),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Task = typeof tasks.$inferSelect
export type NewTask = typeof tasks.$inferInsert

28
db/schema/taxtypes.ts Normal file
View File

@@ -0,0 +1,28 @@
import {
pgTable,
bigint,
timestamp,
text,
uuid,
} from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
export const taxTypes = pgTable("taxtypes", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
label: text("label").notNull(),
percentage: bigint("percentage", { mode: "number" }).notNull(),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type TaxType = typeof taxTypes.$inferSelect
export type NewTaxType = typeof taxTypes.$inferInsert

140
db/schema/tenants.ts Normal file
View File

@@ -0,0 +1,140 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
integer,
smallint,
date,
uuid,
pgEnum,
} from "drizzle-orm/pg-core"
import { authUsers } from "./auth_users"
import {lockedTenantEnum} from "./enums";
export const tenants = pgTable(
"tenants",
{
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
short: text("short").notNull(),
calendarConfig: jsonb("calendarConfig").default({
eventTypes: [
{ color: "blue", label: "Büro" },
{ color: "yellow", label: "Besprechung" },
{ color: "green", label: "Umsetzung" },
{ color: "red", label: "Vor Ort Termin" },
],
}),
timeConfig: jsonb("timeConfig").notNull().default({}),
tags: jsonb("tags").notNull().default({
products: [],
documents: [],
}),
measures: jsonb("measures")
.notNull()
.default([
{ name: "Netzwerktechnik", short: "NWT" },
{ name: "Elektrotechnik", short: "ELT" },
{ name: "Photovoltaik", short: "PV" },
{ name: "Videüberwachung", short: "VÜA" },
{ name: "Projekt", short: "PRJ" },
{ name: "Smart Home", short: "SHO" },
]),
businessInfo: jsonb("businessInfo").default({
zip: "",
city: "",
name: "",
street: "",
}),
features: jsonb("features").default({
objects: true,
calendar: true,
contacts: true,
projects: true,
vehicles: true,
contracts: true,
inventory: true,
accounting: true,
timeTracking: true,
planningBoard: true,
workingTimeTracking: true,
}),
ownFields: jsonb("ownFields"),
numberRanges: jsonb("numberRanges")
.notNull()
.default({
vendors: { prefix: "", suffix: "", nextNumber: 10000 },
customers: { prefix: "", suffix: "", nextNumber: 10000 },
products: { prefix: "AT-", suffix: "", nextNumber: 1000 },
quotes: { prefix: "AN-", suffix: "", nextNumber: 1000 },
confirmationOrders: { prefix: "AB-", suffix: "", nextNumber: 1000 },
invoices: { prefix: "RE-", suffix: "", nextNumber: 1000 },
spaces: { prefix: "LP-", suffix: "", nextNumber: 1000 },
inventoryitems: { prefix: "IA-", suffix: "", nextNumber: 1000 },
projects: { prefix: "PRJ-", suffix: "", nextNumber: 1000 },
costcentres: { prefix: "KST-", suffix: "", nextNumber: 1000 },
}),
standardEmailForInvoices: text("standardEmailForInvoices"),
extraModules: jsonb("extraModules").notNull().default([]),
isInTrial: boolean("isInTrial").default(false),
trialEndDate: date("trialEndDate"),
stripeCustomerId: text("stripeCustomerId"),
hasActiveLicense: boolean("hasActiveLicense").notNull().default(false),
userLicenseCount: integer("userLicenseCount")
.notNull()
.default(0),
workstationLicenseCount: integer("workstationLicenseCount")
.notNull()
.default(0),
standardPaymentDays: smallint("standardPaymentDays")
.notNull()
.default(14),
dokuboxEmailAddresses: jsonb("dokuboxEmailAddresses").default([]),
dokuboxkey: uuid("dokuboxkey").notNull().defaultRandom(),
autoPrepareIncomingInvoices: boolean("autoPrepareIncomingInvoices")
.default(true),
portalDomain: text("portalDomain"),
portalConfig: jsonb("portalConfig")
.notNull()
.default({ primayColor: "#69c350" }),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
locked: lockedTenantEnum("locked"),
}
)
export type Tenant = typeof tenants.$inferSelect
export type NewTenant = typeof tenants.$inferInsert

View File

@@ -0,0 +1,44 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import { textTemplatePositionsEnum } from "./enums"
export const textTemplates = pgTable("texttemplates", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
name: text("name").notNull(),
text: text("text").notNull(),
documentType: text("documentType").default(""),
default: boolean("default").notNull().default(false),
pos: textTemplatePositionsEnum("pos").notNull(),
archived: boolean("archived").notNull().default(false),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type TextTemplate = typeof textTemplates.$inferSelect
export type NewTextTemplate = typeof textTemplates.$inferInsert

27
db/schema/units.ts Normal file
View File

@@ -0,0 +1,27 @@
import {
pgTable,
bigint,
timestamp,
text,
} from "drizzle-orm/pg-core"
export const units = pgTable("units", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
single: text("single").notNull(),
multiple: text("multiple"),
short: text("short"),
step: text("step").notNull().default("1"),
})
export type Unit = typeof units.$inferSelect
export type NewUnit = typeof units.$inferInsert

View File

@@ -0,0 +1,53 @@
import {
pgTable,
uuid,
timestamp,
bigint,
boolean,
jsonb,
numeric, pgEnum,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
import {credentialTypesEnum} from "./enums";
export const userCredentials = pgTable("user_credentials", {
id: uuid("id").primaryKey().defaultRandom(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
userId: uuid("user_id")
.notNull()
.references(() => authUsers.id),
updatedAt: timestamp("updated_at", { withTimezone: true }),
tenantId: bigint("tenant_id", { mode: "number" })
.notNull()
.references(() => tenants.id),
smtpPort: numeric("smtp_port"),
smtpSsl: boolean("smtp_ssl"),
type: credentialTypesEnum("type").notNull(),
imapPort: numeric("imap_port"),
imapSsl: boolean("imap_ssl"),
emailEncrypted: jsonb("email_encrypted"),
passwordEncrypted: jsonb("password_encrypted"),
smtpHostEncrypted: jsonb("smtp_host_encrypted"),
imapHostEncrypted: jsonb("imap_host_encrypted"),
accessTokenEncrypted: jsonb("access_token_encrypted"),
refreshTokenEncrypted: jsonb("refresh_token_encrypted"),
})
export type UserCredential = typeof userCredentials.$inferSelect
export type NewUserCredential = typeof userCredentials.$inferInsert

57
db/schema/vehicles.ts Normal file
View File

@@ -0,0 +1,57 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
uuid,
doublePrecision,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const vehicles = pgTable("vehicles", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
licensePlate: text("licensePlate"),
name: text("name"),
type: text("type"),
active: boolean("active").default(true),
// FIXED: driver references auth_users.id
driver: uuid("driver").references(() => authUsers.id),
vin: text("vin"),
tankSize: doublePrecision("tankSize").notNull().default(0),
archived: boolean("archived").notNull().default(false),
buildYear: text("buildYear"),
towingCapacity: bigint("towingCapacity", { mode: "number" }),
powerInKW: bigint("powerInKW", { mode: "number" }),
color: text("color"),
profiles: jsonb("profiles").notNull().default([]),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Vehicle = typeof vehicles.$inferSelect
export type NewVehicle = typeof vehicles.$inferInsert

45
db/schema/vendors.ts Normal file
View File

@@ -0,0 +1,45 @@
import {
pgTable,
bigint,
text,
timestamp,
boolean,
jsonb,
uuid,
} from "drizzle-orm/pg-core"
import { tenants } from "./tenants"
import { authUsers } from "./auth_users"
export const vendors = pgTable("vendors", {
id: bigint("id", { mode: "number" })
.primaryKey()
.generatedByDefaultAsIdentity(),
createdAt: timestamp("created_at", { withTimezone: true })
.notNull()
.defaultNow(),
name: text("name").notNull(),
vendorNumber: text("vendorNumber").notNull(),
tenant: bigint("tenant", { mode: "number" })
.notNull()
.references(() => tenants.id),
infoData: jsonb("infoData").notNull().default({}),
notes: text("notes"),
hasSEPA: boolean("hasSEPA").notNull().default(false),
profiles: jsonb("profiles").notNull().default([]),
archived: boolean("archived").notNull().default(false),
defaultPaymentMethod: text("defaultPaymentMethod"),
updatedAt: timestamp("updated_at", { withTimezone: true }),
updatedBy: uuid("updated_by").references(() => authUsers.id),
})
export type Vendor = typeof vendors.$inferSelect
export type NewVendor = typeof vendors.$inferInsert

11
drizzle.config.ts Normal file
View File

@@ -0,0 +1,11 @@
import { defineConfig } from "drizzle-kit"
import {secrets} from "./src/utils/secrets";
export default defineConfig({
dialect: "postgresql",
schema: "./db/schema",
out: "./db/migrations",
dbCredentials: {
url: secrets.DATABASE_URL || process.env.DATABASE_URL,
},
})

797
package-lock.json generated
View File

@@ -28,12 +28,14 @@
"canvas": "^3.2.0",
"crypto": "^1.0.1",
"dayjs": "^1.11.18",
"drizzle-orm": "^0.45.0",
"fastify": "^5.5.0",
"fastify-plugin": "^5.0.1",
"imapflow": "^1.1.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^7.0.6",
"pdf-lib": "^1.17.1",
"pg": "^8.16.3",
"pngjs": "^7.0.0",
"sharp": "^0.34.5",
"xmlbuilder": "^15.1.1",
@@ -44,6 +46,7 @@
"@types/bcrypt": "^6.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.3.0",
"drizzle-kit": "^0.31.8",
"prisma": "^6.15.0",
"tsx": "^4.20.5",
"typescript": "^5.9.2"
@@ -5273,6 +5276,13 @@
"tslib": "^2.1.0"
}
},
"node_modules/@drizzle-team/brocli": {
"version": "0.10.2",
"resolved": "https://registry.npmjs.org/@drizzle-team/brocli/-/brocli-0.10.2.tgz",
"integrity": "sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==",
"dev": true,
"license": "Apache-2.0"
},
"node_modules/@emnapi/runtime": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.7.0.tgz",
@@ -5283,6 +5293,442 @@
"tslib": "^2.4.0"
}
},
"node_modules/@esbuild-kit/core-utils": {
"version": "3.3.2",
"resolved": "https://registry.npmjs.org/@esbuild-kit/core-utils/-/core-utils-3.3.2.tgz",
"integrity": "sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"esbuild": "~0.18.20",
"source-map-support": "^0.5.21"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.18.20.tgz",
"integrity": "sha512-fyi7TDI/ijKKNZTUJAQqiG5T7YjJXgnzkURqmGj13C6dCqckZBLdl4h7bkhHt/t0WP+zO9/zwroDvANaOqO5Sw==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.18.20.tgz",
"integrity": "sha512-Nz4rJcchGDtENV0eMKUNa6L12zz2zBDXuhj/Vjh18zGqB44Bi7MBMSXjgunJgjRhCmKOjnPuZp4Mb6OKqtMHLQ==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/android-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.18.20.tgz",
"integrity": "sha512-8GDdlePJA8D6zlZYJV/jnrRAi6rOiNaCC/JclcXpB+KIuvfBN4owLtgzY2bsxnx666XjJx2kDPUmnTtR8qKQUg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.18.20.tgz",
"integrity": "sha512-bxRHW5kHU38zS2lPTPOyuyTm+S+eobPUnTNkdJEfAddYgEcll4xkT8DB9d2008DtTbl7uJag2HuE5NZAZgnNEA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/darwin-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.18.20.tgz",
"integrity": "sha512-pc5gxlMDxzm513qPGbCbDukOdsGtKhfxD1zJKXjCCcU7ju50O7MeAZ8c4krSJcOIJGFR+qx21yMMVYwiQvyTyQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.18.20.tgz",
"integrity": "sha512-yqDQHy4QHevpMAaxhhIwYPMv1NECwOvIpGCZkECn8w2WFHXjEwrBn3CeNIYsibZ/iZEUemj++M26W3cNR5h+Tw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/freebsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.18.20.tgz",
"integrity": "sha512-tgWRPPuQsd3RmBZwarGVHZQvtzfEBOreNuxEMKFcd5DaDn2PbBxfwLcj4+aenoh7ctXcbXmOQIn8HI6mCSw5MQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.18.20.tgz",
"integrity": "sha512-/5bHkMWnq1EgKr1V+Ybz3s1hWXok7mDFUMQ4cG10AfW3wL02PSZi5kFpYKrptDsgb2WAJIvRcDm+qIvXf/apvg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.18.20.tgz",
"integrity": "sha512-2YbscF+UL7SQAVIpnWvYwM+3LskyDmPhe31pE7/aoTMFKKzIc9lLbyGUpmmb8a8AixOL61sQ/mFh3jEjHYFvdA==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.18.20.tgz",
"integrity": "sha512-P4etWwq6IsReT0E1KHU40bOnzMHoH73aXp96Fs8TIT6z9Hu8G6+0SHSw9i2isWrD2nbx2qo5yUqACgdfVGx7TA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-loong64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.18.20.tgz",
"integrity": "sha512-nXW8nqBTrOpDLPgPY9uV+/1DjxoQ7DoB2N8eocyq8I9XuqJ7BiAMDMf9n1xZM9TgW0J8zrquIb/A7s3BJv7rjg==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-mips64el": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.18.20.tgz",
"integrity": "sha512-d5NeaXZcHp8PzYy5VnXV3VSd2D328Zb+9dEq5HE6bw6+N86JVPExrA6O68OPwobntbNJ0pzCpUFZTo3w0GyetQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-ppc64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.18.20.tgz",
"integrity": "sha512-WHPyeScRNcmANnLQkq6AfyXRFr5D6N2sKgkFo2FqguP44Nw2eyDlbTdZwd9GYk98DZG9QItIiTlFLHJHjxP3FA==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-riscv64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.18.20.tgz",
"integrity": "sha512-WSxo6h5ecI5XH34KC7w5veNnKkju3zBRLEQNY7mv5mtBmrP/MjNBCAlsM2u5hDBlS3NGcTQpoBvRzqBcRtpq1A==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-s390x": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.18.20.tgz",
"integrity": "sha512-+8231GMs3mAEth6Ja1iK0a1sQ3ohfcpzpRLH8uuc5/KVDFneH6jtAJLFGafpzpMRO6DzJ6AvXKze9LfFMrIHVQ==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/linux-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.18.20.tgz",
"integrity": "sha512-UYqiqemphJcNsFEskc73jQ7B9jgwjWrSayxawS6UVFZGWrAAtkzjxSqnoclCXxWtfwLdzU+vTpcNYhpn43uP1w==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/netbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.18.20.tgz",
"integrity": "sha512-iO1c++VP6xUBUmltHZoMtCUdPlnPGdBom6IrO4gyKPFFVBKioIImVooR5I83nTew5UOYrk3gIJhbZh8X44y06A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/openbsd-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.18.20.tgz",
"integrity": "sha512-e5e4YSsuQfX4cxcygw/UCPIEP6wbIL+se3sxPdCiMbFLBWu0eiZOJ7WoD+ptCLrmjZBK1Wk7I6D/I3NglUGOxg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/sunos-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.18.20.tgz",
"integrity": "sha512-kDbFRFp0YpTQVVrqUd5FTYmWo45zGaXe0X8E1G/LKFC0v8x0vWrhOWSLITcCn63lmZIxfOMXtCfti/RxN/0wnQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-arm64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.18.20.tgz",
"integrity": "sha512-ddYFR6ItYgoaq4v4JmQQaAI5s7npztfV4Ag6NrhiaW0RrnOXqBkgwZLofVTlq1daVTQNhtI5oieTvkRPfZrePg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-ia32": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.18.20.tgz",
"integrity": "sha512-Wv7QBi3ID/rROT08SABTS7eV4hX26sVduqDOTe1MvGMjNd3EjOz4b7zeexIR62GTIEKrfJXKL9LFxTYgkyeu7g==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/@esbuild/win32-x64": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.18.20.tgz",
"integrity": "sha512-kTdfRcSiDfQca/y9QIkng02avJ+NCaQvrMejlsB3RRv5sE9rRoeBPISaZpKxHELzRxZyLvNts1P27W3wV+8geQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=12"
}
},
"node_modules/@esbuild-kit/core-utils/node_modules/esbuild": {
"version": "0.18.20",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.18.20.tgz",
"integrity": "sha512-ceqxoedUrcayh7Y7ZX6NdbbDzGROiyVBgC4PriJThBKSVPWnnFHZAkfI1lJT8QFkOwH4qOS2SJkS4wvpGl8BpA==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=12"
},
"optionalDependencies": {
"@esbuild/android-arm": "0.18.20",
"@esbuild/android-arm64": "0.18.20",
"@esbuild/android-x64": "0.18.20",
"@esbuild/darwin-arm64": "0.18.20",
"@esbuild/darwin-x64": "0.18.20",
"@esbuild/freebsd-arm64": "0.18.20",
"@esbuild/freebsd-x64": "0.18.20",
"@esbuild/linux-arm": "0.18.20",
"@esbuild/linux-arm64": "0.18.20",
"@esbuild/linux-ia32": "0.18.20",
"@esbuild/linux-loong64": "0.18.20",
"@esbuild/linux-mips64el": "0.18.20",
"@esbuild/linux-ppc64": "0.18.20",
"@esbuild/linux-riscv64": "0.18.20",
"@esbuild/linux-s390x": "0.18.20",
"@esbuild/linux-x64": "0.18.20",
"@esbuild/netbsd-x64": "0.18.20",
"@esbuild/openbsd-x64": "0.18.20",
"@esbuild/sunos-x64": "0.18.20",
"@esbuild/win32-arm64": "0.18.20",
"@esbuild/win32-ia32": "0.18.20",
"@esbuild/win32-x64": "0.18.20"
}
},
"node_modules/@esbuild-kit/esm-loader": {
"version": "2.6.5",
"resolved": "https://registry.npmjs.org/@esbuild-kit/esm-loader/-/esm-loader-2.6.5.tgz",
"integrity": "sha512-FxEMIkJKnodyA1OaCUoEvbYRkoZlLZ4d/eXFu9Fh8CbBBgP5EmZxrfTRyN0qpXZ4vOvqnE5YdRdcrmUUXuU+dA==",
"deprecated": "Merged into tsx: https://tsx.is",
"dev": true,
"license": "MIT",
"dependencies": {
"@esbuild-kit/core-utils": "^3.3.2",
"get-tsconfig": "^4.7.0"
}
},
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.9",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.9.tgz",
@@ -7686,9 +8132,9 @@
}
},
"node_modules/archiver-utils/node_modules/glob": {
"version": "10.4.5",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz",
"integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==",
"version": "10.5.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz",
"integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==",
"license": "ISC",
"dependencies": {
"foreground-child": "^3.1.0",
@@ -7964,6 +8410,13 @@
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
"integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==",
"dev": true,
"license": "MIT"
},
"node_modules/bwip-js": {
"version": "4.8.0",
"resolved": "https://registry.npmjs.org/bwip-js/-/bwip-js-4.8.0.tgz",
@@ -8323,6 +8776,147 @@
"url": "https://dotenvx.com"
}
},
"node_modules/drizzle-kit": {
"version": "0.31.8",
"resolved": "https://registry.npmjs.org/drizzle-kit/-/drizzle-kit-0.31.8.tgz",
"integrity": "sha512-O9EC/miwdnRDY10qRxM8P3Pg8hXe3LyU4ZipReKOgTwn4OqANmftj8XJz1UPUAS6NMHf0E2htjsbQujUTkncCg==",
"dev": true,
"license": "MIT",
"dependencies": {
"@drizzle-team/brocli": "^0.10.2",
"@esbuild-kit/esm-loader": "^2.5.5",
"esbuild": "^0.25.4",
"esbuild-register": "^3.5.0"
},
"bin": {
"drizzle-kit": "bin.cjs"
}
},
"node_modules/drizzle-orm": {
"version": "0.45.0",
"resolved": "https://registry.npmjs.org/drizzle-orm/-/drizzle-orm-0.45.0.tgz",
"integrity": "sha512-lyd9VRk3SXKRjV/gQckQzmJgkoYMvVG3A2JAV0vh3L+Lwk+v9+rK5Gj0H22y+ZBmxsrRBgJ5/RbQCN7DWd1dtQ==",
"license": "Apache-2.0",
"peerDependencies": {
"@aws-sdk/client-rds-data": ">=3",
"@cloudflare/workers-types": ">=4",
"@electric-sql/pglite": ">=0.2.0",
"@libsql/client": ">=0.10.0",
"@libsql/client-wasm": ">=0.10.0",
"@neondatabase/serverless": ">=0.10.0",
"@op-engineering/op-sqlite": ">=2",
"@opentelemetry/api": "^1.4.1",
"@planetscale/database": ">=1.13",
"@prisma/client": "*",
"@tidbcloud/serverless": "*",
"@types/better-sqlite3": "*",
"@types/pg": "*",
"@types/sql.js": "*",
"@upstash/redis": ">=1.34.7",
"@vercel/postgres": ">=0.8.0",
"@xata.io/client": "*",
"better-sqlite3": ">=7",
"bun-types": "*",
"expo-sqlite": ">=14.0.0",
"gel": ">=2",
"knex": "*",
"kysely": "*",
"mysql2": ">=2",
"pg": ">=8",
"postgres": ">=3",
"sql.js": ">=1",
"sqlite3": ">=5"
},
"peerDependenciesMeta": {
"@aws-sdk/client-rds-data": {
"optional": true
},
"@cloudflare/workers-types": {
"optional": true
},
"@electric-sql/pglite": {
"optional": true
},
"@libsql/client": {
"optional": true
},
"@libsql/client-wasm": {
"optional": true
},
"@neondatabase/serverless": {
"optional": true
},
"@op-engineering/op-sqlite": {
"optional": true
},
"@opentelemetry/api": {
"optional": true
},
"@planetscale/database": {
"optional": true
},
"@prisma/client": {
"optional": true
},
"@tidbcloud/serverless": {
"optional": true
},
"@types/better-sqlite3": {
"optional": true
},
"@types/pg": {
"optional": true
},
"@types/sql.js": {
"optional": true
},
"@upstash/redis": {
"optional": true
},
"@vercel/postgres": {
"optional": true
},
"@xata.io/client": {
"optional": true
},
"better-sqlite3": {
"optional": true
},
"bun-types": {
"optional": true
},
"expo-sqlite": {
"optional": true
},
"gel": {
"optional": true
},
"knex": {
"optional": true
},
"kysely": {
"optional": true
},
"mysql2": {
"optional": true
},
"pg": {
"optional": true
},
"postgres": {
"optional": true
},
"prisma": {
"optional": true
},
"sql.js": {
"optional": true
},
"sqlite3": {
"optional": true
}
}
},
"node_modules/dunder-proto": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz",
@@ -8484,6 +9078,19 @@
"@esbuild/win32-x64": "0.25.9"
}
},
"node_modules/esbuild-register": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"dev": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
"esbuild": ">=0.12 <1"
}
},
"node_modules/escape-html": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz",
@@ -8866,14 +9473,14 @@
"license": "MIT"
},
"node_modules/glob": {
"version": "11.0.3",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz",
"integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==",
"license": "ISC",
"version": "11.1.0",
"resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz",
"integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==",
"license": "BlueOak-1.0.0",
"dependencies": {
"foreground-child": "^3.3.1",
"jackspeak": "^4.1.1",
"minimatch": "^10.0.3",
"minimatch": "^10.1.1",
"minipass": "^7.1.2",
"package-json-from-dist": "^1.0.0",
"path-scurry": "^2.0.0"
@@ -9200,12 +9807,12 @@
}
},
"node_modules/jws": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.2.tgz",
"integrity": "sha512-YHlZCB6lMTllWDtSPHz/ZXTsi8S00usEV6v1tjq8tOUZzw7DpSDWVXjXDre6ed1w/pd495ODpHZYSdkRTsa0HA==",
"version": "3.2.3",
"resolved": "https://registry.npmjs.org/jws/-/jws-3.2.3.tgz",
"integrity": "sha512-byiJ0FLRdLdSVSReO/U4E7RoEyOCKnEnEPMjq3HxWtvzLsV08/i5RQKsFVNkCldrCaPr2vDNAOMsfs8T/Hze7g==",
"license": "MIT",
"dependencies": {
"jwa": "^1.4.1",
"jwa": "^1.4.2",
"safe-buffer": "^5.0.1"
}
},
@@ -9436,10 +10043,10 @@
}
},
"node_modules/minimatch": {
"version": "10.0.3",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz",
"integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==",
"license": "ISC",
"version": "10.1.1",
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz",
"integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==",
"license": "BlueOak-1.0.0",
"dependencies": {
"@isaacs/brace-expansion": "^5.0.0"
},
@@ -9663,6 +10270,95 @@
"devOptional": true,
"license": "MIT"
},
"node_modules/pg": {
"version": "8.16.3",
"resolved": "https://registry.npmjs.org/pg/-/pg-8.16.3.tgz",
"integrity": "sha512-enxc1h0jA/aq5oSDMvqyW3q89ra6XIIDZgCX9vkMrnz5DFTw/Ny3Li2lFQ+pt3L6MCgm/5o2o8HW9hiJji+xvw==",
"license": "MIT",
"dependencies": {
"pg-connection-string": "^2.9.1",
"pg-pool": "^3.10.1",
"pg-protocol": "^1.10.3",
"pg-types": "2.2.0",
"pgpass": "1.0.5"
},
"engines": {
"node": ">= 16.0.0"
},
"optionalDependencies": {
"pg-cloudflare": "^1.2.7"
},
"peerDependencies": {
"pg-native": ">=3.0.1"
},
"peerDependenciesMeta": {
"pg-native": {
"optional": true
}
}
},
"node_modules/pg-cloudflare": {
"version": "1.2.7",
"resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.2.7.tgz",
"integrity": "sha512-YgCtzMH0ptvZJslLM1ffsY4EuGaU0cx4XSdXLRFae8bPP4dS5xL1tNB3k2o/N64cHJpwU7dxKli/nZ2lUa5fLg==",
"license": "MIT",
"optional": true
},
"node_modules/pg-connection-string": {
"version": "2.9.1",
"resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.9.1.tgz",
"integrity": "sha512-nkc6NpDcvPVpZXxrreI/FOtX3XemeLl8E0qFr6F2Lrm/I8WOnaWNhIPK2Z7OHpw7gh5XJThi6j6ppgNoaT1w4w==",
"license": "MIT"
},
"node_modules/pg-int8": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz",
"integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==",
"license": "ISC",
"engines": {
"node": ">=4.0.0"
}
},
"node_modules/pg-pool": {
"version": "3.10.1",
"resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.10.1.tgz",
"integrity": "sha512-Tu8jMlcX+9d8+QVzKIvM/uJtp07PKr82IUOYEphaWcoBhIYkoHpLXN3qO59nAI11ripznDsEzEv8nUxBVWajGg==",
"license": "MIT",
"peerDependencies": {
"pg": ">=8.0"
}
},
"node_modules/pg-protocol": {
"version": "1.10.3",
"resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.10.3.tgz",
"integrity": "sha512-6DIBgBQaTKDJyxnXaLiLR8wBpQQcGWuAESkRBX/t6OwA8YsqP+iVSiond2EDy6Y/dsGk8rh/jtax3js5NeV7JQ==",
"license": "MIT"
},
"node_modules/pg-types": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz",
"integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==",
"license": "MIT",
"dependencies": {
"pg-int8": "1.0.1",
"postgres-array": "~2.0.0",
"postgres-bytea": "~1.0.0",
"postgres-date": "~1.0.4",
"postgres-interval": "^1.1.0"
},
"engines": {
"node": ">=4"
}
},
"node_modules/pgpass": {
"version": "1.0.5",
"resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz",
"integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==",
"license": "MIT",
"dependencies": {
"split2": "^4.1.0"
}
},
"node_modules/pino": {
"version": "9.9.0",
"resolved": "https://registry.npmjs.org/pino/-/pino-9.9.0.tgz",
@@ -9721,6 +10417,45 @@
"node": ">=14.19.0"
}
},
"node_modules/postgres-array": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz",
"integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==",
"license": "MIT",
"engines": {
"node": ">=4"
}
},
"node_modules/postgres-bytea": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz",
"integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-date": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz",
"integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==",
"license": "MIT",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/postgres-interval": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz",
"integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==",
"license": "MIT",
"dependencies": {
"xtend": "^4.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/prebuild-install": {
"version": "7.1.3",
"resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz",
@@ -10222,6 +10957,27 @@
"atomic-sleep": "^1.0.0"
}
},
"node_modules/source-map": {
"version": "0.6.1",
"resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz",
"integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==",
"dev": true,
"license": "BSD-3-Clause",
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/source-map-support": {
"version": "0.5.21",
"resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz",
"integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==",
"dev": true,
"license": "MIT",
"dependencies": {
"buffer-from": "^1.0.0",
"source-map": "^0.6.0"
}
},
"node_modules/split2": {
"version": "4.2.0",
"resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz",
@@ -10715,6 +11471,15 @@
"node": ">=8.0"
}
},
"node_modules/xtend": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz",
"integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==",
"license": "MIT",
"engines": {
"node": ">=0.4"
}
},
"node_modules/yaml": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.1.tgz",

View File

@@ -6,7 +6,8 @@
"scripts": {
"dev": "tsx watch src/index.ts",
"build": "tsc",
"start": "node dist/index.js"
"start": "node dist/index.js",
"schema:index": "ts-node scripts/generate-schema-index.ts"
},
"repository": {
"type": "git",
@@ -35,12 +36,14 @@
"canvas": "^3.2.0",
"crypto": "^1.0.1",
"dayjs": "^1.11.18",
"drizzle-orm": "^0.45.0",
"fastify": "^5.5.0",
"fastify-plugin": "^5.0.1",
"imapflow": "^1.1.1",
"jsonwebtoken": "^9.0.2",
"nodemailer": "^7.0.6",
"pdf-lib": "^1.17.1",
"pg": "^8.16.3",
"pngjs": "^7.0.0",
"sharp": "^0.34.5",
"xmlbuilder": "^15.1.1",
@@ -51,6 +54,7 @@
"@types/bcrypt": "^6.0.0",
"@types/jsonwebtoken": "^9.0.10",
"@types/node": "^24.3.0",
"drizzle-kit": "^0.31.8",
"prisma": "^6.15.0",
"tsx": "^4.20.5",
"typescript": "^5.9.2"

View File

@@ -0,0 +1,16 @@
import fs from "node:fs"
import path from "node:path"
const schemaDir = path.resolve("db/schema")
const indexFile = path.join(schemaDir, "index.ts")
const files = fs
.readdirSync(schemaDir)
.filter((f) => f.endsWith(".ts") && f !== "index.ts")
const exportsToWrite = files
.map((f) => `export * from "./${f.replace(".ts", "")}"`)
.join("\n")
fs.writeFileSync(indexFile, exportsToWrite)
console.log("✓ schema/index.ts generated")

View File

@@ -12,6 +12,7 @@ import authPlugin from "./plugins/auth";
import adminRoutes from "./routes/admin";
import corsPlugin from "./plugins/cors";
import queryConfigPlugin from "./plugins/queryconfig";
import dbPlugin from "./plugins/db";
import resourceRoutes from "./routes/resources";
import resourceRoutesSpecial from "./routes/resourcesSpecial";
import fastifyCookie from "@fastify/cookie";
@@ -57,6 +58,7 @@ async function main() {
await app.register(supabasePlugin);
await app.register(tenantPlugin);
await app.register(dayjsPlugin);
await app.register(dbPlugin);
app.addHook('preHandler', (req, reply, done) => {
console.log(req.method)
@@ -114,6 +116,14 @@ async function main() {
},{prefix: "/api"})
app.ready(async () => {
try {
const result = await app.db.execute("SELECT NOW()");
console.log("✓ DB connection OK: " + JSON.stringify(result.rows[0]));
} catch (err) {
console.log("❌ DB connection failed:", err);
}
});
// Start
try {

34
src/plugins/db.ts Normal file
View File

@@ -0,0 +1,34 @@
import fp from "fastify-plugin"
import {drizzle, NodePgDatabase} from "drizzle-orm/node-postgres"
import { Pool } from "pg"
import * as schema from "../../db/schema"
export default fp(async (server, opts) => {
const pool = new Pool({
host: "db-001.netbird.cloud",
port: Number(process.env.DB_PORT || 5432),
user: "postgres",
password: "wJw7aNpEBJdcxgoct6GXNpvY4Cn6ECqu",
database: "fedeo",
ssl: process.env.DB_DISABLE_SSL === "true" ? false : undefined,
})
// Drizzle instance
const db = drizzle(pool, { schema })
// Dekorieren -> überall server.db
server.decorate("db", db)
// Graceful Shutdown
server.addHook("onClose", async () => {
await pool.end()
})
server.log.info("Drizzle database connected")
})
declare module "fastify" {
interface FastifyInstance {
db:NodePgDatabase<typeof schema>
}
}

View File

@@ -1,13 +1,21 @@
import { FastifyInstance } from "fastify";
import bcrypt from "bcrypt";
import jwt from "jsonwebtoken";
import { generateRandomPassword, hashPassword } from "../../utils/password"
import { sendMail } from "../../utils/mailer"
import {secrets} from "../../utils/secrets";
import { generateRandomPassword, hashPassword } from "../../utils/password";
import { sendMail } from "../../utils/mailer";
import { secrets } from "../../utils/secrets";
import { authUsers } from "../../../db/schema";
import { authTenantUsers } from "../../../db/schema";
import { tenants } from "../../../db/schema";
import { eq, and } from "drizzle-orm";
export default async function authRoutes(server: FastifyInstance) {
// Registrierung
server.post("/auth/register",{
// -----------------------------------------------------
// REGISTER
// -----------------------------------------------------
server.post("/auth/register", {
schema: {
tags: ["Auth"],
summary: "Register User",
@@ -19,43 +27,31 @@ export default async function authRoutes(server: FastifyInstance) {
password: { type: "string" },
},
},
response: {
200: {
type: "object",
properties: {
user: { type: "object" },
},
},
},
},
}, async (req, reply) => {
const body = req.body as { email: string; password: string };
if (!body.email || !body.password) {
// @ts-ignore
return reply.code(400).send({ error: "Email and password required" });
}
// Passwort hashen
const passwordHash = await bcrypt.hash(body.password, 10);
// User speichern
const { data, error } = await server.supabase
.from("auth_users")
.insert({ email: body.email, password_hash: passwordHash })
.select("id, email")
.single();
const [user] = await server.db
.insert(authUsers)
.values({
email: body.email.toLowerCase(),
passwordHash,
})
.returning({
id: authUsers.id,
email: authUsers.email,
});
if (error) {
// @ts-ignore
return reply.code(400).send({ error: error.message });
}
return { user: data };
return { user };
});
// Login
server.post("/auth/login",{
// -----------------------------------------------------
// LOGIN
// -----------------------------------------------------
server.post("/auth/login", {
schema: {
tags: ["Auth"],
summary: "Login User",
@@ -67,103 +63,110 @@ export default async function authRoutes(server: FastifyInstance) {
password: { type: "string" },
},
},
response: {
200: {
type: "object",
properties: {
token: { type: "string" },
},
},
},
},
}, async (req, reply) => {
const body = req.body as { email: string; password: string };
if (!body.email || !body.password) {
// @ts-ignore
return reply.code(400).send({ error: "Email and password required" });
}
let user: any = null;
/**
* Wenn das Tenant Objekt verfügbar ist, befindet sich das Backend im Single Tenant Modus.
* Es werden nur Benutzer zugelassen, welche auschließlich diesem Tenant angehören.
* Das zeigt sich über das im User gesetzte Tenant Feld
*
* */
let user = null
let error = null
if(req.tenant) {
// User finden
const { data, error } = await server.supabase
.from("auth_users")
.select("*, tenants!auth_tenant_users(*)")
.eq("email", body.email.toLowerCase())
// -------------------------------
// SINGLE TENANT MODE
// -------------------------------
/* if (req.tenant) {
const tenantId = req.tenant.id;
// @ts-ignore
user = (data || []).find(i => i.tenants.find(x => x.id === req.tenant.id))
if(error) {
// @ts-ignore
return reply.code(500).send({ error: "Internal Server Error" });
}
} else {
// User finden
const { data, error } = await server.supabase
.from("auth_users")
.select("*")
.eq("email", body.email)
.single();
user = data
if(error) {
// @ts-ignore
return reply.code(500).send({ error: "Internal Server Error" });
}
}
if(!user) {
// @ts-ignore
return reply.code(401).send({ error: "Invalid credentials" });
} else {
const valid = await bcrypt.compare(body.password, user.password_hash);
if (!valid) {
// @ts-ignore
return reply.code(401).send({ error: "Invalid credentials" });
} else {
const token = jwt.sign(
{ user_id: user.id, email: user.email, tenant_id: null },
secrets.JWT_SECRET!,
{ expiresIn: "6h" }
);
reply.setCookie("token", token, {
path: "/",
httpOnly: true,
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
secure: process.env.NODE_ENV === "production", // lokal: false, prod: true
maxAge: 60 * 60 * 3, // 3 Stunden
const result = await server.db
.select({
user: authUsers,
})
.from(authUsers)
.innerJoin(
authTenantUsers,
eq(authTenantUsers.userId, authUsers.id)
)
.innerJoin(
tenants,
eq(authTenantUsers.tenantId, tenants.id)
)
.where(and(
eq(authUsers.email, body.email.toLowerCase()),
eq(authTenantUsers.tenantId, tenantId)
));
return { token };
if (result.length === 0) {
return reply.code(401).send({ error: "Invalid credentials" });
}
user = result[0].user;
// -------------------------------
// MULTI TENANT MODE
// -------------------------------
} else {*/
const [found] = await server.db
.select()
.from(authUsers)
.where(eq(authUsers.email, body.email.toLowerCase()))
.limit(1);
if (!found) {
return reply.code(401).send({ error: "Invalid credentials" });
}
user = found;
/*}*/
// Passwort prüfen
const valid = await bcrypt.compare(body.password, user.passwordHash);
if (!valid) {
return reply.code(401).send({ error: "Invalid credentials" });
}
const token = jwt.sign(
{
user_id: user.id,
email: user.email,
tenant_id: req.tenant?.id ?? null,
},
secrets.JWT_SECRET!,
{ expiresIn: "6h" }
);
reply.setCookie("token", token, {
path: "/",
httpOnly: true,
sameSite: process.env.NODE_ENV === "production" ? "none" : "lax",
secure: process.env.NODE_ENV === "production",
maxAge: 60 * 60 * 3,
});
return { token };
});
// -----------------------------------------------------
// LOGOUT
// -----------------------------------------------------
server.post("/auth/logout", {
schema: {
tags: ["Auth"],
summary: "Logout User (löscht Cookie)"
},
summary: "Logout User"
}
}, async (req, reply) => {
reply.clearCookie("token", {
path: "/",
httpOnly: true,
secure: process.env.NODE_ENV === "production",
sameSite: "lax",
})
});
return { success: true }
})
return { success: true };
});
// -----------------------------------------------------
// PASSWORD RESET
// -----------------------------------------------------
server.post("/auth/password/reset", {
schema: {
tags: ["Auth"],
@@ -177,43 +180,43 @@ export default async function authRoutes(server: FastifyInstance) {
}
}
}, async (req, reply) => {
const { email } = req.body as { email: string }
const { email } = req.body as { email: string };
// User finden
const { data: user, error } = await server.supabase
.from("auth_users")
.select("id, email")
.eq("email", email)
.single()
const [user] = await server.db
.select({
id: authUsers.id,
email: authUsers.email,
})
.from(authUsers)
.where(eq(authUsers.email, email.toLowerCase()))
.limit(1);
if (error || !user) {
return reply.code(404).send({ error: "User not found" })
if (!user) {
return reply.code(404).send({ error: "User not found" });
}
// Neues Passwort generieren
const plainPassword = generateRandomPassword()
const passwordHash = await hashPassword(plainPassword)
const plainPassword = generateRandomPassword();
const passwordHash = await hashPassword(plainPassword);
// In DB updaten
const { error: updateError } = await server.supabase
.from("auth_users")
.update({ password_hash: passwordHash, must_change_password: true })
.eq("id", user.id)
await server.db
.update(authUsers)
.set({
passwordHash,
mustChangePassword: true,
})
.where(eq(authUsers.id, user.id));
if (updateError) {
return reply.code(500).send({ error: "Could not update password" })
}
// Mail verschicken
await sendMail(
user.email,
"FEDEO | Dein neues Passwort",
`<p>Hallo,</p>
<p>dein Passwort wurde zurückgesetzt.</p>
<p><strong>Neues Passwort:</strong> ${plainPassword}</p>
<p>Bitte ändere es nach dem Login umgehend.</p>`
)
`
<p>Hallo,</p>
<p>Dein Passwort wurde zurückgesetzt.</p>
<p><strong>Neues Passwort:</strong> ${plainPassword}</p>
<p>Bitte ändere es nach dem Login umgehend.</p>
`
);
return { success: true }
})
}
return { success: true };
});
}

View File

@@ -13,6 +13,7 @@ export let secrets = {
JWT_SECRET: string
PORT: number
HOST: string
DATABASE_URL: string
SUPABASE_URL: string
SUPABASE_SERVICE_ROLE_KEY: string
S3_BUCKET: string