@milaboratories/pl-middle-layer 1.66.19 → 1.67.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.
Files changed (130) hide show
  1. package/dist/block_registry/index.cjs +3 -0
  2. package/dist/block_registry/index.d.ts +2 -1
  3. package/dist/block_registry/index.js +2 -1
  4. package/dist/block_registry/location_provider.cjs +117 -0
  5. package/dist/block_registry/location_provider.cjs.map +1 -0
  6. package/dist/block_registry/location_provider.js +114 -0
  7. package/dist/block_registry/location_provider.js.map +1 -0
  8. package/dist/block_registry/registry.cjs +13 -0
  9. package/dist/block_registry/registry.cjs.map +1 -1
  10. package/dist/block_registry/registry.d.ts +14 -1
  11. package/dist/block_registry/registry.d.ts.map +1 -1
  12. package/dist/block_registry/registry.js +13 -0
  13. package/dist/block_registry/registry.js.map +1 -1
  14. package/dist/block_registry/template_provider.cjs +104 -0
  15. package/dist/block_registry/template_provider.cjs.map +1 -0
  16. package/dist/block_registry/template_provider.d.ts +62 -0
  17. package/dist/block_registry/template_provider.d.ts.map +1 -0
  18. package/dist/block_registry/template_provider.js +103 -0
  19. package/dist/block_registry/template_provider.js.map +1 -0
  20. package/dist/block_registry/watcher.cjs +3 -3
  21. package/dist/block_registry/watcher.js +2 -2
  22. package/dist/dev_env/util.js +2 -2
  23. package/dist/dev_env/util.js.map +1 -1
  24. package/dist/index.cjs +10 -0
  25. package/dist/index.d.ts +5 -1
  26. package/dist/index.js +5 -1
  27. package/dist/js_render/computable_context.cjs +1 -1
  28. package/dist/js_render/computable_context.js +1 -1
  29. package/dist/middle_layer/driver_kit.d.ts +1 -1
  30. package/dist/middle_layer/middle_layer.cjs +144 -2
  31. package/dist/middle_layer/middle_layer.cjs.map +1 -1
  32. package/dist/middle_layer/middle_layer.d.ts +67 -1
  33. package/dist/middle_layer/middle_layer.d.ts.map +1 -1
  34. package/dist/middle_layer/middle_layer.js +146 -4
  35. package/dist/middle_layer/middle_layer.js.map +1 -1
  36. package/dist/middle_layer/project.cjs +1 -0
  37. package/dist/middle_layer/project.cjs.map +1 -1
  38. package/dist/middle_layer/project.d.ts +2 -2
  39. package/dist/middle_layer/project.js +4 -4
  40. package/dist/middle_layer/project.js.map +1 -1
  41. package/dist/middle_layer/sharing_list.d.ts +1 -1
  42. package/dist/model/args.cjs +9 -28
  43. package/dist/model/args.cjs.map +1 -1
  44. package/dist/model/args.js +9 -28
  45. package/dist/model/args.js.map +1 -1
  46. package/dist/model/index.cjs +7 -0
  47. package/dist/model/index.d.ts +4 -1
  48. package/dist/model/index.js +4 -1
  49. package/dist/model/project_helper.cjs +111 -0
  50. package/dist/model/project_helper.cjs.map +1 -1
  51. package/dist/model/project_helper.d.ts +76 -1
  52. package/dist/model/project_helper.d.ts.map +1 -1
  53. package/dist/model/project_helper.js +111 -0
  54. package/dist/model/project_helper.js.map +1 -1
  55. package/dist/model/project_model.d.ts +1 -1
  56. package/dist/model/sharing_model.d.ts +1 -1
  57. package/dist/model/template_apply.cjs +67 -0
  58. package/dist/model/template_apply.cjs.map +1 -0
  59. package/dist/model/template_apply.d.ts +50 -0
  60. package/dist/model/template_apply.d.ts.map +1 -0
  61. package/dist/model/template_apply.js +64 -0
  62. package/dist/model/template_apply.js.map +1 -0
  63. package/dist/model/template_export.cjs +82 -0
  64. package/dist/model/template_export.cjs.map +1 -0
  65. package/dist/model/template_export.d.ts +9 -0
  66. package/dist/model/template_export.d.ts.map +1 -0
  67. package/dist/model/template_export.js +82 -0
  68. package/dist/model/template_export.js.map +1 -0
  69. package/dist/model/template_parser.cjs +74 -0
  70. package/dist/model/template_parser.cjs.map +1 -0
  71. package/dist/model/template_parser.d.ts +43 -0
  72. package/dist/model/template_parser.d.ts.map +1 -0
  73. package/dist/model/template_parser.js +72 -0
  74. package/dist/model/template_parser.js.map +1 -0
  75. package/dist/model/template_resolve.cjs +134 -0
  76. package/dist/model/template_resolve.cjs.map +1 -0
  77. package/dist/model/template_resolve.d.ts +193 -0
  78. package/dist/model/template_resolve.d.ts.map +1 -0
  79. package/dist/model/template_resolve.js +133 -0
  80. package/dist/model/template_resolve.js.map +1 -0
  81. package/dist/model/template_serializer.cjs +162 -0
  82. package/dist/model/template_serializer.cjs.map +1 -0
  83. package/dist/model/template_serializer.d.ts +17 -0
  84. package/dist/model/template_serializer.d.ts.map +1 -0
  85. package/dist/model/template_serializer.js +157 -0
  86. package/dist/model/template_serializer.js.map +1 -0
  87. package/dist/mutator/block-pack/block_pack.cjs +1 -1
  88. package/dist/mutator/block-pack/block_pack.js +1 -1
  89. package/dist/mutator/migration.cjs +1 -1
  90. package/dist/mutator/migration.js +1 -1
  91. package/dist/mutator/project.cjs +35 -5
  92. package/dist/mutator/project.cjs.map +1 -1
  93. package/dist/mutator/project.d.ts.map +1 -1
  94. package/dist/mutator/project.js +35 -5
  95. package/dist/mutator/project.js.map +1 -1
  96. package/dist/mutator/template_construct.cjs +77 -0
  97. package/dist/mutator/template_construct.cjs.map +1 -0
  98. package/dist/mutator/template_construct.js +77 -0
  99. package/dist/mutator/template_construct.js.map +1 -0
  100. package/dist/network_check/template.js +3 -3
  101. package/dist/network_check/template.js.map +1 -1
  102. package/package.json +18 -18
  103. package/src/block_registry/index.ts +1 -0
  104. package/src/block_registry/location_provider.test.ts +236 -0
  105. package/src/block_registry/location_provider.ts +124 -0
  106. package/src/block_registry/registry.ts +22 -0
  107. package/src/block_registry/template_provider.test.ts +275 -0
  108. package/src/block_registry/template_provider.ts +168 -0
  109. package/src/middle_layer/middle_layer.ts +192 -1
  110. package/src/middle_layer/project.ts +1 -1
  111. package/src/model/args.test.ts +26 -1
  112. package/src/model/args.ts +16 -31
  113. package/src/model/index.ts +21 -0
  114. package/src/model/project_helper.ts +238 -0
  115. package/src/model/project_helper_params_init.test.ts +249 -0
  116. package/src/model/template_apply.test.ts +65 -0
  117. package/src/model/template_apply.ts +128 -0
  118. package/src/model/template_export.test.ts +255 -0
  119. package/src/model/template_export.ts +136 -0
  120. package/src/model/template_parser.test.ts +264 -0
  121. package/src/model/template_parser.ts +108 -0
  122. package/src/model/template_resolve.test.ts +397 -0
  123. package/src/model/template_resolve.ts +346 -0
  124. package/src/model/template_serializer.test.ts +398 -0
  125. package/src/model/template_serializer.ts +232 -0
  126. package/src/model/template_serializer_fixtures.test.ts +274 -0
  127. package/src/mutator/project-v3.test.ts +11 -11
  128. package/src/mutator/project.ts +92 -5
  129. package/src/mutator/template_construct.test.ts +456 -0
  130. package/src/mutator/template_construct.ts +150 -0
