@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.
@@ -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 { TemplateShareProblem } from \"../model/template_share\";\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, or every entry in the way. */\nexport type ShareTemplateOutcome =\n | { readonly ok: true; readonly shareId: ShareId }\n | { readonly ok: false; readonly problems: readonly TemplateShareProblem[] };\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":";;;;;AA2BA,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;AA0DA,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"}
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"}
@@ -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;
@@ -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, type TemplateShareProblem, acceptanceField, acceptanceFieldLogin, asShareId, canGrantToEveryone, canImpersonate, decisionField, decodeEnvelopeData, envelopeProjectMap, isAcceptanceField, locationOf, newShareId, normalizeEnvelopeData, parseBlockPackName, parseProjectTemplateV1Yaml, resolveTemplateEntries, stringifyProjectTemplateV1, unshareableTemplateEntries };
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 };
@@ -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
- import { unshareableTemplateEntries } from "./template_share.js";
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.70.0",
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
- * A template holding a block installed from a folder on this machine is refused rather than sent,
1006
- * with every offending entry named. {@link checkTemplateShareable} answers the same question
1007
- * without attempting the share, so a UI can state it on the template itself.
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 loaded = await this.loadShareableTemplate(id);
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
- loaded.template,
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 { ok: true, shareId: shareId! };
1038
+ return { shareId: shareId! };
1041
1039
  }
1042
1040
 
1043
- /**
1044
- * Every entry of a stored template that stands in the way of sharing it, empty for a template
1045
- * that can be shared. Reads the template and nothing else, so a UI can state the refusal on the
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
- const template = await this.pl.withReadTx("MLReadTemplateForShare", async (tx) => {
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. Sharing the named template must be permitted (see
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, and the refusal it can produce
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.loadShareableTemplate(opts.templateId);
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
- tx,
1142
- this.sharingOutboxResourceId,
1143
- target.template,
1144
- {
1145
- sender: self,
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, or every entry in the way. */
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 refused rather than shared", async () => {
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 stored = await storeTemplate(ml, "Built here", {
97
- schemaVersion: 1,
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
- // Asked of a template that is merely being displayed, so the refusal can be stated on the
102
- // template itself instead of only once the user has tried to send it.
103
- expect((await ml.checkTemplateShareable(stored.id)).map((p) => p.entryId)).toStrictEqual(["b"]);
104
-
105
- const outcome = await ml.shareTemplate(stored.id, {
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
- if (outcome.ok) throw new Error("a template with a file: entry must not be shareable");
111
- expect(outcome.problems.map((p) => p.entryId)).toStrictEqual(["b"]);
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",
@@ -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
- }