@mulmoclaude/core 3.5.0 → 3.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/assets/helps/collection-skills.md +49 -1
- package/assets/helps/error-recovery.md +53 -0
- package/dist/calendarGrid-CQ8MVSRb.js.map +1 -1
- package/dist/calendarGrid-DGILaVxI.cjs.map +1 -1
- package/dist/collection/core/schema.d.ts +8 -1
- package/dist/collection/core/schemaZ.d.ts +36 -26
- package/dist/collection/firestore.cjs +51 -0
- package/dist/collection/firestore.cjs.map +1 -0
- package/dist/collection/firestore.d.ts +1 -0
- package/dist/collection/firestore.js +50 -0
- package/dist/collection/firestore.js.map +1 -0
- package/dist/collection/registry/server/index.cjs +2 -2
- package/dist/collection/registry/server/index.js +2 -2
- package/dist/collection/server/appManifest.d.ts +53 -0
- package/dist/collection/server/delete.d.ts +10 -0
- package/dist/collection/server/discoveredCollection.d.ts +10 -0
- package/dist/collection/server/discovery.d.ts +1 -0
- package/dist/collection/server/firestoreDocs.d.ts +39 -0
- package/dist/collection/server/firestoreStore.d.ts +14 -0
- package/dist/collection/server/host.d.ts +41 -0
- package/dist/collection/server/index.cjs +9 -2
- package/dist/collection/server/index.d.ts +4 -1
- package/dist/collection/server/index.js +3 -3
- package/dist/collection-watchers/index.cjs +132 -41
- package/dist/collection-watchers/index.cjs.map +1 -1
- package/dist/collection-watchers/index.js +132 -41
- package/dist/collection-watchers/index.js.map +1 -1
- package/dist/collection-watchers/reconciler.d.ts +1 -1
- package/dist/{discovery-DH9wweuj.js → discovery-CdkaURVY.js} +368 -57
- package/dist/discovery-CdkaURVY.js.map +1 -0
- package/dist/{discovery-Ck4AqikY.cjs → discovery-Cw5xWzZD.cjs} +407 -54
- package/dist/discovery-Cw5xWzZD.cjs.map +1 -0
- package/dist/feeds/server/index.cjs +2 -2
- package/dist/feeds/server/index.js +2 -2
- package/dist/google/index.cjs +1 -1
- package/dist/google/index.js +1 -1
- package/dist/{server-5EMj3naj.cjs → server-9mFaGLdq.cjs} +23 -4
- package/dist/server-9mFaGLdq.cjs.map +1 -0
- package/dist/{server-BiRLLMpW.js → server-DdHaX_Uu.js} +23 -4
- package/dist/server-DdHaX_Uu.js.map +1 -0
- package/package.json +7 -1
- package/dist/discovery-Ck4AqikY.cjs.map +0 -1
- package/dist/discovery-DH9wweuj.js.map +0 -1
- package/dist/server-5EMj3naj.cjs.map +0 -1
- package/dist/server-BiRLLMpW.js.map +0 -1
|
@@ -106,6 +106,7 @@ function collectionChangeKey(payload, fallbackRoot) {
|
|
|
106
106
|
}
|
|
107
107
|
var hostSlot = createHostSlot("@mulmoclaude/core/collection/server: configureCollectionHost()");
|
|
108
108
|
var changePublisher = null;
|
|
109
|
+
var firestoreAccessor = null;
|
|
109
110
|
/** Wire the engine to a host. Call once at server startup, before any
|
|
110
111
|
* collection storage operation. Re-binding to a *different* host throws —
|
|
111
112
|
* silently redirecting later filesystem operations to another workspace
|
|
@@ -129,6 +130,25 @@ function setCollectionChangePublisher(publish) {
|
|
|
129
130
|
function publishCollectionChange(payload) {
|
|
130
131
|
changePublisher?.(payload);
|
|
131
132
|
}
|
|
133
|
+
/** Wire the accessor for the host's authenticated Firestore session.
|
|
134
|
+
*
|
|
135
|
+
* Separate from `configureCollectionHost` for the same reason
|
|
136
|
+
* `setCollectionChangePublisher` is: the host binding is set at the top of
|
|
137
|
+
* server startup, but this session doesn't exist until the user connects
|
|
138
|
+
* remote-host (and closes again on disconnect), so it cannot be part of a
|
|
139
|
+
* one-shot binding. Optional — left unwired, only shared collections are
|
|
140
|
+
* affected, and they report "not connected". Pass `null` to detach. */
|
|
141
|
+
function setFirestoreAccessor(accessor) {
|
|
142
|
+
firestoreAccessor = accessor;
|
|
143
|
+
}
|
|
144
|
+
/** The host's live Firestore access, or null when there is no session (or the
|
|
145
|
+
* host never wired one — every non-shared backend leaves it unset).
|
|
146
|
+
* Callers MUST surface null as an actionable "connect remote-host first",
|
|
147
|
+
* never as an empty result: silence would be indistinguishable from a
|
|
148
|
+
* collection that genuinely has no records. */
|
|
149
|
+
function firestoreHandle() {
|
|
150
|
+
return firestoreAccessor?.() ?? null;
|
|
151
|
+
}
|
|
132
152
|
function requireHost() {
|
|
133
153
|
return hostSlot.get();
|
|
134
154
|
}
|
|
@@ -193,6 +213,18 @@ function isPresetSlug(slug) {
|
|
|
193
213
|
* without a workspace root. */
|
|
194
214
|
var log = createForwardingLogger(() => hostSlot.peek()?.log ?? null);
|
|
195
215
|
//#endregion
|
|
216
|
+
//#region src/collection/server/backendAvailability.ts
|
|
217
|
+
/** Thrown by a store when its engine or session cannot serve the request. */
|
|
218
|
+
var BackendUnavailableError = class extends Error {
|
|
219
|
+
constructor(message) {
|
|
220
|
+
super(message);
|
|
221
|
+
this.name = "BackendUnavailableError";
|
|
222
|
+
}
|
|
223
|
+
};
|
|
224
|
+
function isBackendUnavailable(err) {
|
|
225
|
+
return err instanceof BackendUnavailableError;
|
|
226
|
+
}
|
|
227
|
+
//#endregion
|
|
196
228
|
//#region src/collection/server/paths.ts
|
|
197
229
|
var SCHEMA_FILE = "schema.json";
|
|
198
230
|
/** Sanitise a user-supplied slug into a safe directory-name leaf.
|
|
@@ -304,6 +336,280 @@ function resolveTemplatePath(skillDir, templateRelPath) {
|
|
|
304
336
|
return resolved;
|
|
305
337
|
}
|
|
306
338
|
//#endregion
|
|
339
|
+
//#region src/collection/server/storePage.ts
|
|
340
|
+
/** Project `fields` (+ the primary key, always) out of each record. Thin
|
|
341
|
+
* server-typed alias over the shared isomorphic `projectRecordFields`
|
|
342
|
+
* (../core/project.ts) — kept as the store layer's exported name. */
|
|
343
|
+
function projectItemFields(items, fields, primaryKey) {
|
|
344
|
+
return require_project.projectRecordFields(items, fields, primaryKey);
|
|
345
|
+
}
|
|
346
|
+
/** Slice + project an already-ordered full read into a `ListPage` — the
|
|
347
|
+
* shared emulation for stores without native paging. Pure, exported for
|
|
348
|
+
* tests. `limit: 0` is a valid "count only" page. */
|
|
349
|
+
function pageFromFullRead(items, opts, primaryKey, truncated) {
|
|
350
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
351
|
+
const end = opts.limit === void 0 ? items.length : offset + Math.max(0, opts.limit);
|
|
352
|
+
return {
|
|
353
|
+
items: projectItemFields(items.slice(offset, end), opts.fields, primaryKey),
|
|
354
|
+
total: items.length,
|
|
355
|
+
truncated
|
|
356
|
+
};
|
|
357
|
+
}
|
|
358
|
+
//#endregion
|
|
359
|
+
//#region src/collection/server/firestoreStore.ts
|
|
360
|
+
/** What every operation throws when there is no live session. Worded as an
|
|
361
|
+
* instruction because it surfaces straight to the user and the agent. */
|
|
362
|
+
var NOT_CONNECTED = "shared collection unavailable: connect remote-host first — these records live in the app's Firestore, not in the workspace, so nothing can be read or written while the session is closed";
|
|
363
|
+
/** What a schema declaring `storage.type: "firestore"` must have had resolved
|
|
364
|
+
* for it before it can be served. Its absence is a programming error here, not
|
|
365
|
+
* a user-facing state: discovery REFUSES such a schema when the repository
|
|
366
|
+
* declares no `aid`, so a collection that reached this store has one. */
|
|
367
|
+
var NO_APP = "shared collection has no app id — discovery should have refused this schema; check that the repository's app.json declares an `aid`";
|
|
368
|
+
/** The records subcollection of one shared collection.
|
|
369
|
+
*
|
|
370
|
+
* Takes a KEY, never loose strings, and the key is the only way to reach this
|
|
371
|
+
* function. `sharedCollectionKey` is where the name rule lives (the charset a
|
|
372
|
+
* Firestore document id, a pubsub channel segment and the completion-bell id
|
|
373
|
+
* must all survive), so building a path cannot be a way around it. */
|
|
374
|
+
function sharedItemsPath(key) {
|
|
375
|
+
return `apps/${key.aid}/collections/${key.cid}/items`;
|
|
376
|
+
}
|
|
377
|
+
/** The collection's identity, from what discovery resolved. Throws on a
|
|
378
|
+
* missing `appId` — see NO_APP. */
|
|
379
|
+
function keyOf(collection) {
|
|
380
|
+
if (collection.appId === void 0) throw new Error(NO_APP);
|
|
381
|
+
return require_calendarGrid.sharedCollectionKey(collection.appId, collection.slug);
|
|
382
|
+
}
|
|
383
|
+
function requireHandle() {
|
|
384
|
+
const handle = firestoreHandle();
|
|
385
|
+
if (handle === null) throw new BackendUnavailableError(NOT_CONNECTED);
|
|
386
|
+
return handle;
|
|
387
|
+
}
|
|
388
|
+
/** Firestore's own refusal, named.
|
|
389
|
+
*
|
|
390
|
+
* `permission-denied` is the failure a shared collection has most often and
|
|
391
|
+
* the one the SDK explains worst ("Missing or insufficient permissions") — it
|
|
392
|
+
* says nothing about WHO was refused, which is the only fact that leads to a
|
|
393
|
+
* fix. Authorization here is the app's member roster, keyed by email, so the
|
|
394
|
+
* signed-in address is what the app's owner needs in order to add it. This is
|
|
395
|
+
* the whole reason `FirestoreHandle` carries `email`.
|
|
396
|
+
*
|
|
397
|
+
* Reported as a `BackendUnavailableError` deliberately, even though it is a
|
|
398
|
+
* refusal rather than an outage: the layers above catch broadly, and without a
|
|
399
|
+
* type to test, `store.read(...).catch(() => null)` reports "record missing"
|
|
400
|
+
* and an ontology count reports 0 — a denial would read as an empty
|
|
401
|
+
* collection, which is the exact confusion this backend refuses to create. */
|
|
402
|
+
function isPermissionDenied(err) {
|
|
403
|
+
return require_dist.isRecord(err) && err.code === "permission-denied";
|
|
404
|
+
}
|
|
405
|
+
function deniedMessage(key, email) {
|
|
406
|
+
return `permission denied on shared collection '${key.cid}' of app '${key.aid}' — signed in as ${email}. A shared collection is authorized by the app's member roster (by email), so this address needs a role for '${key.cid}' (or '*'); only the app's owner can add it.`;
|
|
407
|
+
}
|
|
408
|
+
/** Run one SDK call, translating a roster denial. Every read and write goes
|
|
409
|
+
* through this — a denial reaching one path and not another would mean the
|
|
410
|
+
* message a user sees depends on which screen they were on. */
|
|
411
|
+
async function guarded(key, email, run) {
|
|
412
|
+
try {
|
|
413
|
+
return await run();
|
|
414
|
+
} catch (err) {
|
|
415
|
+
if (!isPermissionDenied(err)) throw err;
|
|
416
|
+
throw new BackendUnavailableError(deniedMessage(key, email));
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
/** A stored document's fields → a record. A document written by hand (or by an
|
|
420
|
+
* older version) can hold anything, so a non-object is dropped rather than
|
|
421
|
+
* surfaced as a broken record — the same fail-soft the file store applies to
|
|
422
|
+
* an unparseable `.json`. */
|
|
423
|
+
function toItem(data) {
|
|
424
|
+
return require_dist.isRecord(data) ? data : null;
|
|
425
|
+
}
|
|
426
|
+
/** Record ids are validated with the SAME helper every other backend uses.
|
|
427
|
+
* Firestore would accept ids the file store refuses, but a record should stay
|
|
428
|
+
* portable between backends — and an id that can't round-trip to a filename
|
|
429
|
+
* would break an export back to a file collection. */
|
|
430
|
+
function withSafeId(itemId, onInvalid, run) {
|
|
431
|
+
const safeId = safeRecordId(itemId);
|
|
432
|
+
if (safeId === null) return onInvalid();
|
|
433
|
+
return run(safeId, requireHandle());
|
|
434
|
+
}
|
|
435
|
+
async function firestoreList(key) {
|
|
436
|
+
const { docs, email } = requireHandle();
|
|
437
|
+
return (await guarded(key, email, () => docs.list(sharedItemsPath(key)))).map((entry) => toItem(entry.data)).filter((item) => item !== null);
|
|
438
|
+
}
|
|
439
|
+
/** Paging is emulated over a full ordered read rather than pushed into
|
|
440
|
+
* Firestore: `offset` has no server-side form there (the cursor API needs the
|
|
441
|
+
* preceding document, which a stateless offset/limit call doesn't have), and
|
|
442
|
+
* `total` needs the full count anyway. Hence `nativePaging: false` — the
|
|
443
|
+
* capability is honest about the cost. */
|
|
444
|
+
async function firestorePage(key, primaryKey, opts) {
|
|
445
|
+
const items = await firestoreList(key);
|
|
446
|
+
const offset = Math.max(0, opts.offset ?? 0);
|
|
447
|
+
return {
|
|
448
|
+
items: projectItemFields(opts.limit === void 0 ? items.slice(offset) : items.slice(offset, offset + Math.max(0, opts.limit)), opts.fields, primaryKey),
|
|
449
|
+
total: items.length,
|
|
450
|
+
truncated: false
|
|
451
|
+
};
|
|
452
|
+
}
|
|
453
|
+
async function firestoreRead(key, itemId) {
|
|
454
|
+
return withSafeId(itemId, () => Promise.resolve(null), async (safeId, { docs, email }) => toItem(await guarded(key, email, () => docs.get(sharedItemsPath(key), safeId))));
|
|
455
|
+
}
|
|
456
|
+
/** Publish the "records changed" ping for a shared collection.
|
|
457
|
+
*
|
|
458
|
+
* `sharedCollectionChangePayload` NEVER stamps a root, and that matters beyond
|
|
459
|
+
* tidiness: this payload is relayed to the browser and on into an
|
|
460
|
+
* LLM-generated custom-view iframe, so a filesystem path on it would be a
|
|
461
|
+
* disclosure. The type makes it unreachable rather than trusting the caller. */
|
|
462
|
+
function publishShared(key, ids, operation) {
|
|
463
|
+
publishCollectionChange(sharedCollectionChangePayload({
|
|
464
|
+
slug: key.cid,
|
|
465
|
+
ids,
|
|
466
|
+
op: operation
|
|
467
|
+
}, key.aid));
|
|
468
|
+
}
|
|
469
|
+
async function firestoreWrite(key, itemId, item, opts) {
|
|
470
|
+
return withSafeId(itemId, () => Promise.resolve({
|
|
471
|
+
kind: "invalid-id",
|
|
472
|
+
itemId
|
|
473
|
+
}), async (safeId, { docs, email }) => {
|
|
474
|
+
const collectionPath = sharedItemsPath(key);
|
|
475
|
+
if (opts.refuseOverwrite) {
|
|
476
|
+
if (!await guarded(key, email, () => docs.create(collectionPath, safeId, item))) return {
|
|
477
|
+
kind: "conflict",
|
|
478
|
+
itemId: safeId
|
|
479
|
+
};
|
|
480
|
+
} else await guarded(key, email, () => docs.set(collectionPath, safeId, item));
|
|
481
|
+
if (opts.slug) publishShared(key, [safeId], "upsert");
|
|
482
|
+
return {
|
|
483
|
+
kind: "ok",
|
|
484
|
+
itemId: safeId,
|
|
485
|
+
item
|
|
486
|
+
};
|
|
487
|
+
});
|
|
488
|
+
}
|
|
489
|
+
async function firestoreDelete(key, itemId, opts) {
|
|
490
|
+
return withSafeId(itemId, () => Promise.resolve({
|
|
491
|
+
kind: "invalid-id",
|
|
492
|
+
itemId
|
|
493
|
+
}), async (safeId, { docs, email }) => {
|
|
494
|
+
if (!await guarded(key, email, () => docs.delete(sharedItemsPath(key), safeId))) return {
|
|
495
|
+
kind: "not-found",
|
|
496
|
+
itemId: safeId
|
|
497
|
+
};
|
|
498
|
+
if (opts.slug) publishShared(key, [safeId], "delete");
|
|
499
|
+
return {
|
|
500
|
+
kind: "ok",
|
|
501
|
+
itemId: safeId
|
|
502
|
+
};
|
|
503
|
+
});
|
|
504
|
+
}
|
|
505
|
+
/** The store factory registered for `storage.type === "firestore"`.
|
|
506
|
+
* Synchronous and connection-agnostic by contract — see the header. */
|
|
507
|
+
function firestoreStoreFor(collection, opts) {
|
|
508
|
+
const { primaryKey } = collection.schema;
|
|
509
|
+
const ioOpts = {
|
|
510
|
+
...opts,
|
|
511
|
+
slug: opts.slug ?? collection.slug
|
|
512
|
+
};
|
|
513
|
+
return {
|
|
514
|
+
capabilities: {
|
|
515
|
+
writable: true,
|
|
516
|
+
nativeQuery: false,
|
|
517
|
+
nativePaging: false
|
|
518
|
+
},
|
|
519
|
+
list: async () => firestoreList(keyOf(collection)),
|
|
520
|
+
page: async (pageOpts = {}) => firestorePage(keyOf(collection), primaryKey, pageOpts),
|
|
521
|
+
read: async (itemId) => firestoreRead(keyOf(collection), itemId),
|
|
522
|
+
write: async (itemId, item, writeOpts = {}) => firestoreWrite(keyOf(collection), itemId, item, {
|
|
523
|
+
...ioOpts,
|
|
524
|
+
refuseOverwrite: writeOpts.refuseOverwrite
|
|
525
|
+
}),
|
|
526
|
+
delete: async (itemId) => firestoreDelete(keyOf(collection), itemId, ioOpts)
|
|
527
|
+
};
|
|
528
|
+
}
|
|
529
|
+
//#endregion
|
|
530
|
+
//#region src/collection/server/appManifest.ts
|
|
531
|
+
/** The app declaration's filename, at the repository root. */
|
|
532
|
+
var APP_MANIFEST_FILE = "app.json";
|
|
533
|
+
/** Read `<root>/app.json` and return its `aid`.
|
|
534
|
+
*
|
|
535
|
+
* SYNCHRONOUS on purpose. The caller is `acceptParsedSchema`, which is sync
|
|
536
|
+
* and is shared by discovery and `manageCollection`'s `putSchema` precisely so
|
|
537
|
+
* that a schema which would be skipped on the next discovery cannot be written
|
|
538
|
+
* as if it were fine. Making this async would split that gate in two, and the
|
|
539
|
+
* half that lost the check is the half the author sees. The file is a few
|
|
540
|
+
* hundred bytes and is read once per firestore collection per discovery pass.
|
|
541
|
+
*
|
|
542
|
+
* Never cached. `app.json` is edited by hand and by the agent, and a cache
|
|
543
|
+
* here would mean the app a collection points at is whatever it was when the
|
|
544
|
+
* server started. */
|
|
545
|
+
function loadAppManifest(root) {
|
|
546
|
+
let raw;
|
|
547
|
+
try {
|
|
548
|
+
raw = (0, node_fs.readFileSync)(node_path.default.join(root, APP_MANIFEST_FILE), "utf-8");
|
|
549
|
+
} catch (err) {
|
|
550
|
+
if (require_dist.isErrorWithCode(err) && err.code === "ENOENT") return {
|
|
551
|
+
ok: false,
|
|
552
|
+
kind: "missing"
|
|
553
|
+
};
|
|
554
|
+
return {
|
|
555
|
+
ok: false,
|
|
556
|
+
kind: "unreadable",
|
|
557
|
+
detail: String(err)
|
|
558
|
+
};
|
|
559
|
+
}
|
|
560
|
+
return parseAppManifest(raw);
|
|
561
|
+
}
|
|
562
|
+
/** The parse half, exported so it can be tested without a filesystem.
|
|
563
|
+
*
|
|
564
|
+
* `aid` is validated with `isValidCollectionName` — the SAME predicate the
|
|
565
|
+
* `CollectionKey` constructors apply — rather than a rule of its own. An `aid`
|
|
566
|
+
* is re-encoded downstream as a Firestore document id, a pubsub channel
|
|
567
|
+
* segment and a cache key, each with a different character that would break
|
|
568
|
+
* it; one rule, stated once, is what keeps those layers from disagreeing.
|
|
569
|
+
* Rejecting here rather than at `sharedCollectionKey` only changes WHERE the
|
|
570
|
+
* author is told: a reason on the collection they wrote, instead of a throw
|
|
571
|
+
* from inside a store call. */
|
|
572
|
+
function parseAppManifest(raw) {
|
|
573
|
+
let parsed;
|
|
574
|
+
try {
|
|
575
|
+
parsed = JSON.parse(raw);
|
|
576
|
+
} catch (err) {
|
|
577
|
+
return {
|
|
578
|
+
ok: false,
|
|
579
|
+
kind: "malformed",
|
|
580
|
+
detail: `not valid JSON (${String(err)})`
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
if (!require_dist.isRecord(parsed)) return {
|
|
584
|
+
ok: false,
|
|
585
|
+
kind: "malformed",
|
|
586
|
+
detail: "is not a JSON object"
|
|
587
|
+
};
|
|
588
|
+
const { aid } = parsed;
|
|
589
|
+
if (typeof aid !== "string" || aid.length === 0) return {
|
|
590
|
+
ok: false,
|
|
591
|
+
kind: "malformed",
|
|
592
|
+
detail: "declares no `aid` string"
|
|
593
|
+
};
|
|
594
|
+
if (!require_calendarGrid.isValidCollectionName(aid)) return {
|
|
595
|
+
ok: false,
|
|
596
|
+
kind: "malformed",
|
|
597
|
+
detail: `\`aid\` '${aid}' is not a valid app id`
|
|
598
|
+
};
|
|
599
|
+
return {
|
|
600
|
+
ok: true,
|
|
601
|
+
manifest: { aid }
|
|
602
|
+
};
|
|
603
|
+
}
|
|
604
|
+
/** The failure as the one line an author can act on. Kept next to the failure
|
|
605
|
+
* type so a new variant cannot be added without wording it. */
|
|
606
|
+
function appManifestReason(failure, root) {
|
|
607
|
+
const manifestPath = node_path.default.join(root, APP_MANIFEST_FILE);
|
|
608
|
+
if (failure.kind === "missing") return `a shared collection needs an app: create ${manifestPath} declaring an \`aid\``;
|
|
609
|
+
if (failure.kind === "unreadable") return `cannot read ${manifestPath}: ${failure.detail}`;
|
|
610
|
+
return `${manifestPath} ${failure.detail}`;
|
|
611
|
+
}
|
|
612
|
+
//#endregion
|
|
307
613
|
//#region src/collection/server/io.ts
|
|
308
614
|
/** True iff `filePath` exists and is a regular file (NOT a symlink).
|
|
309
615
|
* Defends `listItems` / `readItem` against `*.json` symlinks placed
|
|
@@ -526,18 +832,6 @@ function resolveCreateItemId(schema, record) {
|
|
|
526
832
|
return typeof primaryRaw === "string" && primaryRaw.length > 0 ? primaryRaw : null;
|
|
527
833
|
}
|
|
528
834
|
//#endregion
|
|
529
|
-
//#region src/collection/server/backendAvailability.ts
|
|
530
|
-
/** Thrown by a store when its engine or session cannot serve the request. */
|
|
531
|
-
var BackendUnavailableError = class extends Error {
|
|
532
|
-
constructor(message) {
|
|
533
|
-
super(message);
|
|
534
|
-
this.name = "BackendUnavailableError";
|
|
535
|
-
}
|
|
536
|
-
};
|
|
537
|
-
function isBackendUnavailable(err) {
|
|
538
|
-
return err instanceof BackendUnavailableError;
|
|
539
|
-
}
|
|
540
|
-
//#endregion
|
|
541
835
|
//#region src/collection/core/queryZ.ts
|
|
542
836
|
/** Result-column aliases double as SQL identifiers and JSON keys — keep
|
|
543
837
|
* them to a conservative identifier charset so neither side needs
|
|
@@ -1131,26 +1425,6 @@ function closerFor(handle) {
|
|
|
1131
1425
|
return handle === null ? null : () => handle.close();
|
|
1132
1426
|
}
|
|
1133
1427
|
//#endregion
|
|
1134
|
-
//#region src/collection/server/storePage.ts
|
|
1135
|
-
/** Project `fields` (+ the primary key, always) out of each record. Thin
|
|
1136
|
-
* server-typed alias over the shared isomorphic `projectRecordFields`
|
|
1137
|
-
* (../core/project.ts) — kept as the store layer's exported name. */
|
|
1138
|
-
function projectItemFields(items, fields, primaryKey) {
|
|
1139
|
-
return require_project.projectRecordFields(items, fields, primaryKey);
|
|
1140
|
-
}
|
|
1141
|
-
/** Slice + project an already-ordered full read into a `ListPage` — the
|
|
1142
|
-
* shared emulation for stores without native paging. Pure, exported for
|
|
1143
|
-
* tests. `limit: 0` is a valid "count only" page. */
|
|
1144
|
-
function pageFromFullRead(items, opts, primaryKey, truncated) {
|
|
1145
|
-
const offset = Math.max(0, opts.offset ?? 0);
|
|
1146
|
-
const end = opts.limit === void 0 ? items.length : offset + Math.max(0, opts.limit);
|
|
1147
|
-
return {
|
|
1148
|
-
items: projectItemFields(items.slice(offset, end), opts.fields, primaryKey),
|
|
1149
|
-
total: items.length,
|
|
1150
|
-
truncated
|
|
1151
|
-
};
|
|
1152
|
-
}
|
|
1153
|
-
//#endregion
|
|
1154
1428
|
//#region src/collection/server/sqliteStore.ts
|
|
1155
1429
|
/** A constructor's parameter and return types are not observable at runtime,
|
|
1156
1430
|
* so the check stops at "DatabaseSync is constructible" — the only member of
|
|
@@ -1500,7 +1774,8 @@ function fileStoreFor(collection, opts) {
|
|
|
1500
1774
|
var storeFactories = /* @__PURE__ */ new Map([
|
|
1501
1775
|
["file", fileStoreFor],
|
|
1502
1776
|
["csv", csvStoreFor],
|
|
1503
|
-
["sqlite", sqliteStoreFor]
|
|
1777
|
+
["sqlite", sqliteStoreFor],
|
|
1778
|
+
["firestore", firestoreStoreFor]
|
|
1504
1779
|
]);
|
|
1505
1780
|
/** Pick the store implementation for a discovered collection via the
|
|
1506
1781
|
* factory registry. An unknown kind cannot normally reach here (the
|
|
@@ -2356,15 +2631,19 @@ var DataSourceZ = zod.z.object({
|
|
|
2356
2631
|
/** Alternative WRITABLE storage backend for a collection's records —
|
|
2357
2632
|
* unlike `dataSource` (external read-only file), a `storage` collection
|
|
2358
2633
|
* behaves like a normal writable collection; only where the rows live
|
|
2359
|
-
* changes.
|
|
2360
|
-
*
|
|
2361
|
-
*
|
|
2362
|
-
*
|
|
2363
|
-
*
|
|
2364
|
-
|
|
2634
|
+
* changes. The store factory registry (`server/store.ts`) picks the
|
|
2635
|
+
* implementation by `type` (plans/done/refactor-storage-virtualization.md).
|
|
2636
|
+
*
|
|
2637
|
+
* A discriminated union rather than one shape with optional keys, because
|
|
2638
|
+
* only the sqlite variant is a workspace FILE: its `path` is
|
|
2639
|
+
* workspace-relative and containment-checked exactly like `dataPath`, while
|
|
2640
|
+
* the firestore variant has no path to check — its records are not on this
|
|
2641
|
+
* machine at all. Optional keys would let each arm accept the other's, and
|
|
2642
|
+
* the compiler would stop being the thing that tells you which. */
|
|
2643
|
+
var StorageZ = zod.z.discriminatedUnion("type", [zod.z.object({
|
|
2365
2644
|
type: zod.z.literal("sqlite"),
|
|
2366
2645
|
path: zod.z.string().min(1)
|
|
2367
|
-
});
|
|
2646
|
+
}), zod.z.object({ type: zod.z.literal("firestore") }).strict()]);
|
|
2368
2647
|
var BareCollectionSchemaZ = zod.z.object({
|
|
2369
2648
|
title: zod.z.string().min(1),
|
|
2370
2649
|
icon: zod.z.string().min(1),
|
|
@@ -2650,16 +2929,45 @@ function acceptParsedSchema(schema, opts) {
|
|
|
2650
2929
|
dataSourceFile
|
|
2651
2930
|
};
|
|
2652
2931
|
}
|
|
2653
|
-
if (schema.storage !== void 0)
|
|
2654
|
-
|
|
2932
|
+
if (schema.storage !== void 0) return acceptStorageSchema(schema.storage, opts);
|
|
2933
|
+
const dataDir = resolveDataDir(schema.dataPath ?? "", opts.workspaceRoot);
|
|
2934
|
+
if (dataDir === null) return {
|
|
2935
|
+
ok: false,
|
|
2936
|
+
reason: `dataPath '${schema.dataPath}' escapes the workspace`
|
|
2937
|
+
};
|
|
2938
|
+
return {
|
|
2939
|
+
ok: true,
|
|
2940
|
+
dataDir
|
|
2941
|
+
};
|
|
2942
|
+
}
|
|
2943
|
+
/** The `storage` arm of the acceptance gate. Every storage backend gets the
|
|
2944
|
+
* conventional phantom dataDir; what differs is what else has to resolve
|
|
2945
|
+
* before the collection can exist at all.
|
|
2946
|
+
*
|
|
2947
|
+
* A FILE-backed backend (sqlite) resolves and containment-checks a
|
|
2948
|
+
* `storageFile`. A SHARED one (firestore) has no path on this machine — it
|
|
2949
|
+
* resolves an IDENTITY instead: the `aid` from the repository's `app.json`,
|
|
2950
|
+
* which together with the slug as `cid` names `apps/{aid}/collections/{cid}`.
|
|
2951
|
+
*
|
|
2952
|
+
* Resolving it HERE, once, is the point. The store then receives a settled
|
|
2953
|
+
* `(aid, cid)` and never reads `app.json` itself — otherwise the questions of
|
|
2954
|
+
* caching, staleness and what to do when the file is missing would be decided
|
|
2955
|
+
* inside a read path, where the only cheap answer is to return nothing, and
|
|
2956
|
+
* "this collection is misconfigured" would reach the user as "this collection
|
|
2957
|
+
* is empty". A missing or malformed `app.json` is a CONFIGURATION error, so it
|
|
2958
|
+
* is reported the same way an escaping `storage.path` is: the schema is
|
|
2959
|
+
* refused, with a reason naming the file to create. */
|
|
2960
|
+
function acceptStorageSchema(storage, opts) {
|
|
2961
|
+
const dataDir = resolveDataDir(conventionalDataPath(opts.slug), opts.workspaceRoot);
|
|
2962
|
+
if (dataDir === null) return {
|
|
2963
|
+
ok: false,
|
|
2964
|
+
reason: `slug '${opts.slug}' yields no workspace-contained data dir`
|
|
2965
|
+
};
|
|
2966
|
+
if (storage.type === "sqlite") {
|
|
2967
|
+
const storageFile = resolveDataDir(storage.path, opts.workspaceRoot);
|
|
2655
2968
|
if (storageFile === null) return {
|
|
2656
2969
|
ok: false,
|
|
2657
|
-
reason: `storage.path '${
|
|
2658
|
-
};
|
|
2659
|
-
const dataDir = resolveDataDir(conventionalDataPath(opts.slug), opts.workspaceRoot);
|
|
2660
|
-
if (dataDir === null) return {
|
|
2661
|
-
ok: false,
|
|
2662
|
-
reason: `slug '${opts.slug}' yields no workspace-contained data dir`
|
|
2970
|
+
reason: `storage.path '${storage.path}' escapes the workspace`
|
|
2663
2971
|
};
|
|
2664
2972
|
return {
|
|
2665
2973
|
ok: true,
|
|
@@ -2667,14 +2975,15 @@ function acceptParsedSchema(schema, opts) {
|
|
|
2667
2975
|
storageFile
|
|
2668
2976
|
};
|
|
2669
2977
|
}
|
|
2670
|
-
const
|
|
2671
|
-
if (
|
|
2978
|
+
const manifest = loadAppManifest(opts.workspaceRoot);
|
|
2979
|
+
if (!manifest.ok) return {
|
|
2672
2980
|
ok: false,
|
|
2673
|
-
reason:
|
|
2981
|
+
reason: appManifestReason(manifest, opts.workspaceRoot)
|
|
2674
2982
|
};
|
|
2675
2983
|
return {
|
|
2676
2984
|
ok: true,
|
|
2677
|
-
dataDir
|
|
2985
|
+
dataDir,
|
|
2986
|
+
appId: manifest.manifest.aid
|
|
2678
2987
|
};
|
|
2679
2988
|
}
|
|
2680
2989
|
async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
|
|
@@ -2732,6 +3041,7 @@ async function loadOneCollection(skillsRoot, slug, source, workspaceRoot) {
|
|
|
2732
3041
|
dataDir: acceptance.dataDir,
|
|
2733
3042
|
...acceptance.dataSourceFile !== void 0 ? { dataSourceFile: acceptance.dataSourceFile } : {},
|
|
2734
3043
|
...acceptance.storageFile !== void 0 ? { storageFile: acceptance.storageFile } : {},
|
|
3044
|
+
...acceptance.appId !== void 0 ? { appId: acceptance.appId } : {},
|
|
2735
3045
|
skillDir: node_path.default.join(skillsRoot, safeName)
|
|
2736
3046
|
};
|
|
2737
3047
|
}
|
|
@@ -2812,7 +3122,8 @@ function toSummary(collection) {
|
|
|
2812
3122
|
title: collection.schema.title,
|
|
2813
3123
|
icon: collection.schema.icon,
|
|
2814
3124
|
source: collection.source,
|
|
2815
|
-
...collection.schema.dataSource !== void 0 ? { readonly: true } : {}
|
|
3125
|
+
...collection.schema.dataSource !== void 0 ? { readonly: true } : {},
|
|
3126
|
+
...collection.appId !== void 0 ? { appId: collection.appId } : {}
|
|
2816
3127
|
};
|
|
2817
3128
|
}
|
|
2818
3129
|
function toDetail(collection) {
|
|
@@ -2822,6 +3133,12 @@ function toDetail(collection) {
|
|
|
2822
3133
|
};
|
|
2823
3134
|
}
|
|
2824
3135
|
//#endregion
|
|
3136
|
+
Object.defineProperty(exports, "APP_MANIFEST_FILE", {
|
|
3137
|
+
enumerable: true,
|
|
3138
|
+
get: function() {
|
|
3139
|
+
return APP_MANIFEST_FILE;
|
|
3140
|
+
}
|
|
3141
|
+
});
|
|
2825
3142
|
Object.defineProperty(exports, "BackendUnavailableError", {
|
|
2826
3143
|
enumerable: true,
|
|
2827
3144
|
get: function() {
|
|
@@ -2876,6 +3193,12 @@ Object.defineProperty(exports, "acceptParsedSchema", {
|
|
|
2876
3193
|
return acceptParsedSchema;
|
|
2877
3194
|
}
|
|
2878
3195
|
});
|
|
3196
|
+
Object.defineProperty(exports, "appManifestReason", {
|
|
3197
|
+
enumerable: true,
|
|
3198
|
+
get: function() {
|
|
3199
|
+
return appManifestReason;
|
|
3200
|
+
}
|
|
3201
|
+
});
|
|
2879
3202
|
Object.defineProperty(exports, "archiveDir", {
|
|
2880
3203
|
enumerable: true,
|
|
2881
3204
|
get: function() {
|
|
@@ -2984,6 +3307,12 @@ Object.defineProperty(exports, "encodeCsvRecordId", {
|
|
|
2984
3307
|
return encodeCsvRecordId;
|
|
2985
3308
|
}
|
|
2986
3309
|
});
|
|
3310
|
+
Object.defineProperty(exports, "firestoreHandle", {
|
|
3311
|
+
enumerable: true,
|
|
3312
|
+
get: function() {
|
|
3313
|
+
return firestoreHandle;
|
|
3314
|
+
}
|
|
3315
|
+
});
|
|
2987
3316
|
Object.defineProperty(exports, "generateItemId", {
|
|
2988
3317
|
enumerable: true,
|
|
2989
3318
|
get: function() {
|
|
@@ -3032,6 +3361,12 @@ Object.defineProperty(exports, "listItems", {
|
|
|
3032
3361
|
return listItems;
|
|
3033
3362
|
}
|
|
3034
3363
|
});
|
|
3364
|
+
Object.defineProperty(exports, "loadAppManifest", {
|
|
3365
|
+
enumerable: true,
|
|
3366
|
+
get: function() {
|
|
3367
|
+
return loadAppManifest;
|
|
3368
|
+
}
|
|
3369
|
+
});
|
|
3035
3370
|
Object.defineProperty(exports, "loadCollection", {
|
|
3036
3371
|
enumerable: true,
|
|
3037
3372
|
get: function() {
|
|
@@ -3062,6 +3397,12 @@ Object.defineProperty(exports, "pageFromFullRead", {
|
|
|
3062
3397
|
return pageFromFullRead;
|
|
3063
3398
|
}
|
|
3064
3399
|
});
|
|
3400
|
+
Object.defineProperty(exports, "parseAppManifest", {
|
|
3401
|
+
enumerable: true,
|
|
3402
|
+
get: function() {
|
|
3403
|
+
return parseAppManifest;
|
|
3404
|
+
}
|
|
3405
|
+
});
|
|
3065
3406
|
Object.defineProperty(exports, "peekWorkspaceRoot", {
|
|
3066
3407
|
enumerable: true,
|
|
3067
3408
|
get: function() {
|
|
@@ -3152,12 +3493,24 @@ Object.defineProperty(exports, "setCollectionChangePublisher", {
|
|
|
3152
3493
|
return setCollectionChangePublisher;
|
|
3153
3494
|
}
|
|
3154
3495
|
});
|
|
3496
|
+
Object.defineProperty(exports, "setFirestoreAccessor", {
|
|
3497
|
+
enumerable: true,
|
|
3498
|
+
get: function() {
|
|
3499
|
+
return setFirestoreAccessor;
|
|
3500
|
+
}
|
|
3501
|
+
});
|
|
3155
3502
|
Object.defineProperty(exports, "sharedCollectionChangePayload", {
|
|
3156
3503
|
enumerable: true,
|
|
3157
3504
|
get: function() {
|
|
3158
3505
|
return sharedCollectionChangePayload;
|
|
3159
3506
|
}
|
|
3160
3507
|
});
|
|
3508
|
+
Object.defineProperty(exports, "sharedItemsPath", {
|
|
3509
|
+
enumerable: true,
|
|
3510
|
+
get: function() {
|
|
3511
|
+
return sharedItemsPath;
|
|
3512
|
+
}
|
|
3513
|
+
});
|
|
3161
3514
|
Object.defineProperty(exports, "skillsStagingDir", {
|
|
3162
3515
|
enumerable: true,
|
|
3163
3516
|
get: function() {
|
|
@@ -3195,4 +3548,4 @@ Object.defineProperty(exports, "writeItem", {
|
|
|
3195
3548
|
}
|
|
3196
3549
|
});
|
|
3197
3550
|
|
|
3198
|
-
//# sourceMappingURL=discovery-
|
|
3551
|
+
//# sourceMappingURL=discovery-Cw5xWzZD.cjs.map
|