@@ -0,0 +1,77 @@
1
+ const require_template_apply = require("../model/template_apply.cjs");
2
+ let _platforma_sdk_model = require("@platforma-sdk/model");
3
+ let node_crypto = require("node:crypto");
4
+ //#region src/mutator/template_construct.ts
5
+ /**
6
+ * Apply a `template-v1` document: create each entry's block, in file order.
7
+ *
8
+ * Everything slow is already done by the time this runs: kinds resolved, block packs
9
+ * prepared and template-cached, the project created and a mutator open. What is left per
10
+ * entry is in-memory work — assign an id, redirect the entry's references against the ids
11
+ * already handed out, ask the block's own model for the storage those params imply, and
12
+ * place the block. All of it inside the caller's single transaction.
13
+ *
14
+ * File order is instantiation order, and a reference to a later entry is rejected before
15
+ * this point, so one forward pass is enough: by the time an entry is created, every entry it
16
+ * references already exists and already has an id. Blocks are appended in the order they are
17
+ * added, so nothing here passes a `before`.
18
+ *
19
+ * **All or nothing.** An entry this project cannot honour throws
20
+ * {@link TemplateEntryRejected}, which leaves the caller's transaction uncommitted, so the
21
+ * project keeps none of the blocks this call placed. A half-applied project is not a useful
22
+ * result: its tail is missing, so whatever the missing entries were supposed to feed is wired
23
+ * to nothing, and the user cannot tell which of the blocks present were configured by the
24
+ * file and which they would have to fix. Failing whole also means the reader gets one
25
+ * statement about their file rather than a project to reconcile.
26
+ *
27
+ * @param entries Prepared entries by template-local id. Resolution covers every entry
28
+ * or the apply does not start, so a missing one is a caller error rather than a
29
+ * property of the file
30
+ * @param newBlockId Source of project-local block ids. Defaults to random UUIDs, the same
31
+ * ids `Project.addBlock` would have generated on its own; injectable so a test can name
32
+ * the ids it expects
33
+ * @throws {TemplateEntryRejected} for the first entry the file describes unusably
34
+ */
35
+ function applyTemplateEntries(deps) {
36
+ const { document, placer, projectHelper, entries } = deps;
37
+ const newBlockId = deps.newBlockId ?? node_crypto.randomUUID;
38
+ const added = [];
39
+ /**
40
+ * Template-local entry id → the block id it was given.
41
+ *
42
+ * An entry lands in here the moment it is given an id, before its block is placed. That is
43
+ * safe precisely because the apply is all-or-nothing: there is no surviving project in
44
+ * which a later entry could point at a block that never got created. It does mean an entry
45
+ * whose params reference itself is wired to itself rather than left dangling — a document
46
+ * the reference check rejects before an apply begins.
47
+ */
48
+ const blockIds = /* @__PURE__ */ new Map();
49
+ for (const entry of document.blocks) {
50
+ const prepared = entries.get(entry.id);
51
+ if (prepared === void 0) throw new require_template_apply.TemplateEntryRejected(entry.id, `No block was prepared for entry '${entry.id}'.`);
52
+ const blockConfig = (0, _platforma_sdk_model.extractConfig)(prepared.blockPack.config);
53
+ if (blockConfig.modelAPIVersion !== _platforma_sdk_model.BLOCK_STORAGE_FACADE_VERSION) throw new require_template_apply.TemplateEntryRejected(entry.id, "This version of the block is too old to be created from a template. Use a newer version of the block, or remove the pinned block version from this entry so a supported one is chosen automatically.");
54
+ const blockId = newBlockId();
55
+ blockIds.set(entry.id, blockId);
56
+ const storage = projectHelper.getInitialStorageFromParamsInVM(blockConfig, entry.params, blockIds);
57
+ if (storage.error !== void 0) throw new require_template_apply.TemplateEntryRejected(entry.id, storage.error.message);
58
+ placer.addBlock({
59
+ id: blockId,
60
+ label: prepared.label,
61
+ renderingMode: blockConfig.renderingMode
62
+ }, {
63
+ storageMode: "fromModel",
64
+ blockPack: prepared.blockPack,
65
+ initialStorage: storage.value
66
+ });
67
+ added.push({
68
+ templateLocalId: entry.id,
69
+ blockId
70
+ });
71
+ }
72
+ return added;
73
+ }
74
+ //#endregion
75
+ exports.applyTemplateEntries = applyTemplateEntries;
76
+
77
+ //# sourceMappingURL=template_construct.cjs.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template_construct.cjs","names":["randomUUID","TemplateEntryRejected","BLOCK_STORAGE_FACADE_VERSION"],"sources":["../../src/mutator/template_construct.ts"],"sourcesContent":["import { BLOCK_STORAGE_FACADE_VERSION, extractConfig } from \"@platforma-sdk/model\";\nimport type { ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport type { BlockPackSpecPrepared } from \"../model\";\nimport type { ProjectHelper } from \"../model/project_helper\";\nimport type { AppliedEntry } from \"../model/template_apply\";\nimport { TemplateEntryRejected } from \"../model/template_apply\";\nimport { randomUUID } from \"node:crypto\";\nimport type { Block } from \"../model/project_model\";\nimport type { NewBlockSpec } from \"./project\";\n\n/** What construction needs to know about one entry, once its block has been located. */\nexport type PreparedTemplateEntry = {\n /** The block pack, already prepared and template-cached. */\n readonly blockPack: BlockPackSpecPrepared;\n /**\n * The label to place the block under: the block package's published title.\n *\n * A template names no block instances, so this cannot come from the file — and must\n * not be derived from it, since an exported template names its entries by the source\n * project's block ids. It reaches here from resolution, the only stage that talks to\n * a registry.\n *\n * A block whose model derives a title shows that instead (`project_overview.ts`\n * prefers the derived one), so for most blocks this is invisible. For the ones that\n * derive none it is the name the user reads.\n */\n readonly label: string;\n};\n\n/** The one method construction needs from a project mutator. */\nexport type BlockPlacer = {\n addBlock: (block: Block, spec: NewBlockSpec, before?: string) => void;\n};\n\n/**\n * Apply a `template-v1` document: create each entry's block, in file order.\n *\n * Everything slow is already done by the time this runs: kinds resolved, block packs\n * prepared and template-cached, the project created and a mutator open. What is left per\n * entry is in-memory work — assign an id, redirect the entry's references against the ids\n * already handed out, ask the block's own model for the storage those params imply, and\n * place the block. All of it inside the caller's single transaction.\n *\n * File order is instantiation order, and a reference to a later entry is rejected before\n * this point, so one forward pass is enough: by the time an entry is created, every entry it\n * references already exists and already has an id. Blocks are appended in the order they are\n * added, so nothing here passes a `before`.\n *\n * **All or nothing.** An entry this project cannot honour throws\n * {@link TemplateEntryRejected}, which leaves the caller's transaction uncommitted, so the\n * project keeps none of the blocks this call placed. A half-applied project is not a useful\n * result: its tail is missing, so whatever the missing entries were supposed to feed is wired\n * to nothing, and the user cannot tell which of the blocks present were configured by the\n * file and which they would have to fix. Failing whole also means the reader gets one\n * statement about their file rather than a project to reconcile.\n *\n * @param entries Prepared entries by template-local id. Resolution covers every entry\n * or the apply does not start, so a missing one is a caller error rather than a\n * property of the file\n * @param newBlockId Source of project-local block ids. Defaults to random UUIDs, the same\n * ids `Project.addBlock` would have generated on its own; injectable so a test can name\n * the ids it expects\n * @throws {TemplateEntryRejected} for the first entry the file describes unusably\n */\nexport function applyTemplateEntries(deps: {\n readonly document: ProjectTemplateV1;\n readonly placer: BlockPlacer;\n readonly projectHelper: ProjectHelper;\n readonly entries: ReadonlyMap<string, PreparedTemplateEntry>;\n readonly newBlockId?: () => string;\n}): AppliedEntry[] {\n const { document, placer, projectHelper, entries } = deps;\n const newBlockId = deps.newBlockId ?? randomUUID;\n const added: AppliedEntry[] = [];\n\n /**\n * Template-local entry id → the block id it was given.\n *\n * An entry lands in here the moment it is given an id, before its block is placed. That is\n * safe precisely because the apply is all-or-nothing: there is no surviving project in\n * which a later entry could point at a block that never got created. It does mean an entry\n * whose params reference itself is wired to itself rather than left dangling — a document\n * the reference check rejects before an apply begins.\n */\n const blockIds = new Map<string, string>();\n\n for (const entry of document.blocks) {\n const prepared = entries.get(entry.id);\n if (prepared === undefined) {\n throw new TemplateEntryRejected(entry.id, `No block was prepared for entry '${entry.id}'.`);\n }\n\n const blockConfig = extractConfig(prepared.blockPack.config);\n\n // Only a block on the current storage facade can be told what to initialize itself\n // with, and it is refused rather than created with its own defaults even when the\n // entry carries no params: every entry names a kind, and a block predating the\n // facade implements none, so creating one would honour the entry's pinned version\n // while contradicting the kind it claims to be. With params it is worse still — a\n // block that looks applied but ignored everything the template said about it.\n //\n // Kind resolution never picks such a block, since declaring a kind requires a\n // recent SDK. An entry pinning an exact version can name anything ever published,\n // which is the way this is reached.\n if (blockConfig.modelAPIVersion !== BLOCK_STORAGE_FACADE_VERSION) {\n throw new TemplateEntryRejected(\n entry.id,\n \"This version of the block is too old to be created from a template. Use a \" +\n \"newer version of the block, or remove the pinned block version from this \" +\n \"entry so a supported one is chosen automatically.\",\n );\n }\n\n const blockId = newBlockId();\n blockIds.set(entry.id, blockId);\n\n // Params travelled from the file untouched, so pointing their references at this project\n // is the block's own job, in its own bundle — nothing here knows which of the values in\n // there carry block ids. It happens inside this same call, along with the kind's check and\n // the block's own init: the block decides what its params mean, and this is the one place\n // that asks it. The map holds the entries created so far, including this one, so an entry\n // referencing itself is wired to itself and one referencing an entry below is left\n // pointing at nothing.\n //\n // Params are a mapping by the time a document exists — the parser reads an omitted key\n // as `{}` — so every entry goes through this path and is checked against its kind. Run\n // before anything is placed, so params the block declines cost nothing but the report.\n const storage = projectHelper.getInitialStorageFromParamsInVM(\n blockConfig,\n entry.params,\n blockIds,\n );\n if (storage.error !== undefined) {\n throw new TemplateEntryRejected(entry.id, storage.error.message);\n }\n\n placer.addBlock(\n { id: blockId, label: prepared.label, renderingMode: blockConfig.renderingMode },\n {\n storageMode: \"fromModel\",\n blockPack: prepared.blockPack,\n initialStorage: storage.value,\n },\n );\n\n added.push({ templateLocalId: entry.id, blockId });\n }\n\n return added;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,qBAAqB,MAMlB;CACjB,MAAM,EAAE,UAAU,QAAQ,eAAe,YAAY;CACrD,MAAM,aAAa,KAAK,cAAcA,YAAAA;CACtC,MAAM,QAAwB,CAAC;;;;;;;;;;CAW/B,MAAM,2BAAW,IAAI,IAAoB;CAEzC,KAAK,MAAM,SAAS,SAAS,QAAQ;EACnC,MAAM,WAAW,QAAQ,IAAI,MAAM,EAAE;EACrC,IAAI,aAAa,KAAA,GACf,MAAM,IAAIC,uBAAAA,sBAAsB,MAAM,IAAI,oCAAoC,MAAM,GAAG,GAAG;EAG5F,MAAM,eAAA,GAAA,qBAAA,cAAA,CAA4B,SAAS,UAAU,MAAM;EAY3D,IAAI,YAAY,oBAAoBC,qBAAAA,8BAClC,MAAM,IAAID,uBAAAA,sBACR,MAAM,IACN,sMAGF;EAGF,MAAM,UAAU,WAAW;EAC3B,SAAS,IAAI,MAAM,IAAI,OAAO;EAa9B,MAAM,UAAU,cAAc,gCAC5B,aACA,MAAM,QACN,QACF;EACA,IAAI,QAAQ,UAAU,KAAA,GACpB,MAAM,IAAIA,uBAAAA,sBAAsB,MAAM,IAAI,QAAQ,MAAM,OAAO;EAGjE,OAAO,SACL;GAAE,IAAI;GAAS,OAAO,SAAS;GAAO,eAAe,YAAY;EAAc,GAC/E;GACE,aAAa;GACb,WAAW,SAAS;GACpB,gBAAgB,QAAQ;EAC1B,CACF;EAEA,MAAM,KAAK;GAAE,iBAAiB,MAAM;GAAI;EAAQ,CAAC;CACnD;CAEA,OAAO;AACT"}
@@ -0,0 +1,77 @@
1
+ import { TemplateEntryRejected } from "../model/template_apply.js";
2
+ import { BLOCK_STORAGE_FACADE_VERSION, extractConfig } from "@platforma-sdk/model";
3
+ import { randomUUID } from "node:crypto";
4
+ //#region src/mutator/template_construct.ts
5
+ /**
6
+ * Apply a `template-v1` document: create each entry's block, in file order.
7
+ *
8
+ * Everything slow is already done by the time this runs: kinds resolved, block packs
9
+ * prepared and template-cached, the project created and a mutator open. What is left per
10
+ * entry is in-memory work — assign an id, redirect the entry's references against the ids
11
+ * already handed out, ask the block's own model for the storage those params imply, and
12
+ * place the block. All of it inside the caller's single transaction.
13
+ *
14
+ * File order is instantiation order, and a reference to a later entry is rejected before
15
+ * this point, so one forward pass is enough: by the time an entry is created, every entry it
16
+ * references already exists and already has an id. Blocks are appended in the order they are
17
+ * added, so nothing here passes a `before`.
18
+ *
19
+ * **All or nothing.** An entry this project cannot honour throws
20
+ * {@link TemplateEntryRejected}, which leaves the caller's transaction uncommitted, so the
21
+ * project keeps none of the blocks this call placed. A half-applied project is not a useful
22
+ * result: its tail is missing, so whatever the missing entries were supposed to feed is wired
23
+ * to nothing, and the user cannot tell which of the blocks present were configured by the
24
+ * file and which they would have to fix. Failing whole also means the reader gets one
25
+ * statement about their file rather than a project to reconcile.
26
+ *
27
+ * @param entries Prepared entries by template-local id. Resolution covers every entry
28
+ * or the apply does not start, so a missing one is a caller error rather than a
29
+ * property of the file
30
+ * @param newBlockId Source of project-local block ids. Defaults to random UUIDs, the same
31
+ * ids `Project.addBlock` would have generated on its own; injectable so a test can name
32
+ * the ids it expects
33
+ * @throws {TemplateEntryRejected} for the first entry the file describes unusably
34
+ */
35
+ function applyTemplateEntries(deps) {
36
+ const { document, placer, projectHelper, entries } = deps;
37
+ const newBlockId = deps.newBlockId ?? randomUUID;
38
+ const added = [];
39
+ /**
40
+ * Template-local entry id → the block id it was given.
41
+ *
42
+ * An entry lands in here the moment it is given an id, before its block is placed. That is
43
+ * safe precisely because the apply is all-or-nothing: there is no surviving project in
44
+ * which a later entry could point at a block that never got created. It does mean an entry
45
+ * whose params reference itself is wired to itself rather than left dangling — a document
46
+ * the reference check rejects before an apply begins.
47
+ */
48
+ const blockIds = /* @__PURE__ */ new Map();
49
+ for (const entry of document.blocks) {
50
+ const prepared = entries.get(entry.id);
51
+ if (prepared === void 0) throw new TemplateEntryRejected(entry.id, `No block was prepared for entry '${entry.id}'.`);
52
+ const blockConfig = extractConfig(prepared.blockPack.config);
53
+ if (blockConfig.modelAPIVersion !== BLOCK_STORAGE_FACADE_VERSION) throw new TemplateEntryRejected(entry.id, "This version of the block is too old to be created from a template. Use a newer version of the block, or remove the pinned block version from this entry so a supported one is chosen automatically.");
54
+ const blockId = newBlockId();
55
+ blockIds.set(entry.id, blockId);
56
+ const storage = projectHelper.getInitialStorageFromParamsInVM(blockConfig, entry.params, blockIds);
57
+ if (storage.error !== void 0) throw new TemplateEntryRejected(entry.id, storage.error.message);
58
+ placer.addBlock({
59
+ id: blockId,
60
+ label: prepared.label,
61
+ renderingMode: blockConfig.renderingMode
62
+ }, {
63
+ storageMode: "fromModel",
64
+ blockPack: prepared.blockPack,
65
+ initialStorage: storage.value
66
+ });
67
+ added.push({
68
+ templateLocalId: entry.id,
69
+ blockId
70
+ });
71
+ }
72
+ return added;
73
+ }
74
+ //#endregion
75
+ export { applyTemplateEntries };
76
+
77
+ //# sourceMappingURL=template_construct.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"template_construct.js","names":[],"sources":["../../src/mutator/template_construct.ts"],"sourcesContent":["import { BLOCK_STORAGE_FACADE_VERSION, extractConfig } from \"@platforma-sdk/model\";\nimport type { ProjectTemplateV1 } from \"@milaboratories/pl-model-common\";\nimport type { BlockPackSpecPrepared } from \"../model\";\nimport type { ProjectHelper } from \"../model/project_helper\";\nimport type { AppliedEntry } from \"../model/template_apply\";\nimport { TemplateEntryRejected } from \"../model/template_apply\";\nimport { randomUUID } from \"node:crypto\";\nimport type { Block } from \"../model/project_model\";\nimport type { NewBlockSpec } from \"./project\";\n\n/** What construction needs to know about one entry, once its block has been located. */\nexport type PreparedTemplateEntry = {\n /** The block pack, already prepared and template-cached. */\n readonly blockPack: BlockPackSpecPrepared;\n /**\n * The label to place the block under: the block package's published title.\n *\n * A template names no block instances, so this cannot come from the file — and must\n * not be derived from it, since an exported template names its entries by the source\n * project's block ids. It reaches here from resolution, the only stage that talks to\n * a registry.\n *\n * A block whose model derives a title shows that instead (`project_overview.ts`\n * prefers the derived one), so for most blocks this is invisible. For the ones that\n * derive none it is the name the user reads.\n */\n readonly label: string;\n};\n\n/** The one method construction needs from a project mutator. */\nexport type BlockPlacer = {\n addBlock: (block: Block, spec: NewBlockSpec, before?: string) => void;\n};\n\n/**\n * Apply a `template-v1` document: create each entry's block, in file order.\n *\n * Everything slow is already done by the time this runs: kinds resolved, block packs\n * prepared and template-cached, the project created and a mutator open. What is left per\n * entry is in-memory work — assign an id, redirect the entry's references against the ids\n * already handed out, ask the block's own model for the storage those params imply, and\n * place the block. All of it inside the caller's single transaction.\n *\n * File order is instantiation order, and a reference to a later entry is rejected before\n * this point, so one forward pass is enough: by the time an entry is created, every entry it\n * references already exists and already has an id. Blocks are appended in the order they are\n * added, so nothing here passes a `before`.\n *\n * **All or nothing.** An entry this project cannot honour throws\n * {@link TemplateEntryRejected}, which leaves the caller's transaction uncommitted, so the\n * project keeps none of the blocks this call placed. A half-applied project is not a useful\n * result: its tail is missing, so whatever the missing entries were supposed to feed is wired\n * to nothing, and the user cannot tell which of the blocks present were configured by the\n * file and which they would have to fix. Failing whole also means the reader gets one\n * statement about their file rather than a project to reconcile.\n *\n * @param entries Prepared entries by template-local id. Resolution covers every entry\n * or the apply does not start, so a missing one is a caller error rather than a\n * property of the file\n * @param newBlockId Source of project-local block ids. Defaults to random UUIDs, the same\n * ids `Project.addBlock` would have generated on its own; injectable so a test can name\n * the ids it expects\n * @throws {TemplateEntryRejected} for the first entry the file describes unusably\n */\nexport function applyTemplateEntries(deps: {\n readonly document: ProjectTemplateV1;\n readonly placer: BlockPlacer;\n readonly projectHelper: ProjectHelper;\n readonly entries: ReadonlyMap<string, PreparedTemplateEntry>;\n readonly newBlockId?: () => string;\n}): AppliedEntry[] {\n const { document, placer, projectHelper, entries } = deps;\n const newBlockId = deps.newBlockId ?? randomUUID;\n const added: AppliedEntry[] = [];\n\n /**\n * Template-local entry id → the block id it was given.\n *\n * An entry lands in here the moment it is given an id, before its block is placed. That is\n * safe precisely because the apply is all-or-nothing: there is no surviving project in\n * which a later entry could point at a block that never got created. It does mean an entry\n * whose params reference itself is wired to itself rather than left dangling — a document\n * the reference check rejects before an apply begins.\n */\n const blockIds = new Map<string, string>();\n\n for (const entry of document.blocks) {\n const prepared = entries.get(entry.id);\n if (prepared === undefined) {\n throw new TemplateEntryRejected(entry.id, `No block was prepared for entry '${entry.id}'.`);\n }\n\n const blockConfig = extractConfig(prepared.blockPack.config);\n\n // Only a block on the current storage facade can be told what to initialize itself\n // with, and it is refused rather than created with its own defaults even when the\n // entry carries no params: every entry names a kind, and a block predating the\n // facade implements none, so creating one would honour the entry's pinned version\n // while contradicting the kind it claims to be. With params it is worse still — a\n // block that looks applied but ignored everything the template said about it.\n //\n // Kind resolution never picks such a block, since declaring a kind requires a\n // recent SDK. An entry pinning an exact version can name anything ever published,\n // which is the way this is reached.\n if (blockConfig.modelAPIVersion !== BLOCK_STORAGE_FACADE_VERSION) {\n throw new TemplateEntryRejected(\n entry.id,\n \"This version of the block is too old to be created from a template. Use a \" +\n \"newer version of the block, or remove the pinned block version from this \" +\n \"entry so a supported one is chosen automatically.\",\n );\n }\n\n const blockId = newBlockId();\n blockIds.set(entry.id, blockId);\n\n // Params travelled from the file untouched, so pointing their references at this project\n // is the block's own job, in its own bundle — nothing here knows which of the values in\n // there carry block ids. It happens inside this same call, along with the kind's check and\n // the block's own init: the block decides what its params mean, and this is the one place\n // that asks it. The map holds the entries created so far, including this one, so an entry\n // referencing itself is wired to itself and one referencing an entry below is left\n // pointing at nothing.\n //\n // Params are a mapping by the time a document exists — the parser reads an omitted key\n // as `{}` — so every entry goes through this path and is checked against its kind. Run\n // before anything is placed, so params the block declines cost nothing but the report.\n const storage = projectHelper.getInitialStorageFromParamsInVM(\n blockConfig,\n entry.params,\n blockIds,\n );\n if (storage.error !== undefined) {\n throw new TemplateEntryRejected(entry.id, storage.error.message);\n }\n\n placer.addBlock(\n { id: blockId, label: prepared.label, renderingMode: blockConfig.renderingMode },\n {\n storageMode: \"fromModel\",\n blockPack: prepared.blockPack,\n initialStorage: storage.value,\n },\n );\n\n added.push({ templateLocalId: entry.id, blockId });\n }\n\n return added;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgEA,SAAgB,qBAAqB,MAMlB;CACjB,MAAM,EAAE,UAAU,QAAQ,eAAe,YAAY;CACrD,MAAM,aAAa,KAAK,cAAc;CACtC,MAAM,QAAwB,CAAC;;;;;;;;;;CAW/B,MAAM,2BAAW,IAAI,IAAoB;CAEzC,KAAK,MAAM,SAAS,SAAS,QAAQ;EACnC,MAAM,WAAW,QAAQ,IAAI,MAAM,EAAE;EACrC,IAAI,aAAa,KAAA,GACf,MAAM,IAAI,sBAAsB,MAAM,IAAI,oCAAoC,MAAM,GAAG,GAAG;EAG5F,MAAM,cAAc,cAAc,SAAS,UAAU,MAAM;EAY3D,IAAI,YAAY,oBAAoB,8BAClC,MAAM,IAAI,sBACR,MAAM,IACN,sMAGF;EAGF,MAAM,UAAU,WAAW;EAC3B,SAAS,IAAI,MAAM,IAAI,OAAO;EAa9B,MAAM,UAAU,cAAc,gCAC5B,aACA,MAAM,QACN,QACF;EACA,IAAI,QAAQ,UAAU,KAAA,GACpB,MAAM,IAAI,sBAAsB,MAAM,IAAI,QAAQ,MAAM,OAAO;EAGjE,OAAO,SACL;GAAE,IAAI;GAAS,OAAO,SAAS;GAAO,eAAe,YAAY;EAAc,GAC/E;GACE,aAAa;GACb,WAAW,SAAS;GACpB,gBAAgB,QAAQ;EAC1B,CACF;EAEA,MAAM,KAAK;GAAE,iBAAiB,MAAM;GAAI;EAAQ,CAAC;CACnD;CAEA,OAAO;AACT"}
@@ -2,7 +2,7 @@ import { createRenderTemplate } from "../mutator/template/render_template.js";
2
2
  import { loadTemplate, prepareTemplateSpec } from "../mutator/template/template_loading.js";
