@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
package/dist/mutator/sharing.cjs
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
const require_project_model = require("../model/project_model.cjs");
|
|
2
2
|
const require_sharing_model = require("../model/sharing_model.cjs");
|
|
3
3
|
const require_project = require("./project.cjs");
|
|
4
|
+
let _milaboratories_pl_model_middle_layer = require("@milaboratories/pl-model-middle-layer");
|
|
4
5
|
let _milaboratories_pl_client = require("@milaboratories/pl-client");
|
|
5
6
|
let node_crypto = require("node:crypto");
|
|
6
7
|
//#region src/mutator/sharing.ts
|
|
@@ -25,39 +26,13 @@ function envelopeProjectFieldUuid(name) {
|
|
|
25
26
|
* @returns the new envelope resource and the generated `EnvelopeData`.
|
|
26
27
|
*/
|
|
27
28
|
async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
29
|
+
const sharedAt = Date.now();
|
|
30
|
+
const snapshots = await snapshotEnvelopeProjects(tx, sources, sharedAt);
|
|
30
31
|
const projects = {};
|
|
31
|
-
const snapshots =
|
|
32
|
-
for (const src of sources) {
|
|
33
|
-
const uuid = (0, node_crypto.randomUUID)();
|
|
34
|
-
if (src.kind === "fresh") {
|
|
35
|
-
const meta = await tx.getKValueJson(src.sourceRid, require_project_model.ProjectMetaKey);
|
|
36
|
-
const ref = await require_project.duplicateProject(tx, src.sourceRid, { label: meta.label });
|
|
37
|
-
projects[uuid] = {
|
|
38
|
-
label: meta.label,
|
|
39
|
-
source: src.projectId,
|
|
40
|
-
updatedAt: sharedAt
|
|
41
|
-
};
|
|
42
|
-
snapshots.push({
|
|
43
|
-
uuid,
|
|
44
|
-
ref
|
|
45
|
-
});
|
|
46
|
-
} else {
|
|
47
|
-
projects[uuid] = {
|
|
48
|
-
label: src.label,
|
|
49
|
-
source: src.projectId,
|
|
50
|
-
updatedAt: src.updatedAt
|
|
51
|
-
};
|
|
52
|
-
snapshots.push({
|
|
53
|
-
uuid,
|
|
54
|
-
ref: src.snapshotRid
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
32
|
+
for (const { uuid, project } of snapshots) projects[uuid] = project;
|
|
58
33
|
const data = {
|
|
59
34
|
schemaVersion: 2,
|
|
60
|
-
shareId,
|
|
35
|
+
shareId: require_sharing_model.newShareId(),
|
|
61
36
|
sharedAt,
|
|
62
37
|
expiresAt: params.expiresAt,
|
|
63
38
|
mode: params.mode,
|
|
@@ -68,12 +43,8 @@ async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
|
68
43
|
projects
|
|
69
44
|
}
|
|
70
45
|
};
|
|
71
|
-
const envelope = tx.createEphemeral(require_sharing_model.SharedEnvelopeResourceType, JSON.stringify(data));
|
|
72
|
-
for (const { uuid, ref } of snapshots) tx.createField((0, _milaboratories_pl_client.field)(envelope, envelopeProjectField(uuid)), "Input", ref);
|
|
73
|
-
tx.lockInputs(envelope);
|
|
74
|
-
tx.createField((0, _milaboratories_pl_client.field)(outboxRid, shareId), "Dynamic", envelope);
|
|
75
46
|
return {
|
|
76
|
-
envelope,
|
|
47
|
+
envelope: sealEnvelope(tx, outboxRid, data, snapshots),
|
|
77
48
|
data
|
|
78
49
|
};
|
|
79
50
|
}
|
|
@@ -91,8 +62,8 @@ async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
|
91
62
|
function buildTemplateShareEnvelope(tx, outboxRid, template, params) {
|
|
92
63
|
const data = {
|
|
93
64
|
schemaVersion: 2,
|
|
94
|
-
shareId:
|
|
95
|
-
sharedAt:
|
|
65
|
+
shareId: require_sharing_model.newShareId(),
|
|
66
|
+
sharedAt: Date.now(),
|
|
96
67
|
expiresAt: params.expiresAt,
|
|
97
68
|
mode: "read-only",
|
|
98
69
|
sender: params.sender,
|
|
@@ -100,77 +71,160 @@ function buildTemplateShareEnvelope(tx, outboxRid, template, params) {
|
|
|
100
71
|
payload: {
|
|
101
72
|
kind: "template",
|
|
102
73
|
document: template.document,
|
|
74
|
+
source: template.source,
|
|
103
75
|
label: template.label,
|
|
76
|
+
...template.description === void 0 ? {} : { description: template.description },
|
|
104
77
|
from: params.sender
|
|
105
78
|
}
|
|
106
79
|
};
|
|
107
|
-
const envelope = tx.createEphemeral(require_sharing_model.SharedEnvelopeResourceType, JSON.stringify(data));
|
|
108
|
-
tx.createField((0, _milaboratories_pl_client.field)(outboxRid, data.shareId), "Dynamic", envelope);
|
|
109
80
|
return {
|
|
110
|
-
envelope,
|
|
81
|
+
envelope: sealEnvelope(tx, outboxRid, data, []),
|
|
111
82
|
data
|
|
112
83
|
};
|
|
113
84
|
}
|
|
114
85
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
86
|
+
* Builds one {@link SharedEnvelopeResourceType} carrying a folder subtree: the folders, every
|
|
87
|
+
* project in them snapshotted as a `project/{uuid}` field, and every template's document inline.
|
|
88
|
+
* Attaches it under `{shareId}` on the donor's outbox. The caller issues the grant — writable,
|
|
89
|
+
* because the recipient copies the project snapshots out — and commits.
|
|
90
|
+
*
|
|
91
|
+
* Folder ids are minted here and mean nothing outside this envelope: the recipient's own document
|
|
92
|
+
* mints its own. What travels is the shape of the subtree.
|
|
93
|
+
*
|
|
94
|
+
* @returns the new envelope resource and the generated `EnvelopeData`.
|
|
118
95
|
*/
|
|
119
|
-
function
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
96
|
+
async function buildFolderShareEnvelope(tx, outboxRid, subtree, params) {
|
|
97
|
+
const sharedAt = Date.now();
|
|
98
|
+
const snapshots = await snapshotEnvelopeProjects(tx, subtree.projects, sharedAt);
|
|
99
|
+
const projects = {};
|
|
100
|
+
for (const { uuid, project, source } of snapshots) projects[uuid] = {
|
|
101
|
+
...project,
|
|
102
|
+
folder: source.folder
|
|
103
|
+
};
|
|
104
|
+
const data = {
|
|
105
|
+
schemaVersion: 2,
|
|
106
|
+
shareId: require_sharing_model.newShareId(),
|
|
107
|
+
sharedAt,
|
|
108
|
+
expiresAt: params.expiresAt,
|
|
109
|
+
mode: "copy",
|
|
110
|
+
sender: params.sender,
|
|
111
|
+
title: params.title,
|
|
112
|
+
payload: {
|
|
113
|
+
kind: "folder",
|
|
114
|
+
source: subtree.source,
|
|
115
|
+
folders: subtree.folders,
|
|
116
|
+
projects,
|
|
117
|
+
templates: subtree.templates,
|
|
118
|
+
from: params.sender
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
envelope: sealEnvelope(tx, outboxRid, data, snapshots),
|
|
123
|
+
data
|
|
123
124
|
};
|
|
124
|
-
const value = tx.createJsonValue(acceptance);
|
|
125
|
-
tx.createField((0, _milaboratories_pl_client.field)(envelopeRid, require_sharing_model.acceptanceField(login)), "Dynamic", value);
|
|
126
125
|
}
|
|
127
126
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* share, not on the envelope instance.
|
|
127
|
+
* Puts a share out of this recipient's sight, as a dynamic field on their own SharingState.
|
|
128
|
+
* Keyed on the shareId, so a share that replaces this one — a new shareId — is shown again.
|
|
131
129
|
*/
|
|
132
|
-
function
|
|
133
|
-
const value = tx.createJsonValue(
|
|
134
|
-
|
|
130
|
+
function writeShareHidden(tx, stateRid, shareId, timestamp) {
|
|
131
|
+
const value = tx.createJsonValue({
|
|
132
|
+
hidden: true,
|
|
133
|
+
timestamp
|
|
134
|
+
});
|
|
135
|
+
tx.createField((0, _milaboratories_pl_client.field)(stateRid, require_sharing_model.hiddenField(shareId)), "Dynamic", value);
|
|
136
|
+
}
|
|
137
|
+
/** Brings a hidden share back into this recipient's list. */
|
|
138
|
+
function clearShareHidden(tx, stateRid, shareId) {
|
|
139
|
+
tx.removeField((0, _milaboratories_pl_client.field)(stateRid, require_sharing_model.hiddenField(shareId)));
|
|
135
140
|
}
|
|
136
141
|
/**
|
|
137
|
-
* Copies every project snapshot inside an envelope into the
|
|
138
|
-
* cross-color attach the backend permits.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
142
|
+
* Copies every project snapshot inside an envelope into the recipient's own project list — a
|
|
143
|
+
* cross-color attach the backend permits. The source is resolved against the envelope tree, not
|
|
144
|
+
* the recipient's own list.
|
|
145
|
+
*
|
|
146
|
+
* `label` names each copy from its snapshot's label. The caller picks the names against the
|
|
147
|
+
* destination the copies land in, because that is where they have to be free; left out, each copy
|
|
148
|
+
* keeps its snapshot's label.
|
|
149
|
+
*
|
|
150
|
+
* Each result carries the envelope field uuid it was copied from, because that uuid is what the
|
|
151
|
+
* payload names a project by — a folder share reads the folder of each copy off it. Pairing by
|
|
152
|
+
* position would happen to work today and quietly stop working the moment the order does.
|
|
141
153
|
*
|
|
142
|
-
* @returns
|
|
154
|
+
* @returns one entry per copied project: the envelope uuid it came from and its new id.
|
|
143
155
|
*/
|
|
144
|
-
async function copyEnvelopeProjectsIntoList(tx, envelopeRid, projectListRid,
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
async function copyEnvelopeProjectsIntoList(tx, envelopeRid, projectListRid, label) {
|
|
157
|
+
const sources = (await tx.getResourceData(envelopeRid, true)).fields.filter((f) => isEnvelopeProjectField(f.name)).flatMap((f) => (0, _milaboratories_pl_client.isNotNullSignedResourceId)(f.value) ? [{
|
|
158
|
+
uuid: envelopeProjectFieldUuid(f.name),
|
|
159
|
+
sourceRid: f.value
|
|
160
|
+
}] : []);
|
|
148
161
|
const created = [];
|
|
149
|
-
for (const sourceRid of
|
|
162
|
+
for (const { uuid, sourceRid } of sources) {
|
|
150
163
|
const sourceMeta = await tx.getKValueJson(sourceRid, require_project_model.ProjectMetaKey);
|
|
151
|
-
const newLabel =
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
const newLabel = label === void 0 ? sourceMeta.label : label(sourceMeta.label);
|
|
165
|
+
const newPrj = await require_project.duplicateProject(tx, sourceRid, {
|
|
166
|
+
...sourceMeta,
|
|
167
|
+
label: newLabel
|
|
168
|
+
});
|
|
154
169
|
tx.createField((0, _milaboratories_pl_client.field)(projectListRid, (0, node_crypto.randomUUID)()), "Dynamic", newPrj);
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
created.push({
|
|
171
|
+
uuid,
|
|
172
|
+
rid: await newPrj.globalId
|
|
173
|
+
});
|
|
157
174
|
}
|
|
158
175
|
return created;
|
|
159
176
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Snapshots each source project by reference, in the given order, under a freshly minted field
|
|
179
|
+
* uuid. The payload entry carries the project's label and description as they were at `sharedAt`,
|
|
180
|
+
* so a share list renders without traversing into the snapshot.
|
|
181
|
+
*/
|
|
182
|
+
async function snapshotEnvelopeProjects(tx, sources, sharedAt) {
|
|
183
|
+
const snapshots = [];
|
|
184
|
+
for (const source of sources) {
|
|
185
|
+
const meta = await tx.getKValueJson(source.sourceRid, require_project_model.ProjectMetaKey);
|
|
186
|
+
const ref = await require_project.duplicateProject(tx, source.sourceRid, meta);
|
|
187
|
+
const description = (0, _milaboratories_pl_model_middle_layer.normalizeDescription)(meta.description);
|
|
188
|
+
snapshots.push({
|
|
189
|
+
uuid: require_sharing_model.newProjectFieldUuid(),
|
|
190
|
+
ref,
|
|
191
|
+
source,
|
|
192
|
+
project: {
|
|
193
|
+
label: meta.label,
|
|
194
|
+
source: source.projectId,
|
|
195
|
+
updatedAt: sharedAt,
|
|
196
|
+
...description === void 0 ? {} : { description }
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return snapshots;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Creates the envelope with its immutable `data`, attaches each snapshot as a `project/{uuid}`
|
|
204
|
+
* input field, and hangs the envelope on the donor's outbox under its shareId.
|
|
205
|
+
*
|
|
206
|
+
* A payload that carries project snapshots has its input set sealed one-way, even when the
|
|
207
|
+
* subtree it describes holds none, because such an envelope is granted writable. A template
|
|
208
|
+
* payload has no input set to seal. The outbox attach happens in the same transaction, so the
|
|
209
|
+
* held-resource rule keeps the ephemeral envelope alive.
|
|
210
|
+
*/
|
|
211
|
+
function sealEnvelope(tx, outboxRid, data, snapshots) {
|
|
212
|
+
const envelope = tx.createEphemeral(require_sharing_model.SharedEnvelopeResourceType, JSON.stringify(data));
|
|
213
|
+
for (const { uuid, ref } of snapshots) tx.createField((0, _milaboratories_pl_client.field)(envelope, envelopeProjectField(uuid)), "Input", ref);
|
|
214
|
+
if (data.payload.kind !== "template") tx.lockInputs(envelope);
|
|
215
|
+
tx.createField((0, _milaboratories_pl_client.field)(outboxRid, data.shareId), "Dynamic", envelope);
|
|
216
|
+
return envelope;
|
|
163
217
|
}
|
|
164
218
|
//#endregion
|
|
165
219
|
exports.EnvelopeProjectFieldPrefix = EnvelopeProjectFieldPrefix;
|
|
220
|
+
exports.buildFolderShareEnvelope = buildFolderShareEnvelope;
|
|
166
221
|
exports.buildShareEnvelope = buildShareEnvelope;
|
|
167
222
|
exports.buildTemplateShareEnvelope = buildTemplateShareEnvelope;
|
|
223
|
+
exports.clearShareHidden = clearShareHidden;
|
|
168
224
|
exports.copyEnvelopeProjectsIntoList = copyEnvelopeProjectsIntoList;
|
|
169
225
|
exports.envelopeProjectField = envelopeProjectField;
|
|
170
226
|
exports.envelopeProjectFieldUuid = envelopeProjectFieldUuid;
|
|
171
227
|
exports.isEnvelopeProjectField = isEnvelopeProjectField;
|
|
172
|
-
exports.
|
|
173
|
-
exports.writeEnvelopeAcceptance = writeEnvelopeAcceptance;
|
|
174
|
-
exports.writeSharingDecision = writeSharingDecision;
|
|
228
|
+
exports.writeShareHidden = writeShareHidden;
|
|
175
229
|
|
|
176
230
|
//# sourceMappingURL=sharing.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharing.cjs","names":["newShareId","ProjectMetaKey","duplicateProject","SharedEnvelopeResourceType","acceptanceField","decisionField","isNotNullSignedResourceId"],"sources":["../../src/mutator/sharing.ts"],"sourcesContent":["import type { PlTransaction, ResourceRef, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { field, isNotNullSignedResourceId, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { ProjectMeta } from \"@milaboratories/pl-model-middle-layer\";\nimport type { ProjectId, ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport { ProjectMetaKey } from \"../model/project_model\";\nimport { duplicateProject } from \"./project\";\nimport type {\n EnvelopeData,\n EnvelopeAcceptance,\n EnvelopeMode,\n EnvelopeProject,\n ProjectFieldUuid,\n ShareId,\n SharingDecision,\n} from \"../model/sharing_model\";\nimport {\n EnvelopeSchemaVersionCurrent,\n SharedEnvelopeResourceType,\n acceptanceField,\n decisionField,\n newShareId,\n} from \"../model/sharing_model\";\n\n/** Field name carrying a project snapshot inside a {@link SharedEnvelopeResourceType}. */\nexport const EnvelopeProjectFieldPrefix = \"project/\";\nexport const envelopeProjectField = (uuid: ProjectFieldUuid) =>\n `${EnvelopeProjectFieldPrefix}${uuid}`;\n\n/** True for an envelope field that carries a project snapshot. */\nexport function isEnvelopeProjectField(name: string): boolean {\n return name.startsWith(EnvelopeProjectFieldPrefix);\n}\n\n/** Extracts the project field uuid from a `project/{uuid}` field name. */\nexport function envelopeProjectFieldUuid(name: string): ProjectFieldUuid {\n return name.slice(EnvelopeProjectFieldPrefix.length) as ProjectFieldUuid;\n}\n\n//\n// Donor side\n//\n\n/**\n * One project going into an envelope: `fresh` snapshots a live source (normal path); `carry`\n * re-attaches an existing snapshot (change's \"keep\", or an \"update\" whose source is gone).\n */\nexport type EnvelopeProjectSource =\n | { kind: \"fresh\"; projectId: ProjectId; sourceRid: SignedResourceId }\n | {\n kind: \"carry\";\n projectId: ProjectId;\n label: string;\n snapshotRid: SignedResourceId;\n /** ms epoch the carried snapshot was last taken; preserved so \"keep\" keeps its timestamp. */\n updatedAt: number;\n };\n\n/**\n * Builds one {@link SharedEnvelopeResourceType} on the donor side inside the given write\n * transaction: snapshots each source project by reference, seals the envelope with its\n * immutable {@link EnvelopeData}, and attaches the envelope under `{shareId}` on the donor's\n * outbox. The caller is responsible for issuing the per-recipient grants and committing the\n * transaction — keeping create + grant atomic.\n *\n * @returns the new envelope resource and the generated `EnvelopeData`.\n */\nexport async function buildShareEnvelope(\n tx: PlTransaction,\n outboxRid: SignedResourceId,\n sources: EnvelopeProjectSource[],\n params: {\n mode: EnvelopeMode;\n sender: string;\n title: string;\n /** ms epoch; sharedAt + ttl for a targeted share, null for share-with-everybody. */\n expiresAt: number | null;\n /** Existing shareId for a change; a fresh one is minted when omitted. */\n shareId?: ShareId;\n sharedAt?: number;\n },\n): Promise<{ envelope: ResourceRef; data: EnvelopeData }> {\n const shareId = params.shareId ?? newShareId();\n const sharedAt = params.sharedAt ?? Date.now();\n\n // Snapshot (fresh) or re-attach (carry) each project, collecting its metadata for the pack.\n const projects: Record<ProjectFieldUuid, EnvelopeProject> = {};\n const snapshots: { uuid: ProjectFieldUuid; ref: ResourceRef | SignedResourceId }[] = [];\n for (const src of sources) {\n const uuid = randomUUID() as ProjectFieldUuid;\n if (src.kind === \"fresh\") {\n const meta = await tx.getKValueJson<ProjectMeta>(src.sourceRid, ProjectMetaKey);\n const ref = await duplicateProject(tx, src.sourceRid, { label: meta.label });\n projects[uuid] = { label: meta.label, source: src.projectId, updatedAt: sharedAt }; // (re)snapshotted now\n snapshots.push({ uuid, ref });\n } else {\n // Re-attach the prior snapshot; the new envelope references it before the old one is\n // detached in the same tx, so it stays alive. Label and timestamp carry unchanged.\n projects[uuid] = { label: src.label, source: src.projectId, updatedAt: src.updatedAt };\n snapshots.push({ uuid, ref: src.snapshotRid });\n }\n }\n\n const data: EnvelopeData = {\n schemaVersion: EnvelopeSchemaVersionCurrent,\n shareId,\n sharedAt,\n expiresAt: params.expiresAt,\n mode: params.mode,\n sender: params.sender,\n title: params.title,\n payload: { kind: \"projects\", projects },\n };\n\n // Immutable data set once at creation, never altered.\n const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));\n\n // Attach the project snapshots as Input fields, then seal the input set one-way.\n for (const { uuid, ref } of snapshots) {\n tx.createField(field(envelope, envelopeProjectField(uuid)), \"Input\", ref);\n }\n tx.lockInputs(envelope);\n\n // Attach to the outbox under {shareId} in the same transaction so the held-resource rule\n // keeps the ephemeral envelope alive.\n tx.createField(field(outboxRid, shareId), \"Dynamic\", envelope);\n\n return { envelope, data };\n}\n\n/**\n * Builds one {@link SharedEnvelopeResourceType} carrying a template document on the donor side,\n * and attaches it under `{shareId}` on the donor's outbox. The caller issues the grant — always\n * read-only — and commits, keeping create + grant atomic.\n *\n * There is nothing to snapshot and no input field to seal: the document is the whole payload and\n * rides in the envelope's immutable `data`, which is also why the recipient needs no write access\n * (it copies no resource out of the envelope).\n *\n * @returns the new envelope resource and the generated `EnvelopeData`.\n */\nexport function buildTemplateShareEnvelope(\n tx: PlTransaction,\n outboxRid: SignedResourceId,\n template: { document: ProjectTemplateV1; label: string },\n params: {\n sender: string;\n title: string;\n /** ms epoch; sharedAt + ttl for a targeted share, null for share-with-everybody. */\n expiresAt: number | null;\n /** Existing shareId for a change; a fresh one is minted when omitted. */\n shareId?: ShareId;\n sharedAt?: number;\n },\n): { envelope: ResourceRef; data: EnvelopeData } {\n const data: EnvelopeData = {\n schemaVersion: EnvelopeSchemaVersionCurrent,\n shareId: params.shareId ?? newShareId(),\n sharedAt: params.sharedAt ?? Date.now(),\n expiresAt: params.expiresAt,\n mode: \"read-only\",\n sender: params.sender,\n title: params.title,\n payload: {\n kind: \"template\",\n document: template.document,\n label: template.label,\n from: params.sender,\n },\n };\n\n // Immutable data set once at creation, never altered.\n const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));\n\n // Attach to the outbox under {shareId} in the same transaction so the held-resource rule\n // keeps the ephemeral envelope alive.\n tx.createField(field(outboxRid, data.shareId), \"Dynamic\", envelope);\n\n return { envelope, data };\n}\n\n/**\n * Records a response onto the envelope as a dynamic `acceptance/{login}` field: the acceptor\n * writing their own decision (their writable grant permits it), or the donor transferring an\n * existing record onto a changed envelope. Accepts the envelope by ref or id.\n */\nexport function writeEnvelopeAcceptance(\n tx: PlTransaction,\n envelopeRid: ResourceRef | SignedResourceId,\n login: string,\n action: EnvelopeAcceptance[\"action\"],\n timestamp: number,\n): void {\n const acceptance: EnvelopeAcceptance = { action, timestamp };\n const value = tx.createJsonValue(acceptance);\n tx.createField(field(envelopeRid, acceptanceField(login)), \"Dynamic\", value);\n}\n\n//\n// Acceptor side\n//\n\n/**\n * Records the acceptor's decision for a handled share on the acceptor's SharingState as a\n * dynamic `decision/{shareId}` field. Keyed on the logical shareId so discovery dedups on the\n * share, not on the envelope instance.\n */\nexport function writeSharingDecision(\n tx: PlTransaction,\n stateRid: SignedResourceId,\n shareId: ShareId,\n decision: SharingDecision,\n): void {\n const value = tx.createJsonValue(decision);\n tx.createField(field(stateRid, decisionField(shareId)), \"Dynamic\", value);\n}\n\n/**\n * Copies every project snapshot inside an envelope into the acceptor's own project list — a\n * cross-color attach the backend permits. Mirrors {@link duplicateProject}'s `rename` contract,\n * but resolves the source against the envelope tree (not the acceptor's own list), so the\n * wrapper cannot be reused.\n *\n * @returns ids of the projects created in the acceptor's list.\n */\nexport async function copyEnvelopeProjectsIntoList(\n tx: PlTransaction,\n envelopeRid: SignedResourceId,\n projectListRid: SignedResourceId,\n rename?: (previousLabel: string, existingLabels: string[]) => string,\n): Promise<SignedResourceId[]> {\n // Read the acceptor's existing project labels once (own color, no relaxation).\n const projectListData = await tx.getResourceData(projectListRid, true);\n const existingRids = projectListData.fields.map((f) => f.value).filter(isNotNullSignedResourceId);\n const existingLabels = (\n await Promise.all(existingRids.map((rid) => tx.getKValueJson<ProjectMeta>(rid, ProjectMetaKey)))\n ).map((m) => m.label);\n\n // Enumerate the envelope's project/{uuid} input field values (signed envelope-colored ids).\n const envelopeData = await tx.getResourceData(envelopeRid, true);\n const sourceRids = envelopeData.fields\n .filter((f) => isEnvelopeProjectField(f.name))\n .map((f) => f.value)\n .filter(isNotNullSignedResourceId);\n\n const created: SignedResourceId[] = [];\n for (const sourceRid of sourceRids) {\n const sourceMeta = await tx.getKValueJson<ProjectMeta>(sourceRid, ProjectMetaKey);\n const newLabel = rename ? rename(sourceMeta.label, existingLabels) : sourceMeta.label;\n existingLabels.push(newLabel);\n\n // Cross-color attach: a new UserProject in the acceptor's color whose fields point at\n // envelope-colored resources. Fails with PermissionDenied: color mismatch on a backend\n // that lacks crossTreeRefs:v1.\n const newPrj = await duplicateProject(tx, sourceRid, { label: newLabel });\n tx.createField(field(projectListRid, randomUUID()), \"Dynamic\", newPrj);\n\n const signedRid = await newPrj.globalId;\n created.push(signedRid);\n }\n\n return created;\n}\n\n/** String-form ids of the projects created by {@link copyEnvelopeProjectsIntoList}. */\nexport function resourceIdsToStrings(ids: SignedResourceId[]): string[] {\n return ids.map((id) => resourceIdToString(id));\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"sharing.cjs","names":["newShareId","hiddenField","ProjectMetaKey","duplicateProject","newProjectFieldUuid","SharedEnvelopeResourceType"],"sources":["../../src/mutator/sharing.ts"],"sourcesContent":["import type { PlTransaction, ResourceRef, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { field, isNotNullSignedResourceId } from \"@milaboratories/pl-client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { FolderId, ProjectMeta } from \"@milaboratories/pl-model-middle-layer\";\nimport { normalizeDescription } from \"@milaboratories/pl-model-middle-layer\";\nimport type { ProjectId, ProjectTemplateV1, TemplateId } from \"@milaboratories/pl-model-common\";\nimport { ProjectMetaKey } from \"../model/project_model\";\nimport { duplicateProject } from \"./project\";\nimport type {\n EnvelopeData,\n EnvelopeFolder,\n EnvelopeFolderId,\n EnvelopeFolderProject,\n EnvelopeFolderTemplate,\n EnvelopeMode,\n EnvelopeProject,\n ProjectFieldUuid,\n ShareId,\n ShareHidden,\n} from \"../model/sharing_model\";\nimport {\n EnvelopeSchemaVersionCurrent,\n SharedEnvelopeResourceType,\n hiddenField,\n newProjectFieldUuid,\n newShareId,\n} from \"../model/sharing_model\";\n\n/** Field name carrying a project snapshot inside a {@link SharedEnvelopeResourceType}. */\nexport const EnvelopeProjectFieldPrefix = \"project/\";\nexport const envelopeProjectField = (uuid: ProjectFieldUuid) =>\n `${EnvelopeProjectFieldPrefix}${uuid}`;\n\n/** True for an envelope field that carries a project snapshot. */\nexport function isEnvelopeProjectField(name: string): boolean {\n return name.startsWith(EnvelopeProjectFieldPrefix);\n}\n\n/** Extracts the project field uuid from a `project/{uuid}` field name. */\nexport function envelopeProjectFieldUuid(name: string): ProjectFieldUuid {\n return name.slice(EnvelopeProjectFieldPrefix.length) as ProjectFieldUuid;\n}\n\n//\n// Donor side\n//\n\n/** One live project going into an envelope; it is snapshotted as the envelope is built. */\nexport type EnvelopeProjectSource = { projectId: ProjectId; sourceRid: SignedResourceId };\n\n/**\n * Builds one {@link SharedEnvelopeResourceType} on the donor side inside the given write\n * transaction: snapshots each source project by reference, seals the envelope with its\n * immutable {@link EnvelopeData}, and attaches the envelope under `{shareId}` on the donor's\n * outbox. The caller is responsible for issuing the per-recipient grants and committing the\n * transaction — keeping create + grant atomic.\n *\n * @returns the new envelope resource and the generated `EnvelopeData`.\n */\nexport async function buildShareEnvelope(\n tx: PlTransaction,\n outboxRid: SignedResourceId,\n sources: EnvelopeProjectSource[],\n params: {\n mode: EnvelopeMode;\n sender: string;\n title: string;\n /** ms epoch; sharedAt + ttl for a targeted share, null for share-with-everybody. */\n expiresAt: number | null;\n },\n): Promise<{ envelope: ResourceRef; data: EnvelopeData }> {\n const sharedAt = Date.now();\n const snapshots = await snapshotEnvelopeProjects(tx, sources, sharedAt);\n\n const projects: Record<ProjectFieldUuid, EnvelopeProject> = {};\n for (const { uuid, project } of snapshots) projects[uuid] = project;\n\n const data: EnvelopeData = {\n schemaVersion: EnvelopeSchemaVersionCurrent,\n shareId: newShareId(),\n sharedAt,\n expiresAt: params.expiresAt,\n mode: params.mode,\n sender: params.sender,\n title: params.title,\n payload: { kind: \"projects\", projects },\n };\n\n return { envelope: sealEnvelope(tx, outboxRid, data, snapshots), data };\n}\n\n/**\n * Builds one {@link SharedEnvelopeResourceType} carrying a template document on the donor side,\n * and attaches it under `{shareId}` on the donor's outbox. The caller issues the grant — always\n * read-only — and commits, keeping create + grant atomic.\n *\n * There is nothing to snapshot and no input field to seal: the document is the whole payload and\n * rides in the envelope's immutable `data`, which is also why the recipient needs no write access\n * (it copies no resource out of the envelope).\n *\n * @returns the new envelope resource and the generated `EnvelopeData`.\n */\nexport function buildTemplateShareEnvelope(\n tx: PlTransaction,\n outboxRid: SignedResourceId,\n template: {\n document: ProjectTemplateV1;\n label: string;\n description?: string;\n source: TemplateId;\n },\n params: {\n sender: string;\n title: string;\n /** ms epoch; sharedAt + ttl for a targeted share, null for share-with-everybody. */\n expiresAt: number | null;\n },\n): { envelope: ResourceRef; data: EnvelopeData } {\n const data: EnvelopeData = {\n schemaVersion: EnvelopeSchemaVersionCurrent,\n shareId: newShareId(),\n sharedAt: Date.now(),\n expiresAt: params.expiresAt,\n mode: \"read-only\",\n sender: params.sender,\n title: params.title,\n payload: {\n kind: \"template\",\n document: template.document,\n source: template.source,\n label: template.label,\n ...(template.description === undefined ? {} : { description: template.description }),\n from: params.sender,\n },\n };\n\n return { envelope: sealEnvelope(tx, outboxRid, data, []), data };\n}\n\n/**\n * Builds one {@link SharedEnvelopeResourceType} carrying a folder subtree: the folders, every\n * project in them snapshotted as a `project/{uuid}` field, and every template's document inline.\n * Attaches it under `{shareId}` on the donor's outbox. The caller issues the grant — writable,\n * because the recipient copies the project snapshots out — and commits.\n *\n * Folder ids are minted here and mean nothing outside this envelope: the recipient's own document\n * mints its own. What travels is the shape of the subtree.\n *\n * @returns the new envelope resource and the generated `EnvelopeData`.\n */\nexport async function buildFolderShareEnvelope(\n tx: PlTransaction,\n outboxRid: SignedResourceId,\n subtree: EnvelopeFolderSubtree,\n params: {\n sender: string;\n title: string;\n /** ms epoch; sharedAt + ttl for a targeted share, null for share-with-everybody. */\n expiresAt: number | null;\n },\n): Promise<{ envelope: ResourceRef; data: EnvelopeData }> {\n const sharedAt = Date.now();\n const snapshots = await snapshotEnvelopeProjects(tx, subtree.projects, sharedAt);\n\n const projects: Record<ProjectFieldUuid, EnvelopeFolderProject> = {};\n for (const { uuid, project, source } of snapshots)\n projects[uuid] = { ...project, folder: source.folder };\n\n const data: EnvelopeData = {\n schemaVersion: EnvelopeSchemaVersionCurrent,\n shareId: newShareId(),\n sharedAt,\n expiresAt: params.expiresAt,\n mode: \"copy\",\n sender: params.sender,\n title: params.title,\n payload: {\n kind: \"folder\",\n source: subtree.source,\n folders: subtree.folders,\n projects,\n templates: subtree.templates,\n from: params.sender,\n },\n };\n\n return { envelope: sealEnvelope(tx, outboxRid, data, snapshots), data };\n}\n\n/** A folder subtree ready to be sealed into an envelope: the folders under their envelope-local\n * ids, the live projects to snapshot, and the templates to carry whole. */\nexport interface EnvelopeFolderSubtree {\n /** Donor's own id of the shared folder, carried so a later share of it finds this one. */\n source: FolderId;\n folders: Record<EnvelopeFolderId, EnvelopeFolder>;\n projects: (EnvelopeProjectSource & { folder: EnvelopeFolderId })[];\n templates: EnvelopeFolderTemplate[];\n}\n\n//\n// Recipient side\n//\n\n/**\n * Puts a share out of this recipient's sight, as a dynamic field on their own SharingState.\n * Keyed on the shareId, so a share that replaces this one — a new shareId — is shown again.\n */\nexport function writeShareHidden(\n tx: PlTransaction,\n stateRid: SignedResourceId,\n shareId: ShareId,\n timestamp: number,\n): void {\n const value = tx.createJsonValue({ hidden: true, timestamp } satisfies ShareHidden);\n tx.createField(field(stateRid, hiddenField(shareId)), \"Dynamic\", value);\n}\n\n/** Brings a hidden share back into this recipient's list. */\nexport function clearShareHidden(\n tx: PlTransaction,\n stateRid: SignedResourceId,\n shareId: ShareId,\n): void {\n tx.removeField(field(stateRid, hiddenField(shareId)));\n}\n\n/**\n * Copies every project snapshot inside an envelope into the recipient's own project list — a\n * cross-color attach the backend permits. The source is resolved against the envelope tree, not\n * the recipient's own list.\n *\n * `label` names each copy from its snapshot's label. The caller picks the names against the\n * destination the copies land in, because that is where they have to be free; left out, each copy\n * keeps its snapshot's label.\n *\n * Each result carries the envelope field uuid it was copied from, because that uuid is what the\n * payload names a project by — a folder share reads the folder of each copy off it. Pairing by\n * position would happen to work today and quietly stop working the moment the order does.\n *\n * @returns one entry per copied project: the envelope uuid it came from and its new id.\n */\nexport async function copyEnvelopeProjectsIntoList(\n tx: PlTransaction,\n envelopeRid: SignedResourceId,\n projectListRid: SignedResourceId,\n label?: (sourceLabel: string) => string,\n): Promise<{ uuid: ProjectFieldUuid; rid: SignedResourceId }[]> {\n // Enumerate the envelope's project/{uuid} input fields (signed envelope-colored ids).\n const envelopeData = await tx.getResourceData(envelopeRid, true);\n const sources = envelopeData.fields\n .filter((f) => isEnvelopeProjectField(f.name))\n .flatMap((f) =>\n isNotNullSignedResourceId(f.value)\n ? [{ uuid: envelopeProjectFieldUuid(f.name), sourceRid: f.value }]\n : [],\n );\n\n const created: { uuid: ProjectFieldUuid; rid: SignedResourceId }[] = [];\n for (const { uuid, sourceRid } of sources) {\n const sourceMeta = await tx.getKValueJson<ProjectMeta>(sourceRid, ProjectMetaKey);\n const newLabel = label === undefined ? sourceMeta.label : label(sourceMeta.label);\n\n // Cross-color attach: a new UserProject in the recipient's color whose fields point at\n // envelope-colored resources. Fails with PermissionDenied: color mismatch on a backend\n // that lacks crossTreeRefs:v1.\n const newPrj = await duplicateProject(tx, sourceRid, { ...sourceMeta, label: newLabel });\n tx.createField(field(projectListRid, randomUUID()), \"Dynamic\", newPrj);\n\n created.push({ uuid, rid: await newPrj.globalId });\n }\n\n return created;\n}\n\n//\n// Internals\n//\n\n/** One source project snapshotted for an envelope: the field uuid it rides under, the snapshot\n * itself, and what the payload says about it. */\ntype EnvelopeSnapshot<Source extends EnvelopeProjectSource> = {\n uuid: ProjectFieldUuid;\n ref: ResourceRef;\n source: Source;\n project: EnvelopeProject;\n};\n\n/**\n * Snapshots each source project by reference, in the given order, under a freshly minted field\n * uuid. The payload entry carries the project's label and description as they were at `sharedAt`,\n * so a share list renders without traversing into the snapshot.\n */\nasync function snapshotEnvelopeProjects<Source extends EnvelopeProjectSource>(\n tx: PlTransaction,\n sources: readonly Source[],\n sharedAt: number,\n): Promise<EnvelopeSnapshot<Source>[]> {\n const snapshots: EnvelopeSnapshot<Source>[] = [];\n for (const source of sources) {\n const meta = await tx.getKValueJson<ProjectMeta>(source.sourceRid, ProjectMetaKey);\n const ref = await duplicateProject(tx, source.sourceRid, meta);\n const description = normalizeDescription(meta.description);\n snapshots.push({\n uuid: newProjectFieldUuid(),\n ref,\n source,\n project: {\n label: meta.label,\n source: source.projectId,\n updatedAt: sharedAt,\n ...(description === undefined ? {} : { description }),\n },\n });\n }\n return snapshots;\n}\n\n/**\n * Creates the envelope with its immutable `data`, attaches each snapshot as a `project/{uuid}`\n * input field, and hangs the envelope on the donor's outbox under its shareId.\n *\n * A payload that carries project snapshots has its input set sealed one-way, even when the\n * subtree it describes holds none, because such an envelope is granted writable. A template\n * payload has no input set to seal. The outbox attach happens in the same transaction, so the\n * held-resource rule keeps the ephemeral envelope alive.\n */\nfunction sealEnvelope(\n tx: PlTransaction,\n outboxRid: SignedResourceId,\n data: EnvelopeData,\n snapshots: readonly { uuid: ProjectFieldUuid; ref: ResourceRef }[],\n): ResourceRef {\n const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));\n\n for (const { uuid, ref } of snapshots)\n tx.createField(field(envelope, envelopeProjectField(uuid)), \"Input\", ref);\n if (data.payload.kind !== \"template\") tx.lockInputs(envelope);\n\n tx.createField(field(outboxRid, data.shareId), \"Dynamic\", envelope);\n return envelope;\n}\n"],"mappings":"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"}
|
package/dist/mutator/sharing.js
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { ProjectMetaKey } from "../model/project_model.js";
|
|
2
|
-
import { SharedEnvelopeResourceType,
|
|
2
|
+
import { SharedEnvelopeResourceType, hiddenField, newProjectFieldUuid, newShareId } from "../model/sharing_model.js";
|
|
3
3
|
import { duplicateProject } from "./project.js";
|
|
4
|
-
import {
|
|
4
|
+
import { normalizeDescription } from "@milaboratories/pl-model-middle-layer";
|
|
5
|
+
import { field, isNotNullSignedResourceId } from "@milaboratories/pl-client";
|
|
5
6
|
import { randomUUID } from "node:crypto";
|
|
6
7
|
//#region src/mutator/sharing.ts
|
|
7
8
|
/** Field name carrying a project snapshot inside a {@link SharedEnvelopeResourceType}. */
|
|
@@ -25,39 +26,13 @@ function envelopeProjectFieldUuid(name) {
|
|
|
25
26
|
* @returns the new envelope resource and the generated `EnvelopeData`.
|
|
26
27
|
*/
|
|
27
28
|
async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
28
|
-
const
|
|
29
|
-
const
|
|
29
|
+
const sharedAt = Date.now();
|
|
30
|
+
const snapshots = await snapshotEnvelopeProjects(tx, sources, sharedAt);
|
|
30
31
|
const projects = {};
|
|
31
|
-
const snapshots =
|
|
32
|
-
for (const src of sources) {
|
|
33
|
-
const uuid = randomUUID();
|
|
34
|
-
if (src.kind === "fresh") {
|
|
35
|
-
const meta = await tx.getKValueJson(src.sourceRid, ProjectMetaKey);
|
|
36
|
-
const ref = await duplicateProject(tx, src.sourceRid, { label: meta.label });
|
|
37
|
-
projects[uuid] = {
|
|
38
|
-
label: meta.label,
|
|
39
|
-
source: src.projectId,
|
|
40
|
-
updatedAt: sharedAt
|
|
41
|
-
};
|
|
42
|
-
snapshots.push({
|
|
43
|
-
uuid,
|
|
44
|
-
ref
|
|
45
|
-
});
|
|
46
|
-
} else {
|
|
47
|
-
projects[uuid] = {
|
|
48
|
-
label: src.label,
|
|
49
|
-
source: src.projectId,
|
|
50
|
-
updatedAt: src.updatedAt
|
|
51
|
-
};
|
|
52
|
-
snapshots.push({
|
|
53
|
-
uuid,
|
|
54
|
-
ref: src.snapshotRid
|
|
55
|
-
});
|
|
56
|
-
}
|
|
57
|
-
}
|
|
32
|
+
for (const { uuid, project } of snapshots) projects[uuid] = project;
|
|
58
33
|
const data = {
|
|
59
34
|
schemaVersion: 2,
|
|
60
|
-
shareId,
|
|
35
|
+
shareId: newShareId(),
|
|
61
36
|
sharedAt,
|
|
62
37
|
expiresAt: params.expiresAt,
|
|
63
38
|
mode: params.mode,
|
|
@@ -68,12 +43,8 @@ async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
|
68
43
|
projects
|
|
69
44
|
}
|
|
70
45
|
};
|
|
71
|
-
const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));
|
|
72
|
-
for (const { uuid, ref } of snapshots) tx.createField(field(envelope, envelopeProjectField(uuid)), "Input", ref);
|
|
73
|
-
tx.lockInputs(envelope);
|
|
74
|
-
tx.createField(field(outboxRid, shareId), "Dynamic", envelope);
|
|
75
46
|
return {
|
|
76
|
-
envelope,
|
|
47
|
+
envelope: sealEnvelope(tx, outboxRid, data, snapshots),
|
|
77
48
|
data
|
|
78
49
|
};
|
|
79
50
|
}
|
|
@@ -91,8 +62,8 @@ async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
|
91
62
|
function buildTemplateShareEnvelope(tx, outboxRid, template, params) {
|
|
92
63
|
const data = {
|
|
93
64
|
schemaVersion: 2,
|
|
94
|
-
shareId:
|
|
95
|
-
sharedAt:
|
|
65
|
+
shareId: newShareId(),
|
|
66
|
+
sharedAt: Date.now(),
|
|
96
67
|
expiresAt: params.expiresAt,
|
|
97
68
|
mode: "read-only",
|
|
98
69
|
sender: params.sender,
|
|
@@ -100,68 +71,151 @@ function buildTemplateShareEnvelope(tx, outboxRid, template, params) {
|
|
|
100
71
|
payload: {
|
|
101
72
|
kind: "template",
|
|
102
73
|
document: template.document,
|
|
74
|
+
source: template.source,
|
|
103
75
|
label: template.label,
|
|
76
|
+
...template.description === void 0 ? {} : { description: template.description },
|
|
104
77
|
from: params.sender
|
|
105
78
|
}
|
|
106
79
|
};
|
|
107
|
-
const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));
|
|
108
|
-
tx.createField(field(outboxRid, data.shareId), "Dynamic", envelope);
|
|
109
80
|
return {
|
|
110
|
-
envelope,
|
|
81
|
+
envelope: sealEnvelope(tx, outboxRid, data, []),
|
|
111
82
|
data
|
|
112
83
|
};
|
|
113
84
|
}
|
|
114
85
|
/**
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
-
*
|
|
86
|
+
* Builds one {@link SharedEnvelopeResourceType} carrying a folder subtree: the folders, every
|
|
87
|
+
* project in them snapshotted as a `project/{uuid}` field, and every template's document inline.
|
|
88
|
+
* Attaches it under `{shareId}` on the donor's outbox. The caller issues the grant — writable,
|
|
89
|
+
* because the recipient copies the project snapshots out — and commits.
|
|
90
|
+
*
|
|
91
|
+
* Folder ids are minted here and mean nothing outside this envelope: the recipient's own document
|
|
92
|
+
* mints its own. What travels is the shape of the subtree.
|
|
93
|
+
*
|
|
94
|
+
* @returns the new envelope resource and the generated `EnvelopeData`.
|
|
118
95
|
*/
|
|
119
|
-
function
|
|
120
|
-
const
|
|
121
|
-
|
|
122
|
-
|
|
96
|
+
async function buildFolderShareEnvelope(tx, outboxRid, subtree, params) {
|
|
97
|
+
const sharedAt = Date.now();
|
|
98
|
+
const snapshots = await snapshotEnvelopeProjects(tx, subtree.projects, sharedAt);
|
|
99
|
+
const projects = {};
|
|
100
|
+
for (const { uuid, project, source } of snapshots) projects[uuid] = {
|
|
101
|
+
...project,
|
|
102
|
+
folder: source.folder
|
|
103
|
+
};
|
|
104
|
+
const data = {
|
|
105
|
+
schemaVersion: 2,
|
|
106
|
+
shareId: newShareId(),
|
|
107
|
+
sharedAt,
|
|
108
|
+
expiresAt: params.expiresAt,
|
|
109
|
+
mode: "copy",
|
|
110
|
+
sender: params.sender,
|
|
111
|
+
title: params.title,
|
|
112
|
+
payload: {
|
|
113
|
+
kind: "folder",
|
|
114
|
+
source: subtree.source,
|
|
115
|
+
folders: subtree.folders,
|
|
116
|
+
projects,
|
|
117
|
+
templates: subtree.templates,
|
|
118
|
+
from: params.sender
|
|
119
|
+
}
|
|
120
|
+
};
|
|
121
|
+
return {
|
|
122
|
+
envelope: sealEnvelope(tx, outboxRid, data, snapshots),
|
|
123
|
+
data
|
|
123
124
|
};
|
|
124
|
-
const value = tx.createJsonValue(acceptance);
|
|
125
|
-
tx.createField(field(envelopeRid, acceptanceField(login)), "Dynamic", value);
|
|
126
125
|
}
|
|
127
126
|
/**
|
|
128
|
-
*
|
|
129
|
-
*
|
|
130
|
-
* share, not on the envelope instance.
|
|
127
|
+
* Puts a share out of this recipient's sight, as a dynamic field on their own SharingState.
|
|
128
|
+
* Keyed on the shareId, so a share that replaces this one — a new shareId — is shown again.
|
|
131
129
|
*/
|
|
132
|
-
function
|
|
133
|
-
const value = tx.createJsonValue(
|
|
134
|
-
|
|
130
|
+
function writeShareHidden(tx, stateRid, shareId, timestamp) {
|
|
131
|
+
const value = tx.createJsonValue({
|
|
132
|
+
hidden: true,
|
|
133
|
+
timestamp
|
|
134
|
+
});
|
|
135
|
+
tx.createField(field(stateRid, hiddenField(shareId)), "Dynamic", value);
|
|
136
|
+
}
|
|
137
|
+
/** Brings a hidden share back into this recipient's list. */
|
|
138
|
+
function clearShareHidden(tx, stateRid, shareId) {
|
|
139
|
+
tx.removeField(field(stateRid, hiddenField(shareId)));
|
|
135
140
|
}
|
|
136
141
|
/**
|
|
137
|
-
* Copies every project snapshot inside an envelope into the
|
|
138
|
-
* cross-color attach the backend permits.
|
|
139
|
-
*
|
|
140
|
-
*
|
|
142
|
+
* Copies every project snapshot inside an envelope into the recipient's own project list — a
|
|
143
|
+
* cross-color attach the backend permits. The source is resolved against the envelope tree, not
|
|
144
|
+
* the recipient's own list.
|
|
145
|
+
*
|
|
146
|
+
* `label` names each copy from its snapshot's label. The caller picks the names against the
|
|
147
|
+
* destination the copies land in, because that is where they have to be free; left out, each copy
|
|
148
|
+
* keeps its snapshot's label.
|
|
149
|
+
*
|
|
150
|
+
* Each result carries the envelope field uuid it was copied from, because that uuid is what the
|
|
151
|
+
* payload names a project by — a folder share reads the folder of each copy off it. Pairing by
|
|
152
|
+
* position would happen to work today and quietly stop working the moment the order does.
|
|
141
153
|
*
|
|
142
|
-
* @returns
|
|
154
|
+
* @returns one entry per copied project: the envelope uuid it came from and its new id.
|
|
143
155
|
*/
|
|
144
|
-
async function copyEnvelopeProjectsIntoList(tx, envelopeRid, projectListRid,
|
|
145
|
-
const
|
|
146
|
-
|
|
147
|
-
|
|
156
|
+
async function copyEnvelopeProjectsIntoList(tx, envelopeRid, projectListRid, label) {
|
|
157
|
+
const sources = (await tx.getResourceData(envelopeRid, true)).fields.filter((f) => isEnvelopeProjectField(f.name)).flatMap((f) => isNotNullSignedResourceId(f.value) ? [{
|
|
158
|
+
uuid: envelopeProjectFieldUuid(f.name),
|
|
159
|
+
sourceRid: f.value
|
|
160
|
+
}] : []);
|
|
148
161
|
const created = [];
|
|
149
|
-
for (const sourceRid of
|
|
162
|
+
for (const { uuid, sourceRid } of sources) {
|
|
150
163
|
const sourceMeta = await tx.getKValueJson(sourceRid, ProjectMetaKey);
|
|
151
|
-
const newLabel =
|
|
152
|
-
|
|
153
|
-
|
|
164
|
+
const newLabel = label === void 0 ? sourceMeta.label : label(sourceMeta.label);
|
|
165
|
+
const newPrj = await duplicateProject(tx, sourceRid, {
|
|
166
|
+
...sourceMeta,
|
|
167
|
+
label: newLabel
|
|
168
|
+
});
|
|
154
169
|
tx.createField(field(projectListRid, randomUUID()), "Dynamic", newPrj);
|
|
155
|
-
|
|
156
|
-
|
|
170
|
+
created.push({
|
|
171
|
+
uuid,
|
|
172
|
+
rid: await newPrj.globalId
|
|
173
|
+
});
|
|
157
174
|
}
|
|
158
175
|
return created;
|
|
159
176
|
}
|
|
160
|
-
/**
|
|
161
|
-
|
|
162
|
-
|
|
177
|
+
/**
|
|
178
|
+
* Snapshots each source project by reference, in the given order, under a freshly minted field
|
|
179
|
+
* uuid. The payload entry carries the project's label and description as they were at `sharedAt`,
|
|
180
|
+
* so a share list renders without traversing into the snapshot.
|
|
181
|
+
*/
|
|
182
|
+
async function snapshotEnvelopeProjects(tx, sources, sharedAt) {
|
|
183
|
+
const snapshots = [];
|
|
184
|
+
for (const source of sources) {
|
|
185
|
+
const meta = await tx.getKValueJson(source.sourceRid, ProjectMetaKey);
|
|
186
|
+
const ref = await duplicateProject(tx, source.sourceRid, meta);
|
|
187
|
+
const description = normalizeDescription(meta.description);
|
|
188
|
+
snapshots.push({
|
|
189
|
+
uuid: newProjectFieldUuid(),
|
|
190
|
+
ref,
|
|
191
|
+
source,
|
|
192
|
+
project: {
|
|
193
|
+
label: meta.label,
|
|
194
|
+
source: source.projectId,
|
|
195
|
+
updatedAt: sharedAt,
|
|
196
|
+
...description === void 0 ? {} : { description }
|
|
197
|
+
}
|
|
198
|
+
});
|
|
199
|
+
}
|
|
200
|
+
return snapshots;
|
|
201
|
+
}
|
|
202
|
+
/**
|
|
203
|
+
* Creates the envelope with its immutable `data`, attaches each snapshot as a `project/{uuid}`
|
|
204
|
+
* input field, and hangs the envelope on the donor's outbox under its shareId.
|
|
205
|
+
*
|
|
206
|
+
* A payload that carries project snapshots has its input set sealed one-way, even when the
|
|
207
|
+
* subtree it describes holds none, because such an envelope is granted writable. A template
|
|
208
|
+
* payload has no input set to seal. The outbox attach happens in the same transaction, so the
|
|
209
|
+
* held-resource rule keeps the ephemeral envelope alive.
|
|
210
|
+
*/
|
|
211
|
+
function sealEnvelope(tx, outboxRid, data, snapshots) {
|
|
212
|
+
const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));
|
|
213
|
+
for (const { uuid, ref } of snapshots) tx.createField(field(envelope, envelopeProjectField(uuid)), "Input", ref);
|
|
214
|
+
if (data.payload.kind !== "template") tx.lockInputs(envelope);
|
|
215
|
+
tx.createField(field(outboxRid, data.shareId), "Dynamic", envelope);
|
|
216
|
+
return envelope;
|
|
163
217
|
}
|
|
164
218
|
//#endregion
|
|
165
|
-
export { EnvelopeProjectFieldPrefix, buildShareEnvelope, buildTemplateShareEnvelope, copyEnvelopeProjectsIntoList, envelopeProjectField, envelopeProjectFieldUuid, isEnvelopeProjectField,
|
|
219
|
+
export { EnvelopeProjectFieldPrefix, buildFolderShareEnvelope, buildShareEnvelope, buildTemplateShareEnvelope, clearShareHidden, copyEnvelopeProjectsIntoList, envelopeProjectField, envelopeProjectFieldUuid, isEnvelopeProjectField, writeShareHidden };
|
|
166
220
|
|
|
167
221
|
//# sourceMappingURL=sharing.js.map
|