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
This commit is contained in:
@@ -0,0 +1,64 @@
|
||||
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;
|
||||
Reference in New Issue
Block a user