3
3
  import path from "node:path";
4
4
  import { notEmpty } from "@milaboratories/ts-helpers";
5
- import fs from "node:fs/promises";
5
+ import fsp from "node:fs/promises";
6
6
  import { ContinuePolling, Pl, field, isNotNullSignedResourceId, isNullSignedResourceId, poll, toGlobalFieldId } from "@milaboratories/pl-client";
7
7
  import { randomBytes } from "node:crypto";
8
8
  import { Templates } from "@platforma-sdk/workflow-tengo";
@@ -249,14 +249,14 @@ async function* listFilesSequence(lsDriver, storage, parent, nLsRequests) {
249
249
  async function createBigTempFile() {
250
250
  const filePath = path.join(os.tmpdir(), `check-network-big-temp-${Date.now()}.bin`);
251
251
  const fileContent = randomBytes(20 * 1024 * 1024);
252
- await fs.appendFile(filePath, fileContent);
252
+ await fsp.appendFile(filePath, fileContent);
253
253
  return { filePath };
254
254
  }
255
255
  /** Creates a temporarly file we could use for uploading and downloading. */
256
256
  async function createTempFile() {
257
257
  const filePath = path.join(os.tmpdir(), `check-network-temp-${Date.now()}.txt`);
258
258
  const fileContent = "Hello, world! " + (/* @__PURE__ */ new Date()).toISOString();
259
- await fs.writeFile(filePath, fileContent);
259
+ await fsp.writeFile(filePath, fileContent);
260
260
  return {
261
261
  filePath,
262
262
  fileContent
@@ -1 +1 @@
1
- {"version":3,"file":"template.js","names":["SdkTemplates"],"sources":["../../src/network_check/template.ts"],"sourcesContent":["import type { FieldId, FieldRef, PlClient, ResourceData } from \"@milaboratories/pl-client\";\nimport {\n type PlTransaction,\n ContinuePolling,\n field,\n isNotNullSignedResourceId,\n isNullSignedResourceId,\n Pl,\n poll,\n toGlobalFieldId,\n} from \"@milaboratories/pl-client\";\nimport { createRenderTemplate } from \"../mutator/template/render_template\";\nimport { Templates as SdkTemplates } from \"@platforma-sdk/workflow-tengo\";\nimport type { TemplateSpecAny } from \"../model/template_spec\";\nimport { loadTemplate, prepareTemplateSpec } from \"../mutator/template/template_loading\";\nimport type { ClientDownload, LsDriver } from \"@milaboratories/pl-drivers\";\nimport {\n ImportFileHandleUploadData,\n isSignMatch,\n isUpload,\n uploadBlob,\n type ClientUpload,\n type LsEntryWithFileStats,\n} from \"@milaboratories/pl-drivers\";\nimport type { Signer } from \"@milaboratories/ts-helpers\";\nimport { notEmpty, type MiLogger } from \"@milaboratories/ts-helpers\";\nimport type { ResourceInfo } from \"@milaboratories/pl-tree\";\nimport { text } from \"node:stream/consumers\";\nimport path from \"node:path\";\nimport fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport type { StorageEntry } from \"@milaboratories/pl-model-common\";\n\nexport interface TemplateReport {\n status: \"ok\" | \"warn\" | \"failed\";\n message: string;\n}\n\n/** Uploads `hello-world` template and checks the output is correct. */\nexport async function uploadTemplate(\n logger: MiLogger,\n pl: PlClient,\n name: string,\n): Promise<TemplateReport> {\n try {\n const gotGreeting = await runUploadTemplate(logger, pl, name);\n if (gotGreeting !== `Hello, ${name}`) {\n return {\n status: \"failed\",\n message: `Template uploading failed: expected: ${name}, got: ${gotGreeting}`,\n };\n }\n\n return { status: \"ok\", message: `Template uploading succeeded: ${gotGreeting}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `Template uploading failed: error occurred: ${e}` };\n }\n}\n\nexport async function runUploadTemplate(\n logger: MiLogger,\n pl: PlClient,\n name: string,\n): Promise<string> {\n const outputs = await runTemplate(\n pl,\n SdkTemplates[\"check_network.upload_template\"],\n true,\n (tx) => ({\n name: tx.createValue(Pl.JsonObject, JSON.stringify(name)),\n }),\n [\"greeting\"],\n );\n\n try {\n return JSON.parse(notEmpty((await getFieldValue(pl, outputs.greeting)).data?.toString()));\n } finally {\n if (outputs != undefined) {\n await deleteFields(pl, Object.values(outputs));\n }\n }\n}\n\n/** Uploads a file to the backend and checks the output is a Blob resource. */\nexport async function uploadFile(\n logger: MiLogger,\n signer: Signer,\n lsDriver: LsDriver,\n uploadClient: ClientUpload,\n pl: PlClient,\n filePath: string,\n): Promise<TemplateReport> {\n try {\n const gotBlob = await runUploadFile(logger, signer, lsDriver, uploadClient, pl, filePath);\n\n if (gotBlob.type.name !== \"Blob\") {\n return { status: \"failed\", message: `File uploading failed: ${gotBlob.type.name}` };\n }\n\n return { status: \"ok\", message: `File uploading succeeded: ${gotBlob.type.name}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `File uploading failed: error occurred: ${e}` };\n }\n}\n\nexport async function runUploadFile(\n logger: MiLogger,\n signer: Signer,\n lsDriver: LsDriver,\n uploadClient: ClientUpload,\n pl: PlClient,\n filePath: string,\n): Promise<ResourceInfo> {\n const handle = await lsDriver.getLocalFileHandle(filePath);\n const result = await runTemplate(\n pl,\n SdkTemplates[\"check_network.upload_blob\"],\n true,\n (tx) => ({\n file: tx.createValue(Pl.JsonObject, JSON.stringify(handle)),\n }),\n [\"progress\", \"file\"],\n );\n\n try {\n const progress = await getFieldValue(pl, result.progress);\n\n if (isUpload(progress)) {\n const uploadData = ImportFileHandleUploadData.parse(\n JSON.parse(notEmpty(progress.data?.toString())),\n );\n const isUploadSignMatch = isSignMatch(signer, uploadData.localPath, uploadData.pathSignature);\n\n if (isUploadSignMatch) {\n await uploadBlob(logger, uploadClient, progress, uploadData, () => false, {\n nPartsWithThisUploadSpeed: 10,\n nPartsToIncreaseUpload: 10,\n currentSpeed: 10,\n maxSpeed: 10,\n });\n }\n }\n\n return await getFieldValue(pl, result.file);\n } finally {\n await deleteFields(pl, Object.values(result));\n }\n}\n\n/** Uploads a file to the backend and then tries to download it back. */\nexport async function downloadFile(\n logger: MiLogger,\n pl: PlClient,\n lsDriver: LsDriver,\n uploadClient: ClientUpload,\n downloadClient: ClientDownload,\n filePath: string,\n fileContent: string,\n): Promise<TemplateReport> {\n try {\n const gotFileContent = await runDownloadFile(\n logger,\n pl,\n lsDriver,\n uploadClient,\n downloadClient,\n filePath,\n );\n\n if (gotFileContent !== fileContent) {\n return {\n status: \"failed\",\n message: `File downloading failed: expected: ${fileContent}, got: ${gotFileContent}`,\n };\n }\n return { status: \"ok\", message: `File downloading succeeded: ${gotFileContent}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `File downloading failed: error occurred: ${e}` };\n }\n}\n\nexport async function runDownloadFile(\n logger: MiLogger,\n pl: PlClient,\n lsDriver: LsDriver,\n uploadClient: ClientUpload,\n downloadClient: ClientDownload,\n filePath: string,\n) {\n const handle = await lsDriver.getLocalFileHandle(filePath);\n\n const outputs = await runTemplate(\n pl,\n SdkTemplates[\"check_network.download_blob\"],\n true,\n (tx) => ({ file: tx.createValue(Pl.JsonObject, JSON.stringify(handle)) }),\n [\"progress\", \"file\"],\n );\n\n try {\n const progress = await getFieldValue(pl, outputs.progress);\n\n await uploadBlob(\n logger,\n uploadClient,\n progress,\n ImportFileHandleUploadData.parse(JSON.parse(notEmpty(progress.data?.toString()))),\n () => false,\n {\n nPartsWithThisUploadSpeed: 1,\n nPartsToIncreaseUpload: 1,\n currentSpeed: 1,\n maxSpeed: 1,\n },\n );\n\n const fileInfo = await getFieldValue(pl, outputs.file);\n\n return await downloadClient.withBlobContent(\n fileInfo,\n {},\n {},\n async (content) => await text(content),\n );\n } finally {\n await deleteFields(pl, Object.values(outputs));\n }\n}\n\n/** Runs Go's hello-world binary. */\nexport async function softwareCheck(pl: PlClient): Promise<TemplateReport> {\n try {\n const gotGreeting = await runSoftware(pl);\n\n if (gotGreeting !== \"Hello from go binary\\n\") {\n return { status: \"failed\", message: `Software check failed: got: ${gotGreeting}` };\n }\n return { status: \"ok\", message: `Software check succeeded: ${gotGreeting}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `Software check failed: error occurred: ${e}` };\n }\n}\n\nexport async function runSoftware(pl: PlClient): Promise<string> {\n const result = await runTemplate(\n pl,\n SdkTemplates[\"check_network.run_hello_world\"],\n true,\n (_: PlTransaction) => ({}),\n [\"greeting\"],\n );\n\n try {\n return notEmpty((await getFieldValue(pl, result.greeting)).data?.toString());\n } finally {\n await deleteFields(pl, Object.values(result));\n }\n}\n\n/** Runs Python hello-world. */\nexport async function pythonSoftware(pl: PlClient, name: string): Promise<TemplateReport> {\n try {\n const gotGreeting = await runPythonSoftware(pl, name);\n\n if (gotGreeting !== `Hello, ${name}!\\n`) {\n return { status: \"failed\", message: `Python software check failed: got: ${gotGreeting}` };\n }\n return { status: \"ok\", message: `Python software check succeeded: ${gotGreeting}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `Python software check failed: error occurred: ${e}` };\n }\n}\n\nexport async function runPythonSoftware(pl: PlClient, name: string): Promise<string> {\n const result = await runTemplate(\n pl,\n SdkTemplates[\"check_network.run_hello_world_py\"],\n true,\n (tx) => ({ name: tx.createValue(Pl.JsonObject, JSON.stringify(name)) }),\n [\"greeting\"],\n );\n\n try {\n return notEmpty((await getFieldValue(pl, result.greeting)).data?.toString());\n } finally {\n await deleteFields(pl, Object.values(result));\n }\n}\n\n/** Tries to download a file from every storage. */\nexport async function downloadFromEveryStorage(\n logger: MiLogger,\n pl: PlClient,\n lsDriver: LsDriver,\n ops: {\n minLsRequests: number;\n bytesLimit: number;\n minFileSize: number;\n maxFileSize: number;\n nFilesToCheck: number;\n },\n): Promise<Record<string, TemplateReport>> {\n try {\n const storages = await lsDriver.getStorageList();\n const results: Record<string, TemplateReport> = {};\n\n for (const storage of storages) {\n const result = await chooseFile(\n lsDriver,\n storage,\n ops.nFilesToCheck,\n ops.minFileSize,\n ops.maxFileSize,\n ops.minLsRequests,\n );\n if (result.file === undefined) {\n results[storage.id] = {\n status: \"warn\",\n message:\n `No file between ${ops.minFileSize} and ${ops.maxFileSize} bytes ` +\n `found in storage ${storage.name}, checked ${result.nCheckedFiles} files, ` +\n `did ${result.nLsRequests} ls requests`,\n };\n continue;\n }\n\n logger.info(`Downloading file ${JSON.stringify(result)} from storage ${storage.name}`);\n const outputs = await runTemplate(\n pl,\n SdkTemplates[\"check_network.create_workdir_from_storage\"],\n true,\n (tx) => ({\n file: tx.createValue(\n Pl.JsonObject,\n JSON.stringify((result.file as { handle: string }).handle),\n ),\n }),\n [\"workdirTypeName\"],\n );\n\n try {\n const workdirTypeName = JSON.parse(\n Buffer.from((await getFieldValue(pl, outputs.workdirTypeName)).data!).toString(),\n ) as string;\n\n if (workdirTypeName?.startsWith(\"WorkingDirectory\")) {\n results[storage.id] = {\n status: \"ok\",\n message:\n `Workdir creation succeeded, size of file: ${result.file?.size}, ` +\n `checked ${result.nCheckedFiles} files, did ${result.nLsRequests} ls requests`,\n };\n } else {\n results[storage.id] = {\n status: \"failed\",\n message:\n `Workdir creation failed: ${workdirTypeName}, size of file: ${result.file?.size}, ` +\n `checked ${result.nCheckedFiles} files, did ${result.nLsRequests} ls requests`,\n };\n }\n } finally {\n await deleteFields(pl, Object.values(outputs));\n }\n }\n\n return results;\n } catch (e: unknown) {\n return {\n unknown: {\n status: \"failed\",\n message: `Download from every storage failed: error occurred: ${e}`,\n },\n };\n }\n}\n\n/** Chooses a random file from the storage in a size range.\n * If we couldn't find a normal-sized file, we'll return a small file to check at least something.\n */\nexport async function chooseFile(\n lsDriver: LsDriver,\n storage: StorageEntry,\n limit: number,\n minSize: number,\n maxSize: number,\n minLsRequests: number,\n): Promise<{\n file: LsEntryWithFileStats | undefined;\n nLsRequests: number;\n nCheckedFiles: number;\n}> {\n const files = listFilesSequence(lsDriver, storage, \"\", 0);\n\n // return small file in case we don't have many normal-sized files.\n // While we'll download only a small range of bytes from the file,\n // we don't want to return a big file in case the underlying S3 doesn't support range requests.\n let smallFile: LsEntryWithFileStats | undefined;\n let nCheckedFiles = 0;\n let maxNLsRequests = 0;\n\n for await (const { file, nLsRequests } of files) {\n maxNLsRequests = Math.max(maxNLsRequests, nLsRequests);\n\n if (nCheckedFiles >= limit && maxNLsRequests > minLsRequests) {\n // we reached a limit on both the number of files and the number of ls requests.\n return { file: smallFile, nLsRequests: maxNLsRequests, nCheckedFiles };\n }\n nCheckedFiles++;\n if (minSize <= file.size && file.size <= maxSize) {\n return { file, nLsRequests: maxNLsRequests, nCheckedFiles };\n } else if (file.size < minSize) {\n smallFile = file;\n }\n }\n\n return { file: smallFile, nLsRequests: maxNLsRequests, nCheckedFiles };\n}\n\n/** Deep-first search for files in the storage. */\nexport async function* listFilesSequence(\n lsDriver: LsDriver,\n storage: StorageEntry,\n parent: string,\n nLsRequests: number,\n): AsyncGenerator<{ file: LsEntryWithFileStats; nLsRequests: number }, void, unknown> {\n nLsRequests++;\n const files = await lsDriver.listRemoteFilesWithFileStats(storage.handle, parent);\n\n for (const file of files.entries) {\n if (file.type === \"file\" && file.fullPath.startsWith(parent)) {\n yield {\n file,\n nLsRequests,\n };\n } else if (file.type === \"dir\") {\n for await (const nestedFile of listFilesSequence(\n lsDriver,\n storage,\n file.fullPath,\n nLsRequests,\n )) {\n nLsRequests = Math.max(nestedFile.nLsRequests, nLsRequests);\n yield nestedFile;\n }\n }\n }\n}\n\n/** Creates a big temporary file with random content. */\nexport async function createBigTempFile(): Promise<{ filePath: string }> {\n const filePath = path.join(os.tmpdir(), `check-network-big-temp-${Date.now()}.bin`);\n const fileSize = 20 * 1024 * 1024; // 20 MiB\n\n const fileContent = randomBytes(fileSize);\n\n await fs.appendFile(filePath, fileContent);\n\n return { filePath };\n}\n\n/** Creates a temporarly file we could use for uploading and downloading. */\nexport async function createTempFile(): Promise<{ filePath: string; fileContent: string }> {\n const filePath = path.join(os.tmpdir(), `check-network-temp-${Date.now()}.txt`);\n\n const fileContent = \"Hello, world! \" + new Date().toISOString();\n await fs.writeFile(filePath, fileContent);\n\n return { filePath, fileContent };\n}\n\n/** Creates a template and RenderTemplate resources, gets all resources from outputs.\n * Throws a error if any of the outputs failed.\n */\nasync function runTemplate(\n client: PlClient,\n tpl: TemplateSpecAny,\n ephemeral: boolean,\n inputs: (tx: PlTransaction) => Pl.PlRecord,\n outputs: string[],\n): Promise<Record<string, FieldId>> {\n return await client.withWriteTx(\"TemplateRender\", async (tx) => {\n const preparedTemplate = await prepareTemplateSpec(tpl);\n const tplResource = loadTemplate(tx, preparedTemplate);\n\n const outputFields: Record<string, FieldRef> = createRenderTemplate(\n tx,\n tplResource,\n ephemeral,\n inputs(tx),\n outputs,\n );\n\n const outputsIds: Record<string, FieldId> = {};\n\n for (const output of outputs) {\n const fieldRef = field(client.clientRoot, output) as FieldId;\n tx.createField(fieldRef, \"Dynamic\", outputFields[output]);\n outputsIds[output] = await toGlobalFieldId(fieldRef);\n }\n\n await tx.commit();\n\n return outputsIds;\n });\n}\n\n/** Gets a resource from field's value or throws a error. */\nasync function getFieldValue(client: PlClient, fieldId: FieldId): Promise<ResourceData> {\n // We could also do polling with pl-tree, but it seemed like an overkill,\n // that's why we have a simple polling here.\n\n return await poll(client, async (tx) => {\n const field = await tx.tx.getField(fieldId);\n if (isNotNullSignedResourceId(field.error)) {\n const err = await tx.tx.getResourceData(field.error, true);\n throw new Error(`getFieldValue of \"${fieldId.fieldName}\" field failed: ${err.data}`);\n }\n\n if (isNullSignedResourceId(field.value)) {\n throw new ContinuePolling();\n }\n\n return await tx.tx.getResourceData(field.value, true);\n });\n}\n\nasync function deleteFields(client: PlClient, fieldIds: FieldId[]) {\n await client.withWriteTx(\"DeleteFields\", async (tx) => {\n for (const fieldId of fieldIds) {\n tx.resetField(fieldId);\n }\n await tx.commit();\n });\n}\n"],"mappings":"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"}
1
+ {"version":3,"file":"template.js","names":["SdkTemplates","fs"],"sources":["../../src/network_check/template.ts"],"sourcesContent":["import type { FieldId, FieldRef, PlClient, ResourceData } from \"@milaboratories/pl-client\";\nimport {\n type PlTransaction,\n ContinuePolling,\n field,\n isNotNullSignedResourceId,\n isNullSignedResourceId,\n Pl,\n poll,\n toGlobalFieldId,\n} from \"@milaboratories/pl-client\";\nimport { createRenderTemplate } from \"../mutator/template/render_template\";\nimport { Templates as SdkTemplates } from \"@platforma-sdk/workflow-tengo\";\nimport type { TemplateSpecAny } from \"../model/template_spec\";\nimport { loadTemplate, prepareTemplateSpec } from \"../mutator/template/template_loading\";\nimport type { ClientDownload, LsDriver } from \"@milaboratories/pl-drivers\";\nimport {\n ImportFileHandleUploadData,\n isSignMatch,\n isUpload,\n uploadBlob,\n type ClientUpload,\n type LsEntryWithFileStats,\n} from \"@milaboratories/pl-drivers\";\nimport type { Signer } from \"@milaboratories/ts-helpers\";\nimport { notEmpty, type MiLogger } from \"@milaboratories/ts-helpers\";\nimport type { ResourceInfo } from \"@milaboratories/pl-tree\";\nimport { text } from \"node:stream/consumers\";\nimport path from \"node:path\";\nimport fs from \"node:fs/promises\";\nimport os from \"node:os\";\nimport { randomBytes } from \"node:crypto\";\nimport type { StorageEntry } from \"@milaboratories/pl-model-common\";\n\nexport interface TemplateReport {\n status: \"ok\" | \"warn\" | \"failed\";\n message: string;\n}\n\n/** Uploads `hello-world` template and checks the output is correct. */\nexport async function uploadTemplate(\n logger: MiLogger,\n pl: PlClient,\n name: string,\n): Promise<TemplateReport> {\n try {\n const gotGreeting = await runUploadTemplate(logger, pl, name);\n if (gotGreeting !== `Hello, ${name}`) {\n return {\n status: \"failed\",\n message: `Template uploading failed: expected: ${name}, got: ${gotGreeting}`,\n };\n }\n\n return { status: \"ok\", message: `Template uploading succeeded: ${gotGreeting}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `Template uploading failed: error occurred: ${e}` };\n }\n}\n\nexport async function runUploadTemplate(\n logger: MiLogger,\n pl: PlClient,\n name: string,\n): Promise<string> {\n const outputs = await runTemplate(\n pl,\n SdkTemplates[\"check_network.upload_template\"],\n true,\n (tx) => ({\n name: tx.createValue(Pl.JsonObject, JSON.stringify(name)),\n }),\n [\"greeting\"],\n );\n\n try {\n return JSON.parse(notEmpty((await getFieldValue(pl, outputs.greeting)).data?.toString()));\n } finally {\n if (outputs != undefined) {\n await deleteFields(pl, Object.values(outputs));\n }\n }\n}\n\n/** Uploads a file to the backend and checks the output is a Blob resource. */\nexport async function uploadFile(\n logger: MiLogger,\n signer: Signer,\n lsDriver: LsDriver,\n uploadClient: ClientUpload,\n pl: PlClient,\n filePath: string,\n): Promise<TemplateReport> {\n try {\n const gotBlob = await runUploadFile(logger, signer, lsDriver, uploadClient, pl, filePath);\n\n if (gotBlob.type.name !== \"Blob\") {\n return { status: \"failed\", message: `File uploading failed: ${gotBlob.type.name}` };\n }\n\n return { status: \"ok\", message: `File uploading succeeded: ${gotBlob.type.name}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `File uploading failed: error occurred: ${e}` };\n }\n}\n\nexport async function runUploadFile(\n logger: MiLogger,\n signer: Signer,\n lsDriver: LsDriver,\n uploadClient: ClientUpload,\n pl: PlClient,\n filePath: string,\n): Promise<ResourceInfo> {\n const handle = await lsDriver.getLocalFileHandle(filePath);\n const result = await runTemplate(\n pl,\n SdkTemplates[\"check_network.upload_blob\"],\n true,\n (tx) => ({\n file: tx.createValue(Pl.JsonObject, JSON.stringify(handle)),\n }),\n [\"progress\", \"file\"],\n );\n\n try {\n const progress = await getFieldValue(pl, result.progress);\n\n if (isUpload(progress)) {\n const uploadData = ImportFileHandleUploadData.parse(\n JSON.parse(notEmpty(progress.data?.toString())),\n );\n const isUploadSignMatch = isSignMatch(signer, uploadData.localPath, uploadData.pathSignature);\n\n if (isUploadSignMatch) {\n await uploadBlob(logger, uploadClient, progress, uploadData, () => false, {\n nPartsWithThisUploadSpeed: 10,\n nPartsToIncreaseUpload: 10,\n currentSpeed: 10,\n maxSpeed: 10,\n });\n }\n }\n\n return await getFieldValue(pl, result.file);\n } finally {\n await deleteFields(pl, Object.values(result));\n }\n}\n\n/** Uploads a file to the backend and then tries to download it back. */\nexport async function downloadFile(\n logger: MiLogger,\n pl: PlClient,\n lsDriver: LsDriver,\n uploadClient: ClientUpload,\n downloadClient: ClientDownload,\n filePath: string,\n fileContent: string,\n): Promise<TemplateReport> {\n try {\n const gotFileContent = await runDownloadFile(\n logger,\n pl,\n lsDriver,\n uploadClient,\n downloadClient,\n filePath,\n );\n\n if (gotFileContent !== fileContent) {\n return {\n status: \"failed\",\n message: `File downloading failed: expected: ${fileContent}, got: ${gotFileContent}`,\n };\n }\n return { status: \"ok\", message: `File downloading succeeded: ${gotFileContent}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `File downloading failed: error occurred: ${e}` };\n }\n}\n\nexport async function runDownloadFile(\n logger: MiLogger,\n pl: PlClient,\n lsDriver: LsDriver,\n uploadClient: ClientUpload,\n downloadClient: ClientDownload,\n filePath: string,\n) {\n const handle = await lsDriver.getLocalFileHandle(filePath);\n\n const outputs = await runTemplate(\n pl,\n SdkTemplates[\"check_network.download_blob\"],\n true,\n (tx) => ({ file: tx.createValue(Pl.JsonObject, JSON.stringify(handle)) }),\n [\"progress\", \"file\"],\n );\n\n try {\n const progress = await getFieldValue(pl, outputs.progress);\n\n await uploadBlob(\n logger,\n uploadClient,\n progress,\n ImportFileHandleUploadData.parse(JSON.parse(notEmpty(progress.data?.toString()))),\n () => false,\n {\n nPartsWithThisUploadSpeed: 1,\n nPartsToIncreaseUpload: 1,\n currentSpeed: 1,\n maxSpeed: 1,\n },\n );\n\n const fileInfo = await getFieldValue(pl, outputs.file);\n\n return await downloadClient.withBlobContent(\n fileInfo,\n {},\n {},\n async (content) => await text(content),\n );\n } finally {\n await deleteFields(pl, Object.values(outputs));\n }\n}\n\n/** Runs Go's hello-world binary. */\nexport async function softwareCheck(pl: PlClient): Promise<TemplateReport> {\n try {\n const gotGreeting = await runSoftware(pl);\n\n if (gotGreeting !== \"Hello from go binary\\n\") {\n return { status: \"failed\", message: `Software check failed: got: ${gotGreeting}` };\n }\n return { status: \"ok\", message: `Software check succeeded: ${gotGreeting}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `Software check failed: error occurred: ${e}` };\n }\n}\n\nexport async function runSoftware(pl: PlClient): Promise<string> {\n const result = await runTemplate(\n pl,\n SdkTemplates[\"check_network.run_hello_world\"],\n true,\n (_: PlTransaction) => ({}),\n [\"greeting\"],\n );\n\n try {\n return notEmpty((await getFieldValue(pl, result.greeting)).data?.toString());\n } finally {\n await deleteFields(pl, Object.values(result));\n }\n}\n\n/** Runs Python hello-world. */\nexport async function pythonSoftware(pl: PlClient, name: string): Promise<TemplateReport> {\n try {\n const gotGreeting = await runPythonSoftware(pl, name);\n\n if (gotGreeting !== `Hello, ${name}!\\n`) {\n return { status: \"failed\", message: `Python software check failed: got: ${gotGreeting}` };\n }\n return { status: \"ok\", message: `Python software check succeeded: ${gotGreeting}` };\n } catch (e: unknown) {\n return { status: \"failed\", message: `Python software check failed: error occurred: ${e}` };\n }\n}\n\nexport async function runPythonSoftware(pl: PlClient, name: string): Promise<string> {\n const result = await runTemplate(\n pl,\n SdkTemplates[\"check_network.run_hello_world_py\"],\n true,\n (tx) => ({ name: tx.createValue(Pl.JsonObject, JSON.stringify(name)) }),\n [\"greeting\"],\n );\n\n try {\n return notEmpty((await getFieldValue(pl, result.greeting)).data?.toString());\n } finally {\n await deleteFields(pl, Object.values(result));\n }\n}\n\n/** Tries to download a file from every storage. */\nexport async function downloadFromEveryStorage(\n logger: MiLogger,\n pl: PlClient,\n lsDriver: LsDriver,\n ops: {\n minLsRequests: number;\n bytesLimit: number;\n minFileSize: number;\n maxFileSize: number;\n nFilesToCheck: number;\n },\n): Promise<Record<string, TemplateReport>> {\n try {\n const storages = await lsDriver.getStorageList();\n const results: Record<string, TemplateReport> = {};\n\n for (const storage of storages) {\n const result = await chooseFile(\n lsDriver,\n storage,\n ops.nFilesToCheck,\n ops.minFileSize,\n ops.maxFileSize,\n ops.minLsRequests,\n );\n if (result.file === undefined) {\n results[storage.id] = {\n status: \"warn\",\n message:\n `No file between ${ops.minFileSize} and ${ops.maxFileSize} bytes ` +\n `found in storage ${storage.name}, checked ${result.nCheckedFiles} files, ` +\n `did ${result.nLsRequests} ls requests`,\n };\n continue;\n }\n\n logger.info(`Downloading file ${JSON.stringify(result)} from storage ${storage.name}`);\n const outputs = await runTemplate(\n pl,\n SdkTemplates[\"check_network.create_workdir_from_storage\"],\n true,\n (tx) => ({\n file: tx.createValue(\n Pl.JsonObject,\n JSON.stringify((result.file as { handle: string }).handle),\n ),\n }),\n [\"workdirTypeName\"],\n );\n\n try {\n const workdirTypeName = JSON.parse(\n Buffer.from((await getFieldValue(pl, outputs.workdirTypeName)).data!).toString(),\n ) as string;\n\n if (workdirTypeName?.startsWith(\"WorkingDirectory\")) {\n results[storage.id] = {\n status: \"ok\",\n message:\n `Workdir creation succeeded, size of file: ${result.file?.size}, ` +\n `checked ${result.nCheckedFiles} files, did ${result.nLsRequests} ls requests`,\n };\n } else {\n results[storage.id] = {\n status: \"failed\",\n message:\n `Workdir creation failed: ${workdirTypeName}, size of file: ${result.file?.size}, ` +\n `checked ${result.nCheckedFiles} files, did ${result.nLsRequests} ls requests`,\n };\n }\n } finally {\n await deleteFields(pl, Object.values(outputs));\n }\n }\n\n return results;\n } catch (e: unknown) {\n return {\n unknown: {\n status: \"failed\",\n message: `Download from every storage failed: error occurred: ${e}`,\n },\n };\n }\n}\n\n/** Chooses a random file from the storage in a size range.\n * If we couldn't find a normal-sized file, we'll return a small file to check at least something.\n */\nexport async function chooseFile(\n lsDriver: LsDriver,\n storage: StorageEntry,\n limit: number,\n minSize: number,\n maxSize: number,\n minLsRequests: number,\n): Promise<{\n file: LsEntryWithFileStats | undefined;\n nLsRequests: number;\n nCheckedFiles: number;\n}> {\n const files = listFilesSequence(lsDriver, storage, \"\", 0);\n\n // return small file in case we don't have many normal-sized files.\n // While we'll download only a small range of bytes from the file,\n // we don't want to return a big file in case the underlying S3 doesn't support range requests.\n let smallFile: LsEntryWithFileStats | undefined;\n let nCheckedFiles = 0;\n let maxNLsRequests = 0;\n\n for await (const { file, nLsRequests } of files) {\n maxNLsRequests = Math.max(maxNLsRequests, nLsRequests);\n\n if (nCheckedFiles >= limit && maxNLsRequests > minLsRequests) {\n // we reached a limit on both the number of files and the number of ls requests.\n return { file: smallFile, nLsRequests: maxNLsRequests, nCheckedFiles };\n }\n nCheckedFiles++;\n if (minSize <= file.size && file.size <= maxSize) {\n return { file, nLsRequests: maxNLsRequests, nCheckedFiles };\n } else if (file.size < minSize) {\n smallFile = file;\n }\n }\n\n return { file: smallFile, nLsRequests: maxNLsRequests, nCheckedFiles };\n}\n\n/** Deep-first search for files in the storage. */\nexport async function* listFilesSequence(\n lsDriver: LsDriver,\n storage: StorageEntry,\n parent: string,\n nLsRequests: number,\n): AsyncGenerator<{ file: LsEntryWithFileStats; nLsRequests: number }, void, unknown> {\n nLsRequests++;\n const files = await lsDriver.listRemoteFilesWithFileStats(storage.handle, parent);\n\n for (const file of files.entries) {\n if (file.type === \"file\" && file.fullPath.startsWith(parent)) {\n yield {\n file,\n nLsRequests,\n };\n } else if (file.type === \"dir\") {\n for await (const nestedFile of listFilesSequence(\n lsDriver,\n storage,\n file.fullPath,\n nLsRequests,\n )) {\n nLsRequests = Math.max(nestedFile.nLsRequests, nLsRequests);\n yield nestedFile;\n }\n }\n }\n}\n\n/** Creates a big temporary file with random content. */\nexport async function createBigTempFile(): Promise<{ filePath: string }> {\n const filePath = path.join(os.tmpdir(), `check-network-big-temp-${Date.now()}.bin`);\n const fileSize = 20 * 1024 * 1024; // 20 MiB\n\n const fileContent = randomBytes(fileSize);\n\n await fs.appendFile(filePath, fileContent);\n\n return { filePath };\n}\n\n/** Creates a temporarly file we could use for uploading and downloading. */\nexport async function createTempFile(): Promise<{ filePath: string; fileContent: string }> {\n const filePath = path.join(os.tmpdir(), `check-network-temp-${Date.now()}.txt`);\n\n const fileContent = \"Hello, world! \" + new Date().toISOString();\n await fs.writeFile(filePath, fileContent);\n\n return { filePath, fileContent };\n}\n\n/** Creates a template and RenderTemplate resources, gets all resources from outputs.\n * Throws a error if any of the outputs failed.\n */\nasync function runTemplate(\n client: PlClient,\n tpl: TemplateSpecAny,\n ephemeral: boolean,\n inputs: (tx: PlTransaction) => Pl.PlRecord,\n outputs: string[],\n): Promise<Record<string, FieldId>> {\n return await client.withWriteTx(\"TemplateRender\", async (tx) => {\n const preparedTemplate = await prepareTemplateSpec(tpl);\n const tplResource = loadTemplate(tx, preparedTemplate);\n\n const outputFields: Record<string, FieldRef> = createRenderTemplate(\n tx,\n tplResource,\n ephemeral,\n inputs(tx),\n outputs,\n );\n\n const outputsIds: Record<string, FieldId> = {};\n\n for (const output of outputs) {\n const fieldRef = field(client.clientRoot, output) as FieldId;\n tx.createField(fieldRef, \"Dynamic\", outputFields[output]);\n outputsIds[output] = await toGlobalFieldId(fieldRef);\n }\n\n await tx.commit();\n\n return outputsIds;\n });\n}\n\n/** Gets a resource from field's value or throws a error. */\nasync function getFieldValue(client: PlClient, fieldId: FieldId): Promise<ResourceData> {\n // We could also do polling with pl-tree, but it seemed like an overkill,\n // that's why we have a simple polling here.\n\n return await poll(client, async (tx) => {\n const field = await tx.tx.getField(fieldId);\n if (isNotNullSignedResourceId(field.error)) {\n const err = await tx.tx.getResourceData(field.error, true);\n throw new Error(`getFieldValue of \"${fieldId.fieldName}\" field failed: ${err.data}`);\n }\n\n if (isNullSignedResourceId(field.value)) {\n throw new ContinuePolling();\n }\n\n return await tx.tx.getResourceData(field.value, true);\n });\n}\n\nasync function deleteFields(client: PlClient, fieldIds: FieldId[]) {\n await client.withWriteTx(\"DeleteFields\", async (tx) => {\n for (const fieldId of fieldIds) {\n tx.resetField(fieldId);\n }\n await tx.commit();\n });\n}\n"],"mappings":"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"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@milaboratories/pl-middle-layer",
3
- "version": "1.66.19",
3
+ "version": "1.67.0",
4
4
  "description": "Pl Middle Layer",
5
5
  "keywords": [],
6
6
  "license": "UNLICENSED",
@@ -31,24 +31,24 @@
31
31
  "yaml": "^2.8.0",
32
32
  "zod": "~3.25.76",
33
33
  "@milaboratories/computable": "2.9.8",
34
- "@milaboratories/columns-collection-driver": "0.2.3",
35
- "@milaboratories/pf-driver": "1.9.0",
34
+ "@milaboratories/pf-driver": "1.9.1",
36
35
  "@milaboratories/helpers": "1.14.5",
37
- "@milaboratories/pf-spec-driver": "1.5.0",
38
- "@milaboratories/pl-client": "3.14.6",
39
- "@milaboratories/pl-drivers": "1.16.15",
40
- "@milaboratories/pl-deployments": "3.0.15",
41
- "@milaboratories/pl-errors": "1.4.35",
42
- "@milaboratories/pl-model-backend": "1.4.20",
36
+ "@milaboratories/pl-client": "3.14.7",
37
+ "@milaboratories/columns-collection-driver": "0.2.4",
38
+ "@milaboratories/pf-spec-driver": "1.5.1",
39
+ "@milaboratories/pl-drivers": "1.16.16",
40
+ "@milaboratories/pl-errors": "1.4.36",
41
+ "@milaboratories/pl-deployments": "3.0.16",
42
+ "@milaboratories/pl-model-backend": "1.4.21",
43
+ "@milaboratories/pl-model-middle-layer": "1.32.0",
43
44
  "@milaboratories/pl-http": "1.2.4",
44
- "@milaboratories/pl-model-middle-layer": "1.31.0",
45
- "@milaboratories/resolve-helper": "1.1.3",
46
- "@milaboratories/pl-model-common": "1.47.3",
45
+ "@milaboratories/pl-tree": "1.13.7",
46
+ "@milaboratories/pl-model-common": "1.48.0",
47
47
  "@milaboratories/ts-helpers": "1.8.6",
48
- "@milaboratories/pl-tree": "1.13.6",
49
- "@platforma-sdk/block-tools": "2.13.0",
50
- "@platforma-sdk/workflow-tengo": "6.8.2",
51
- "@platforma-sdk/model": "1.81.1"
48
+ "@platforma-sdk/model": "1.82.0",
49
+ "@platforma-sdk/block-tools": "2.14.0",
50
+ "@milaboratories/resolve-helper": "1.1.3",
51
+ "@platforma-sdk/workflow-tengo": "6.8.2"
52
52
  },
53
53
  "devDependencies": {
54
54
  "@types/node": "~24.5.2",
@@ -57,8 +57,8 @@
57
57
  "typescript": "~5.9.3",
58
58
  "vitest": "^4.1.3",
59
59
  "@milaboratories/build-configs": "2.0.0",
60
- "@milaboratories/ts-builder": "1.6.2",
61
- "@milaboratories/ts-configs": "1.4.0"
60
+ "@milaboratories/ts-configs": "1.4.0",
61
+ "@milaboratories/ts-builder": "1.7.0"
62
62
  },
63
63
  "engines": {
64
64
  "node": ">=22.19.0"
@@ -1,3 +1,4 @@
1
1
  export * from "./registry";
2
2
  export * from "./registry-v2-provider";
3
3
  export * from "./well_known_registries";
4
+ export * from "./template_provider";
@@ -0,0 +1,236 @@
1
+ import { beforeAll, describe, expect, test } from "vitest";
2
+ import fsp from "node:fs/promises";
3
+ import os from "node:os";
4
+ import path from "node:path";
5
+ import { pathToFileURL } from "node:url";
6
+ import type { BlockPackLocationReference } from "@milaboratories/pl-model-common";
7
+ import { resolveBlockPackLocation } from "./location_provider";
8
+
9
+ /**
10
+ * The filesystem side of template import, driven against real directories.
11
+ *
12
+ * Everything here is about what is actually on disk — which layout is present, which spec
13
+ * addresses it, whether a path with a space still resolves — so these tests build the layouts
14
+ * rather than mock the readers. What each failure *says* to the reader is the resolver's, and
15
+ * lives with it.
16
+ */
17
+
18
+ const KIND = "@milaboratories/milaboratories.test-loc.kind@1.2.3";
19
+
20
+ let root: string;
21
+
22
+ beforeAll(async () => {
23
+ root = await fsp.mkdtemp(path.join(os.tmpdir(), "template-location-"));
24
+ });
25
+
26
+ const locate = (dir: string) =>
27
+ resolveBlockPackLocation(pathToFileURL(dir).href as BlockPackLocationReference);
28
+
29
+ /**
30
+ * A built-but-never-packed block: the source `package.json` names the components, and
31
+ * the kind is only in the compiled model. The shape a developer's working tree is in.
32
+ */
33
+ async function writeDevBlock(
34
+ dir: string,
35
+ options: { title?: string; kind?: string | null } = {},
36
+ ): Promise<string> {
37
+ const { title = "Located Block", kind = KIND } = options;
38
+ await fsp.mkdir(path.join(dir, "ui"), { recursive: true });
39
+ await fsp.writeFile(
40
+ path.join(dir, "package.json"),
41
+ JSON.stringify({
42
+ name: "@milaboratories/milaboratories.test-loc",
43
+ version: "1.0.0",
44
+ block: {
45
+ components: {
46
+ workflow: "./main.plj.gz",
47
+ model: "./model.json",
48
+ ui: "./ui",
49
+ },
50
+ meta: {
51
+ title,
52
+ description: "A block used to exercise location resolution",
53
+ organization: { name: "MiLaboratories Inc", url: "https://milaboratories.com/" },
54
+ },
55
+ },
56
+ }),
57
+ );
58
+ await fsp.writeFile(path.join(dir, "main.plj.gz"), "workflow-bytes");
59
+ await fsp.writeFile(path.join(dir, "ui", "index.html"), "<html></html>");
60
+ // A real config container, because loading a source description normalizes the model
61
+ // to read its feature flags and refuses anything it cannot recognize. The render
62
+ // envelope is left empty: the kind sits above it, and nothing on this path runs the
63
+ // block.
64
+ await fsp.writeFile(
65
+ path.join(dir, "model.json"),
66
+ JSON.stringify({ v3: {}, code: {}, ...(kind === null ? {} : { kind }) }),
67
+ );
68
+ return dir;
69
+ }
70
+
71
+ /** A packed block: one manifest naming everything, kind included. */
72
+ async function writePackedBlock(
73
+ dir: string,
74
+ options: { title?: string; kind?: string | null } = {},
75
+ ): Promise<string> {
76
+ const { title = "Packed Block", kind = KIND } = options;
77
+ await fsp.mkdir(dir, { recursive: true });
78
+ await fsp.writeFile(
79
+ path.join(dir, "manifest.json"),
80
+ JSON.stringify({
81
+ schema: "v2",
82
+ description: {
83
+ id: { organization: "milaboratories", name: "test-loc", version: "1.0.0" },
84
+ components: {
85
+ workflow: { type: "workflow-v1", main: { type: "relative", path: "main.plj.gz" } },
86
+ model: { type: "relative", path: "model.json" },
87
+ ui: { type: "relative", path: "ui.tgz" },
88
+ },
89
+ meta: {
90
+ title,
91
+ description: "A packed block used to exercise location resolution",
92
+ organization: { name: "MiLaboratories Inc", url: "https://milaboratories.com/" },
93
+ },
94
+ ...(kind === null ? {} : { kind }),
95
+ },
96
+ files: [],
97
+ }),
98
+ );
99
+ return dir;
100
+ }
101
+
102
+ describe("a block built from source", () => {
103
+ test("resolves to the dev spec pointing at that very folder", async () => {
104
+ const dir = await writeDevBlock(path.join(root, "dev-basic"));
105
+
106
+ const outcome = await locate(dir);
107
+
108
+ expect(outcome.ok).toBe(true);
109
+ if (!outcome.ok) return;
110
+ expect(outcome.spec).toEqual({ type: "dev-v2", folder: dir });
111
+ expect(outcome.title).toBe("Located Block");
112
+ });
113
+
114
+ test("a model declaring no kind still resolves", async () => {
115
+ // Whether the block is the one the entry meant is settled once it has been prepared,
116
+ // from the compiled model there. Here it is only a block that was found.
117
+ const dir = await writeDevBlock(path.join(root, "dev-no-kind"), { kind: null });
118
+
119
+ const outcome = await locate(dir);
120
+
121
+ expect(outcome.ok).toBe(true);
122
+ });
123
+ });
124
+
125
+ describe("a packed block", () => {
126
+ test("resolves to the pack spec addressing the folder the manifest was read from", async () => {
127
+ const dir = await writePackedBlock(path.join(root, "packed"));
128
+
129
+ const outcome = await locate(dir);
130
+
131
+ expect(outcome.ok).toBe(true);
132
+ if (!outcome.ok) return;
133
+ expect(outcome.spec).toEqual({ type: "from-pack-v2", packUrl: pathToFileURL(dir).href });
134
+ expect(outcome.title).toBe("Packed Block");
135
+ });
136
+
137
+ test("the manifest wins over a package.json beside it", async () => {
138
+ // A facade package that has been packed holds both shapes; the manifest is the
139
+ // built artifact, and it is what the preparer will read.
140
+ const dir = path.join(root, "both-shapes");
141
+ await writeDevBlock(dir);
142
+ await writePackedBlock(dir, { title: "Packed Block" });
143
+
144
+ const outcome = await locate(dir);
145
+
146
+ expect(outcome.ok && outcome.spec.type).toBe("from-pack-v2");
147
+ expect(outcome.ok && outcome.title).toBe("Packed Block");
148
+ });
149
+ });
150
+
151
+ describe("a folder that is not itself the block", () => {
152
+ test("the conventional 'block' subfolder is found", async () => {
153
+ // So a hand-written entry may name what a person calls the block. Export never
154
+ // writes this form: it emits the folder the loader accepts.
155
+ const parent = path.join(root, "parent-block");
156
+ await writeDevBlock(path.join(parent, "block"));
157
+
158
+ const outcome = await locate(parent);
159
+
160
+ expect(outcome.ok).toBe(true);
161
+ expect(outcome.ok && outcome.spec).toEqual({
162
+ type: "dev-v2",
163
+ folder: path.join(parent, "block"),
164
+ });
165
+ });
166
+
167
+ test("the 'meta' subfolder too", async () => {
168
+ const parent = path.join(root, "parent-meta");
169
+ await writeDevBlock(path.join(parent, "meta"));
170
+
171
+ const outcome = await locate(parent);
172
+
173
+ expect(outcome.ok && outcome.spec).toEqual({
174
+ type: "dev-v2",
175
+ folder: path.join(parent, "meta"),
176
+ });
177
+ });
178
+ });
179
+
180
+ describe("what cannot be read", () => {
181
+ test("a folder that does not exist is not-found", async () => {
182
+ const outcome = await locate(path.join(root, "nowhere-at-all"));
183
+
184
+ expect(outcome).toEqual({ ok: false, reason: "not-found" });
185
+ });
186
+
187
+ test("a folder that exists but holds nothing is not-a-block", async () => {
188
+ // The two are kept apart because they send the reader somewhere different: one is
189
+ // a template carried to another machine, the other is a path one level off.
190
+ const dir = path.join(root, "empty");
191
+ await fsp.mkdir(dir, { recursive: true });
192
+
193
+ const outcome = await locate(dir);
194
+
195
+ expect(outcome).toEqual({ ok: false, reason: "not-a-block" });
196
+ });
197
+
198
+ test("an ordinary npm package is not a block", async () => {
199
+ // A `package.json` with no block description must not reach the loader, which
200
+ // would throw rather than report.
201
+ const dir = path.join(root, "plain-package");
202
+ await fsp.mkdir(dir, { recursive: true });
203
+ await fsp.writeFile(
204
+ path.join(dir, "package.json"),
205
+ JSON.stringify({ name: "just-a-package", version: "1.0.0" }),
206
+ );
207
+
208
+ const outcome = await locate(dir);
209
+
210
+ expect(outcome).toEqual({ ok: false, reason: "not-a-block" });
211
+ });
212
+
213
+ test("a scheme with no reader here is reported as such, not as a missing block", async () => {
214
+ const outcome = await resolveBlockPackLocation(
215
+ "https://blocks.internal/enter-numbers" as BlockPackLocationReference,
216
+ );
217
+
218
+ expect(outcome).toEqual({ ok: false, reason: "unsupported-scheme" });
219
+ });
220
+ });
221
+
222
+ describe("paths that need decoding", () => {
223
+ test("a folder whose name contains a space resolves", async () => {
224
+ // The value is a URL, so the space arrives as `%20`; reading the URL's pathname
225
+ // directly instead of converting it would look correct and never find the folder.
226
+ const dir = await writeDevBlock(path.join(root, "my blocks", "spaced"));
227
+
228
+ const location = pathToFileURL(dir).href;
229
+ expect(location).toContain("%20");
230
+
231
+ const outcome = await resolveBlockPackLocation(location as BlockPackLocationReference);
232
+
233
+ expect(outcome.ok).toBe(true);
234
+ expect(outcome.ok && outcome.spec).toEqual({ type: "dev-v2", folder: dir });
235
+ });
236
+ });