@milaboratories/pl-middle-layer 1.70.0 → 1.71.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 +0 -2
- package/dist/index.d.ts +1 -2
- package/dist/index.js +1 -2
- package/dist/middle_layer/build_stamp.cjs +1 -1
- package/dist/middle_layer/build_stamp.js +1 -1
- package/dist/middle_layer/middle_layer.cjs +14 -45
- package/dist/middle_layer/middle_layer.cjs.map +1 -1
- package/dist/middle_layer/middle_layer.d.ts +8 -15
- package/dist/middle_layer/middle_layer.d.ts.map +1 -1
- package/dist/middle_layer/middle_layer.js +14 -45
- package/dist/middle_layer/middle_layer.js.map +1 -1
- package/dist/middle_layer/template_list.cjs.map +1 -1
- package/dist/middle_layer/template_list.d.ts +1 -6
- package/dist/middle_layer/template_list.d.ts.map +1 -1
- package/dist/middle_layer/template_list.js.map +1 -1
- package/dist/model/index.cjs +0 -2
- package/dist/model/index.d.ts +1 -2
- package/dist/model/index.js +1 -2
- package/package.json +8 -8
- package/src/middle_layer/middle_layer.ts +21 -57
- package/src/middle_layer/template_list.ts +2 -5
- package/src/middle_layer/templates.test.ts +11 -20
- package/src/model/index.ts +0 -1
- package/dist/model/template_share.cjs +0 -42
- package/dist/model/template_share.cjs.map +0 -1
- package/dist/model/template_share.d.ts +0 -27
- package/dist/model/template_share.d.ts.map +0 -1
- package/dist/model/template_share.js +0 -42
- package/dist/model/template_share.js.map +0 -1
- package/src/model/template_share.test.ts +0 -78
- package/src/model/template_share.ts +0 -52
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"template_list.js","names":[],"sources":["../../src/middle_layer/template_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { TreeAndComputableU } from \"./types\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { Branded, ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport type { TemplateExportProblem } from \"../model/template_export\";\nimport type {
|
|
1
|
+
{"version":3,"file":"template_list.js","names":[],"sources":["../../src/middle_layer/template_list.ts"],"sourcesContent":["import type { PruningFunction } from \"@milaboratories/pl-tree\";\nimport { SynchronizedTreeState } from \"@milaboratories/pl-tree\";\nimport type {\n Filter,\n PlClient,\n PlTransaction,\n ResourceType,\n SignedResourceId,\n} from \"@milaboratories/pl-client\";\nimport {\n field,\n isNullSignedResourceId,\n resourceIdToString,\n resourceTypesEqual,\n treeFilter,\n} from \"@milaboratories/pl-client\";\nimport type { TreeAndComputableU } from \"./types\";\nimport { Computable } from \"@milaboratories/computable\";\nimport type { MiddleLayerEnvironment } from \"./middle_layer\";\nimport { notEmpty } from \"@milaboratories/ts-helpers\";\nimport type { Branded, ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport type { TemplateExportProblem } from \"../model/template_export\";\nimport type { ShareId } from \"../model/sharing_model\";\nimport type { AppliedEntry, TemplateApplyProblem } from \"../model/template_apply\";\nimport type { ProjectId } from \"../model/project_model\";\n\nexport const TemplatesField = \"templates\";\nexport const TemplatesResourceType: ResourceType = { name: \"Templates\", version: \"1\" };\nexport const TemplateResourceType: ResourceType = { name: \"UserTemplate\", version: \"1\" };\n\n/** Mutable: the only part of a stored template a rename may touch. */\nexport const TemplateLabelKey = \"TemplateLabel\";\nexport const TemplateCreatedTimestamp = \"TemplateCreated\";\n\n/**\n * Unique template identifier in middle layer, the stringified signed resource id of the\n * `UserTemplate`. Branded so it cannot be confused with a {@link ProjectId} — both are\n * stringified resource ids and every template method takes one of them.\n */\nexport type TemplateId = Branded<string, \"TemplateId\">;\n\n/**\n * Immutable `data` on a UserTemplate: the document plus what was true when it was taken.\n *\n * The document lives here and never in KV: KV is listed and fully re-read on every poll,\n * while resource data syncs incrementally, and a template document is a multi-kilobyte\n * value that never changes.\n */\nexport interface StoredTemplateData {\n schemaVersion: 1;\n document: ProjectTemplateV1;\n /** Provenance, display only; absent for a template that arrived as a share. */\n sourceProjectLabel?: string;\n /** Login of the sender, when it arrived as a share. */\n sender?: string;\n}\n\n/** Decodes the immutable `data` blob of a `UserTemplate` read through a transaction. The\n * single raw-decode site; the tree side reads the same JSON with `getDataAsJson`. */\nexport function decodeStoredTemplateData(data: Uint8Array): StoredTemplateData {\n return JSON.parse(Buffer.from(data).toString(\"utf-8\")) as StoredTemplateData;\n}\n\n/** One template as the template list surfaces it. */\nexport interface TemplateListEntry {\n /** Unique template identifier in middle layer. Use to operate with the given template. */\n id: TemplateId;\n /** The mutable label, the only part a rename changes. */\n label: string;\n created: Date;\n /** Number of blocks the stored document lists — derived, not stored. */\n blockCount: number;\n sourceProjectLabel?: string;\n sender?: string;\n}\n\n/** What saving a project as a template yields: the stored template, or every block in the way. */\nexport type SaveProjectAsTemplateOutcome =\n | { readonly ok: true; readonly templateId: TemplateId }\n | { readonly ok: false; readonly problems: readonly TemplateExportProblem[] };\n\n/** What sharing a stored template yields: the share's logical id. */\nexport type ShareTemplateOutcome = { readonly shareId: ShareId };\n\n/**\n * What applying a stored template yields.\n *\n * `ok: false` carries no project id because no project was created: nothing is written\n * until every entry has an installable block.\n */\nexport type CreateProjectFromTemplateOutcome =\n | {\n readonly ok: true;\n readonly projectId: ProjectId;\n readonly added: readonly AppliedEntry[];\n }\n | { readonly ok: false; readonly problems: readonly TemplateApplyProblem[] };\n\n/**\n * Resolves the templates-list resource on the transaction's client root, lazily creating (and\n * locking) an empty one when the {@link TemplatesField} is not yet populated. Returns its signed\n * id. Used when writing into a root that may have no templates list yet, e.g. a template landing\n * in a recipient's root.\n */\nexport async function ensureTemplateListRid(tx: PlTransaction): Promise<SignedResourceId> {\n const templatesField = field(tx.clientRoot, TemplatesField);\n tx.createField(templatesField, \"Dynamic\");\n const fData = await tx.getField(templatesField);\n if (isNullSignedResourceId(fData.value)) {\n const ref = tx.createEphemeral(TemplatesResourceType);\n tx.lock(ref);\n tx.setField(templatesField, ref);\n return await ref.globalId;\n }\n return fData.value;\n}\n\nexport const TemplatesListTreePruningFunction: PruningFunction = (resource) => {\n if (!resourceTypesEqual(resource.type, TemplatesResourceType)) return [];\n return resource.fields;\n};\n\nexport const templatesListFieldFilter: Filter = treeFilter.resourceTypeEq(\n TemplatesResourceType.name,\n);\n\nexport async function createTemplateList(\n pl: PlClient,\n rid: SignedResourceId,\n env: MiddleLayerEnvironment,\n): Promise<TreeAndComputableU<TemplateListEntry[]>> {\n const tree = await SynchronizedTreeState.init(\n pl,\n rid,\n {\n ...env.ops.defaultTreeOptions,\n pruning: TemplatesListTreePruningFunction,\n fieldFilter: templatesListFieldFilter,\n },\n env.logger,\n );\n\n const c = Computable.make((ctx) => {\n const node = ctx.accessor(tree.entry()).node();\n if (node === undefined) return undefined;\n const result: TemplateListEntry[] = [];\n\n // Templates list resource keeps templates assigned to fields. Each field name is a UUID\n for (const field of node.listDynamicFields()) {\n const tpl = node.traverse(field);\n if (tpl === undefined) continue;\n const data = tpl.getDataAsJson<StoredTemplateData>();\n // A template whose data has not synced yet is not an entry with unknown content —\n // it is an entry we cannot describe at all, so it stays out of the list until it has.\n if (data === undefined) continue;\n const label = notEmpty(tpl.getKeyValueAsJson<string>(TemplateLabelKey));\n const created = notEmpty(tpl.getKeyValueAsJson<number>(TemplateCreatedTimestamp));\n result.push({\n id: resourceIdToString(tpl.id) as TemplateId,\n label,\n created: new Date(created),\n blockCount: data.document.blocks.length,\n ...(data.sourceProjectLabel !== undefined\n ? { sourceProjectLabel: data.sourceProjectLabel }\n : {}),\n ...(data.sender !== undefined ? { sender: data.sender } : {}),\n });\n }\n result.sort((a, b) => b.created.valueOf() - a.created.valueOf());\n return result;\n }).withStableType();\n\n return { computable: c, tree };\n}\n"],"mappings":";;;;;AA0BA,MAAa,iBAAiB;AAC9B,MAAa,wBAAsC;CAAE,MAAM;CAAa,SAAS;AAAI;AACrF,MAAa,uBAAqC;CAAE,MAAM;CAAgB,SAAS;AAAI;;AAGvF,MAAa,mBAAmB;AAChC,MAAa,2BAA2B;;;AA2BxC,SAAgB,yBAAyB,MAAsC;CAC7E,OAAO,KAAK,MAAM,OAAO,KAAK,IAAI,CAAC,CAAC,SAAS,OAAO,CAAC;AACvD;AAwDA,MAAa,oCAAqD,aAAa;CAC7E,IAAI,CAAC,mBAAmB,SAAS,MAAM,qBAAqB,GAAG,OAAO,CAAC;CACvE,OAAO,SAAS;AAClB;AAEA,MAAa,2BAAmC,WAAW,eACzD,sBAAsB,IACxB;AAEA,eAAsB,mBACpB,IACA,KACA,KACkD;CAClD,MAAM,OAAO,MAAM,sBAAsB,KACvC,IACA,KACA;EACE,GAAG,IAAI,IAAI;EACX,SAAS;EACT,aAAa;CACf,GACA,IAAI,MACN;CAgCA,OAAO;EAAE,YA9BC,WAAW,MAAM,QAAQ;GACjC,MAAM,OAAO,IAAI,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,KAAK;GAC7C,IAAI,SAAS,KAAA,GAAW,OAAO,KAAA;GAC/B,MAAM,SAA8B,CAAC;GAGrC,KAAK,MAAM,SAAS,KAAK,kBAAkB,GAAG;IAC5C,MAAM,MAAM,KAAK,SAAS,KAAK;IAC/B,IAAI,QAAQ,KAAA,GAAW;IACvB,MAAM,OAAO,IAAI,cAAkC;IAGnD,IAAI,SAAS,KAAA,GAAW;IACxB,MAAM,QAAQ,SAAS,IAAI,kBAA0B,gBAAgB,CAAC;IACtE,MAAM,UAAU,SAAS,IAAI,kBAA0B,wBAAwB,CAAC;IAChF,OAAO,KAAK;KACV,IAAI,mBAAmB,IAAI,EAAE;KAC7B;KACA,SAAS,IAAI,KAAK,OAAO;KACzB,YAAY,KAAK,SAAS,OAAO;KACjC,GAAI,KAAK,uBAAuB,KAAA,IAC5B,EAAE,oBAAoB,KAAK,mBAAmB,IAC9C,CAAC;KACL,GAAI,KAAK,WAAW,KAAA,IAAY,EAAE,QAAQ,KAAK,OAAO,IAAI,CAAC;IAC7D,CAAC;GACH;GACA,OAAO,MAAM,GAAG,MAAM,EAAE,QAAQ,QAAQ,IAAI,EAAE,QAAQ,QAAQ,CAAC;GAC/D,OAAO;EACT,CAAC,CAAC,CAAC,eAEkB;EAAG;CAAK;AAC/B"}
|
package/dist/model/index.cjs
CHANGED
|
@@ -5,7 +5,6 @@ const require_template_parser = require("./template_parser.cjs");
|
|
|
5
5
|
const require_template_resolve = require("./template_resolve.cjs");
|
|
6
6
|
const require_template_apply = require("./template_apply.cjs");
|
|
7
7
|
const require_template_serializer = require("./template_serializer.cjs");
|
|
8
|
-
const require_template_share = require("./template_share.cjs");
|
|
9
8
|
exports.AcceptanceFieldPrefix = require_sharing_model.AcceptanceFieldPrefix;
|
|
10
9
|
exports.BlockArgsAuthorKeyPrefix = require_project_model.BlockArgsAuthorKeyPrefix;
|
|
11
10
|
exports.EnvelopeSchemaVersionCurrent = require_sharing_model.EnvelopeSchemaVersionCurrent;
|
|
@@ -42,4 +41,3 @@ exports.parseBlockPackName = require_template_resolve.parseBlockPackName;
|
|
|
42
41
|
exports.parseProjectTemplateV1Yaml = require_template_parser.parseProjectTemplateV1Yaml;
|
|
43
42
|
exports.resolveTemplateEntries = require_template_resolve.resolveTemplateEntries;
|
|
44
43
|
exports.stringifyProjectTemplateV1 = require_template_serializer.stringifyProjectTemplateV1;
|
|
45
|
-
exports.unshareableTemplateEntries = require_template_share.unshareableTemplateEntries;
|
package/dist/model/index.d.ts
CHANGED
|
@@ -2,9 +2,8 @@ import { AppliedEntry, TemplateApplyProblem, TemplateApplyReport, TemplateEntryR
|
|
|
2
2
|
import { BlockPackProvider, ExactResolution, KindResolution, ResolvedEntry, TemplateResolveOutcome, parseBlockPackName, resolveTemplateEntries } from "./template_resolve.js";
|
|
3
3
|
import { BlockArgsAuthorKeyPrefix, ProjectCreatedTimestamp, ProjectField, ProjectId, ProjectLastModifiedTimestamp, ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey } from "./project_model.js";
|
|
4
4
|
import { TemplateExportProblem } from "./template_export.js";
|
|
5
|
-
import { TemplateShareProblem, unshareableTemplateEntries } from "./template_share.js";
|
|
6
5
|
import { AcceptanceFieldPrefix, EnvelopeAcceptance, EnvelopeData, EnvelopeMode, EnvelopePayload, EnvelopePayloadKind, EnvelopeProject, EnvelopeSchemaVersion, EnvelopeSchemaVersionCurrent, ProjectChangeAction, ProjectFieldUuid, ShareId, ShareProjectsOptions, ShareTemplateOptions, SharedEnvelopeResourceType, SharingDecision, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, canImpersonate, decisionField, decodeEnvelopeData, envelopeProjectMap, isAcceptanceField, newShareId, normalizeEnvelopeData } from "./sharing_model.js";
|
|
7
6
|
import { ProjectTemplateExportOutcome, locationOf, stringifyProjectTemplateV1 } from "./template_serializer.js";
|
|
8
7
|
import { BlockPackExplicit, BlockPackSpecAny, BlockPackSpecPrepared, FrontendFromFolder, FrontendFromFolderData, FrontendFromFolderResourceType, FrontendFromLocalTgz, FrontendFromLocalTgzData, FrontendFromLocalTgzResourceType, FrontendFromUrl, FrontendFromUrlData, FrontendFromUrlResourceType, FrontendSpec } from "./block_pack_spec.js";
|
|
9
8
|
import { TemplateParseOutcome, parseProjectTemplateV1Yaml } from "./template_parser.js";
|
|
10
|
-
export { AcceptanceFieldPrefix, type AppliedEntry, BlockArgsAuthorKeyPrefix, BlockPackExplicit, type BlockPackProvider, BlockPackSpecAny, BlockPackSpecPrepared, EnvelopeAcceptance, EnvelopeData, EnvelopeMode, EnvelopePayload, EnvelopePayloadKind, EnvelopeProject, EnvelopeSchemaVersion, EnvelopeSchemaVersionCurrent, type ExactResolution, FrontendFromFolder, FrontendFromFolderData, FrontendFromFolderResourceType, FrontendFromLocalTgz, FrontendFromLocalTgzData, FrontendFromLocalTgzResourceType, FrontendFromUrl, FrontendFromUrlData, FrontendFromUrlResourceType, FrontendSpec, type KindResolution, ProjectChangeAction, ProjectCreatedTimestamp, type ProjectField, ProjectFieldUuid, ProjectLastModifiedTimestamp, type ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, type ProjectTemplateExportOutcome, type ResolvedEntry, SchemaVersionCurrent, SchemaVersionKey, ShareId, ShareProjectsOptions, ShareTemplateOptions, SharedEnvelopeResourceType, SharingDecision, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, type TemplateApplyProblem, type TemplateApplyReport, TemplateEntryRejected, type TemplateExportProblem, type TemplateParseOutcome, type TemplateResolveOutcome,
|
|
9
|
+
export { AcceptanceFieldPrefix, type AppliedEntry, BlockArgsAuthorKeyPrefix, BlockPackExplicit, type BlockPackProvider, BlockPackSpecAny, BlockPackSpecPrepared, EnvelopeAcceptance, EnvelopeData, EnvelopeMode, EnvelopePayload, EnvelopePayloadKind, EnvelopeProject, EnvelopeSchemaVersion, EnvelopeSchemaVersionCurrent, type ExactResolution, FrontendFromFolder, FrontendFromFolderData, FrontendFromFolderResourceType, FrontendFromLocalTgz, FrontendFromLocalTgzData, FrontendFromLocalTgzResourceType, FrontendFromUrl, FrontendFromUrlData, FrontendFromUrlResourceType, FrontendSpec, type KindResolution, ProjectChangeAction, ProjectCreatedTimestamp, type ProjectField, ProjectFieldUuid, ProjectLastModifiedTimestamp, type ProjectListEntry, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, type ProjectTemplateExportOutcome, type ResolvedEntry, SchemaVersionCurrent, SchemaVersionKey, ShareId, ShareProjectsOptions, ShareTemplateOptions, SharedEnvelopeResourceType, SharingDecision, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, type TemplateApplyProblem, type TemplateApplyReport, TemplateEntryRejected, type TemplateExportProblem, type TemplateParseOutcome, type TemplateResolveOutcome, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, canImpersonate, decisionField, decodeEnvelopeData, envelopeProjectMap, isAcceptanceField, locationOf, newShareId, normalizeEnvelopeData, parseBlockPackName, parseProjectTemplateV1Yaml, resolveTemplateEntries, stringifyProjectTemplateV1 };
|
package/dist/model/index.js
CHANGED
|
@@ -5,5 +5,4 @@ import { parseProjectTemplateV1Yaml } from "./template_parser.js";
|
|
|
5
5
|
import { parseBlockPackName, resolveTemplateEntries } from "./template_resolve.js";
|
|
6
6
|
import { TemplateEntryRejected } from "./template_apply.js";
|
|
7
7
|
import { locationOf, stringifyProjectTemplateV1 } from "./template_serializer.js";
|
|
8
|
-
|
|
9
|
-
export { AcceptanceFieldPrefix, BlockArgsAuthorKeyPrefix, EnvelopeSchemaVersionCurrent, FrontendFromFolderResourceType, FrontendFromLocalTgzResourceType, FrontendFromUrlResourceType, ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, TemplateEntryRejected, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, canImpersonate, decisionField, decodeEnvelopeData, envelopeProjectMap, isAcceptanceField, locationOf, newShareId, normalizeEnvelopeData, parseBlockPackName, parseProjectTemplateV1Yaml, resolveTemplateEntries, stringifyProjectTemplateV1, unshareableTemplateEntries };
|
|
8
|
+
export { AcceptanceFieldPrefix, BlockArgsAuthorKeyPrefix, EnvelopeSchemaVersionCurrent, FrontendFromFolderResourceType, FrontendFromLocalTgzResourceType, FrontendFromUrlResourceType, ProjectCreatedTimestamp, ProjectLastModifiedTimestamp, ProjectMetaKey, ProjectResourceType, ProjectStructureAuthorKey, ProjectStructureKey, SchemaVersionCurrent, SchemaVersionKey, SharedEnvelopeResourceType, SharingOutboxField, SharingOutboxResourceType, SharingStateField, SharingStateResourceType, TemplateEntryRejected, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, canImpersonate, decisionField, decodeEnvelopeData, envelopeProjectMap, isAcceptanceField, locationOf, newShareId, normalizeEnvelopeData, parseBlockPackName, parseProjectTemplateV1Yaml, resolveTemplateEntries, stringifyProjectTemplateV1 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-middle-layer",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.71.0",
|
|
4
4
|
"description": "Pl Middle Layer",
|
|
5
5
|
"keywords": [],
|
|
6
6
|
"license": "UNLICENSED",
|
|
@@ -30,25 +30,25 @@
|
|
|
30
30
|
"utility-types": "^3.11.0",
|
|
31
31
|
"yaml": "^2.8.0",
|
|
32
32
|
"zod": "~3.25.76",
|
|
33
|
-
"@milaboratories/helpers": "1.14.5",
|
|
34
|
-
"@milaboratories/pf-spec-driver": "1.5.1",
|
|
35
33
|
"@milaboratories/columns-collection-driver": "0.2.4",
|
|
34
|
+
"@milaboratories/helpers": "1.14.5",
|
|
36
35
|
"@milaboratories/computable": "2.9.8",
|
|
37
36
|
"@milaboratories/pf-driver": "1.9.1",
|
|
37
|
+
"@milaboratories/pf-spec-driver": "1.5.1",
|
|
38
38
|
"@milaboratories/pl-deployments": "3.0.16",
|
|
39
|
-
"@milaboratories/pl-client": "3.16.0",
|
|
40
39
|
"@milaboratories/pl-drivers": "1.16.19",
|
|
41
40
|
"@milaboratories/pl-http": "1.2.4",
|
|
42
41
|
"@milaboratories/pl-model-backend": "1.4.23",
|
|
42
|
+
"@milaboratories/pl-client": "3.16.0",
|
|
43
|
+
"@milaboratories/pl-model-common": "1.48.0",
|
|
43
44
|
"@milaboratories/pl-model-middle-layer": "1.32.0",
|
|
44
45
|
"@milaboratories/pl-tree": "1.14.2",
|
|
45
|
-
"@milaboratories/pl-model-common": "1.48.0",
|
|
46
|
-
"@milaboratories/resolve-helper": "1.1.3",
|
|
47
46
|
"@milaboratories/ts-helpers": "1.8.6",
|
|
48
47
|
"@milaboratories/pl-errors": "1.4.38",
|
|
49
|
-
"@platforma-sdk/workflow-tengo": "6.9.0",
|
|
50
48
|
"@platforma-sdk/block-tools": "2.14.6",
|
|
51
|
-
"@platforma-sdk/model": "1.83.9"
|
|
49
|
+
"@platforma-sdk/model": "1.83.9",
|
|
50
|
+
"@milaboratories/resolve-helper": "1.1.3",
|
|
51
|
+
"@platforma-sdk/workflow-tengo": "6.9.0"
|
|
52
52
|
},
|
|
53
53
|
"devDependencies": {
|
|
54
54
|
"@types/node": "~24.5.2",
|
|
@@ -75,8 +75,6 @@ import {
|
|
|
75
75
|
type ShareProjectsOptions,
|
|
76
76
|
type ShareTemplateOptions,
|
|
77
77
|
} from "../model/sharing_model";
|
|
78
|
-
import type { TemplateShareProblem } from "../model/template_share";
|
|
79
|
-
import { unshareableTemplateEntries } from "../model/template_share";
|
|
80
78
|
import {
|
|
81
79
|
buildShareEnvelope,
|
|
82
80
|
buildTemplateShareEnvelope,
|
|
@@ -1002,9 +1000,10 @@ export class MiddleLayer {
|
|
|
1002
1000
|
* The cost of that is the donor's receipt: nobody can write an acceptance onto a read-only
|
|
1003
1001
|
* envelope, so a template share never reports who accepted it.
|
|
1004
1002
|
*
|
|
1005
|
-
*
|
|
1006
|
-
*
|
|
1007
|
-
*
|
|
1003
|
+
* Nothing about the document is checked: a stored template is shareable by virtue of existing.
|
|
1004
|
+
* An entry the recipient cannot resolve — a block installed from a folder on the sender's
|
|
1005
|
+
* machine, say — is theirs to see when they preview or apply it, where every unresolvable entry
|
|
1006
|
+
* is named anyway.
|
|
1008
1007
|
*
|
|
1009
1008
|
* @param id template to share
|
|
1010
1009
|
* @param options recipients XOR everyone, plus the title recipients see
|
|
@@ -1013,8 +1012,7 @@ export class MiddleLayer {
|
|
|
1013
1012
|
id: TemplateId,
|
|
1014
1013
|
options: ShareTemplateOptions,
|
|
1015
1014
|
): Promise<ShareTemplateOutcome> {
|
|
1016
|
-
const
|
|
1017
|
-
if (!loaded.ok) return { ok: false, problems: loaded.problems };
|
|
1015
|
+
const template = await this.loadTemplateForShare(id);
|
|
1018
1016
|
|
|
1019
1017
|
const everyone = "everyone" in options;
|
|
1020
1018
|
const sender = this.currentUserLogin ?? "";
|
|
@@ -1026,7 +1024,7 @@ export class MiddleLayer {
|
|
|
1026
1024
|
const { envelope, data } = buildTemplateShareEnvelope(
|
|
1027
1025
|
tx,
|
|
1028
1026
|
this.sharingOutboxResourceId,
|
|
1029
|
-
|
|
1027
|
+
template,
|
|
1030
1028
|
{ sender, title: options.title, expiresAt },
|
|
1031
1029
|
);
|
|
1032
1030
|
shareId = data.shareId;
|
|
@@ -1037,29 +1035,16 @@ export class MiddleLayer {
|
|
|
1037
1035
|
});
|
|
1038
1036
|
|
|
1039
1037
|
await this.sharingOutboxTree.refreshState();
|
|
1040
|
-
return {
|
|
1038
|
+
return { shareId: shareId! };
|
|
1041
1039
|
}
|
|
1042
1040
|
|
|
1043
|
-
/**
|
|
1044
|
-
*
|
|
1045
|
-
|
|
1046
|
-
* template itself instead of only when the user tries to share it.
|
|
1047
|
-
*/
|
|
1048
|
-
public async checkTemplateShareable(id: TemplateId): Promise<readonly TemplateShareProblem[]> {
|
|
1049
|
-
const stored = await this.getTemplateData(id);
|
|
1050
|
-
return unshareableTemplateEntries(stored.document);
|
|
1051
|
-
}
|
|
1052
|
-
|
|
1053
|
-
/** The document and the label of a template that may be shared, or every entry that stops it.
|
|
1054
|
-
* The label is what the recipient's own list will show, so it travels with the document. */
|
|
1055
|
-
private async loadShareableTemplate(
|
|
1041
|
+
/** The document and the label of a template about to be shared. The label is what the
|
|
1042
|
+
* recipient's own list will show, so it travels with the document. */
|
|
1043
|
+
private async loadTemplateForShare(
|
|
1056
1044
|
id: TemplateId,
|
|
1057
|
-
): Promise<
|
|
1058
|
-
| { ok: true; template: { document: ProjectTemplateV1; label: string } }
|
|
1059
|
-
| { ok: false; problems: readonly TemplateShareProblem[] }
|
|
1060
|
-
> {
|
|
1045
|
+
): Promise<{ document: ProjectTemplateV1; label: string }> {
|
|
1061
1046
|
const rid = await this.resolveTemplateId(id);
|
|
1062
|
-
|
|
1047
|
+
return await this.pl.withReadTx("MLReadTemplateForShare", async (tx) => {
|
|
1063
1048
|
const rd = await tx.getResourceData(rid, false);
|
|
1064
1049
|
if (rd.data === undefined) throw new Error(`Template ${id} carries no document.`);
|
|
1065
1050
|
return {
|
|
@@ -1067,10 +1052,6 @@ export class MiddleLayer {
|
|
|
1067
1052
|
label: await tx.getKValueJson<string>(rid, TemplateLabelKey),
|
|
1068
1053
|
};
|
|
1069
1054
|
});
|
|
1070
|
-
|
|
1071
|
-
const problems = unshareableTemplateEntries(template.document);
|
|
1072
|
-
if (problems.length > 0) return { ok: false, problems };
|
|
1073
|
-
return { ok: true, template };
|
|
1074
1055
|
}
|
|
1075
1056
|
|
|
1076
1057
|
/**
|
|
@@ -1092,8 +1073,7 @@ export class MiddleLayer {
|
|
|
1092
1073
|
* `opts.templateId` is required for, and only used by, a share that carries a template: a stored
|
|
1093
1074
|
* template is immutable, so an improved one is a different template and the share cannot re-read
|
|
1094
1075
|
* the one it started from — the caller names the new target. Every other option means the same
|
|
1095
|
-
* thing for both kinds of share.
|
|
1096
|
-
* {@link checkTemplateShareable}) or this throws.
|
|
1076
|
+
* thing for both kinds of share.
|
|
1097
1077
|
*/
|
|
1098
1078
|
public async changeShare(
|
|
1099
1079
|
shareId: ShareId,
|
|
@@ -1105,14 +1085,9 @@ export class MiddleLayer {
|
|
|
1105
1085
|
templateId?: TemplateId;
|
|
1106
1086
|
} = {},
|
|
1107
1087
|
): Promise<void> {
|
|
1108
|
-
// Read outside the write tx: it is two round-trips of its own
|
|
1109
|
-
// must be raised before anything is torn down.
|
|
1088
|
+
// Read outside the write tx: it is two round-trips of its own.
|
|
1110
1089
|
const target =
|
|
1111
|
-
opts.templateId === undefined ? undefined : await this.
|
|
1112
|
-
if (target !== undefined && !target.ok)
|
|
1113
|
-
throw new Error(
|
|
1114
|
-
`changeShare: template ${opts.templateId} cannot be shared: ${describeShareProblems(target.problems)}`,
|
|
1115
|
-
);
|
|
1090
|
+
opts.templateId === undefined ? undefined : await this.loadTemplateForShare(opts.templateId);
|
|
1116
1091
|
|
|
1117
1092
|
await this.pl.withWriteTx("MLChangeShare", async (tx) => {
|
|
1118
1093
|
const old = await this.resolveOutboxEnvelope(tx, shareId);
|
|
@@ -1137,17 +1112,12 @@ export class MiddleLayer {
|
|
|
1137
1112
|
|
|
1138
1113
|
// Same shareId, same outbox field name — detach the old field before rebuilding, or they collide.
|
|
1139
1114
|
tx.removeField(field(this.sharingOutboxResourceId, old.fieldName));
|
|
1140
|
-
const { envelope } = buildTemplateShareEnvelope(
|
|
1141
|
-
|
|
1142
|
-
|
|
1143
|
-
|
|
1144
|
-
|
|
1145
|
-
|
|
1146
|
-
title: opts.title === undefined ? old.data.title : opts.title.trim(),
|
|
1147
|
-
expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs,
|
|
1148
|
-
shareId, // SAME shareId — the essence of change
|
|
1149
|
-
},
|
|
1150
|
-
);
|
|
1115
|
+
const { envelope } = buildTemplateShareEnvelope(tx, this.sharingOutboxResourceId, target, {
|
|
1116
|
+
sender: self,
|
|
1117
|
+
title: opts.title === undefined ? old.data.title : opts.title.trim(),
|
|
1118
|
+
expiresAt: everyone ? null : Date.now() + this.env.ops.envelopeTtlMs,
|
|
1119
|
+
shareId, // SAME shareId — the essence of change
|
|
1120
|
+
});
|
|
1151
1121
|
|
|
1152
1122
|
// Nothing to transfer: a read-only grant cannot write an acceptance, so a template share
|
|
1153
1123
|
// never accumulated one.
|
|
@@ -1828,9 +1798,3 @@ export class MiddleLayer {
|
|
|
1828
1798
|
//
|
|
1829
1799
|
// Internals
|
|
1830
1800
|
//
|
|
1831
|
-
|
|
1832
|
-
/** Refusal reasons as one line, each naming the entry it belongs to, so a throw that escapes to a
|
|
1833
|
-
* log still says which block stands in the way. */
|
|
1834
|
-
function describeShareProblems(problems: readonly TemplateShareProblem[]): string {
|
|
1835
|
-
return problems.map((p) => `${p.entryId}: ${p.error}`).join("; ");
|
|
1836
|
-
}
|
|
@@ -20,7 +20,6 @@ import type { MiddleLayerEnvironment } from "./middle_layer";
|
|
|
20
20
|
import { notEmpty } from "@milaboratories/ts-helpers";
|
|
21
21
|
import type { Branded, ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
22
22
|
import type { TemplateExportProblem } from "../model/template_export";
|
|
23
|
-
import type { TemplateShareProblem } from "../model/template_share";
|
|
24
23
|
import type { ShareId } from "../model/sharing_model";
|
|
25
24
|
import type { AppliedEntry, TemplateApplyProblem } from "../model/template_apply";
|
|
26
25
|
import type { ProjectId } from "../model/project_model";
|
|
@@ -80,10 +79,8 @@ export type SaveProjectAsTemplateOutcome =
|
|
|
80
79
|
| { readonly ok: true; readonly templateId: TemplateId }
|
|
81
80
|
| { readonly ok: false; readonly problems: readonly TemplateExportProblem[] };
|
|
82
81
|
|
|
83
|
-
/** What sharing a stored template yields: the share's logical id
|
|
84
|
-
export type ShareTemplateOutcome =
|
|
85
|
-
| { readonly ok: true; readonly shareId: ShareId }
|
|
86
|
-
| { readonly ok: false; readonly problems: readonly TemplateShareProblem[] };
|
|
82
|
+
/** What sharing a stored template yields: the share's logical id. */
|
|
83
|
+
export type ShareTemplateOutcome = { readonly shareId: ShareId };
|
|
87
84
|
|
|
88
85
|
/**
|
|
89
86
|
* What applying a stored template yields.
|
|
@@ -91,26 +91,19 @@ test("an entry nothing can resolve creates no project, and every entry is report
|
|
|
91
91
|
});
|
|
92
92
|
});
|
|
93
93
|
|
|
94
|
-
test("a template holding a block from a folder on this machine is
|
|
94
|
+
test("a template holding a block from a folder on this machine is shared like any other", async () => {
|
|
95
95
|
await withMl(async (ml) => {
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
document: documentOf(entry("a"), entry("b", LOCAL_FOLDER)),
|
|
99
|
-
});
|
|
96
|
+
const document = documentOf(entry("a"), entry("b", LOCAL_FOLDER));
|
|
97
|
+
const stored = await storeTemplate(ml, "Built here", { schemaVersion: 1, document });
|
|
100
98
|
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
recipients: ["colleague"],
|
|
107
|
-
title: "Built here",
|
|
108
|
-
});
|
|
99
|
+
// A stored template is shareable by existing. Whether the recipient can resolve every entry
|
|
100
|
+
// is their question, answered where they preview or apply it — not a gate on sending.
|
|
101
|
+
// With everyone, like the other share tests here: a named recipient must already exist on
|
|
102
|
+
// the backend, and the test server has no second user.
|
|
103
|
+
const shared = await ml.shareTemplate(stored.id, { everyone: true, title: "Built here" });
|
|
109
104
|
|
|
110
|
-
|
|
111
|
-
expect(
|
|
112
|
-
// Refused before anything was written: no envelope, so nothing to revoke.
|
|
113
|
-
expect((await ml.outgoingShares.getValue()) ?? []).toStrictEqual([]);
|
|
105
|
+
const outgoing = await ml.outgoingShares.awaitStableValue();
|
|
106
|
+
expect((outgoing ?? []).map((s) => s.shareId)).toStrictEqual([shared.shareId]);
|
|
114
107
|
});
|
|
115
108
|
});
|
|
116
109
|
|
|
@@ -142,7 +135,6 @@ test("an accepted template share lands on the acceptor's shelf and builds nothin
|
|
|
142
135
|
});
|
|
143
136
|
|
|
144
137
|
const shared = await ml.shareTemplate(stored.id, { everyone: true, title: "A pipeline" });
|
|
145
|
-
if (!shared.ok) throw new Error(`share refused: ${JSON.stringify(shared.problems)}`);
|
|
146
138
|
|
|
147
139
|
const outcome = await ml.acceptShare([shared.shareId]);
|
|
148
140
|
|
|
@@ -175,7 +167,6 @@ test("a changed template share keeps its id, and whoever already responded is no
|
|
|
175
167
|
});
|
|
176
168
|
|
|
177
169
|
const shared = await ml.shareTemplate(first.id, { everyone: true, title: "First" });
|
|
178
|
-
if (!shared.ok) throw new Error(`share refused: ${JSON.stringify(shared.problems)}`);
|
|
179
170
|
|
|
180
171
|
// Someone responds to the share, which is what the replace below must not undo.
|
|
181
172
|
const accept = await ml.acceptShare([shared.shareId]);
|
|
@@ -186,7 +177,7 @@ test("a changed template share keeps its id, and whoever already responded is no
|
|
|
186
177
|
await ml.changeShare(shared.shareId, { templateId: second.id, title: "Second" });
|
|
187
178
|
|
|
188
179
|
const outgoing = (await ml.outgoingShares.getValue()) ?? [];
|
|
189
|
-
expect(outgoing.map((s) => s.shareId)).toStrictEqual([shared.shareId]);
|
|
180
|
+
expect((outgoing ?? []).map((s) => s.shareId)).toStrictEqual([shared.shareId]);
|
|
190
181
|
expect(outgoing[0]).toMatchObject({
|
|
191
182
|
payloadKind: "template",
|
|
192
183
|
title: "Second",
|
package/src/model/index.ts
CHANGED
|
@@ -48,4 +48,3 @@ export type { TemplateExportProblem } from "./template_export";
|
|
|
48
48
|
|
|
49
49
|
// The template share path. Whether a template may be shared at all is a question a UI asks about
|
|
50
50
|
// a template it is merely displaying, so the check and its problem type are public.
|
|
51
|
-
export { unshareableTemplateEntries, type TemplateShareProblem } from "./template_share";
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
let _milaboratories_pl_model_common = require("@milaboratories/pl-model-common");
|
|
2
|
-
//#region src/model/template_share.ts
|
|
3
|
-
/**
|
|
4
|
-
* Every entry of a template that cannot travel to another machine, or an empty list for a
|
|
5
|
-
* template that can be shared.
|
|
6
|
-
*
|
|
7
|
-
* An entry's `location` names a place rather than a name, and a `file:` place is a folder on
|
|
8
|
-
* the author's own disk: a recipient resolving it finds nothing, or worse finds something
|
|
9
|
-
* else. Such a template stays perfectly usable where it was made, so it is stored and applied
|
|
10
|
-
* as normal — only sharing it is refused.
|
|
11
|
-
*
|
|
12
|
-
* A location whose scheme cannot be read is refused for the same reason: nothing can resolve
|
|
13
|
-
* it anywhere, here included.
|
|
14
|
-
*
|
|
15
|
-
* Every offending entry is reported, not only the first, so a UI can name each block instead
|
|
16
|
-
* of sending its user round the loop once per entry.
|
|
17
|
-
*/
|
|
18
|
-
function unshareableTemplateEntries(document) {
|
|
19
|
-
const problems = [];
|
|
20
|
-
for (const entry of document.blocks) {
|
|
21
|
-
if (entry.location === void 0) continue;
|
|
22
|
-
let scheme;
|
|
23
|
-
try {
|
|
24
|
-
scheme = (0, _milaboratories_pl_model_common.parseBlockPackLocation)(entry.location).scheme;
|
|
25
|
-
} catch (e) {
|
|
26
|
-
problems.push({
|
|
27
|
-
entryId: entry.id,
|
|
28
|
-
error: `Block is installed from a location nothing can resolve: ${e instanceof Error ? e.message : String(e)}`
|
|
29
|
-
});
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (scheme === "file") problems.push({
|
|
33
|
-
entryId: entry.id,
|
|
34
|
-
error: `Block is installed from ${entry.location}, a folder on this machine — it resolves to nothing on the recipient's, so this template cannot be shared`
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return problems;
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
exports.unshareableTemplateEntries = unshareableTemplateEntries;
|
|
41
|
-
|
|
42
|
-
//# sourceMappingURL=template_share.cjs.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"template_share.cjs","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,UAAA,GAAA,gCAAA,uBAAA,CAAgC,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"}
|
|
@@ -1,27 +0,0 @@
|
|
|
1
|
-
import { ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
2
|
-
//#region src/model/template_share.d.ts
|
|
3
|
-
/** One template entry standing in the way of sharing the template, and why. */
|
|
4
|
-
export type TemplateShareProblem = {
|
|
5
|
-
/** The template-local id of the entry the problem belongs to; on an exported template it is
|
|
6
|
-
* the block's project-local uuid. */
|
|
7
|
-
readonly entryId: string;
|
|
8
|
-
readonly error: string;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* Every entry of a template that cannot travel to another machine, or an empty list for a
|
|
12
|
-
* template that can be shared.
|
|
13
|
-
*
|
|
14
|
-
* An entry's `location` names a place rather than a name, and a `file:` place is a folder on
|
|
15
|
-
* the author's own disk: a recipient resolving it finds nothing, or worse finds something
|
|
16
|
-
* else. Such a template stays perfectly usable where it was made, so it is stored and applied
|
|
17
|
-
* as normal — only sharing it is refused.
|
|
18
|
-
*
|
|
19
|
-
* A location whose scheme cannot be read is refused for the same reason: nothing can resolve
|
|
20
|
-
* it anywhere, here included.
|
|
21
|
-
*
|
|
22
|
-
* Every offending entry is reported, not only the first, so a UI can name each block instead
|
|
23
|
-
* of sending its user round the loop once per entry.
|
|
24
|
-
*/
|
|
25
|
-
export declare function unshareableTemplateEntries(document: ProjectTemplateV1): readonly TemplateShareProblem[];
|
|
26
|
-
//#endregion
|
|
27
|
-
//# sourceMappingURL=template_share.d.ts.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"template_share.d.ts","names":[],"sources":["../../src/model/template_share.ts"],"mappings":";;;YAIY;;;WAGD;WACA;;;;;;;;;;;;;;;;;wBAkBK,2BACd,UAAU,6BACA"}
|
|
@@ -1,42 +0,0 @@
|
|
|
1
|
-
import { parseBlockPackLocation } from "@milaboratories/pl-model-common";
|
|
2
|
-
//#region src/model/template_share.ts
|
|
3
|
-
/**
|
|
4
|
-
* Every entry of a template that cannot travel to another machine, or an empty list for a
|
|
5
|
-
* template that can be shared.
|
|
6
|
-
*
|
|
7
|
-
* An entry's `location` names a place rather than a name, and a `file:` place is a folder on
|
|
8
|
-
* the author's own disk: a recipient resolving it finds nothing, or worse finds something
|
|
9
|
-
* else. Such a template stays perfectly usable where it was made, so it is stored and applied
|
|
10
|
-
* as normal — only sharing it is refused.
|
|
11
|
-
*
|
|
12
|
-
* A location whose scheme cannot be read is refused for the same reason: nothing can resolve
|
|
13
|
-
* it anywhere, here included.
|
|
14
|
-
*
|
|
15
|
-
* Every offending entry is reported, not only the first, so a UI can name each block instead
|
|
16
|
-
* of sending its user round the loop once per entry.
|
|
17
|
-
*/
|
|
18
|
-
function unshareableTemplateEntries(document) {
|
|
19
|
-
const problems = [];
|
|
20
|
-
for (const entry of document.blocks) {
|
|
21
|
-
if (entry.location === void 0) continue;
|
|
22
|
-
let scheme;
|
|
23
|
-
try {
|
|
24
|
-
scheme = parseBlockPackLocation(entry.location).scheme;
|
|
25
|
-
} catch (e) {
|
|
26
|
-
problems.push({
|
|
27
|
-
entryId: entry.id,
|
|
28
|
-
error: `Block is installed from a location nothing can resolve: ${e instanceof Error ? e.message : String(e)}`
|
|
29
|
-
});
|
|
30
|
-
continue;
|
|
31
|
-
}
|
|
32
|
-
if (scheme === "file") problems.push({
|
|
33
|
-
entryId: entry.id,
|
|
34
|
-
error: `Block is installed from ${entry.location}, a folder on this machine — it resolves to nothing on the recipient's, so this template cannot be shared`
|
|
35
|
-
});
|
|
36
|
-
}
|
|
37
|
-
return problems;
|
|
38
|
-
}
|
|
39
|
-
//#endregion
|
|
40
|
-
export { unshareableTemplateEntries };
|
|
41
|
-
|
|
42
|
-
//# sourceMappingURL=template_share.js.map
|
|
@@ -1 +0,0 @@
|
|
|
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"}
|
|
@@ -1,78 +0,0 @@
|
|
|
1
|
-
import { describe, expect, test } from "vitest";
|
|
2
|
-
import type {
|
|
3
|
-
BlockKindSelectorReference,
|
|
4
|
-
BlockPackLocationReference,
|
|
5
|
-
ProjectTemplateV1,
|
|
6
|
-
ProjectTemplateV1Entry,
|
|
7
|
-
} from "@milaboratories/pl-model-common";
|
|
8
|
-
import { PROJECT_TEMPLATE_SCHEMA_V1 } from "@milaboratories/pl-model-common";
|
|
9
|
-
import { unshareableTemplateEntries } from "./template_share";
|
|
10
|
-
|
|
11
|
-
/**
|
|
12
|
-
* Whether a stored template may travel to another machine.
|
|
13
|
-
*
|
|
14
|
-
* A function of the document alone, which is why it is asked both when a share is attempted
|
|
15
|
-
* and when a template is merely displayed — the refusal has to be visible on the template
|
|
16
|
-
* itself, not only after the user tries.
|
|
17
|
-
*/
|
|
18
|
-
|
|
19
|
-
const KIND = "@platforma-open/milaboratories.demo.kind@^1.0.0" as BlockKindSelectorReference;
|
|
20
|
-
|
|
21
|
-
const entry = (id: string, location?: string): ProjectTemplateV1Entry => ({
|
|
22
|
-
id,
|
|
23
|
-
kind: KIND,
|
|
24
|
-
params: {},
|
|
25
|
-
...(location !== undefined ? { location: location as BlockPackLocationReference } : {}),
|
|
26
|
-
});
|
|
27
|
-
|
|
28
|
-
const documentOf = (...blocks: ProjectTemplateV1Entry[]): ProjectTemplateV1 => ({
|
|
29
|
-
schema: PROJECT_TEMPLATE_SCHEMA_V1,
|
|
30
|
-
blocks,
|
|
31
|
-
});
|
|
32
|
-
|
|
33
|
-
describe("unshareableTemplateEntries", () => {
|
|
34
|
-
test("a template whose entries name no place at all can be shared", () => {
|
|
35
|
-
// The common case: every entry resolves through its kind, which means the same thing
|
|
36
|
-
// on the recipient's machine as it does here.
|
|
37
|
-
expect(unshareableTemplateEntries(documentOf(entry("a"), entry("b")))).toStrictEqual([]);
|
|
38
|
-
});
|
|
39
|
-
|
|
40
|
-
test("an entry installed from a folder on this machine refuses the share, and says which folder", () => {
|
|
41
|
-
const problems = unshareableTemplateEntries(
|
|
42
|
-
documentOf(entry("a"), entry("b", "file:///Users/dev/blocks/demo/block")),
|
|
43
|
-
);
|
|
44
|
-
|
|
45
|
-
expect(problems.map((p) => p.entryId)).toStrictEqual(["b"]);
|
|
46
|
-
expect(problems[0].error).toContain("file:///Users/dev/blocks/demo/block");
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
test("every offending entry is reported, not only the first", () => {
|
|
50
|
-
// A UI names each block once instead of sending its user round the loop per entry.
|
|
51
|
-
const problems = unshareableTemplateEntries(
|
|
52
|
-
documentOf(
|
|
53
|
-
entry("a", "file:///blocks/a"),
|
|
54
|
-
entry("b"),
|
|
55
|
-
entry("c", "FILE:///blocks/c"), // the scheme is case-insensitive
|
|
56
|
-
),
|
|
57
|
-
);
|
|
58
|
-
|
|
59
|
-
expect(problems.map((p) => p.entryId)).toStrictEqual(["a", "c"]);
|
|
60
|
-
});
|
|
61
|
-
|
|
62
|
-
test("a location whose scheme cannot be read is refused too", () => {
|
|
63
|
-
// Nothing can resolve it anywhere, here included — so the reason differs from the
|
|
64
|
-
// `file:` one, and the message says so rather than blaming the recipient's machine.
|
|
65
|
-
const problems = unshareableTemplateEntries(documentOf(entry("a", "/blocks/a")));
|
|
66
|
-
|
|
67
|
-
expect(problems.map((p) => p.entryId)).toStrictEqual(["a"]);
|
|
68
|
-
expect(problems[0].error).toContain("nothing can resolve");
|
|
69
|
-
});
|
|
70
|
-
|
|
71
|
-
test("a location naming a place both machines can reach is not refused", () => {
|
|
72
|
-
// The rule is about a place that means something different elsewhere, not about
|
|
73
|
-
// locations as such.
|
|
74
|
-
expect(
|
|
75
|
-
unshareableTemplateEntries(documentOf(entry("a", "https://blocks.example.org/demo"))),
|
|
76
|
-
).toStrictEqual([]);
|
|
77
|
-
});
|
|
78
|
-
});
|
|
@@ -1,52 +0,0 @@
|
|
|
1
|
-
import type { ProjectTemplateV1 } from "@milaboratories/pl-model-common";
|
|
2
|
-
import { parseBlockPackLocation } from "@milaboratories/pl-model-common";
|
|
3
|
-
|
|
4
|
-
/** One template entry standing in the way of sharing the template, and why. */
|
|
5
|
-
export type TemplateShareProblem = {
|
|
6
|
-
/** The template-local id of the entry the problem belongs to; on an exported template it is
|
|
7
|
-
* the block's project-local uuid. */
|
|
8
|
-
readonly entryId: string;
|
|
9
|
-
readonly error: string;
|
|
10
|
-
};
|
|
11
|
-
|
|
12
|
-
/**
|
|
13
|
-
* Every entry of a template that cannot travel to another machine, or an empty list for a
|
|
14
|
-
* template that can be shared.
|
|
15
|
-
*
|
|
16
|
-
* An entry's `location` names a place rather than a name, and a `file:` place is a folder on
|
|
17
|
-
* the author's own disk: a recipient resolving it finds nothing, or worse finds something
|
|
18
|
-
* else. Such a template stays perfectly usable where it was made, so it is stored and applied
|
|
19
|
-
* as normal — only sharing it is refused.
|
|
20
|
-
*
|
|
21
|
-
* A location whose scheme cannot be read is refused for the same reason: nothing can resolve
|
|
22
|
-
* it anywhere, here included.
|
|
23
|
-
*
|
|
24
|
-
* Every offending entry is reported, not only the first, so a UI can name each block instead
|
|
25
|
-
* of sending its user round the loop once per entry.
|
|
26
|
-
*/
|
|
27
|
-
export function unshareableTemplateEntries(
|
|
28
|
-
document: ProjectTemplateV1,
|
|
29
|
-
): readonly TemplateShareProblem[] {
|
|
30
|
-
const problems: TemplateShareProblem[] = [];
|
|
31
|
-
for (const entry of document.blocks) {
|
|
32
|
-
if (entry.location === undefined) continue;
|
|
33
|
-
let scheme: string;
|
|
34
|
-
try {
|
|
35
|
-
scheme = parseBlockPackLocation(entry.location).scheme;
|
|
36
|
-
} catch (e) {
|
|
37
|
-
problems.push({
|
|
38
|
-
entryId: entry.id,
|
|
39
|
-
error: `Block is installed from a location nothing can resolve: ${e instanceof Error ? e.message : String(e)}`,
|
|
40
|
-
});
|
|
41
|
-
continue;
|
|
42
|
-
}
|
|
43
|
-
if (scheme === "file")
|
|
44
|
-
problems.push({
|
|
45
|
-
entryId: entry.id,
|
|
46
|
-
error:
|
|
47
|
-
`Block is installed from ${entry.location}, a folder on this machine — it resolves ` +
|
|
48
|
-
"to nothing on the recipient's, so this template cannot be shared",
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
|
-
return problems;
|
|
52
|
-
}
|