@milaboratories/pl-model-common 1.19.5 → 1.19.6

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.
@@ -0,0 +1,182 @@
1
+ import {
2
+ Annotation,
3
+ AxisSpec,
4
+ AxisSpecNormalized,
5
+ getAxisId,
6
+ ValueType,
7
+ getDenormalizedAxesList,
8
+ getNormalizedAxesList,
9
+ canonicalizeAxisWithParents,
10
+ } from './spec';
11
+ import { canonicalizeJson, stringifyJson } from '../../../json'
12
+ import {
13
+ describe,
14
+ expect,
15
+ test,
16
+ } from 'vitest';
17
+
18
+ function makeTestAxis(params: {
19
+ name: string;
20
+ parents?: AxisSpec[];
21
+ }): AxisSpec {
22
+ return {
23
+ type: ValueType.Int,
24
+ name: params.name,
25
+ annotations: {
26
+ [Annotation.Label]: `${params.name} axis`,
27
+ ...(params.parents && params.parents.length > 0
28
+ ? { [Annotation.Parents]: stringifyJson(params.parents) }
29
+ : {}
30
+ ),
31
+ } satisfies Annotation,
32
+ };
33
+ }
34
+
35
+ function makeTestAxisWithParentIdxs(params: {
36
+ name: string;
37
+ parents?: number[];
38
+ }): AxisSpec {
39
+ return {
40
+ type: ValueType.Int,
41
+ name: params.name,
42
+ parentAxes: params.parents
43
+ };
44
+ }
45
+
46
+ describe('Linker columns', () => {
47
+ test('Normalization of axes with parents in indexes/annotations', () => {
48
+ function compareAxesLists(list1:AxisSpecNormalized[], list2:AxisSpecNormalized[]) {
49
+ list1.forEach((el1, idx) => {
50
+ const el2 = list2[idx];
51
+ const id1 = canonicalizeJson(getAxisId(el1));
52
+ const id2 = canonicalizeJson(getAxisId(el2));
53
+ expect(id1).toEqual(id2);
54
+
55
+ const parents1 = canonicalizeJson(el1.parentAxesSpec.map(getAxisId));
56
+ const parents2 = canonicalizeJson(el2.parentAxesSpec.map(getAxisId));
57
+ expect(parents1).toEqual(parents2);
58
+ })
59
+ }
60
+
61
+ // case 1
62
+ const normalized1 = getNormalizedAxesList([
63
+ makeTestAxisWithParentIdxs({ name: 'd' }),
64
+ makeTestAxisWithParentIdxs({ name: 'c', parents: [0] }), // parent D
65
+ makeTestAxisWithParentIdxs({ name: 'b' }),
66
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [2] }) // parent B
67
+ ])
68
+
69
+ const axisD1 = makeTestAxis({ name: 'd' });
70
+ const axisC1 = makeTestAxis({ name: 'c', parents: [axisD1] });
71
+ const axisB1 = makeTestAxis({ name: 'b' });
72
+ const axisA1 = makeTestAxis({ name: 'a', parents: [axisB1] });
73
+ const normalized2 = getNormalizedAxesList([axisD1, axisC1, axisB1, axisA1]);
74
+
75
+ compareAxesLists(normalized1, normalized2);
76
+
77
+ // case 2
78
+ const normalized3 = getNormalizedAxesList([
79
+ makeTestAxisWithParentIdxs({ name: 'd' }),
80
+ makeTestAxisWithParentIdxs({ name: 'c' }),
81
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [0] }), // parent D
82
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [1, 2] }) // parents B C
83
+ ])
84
+
85
+ const axisD2 = makeTestAxis({ name: 'd' });
86
+ const axisC2 = makeTestAxis({ name: 'c' });
87
+ const axisB2 = makeTestAxis({ name: 'b', parents: [axisD2] });
88
+ const axisA2 = makeTestAxis({ name: 'a', parents: [axisB2, axisC2] });
89
+ const normalized4 = getNormalizedAxesList([axisD2, axisC2, axisB2, axisA2]);
90
+
91
+ compareAxesLists(normalized3, normalized4);
92
+
93
+ // case 3
94
+
95
+ const normalized5 = getNormalizedAxesList([
96
+ makeTestAxisWithParentIdxs({ name: 'e' }),
97
+ makeTestAxisWithParentIdxs({ name: 'd' }),
98
+ makeTestAxisWithParentIdxs({ name: 'c', parents: [1] }), // parent D
99
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [2] }), // parent C
100
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [3] }) // parent B
101
+ ])
102
+
103
+ const axisE3 = makeTestAxis({ name: 'e' });
104
+ const axisD3 = makeTestAxis({ name: 'd' });
105
+ const axisC3 = makeTestAxis({ name: 'c', parents: [axisD3]});
106
+ const axisB3 = makeTestAxis({ name: 'b', parents: [axisC3] });
107
+ const axisA3 = makeTestAxis({ name: 'a', parents: [axisB3] });
108
+ const normalized6 = getNormalizedAxesList([axisE3, axisD3, axisC3, axisB3, axisA3]);
109
+
110
+ compareAxesLists(normalized5, normalized6);
111
+ })
112
+
113
+ test('Sort parents during normalization - idxs', () => {
114
+ const sourceAxesList1 = [
115
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [2, 3] }),
116
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [3, 2] }),
117
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [4] }),
118
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [5] }),
119
+ makeTestAxisWithParentIdxs({ name: 'c1' }),
120
+ makeTestAxisWithParentIdxs({ name: 'c2' }),
121
+ ];
122
+ const normalized1 = getNormalizedAxesList(sourceAxesList1);
123
+ const a1 = normalized1[0];
124
+ const a2 = normalized1[1];
125
+ expect(canonicalizeAxisWithParents(a1)).toEqual(canonicalizeAxisWithParents(a2));
126
+
127
+
128
+ const axisC1 = makeTestAxis({ name: 'c1' });
129
+ const axisC2 = makeTestAxis({ name: 'c2' });
130
+ const axisB1 = makeTestAxis({ name: 'b', parents: [axisC1] });
131
+ const axisB2 = makeTestAxis({ name: 'b', parents: [axisC2] });
132
+ const sourceAxesList2 = [
133
+ makeTestAxis({ name: 'a', parents: [axisB1, axisB2] }),
134
+ makeTestAxis({ name: 'a', parents: [axisB2, axisB1] }),
135
+ axisB1,
136
+ axisB2,
137
+ axisC1,
138
+ axisC2
139
+ ];
140
+ const normalized2 = getNormalizedAxesList(sourceAxesList2);
141
+ const a3 = normalized2[0];
142
+ const a4 = normalized2[1];
143
+ expect(canonicalizeAxisWithParents(a3)).toEqual(canonicalizeAxisWithParents(a4));
144
+
145
+ expect(canonicalizeAxisWithParents(a1)).toEqual(canonicalizeAxisWithParents(a3));
146
+ expect(canonicalizeAxisWithParents(a1)).toEqual(canonicalizeAxisWithParents(a4));
147
+ })
148
+
149
+ test('Denormalization of axes list', () => {
150
+ const sourceAxesList = [
151
+ makeTestAxisWithParentIdxs({ name: 'e' }),
152
+ makeTestAxisWithParentIdxs({ name: 'd' }),
153
+ makeTestAxisWithParentIdxs({ name: 'c', parents: [1] }), // parent D
154
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [2] }), // parent C
155
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [3] }) // parent B
156
+ ];
157
+ const normalized = getNormalizedAxesList(sourceAxesList);
158
+ const denormalized = getDenormalizedAxesList(normalized);
159
+
160
+ sourceAxesList.forEach((el1, idx) => {
161
+ const el2 = denormalized[idx];
162
+ const id1 = canonicalizeJson(getAxisId(el1));
163
+ const id2 = canonicalizeJson(getAxisId(el2));
164
+ expect(id1).toEqual(id2);
165
+ expect(el1.parentAxes).toEqual(el2.parentAxes);
166
+ });
167
+ })
168
+
169
+ test('Remove parent cycles from normalized axes', () => {
170
+ const sourceAxesList = [
171
+ makeTestAxisWithParentIdxs({ name: 'a', parents: [1] }),
172
+ makeTestAxisWithParentIdxs({ name: 'b', parents: [2] }),
173
+ makeTestAxisWithParentIdxs({ name: 'c', parents: [0] })
174
+ ];
175
+ const normalized = getNormalizedAxesList(sourceAxesList);
176
+
177
+ const [a, b, c] = normalized;
178
+ expect(a.parentAxesSpec.length).toBe(0);
179
+ expect(b.parentAxesSpec.length).toBe(0);
180
+ expect(c.parentAxesSpec.length).toBe(0);
181
+ })
182
+ });
@@ -1,21 +1,216 @@
1
- import type { PObject, PObjectId, PObjectSpec } from '../../../pool';
2
- import canonicalize from 'canonicalize';
3
-
4
- export type ValueTypeInt = 'Int';
5
- export type ValueTypeLong = 'Long';
6
- export type ValueTypeFloat = 'Float';
7
- export type ValueTypeDouble = 'Double';
8
- export type ValueTypeString = 'String';
9
- export type ValueTypeBytes = 'Bytes';
1
+ import { ensureError } from '../../../errors';
2
+ import {
3
+ canonicalizeJson,
4
+ type CanonicalizedJson,
5
+ type StringifiedJson,
6
+ } from '../../../json';
7
+ import type {
8
+ PObject,
9
+ PObjectId,
10
+ PObjectSpec,
11
+ } from '../../../pool';
12
+ import { z } from 'zod';
13
+
14
+ type Expect<T extends true> = T;
15
+ type Equal<X, Y> =
16
+ (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;
17
+
18
+ export const ValueType = {
19
+ Int: 'Int',
20
+ Long: 'Long',
21
+ Float: 'Float',
22
+ Double: 'Double',
23
+ String: 'String',
24
+ Bytes: 'Bytes',
25
+ } as const;
10
26
 
11
27
  /** PFrame columns and axes within them may store one of these types. */
12
- export type ValueType =
13
- | ValueTypeInt
14
- | ValueTypeLong
15
- | ValueTypeFloat
16
- | ValueTypeDouble
17
- | ValueTypeString
18
- | ValueTypeBytes;
28
+ export type ValueType = (typeof ValueType)[keyof typeof ValueType];
29
+
30
+ export type Metadata = Record<string, string>;
31
+
32
+ export function readMetadata<U extends Metadata, T extends keyof U = keyof U>(
33
+ metadata: Metadata | undefined,
34
+ key: T,
35
+ ): U[T] | undefined {
36
+ return (metadata as U | undefined)?.[key];
37
+ }
38
+
39
+ type MetadataJsonImpl<M> = {
40
+ [P in keyof M as (M[P] extends StringifiedJson ? P : never)]: M[P] extends StringifiedJson<infer U> ? z.ZodType<U> : never;
41
+ };
42
+ export type MetadataJson<M> = MetadataJsonImpl<Required<M>>;
43
+
44
+ export function readMetadataJsonOrThrow<M extends Metadata, T extends keyof MetadataJson<M>>(
45
+ metadata: Metadata | undefined,
46
+ metadataJson: MetadataJson<M>,
47
+ key: T,
48
+ methodNameInError: string = 'readMetadataJsonOrThrow',
49
+ ): z.infer<MetadataJson<M>[T]> | undefined {
50
+ const json = readMetadata<M, T>(metadata, key);
51
+ if (json === undefined) return undefined;
52
+
53
+ const schema = metadataJson[key];
54
+ try {
55
+ const value = JSON.parse(json);
56
+ return schema.parse(value);
57
+ } catch (error: unknown) {
58
+ throw new Error(
59
+ `${methodNameInError} failed, `
60
+ + `key: ${String(key)}, `
61
+ + `value: ${json}, `
62
+ + `error: ${ensureError(error)}`,
63
+ );
64
+ }
65
+ }
66
+
67
+ export function readMetadataJson<M extends Metadata, T extends keyof MetadataJson<M>>(
68
+ metadata: Metadata | undefined,
69
+ metadataJson: MetadataJson<M>,
70
+ key: T,
71
+ ): z.infer<MetadataJson<M>[T]> | undefined {
72
+ try {
73
+ return readMetadataJsonOrThrow(metadata, metadataJson, key);
74
+ } catch {
75
+ return undefined; // treat invalid values as unset
76
+ }
77
+ }
78
+
79
+ /// Well-known domains
80
+ export const Domain = {
81
+ Alphabet: 'pl7.app/alphabet',
82
+ BlockId: 'pl7.app/blockId',
83
+ } as const;
84
+
85
+ export type Domain = Metadata & Partial<{
86
+ [Domain.Alphabet]: 'nucleotide' | 'aminoacid' | string;
87
+ [Domain.BlockId]: string;
88
+ }>;
89
+
90
+ export type DomainJson = MetadataJson<Domain>;
91
+ export const DomainJson: DomainJson = {};
92
+
93
+ /// Helper function for reading plain domain values
94
+ export function readDomain<T extends keyof Domain>(
95
+ spec: { domain?: Metadata | undefined } | undefined,
96
+ key: T,
97
+ ): Domain[T] | undefined {
98
+ return readMetadata<Domain, T>(spec?.domain, key);
99
+ }
100
+
101
+ /// Helper function for reading json-encoded domain values, throws on JSON parsing error
102
+ export function readDomainJsonOrThrow<T extends keyof DomainJson>(
103
+ spec: { domain?: Metadata | undefined } | undefined,
104
+ key: T,
105
+ ): z.infer<DomainJson[T]> | undefined {
106
+ return readMetadataJsonOrThrow<Domain, T>(spec?.domain, DomainJson, key, 'readDomainJsonOrThrow');
107
+ }
108
+
109
+ /// Helper function for reading json-encoded domain values, returns undefined on JSON parsing error
110
+ export function readDomainJson<T extends keyof DomainJson>(
111
+ spec: { domain?: Metadata | undefined } | undefined,
112
+ key: T,
113
+ ): z.infer<DomainJson[T]> | undefined {
114
+ return readMetadataJson<Domain, T>(spec?.domain, DomainJson, key);
115
+ }
116
+
117
+ /// Well-known annotations
118
+ export const Annotation = {
119
+ Alphabet: 'pl7.app/alphabet',
120
+ DiscreteValues: 'pl7.app/discreteValues',
121
+ Format: 'pl7.app/format',
122
+ Graph: {
123
+ IsVirtual: 'pl7.app/graph/isVirtual',
124
+ },
125
+ HideDataFromUi: 'pl7.app/hideDataFromUi',
126
+ IsLinkerColumn: 'pl7.app/isLinkerColumn',
127
+ Label: 'pl7.app/label',
128
+ Max: 'pl7.app/max',
129
+ Min: 'pl7.app/min',
130
+ Parents: 'pl7.app/parents',
131
+ Sequence: {
132
+ Annotation: {
133
+ Mapping: 'pl7.app/sequence/annotation/mapping',
134
+ },
135
+ IsAnnotation: 'pl7.app/sequence/isAnnotation',
136
+ },
137
+ Table: {
138
+ FontFamily: 'pl7.app/table/fontFamily',
139
+ OrderPriority: 'pl7.app/table/orderPriority',
140
+ Visibility: 'pl7.app/table/visibility',
141
+ },
142
+ Trace: 'pl7.app/trace',
143
+ } as const;
144
+
145
+ export type Annotation = Metadata & Partial<{
146
+ [Annotation.Alphabet]: 'nucleotide' | 'aminoacid' | string;
147
+ [Annotation.DiscreteValues]: StringifiedJson<number[]> | StringifiedJson<string[]>;
148
+ [Annotation.Format]: string;
149
+ [Annotation.Graph.IsVirtual]: StringifiedJson<boolean>;
150
+ [Annotation.HideDataFromUi]: StringifiedJson<boolean>;
151
+ [Annotation.IsLinkerColumn]: StringifiedJson<boolean>;
152
+ [Annotation.Label]: string;
153
+ [Annotation.Max]: StringifiedJson<number>;
154
+ [Annotation.Min]: StringifiedJson<number>;
155
+ [Annotation.Parents]: StringifiedJson<AxisSpec[]>;
156
+ [Annotation.Sequence.Annotation.Mapping]: StringifiedJson<Record<string, string>>;
157
+ [Annotation.Sequence.IsAnnotation]: StringifiedJson<boolean>;
158
+ [Annotation.Table.FontFamily]: string;
159
+ [Annotation.Table.OrderPriority]: StringifiedJson<number>;
160
+ [Annotation.Table.Visibility]: 'hidden' | 'optional' | string;
161
+ [Annotation.Trace]: StringifiedJson<Record<string, unknown>>;
162
+ }>;
163
+
164
+ export const AxisSpec = z.object({
165
+ type: z.nativeEnum(ValueType),
166
+ name: z.string(),
167
+ domain: z.record(z.string(), z.string()).optional(),
168
+ annotations: z.record(z.string(), z.string()).optional(),
169
+ parentAxes: z.array(z.number()).optional(),
170
+ }).passthrough();
171
+ type _test = Expect<Equal<
172
+ Readonly<z.infer<typeof AxisSpec>>,
173
+ Readonly<AxisSpec & Record<string, unknown>>
174
+ >>;
175
+
176
+ export type AnnotationJson = MetadataJson<Annotation>;
177
+ export const AnnotationJson: AnnotationJson = {
178
+ [Annotation.DiscreteValues]: z.array(z.string()).or(z.array(z.number())),
179
+ [Annotation.Graph.IsVirtual]: z.boolean(),
180
+ [Annotation.HideDataFromUi]: z.boolean(),
181
+ [Annotation.IsLinkerColumn]: z.boolean(),
182
+ [Annotation.Max]: z.number(),
183
+ [Annotation.Min]: z.number(),
184
+ [Annotation.Parents]: z.array(AxisSpec),
185
+ [Annotation.Sequence.Annotation.Mapping]: z.record(z.string(), z.string()),
186
+ [Annotation.Sequence.IsAnnotation]: z.boolean(),
187
+ [Annotation.Table.OrderPriority]: z.number(),
188
+ [Annotation.Trace]: z.record(z.string(), z.unknown()),
189
+ };
190
+
191
+ /// Helper function for reading plain annotation values
192
+ export function readAnnotation<T extends keyof Annotation>(
193
+ spec: { annotations?: Metadata | undefined } | undefined,
194
+ key: T,
195
+ ): Annotation[T] | undefined {
196
+ return readMetadata<Annotation, T>(spec?.annotations, key);
197
+ }
198
+
199
+ /// Helper function for reading json-encoded annotation values, throws on JSON parsing error
200
+ export function readAnnotationJsonOrThrow<T extends keyof AnnotationJson>(
201
+ spec: { annotations?: Metadata | undefined } | undefined,
202
+ key: T,
203
+ ): z.infer<AnnotationJson[T]> | undefined {
204
+ return readMetadataJsonOrThrow<Annotation, T>(spec?.annotations, AnnotationJson, key, 'readAnnotationJsonOrThrow');
205
+ }
206
+
207
+ /// Helper function for reading json-encoded annotation values, returns undefined on JSON parsing error
208
+ export function readAnnotationJson<T extends keyof AnnotationJson>(
209
+ spec: { annotations?: Metadata | undefined } | undefined,
210
+ key: T,
211
+ ): z.infer<AnnotationJson[T]> | undefined {
212
+ return readMetadataJson<Annotation, T>(spec?.annotations, AnnotationJson, key);
213
+ }
19
214
 
20
215
  /**
21
216
  * Specification of an individual axis.
@@ -61,9 +256,199 @@ export interface AxisSpec {
61
256
  readonly parentAxes?: number[];
62
257
  }
63
258
 
259
+ /** Parents are specs, not indexes; normalized axis can be used considering its parents independently from column */
260
+ export interface AxisSpecNormalized extends Omit<AxisSpec, 'parentAxes'> {
261
+ parentAxesSpec: AxisSpecNormalized[];
262
+ }
263
+
264
+ /** Tree: axis is a root, its parents are children */
265
+ export type AxisTree = {
266
+ axis: AxisSpecNormalized;
267
+ children: AxisTree[]; // parents
268
+ };
269
+
270
+ function makeAxisTree(axis: AxisSpecNormalized): AxisTree {
271
+ return { axis, children: [] };
272
+ }
273
+
274
+ /** Build tree by axis parents annotations */
275
+ export function getAxesTree(rootAxis: AxisSpecNormalized): AxisTree {
276
+ const root = makeAxisTree(rootAxis);
277
+ let nodesQ = [root];
278
+ while (nodesQ.length) {
279
+ const nextNodes: AxisTree[] = [];
280
+ for (const node of nodesQ) {
281
+ node.children = node.axis.parentAxesSpec.map(makeAxisTree);
282
+ nextNodes.push(...node.children);
283
+ }
284
+ nodesQ = nextNodes;
285
+ }
286
+ return root;
287
+ }
288
+
289
+ /** Get set of canonicalized axisIds from axisTree */
290
+ export function getSetFromAxisTree(tree: AxisTree): Set<CanonicalizedJson<AxisId>> {
291
+ const set = new Set([canonicalizeJson(getAxisId(tree.axis))]);
292
+ let nodesQ = [tree];
293
+ while (nodesQ.length) {
294
+ const nextNodes = [];
295
+ for (const node of nodesQ) {
296
+ for (const parent of node.children) {
297
+ set.add(canonicalizeJson(getAxisId(parent.axis)));
298
+ nextNodes.push(parent);
299
+ }
300
+ }
301
+ nodesQ = nextNodes;
302
+ }
303
+ return set;
304
+ }
305
+
306
+ /** Get array of axisSpecs from axisTree */
307
+ export function getArrayFromAxisTree(tree: AxisTree): AxisSpecNormalized[] {
308
+ const res = [tree.axis];
309
+ let nodesQ = [tree];
310
+ while (nodesQ.length) {
311
+ const nextNodes = [];
312
+ for (const node of nodesQ) {
313
+ for (const parent of node.children) {
314
+ res.push(parent.axis);
315
+ nextNodes.push(parent);
316
+ }
317
+ }
318
+ nodesQ = nextNodes;
319
+ }
320
+ return res;
321
+ }
322
+
323
+ export function canonicalizeAxisWithParents(axis: AxisSpecNormalized) {
324
+ return canonicalizeJson(getArrayFromAxisTree(getAxesTree(axis)).map(getAxisId));
325
+ }
326
+
327
+ function normalizingAxesComparator(axis1: AxisSpecNormalized, axis2: AxisSpecNormalized): 1 | -1 | 0 {
328
+ if (axis1.name !== axis2.name) {
329
+ return axis1.name < axis2.name ? 1 : -1;
330
+ }
331
+ if (axis1.type !== axis2.type) {
332
+ return axis1.type < axis2.type ? 1 : -1;
333
+ }
334
+ const domain1 = canonicalizeJson(axis1.domain ?? {});
335
+ const domain2 = canonicalizeJson(axis2.domain ?? {});
336
+ if (domain1 !== domain2) {
337
+ return domain1 < domain2 ? 1 : -1;
338
+ }
339
+
340
+ const parents1 = canonicalizeAxisWithParents(axis1);
341
+ const parents2 = canonicalizeAxisWithParents(axis2);
342
+
343
+ if (parents1 !== parents2) {
344
+ return parents1 < parents2 ? 1 : -1;
345
+ }
346
+
347
+ const annotation1 = canonicalizeJson(axis1.annotations ?? {});
348
+ const annotation2 = canonicalizeJson(axis2.annotations ?? {});
349
+ if (annotation1 !== annotation2) {
350
+ return annotation1 < annotation2 ? 1 : -1;
351
+ }
352
+ return 0;
353
+ }
354
+
355
+ function parseParentsFromAnnotations(axis: AxisSpec) {
356
+ const parentsList = readAnnotationJson(axis, Annotation.Parents);
357
+ if (parentsList === undefined) {
358
+ return [];
359
+ }
360
+ return parentsList;
361
+ }
362
+
363
+ function sortParentsDeep(axisSpec: AxisSpecNormalized) {
364
+ axisSpec.parentAxesSpec.forEach(sortParentsDeep);
365
+ axisSpec.parentAxesSpec.sort(normalizingAxesComparator);
366
+ }
367
+
368
+ function hasCycleOfParents(axisSpec: AxisSpecNormalized) {
369
+ const root = makeAxisTree(axisSpec);
370
+ let nodesQ = [root];
371
+ const ancestors = new Set(canonicalizeJson(getAxisId(axisSpec)));
372
+ while (nodesQ.length) {
373
+ const nextNodes: AxisTree[] = [];
374
+ const levelIds = new Set<CanonicalizedJson<AxisId>>();
375
+ for (const node of nodesQ) {
376
+ node.children = node.axis.parentAxesSpec.map(makeAxisTree);
377
+ for (const child of node.children) {
378
+ const childId = canonicalizeJson(getAxisId(child.axis));
379
+ if (!levelIds.has(childId)) {
380
+ nextNodes.push(child);
381
+ levelIds.add(childId);
382
+ if (ancestors.has(childId)) {
383
+ return true;
384
+ }
385
+ ancestors.add(childId);
386
+ }
387
+ }
388
+ }
389
+ nodesQ = nextNodes;
390
+ }
391
+ return false;
392
+ }
393
+
394
+ /** Create list of normalized axisSpec (parents are in array of specs, not indexes) */
395
+ export function getNormalizedAxesList(axes: AxisSpec[]): AxisSpecNormalized[] {
396
+ if (!axes.length) {
397
+ return [];
398
+ }
399
+ const modifiedAxes: AxisSpecNormalized[] = axes.map((axis) => {
400
+ const { parentAxes: _, ...copiedRest } = axis;
401
+ return { ...copiedRest, annotations: { ...copiedRest.annotations }, parentAxesSpec: [] };
402
+ });
403
+
404
+ axes.forEach((axis, idx) => {
405
+ const modifiedAxis = modifiedAxes[idx];
406
+ if (axis.parentAxes) { // if we have parents by indexes then take from the list
407
+ modifiedAxis.parentAxesSpec = axis.parentAxes.map((idx) => modifiedAxes[idx]);
408
+ } else { // else try to parse from annotation and normalize recursively
409
+ modifiedAxis.parentAxesSpec = getNormalizedAxesList(parseParentsFromAnnotations(axis));
410
+ delete modifiedAxis.annotations?.[Annotation.Parents];
411
+ }
412
+ });
413
+
414
+ if (modifiedAxes.some(hasCycleOfParents)) { // Axes list is broken
415
+ modifiedAxes.forEach((axis) => {
416
+ axis.parentAxesSpec = [];
417
+ });
418
+ } else {
419
+ modifiedAxes.forEach((axis) => {
420
+ sortParentsDeep(axis);
421
+ });
422
+ }
423
+
424
+ return modifiedAxes;
425
+ }
426
+
427
+ /** Create list of regular axisSpec from normalized (parents are indexes, inside of current axes list) */
428
+ export function getDenormalizedAxesList(axesSpec: AxisSpecNormalized[]): AxisSpec[] {
429
+ const idsList = axesSpec.map((axisSpec) => canonicalizeJson(getAxisId(axisSpec)));
430
+ return axesSpec.map((axisSpec) => {
431
+ const parentsIds = axisSpec.parentAxesSpec.map((axisSpec) => canonicalizeJson(getAxisId(axisSpec)));
432
+ const parentIdxs = parentsIds.map((id) => idsList.indexOf(id));
433
+ const { parentAxesSpec: _, ...copiedRest } = axisSpec;
434
+ if (parentIdxs.length) {
435
+ return { ...copiedRest, parentAxes: parentIdxs } as AxisSpec;
436
+ }
437
+ return copiedRest;
438
+ });
439
+ }
440
+
64
441
  /** Common type representing spec for all the axes in a column */
65
442
  export type AxesSpec = AxisSpec[];
66
443
 
444
+ /// Well-known column names
445
+ export const PColumnName = {
446
+ Label: 'pl7.app/label',
447
+ Table: {
448
+ RowSelection: 'pl7.app/table/row-selection',
449
+ },
450
+ } as const;
451
+
67
452
  /**
68
453
  * Full column specification including all axes specs and specs of the column
69
454
  * itself.
@@ -211,12 +596,9 @@ export function getAxesId(spec: AxesSpec): AxesId {
211
596
  return spec.map(getAxisId);
212
597
  }
213
598
 
214
- /**
215
- * Canonicalizes axis id
216
- * @deprecated Use {@link canonicalizeJson} instead to preserve type
217
- */
218
- export function canonicalizeAxisId(id: AxisId): string {
219
- return canonicalize(getAxisId(id))!;
599
+ /** Canonicalizes axis id */
600
+ export function canonicalizeAxisId(id: AxisId): CanonicalizedJson<AxisId> {
601
+ return canonicalizeJson(getAxisId(id));
220
602
  }
221
603
 
222
604
  /** Returns true if all domains from query are found in target */
package/src/json.ts CHANGED
@@ -9,14 +9,14 @@ type JsonValue = JsonPrimitive | JsonValue[] | {
9
9
  // eslint-disable-next-line @typescript-eslint/no-unsafe-function-type
10
10
  type NotAssignableToJson = bigint | symbol | Function;
11
11
 
12
- export type JsonCompatible<T> = unknown extends T ? never : {
12
+ export type JsonCompatible<T> = unknown extends T ? unknown : {
13
13
  [P in keyof T]:
14
14
  T[P] extends JsonValue ? T[P] :
15
15
  T[P] extends NotAssignableToJson ? never :
16
16
  JsonCompatible<T[P]>;
17
17
  };
18
18
 
19
- export type StringifiedJson<T> = JsonCompatible<T> extends never ? never : string & {
19
+ export type StringifiedJson<T = unknown> = JsonCompatible<T> extends never ? never : string & {
20
20
  __json_stringified: T;
21
21
  };
22
22
 
@@ -24,7 +24,7 @@ export function stringifyJson<T>(value: JsonCompatible<T>): StringifiedJson<T> {
24
24
  return JSON.stringify(value)! as StringifiedJson<T>;
25
25
  }
26
26
 
27
- export type CanonicalizedJson<T> = JsonCompatible<T> extends never ? never : string & {
27
+ export type CanonicalizedJson<T = unknown> = JsonCompatible<T> extends never ? never : string & {
28
28
  __json_canonicalized: T;
29
29
  };
30
30