@powerhousedao/reactor-drive 6.0.0-dev.253 → 6.0.0-dev.254

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.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