@milaboratories/pl-middle-layer 1.68.2 → 1.69.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.cjs +10 -0
- package/dist/index.d.ts +6 -2
- package/dist/index.js +5 -2
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/index.cjs +2 -0
- package/dist/middle_layer/index.d.ts +2 -1
- package/dist/middle_layer/index.js +2 -1
- package/dist/middle_layer/middle_layer.cjs +333 -26
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +125 -7
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +335 -28
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/project_list.d.ts +1 -1
- package/dist/middle_layer/sharing_list.cjs +13 -5
- package/dist/middle_layer/sharing_list.cjs.map +1 -1
- package/dist/middle_layer/sharing_list.d.ts +16 -2
- package/dist/middle_layer/sharing_list.d.ts.map +1 -1
- package/dist/middle_layer/sharing_list.js +14 -6
- package/dist/middle_layer/sharing_list.js.map +1 -1
- package/dist/middle_layer/template_list.cjs +72 -0
- package/dist/middle_layer/template_list.cjs.map +1 -0
- package/dist/middle_layer/template_list.d.ts +77 -0
- package/dist/middle_layer/template_list.d.ts.map +1 -0
- package/dist/middle_layer/template_list.js +64 -0
- package/dist/middle_layer/template_list.js.map +1 -0
- package/dist/model/index.cjs +8 -0
- package/dist/model/index.d.ts +5 -2
- package/dist/model/index.js +4 -2
- package/dist/model/sharing_model.cjs +59 -2
- package/dist/model/sharing_model.cjs.map +1 -1
- package/dist/model/sharing_model.d.ts +70 -7
- package/dist/model/sharing_model.d.ts.map +1 -1
- package/dist/model/sharing_model.js +57 -3
- package/dist/model/sharing_model.js.map +1 -1
- package/dist/model/template_serializer.d.ts +36 -0
- package/dist/model/template_serializer.d.ts.map +1 -1
- package/dist/model/template_share.cjs +42 -0
- package/dist/model/template_share.cjs.map +1 -0
- package/dist/model/template_share.d.ts +27 -0
- package/dist/model/template_share.d.ts.map +1 -0
- package/dist/model/template_share.js +42 -0
- package/dist/model/template_share.js.map +1 -0
- package/dist/mutator/project.cjs +2 -2
- package/dist/mutator/project.js +2 -2
- package/dist/mutator/sharing.cjs +40 -2
- package/dist/mutator/sharing.cjs.map +1 -1
- package/dist/mutator/sharing.js +40 -3
- package/dist/mutator/sharing.js.map +1 -1
- package/dist/mutator/template.cjs +54 -0
- package/dist/mutator/template.cjs.map +1 -0
- package/dist/mutator/template.js +52 -0
- package/dist/mutator/template.js.map +1 -0
- package/package.json +10 -10
- package/src/middle_layer/index.ts +9 -0
- package/src/middle_layer/middle_layer.ts +449 -36
- package/src/middle_layer/sharing_list.ts +37 -7
- package/src/middle_layer/template_list.ts +177 -0
- package/src/middle_layer/templates.test.ts +301 -0
- package/src/model/index.ts +14 -0
- package/src/model/sharing_model.test.ts +115 -1
- package/src/model/sharing_model.ts +134 -9
- package/src/model/template_share.test.ts +78 -0
- package/src/model/template_share.ts +52 -0
- package/src/mutator/sharing.ts +55 -3
- package/src/mutator/template.ts +75 -0
- package/src/test/with_ml.ts +38 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_share.js","names":[],"sources":["../../src/model/template_share.ts"],"sourcesContent":["import type { ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport { parseBlockPackLocation } from \"@milaboratories/pl-model-common\";\n\n/** One template entry standing in the way of sharing the template, and why. */\nexport type TemplateShareProblem = {\n /** The template-local id of the entry the problem belongs to; on an exported template it is\n * the block's project-local uuid. */\n readonly entryId: string;\n readonly error: string;\n};\n\n/**\n * Every entry of a template that cannot travel to another machine, or an empty list for a\n * template that can be shared.\n *\n * An entry's `location` names a place rather than a name, and a `file:` place is a folder on\n * the author's own disk: a recipient resolving it finds nothing, or worse finds something\n * else. Such a template stays perfectly usable where it was made, so it is stored and applied\n * as normal — only sharing it is refused.\n *\n * A location whose scheme cannot be read is refused for the same reason: nothing can resolve\n * it anywhere, here included.\n *\n * Every offending entry is reported, not only the first, so a UI can name each block instead\n * of sending its user round the loop once per entry.\n */\nexport function unshareableTemplateEntries(\n document: ProjectTemplateV1,\n): readonly TemplateShareProblem[] {\n const problems: TemplateShareProblem[] = [];\n for (const entry of document.blocks) {\n if (entry.location === undefined) continue;\n let scheme: string;\n try {\n scheme = parseBlockPackLocation(entry.location).scheme;\n } catch (e) {\n problems.push({\n entryId: entry.id,\n error: `Block is installed from a location nothing can resolve: ${e instanceof Error ? e.message : String(e)}`,\n });\n continue;\n }\n if (scheme === \"file\")\n problems.push({\n entryId: entry.id,\n error:\n `Block is installed from ${entry.location}, a folder on this machine — it resolves ` +\n \"to nothing on the recipient's, so this template cannot be shared\",\n });\n }\n return problems;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;AA0BA,SAAgB,2BACd,UACiC;CACjC,MAAM,WAAmC,CAAC;CAC1C,KAAK,MAAM,SAAS,SAAS,QAAQ;EACnC,IAAI,MAAM,aAAa,KAAA,GAAW;EAClC,IAAI;EACJ,IAAI;GACF,SAAS,uBAAuB,MAAM,QAAQ,CAAC,CAAC;EAClD,SAAS,GAAG;GACV,SAAS,KAAK;IACZ,SAAS,MAAM;IACf,OAAO,2DAA2D,aAAa,QAAQ,EAAE,UAAU,OAAO,CAAC;GAC7G,CAAC;GACD;EACF;EACA,IAAI,WAAW,QACb,SAAS,KAAK;GACZ,SAAS,MAAM;GACf,OACE,2BAA2B,MAAM,SAAS;EAE9C,CAAC;CACL;CACA,OAAO;AACT"}
|
package/dist/mutator/project.cjs
CHANGED
|
@@ -2,10 +2,10 @@ const require_runtime = require("../_virtual/_rolldown/runtime.cjs");
|
|
|
2
2
|
const require_project_model = require("../model/project_model.cjs");
|
|
3
3
|
const require_render_block = require("./template/render_block.cjs");
|
|
4
4
|
const require_template_loading = require("./template/template_loading.cjs");
|
|
5
|
-
const require_block_pack = require("./block-pack/block_pack.cjs");
|
|
6
5
|
const require_project_model_util = require("../model/project_model_util.cjs");
|
|
7
|
-
const require_context_export = require("./context_export.cjs");
|
|
8
6
|
const require_template_serializer = require("../model/template_serializer.cjs");
|
|
7
|
+
const require_block_pack = require("./block-pack/block_pack.cjs");
|
|
8
|
+
const require_context_export = require("./context_export.cjs");
|
|
9
9
|
const require_index = require("../debug/index.cjs");
|
|
10
10
|
let _platforma_sdk_model = require("@platforma-sdk/model");
|
|
11
11
|
let _milaboratories_pl_model_middle_layer = require("@milaboratories/pl-model-middle-layer");
|
package/dist/mutator/project.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { BlockArgsAuthorKeyPrefix, BlockRenderingStateKey, FieldsToDuplicate, InitialBlockMeta, InitialBlockStructure, InitialProjectRenderingState, ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionKey, blockArgsAuthorKey, getServiceTemplateField, parseProjectField, projectFieldName } from "../model/project_model.js";
|
|
2
2
|
import { createBContextEnd, createBContextFromUpstreams, createRenderHeavyBlock } from "./template/render_block.js";
|
|
3
3
|
import { loadTemplate } from "./template/template_loading.js";
|
|
4
|
-
import { BlockPackTemplateField, createBlockPack } from "./block-pack/block_pack.js";
|
|
5
4
|
import { allBlocks, graphDiff, productionGraph, stagingGraph } from "../model/project_model_util.js";
|
|
6
|
-
import { exportContext, getPreparedExportTemplateEnvelope } from "./context_export.js";
|
|
7
5
|
import { exportProjectAsTemplateV1 } from "../model/template_serializer.js";
|
|
6
|
+
import { BlockPackTemplateField, createBlockPack } from "./block-pack/block_pack.js";
|
|
7
|
+
import { exportContext, getPreparedExportTemplateEnvelope } from "./context_export.js";
|
|
8
8
|
import { getDebugFlags } from "../debug/index.js";
|
|
9
9
|
import { BLOCK_STORAGE_FACADE_VERSION, UiError, extractConfig } from "@platforma-sdk/model";
|
|
10
10
|
import { InitialBlockSettings } from "@milaboratories/pl-model-middle-layer";
|
package/dist/mutator/sharing.cjs
CHANGED
|
@@ -56,14 +56,17 @@ async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const data = {
|
|
59
|
-
schemaVersion:
|
|
59
|
+
schemaVersion: 2,
|
|
60
60
|
shareId,
|
|
61
61
|
sharedAt,
|
|
62
62
|
expiresAt: params.expiresAt,
|
|
63
63
|
mode: params.mode,
|
|
64
64
|
sender: params.sender,
|
|
65
65
|
title: params.title,
|
|
66
|
-
|
|
66
|
+
payload: {
|
|
67
|
+
kind: "projects",
|
|
68
|
+
projects
|
|
69
|
+
}
|
|
67
70
|
};
|
|
68
71
|
const envelope = tx.createEphemeral(require_sharing_model.SharedEnvelopeResourceType, JSON.stringify(data));
|
|
69
72
|
for (const { uuid, ref } of snapshots) tx.createField((0, _milaboratories_pl_client.field)(envelope, envelopeProjectField(uuid)), "Input", ref);
|
|
@@ -75,6 +78,40 @@ async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
81
|
+
* Builds one {@link SharedEnvelopeResourceType} carrying a template document on the donor side,
|
|
82
|
+
* and attaches it under `{shareId}` on the donor's outbox. The caller issues the grant — always
|
|
83
|
+
* read-only — and commits, keeping create + grant atomic.
|
|
84
|
+
*
|
|
85
|
+
* There is nothing to snapshot and no input field to seal: the document is the whole payload and
|
|
86
|
+
* rides in the envelope's immutable `data`, which is also why the recipient needs no write access
|
|
87
|
+
* (it copies no resource out of the envelope).
|
|
88
|
+
*
|
|
89
|
+
* @returns the new envelope resource and the generated `EnvelopeData`.
|
|
90
|
+
*/
|
|
91
|
+
function buildTemplateShareEnvelope(tx, outboxRid, template, params) {
|
|
92
|
+
const data = {
|
|
93
|
+
schemaVersion: 2,
|
|
94
|
+
shareId: params.shareId ?? require_sharing_model.newShareId(),
|
|
95
|
+
sharedAt: params.sharedAt ?? Date.now(),
|
|
96
|
+
expiresAt: params.expiresAt,
|
|
97
|
+
mode: "read-only",
|
|
98
|
+
sender: params.sender,
|
|
99
|
+
title: params.title,
|
|
100
|
+
payload: {
|
|
101
|
+
kind: "template",
|
|
102
|
+
document: template.document,
|
|
103
|
+
label: template.label,
|
|
104
|
+
from: params.sender
|
|
105
|
+
}
|
|
106
|
+
};
|
|
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
|
+
return {
|
|
110
|
+
envelope,
|
|
111
|
+
data
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
78
115
|
* Records a response onto the envelope as a dynamic `acceptance/{login}` field: the acceptor
|
|
79
116
|
* writing their own decision (their writable grant permits it), or the donor transferring an
|
|
80
117
|
* existing record onto a changed envelope. Accepts the envelope by ref or id.
|
|
@@ -127,6 +164,7 @@ function resourceIdsToStrings(ids) {
|
|
|
127
164
|
//#endregion
|
|
128
165
|
exports.EnvelopeProjectFieldPrefix = EnvelopeProjectFieldPrefix;
|
|
129
166
|
exports.buildShareEnvelope = buildShareEnvelope;
|
|
167
|
+
exports.buildTemplateShareEnvelope = buildTemplateShareEnvelope;
|
|
130
168
|
exports.copyEnvelopeProjectsIntoList = copyEnvelopeProjectsIntoList;
|
|
131
169
|
exports.envelopeProjectField = envelopeProjectField;
|
|
132
170
|
exports.envelopeProjectFieldUuid = envelopeProjectFieldUuid;
|
|
@@ -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 } 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 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: 1,\n shareId,\n sharedAt,\n expiresAt: params.expiresAt,\n mode: params.mode,\n sender: params.sender,\n title: params.title,\n 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 * 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","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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"}
|
package/dist/mutator/sharing.js
CHANGED
|
@@ -56,14 +56,17 @@ async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
|
56
56
|
}
|
|
57
57
|
}
|
|
58
58
|
const data = {
|
|
59
|
-
schemaVersion:
|
|
59
|
+
schemaVersion: 2,
|
|
60
60
|
shareId,
|
|
61
61
|
sharedAt,
|
|
62
62
|
expiresAt: params.expiresAt,
|
|
63
63
|
mode: params.mode,
|
|
64
64
|
sender: params.sender,
|
|
65
65
|
title: params.title,
|
|
66
|
-
|
|
66
|
+
payload: {
|
|
67
|
+
kind: "projects",
|
|
68
|
+
projects
|
|
69
|
+
}
|
|
67
70
|
};
|
|
68
71
|
const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));
|
|
69
72
|
for (const { uuid, ref } of snapshots) tx.createField(field(envelope, envelopeProjectField(uuid)), "Input", ref);
|
|
@@ -75,6 +78,40 @@ async function buildShareEnvelope(tx, outboxRid, sources, params) {
|
|
|
75
78
|
};
|
|
76
79
|
}
|
|
77
80
|
/**
|
|
81
|
+
* Builds one {@link SharedEnvelopeResourceType} carrying a template document on the donor side,
|
|
82
|
+
* and attaches it under `{shareId}` on the donor's outbox. The caller issues the grant — always
|
|
83
|
+
* read-only — and commits, keeping create + grant atomic.
|
|
84
|
+
*
|
|
85
|
+
* There is nothing to snapshot and no input field to seal: the document is the whole payload and
|
|
86
|
+
* rides in the envelope's immutable `data`, which is also why the recipient needs no write access
|
|
87
|
+
* (it copies no resource out of the envelope).
|
|
88
|
+
*
|
|
89
|
+
* @returns the new envelope resource and the generated `EnvelopeData`.
|
|
90
|
+
*/
|
|
91
|
+
function buildTemplateShareEnvelope(tx, outboxRid, template, params) {
|
|
92
|
+
const data = {
|
|
93
|
+
schemaVersion: 2,
|
|
94
|
+
shareId: params.shareId ?? newShareId(),
|
|
95
|
+
sharedAt: params.sharedAt ?? Date.now(),
|
|
96
|
+
expiresAt: params.expiresAt,
|
|
97
|
+
mode: "read-only",
|
|
98
|
+
sender: params.sender,
|
|
99
|
+
title: params.title,
|
|
100
|
+
payload: {
|
|
101
|
+
kind: "template",
|
|
102
|
+
document: template.document,
|
|
103
|
+
label: template.label,
|
|
104
|
+
from: params.sender
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
const envelope = tx.createEphemeral(SharedEnvelopeResourceType, JSON.stringify(data));
|
|
108
|
+
tx.createField(field(outboxRid, data.shareId), "Dynamic", envelope);
|
|
109
|
+
return {
|
|
110
|
+
envelope,
|
|
111
|
+
data
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
78
115
|
* Records a response onto the envelope as a dynamic `acceptance/{login}` field: the acceptor
|
|
79
116
|
* writing their own decision (their writable grant permits it), or the donor transferring an
|
|
80
117
|
* existing record onto a changed envelope. Accepts the envelope by ref or id.
|
|
@@ -125,6 +162,6 @@ function resourceIdsToStrings(ids) {
|
|
|
125
162
|
return ids.map((id) => resourceIdToString(id));
|
|
126
163
|
}
|
|
127
164
|
//#endregion
|
|
128
|
-
export { EnvelopeProjectFieldPrefix, buildShareEnvelope, copyEnvelopeProjectsIntoList, envelopeProjectField, envelopeProjectFieldUuid, isEnvelopeProjectField, resourceIdsToStrings, writeEnvelopeAcceptance, writeSharingDecision };
|
|
165
|
+
export { EnvelopeProjectFieldPrefix, buildShareEnvelope, buildTemplateShareEnvelope, copyEnvelopeProjectsIntoList, envelopeProjectField, envelopeProjectFieldUuid, isEnvelopeProjectField, resourceIdsToStrings, writeEnvelopeAcceptance, writeSharingDecision };
|
|
129
166
|
|
|
130
167
|
//# sourceMappingURL=sharing.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sharing.js","names":[],"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 } 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 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: 1,\n shareId,\n sharedAt,\n expiresAt: params.expiresAt,\n mode: params.mode,\n sender: params.sender,\n title: params.title,\n 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 * 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.js","names":[],"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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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
const require_template_list = require("../middle_layer/template_list.cjs");
|
|
2
|
+
let _milaboratories_pl_client = require("@milaboratories/pl-client");
|
|
3
|
+
let node_crypto = require("node:crypto");
|
|
4
|
+
//#region src/mutator/template.ts
|
|
5
|
+
/**
|
|
6
|
+
* Creates one `UserTemplate` inside the given write transaction and attaches it to the
|
|
7
|
+
* templates list under a freshly minted uuid field.
|
|
8
|
+
*
|
|
9
|
+
* Create and attach are the same transaction on purpose: an ephemeral resource nothing
|
|
10
|
+
* holds is collectable, so the list field is what keeps the template alive.
|
|
11
|
+
*
|
|
12
|
+
* The document rides in the immutable `data` blob, set once here and never altered; only
|
|
13
|
+
* the label and the creation timestamp go to KV, and only the label is ever written again.
|
|
14
|
+
*
|
|
15
|
+
* @returns the new template resource; the caller reads its `globalId` after the commit.
|
|
16
|
+
*/
|
|
17
|
+
function createTemplate(tx, listRid, label, data) {
|
|
18
|
+
const tpl = tx.createEphemeral(require_template_list.TemplateResourceType, JSON.stringify(data));
|
|
19
|
+
tx.lock(tpl);
|
|
20
|
+
tx.setKValue(tpl, require_template_list.TemplateLabelKey, JSON.stringify(label));
|
|
21
|
+
tx.setKValue(tpl, require_template_list.TemplateCreatedTimestamp, String(Date.now()));
|
|
22
|
+
tx.createField((0, _milaboratories_pl_client.field)(listRid, (0, node_crypto.randomUUID)()), "Dynamic", tpl);
|
|
23
|
+
return tpl;
|
|
24
|
+
}
|
|
25
|
+
/** Renames a stored template. Touches the label KV entry and nothing else, so the stored
|
|
26
|
+
* document stays byte-identical. */
|
|
27
|
+
function renameTemplate(tx, rid, label) {
|
|
28
|
+
tx.setKValue(rid, require_template_list.TemplateLabelKey, JSON.stringify(label));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Detaches a template from the templates list, which is what destroys it — the list field is
|
|
32
|
+
* the only thing holding the ephemeral resource.
|
|
33
|
+
*
|
|
34
|
+
* The field name is a uuid unrelated to the template id, so the field carrying the template
|
|
35
|
+
* is found by value, the same way a project is removed from the project list.
|
|
36
|
+
*/
|
|
37
|
+
async function deleteTemplate(tx, listRid, id) {
|
|
38
|
+
const fieldName = await findTemplateField(tx, listRid, id);
|
|
39
|
+
if (fieldName === void 0) throw new Error(`Template ${id} not found in template list.`);
|
|
40
|
+
tx.removeField((0, _milaboratories_pl_client.field)(listRid, fieldName));
|
|
41
|
+
}
|
|
42
|
+
async function findTemplateField(tx, listRid, id) {
|
|
43
|
+
const data = await tx.getResourceData(listRid, true);
|
|
44
|
+
for (const f of data.fields) {
|
|
45
|
+
if ((0, _milaboratories_pl_client.isNullSignedResourceId)(f.value)) continue;
|
|
46
|
+
if ((0, _milaboratories_pl_client.resourceIdToString)(f.value) === id) return f.name;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
exports.createTemplate = createTemplate;
|
|
51
|
+
exports.deleteTemplate = deleteTemplate;
|
|
52
|
+
exports.renameTemplate = renameTemplate;
|
|
53
|
+
|
|
54
|
+
//# sourceMappingURL=template.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.cjs","names":["TemplateResourceType","TemplateLabelKey","TemplateCreatedTimestamp"],"sources":["../../src/mutator/template.ts"],"sourcesContent":["import type { PlTransaction, ResourceRef, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { field, isNullSignedResourceId, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { StoredTemplateData, TemplateId } from \"../middle_layer/template_list\";\nimport {\n TemplateCreatedTimestamp,\n TemplateLabelKey,\n TemplateResourceType,\n} from \"../middle_layer/template_list\";\n\n/**\n * Creates one `UserTemplate` inside the given write transaction and attaches it to the\n * templates list under a freshly minted uuid field.\n *\n * Create and attach are the same transaction on purpose: an ephemeral resource nothing\n * holds is collectable, so the list field is what keeps the template alive.\n *\n * The document rides in the immutable `data` blob, set once here and never altered; only\n * the label and the creation timestamp go to KV, and only the label is ever written again.\n *\n * @returns the new template resource; the caller reads its `globalId` after the commit.\n */\nexport function createTemplate(\n tx: PlTransaction,\n listRid: SignedResourceId,\n label: string,\n data: StoredTemplateData,\n): ResourceRef {\n const tpl = tx.createEphemeral(TemplateResourceType, JSON.stringify(data));\n tx.lock(tpl);\n tx.setKValue(tpl, TemplateLabelKey, JSON.stringify(label));\n tx.setKValue(tpl, TemplateCreatedTimestamp, String(Date.now()));\n tx.createField(field(listRid, randomUUID()), \"Dynamic\", tpl);\n return tpl;\n}\n\n/** Renames a stored template. Touches the label KV entry and nothing else, so the stored\n * document stays byte-identical. */\nexport function renameTemplate(tx: PlTransaction, rid: SignedResourceId, label: string): void {\n tx.setKValue(rid, TemplateLabelKey, JSON.stringify(label));\n}\n\n/**\n * Detaches a template from the templates list, which is what destroys it — the list field is\n * the only thing holding the ephemeral resource.\n *\n * The field name is a uuid unrelated to the template id, so the field carrying the template\n * is found by value, the same way a project is removed from the project list.\n */\nexport async function deleteTemplate(\n tx: PlTransaction,\n listRid: SignedResourceId,\n id: TemplateId,\n): Promise<void> {\n const fieldName = await findTemplateField(tx, listRid, id);\n if (fieldName === undefined) throw new Error(`Template ${id} not found in template list.`);\n tx.removeField(field(listRid, fieldName));\n}\n\n//\n// Internals\n//\n\nasync function findTemplateField(\n tx: PlTransaction,\n listRid: SignedResourceId,\n id: TemplateId,\n): Promise<string | undefined> {\n const data = await tx.getResourceData(listRid, true);\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n if (resourceIdToString(f.value) === (id as string)) return f.name;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAsBA,SAAgB,eACd,IACA,SACA,OACA,MACa;CACb,MAAM,MAAM,GAAG,gBAAgBA,sBAAAA,sBAAsB,KAAK,UAAU,IAAI,CAAC;CACzE,GAAG,KAAK,GAAG;CACX,GAAG,UAAU,KAAKC,sBAAAA,kBAAkB,KAAK,UAAU,KAAK,CAAC;CACzD,GAAG,UAAU,KAAKC,sBAAAA,0BAA0B,OAAO,KAAK,IAAI,CAAC,CAAC;CAC9D,GAAG,aAAA,GAAA,0BAAA,MAAA,CAAkB,UAAA,GAAA,YAAA,WAAA,CAAoB,CAAC,GAAG,WAAW,GAAG;CAC3D,OAAO;AACT;;;AAIA,SAAgB,eAAe,IAAmB,KAAuB,OAAqB;CAC5F,GAAG,UAAU,KAAKD,sBAAAA,kBAAkB,KAAK,UAAU,KAAK,CAAC;AAC3D;;;;;;;;AASA,eAAsB,eACpB,IACA,SACA,IACe;CACf,MAAM,YAAY,MAAM,kBAAkB,IAAI,SAAS,EAAE;CACzD,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,YAAY,GAAG,6BAA6B;CACzF,GAAG,aAAA,GAAA,0BAAA,MAAA,CAAkB,SAAS,SAAS,CAAC;AAC1C;AAMA,eAAe,kBACb,IACA,SACA,IAC6B;CAC7B,MAAM,OAAO,MAAM,GAAG,gBAAgB,SAAS,IAAI;CACnD,KAAK,MAAM,KAAK,KAAK,QAAQ;EAC3B,KAAA,GAAA,0BAAA,uBAAA,CAA2B,EAAE,KAAK,GAAG;EACrC,KAAA,GAAA,0BAAA,mBAAA,CAAuB,EAAE,KAAK,MAAO,IAAe,OAAO,EAAE;CAC/D;AAEF"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { TemplateCreatedTimestamp, TemplateLabelKey, TemplateResourceType } from "../middle_layer/template_list.js";
|
|
2
|
+
import { field, isNullSignedResourceId, resourceIdToString } from "@milaboratories/pl-client";
|
|
3
|
+
import { randomUUID } from "node:crypto";
|
|
4
|
+
//#region src/mutator/template.ts
|
|
5
|
+
/**
|
|
6
|
+
* Creates one `UserTemplate` inside the given write transaction and attaches it to the
|
|
7
|
+
* templates list under a freshly minted uuid field.
|
|
8
|
+
*
|
|
9
|
+
* Create and attach are the same transaction on purpose: an ephemeral resource nothing
|
|
10
|
+
* holds is collectable, so the list field is what keeps the template alive.
|
|
11
|
+
*
|
|
12
|
+
* The document rides in the immutable `data` blob, set once here and never altered; only
|
|
13
|
+
* the label and the creation timestamp go to KV, and only the label is ever written again.
|
|
14
|
+
*
|
|
15
|
+
* @returns the new template resource; the caller reads its `globalId` after the commit.
|
|
16
|
+
*/
|
|
17
|
+
function createTemplate(tx, listRid, label, data) {
|
|
18
|
+
const tpl = tx.createEphemeral(TemplateResourceType, JSON.stringify(data));
|
|
19
|
+
tx.lock(tpl);
|
|
20
|
+
tx.setKValue(tpl, TemplateLabelKey, JSON.stringify(label));
|
|
21
|
+
tx.setKValue(tpl, TemplateCreatedTimestamp, String(Date.now()));
|
|
22
|
+
tx.createField(field(listRid, randomUUID()), "Dynamic", tpl);
|
|
23
|
+
return tpl;
|
|
24
|
+
}
|
|
25
|
+
/** Renames a stored template. Touches the label KV entry and nothing else, so the stored
|
|
26
|
+
* document stays byte-identical. */
|
|
27
|
+
function renameTemplate(tx, rid, label) {
|
|
28
|
+
tx.setKValue(rid, TemplateLabelKey, JSON.stringify(label));
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Detaches a template from the templates list, which is what destroys it — the list field is
|
|
32
|
+
* the only thing holding the ephemeral resource.
|
|
33
|
+
*
|
|
34
|
+
* The field name is a uuid unrelated to the template id, so the field carrying the template
|
|
35
|
+
* is found by value, the same way a project is removed from the project list.
|
|
36
|
+
*/
|
|
37
|
+
async function deleteTemplate(tx, listRid, id) {
|
|
38
|
+
const fieldName = await findTemplateField(tx, listRid, id);
|
|
39
|
+
if (fieldName === void 0) throw new Error(`Template ${id} not found in template list.`);
|
|
40
|
+
tx.removeField(field(listRid, fieldName));
|
|
41
|
+
}
|
|
42
|
+
async function findTemplateField(tx, listRid, id) {
|
|
43
|
+
const data = await tx.getResourceData(listRid, true);
|
|
44
|
+
for (const f of data.fields) {
|
|
45
|
+
if (isNullSignedResourceId(f.value)) continue;
|
|
46
|
+
if (resourceIdToString(f.value) === id) return f.name;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
//#endregion
|
|
50
|
+
export { createTemplate, deleteTemplate, renameTemplate };
|
|
51
|
+
|
|
52
|
+
//# sourceMappingURL=template.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template.js","names":[],"sources":["../../src/mutator/template.ts"],"sourcesContent":["import type { PlTransaction, ResourceRef, SignedResourceId } from \"@milaboratories/pl-client\";\nimport { field, isNullSignedResourceId, resourceIdToString } from \"@milaboratories/pl-client\";\nimport { randomUUID } from \"node:crypto\";\nimport type { StoredTemplateData, TemplateId } from \"../middle_layer/template_list\";\nimport {\n TemplateCreatedTimestamp,\n TemplateLabelKey,\n TemplateResourceType,\n} from \"../middle_layer/template_list\";\n\n/**\n * Creates one `UserTemplate` inside the given write transaction and attaches it to the\n * templates list under a freshly minted uuid field.\n *\n * Create and attach are the same transaction on purpose: an ephemeral resource nothing\n * holds is collectable, so the list field is what keeps the template alive.\n *\n * The document rides in the immutable `data` blob, set once here and never altered; only\n * the label and the creation timestamp go to KV, and only the label is ever written again.\n *\n * @returns the new template resource; the caller reads its `globalId` after the commit.\n */\nexport function createTemplate(\n tx: PlTransaction,\n listRid: SignedResourceId,\n label: string,\n data: StoredTemplateData,\n): ResourceRef {\n const tpl = tx.createEphemeral(TemplateResourceType, JSON.stringify(data));\n tx.lock(tpl);\n tx.setKValue(tpl, TemplateLabelKey, JSON.stringify(label));\n tx.setKValue(tpl, TemplateCreatedTimestamp, String(Date.now()));\n tx.createField(field(listRid, randomUUID()), \"Dynamic\", tpl);\n return tpl;\n}\n\n/** Renames a stored template. Touches the label KV entry and nothing else, so the stored\n * document stays byte-identical. */\nexport function renameTemplate(tx: PlTransaction, rid: SignedResourceId, label: string): void {\n tx.setKValue(rid, TemplateLabelKey, JSON.stringify(label));\n}\n\n/**\n * Detaches a template from the templates list, which is what destroys it — the list field is\n * the only thing holding the ephemeral resource.\n *\n * The field name is a uuid unrelated to the template id, so the field carrying the template\n * is found by value, the same way a project is removed from the project list.\n */\nexport async function deleteTemplate(\n tx: PlTransaction,\n listRid: SignedResourceId,\n id: TemplateId,\n): Promise<void> {\n const fieldName = await findTemplateField(tx, listRid, id);\n if (fieldName === undefined) throw new Error(`Template ${id} not found in template list.`);\n tx.removeField(field(listRid, fieldName));\n}\n\n//\n// Internals\n//\n\nasync function findTemplateField(\n tx: PlTransaction,\n listRid: SignedResourceId,\n id: TemplateId,\n): Promise<string | undefined> {\n const data = await tx.getResourceData(listRid, true);\n for (const f of data.fields) {\n if (isNullSignedResourceId(f.value)) continue;\n if (resourceIdToString(f.value) === (id as string)) return f.name;\n }\n return undefined;\n}\n"],"mappings":";;;;;;;;;;;;;;;;AAsBA,SAAgB,eACd,IACA,SACA,OACA,MACa;CACb,MAAM,MAAM,GAAG,gBAAgB,sBAAsB,KAAK,UAAU,IAAI,CAAC;CACzE,GAAG,KAAK,GAAG;CACX,GAAG,UAAU,KAAK,kBAAkB,KAAK,UAAU,KAAK,CAAC;CACzD,GAAG,UAAU,KAAK,0BAA0B,OAAO,KAAK,IAAI,CAAC,CAAC;CAC9D,GAAG,YAAY,MAAM,SAAS,WAAW,CAAC,GAAG,WAAW,GAAG;CAC3D,OAAO;AACT;;;AAIA,SAAgB,eAAe,IAAmB,KAAuB,OAAqB;CAC5F,GAAG,UAAU,KAAK,kBAAkB,KAAK,UAAU,KAAK,CAAC;AAC3D;;;;;;;;AASA,eAAsB,eACpB,IACA,SACA,IACe;CACf,MAAM,YAAY,MAAM,kBAAkB,IAAI,SAAS,EAAE;CACzD,IAAI,cAAc,KAAA,GAAW,MAAM,IAAI,MAAM,YAAY,GAAG,6BAA6B;CACzF,GAAG,YAAY,MAAM,SAAS,SAAS,CAAC;AAC1C;AAMA,eAAe,kBACb,IACA,SACA,IAC6B;CAC7B,MAAM,OAAO,MAAM,GAAG,gBAAgB,SAAS,IAAI;CACnD,KAAK,MAAM,KAAK,KAAK,QAAQ;EAC3B,IAAI,uBAAuB,EAAE,KAAK,GAAG;EACrC,IAAI,mBAAmB,EAAE,KAAK,MAAO,IAAe,OAAO,EAAE;CAC/D;AAEF"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-middle-layer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.69.0",
|
|
4
4
|
"description": "Pl Middle Layer",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -31,24 +31,24 @@
|
|
|
31
31
|
"yaml": "^2.8.0",
|
|
32
32
|
"zod": "~3.25.76",
|
|
33
33
|
"@milaboratories/columns-collection-driver": "0.2.4",
|
|
34
|
-
"@milaboratories/helpers": "1.14.5",
|
|
35
34
|
"@milaboratories/computable": "2.9.8",
|
|
35
|
+
"@milaboratories/helpers": "1.14.5",
|
|
36
36
|
"@milaboratories/pf-driver": "1.9.1",
|
|
37
|
+
"@milaboratories/pl-client": "3.16.0",
|
|
37
38
|
"@milaboratories/pf-spec-driver": "1.5.1",
|
|
38
|
-
"@milaboratories/pl-
|
|
39
|
-
"@milaboratories/pl-drivers": "1.16.18",
|
|
39
|
+
"@milaboratories/pl-drivers": "1.16.19",
|
|
40
40
|
"@milaboratories/pl-deployments": "3.0.16",
|
|
41
|
-
"@milaboratories/pl-
|
|
42
|
-
"@milaboratories/pl-
|
|
41
|
+
"@milaboratories/pl-model-backend": "1.4.23",
|
|
42
|
+
"@milaboratories/pl-errors": "1.4.38",
|
|
43
|
+
"@milaboratories/pl-model-common": "1.48.0",
|
|
43
44
|
"@milaboratories/pl-http": "1.2.4",
|
|
44
45
|
"@milaboratories/pl-model-middle-layer": "1.32.0",
|
|
45
|
-
"@milaboratories/pl-
|
|
46
|
-
"@milaboratories/pl-tree": "1.14.1",
|
|
46
|
+
"@milaboratories/pl-tree": "1.14.2",
|
|
47
47
|
"@milaboratories/resolve-helper": "1.1.3",
|
|
48
48
|
"@milaboratories/ts-helpers": "1.8.6",
|
|
49
49
|
"@platforma-sdk/model": "1.83.0",
|
|
50
|
-
"@platforma-sdk/
|
|
51
|
-
"@platforma-sdk/
|
|
50
|
+
"@platforma-sdk/workflow-tengo": "6.8.3",
|
|
51
|
+
"@platforma-sdk/block-tools": "2.14.6"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "~24.5.2",
|
|
@@ -5,3 +5,12 @@ export * from "./ops";
|
|
|
5
5
|
export type { TreeSnapshotMiss, TreeSnapshotStat } from "./tree_snapshot_store";
|
|
6
6
|
export { ProjectsField, ProjectsResourceType } from "./project_list";
|
|
7
7
|
export type { OutgoingShare, PendingShare } from "./sharing_list";
|
|
8
|
+
export { TemplatesField } from "./template_list";
|
|
9
|
+
export type {
|
|
10
|
+
CreateProjectFromTemplateOutcome,
|
|
11
|
+
SaveProjectAsTemplateOutcome,
|
|
12
|
+
ShareTemplateOutcome,
|
|
13
|
+
StoredTemplateData,
|
|
14
|
+
TemplateId,
|
|
15
|
+
TemplateListEntry,
|
|
16
|
+
} from "./template_list";
|