b59904fdae
Data model - Properties, rooms (+optional floors), assets (typed custom fields + Zod runtime validator + move history), documents (polymorphic scope) - Projects -> work packages -> tasks -> subtasks - Decision events (scoped to project/property/asset/work_package) - Checklist templates + instances, maintenance schedules (time + usage) with auto-materialized checklists on event recording - Wiki (global + per-project) with revisions + tsvector FTS - Property accounts (utility/meter numbers by kind) - Notifications table + per-user channel prefs Infra - RBAC guards (requireCompany / requireAdmin) - Storage abstraction: LocalDiskStorage (HMAC signed URLs) + S3Storage behind the same interface, switchable via STORAGE_BACKEND - CSV export for assets / maintenance / decisions - QR labels: /api/qr SVG endpoint + printable /assets/[id]/label - Notifications: in-app + SMTP (own server via nodemailer) + Matrix (Client-Server API, per-company room) with opt-in per user - Company switcher + auto-select first company on login UI - Topbar: bell with unread count, theme toggle, name, Sign Out (flat) - Sidebar: main nav + dedicated Admin section (Asset types, Users, Company) - Nested-route tabs on property / project / asset detail pages - Admin UIs for users (invite, role, reset pw, deactivate) and company settings (default currency, Matrix room id) - Custom asset type creation + field-def editor with immutable key/type guard and auto-deprecate when removing a field still referenced Graph - graphify-out/ committed: GRAPH_REPORT.md, graph.html, graph.json
65 lines
2.1 KiB
TypeScript
65 lines
2.1 KiB
TypeScript
import {
|
|
pgTable,
|
|
varchar,
|
|
text,
|
|
integer,
|
|
uuid,
|
|
index,
|
|
uniqueIndex
|
|
} from 'drizzle-orm/pg-core';
|
|
import { companies, users } from './tenancy';
|
|
import { wikiScopeEnum, pk, fk, createdAt, updatedAt, deletedAt, slugCol } from './_shared';
|
|
|
|
// One wiki page per (company, scope, slug). scope_id is null for global pages.
|
|
// Unique-with-NULLS-NOT-DISTINCT on (company_id, scope_type, scope_id, slug)
|
|
// is added in the follow-up SQL migration (Drizzle can't express it).
|
|
export const wikiPages = pgTable(
|
|
'wiki_pages',
|
|
{
|
|
id: pk(),
|
|
companyId: fk('company_id')
|
|
.notNull()
|
|
.references(() => companies.id, { onDelete: 'cascade' }),
|
|
scopeType: wikiScopeEnum('scope_type').notNull(),
|
|
scopeId: uuid('scope_id'), // null for global
|
|
slug: slugCol(),
|
|
title: varchar('title', { length: 255 }).notNull(),
|
|
// Pointer to the latest wiki_revisions row. Set after first revision is written.
|
|
currentRevisionId: fk('current_revision_id'),
|
|
createdBy: fk('created_by').references(() => users.id, { onDelete: 'set null' }),
|
|
createdAt: createdAt(),
|
|
updatedAt: updatedAt(),
|
|
deletedAt: deletedAt()
|
|
},
|
|
(t) => ({
|
|
byScope: index('wiki_by_scope').on(t.scopeType, t.scopeId)
|
|
// (company_id, scope_type, scope_id, slug) unique with NULLS NOT DISTINCT
|
|
// is created in the follow-up SQL migration.
|
|
})
|
|
);
|
|
|
|
export const wikiRevisions = pgTable(
|
|
'wiki_revisions',
|
|
{
|
|
id: pk(),
|
|
pageId: fk('page_id')
|
|
.notNull()
|
|
.references(() => wikiPages.id, { onDelete: 'cascade' }),
|
|
revision: integer('revision').notNull(),
|
|
title: varchar('title', { length: 255 }).notNull(),
|
|
bodyMd: text('body_md').notNull(),
|
|
// Maintained by trigger (declared as text, ALTERed to tsvector in follow-up).
|
|
bodyTsv: text('body_tsv'),
|
|
editedBy: fk('edited_by').references(() => users.id, { onDelete: 'set null' }),
|
|
editedAt: createdAt(),
|
|
comment: varchar('comment', { length: 500 })
|
|
},
|
|
(t) => ({
|
|
pageRevUq: uniqueIndex('wiki_rev_page_rev_uq').on(t.pageId, t.revision),
|
|
byPage: index('wiki_rev_by_page').on(t.pageId, t.revision)
|
|
})
|
|
);
|
|
|
|
export type WikiPage = typeof wikiPages.$inferSelect;
|
|
export type WikiRevision = typeof wikiRevisions.$inferSelect;
|