@milaboratories/pl-model-common 1.46.4 → 1.47.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 (178) hide show
  1. package/dist/columns/accessor_traversal.cjs +119 -0
  2. package/dist/columns/accessor_traversal.cjs.map +1 -0
  3. package/dist/columns/accessor_traversal.d.ts +52 -0
  4. package/dist/columns/accessor_traversal.d.ts.map +1 -0
  5. package/dist/columns/accessor_traversal.js +114 -0
  6. package/dist/columns/accessor_traversal.js.map +1 -0
  7. package/dist/columns/column_registry.cjs +40 -0
  8. package/dist/columns/column_registry.cjs.map +1 -0
  9. package/dist/columns/column_registry.d.ts +31 -0
  10. package/dist/columns/column_registry.d.ts.map +1 -0
  11. package/dist/columns/column_registry.js +40 -0
  12. package/dist/columns/column_registry.js.map +1 -0
  13. package/dist/columns/column_selector.cjs +49 -0
  14. package/dist/columns/column_selector.cjs.map +1 -0
  15. package/dist/columns/column_selector.d.ts +33 -0
  16. package/dist/columns/column_selector.d.ts.map +1 -0
  17. package/dist/columns/column_selector.js +47 -0
  18. package/dist/columns/column_selector.js.map +1 -0
  19. package/dist/columns/dedup.cjs +47 -0
  20. package/dist/columns/dedup.cjs.map +1 -0
  21. package/dist/columns/dedup.d.ts +27 -0
  22. package/dist/columns/dedup.d.ts.map +1 -0
  23. package/dist/columns/dedup.js +47 -0
  24. package/dist/columns/dedup.js.map +1 -0
  25. package/dist/columns/index.cjs +18 -0
  26. package/dist/columns/index.d.ts +8 -0
  27. package/dist/columns/index.js +6 -0
  28. package/dist/columns/providers.cjs +62 -0
  29. package/dist/columns/providers.cjs.map +1 -0
  30. package/dist/columns/providers.d.ts +39 -0
  31. package/dist/columns/providers.d.ts.map +1 -0
  32. package/dist/columns/providers.js +61 -0
  33. package/dist/columns/providers.js.map +1 -0
  34. package/dist/columns/types.d.ts +108 -0
  35. package/dist/columns/types.d.ts.map +1 -0
  36. package/dist/driver_kit.d.ts +1 -2
  37. package/dist/driver_kit.d.ts.map +1 -1
  38. package/dist/drivers/columns/columns_collection_driver.d.ts +124 -0
  39. package/dist/drivers/columns/columns_collection_driver.d.ts.map +1 -0
  40. package/dist/drivers/columns/discover_columns_options.cjs +28 -0
  41. package/dist/drivers/columns/discover_columns_options.cjs.map +1 -0
  42. package/dist/drivers/columns/discover_columns_options.d.ts +64 -0
  43. package/dist/drivers/columns/discover_columns_options.d.ts.map +1 -0
  44. package/dist/drivers/columns/discover_columns_options.js +28 -0
  45. package/dist/drivers/columns/discover_columns_options.js.map +1 -0
  46. package/dist/drivers/columns/index.d.ts +3 -0
  47. package/dist/drivers/index.cjs +32 -7
  48. package/dist/drivers/index.d.ts +12 -9
  49. package/dist/drivers/index.js +6 -4
  50. package/dist/drivers/pframe/driver.d.ts +1 -2
  51. package/dist/drivers/pframe/driver.d.ts.map +1 -1
  52. package/dist/drivers/pframe/index.cjs +30 -7
  53. package/dist/drivers/pframe/index.d.ts +9 -8
  54. package/dist/drivers/pframe/index.js +5 -4
  55. package/dist/drivers/pframe/query/index.d.ts +2 -2
  56. package/dist/drivers/pframe/query/query_common.d.ts +24 -1
  57. package/dist/drivers/pframe/query/query_common.d.ts.map +1 -1
  58. package/dist/drivers/pframe/query/query_spec.d.ts +25 -24
  59. package/dist/drivers/pframe/query/query_spec.d.ts.map +1 -1
  60. package/dist/drivers/pframe/query/utils.cjs +18 -8
  61. package/dist/drivers/pframe/query/utils.cjs.map +1 -1
  62. package/dist/drivers/pframe/query/utils.d.ts +5 -1
  63. package/dist/drivers/pframe/query/utils.d.ts.map +1 -1
  64. package/dist/drivers/pframe/query/utils.js +18 -8
  65. package/dist/drivers/pframe/query/utils.js.map +1 -1
  66. package/dist/drivers/pframe/spec/anchored.d.ts +2 -2
  67. package/dist/drivers/pframe/spec/discovered_column.cjs +31 -15
  68. package/dist/drivers/pframe/spec/discovered_column.cjs.map +1 -1
  69. package/dist/drivers/pframe/spec/discovered_column.d.ts +23 -13
  70. package/dist/drivers/pframe/spec/discovered_column.d.ts.map +1 -1
  71. package/dist/drivers/pframe/spec/discovered_column.js +26 -12
  72. package/dist/drivers/pframe/spec/discovered_column.js.map +1 -1
  73. package/dist/drivers/pframe/spec/filtered_column.cjs +46 -0
  74. package/dist/drivers/pframe/spec/filtered_column.cjs.map +1 -1
  75. package/dist/drivers/pframe/spec/filtered_column.d.ts +39 -1
  76. package/dist/drivers/pframe/spec/filtered_column.d.ts.map +1 -1
  77. package/dist/drivers/pframe/spec/filtered_column.js +41 -1
  78. package/dist/drivers/pframe/spec/filtered_column.js.map +1 -1
  79. package/dist/drivers/pframe/spec/ids.cjs +62 -10
  80. package/dist/drivers/pframe/spec/ids.cjs.map +1 -1
  81. package/dist/drivers/pframe/spec/ids.d.ts +48 -15
  82. package/dist/drivers/pframe/spec/ids.d.ts.map +1 -1
  83. package/dist/drivers/pframe/spec/ids.js +60 -9
  84. package/dist/drivers/pframe/spec/ids.js.map +1 -1
  85. package/dist/drivers/pframe/spec/index.cjs +30 -7
  86. package/dist/drivers/pframe/spec/index.d.ts +6 -5
  87. package/dist/drivers/pframe/spec/index.js +5 -4
  88. package/dist/drivers/pframe/spec/native_id.cjs +1 -0
  89. package/dist/drivers/pframe/spec/native_id.cjs.map +1 -1
  90. package/dist/drivers/pframe/spec/native_id.js +1 -0
  91. package/dist/drivers/pframe/spec/native_id.js.map +1 -1
  92. package/dist/drivers/pframe/spec/overridden.cjs +212 -0
  93. package/dist/drivers/pframe/spec/overridden.cjs.map +1 -0
  94. package/dist/drivers/pframe/spec/overridden.d.ts +67 -0
  95. package/dist/drivers/pframe/spec/overridden.d.ts.map +1 -0
  96. package/dist/drivers/pframe/spec/overridden.js +202 -0
  97. package/dist/drivers/pframe/spec/overridden.js.map +1 -0
  98. package/dist/drivers/pframe/spec/selectors.cjs +1 -0
  99. package/dist/drivers/pframe/spec/selectors.cjs.map +1 -1
  100. package/dist/drivers/pframe/spec/selectors.d.ts +3 -0
  101. package/dist/drivers/pframe/spec/selectors.d.ts.map +1 -1
  102. package/dist/drivers/pframe/spec/selectors.js +1 -0
  103. package/dist/drivers/pframe/spec/selectors.js.map +1 -1
  104. package/dist/drivers/pframe/spec/spec.cjs.map +1 -1
  105. package/dist/drivers/pframe/spec/spec.d.ts +2 -3
  106. package/dist/drivers/pframe/spec/spec.d.ts.map +1 -1
  107. package/dist/drivers/pframe/spec/spec.js.map +1 -1
  108. package/dist/drivers/pframe/spec_driver.d.ts +2 -2
  109. package/dist/drivers/pframe/spec_driver.d.ts.map +1 -1
  110. package/dist/drivers/pframe/table_calculate.cjs +2 -2
  111. package/dist/drivers/pframe/table_calculate.cjs.map +1 -1
  112. package/dist/drivers/pframe/table_calculate.d.ts +6 -2
  113. package/dist/drivers/pframe/table_calculate.d.ts.map +1 -1
  114. package/dist/drivers/pframe/table_calculate.js +2 -2
  115. package/dist/drivers/pframe/table_calculate.js.map +1 -1
  116. package/dist/drivers/pframe/table_common.cjs.map +1 -1
  117. package/dist/drivers/pframe/table_common.d.ts +11 -4
  118. package/dist/drivers/pframe/table_common.d.ts.map +1 -1
  119. package/dist/drivers/pframe/table_common.js.map +1 -1
  120. package/dist/index.cjs +63 -10
  121. package/dist/index.d.ts +20 -11
  122. package/dist/index.js +15 -6
  123. package/dist/pool/index.cjs +18 -0
  124. package/dist/pool/index.d.ts +2 -2
  125. package/dist/pool/index.js +3 -0
  126. package/dist/pool/spec.cjs +59 -11
  127. package/dist/pool/spec.cjs.map +1 -1
  128. package/dist/pool/spec.d.ts +24 -9
  129. package/dist/pool/spec.d.ts.map +1 -1
  130. package/dist/pool/spec.js +50 -10
  131. package/dist/pool/spec.js.map +1 -1
  132. package/dist/services/index.d.ts +2 -2
  133. package/dist/services/service_declarations.cjs +22 -0
  134. package/dist/services/service_declarations.cjs.map +1 -1
  135. package/dist/services/service_declarations.d.ts +5 -3
  136. package/dist/services/service_declarations.d.ts.map +1 -1
  137. package/dist/services/service_declarations.js +22 -0
  138. package/dist/services/service_declarations.js.map +1 -1
  139. package/dist/services/service_registry.cjs.map +1 -1
  140. package/dist/services/service_registry.d.ts +3 -3
  141. package/dist/services/service_registry.d.ts.map +1 -1
  142. package/dist/services/service_registry.js.map +1 -1
  143. package/dist/services/service_types.cjs.map +1 -1
  144. package/dist/services/service_types.d.ts +25 -9
  145. package/dist/services/service_types.d.ts.map +1 -1
  146. package/dist/services/service_types.js.map +1 -1
  147. package/package.json +2 -1
  148. package/src/columns/accessor_traversal.ts +139 -0
  149. package/src/columns/column_registry.ts +41 -0
  150. package/src/columns/column_selector.ts +100 -0
  151. package/src/columns/dedup.ts +49 -0
  152. package/src/columns/index.ts +6 -0
  153. package/src/columns/providers.ts +74 -0
  154. package/src/columns/types.ts +116 -0
  155. package/src/drivers/columns/columns_collection_driver.ts +146 -0
  156. package/src/drivers/columns/discover_columns_options.ts +91 -0
  157. package/src/drivers/columns/index.ts +2 -0
  158. package/src/drivers/index.ts +1 -0
  159. package/src/drivers/pframe/index.ts +0 -1
  160. package/src/drivers/pframe/query/query_common.ts +24 -0
  161. package/src/drivers/pframe/query/query_spec.ts +38 -25
  162. package/src/drivers/pframe/query/utils.test.ts +6 -6
  163. package/src/drivers/pframe/query/utils.ts +27 -7
  164. package/src/drivers/pframe/spec/discovered_column.ts +43 -28
  165. package/src/drivers/pframe/spec/filtered_column.ts +66 -0
  166. package/src/drivers/pframe/spec/ids.ts +142 -17
  167. package/src/drivers/pframe/spec/index.ts +1 -0
  168. package/src/drivers/pframe/spec/overridden.ts +285 -0
  169. package/src/drivers/pframe/spec/selectors.ts +3 -0
  170. package/src/drivers/pframe/spec/spec.ts +7 -3
  171. package/src/drivers/pframe/spec_driver.ts +3 -3
  172. package/src/drivers/pframe/table_calculate.ts +13 -3
  173. package/src/drivers/pframe/table_common.ts +10 -3
  174. package/src/index.ts +1 -0
  175. package/src/pool/spec.ts +69 -23
  176. package/src/services/service_declarations.ts +31 -0
  177. package/src/services/service_registry.ts +8 -4
  178. package/src/services/service_types.ts +35 -9
