@powerhousedao/reactor-drive 6.0.0-dev.252 → 6.0.2-staging.9
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/index.d.ts +353 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +1228 -0
- package/dist/index.js.map +1 -0
- package/package.json +8 -5
package/dist/index.js
ADDED
|
@@ -0,0 +1,1228 @@
|
|
|
1
|
+
import { baseCreateDocument, baseLoadFromInput, baseSaveToFileHandle, createPresignedHeader, createReducer, createState, defaultBaseState, generateId, isDocumentAction, replayDocument } from "@powerhousedao/shared/document-model";
|
|
2
|
+
import { Migrator, sql } from "kysely";
|
|
3
|
+
import { BaseReadModel, addRelationshipAction, createDocumentAction, removeRelationshipAction, upgradeDocumentAction } from "@powerhousedao/reactor";
|
|
4
|
+
import { gql } from "graphql-tag";
|
|
5
|
+
//#region \0rolldown/runtime.js
|
|
6
|
+
var __defProp = Object.defineProperty;
|
|
7
|
+
var __exportAll = (all, no_symbols) => {
|
|
8
|
+
let target = {};
|
|
9
|
+
for (var name in all) __defProp(target, name, {
|
|
10
|
+
get: all[name],
|
|
11
|
+
enumerable: true
|
|
12
|
+
});
|
|
13
|
+
if (!no_symbols) __defProp(target, Symbol.toStringTag, { value: "Module" });
|
|
14
|
+
return target;
|
|
15
|
+
};
|
|
16
|
+
//#endregion
|
|
17
|
+
//#region src/constants.ts
|
|
18
|
+
const REACTOR_DRIVE_DOCUMENT_TYPE = "powerhouse/reactor-drive";
|
|
19
|
+
const DRIVE_CHILD_RELATIONSHIP_TYPE = "drive/child";
|
|
20
|
+
const REACTOR_DRIVE_FILE_EXTENSION = "phrd";
|
|
21
|
+
//#endregion
|
|
22
|
+
//#region src/actions.ts
|
|
23
|
+
function setDriveNameAction(input) {
|
|
24
|
+
return {
|
|
25
|
+
id: generateId(),
|
|
26
|
+
type: "SET_DRIVE_NAME",
|
|
27
|
+
scope: "global",
|
|
28
|
+
timestampUtcMs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
29
|
+
input
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function setDriveIconAction(input) {
|
|
33
|
+
return {
|
|
34
|
+
id: generateId(),
|
|
35
|
+
type: "SET_DRIVE_ICON",
|
|
36
|
+
scope: "global",
|
|
37
|
+
timestampUtcMs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
38
|
+
input
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
function setSharingTypeAction(input) {
|
|
42
|
+
return {
|
|
43
|
+
id: generateId(),
|
|
44
|
+
type: "SET_SHARING_TYPE",
|
|
45
|
+
scope: "local",
|
|
46
|
+
timestampUtcMs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
47
|
+
input
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
function setAvailableOfflineAction(input) {
|
|
51
|
+
return {
|
|
52
|
+
id: generateId(),
|
|
53
|
+
type: "SET_AVAILABLE_OFFLINE",
|
|
54
|
+
scope: "local",
|
|
55
|
+
timestampUtcMs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
56
|
+
input
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function addFolderAction(input) {
|
|
60
|
+
return {
|
|
61
|
+
id: generateId(),
|
|
62
|
+
type: "ADD_FOLDER",
|
|
63
|
+
scope: "document",
|
|
64
|
+
timestampUtcMs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
65
|
+
input
|
|
66
|
+
};
|
|
67
|
+
}
|
|
68
|
+
function updateFolderAction(input) {
|
|
69
|
+
return {
|
|
70
|
+
id: generateId(),
|
|
71
|
+
type: "UPDATE_FOLDER",
|
|
72
|
+
scope: "document",
|
|
73
|
+
timestampUtcMs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
74
|
+
input
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
function removeFolderAction(input) {
|
|
78
|
+
return {
|
|
79
|
+
id: generateId(),
|
|
80
|
+
type: "REMOVE_FOLDER",
|
|
81
|
+
scope: "document",
|
|
82
|
+
timestampUtcMs: (/* @__PURE__ */ new Date()).toISOString(),
|
|
83
|
+
input
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
const reactorDriveActions = {
|
|
87
|
+
setDriveName: setDriveNameAction,
|
|
88
|
+
setDriveIcon: setDriveIconAction,
|
|
89
|
+
setSharingType: setSharingTypeAction,
|
|
90
|
+
setAvailableOffline: setAvailableOfflineAction,
|
|
91
|
+
addFolder: addFolderAction,
|
|
92
|
+
updateFolder: updateFolderAction,
|
|
93
|
+
removeFolder: removeFolderAction
|
|
94
|
+
};
|
|
95
|
+
//#endregion
|
|
96
|
+
//#region src/reducer/drive.ts
|
|
97
|
+
const reactorDriveStateReducer = (state, action) => {
|
|
98
|
+
if (isDocumentAction(action)) return state;
|
|
99
|
+
const typedAction = action;
|
|
100
|
+
switch (typedAction.type) {
|
|
101
|
+
case "SET_DRIVE_NAME": {
|
|
102
|
+
const input = typedAction.input;
|
|
103
|
+
state.global.name = input.name;
|
|
104
|
+
return state;
|
|
105
|
+
}
|
|
106
|
+
case "SET_DRIVE_ICON": {
|
|
107
|
+
const input = typedAction.input;
|
|
108
|
+
state.global.icon = input.icon;
|
|
109
|
+
return state;
|
|
110
|
+
}
|
|
111
|
+
case "SET_SHARING_TYPE": {
|
|
112
|
+
const input = typedAction.input;
|
|
113
|
+
state.local.sharingType = input.sharingType;
|
|
114
|
+
return state;
|
|
115
|
+
}
|
|
116
|
+
case "SET_AVAILABLE_OFFLINE": {
|
|
117
|
+
const input = typedAction.input;
|
|
118
|
+
state.local.availableOffline = input.availableOffline;
|
|
119
|
+
return state;
|
|
120
|
+
}
|
|
121
|
+
case "ADD_FOLDER":
|
|
122
|
+
case "UPDATE_FOLDER":
|
|
123
|
+
case "REMOVE_FOLDER": return state;
|
|
124
|
+
default: return state;
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
//#endregion
|
|
128
|
+
//#region src/module.ts
|
|
129
|
+
const initialGlobalState = {
|
|
130
|
+
name: "",
|
|
131
|
+
icon: null
|
|
132
|
+
};
|
|
133
|
+
const initialLocalState = {
|
|
134
|
+
sharingType: "private",
|
|
135
|
+
availableOffline: false
|
|
136
|
+
};
|
|
137
|
+
const reactorDriveCreateState = (state) => {
|
|
138
|
+
return {
|
|
139
|
+
...defaultBaseState(),
|
|
140
|
+
global: {
|
|
141
|
+
...initialGlobalState,
|
|
142
|
+
...state?.global
|
|
143
|
+
},
|
|
144
|
+
local: {
|
|
145
|
+
...initialLocalState,
|
|
146
|
+
...state?.local
|
|
147
|
+
}
|
|
148
|
+
};
|
|
149
|
+
};
|
|
150
|
+
const reactorDriveCreateDocument = (state) => {
|
|
151
|
+
const document = baseCreateDocument(reactorDriveCreateState, state);
|
|
152
|
+
document.header.documentType = REACTOR_DRIVE_DOCUMENT_TYPE;
|
|
153
|
+
return document;
|
|
154
|
+
};
|
|
155
|
+
const reactorDriveSaveToFileHandle = (document, input) => {
|
|
156
|
+
return baseSaveToFileHandle(document, input);
|
|
157
|
+
};
|
|
158
|
+
const reactorDriveLoadFromInput = (input) => {
|
|
159
|
+
return baseLoadFromInput(input, reactorDriveDocumentReducer);
|
|
160
|
+
};
|
|
161
|
+
const isReactorDriveState = (state) => {
|
|
162
|
+
return typeof state === "object" && state !== null && "global" in state && "local" in state;
|
|
163
|
+
};
|
|
164
|
+
const assertIsReactorDriveState = (state) => {
|
|
165
|
+
if (!isReactorDriveState(state)) throw new Error("Not a reactor-drive state");
|
|
166
|
+
};
|
|
167
|
+
const isReactorDriveDocument = (document) => {
|
|
168
|
+
return typeof document === "object" && document !== null && "header" in document && document.header.documentType === "powerhouse/reactor-drive";
|
|
169
|
+
};
|
|
170
|
+
const assertIsReactorDriveDocument = (document) => {
|
|
171
|
+
if (!isReactorDriveDocument(document)) throw new Error("Not a reactor-drive document");
|
|
172
|
+
};
|
|
173
|
+
const reactorDriveDocumentReducer = createReducer(reactorDriveStateReducer);
|
|
174
|
+
const reactorDriveDocumentGlobalState = {
|
|
175
|
+
id: REACTOR_DRIVE_DOCUMENT_TYPE,
|
|
176
|
+
name: "ReactorDrive",
|
|
177
|
+
extension: REACTOR_DRIVE_FILE_EXTENSION,
|
|
178
|
+
description: "",
|
|
179
|
+
author: {
|
|
180
|
+
name: "Powerhouse Inc",
|
|
181
|
+
website: "https://www.powerhouse.inc/"
|
|
182
|
+
},
|
|
183
|
+
specifications: [{
|
|
184
|
+
version: 1,
|
|
185
|
+
changeLog: [],
|
|
186
|
+
state: {
|
|
187
|
+
global: {
|
|
188
|
+
schema: "type ReactorDriveState {\n name: String!\n icon: String\n}",
|
|
189
|
+
initialValue: JSON.stringify(JSON.stringify(initialGlobalState)),
|
|
190
|
+
examples: []
|
|
191
|
+
},
|
|
192
|
+
local: {
|
|
193
|
+
schema: "type ReactorDriveLocalState {\n sharingType: String!\n availableOffline: Boolean!\n}",
|
|
194
|
+
initialValue: JSON.stringify(JSON.stringify(initialLocalState)),
|
|
195
|
+
examples: []
|
|
196
|
+
}
|
|
197
|
+
},
|
|
198
|
+
modules: [{
|
|
199
|
+
id: "reactor-drive/base-operations",
|
|
200
|
+
name: "base_operations",
|
|
201
|
+
description: "",
|
|
202
|
+
operations: [
|
|
203
|
+
{
|
|
204
|
+
id: "SET_DRIVE_NAME",
|
|
205
|
+
name: "SET_DRIVE_NAME",
|
|
206
|
+
description: "",
|
|
207
|
+
schema: "input SetDriveNameInput { name: String! }",
|
|
208
|
+
template: "",
|
|
209
|
+
reducer: "",
|
|
210
|
+
errors: [],
|
|
211
|
+
examples: [],
|
|
212
|
+
scope: "global"
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
id: "SET_DRIVE_ICON",
|
|
216
|
+
name: "SET_DRIVE_ICON",
|
|
217
|
+
description: "",
|
|
218
|
+
schema: "input SetDriveIconInput { icon: String }",
|
|
219
|
+
template: "",
|
|
220
|
+
reducer: "",
|
|
221
|
+
errors: [],
|
|
222
|
+
examples: [],
|
|
223
|
+
scope: "global"
|
|
224
|
+
},
|
|
225
|
+
{
|
|
226
|
+
id: "SET_SHARING_TYPE",
|
|
227
|
+
name: "SET_SHARING_TYPE",
|
|
228
|
+
description: "",
|
|
229
|
+
schema: "input SetSharingTypeInput { sharingType: String! }",
|
|
230
|
+
template: "",
|
|
231
|
+
reducer: "",
|
|
232
|
+
errors: [],
|
|
233
|
+
examples: [],
|
|
234
|
+
scope: "local"
|
|
235
|
+
},
|
|
236
|
+
{
|
|
237
|
+
id: "SET_AVAILABLE_OFFLINE",
|
|
238
|
+
name: "SET_AVAILABLE_OFFLINE",
|
|
239
|
+
description: "",
|
|
240
|
+
schema: "input SetAvailableOfflineInput { availableOffline: Boolean! }",
|
|
241
|
+
template: "",
|
|
242
|
+
reducer: "",
|
|
243
|
+
errors: [],
|
|
244
|
+
examples: [],
|
|
245
|
+
scope: "local"
|
|
246
|
+
},
|
|
247
|
+
{
|
|
248
|
+
id: "ADD_FOLDER",
|
|
249
|
+
name: "ADD_FOLDER",
|
|
250
|
+
description: "",
|
|
251
|
+
schema: "input AddFolderInput { folderId: String! parentFolderId: String name: String! }",
|
|
252
|
+
template: "",
|
|
253
|
+
reducer: "",
|
|
254
|
+
errors: [],
|
|
255
|
+
examples: [],
|
|
256
|
+
scope: "document"
|
|
257
|
+
},
|
|
258
|
+
{
|
|
259
|
+
id: "UPDATE_FOLDER",
|
|
260
|
+
name: "UPDATE_FOLDER",
|
|
261
|
+
description: "",
|
|
262
|
+
schema: "input UpdateFolderInput { folderId: String! name: String parentFolderId: String }",
|
|
263
|
+
template: "",
|
|
264
|
+
reducer: "",
|
|
265
|
+
errors: [],
|
|
266
|
+
examples: [],
|
|
267
|
+
scope: "document"
|
|
268
|
+
},
|
|
269
|
+
{
|
|
270
|
+
id: "REMOVE_FOLDER",
|
|
271
|
+
name: "REMOVE_FOLDER",
|
|
272
|
+
description: "",
|
|
273
|
+
schema: "input RemoveFolderInput { folderId: String! }",
|
|
274
|
+
template: "",
|
|
275
|
+
reducer: "",
|
|
276
|
+
errors: [],
|
|
277
|
+
examples: [],
|
|
278
|
+
scope: "document"
|
|
279
|
+
}
|
|
280
|
+
]
|
|
281
|
+
}]
|
|
282
|
+
}]
|
|
283
|
+
};
|
|
284
|
+
const reactorDriveDocumentModelModule = {
|
|
285
|
+
actions: reactorDriveActions,
|
|
286
|
+
reducer: reactorDriveDocumentReducer,
|
|
287
|
+
documentModel: createState(defaultBaseState(), reactorDriveDocumentGlobalState),
|
|
288
|
+
utils: {
|
|
289
|
+
fileExtension: REACTOR_DRIVE_FILE_EXTENSION,
|
|
290
|
+
createState: reactorDriveCreateState,
|
|
291
|
+
createDocument: reactorDriveCreateDocument,
|
|
292
|
+
loadFromInput: reactorDriveLoadFromInput,
|
|
293
|
+
saveToFileHandle: reactorDriveSaveToFileHandle,
|
|
294
|
+
isStateOfType: isReactorDriveState,
|
|
295
|
+
assertIsStateOfType: assertIsReactorDriveState,
|
|
296
|
+
isDocumentOfType: isReactorDriveDocument,
|
|
297
|
+
assertIsDocumentOfType: assertIsReactorDriveDocument
|
|
298
|
+
}
|
|
299
|
+
};
|
|
300
|
+
//#endregion
|
|
301
|
+
//#region src/schema/migrations/0001_drive_node.ts
|
|
302
|
+
var _0001_drive_node_exports = /* @__PURE__ */ __exportAll({
|
|
303
|
+
down: () => down$1,
|
|
304
|
+
up: () => up$1
|
|
305
|
+
});
|
|
306
|
+
async function up$1(db) {
|
|
307
|
+
await db.schema.createTable("DriveNode").ifNotExists().addColumn("driveId", "text", (col) => col.notNull()).addColumn("id", "text", (col) => col.notNull()).addColumn("kind", "text", (col) => col.notNull()).addColumn("name", "text", (col) => col.notNull()).addColumn("requestedName", "text", (col) => col.notNull()).addColumn("parentFolder", "text").addColumn("documentType", "text").addColumn("createdAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).addPrimaryKeyConstraint("pk_drive_node", ["driveId", "id"]).addCheckConstraint("chk_drive_node_document_type", sql`(kind = 'file' AND "documentType" IS NOT NULL) OR (kind = 'folder' AND "documentType" IS NULL)`).execute();
|
|
308
|
+
await db.schema.createIndex("idx_drive_node_parent_name").ifNotExists().on("DriveNode").columns([
|
|
309
|
+
"driveId",
|
|
310
|
+
"parentFolder",
|
|
311
|
+
"name"
|
|
312
|
+
]).execute();
|
|
313
|
+
await db.schema.createIndex("idx_drive_node_parent_kind_id").ifNotExists().on("DriveNode").columns([
|
|
314
|
+
"driveId",
|
|
315
|
+
"parentFolder",
|
|
316
|
+
"kind",
|
|
317
|
+
"id"
|
|
318
|
+
]).execute();
|
|
319
|
+
}
|
|
320
|
+
async function down$1(db) {
|
|
321
|
+
await db.schema.dropTable("DriveNode").execute();
|
|
322
|
+
}
|
|
323
|
+
//#endregion
|
|
324
|
+
//#region src/schema/migrations/0002_document_name.ts
|
|
325
|
+
var _0002_document_name_exports = /* @__PURE__ */ __exportAll({
|
|
326
|
+
down: () => down,
|
|
327
|
+
up: () => up
|
|
328
|
+
});
|
|
329
|
+
async function up(db) {
|
|
330
|
+
await db.schema.createTable("DocumentName").ifNotExists().addColumn("docId", "text", (col) => col.primaryKey()).addColumn("name", "text", (col) => col.notNull()).addColumn("updatedAt", "timestamptz", (col) => col.notNull().defaultTo(sql`NOW()`)).execute();
|
|
331
|
+
}
|
|
332
|
+
async function down(db) {
|
|
333
|
+
await db.schema.dropTable("DocumentName").execute();
|
|
334
|
+
}
|
|
335
|
+
//#endregion
|
|
336
|
+
//#region src/schema/migrations/migrator.ts
|
|
337
|
+
const migrations = {
|
|
338
|
+
"0001_drive_node": _0001_drive_node_exports,
|
|
339
|
+
"0002_document_name": _0002_document_name_exports
|
|
340
|
+
};
|
|
341
|
+
const REACTOR_DRIVE_MIGRATION_TABLE = "kysely_migration_reactor_drive";
|
|
342
|
+
const REACTOR_DRIVE_MIGRATION_LOCK_TABLE = "kysely_migration_reactor_drive_lock";
|
|
343
|
+
var ProgrammaticMigrationProvider = class {
|
|
344
|
+
getMigrations() {
|
|
345
|
+
return Promise.resolve(migrations);
|
|
346
|
+
}
|
|
347
|
+
};
|
|
348
|
+
async function runReactorDriveMigrations(db, schema) {
|
|
349
|
+
try {
|
|
350
|
+
await sql`CREATE SCHEMA IF NOT EXISTS ${sql.id(schema)}`.execute(db);
|
|
351
|
+
} catch (error) {
|
|
352
|
+
return {
|
|
353
|
+
success: false,
|
|
354
|
+
migrationsExecuted: [],
|
|
355
|
+
error: error instanceof Error ? error : /* @__PURE__ */ new Error("Failed to create schema")
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
const migrator = new Migrator({
|
|
359
|
+
db: db.withSchema(schema),
|
|
360
|
+
provider: new ProgrammaticMigrationProvider(),
|
|
361
|
+
migrationTableSchema: schema,
|
|
362
|
+
migrationTableName: REACTOR_DRIVE_MIGRATION_TABLE,
|
|
363
|
+
migrationLockTableName: REACTOR_DRIVE_MIGRATION_LOCK_TABLE
|
|
364
|
+
});
|
|
365
|
+
let error;
|
|
366
|
+
let results;
|
|
367
|
+
try {
|
|
368
|
+
const result = await migrator.migrateToLatest();
|
|
369
|
+
error = result.error;
|
|
370
|
+
results = result.results;
|
|
371
|
+
} catch (e) {
|
|
372
|
+
error = e;
|
|
373
|
+
results = [];
|
|
374
|
+
}
|
|
375
|
+
const migrationsExecuted = results?.map((result) => result.migrationName) ?? [];
|
|
376
|
+
if (error) return {
|
|
377
|
+
success: false,
|
|
378
|
+
migrationsExecuted,
|
|
379
|
+
error: error instanceof Error ? error : /* @__PURE__ */ new Error("Unknown migration error")
|
|
380
|
+
};
|
|
381
|
+
return {
|
|
382
|
+
success: true,
|
|
383
|
+
migrationsExecuted
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
async function getReactorDriveMigrationStatus(db, schema) {
|
|
387
|
+
return await new Migrator({
|
|
388
|
+
db: db.withSchema(schema),
|
|
389
|
+
provider: new ProgrammaticMigrationProvider(),
|
|
390
|
+
migrationTableSchema: schema,
|
|
391
|
+
migrationTableName: REACTOR_DRIVE_MIGRATION_TABLE,
|
|
392
|
+
migrationLockTableName: REACTOR_DRIVE_MIGRATION_LOCK_TABLE
|
|
393
|
+
}).getMigrations();
|
|
394
|
+
}
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/processors/utils/collisions.ts
|
|
397
|
+
/**
|
|
398
|
+
* Deterministic per-folder name collision rule. Given a desired name and the
|
|
399
|
+
* set of names already taken by siblings, returns the next available name as
|
|
400
|
+
* `requested`, `requested (2)`, `requested (3)`, etc.
|
|
401
|
+
*
|
|
402
|
+
* The rule matches the legacy document-drive module so that migration from
|
|
403
|
+
* the legacy state produces stable suffixes.
|
|
404
|
+
*/
|
|
405
|
+
function resolveCollision(requested, takenNames) {
|
|
406
|
+
const taken = /* @__PURE__ */ new Set();
|
|
407
|
+
for (const name of takenNames) taken.add(name);
|
|
408
|
+
if (!taken.has(requested)) return requested;
|
|
409
|
+
let suffix = 2;
|
|
410
|
+
while (taken.has(`${requested} (${suffix})`)) suffix += 1;
|
|
411
|
+
return `${requested} (${suffix})`;
|
|
412
|
+
}
|
|
413
|
+
//#endregion
|
|
414
|
+
//#region src/processors/node-processor.ts
|
|
415
|
+
const NAME_ACTION_TYPES = new Set([
|
|
416
|
+
"CREATE_DOCUMENT",
|
|
417
|
+
"UPGRADE_DOCUMENT",
|
|
418
|
+
"SET_NAME"
|
|
419
|
+
]);
|
|
420
|
+
const STRUCTURE_ACTION_TYPES = new Set([
|
|
421
|
+
"ADD_RELATIONSHIP",
|
|
422
|
+
"REMOVE_RELATIONSHIP",
|
|
423
|
+
"ADD_FOLDER",
|
|
424
|
+
"UPDATE_FOLDER",
|
|
425
|
+
"REMOVE_FOLDER"
|
|
426
|
+
]);
|
|
427
|
+
var NodeProcessor = class extends BaseReadModel {
|
|
428
|
+
driveDb;
|
|
429
|
+
baseDb;
|
|
430
|
+
schema;
|
|
431
|
+
constructor(baseDb, schema, operationIndex, writeCache, consistencyTracker) {
|
|
432
|
+
const scopedDb = baseDb.withSchema(schema);
|
|
433
|
+
super(scopedDb, operationIndex, writeCache, consistencyTracker, {
|
|
434
|
+
readModelId: "reactor-drive-node-processor",
|
|
435
|
+
rebuildStateOnInit: false
|
|
436
|
+
});
|
|
437
|
+
this.driveDb = scopedDb;
|
|
438
|
+
this.baseDb = baseDb;
|
|
439
|
+
this.schema = schema;
|
|
440
|
+
}
|
|
441
|
+
async init() {
|
|
442
|
+
const result = await runReactorDriveMigrations(this.baseDb, this.schema);
|
|
443
|
+
if (!result.success && result.error) throw new Error(`Reactor drive migrations failed: ${result.error.message}`);
|
|
444
|
+
await super.init();
|
|
445
|
+
}
|
|
446
|
+
async commitOperations(items) {
|
|
447
|
+
await this.driveDb.transaction().execute(async (trx) => {
|
|
448
|
+
for (const item of items) {
|
|
449
|
+
const actionType = item.operation.action.type;
|
|
450
|
+
if (NAME_ACTION_TYPES.has(actionType)) {
|
|
451
|
+
await this.applyNameOperation(trx, item);
|
|
452
|
+
continue;
|
|
453
|
+
}
|
|
454
|
+
if (STRUCTURE_ACTION_TYPES.has(actionType)) {
|
|
455
|
+
await this.applyStructureOperation(trx, item);
|
|
456
|
+
continue;
|
|
457
|
+
}
|
|
458
|
+
if (actionType === "DELETE_DOCUMENT") await this.applyDeleteDocument(trx, item);
|
|
459
|
+
}
|
|
460
|
+
});
|
|
461
|
+
}
|
|
462
|
+
async applyNameOperation(trx, item) {
|
|
463
|
+
const action = item.operation.action;
|
|
464
|
+
const docId = item.context.documentId;
|
|
465
|
+
let name;
|
|
466
|
+
if (action.type === "CREATE_DOCUMENT") name = action.input.name;
|
|
467
|
+
else if (action.type === "UPGRADE_DOCUMENT") name = (action.input.initialState ?? {}).header?.name;
|
|
468
|
+
else if (action.type === "SET_NAME") name = action.input.name;
|
|
469
|
+
if (name === void 0) return;
|
|
470
|
+
await this.upsertDocumentName(trx, docId, name);
|
|
471
|
+
const linkedRows = await trx.selectFrom("DriveNode").selectAll().where("id", "=", docId).where("kind", "=", "file").execute();
|
|
472
|
+
for (const row of linkedRows) {
|
|
473
|
+
const resolved = await this.resolveSiblingName(trx, row.driveId, row.parentFolder, name, docId);
|
|
474
|
+
await trx.updateTable("DriveNode").set({
|
|
475
|
+
name: resolved,
|
|
476
|
+
requestedName: name,
|
|
477
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
478
|
+
}).where("driveId", "=", row.driveId).where("id", "=", docId).execute();
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
async applyStructureOperation(trx, item) {
|
|
482
|
+
if (item.context.documentType !== "powerhouse/reactor-drive") return;
|
|
483
|
+
const action = item.operation.action;
|
|
484
|
+
const driveId = item.context.documentId;
|
|
485
|
+
if (action.type === "ADD_RELATIONSHIP") {
|
|
486
|
+
const input = action.input;
|
|
487
|
+
if (input.relationshipType !== "drive/child") return;
|
|
488
|
+
await this.handleAddFileRelationship(trx, driveId, input);
|
|
489
|
+
return;
|
|
490
|
+
}
|
|
491
|
+
if (action.type === "REMOVE_RELATIONSHIP") {
|
|
492
|
+
const input = action.input;
|
|
493
|
+
if (input.relationshipType !== "drive/child") return;
|
|
494
|
+
await trx.deleteFrom("DriveNode").where("driveId", "=", driveId).where("id", "=", input.targetId).execute();
|
|
495
|
+
return;
|
|
496
|
+
}
|
|
497
|
+
if (action.type === "ADD_FOLDER") {
|
|
498
|
+
await this.handleAddFolder(trx, driveId, action.input);
|
|
499
|
+
return;
|
|
500
|
+
}
|
|
501
|
+
if (action.type === "UPDATE_FOLDER") {
|
|
502
|
+
await this.handleUpdateFolder(trx, driveId, action.input);
|
|
503
|
+
return;
|
|
504
|
+
}
|
|
505
|
+
if (action.type === "REMOVE_FOLDER") {
|
|
506
|
+
const input = action.input;
|
|
507
|
+
await trx.deleteFrom("DriveNode").where("driveId", "=", driveId).where("id", "=", input.folderId).execute();
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async applyDeleteDocument(trx, item) {
|
|
511
|
+
const docId = item.operation.action.input.documentId || item.context.documentId;
|
|
512
|
+
await trx.deleteFrom("DriveNode").where("id", "=", docId).execute();
|
|
513
|
+
await trx.deleteFrom("DocumentName").where("docId", "=", docId).execute();
|
|
514
|
+
}
|
|
515
|
+
async handleAddFileRelationship(trx, driveId, input) {
|
|
516
|
+
const metadata = this.parseFileMetadata(input);
|
|
517
|
+
const parentFolder = metadata.parentFolderId ?? null;
|
|
518
|
+
const documentType = metadata.documentType;
|
|
519
|
+
const requestedName = await this.lookupDocumentName(trx, input.targetId) ?? "";
|
|
520
|
+
const resolved = await this.resolveSiblingName(trx, driveId, parentFolder, requestedName, input.targetId);
|
|
521
|
+
await trx.insertInto("DriveNode").values({
|
|
522
|
+
driveId,
|
|
523
|
+
id: input.targetId,
|
|
524
|
+
kind: "file",
|
|
525
|
+
name: resolved,
|
|
526
|
+
requestedName,
|
|
527
|
+
parentFolder,
|
|
528
|
+
documentType
|
|
529
|
+
}).onConflict((oc) => oc.columns(["driveId", "id"]).doUpdateSet({
|
|
530
|
+
parentFolder,
|
|
531
|
+
name: resolved,
|
|
532
|
+
requestedName,
|
|
533
|
+
kind: "file",
|
|
534
|
+
documentType,
|
|
535
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
536
|
+
})).execute();
|
|
537
|
+
}
|
|
538
|
+
async handleAddFolder(trx, driveId, input) {
|
|
539
|
+
const parentFolder = input.parentFolderId ?? null;
|
|
540
|
+
const resolved = await this.resolveSiblingName(trx, driveId, parentFolder, input.name, input.folderId);
|
|
541
|
+
await trx.insertInto("DriveNode").values({
|
|
542
|
+
driveId,
|
|
543
|
+
id: input.folderId,
|
|
544
|
+
kind: "folder",
|
|
545
|
+
name: resolved,
|
|
546
|
+
requestedName: input.name,
|
|
547
|
+
parentFolder,
|
|
548
|
+
documentType: null
|
|
549
|
+
}).onConflict((oc) => oc.columns(["driveId", "id"]).doUpdateSet({
|
|
550
|
+
parentFolder,
|
|
551
|
+
name: resolved,
|
|
552
|
+
requestedName: input.name,
|
|
553
|
+
kind: "folder",
|
|
554
|
+
documentType: null,
|
|
555
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
556
|
+
})).execute();
|
|
557
|
+
}
|
|
558
|
+
async handleUpdateFolder(trx, driveId, input) {
|
|
559
|
+
const row = await trx.selectFrom("DriveNode").selectAll().where("driveId", "=", driveId).where("id", "=", input.folderId).executeTakeFirst();
|
|
560
|
+
if (!row) return;
|
|
561
|
+
const nextParentFolder = input.parentFolderId === void 0 ? row.parentFolder : input.parentFolderId ?? null;
|
|
562
|
+
const nextRequestedName = typeof input.name === "string" ? input.name : row.requestedName;
|
|
563
|
+
const resolved = await this.resolveSiblingName(trx, driveId, nextParentFolder, nextRequestedName, input.folderId);
|
|
564
|
+
await trx.updateTable("DriveNode").set({
|
|
565
|
+
parentFolder: nextParentFolder,
|
|
566
|
+
name: resolved,
|
|
567
|
+
requestedName: nextRequestedName,
|
|
568
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
569
|
+
}).where("driveId", "=", driveId).where("id", "=", input.folderId).execute();
|
|
570
|
+
}
|
|
571
|
+
async resolveSiblingName(trx, driveId, parentFolder, requested, excludeId) {
|
|
572
|
+
let query = trx.selectFrom("DriveNode").select("name").where("driveId", "=", driveId).where("id", "!=", excludeId);
|
|
573
|
+
query = parentFolder === null ? query.where("parentFolder", "is", null) : query.where("parentFolder", "=", parentFolder);
|
|
574
|
+
return resolveCollision(requested, (await query.execute()).map((r) => r.name));
|
|
575
|
+
}
|
|
576
|
+
async lookupDocumentName(trx, docId) {
|
|
577
|
+
return (await trx.selectFrom("DocumentName").select("name").where("docId", "=", docId).executeTakeFirst())?.name;
|
|
578
|
+
}
|
|
579
|
+
parseFileMetadata(input) {
|
|
580
|
+
const metadata = input.metadata;
|
|
581
|
+
if (!metadata || typeof metadata !== "object" || metadata.kind !== "file" || typeof metadata.documentType !== "string") throw new Error(`ADD_RELATIONSHIP for target ${input.targetId}: missing or invalid drive/child file metadata (expected { kind: "file", parentFolderId, documentType })`);
|
|
582
|
+
const typed = metadata;
|
|
583
|
+
return {
|
|
584
|
+
kind: "file",
|
|
585
|
+
parentFolderId: typed.parentFolderId ?? null,
|
|
586
|
+
documentType: typed.documentType
|
|
587
|
+
};
|
|
588
|
+
}
|
|
589
|
+
async upsertDocumentName(trx, docId, name) {
|
|
590
|
+
await trx.insertInto("DocumentName").values({
|
|
591
|
+
docId,
|
|
592
|
+
name
|
|
593
|
+
}).onConflict((oc) => oc.column("docId").doUpdateSet({
|
|
594
|
+
name,
|
|
595
|
+
updatedAt: /* @__PURE__ */ new Date()
|
|
596
|
+
})).execute();
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
//#endregion
|
|
600
|
+
//#region src/read-model/drive-node-view.ts
|
|
601
|
+
const DEFAULT_LIMIT$1 = 100;
|
|
602
|
+
function parseListChildrenPaging(paging) {
|
|
603
|
+
if (paging === void 0) return {
|
|
604
|
+
limit: DEFAULT_LIMIT$1,
|
|
605
|
+
cursor: null
|
|
606
|
+
};
|
|
607
|
+
if (!Number.isInteger(paging.limit) || paging.limit < 1) throw new Error(`Invalid paging limit: ${String(paging.limit)} (must be an integer >= 1)`);
|
|
608
|
+
if (paging.cursor === "") return {
|
|
609
|
+
limit: paging.limit,
|
|
610
|
+
cursor: null
|
|
611
|
+
};
|
|
612
|
+
return {
|
|
613
|
+
limit: paging.limit,
|
|
614
|
+
cursor: decodeKeysetCursor(paging.cursor)
|
|
615
|
+
};
|
|
616
|
+
}
|
|
617
|
+
function encodeKeysetCursor(createdAt, id) {
|
|
618
|
+
return globalThis.btoa(JSON.stringify({
|
|
619
|
+
createdAt: createdAt.toISOString(),
|
|
620
|
+
id
|
|
621
|
+
}));
|
|
622
|
+
}
|
|
623
|
+
function decodeKeysetCursor(cursor) {
|
|
624
|
+
let decoded;
|
|
625
|
+
try {
|
|
626
|
+
decoded = globalThis.atob(cursor);
|
|
627
|
+
} catch {
|
|
628
|
+
throw new Error(`Invalid paging cursor: ${JSON.stringify(cursor)} (must be a keyset cursor returned by a prior page)`);
|
|
629
|
+
}
|
|
630
|
+
let parsed;
|
|
631
|
+
try {
|
|
632
|
+
parsed = JSON.parse(decoded);
|
|
633
|
+
} catch {
|
|
634
|
+
throw new Error(`Invalid paging cursor: ${JSON.stringify(cursor)} (must be a keyset cursor returned by a prior page)`);
|
|
635
|
+
}
|
|
636
|
+
if (parsed === null || typeof parsed !== "object" || typeof parsed.createdAt !== "string" || typeof parsed.id !== "string") throw new Error(`Invalid paging cursor: ${JSON.stringify(cursor)} (must be a keyset cursor returned by a prior page)`);
|
|
637
|
+
const createdAt = new Date(parsed.createdAt);
|
|
638
|
+
if (Number.isNaN(createdAt.getTime())) throw new Error(`Invalid paging cursor: ${JSON.stringify(cursor)} (createdAt is not a valid timestamp)`);
|
|
639
|
+
return {
|
|
640
|
+
createdAt,
|
|
641
|
+
id: parsed.id
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
var DriveNodeView = class {
|
|
645
|
+
constructor(db) {
|
|
646
|
+
this.db = db;
|
|
647
|
+
}
|
|
648
|
+
async getNode(driveId, nodeId) {
|
|
649
|
+
const row = await this.db.selectFrom("DriveNode").selectAll().where("driveId", "=", driveId).where("id", "=", nodeId).executeTakeFirst();
|
|
650
|
+
if (!row) return void 0;
|
|
651
|
+
return rowToNode(row);
|
|
652
|
+
}
|
|
653
|
+
async listChildren(driveId, parentFolder, paging) {
|
|
654
|
+
const { limit, cursor } = parseListChildrenPaging(paging);
|
|
655
|
+
let query = this.db.selectFrom("DriveNode").selectAll().where("driveId", "=", driveId);
|
|
656
|
+
if (parentFolder === null) query = query.where("parentFolder", "is", null);
|
|
657
|
+
else if (parentFolder !== void 0) query = query.where("parentFolder", "=", parentFolder);
|
|
658
|
+
if (cursor !== null) query = query.where((eb) => eb(eb.refTuple("createdAt", "id"), ">", eb.tuple(cursor.createdAt, cursor.id)));
|
|
659
|
+
const rows = await query.orderBy("createdAt", "asc").orderBy("id", "asc").limit(limit + 1).execute();
|
|
660
|
+
const hasMore = rows.length > limit;
|
|
661
|
+
const sliced = hasMore ? rows.slice(0, limit) : rows;
|
|
662
|
+
const last = sliced[sliced.length - 1];
|
|
663
|
+
return {
|
|
664
|
+
results: sliced.map(rowToNode),
|
|
665
|
+
options: {
|
|
666
|
+
cursor: paging?.cursor ?? "",
|
|
667
|
+
limit
|
|
668
|
+
},
|
|
669
|
+
nextCursor: hasMore ? encodeKeysetCursor(last.createdAt, last.id) : void 0
|
|
670
|
+
};
|
|
671
|
+
}
|
|
672
|
+
async listAll(driveId) {
|
|
673
|
+
return (await this.db.selectFrom("DriveNode").selectAll().where("driveId", "=", driveId).orderBy("createdAt", "asc").orderBy("id", "asc").execute()).map(rowToNode);
|
|
674
|
+
}
|
|
675
|
+
async getDescendants(driveId, root) {
|
|
676
|
+
return (await this.db.withRecursive("descendants", (qb) => qb.selectFrom("DriveNode").select([
|
|
677
|
+
"driveId",
|
|
678
|
+
"id",
|
|
679
|
+
"kind",
|
|
680
|
+
"name",
|
|
681
|
+
"requestedName",
|
|
682
|
+
"parentFolder",
|
|
683
|
+
"documentType",
|
|
684
|
+
"createdAt"
|
|
685
|
+
]).where("driveId", "=", driveId).where("id", "=", root).unionAll(qb.selectFrom("DriveNode").innerJoin("descendants", "DriveNode.parentFolder", "descendants.id").where("DriveNode.driveId", "=", driveId).select([
|
|
686
|
+
"DriveNode.driveId",
|
|
687
|
+
"DriveNode.id",
|
|
688
|
+
"DriveNode.kind",
|
|
689
|
+
"DriveNode.name",
|
|
690
|
+
"DriveNode.requestedName",
|
|
691
|
+
"DriveNode.parentFolder",
|
|
692
|
+
"DriveNode.documentType",
|
|
693
|
+
"DriveNode.createdAt"
|
|
694
|
+
]))).selectFrom("descendants").select([
|
|
695
|
+
"driveId",
|
|
696
|
+
"id",
|
|
697
|
+
"kind",
|
|
698
|
+
"name",
|
|
699
|
+
"requestedName",
|
|
700
|
+
"parentFolder",
|
|
701
|
+
"documentType"
|
|
702
|
+
]).orderBy("createdAt", "asc").orderBy("id", "asc").execute()).map(rowToNode);
|
|
703
|
+
}
|
|
704
|
+
};
|
|
705
|
+
function rowToNode(row) {
|
|
706
|
+
if (row.kind === "file") {
|
|
707
|
+
if (row.documentType === null) throw new Error(`DriveNode ${row.driveId}/${row.id}: file row has null documentType, which violates the schema CHECK constraint`);
|
|
708
|
+
return {
|
|
709
|
+
kind: "file",
|
|
710
|
+
id: row.id,
|
|
711
|
+
driveId: row.driveId,
|
|
712
|
+
parentFolder: row.parentFolder,
|
|
713
|
+
name: row.name,
|
|
714
|
+
documentType: row.documentType
|
|
715
|
+
};
|
|
716
|
+
}
|
|
717
|
+
return {
|
|
718
|
+
kind: "folder",
|
|
719
|
+
id: row.id,
|
|
720
|
+
driveId: row.driveId,
|
|
721
|
+
parentFolder: row.parentFolder,
|
|
722
|
+
name: row.name
|
|
723
|
+
};
|
|
724
|
+
}
|
|
725
|
+
//#endregion
|
|
726
|
+
//#region src/client/reactor-drive-client.ts
|
|
727
|
+
/**
|
|
728
|
+
* Implementation of {@link IDriveClient} backed by the reactor's relationship
|
|
729
|
+
* primitives and the drive-scoped folder actions. Folder structure lives in
|
|
730
|
+
* the operation log (ADD_FOLDER/UPDATE_FOLDER/REMOVE_FOLDER + ADD_RELATIONSHIP
|
|
731
|
+
* for files) and is materialised by `NodeProcessor` into the `DriveNode`
|
|
732
|
+
* table consumed via {@link IDriveReadModel}.
|
|
733
|
+
*/
|
|
734
|
+
var ReactorDriveClient = class {
|
|
735
|
+
reactor;
|
|
736
|
+
readModel;
|
|
737
|
+
constructor(args) {
|
|
738
|
+
this.reactor = args.reactor;
|
|
739
|
+
this.readModel = args.readModel;
|
|
740
|
+
}
|
|
741
|
+
async create(input, signal) {
|
|
742
|
+
const driveDoc = reactorDriveCreateDocument({ global: {
|
|
743
|
+
name: input.global.name,
|
|
744
|
+
icon: input.global.icon ?? null
|
|
745
|
+
} });
|
|
746
|
+
if (input.local) {
|
|
747
|
+
if (typeof input.local.sharingType === "string") driveDoc.state.local.sharingType = input.local.sharingType;
|
|
748
|
+
if (typeof input.local.availableOffline === "boolean") driveDoc.state.local.availableOffline = input.local.availableOffline;
|
|
749
|
+
}
|
|
750
|
+
if (input.preferredEditor) driveDoc.header.meta = {
|
|
751
|
+
...driveDoc.header.meta,
|
|
752
|
+
preferredEditor: input.preferredEditor
|
|
753
|
+
};
|
|
754
|
+
const created = await this.reactor.create(driveDoc, void 0, signal);
|
|
755
|
+
return this.toLegacyDriveDocument(created, created.header.id);
|
|
756
|
+
}
|
|
757
|
+
async addFile(driveIdentifier, document, parentFolder, signal) {
|
|
758
|
+
const documentId = document.header.id;
|
|
759
|
+
const createInput = {
|
|
760
|
+
model: document.header.documentType,
|
|
761
|
+
version: 0,
|
|
762
|
+
documentId,
|
|
763
|
+
signing: {
|
|
764
|
+
signature: documentId,
|
|
765
|
+
publicKey: document.header.sig.publicKey,
|
|
766
|
+
nonce: document.header.sig.nonce,
|
|
767
|
+
createdAtUtcIso: document.header.createdAtUtcIso,
|
|
768
|
+
documentType: document.header.documentType
|
|
769
|
+
},
|
|
770
|
+
slug: document.header.slug,
|
|
771
|
+
name: document.header.name,
|
|
772
|
+
branch: document.header.branch,
|
|
773
|
+
meta: document.header.meta,
|
|
774
|
+
protocolVersions: document.header.protocolVersions ?? { "base-reducer": 2 }
|
|
775
|
+
};
|
|
776
|
+
const metadata = {
|
|
777
|
+
kind: "file",
|
|
778
|
+
parentFolderId: parentFolder ?? null,
|
|
779
|
+
documentType: document.header.documentType
|
|
780
|
+
};
|
|
781
|
+
const request = { jobs: [{
|
|
782
|
+
key: "create",
|
|
783
|
+
documentId,
|
|
784
|
+
scope: "document",
|
|
785
|
+
branch: "main",
|
|
786
|
+
actions: [createDocumentAction(createInput), upgradeDocumentAction({
|
|
787
|
+
documentId,
|
|
788
|
+
model: document.header.documentType,
|
|
789
|
+
fromVersion: 0,
|
|
790
|
+
toVersion: document.state.document.version,
|
|
791
|
+
initialState: document.state
|
|
792
|
+
})],
|
|
793
|
+
dependsOn: []
|
|
794
|
+
}, {
|
|
795
|
+
key: "link",
|
|
796
|
+
documentId: driveIdentifier,
|
|
797
|
+
scope: "document",
|
|
798
|
+
branch: "main",
|
|
799
|
+
actions: [addRelationshipAction(driveIdentifier, documentId, DRIVE_CHILD_RELATIONSHIP_TYPE, metadata)],
|
|
800
|
+
dependsOn: ["create"]
|
|
801
|
+
}] };
|
|
802
|
+
await this.reactor.executeBatch(request, signal);
|
|
803
|
+
return this.reactor.get(documentId, void 0, signal);
|
|
804
|
+
}
|
|
805
|
+
async addFolder(driveIdentifier, name, parentFolder, signal) {
|
|
806
|
+
const folderId = generateId();
|
|
807
|
+
await this.reactor.execute(driveIdentifier, "main", [addFolderAction({
|
|
808
|
+
folderId,
|
|
809
|
+
parentFolderId: parentFolder ?? null,
|
|
810
|
+
name
|
|
811
|
+
})], signal);
|
|
812
|
+
return {
|
|
813
|
+
id: folderId,
|
|
814
|
+
kind: "folder",
|
|
815
|
+
name,
|
|
816
|
+
parentFolder: parentFolder ?? null
|
|
817
|
+
};
|
|
818
|
+
}
|
|
819
|
+
async removeNode(driveIdentifier, nodeId, signal) {
|
|
820
|
+
const node = await this.readModel.getNode(driveIdentifier, nodeId, signal);
|
|
821
|
+
if (!node) throw new Error(`Node ${nodeId} not found in drive ${driveIdentifier}`);
|
|
822
|
+
if (node.kind === "folder") {
|
|
823
|
+
const subtree = await this.readModel.getDescendants(driveIdentifier, nodeId, signal);
|
|
824
|
+
const fileDescendants = subtree.filter((n) => n.kind === "file");
|
|
825
|
+
const subFolders = subtree.filter((n) => n.kind === "folder").filter((f) => f.id !== nodeId);
|
|
826
|
+
const deepestFirstFolders = this.orderDeepestFirst(subFolders, nodeId);
|
|
827
|
+
const batch = [
|
|
828
|
+
...fileDescendants.map((f) => removeRelationshipAction(driveIdentifier, f.id, DRIVE_CHILD_RELATIONSHIP_TYPE)),
|
|
829
|
+
...deepestFirstFolders.map((f) => removeFolderAction({ folderId: f.id })),
|
|
830
|
+
removeFolderAction({ folderId: nodeId })
|
|
831
|
+
];
|
|
832
|
+
await this.reactor.execute(driveIdentifier, "main", batch, signal);
|
|
833
|
+
for (const file of fileDescendants) await this.reactor.deleteDocument(file.id, "cascade", signal);
|
|
834
|
+
return;
|
|
835
|
+
}
|
|
836
|
+
await this.reactor.execute(driveIdentifier, "main", [removeRelationshipAction(driveIdentifier, nodeId, DRIVE_CHILD_RELATIONSHIP_TYPE)], signal);
|
|
837
|
+
await this.reactor.deleteDocument(nodeId, void 0, signal);
|
|
838
|
+
}
|
|
839
|
+
async renameNode(driveIdentifier, nodeId, name, signal) {
|
|
840
|
+
const node = await this.readModel.getNode(driveIdentifier, nodeId, signal);
|
|
841
|
+
if (!node) throw new Error(`Node ${nodeId} not found in drive ${driveIdentifier}`);
|
|
842
|
+
if (node.kind === "file") await this.reactor.rename(nodeId, name, "main", signal);
|
|
843
|
+
else await this.reactor.execute(driveIdentifier, "main", [updateFolderAction({
|
|
844
|
+
folderId: nodeId,
|
|
845
|
+
name
|
|
846
|
+
})], signal);
|
|
847
|
+
const updated = await this.readModel.getNode(driveIdentifier, nodeId, signal);
|
|
848
|
+
if (!updated) throw new Error("Node missing from drive after rename");
|
|
849
|
+
return this.toLegacyNode(updated);
|
|
850
|
+
}
|
|
851
|
+
async setPreferredEditorOnNode(nodeId, preferredEditor, signal) {
|
|
852
|
+
return this.reactor.setPreferredEditor(nodeId, preferredEditor, "main", signal);
|
|
853
|
+
}
|
|
854
|
+
async moveNode(driveIdentifier, srcNodeId, targetParentFolderId, signal) {
|
|
855
|
+
const node = await this.readModel.getNode(driveIdentifier, srcNodeId, signal);
|
|
856
|
+
if (!node) throw new Error(`Node ${srcNodeId} not found in drive ${driveIdentifier}`);
|
|
857
|
+
if (node.kind === "folder") await this.reactor.execute(driveIdentifier, "main", [updateFolderAction({
|
|
858
|
+
folderId: srcNodeId,
|
|
859
|
+
parentFolderId: targetParentFolderId ?? null
|
|
860
|
+
})], signal);
|
|
861
|
+
else {
|
|
862
|
+
const metadata = {
|
|
863
|
+
kind: "file",
|
|
864
|
+
parentFolderId: targetParentFolderId ?? null,
|
|
865
|
+
documentType: node.documentType
|
|
866
|
+
};
|
|
867
|
+
await this.reactor.execute(driveIdentifier, "main", [removeRelationshipAction(driveIdentifier, srcNodeId, DRIVE_CHILD_RELATIONSHIP_TYPE), addRelationshipAction(driveIdentifier, srcNodeId, DRIVE_CHILD_RELATIONSHIP_TYPE, metadata)], signal);
|
|
868
|
+
}
|
|
869
|
+
const drive = await this.reactor.get(driveIdentifier, void 0, signal);
|
|
870
|
+
return this.toLegacyDriveDocument(drive, driveIdentifier);
|
|
871
|
+
}
|
|
872
|
+
async copyNode(driveIdentifier, srcNodeId, targetParentFolderId, signal) {
|
|
873
|
+
if (!await this.readModel.getNode(driveIdentifier, srcNodeId, signal)) throw new Error(`Node ${srcNodeId} not found in drive ${driveIdentifier}`);
|
|
874
|
+
const subtree = await this.readModel.getDescendants(driveIdentifier, srcNodeId, signal);
|
|
875
|
+
if (targetParentFolderId !== void 0 && subtree.some((n) => n.id === targetParentFolderId)) throw new Error(`Cannot copy node ${srcNodeId} into itself or one of its descendants (target: ${targetParentFolderId})`);
|
|
876
|
+
const idMap = /* @__PURE__ */ new Map();
|
|
877
|
+
for (const node of subtree) idMap.set(node.id, generateId());
|
|
878
|
+
const jobs = [];
|
|
879
|
+
const driveActions = [];
|
|
880
|
+
const fileCreateKeys = [];
|
|
881
|
+
for (const node of subtree) {
|
|
882
|
+
const newId = idMap.get(node.id);
|
|
883
|
+
let newParent;
|
|
884
|
+
if (node.id === srcNodeId) newParent = targetParentFolderId ?? null;
|
|
885
|
+
else {
|
|
886
|
+
if (node.parentFolder == null) throw new Error(`copyNode: descendant ${node.id} has no parentFolder`);
|
|
887
|
+
const mapped = idMap.get(node.parentFolder);
|
|
888
|
+
if (mapped === void 0) throw new Error(`copyNode: descendant ${node.id} parent ${node.parentFolder} missing from idMap`);
|
|
889
|
+
newParent = mapped;
|
|
890
|
+
}
|
|
891
|
+
if (node.kind === "folder") {
|
|
892
|
+
driveActions.push(addFolderAction({
|
|
893
|
+
folderId: newId,
|
|
894
|
+
parentFolderId: newParent,
|
|
895
|
+
name: node.name
|
|
896
|
+
}));
|
|
897
|
+
continue;
|
|
898
|
+
}
|
|
899
|
+
const srcDoc = await this.reactor.get(node.id, void 0, signal);
|
|
900
|
+
const documentModelModule = await this.reactor.getDocumentModelModule(srcDoc.header.documentType);
|
|
901
|
+
const duplicated = replayDocument(srcDoc.initialState, srcDoc.operations, documentModelModule.reducer, createPresignedHeader(newId, srcDoc.header.documentType));
|
|
902
|
+
duplicated.header.name = node.name;
|
|
903
|
+
const createInput = {
|
|
904
|
+
model: duplicated.header.documentType,
|
|
905
|
+
version: 0,
|
|
906
|
+
documentId: newId,
|
|
907
|
+
signing: {
|
|
908
|
+
signature: newId,
|
|
909
|
+
publicKey: duplicated.header.sig.publicKey,
|
|
910
|
+
nonce: duplicated.header.sig.nonce,
|
|
911
|
+
createdAtUtcIso: duplicated.header.createdAtUtcIso,
|
|
912
|
+
documentType: duplicated.header.documentType
|
|
913
|
+
},
|
|
914
|
+
slug: duplicated.header.slug,
|
|
915
|
+
name: duplicated.header.name,
|
|
916
|
+
branch: duplicated.header.branch,
|
|
917
|
+
meta: duplicated.header.meta,
|
|
918
|
+
protocolVersions: duplicated.header.protocolVersions ?? { "base-reducer": 2 }
|
|
919
|
+
};
|
|
920
|
+
const createKey = `create:${newId}`;
|
|
921
|
+
jobs.push({
|
|
922
|
+
key: createKey,
|
|
923
|
+
documentId: newId,
|
|
924
|
+
scope: "document",
|
|
925
|
+
branch: "main",
|
|
926
|
+
actions: [createDocumentAction(createInput), upgradeDocumentAction({
|
|
927
|
+
documentId: newId,
|
|
928
|
+
model: duplicated.header.documentType,
|
|
929
|
+
fromVersion: 0,
|
|
930
|
+
toVersion: duplicated.state.document.version,
|
|
931
|
+
initialState: duplicated.state
|
|
932
|
+
})],
|
|
933
|
+
dependsOn: []
|
|
934
|
+
});
|
|
935
|
+
fileCreateKeys.push(createKey);
|
|
936
|
+
const metadata = {
|
|
937
|
+
kind: "file",
|
|
938
|
+
parentFolderId: newParent,
|
|
939
|
+
documentType: duplicated.header.documentType
|
|
940
|
+
};
|
|
941
|
+
driveActions.push(addRelationshipAction(driveIdentifier, newId, DRIVE_CHILD_RELATIONSHIP_TYPE, metadata));
|
|
942
|
+
}
|
|
943
|
+
if (driveActions.length > 0) jobs.push({
|
|
944
|
+
key: "drive",
|
|
945
|
+
documentId: driveIdentifier,
|
|
946
|
+
scope: "document",
|
|
947
|
+
branch: "main",
|
|
948
|
+
actions: driveActions,
|
|
949
|
+
dependsOn: fileCreateKeys
|
|
950
|
+
});
|
|
951
|
+
if (jobs.length > 0) {
|
|
952
|
+
const request = { jobs };
|
|
953
|
+
await this.reactor.executeBatch(request, signal);
|
|
954
|
+
}
|
|
955
|
+
const drive = await this.reactor.get(driveIdentifier, void 0, signal);
|
|
956
|
+
return this.toLegacyDriveDocument(drive, driveIdentifier);
|
|
957
|
+
}
|
|
958
|
+
async getNode(driveIdentifier, nodeId, signal) {
|
|
959
|
+
const node = await this.readModel.getNode(driveIdentifier, nodeId, signal);
|
|
960
|
+
if (!node) throw new Error(`Node ${nodeId} not found in drive ${driveIdentifier}`);
|
|
961
|
+
return this.toLegacyNode(node);
|
|
962
|
+
}
|
|
963
|
+
async listNodes(driveIdentifier, parentFolder, paging, signal) {
|
|
964
|
+
const page = await this.readModel.listChildren(driveIdentifier, parentFolder, paging, signal);
|
|
965
|
+
return {
|
|
966
|
+
results: page.results.map((node) => this.toLegacyNode(node)),
|
|
967
|
+
options: page.options,
|
|
968
|
+
...page.nextCursor !== void 0 ? { nextCursor: page.nextCursor } : {},
|
|
969
|
+
...page.totalCount !== void 0 ? { totalCount: page.totalCount } : {}
|
|
970
|
+
};
|
|
971
|
+
}
|
|
972
|
+
toLegacyNode(node) {
|
|
973
|
+
if (node.kind === "file") {
|
|
974
|
+
const file = node;
|
|
975
|
+
return {
|
|
976
|
+
id: file.id,
|
|
977
|
+
kind: "file",
|
|
978
|
+
name: file.name,
|
|
979
|
+
parentFolder: file.parentFolder,
|
|
980
|
+
documentType: file.documentType
|
|
981
|
+
};
|
|
982
|
+
}
|
|
983
|
+
const folder = node;
|
|
984
|
+
return {
|
|
985
|
+
id: folder.id,
|
|
986
|
+
kind: "folder",
|
|
987
|
+
name: folder.name,
|
|
988
|
+
parentFolder: folder.parentFolder
|
|
989
|
+
};
|
|
990
|
+
}
|
|
991
|
+
async toLegacyDriveDocument(doc, driveId) {
|
|
992
|
+
if (doc.header.documentType !== "powerhouse/reactor-drive") throw new Error(`Document ${doc.header.id} is not a reactor-drive document`);
|
|
993
|
+
const allNodes = await this.readModel.listAll(driveId);
|
|
994
|
+
const legacy = structuredClone(doc);
|
|
995
|
+
const existingGlobal = legacy.state.global;
|
|
996
|
+
legacy.state.global = {
|
|
997
|
+
name: existingGlobal.name ?? "",
|
|
998
|
+
icon: existingGlobal.icon ?? null,
|
|
999
|
+
nodes: allNodes.map((n) => this.toLegacyNode(n))
|
|
1000
|
+
};
|
|
1001
|
+
return legacy;
|
|
1002
|
+
}
|
|
1003
|
+
orderDeepestFirst(folders, rootId) {
|
|
1004
|
+
const depthById = /* @__PURE__ */ new Map();
|
|
1005
|
+
const compute = (id) => {
|
|
1006
|
+
if (id === rootId) return 0;
|
|
1007
|
+
const cached = depthById.get(id);
|
|
1008
|
+
if (cached !== void 0) return cached;
|
|
1009
|
+
const folder = folders.find((f) => f.id === id);
|
|
1010
|
+
if (!folder) return 0;
|
|
1011
|
+
const depth = compute(folder.parentFolder ?? rootId) + 1;
|
|
1012
|
+
depthById.set(id, depth);
|
|
1013
|
+
return depth;
|
|
1014
|
+
};
|
|
1015
|
+
return folders.slice().sort((a, b) => compute(b.id) - compute(a.id));
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
//#endregion
|
|
1019
|
+
//#region src/subgraph/schema.ts
|
|
1020
|
+
const typeDefs = gql`
|
|
1021
|
+
enum ReactorDriveNodeKind {
|
|
1022
|
+
FILE
|
|
1023
|
+
FOLDER
|
|
1024
|
+
}
|
|
1025
|
+
|
|
1026
|
+
input ReactorDrivePagingInput {
|
|
1027
|
+
cursor: String
|
|
1028
|
+
limit: Int
|
|
1029
|
+
}
|
|
1030
|
+
|
|
1031
|
+
type ReactorDriveFileNode {
|
|
1032
|
+
id: ID!
|
|
1033
|
+
driveId: ID!
|
|
1034
|
+
name: String!
|
|
1035
|
+
parentFolder: ID
|
|
1036
|
+
documentType: String!
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
type ReactorDriveFolderNode {
|
|
1040
|
+
id: ID!
|
|
1041
|
+
driveId: ID!
|
|
1042
|
+
name: String!
|
|
1043
|
+
parentFolder: ID
|
|
1044
|
+
children(
|
|
1045
|
+
paging: ReactorDrivePagingInput
|
|
1046
|
+
kind: ReactorDriveNodeKind
|
|
1047
|
+
): ReactorDriveNodePage!
|
|
1048
|
+
}
|
|
1049
|
+
|
|
1050
|
+
union ReactorDriveNode = ReactorDriveFileNode | ReactorDriveFolderNode
|
|
1051
|
+
|
|
1052
|
+
type ReactorDriveNodePage {
|
|
1053
|
+
results: [ReactorDriveNode!]!
|
|
1054
|
+
nextCursor: String
|
|
1055
|
+
hasMore: Boolean!
|
|
1056
|
+
totalCount: Int
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
type ReactorDrive {
|
|
1060
|
+
id: ID!
|
|
1061
|
+
name: String!
|
|
1062
|
+
icon: String
|
|
1063
|
+
sharingType: String!
|
|
1064
|
+
availableOffline: Boolean!
|
|
1065
|
+
rootNodes(
|
|
1066
|
+
paging: ReactorDrivePagingInput
|
|
1067
|
+
kind: ReactorDriveNodeKind
|
|
1068
|
+
): ReactorDriveNodePage!
|
|
1069
|
+
}
|
|
1070
|
+
|
|
1071
|
+
type Query {
|
|
1072
|
+
reactorDrive(id: ID!): ReactorDrive
|
|
1073
|
+
reactorDriveNode(driveId: ID!, id: ID!): ReactorDriveNode
|
|
1074
|
+
reactorDriveDescendants(driveId: ID!, root: ID!): [ReactorDriveNode!]!
|
|
1075
|
+
}
|
|
1076
|
+
`;
|
|
1077
|
+
//#endregion
|
|
1078
|
+
//#region src/subgraph/resolvers.ts
|
|
1079
|
+
const DEFAULT_LIMIT = 100;
|
|
1080
|
+
function toPaging(input) {
|
|
1081
|
+
if (!input) return void 0;
|
|
1082
|
+
return {
|
|
1083
|
+
cursor: input.cursor ?? "",
|
|
1084
|
+
limit: input.limit ?? DEFAULT_LIMIT
|
|
1085
|
+
};
|
|
1086
|
+
}
|
|
1087
|
+
function filterByKind(page, kind) {
|
|
1088
|
+
if (!kind) return page;
|
|
1089
|
+
const wanted = kind === "FILE" ? "file" : "folder";
|
|
1090
|
+
return {
|
|
1091
|
+
...page,
|
|
1092
|
+
results: page.results.filter((node) => node.kind === wanted)
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
function shapePage(page) {
|
|
1096
|
+
return {
|
|
1097
|
+
results: page.results,
|
|
1098
|
+
nextCursor: page.nextCursor,
|
|
1099
|
+
hasMore: page.nextCursor !== void 0,
|
|
1100
|
+
totalCount: page.totalCount
|
|
1101
|
+
};
|
|
1102
|
+
}
|
|
1103
|
+
/**
|
|
1104
|
+
* Builds GraphQL resolvers backed by the drive read model. The resolvers are
|
|
1105
|
+
* pure — every external dependency is read off the GraphQL context. Wiring
|
|
1106
|
+
* the resolvers into a subgraph (e.g. `reactor-api`'s `ISubgraph`) is the
|
|
1107
|
+
* caller's responsibility.
|
|
1108
|
+
*/
|
|
1109
|
+
function createReactorDriveResolvers() {
|
|
1110
|
+
return {
|
|
1111
|
+
Query: {
|
|
1112
|
+
async reactorDrive(_root, args, ctx) {
|
|
1113
|
+
const document = await ctx.reactorClient.get(args.id);
|
|
1114
|
+
if (document.header.documentType !== "powerhouse/reactor-drive") return null;
|
|
1115
|
+
return {
|
|
1116
|
+
id: document.header.id,
|
|
1117
|
+
name: document.state.global.name,
|
|
1118
|
+
icon: document.state.global.icon,
|
|
1119
|
+
sharingType: document.state.local.sharingType,
|
|
1120
|
+
availableOffline: document.state.local.availableOffline
|
|
1121
|
+
};
|
|
1122
|
+
},
|
|
1123
|
+
async reactorDriveNode(_root, args, ctx) {
|
|
1124
|
+
return ctx.readModel.getNode(args.driveId, args.id);
|
|
1125
|
+
},
|
|
1126
|
+
async reactorDriveDescendants(_root, args, ctx) {
|
|
1127
|
+
return ctx.readModel.getDescendants(args.driveId, args.root);
|
|
1128
|
+
}
|
|
1129
|
+
},
|
|
1130
|
+
ReactorDrive: { async rootNodes(parent, args, ctx) {
|
|
1131
|
+
return shapePage(filterByKind(await ctx.readModel.listChildren(parent.id, null, toPaging(args.paging)), args.kind));
|
|
1132
|
+
} },
|
|
1133
|
+
ReactorDriveFolderNode: { async children(parent, args, ctx) {
|
|
1134
|
+
return shapePage(filterByKind(await ctx.readModel.listChildren(parent.driveId, parent.id, toPaging(args.paging)), args.kind));
|
|
1135
|
+
} },
|
|
1136
|
+
ReactorDriveNode: { __resolveType(node) {
|
|
1137
|
+
return node.kind === "file" ? "ReactorDriveFileNode" : "ReactorDriveFolderNode";
|
|
1138
|
+
} }
|
|
1139
|
+
};
|
|
1140
|
+
}
|
|
1141
|
+
//#endregion
|
|
1142
|
+
//#region src/migration/migrate-legacy-state.ts
|
|
1143
|
+
/**
|
|
1144
|
+
* Translates a legacy `document-drive` `state.global.nodes` array into the
|
|
1145
|
+
* action vocabulary used by the new reactor-drive module.
|
|
1146
|
+
*
|
|
1147
|
+
* Folder nodes are emitted as `ADD_FOLDER` actions targeting the drive
|
|
1148
|
+
* document. File nodes are emitted as `ADD_RELATIONSHIP` actions on the drive
|
|
1149
|
+
* with `drive/child` metadata carrying the parent folder id. File nodes
|
|
1150
|
+
* assume the underlying PHDocument still exists under the same id — the
|
|
1151
|
+
* migration only re-links it into the new drive, it does not recreate
|
|
1152
|
+
* documents.
|
|
1153
|
+
*
|
|
1154
|
+
* Re-running the migration is safe: existing `DriveNode` rows are skipped
|
|
1155
|
+
* so already-migrated nodes are left untouched.
|
|
1156
|
+
*/
|
|
1157
|
+
async function migrateLegacyDriveState(args) {
|
|
1158
|
+
const { reactor, readModel, driveId, nodes, signal } = args;
|
|
1159
|
+
const branch = args.branch ?? "main";
|
|
1160
|
+
const ordered = orderLegacyNodes(nodes);
|
|
1161
|
+
const actions = [];
|
|
1162
|
+
let skippedExisting = 0;
|
|
1163
|
+
for (const node of ordered) {
|
|
1164
|
+
if (await readModel.getNode(driveId, node.id, signal)) {
|
|
1165
|
+
skippedExisting += 1;
|
|
1166
|
+
continue;
|
|
1167
|
+
}
|
|
1168
|
+
actions.push(toAction(driveId, node));
|
|
1169
|
+
}
|
|
1170
|
+
if (actions.length === 0) return {
|
|
1171
|
+
emittedActions: 0,
|
|
1172
|
+
skippedExisting
|
|
1173
|
+
};
|
|
1174
|
+
await reactor.execute(driveId, branch, actions, signal);
|
|
1175
|
+
return {
|
|
1176
|
+
emittedActions: actions.length,
|
|
1177
|
+
skippedExisting
|
|
1178
|
+
};
|
|
1179
|
+
}
|
|
1180
|
+
function toAction(driveId, node) {
|
|
1181
|
+
if (node.kind === "folder") return addFolderAction({
|
|
1182
|
+
folderId: node.id,
|
|
1183
|
+
parentFolderId: node.parentFolder ?? null,
|
|
1184
|
+
name: node.name
|
|
1185
|
+
});
|
|
1186
|
+
const fileNode = node;
|
|
1187
|
+
const metadata = {
|
|
1188
|
+
kind: "file",
|
|
1189
|
+
parentFolderId: fileNode.parentFolder ?? null,
|
|
1190
|
+
documentType: fileNode.documentType
|
|
1191
|
+
};
|
|
1192
|
+
return addRelationshipAction(driveId, fileNode.id, DRIVE_CHILD_RELATIONSHIP_TYPE, metadata);
|
|
1193
|
+
}
|
|
1194
|
+
/**
|
|
1195
|
+
* Sorts legacy nodes so that any folder appears before its children. The
|
|
1196
|
+
* legacy state stores nodes in insertion order, which usually already
|
|
1197
|
+
* satisfies that property, but defensive sorting keeps replay deterministic
|
|
1198
|
+
* if the input was constructed out of order.
|
|
1199
|
+
*/
|
|
1200
|
+
function orderLegacyNodes(nodes) {
|
|
1201
|
+
const byId = /* @__PURE__ */ new Map();
|
|
1202
|
+
for (const node of nodes) byId.set(node.id, node);
|
|
1203
|
+
const visited = /* @__PURE__ */ new Set();
|
|
1204
|
+
const ordered = [];
|
|
1205
|
+
for (const start of nodes) {
|
|
1206
|
+
if (visited.has(start.id)) continue;
|
|
1207
|
+
const chain = [];
|
|
1208
|
+
const seenInWalk = /* @__PURE__ */ new Set();
|
|
1209
|
+
let current = start;
|
|
1210
|
+
while (current && !visited.has(current.id) && !seenInWalk.has(current.id)) {
|
|
1211
|
+
seenInWalk.add(current.id);
|
|
1212
|
+
chain.push(current);
|
|
1213
|
+
const parentId = current.parentFolder;
|
|
1214
|
+
current = parentId ? byId.get(parentId) : void 0;
|
|
1215
|
+
}
|
|
1216
|
+
while (chain.length > 0) {
|
|
1217
|
+
const node = chain.pop();
|
|
1218
|
+
if (visited.has(node.id)) continue;
|
|
1219
|
+
visited.add(node.id);
|
|
1220
|
+
ordered.push(node);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return ordered;
|
|
1224
|
+
}
|
|
1225
|
+
//#endregion
|
|
1226
|
+
export { DRIVE_CHILD_RELATIONSHIP_TYPE, DriveNodeView, NodeProcessor, REACTOR_DRIVE_DOCUMENT_TYPE, REACTOR_DRIVE_FILE_EXTENSION, ReactorDriveClient, addFolderAction, createReactorDriveResolvers, getReactorDriveMigrationStatus, migrateLegacyDriveState, reactorDriveActions, reactorDriveCreateDocument, reactorDriveCreateState, reactorDriveDocumentModelModule, reactorDriveDocumentReducer, reactorDriveStateReducer, typeDefs as reactorDriveSubgraphTypeDefs, removeFolderAction, resolveCollision, runReactorDriveMigrations, setAvailableOfflineAction, setDriveIconAction, setDriveNameAction, setSharingTypeAction, updateFolderAction };
|
|
1227
|
+
|
|
1228
|
+
//# sourceMappingURL=index.js.map
|