@hiai-gg/docsmint 0.4.1 → 0.4.3
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.
package/dist/backend/index.js
CHANGED
|
@@ -214237,7 +214237,7 @@ var swaggerConfig = {
|
|
|
214237
214237
|
documentation: {
|
|
214238
214238
|
info: {
|
|
214239
214239
|
title: "DocsMint API",
|
|
214240
|
-
version: "0.4.
|
|
214240
|
+
version: "0.4.3",
|
|
214241
214241
|
description: "Self-hosted AI-first documentation platform. Full-text + semantic search, version history, sharing, and folder organization.",
|
|
214242
214242
|
contact: { name: "HiAi-gg", url: "https://github.com/HiAi-gg/docsmint" },
|
|
214243
214243
|
license: {
|
|
@@ -12,22 +12,60 @@ export function createPersistentLifecycleRuntime(options) {
|
|
|
12
12
|
const lifecycle = createUserDataLifecycle({
|
|
13
13
|
async *exportUserData(context) {
|
|
14
14
|
const immutable = immutableContext(context);
|
|
15
|
-
const records = await options.runtime.database(immutable,
|
|
16
|
-
const result = [];
|
|
17
|
-
for await (const record of options.runtime.adapter.exportUserData(immutable))
|
|
18
|
-
result.push(record);
|
|
19
|
-
return result;
|
|
20
|
-
});
|
|
15
|
+
const records = await options.runtime.database(immutable, () => composeExport(options.runtime.adapter, hostSteps, immutable));
|
|
21
16
|
for (const record of records)
|
|
22
17
|
yield record;
|
|
23
18
|
},
|
|
24
19
|
async purgeUserData(context, gate) {
|
|
25
20
|
const immutable = immutableContext(context);
|
|
26
|
-
return options.runtime.database(immutable, () =>
|
|
21
|
+
return options.runtime.database(immutable, async () => {
|
|
22
|
+
const result = await options.runtime.adapter.purgeUserData(immutable, gate);
|
|
23
|
+
if (result.status === "already_completed")
|
|
24
|
+
return result;
|
|
25
|
+
const deletedByDomain = { ...result.deletedByDomain };
|
|
26
|
+
for (const step of hostSteps) {
|
|
27
|
+
if (!step.purge)
|
|
28
|
+
continue;
|
|
29
|
+
const outcome = await step.purge(immutable);
|
|
30
|
+
deletedByDomain[`host:${step.id}`] = outcome.deletedCount;
|
|
31
|
+
}
|
|
32
|
+
return { ...result, deletedByDomain };
|
|
33
|
+
});
|
|
27
34
|
},
|
|
28
35
|
}, async (context) => options.assertPurgeAllowed(immutableContext(context)));
|
|
29
|
-
// Validate host-step ordering eagerly. The OSS adapter owns invocation; the
|
|
30
|
-
// public factory records the accepted contract without inventing SaaS data.
|
|
31
|
-
void hostSteps;
|
|
32
36
|
return lifecycle;
|
|
33
37
|
}
|
|
38
|
+
async function composeExport(adapter, hostSteps, context) {
|
|
39
|
+
const ossRecords = [];
|
|
40
|
+
for await (const record of adapter.exportUserData(context))
|
|
41
|
+
ossRecords.push(record);
|
|
42
|
+
const manifest = ossRecords.shift();
|
|
43
|
+
const complete = ossRecords.pop();
|
|
44
|
+
if (manifest?.type !== "manifest" || complete?.type !== "complete") {
|
|
45
|
+
throw new Error("Persistent lifecycle adapter returned an invalid export stream");
|
|
46
|
+
}
|
|
47
|
+
if (ossRecords.some((record) => record.type === "manifest" || record.type === "complete")) {
|
|
48
|
+
throw new Error("Persistent lifecycle adapter returned a reserved record type");
|
|
49
|
+
}
|
|
50
|
+
const hash = new Bun.CryptoHasher("sha256");
|
|
51
|
+
const records = [];
|
|
52
|
+
const append = (record) => {
|
|
53
|
+
hash.update(`${JSON.stringify(record)}\n`, "utf8");
|
|
54
|
+
records.push(record);
|
|
55
|
+
};
|
|
56
|
+
append(manifest);
|
|
57
|
+
for (const record of ossRecords)
|
|
58
|
+
append(record);
|
|
59
|
+
for (const step of hostSteps) {
|
|
60
|
+
if (!step.export)
|
|
61
|
+
continue;
|
|
62
|
+
for await (const record of step.export(context)) {
|
|
63
|
+
if (record.type === "manifest" || record.type === "complete") {
|
|
64
|
+
throw new Error(`Lifecycle host step ${step.id} emitted a reserved record type`);
|
|
65
|
+
}
|
|
66
|
+
append(record);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
records.push({ type: "complete", recordCount: records.length, checksum: hash.digest("hex") });
|
|
70
|
+
return records;
|
|
71
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { AssertPurgeAllowed, LifecycleHostStep, UserDataLifecycle } from "./lifecycle";
|
|
2
|
+
/**
|
|
3
|
+
* Server-only external effects required by the durable OSS lifecycle saga.
|
|
4
|
+
* Implementations must perform real deletion work; the runtime deliberately
|
|
5
|
+
* has no permissive defaults for queues, object storage, Redis, collaboration,
|
|
6
|
+
* or graph state.
|
|
7
|
+
*/
|
|
8
|
+
export type LifecycleRuntimeAdapters = Readonly<{
|
|
9
|
+
verifyPurgeFence: (context: Parameters<AssertPurgeAllowed>[0], fenceToken: string) => Promise<void>;
|
|
10
|
+
deleteObjects: (keys: readonly string[], signal?: AbortSignal) => Promise<number>;
|
|
11
|
+
cancelAccountJobs: (actorUserId: string, signal?: AbortSignal) => Promise<number>;
|
|
12
|
+
clearAccountRedisState: (actorUserId: string, signal?: AbortSignal) => Promise<number>;
|
|
13
|
+
removeCollaborationState: (actorUserId: string, signal?: AbortSignal) => Promise<number>;
|
|
14
|
+
removeGraphState: (documentIds: readonly string[], signal?: AbortSignal) => Promise<number>;
|
|
15
|
+
}>;
|
|
16
|
+
/**
|
|
17
|
+
* Host-owned request/RLS transaction executor. TTransaction is intentionally
|
|
18
|
+
* generic so a consumer can retain the exact Drizzle transaction type without
|
|
19
|
+
* this public contract importing its database singleton.
|
|
20
|
+
*/
|
|
21
|
+
export type LifecycleScopedDatabaseExecutor<TTransaction = unknown> = Readonly<{
|
|
22
|
+
withActorTransaction<T>(actorUserId: string, operation: (transaction: TTransaction) => Promise<T>): Promise<T>;
|
|
23
|
+
}>;
|
|
24
|
+
export type PersistentLifecycleRuntimeOptions<TTransaction = unknown> = Readonly<{
|
|
25
|
+
runtime: LifecycleRuntimeAdapters;
|
|
26
|
+
database: LifecycleScopedDatabaseExecutor<TTransaction>;
|
|
27
|
+
assertPurgeAllowed: AssertPurgeAllowed;
|
|
28
|
+
hostSteps?: readonly LifecycleHostStep[];
|
|
29
|
+
}>;
|
|
30
|
+
/**
|
|
31
|
+
* Creates the concrete durable PostgreSQL lifecycle saga shipped by DocsMint.
|
|
32
|
+
* The JavaScript implementation is bundled from the OSS backend at build time;
|
|
33
|
+
* this source file is the stable public declaration surface.
|
|
34
|
+
*/
|
|
35
|
+
export declare function createPersistentLifecycleRuntime<TTransaction = unknown>(options: PersistentLifecycleRuntimeOptions<TTransaction>): UserDataLifecycle;
|
|
@@ -0,0 +1,811 @@
|
|
|
1
|
+
// @bun
|
|
2
|
+
// ../db/src/schema.ts
|
|
3
|
+
import { pgTable, uuid, text, timestamp, bigint, jsonb, index, uniqueIndex, customType, boolean, check, integer, pgEnum } from "drizzle-orm/pg-core";
|
|
4
|
+
import { relations, sql } from "drizzle-orm";
|
|
5
|
+
var vector = customType({
|
|
6
|
+
dataType(config) {
|
|
7
|
+
return `vector(${config.dimensions})`;
|
|
8
|
+
},
|
|
9
|
+
toDriver(value) {
|
|
10
|
+
return JSON.stringify(value);
|
|
11
|
+
},
|
|
12
|
+
fromDriver(value) {
|
|
13
|
+
if (typeof value === "string")
|
|
14
|
+
return JSON.parse(value);
|
|
15
|
+
return value;
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
var tsvector = customType({
|
|
19
|
+
dataType() {
|
|
20
|
+
return "tsvector";
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
var documentVisibilityEnum = pgEnum("document_visibility", ["private", "shared", "public"]);
|
|
24
|
+
var shareRoleEnum = pgEnum("share_role", ["viewer", "commenter", "editor"]);
|
|
25
|
+
var embeddingStatusEnum = pgEnum("embedding_status", [
|
|
26
|
+
"pending",
|
|
27
|
+
"processing",
|
|
28
|
+
"ready",
|
|
29
|
+
"failed",
|
|
30
|
+
"stale"
|
|
31
|
+
]);
|
|
32
|
+
var pipelineStageEnum = pgEnum("pipeline_stage", [
|
|
33
|
+
"prepare",
|
|
34
|
+
"embed",
|
|
35
|
+
"graph",
|
|
36
|
+
"summarize",
|
|
37
|
+
"finalize"
|
|
38
|
+
]);
|
|
39
|
+
var pipelineStatusEnum = pgEnum("pipeline_status", [
|
|
40
|
+
"pending",
|
|
41
|
+
"processing",
|
|
42
|
+
"ready",
|
|
43
|
+
"retrying",
|
|
44
|
+
"failed",
|
|
45
|
+
"ready_with_warnings",
|
|
46
|
+
"skipped",
|
|
47
|
+
"cancelled"
|
|
48
|
+
]);
|
|
49
|
+
var lifecycleOperationKindEnum = pgEnum("lifecycle_operation_kind", [
|
|
50
|
+
"export",
|
|
51
|
+
"purge"
|
|
52
|
+
]);
|
|
53
|
+
var lifecycleOperationStatusEnum = pgEnum("lifecycle_operation_status", [
|
|
54
|
+
"pending",
|
|
55
|
+
"running",
|
|
56
|
+
"retryable",
|
|
57
|
+
"completed",
|
|
58
|
+
"rejected"
|
|
59
|
+
]);
|
|
60
|
+
var users = pgTable("users", {
|
|
61
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
62
|
+
email: text("email").notNull().unique(),
|
|
63
|
+
name: text("name"),
|
|
64
|
+
emailVerified: boolean("email_verified").default(false),
|
|
65
|
+
image: text("image"),
|
|
66
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
67
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
68
|
+
});
|
|
69
|
+
var sessions = pgTable("sessions", {
|
|
70
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
71
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
72
|
+
token: text("token").notNull().unique(),
|
|
73
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
74
|
+
ipAddress: text("ip_address"),
|
|
75
|
+
userAgent: text("user_agent"),
|
|
76
|
+
revokedAt: timestamp("revoked_at"),
|
|
77
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
78
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
79
|
+
}, (table) => [
|
|
80
|
+
index("sessions_user_id_idx").on(table.userId),
|
|
81
|
+
index("sessions_revoked_at_idx").on(table.revokedAt).where(sql`${table.revokedAt} IS NOT NULL`)
|
|
82
|
+
]);
|
|
83
|
+
var accounts = pgTable("accounts", {
|
|
84
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
85
|
+
userId: uuid("user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
86
|
+
accountId: text("account_id").notNull(),
|
|
87
|
+
providerId: text("provider_id").notNull(),
|
|
88
|
+
accessToken: text("access_token"),
|
|
89
|
+
refreshToken: text("refresh_token"),
|
|
90
|
+
accessTokenExpiresAt: timestamp("access_token_expires_at"),
|
|
91
|
+
refreshTokenExpiresAt: timestamp("refresh_token_expires_at"),
|
|
92
|
+
scope: text("scope"),
|
|
93
|
+
password: text("password"),
|
|
94
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
95
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
96
|
+
}, (table) => [
|
|
97
|
+
index("accounts_user_id_idx").on(table.userId),
|
|
98
|
+
uniqueIndex("accounts_provider_account_idx").on(table.providerId, table.accountId)
|
|
99
|
+
]);
|
|
100
|
+
var verifications = pgTable("verifications", {
|
|
101
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
102
|
+
identifier: text("identifier").notNull(),
|
|
103
|
+
value: text("value").notNull(),
|
|
104
|
+
expiresAt: timestamp("expires_at").notNull(),
|
|
105
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
106
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
107
|
+
}, (table) => [
|
|
108
|
+
index("verifications_identifier_idx").on(table.identifier)
|
|
109
|
+
]);
|
|
110
|
+
var folders = pgTable("folders", {
|
|
111
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
112
|
+
ownerId: uuid("owner_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
113
|
+
workspaceId: text("workspace_id"),
|
|
114
|
+
parentId: uuid("parent_id").references(() => folders.id, {
|
|
115
|
+
onDelete: "set null"
|
|
116
|
+
}),
|
|
117
|
+
categoryId: uuid("category_id").references(() => categories.id, {
|
|
118
|
+
onDelete: "set null"
|
|
119
|
+
}),
|
|
120
|
+
name: text("name").notNull(),
|
|
121
|
+
order: integer("order").notNull().default(0),
|
|
122
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
123
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
124
|
+
}, (table) => [
|
|
125
|
+
index("folders_owner_id_idx").on(table.ownerId),
|
|
126
|
+
index("folders_parent_id_idx").on(table.parentId),
|
|
127
|
+
index("folders_category_id_idx").on(table.categoryId)
|
|
128
|
+
]);
|
|
129
|
+
var folderRelations = relations(folders, ({ one, many }) => ({
|
|
130
|
+
owner: one(users, { fields: [folders.ownerId], references: [users.id] }),
|
|
131
|
+
parent: one(folders, {
|
|
132
|
+
fields: [folders.parentId],
|
|
133
|
+
references: [folders.id],
|
|
134
|
+
relationName: "folderParent"
|
|
135
|
+
}),
|
|
136
|
+
category: one(categories, {
|
|
137
|
+
fields: [folders.categoryId],
|
|
138
|
+
references: [categories.id]
|
|
139
|
+
}),
|
|
140
|
+
children: many(folders, { relationName: "folderParent" }),
|
|
141
|
+
documents: many(documents)
|
|
142
|
+
}));
|
|
143
|
+
var documents = pgTable("documents", {
|
|
144
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
145
|
+
ownerId: uuid("owner_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
146
|
+
workspaceId: text("workspace_id"),
|
|
147
|
+
folderId: uuid("folder_id").references(() => folders.id, {
|
|
148
|
+
onDelete: "set null"
|
|
149
|
+
}),
|
|
150
|
+
categoryId: uuid("category_id").references(() => categories.id, {
|
|
151
|
+
onDelete: "set null"
|
|
152
|
+
}),
|
|
153
|
+
title: text("title").notNull().default("Untitled"),
|
|
154
|
+
content: text("content").default(""),
|
|
155
|
+
contentJson: jsonb("content_json"),
|
|
156
|
+
metadata: jsonb("metadata"),
|
|
157
|
+
visibility: documentVisibilityEnum("visibility").notNull().default("private"),
|
|
158
|
+
contentHash: text("content_hash"),
|
|
159
|
+
lastSignificantHash: text("last_significant_hash"),
|
|
160
|
+
lastSignificantUpdateAt: timestamp("last_significant_update_at"),
|
|
161
|
+
pendingMinorChanges: boolean("pending_minor_changes").default(false).notNull(),
|
|
162
|
+
metadataChangedAt: timestamp("metadata_changed_at"),
|
|
163
|
+
searchVector: tsvector("search_vector").generatedAlwaysAs(sql`to_tsvector('english', left(COALESCE(title, '') || ' ' || regexp_replace(COALESCE(content, ''), 'data:[^[:space:])>]+', ' ', 'g'), 200000))`),
|
|
164
|
+
searchVectorSimple: tsvector("search_vector_simple").generatedAlwaysAs(sql`to_tsvector('simple', left(COALESCE(title, '') || ' ' || regexp_replace(COALESCE(content, ''), 'data:[^[:space:])>]+', ' ', 'g'), 200000))`),
|
|
165
|
+
embeddingStatus: embeddingStatusEnum("embedding_status").notNull().default("pending"),
|
|
166
|
+
activeEmbeddingGeneration: uuid("active_embedding_generation"),
|
|
167
|
+
pendingEmbeddingGeneration: uuid("pending_embedding_generation"),
|
|
168
|
+
embeddingProfile: text("embedding_profile"),
|
|
169
|
+
embeddingErrorCode: text("embedding_error_code"),
|
|
170
|
+
embeddingUpdatedAt: timestamp("embedding_updated_at"),
|
|
171
|
+
deletedAt: timestamp("deleted_at"),
|
|
172
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
173
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
174
|
+
}, (table) => [
|
|
175
|
+
index("documents_owner_id_idx").on(table.ownerId),
|
|
176
|
+
index("documents_folder_id_idx").on(table.folderId),
|
|
177
|
+
index("documents_category_id_idx").on(table.categoryId),
|
|
178
|
+
index("documents_created_at_idx").on(table.createdAt),
|
|
179
|
+
index("idx_documents_search_vector").using("gin", table.searchVector),
|
|
180
|
+
index("idx_documents_search_vector_simple").using("gin", table.searchVectorSimple),
|
|
181
|
+
index("documents_embedding_status_idx").on(table.embeddingStatus),
|
|
182
|
+
index("documents_workspace_deleted_at_idx").on(table.workspaceId, table.deletedAt),
|
|
183
|
+
index("idx_documents_title_trgm").using("gin", sql`${table.title} gin_trgm_ops`)
|
|
184
|
+
]);
|
|
185
|
+
var documentRelations = relations(documents, ({ one, many }) => ({
|
|
186
|
+
owner: one(users, { fields: [documents.ownerId], references: [users.id] }),
|
|
187
|
+
folder: one(folders, {
|
|
188
|
+
fields: [documents.folderId],
|
|
189
|
+
references: [folders.id]
|
|
190
|
+
}),
|
|
191
|
+
category: one(categories, {
|
|
192
|
+
fields: [documents.categoryId],
|
|
193
|
+
references: [categories.id]
|
|
194
|
+
}),
|
|
195
|
+
tags: many(documentTags),
|
|
196
|
+
attachments: many(attachments),
|
|
197
|
+
versions: many(versions)
|
|
198
|
+
}));
|
|
199
|
+
var documentPipelineRuns = pgTable("document_pipeline_runs", {
|
|
200
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
201
|
+
documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
|
|
202
|
+
ownerId: uuid("owner_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
203
|
+
workspaceId: text("workspace_id"),
|
|
204
|
+
generationId: uuid("generation_id").notNull(),
|
|
205
|
+
revision: text("revision").notNull(),
|
|
206
|
+
source: text("source").notNull(),
|
|
207
|
+
status: pipelineStatusEnum("status").notNull().default("pending"),
|
|
208
|
+
prepareStatus: pipelineStatusEnum("prepare_status").notNull().default("pending"),
|
|
209
|
+
embedStatus: pipelineStatusEnum("embed_status").notNull().default("pending"),
|
|
210
|
+
graphStatus: pipelineStatusEnum("graph_status").notNull().default("pending"),
|
|
211
|
+
summarizeStatus: pipelineStatusEnum("summarize_status").notNull().default("pending"),
|
|
212
|
+
finalizeStatus: pipelineStatusEnum("finalize_status").notNull().default("pending"),
|
|
213
|
+
totalBatches: integer("total_batches").notNull().default(0),
|
|
214
|
+
completedBatches: integer("completed_batches").notNull().default(0),
|
|
215
|
+
failedBatches: integer("failed_batches").notNull().default(0),
|
|
216
|
+
errorCode: text("error_code"),
|
|
217
|
+
attempts: integer("attempts").notNull().default(0),
|
|
218
|
+
requestedAt: timestamp("requested_at").defaultNow().notNull(),
|
|
219
|
+
startedAt: timestamp("started_at"),
|
|
220
|
+
completedAt: timestamp("completed_at"),
|
|
221
|
+
heartbeatAt: timestamp("heartbeat_at"),
|
|
222
|
+
availableAt: timestamp("available_at"),
|
|
223
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
224
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
225
|
+
}, (table) => [
|
|
226
|
+
uniqueIndex("document_pipeline_runs_document_generation_idx").on(table.documentId, table.generationId),
|
|
227
|
+
index("document_pipeline_runs_owner_status_updated_idx").on(table.ownerId, table.status, table.updatedAt)
|
|
228
|
+
]);
|
|
229
|
+
var documentPipelineBatches = pgTable("document_pipeline_batches", {
|
|
230
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
231
|
+
workspaceId: text("workspace_id"),
|
|
232
|
+
documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
|
|
233
|
+
generationId: uuid("generation_id").notNull(),
|
|
234
|
+
batchIndex: integer("batch_index").notNull(),
|
|
235
|
+
stage: pipelineStageEnum("stage").notNull().default("embed"),
|
|
236
|
+
chunkStart: integer("chunk_start").notNull(),
|
|
237
|
+
chunkEnd: integer("chunk_end").notNull(),
|
|
238
|
+
status: pipelineStatusEnum("status").notNull().default("pending"),
|
|
239
|
+
attempts: integer("attempts").notNull().default(0),
|
|
240
|
+
embeddingProfile: text("embedding_profile"),
|
|
241
|
+
errorCode: text("error_code"),
|
|
242
|
+
availableAt: timestamp("available_at"),
|
|
243
|
+
startedAt: timestamp("started_at"),
|
|
244
|
+
completedAt: timestamp("completed_at"),
|
|
245
|
+
heartbeatAt: timestamp("heartbeat_at"),
|
|
246
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
247
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
248
|
+
}, (table) => [
|
|
249
|
+
uniqueIndex("document_pipeline_batches_generation_index_idx").on(table.generationId, table.batchIndex),
|
|
250
|
+
index("document_pipeline_batches_stage_status_available_idx").on(table.stage, table.status, table.availableAt),
|
|
251
|
+
index("document_pipeline_batches_document_id_idx").on(table.documentId)
|
|
252
|
+
]);
|
|
253
|
+
var tags = pgTable("tags", {
|
|
254
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
255
|
+
ownerId: uuid("owner_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
256
|
+
workspaceId: text("workspace_id"),
|
|
257
|
+
name: text("name").notNull(),
|
|
258
|
+
color: text("color"),
|
|
259
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
260
|
+
}, (table) => [
|
|
261
|
+
index("tags_owner_id_idx").on(table.ownerId),
|
|
262
|
+
uniqueIndex("tags_owner_name_idx").on(table.ownerId, table.name)
|
|
263
|
+
]);
|
|
264
|
+
var tagRelations = relations(tags, ({ many }) => ({
|
|
265
|
+
documents: many(documentTags)
|
|
266
|
+
}));
|
|
267
|
+
var categories = pgTable("categories", {
|
|
268
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
269
|
+
ownerId: uuid("owner_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
270
|
+
workspaceId: text("workspace_id"),
|
|
271
|
+
name: text("name").notNull(),
|
|
272
|
+
order: integer("order").notNull().default(0),
|
|
273
|
+
apiMode: text("api_mode").notNull().default("unavailable"),
|
|
274
|
+
apiPermissionRead: boolean("api_permission_read").notNull().default(false),
|
|
275
|
+
apiPermissionEdit: boolean("api_permission_edit").notNull().default(false),
|
|
276
|
+
apiPermissionWrite: boolean("api_permission_write").notNull().default(false),
|
|
277
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
278
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull()
|
|
279
|
+
}, (table) => [
|
|
280
|
+
index("categories_owner_id_idx").on(table.ownerId),
|
|
281
|
+
index("categories_api_mode_idx").on(table.apiMode)
|
|
282
|
+
]);
|
|
283
|
+
var categoryRelations = relations(categories, ({ one, many }) => ({
|
|
284
|
+
owner: one(users, { fields: [categories.ownerId], references: [users.id] }),
|
|
285
|
+
folders: many(folders),
|
|
286
|
+
documents: many(documents)
|
|
287
|
+
}));
|
|
288
|
+
var documentTags = pgTable("document_tags", {
|
|
289
|
+
workspaceId: text("workspace_id"),
|
|
290
|
+
documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
|
|
291
|
+
tagId: uuid("tag_id").notNull().references(() => tags.id, { onDelete: "cascade" })
|
|
292
|
+
}, (table) => [
|
|
293
|
+
uniqueIndex("document_tags_unique_idx").on(table.documentId, table.tagId)
|
|
294
|
+
]);
|
|
295
|
+
var documentTagRelations = relations(documentTags, ({ one }) => ({
|
|
296
|
+
document: one(documents, {
|
|
297
|
+
fields: [documentTags.documentId],
|
|
298
|
+
references: [documents.id]
|
|
299
|
+
}),
|
|
300
|
+
tag: one(tags, { fields: [documentTags.tagId], references: [tags.id] })
|
|
301
|
+
}));
|
|
302
|
+
var shareLinks = pgTable("share_links", {
|
|
303
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
304
|
+
documentId: uuid("document_id").references(() => documents.id, {
|
|
305
|
+
onDelete: "cascade"
|
|
306
|
+
}),
|
|
307
|
+
folderId: uuid("folder_id").references(() => folders.id, {
|
|
308
|
+
onDelete: "cascade"
|
|
309
|
+
}),
|
|
310
|
+
categoryId: uuid("category_id").references(() => categories.id, {
|
|
311
|
+
onDelete: "cascade"
|
|
312
|
+
}),
|
|
313
|
+
token: text("token").notNull().unique(),
|
|
314
|
+
passwordHash: text("password_hash"),
|
|
315
|
+
role: shareRoleEnum("role").notNull().default("viewer"),
|
|
316
|
+
expiresAt: timestamp("expires_at"),
|
|
317
|
+
createdBy: uuid("created_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
318
|
+
workspaceId: text("workspace_id"),
|
|
319
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
320
|
+
}, (table) => [
|
|
321
|
+
index("share_links_token_idx").on(table.token),
|
|
322
|
+
index("share_links_document_id_idx").on(table.documentId),
|
|
323
|
+
index("share_links_folder_id_idx").on(table.folderId),
|
|
324
|
+
index("share_links_category_id_idx").on(table.categoryId),
|
|
325
|
+
check("share_links_exactly_one_target_check", sql`num_nonnulls(${table.documentId}, ${table.folderId}, ${table.categoryId}) = 1`)
|
|
326
|
+
]);
|
|
327
|
+
var shareLinkRelations = relations(shareLinks, ({ one, many }) => ({
|
|
328
|
+
document: one(documents, {
|
|
329
|
+
fields: [shareLinks.documentId],
|
|
330
|
+
references: [documents.id]
|
|
331
|
+
}),
|
|
332
|
+
folder: one(folders, {
|
|
333
|
+
fields: [shareLinks.folderId],
|
|
334
|
+
references: [folders.id]
|
|
335
|
+
}),
|
|
336
|
+
category: one(categories, {
|
|
337
|
+
fields: [shareLinks.categoryId],
|
|
338
|
+
references: [categories.id]
|
|
339
|
+
}),
|
|
340
|
+
creator: one(users, {
|
|
341
|
+
fields: [shareLinks.createdBy],
|
|
342
|
+
references: [users.id]
|
|
343
|
+
}),
|
|
344
|
+
guestAccess: many(guestAccess)
|
|
345
|
+
}));
|
|
346
|
+
var guestAccess = pgTable("guest_access", {
|
|
347
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
348
|
+
shareLinkId: uuid("share_link_id").notNull().references(() => shareLinks.id, { onDelete: "cascade" }),
|
|
349
|
+
workspaceId: text("workspace_id"),
|
|
350
|
+
guestEmail: text("guest_email").notNull(),
|
|
351
|
+
grantedAt: timestamp("granted_at").defaultNow().notNull()
|
|
352
|
+
}, (table) => [index("guest_access_share_link_idx").on(table.shareLinkId)]);
|
|
353
|
+
var guestAccessRelations = relations(guestAccess, ({ one }) => ({
|
|
354
|
+
shareLink: one(shareLinks, {
|
|
355
|
+
fields: [guestAccess.shareLinkId],
|
|
356
|
+
references: [shareLinks.id]
|
|
357
|
+
})
|
|
358
|
+
}));
|
|
359
|
+
var attachments = pgTable("attachments", {
|
|
360
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
361
|
+
documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
|
|
362
|
+
workspaceId: text("workspace_id"),
|
|
363
|
+
filename: text("filename").notNull(),
|
|
364
|
+
mimeType: text("mime_type").notNull(),
|
|
365
|
+
size: bigint("size", { mode: "number" }).notNull(),
|
|
366
|
+
storageKey: text("storage_key").notNull(),
|
|
367
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
368
|
+
}, (table) => [index("attachments_document_id_idx").on(table.documentId)]);
|
|
369
|
+
var attachmentRelations = relations(attachments, ({ one }) => ({
|
|
370
|
+
document: one(documents, {
|
|
371
|
+
fields: [attachments.documentId],
|
|
372
|
+
references: [documents.id]
|
|
373
|
+
})
|
|
374
|
+
}));
|
|
375
|
+
var versions = pgTable("versions", {
|
|
376
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
377
|
+
documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
|
|
378
|
+
workspaceId: text("workspace_id"),
|
|
379
|
+
content: text("content").notNull(),
|
|
380
|
+
contentJson: jsonb("content_json"),
|
|
381
|
+
createdBy: uuid("created_by").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
382
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
383
|
+
label: text("label"),
|
|
384
|
+
description: text("description"),
|
|
385
|
+
isSnapshot: boolean("is_snapshot").default(false),
|
|
386
|
+
restoredFrom: uuid("restored_from")
|
|
387
|
+
}, (table) => [
|
|
388
|
+
index("versions_document_id_idx").on(table.documentId),
|
|
389
|
+
index("versions_created_at_idx").on(table.createdAt),
|
|
390
|
+
index("versions_is_snapshot_idx").on(table.isSnapshot)
|
|
391
|
+
]);
|
|
392
|
+
var documentEmbeddings = pgTable("document_embeddings", {
|
|
393
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
394
|
+
documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
|
|
395
|
+
workspaceId: text("workspace_id"),
|
|
396
|
+
chunkIndex: bigint("chunk_index", { mode: "number" }).notNull(),
|
|
397
|
+
chunkText: text("chunk_text").notNull(),
|
|
398
|
+
chunkHash: text("chunk_hash"),
|
|
399
|
+
charStart: integer("char_start").notNull().default(0),
|
|
400
|
+
charEnd: integer("char_end").notNull().default(0),
|
|
401
|
+
embedding: vector("embedding", { dimensions: 1024 }),
|
|
402
|
+
embeddingModel: text("embedding_model").notNull().default(""),
|
|
403
|
+
generationId: uuid("generation_id").notNull(),
|
|
404
|
+
embeddingDimensions: integer("embedding_dimensions").notNull().default(1024),
|
|
405
|
+
embeddingProfile: text("embedding_profile").notNull().default("legacy"),
|
|
406
|
+
isValid: boolean("is_valid").notNull().default(false),
|
|
407
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
408
|
+
}, (table) => [
|
|
409
|
+
index("document_embeddings_doc_id_idx").on(table.documentId),
|
|
410
|
+
uniqueIndex("document_embeddings_doc_chunk_idx").on(table.documentId, table.generationId, table.chunkIndex),
|
|
411
|
+
index("document_embeddings_generation_valid_idx").on(table.documentId, table.generationId, table.isValid),
|
|
412
|
+
index("idx_document_embeddings_embedding_model").on(table.embeddingModel),
|
|
413
|
+
index("idx_document_embeddings_hnsw").using("hnsw", sql`${table.embedding} vector_cosine_ops`),
|
|
414
|
+
index("idx_document_embeddings_diskann").using("diskann", sql`${table.embedding} vector_cosine_ops`)
|
|
415
|
+
]);
|
|
416
|
+
var documentEmbeddingRelations = relations(documentEmbeddings, ({ one }) => ({
|
|
417
|
+
document: one(documents, {
|
|
418
|
+
fields: [documentEmbeddings.documentId],
|
|
419
|
+
references: [documents.id]
|
|
420
|
+
})
|
|
421
|
+
}));
|
|
422
|
+
var apiKeys = pgTable("api_keys", {
|
|
423
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
424
|
+
ownerId: uuid("owner_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
425
|
+
workspaceId: text("workspace_id"),
|
|
426
|
+
name: text("name").notNull(),
|
|
427
|
+
keyHash: text("key_hash").notNull().unique(),
|
|
428
|
+
prefix: text("prefix").notNull(),
|
|
429
|
+
encryptedKey: text("encrypted_key"),
|
|
430
|
+
scopes: jsonb("scopes").notNull().default("[]"),
|
|
431
|
+
lastUsedAt: timestamp("last_used_at"),
|
|
432
|
+
expiresAt: timestamp("expires_at"),
|
|
433
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
434
|
+
}, (table) => [
|
|
435
|
+
index("idx_api_keys_owner").on(table.ownerId),
|
|
436
|
+
index("idx_api_keys_prefix").on(table.prefix)
|
|
437
|
+
]);
|
|
438
|
+
var apiKeyRelations = relations(apiKeys, ({ one }) => ({
|
|
439
|
+
owner: one(users, { fields: [apiKeys.ownerId], references: [users.id] })
|
|
440
|
+
}));
|
|
441
|
+
var auditLog = pgTable("audit_log", {
|
|
442
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
443
|
+
actorId: uuid("actor_id").notNull(),
|
|
444
|
+
workspaceId: text("workspace_id"),
|
|
445
|
+
action: text("action").notNull(),
|
|
446
|
+
resourceType: text("resource_type").notNull(),
|
|
447
|
+
resourceId: uuid("resource_id"),
|
|
448
|
+
details: jsonb("details").notNull().default("{}"),
|
|
449
|
+
ipAddress: text("ip_address"),
|
|
450
|
+
userAgent: text("user_agent"),
|
|
451
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
452
|
+
}, (table) => [
|
|
453
|
+
index("idx_audit_log_actor").on(table.actorId),
|
|
454
|
+
index("idx_audit_log_resource").on(table.resourceType, table.resourceId),
|
|
455
|
+
index("idx_audit_log_created").on(table.createdAt)
|
|
456
|
+
]);
|
|
457
|
+
var lifecycleOperations = pgTable("lifecycle_operations", {
|
|
458
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
459
|
+
actorUserId: uuid("actor_user_id").references(() => users.id, { onDelete: "set null" }),
|
|
460
|
+
actorSubjectHash: text("actor_subject_hash").notNull(),
|
|
461
|
+
idempotencyKey: text("idempotency_key").notNull(),
|
|
462
|
+
operationKind: lifecycleOperationKindEnum("operation_kind").notNull(),
|
|
463
|
+
status: lifecycleOperationStatusEnum("status").notNull().default("pending"),
|
|
464
|
+
leaseOwner: text("lease_owner"),
|
|
465
|
+
leaseExpiresAt: timestamp("lease_expires_at"),
|
|
466
|
+
fenceTokenHash: text("fence_token_hash"),
|
|
467
|
+
completedSteps: jsonb("completed_steps").notNull().default("[]"),
|
|
468
|
+
terminalResult: jsonb("terminal_result"),
|
|
469
|
+
safeErrorCode: text("safe_error_code"),
|
|
470
|
+
attemptCount: integer("attempt_count").notNull().default(0),
|
|
471
|
+
createdAt: timestamp("created_at").defaultNow().notNull(),
|
|
472
|
+
updatedAt: timestamp("updated_at").defaultNow().notNull(),
|
|
473
|
+
completedAt: timestamp("completed_at")
|
|
474
|
+
}, (table) => [
|
|
475
|
+
uniqueIndex("lifecycle_operations_actor_idempotency_idx").on(table.actorUserId, table.idempotencyKey),
|
|
476
|
+
index("lifecycle_operations_status_lease_idx").on(table.status, table.leaseExpiresAt),
|
|
477
|
+
index("lifecycle_operations_actor_idx").on(table.actorUserId),
|
|
478
|
+
index("lifecycle_operations_retryable_idx").on(table.status).where(sql`${table.status} = 'retryable'`),
|
|
479
|
+
check("lifecycle_operations_actor_subject_hash", sql`${table.actorSubjectHash} ~ '^[a-f0-9]{64}$'`)
|
|
480
|
+
]);
|
|
481
|
+
var documentCreateOperations = pgTable("document_create_operations", {
|
|
482
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
483
|
+
workspaceId: text("workspace_id").notNull(),
|
|
484
|
+
actorUserId: uuid("actor_user_id").notNull().references(() => users.id, { onDelete: "cascade" }),
|
|
485
|
+
idempotencyKey: text("idempotency_key").notNull(),
|
|
486
|
+
documentId: uuid("document_id").notNull().references(() => documents.id, { onDelete: "cascade" }),
|
|
487
|
+
createdAt: timestamp("created_at").defaultNow().notNull()
|
|
488
|
+
}, (table) => [
|
|
489
|
+
uniqueIndex("document_create_operations_workspace_actor_key_idx").on(table.workspaceId, table.actorUserId, table.idempotencyKey),
|
|
490
|
+
uniqueIndex("document_create_operations_document_idx").on(table.documentId)
|
|
491
|
+
]);
|
|
492
|
+
var versionRelations = relations(versions, ({ one }) => ({
|
|
493
|
+
document: one(documents, {
|
|
494
|
+
fields: [versions.documentId],
|
|
495
|
+
references: [documents.id]
|
|
496
|
+
}),
|
|
497
|
+
creator: one(users, {
|
|
498
|
+
fields: [versions.createdBy],
|
|
499
|
+
references: [users.id]
|
|
500
|
+
})
|
|
501
|
+
}));
|
|
502
|
+
|
|
503
|
+
// ../../backend/src/lib/lifecycle-service.ts
|
|
504
|
+
import { and, eq, inArray, lt, or, sql as sql2 } from "drizzle-orm";
|
|
505
|
+
var LEASE_MS = 60000;
|
|
506
|
+
|
|
507
|
+
class LifecycleFenceRejectedError extends Error {
|
|
508
|
+
code = "fence_rejected";
|
|
509
|
+
constructor(message = "Purge fence rejected") {
|
|
510
|
+
super(message);
|
|
511
|
+
this.name = "LifecycleFenceRejectedError";
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
class LifecycleLeaseLostError extends Error {
|
|
516
|
+
code = "lease_lost";
|
|
517
|
+
constructor() {
|
|
518
|
+
super("Lifecycle operation lease was lost");
|
|
519
|
+
this.name = "LifecycleLeaseLostError";
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
function throwIfAborted(signal) {
|
|
523
|
+
if (signal?.aborted)
|
|
524
|
+
throw signal.reason ?? new DOMException("Aborted", "AbortError");
|
|
525
|
+
}
|
|
526
|
+
function safeErrorCode(error) {
|
|
527
|
+
if (error instanceof DOMException && error.name === "AbortError")
|
|
528
|
+
return "aborted";
|
|
529
|
+
if (error instanceof LifecycleFenceRejectedError)
|
|
530
|
+
return error.code;
|
|
531
|
+
if (error instanceof LifecycleLeaseLostError)
|
|
532
|
+
return error.code;
|
|
533
|
+
return "persistence_failed";
|
|
534
|
+
}
|
|
535
|
+
function tokenHash(token) {
|
|
536
|
+
return new Bun.CryptoHasher("sha256").update(token).digest("hex");
|
|
537
|
+
}
|
|
538
|
+
function subjectHash(actorUserId) {
|
|
539
|
+
return new Bun.CryptoHasher("sha256").update(actorUserId).digest("hex");
|
|
540
|
+
}
|
|
541
|
+
function checksumLine(hash, record) {
|
|
542
|
+
hash.update(`${JSON.stringify(record)}
|
|
543
|
+
`, "utf8");
|
|
544
|
+
}
|
|
545
|
+
function counts(value) {
|
|
546
|
+
return typeof value === "object" && value !== null ? value : {};
|
|
547
|
+
}
|
|
548
|
+
function operationCounts(operation) {
|
|
549
|
+
const result = operation.terminalResult;
|
|
550
|
+
return counts(result?.deletedByDomain);
|
|
551
|
+
}
|
|
552
|
+
function requireLeaseWrite(rows) {
|
|
553
|
+
if (rows.length !== 1)
|
|
554
|
+
throw new LifecycleLeaseLostError;
|
|
555
|
+
}
|
|
556
|
+
function createPersistentLifecycleService(runtime, database, hostSteps = []) {
|
|
557
|
+
const orderedSteps = [...hostSteps].sort((a, b) => a.order - b.order || a.id.localeCompare(b.id));
|
|
558
|
+
if (new Set(orderedSteps.map((step) => step.id)).size !== orderedSteps.length) {
|
|
559
|
+
throw new Error("Lifecycle host step IDs must be globally unique");
|
|
560
|
+
}
|
|
561
|
+
const withActor = (actorUserId, operation) => database.withActorTransaction(actorUserId, operation);
|
|
562
|
+
async function getOrCreateOperation(ctx) {
|
|
563
|
+
return withActor(ctx.actorUserId, async (tx) => {
|
|
564
|
+
await tx.insert(lifecycleOperations).values({
|
|
565
|
+
actorUserId: ctx.actorUserId,
|
|
566
|
+
actorSubjectHash: subjectHash(ctx.actorUserId),
|
|
567
|
+
idempotencyKey: ctx.idempotencyKey,
|
|
568
|
+
operationKind: "purge",
|
|
569
|
+
status: "pending"
|
|
570
|
+
}).onConflictDoNothing();
|
|
571
|
+
const operation = await tx.query.lifecycleOperations.findFirst({
|
|
572
|
+
where: and(eq(lifecycleOperations.actorUserId, ctx.actorUserId), eq(lifecycleOperations.idempotencyKey, ctx.idempotencyKey))
|
|
573
|
+
});
|
|
574
|
+
if (operation?.operationKind !== "purge")
|
|
575
|
+
throw new Error("persistence_failed");
|
|
576
|
+
return operation;
|
|
577
|
+
});
|
|
578
|
+
}
|
|
579
|
+
async function acquireLease(operation, actorUserId, owner) {
|
|
580
|
+
return withActor(actorUserId, async (tx) => {
|
|
581
|
+
const now = new Date;
|
|
582
|
+
const expiry = new Date(now.getTime() + LEASE_MS);
|
|
583
|
+
const updated = await tx.update(lifecycleOperations).set({
|
|
584
|
+
status: "running",
|
|
585
|
+
leaseOwner: owner,
|
|
586
|
+
leaseExpiresAt: expiry,
|
|
587
|
+
attemptCount: sql2`${lifecycleOperations.attemptCount} + 1`,
|
|
588
|
+
updatedAt: now
|
|
589
|
+
}).where(and(eq(lifecycleOperations.id, operation.id), or(eq(lifecycleOperations.status, "pending"), eq(lifecycleOperations.status, "retryable"), and(eq(lifecycleOperations.status, "running"), lt(lifecycleOperations.leaseExpiresAt, now))))).returning({ id: lifecycleOperations.id });
|
|
590
|
+
return updated.length === 1;
|
|
591
|
+
});
|
|
592
|
+
}
|
|
593
|
+
async function persistStep(actorUserId, operationId, leaseOwner, step, deletedByDomain) {
|
|
594
|
+
await withActor(actorUserId, async (tx) => {
|
|
595
|
+
const current = await tx.query.lifecycleOperations.findFirst({
|
|
596
|
+
where: and(eq(lifecycleOperations.id, operationId), eq(lifecycleOperations.leaseOwner, leaseOwner))
|
|
597
|
+
});
|
|
598
|
+
if (current?.status !== "running" || current.leaseExpiresAt && current.leaseExpiresAt < new Date) {
|
|
599
|
+
throw new LifecycleLeaseLostError;
|
|
600
|
+
}
|
|
601
|
+
const completed = Array.isArray(current.completedSteps) ? current.completedSteps.filter((value) => typeof value === "string") : [];
|
|
602
|
+
if (!completed.includes(step))
|
|
603
|
+
completed.push(step);
|
|
604
|
+
const updated = await tx.update(lifecycleOperations).set({
|
|
605
|
+
completedSteps: completed,
|
|
606
|
+
terminalResult: { deletedByDomain },
|
|
607
|
+
updatedAt: new Date
|
|
608
|
+
}).where(and(eq(lifecycleOperations.id, operationId), eq(lifecycleOperations.leaseOwner, leaseOwner))).returning({ id: lifecycleOperations.id });
|
|
609
|
+
requireLeaseWrite(updated);
|
|
610
|
+
});
|
|
611
|
+
}
|
|
612
|
+
async function runStep(operation, actorUserId, leaseOwner, step, deletedByDomain, action) {
|
|
613
|
+
const completed = Array.isArray(operation.completedSteps) ? operation.completedSteps : [];
|
|
614
|
+
if (completed.includes(step))
|
|
615
|
+
return;
|
|
616
|
+
const count = await action();
|
|
617
|
+
deletedByDomain[step] = count;
|
|
618
|
+
await persistStep(actorUserId, operation.id, leaseOwner, step, deletedByDomain);
|
|
619
|
+
operation.completedSteps = [...completed, step];
|
|
620
|
+
}
|
|
621
|
+
return {
|
|
622
|
+
async* exportUserData(ctx) {
|
|
623
|
+
throwIfAborted(ctx.signal);
|
|
624
|
+
const exportId = crypto.randomUUID();
|
|
625
|
+
const hash = new Bun.CryptoHasher("sha256");
|
|
626
|
+
let recordCount = 0;
|
|
627
|
+
const manifest = {
|
|
628
|
+
type: "manifest",
|
|
629
|
+
schemaVersion: 1,
|
|
630
|
+
exportId,
|
|
631
|
+
actorUserId: ctx.actorUserId,
|
|
632
|
+
generatedAt: new Date().toISOString()
|
|
633
|
+
};
|
|
634
|
+
checksumLine(hash, manifest);
|
|
635
|
+
recordCount += 1;
|
|
636
|
+
yield manifest;
|
|
637
|
+
const ownedDocuments = await withActor(ctx.actorUserId, (tx) => tx.select().from(documents).where(eq(documents.ownerId, ctx.actorUserId)));
|
|
638
|
+
for (const document of ownedDocuments) {
|
|
639
|
+
throwIfAborted(ctx.signal);
|
|
640
|
+
const record = {
|
|
641
|
+
type: "data",
|
|
642
|
+
domain: "documents",
|
|
643
|
+
resourceType: "document",
|
|
644
|
+
resourceId: document.id,
|
|
645
|
+
workspaceId: document.workspaceId,
|
|
646
|
+
payload: {
|
|
647
|
+
title: document.title,
|
|
648
|
+
content: document.content,
|
|
649
|
+
contentJson: document.contentJson,
|
|
650
|
+
metadata: document.metadata,
|
|
651
|
+
visibility: document.visibility,
|
|
652
|
+
createdAt: document.createdAt,
|
|
653
|
+
updatedAt: document.updatedAt
|
|
654
|
+
}
|
|
655
|
+
};
|
|
656
|
+
checksumLine(hash, record);
|
|
657
|
+
recordCount += 1;
|
|
658
|
+
yield record;
|
|
659
|
+
}
|
|
660
|
+
const ownedAttachments = await withActor(ctx.actorUserId, (tx) => tx.select({
|
|
661
|
+
id: attachments.id,
|
|
662
|
+
workspaceId: attachments.workspaceId,
|
|
663
|
+
filename: attachments.filename,
|
|
664
|
+
mimeType: attachments.mimeType,
|
|
665
|
+
size: attachments.size
|
|
666
|
+
}).from(attachments).innerJoin(documents, eq(attachments.documentId, documents.id)).where(eq(documents.ownerId, ctx.actorUserId)));
|
|
667
|
+
for (const attachment of ownedAttachments) {
|
|
668
|
+
throwIfAborted(ctx.signal);
|
|
669
|
+
const record = {
|
|
670
|
+
type: "attachment",
|
|
671
|
+
attachmentId: attachment.id,
|
|
672
|
+
workspaceId: attachment.workspaceId,
|
|
673
|
+
filename: attachment.filename,
|
|
674
|
+
contentType: attachment.mimeType,
|
|
675
|
+
size: attachment.size,
|
|
676
|
+
sha256: null
|
|
677
|
+
};
|
|
678
|
+
checksumLine(hash, record);
|
|
679
|
+
recordCount += 1;
|
|
680
|
+
yield record;
|
|
681
|
+
}
|
|
682
|
+
for (const step of orderedSteps) {
|
|
683
|
+
if (!step.export)
|
|
684
|
+
continue;
|
|
685
|
+
for await (const record of step.export(ctx)) {
|
|
686
|
+
throwIfAborted(ctx.signal);
|
|
687
|
+
if (record.type === "manifest" || record.type === "complete")
|
|
688
|
+
throw new Error("Host export step emitted a reserved record type");
|
|
689
|
+
checksumLine(hash, record);
|
|
690
|
+
recordCount += 1;
|
|
691
|
+
yield record;
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
yield { type: "complete", recordCount, checksum: hash.digest("hex") };
|
|
695
|
+
},
|
|
696
|
+
async purgeUserData(ctx, gate) {
|
|
697
|
+
throwIfAborted(ctx.signal);
|
|
698
|
+
let operation = await getOrCreateOperation(ctx);
|
|
699
|
+
if (operation.status === "completed")
|
|
700
|
+
return {
|
|
701
|
+
status: "already_completed",
|
|
702
|
+
operationId: operation.id,
|
|
703
|
+
deletedByDomain: operationCounts(operation)
|
|
704
|
+
};
|
|
705
|
+
const leaseOwner = crypto.randomUUID();
|
|
706
|
+
if (!await acquireLease(operation, ctx.actorUserId, leaseOwner))
|
|
707
|
+
throw new LifecycleLeaseLostError;
|
|
708
|
+
operation = await withActor(ctx.actorUserId, async (tx) => await tx.query.lifecycleOperations.findFirst({
|
|
709
|
+
where: eq(lifecycleOperations.id, operation.id)
|
|
710
|
+
}) ?? operation);
|
|
711
|
+
const deletedByDomain = operationCounts(operation);
|
|
712
|
+
try {
|
|
713
|
+
throwIfAborted(ctx.signal);
|
|
714
|
+
await withActor(ctx.actorUserId, async (tx) => {
|
|
715
|
+
const updated = await tx.update(lifecycleOperations).set({
|
|
716
|
+
fenceTokenHash: tokenHash(gate.fenceToken),
|
|
717
|
+
updatedAt: new Date
|
|
718
|
+
}).where(and(eq(lifecycleOperations.id, operation.id), eq(lifecycleOperations.leaseOwner, leaseOwner))).returning({ id: lifecycleOperations.id });
|
|
719
|
+
requireLeaseWrite(updated);
|
|
720
|
+
});
|
|
721
|
+
await runtime.verifyPurgeFence(ctx, gate.fenceToken);
|
|
722
|
+
const dbDelete = (action) => withActor(ctx.actorUserId, action).then((rows) => rows.length);
|
|
723
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "cancel_account_jobs", deletedByDomain, () => runtime.cancelAccountJobs(ctx.actorUserId, ctx.signal));
|
|
724
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "remove_collaboration_state", deletedByDomain, () => runtime.removeCollaborationState(ctx.actorUserId, ctx.signal));
|
|
725
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "remove_subject_created_shares", deletedByDomain, () => dbDelete((tx) => tx.delete(shareLinks).where(eq(shareLinks.createdBy, ctx.actorUserId)).returning({ id: shareLinks.id })));
|
|
726
|
+
const owned = await withActor(ctx.actorUserId, (tx) => tx.select({ id: documents.id }).from(documents).where(eq(documents.ownerId, ctx.actorUserId)));
|
|
727
|
+
const documentIds = owned.map((row) => row.id);
|
|
728
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "remove_document_versions", deletedByDomain, () => documentIds.length ? dbDelete((tx) => tx.delete(versions).where(inArray(versions.documentId, documentIds)).returning({ id: versions.id })) : Promise.resolve(0));
|
|
729
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "remove_chunks_and_embeddings", deletedByDomain, () => documentIds.length ? dbDelete((tx) => tx.delete(documentEmbeddings).where(inArray(documentEmbeddings.documentId, documentIds)).returning({ id: documentEmbeddings.id })) : Promise.resolve(0));
|
|
730
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "remove_graph_state", deletedByDomain, () => runtime.removeGraphState(documentIds, ctx.signal));
|
|
731
|
+
const objectRows = documentIds.length ? await withActor(ctx.actorUserId, (tx) => tx.select({
|
|
732
|
+
id: attachments.id,
|
|
733
|
+
storageKey: attachments.storageKey
|
|
734
|
+
}).from(attachments).where(inArray(attachments.documentId, documentIds))) : [];
|
|
735
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "delete_attachment_objects", deletedByDomain, () => runtime.deleteObjects(objectRows.map((row) => row.storageKey), ctx.signal));
|
|
736
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "remove_attachment_rows", deletedByDomain, () => objectRows.length ? dbDelete((tx) => tx.delete(attachments).where(inArray(attachments.id, objectRows.map((row) => row.id))).returning({ id: attachments.id })) : Promise.resolve(0));
|
|
737
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "remove_subject_documents", deletedByDomain, () => dbDelete((tx) => tx.delete(documents).where(eq(documents.ownerId, ctx.actorUserId)).returning({ id: documents.id })));
|
|
738
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "clear_redis_state", deletedByDomain, () => runtime.clearAccountRedisState(ctx.actorUserId, ctx.signal));
|
|
739
|
+
for (const step of orderedSteps)
|
|
740
|
+
if (step.purge)
|
|
741
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, `host:${step.id}`, deletedByDomain, async () => (await step.purge?.(ctx))?.deletedCount ?? 0);
|
|
742
|
+
await runStep(operation, ctx.actorUserId, leaseOwner, "write_deletion_audit", deletedByDomain, async () => {
|
|
743
|
+
await withActor(ctx.actorUserId, (tx) => tx.insert(auditLog).values({
|
|
744
|
+
actorId: ctx.actorUserId,
|
|
745
|
+
action: "account_data_purged",
|
|
746
|
+
resourceType: "lifecycle_operation",
|
|
747
|
+
details: {
|
|
748
|
+
operationId: operation.id,
|
|
749
|
+
outcome: "completed",
|
|
750
|
+
deletedByDomain
|
|
751
|
+
}
|
|
752
|
+
}));
|
|
753
|
+
return 1;
|
|
754
|
+
});
|
|
755
|
+
await withActor(ctx.actorUserId, async (tx) => {
|
|
756
|
+
const updated = await tx.update(lifecycleOperations).set({
|
|
757
|
+
status: "completed",
|
|
758
|
+
terminalResult: { deletedByDomain },
|
|
759
|
+
completedAt: new Date,
|
|
760
|
+
leaseOwner: null,
|
|
761
|
+
leaseExpiresAt: null,
|
|
762
|
+
updatedAt: new Date
|
|
763
|
+
}).where(and(eq(lifecycleOperations.id, operation.id), eq(lifecycleOperations.leaseOwner, leaseOwner))).returning({ id: lifecycleOperations.id });
|
|
764
|
+
requireLeaseWrite(updated);
|
|
765
|
+
});
|
|
766
|
+
return {
|
|
767
|
+
status: "completed",
|
|
768
|
+
operationId: operation.id,
|
|
769
|
+
deletedByDomain
|
|
770
|
+
};
|
|
771
|
+
} catch (error) {
|
|
772
|
+
if (error instanceof LifecycleLeaseLostError)
|
|
773
|
+
throw error;
|
|
774
|
+
const code = safeErrorCode(error);
|
|
775
|
+
await withActor(ctx.actorUserId, async (tx) => {
|
|
776
|
+
const updated = await tx.update(lifecycleOperations).set({
|
|
777
|
+
status: error instanceof LifecycleFenceRejectedError ? "rejected" : "retryable",
|
|
778
|
+
safeErrorCode: code,
|
|
779
|
+
leaseOwner: null,
|
|
780
|
+
leaseExpiresAt: null,
|
|
781
|
+
completedAt: error instanceof LifecycleFenceRejectedError ? new Date : null,
|
|
782
|
+
updatedAt: new Date
|
|
783
|
+
}).where(and(eq(lifecycleOperations.id, operation.id), eq(lifecycleOperations.leaseOwner, leaseOwner))).returning({ id: lifecycleOperations.id });
|
|
784
|
+
requireLeaseWrite(updated);
|
|
785
|
+
});
|
|
786
|
+
throw error;
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
};
|
|
790
|
+
}
|
|
791
|
+
function bindPersistentLifecycle(service, assertPurgeAllowed) {
|
|
792
|
+
return {
|
|
793
|
+
exportUserData: service.exportUserData,
|
|
794
|
+
async purgeUserData(ctx) {
|
|
795
|
+
const gate = await assertPurgeAllowed(ctx);
|
|
796
|
+
return service.purgeUserData(ctx, gate);
|
|
797
|
+
}
|
|
798
|
+
};
|
|
799
|
+
}
|
|
800
|
+
function createPersistentLifecycleRuntime(options) {
|
|
801
|
+
const service = createPersistentLifecycleService(options.runtime, options.database, options.hostSteps ?? []);
|
|
802
|
+
return bindPersistentLifecycle(service, options.assertPurgeAllowed);
|
|
803
|
+
}
|
|
804
|
+
export {
|
|
805
|
+
requireLeaseWrite,
|
|
806
|
+
createPersistentLifecycleService,
|
|
807
|
+
createPersistentLifecycleRuntime,
|
|
808
|
+
bindPersistentLifecycle,
|
|
809
|
+
LifecycleLeaseLostError,
|
|
810
|
+
LifecycleFenceRejectedError
|
|
811
|
+
};
|
package/package.json
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hiai-gg/docsmint",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.3",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"browser": {
|
|
6
6
|
"./dist/backend-launcher.js": false,
|
|
7
7
|
"./dist/lifecycle.js": false,
|
|
8
8
|
"./dist/lifecycle-persistent.js": false,
|
|
9
|
+
"./dist/lifecycle-runtime.js": false,
|
|
9
10
|
"./dist/storage-quota.js": false,
|
|
10
11
|
"./dist/workspace.js": false
|
|
11
12
|
},
|
|
@@ -59,6 +60,11 @@
|
|
|
59
60
|
"import": "./dist/lifecycle-persistent.js",
|
|
60
61
|
"types": "./dist/lifecycle-persistent.d.ts"
|
|
61
62
|
},
|
|
63
|
+
"./lifecycle/runtime": {
|
|
64
|
+
"browser": "./dist/server-only-browser-entry.js",
|
|
65
|
+
"import": "./dist/lifecycle-runtime.js",
|
|
66
|
+
"types": "./dist/lifecycle-runtime.d.ts"
|
|
67
|
+
},
|
|
62
68
|
"./workspace": {
|
|
63
69
|
"browser": "./dist/server-only-browser-entry.js",
|
|
64
70
|
"import": "./dist/workspace.js",
|
|
@@ -24,7 +24,7 @@ import { registerSearch } from "./commands/search.js";
|
|
|
24
24
|
import { registerSnapshot } from "./commands/snapshot.js";
|
|
25
25
|
import { registerUpdate } from "./commands/update.js";
|
|
26
26
|
|
|
27
|
-
const VERSION = "0.4.
|
|
27
|
+
const VERSION = "0.4.3";
|
|
28
28
|
|
|
29
29
|
const program = new Command();
|
|
30
30
|
program
|