@milaboratories/pl-model-common 1.47.2 → 1.48.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/bmodel/block_kind_ref.cjs +45 -0
- package/dist/bmodel/block_kind_ref.cjs.map +1 -0
- package/dist/bmodel/block_kind_ref.d.ts +57 -0
- package/dist/bmodel/block_kind_ref.d.ts.map +1 -0
- package/dist/bmodel/block_kind_ref.js +43 -0
- package/dist/bmodel/block_kind_ref.js.map +1 -0
- package/dist/bmodel/container.d.ts +8 -0
- package/dist/bmodel/container.d.ts.map +1 -1
- package/dist/bmodel/index.cjs +4 -0
- package/dist/bmodel/index.d.ts +2 -1
- package/dist/bmodel/index.js +2 -1
- package/dist/columns/dedup.cjs +1 -1
- package/dist/columns/dedup.cjs.map +1 -1
- package/dist/columns/dedup.d.ts +1 -1
- package/dist/columns/dedup.js +1 -1
- package/dist/columns/dedup.js.map +1 -1
- package/dist/columns/providers.cjs +1 -1
- package/dist/columns/providers.cjs.map +1 -1
- package/dist/columns/providers.d.ts +1 -1
- package/dist/columns/providers.js +1 -1
- package/dist/columns/providers.js.map +1 -1
- package/dist/drivers/index.cjs +4 -0
- package/dist/drivers/index.d.ts +2 -2
- package/dist/drivers/index.js +2 -2
- package/dist/drivers/pframe/data_types.cjs.map +1 -1
- package/dist/drivers/pframe/data_types.d.ts +24 -0
- package/dist/drivers/pframe/data_types.d.ts.map +1 -1
- package/dist/drivers/pframe/data_types.js.map +1 -1
- package/dist/drivers/pframe/index.cjs +4 -0
- package/dist/drivers/pframe/index.d.ts +2 -2
- package/dist/drivers/pframe/index.js +2 -2
- package/dist/drivers/pframe/spec/ids.cjs +151 -0
- package/dist/drivers/pframe/spec/ids.cjs.map +1 -1
- package/dist/drivers/pframe/spec/ids.d.ts +53 -1
- package/dist/drivers/pframe/spec/ids.d.ts.map +1 -1
- package/dist/drivers/pframe/spec/ids.js +150 -3
- package/dist/drivers/pframe/spec/ids.js.map +1 -1
- package/dist/drivers/pframe/spec/index.cjs +4 -0
- package/dist/drivers/pframe/spec/index.d.ts +2 -2
- package/dist/drivers/pframe/spec/index.js +2 -2
- package/dist/index.cjs +29 -0
- package/dist/index.d.ts +7 -2
- package/dist/index.js +8 -2
- package/dist/plid.cjs +1 -1
- package/dist/plid.cjs.map +1 -1
- package/dist/plid.d.ts +3 -2
- package/dist/plid.d.ts.map +1 -1
- package/dist/plid.js +1 -1
- package/dist/plid.js.map +1 -1
- package/dist/template/index.cjs +20 -0
- package/dist/template/index.d.ts +5 -0
- package/dist/template/index.js +5 -0
- package/dist/template/kind_selector.cjs +92 -0
- package/dist/template/kind_selector.cjs.map +1 -0
- package/dist/template/kind_selector.d.ts +80 -0
- package/dist/template/kind_selector.d.ts.map +1 -0
- package/dist/template/kind_selector.js +87 -0
- package/dist/template/kind_selector.js.map +1 -0
- package/dist/template/project_template_v1.cjs +231 -0
- package/dist/template/project_template_v1.cjs.map +1 -0
- package/dist/template/project_template_v1.d.ts +215 -0
- package/dist/template/project_template_v1.d.ts.map +1 -0
- package/dist/template/project_template_v1.js +225 -0
- package/dist/template/project_template_v1.js.map +1 -0
- package/dist/template/template_ref_form.cjs +73 -0
- package/dist/template/template_ref_form.cjs.map +1 -0
- package/dist/template/template_ref_form.d.ts +74 -0
- package/dist/template/template_ref_form.d.ts.map +1 -0
- package/dist/template/template_ref_form.js +72 -0
- package/dist/template/template_ref_form.js.map +1 -0
- package/dist/template/template_relocate.cjs +46 -0
- package/dist/template/template_relocate.cjs.map +1 -0
- package/dist/template/template_relocate.d.ts +32 -0
- package/dist/template/template_relocate.d.ts.map +1 -0
- package/dist/template/template_relocate.js +46 -0
- package/dist/template/template_relocate.js.map +1 -0
- package/package.json +5 -5
- package/src/bmodel/block_kind_ref.ts +59 -0
- package/src/bmodel/container.ts +9 -0
- package/src/bmodel/index.ts +1 -0
- package/src/columns/dedup.ts +1 -1
- package/src/columns/providers.ts +1 -1
- package/src/drivers/pframe/data_types.ts +24 -0
- package/src/drivers/pframe/spec/ids.test.ts +90 -0
- package/src/drivers/pframe/spec/ids.ts +191 -1
- package/src/index.ts +1 -0
- package/src/plid.ts +5 -5
- package/src/template/index.ts +4 -0
- package/src/template/kind_selector.ts +126 -0
- package/src/template/project_template_v1.test.ts +315 -0
- package/src/template/project_template_v1.ts +444 -0
- package/src/template/template_ref_form.test.ts +86 -0
- package/src/template/template_ref_form.ts +108 -0
- package/src/template/template_relocate.test.ts +182 -0
- package/src/template/template_relocate.ts +61 -0
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_ref_form.d.ts","names":[],"sources":["../../src/template/template_ref_form.ts"],"mappings":";;AA6BA;;;;AAIe;AAUf;;;;;;;;AAAuE;AAsCvE;;;;;;;;;AAAmD;;;KApDvC,aAAA;oEAED,KAAA;WAEA,IAAI;AAAA;;;;;;;;iBAUC,eAAA,CAAgB,KAAA,YAAiB,KAAA,IAAS,aAAa;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;iBAsCvD,kBAAA,IAAsB,MAAA,EAAQ,CAAA,GAAI,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
//#region src/template/template_ref_form.ts
|
|
2
|
+
/**
|
|
3
|
+
* Whether `value` is a reference in the readable spelling.
|
|
4
|
+
*
|
|
5
|
+
* Exact about its keys, because the shape lives inside params a kind owns: `{ block, name }`
|
|
6
|
+
* and nothing else. A value carrying `__isRef` is a `PlRef` already and is not this — the two
|
|
7
|
+
* are told apart by shape and never overlap.
|
|
8
|
+
*/
|
|
9
|
+
function isTemplatePlRef(value) {
|
|
10
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
11
|
+
const keys = Object.keys(value);
|
|
12
|
+
if (keys.length !== 2 || !keys.includes("block") || !keys.includes("name")) return false;
|
|
13
|
+
const { block, name } = value;
|
|
14
|
+
return typeof block === "string" && typeof name === "string";
|
|
15
|
+
}
|
|
16
|
+
/**
|
|
17
|
+
* Expand every readable reference form in `params` into the form the system stores.
|
|
18
|
+
*
|
|
19
|
+
* Named for the job and not for today's only case. One readable form exists so far —
|
|
20
|
+
* {@link TemplatePlRef}, the leaf reference — and the rest of the identifier system is meant to
|
|
21
|
+
* follow: `TemplateCUId` / `TemplateCUKey`, readable spellings of the filtered, discovered and
|
|
22
|
+
* overridden column keys, whose long forms are far worse to type than a `PlRef`'s.
|
|
23
|
+
*
|
|
24
|
+
* **Adding one is a recognizer plus an expander**, checked before the generic object case, the
|
|
25
|
+
* way `isTemplatePlRef` / `expandTemplatePlRef` are below. Two rules a nesting form has to
|
|
26
|
+
* respect, both consequences of how the stored forms are built:
|
|
27
|
+
*
|
|
28
|
+
* - **Expand bottom-up.** A wrapper key holds its source as a canonical *string*, not as an
|
|
29
|
+
* object, so the inner reference must be expanded and serialized before the outer key can be
|
|
30
|
+
* assembled. Descending after building the outer form would leave the inner spelling inside a
|
|
31
|
+
* string nothing looks at again.
|
|
32
|
+
* - **Canonicalize what you build.** An identifier IS its canonical string; a key assembled
|
|
33
|
+
* with keys in another order is a different identifier for the same column.
|
|
34
|
+
*
|
|
35
|
+
* What comes out names its upstreams by template-local entry id — the same thing a `PlRef` in a
|
|
36
|
+
* template file means. Turning those into the ids of real blocks is {@link relocateBlockIds},
|
|
37
|
+
* which runs right after and treats an expanded reference exactly like one the file spelled out
|
|
38
|
+
* in full.
|
|
39
|
+
*
|
|
40
|
+
* Needs nothing but the params. That is a property of the forms, not a coincidence: a readable
|
|
41
|
+
* spelling carries the same information as the form it stands for, so expansion is a rewrite and
|
|
42
|
+
* never a lookup. A form that needed the document to expand — a reference by position, say —
|
|
43
|
+
* would have to be resolved somewhere that knows the document, and would drag that knowledge
|
|
44
|
+
* into every caller of this. Keep them information-preserving.
|
|
45
|
+
*/
|
|
46
|
+
function expandTemplateRefs(params) {
|
|
47
|
+
const walk = (node) => {
|
|
48
|
+
if (isTemplatePlRef(node)) return expandTemplatePlRef(node);
|
|
49
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
50
|
+
if (typeof node === "object" && node !== null) return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, walk(v)]));
|
|
51
|
+
return node;
|
|
52
|
+
};
|
|
53
|
+
return walk(params);
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* The leaf form's expander: `{ block, name }` becomes the `PlRef` it stands for.
|
|
57
|
+
*
|
|
58
|
+
* An id naming no entry is passed through, like a hand-written `PlRef` would be: an id naming
|
|
59
|
+
* nothing and an id naming an entry created later are indistinguishable, and both are meant to
|
|
60
|
+
* arrive at a block that reports itself as missing references.
|
|
61
|
+
*/
|
|
62
|
+
function expandTemplatePlRef(ref) {
|
|
63
|
+
return {
|
|
64
|
+
__isRef: true,
|
|
65
|
+
blockId: ref.block,
|
|
66
|
+
name: ref.name
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
//#endregion
|
|
70
|
+
export { expandTemplateRefs, isTemplatePlRef };
|
|
71
|
+
|
|
72
|
+
//# sourceMappingURL=template_ref_form.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_ref_form.js","names":[],"sources":["../../src/template/template_ref_form.ts"],"sourcesContent":["import type { PlRef } from \"../ref\";\n\n/**\n * A reference as a person writes it: the entry it points at, and the output name.\n *\n * The readable spelling of a {@link PlRef}, and input only. The two say the same thing, and\n * differ only in what a reader has to carry:\n *\n * ```yaml\n * # what an export writes, and what a block holds\n * sources:\n * - __isRef: true\n * blockId: samples\n * name: numbers\n *\n * # the same reference, written by hand\n * sources:\n * - { block: samples, name: numbers }\n * ```\n *\n * `block` is a template-local entry id — the same thing a `PlRef`'s `blockId` holds inside a\n * template file. Nothing else: this form exists to drop the `__isRef` marker and the word\n * `blockId`, not to introduce a second way of naming an entry.\n *\n * Export never emits it — a block holds live `PlRef`s and a template holds what the block\n * holds. It is expanded on the way in, inside the block's own bundle and before the kind's\n * parser runs, so a kind's params contract is written against `PlRef` alone and never learns\n * this type exists.\n */\nexport type TemplatePlRef = {\n /** The template-local id of the entry this points at. */\n readonly block: string;\n /** The upstream output's name, exactly as a `PlRef` spells it. */\n readonly name: string;\n};\n\n/**\n * Whether `value` is a reference in the readable spelling.\n *\n * Exact about its keys, because the shape lives inside params a kind owns: `{ block, name }`\n * and nothing else. A value carrying `__isRef` is a `PlRef` already and is not this — the two\n * are told apart by shape and never overlap.\n */\nexport function isTemplatePlRef(value: unknown): value is TemplatePlRef {\n if (typeof value !== \"object\" || value === null || Array.isArray(value)) return false;\n const keys = Object.keys(value);\n if (keys.length !== 2 || !keys.includes(\"block\") || !keys.includes(\"name\")) return false;\n const { block, name } = value as { block: unknown; name: unknown };\n return typeof block === \"string\" && typeof name === \"string\";\n}\n\n/**\n * Expand every readable reference form in `params` into the form the system stores.\n *\n * Named for the job and not for today's only case. One readable form exists so far —\n * {@link TemplatePlRef}, the leaf reference — and the rest of the identifier system is meant to\n * follow: `TemplateCUId` / `TemplateCUKey`, readable spellings of the filtered, discovered and\n * overridden column keys, whose long forms are far worse to type than a `PlRef`'s.\n *\n * **Adding one is a recognizer plus an expander**, checked before the generic object case, the\n * way `isTemplatePlRef` / `expandTemplatePlRef` are below. Two rules a nesting form has to\n * respect, both consequences of how the stored forms are built:\n *\n * - **Expand bottom-up.** A wrapper key holds its source as a canonical *string*, not as an\n * object, so the inner reference must be expanded and serialized before the outer key can be\n * assembled. Descending after building the outer form would leave the inner spelling inside a\n * string nothing looks at again.\n * - **Canonicalize what you build.** An identifier IS its canonical string; a key assembled\n * with keys in another order is a different identifier for the same column.\n *\n * What comes out names its upstreams by template-local entry id — the same thing a `PlRef` in a\n * template file means. Turning those into the ids of real blocks is {@link relocateBlockIds},\n * which runs right after and treats an expanded reference exactly like one the file spelled out\n * in full.\n *\n * Needs nothing but the params. That is a property of the forms, not a coincidence: a readable\n * spelling carries the same information as the form it stands for, so expansion is a rewrite and\n * never a lookup. A form that needed the document to expand — a reference by position, say —\n * would have to be resolved somewhere that knows the document, and would drag that knowledge\n * into every caller of this. Keep them information-preserving.\n */\nexport function expandTemplateRefs<T>(params: T): T {\n const walk = (node: unknown): unknown => {\n // One line per readable form, before the generic object case: a form IS an object, and\n // descending into one would rewrite its parts instead of expanding it as a whole.\n if (isTemplatePlRef(node)) return expandTemplatePlRef(node);\n\n if (Array.isArray(node)) return node.map(walk);\n\n if (typeof node === \"object\" && node !== null) {\n return Object.fromEntries(Object.entries(node).map(([k, v]) => [k, walk(v)]));\n }\n\n return node;\n };\n return walk(params) as T;\n}\n\n/**\n * The leaf form's expander: `{ block, name }` becomes the `PlRef` it stands for.\n *\n * An id naming no entry is passed through, like a hand-written `PlRef` would be: an id naming\n * nothing and an id naming an entry created later are indistinguishable, and both are meant to\n * arrive at a block that reports itself as missing references.\n */\nfunction expandTemplatePlRef(ref: TemplatePlRef): PlRef {\n return { __isRef: true, blockId: ref.block, name: ref.name };\n}\n"],"mappings":";;;;;;;;AA2CA,SAAgB,gBAAgB,OAAwC;CACtE,IAAI,OAAO,UAAU,YAAY,UAAU,QAAQ,MAAM,QAAQ,KAAK,GAAG,OAAO;CAChF,MAAM,OAAO,OAAO,KAAK,KAAK;CAC9B,IAAI,KAAK,WAAW,KAAK,CAAC,KAAK,SAAS,OAAO,KAAK,CAAC,KAAK,SAAS,MAAM,GAAG,OAAO;CACnF,MAAM,EAAE,OAAO,SAAS;CACxB,OAAO,OAAO,UAAU,YAAY,OAAO,SAAS;AACtD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAgCA,SAAgB,mBAAsB,QAAc;CAClD,MAAM,QAAQ,SAA2B;EAGvC,IAAI,gBAAgB,IAAI,GAAG,OAAO,oBAAoB,IAAI;EAE1D,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,IAAI,IAAI;EAE7C,IAAI,OAAO,SAAS,YAAY,SAAS,MACvC,OAAO,OAAO,YAAY,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,GAAG,KAAK,CAAC,CAAC,CAAC,CAAC;EAG9E,OAAO;CACT;CACA,OAAO,KAAK,MAAM;AACpB;;;;;;;;AASA,SAAS,oBAAoB,KAA2B;CACtD,OAAO;EAAE,SAAS;EAAM,SAAS,IAAI;EAAO,MAAM,IAAI;CAAK;AAC7D"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
const require_ids = require("../drivers/pframe/spec/ids.cjs");
|
|
2
|
+
require("../drivers/index.cjs");
|
|
3
|
+
//#region src/template/template_relocate.ts
|
|
4
|
+
/**
|
|
5
|
+
* Point every column identifier in a block's params at the blocks of the project being built.
|
|
6
|
+
*
|
|
7
|
+
* The whole of what a template does about references, and it lives here — in the package the
|
|
8
|
+
* block's own bundle imports — because knowing which values carry block ids is knowing the
|
|
9
|
+
* reference system. The engine carrying the params neither marks them, reads them, nor
|
|
10
|
+
* rewrites them: it hands the block its params and this map, and takes back what comes out.
|
|
11
|
+
*
|
|
12
|
+
* Params travel verbatim precisely so that this is possible. A file holds a `PlRef` as the
|
|
13
|
+
* object the block stored and a column id as the canonical string the block stored, with no
|
|
14
|
+
* marker of any kind, and the identifiers are found here by recognizing them — the same way
|
|
15
|
+
* the project's own dependency detector finds them in live args.
|
|
16
|
+
*
|
|
17
|
+
* Rewriting is structural, never textual: an identifier is taken apart, its `blockId` fields
|
|
18
|
+
* are replaced, and it is rebuilt canonically. That is what keeps a value that merely *looks*
|
|
19
|
+
* like an id — a `domain` entry, an axis filter — from being rewritten along with it, and
|
|
20
|
+
* what re-sorts a qualifications map whose keys are identifiers.
|
|
21
|
+
*
|
|
22
|
+
* An id the map does not mention is left as it is. That is the ordering rule doing its work:
|
|
23
|
+
* a caller building the map as it creates blocks passes only the entries already created, so
|
|
24
|
+
* a reference to an entry further down the file stays pointing at a block that does not
|
|
25
|
+
* exist, and the applied block reports itself as missing references rather than being wired
|
|
26
|
+
* to something below it.
|
|
27
|
+
*
|
|
28
|
+
* @param params Whatever the block projected, as the document stored it
|
|
29
|
+
* @param blockIds template-local entry id → the block id that entry was given
|
|
30
|
+
*/
|
|
31
|
+
function relocateBlockIds(params, blockIds) {
|
|
32
|
+
if (blockIds.size === 0) return params;
|
|
33
|
+
const remapBlockId = (blockId) => blockIds.get(blockId) ?? blockId;
|
|
34
|
+
const walk = (node) => {
|
|
35
|
+
if (typeof node === "string") return require_ids.remapColumnIdBlockIds(node, remapBlockId);
|
|
36
|
+
if (require_ids.isColumnUniversalKey(node)) return require_ids.remapColumnIdBlockIds(node, remapBlockId);
|
|
37
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
38
|
+
if (typeof node === "object" && node !== null) return Object.fromEntries(Object.entries(node).map(([key, value]) => [require_ids.remapColumnIdBlockIds(key, remapBlockId), walk(value)]));
|
|
39
|
+
return node;
|
|
40
|
+
};
|
|
41
|
+
return walk(params);
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
exports.relocateBlockIds = relocateBlockIds;
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=template_relocate.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_relocate.cjs","names":["remapColumnIdBlockIds","isColumnUniversalKey"],"sources":["../../src/template/template_relocate.ts"],"sourcesContent":["import { isColumnUniversalKey, remapColumnIdBlockIds } from \"../drivers\";\n\n/**\n * Point every column identifier in a block's params at the blocks of the project being built.\n *\n * The whole of what a template does about references, and it lives here — in the package the\n * block's own bundle imports — because knowing which values carry block ids is knowing the\n * reference system. The engine carrying the params neither marks them, reads them, nor\n * rewrites them: it hands the block its params and this map, and takes back what comes out.\n *\n * Params travel verbatim precisely so that this is possible. A file holds a `PlRef` as the\n * object the block stored and a column id as the canonical string the block stored, with no\n * marker of any kind, and the identifiers are found here by recognizing them — the same way\n * the project's own dependency detector finds them in live args.\n *\n * Rewriting is structural, never textual: an identifier is taken apart, its `blockId` fields\n * are replaced, and it is rebuilt canonically. That is what keeps a value that merely *looks*\n * like an id — a `domain` entry, an axis filter — from being rewritten along with it, and\n * what re-sorts a qualifications map whose keys are identifiers.\n *\n * An id the map does not mention is left as it is. That is the ordering rule doing its work:\n * a caller building the map as it creates blocks passes only the entries already created, so\n * a reference to an entry further down the file stays pointing at a block that does not\n * exist, and the applied block reports itself as missing references rather than being wired\n * to something below it.\n *\n * @param params Whatever the block projected, as the document stored it\n * @param blockIds template-local entry id → the block id that entry was given\n */\nexport function relocateBlockIds<T>(params: T, blockIds: ReadonlyMap<string, string>): T {\n if (blockIds.size === 0) return params;\n const remapBlockId = (blockId: string) => blockIds.get(blockId) ?? blockId;\n\n const walk = (node: unknown): unknown => {\n // Any string may be an identifier under any amount of escaping; one that is not comes\n // back as the very same string, so this needs no test of its own here.\n if (typeof node === \"string\") return remapColumnIdBlockIds(node, remapBlockId);\n\n // Before the generic object case: an identifier IS an object, and descending into one\n // would rewrite the strings nested in it piecemeal instead of rebuilding the whole id —\n // losing the bottom-up canonicalization that keeps the result a valid identifier.\n if (isColumnUniversalKey(node)) return remapColumnIdBlockIds(node, remapBlockId);\n\n if (Array.isArray(node)) return node.map(walk);\n\n if (typeof node === \"object\" && node !== null) {\n // Keys as well as values: params may be keyed by column id — per-column settings, say\n // — and a key is exactly as much of a reference as a value is.\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n remapColumnIdBlockIds(key, remapBlockId),\n walk(value),\n ]),\n );\n }\n\n return node;\n };\n\n return walk(params) as T;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,iBAAoB,QAAW,UAA0C;CACvF,IAAI,SAAS,SAAS,GAAG,OAAO;CAChC,MAAM,gBAAgB,YAAoB,SAAS,IAAI,OAAO,KAAK;CAEnE,MAAM,QAAQ,SAA2B;EAGvC,IAAI,OAAO,SAAS,UAAU,OAAOA,YAAAA,sBAAsB,MAAM,YAAY;EAK7E,IAAIC,YAAAA,qBAAqB,IAAI,GAAG,OAAOD,YAAAA,sBAAsB,MAAM,YAAY;EAE/E,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,IAAI,IAAI;EAE7C,IAAI,OAAO,SAAS,YAAY,SAAS,MAGvC,OAAO,OAAO,YACZ,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACzCA,YAAAA,sBAAsB,KAAK,YAAY,GACvC,KAAK,KAAK,CACZ,CAAC,CACH;EAGF,OAAO;CACT;CAEA,OAAO,KAAK,MAAM;AACpB"}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
//#region src/template/template_relocate.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Point every column identifier in a block's params at the blocks of the project being built.
|
|
4
|
+
*
|
|
5
|
+
* The whole of what a template does about references, and it lives here — in the package the
|
|
6
|
+
* block's own bundle imports — because knowing which values carry block ids is knowing the
|
|
7
|
+
* reference system. The engine carrying the params neither marks them, reads them, nor
|
|
8
|
+
* rewrites them: it hands the block its params and this map, and takes back what comes out.
|
|
9
|
+
*
|
|
10
|
+
* Params travel verbatim precisely so that this is possible. A file holds a `PlRef` as the
|
|
11
|
+
* object the block stored and a column id as the canonical string the block stored, with no
|
|
12
|
+
* marker of any kind, and the identifiers are found here by recognizing them — the same way
|
|
13
|
+
* the project's own dependency detector finds them in live args.
|
|
14
|
+
*
|
|
15
|
+
* Rewriting is structural, never textual: an identifier is taken apart, its `blockId` fields
|
|
16
|
+
* are replaced, and it is rebuilt canonically. That is what keeps a value that merely *looks*
|
|
17
|
+
* like an id — a `domain` entry, an axis filter — from being rewritten along with it, and
|
|
18
|
+
* what re-sorts a qualifications map whose keys are identifiers.
|
|
19
|
+
*
|
|
20
|
+
* An id the map does not mention is left as it is. That is the ordering rule doing its work:
|
|
21
|
+
* a caller building the map as it creates blocks passes only the entries already created, so
|
|
22
|
+
* a reference to an entry further down the file stays pointing at a block that does not
|
|
23
|
+
* exist, and the applied block reports itself as missing references rather than being wired
|
|
24
|
+
* to something below it.
|
|
25
|
+
*
|
|
26
|
+
* @param params Whatever the block projected, as the document stored it
|
|
27
|
+
* @param blockIds template-local entry id → the block id that entry was given
|
|
28
|
+
*/
|
|
29
|
+
declare function relocateBlockIds<T>(params: T, blockIds: ReadonlyMap<string, string>): T;
|
|
30
|
+
//#endregion
|
|
31
|
+
export { relocateBlockIds };
|
|
32
|
+
//# sourceMappingURL=template_relocate.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_relocate.d.ts","names":[],"sources":["../../src/template/template_relocate.ts"],"mappings":";;AA6BA;;;;;;;;;;;;;;;AAAwF;;;;;;;;;;;iBAAxE,gBAAA,IAAoB,MAAA,EAAQ,CAAA,EAAG,QAAA,EAAU,WAAA,mBAA8B,CAAA"}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { isColumnUniversalKey, remapColumnIdBlockIds } from "../drivers/pframe/spec/ids.js";
|
|
2
|
+
import "../drivers/index.js";
|
|
3
|
+
//#region src/template/template_relocate.ts
|
|
4
|
+
/**
|
|
5
|
+
* Point every column identifier in a block's params at the blocks of the project being built.
|
|
6
|
+
*
|
|
7
|
+
* The whole of what a template does about references, and it lives here — in the package the
|
|
8
|
+
* block's own bundle imports — because knowing which values carry block ids is knowing the
|
|
9
|
+
* reference system. The engine carrying the params neither marks them, reads them, nor
|
|
10
|
+
* rewrites them: it hands the block its params and this map, and takes back what comes out.
|
|
11
|
+
*
|
|
12
|
+
* Params travel verbatim precisely so that this is possible. A file holds a `PlRef` as the
|
|
13
|
+
* object the block stored and a column id as the canonical string the block stored, with no
|
|
14
|
+
* marker of any kind, and the identifiers are found here by recognizing them — the same way
|
|
15
|
+
* the project's own dependency detector finds them in live args.
|
|
16
|
+
*
|
|
17
|
+
* Rewriting is structural, never textual: an identifier is taken apart, its `blockId` fields
|
|
18
|
+
* are replaced, and it is rebuilt canonically. That is what keeps a value that merely *looks*
|
|
19
|
+
* like an id — a `domain` entry, an axis filter — from being rewritten along with it, and
|
|
20
|
+
* what re-sorts a qualifications map whose keys are identifiers.
|
|
21
|
+
*
|
|
22
|
+
* An id the map does not mention is left as it is. That is the ordering rule doing its work:
|
|
23
|
+
* a caller building the map as it creates blocks passes only the entries already created, so
|
|
24
|
+
* a reference to an entry further down the file stays pointing at a block that does not
|
|
25
|
+
* exist, and the applied block reports itself as missing references rather than being wired
|
|
26
|
+
* to something below it.
|
|
27
|
+
*
|
|
28
|
+
* @param params Whatever the block projected, as the document stored it
|
|
29
|
+
* @param blockIds template-local entry id → the block id that entry was given
|
|
30
|
+
*/
|
|
31
|
+
function relocateBlockIds(params, blockIds) {
|
|
32
|
+
if (blockIds.size === 0) return params;
|
|
33
|
+
const remapBlockId = (blockId) => blockIds.get(blockId) ?? blockId;
|
|
34
|
+
const walk = (node) => {
|
|
35
|
+
if (typeof node === "string") return remapColumnIdBlockIds(node, remapBlockId);
|
|
36
|
+
if (isColumnUniversalKey(node)) return remapColumnIdBlockIds(node, remapBlockId);
|
|
37
|
+
if (Array.isArray(node)) return node.map(walk);
|
|
38
|
+
if (typeof node === "object" && node !== null) return Object.fromEntries(Object.entries(node).map(([key, value]) => [remapColumnIdBlockIds(key, remapBlockId), walk(value)]));
|
|
39
|
+
return node;
|
|
40
|
+
};
|
|
41
|
+
return walk(params);
|
|
42
|
+
}
|
|
43
|
+
//#endregion
|
|
44
|
+
export { relocateBlockIds };
|
|
45
|
+
|
|
46
|
+
//# sourceMappingURL=template_relocate.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"template_relocate.js","names":[],"sources":["../../src/template/template_relocate.ts"],"sourcesContent":["import { isColumnUniversalKey, remapColumnIdBlockIds } from \"../drivers\";\n\n/**\n * Point every column identifier in a block's params at the blocks of the project being built.\n *\n * The whole of what a template does about references, and it lives here — in the package the\n * block's own bundle imports — because knowing which values carry block ids is knowing the\n * reference system. The engine carrying the params neither marks them, reads them, nor\n * rewrites them: it hands the block its params and this map, and takes back what comes out.\n *\n * Params travel verbatim precisely so that this is possible. A file holds a `PlRef` as the\n * object the block stored and a column id as the canonical string the block stored, with no\n * marker of any kind, and the identifiers are found here by recognizing them — the same way\n * the project's own dependency detector finds them in live args.\n *\n * Rewriting is structural, never textual: an identifier is taken apart, its `blockId` fields\n * are replaced, and it is rebuilt canonically. That is what keeps a value that merely *looks*\n * like an id — a `domain` entry, an axis filter — from being rewritten along with it, and\n * what re-sorts a qualifications map whose keys are identifiers.\n *\n * An id the map does not mention is left as it is. That is the ordering rule doing its work:\n * a caller building the map as it creates blocks passes only the entries already created, so\n * a reference to an entry further down the file stays pointing at a block that does not\n * exist, and the applied block reports itself as missing references rather than being wired\n * to something below it.\n *\n * @param params Whatever the block projected, as the document stored it\n * @param blockIds template-local entry id → the block id that entry was given\n */\nexport function relocateBlockIds<T>(params: T, blockIds: ReadonlyMap<string, string>): T {\n if (blockIds.size === 0) return params;\n const remapBlockId = (blockId: string) => blockIds.get(blockId) ?? blockId;\n\n const walk = (node: unknown): unknown => {\n // Any string may be an identifier under any amount of escaping; one that is not comes\n // back as the very same string, so this needs no test of its own here.\n if (typeof node === \"string\") return remapColumnIdBlockIds(node, remapBlockId);\n\n // Before the generic object case: an identifier IS an object, and descending into one\n // would rewrite the strings nested in it piecemeal instead of rebuilding the whole id —\n // losing the bottom-up canonicalization that keeps the result a valid identifier.\n if (isColumnUniversalKey(node)) return remapColumnIdBlockIds(node, remapBlockId);\n\n if (Array.isArray(node)) return node.map(walk);\n\n if (typeof node === \"object\" && node !== null) {\n // Keys as well as values: params may be keyed by column id — per-column settings, say\n // — and a key is exactly as much of a reference as a value is.\n return Object.fromEntries(\n Object.entries(node).map(([key, value]) => [\n remapColumnIdBlockIds(key, remapBlockId),\n walk(value),\n ]),\n );\n }\n\n return node;\n };\n\n return walk(params) as T;\n}\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AA6BA,SAAgB,iBAAoB,QAAW,UAA0C;CACvF,IAAI,SAAS,SAAS,GAAG,OAAO;CAChC,MAAM,gBAAgB,YAAoB,SAAS,IAAI,OAAO,KAAK;CAEnE,MAAM,QAAQ,SAA2B;EAGvC,IAAI,OAAO,SAAS,UAAU,OAAO,sBAAsB,MAAM,YAAY;EAK7E,IAAI,qBAAqB,IAAI,GAAG,OAAO,sBAAsB,MAAM,YAAY;EAE/E,IAAI,MAAM,QAAQ,IAAI,GAAG,OAAO,KAAK,IAAI,IAAI;EAE7C,IAAI,OAAO,SAAS,YAAY,SAAS,MAGvC,OAAO,OAAO,YACZ,OAAO,QAAQ,IAAI,CAAC,CAAC,KAAK,CAAC,KAAK,WAAW,CACzC,sBAAsB,KAAK,YAAY,GACvC,KAAK,KAAK,CACZ,CAAC,CACH;EAGF,OAAO;CACT;CAEA,OAAO,KAAK,MAAM;AACpB"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@milaboratories/pl-model-common",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.48.0",
|
|
4
4
|
"description": "Platforma SDK Model",
|
|
5
5
|
"files": [
|
|
6
6
|
"./dist/**/*",
|
|
@@ -20,16 +20,16 @@
|
|
|
20
20
|
"canonicalize": "~2.1.0",
|
|
21
21
|
"es-toolkit": "^1.39.10",
|
|
22
22
|
"zod": "~3.25.76",
|
|
23
|
-
"@milaboratories/
|
|
24
|
-
"@milaboratories/
|
|
23
|
+
"@milaboratories/pl-error-like": "1.12.10",
|
|
24
|
+
"@milaboratories/helpers": "1.14.5"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"@vitest/coverage-istanbul": "^4.1.3",
|
|
28
28
|
"typescript": "~5.9.3",
|
|
29
29
|
"vitest": "^4.1.3",
|
|
30
|
+
"@milaboratories/ts-configs": "1.4.0",
|
|
30
31
|
"@milaboratories/build-configs": "2.0.0",
|
|
31
|
-
"@milaboratories/ts-
|
|
32
|
-
"@milaboratories/ts-builder": "1.6.1"
|
|
32
|
+
"@milaboratories/ts-builder": "1.7.0"
|
|
33
33
|
},
|
|
34
34
|
"scripts": {
|
|
35
35
|
"build": "ts-builder build --target node",
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import type { Branded } from "@milaboratories/helpers";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* On-wire reference to a block kind, canonically the string `{name}@{version}`.
|
|
5
|
+
*
|
|
6
|
+
* A branded string: readers overwhelmingly need identity equality ("does block
|
|
7
|
+
* X implement kind Y?"), for which an opaque canonical string is ideal. Any
|
|
8
|
+
* reader that needs the parts calls {@link parseKindRef}; any writer composes
|
|
9
|
+
* the reference through {@link formatKindRef}. Keeping composition in a single
|
|
10
|
+
* function localizes the one open decision — whether the name segment has to be
|
|
11
|
+
* org-qualified for global uniqueness — to one place.
|
|
12
|
+
*/
|
|
13
|
+
export type BlockKindReference = Branded<string, "BlockKindReference">;
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Compose a {@link BlockKindReference} from a kind's `name`/`version`.
|
|
17
|
+
*
|
|
18
|
+
* The single place that decides how the reference is assembled. If global
|
|
19
|
+
* uniqueness later requires the name segment to be org-qualified, this is the
|
|
20
|
+
* one line that changes.
|
|
21
|
+
*/
|
|
22
|
+
export const formatKindRef = (k: { name: string; version: string }): BlockKindReference =>
|
|
23
|
+
`${k.name}@${k.version}` as BlockKindReference;
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Split a `{name}@{version}` string on its version separator.
|
|
27
|
+
*
|
|
28
|
+
* The one place that decides where the name ends. Uses the LAST `@` so an
|
|
29
|
+
* org-qualified npm name that itself starts with `@` (e.g.
|
|
30
|
+
* `@platforma-open/pkg.kind`) keeps its whole name. A leading/absent separator
|
|
31
|
+
* (`lastIndexOf("@") <= 0`) means the string carries no version segment —
|
|
32
|
+
* malformed — so this throws rather than returning a silently version-less
|
|
33
|
+
* result.
|
|
34
|
+
*
|
|
35
|
+
* Shared with the template layer, whose `{name}@{selector}` references use the
|
|
36
|
+
* same split and differ only in how the right half is interpreted (see
|
|
37
|
+
* `parseKindSelectorReference`). `what` names the thing being parsed so the
|
|
38
|
+
* error message stays specific to the caller's reference type.
|
|
39
|
+
*/
|
|
40
|
+
export const splitVersionedName = (
|
|
41
|
+
ref: string,
|
|
42
|
+
what = "block kind reference",
|
|
43
|
+
expected = "{name}@{version}",
|
|
44
|
+
): { name: string; version: string } => {
|
|
45
|
+
const at = ref.lastIndexOf("@");
|
|
46
|
+
if (at <= 0) {
|
|
47
|
+
throw new Error(`Malformed ${what} (expected '${expected}'): ${ref}`);
|
|
48
|
+
}
|
|
49
|
+
return { name: ref.slice(0, at), version: ref.slice(at + 1) };
|
|
50
|
+
};
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Split a {@link BlockKindReference} back into `{ name, version }`.
|
|
54
|
+
*
|
|
55
|
+
* Throws on a reference with no version segment — see
|
|
56
|
+
* {@link splitVersionedName}, which owns the split rule.
|
|
57
|
+
*/
|
|
58
|
+
export const parseKindRef = (ref: BlockKindReference): { name: string; version: string } =>
|
|
59
|
+
splitVersionedName(ref);
|
package/src/bmodel/container.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { BlockConfigV3Generic, BlockConfigV4Generic } from "./block_config";
|
|
2
|
+
import type { BlockKindReference } from "./block_kind_ref";
|
|
2
3
|
import type { Code } from "./code";
|
|
3
4
|
import type { BlockRenderingMode } from "./types";
|
|
4
5
|
|
|
@@ -11,6 +12,14 @@ export type BlockConfigContainer = {
|
|
|
11
12
|
/** Config code bundle. Actually is required, but we keep it optional for backward compatibility */
|
|
12
13
|
readonly code?: Code;
|
|
13
14
|
|
|
15
|
+
/**
|
|
16
|
+
* Reference to the block kind this config implements, in `{name}@{version}`
|
|
17
|
+
* form. Version-independent block identity — lives at the container level
|
|
18
|
+
* beside {@link code}, orthogonal to which render envelope (`v3`/`v4`)
|
|
19
|
+
* applies. Optional for backward compatibility with kind-less blocks.
|
|
20
|
+
*/
|
|
21
|
+
readonly kind?: BlockKindReference;
|
|
22
|
+
|
|
14
23
|
//
|
|
15
24
|
// Fields below are used to read previous config versions
|
|
16
25
|
//
|
package/src/bmodel/index.ts
CHANGED
package/src/columns/dedup.ts
CHANGED
|
@@ -20,7 +20,7 @@ import { isPObjectId } from "../pool";
|
|
|
20
20
|
*
|
|
21
21
|
* Shared by sandbox-side `extractColumns` (column_providers) and host-side
|
|
22
22
|
* `ColumnsCollectionDriverImpl.getColumns` — both layers need identical
|
|
23
|
-
* dedup semantics, but operate on different concrete item types (
|
|
23
|
+
* dedup semantics, but operate on different concrete item types (DataColumnRecipe
|
|
24
24
|
* vs. raw ColumnUniversalId).
|
|
25
25
|
*/
|
|
26
26
|
export function dedupColumns<T>(
|
package/src/columns/providers.ts
CHANGED
|
@@ -8,7 +8,7 @@ import type { AccessorLike, ColumnEntriesProvider, LeafEntry, UpstreamBlockCtx }
|
|
|
8
8
|
* exposes `isFinal()` via the root's `getInputsLocked()`.
|
|
9
9
|
*
|
|
10
10
|
* Used directly on the host side; sandbox extends it with `getColumns()`
|
|
11
|
-
* returning {@link
|
|
11
|
+
* returning {@link DataColumnRecipe}s — see `AccessorColumnsProvider` in
|
|
12
12
|
* `@platforma-sdk/model`.
|
|
13
13
|
*/
|
|
14
14
|
export class AccessorEntriesProvider<
|
|
@@ -248,6 +248,18 @@ export interface WritePTableToFsOptions {
|
|
|
248
248
|
path: string;
|
|
249
249
|
format: PTableDownloadFormat;
|
|
250
250
|
columnIndices: number[];
|
|
251
|
+
/**
|
|
252
|
+
* Optional header names aligned 1:1 with {@link columnIndices}. When provided,
|
|
253
|
+
* each is written verbatim — letting callers supply disambiguated labels the
|
|
254
|
+
* spec's intrinsic `pl7.app/label` may not carry. When omitted (or an
|
|
255
|
+
* individual entry is empty), the header falls back to the column's
|
|
256
|
+
* spec-derived label.
|
|
257
|
+
*
|
|
258
|
+
* Additive on purpose: the UI (ui-vue) and the desktop-app runtime version
|
|
259
|
+
* independently, so a newer UI must not send a shape an older runtime cannot
|
|
260
|
+
* read. Older runtimes ignore this field and use {@link columnIndices} alone.
|
|
261
|
+
*/
|
|
262
|
+
headerNames?: string[];
|
|
251
263
|
range?: TableRange;
|
|
252
264
|
chunkSize?: number;
|
|
253
265
|
includeHeader?: boolean;
|
|
@@ -281,4 +293,16 @@ export interface ExportPTableOptions {
|
|
|
281
293
|
/** Unified indices of the columns to export, in output order
|
|
282
294
|
* (axes first, then data columns). */
|
|
283
295
|
columnIndices: number[];
|
|
296
|
+
/**
|
|
297
|
+
* Optional header names aligned 1:1 with {@link columnIndices}. When provided,
|
|
298
|
+
* each is written verbatim — letting callers supply disambiguated labels the
|
|
299
|
+
* spec's intrinsic `pl7.app/label` may not carry. When omitted (or an
|
|
300
|
+
* individual entry is empty), the header falls back to the column's
|
|
301
|
+
* spec-derived label.
|
|
302
|
+
*
|
|
303
|
+
* Additive on purpose: the UI (ui-vue) and the desktop-app runtime version
|
|
304
|
+
* independently, so a newer UI must not send a shape an older runtime cannot
|
|
305
|
+
* read. Older runtimes ignore this field and use {@link columnIndices} alone.
|
|
306
|
+
*/
|
|
307
|
+
headerNames?: string[];
|
|
284
308
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { describe, expect, test } from "vitest";
|
|
2
|
+
import { createGlobalPObjectId, createLocalPObjectId } from "../../../pool";
|
|
3
|
+
import { createColumnDiscoveredId } from "./discovered_column";
|
|
4
|
+
import { createColumnFilteredId } from "./filtered_column";
|
|
5
|
+
import { peelJsonLayers, type ColumnUniversalId } from "./ids";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* The escape-layer peeler.
|
|
9
|
+
*
|
|
10
|
+
* It is the one definition of "how a value can be hiding inside a string", and the reference
|
|
11
|
+
* detector in `pl-middle-layer` (`inferAllReferencedBlocks`) is built on it — a block id can
|
|
12
|
+
* sit under any number of `JSON.stringify` passes, and a walk over object properties reaches
|
|
13
|
+
* none of them. It deliberately says nothing about which values count as identifiers; the
|
|
14
|
+
* cases below are identifiers only because that is what the callers care about.
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const leaf = (blockId: string, name: string) =>
|
|
18
|
+
createGlobalPObjectId(blockId, name) as ColumnUniversalId;
|
|
19
|
+
|
|
20
|
+
describe("peelJsonLayers", () => {
|
|
21
|
+
test("a canonical id is layer zero — encoded once, and that once is the id itself", () => {
|
|
22
|
+
const id = leaf("samples", "reads");
|
|
23
|
+
|
|
24
|
+
expect(peelJsonLayers(id)).toEqual({
|
|
25
|
+
value: { __isRef: true, blockId: "samples", name: "reads" },
|
|
26
|
+
layers: 0,
|
|
27
|
+
});
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
test("each extra stringify pass is one more layer", () => {
|
|
31
|
+
const id = leaf("samples", "reads");
|
|
32
|
+
|
|
33
|
+
expect(peelJsonLayers(JSON.stringify(id))?.layers).toBe(1);
|
|
34
|
+
expect(peelJsonLayers(JSON.stringify(JSON.stringify(id)))?.layers).toBe(2);
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
test("the value at the bottom is the same however deep it was", () => {
|
|
38
|
+
const id = leaf("samples", "reads");
|
|
39
|
+
const bottom = { __isRef: true, blockId: "samples", name: "reads" };
|
|
40
|
+
|
|
41
|
+
expect(peelJsonLayers(JSON.stringify(JSON.stringify(id)))?.value).toEqual(bottom);
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
test("a nested identifier peels to its own outer key, not to the leaf", () => {
|
|
45
|
+
// Wrapper forms nest by *string*, so peeling reaches the outermost key and stops. Walking
|
|
46
|
+
// further in is the caller's business, and no caller does — which is the point.
|
|
47
|
+
const filtered = createColumnFilteredId({
|
|
48
|
+
source: createColumnDiscoveredId({ column: leaf("samples", "clonotypes") }),
|
|
49
|
+
axisFilters: [[0, "IGH"]],
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
const peeled = peelJsonLayers(filtered);
|
|
53
|
+
|
|
54
|
+
expect(peeled?.layers).toBe(0);
|
|
55
|
+
expect(peeled?.value).toMatchObject({ __isFiltered: true });
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test("a local leaf peels too, though it carries no marker at all", () => {
|
|
59
|
+
// The gate must not demand `__isRef`: a filtered id whose innermost leaf is local has
|
|
60
|
+
// none, and a peeler that required one would miss the whole chain.
|
|
61
|
+
const id = createLocalPObjectId(["pf", "byChain"], "abundance");
|
|
62
|
+
|
|
63
|
+
expect(peelJsonLayers(id)?.value).toEqual({
|
|
64
|
+
resolvePath: ["pf", "byChain"],
|
|
65
|
+
name: "abundance",
|
|
66
|
+
});
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
test("an ordinary string is not JSON and stops at the first character", () => {
|
|
70
|
+
for (const value of ["samples", "", "not json", "1.0", "yes"]) {
|
|
71
|
+
expect(peelJsonLayers(value)).toBeUndefined();
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
test("a quoted string that only ever yields strings is refused", () => {
|
|
76
|
+
// `"\"abc\""` peels to `abc`, which is not JSON — there is no encoded value in there, so
|
|
77
|
+
// there is nothing for a caller to look at.
|
|
78
|
+
expect(peelJsonLayers(JSON.stringify("abc"))).toBeUndefined();
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
test("malformed JSON is refused rather than thrown", () => {
|
|
82
|
+
expect(peelJsonLayers('{"__isRef": true')).toBeUndefined();
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
test("a JSON array or scalar is a value like any other", () => {
|
|
86
|
+
// Nothing here is identifier-specific: the peeler answers "what was encoded", full stop.
|
|
87
|
+
expect(peelJsonLayers("[1,2]")).toBeUndefined();
|
|
88
|
+
expect(peelJsonLayers('{"a":1}')).toEqual({ value: { a: 1 }, layers: 0 });
|
|
89
|
+
});
|
|
90
|
+
});
|