@hiai-gg/docsmint 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) hide show
  1. package/LICENSE +171 -0
  2. package/README.md +348 -0
  3. package/backend/src/lib/logger.ts +18 -0
  4. package/backend/src/lib/redis-factory.ts +40 -0
  5. package/backend/src/lib/storage-factory.ts +56 -0
  6. package/frontend/src/lib/components/editor/shared-document.ts +237 -0
  7. package/frontend/src/lib/extensions/context.ts +60 -0
  8. package/frontend/src/lib/extensions/doc-tabs.ts +18 -0
  9. package/frontend/src/lib/extensions/resolve.ts +48 -0
  10. package/frontend/src/lib/extensions/types.ts +202 -0
  11. package/frontend/src/lib/hosts/DocsmintSharedDocumentHost.svelte +65 -0
  12. package/frontend/src/lib/hosts/HiaiDocsDashboardHost.svelte +1007 -0
  13. package/frontend/src/lib/hosts/HiaiDocsExtensionProvider.svelte +20 -0
  14. package/frontend/src/lib/hosts/HiaiDocsSearchHost.svelte +996 -0
  15. package/frontend/src/lib/hosts/index.ts +25 -0
  16. package/frontend/src/lib/index.ts +65 -0
  17. package/frontend/src/lib/stores/doc-tab-registry.svelte.ts +68 -0
  18. package/package.json +178 -0
  19. package/packages/cli/src/client.ts +271 -0
  20. package/packages/cli/src/commands/config.ts +47 -0
  21. package/packages/cli/src/commands/create.ts +35 -0
  22. package/packages/cli/src/commands/delete.ts +37 -0
  23. package/packages/cli/src/commands/export.ts +36 -0
  24. package/packages/cli/src/commands/folders.ts +88 -0
  25. package/packages/cli/src/commands/history.ts +55 -0
  26. package/packages/cli/src/commands/list.ts +61 -0
  27. package/packages/cli/src/commands/read.ts +38 -0
  28. package/packages/cli/src/commands/restore.ts +30 -0
  29. package/packages/cli/src/commands/search.ts +56 -0
  30. package/packages/cli/src/commands/snapshot.ts +35 -0
  31. package/packages/cli/src/commands/update.ts +54 -0
  32. package/packages/cli/src/config.ts +83 -0
  33. package/packages/cli/src/format.ts +153 -0
  34. package/packages/cli/src/index.ts +73 -0
  35. package/packages/db/src/client.ts +20 -0
  36. package/packages/db/src/index.ts +5 -0
  37. package/packages/db/src/schema.ts +692 -0
  38. package/packages/db/src/with-tenant.ts +75 -0
  39. package/packages/mcp-server/src/client.ts +172 -0
  40. package/packages/mcp-server/src/index.ts +109 -0
  41. package/packages/mcp-server/src/tools/create-document.ts +32 -0
  42. package/packages/mcp-server/src/tools/create-folder.ts +24 -0
  43. package/packages/mcp-server/src/tools/create-snapshot.ts +30 -0
  44. package/packages/mcp-server/src/tools/export-document.ts +22 -0
  45. package/packages/mcp-server/src/tools/get-document.ts +20 -0
  46. package/packages/mcp-server/src/tools/list-documents.ts +42 -0
  47. package/packages/mcp-server/src/tools/list-folders.ts +25 -0
  48. package/packages/mcp-server/src/tools/search.ts +42 -0
  49. package/packages/mcp-server/src/tools/update-document.ts +30 -0
  50. package/packages/mcp-server/src/tools/version-history.ts +32 -0
  51. package/packages/mcp-server/src/types.ts +126 -0
  52. package/packages/sdk/dist/client.d.ts +187 -0
  53. package/packages/sdk/dist/client.js +568 -0
  54. package/packages/sdk/dist/index.d.ts +3 -0
  55. package/packages/sdk/dist/index.js +1 -0
  56. package/packages/sdk/dist/types.d.ts +391 -0
  57. package/packages/sdk/dist/types.js +8 -0
