@milaboratories/pl-middle-layer 1.71.16 → 1.73.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 +636 -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 +617 -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 +17 -17
- package/src/middle_layer/folders.test.ts +1076 -0
- package/src/middle_layer/folders.ts +1069 -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
|
@@ -1,10 +1,16 @@
|
|
|
1
1
|
import type { ResourceType, Role } from "@milaboratories/pl-client";
|
|
2
2
|
import { Role as RoleEnum } from "@milaboratories/pl-client";
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
Branded,
|
|
5
|
+
ProjectId,
|
|
6
|
+
ProjectTemplateV1,
|
|
7
|
+
TemplateId,
|
|
8
|
+
} from "@milaboratories/pl-model-common";
|
|
9
|
+
import type { FolderId } from "@milaboratories/pl-model-middle-layer";
|
|
4
10
|
import { randomUUID } from "node:crypto";
|
|
5
11
|
|
|
6
12
|
/**
|
|
7
|
-
*
|
|
13
|
+
* Identity of one share. A donor-generated UUID string,
|
|
8
14
|
* branded so it cannot be silently confused with a project id, a login, or a raw field
|
|
9
15
|
* name. Minted once with {@link newShareId}; every other site receives it (from decoded
|
|
10
16
|
* {@link EnvelopeData} or by parsing a `decision/{shareId}` field name) and threads it
|
|
@@ -32,7 +38,7 @@ export function asShareId(id: string): ShareId {
|
|
|
32
38
|
|
|
33
39
|
/** Field on the donor's clientRoot holding the {@link SharingOutboxResourceType} resource. */
|
|
34
40
|
export const SharingOutboxField = "sharingOutbox";
|
|
35
|
-
/** Field on the
|
|
41
|
+
/** Field on the recipient's clientRoot holding the {@link SharingStateResourceType} resource. */
|
|
36
42
|
export const SharingStateField = "sharingState";
|
|
37
43
|
|
|
38
44
|
export const SharingOutboxResourceType: ResourceType = { name: "SharingOutbox", version: "1" };
|
|
@@ -41,14 +47,15 @@ export const SharingStateResourceType: ResourceType = { name: "SharingState", ve
|
|
|
41
47
|
|
|
42
48
|
export type EnvelopeMode = "copy" | "read-only" | "collaboration";
|
|
43
49
|
|
|
44
|
-
/** Per-project decision on change, matching the UI labels: re-snapshot the live source ("update"),
|
|
45
|
-
* carry the existing snapshot ("keep"), or drop the project from the pack ("remove"). */
|
|
46
|
-
export type ProjectChangeAction = "keep" | "update" | "remove";
|
|
47
|
-
|
|
48
50
|
/** Key of the per-project envelope maps: a uuid minted per snapshot to name the `project/{uuid}`
|
|
49
51
|
* field. Distinct from {@link ProjectId} — re-snapshotting one source yields a new uuid each time. */
|
|
50
52
|
export type ProjectFieldUuid = Branded<string, "ProjectFieldUuid">;
|
|
51
53
|
|
|
54
|
+
/** Mints a fresh {@link ProjectFieldUuid} for one snapshot. */
|
|
55
|
+
export function newProjectFieldUuid(): ProjectFieldUuid {
|
|
56
|
+
return randomUUID() as ProjectFieldUuid;
|
|
57
|
+
}
|
|
58
|
+
|
|
52
59
|
/**
|
|
53
60
|
* Whether a role may make a resource public (grant to everyone): true for controller,
|
|
54
61
|
* admin; false for workflow and unspecified. The middle layer carries no policy
|
|
@@ -87,9 +94,50 @@ export function canImpersonate(role: Role | null): boolean {
|
|
|
87
94
|
/** One project's snapshot inside an envelope, keyed by {@link ProjectFieldUuid} in a
|
|
88
95
|
* `projects` {@link EnvelopePayload}. */
|
|
89
96
|
export interface EnvelopeProject {
|
|
90
|
-
label: string; // carried so the
|
|
91
|
-
source: ProjectId; // donor's source projectId;
|
|
97
|
+
label: string; // carried so the share lists render without traversing into the project
|
|
98
|
+
source: ProjectId; // donor's source projectId; what a prior share of the same project is matched on
|
|
92
99
|
updatedAt: number; // ms epoch of the last (re)snapshot
|
|
100
|
+
/** What the project said about itself when it was snapshotted, carried for the same reason as
|
|
101
|
+
* `label`. Absent when it had none, and on envelopes written before it was carried. */
|
|
102
|
+
description?: string;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Identifier of a folder inside one envelope.
|
|
107
|
+
*
|
|
108
|
+
* Local to the envelope, because the recipient's own folder document mints its own ids and the
|
|
109
|
+
* donor's mean nothing there. What travels is the shape of the subtree, not its identity.
|
|
110
|
+
*/
|
|
111
|
+
export type EnvelopeFolderId = Branded<string, "EnvelopeFolderId">;
|
|
112
|
+
|
|
113
|
+
/** Mints a fresh {@link EnvelopeFolderId} for one folder of the envelope being built. */
|
|
114
|
+
export function newEnvelopeFolderId(): EnvelopeFolderId {
|
|
115
|
+
return randomUUID() as EnvelopeFolderId;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** One folder of a shared subtree. */
|
|
119
|
+
export interface EnvelopeFolder {
|
|
120
|
+
name: string;
|
|
121
|
+
/** Absent for the subtree's root — the folder that was shared. */
|
|
122
|
+
parent?: EnvelopeFolderId;
|
|
123
|
+
/** What the donor wrote about the folder. Absent when there is none. */
|
|
124
|
+
description?: string;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** A project of a shared subtree: an {@link EnvelopeProject} placed in the subtree. */
|
|
128
|
+
export interface EnvelopeFolderProject extends EnvelopeProject {
|
|
129
|
+
folder: EnvelopeFolderId;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/** A template of a shared subtree. The document rides here whole, exactly as a `template`
|
|
133
|
+
* payload carries it — a template is never snapshotted. */
|
|
134
|
+
export interface EnvelopeFolderTemplate {
|
|
135
|
+
document: ProjectTemplateV1;
|
|
136
|
+
/** Label to give the template among the recipient's templates. */
|
|
137
|
+
label: string;
|
|
138
|
+
/** What the template says about itself. Absent when the donor described it with nothing. */
|
|
139
|
+
description?: string;
|
|
140
|
+
folder: EnvelopeFolderId;
|
|
93
141
|
}
|
|
94
142
|
|
|
95
143
|
/**
|
|
@@ -105,9 +153,31 @@ export type EnvelopePayload =
|
|
|
105
153
|
| {
|
|
106
154
|
kind: "template";
|
|
107
155
|
document: ProjectTemplateV1;
|
|
108
|
-
/**
|
|
156
|
+
/** Donor's own id of the shared template; what a prior share of the same template is
|
|
157
|
+
* matched on. It names nothing in the recipient's tree, and envelopes written before it
|
|
158
|
+
* existed carry none — those match no later share of anything. */
|
|
159
|
+
source?: TemplateId;
|
|
160
|
+
/** Label to give the template among the recipient's templates. */
|
|
109
161
|
label: string;
|
|
110
|
-
/**
|
|
162
|
+
/** What the template says about itself, carried to the recipient's copy. Absent when the
|
|
163
|
+
* donor described it with nothing. */
|
|
164
|
+
description?: string;
|
|
165
|
+
/** Donor login, kept on each copy of the template as its provenance. */
|
|
166
|
+
from: string;
|
|
167
|
+
}
|
|
168
|
+
| {
|
|
169
|
+
kind: "folder";
|
|
170
|
+
/** Donor's own id of the shared folder; what a prior share of the same folder is matched
|
|
171
|
+
* on. It names nothing in the recipient's tree. */
|
|
172
|
+
source: FolderId;
|
|
173
|
+
/** The shared subtree. Exactly one folder has no parent, and that one is its root. */
|
|
174
|
+
folders: Record<EnvelopeFolderId, EnvelopeFolder>;
|
|
175
|
+
/** Project snapshots, each tagged with the folder of the subtree holding it. Their
|
|
176
|
+
* `project/{uuid}` fields are the same ones a `projects` payload describes. */
|
|
177
|
+
projects: Record<ProjectFieldUuid, EnvelopeFolderProject>;
|
|
178
|
+
/** Templates of the subtree, documents and all. Nothing of a template is snapshotted. */
|
|
179
|
+
templates: EnvelopeFolderTemplate[];
|
|
180
|
+
/** Donor login, kept on each copied template as its provenance. */
|
|
111
181
|
from: string;
|
|
112
182
|
};
|
|
113
183
|
|
|
@@ -130,10 +200,10 @@ export const EnvelopeSchemaVersionCurrent = 2 satisfies EnvelopeSchemaVersion;
|
|
|
130
200
|
*/
|
|
131
201
|
export interface EnvelopeData {
|
|
132
202
|
schemaVersion: typeof EnvelopeSchemaVersionCurrent;
|
|
133
|
-
shareId: ShareId; // donor-generated UUID;
|
|
134
|
-
sharedAt: number; // ms epoch;
|
|
203
|
+
shareId: ShareId; // donor-generated UUID; identity of this share alone
|
|
204
|
+
sharedAt: number; // ms epoch; when the share was created
|
|
135
205
|
expiresAt: number | null; // ms epoch; sharedAt + ttl (default 14 days) for a targeted share; null for share-with-everybody (never expires)
|
|
136
|
-
mode: EnvelopeMode; // what the
|
|
206
|
+
mode: EnvelopeMode; // what the recipient's app should do with the contents
|
|
137
207
|
sender: string; // donor login (informational; backend granted_by is authoritative)
|
|
138
208
|
title: string; // display name shown to recipients; defaults to the first project's name
|
|
139
209
|
payload: EnvelopePayload; // what the share carries
|
|
@@ -145,31 +215,44 @@ export function envelopeProjectMap(data: EnvelopeData): Record<ProjectFieldUuid,
|
|
|
145
215
|
return data.payload.kind === "projects" ? data.payload.projects : {};
|
|
146
216
|
}
|
|
147
217
|
|
|
148
|
-
/**
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
218
|
+
/**
|
|
219
|
+
* The shared folder itself: the one folder of the subtree that has no parent.
|
|
220
|
+
*
|
|
221
|
+
* Derived rather than stored, so it cannot disagree with the folders beside it. `undefined` for
|
|
222
|
+
* a subtree with no root or more than one, which is an envelope nothing can be reconstructed
|
|
223
|
+
* from — the copy reports it rather than guessing which folder was meant.
|
|
224
|
+
*/
|
|
225
|
+
export function envelopeFolderRoot(
|
|
226
|
+
folders: Record<EnvelopeFolderId, EnvelopeFolder>,
|
|
227
|
+
): EnvelopeFolderId | undefined {
|
|
228
|
+
const roots = (Object.keys(folders) as EnvelopeFolderId[]).filter(
|
|
229
|
+
(id) => folders[id].parent === undefined,
|
|
230
|
+
);
|
|
231
|
+
return roots.length === 1 ? roots[0] : undefined;
|
|
156
232
|
}
|
|
157
233
|
|
|
158
|
-
/**
|
|
159
|
-
*
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
234
|
+
/**
|
|
235
|
+
* Dynamic field on SharingState, one per share this user has hidden, keyed by shareId.
|
|
236
|
+
*
|
|
237
|
+
* Hiding is private to the recipient and reversible: the field is written to put a share out of
|
|
238
|
+
* sight and removed to bring it back. It says nothing to the donor and nothing about whether
|
|
239
|
+
* anything was ever copied out of the share — a share can be copied from any number of times,
|
|
240
|
+
* before or after being hidden.
|
|
241
|
+
*
|
|
242
|
+
* The field name keeps its original `decision/` prefix. Records written before hiding replaced
|
|
243
|
+
* accept/reject carry a different value under the same key, and every reader treats the presence
|
|
244
|
+
* of the field as the whole answer: someone who accepted or rejected a share back then does not
|
|
245
|
+
* want to see it, which is exactly what hidden means.
|
|
246
|
+
*/
|
|
247
|
+
export const HiddenFieldPrefix = "decision/";
|
|
248
|
+
export const hiddenField = (shareId: ShareId) => `${HiddenFieldPrefix}${shareId}`;
|
|
249
|
+
export const isHiddenField = (name: string) => name.startsWith(HiddenFieldPrefix);
|
|
250
|
+
export const hiddenFieldShareId = (name: string): ShareId =>
|
|
251
|
+
asShareId(name.slice(HiddenFieldPrefix.length));
|
|
252
|
+
|
|
253
|
+
export interface ShareHidden {
|
|
254
|
+
hidden: true;
|
|
255
|
+
timestamp: number; // ms epoch — when the recipient hid it
|
|
173
256
|
}
|
|
174
257
|
|
|
175
258
|
/**
|
|
@@ -218,48 +301,52 @@ export function normalizeEnvelopeData(raw: unknown): EnvelopeData | undefined {
|
|
|
218
301
|
}
|
|
219
302
|
|
|
220
303
|
/**
|
|
221
|
-
*
|
|
304
|
+
* Who a share is granted to: named recipients XOR everyone — two clean variants, not one struct
|
|
305
|
+
* with mutually exclusive optional fields.
|
|
222
306
|
*
|
|
223
|
-
*
|
|
224
|
-
*
|
|
225
|
-
*
|
|
226
|
-
* named recipient and the envelope expires after the default TTL.
|
|
307
|
+
* The everyone variant issues a single make-public grant, and the envelope's `expiresAt` is `null`,
|
|
308
|
+
* so it never expires. The recipients variant grants each named login, and the envelope expires
|
|
309
|
+
* after the default TTL.
|
|
227
310
|
*/
|
|
228
|
-
export type
|
|
229
|
-
| {
|
|
230
|
-
|
|
231
|
-
title: string; // display name shown to recipients; defaults to the first project's name
|
|
232
|
-
mode: EnvelopeMode; // v1 UI always sends "copy"
|
|
233
|
-
}
|
|
234
|
-
| {
|
|
235
|
-
everyone: true; // share with all users on the server
|
|
236
|
-
/**
|
|
237
|
-
* When true and an everyone-share of the same project already exists, refresh it under its
|
|
238
|
-
* stable shareId (recipients who already accepted or rejected are not re-prompted) instead of
|
|
239
|
-
* minting a new share. No-op when no prior everyone-share of the project exists. Callers that
|
|
240
|
-
* don't care pass `false`.
|
|
241
|
-
*/
|
|
242
|
-
replace: boolean;
|
|
243
|
-
title: string;
|
|
244
|
-
mode: EnvelopeMode;
|
|
245
|
-
};
|
|
311
|
+
export type ShareAudience =
|
|
312
|
+
| { recipients: string[] } // recipient logins
|
|
313
|
+
| { everyone: true }; // every user on the server
|
|
246
314
|
|
|
247
315
|
/**
|
|
248
|
-
* Options
|
|
316
|
+
* Options every share takes: the audience, the title recipients see it under, and the prior
|
|
317
|
+
* shares it replaces.
|
|
318
|
+
*/
|
|
319
|
+
export type ShareOptions = ShareAudience & {
|
|
320
|
+
title: string;
|
|
321
|
+
replace?: ShareReplaceOption;
|
|
322
|
+
};
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* Prior shares the new one supersedes: each is deleted in the same transaction that creates the
|
|
326
|
+
* replacement, so the outbox never holds both.
|
|
327
|
+
*
|
|
328
|
+
* The set is the caller's, never inferred here. The author is shown the shares that will go and
|
|
329
|
+
* agrees to that list, so what was shown has to be what is deleted — an overlap rule computed on
|
|
330
|
+
* this side would diverge from it. Ids that no longer resolve are skipped: a share revoked between
|
|
331
|
+
* the dialog opening and the confirm is nothing to undo.
|
|
249
332
|
*
|
|
250
|
-
*
|
|
251
|
-
*
|
|
252
|
-
* envelope — the document is in the envelope's own data.
|
|
333
|
+
* The replacement is a new {@link ShareId}: a recipient who had hidden the old share sees the new
|
|
334
|
+
* one, and copies already taken from the old share are untouched.
|
|
253
335
|
*/
|
|
254
|
-
export type
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
336
|
+
export type ShareReplaceOption = ShareId[];
|
|
337
|
+
|
|
338
|
+
/** Options for {@link MiddleLayer.shareProjects}: the common ones, plus what the recipient's app
|
|
339
|
+
* does with the projects. */
|
|
340
|
+
export type ShareProjectsOptions = ShareOptions & { mode: EnvelopeMode };
|
|
341
|
+
|
|
342
|
+
/** Options for {@link MiddleLayer.shareTemplate}. */
|
|
343
|
+
export type ShareTemplateOptions = ShareOptions;
|
|
344
|
+
|
|
345
|
+
/** Options for {@link MiddleLayer.shareFolder}. */
|
|
346
|
+
export type ShareFolderOptions = ShareOptions;
|
|
347
|
+
|
|
348
|
+
/** What creating a share hands back: the id of the share just created. */
|
|
349
|
+
export type ShareOutcome = { readonly shareId: ShareId };
|
|
263
350
|
|
|
264
351
|
//
|
|
265
352
|
// Internals
|
|
@@ -271,6 +358,7 @@ export type ShareTemplateOptions =
|
|
|
271
358
|
const KnownPayloadKinds: Record<EnvelopePayloadKind, true> = {
|
|
272
359
|
projects: true,
|
|
273
360
|
template: true,
|
|
361
|
+
folder: true,
|
|
274
362
|
};
|
|
275
363
|
|
|
276
364
|
/** Every schema version {@link normalizeEnvelopeData} accepts. Keyed by
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import type { PlTransaction, SignedResourceId } from "@milaboratories/pl-client";
|
|
2
|
+
import { isNullSignedResourceId, resourceIdToString } from "@milaboratories/pl-client";
|
|
3
|
+
|
|
4
|
+
/** One entry of a list resource: the field holding it and the resource that field points at. */
|
|
5
|
+
export interface ListedEntry {
|
|
6
|
+
readonly fieldName: string;
|
|
7
|
+
readonly rid: SignedResourceId;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
/** The kinds of list a root keeps, as a not-found message names them. */
|
|
11
|
+
export type ListedKind = "Project" | "Template";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Every entry of a list resource — the project list or the template list — keyed by the string
|
|
15
|
+
* form of the resource id it points at, which is what a project or template id is.
|
|
16
|
+
*
|
|
17
|
+
* A list field's name is a uuid unrelated to the id of what it holds, so an entry is only ever
|
|
18
|
+
* found by value. Fields that point at nothing yet are left out.
|
|
19
|
+
*/
|
|
20
|
+
export async function listedById(
|
|
21
|
+
tx: PlTransaction,
|
|
22
|
+
listRid: SignedResourceId,
|
|
23
|
+
): Promise<Map<string, ListedEntry>> {
|
|
24
|
+
const data = await tx.getResourceData(listRid, true);
|
|
25
|
+
const entries = new Map<string, ListedEntry>();
|
|
26
|
+
for (const f of data.fields) {
|
|
27
|
+
if (isNullSignedResourceId(f.value)) continue;
|
|
28
|
+
entries.set(resourceIdToString(f.value), { fieldName: f.name, rid: f.value });
|
|
29
|
+
}
|
|
30
|
+
return entries;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** What is thrown for an id the list does not hold, worded the same for every list. */
|
|
34
|
+
export function notListedError(kind: ListedKind, id: string): Error {
|
|
35
|
+
return new Error(`${kind} ${id} not found in the ${kind.toLowerCase()} list.`);
|
|
36
|
+
}
|
|
@@ -23,6 +23,8 @@ const BPSpecSumV3: BlockPackSpec = {
|
|
|
23
23
|
folder: path.resolve(__dirname, "../../../../../etc/blocks/sum-numbers/block"),
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
+
// Waits for two blocks to compute on the backend, so it runs as long as the backend is busy with
|
|
27
|
+
// whatever else the suite is running; it gets twice the suite's default.
|
|
26
28
|
test("v3 blocks: basic test with unified state", async () => {
|
|
27
29
|
const quickJs = await getQuickJS();
|
|
28
30
|
|
|
@@ -154,7 +156,7 @@ test("v3 blocks: basic test with unified state", async () => {
|
|
|
154
156
|
expect(sum).toBe(21);
|
|
155
157
|
});
|
|
156
158
|
});
|
|
157
|
-
});
|
|
159
|
+
}, 160_000);
|
|
158
160
|
|
|
159
161
|
test("v3 blocks: prerunArgs skip test", async () => {
|
|
160
162
|
const quickJs = await getQuickJS();
|
package/src/mutator/project.ts
CHANGED
|
@@ -63,7 +63,7 @@ import type {
|
|
|
63
63
|
BlockSettings,
|
|
64
64
|
ProjectMeta,
|
|
65
65
|
} from "@milaboratories/pl-model-middle-layer";
|
|
66
|
-
import { InitialBlockSettings } from "@milaboratories/pl-model-middle-layer";
|
|
66
|
+
import { InitialBlockSettings, normalizeProjectMeta } from "@milaboratories/pl-model-middle-layer";
|
|
67
67
|
import Denque from "denque";
|
|
68
68
|
import { exportContext, getPreparedExportTemplateEnvelope } from "./context_export";
|
|
69
69
|
import { loadTemplate } from "./template/template_loading";
|
|
@@ -1796,11 +1796,22 @@ export class ProjectMutator {
|
|
|
1796
1796
|
|
|
1797
1797
|
/** Updates project metadata */
|
|
1798
1798
|
public setMeta(meta: ProjectMeta): void {
|
|
1799
|
-
this.meta = meta;
|
|
1799
|
+
this.meta = normalizeProjectMeta(meta);
|
|
1800
1800
|
this.metaChanged = true;
|
|
1801
1801
|
this.updateLastModified();
|
|
1802
1802
|
}
|
|
1803
1803
|
|
|
1804
|
+
/**
|
|
1805
|
+
* Updates the metadata fields the caller names, leaving the rest as they are.
|
|
1806
|
+
*
|
|
1807
|
+
* The merge happens against the metadata this mutator loaded inside the very transaction it
|
|
1808
|
+
* writes in, so a rename and a description edit racing each other cannot make one of them
|
|
1809
|
+
* revert the other's field.
|
|
1810
|
+
*/
|
|
1811
|
+
public updateMeta(patch: Partial<ProjectMeta>): void {
|
|
1812
|
+
this.setMeta({ ...this.meta, ...patch });
|
|
1813
|
+
}
|
|
1814
|
+
|
|
1804
1815
|
//
|
|
1805
1816
|
// Maintenance
|
|
1806
1817
|
//
|