@milaboratories/pl-middle-layer 1.71.16 → 1.72.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 -5
- package/dist/index.d.ts +5 -4
- package/dist/index.js +3 -2
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/folders.cjs +631 -0
- package/dist/middle_layer/folders.cjs.map +1 -0
- package/dist/middle_layer/folders.d.ts +39 -0
- package/dist/middle_layer/folders.d.ts.map +1 -0
- package/dist/middle_layer/folders.js +613 -0
- package/dist/middle_layer/folders.js.map +1 -0
- package/dist/middle_layer/index.cjs +3 -0
- package/dist/middle_layer/index.d.ts +4 -3
- package/dist/middle_layer/index.js +2 -1
- package/dist/middle_layer/middle_layer.cjs +585 -370
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +215 -122
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +593 -378
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project_list.cjs +36 -19
- package/dist/middle_layer/project_list.cjs.map +1 -1
- package/dist/middle_layer/project_list.d.ts +1 -1
- package/dist/middle_layer/project_list.d.ts.map +1 -1
- package/dist/middle_layer/project_list.js +37 -21
- package/dist/middle_layer/project_list.js.map +1 -1
- package/dist/middle_layer/sharing_list.cjs +123 -93
- package/dist/middle_layer/sharing_list.cjs.map +1 -1
- package/dist/middle_layer/sharing_list.d.ts +43 -19
- package/dist/middle_layer/sharing_list.d.ts.map +1 -1
- package/dist/middle_layer/sharing_list.js +123 -93
- package/dist/middle_layer/sharing_list.js.map +1 -1
- package/dist/middle_layer/template_list.cjs +45 -20
- package/dist/middle_layer/template_list.cjs.map +1 -1
- package/dist/middle_layer/template_list.d.ts +7 -15
- package/dist/middle_layer/template_list.d.ts.map +1 -1
- package/dist/middle_layer/template_list.js +44 -21
- package/dist/middle_layer/template_list.js.map +1 -1
- package/dist/model/index.cjs +7 -5
- package/dist/model/index.d.ts +2 -2
- package/dist/model/index.js +2 -2
- package/dist/model/sharing_model.cjs +46 -20
- package/dist/model/sharing_model.cjs.map +1 -1
- package/dist/model/sharing_model.d.ts +125 -55
- package/dist/model/sharing_model.d.ts.map +1 -1
- package/dist/model/sharing_model.js +40 -16
- package/dist/model/sharing_model.js.map +1 -1
- package/dist/mutator/list.cjs +30 -0
- package/dist/mutator/list.cjs.map +1 -0
- package/dist/mutator/list.js +29 -0
- package/dist/mutator/list.js.map +1 -0
- package/dist/mutator/project.cjs +14 -1
- package/dist/mutator/project.cjs.map +1 -1
- package/dist/mutator/project.d.ts.map +1 -1
- package/dist/mutator/project.js +15 -2
- package/dist/mutator/project.js.map +1 -1
- package/dist/mutator/sharing.cjs +130 -76
- package/dist/mutator/sharing.cjs.map +1 -1
- package/dist/mutator/sharing.js +130 -76
- package/dist/mutator/sharing.js.map +1 -1
- package/dist/mutator/template.cjs +24 -17
- package/dist/mutator/template.cjs.map +1 -1
- package/dist/mutator/template.js +26 -20
- package/dist/mutator/template.js.map +1 -1
- package/package.json +19 -19
- package/src/middle_layer/folders.test.ts +1068 -0
- package/src/middle_layer/folders.ts +1059 -0
- package/src/middle_layer/folders_read.test.ts +359 -0
- package/src/middle_layer/index.ts +3 -2
- package/src/middle_layer/middle_layer.ts +813 -547
- package/src/middle_layer/project_list.test.ts +183 -0
- package/src/middle_layer/project_list.ts +58 -21
- package/src/middle_layer/sharing.test.ts +183 -0
- package/src/middle_layer/sharing_list.ts +208 -152
- package/src/middle_layer/template_list.test.ts +148 -0
- package/src/middle_layer/template_list.ts +72 -32
- package/src/middle_layer/templates.test.ts +76 -45
- package/src/model/sharing_model.test.ts +69 -2
- package/src/model/sharing_model.ts +161 -73
- package/src/mutator/list.ts +36 -0
- package/src/mutator/project-v3.test.ts +3 -1
- package/src/mutator/project.ts +13 -2
- package/src/mutator/sharing.ts +193 -120
- package/src/mutator/template.ts +37 -29
- package/src/test/with_ml.ts +73 -16
|
@@ -2,9 +2,11 @@ import { V2RegistryProvider } from "../block_registry/registry-v2-provider.js";
|
|
|
2
2
|
import "../block_registry/index.js";
|
|
3
3
|
import { ProjectMetaKey } from "../model/project_model.js";
|
|
4
4
|
import { ProjectsField, ProjectsResourceType, createProjectList, ensureProjectListRid } from "./project_list.js";
|
|
5
|
-
import { TemplateLabelKey, TemplatesField, TemplatesResourceType, createTemplateList, decodeStoredTemplateData } from "./template_list.js";
|
|
6
|
-
import {
|
|
7
|
-
import {
|
|
5
|
+
import { TemplateDescriptionKey, TemplateLabelKey, TemplatesField, TemplatesResourceType, createTemplateList, decodeStoredTemplateData } from "./template_list.js";
|
|
6
|
+
import { listedById, notListedError } from "../mutator/list.js";
|
|
7
|
+
import { createTemplate, deleteTemplate, renameTemplate, setTemplateDescription } from "../mutator/template.js";
|
|
8
|
+
import { FoldersField, FoldersResourceType, createFolder, createFolderList, deleteFolder, foldersLocalSubtree, moveFolderItems, openFoldersTx, previewFolderDeletion, previewFoldersMove, renameFolder, resetFolders, setFolderDescription } from "./folders.js";
|
|
9
|
+
import { SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, canGrantToEveryone, canImpersonate, decodeEnvelopeData, envelopeFolderRoot, newEnvelopeFolderId } from "../model/sharing_model.js";
|
|
8
10
|
import { resolveTemplateEntries } from "../model/template_resolve.js";
|
|
9
11
|
import { TemplateEntryRejected, kindMismatch } from "../model/template_apply.js";
|
|
10
12
|
import { BlockPackPreparer } from "../mutator/block-pack/block_pack.js";
|
|
@@ -13,8 +15,8 @@ import { createProject, duplicateProject, withProject, withProjectAuthored } fro
|
|
|
13
15
|
import { applyTemplateEntries } from "../mutator/template_construct.js";
|
|
14
16
|
import { cacheBlockPackTemplate } from "../mutator/template/template_cache.js";
|
|
15
17
|
import { Project, throwIfMissingServerCapabilities } from "./project.js";
|
|
16
|
-
import { buildShareEnvelope, buildTemplateShareEnvelope,
|
|
17
|
-
import {
|
|
18
|
+
import { buildFolderShareEnvelope, buildShareEnvelope, buildTemplateShareEnvelope, clearShareHidden, copyEnvelopeProjectsIntoList, writeShareHidden } from "../mutator/sharing.js";
|
|
19
|
+
import { createAvailableSharesComputable, createAvailableSharesTree, createLiveEnvelopesComputable, createOutgoingShares, createSharingStateTree } from "./sharing_list.js";
|
|
18
20
|
import { DefaultMiddleLayerOpsPaths, DefaultMiddleLayerOpsSettings } from "./ops.js";
|
|
19
21
|
import { BlockUpdateWatcher } from "../block_registry/watcher.js";
|
|
20
22
|
import { initDriverKit } from "./driver_kit.js";
|
|
@@ -22,10 +24,11 @@ import { createModelServiceRegistry } from "../service_factories.js";
|
|
|
22
24
|
import { ProjectHelper } from "../model/project_helper.js";
|
|
23
25
|
import { TreeSnapshotStore } from "./tree_snapshot_store.js";
|
|
24
26
|
import { RuntimeCapabilities, ensureError, extractConfig } from "@platforma-sdk/model";
|
|
27
|
+
import { foldersNameTaken, foldersUniqueName, inheritedFolder, normalizeDescription } from "@milaboratories/pl-model-middle-layer";
|
|
25
28
|
import { RetryAgent } from "undici";
|
|
26
|
-
import { BlockEventDispatcher, HmacSha256Signer
|
|
27
|
-
import { REQUIRES_PFRAMES_VERSION, registerServiceCapabilities } from "@milaboratories/pl-model-common";
|
|
28
|
-
import { GrantType, field,
|
|
29
|
+
import { BlockEventDispatcher, HmacSha256Signer } from "@milaboratories/ts-helpers";
|
|
30
|
+
import { REQUIRES_PFRAMES_VERSION, asProjectId, asTemplateId, registerServiceCapabilities } from "@milaboratories/pl-model-common";
|
|
31
|
+
import { GrantType, field, isNullSignedResourceId, resourceIdToString } from "@milaboratories/pl-client";
|
|
29
32
|
import { LRUCache } from "lru-cache";
|
|
30
33
|
import { WatchableValue } from "@milaboratories/computable";
|
|
31
34
|
import { randomUUID } from "node:crypto";
|
|
@@ -55,20 +58,23 @@ var MiddleLayer = class MiddleLayer {
|
|
|
55
58
|
templateListResourceId;
|
|
56
59
|
sharingOutboxResourceId;
|
|
57
60
|
sharingStateResourceId;
|
|
61
|
+
foldersResourceId;
|
|
58
62
|
openedProjectsList;
|
|
59
63
|
projectListTree;
|
|
60
64
|
templateListTree;
|
|
65
|
+
foldersTree;
|
|
61
66
|
sharingOutboxTree;
|
|
62
67
|
sharingStateTree;
|
|
63
|
-
|
|
68
|
+
availableSharesTree;
|
|
64
69
|
blockRegistryProvider;
|
|
65
70
|
projectList;
|
|
66
71
|
templateList;
|
|
72
|
+
folders;
|
|
67
73
|
outgoingShares;
|
|
68
|
-
|
|
74
|
+
availableShares;
|
|
69
75
|
liveEnvelopes;
|
|
70
76
|
pl;
|
|
71
|
-
constructor(env, driverKit, signer, projectListResourceId, templateListResourceId, sharingOutboxResourceId, sharingStateResourceId, openedProjectsList, projectListTree, templateListTree, sharingOutboxTree, sharingStateTree,
|
|
77
|
+
constructor(env, driverKit, signer, projectListResourceId, templateListResourceId, sharingOutboxResourceId, sharingStateResourceId, foldersResourceId, openedProjectsList, projectListTree, templateListTree, foldersTree, sharingOutboxTree, sharingStateTree, availableSharesTree, blockRegistryProvider, projectList, templateList, folders, outgoingShares, availableShares, liveEnvelopes) {
|
|
72
78
|
this.env = env;
|
|
73
79
|
this.driverKit = driverKit;
|
|
74
80
|
this.signer = signer;
|
|
@@ -76,17 +82,20 @@ var MiddleLayer = class MiddleLayer {
|
|
|
76
82
|
this.templateListResourceId = templateListResourceId;
|
|
77
83
|
this.sharingOutboxResourceId = sharingOutboxResourceId;
|
|
78
84
|
this.sharingStateResourceId = sharingStateResourceId;
|
|
85
|
+
this.foldersResourceId = foldersResourceId;
|
|
79
86
|
this.openedProjectsList = openedProjectsList;
|
|
80
87
|
this.projectListTree = projectListTree;
|
|
81
88
|
this.templateListTree = templateListTree;
|
|
89
|
+
this.foldersTree = foldersTree;
|
|
82
90
|
this.sharingOutboxTree = sharingOutboxTree;
|
|
83
91
|
this.sharingStateTree = sharingStateTree;
|
|
84
|
-
this.
|
|
92
|
+
this.availableSharesTree = availableSharesTree;
|
|
85
93
|
this.blockRegistryProvider = blockRegistryProvider;
|
|
86
94
|
this.projectList = projectList;
|
|
87
95
|
this.templateList = templateList;
|
|
96
|
+
this.folders = folders;
|
|
88
97
|
this.outgoingShares = outgoingShares;
|
|
89
|
-
this.
|
|
98
|
+
this.availableShares = availableShares;
|
|
90
99
|
this.liveEnvelopes = liveEnvelopes;
|
|
91
100
|
this.pl = this.env.pl;
|
|
92
101
|
this.startEnvelopeCleanup();
|
|
@@ -117,7 +126,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
117
126
|
/**
|
|
118
127
|
* Whether the connected backend supports project sharing. Synthetic — computed
|
|
119
128
|
* in the middle layer from the backend capabilities the share flow needs (the
|
|
120
|
-
* cross-color field-reference relaxation
|
|
129
|
+
* cross-color field-reference relaxation a copy out of a share rests on). It can absorb
|
|
121
130
|
* additional required capabilities later without a UI change.
|
|
122
131
|
*/
|
|
123
132
|
get sharingSupported() {
|
|
@@ -176,38 +185,258 @@ var MiddleLayer = class MiddleLayer {
|
|
|
176
185
|
const cached = this.projectIdCache.get(projectId);
|
|
177
186
|
if (cached !== void 0) return cached;
|
|
178
187
|
const rid = await this.pl.withReadTx("ResolveProjectId", async (tx) => {
|
|
179
|
-
const
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
if (resourceIdToString(f.value) === projectId) return f.value;
|
|
183
|
-
}
|
|
184
|
-
throw new Error(`Project ${projectId} not found in project list.`);
|
|
188
|
+
const entry = (await listedById(tx, this.projectListResourceId)).get(projectId);
|
|
189
|
+
if (entry === void 0) throw notListedError("Project", projectId);
|
|
190
|
+
return entry.rid;
|
|
185
191
|
});
|
|
186
192
|
this.projectIdCache.set(projectId, rid);
|
|
187
193
|
return rid;
|
|
188
194
|
}
|
|
189
|
-
/**
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
195
|
+
/**
|
|
196
|
+
* Creates a project with initial state and adds it to project list.
|
|
197
|
+
*
|
|
198
|
+
* `folder` is where it lands, placed in the transaction that creates it so the project never
|
|
199
|
+
* shows up at the top level first. Left out, or naming a folder that is gone, it lands at the
|
|
200
|
+
* top level.
|
|
201
|
+
*/
|
|
202
|
+
async createProject(meta, folder) {
|
|
203
|
+
const signedRid = await this.pl.withWriteTx("MLCreateProject", async (tx) => {
|
|
204
|
+
const tree = folder === void 0 ? void 0 : await openFoldersTx(tx, this.foldersRids);
|
|
205
|
+
const prj = await createProject(tx, meta);
|
|
194
206
|
tx.createField(field(this.projectListResourceId, randomUUID()), "Dynamic", prj);
|
|
207
|
+
const rid = await prj.globalId;
|
|
208
|
+
if (tree !== void 0 && tree.view.folders.some((candidate) => candidate.id === folder)) tree.place([{
|
|
209
|
+
kind: "project",
|
|
210
|
+
id: asProjectId(resourceIdToString(rid))
|
|
211
|
+
}], folder);
|
|
195
212
|
await tx.commit();
|
|
213
|
+
return rid;
|
|
196
214
|
});
|
|
197
|
-
await this.projectListTree.refreshState();
|
|
198
|
-
const
|
|
199
|
-
const projectId = resourceIdToString(signedRid);
|
|
215
|
+
await Promise.all([this.projectListTree.refreshState(), ...folder === void 0 ? [] : [this.foldersTree.refreshState()]]);
|
|
216
|
+
const projectId = asProjectId(resourceIdToString(signedRid));
|
|
200
217
|
this.projectIdCache.set(projectId, signedRid);
|
|
201
218
|
return projectId;
|
|
202
219
|
}
|
|
203
|
-
/**
|
|
220
|
+
/**
|
|
221
|
+
* Updates the project metadata fields the caller names, leaving the others as they are.
|
|
222
|
+
*
|
|
223
|
+
* A patch rather than a replacement because the label and the description are edited from two
|
|
224
|
+
* different places: a rename that carried a stale description alongside the new name would
|
|
225
|
+
* undo a description edit that happened in between, and the reverse.
|
|
226
|
+
*
|
|
227
|
+
* The label is stored trimmed, and it goes into the namespace folders share, so a label
|
|
228
|
+
* already carried by something else inside the same folder is refused rather than written — a
|
|
229
|
+
* human typed it, and a name that silently becomes a different name is worse than one that is
|
|
230
|
+
* turned down. The check and the write share one transaction, so two renames racing for the
|
|
231
|
+
* same name cannot both win. Duplicates an account already holds are tolerated and never
|
|
232
|
+
* rewritten; see the name check of {@link openFoldersTx}.
|
|
233
|
+
*/
|
|
204
234
|
async setProjectMeta(id, meta, author) {
|
|
205
235
|
const rid = await this.resolveProjectId(id);
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
236
|
+
const label = meta.label?.trim();
|
|
237
|
+
const patch = label === void 0 ? meta : {
|
|
238
|
+
...meta,
|
|
239
|
+
label
|
|
240
|
+
};
|
|
241
|
+
await this.pl.withWriteTx("ProjectAction: setProjectMeta", async (tx) => {
|
|
242
|
+
if (label !== void 0) (await openFoldersTx(tx, this.foldersRids)).assertNameFree({
|
|
243
|
+
kind: "project",
|
|
244
|
+
id
|
|
245
|
+
}, label);
|
|
246
|
+
await withProjectAuthored(this.env.projectHelper, tx, rid, author, (prj) => {
|
|
247
|
+
prj.updateMeta(patch);
|
|
248
|
+
});
|
|
249
|
+
await tx.commit();
|
|
250
|
+
});
|
|
209
251
|
await this.projectListTree.refreshState();
|
|
210
252
|
}
|
|
253
|
+
get foldersRids() {
|
|
254
|
+
return {
|
|
255
|
+
folders: this.foldersResourceId,
|
|
256
|
+
projects: this.projectListResourceId,
|
|
257
|
+
templates: this.templateListResourceId
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
/** Creates a folder inside `parent`, or at the top level, and returns its id. A name already
|
|
261
|
+
* used there is rejected. */
|
|
262
|
+
async createFolder(name, parent) {
|
|
263
|
+
const id = await createFolder(this.pl, this.foldersRids, name, parent);
|
|
264
|
+
await this.foldersTree.refreshState();
|
|
265
|
+
return id;
|
|
266
|
+
}
|
|
267
|
+
/** Renames a folder. A name already used beside it is rejected. */
|
|
268
|
+
async renameFolder(folder, name) {
|
|
269
|
+
await renameFolder(this.pl, this.foldersRids, folder, name);
|
|
270
|
+
await this.foldersTree.refreshState();
|
|
271
|
+
}
|
|
272
|
+
/** Sets what a folder says about itself; blank clears it. Descriptions are in no namespace, so
|
|
273
|
+
* nothing is refused here. */
|
|
274
|
+
async setFolderDescription(folder, description) {
|
|
275
|
+
await setFolderDescription(this.pl, this.foldersRids, folder, description);
|
|
276
|
+
await this.foldersTree.refreshState();
|
|
277
|
+
}
|
|
278
|
+
/** The plan a move would produce — every item and the name it ends up with. Shown for
|
|
279
|
+
* confirmation, then handed back to {@link moveFolderItems} unchanged. */
|
|
280
|
+
async previewFoldersMove(items, destination) {
|
|
281
|
+
return await previewFoldersMove(this.pl, this.foldersRids, items, destination);
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Moves folders, projects and templates into one destination.
|
|
285
|
+
*
|
|
286
|
+
* The plan is recomputed inside the write transaction and the move commits only when it is
|
|
287
|
+
* identical to `confirmedPlan`; otherwise nothing is written and the fresh plan comes back to
|
|
288
|
+
* be confirmed again.
|
|
289
|
+
*/
|
|
290
|
+
async moveFolderItems(items, destination, confirmedPlan) {
|
|
291
|
+
const outcome = await moveFolderItems(this.pl, this.foldersRids, items, destination, confirmedPlan);
|
|
292
|
+
if (outcome.ok) await Promise.all([
|
|
293
|
+
this.foldersTree.refreshState(),
|
|
294
|
+
this.projectListTree.refreshState(),
|
|
295
|
+
this.templateListTree.refreshState()
|
|
296
|
+
]);
|
|
297
|
+
return outcome;
|
|
298
|
+
}
|
|
299
|
+
/** What deleting a folder would destroy: the subtree of folders, and everything in it. */
|
|
300
|
+
async previewFolderDeletion(folder) {
|
|
301
|
+
return await previewFolderDeletion(this.pl, this.foldersRids, folder);
|
|
302
|
+
}
|
|
303
|
+
/** Deletes a folder, every folder inside it, and every project and template held anywhere in
|
|
304
|
+
* that subtree.
|
|
305
|
+
* Refused as `needs-confirmation` until the removal it returns is passed back as
|
|
306
|
+
* `confirmedRemoval`, and as `plan-changed` if the subtree has changed since. */
|
|
307
|
+
async deleteFolder(folder, confirmedRemoval) {
|
|
308
|
+
const outcome = await deleteFolder(this.pl, this.foldersRids, folder, confirmedRemoval);
|
|
309
|
+
if (outcome.ok) {
|
|
310
|
+
for (const id of outcome.removal.projects) this.projectIdCache.delete(id);
|
|
311
|
+
for (const id of outcome.removal.templates) this.templateIdCache.delete(id);
|
|
312
|
+
await Promise.all([
|
|
313
|
+
this.foldersTree.refreshState(),
|
|
314
|
+
this.projectListTree.refreshState(),
|
|
315
|
+
this.templateListTree.refreshState()
|
|
316
|
+
]);
|
|
317
|
+
}
|
|
318
|
+
return outcome;
|
|
319
|
+
}
|
|
320
|
+
/**
|
|
321
|
+
* Replaces a folder document this build cannot read — one written by a newer version, or one
|
|
322
|
+
* nothing here can parse — with an empty one. Every folder is gone afterwards; every project and
|
|
323
|
+
* template stays and shows at the top level. Refused while the document can be read.
|
|
324
|
+
*/
|
|
325
|
+
async resetFolders() {
|
|
326
|
+
await resetFolders(this.pl, this.foldersRids);
|
|
327
|
+
await this.foldersTree.refreshState();
|
|
328
|
+
}
|
|
329
|
+
/**
|
|
330
|
+
* Duplicates a folder and everything under it: the folders inside, a duplicate of every project
|
|
331
|
+
* in them, and a copy of every template.
|
|
332
|
+
*
|
|
333
|
+
* The copy lands beside the source, so only its root needs a name of its own — `X (Copy)`.
|
|
334
|
+
* Nothing inside is renamed: names are compared among siblings, and a copied folder's children
|
|
335
|
+
* are only ever compared with each other, where they came in distinct already.
|
|
336
|
+
*
|
|
337
|
+
* The subtree is read first and rebuilt in one write transaction, so the folders and everything
|
|
338
|
+
* they hold appear together or not at all.
|
|
339
|
+
*/
|
|
340
|
+
async duplicateFolder(folder) {
|
|
341
|
+
const subtree = await this.loadFolderSubtree(folder, () => randomUUID());
|
|
342
|
+
const created = await this.pl.withWriteTx("MLDuplicateFolder", async (tx) => {
|
|
343
|
+
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
344
|
+
const items = [];
|
|
345
|
+
const projects = [];
|
|
346
|
+
const templates = [];
|
|
347
|
+
for (const project of subtree.projects) {
|
|
348
|
+
const meta = await tx.getKValueJson(project.rid, ProjectMetaKey);
|
|
349
|
+
const copy = await duplicateProject(tx, project.rid, meta, this.env.projectHelper);
|
|
350
|
+
tx.createField(field(this.projectListResourceId, randomUUID()), "Dynamic", copy);
|
|
351
|
+
const rid = await copy.globalId;
|
|
352
|
+
projects.push(rid);
|
|
353
|
+
items.push({
|
|
354
|
+
kind: "project",
|
|
355
|
+
id: asProjectId(resourceIdToString(rid)),
|
|
356
|
+
folder: project.folder
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
for (const template of subtree.templates) {
|
|
360
|
+
const rid = await createTemplate(tx, this.templateListResourceId, {
|
|
361
|
+
label: template.label,
|
|
362
|
+
description: template.description
|
|
363
|
+
}, template.data).globalId;
|
|
364
|
+
const id = asTemplateId(resourceIdToString(rid));
|
|
365
|
+
templates.push({
|
|
366
|
+
id,
|
|
367
|
+
rid
|
|
368
|
+
});
|
|
369
|
+
items.push({
|
|
370
|
+
kind: "template",
|
|
371
|
+
id,
|
|
372
|
+
folder: template.folder
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
tree.graft({
|
|
376
|
+
root: subtree.root,
|
|
377
|
+
folders: subtree.folders,
|
|
378
|
+
items
|
|
379
|
+
}, subtree.parent);
|
|
380
|
+
await tx.commit();
|
|
381
|
+
return {
|
|
382
|
+
projects,
|
|
383
|
+
templates
|
|
384
|
+
};
|
|
385
|
+
});
|
|
386
|
+
for (const rid of created.projects) this.projectIdCache.set(asProjectId(resourceIdToString(rid)), rid);
|
|
387
|
+
for (const { id, rid } of created.templates) this.templateIdCache.set(id, rid);
|
|
388
|
+
await Promise.all([
|
|
389
|
+
this.foldersTree.refreshState(),
|
|
390
|
+
this.projectListTree.refreshState(),
|
|
391
|
+
this.templateListTree.refreshState()
|
|
392
|
+
]);
|
|
393
|
+
}
|
|
394
|
+
/**
|
|
395
|
+
* A folder subtree as a copy needs it: the folders under ids local to this read, the key the
|
|
396
|
+
* subtree's root goes by, the resource of every project in them, every template read whole, and
|
|
397
|
+
* the folder the source sits in — which is where a duplicate lands.
|
|
398
|
+
*
|
|
399
|
+
* Read in a transaction of its own, before the write that copies it: every template is a read,
|
|
400
|
+
* and none of it has to be atomic with the copying, because a project or a template that
|
|
401
|
+
* disappears meanwhile fails that write on its own.
|
|
402
|
+
*/
|
|
403
|
+
async loadFolderSubtree(folder, mint) {
|
|
404
|
+
return await this.pl.withReadTx("MLReadFolderSubtree", async (tx) => {
|
|
405
|
+
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
406
|
+
const source = tree.view.folders.find((candidate) => candidate.id === folder);
|
|
407
|
+
if (source === void 0) throw new Error(`Folder ${folder} does not exist.`);
|
|
408
|
+
const { inSubtree, localId, folders } = foldersLocalSubtree(tree.view, folder, mint);
|
|
409
|
+
const projects = [];
|
|
410
|
+
for (const project of tree.view.projects) {
|
|
411
|
+
if (project.folder === void 0 || !inSubtree.has(project.folder)) continue;
|
|
412
|
+
const rid = tree.projectRids.get(project.id);
|
|
413
|
+
if (rid === void 0) throw notListedError("Project", project.id);
|
|
414
|
+
projects.push({
|
|
415
|
+
projectId: project.id,
|
|
416
|
+
rid,
|
|
417
|
+
folder: localId(project.folder)
|
|
418
|
+
});
|
|
419
|
+
}
|
|
420
|
+
const templates = [];
|
|
421
|
+
for (const template of tree.view.templates) {
|
|
422
|
+
if (template.folder === void 0 || !inSubtree.has(template.folder)) continue;
|
|
423
|
+
const rid = tree.templateRids.get(template.id);
|
|
424
|
+
if (rid === void 0) throw notListedError("Template", template.id);
|
|
425
|
+
const local = localId(template.folder);
|
|
426
|
+
templates.push(readStoredTemplate(tx, template.id, rid).then((stored) => ({
|
|
427
|
+
...stored,
|
|
428
|
+
folder: local
|
|
429
|
+
})));
|
|
430
|
+
}
|
|
431
|
+
return {
|
|
432
|
+
...source.parent === void 0 ? {} : { parent: source.parent },
|
|
433
|
+
root: localId(folder),
|
|
434
|
+
folders,
|
|
435
|
+
projects,
|
|
436
|
+
templates: await Promise.all(templates)
|
|
437
|
+
};
|
|
438
|
+
});
|
|
439
|
+
}
|
|
211
440
|
/**
|
|
212
441
|
* Renders a project as a `template-v1` YAML document, or reports every reason it
|
|
213
442
|
* cannot be — the backing call for an "Export Project as Template…" command.
|
|
@@ -380,41 +609,81 @@ var MiddleLayer = class MiddleLayer {
|
|
|
380
609
|
* A block that cannot be expressed as a template entry stores nothing at all, and every
|
|
381
610
|
* such block is reported — fixing an unexportable project takes one pass, not one per block.
|
|
382
611
|
*
|
|
612
|
+
* The template lands beside its project, and is named by the rule everything there is named
|
|
613
|
+
* by: a label the caller chose is stored trimmed and refused if it is already used there, and
|
|
614
|
+
* without one the project's own label is taken — suffixed, since the project itself already
|
|
615
|
+
* answers to it.
|
|
616
|
+
*
|
|
617
|
+
* The template's description is the one the caller gives, stored trimmed; blank means none.
|
|
618
|
+
* Without one the template is stored undescribed, whatever the project's own description says.
|
|
619
|
+
*
|
|
383
620
|
* @param projectId project to snapshot
|
|
384
|
-
* @param label label for the template; defaults to the project's own label
|
|
621
|
+
* @param label label for the template; defaults to the project's own label, made free
|
|
622
|
+
* @param description what the template is for; blank or absent stores none
|
|
385
623
|
*/
|
|
386
|
-
async saveProjectAsTemplate(projectId, label) {
|
|
624
|
+
async saveProjectAsTemplate(projectId, label, description) {
|
|
387
625
|
const outcome = await this.exportProjectAsTemplate(projectId);
|
|
388
626
|
if (!outcome.ok) return {
|
|
389
627
|
ok: false,
|
|
390
628
|
problems: outcome.problems
|
|
391
629
|
};
|
|
392
630
|
const rid = await this.resolveProjectId(projectId);
|
|
393
|
-
|
|
394
|
-
await this.pl.withWriteTx("MLSaveProjectAsTemplate", async (tx) => {
|
|
631
|
+
const wanted = label?.trim();
|
|
632
|
+
const signedRid = await this.pl.withWriteTx("MLSaveProjectAsTemplate", async (tx) => {
|
|
395
633
|
const meta = await tx.getKValueJson(rid, ProjectMetaKey);
|
|
396
|
-
|
|
634
|
+
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
635
|
+
const taken = tree.namesTakenBeside(projectId);
|
|
636
|
+
if (wanted !== void 0 && foldersNameTaken(wanted, taken)) throw new Error(`"${wanted}" is already used here.`);
|
|
637
|
+
const name = wanted ?? foldersUniqueName(meta.label, [meta.label, ...taken]);
|
|
638
|
+
const created = await createTemplate(tx, this.templateListResourceId, {
|
|
639
|
+
label: name,
|
|
640
|
+
description
|
|
641
|
+
}, {
|
|
397
642
|
schemaVersion: 1,
|
|
398
643
|
document: outcome.document,
|
|
399
644
|
sourceProjectLabel: meta.label
|
|
400
|
-
});
|
|
645
|
+
}).globalId;
|
|
646
|
+
tree.place([{
|
|
647
|
+
kind: "template",
|
|
648
|
+
id: asTemplateId(resourceIdToString(created))
|
|
649
|
+
}], tree.folderOf(projectId));
|
|
401
650
|
await tx.commit();
|
|
651
|
+
return created;
|
|
402
652
|
});
|
|
403
|
-
await this.templateListTree.refreshState();
|
|
404
|
-
const
|
|
405
|
-
const templateId = resourceIdToString(signedRid);
|
|
653
|
+
await Promise.all([this.templateListTree.refreshState(), this.foldersTree.refreshState()]);
|
|
654
|
+
const templateId = asTemplateId(resourceIdToString(signedRid));
|
|
406
655
|
this.templateIdCache.set(templateId, signedRid);
|
|
407
656
|
return {
|
|
408
657
|
ok: true,
|
|
409
658
|
templateId
|
|
410
659
|
};
|
|
411
660
|
}
|
|
412
|
-
/**
|
|
413
|
-
*
|
|
661
|
+
/**
|
|
662
|
+
* Changes a template's label. The stored document is immutable and stays untouched —
|
|
663
|
+
* improving a template means saving a new one.
|
|
664
|
+
*
|
|
665
|
+
* The label is stored trimmed. It is in the namespace folders, projects and templates share,
|
|
666
|
+
* so one already used beside the template is refused, in the transaction that writes it.
|
|
667
|
+
*/
|
|
414
668
|
async renameTemplate(id, label) {
|
|
415
669
|
const rid = await this.resolveTemplateId(id);
|
|
670
|
+
const wanted = label.trim();
|
|
416
671
|
await this.pl.withWriteTx("MLRenameTemplate", async (tx) => {
|
|
417
|
-
|
|
672
|
+
(await openFoldersTx(tx, this.foldersRids)).assertNameFree({
|
|
673
|
+
kind: "template",
|
|
674
|
+
id
|
|
675
|
+
}, wanted);
|
|
676
|
+
renameTemplate(tx, rid, wanted);
|
|
677
|
+
await tx.commit();
|
|
678
|
+
});
|
|
679
|
+
await this.templateListTree.refreshState();
|
|
680
|
+
}
|
|
681
|
+
/** Sets what a stored template says about itself; blank clears it. The stored document is not
|
|
682
|
+
* touched, so it stays byte-identical. */
|
|
683
|
+
async setTemplateDescription(id, description) {
|
|
684
|
+
const rid = await this.resolveTemplateId(id);
|
|
685
|
+
await this.pl.withWriteTx("MLSetTemplateDescription", async (tx) => {
|
|
686
|
+
setTemplateDescription(tx, rid, description);
|
|
418
687
|
await tx.commit();
|
|
419
688
|
});
|
|
420
689
|
await this.templateListTree.refreshState();
|
|
@@ -455,7 +724,8 @@ var MiddleLayer = class MiddleLayer {
|
|
|
455
724
|
* @param id template to apply
|
|
456
725
|
* @param label label for the new project
|
|
457
726
|
* @param provider where each entry's block comes from
|
|
458
|
-
* @param options `allowUnstable` widens resolution to pre-release implementations
|
|
727
|
+
* @param options `allowUnstable` widens resolution to pre-release implementations; `folder` is
|
|
728
|
+
* where the new project lands
|
|
459
729
|
*/
|
|
460
730
|
async createProjectFromTemplate(id, label, provider, options = {}) {
|
|
461
731
|
const stored = await this.getTemplateData(id);
|
|
@@ -464,7 +734,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
464
734
|
ok: false,
|
|
465
735
|
problems: preparation.problems
|
|
466
736
|
};
|
|
467
|
-
const projectId = await this.createProject({ label });
|
|
737
|
+
const projectId = await this.createProject({ label }, options.folder);
|
|
468
738
|
const report = await this.applyPreparedEntries(projectId, stored.document, preparation.prepared, options.author);
|
|
469
739
|
if (report.problems.length > 0) {
|
|
470
740
|
await this.deleteProject(projectId);
|
|
@@ -485,12 +755,9 @@ var MiddleLayer = class MiddleLayer {
|
|
|
485
755
|
const cached = this.templateIdCache.get(templateId);
|
|
486
756
|
if (cached !== void 0) return cached;
|
|
487
757
|
const rid = await this.pl.withReadTx("ResolveTemplateId", async (tx) => {
|
|
488
|
-
const
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
if (resourceIdToString(f.value) === templateId) return f.value;
|
|
492
|
-
}
|
|
493
|
-
throw new Error(`Template ${templateId} not found in template list.`);
|
|
758
|
+
const entry = (await listedById(tx, this.templateListResourceId)).get(templateId);
|
|
759
|
+
if (entry === void 0) throw notListedError("Template", templateId);
|
|
760
|
+
return entry.rid;
|
|
494
761
|
});
|
|
495
762
|
this.templateIdCache.set(templateId, rid);
|
|
496
763
|
return rid;
|
|
@@ -499,43 +766,50 @@ var MiddleLayer = class MiddleLayer {
|
|
|
499
766
|
* destruction of all attached objects, like files, analysis results etc. */
|
|
500
767
|
async deleteProject(id) {
|
|
501
768
|
await this.pl.withWriteTx("MLRemoveProject", async (tx) => {
|
|
502
|
-
const
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
if (isNullSignedResourceId(f.value)) continue;
|
|
506
|
-
if (resourceIdToString(f.value) === id) {
|
|
507
|
-
fieldName = f.name;
|
|
508
|
-
break;
|
|
509
|
-
}
|
|
510
|
-
}
|
|
511
|
-
if (fieldName === void 0) throw new Error(`Project ${id} not found in project list.`);
|
|
512
|
-
tx.removeField(field(this.projectListResourceId, fieldName));
|
|
769
|
+
const entry = (await listedById(tx, this.projectListResourceId)).get(id);
|
|
770
|
+
if (entry === void 0) throw notListedError("Project", id);
|
|
771
|
+
tx.removeField(field(this.projectListResourceId, entry.fieldName));
|
|
513
772
|
await tx.commit();
|
|
514
773
|
});
|
|
515
774
|
this.projectIdCache.delete(id);
|
|
516
775
|
await this.projectListTree.refreshState();
|
|
517
776
|
}
|
|
518
777
|
/**
|
|
519
|
-
* Duplicates an existing project and adds the copy to this user's project list
|
|
778
|
+
* Duplicates an existing project and adds the copy to this user's project list, beside the
|
|
779
|
+
* project it was copied from.
|
|
780
|
+
*
|
|
781
|
+
* Without `rename` the copy is named by the rule everything beside the source is named by: the
|
|
782
|
+
* source's own label, suffixed to be free there — `X (Copy)`. The name is chosen inside the
|
|
783
|
+
* transaction that creates the copy, against the tree that transaction reads.
|
|
520
784
|
*
|
|
521
785
|
* @param srcProjectId - project id of the project to duplicate
|
|
522
786
|
* @param rename - optional function that receives the source label and all existing
|
|
523
|
-
* project labels (read within the same transaction), and returns the label for the copy
|
|
787
|
+
* project labels (read within the same transaction), and returns the label for the copy.
|
|
788
|
+
* A label chosen this way is the caller's and is never suffixed: when it is already taken
|
|
789
|
+
* beside the source, the copy lands at the top level instead.
|
|
524
790
|
*/
|
|
525
791
|
async duplicateProject(srcProjectId, rename) {
|
|
526
792
|
const sourceRid = await this.resolveProjectId(srcProjectId);
|
|
527
793
|
const newPrj = await this.pl.withWriteTx("MLDuplicateProject", async (tx) => {
|
|
528
794
|
const sourceMeta = await tx.getKValueJson(sourceRid, ProjectMetaKey);
|
|
529
|
-
const
|
|
530
|
-
const
|
|
531
|
-
const newPrj = await duplicateProject(tx, sourceRid, {
|
|
795
|
+
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
796
|
+
const label = rename === void 0 ? foldersUniqueName(sourceMeta.label, [sourceMeta.label, ...tree.namesTakenBeside(srcProjectId)]) : rename(sourceMeta.label, await existingProjectLabels(tx, this.projectListResourceId));
|
|
797
|
+
const newPrj = await duplicateProject(tx, sourceRid, {
|
|
798
|
+
...sourceMeta,
|
|
799
|
+
label
|
|
800
|
+
}, this.env.projectHelper);
|
|
532
801
|
tx.createField(field(this.projectListResourceId, randomUUID()), "Dynamic", newPrj);
|
|
802
|
+
const created = {
|
|
803
|
+
kind: "project",
|
|
804
|
+
id: asProjectId(resourceIdToString(await newPrj.globalId))
|
|
805
|
+
};
|
|
806
|
+
tree.place([created], rename === void 0 ? tree.folderOf(srcProjectId) : inheritedFolder(tree.view, srcProjectId, created, label));
|
|
533
807
|
await tx.commit();
|
|
534
808
|
return newPrj;
|
|
535
809
|
});
|
|
536
|
-
await this.projectListTree.refreshState();
|
|
810
|
+
await Promise.all([this.projectListTree.refreshState(), this.foldersTree.refreshState()]);
|
|
537
811
|
const signedRid = await newPrj.globalId;
|
|
538
|
-
const newProjectId = resourceIdToString(signedRid);
|
|
812
|
+
const newProjectId = asProjectId(resourceIdToString(signedRid));
|
|
539
813
|
this.projectIdCache.set(newProjectId, signedRid);
|
|
540
814
|
return newProjectId;
|
|
541
815
|
}
|
|
@@ -543,7 +817,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
543
817
|
* Duplicates a project into another user's root, minted in the TARGET user's color so the target
|
|
544
818
|
* owns it. Sibling of {@link duplicateProject}, but writes into a different root. The source
|
|
545
819
|
* project (on the current client root) is referenced cross-color for its block data, kept alive
|
|
546
|
-
* by refcounting, exactly like
|
|
820
|
+
* by refcounting, exactly like a project copied out of a share. Works both ways: pull (while
|
|
547
821
|
* impersonating a user, copy their project to yourself) and push (from your own root, copy a
|
|
548
822
|
* project to a user). Admin cross-root op; requires the crossTreeRefs:v1 backend capability.
|
|
549
823
|
*/
|
|
@@ -557,100 +831,36 @@ var MiddleLayer = class MiddleLayer {
|
|
|
557
831
|
await this.pl.withWriteTxOnRoot(targetRoot, "MLDuplicateProjectToUser", async (tx) => {
|
|
558
832
|
const targetProjectListRid = await ensureProjectListRid(tx);
|
|
559
833
|
const sourceMeta = await tx.getKValueJson(sourceRid, ProjectMetaKey);
|
|
560
|
-
const
|
|
561
|
-
const
|
|
562
|
-
const newPrj = await duplicateProject(tx, sourceRid, {
|
|
834
|
+
const existingLabels = await existingProjectLabels(tx, targetProjectListRid);
|
|
835
|
+
const newLabel = rename ? rename(sourceMeta.label, existingLabels) : sourceMeta.label;
|
|
836
|
+
const newPrj = await duplicateProject(tx, sourceRid, {
|
|
837
|
+
...sourceMeta,
|
|
838
|
+
label: newLabel
|
|
839
|
+
}, this.env.projectHelper);
|
|
563
840
|
tx.createField(field(targetProjectListRid, randomUUID()), "Dynamic", newPrj);
|
|
564
841
|
await tx.commit();
|
|
565
842
|
});
|
|
566
843
|
}
|
|
567
844
|
/**
|
|
568
|
-
* Shares the given projects (Copy & Share)
|
|
569
|
-
*
|
|
570
|
-
* and the outbox is left as it was.
|
|
571
|
-
*
|
|
572
|
-
* Two variants (see {@link ShareProjectsOptions}):
|
|
573
|
-
* - `{ recipients }` — one writable grant per named recipient; the envelope expires after the
|
|
574
|
-
* default TTL (`sharedAt + envelopeTtlMs`).
|
|
575
|
-
* - `{ everyone: true }` — one make-public grant (backend rewrites the target to the
|
|
576
|
-
* everyone-user); the envelope's `expiresAt` is `null`, so it never expires.
|
|
845
|
+
* Shares the given projects (Copy & Share): snapshots them into one envelope, in the transaction
|
|
846
|
+
* {@link shareEnvelope} describes.
|
|
577
847
|
*
|
|
578
848
|
* v1 always passes `mode: "copy"`.
|
|
579
849
|
*/
|
|
580
850
|
async shareProjects(projectIds, options) {
|
|
581
851
|
if (projectIds.length === 0) throw new Error("shareProjects: no projects given");
|
|
582
|
-
if ("everyone" in options && options.replace) {
|
|
583
|
-
const priorEveryone = (await this.findSupersedableEnvelopes(projectIds)).find((p) => p.everyone);
|
|
584
|
-
if (priorEveryone !== void 0) {
|
|
585
|
-
await this.changeShare(priorEveryone.shareId, { title: options.title });
|
|
586
|
-
return;
|
|
587
|
-
}
|
|
588
|
-
}
|
|
589
|
-
await this.createNewShare(projectIds, options);
|
|
590
|
-
}
|
|
591
|
-
/**
|
|
592
|
-
* Mints a fresh share: snapshots the projects into one new envelope (a fresh shareId),
|
|
593
|
-
* supersedes prior shares of the same project, and grants it — all in one atomic write
|
|
594
|
-
* transaction, so a failed grant rolls the whole thing back and the outbox is left as it was.
|
|
595
|
-
* The everyone-refresh path is the {@link changeShare} branch of {@link shareProjects}; this is
|
|
596
|
-
* the mint-a-new-envelope branch.
|
|
597
|
-
*/
|
|
598
|
-
async createNewShare(projectIds, options) {
|
|
599
|
-
const everyone = "everyone" in options;
|
|
600
852
|
const sources = await Promise.all(projectIds.map(async (id) => ({
|
|
601
|
-
kind: "fresh",
|
|
602
853
|
projectId: id,
|
|
603
854
|
sourceRid: await this.resolveProjectId(id)
|
|
604
855
|
})));
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
if (everyone) {
|
|
610
|
-
for (const prior of priors) if (prior.everyone) tx.removeField(field(this.sharingOutboxResourceId, prior.fieldName));
|
|
611
|
-
} else {
|
|
612
|
-
const newRecipients = new Set(options.recipients);
|
|
613
|
-
for (const prior of priors) {
|
|
614
|
-
if (prior.everyone) continue;
|
|
615
|
-
const toRemove = prior.recipients.filter((u) => newRecipients.has(u));
|
|
616
|
-
if (toRemove.length === 0) continue;
|
|
617
|
-
if (prior.recipients.filter((u) => !newRecipients.has(u)).length === 0) tx.removeField(field(this.sharingOutboxResourceId, prior.fieldName));
|
|
618
|
-
else for (const u of toRemove) tx.revokeAccess(prior.rid, u);
|
|
619
|
-
}
|
|
620
|
-
}
|
|
621
|
-
const { envelope } = await buildShareEnvelope(tx, this.sharingOutboxResourceId, sources, {
|
|
622
|
-
mode: options.mode,
|
|
623
|
-
sender,
|
|
624
|
-
title: options.title,
|
|
625
|
-
expiresAt
|
|
626
|
-
});
|
|
627
|
-
await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, { writable: true });
|
|
628
|
-
await tx.commit();
|
|
629
|
-
});
|
|
630
|
-
await this.sharingOutboxTree.refreshState();
|
|
631
|
-
}
|
|
632
|
-
/**
|
|
633
|
-
* Grants one freshly built envelope inside the transaction that created it: a single make-public
|
|
634
|
-
* grant for an everyone-share (empty/ignored target, ANY_AUTHORISED — the backend rewrites the
|
|
635
|
-
* target to the everyone-user, gated by role + permission ceiling), or one grant per named
|
|
636
|
-
* recipient.
|
|
637
|
-
*
|
|
638
|
-
* `writable` is not a preference. A project pack needs a writable grant because accepting copies
|
|
639
|
-
* the snapshots out of the envelope, and the cross-color attach rule permits that only to a
|
|
640
|
-
* writable grant holder. A template share copies nothing — the document sits in the envelope's
|
|
641
|
-
* own immutable data — so it is granted read-only, and must be: a writable everyone-grant would
|
|
642
|
-
* hand every user on the server write access to the envelope.
|
|
643
|
-
*/
|
|
644
|
-
async grantShareEnvelope(tx, envelope, everyone, recipients, permissions) {
|
|
645
|
-
const gid = await envelope.globalId;
|
|
646
|
-
if (everyone) tx.grantAccess(gid, "", permissions, GrantType.ANY_AUTHORISED);
|
|
647
|
-
else for (const r of recipients) tx.grantAccess(gid, r, permissions);
|
|
856
|
+
return await this.shareEnvelope("MLShareProjects", options, { writable: true }, (tx, meta) => buildShareEnvelope(tx, this.sharingOutboxResourceId, sources, {
|
|
857
|
+
mode: options.mode,
|
|
858
|
+
...meta
|
|
859
|
+
}));
|
|
648
860
|
}
|
|
649
861
|
/**
|
|
650
862
|
* Shares one stored template. The envelope carries the document itself, so there is no project
|
|
651
863
|
* snapshot and no resource for the recipient to copy out — which is why the grant is read-only.
|
|
652
|
-
* The cost of that is the donor's receipt: nobody can write an acceptance onto a read-only
|
|
653
|
-
* envelope, so a template share never reports who accepted it.
|
|
654
864
|
*
|
|
655
865
|
* Nothing about the document is checked: a stored template is shareable by virtue of existing.
|
|
656
866
|
* An entry the recipient cannot resolve — a block installed from a folder on the sender's
|
|
@@ -661,187 +871,110 @@ var MiddleLayer = class MiddleLayer {
|
|
|
661
871
|
* @param options recipients XOR everyone, plus the title recipients see
|
|
662
872
|
*/
|
|
663
873
|
async shareTemplate(id, options) {
|
|
664
|
-
const template = await this.loadTemplateForShare(id);
|
|
665
|
-
const everyone = "everyone" in options;
|
|
666
|
-
const sender = this.currentUserLogin ?? "";
|
|
667
|
-
const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;
|
|
668
|
-
let shareId;
|
|
669
|
-
await this.pl.withWriteTx("MLShareTemplate", async (tx) => {
|
|
670
|
-
const { envelope, data } = buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, template, {
|
|
671
|
-
sender,
|
|
672
|
-
title: options.title,
|
|
673
|
-
expiresAt
|
|
674
|
-
});
|
|
675
|
-
shareId = data.shareId;
|
|
676
|
-
await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, { writable: false });
|
|
677
|
-
await tx.commit();
|
|
678
|
-
});
|
|
679
|
-
await this.sharingOutboxTree.refreshState();
|
|
680
|
-
return { shareId };
|
|
681
|
-
}
|
|
682
|
-
/** The document and the label of a template about to be shared. The label is what the
|
|
683
|
-
* recipient's own list will show, so it travels with the document. */
|
|
684
|
-
async loadTemplateForShare(id) {
|
|
685
874
|
const rid = await this.resolveTemplateId(id);
|
|
686
|
-
|
|
687
|
-
|
|
688
|
-
|
|
689
|
-
|
|
690
|
-
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
});
|
|
875
|
+
const stored = await this.pl.withReadTx("MLReadStoredTemplate", (tx) => readStoredTemplate(tx, id, rid));
|
|
876
|
+
return await this.shareEnvelope("MLShareTemplate", options, { writable: false }, (tx, meta) => buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, {
|
|
877
|
+
document: stored.data.document,
|
|
878
|
+
label: stored.label,
|
|
879
|
+
...stored.description === void 0 ? {} : { description: stored.description },
|
|
880
|
+
source: id
|
|
881
|
+
}, meta));
|
|
694
882
|
}
|
|
695
883
|
/**
|
|
696
|
-
*
|
|
697
|
-
*
|
|
698
|
-
* transfers already-decided recipients' accept/reject records (they keep their copy and aren't
|
|
699
|
-
* re-prompted); re-grants; drops the old envelope.
|
|
884
|
+
* Shares one folder and everything under it: the subtree's folders, every project in them
|
|
885
|
+
* snapshotted, and every template carried whole.
|
|
700
886
|
*
|
|
701
|
-
*
|
|
702
|
-
*
|
|
703
|
-
*
|
|
887
|
+
* The grant is writable, because a folder holding projects is copied out of the envelope the
|
|
888
|
+
* way a project pack is. An everyone-share of a folder therefore hands every user on the server
|
|
889
|
+
* write access to the envelope — the same trade a project share already makes.
|
|
704
890
|
*
|
|
705
|
-
*
|
|
706
|
-
*
|
|
707
|
-
* existing snapshot (and its timestamp), `remove` drops the project from the pack. A project not
|
|
708
|
-
* in the map defaults to `keep`. Omit the whole map for the legacy auto behavior (live sources
|
|
709
|
-
* updated, gone ones kept) — the everyone-refresh path relies on that.
|
|
891
|
+
* Folder ids do not travel. What the recipient gets is the shape of the subtree, rebuilt under
|
|
892
|
+
* a folder of their own choosing with ids their own document mints.
|
|
710
893
|
*
|
|
711
|
-
*
|
|
712
|
-
*
|
|
713
|
-
* the one it started from — the caller names the new target. Every other option means the same
|
|
714
|
-
* thing for both kinds of share.
|
|
894
|
+
* @param folder folder to share; everything beneath it goes with it
|
|
895
|
+
* @param options recipients XOR everyone, plus the title recipients see
|
|
715
896
|
*/
|
|
716
|
-
async
|
|
717
|
-
const
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
|
|
730
|
-
|
|
731
|
-
|
|
732
|
-
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
}
|
|
754
|
-
|
|
755
|
-
const recipients = everyone ? [] : Array.from(/* @__PURE__ */ new Set([...opts.recipients ?? priorRecipients, ...decidedLogins]));
|
|
756
|
-
const liveProjects = /* @__PURE__ */ new Map();
|
|
757
|
-
const projList = await tx.getResourceData(this.projectListResourceId, true);
|
|
758
|
-
for (const f of projList.fields) {
|
|
759
|
-
if (isNullSignedResourceId(f.value)) continue;
|
|
760
|
-
liveProjects.set(resourceIdToString(f.value), f.value);
|
|
761
|
-
}
|
|
762
|
-
const actions = opts.projectActions;
|
|
763
|
-
const sources = [];
|
|
764
|
-
const oldProjects = envelopeProjectMap(old.data);
|
|
765
|
-
for (const uuid of Object.keys(oldProjects)) {
|
|
766
|
-
const { label, source, updatedAt } = oldProjects[uuid];
|
|
767
|
-
const liveRid = liveProjects.get(source);
|
|
768
|
-
const action = actions ? actions[source] ?? "keep" : liveRid !== void 0 ? "update" : "keep";
|
|
769
|
-
if (action === "remove") continue;
|
|
770
|
-
if (action === "update" && liveRid !== void 0) sources.push({
|
|
771
|
-
kind: "fresh",
|
|
772
|
-
projectId: source,
|
|
773
|
-
sourceRid: liveRid
|
|
774
|
-
});
|
|
775
|
-
else {
|
|
776
|
-
const snapshotRid = snapshotByUuid.get(uuid);
|
|
777
|
-
if (snapshotRid !== void 0) sources.push({
|
|
778
|
-
kind: "carry",
|
|
779
|
-
projectId: source,
|
|
780
|
-
label,
|
|
781
|
-
snapshotRid,
|
|
782
|
-
updatedAt
|
|
783
|
-
});
|
|
784
|
-
}
|
|
785
|
-
}
|
|
786
|
-
const title = opts.title === void 0 ? old.data.title : opts.title.trim();
|
|
787
|
-
const expiresAt = everyone ? null : Date.now() + this.env.ops.envelopeTtlMs;
|
|
788
|
-
tx.removeField(field(this.sharingOutboxResourceId, old.fieldName));
|
|
789
|
-
const { envelope } = await buildShareEnvelope(tx, this.sharingOutboxResourceId, sources, {
|
|
790
|
-
mode: old.data.mode,
|
|
791
|
-
sender: self,
|
|
792
|
-
title,
|
|
793
|
-
expiresAt,
|
|
794
|
-
shareId
|
|
795
|
-
});
|
|
796
|
-
for (const { login, acc } of acceptances) {
|
|
797
|
-
if (!everyone && !recipients.includes(login)) continue;
|
|
798
|
-
writeEnvelopeAcceptance(tx, envelope, login, acc.action, acc.timestamp);
|
|
799
|
-
}
|
|
800
|
-
await this.grantShareEnvelope(tx, envelope, everyone, recipients, { writable: true });
|
|
897
|
+
async shareFolder(folder, options) {
|
|
898
|
+
const loaded = await this.loadFolderSubtree(folder, newEnvelopeFolderId);
|
|
899
|
+
const subtree = {
|
|
900
|
+
source: folder,
|
|
901
|
+
folders: loaded.folders,
|
|
902
|
+
projects: loaded.projects.map((project) => ({
|
|
903
|
+
projectId: project.projectId,
|
|
904
|
+
sourceRid: project.rid,
|
|
905
|
+
folder: project.folder
|
|
906
|
+
})),
|
|
907
|
+
templates: loaded.templates.map((template) => ({
|
|
908
|
+
document: template.data.document,
|
|
909
|
+
label: template.label,
|
|
910
|
+
...template.description === void 0 ? {} : { description: template.description },
|
|
911
|
+
folder: template.folder
|
|
912
|
+
}))
|
|
913
|
+
};
|
|
914
|
+
return await this.shareEnvelope("MLShareFolder", options, { writable: true }, (tx, meta) => buildFolderShareEnvelope(tx, this.sharingOutboxResourceId, subtree, meta));
|
|
915
|
+
}
|
|
916
|
+
/**
|
|
917
|
+
* The one transaction every share is made in: the shares named by `options.replace` are
|
|
918
|
+
* dropped, the envelope `build` makes is created, and it is granted — all at once, so a failed
|
|
919
|
+
* grant rolls the whole thing back and the outbox is left as it was.
|
|
920
|
+
*
|
|
921
|
+
* A share with named recipients grants each of them and expires after the default TTL
|
|
922
|
+
* (`sharedAt + envelopeTtlMs`). A share with everyone is one make-public grant, and its
|
|
923
|
+
* `expiresAt` is `null`, so it never expires.
|
|
924
|
+
*/
|
|
925
|
+
async shareEnvelope(txName, options, permissions, build) {
|
|
926
|
+
const everyone = "everyone" in options;
|
|
927
|
+
const meta = {
|
|
928
|
+
sender: this.currentUserLogin ?? "",
|
|
929
|
+
title: options.title,
|
|
930
|
+
expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs
|
|
931
|
+
};
|
|
932
|
+
const outcome = await this.pl.withWriteTx(txName, async (tx) => {
|
|
933
|
+
await this.dropShares(tx, options.replace);
|
|
934
|
+
const { envelope, data } = await build(tx, meta);
|
|
935
|
+
await this.grantShareEnvelope(tx, envelope, everyone, everyone ? [] : options.recipients, permissions);
|
|
801
936
|
await tx.commit();
|
|
937
|
+
return { shareId: data.shareId };
|
|
802
938
|
});
|
|
803
939
|
await this.sharingOutboxTree.refreshState();
|
|
940
|
+
return outcome;
|
|
804
941
|
}
|
|
805
942
|
/**
|
|
806
|
-
*
|
|
807
|
-
* the
|
|
808
|
-
*
|
|
809
|
-
*
|
|
943
|
+
* Detaches the named shares from the donor's outbox inside the caller's transaction, so a
|
|
944
|
+
* replacement and the shares it supersedes land together or not at all.
|
|
945
|
+
*
|
|
946
|
+
* A share that no longer resolves is skipped rather than reported: the caller names shares the
|
|
947
|
+
* author saw a moment ago, and one revoked meanwhile is already in the wanted state.
|
|
810
948
|
*/
|
|
811
|
-
async
|
|
812
|
-
const
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
|
|
818
|
-
|
|
819
|
-
|
|
820
|
-
|
|
821
|
-
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
rid,
|
|
835
|
-
shareId,
|
|
836
|
-
everyone: grants.some((g) => isEveryoneUserLogin(g.user)),
|
|
837
|
-
recipients: grants.filter((g) => !isEveryoneUserLogin(g.user)).map((g) => g.user)
|
|
838
|
-
};
|
|
839
|
-
}));
|
|
949
|
+
async dropShares(tx, shareIds) {
|
|
950
|
+
for (const shareId of shareIds ?? []) {
|
|
951
|
+
const target = await this.resolveOutboxEnvelope(tx, shareId);
|
|
952
|
+
if (target === void 0) continue;
|
|
953
|
+
tx.removeField(field(this.sharingOutboxResourceId, target.fieldName));
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
/**
|
|
957
|
+
* Grants one freshly built envelope inside the transaction that created it: a single make-public
|
|
958
|
+
* grant for an everyone-share (empty/ignored target, ANY_AUTHORISED — the backend rewrites the
|
|
959
|
+
* target to the everyone-user, gated by role + permission ceiling), or one grant per named
|
|
960
|
+
* recipient.
|
|
961
|
+
*
|
|
962
|
+
* `writable` is not a preference. A project pack needs a writable grant because a copy out of
|
|
963
|
+
* it takes the snapshots out of the envelope, and the cross-color attach rule permits that only
|
|
964
|
+
* to a writable grant holder. A template share copies nothing — the document sits in the
|
|
965
|
+
* envelope's own immutable data — so it is granted read-only, and must be: a writable
|
|
966
|
+
* everyone-grant would hand every user on the server write access to the envelope.
|
|
967
|
+
*/
|
|
968
|
+
async grantShareEnvelope(tx, envelope, everyone, recipients, permissions) {
|
|
969
|
+
const gid = await envelope.globalId;
|
|
970
|
+
if (everyone) tx.grantAccess(gid, "", permissions, GrantType.ANY_AUTHORISED);
|
|
971
|
+
else for (const r of recipients) tx.grantAccess(gid, r, permissions);
|
|
840
972
|
}
|
|
841
973
|
/**
|
|
842
974
|
* Revokes and deletes an outgoing share for all recipients: detaches and deletes the envelope, and
|
|
843
|
-
* its grants are revoked along with it.
|
|
844
|
-
* the
|
|
975
|
+
* its grants are revoked along with it. Copies recipients already took out of it are unaffected
|
|
976
|
+
* (ref-counting keeps the resources they point at alive). Idempotent — revoking a share that is
|
|
977
|
+
* already gone is a no-op.
|
|
845
978
|
*/
|
|
846
979
|
async revokeShare(shareId) {
|
|
847
980
|
await this.pl.withWriteTx("MLRevokeShare", async (tx) => {
|
|
@@ -878,36 +1011,37 @@ var MiddleLayer = class MiddleLayer {
|
|
|
878
1011
|
* the envelope's logical `shareId`.
|
|
879
1012
|
*
|
|
880
1013
|
* Reads the {@link liveEnvelopes} Computable — the same shared-resource discovery tree that
|
|
881
|
-
* feeds {@link
|
|
882
|
-
* `ListUserResources` re-stream on every
|
|
1014
|
+
* feeds {@link availableShares}. This is the single discovery mechanism: there is no separate
|
|
1015
|
+
* `ListUserResources` re-stream on every copy. `refreshState()` is awaited first so a
|
|
883
1016
|
* just-granted envelope is observed (the tree's discovery poll may otherwise lag a freshly
|
|
884
1017
|
* landed grant). The tree is gRPC-only, so this is empty on a REST-connected client.
|
|
885
1018
|
*/
|
|
886
1019
|
async resolveLiveEnvelopes() {
|
|
887
|
-
await this.
|
|
1020
|
+
await this.availableSharesTree.refreshState();
|
|
888
1021
|
const live = await this.liveEnvelopes.getValue() ?? [];
|
|
889
1022
|
const map = /* @__PURE__ */ new Map();
|
|
890
1023
|
for (const e of live) map.set(e.data.shareId, e);
|
|
891
1024
|
return map;
|
|
892
1025
|
}
|
|
893
1026
|
/**
|
|
894
|
-
*
|
|
895
|
-
* pack of projects is duplicated into this user's project list, while a template is added to this
|
|
896
|
-
* user's own template list and builds nothing — the recipient decides later whether to apply it.
|
|
897
|
-
* Either way the decision is recorded per share, and a read-write share also gets the
|
|
898
|
-
* donor-visible acceptance written onto its envelope. Per-share failures (e.g. an expiry race)
|
|
899
|
-
* are collected, not short-circuited — the rest still get accepted. Accept-all = pass every
|
|
900
|
-
* current pending shareId.
|
|
1027
|
+
* Copies what one or more shares carry into this user's own tree, optionally into a folder.
|
|
901
1028
|
*
|
|
902
|
-
*
|
|
903
|
-
* the
|
|
904
|
-
*
|
|
1029
|
+
* A share is a shelf, not an invitation: copying takes nothing off it and records no decision,
|
|
1030
|
+
* so the same share can be copied from again, by this user or anyone else it was granted to.
|
|
1031
|
+
* What a copy produces depends on the payload — a pack of projects lands in the project list,
|
|
1032
|
+
* a template among their templates, building nothing until the recipient applies it, and a
|
|
1033
|
+
* folder is rebuilt whole with everything it held.
|
|
1034
|
+
*
|
|
1035
|
+
* Names are chosen against the destination folder, since that is where the uniqueness rule
|
|
1036
|
+
* applies, and a project and a template follow the same rule. A destination deleted meanwhile
|
|
1037
|
+
* fails the copy rather than spilling it at the top level.
|
|
1038
|
+
* Per-share failures (a revoked envelope, say) are collected rather than short-circuited, so
|
|
1039
|
+
* one dead share does not cost the others.
|
|
905
1040
|
*/
|
|
906
|
-
async
|
|
1041
|
+
async copyShare(shareIds, destination) {
|
|
907
1042
|
const live = await this.resolveLiveEnvelopes();
|
|
908
|
-
const
|
|
909
|
-
const
|
|
910
|
-
const acceptedTemplates = [];
|
|
1043
|
+
const projects = [];
|
|
1044
|
+
const templates = [];
|
|
911
1045
|
const failed = [];
|
|
912
1046
|
for (const shareId of shareIds) {
|
|
913
1047
|
const envelope = live.get(shareId);
|
|
@@ -919,45 +1053,100 @@ var MiddleLayer = class MiddleLayer {
|
|
|
919
1053
|
continue;
|
|
920
1054
|
}
|
|
921
1055
|
try {
|
|
922
|
-
const now = Date.now();
|
|
923
1056
|
const payload = envelope.data.payload;
|
|
924
1057
|
if (payload.kind === "template") {
|
|
925
|
-
const rid = await this.pl.withWriteTx("
|
|
926
|
-
const
|
|
1058
|
+
const rid = await this.pl.withWriteTx("MLCopyTemplateShare", async (tx) => {
|
|
1059
|
+
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
1060
|
+
const created = await createTemplate(tx, this.templateListResourceId, {
|
|
1061
|
+
label: foldersUniqueName(payload.label, tree.namesTakenIn(destination)),
|
|
1062
|
+
description: payload.description
|
|
1063
|
+
}, {
|
|
927
1064
|
schemaVersion: 1,
|
|
928
1065
|
document: payload.document,
|
|
929
1066
|
sender: payload.from
|
|
930
|
-
});
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
acceptedProjects: []
|
|
936
|
-
});
|
|
1067
|
+
}).globalId;
|
|
1068
|
+
tree.place([{
|
|
1069
|
+
kind: "template",
|
|
1070
|
+
id: asTemplateId(resourceIdToString(created))
|
|
1071
|
+
}], destination);
|
|
937
1072
|
await tx.commit();
|
|
938
|
-
return
|
|
1073
|
+
return created;
|
|
939
1074
|
});
|
|
940
|
-
const templateId = resourceIdToString(rid);
|
|
1075
|
+
const templateId = asTemplateId(resourceIdToString(rid));
|
|
941
1076
|
this.templateIdCache.set(templateId, rid);
|
|
942
|
-
|
|
1077
|
+
templates.push(templateId);
|
|
943
1078
|
continue;
|
|
944
1079
|
}
|
|
945
|
-
|
|
946
|
-
const
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
1080
|
+
if (payload.kind === "folder") {
|
|
1081
|
+
const root = envelopeFolderRoot(payload.folders);
|
|
1082
|
+
if (root === void 0) throw new Error("This share does not describe one folder, so nothing can be rebuilt.");
|
|
1083
|
+
const copied = await this.pl.withWriteTx("MLCopyFolderShare", async (tx) => {
|
|
1084
|
+
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
1085
|
+
const created = await copyEnvelopeProjectsIntoList(tx, envelope.rid, this.projectListResourceId);
|
|
1086
|
+
const createdTemplates = [];
|
|
1087
|
+
const items = [];
|
|
1088
|
+
for (const { uuid, rid } of created) {
|
|
1089
|
+
const inFolder = payload.projects[uuid];
|
|
1090
|
+
items.push({
|
|
1091
|
+
kind: "project",
|
|
1092
|
+
id: asProjectId(resourceIdToString(rid)),
|
|
1093
|
+
folder: inFolder?.folder ?? root
|
|
1094
|
+
});
|
|
1095
|
+
}
|
|
1096
|
+
for (const carried of payload.templates) {
|
|
1097
|
+
const id = asTemplateId(resourceIdToString(await createTemplate(tx, this.templateListResourceId, {
|
|
1098
|
+
label: carried.label,
|
|
1099
|
+
description: carried.description
|
|
1100
|
+
}, {
|
|
1101
|
+
schemaVersion: 1,
|
|
1102
|
+
document: carried.document,
|
|
1103
|
+
sender: payload.from
|
|
1104
|
+
}).globalId));
|
|
1105
|
+
createdTemplates.push(id);
|
|
1106
|
+
items.push({
|
|
1107
|
+
kind: "template",
|
|
1108
|
+
id,
|
|
1109
|
+
folder: carried.folder
|
|
1110
|
+
});
|
|
1111
|
+
}
|
|
1112
|
+
tree.graft({
|
|
1113
|
+
root,
|
|
1114
|
+
folders: payload.folders,
|
|
1115
|
+
items
|
|
1116
|
+
}, destination);
|
|
1117
|
+
await tx.commit();
|
|
1118
|
+
return {
|
|
1119
|
+
projects: created,
|
|
1120
|
+
templates: createdTemplates
|
|
1121
|
+
};
|
|
952
1122
|
});
|
|
953
|
-
|
|
1123
|
+
for (const { rid } of copied.projects) {
|
|
1124
|
+
const projectId = asProjectId(resourceIdToString(rid));
|
|
1125
|
+
this.projectIdCache.set(projectId, rid);
|
|
1126
|
+
projects.push(projectId);
|
|
1127
|
+
}
|
|
1128
|
+
templates.push(...copied.templates);
|
|
1129
|
+
continue;
|
|
1130
|
+
}
|
|
1131
|
+
const createdRids = await this.pl.withWriteTx("MLCopyShare", async (tx) => {
|
|
1132
|
+
const tree = await openFoldersTx(tx, this.foldersRids);
|
|
1133
|
+
const taken = [...tree.namesTakenIn(destination)];
|
|
1134
|
+
const created = await copyEnvelopeProjectsIntoList(tx, envelope.rid, this.projectListResourceId, (sourceLabel) => {
|
|
1135
|
+
const name = foldersUniqueName(sourceLabel, taken);
|
|
1136
|
+
taken.push(name);
|
|
1137
|
+
return name;
|
|
1138
|
+
});
|
|
1139
|
+
tree.place(created.map(({ rid }) => ({
|
|
1140
|
+
kind: "project",
|
|
1141
|
+
id: asProjectId(resourceIdToString(rid))
|
|
1142
|
+
})), destination);
|
|
954
1143
|
await tx.commit();
|
|
955
1144
|
return created;
|
|
956
1145
|
});
|
|
957
|
-
for (const rid of createdRids) {
|
|
958
|
-
const projectId = resourceIdToString(rid);
|
|
1146
|
+
for (const { rid } of createdRids) {
|
|
1147
|
+
const projectId = asProjectId(resourceIdToString(rid));
|
|
959
1148
|
this.projectIdCache.set(projectId, rid);
|
|
960
|
-
|
|
1149
|
+
projects.push(projectId);
|
|
961
1150
|
}
|
|
962
1151
|
} catch (e) {
|
|
963
1152
|
failed.push({
|
|
@@ -969,27 +1158,31 @@ var MiddleLayer = class MiddleLayer {
|
|
|
969
1158
|
await Promise.all([
|
|
970
1159
|
this.projectListTree.refreshState(),
|
|
971
1160
|
this.templateListTree.refreshState(),
|
|
972
|
-
this.
|
|
1161
|
+
this.foldersTree.refreshState()
|
|
973
1162
|
]);
|
|
974
1163
|
return {
|
|
975
|
-
|
|
976
|
-
|
|
1164
|
+
projects,
|
|
1165
|
+
templates,
|
|
977
1166
|
failed
|
|
978
1167
|
};
|
|
979
1168
|
}
|
|
980
|
-
/**
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
1169
|
+
/**
|
|
1170
|
+
* Puts a share out of this user's sight. Private to them and reversible with
|
|
1171
|
+
* {@link unhideShare}: nothing is deleted, the donor is not told, and what was already copied
|
|
1172
|
+
* out of it is unaffected.
|
|
1173
|
+
*/
|
|
1174
|
+
async hideShare(shareId) {
|
|
984
1175
|
const now = Date.now();
|
|
985
|
-
await this.pl.withWriteTx("
|
|
986
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
1176
|
+
await this.pl.withWriteTx("MLHideShare", async (tx) => {
|
|
1177
|
+
writeShareHidden(tx, this.sharingStateResourceId, shareId, now);
|
|
1178
|
+
await tx.commit();
|
|
1179
|
+
});
|
|
1180
|
+
await this.sharingStateTree.refreshState();
|
|
1181
|
+
}
|
|
1182
|
+
/** Brings a hidden share back into this user's list. */
|
|
1183
|
+
async unhideShare(shareId) {
|
|
1184
|
+
await this.pl.withWriteTx("MLUnhideShare", async (tx) => {
|
|
1185
|
+
clearShareHidden(tx, this.sharingStateResourceId, shareId);
|
|
993
1186
|
await tx.commit();
|
|
994
1187
|
});
|
|
995
1188
|
await this.sharingStateTree.refreshState();
|
|
@@ -1103,9 +1296,10 @@ var MiddleLayer = class MiddleLayer {
|
|
|
1103
1296
|
await Promise.all([
|
|
1104
1297
|
this.projectListTree.terminate(),
|
|
1105
1298
|
this.templateListTree.terminate(),
|
|
1299
|
+
this.foldersTree.terminate(),
|
|
1106
1300
|
this.sharingOutboxTree.terminate(),
|
|
1107
1301
|
this.sharingStateTree.terminate(),
|
|
1108
|
-
this.
|
|
1302
|
+
this.availableSharesTree.terminate()
|
|
1109
1303
|
]);
|
|
1110
1304
|
await this.drainSnapshotWrites(SNAPSHOT_DRAIN_TIMEOUT_MS);
|
|
1111
1305
|
await this.env.dispose();
|
|
@@ -1134,7 +1328,7 @@ var MiddleLayer = class MiddleLayer {
|
|
|
1134
1328
|
ops.defaultTreeOptions.logStat = getDebugFlags().logTreeStats;
|
|
1135
1329
|
ops.debugOps.dumpInitialTreeState = getDebugFlags().dumpInitialTreeState;
|
|
1136
1330
|
if (ops.defaultTreeOptions.traversalMode === void 0 && getDebugFlags().treeTraversalMode !== void 0) ops.defaultTreeOptions.traversalMode = getDebugFlags().treeTraversalMode;
|
|
1137
|
-
const { projects, templates, sharingOutbox, sharingState } = await pl.withWriteTx("MLInitialization", async (tx) => {
|
|
1331
|
+
const { projects, templates, sharingOutbox, sharingState, folders } = await pl.withWriteTx("MLInitialization", async (tx) => {
|
|
1138
1332
|
const lazyInit = async (fieldName, type) => {
|
|
1139
1333
|
const f = field(tx.clientRoot, fieldName);
|
|
1140
1334
|
tx.createField(f, "Dynamic");
|
|
@@ -1151,12 +1345,15 @@ var MiddleLayer = class MiddleLayer {
|
|
|
1151
1345
|
const templatesR = await lazyInit(TemplatesField, TemplatesResourceType);
|
|
1152
1346
|
const outboxR = await lazyInit(SharingOutboxField, SharingOutboxResourceType);
|
|
1153
1347
|
const stateR = await lazyInit(SharingStateField, SharingStateResourceType);
|
|
1348
|
+
const foldersR = await lazyInit(FoldersField, FoldersResourceType);
|
|
1154
1349
|
await tx.commit();
|
|
1350
|
+
const idOf = async (r) => "existing" in r ? r.existing : await r.ref.globalId;
|
|
1155
1351
|
return {
|
|
1156
|
-
projects:
|
|
1157
|
-
templates:
|
|
1158
|
-
sharingState:
|
|
1159
|
-
sharingOutbox:
|
|
1352
|
+
projects: await idOf(projectsR),
|
|
1353
|
+
templates: await idOf(templatesR),
|
|
1354
|
+
sharingState: await idOf(stateR),
|
|
1355
|
+
sharingOutbox: await idOf(outboxR),
|
|
1356
|
+
folders: await idOf(foldersR)
|
|
1160
1357
|
};
|
|
1161
1358
|
});
|
|
1162
1359
|
const logger = ops.logger;
|
|
@@ -1210,14 +1407,32 @@ var MiddleLayer = class MiddleLayer {
|
|
|
1210
1407
|
const openedProjects = new WatchableValue([]);
|
|
1211
1408
|
const projectListTC = await createProjectList(pl, projects, openedProjects, env);
|
|
1212
1409
|
const templateListTC = await createTemplateList(pl, templates, env);
|
|
1410
|
+
const foldersTC = await createFolderList(pl, folders, projectListTC.tree, templateListTC.tree, openedProjects, env);
|
|
1213
1411
|
const outgoingTC = await createOutgoingShares(pl, sharingOutbox, env);
|
|
1214
1412
|
const sharingStateTree = await createSharingStateTree(pl, sharingState, env);
|
|
1215
|
-
const
|
|
1216
|
-
const
|
|
1217
|
-
const liveEnvelopes = createLiveEnvelopesComputable(
|
|
1218
|
-
return new MiddleLayer(env, driverKit, driverKit.signer, projects, templates, sharingOutbox, sharingState, openedProjects, projectListTC.tree, templateListTC.tree, outgoingTC.tree, sharingStateTree,
|
|
1413
|
+
const availableSharesTree = await createAvailableSharesTree(pl, env);
|
|
1414
|
+
const availableShares = createAvailableSharesComputable(availableSharesTree, sharingStateTree, pl.userResources.authUser);
|
|
1415
|
+
const liveEnvelopes = createLiveEnvelopesComputable(availableSharesTree);
|
|
1416
|
+
return new MiddleLayer(env, driverKit, driverKit.signer, projects, templates, sharingOutbox, sharingState, folders, openedProjects, projectListTC.tree, templateListTC.tree, foldersTC.tree, outgoingTC.tree, sharingStateTree, availableSharesTree, v2RegistryProvider, projectListTC.computable, templateListTC.computable, foldersTC.computable, outgoingTC.computable, availableShares, liveEnvelopes);
|
|
1219
1417
|
}
|
|
1220
1418
|
};
|
|
1419
|
+
/** Reads one stored template within the caller's transaction. A share carries its document on;
|
|
1420
|
+
* a duplicate carries the blob whole, so the copy says of itself what the original did. */
|
|
1421
|
+
async function readStoredTemplate(tx, id, rid) {
|
|
1422
|
+
const rd = await tx.getResourceData(rid, false);
|
|
1423
|
+
if (rd.data === void 0) throw new Error(`Template ${id} carries no document.`);
|
|
1424
|
+
const [label, description] = await Promise.all([tx.getKValueJson(rid, TemplateLabelKey), tx.getKValueJsonIfExists(rid, TemplateDescriptionKey).then(normalizeDescription)]);
|
|
1425
|
+
return {
|
|
1426
|
+
data: decodeStoredTemplateData(rd.data),
|
|
1427
|
+
label,
|
|
1428
|
+
...description === void 0 ? {} : { description }
|
|
1429
|
+
};
|
|
1430
|
+
}
|
|
1431
|
+
/** The label of every project in a project list, read within the caller's transaction. */
|
|
1432
|
+
async function existingProjectLabels(tx, listRid) {
|
|
1433
|
+
const listed = await listedById(tx, listRid);
|
|
1434
|
+
return (await Promise.all([...listed.values()].map(({ rid }) => tx.getKValueJson(rid, ProjectMetaKey)))).map((meta) => meta.label);
|
|
1435
|
+
}
|
|
1221
1436
|
//#endregion
|
|
1222
1437
|
export { MiddleLayer };
|
|
1223
1438
|
|