@@ -1,35 +1,39 @@
1
- import { Branded, throwError } from "@milaboratories/helpers";
1
+ import { type Branded, throwError } from "@milaboratories/helpers";
2
+ import { type CanonicalizedJson, canonicalizeJson, parseJsonSafely } from "../../../json";
2
3
  import { PObjectId } from "../../../pool";
3
4
  import { AxisQualification } from "./selectors";
4
- import { canonicalizeJson } from "../../../json";
5
+ import { ColumnUniversalId } from "./ids";
5
6
 
6
- export type DiscoveredPColumn = {
7
- column: PObjectId;
7
+ export interface ColumnDiscoveredKey {
8
+ __isDiscovered: true;
9
+ column: ColumnUniversalId;
8
10
  path?: PathItem[];
9
11
  columnQualifications?: AxisQualification[];
10
12
  queriesQualifications?: Record<PObjectId, AxisQualification[]>;
11
- };
13
+ }
12
14
 
13
- export type DiscoveredPColumnId = Branded<PObjectId, "DiscoveredPColumnId">; // CanonicalizedJson<DiscoveredPColumn>;
15
+ export type ColumnDiscoveredId = Branded<
16
+ CanonicalizedJson<ColumnDiscoveredKey>,
17
+ "ColumnDiscoveredId"
18
+ >;
14
19
 
