Files
buildfor_life_ops/src/lib/server/db/schema/wiki.ts
T
grabowski b59904fdae Phases 1-5 + rooms/floors, accounts, custom types, users, notifications
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
2026-04-23 15:18:11 +07:00

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;