@milaboratories/pl-middle-layer 1.68.3 → 1.69.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/dist/index.cjs +10 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -2
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/index.cjs +2 -0
- package/dist/middle_layer/index.d.ts +2 -1
- package/dist/middle_layer/index.js +2 -1
- package/dist/middle_layer/middle_layer.cjs +333 -26
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +125 -7
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +335 -28
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project_list.d.ts +1 -1
- package/dist/middle_layer/sharing_list.cjs +13 -5
- package/dist/middle_layer/sharing_list.cjs.map +1 -1
- package/dist/middle_layer/sharing_list.d.ts +16 -2
- package/dist/middle_layer/sharing_list.d.ts.map +1 -1
- package/dist/middle_layer/sharing_list.js +14 -6
- package/dist/middle_layer/sharing_list.js.map +1 -1
- package/dist/middle_layer/template_list.cjs +72 -0
- package/dist/middle_layer/template_list.cjs.map +1 -0
- package/dist/middle_layer/template_list.d.ts +77 -0
- package/dist/middle_layer/template_list.d.ts.map +1 -0
- package/dist/middle_layer/template_list.js +64 -0
- package/dist/middle_layer/template_list.js.map +1 -0
- package/dist/model/index.cjs +8 -0
- package/dist/model/index.d.ts +5 -2
- package/dist/model/index.js +4 -2
- package/dist/model/sharing_model.cjs +59 -2
- package/dist/model/sharing_model.cjs.map +1 -1
- package/dist/model/sharing_model.d.ts +70 -7
- package/dist/model/sharing_model.d.ts.map +1 -1
- package/dist/model/sharing_model.js +57 -3
- package/dist/model/sharing_model.js.map +1 -1
- package/dist/model/template_serializer.d.ts +36 -0
- package/dist/model/template_serializer.d.ts.map +1 -1
- package/dist/model/template_share.cjs +42 -0
- package/dist/model/template_share.cjs.map +1 -0
- package/dist/model/template_share.d.ts +27 -0
- package/dist/model/template_share.d.ts.map +1 -0
- package/dist/model/template_share.js +42 -0
- package/dist/model/template_share.js.map +1 -0
- package/dist/mutator/project.cjs +2 -2
- package/dist/mutator/project.js +2 -2
- package/dist/mutator/sharing.cjs +40 -2
- package/dist/mutator/sharing.cjs.map +1 -1
- package/dist/mutator/sharing.js +40 -3
- package/dist/mutator/sharing.js.map +1 -1
- package/dist/mutator/template.cjs +54 -0
- package/dist/mutator/template.cjs.map +1 -0
- package/dist/mutator/template.js +52 -0
- package/dist/mutator/template.js.map +1 -0
- package/package.json +8 -8
- package/src/middle_layer/index.ts +9 -0
- package/src/middle_layer/middle_layer.ts +449 -36
- package/src/middle_layer/sharing_list.ts +37 -7
- package/src/middle_layer/template_list.ts +177 -0
- package/src/middle_layer/templates.test.ts +301 -0
- package/src/model/index.ts +14 -0
- package/src/model/sharing_model.test.ts +115 -1
- package/src/model/sharing_model.ts +134 -9
- package/src/model/template_share.test.ts +78 -0
- package/src/model/template_share.ts +52 -0
- package/src/mutator/sharing.ts +55 -3
- package/src/mutator/template.ts +75 -0
- package/src/test/with_ml.ts +38 -0
|
@@ -2,9 +2,12 @@ const require_registry_v2_provider = require("../block_registry/registry-v2-prov
|
|
|
2
2
|
require("../block_registry/index.cjs");
|
|
3
3
|
const require_project_model = require("../model/project_model.cjs");
|
|
4
4
|
const require_project_list = require("./project_list.cjs");
|
|
5
|
+
const require_template_list = require("./template_list.cjs");
|
|
6
|
+
const require_template = require("../mutator/template.cjs");
|
|
5
7
|
const require_sharing_model = require("../model/sharing_model.cjs");
|
|
6
8
|
const require_template_resolve = require("../model/template_resolve.cjs");
|
|
7
9
|
const require_template_apply = require("../model/template_apply.cjs");
|
|
10
|
+
const require_template_share = require("../model/template_share.cjs");
|
|
8
11
|
const require_block_pack = require("../mutator/block-pack/block_pack.cjs");
|
|
9
12
|
const require_index$1 = require("../debug/index.cjs");
|
|
10
13
|
const require_project = require("../mutator/project.cjs");
|
|
@@ -50,33 +53,39 @@ var MiddleLayer = class MiddleLayer {
|
|
|
50
53
|
driverKit;
|
|
51
54
|
signer;
|
|
52
55
|
projectListResourceId;
|
|
56
|
+
templateListResourceId;
|
|
53
57
|
sharingOutboxResourceId;
|
|
54
58
|
sharingStateResourceId;
|
|
55
59
|
openedProjectsList;
|
|
56
60
|
projectListTree;
|
|
61
|
+
templateListTree;
|
|
57
62
|
sharingOutboxTree;
|
|
58
63
|
sharingStateTree;
|
|
59
64
|
pendingSharesTree;
|
|
60
65
|
blockRegistryProvider;
|
|
61
66
|
projectList;
|
|
67
|
+
templateList;
|
|
62
68
|
outgoingShares;
|
|
63
69
|
pendingShares;
|
|
64
70
|
liveEnvelopes;
|
|
65
71
|
pl;
|
|
66
|
-
constructor(env, driverKit, signer, projectListResourceId, sharingOutboxResourceId, sharingStateResourceId, openedProjectsList, projectListTree, sharingOutboxTree, sharingStateTree, pendingSharesTree, blockRegistryProvider, projectList, outgoingShares, pendingShares, liveEnvelopes) {
|
|
72
|
+
constructor(env, driverKit, signer, projectListResourceId, templateListResourceId, sharingOutboxResourceId, sharingStateResourceId, openedProjectsList, projectListTree, templateListTree, sharingOutboxTree, sharingStateTree, pendingSharesTree, blockRegistryProvider, projectList, templateList, outgoingShares, pendingShares, liveEnvelopes) {
|
|
67
73
|
this.env = env;
|
|
68
74
|
this.driverKit = driverKit;
|
|
69
75
|
this.signer = signer;
|
|
70
76
|
this.projectListResourceId = projectListResourceId;
|
|
77
|
+
this.templateListResourceId = templateListResourceId;
|
|
71
78
|
this.sharingOutboxResourceId = sharingOutboxResourceId;
|
|
72
79
|
this.sharingStateResourceId = sharingStateResourceId;
|
|
73
80
|
this.openedProjectsList = openedProjectsList;
|
|
74
81
|
this.projectListTree = projectListTree;
|
|
82
|
+
this.templateListTree = templateListTree;
|
|
75
83
|
this.sharingOutboxTree = sharingOutboxTree;
|
|
76
84
|
this.sharingStateTree = sharingStateTree;
|
|
77
85
|
this.pendingSharesTree = pendingSharesTree;
|
|
78
86
|
this.blockRegistryProvider = blockRegistryProvider;
|
|
79
87
|
this.projectList = projectList;
|
|
88
|
+
this.templateList = templateList;
|
|
80
89
|
this.outgoingShares = outgoingShares;
|
|
81
90
|
this.pendingShares = pendingShares;
|
|
82
91
|
this.liveEnvelopes = liveEnvelopes;
|
|
@@ -263,13 +272,29 @@ var MiddleLayer = class MiddleLayer {
|
|
|
263
272
|
* the whole document
|
|
264
273
|
*/
|
|
265
274
|
async applyTemplateToProject(id, document, provider, options = {}) {
|
|
266
|
-
const
|
|
267
|
-
if (
|
|
275
|
+
const preparation = await this.prepareTemplateEntries(document, provider, { allowUnstable: options.allowUnstable ?? false });
|
|
276
|
+
if (preparation.problems.length > 0) return {
|
|
268
277
|
added: [],
|
|
269
|
-
problems:
|
|
278
|
+
problems: preparation.problems
|
|
270
279
|
};
|
|
271
|
-
|
|
280
|
+
return await this.applyPreparedEntries(id, document, preparation.prepared, options.author);
|
|
281
|
+
}
|
|
282
|
+
/**
|
|
283
|
+
* Stages 1 and 2 of an apply — resolve every entry to a block pack, then prepare every
|
|
284
|
+
* block — for a document that may not have a project yet.
|
|
285
|
+
*
|
|
286
|
+
* Neither stage creates anything, which is what lets a caller run them before it decides to
|
|
287
|
+
* create a project at all: {@link createProjectFromTemplate} does exactly that, so an
|
|
288
|
+
* unapplicable template leaves no empty project behind.
|
|
289
|
+
*/
|
|
290
|
+
async prepareTemplateEntries(document, provider, options) {
|
|
272
291
|
const prepared = /* @__PURE__ */ new Map();
|
|
292
|
+
const resolution = await require_template_resolve.resolveTemplateEntries(document, provider, options);
|
|
293
|
+
if (resolution.problems.length > 0) return {
|
|
294
|
+
prepared,
|
|
295
|
+
problems: [...resolution.problems]
|
|
296
|
+
};
|
|
297
|
+
const byEntryId = new Map(document.blocks.map((entry) => [entry.id, entry]));
|
|
273
298
|
const problems = [];
|
|
274
299
|
for (const entry of resolution.resolved) try {
|
|
275
300
|
const documentEntry = byEntryId.get(entry.entryId);
|
|
@@ -305,14 +330,22 @@ var MiddleLayer = class MiddleLayer {
|
|
|
305
330
|
error: `This entry's block could not be installed: ${(0, _platforma_sdk_model.ensureError)(e).message}`
|
|
306
331
|
});
|
|
307
332
|
}
|
|
308
|
-
|
|
309
|
-
|
|
333
|
+
return {
|
|
334
|
+
prepared,
|
|
310
335
|
problems
|
|
311
336
|
};
|
|
337
|
+
}
|
|
338
|
+
/**
|
|
339
|
+
* Stage 3 of an apply — create the blocks in one transaction, all or nothing.
|
|
340
|
+
*
|
|
341
|
+
* An entry it cannot create throws, the transaction is never committed, and the project keeps
|
|
342
|
+
* none of the blocks the apply had placed.
|
|
343
|
+
*/
|
|
344
|
+
async applyPreparedEntries(id, document, prepared, author) {
|
|
312
345
|
const rid = await this.resolveProjectId(id);
|
|
313
346
|
let added = [];
|
|
314
347
|
try {
|
|
315
|
-
await require_project.withProjectAuthored(this.env.projectHelper, this.pl, rid,
|
|
348
|
+
await require_project.withProjectAuthored(this.env.projectHelper, this.pl, rid, author, (mut) => {
|
|
316
349
|
added = require_template_construct.applyTemplateEntries({
|
|
317
350
|
document,
|
|
318
351
|
placer: mut,
|
|
@@ -338,6 +371,131 @@ var MiddleLayer = class MiddleLayer {
|
|
|
338
371
|
problems: []
|
|
339
372
|
};
|
|
340
373
|
}
|
|
374
|
+
templateIdCache = new lru_cache.LRUCache({ max: 1024 });
|
|
375
|
+
/**
|
|
376
|
+
* Saves a project as a template: a snapshot of its blocks and their params, no data.
|
|
377
|
+
*
|
|
378
|
+
* The document the export produced is what gets stored; the YAML it also rendered is a
|
|
379
|
+
* file format, and a stored template is rendered to it only on download.
|
|
380
|
+
*
|
|
381
|
+
* A block that cannot be expressed as a template entry stores nothing at all, and every
|
|
382
|
+
* such block is reported — fixing an unexportable project takes one pass, not one per block.
|
|
383
|
+
*
|
|
384
|
+
* @param projectId project to snapshot
|
|
385
|
+
* @param label label for the template; defaults to the project's own label
|
|
386
|
+
*/
|
|
387
|
+
async saveProjectAsTemplate(projectId, label) {
|
|
388
|
+
const outcome = await this.exportProjectAsTemplate(projectId);
|
|
389
|
+
if (!outcome.ok) return {
|
|
390
|
+
ok: false,
|
|
391
|
+
problems: outcome.problems
|
|
392
|
+
};
|
|
393
|
+
const rid = await this.resolveProjectId(projectId);
|
|
394
|
+
let tpl;
|
|
395
|
+
await this.pl.withWriteTx("MLSaveProjectAsTemplate", async (tx) => {
|
|
396
|
+
const meta = await tx.getKValueJson(rid, require_project_model.ProjectMetaKey);
|
|
397
|
+
tpl = require_template.createTemplate(tx, this.templateListResourceId, label ?? meta.label, {
|
|
398
|
+
schemaVersion: 1,
|
|
399
|
+
document: outcome.document,
|
|
400
|
+
sourceProjectLabel: meta.label
|
|
401
|
+
});
|
|
402
|
+
await tx.commit();
|
|
403
|
+
});
|
|
404
|
+
await this.templateListTree.refreshState();
|
|
405
|
+
const signedRid = await tpl.globalId;
|
|
406
|
+
const templateId = (0, _milaboratories_pl_client.resourceIdToString)(signedRid);
|
|
407
|
+
this.templateIdCache.set(templateId, signedRid);
|
|
408
|
+
return {
|
|
409
|
+
ok: true,
|
|
410
|
+
templateId
|
|
411
|
+
};
|
|
412
|
+
}
|
|
413
|
+
/** Changes a template's label. The stored document is immutable and stays untouched —
|
|
414
|
+
* improving a template means saving a new one. */
|
|
415
|
+
async renameTemplate(id, label) {
|
|
416
|
+
const rid = await this.resolveTemplateId(id);
|
|
417
|
+
await this.pl.withWriteTx("MLRenameTemplate", async (tx) => {
|
|
418
|
+
require_template.renameTemplate(tx, rid, label);
|
|
419
|
+
await tx.commit();
|
|
420
|
+
});
|
|
421
|
+
await this.templateListTree.refreshState();
|
|
422
|
+
}
|
|
423
|
+
/** Permanently deletes a template from the template list. */
|
|
424
|
+
async deleteTemplate(id) {
|
|
425
|
+
await this.pl.withWriteTx("MLRemoveTemplate", async (tx) => {
|
|
426
|
+
await require_template.deleteTemplate(tx, this.templateListResourceId, id);
|
|
427
|
+
await tx.commit();
|
|
428
|
+
});
|
|
429
|
+
this.templateIdCache.delete(id);
|
|
430
|
+
await this.templateListTree.refreshState();
|
|
431
|
+
}
|
|
432
|
+
/** Reads a stored template: its document plus what was true when it was taken. */
|
|
433
|
+
async getTemplateData(id) {
|
|
434
|
+
const rid = await this.resolveTemplateId(id);
|
|
435
|
+
return await this.pl.withReadTx("MLGetTemplate", async (tx) => {
|
|
436
|
+
const rd = await tx.getResourceData(rid, false);
|
|
437
|
+
if (rd.data === void 0) throw new Error(`Template ${id} carries no document.`);
|
|
438
|
+
return require_template_list.decodeStoredTemplateData(rd.data);
|
|
439
|
+
});
|
|
440
|
+
}
|
|
441
|
+
/**
|
|
442
|
+
* Where each entry of a stored template would get its block from, and which entries have
|
|
443
|
+
* nowhere to get one — resolution creates nothing, so this is the preview a UI shows before
|
|
444
|
+
* offering Apply. {@link createProjectFromTemplate} runs the same stage itself.
|
|
445
|
+
*/
|
|
446
|
+
async resolveTemplate(id, provider, options = {}) {
|
|
447
|
+
return await require_template_resolve.resolveTemplateEntries((await this.getTemplateData(id)).document, provider, { allowUnstable: options.allowUnstable ?? false });
|
|
448
|
+
}
|
|
449
|
+
/**
|
|
450
|
+
* Creates one project holding every block the stored template lists, in the template's order.
|
|
451
|
+
*
|
|
452
|
+
* Resolution and preparation run before the project exists, so a template with an entry
|
|
453
|
+
* nothing can supply a block for leaves no empty project in the list. The one write that
|
|
454
|
+
* follows is all or nothing, and an entry it rejects takes the project with it.
|
|
455
|
+
*
|
|
456
|
+
* @param id template to apply
|
|
457
|
+
* @param label label for the new project
|
|
458
|
+
* @param provider where each entry's block comes from
|
|
459
|
+
* @param options `allowUnstable` widens resolution to pre-release implementations
|
|
460
|
+
*/
|
|
461
|
+
async createProjectFromTemplate(id, label, provider, options = {}) {
|
|
462
|
+
const stored = await this.getTemplateData(id);
|
|
463
|
+
const preparation = await this.prepareTemplateEntries(stored.document, provider, { allowUnstable: options.allowUnstable ?? false });
|
|
464
|
+
if (preparation.problems.length > 0) return {
|
|
465
|
+
ok: false,
|
|
466
|
+
problems: preparation.problems
|
|
467
|
+
};
|
|
468
|
+
const projectId = await this.createProject({ label });
|
|
469
|
+
const report = await this.applyPreparedEntries(projectId, stored.document, preparation.prepared, options.author);
|
|
470
|
+
if (report.problems.length > 0) {
|
|
471
|
+
await this.deleteProject(projectId);
|
|
472
|
+
return {
|
|
473
|
+
ok: false,
|
|
474
|
+
problems: report.problems
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return {
|
|
478
|
+
ok: true,
|
|
479
|
+
projectId,
|
|
480
|
+
added: report.added
|
|
481
|
+
};
|
|
482
|
+
}
|
|
483
|
+
/** Resolves a TemplateId to a signed SignedResourceId.
|
|
484
|
+
* Uses LRU cache with TX-scan fallback. */
|
|
485
|
+
async resolveTemplateId(templateId) {
|
|
486
|
+
const cached = this.templateIdCache.get(templateId);
|
|
487
|
+
if (cached !== void 0) return cached;
|
|
488
|
+
const rid = await this.pl.withReadTx("ResolveTemplateId", async (tx) => {
|
|
489
|
+
const data = await tx.getResourceData(this.templateListResourceId, true);
|
|
490
|
+
for (const f of data.fields) {
|
|
491
|
+
if ((0, _milaboratories_pl_client.isNullSignedResourceId)(f.value)) continue;
|
|
492
|
+
if ((0, _milaboratories_pl_client.resourceIdToString)(f.value) === templateId) return f.value;
|
|
493
|
+
}
|
|
494
|
+
throw new Error(`Template ${templateId} not found in template list.`);
|
|
495
|
+
});
|
|
496
|
+
this.templateIdCache.set(templateId, rid);
|
|
497
|
+
return rid;
|
|
498
|
+
}
|
|
341
499
|
/** Permanently deletes project from the project list, this will result in
|
|
342
500
|
* destruction of all attached objects, like files, analysis results etc. */
|
|
343
501
|
async deleteProject(id) {
|
|
@@ -467,14 +625,98 @@ var MiddleLayer = class MiddleLayer {
|
|
|
467
625
|
title: options.title,
|
|
468
626
|
expiresAt
|
|
469
627
|
});
|
|
470
|
-
|
|
471
|
-
if (everyone) tx.grantAccess(envelopeGid, "", { writable: true }, _milaboratories_pl_client.GrantType.ANY_AUTHORISED);
|
|
472
|
-
else for (const recipient of options.recipients) tx.grantAccess(envelopeGid, recipient, { writable: true });
|
|
628
|
+
await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, { writable: true });
|
|
473
629
|
await tx.commit();
|
|
474
630
|
});
|
|
475
631
|
await this.sharingOutboxTree.refreshState();
|
|
476
632
|
}
|
|
477
633
|
/**
|
|
634
|
+
* Grants one freshly built envelope inside the transaction that created it: a single make-public
|
|
635
|
+
* grant for an everyone-share (empty/ignored target, ANY_AUTHORISED — the backend rewrites the
|
|
636
|
+
* target to the everyone-user, gated by role + permission ceiling), or one grant per named
|
|
637
|
+
* recipient.
|
|
638
|
+
*
|
|
639
|
+
* `writable` is not a preference. A project pack needs a writable grant because accepting copies
|
|
640
|
+
* the snapshots out of the envelope, and the cross-color attach rule permits that only to a
|
|
641
|
+
* writable grant holder. A template share copies nothing — the document sits in the envelope's
|
|
642
|
+
* own immutable data — so it is granted read-only, and must be: a writable everyone-grant would
|
|
643
|
+
* hand every user on the server write access to the envelope.
|
|
644
|
+
*/
|
|
645
|
+
async grantShareEnvelope(tx, envelope, everyone, recipients, permissions) {
|
|
646
|
+
const gid = await envelope.globalId;
|
|
647
|
+
if (everyone) tx.grantAccess(gid, "", permissions, _milaboratories_pl_client.GrantType.ANY_AUTHORISED);
|
|
648
|
+
else for (const r of recipients) tx.grantAccess(gid, r, permissions);
|
|
649
|
+
}
|
|
650
|
+
/**
|
|
651
|
+
* Shares one stored template. The envelope carries the document itself, so there is no project
|
|
652
|
+
* snapshot and no resource for the recipient to copy out — which is why the grant is read-only.
|
|
653
|
+
* The cost of that is the donor's receipt: nobody can write an acceptance onto a read-only
|
|
654
|
+
* envelope, so a template share never reports who accepted it.
|
|
655
|
+
*
|
|
656
|
+
* A template holding a block installed from a folder on this machine is refused rather than sent,
|
|
657
|
+
* with every offending entry named. {@link checkTemplateShareable} answers the same question
|
|
658
|
+
* without attempting the share, so a UI can state it on the template itself.
|
|
659
|
+
*
|
|
660
|
+
* @param id template to share
|
|
661
|
+
* @param options recipients XOR everyone, plus the title recipients see
|
|
662
|
+
*/
|
|
663
|
+
async shareTemplate(id, options) {
|
|
664
|
+
const loaded = await this.loadShareableTemplate(id);
|
|
665
|
+
if (!loaded.ok) return {
|
|
666
|
+
ok: false,
|
|
667
|
+
problems: loaded.problems
|
|
668
|
+
};
|
|
669
|
+
const everyone = "everyone" in options;
|
|
670
|
+
const sender = this.currentUserLogin ?? "";
|
|
671
|
+
const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;
|
|
672
|
+
let shareId;
|
|
673
|
+
await this.pl.withWriteTx("MLShareTemplate", async (tx) => {
|
|
674
|
+
const { envelope, data } = require_sharing.buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, loaded.template, {
|
|
675
|
+
sender,
|
|
676
|
+
title: options.title,
|
|
677
|
+
expiresAt
|
|
678
|
+
});
|
|
679
|
+
shareId = data.shareId;
|
|
680
|
+
await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, { writable: false });
|
|
681
|
+
await tx.commit();
|
|
682
|
+
});
|
|
683
|
+
await this.sharingOutboxTree.refreshState();
|
|
684
|
+
return {
|
|
685
|
+
ok: true,
|
|
686
|
+
shareId
|
|
687
|
+
};
|
|
688
|
+
}
|
|
689
|
+
/**
|
|
690
|
+
* Every entry of a stored template that stands in the way of sharing it, empty for a template
|
|
691
|
+
* that can be shared. Reads the template and nothing else, so a UI can state the refusal on the
|
|
692
|
+
* template itself instead of only when the user tries to share it.
|
|
693
|
+
*/
|
|
694
|
+
async checkTemplateShareable(id) {
|
|
695
|
+
return require_template_share.unshareableTemplateEntries((await this.getTemplateData(id)).document);
|
|
696
|
+
}
|
|
697
|
+
/** The document and the label of a template that may be shared, or every entry that stops it.
|
|
698
|
+
* The label is what the recipient's own list will show, so it travels with the document. */
|
|
699
|
+
async loadShareableTemplate(id) {
|
|
700
|
+
const rid = await this.resolveTemplateId(id);
|
|
701
|
+
const template = await this.pl.withReadTx("MLReadTemplateForShare", async (tx) => {
|
|
702
|
+
const rd = await tx.getResourceData(rid, false);
|
|
703
|
+
if (rd.data === void 0) throw new Error(`Template ${id} carries no document.`);
|
|
704
|
+
return {
|
|
705
|
+
document: require_template_list.decodeStoredTemplateData(rd.data).document,
|
|
706
|
+
label: await tx.getKValueJson(rid, require_template_list.TemplateLabelKey)
|
|
707
|
+
};
|
|
708
|
+
});
|
|
709
|
+
const problems = require_template_share.unshareableTemplateEntries(template.document);
|
|
710
|
+
if (problems.length > 0) return {
|
|
711
|
+
ok: false,
|
|
712
|
+
problems
|
|
713
|
+
};
|
|
714
|
+
return {
|
|
715
|
+
ok: true,
|
|
716
|
+
template
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
478
720
|
* Changes a share in place (same {@link ShareId}), in one write transaction: re-snapshots live
|
|
479
721
|
* source projects and carries deleted ones' snapshots forward; applies edited recipients/title;
|
|
480
722
|
* transfers already-decided recipients' accept/reject records (they keep their copy and aren't
|
|
@@ -489,14 +731,37 @@ var MiddleLayer = class MiddleLayer {
|
|
|
489
731
|
* existing snapshot (and its timestamp), `remove` drops the project from the pack. A project not
|
|
490
732
|
* in the map defaults to `keep`. Omit the whole map for the legacy auto behavior (live sources
|
|
491
733
|
* updated, gone ones kept) — the everyone-refresh path relies on that.
|
|
734
|
+
*
|
|
735
|
+
* `opts.templateId` is required for, and only used by, a share that carries a template: a stored
|
|
736
|
+
* template is immutable, so an improved one is a different template and the share cannot re-read
|
|
737
|
+
* the one it started from — the caller names the new target. Every other option means the same
|
|
738
|
+
* thing for both kinds of share. Sharing the named template must be permitted (see
|
|
739
|
+
* {@link checkTemplateShareable}) or this throws.
|
|
492
740
|
*/
|
|
493
741
|
async changeShare(shareId, opts = {}) {
|
|
742
|
+
const target = opts.templateId === void 0 ? void 0 : await this.loadShareableTemplate(opts.templateId);
|
|
743
|
+
if (target !== void 0 && !target.ok) throw new Error(`changeShare: template ${opts.templateId} cannot be shared: ${describeShareProblems(target.problems)}`);
|
|
494
744
|
await this.pl.withWriteTx("MLChangeShare", async (tx) => {
|
|
495
745
|
const old = await this.resolveOutboxEnvelope(tx, shareId);
|
|
496
746
|
if (old === void 0) throw new Error(`changeShare: no live share with id ${shareId} in the outbox.`);
|
|
497
747
|
const self = this.currentUserLogin ?? "";
|
|
498
748
|
const grants = await tx.listGrants(old.rid);
|
|
499
749
|
const everyone = grants.some((g) => (0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user)) || opts.everyone === true;
|
|
750
|
+
const priorRecipients = grants.filter((g) => !(0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user) && g.user !== self).map((g) => g.user);
|
|
751
|
+
if (old.data.payload.kind === "template") {
|
|
752
|
+
if (target === void 0) throw new Error(`changeShare: share ${shareId} carries a template, so it needs an explicit target template — a stored template never changes, so an improved one is a different template.`);
|
|
753
|
+
const recipients = everyone ? [] : opts.recipients ?? priorRecipients;
|
|
754
|
+
tx.removeField((0, _milaboratories_pl_client.field)(this.sharingOutboxResourceId, old.fieldName));
|
|
755
|
+
const { envelope } = require_sharing.buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, target.template, {
|
|
756
|
+
sender: self,
|
|
757
|
+
title: opts.title === void 0 ? old.data.title : opts.title.trim(),
|
|
758
|
+
expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs,
|
|
759
|
+
shareId
|
|
760
|
+
});
|
|
761
|
+
await this.grantShareEnvelope(tx, envelope, everyone, recipients, { writable: false });
|
|
762
|
+
await tx.commit();
|
|
763
|
+
return;
|
|
764
|
+
}
|
|
500
765
|
const oldRd = await tx.getResourceData(old.rid, true);
|
|
501
766
|
const snapshotByUuid = /* @__PURE__ */ new Map();
|
|
502
767
|
const acceptances = [];
|
|
@@ -513,7 +778,6 @@ var MiddleLayer = class MiddleLayer {
|
|
|
513
778
|
}
|
|
514
779
|
}
|
|
515
780
|
const decidedLogins = acceptances.map((a) => a.login);
|
|
516
|
-
const priorRecipients = grants.filter((g) => !(0, _milaboratories_pl_client.isEveryoneUserLogin)(g.user) && g.user !== self).map((g) => g.user);
|
|
517
781
|
const recipients = everyone ? [] : Array.from(/* @__PURE__ */ new Set([...opts.recipients ?? priorRecipients, ...decidedLogins]));
|
|
518
782
|
const liveProjects = /* @__PURE__ */ new Map();
|
|
519
783
|
const projList = await tx.getResourceData(this.projectListResourceId, true);
|
|
@@ -523,8 +787,9 @@ var MiddleLayer = class MiddleLayer {
|
|
|
523
787
|
}
|
|
524
788
|
const actions = opts.projectActions;
|
|
525
789
|
const sources = [];
|
|
526
|
-
|
|
527
|
-
|
|
790
|
+
const oldProjects = require_sharing_model.envelopeProjectMap(old.data);
|
|
791
|
+
for (const uuid of Object.keys(oldProjects)) {
|
|
792
|
+
const { label, source, updatedAt } = oldProjects[uuid];
|
|
528
793
|
const liveRid = liveProjects.get(source);
|
|
529
794
|
const action = actions ? actions[source] ?? "keep" : liveRid !== void 0 ? "update" : "keep";
|
|
530
795
|
if (action === "remove") continue;
|
|
@@ -558,9 +823,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
558
823
|
if (!everyone && !recipients.includes(login)) continue;
|
|
559
824
|
require_sharing.writeEnvelopeAcceptance(tx, envelope, login, acc.action, acc.timestamp);
|
|
560
825
|
}
|
|
561
|
-
|
|
562
|
-
if (everyone) tx.grantAccess(gid, "", { writable: true }, _milaboratories_pl_client.GrantType.ANY_AUTHORISED);
|
|
563
|
-
else for (const r of recipients) tx.grantAccess(gid, r, { writable: true });
|
|
826
|
+
await this.grantShareEnvelope(tx, envelope, everyone, recipients, { writable: true });
|
|
564
827
|
await tx.commit();
|
|
565
828
|
});
|
|
566
829
|
await this.sharingOutboxTree.refreshState();
|
|
@@ -581,7 +844,8 @@ var MiddleLayer = class MiddleLayer {
|
|
|
581
844
|
const rd = await tx.getResourceData(f.value, false);
|
|
582
845
|
if (rd.data === void 0) continue;
|
|
583
846
|
const data = require_sharing_model.decodeEnvelopeData(rd.data);
|
|
584
|
-
if (
|
|
847
|
+
if (data === void 0) continue;
|
|
848
|
+
if (Object.values(require_sharing_model.envelopeProjectMap(data)).some((p) => wanted.has(p.source))) out.push({
|
|
585
849
|
fieldName: f.name,
|
|
586
850
|
rid: f.value,
|
|
587
851
|
shareId: data.shareId
|
|
@@ -627,6 +891,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
627
891
|
const rd = await tx.getResourceData(f.value, false);
|
|
628
892
|
if (rd.data === void 0) continue;
|
|
629
893
|
const data = require_sharing_model.decodeEnvelopeData(rd.data);
|
|
894
|
+
if (data === void 0) continue;
|
|
630
895
|
if (data.shareId === shareId) return {
|
|
631
896
|
fieldName: f.name,
|
|
632
897
|
rid: f.value,
|
|
@@ -652,18 +917,23 @@ var MiddleLayer = class MiddleLayer {
|
|
|
652
917
|
return map;
|
|
653
918
|
}
|
|
654
919
|
/**
|
|
655
|
-
* Accepts one or more pending shares
|
|
656
|
-
*
|
|
657
|
-
*
|
|
658
|
-
*
|
|
920
|
+
* Accepts one or more pending shares. What accepting does depends on what the share carries: a
|
|
921
|
+
* pack of projects is duplicated into this user's project list, while a template is added to this
|
|
922
|
+
* user's own template list and builds nothing — the recipient decides later whether to apply it.
|
|
923
|
+
* Either way the decision is recorded per share, and a read-write share also gets the
|
|
924
|
+
* donor-visible acceptance written onto its envelope. Per-share failures (e.g. an expiry race)
|
|
925
|
+
* are collected, not short-circuited — the rest still get accepted. Accept-all = pass every
|
|
926
|
+
* current pending shareId.
|
|
659
927
|
*
|
|
660
928
|
* `rename` resolves label collisions (same callback contract as {@link duplicateProject}), but
|
|
661
|
-
* the source lives in the envelope tree, so accept calls the low-level mutator directly.
|
|
929
|
+
* the source lives in the envelope tree, so accept calls the low-level mutator directly. It does
|
|
930
|
+
* not apply to a template share, whose label is not required to be unique.
|
|
662
931
|
*/
|
|
663
932
|
async acceptShare(shareIds, rename) {
|
|
664
933
|
const live = await this.resolveLiveEnvelopes();
|
|
665
934
|
const login = this.currentUserLogin;
|
|
666
935
|
const accepted = [];
|
|
936
|
+
const acceptedTemplates = [];
|
|
667
937
|
const failed = [];
|
|
668
938
|
for (const shareId of shareIds) {
|
|
669
939
|
const envelope = live.get(shareId);
|
|
@@ -676,6 +946,28 @@ var MiddleLayer = class MiddleLayer {
|
|
|
676
946
|
}
|
|
677
947
|
try {
|
|
678
948
|
const now = Date.now();
|
|
949
|
+
const payload = envelope.data.payload;
|
|
950
|
+
if (payload.kind === "template") {
|
|
951
|
+
const rid = await this.pl.withWriteTx("MLAcceptTemplateShare", async (tx) => {
|
|
952
|
+
const tpl = require_template.createTemplate(tx, this.templateListResourceId, payload.label, {
|
|
953
|
+
schemaVersion: 1,
|
|
954
|
+
document: payload.document,
|
|
955
|
+
sender: payload.from
|
|
956
|
+
});
|
|
957
|
+
require_sharing.writeSharingDecision(tx, this.sharingStateResourceId, shareId, {
|
|
958
|
+
decision: "accepted",
|
|
959
|
+
timestamp: now,
|
|
960
|
+
envelopeSharedAt: envelope.data.sharedAt,
|
|
961
|
+
acceptedProjects: []
|
|
962
|
+
});
|
|
963
|
+
await tx.commit();
|
|
964
|
+
return await tpl.globalId;
|
|
965
|
+
});
|
|
966
|
+
const templateId = (0, _milaboratories_pl_client.resourceIdToString)(rid);
|
|
967
|
+
this.templateIdCache.set(templateId, rid);
|
|
968
|
+
acceptedTemplates.push(templateId);
|
|
969
|
+
continue;
|
|
970
|
+
}
|
|
679
971
|
const createdRids = await this.pl.withWriteTx("MLAcceptShare", async (tx) => {
|
|
680
972
|
const created = await require_sharing.copyEnvelopeProjectsIntoList(tx, envelope.rid, this.projectListResourceId, rename);
|
|
681
973
|
require_sharing.writeSharingDecision(tx, this.sharingStateResourceId, shareId, {
|
|
@@ -700,9 +992,14 @@ var MiddleLayer = class MiddleLayer {
|
|
|
700
992
|
});
|
|
701
993
|
}
|
|
702
994
|
}
|
|
703
|
-
await Promise.all([
|
|
995
|
+
await Promise.all([
|
|
996
|
+
this.projectListTree.refreshState(),
|
|
997
|
+
this.templateListTree.refreshState(),
|
|
998
|
+
this.sharingStateTree.refreshState()
|
|
999
|
+
]);
|
|
704
1000
|
return {
|
|
705
1001
|
accepted,
|
|
1002
|
+
acceptedTemplates,
|
|
706
1003
|
failed
|
|
707
1004
|
};
|
|
708
1005
|
}
|
|
@@ -746,6 +1043,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
746
1043
|
const rd = await tx.getResourceData(f.value, false);
|
|
747
1044
|
if (rd.data === void 0) continue;
|
|
748
1045
|
const envData = require_sharing_model.decodeEnvelopeData(rd.data);
|
|
1046
|
+
if (envData === void 0) continue;
|
|
749
1047
|
if (envData.expiresAt === null) continue;
|
|
750
1048
|
if (envData.expiresAt <= now) toDelete.push({ fieldName: f.name });
|
|
751
1049
|
}
|
|
@@ -830,6 +1128,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
830
1128
|
await Promise.all([...this.openedProjects.values()].map((prj) => prj.destroy()));
|
|
831
1129
|
await Promise.all([
|
|
832
1130
|
this.projectListTree.terminate(),
|
|
1131
|
+
this.templateListTree.terminate(),
|
|
833
1132
|
this.sharingOutboxTree.terminate(),
|
|
834
1133
|
this.sharingStateTree.terminate(),
|
|
835
1134
|
this.pendingSharesTree.terminate()
|
|
@@ -861,7 +1160,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
861
1160
|
ops.defaultTreeOptions.logStat = require_index$1.getDebugFlags().logTreeStats;
|
|
862
1161
|
ops.debugOps.dumpInitialTreeState = require_index$1.getDebugFlags().dumpInitialTreeState;
|
|
863
1162
|
if (ops.defaultTreeOptions.traversalMode === void 0 && require_index$1.getDebugFlags().treeTraversalMode !== void 0) ops.defaultTreeOptions.traversalMode = require_index$1.getDebugFlags().treeTraversalMode;
|
|
864
|
-
const { projects, sharingOutbox, sharingState } = await pl.withWriteTx("MLInitialization", async (tx) => {
|
|
1163
|
+
const { projects, templates, sharingOutbox, sharingState } = await pl.withWriteTx("MLInitialization", async (tx) => {
|
|
865
1164
|
const lazyInit = async (fieldName, type) => {
|
|
866
1165
|
const f = (0, _milaboratories_pl_client.field)(tx.clientRoot, fieldName);
|
|
867
1166
|
tx.createField(f, "Dynamic");
|
|
@@ -875,11 +1174,13 @@ var MiddleLayer = class MiddleLayer {
|
|
|
875
1174
|
return { existing: fData.value };
|
|
876
1175
|
};
|
|
877
1176
|
const projectsR = await lazyInit(require_project_list.ProjectsField, require_project_list.ProjectsResourceType);
|
|
1177
|
+
const templatesR = await lazyInit(require_template_list.TemplatesField, require_template_list.TemplatesResourceType);
|
|
878
1178
|
const outboxR = await lazyInit(require_sharing_model.SharingOutboxField, require_sharing_model.SharingOutboxResourceType);
|
|
879
1179
|
const stateR = await lazyInit(require_sharing_model.SharingStateField, require_sharing_model.SharingStateResourceType);
|
|
880
1180
|
await tx.commit();
|
|
881
1181
|
return {
|
|
882
1182
|
projects: projectsR.existing ?? await projectsR.ref.globalId,
|
|
1183
|
+
templates: templatesR.existing ?? await templatesR.ref.globalId,
|
|
883
1184
|
sharingState: stateR.existing ?? await stateR.ref.globalId,
|
|
884
1185
|
sharingOutbox: outboxR.existing ?? await outboxR.ref.globalId
|
|
885
1186
|
};
|
|
@@ -934,14 +1235,20 @@ var MiddleLayer = class MiddleLayer {
|
|
|
934
1235
|
};
|
|
935
1236
|
const openedProjects = new _milaboratories_computable.WatchableValue([]);
|
|
936
1237
|
const projectListTC = await require_project_list.createProjectList(pl, projects, openedProjects, env);
|
|
1238
|
+
const templateListTC = await require_template_list.createTemplateList(pl, templates, env);
|
|
937
1239
|
const outgoingTC = await require_sharing_list.createOutgoingShares(pl, sharingOutbox, env);
|
|
938
1240
|
const sharingStateTree = await require_sharing_list.createSharingStateTree(pl, sharingState, env);
|
|
939
1241
|
const pendingSharesTree = await require_sharing_list.createPendingSharesTree(pl, env);
|
|
940
1242
|
const pendingShares = require_sharing_list.createPendingSharesComputable(pendingSharesTree, sharingStateTree, pl.userResources.authUser);
|
|
941
1243
|
const liveEnvelopes = require_sharing_list.createLiveEnvelopesComputable(pendingSharesTree);
|
|
942
|
-
return new MiddleLayer(env, driverKit, driverKit.signer, projects, sharingOutbox, sharingState, openedProjects, projectListTC.tree, outgoingTC.tree, sharingStateTree, pendingSharesTree, v2RegistryProvider, projectListTC.computable, outgoingTC.computable, pendingShares, liveEnvelopes);
|
|
1244
|
+
return new MiddleLayer(env, driverKit, driverKit.signer, projects, templates, sharingOutbox, sharingState, openedProjects, projectListTC.tree, templateListTC.tree, outgoingTC.tree, sharingStateTree, pendingSharesTree, v2RegistryProvider, projectListTC.computable, templateListTC.computable, outgoingTC.computable, pendingShares, liveEnvelopes);
|
|
943
1245
|
}
|
|
944
1246
|
};
|
|
1247
|
+
/** Refusal reasons as one line, each naming the entry it belongs to, so a throw that escapes to a
|
|
1248
|
+
* log still says which block stands in the way. */
|
|
1249
|
+
function describeShareProblems(problems) {
|
|
1250
|
+
return problems.map((p) => `${p.entryId}: ${p.error}`).join("; ");
|
|
1251
|
+
}
|
|
945
1252
|
//#endregion
|
|
946
1253
|
exports.MiddleLayer = MiddleLayer;
|
|
947
1254
|
|