15
20
  type PathItem = {
16
21
  type: "linker";
17
- column: PObjectId;
22
+ column: ColumnUniversalId;
18
23
  };
19
24
 
20
- export function isDiscoveredPColumn(obj: unknown): obj is DiscoveredPColumn {
21
- return (
22
- typeof obj === "object" &&
23
- obj !== null &&
24
- "path" in obj &&
25
- "column" in obj &&
26
- "columnQualifications" in obj &&
27
- "queriesQualifications" in obj
28
- );
25
+ export function isColumnDiscoveredKey(obj: unknown): obj is ColumnDiscoveredKey {
26
+ return typeof obj === "object" && obj !== null && "__isDiscovered" in obj;
29
27
  }
30
28
 
31
- export function distillDiscoveredPColumn(props: DiscoveredPColumn): DiscoveredPColumn {
29
+ export function isColumnDiscoveredId(str: unknown): str is ColumnDiscoveredId {
30
+ if (typeof str !== "string") return false;
31
+ return isColumnDiscoveredKey(parseJsonSafely(str));
32
+ }
33
+
34
+ export function distillColumnDiscoveredKey(props: ColumnDiscoveredKey): ColumnDiscoveredKey {
32
35
  return {
36
+ __isDiscovered: true,
33
37
  column: props.column,
34
38
  path: Array.isArray(props.path) && props.path.length > 0 ? props.path : undefined,
35
39
  columnQualifications:
@@ -43,30 +47,41 @@ export function distillDiscoveredPColumn(props: DiscoveredPColumn): DiscoveredPC
43
47
  };
44
48
  }
45
49
 
46
- export function createDiscoveredPColumnId(props: {
47
- column: PObjectId;
50
+ export function createColumnDiscoveredId(props: {
51
+ column: ColumnUniversalId;
52
+ path?: PathItem[];
53
+ columnQualifications?: AxisQualification[];
54
+ queriesQualifications?: Record<PObjectId, AxisQualification[]>;
55
+ }): ColumnDiscoveredId {
56
+ return stringifyColumnDiscoveredId(props);
57
+ }
58
+
59
+ export function createColumnDiscoveredKey(props: {
60
+ column: ColumnUniversalId;
48
61
  path?: PathItem[];
49
62
  columnQualifications?: AxisQualification[];
50
63
  queriesQualifications?: Record<PObjectId, AxisQualification[]>;
51
- }): DiscoveredPColumnId {
52
- return stringifyDiscoveredPColumnId(props);
64
+ }): ColumnDiscoveredKey {
65
+ return distillColumnDiscoveredKey({ __isDiscovered: true, ...props });
53
66
  }
54
67
 
55
- export function parseDiscoveredPColumnId(id: DiscoveredPColumnId): DiscoveredPColumn {
68
+ export function parseColumnDiscoveredId(id: ColumnDiscoveredId): ColumnDiscoveredKey {
56
69
  try {
57
70
  const parsed = JSON.parse(id);
58
- return isDiscoveredPColumn(parsed)
71
+ return isColumnDiscoveredKey(parsed)
59
72
  ? parsed
60
73
  : throwError("Parsed object is not a valid DiscoveredPColumn");
61
74
  } catch {
62
75
  throw new Error(
63
- "Invalid DiscoveredPColumnId: not a valid JSON or does not conform to DiscoveredPColumn structure",
76
+ "Invalid ColumnDiscoveredId: not a valid JSON or does not conform to DiscoveredPColumn structure",
64
77
  );
65
78
  }
66
79
  }
67
80
 
68
- export function stringifyDiscoveredPColumnId(id: DiscoveredPColumn) {
69
- return canonicalizeJson<DiscoveredPColumn>(
70
- distillDiscoveredPColumn(id),
71
- ) as string as DiscoveredPColumnId;
81
+ export function stringifyColumnDiscoveredId(
82
+ id: Omit<ColumnDiscoveredKey, "__isDiscovered">,
83
+ ): ColumnDiscoveredId {
84
+ return canonicalizeJson<ColumnDiscoveredKey>(
85
+ distillColumnDiscoveredKey({ __isDiscovered: true, ...id }),
86
+ ) as ColumnDiscoveredId;
72
87
  }
@@ -1,4 +1,8 @@
1
+ import { type Branded } from "@milaboratories/helpers";
2
+ import { type CanonicalizedJson, canonicalizeJson, parseJsonSafely } from "../../../json";
1
3
  import type { AnchoredPColumnId } from "./selectors";
4
+ import { ColumnUniversalId } from "./ids";
5
+ import type { PColumnSpec } from "./spec";
2
6
 
3
7
  /** Value of an axis filter */
4
8
  export type AxisFilterValue = number | string;
@@ -29,13 +33,75 @@ export type FilteredPColumn<CID = AnchoredPColumnId, AFI = AxisFilter> = {
29
33
  axisFilters: AFI[];
30
34
  };
31
35
 
36
+ /** @deprecated */
32
37
  export type FilteredPColumnId = FilteredPColumn<AnchoredPColumnId, AxisFilterByIdx>;
33
38
 
34
39
  /**
35
40
  * Checks if a given value is a FilteredPColumn
36
41
  * @param id - The value to check
37
42
  * @returns True if the value is a FilteredPColumn, false otherwise
43
+ * @deprecated use {@link isColumnFilteredKey} for the key form and `source` checks for the id form
38
44
  */
39
45
  export function isFilteredPColumn(id: unknown): id is FilteredPColumn {
40
46
  return typeof id === "object" && id !== null && "source" in id && "axisFilters" in id;
41
47
  }
48
+
49
+ /**
50
+ * `source` is either a leaf {@link PObjectId} or a {@link ColumnDiscoveredId}.
51
+ * Filtered never nests inside Filtered (flat-merge invariant), and Filtered is
52
+ * never the outer wrapper around Overridden — Overridden is always outermost.
53
+ */
54
+ export interface ColumnFilteredKey {
55
+ __isFiltered: true;
56
+ source: ColumnUniversalId;
57
+ axisFilters: AxisFilterByIdx[];
58
+ }
59
+
60
+ export type ColumnFilteredId = Branded<CanonicalizedJson<ColumnFilteredKey>, "ColumnFilteredId">;
61
+
62
+ export function stringifyColumnFilteredId(
63
+ key: Omit<ColumnFilteredKey, "__isFiltered">,
64
+ ): ColumnFilteredId {
65
+ return canonicalizeJson<ColumnFilteredKey>(createColumnFilteredKey(key)) as ColumnFilteredId;
66
+ }
67
+
68
+ export function isColumnFilteredKey(obj: unknown): obj is ColumnFilteredKey {
69
+ return typeof obj === "object" && obj !== null && "__isFiltered" in obj;
70
+ }
71
+
72
+ export function isColumnFilteredId(id: unknown): id is ColumnFilteredId {
73
+ if (typeof id !== "string") return false;
74
+ return isColumnFilteredKey(parseJsonSafely(id));
75
+ }
76
+
77
+ export function createColumnFilteredId(props: {
78
+ source: ColumnUniversalId;
79
+ axisFilters: AxisFilterByIdx[];
80
+ }): ColumnFilteredId {
81
+ return stringifyColumnFilteredId(props);
82
+ }
83
+
84
+ export function createColumnFilteredKey(props: {
85
+ source: ColumnUniversalId;
86
+ axisFilters: AxisFilterByIdx[];
87
+ }): ColumnFilteredKey {
88
+ // `axisFilters` must be ordered by axis index so logically
89
+ // identical filtered columns produce one canonical id.
90
+ const axisFilters = props.axisFilters.toSorted((a, b) => a[0] - b[0]);
91
+ return { __isFiltered: true, source: props.source, axisFilters };
92
+ }
93
+
94
+ /**
95
+ * Drop the axes pinned by `axisFilters` from a {@link PColumnSpec}'s
96
+ * `axesSpec`. Indices are positional against `spec.axesSpec`. Returns `spec`
97
+ * unchanged when there are no filters.
98
+ *
99
+ * Single source of the Filtered-layer spec math — shared by
100
+ * `reconstructSpecFromId` (host, id-walking) and `ColumnFilteredRecipe.getSpec`
101
+ * (sandbox, recipe-graph walking).
102
+ */
103
+ export function applyAxisFilters(spec: PColumnSpec, axisFilters: AxisFilterByIdx[]): PColumnSpec {
104
+ if (axisFilters.length === 0) return spec;
105
+ const fixed = new Set(axisFilters.map(([i]) => i));
106
+ return { ...spec, axesSpec: spec.axesSpec.filter((_, i) => !fixed.has(i)) };
107
+ }
@@ -1,34 +1,159 @@
1
- import type { Branded } from "../../../branding";
2
1
  import type { AnchoredPColumnId } from "./selectors";
3
- import type { FilteredPColumnId } from "./filtered_column";
4
- import canonicalize from "canonicalize";
5
- import type { PObjectId } from "../../../pool";
6
- import { DiscoveredPColumn } from "./discovered_column";
2
+ import {
3
+ applyAxisFilters,
4
+ isColumnFilteredKey,
5
+ type ColumnFilteredId,
6
+ type ColumnFilteredKey,
7
+ type FilteredPColumnId,
8
+ } from "./filtered_column";
9
+ import {
10
+ createPObjectId,
11
+ isPObjectId,
12
+ isPObjectKey,
13
+ LocalPObjectKey,
14
+ type GlobalPObjectId,
15
+ type GlobalPObjectKey,
16
+ type LocalPObjectId,
17
+ type PObjectId,
18
+ } from "../../../pool";
19
+ import {
20
+ isColumnDiscoveredKey,
21
+ type ColumnDiscoveredId,
22
+ type ColumnDiscoveredKey,
23
+ } from "./discovered_column";
24
+ import { throwError } from "@milaboratories/helpers";
25
+ import {
26
+ applySpecOverrides,
27
+ isColumnOverriddenKey,
28
+ type ColumnOverriddenId,
29
+ type ColumnOverriddenKey,
30
+ } from "./overridden";
31
+ import { canonicalizeJson } from "../../../json";
32
+ import { AxisSpec, PColumnSpec } from "./spec";
33
+ import { isString } from "es-toolkit";
34
+
35
+ /**
36
+ * Per-axis patches keyed by positional index in the base spec's `axesSpec`.
37
+ *
38
+ * Using position rather than `name` lets us disambiguate linker-style specs
39
+ * that carry multiple axes with the same `name` differentiated by `domain` /
40
+ * `contextDomain` (e.g. a `group`, `group/primary`, `group/secondary` triple).
41
+ *
42
+ * A patch at index `>= base.axesSpec.length` appends a new axis at that slot.
43
+ */
44
+ export type AxisPatches = Record<number, Partial<AxisSpec>>;
7
45
 
8
46
  /**
9
47
  * Universal column identifier optionally anchored and optionally filtered.
48
+ * @deprecated use {@link ColumnUniversalKey}
10
49
  */
11
- export type UniversalPColumnId = AnchoredPColumnId | FilteredPColumnId | DiscoveredPColumn;
50
+ export type UniversalPColumnId = AnchoredPColumnId | FilteredPColumnId;
12
51
 
13
52
  /**
14
53
  * Canonically serialized {@link UniversalPColumnId}.
54
+ * @deprecated use {@link ColumnUniversalId}
15
55
  */
16
- export type SUniversalPColumnId = Branded<PObjectId, "SUniversalPColumnId", "__pl_model_brand_2__">;
56
+ export type SUniversalPColumnId = ColumnUniversalId;
57
+ // export type SUniversalPColumnId = Branded<PObjectId, "SUniversalPColumnId", "__pl_model_brand_2__">;
58
+
59
+ export type ColumnUniversalKey =
60
+ | LocalPObjectKey
61
+ | GlobalPObjectKey
62
+ | ColumnFilteredKey
63
+ | ColumnDiscoveredKey
64
+ | ColumnOverriddenKey;
65
+
66
+ export type ColumnUniversalId =
67
+ | LocalPObjectId
68
+ | GlobalPObjectId
69
+ | ColumnFilteredId
70
+ | ColumnDiscoveredId
71
+ | ColumnOverriddenId;
17
72
 
18
73
  /**
19
- * Canonically serializes a {@link UniversalPColumnId} to a string.
20
- * @param id - The column identifier to serialize
21
- * @returns The canonically serialized string
74
+ * Canonically serializes a column key to a branded string id. Accepts both
75
+ * the new {@link ColumnUniversalKey} and the deprecated {@link UniversalPColumnId}
76
+ * (anchored / old filtered object form).
22
77
  */
23
- export function stringifyColumnId(id: UniversalPColumnId): SUniversalPColumnId {
24
- return canonicalize(id)! as SUniversalPColumnId;
78
+ export function stringifyColumnId(id: ColumnUniversalKey | UniversalPColumnId): ColumnUniversalId {
79
+ return canonicalizeJson(id) as ColumnUniversalId;
80
+ }
81
+
82
+ /**
83
+ * Parses a canonically serialized column id back to its key form.
84
+ */
85
+ export function parseColumnId(str: ColumnUniversalId): ColumnUniversalKey {
86
+ return JSON.parse(str) as ColumnUniversalKey;
87
+ }
88
+
89
+ export function parseColumnIdSafely(
90
+ str: ColumnUniversalId,
91
+ fallback = undefined,
92
+ ): ColumnUniversalKey | typeof fallback {
93
+ try {
94
+ return JSON.parse(str) as ColumnUniversalKey;
95
+ } catch {
96
+ return fallback;
97
+ }
25
98
  }
26
99
 
27
100
  /**
28
- * Parses a canonically serialized {@link UniversalPColumnId} from a string.
29
- * @param str - The string to parse
30
- * @returns The parsed column identifier
101
+ * Walk a rich column id down to its terminal leaf {@link PObjectId}.
31
102
  */
32
- export function parseColumnId(str: SUniversalPColumnId): UniversalPColumnId {
33
- return JSON.parse(str) as UniversalPColumnId;
103
+ export function extractPObjectId(id: ColumnUniversalId | ColumnUniversalKey): PObjectId {
104
+ if (isString(id)) {
105
+ if (isPObjectId(id)) return id;
106
+
107
+ const parsed =
108
+ parseColumnIdSafely(id) ??
109
+ throwError(`extractPObjectId: id "${id}" is not a valid canonical column id`);
110
+ return extractPObjectId(parsed);
111
+ }
112
+
113
+ if (isPObjectKey(id)) return createPObjectId(id);
114
+ if (isColumnFilteredKey(id)) return extractPObjectId(id.source);
115
+ if (isColumnOverriddenKey(id)) return extractPObjectId(id.source);
116
+ if (isColumnDiscoveredKey(id)) return extractPObjectId(id.column);
117
+
118
+ throw new Error(`extractPObjectId: unrecognized column id structure: ${JSON.stringify(id)}`);
119
+ }
120
+
121
+ /**
122
+ * Reconstruct the effective {@link PColumnSpec} for a rich column id by walking
123
+ * the id chain from leaf to outermost wrapper, applying each layer's spec
124
+ * transformation in the same order the corresponding recipe would.
125
+ *
126
+ * Layer semantics:
127
+ * - Leaf ({@link LocalPObjectKey} / {@link GlobalPObjectKey}): no transformation.
128
+ * - {@link ColumnDiscoveredKey}: pass-through, descends into `column`.
129
+ * - {@link ColumnFilteredKey}: drops the axes whose positional index appears in
130
+ * `axisFilters[i][0]` from the inner spec's `axesSpec` — mirrors
131
+ * `ColumnFilteredRecipe.getSpec()`.
132
+ * - {@link ColumnOverriddenKey}: applies `specOverrides` via
133
+ * {@link applySpecOverrides} on top of the inner spec.
134
+ */
135
+ export function reconstructSpecFromId(
136
+ baseSpec: PColumnSpec,
137
+ id: ColumnUniversalId | ColumnUniversalKey,
138
+ ): PColumnSpec {
139
+ if (isString(id)) {
140
+ if (isPObjectId(id)) return baseSpec;
141
+
142
+ const parsed =
143
+ parseColumnIdSafely(id) ??
144
+ throwError(`reconstructSpecFromId: id "${id}" is not a valid canonical column id`);
145
+ return reconstructSpecFromId(baseSpec, parsed);
146
+ }
147
+
148
+ if (isPObjectKey(id)) return baseSpec;
149
+ if (isColumnDiscoveredKey(id)) return reconstructSpecFromId(baseSpec, id.column);
150
+ if (isColumnFilteredKey(id)) {
151
+ return applyAxisFilters(reconstructSpecFromId(baseSpec, id.source), id.axisFilters);
152
+ }
153
+ if (isColumnOverriddenKey(id)) {
154
+ const inner = reconstructSpecFromId(baseSpec, id.source);
155
+ return applySpecOverrides(inner, id.specOverrides);
156
+ }
157
+
158
+ throw new Error(`reconstructSpecFromId: unrecognized column id structure: ${JSON.stringify(id)}`);
34
159
  }
@@ -5,3 +5,4 @@ export * from "./spec";
5
5
  export * from "./selectors";
6
6
  export * from "./native_id";
7
7
  export * from "./discovered_column";
8
+ export * from "./overridden";
@@ -0,0 +1,285 @@
1
+ import { type Branded, throwError, type Mutable, Nil } from "@milaboratories/helpers";
2
+ import { type CanonicalizedJson, canonicalizeJson } from "../../../json";
3
+ import { AxisPatches, type ColumnUniversalId, parseColumnIdSafely } from "./ids";
4
+ import type { AxisSpec, PColumnSpec } from "./spec";
5
+ import { isNil, isString } from "es-toolkit";
6
+
7
+ export type SpecOverrides = Pick<PColumnSpec, "domain" | "contextDomain" | "annotations"> & {
8
+ axesSpec?: AxisPatches;
9
+ };
10
+
11
+ /**
12
+ * `source` can reference a leaf or a Filtered/Discovered id, but never another
13
+ * Overridden id — there is no `Overridden<Overridden<...>>`. Repeated overrides
14
+ * merge at the outer wrapper via {@link mergeSpecOverrides}.
15
+ */
16
+ export interface ColumnOverriddenKey {
17
+ __isOverridden: true;
18
+ source: Exclude<ColumnUniversalId, ColumnOverriddenId>;
19
+ specOverrides: SpecOverrides;
20
+ }
21
+
22
+ export type ColumnOverriddenId = Branded<
23
+ CanonicalizedJson<ColumnOverriddenKey>,
24
+ "ColumnOverriddenId"
25
+ >;
26
+
27
+ export function isColumnOverriddenKey(obj: unknown): obj is ColumnOverriddenKey {
28
+ return typeof obj === "object" && obj !== null && "__isOverridden" in obj;
29
+ }
30
+
31
+ export function distillColumnOverriddenKey(props: ColumnOverriddenKey): ColumnOverriddenKey {
32
+ return {
33
+ __isOverridden: true,
34
+ source: props.source,
35
+ specOverrides: props.specOverrides,
36
+ };
37
+ }
38
+
39
+ export function createColumnOverriddenId(props: {
40
+ source: ColumnUniversalId;
41
+ specOverrides: SpecOverrides;
42
+ }): ColumnOverriddenId {
43
+ return stringifyColumnOverriddenId(createColumnOverriddenKey(props));
44
+ }
45
+
46
+ export function createColumnOverriddenKey(props: {
47
+ source: ColumnUniversalId;
48
+ specOverrides: SpecOverrides;
49
+ }): ColumnOverriddenKey {
50
+ const { source, specOverrides } = props;
51
+ const unwrapped = unwrapOverrides(source);
52
+ const baseSource = unwrapped
53
+ ? unwrapped.source
54
+ : (source as Exclude<ColumnUniversalId, ColumnOverriddenId>);
55
+ const mergedOverrides = unwrapped
56
+ ? mergeSpecOverrides(unwrapped.specOverrides, specOverrides)
57
+ : specOverrides;
58
+
59
+ return {
60
+ __isOverridden: true,
61
+ source: baseSource,
62
+ specOverrides: mergedOverrides,
63
+ };
64
+ }
65
+
66
+ export function parseColumnOverriddenId(id: ColumnUniversalId): ColumnOverriddenKey {
67
+ try {
68
+ const parsed = JSON.parse(id);
69
+ return isColumnOverriddenKey(parsed)
70
+ ? parsed
71
+ : throwError("Parsed object is not a valid OverriddenPColumn");
72
+ } catch {
73
+ throw new Error(
74
+ "Invalid ColumnOverriddenId: not a valid JSON or does not conform to OverriddenPColumn structure",
75
+ );
76
+ }
77
+ }
78
+
79
+ export function stringifyColumnOverriddenId(id: ColumnOverriddenKey): ColumnOverriddenId {
80
+ return canonicalizeJson<ColumnOverriddenKey>(
81
+ distillColumnOverriddenKey(id),
82
+ ) as ColumnOverriddenId;
83
+ }
84
+
85
+ /**
86
+ * Peel one override-wrap layer.
87
+ *
88
+ * Invariant: `{source, specOverrides}` can only appear at the top level —
89
+ * the inner `source` is never itself an override-wrap. Anything else throws.
90
+ */
91
+ export function unwrapOverrides(id: ColumnUniversalId): ColumnOverriddenKey | undefined {
92
+ const parsed = parseColumnIdSafely(id);
93
+ if (parsed === undefined || !isColumnOverriddenKey(parsed)) return undefined;
94
+ const inner = parseColumnIdSafely(parsed.source);
95
+ if (inner !== undefined && isColumnOverriddenKey(inner)) {
96
+ throw new Error("nested override-wrap detected — invariant broken");
97
+ }
98
+ return parsed;
99
+ }
100
+
101
+ /**
102
+ * Diff two specs into a {@link SpecOverrides} patch that, when applied on top
103
+ * of `base`, reconstructs `next`.
104
+ *
105
+ * - For top-level `annotations` / `domain` / `contextDomain`: include keys
106
+ * whose value in `next` differs from `base` (or is missing in `base`).
107
+ * Keys present in `base` but missing in `next` are not emitted — overrides
108
+ * merge, they cannot delete.
109
+ * - For `axesSpec`: matched by **positional index**. Each `next[i]` is diffed
110
+ * against `base[i]`; only changed fields are emitted as a patch under key
111
+ * `i`. Indices past `base.length` carry the full new axis (append).
112
+ */
113
+ export function deriveSpecDelta(base: PColumnSpec, next: PColumnSpec): SpecOverrides {
114
+ const annotations = recordDelta(base.annotations, next.annotations);
115
+ const domain = recordDelta(base.domain, next.domain);
116
+ const contextDomain = recordDelta(base.contextDomain, next.contextDomain);
117
+ const axesSpec = deriveAxesDelta(base.axesSpec, next.axesSpec);
118
+ return {
119
+ ...(axesSpec && { axesSpec }),
120
+ ...(annotations && { annotations }),
121
+ ...(domain && { domain }),
122
+ ...(contextDomain && { contextDomain }),
123
+ };
124
+ }
125
+
126
+ export function isEmptySpecDelta(delta: SpecOverrides): boolean {
127
+ return (
128
+ (!delta.annotations || Object.keys(delta.annotations).length === 0) &&
129
+ (!delta.domain || Object.keys(delta.domain).length === 0) &&
130
+ (!delta.contextDomain || Object.keys(delta.contextDomain).length === 0) &&
131
+ (!delta.axesSpec || Object.keys(delta.axesSpec).length === 0)
132
+ );
133
+ }
134
+
135
+ function deriveAxesDelta(
136
+ base: readonly AxisSpec[],
137
+ next: readonly AxisSpec[],
138
+ ): AxisPatches | undefined {
139
+ const out: AxisPatches = {};
140
+ let any = false;
141
+ for (let i = 0; i < next.length; i++) {
142
+ const a = next[i];
143
+ const b = base[i];
144
+ if (b === undefined) {
145
+ out[i] = a;
146
+ any = true;
147
+ continue;
148
+ }
149
+ const nameChanged = a.name !== b.name;
150
+ const typeChanged = a.type !== b.type;
151
+ const annotations = recordDelta(b.annotations, a.annotations);
152
+ const domain = recordDelta(b.domain, a.domain);
153
+ const contextDomain = recordDelta(b.contextDomain, a.contextDomain);
154
+ if (!nameChanged && !typeChanged && !annotations && !domain && !contextDomain) continue;
155
+ out[i] = {
156
+ ...(nameChanged && { name: a.name }),
157
+ ...(typeChanged && { type: a.type }),
158
+ ...(annotations && { annotations }),
159
+ ...(domain && { domain }),
160
+ ...(contextDomain && { contextDomain }),
161
+ };
162
+ any = true;
163
+ }
164
+ return any ? out : undefined;
165
+ }
166
+
167
+ function recordDelta(
168
+ base: Record<string, string> | undefined,
169
+ next: Record<string, string> | undefined,
170
+ ): Record<string, string> | undefined {
171
+ if (!next) return undefined;
172
+ let any = false;
173
+ const diff: Record<string, string> = {};
174
+ for (const [k, v] of Object.entries(next)) {
175
+ if (!base || base[k] !== v) {
176
+ diff[k] = v;
177
+ any = true;
178
+ }
179
+ }
180
+ return any ? diff : undefined;
181
+ }
182
+
183
+ /**
184
+ * Apply `specOverrides` from a rich id (`SUniversalPColumnId`) on top of a
185
+ * resolved {@link PColumnSpec}. Returns `base` unchanged when the id carries
186
+ * no overrides.
187
+ */
188
+ export function applySpecOverrides(
189
+ base: PColumnSpec,
190
+ idOrOverride: Nil | SpecOverrides | ColumnUniversalId,
191
+ ): PColumnSpec {
192
+ if (isNil(idOrOverride)) {
193
+ return base;
194
+ }
195
+
196
+ const overrides = isString(idOrOverride)
197
+ ? unwrapOverrides(idOrOverride)?.specOverrides
198
+ : idOrOverride;
199
+
200
+ if (isNil(overrides)) {
201
+ return base;
202
+ }
203
+
204
+ const result = { ...base };
205
+ if (overrides.annotations)
206
+ result.annotations = mergeRecord(base.annotations, overrides.annotations);
207
+ if (overrides.domain) result.domain = mergeRecord(base.domain, overrides.domain);
208
+ if (overrides.contextDomain)
209
+ result.contextDomain = mergeRecord(base.contextDomain, overrides.contextDomain);
210
+ if (overrides.axesSpec) result.axesSpec = applyAxesPatches(base.axesSpec, overrides.axesSpec);
211
+ return result;
212
+ }
213
+
214
+ /**
215
+ * Compose two override patches: applying `mergeSpecOverrides(prior, next)` on
216
+ * top of a base spec produces the same result as applying `prior` then `next`.
217
+ */
218
+ export function mergeSpecOverrides(prior: SpecOverrides, next: SpecOverrides): SpecOverrides {
219
+ const result: Mutable<SpecOverrides> = {};
220
+ const axesSpec = mergeAxesPatches(prior.axesSpec, next.axesSpec);
221
+ if (axesSpec !== undefined) result.axesSpec = axesSpec;
222
+ const annotations = mergeRecord(prior.annotations, next.annotations);
223
+ if (annotations !== undefined) result.annotations = annotations;
224
+ const domain = mergeRecord(prior.domain, next.domain);
225
+ if (domain !== undefined) result.domain = domain;
226
+ const contextDomain = mergeRecord(prior.contextDomain, next.contextDomain);
227
+ if (contextDomain !== undefined) result.contextDomain = contextDomain;
228
+ return result;
229
+ }
230
+
231
+ /**
232
+ * Apply positional `patches` onto `base.axesSpec`. Patches deep-merge
233
+ * `annotations` / `domain` / `contextDomain` and shallow-override `name` /
234
+ * `type` at their slot. Indices `>= base.length` append new axes; intermediate
235
+ * gaps (if any) are filled with the patch itself, so callers should not leave
236
+ * holes between base.length and the highest patched index.
237
+ */
238
+ function applyAxesPatches(base: readonly AxisSpec[], patches: AxisPatches): AxisSpec[] {
239
+ const result: AxisSpec[] = base.slice();
240
+ for (const [k, p] of Object.entries(patches)) {
241
+ const i = Number(k);
242
+ const existing = result[i];
243
+ result[i] = (existing === undefined ? p : mergeAxisPatch(existing, p)) as AxisSpec;
244
+ }
245
+ return result;
246
+ }
247
+
248
+ /**
249
+ * Compose two positional patch maps: `mergeAxesPatches(prior, next)` applied to
250
+ * a base spec must equal applying `prior` then `next`. Patches sharing an
251
+ * index deep-merge field-by-field.
252
+ */
253
+ function mergeAxesPatches(
254
+ prior: AxisPatches | undefined,
255
+ next: AxisPatches | undefined,
256
+ ): AxisPatches | undefined {
257
+ if (!prior || Object.keys(prior).length === 0) return next;
258
+ if (!next || Object.keys(next).length === 0) return prior;
259
+ const result: AxisPatches = { ...prior };
260
+ for (const [k, p] of Object.entries(next)) {
261
+ const i = Number(k);
262
+ const existing = result[i];
263
+ result[i] = existing === undefined ? p : mergeAxisPatch(existing, p);
264
+ }
265
+ return result;
266
+ }
267
+
268
+ function mergeAxisPatch<A extends Partial<AxisSpec>>(axis: A, patch: Partial<AxisSpec>): A {
269
+ const result: Mutable<A> = { ...axis, ...patch };
270
+ if (patch.annotations) result.annotations = mergeRecord(axis.annotations, patch.annotations);
271
+ if (patch.domain) result.domain = mergeRecord(axis.domain, patch.domain);
272
+ if (patch.contextDomain) {
273
+ result.contextDomain = mergeRecord(axis.contextDomain, patch.contextDomain);
274
+ }
275
+ return result;
276
+ }
277
+
278
+ function mergeRecord(
279
+ a: Record<string, string> | undefined,
280
+ b: Record<string, string> | undefined,
281
+ ): Record<string, string> | undefined {
282
+ if (!b) return a;
283
+ if (!a) return b;
284
+ return { ...a, ...b };
285
+ }
@@ -64,6 +64,8 @@ export interface SingleAxisSelector {
64
64
  type?: AxisValueType;
65
65
  /** Domain requirements (optional) */
66
66
  domain?: Domain;
67
+ /** Context-domain requirements (optional) */
68
+ contextDomain?: Domain;
67
69
  /** Parent axes requirements (optional) */
68
70
  parentAxes?: SingleAxisSelector[];
69
71
  }
@@ -170,6 +172,7 @@ export interface PColumnSelector extends AnchoredPColumnSelector {
170
172
  /**
171
173
  * Strict identifier for PColumns in an anchored context
172
174
  * Unlike APColumnMatcher, this requires exact matches on domain and axes
175
+ * @deprecated This is part of the legacy column matching API. The new Columns API (see sdk/model/src/columns/) now handles column and axis
173
176
  */
174
177
  export interface AnchoredPColumnId extends AnchoredPColumnSelector {
175
178
  /** Name is required for exact column identification */