@@ -0,0 +1,692 @@
1
+ import { pgTable, uuid, text, timestamp, bigint, jsonb, index, uniqueIndex, customType, boolean, check, integer, pgEnum, type AnyPgColumn } from "drizzle-orm/pg-core";
2
+
3
+ // pgvector vector type — maps to PostgreSQL vector(n) column
4
+ const vector = customType<{
5
+ data: number[];
6
+ config: { dimensions: number };
7
+ configRequired: true;
8
+ }>({
9
+ dataType(config) {
10
+ return `vector(${config.dimensions})`;
11
+ },
12
+ toDriver(value: number[]) {
13
+ return JSON.stringify(value);
14
+ },
15
+ fromDriver(value: unknown) {
16
+ if (typeof value === "string") return JSON.parse(value) as number[];
17
+ return value as number[];
18
+ },
19
+ });
20
+
21
+ // PostgreSQL tsvector type — used for documents.search_vector full-text search
22
+ const tsvector = customType<{ data: string }>({
23
+ dataType() {
24
+ return "tsvector";
25
+ },
26
+ });
27
+
28
+ import { relations, sql } from "drizzle-orm";
29
+
30
+ // ============================================
31
+ // Enums
32
+ // ============================================
33
+ export const documentVisibilityEnum = pgEnum("document_visibility", ["private", "shared", "public"]);
34
+ export const shareRoleEnum = pgEnum("share_role", ["viewer", "commenter", "editor"]);
35
+ export const embeddingStatusEnum = pgEnum("embedding_status", [
36
+ "pending",
37
+ "processing",
38
+ "ready",
39
+ "failed",
40
+ "stale",
41
+ ]);
42
+ export const pipelineStageEnum = pgEnum("pipeline_stage", [
43
+ "prepare",
44
+ "embed",
45
+ "graph",
46
+ "summarize",
47
+ "finalize",
48
+ ]);
49
+ export const pipelineStatusEnum = pgEnum("pipeline_status", [
50
+ "pending",
51
+ "processing",
52
+ "ready",
53
+ "retrying",
54
+ "failed",
55
+ "ready_with_warnings",
56
+ "skipped",
57
+ "cancelled",
58
+ ]);
59
+
60
+ // ============================================
61
+ // users — managed by Better Auth
62
+ // ============================================
63
+ export const users = pgTable("users", {
64
+ id: uuid("id").primaryKey().defaultRandom(),
65
+ email: text("email").notNull().unique(),
66
+ name: text("name"),
67
+ emailVerified: boolean("email_verified").default(false),
68
+ image: text("image"),
69
+ createdAt: timestamp("created_at").defaultNow().notNull(),
70
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
71
+ });
72
+
73
+ // ============================================
74
+ // sessions — managed by Better Auth
75
+ // ============================================
76
+ export const sessions = pgTable("sessions", {
77
+ id: uuid("id").primaryKey().defaultRandom(),
78
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
79
+ token: text("token").notNull().unique(),
80
+ expiresAt: timestamp("expires_at").notNull(),
81
+ ipAddress: text("ip_address"),
82
+ userAgent: text("user_agent"),
83
+ revokedAt: timestamp("revoked_at"),
84
+ createdAt: timestamp("created_at").defaultNow().notNull(),
85
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
86
+ }, (table) => [
87
+ index("sessions_user_id_idx").on(table.userId),
88
+ index("sessions_revoked_at_idx")
89
+ .on(table.revokedAt)
90
+ .where(sql`${table.revokedAt} IS NOT NULL`),
91
+ ]);
92
+
93
+ // ============================================
94
+ // accounts — managed by Better Auth
95
+ // ============================================
96
+ export const accounts = pgTable("accounts", {
97
+ id: uuid("id").primaryKey().defaultRandom(),
98
+ userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
99
+ accountId: text("account_id").notNull(),
100
+ providerId: text("provider_id").notNull(),
101
+ accessToken: text("access_token"),
102
+ refreshToken: text("refresh_token"),
103
+ accessTokenExpiresAt: timestamp("access_token_expires_at"),
104
+ refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
105
+ scope: text("scope"),
106
+ password: text("password"),
107
+ createdAt: timestamp("created_at").defaultNow().notNull(),
108
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
109
+ }, (table) => [
110
+ index("accounts_user_id_idx").on(table.userId),
111
+ uniqueIndex("accounts_provider_account_idx").on(table.providerId, table.accountId),
112
+ ]);
113
+
114
+ // ============================================
115
+ // verifications — managed by Better Auth
116
+ // ============================================
117
+ export const verifications = pgTable("verifications", {
118
+ id: uuid("id").primaryKey().defaultRandom(),
119
+ identifier: text("identifier").notNull(),
120
+ value: text("value").notNull(),
121
+ expiresAt: timestamp("expires_at").notNull(),
122
+ createdAt: timestamp("created_at").defaultNow().notNull(),
123
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
124
+ }, (table) => [
125
+ index("verifications_identifier_idx").on(table.identifier),
126
+ ]);
127
+
128
+ // ============================================
129
+ // folders — hierarchical folder structure
130
+ // ============================================
131
+ export const folders = pgTable(
132
+ "folders",
133
+ {
134
+ id: uuid("id").primaryKey().defaultRandom(),
135
+ ownerId: uuid("owner_id")
136
+ .notNull()
137
+ .references(() => users.id, { onDelete: "cascade" }),
138
+ workspaceId: text("workspace_id"),
139
+ parentId: uuid("parent_id").references((): AnyPgColumn => folders.id, {
140
+ onDelete: "set null",
141
+ }),
142
+ categoryId: uuid("category_id").references(() => categories.id, {
143
+ onDelete: "set null",
144
+ }),
145
+ name: text("name").notNull(),
146
+ order: integer("order").notNull().default(0),
147
+ createdAt: timestamp("created_at").defaultNow().notNull(),
148
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
149
+ },
150
+ (table) => [
151
+ index("folders_owner_id_idx").on(table.ownerId),
152
+ index("folders_parent_id_idx").on(table.parentId),
153
+ index("folders_category_id_idx").on(table.categoryId),
154
+ ]
155
+ );
156
+
157
+ // Self-referencing for parent folder
158
+ export const folderRelations = relations(folders, ({ one, many }) => ({
159
+ owner: one(users, { fields: [folders.ownerId], references: [users.id] }),
160
+ parent: one(folders, {
161
+ fields: [folders.parentId],
162
+ references: [folders.id],
163
+ relationName: "folderParent",
164
+ }),
165
+ category: one(categories, {
166
+ fields: [folders.categoryId],
167
+ references: [categories.id],
168
+ }),
169
+ children: many(folders, { relationName: "folderParent" }),
170
+ documents: many(documents),
171
+ }));
172
+
173
+ // ============================================
174
+ // documents — core content
175
+ // ============================================
176
+ export const documents = pgTable(
177
+ "documents",
178
+ {
179
+ id: uuid("id").primaryKey().defaultRandom(),
180
+ ownerId: uuid("owner_id")
181
+ .notNull()
182
+ .references(() => users.id, { onDelete: "cascade" }),
183
+ workspaceId: text("workspace_id"),
184
+ folderId: uuid("folder_id").references(() => folders.id, {
185
+ onDelete: "set null",
186
+ }),
187
+ categoryId: uuid("category_id").references(() => categories.id, {
188
+ onDelete: "set null",
189
+ }),
190
+ title: text("title").notNull().default("Untitled"),
191
+ content: text("content").default(""),
192
+ contentJson: jsonb("content_json"),
193
+ metadata: jsonb("metadata"),
194
+ visibility: documentVisibilityEnum("visibility").notNull().default("private"),
195
+ contentHash: text("content_hash"), // SHA-256 of title+content for smart re-embed
196
+ // Smart-reembed fields (added by migration 0009_lying_hardball.sql).
197
+ // These four columns are required by `backend/src/lib/reembed.ts` and
198
+ // `backend/src/lib/reembed-cron.ts`; the TypeScript schema must stay in
199
+ // sync with the migration or Drizzle queries fail at compile time.
200
+ lastSignificantHash: text("last_significant_hash"),
201
+ lastSignificantUpdateAt: timestamp("last_significant_update_at"),
202
+ pendingMinorChanges: boolean("pending_minor_changes")
203
+ .default(false)
204
+ .notNull(),
205
+ metadataChangedAt: timestamp("metadata_changed_at"),
206
+ searchVector: tsvector("search_vector").generatedAlwaysAs(
207
+ sql`to_tsvector('english', left(COALESCE(title, '') || ' ' || regexp_replace(COALESCE(content, ''), 'data:[^[:space:])>]+', ' ', 'g'), 200000))`
208
+ ),
209
+ searchVectorSimple: tsvector("search_vector_simple").generatedAlwaysAs(
210
+ sql`to_tsvector('simple', left(COALESCE(title, '') || ' ' || regexp_replace(COALESCE(content, ''), 'data:[^[:space:])>]+', ' ', 'g'), 200000))`
211
+ ),
212
+ embeddingStatus: embeddingStatusEnum("embedding_status").notNull().default("pending"),
213
+ activeEmbeddingGeneration: uuid("active_embedding_generation"),
214
+ pendingEmbeddingGeneration: uuid("pending_embedding_generation"),
215
+ embeddingProfile: text("embedding_profile"),
216
+ embeddingErrorCode: text("embedding_error_code"),
217
+ embeddingUpdatedAt: timestamp("embedding_updated_at"),
218
+ createdAt: timestamp("created_at").defaultNow().notNull(),
219
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
220
+ },
221
+ (table) => [
222
+ index("documents_owner_id_idx").on(table.ownerId),
223
+ index("documents_folder_id_idx").on(table.folderId),
224
+ index("documents_category_id_idx").on(table.categoryId),
225
+ index("documents_created_at_idx").on(table.createdAt),
226
+ index("idx_documents_search_vector").using("gin", table.searchVector),
227
+ index("idx_documents_search_vector_simple").using("gin", table.searchVectorSimple),
228
+ index("documents_embedding_status_idx").on(table.embeddingStatus),
229
+ index("idx_documents_title_trgm").using(
230
+ "gin",
231
+ sql`${table.title} gin_trgm_ops`
232
+ ),
233
+ ]
234
+ );
235
+
236
+ export const documentRelations = relations(documents, ({ one, many }) => ({
237
+ owner: one(users, { fields: [documents.ownerId], references: [users.id] }),
238
+ folder: one(folders, {
239
+ fields: [documents.folderId],
240
+ references: [folders.id],
241
+ }),
242
+ category: one(categories, {
243
+ fields: [documents.categoryId],
244
+ references: [categories.id],
245
+ }),
246
+ tags: many(documentTags),
247
+ attachments: many(attachments),
248
+ versions: many(versions),
249
+ }));
250
+
251
+ // ============================================
252
+ // document_pipeline_runs — durable multi-stage pipeline state
253
+ // ============================================
254
+ export const documentPipelineRuns = pgTable(
255
+ "document_pipeline_runs",
256
+ {
257
+ id: uuid("id").primaryKey().defaultRandom(),
258
+ documentId: uuid("document_id")
259
+ .notNull()
260
+ .references(() => documents.id, { onDelete: "cascade" }),
261
+ ownerId: uuid("owner_id")
262
+ .notNull()
263
+ .references(() => users.id, { onDelete: "cascade" }),
264
+ workspaceId: text("workspace_id"),
265
+ generationId: uuid("generation_id").notNull(),
266
+ revision: text("revision").notNull(),
267
+ source: text("source").notNull(),
268
+ status: pipelineStatusEnum("status").notNull().default("pending"),
269
+ prepareStatus: pipelineStatusEnum("prepare_status").notNull().default("pending"),
270
+ embedStatus: pipelineStatusEnum("embed_status").notNull().default("pending"),
271
+ graphStatus: pipelineStatusEnum("graph_status").notNull().default("pending"),
272
+ summarizeStatus: pipelineStatusEnum("summarize_status").notNull().default("pending"),
273
+ finalizeStatus: pipelineStatusEnum("finalize_status").notNull().default("pending"),
274
+ totalBatches: integer("total_batches").notNull().default(0),
275
+ completedBatches: integer("completed_batches").notNull().default(0),
276
+ failedBatches: integer("failed_batches").notNull().default(0),
277
+ errorCode: text("error_code"),
278
+ attempts: integer("attempts").notNull().default(0),
279
+ requestedAt: timestamp("requested_at").defaultNow().notNull(),
280
+ startedAt: timestamp("started_at"),
281
+ completedAt: timestamp("completed_at"),
282
+ heartbeatAt: timestamp("heartbeat_at"),
283
+ availableAt: timestamp("available_at"),
284
+ createdAt: timestamp("created_at").defaultNow().notNull(),
285
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
286
+ },
287
+ (table) => [
288
+ uniqueIndex("document_pipeline_runs_document_generation_idx").on(
289
+ table.documentId,
290
+ table.generationId,
291
+ ),
292
+ index("document_pipeline_runs_owner_status_updated_idx").on(
293
+ table.ownerId,
294
+ table.status,
295
+ table.updatedAt,
296
+ ),
297
+ ],
298
+ );
299
+
300
+ // ============================================
301
+ // document_pipeline_batches — idempotent embedding work units
302
+ // ============================================
303
+ export const documentPipelineBatches = pgTable(
304
+ "document_pipeline_batches",
305
+ {
306
+ id: uuid("id").primaryKey().defaultRandom(),
307
+ workspaceId: text("workspace_id"),
308
+ documentId: uuid("document_id")
309
+ .notNull()
310
+ .references(() => documents.id, { onDelete: "cascade" }),
311
+ generationId: uuid("generation_id").notNull(),
312
+ batchIndex: integer("batch_index").notNull(),
313
+ stage: pipelineStageEnum("stage").notNull().default("embed"),
314
+ chunkStart: integer("chunk_start").notNull(),
315
+ chunkEnd: integer("chunk_end").notNull(),
316
+ status: pipelineStatusEnum("status").notNull().default("pending"),
317
+ attempts: integer("attempts").notNull().default(0),
318
+ embeddingProfile: text("embedding_profile"),
319
+ errorCode: text("error_code"),
320
+ availableAt: timestamp("available_at"),
321
+ startedAt: timestamp("started_at"),
322
+ completedAt: timestamp("completed_at"),
323
+ heartbeatAt: timestamp("heartbeat_at"),
324
+ createdAt: timestamp("created_at").defaultNow().notNull(),
325
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
326
+ },
327
+ (table) => [
328
+ uniqueIndex("document_pipeline_batches_generation_index_idx").on(
329
+ table.generationId,
330
+ table.batchIndex,
331
+ ),
332
+ index("document_pipeline_batches_stage_status_available_idx").on(
333
+ table.stage,
334
+ table.status,
335
+ table.availableAt,
336
+ ),
337
+ index("document_pipeline_batches_document_id_idx").on(table.documentId),
338
+ ],
339
+ );
340
+
341
+ // ============================================
342
+ // tags — document tags
343
+ // ============================================
344
+ export const tags = pgTable(
345
+ "tags",
346
+ {
347
+ id: uuid("id").primaryKey().defaultRandom(),
348
+ ownerId: uuid("owner_id")
349
+ .notNull()
350
+ .references(() => users.id, { onDelete: "cascade" }),
351
+ workspaceId: text("workspace_id"),
352
+ name: text("name").notNull(),
353
+ color: text("color"),
354
+ createdAt: timestamp("created_at").defaultNow().notNull(),
355
+ },
356
+ (table) => [
357
+ index("tags_owner_id_idx").on(table.ownerId),
358
+ uniqueIndex("tags_owner_name_idx").on(table.ownerId, table.name),
359
+ ]
360
+ );
361
+
362
+ export const tagRelations = relations(tags, ({ many }) => ({
363
+ documents: many(documentTags),
364
+ }));
365
+
366
+ // ============================================
367
+ // categories — document/folder classification
368
+ // ============================================
369
+ export const categories = pgTable(
370
+ "categories",
371
+ {
372
+ id: uuid("id").primaryKey().defaultRandom(),
373
+ ownerId: uuid("owner_id")
374
+ .notNull()
375
+ .references(() => users.id, { onDelete: "cascade" }),
376
+ workspaceId: text("workspace_id"),
377
+ name: text("name").notNull(),
378
+ order: integer("order").notNull().default(0),
379
+ apiMode: text("api_mode").notNull().default("unavailable"),
380
+ apiPermissionRead: boolean("api_permission_read").notNull().default(false),
381
+ apiPermissionEdit: boolean("api_permission_edit").notNull().default(false),
382
+ apiPermissionWrite: boolean("api_permission_write").notNull().default(false),
383
+ createdAt: timestamp("created_at").defaultNow().notNull(),
384
+ updatedAt: timestamp("updated_at").defaultNow().notNull(),
385
+ },
386
+ (table) => [
387
+ index("categories_owner_id_idx").on(table.ownerId),
388
+ index("categories_api_mode_idx").on(table.apiMode),
389
+ ]
390
+ );
391
+
392
+ export const categoryRelations = relations(categories, ({ one, many }) => ({
393
+ owner: one(users, { fields: [categories.ownerId], references: [users.id] }),
394
+ folders: many(folders),
395
+ documents: many(documents),
396
+ }));
397
+
398
+ // ============================================
399
+ // document_tags — many-to-many
400
+ // ============================================
401
+ export const documentTags = pgTable(
402
+ "document_tags",
403
+ {
404
+ workspaceId: text("workspace_id"),
405
+ documentId: uuid("document_id")
406
+ .notNull()
407
+ .references(() => documents.id, { onDelete: "cascade" }),
408
+ tagId: uuid("tag_id")
409
+ .notNull()
410
+ .references(() => tags.id, { onDelete: "cascade" }),
411
+ },
412
+ (table) => [
413
+ uniqueIndex("document_tags_unique_idx").on(table.documentId, table.tagId),
414
+ ]
415
+ );
416
+
417
+ export const documentTagRelations = relations(documentTags, ({ one }) => ({
418
+ document: one(documents, {
419
+ fields: [documentTags.documentId],
420
+ references: [documents.id],
421
+ }),
422
+ tag: one(tags, { fields: [documentTags.tagId], references: [tags.id] }),
423
+ }));
424
+
425
+ // ============================================
426
+ // share_links — sharing tokens
427
+ // ============================================
428
+ export const shareLinks = pgTable(
429
+ "share_links",
430
+ {
431
+ id: uuid("id").primaryKey().defaultRandom(),
432
+ documentId: uuid("document_id").references(() => documents.id, {
433
+ onDelete: "cascade",
434
+ }),
435
+ folderId: uuid("folder_id").references(() => folders.id, {
436
+ onDelete: "cascade",
437
+ }),
438
+ categoryId: uuid("category_id").references(() => categories.id, {
439
+ onDelete: "cascade",
440
+ }),
441
+ token: text("token").notNull().unique(),
442
+ passwordHash: text("password_hash"),
443
+ role: shareRoleEnum("role").notNull().default("viewer"),
444
+ expiresAt: timestamp("expires_at"),
445
+ createdBy: uuid("created_by")
446
+ .notNull()
447
+ .references(() => users.id, { onDelete: "cascade" }),
448
+ workspaceId: text("workspace_id"),
449
+ createdAt: timestamp("created_at").defaultNow().notNull(),
450
+ },
451
+ (table) => [
452
+ index("share_links_token_idx").on(table.token),
453
+ index("share_links_document_id_idx").on(table.documentId),
454
+ index("share_links_folder_id_idx").on(table.folderId),
455
+ index("share_links_category_id_idx").on(table.categoryId),
456
+ check(
457
+ "share_links_exactly_one_target_check",
458
+ sql`num_nonnulls(${table.documentId}, ${table.folderId}, ${table.categoryId}) = 1`,
459
+ ),
460
+ ]
461
+ );
462
+
463
+ export const shareLinkRelations = relations(shareLinks, ({ one, many }) => ({
464
+ document: one(documents, {
465
+ fields: [shareLinks.documentId],
466
+ references: [documents.id],
467
+ }),
468
+ folder: one(folders, {
469
+ fields: [shareLinks.folderId],
470
+ references: [folders.id],
471
+ }),
472
+ category: one(categories, {
473
+ fields: [shareLinks.categoryId],
474
+ references: [categories.id],
475
+ }),
476
+ creator: one(users, {
477
+ fields: [shareLinks.createdBy],
478
+ references: [users.id],
479
+ }),
480
+ guestAccess: many(guestAccess),
481
+ }));
482
+
483
+ // ============================================
484
+ // guest_access — guest email grants
485
+ // ============================================
486
+ export const guestAccess = pgTable(
487
+ "guest_access",
488
+ {
489
+ id: uuid("id").primaryKey().defaultRandom(),
490
+ shareLinkId: uuid("share_link_id")
491
+ .notNull()
492
+ .references(() => shareLinks.id, { onDelete: "cascade" }),
493
+ workspaceId: text("workspace_id"),
494
+ guestEmail: text("guest_email").notNull(),
495
+ grantedAt: timestamp("granted_at").defaultNow().notNull(),
496
+ },
497
+ (table) => [index("guest_access_share_link_idx").on(table.shareLinkId)]
498
+ );
499
+
500
+ export const guestAccessRelations = relations(guestAccess, ({ one }) => ({
501
+ shareLink: one(shareLinks, {
502
+ fields: [guestAccess.shareLinkId],
503
+ references: [shareLinks.id],
504
+ }),
505
+ }));
506
+
507
+ // ============================================
508
+ // attachments — file uploads (SeaweedFS)
509
+ // ============================================
510
+ export const attachments = pgTable(
511
+ "attachments",
512
+ {
513
+ id: uuid("id").primaryKey().defaultRandom(),
514
+ documentId: uuid("document_id")
515
+ .notNull()
516
+ .references(() => documents.id, { onDelete: "cascade" }),
517
+ workspaceId: text("workspace_id"),
518
+ filename: text("filename").notNull(),
519
+ mimeType: text("mime_type").notNull(),
520
+ size: bigint("size", { mode: "number" }).notNull(),
521
+ storageKey: text("storage_key").notNull(),
522
+ createdAt: timestamp("created_at").defaultNow().notNull(),
523
+ },
524
+ (table) => [index("attachments_document_id_idx").on(table.documentId)]
525
+ );
526
+
527
+ export const attachmentRelations = relations(attachments, ({ one }) => ({
528
+ document: one(documents, {
529
+ fields: [attachments.documentId],
530
+ references: [documents.id],
531
+ }),
532
+ }));
533
+
534
+ // ============================================
535
+ // versions — document version history
536
+ // ============================================
537
+ export const versions = pgTable(
538
+ "versions",
539
+ {
540
+ id: uuid("id").primaryKey().defaultRandom(),
541
+ documentId: uuid("document_id")
542
+ .notNull()
543
+ .references(() => documents.id, { onDelete: "cascade" }),
544
+ workspaceId: text("workspace_id"),
545
+ content: text("content").notNull(),
546
+ contentJson: jsonb("content_json"),
547
+ createdBy: uuid("created_by")
548
+ .notNull()
549
+ .references(() => users.id, { onDelete: "cascade" }),
550
+ createdAt: timestamp("created_at").defaultNow().notNull(),
551
+ label: text("label"),
552
+ description: text("description"),
553
+ isSnapshot: boolean("is_snapshot").default(false),
554
+ restoredFrom: uuid("restored_from"),
555
+ },
556
+ (table) => [
557
+ index("versions_document_id_idx").on(table.documentId),
558
+ index("versions_created_at_idx").on(table.createdAt),
559
+ index("versions_is_snapshot_idx").on(table.isSnapshot),
560
+ ]
561
+ );
562
+
563
+ // ============================================
564
+ // document_embeddings — multi-chunk pgvector storage
565
+ // ============================================
566
+ export const documentEmbeddings = pgTable(
567
+ "document_embeddings",
568
+ {
569
+ id: uuid("id").primaryKey().defaultRandom(),
570
+ documentId: uuid("document_id")
571
+ .notNull()
572
+ .references(() => documents.id, { onDelete: "cascade" }),
573
+ workspaceId: text("workspace_id"),
574
+ chunkIndex: bigint("chunk_index", { mode: "number" }).notNull(),
575
+ chunkText: text("chunk_text").notNull(),
576
+ chunkHash: text("chunk_hash"),
577
+ charStart: integer("char_start").notNull().default(0),
578
+ charEnd: integer("char_end").notNull().default(0),
579
+ embedding: vector("embedding", { dimensions: 1024 }),
580
+ // Identifier of the embedding model that produced the vector above.
581
+ // Empty string ("") means "unknown / legacy row" (pre-v1 rows that
582
+ // existed before this column was introduced). The targeted reindex
583
+ // endpoint at POST /api/admin/reindex/model filters on this column
584
+ // to refresh only docs whose stored model does not match the
585
+ // currently-configured EMBEDDING_MODEL.
586
+ embeddingModel: text("embedding_model").notNull().default(""),
587
+ generationId: uuid("generation_id").notNull(),
588
+ embeddingDimensions: integer("embedding_dimensions").notNull().default(1024),
589
+ embeddingProfile: text("embedding_profile").notNull().default("legacy"),
590
+ isValid: boolean("is_valid").notNull().default(false),
591
+ createdAt: timestamp("created_at").defaultNow().notNull(),
592
+ },
593
+ (table) => [
594
+ index("document_embeddings_doc_id_idx").on(table.documentId),
595
+ uniqueIndex("document_embeddings_doc_chunk_idx").on(
596
+ table.documentId,
597
+ table.generationId,
598
+ table.chunkIndex,
599
+ ),
600
+ index("document_embeddings_generation_valid_idx").on(
601
+ table.documentId,
602
+ table.generationId,
603
+ table.isValid,
604
+ ),
605
+ // Backs POST /api/admin/reindex/model which selects docs whose stored
606
+ // embedding model differs from the currently-configured EMBEDDING_MODEL.
607
+ index("idx_document_embeddings_embedding_model").on(table.embeddingModel),
608
+ index("idx_document_embeddings_hnsw").using(
609
+ "hnsw",
610
+ sql`${table.embedding} vector_cosine_ops`
611
+ ),
612
+ // StreamingDiskANN index for >100k row corpora. Co-exists with HNSW;
613
+ // the planner picks the right one based on table size and memory.
614
+ // Requires the pgvectorscale extension (enabled in postgres/init.sql).
615
+ index("idx_document_embeddings_diskann").using(
616
+ "diskann",
617
+ sql`${table.embedding} vector_cosine_ops`
618
+ ),
619
+ ]
620
+ );
621
+
622
+ export const documentEmbeddingRelations = relations(documentEmbeddings, ({ one }) => ({
623
+ document: one(documents, {
624
+ fields: [documentEmbeddings.documentId],
625
+ references: [documents.id],
626
+ }),
627
+ }));
628
+
629
+ // ============================================
630
+ // api_keys — user API keys
631
+ // ============================================
632
+ export const apiKeys = pgTable(
633
+ "api_keys",
634
+ {
635
+ id: uuid("id").primaryKey().defaultRandom(),
636
+ ownerId: uuid("owner_id")
637
+ .notNull()
638
+ .references(() => users.id, { onDelete: "cascade" }),
639
+ workspaceId: text("workspace_id"),
640
+ name: text("name").notNull(),
641
+ keyHash: text("key_hash").notNull().unique(),
642
+ prefix: text("prefix").notNull(),
643
+ encryptedKey: text("encrypted_key"),
644
+ scopes: jsonb("scopes").notNull().default("[]"),
645
+ lastUsedAt: timestamp("last_used_at"),
646
+ expiresAt: timestamp("expires_at"),
647
+ createdAt: timestamp("created_at").defaultNow().notNull(),
648
+ },
649
+ (table) => [
650
+ index("idx_api_keys_owner").on(table.ownerId),
651
+ index("idx_api_keys_prefix").on(table.prefix),
652
+ ]
653
+ );
654
+
655
+ export const apiKeyRelations = relations(apiKeys, ({ one }) => ({
656
+ owner: one(users, { fields: [apiKeys.ownerId], references: [users.id] }),
657
+ }));
658
+
659
+ // ============================================
660
+ // audit_log — append-only audit trail
661
+ // ============================================
662
+ export const auditLog = pgTable(
663
+ "audit_log",
664
+ {
665
+ id: uuid("id").primaryKey().defaultRandom(),
666
+ actorId: uuid("actor_id").notNull(),
667
+ workspaceId: text("workspace_id"),
668
+ action: text("action").notNull(),
669
+ resourceType: text("resource_type").notNull(),
670
+ resourceId: uuid("resource_id"),
671
+ details: jsonb("details").notNull().default("{}"),
672
+ ipAddress: text("ip_address"),
673
+ userAgent: text("user_agent"),
674
+ createdAt: timestamp("created_at").defaultNow().notNull(),
675
+ },
676
+ (table) => [
677
+ index("idx_audit_log_actor").on(table.actorId),
678
+ index("idx_audit_log_resource").on(table.resourceType, table.resourceId),
679
+ index("idx_audit_log_created").on(table.createdAt),
680
+ ]
681
+ );
682
+
683
+ export const versionRelations = relations(versions, ({ one }) => ({
684
+ document: one(documents, {
685
+ fields: [versions.documentId],
686
+ references: [documents.id],
687
+ }),
688
+ creator: one(users, {
689
+ fields: [versions.createdBy],
690
+ references: [users.id],
691
+ }),
692
+ }));