@platforma-sdk/model 1.40.1 → 1.40.5

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,679 @@
1
+ import type { SUniversalPColumnId } from '@milaboratories/pl-model-common';
2
+ import type { AnnotationFilter, AnnotationMode, AnnotationScript, IsNA, NotFilter, NumericalComparisonFilter, PatternFilter, PatternPredicate, ValueRank } from './filter';
3
+ import type { SimplifiedPColumnSpec } from './types';
4
+
5
+ export function unreachable(x: never): never {
6
+ // eslint-disable-next-line @typescript-eslint/restrict-plus-operands
7
+ throw new Error('Unexpected object: ' + x);
8
+ }
9
+
10
+ function isNumericValueType(spec: SimplifiedPColumnSpec): boolean {
11
+ return spec.valueType === 'Int' || spec.valueType === 'Long' || spec.valueType === 'Float' || spec.valueType === 'Double';
12
+ }
13
+
14
+ function isStringValueType(spec: SimplifiedPColumnSpec): boolean {
15
+ return spec.valueType === 'String';
16
+ }
17
+
18
+ // Define recursive type explicitly
19
+ export type FilterUi = { id?: number; name?: string; isExpanded?: boolean }
20
+ & ({ type: undefined }
21
+ | { type: 'or'; filters: FilterUi[] }
22
+ | { type: 'and'; filters: FilterUi[] }
23
+ | { type: 'not'; filter: FilterUi }
24
+ | { type: 'isNA'; column: SUniversalPColumnId }
25
+ | { type: 'isNotNA'; column: SUniversalPColumnId }
26
+ | { type: 'patternEquals'; column: SUniversalPColumnId; value: string }
27
+ | { type: 'patternNotEquals'; column: SUniversalPColumnId; value: string }
28
+ | { type: 'patternContainSubsequence'; column: SUniversalPColumnId; value: string }
29
+ | { type: 'patternNotContainSubsequence'; column: SUniversalPColumnId; value: string }
30
+ | { type: 'topN'; column: SUniversalPColumnId; n: number }
31
+ | { type: 'bottomN'; column: SUniversalPColumnId; n: number }
32
+ | { type: 'lessThan'; column: SUniversalPColumnId; x: number }
33
+ | { type: 'greaterThan'; column: SUniversalPColumnId; x: number }
34
+ | { type: 'lessThanOrEqual'; column: SUniversalPColumnId; x: number }
35
+ | { type: 'greaterThanOrEqual'; column: SUniversalPColumnId; x: number }
36
+ | { type: 'lessThanColumn'; column: SUniversalPColumnId; rhs: SUniversalPColumnId; minDiff?: number }
37
+ | { type: 'lessThanColumnOrEqual'; column: SUniversalPColumnId; rhs: SUniversalPColumnId; minDiff?: number });
38
+
39
+ export type FilterUiType = Exclude<FilterUi, { type: undefined }>['type'];
40
+
41
+ export type FilterUiOfType<T extends FilterUiType> = Extract<FilterUi, { type: T }>;
42
+
43
+ export type TypeToLiteral<T> =
44
+ [T] extends [FilterUiType] ? 'FilterUiType' :
45
+ [T] extends [SUniversalPColumnId] ? 'SUniversalPColumnId' :
46
+ [T] extends [PatternPredicate] ? 'PatternPredicate' :
47
+ [T] extends [AnnotationFilter[]] ? 'AnnotationFilter[]' :
48
+ [T] extends [AnnotationFilter] ? 'AnnotationFilter' :
49
+ [T] extends [number] ? 'number' :
50
+ [T] extends [number | undefined] ? 'number?' :
51
+ [T] extends [string] ? 'string' :
52
+ [T] extends [string | undefined] ? 'string?' :
53
+ [T] extends [boolean] ? 'boolean' :
54
+ [T] extends [boolean | undefined] ? 'boolean?' :
55
+ [T] extends [unknown[]] ? 'unknown[]' :
56
+ // this is special
57
+ T extends number ? 'number' :
58
+ T extends string ? 'string' :
59
+ T extends boolean ? 'boolean' :
60
+ T extends Record<string, unknown> ? 'form' :
61
+ 'unknown';
62
+
63
+ // @TODO: "parse" option
64
+ export type TypeField<V> = {
65
+ fieldType: TypeToLiteral<V>;
66
+ label: string;
67
+ defaultValue: () => V | undefined;
68
+ };
69
+
70
+ export type TypeFieldRecord<T> = { [K in keyof T]: TypeField<T[K]>; };
71
+
72
+ export type TypeForm<T> = {
73
+ [P in keyof T]: T[P] extends Record<string, unknown> ? {
74
+ fieldType: 'form';
75
+ label?: string;
76
+ form?: T[P] extends Record<string, unknown> ? TypeForm<T[P]> : undefined;
77
+ defaultValue: () => T[P];
78
+ } : TypeField<T[P]>;
79
+ };
80
+
81
+ export type FormField =
82
+ {
83
+ fieldType: 'form';
84
+ form?: Record<string, FormField>;
85
+ defaultValue: () => Record<string, unknown>;
86
+ }
87
+ | TypeField<FilterUiType>
88
+ | TypeField<string>
89
+ | TypeField<number>
90
+ | TypeField<number | undefined>
91
+ | TypeField<boolean>
92
+ | TypeField<boolean | undefined>
93
+ | TypeField<SUniversalPColumnId>;
94
+
95
+ export type AnyForm = Record<string, FormField>;
96
+
97
+ type CreateFilterUiMetadataMap<T extends FilterUiType> = {
98
+ [P in T]: {
99
+ label: string;
100
+ form: TypeForm<FilterUiOfType<T>>; // TODO: simplify this to `TypeField<T>`
101
+ supportedFor: (spec1: SimplifiedPColumnSpec, spec2: SimplifiedPColumnSpec | undefined) => boolean;
102
+ }
103
+ };
104
+
105
+ export const filterUiMetadata = {
106
+ lessThan: {
107
+ label: 'Col < X (Less Than)',
108
+ form: {
109
+ column: {
110
+ label: 'Column',
111
+ fieldType: 'SUniversalPColumnId',
112
+ defaultValue: () => undefined,
113
+ },
114
+ type: {
115
+ label: 'Predicate',
116
+ fieldType: 'FilterUiType',
117
+ defaultValue: () => 'lessThan',
118
+ },
119
+ x: {
120
+ label: 'X',
121
+ fieldType: 'number',
122
+ defaultValue: () => 0,
123
+ },
124
+ },
125
+ supportedFor: isNumericValueType,
126
+ },
127
+ greaterThan: {
128
+ label: 'Col > X (Greater Than)',
129
+ form: {
130
+ column: {
131
+ label: 'Column',
132
+ fieldType: 'SUniversalPColumnId',
133
+ defaultValue: () => undefined,
134
+ },
135
+ type: {
136
+ label: 'Predicate',
137
+ fieldType: 'FilterUiType',
138
+ defaultValue: () => 'greaterThan',
139
+ },
140
+ x: {
141
+ label: 'X',
142
+ fieldType: 'number',
143
+ defaultValue: () => 0,
144
+ },
145
+ },
146
+ supportedFor: isNumericValueType,
147
+ },
148
+ lessThanOrEqual: {
149
+ label: 'Col ≤ X (Less Than or Equal)',
150
+ form: {
151
+ column: {
152
+ label: 'Column',
153
+ fieldType: 'SUniversalPColumnId',
154
+ defaultValue: () => undefined,
155
+ },
156
+ type: {
157
+ label: 'Predicate',
158
+ fieldType: 'FilterUiType',
159
+ defaultValue: () => 'lessThanOrEqual',
160
+ },
161
+ x: {
162
+ label: 'X',
163
+ fieldType: 'number',
164
+ defaultValue: () => 0,
165
+ },
166
+ },
167
+ supportedFor: isNumericValueType,
168
+ },
169
+ greaterThanOrEqual: {
170
+ label: 'Col ≥ X (Greater Than or Equal)',
171
+ form: {
172
+ column: {
173
+ label: 'Column',
174
+ fieldType: 'SUniversalPColumnId',
175
+ defaultValue: () => undefined,
176
+ },
177
+ type: {
178
+ label: 'Predicate',
179
+ fieldType: 'FilterUiType',
180
+ defaultValue: () => 'greaterThanOrEqual',
181
+ },
182
+ x: {
183
+ label: 'X',
184
+ fieldType: 'number',
185
+ defaultValue: () => 0,
186
+ },
187
+ },
188
+ supportedFor: isNumericValueType,
189
+ },
190
+ lessThanColumn: {
191
+ label: 'Col₁ < Col₂ (Compare Columns)',
192
+ form: {
193
+ column: {
194
+ label: 'Col₁',
195
+ fieldType: 'SUniversalPColumnId',
196
+ defaultValue: () => undefined,
197
+ },
198
+ type: {
199
+ label: 'Predicate',
200
+ fieldType: 'FilterUiType',
201
+ defaultValue: () => 'lessThanColumn',
202
+ },
203
+ rhs: {
204
+ label: 'Col₂',
205
+ fieldType: 'SUniversalPColumnId',
206
+ defaultValue: () => undefined,
207
+ },
208
+ minDiff: {
209
+ label: 'Margin (positive)',
210
+ fieldType: 'number?',
211
+ defaultValue: () => undefined,
212
+ },
213
+ },
214
+ supportedFor: (spec1: SimplifiedPColumnSpec, spec2?: SimplifiedPColumnSpec): boolean => {
215
+ return isNumericValueType(spec1) && (spec2 === undefined || isNumericValueType(spec2));
216
+ },
217
+ },
218
+ lessThanColumnOrEqual: {
219
+ label: 'Col₁ ≤ Col₂ (Compare Columns)',
220
+ form: {
221
+ column: {
222
+ label: 'Col₁',
223
+ fieldType: 'SUniversalPColumnId',
224
+ defaultValue: () => undefined,
225
+ },
226
+ type: {
227
+ label: 'Predicate',
228
+ fieldType: 'FilterUiType',
229
+ defaultValue: () => 'lessThanColumnOrEqual',
230
+ },
231
+ rhs: {
232
+ label: 'Col₂',
233
+ fieldType: 'SUniversalPColumnId',
234
+ defaultValue: () => undefined,
235
+ },
236
+ minDiff: {
237
+ label: 'Margin (positive)',
238
+ fieldType: 'number?',
239
+ defaultValue: () => undefined,
240
+ },
241
+ },
242
+ supportedFor: (spec1: SimplifiedPColumnSpec, spec2?: SimplifiedPColumnSpec): boolean => {
243
+ return isNumericValueType(spec1) && (spec2 === undefined || isNumericValueType(spec2));
244
+ },
245
+ },
246
+ topN: {
247
+ label: 'Top N',
248
+ form: {
249
+ column: {
250
+ label: 'Rank By Column',
251
+ fieldType: 'SUniversalPColumnId',
252
+ defaultValue: () => undefined,
253
+ },
254
+ type: {
255
+ label: 'Predicate',
256
+ fieldType: 'FilterUiType',
257
+ defaultValue: () => 'topN',
258
+ },
259
+ n: {
260
+ label: 'N',
261
+ fieldType: 'number',
262
+ defaultValue: () => 10,
263
+ },
264
+ },
265
+ supportedFor: isNumericValueType,
266
+ },
267
+ bottomN: {
268
+ label: 'Bottom N',
269
+ form: {
270
+ column: {
271
+ label: 'Rank By Column',
272
+ fieldType: 'SUniversalPColumnId',
273
+ defaultValue: () => undefined,
274
+ },
275
+ type: {
276
+ label: 'Predicate',
277
+ fieldType: 'FilterUiType',
278
+ defaultValue: () => 'bottomN',
279
+ },
280
+ n: {
281
+ label: 'N',
282
+ fieldType: 'number',
283
+ defaultValue: () => 10,
284
+ },
285
+ },
286
+ supportedFor: isNumericValueType,
287
+ },
288
+ patternContainSubsequence: {
289
+ label: 'Col ~ Seq (Contain Subsequence)',
290
+ form: {
291
+ column: {
292
+ label: 'Column',
293
+ fieldType: 'SUniversalPColumnId',
294
+ defaultValue: () => undefined,
295
+ },
296
+ type: {
297
+ label: 'Predicate',
298
+ fieldType: 'FilterUiType',
299
+ defaultValue: () => 'patternContainSubsequence',
300
+ },
301
+ value: {
302
+ label: 'Seq',
303
+ fieldType: 'string',
304
+ defaultValue: () => '',
305
+ },
306
+ },
307
+ supportedFor: isStringValueType,
308
+ },
309
+ patternNotContainSubsequence: {
310
+ label: 'Col ≁ Seq (Not Contain Subsequence)',
311
+ form: {
312
+ column: {
313
+ label: 'Column',
314
+ fieldType: 'SUniversalPColumnId',
315
+ defaultValue: () => undefined,
316
+ },
317
+ type: {
318
+ label: 'Predicate',
319
+ fieldType: 'FilterUiType',
320
+ defaultValue: () => 'patternNotContainSubsequence',
321
+ },
322
+ value: {
323
+ label: 'Seq',
324
+ fieldType: 'string',
325
+ defaultValue: () => '',
326
+ },
327
+ },
328
+ supportedFor: isStringValueType,
329
+ },
330
+ patternEquals: {
331
+ label: 'Col = Seq (Equals)',
332
+ form: {
333
+ column: {
334
+ label: 'Column',
335
+ fieldType: 'SUniversalPColumnId',
336
+ defaultValue: () => undefined,
337
+ },
338
+ type: {
339
+ label: 'Predicate',
340
+ fieldType: 'FilterUiType',
341
+ defaultValue: () => 'patternEquals',
342
+ },
343
+ value: {
344
+ label: 'Seq',
345
+ fieldType: 'string',
346
+ defaultValue: () => '',
347
+ },
348
+ },
349
+ supportedFor: isStringValueType,
350
+ },
351
+ patternNotEquals: {
352
+ label: 'Col ≠ Seq (Not Equal)',
353
+ form: {
354
+ column: {
355
+ label: 'Column',
356
+ fieldType: 'SUniversalPColumnId',
357
+ defaultValue: () => undefined,
358
+ },
359
+ type: {
360
+ label: 'Predicate',
361
+ fieldType: 'FilterUiType',
362
+ defaultValue: () => 'patternNotEquals',
363
+ },
364
+ value: {
365
+ label: 'Seq',
366
+ fieldType: 'string',
367
+ defaultValue: () => '',
368
+ },
369
+ },
370
+ supportedFor: isStringValueType,
371
+ },
372
+ isNA: {
373
+ label: 'Is NA',
374
+ form: {
375
+ column: {
376
+ label: 'Column',
377
+ fieldType: 'SUniversalPColumnId',
378
+ defaultValue: () => undefined,
379
+ },
380
+ type: {
381
+ label: 'Predicate',
382
+ fieldType: 'FilterUiType',
383
+ defaultValue: () => 'isNA',
384
+ },
385
+ },
386
+ supportedFor: () => true,
387
+ },
388
+ isNotNA: {
389
+ label: 'Is Not NA',
390
+ form: {
391
+ column: {
392
+ label: 'Column',
393
+ fieldType: 'SUniversalPColumnId',
394
+ defaultValue: () => undefined,
395
+ },
396
+ type: {
397
+ label: 'Predicate',
398
+ fieldType: 'FilterUiType',
399
+ defaultValue: () => 'isNotNA',
400
+ },
401
+ },
402
+ supportedFor: () => true,
403
+ },
404
+ or: {
405
+ label: 'Or',
406
+ form: {
407
+ type: {
408
+ fieldType: 'FilterUiType',
409
+ label: 'Predicate',
410
+ defaultValue: () => 'or',
411
+ },
412
+ filters: {
413
+ fieldType: 'unknown[]',
414
+ label: 'Filters',
415
+ defaultValue: () => [],
416
+ },
417
+ },
418
+ supportedFor: () => false,
419
+ },
420
+ and: {
421
+ label: 'And',
422
+ form: {
423
+ type: {
424
+ fieldType: 'FilterUiType',
425
+ label: 'Predicate',
426
+ defaultValue: () => 'and',
427
+ },
428
+ filters: {
429
+ fieldType: 'unknown[]',
430
+ label: 'Filters',
431
+ defaultValue: () => [],
432
+ },
433
+ },
434
+ supportedFor: () => false,
435
+ },
436
+ not: {
437
+ label: 'Not',
438
+ form: {
439
+ type: {
440
+ fieldType: 'FilterUiType',
441
+ label: 'Predicate',
442
+ defaultValue: () => 'not',
443
+ },
444
+ filter: {
445
+ fieldType: 'form',
446
+ label: 'Filter',
447
+ defaultValue: () => undefined as unknown as FilterUi, // TODO:
448
+ },
449
+ },
450
+ supportedFor: () => false,
451
+ },
452
+ } satisfies CreateFilterUiMetadataMap<FilterUiType>;
453
+
454
+ export function getFilterUiTypeOptions(columnSpec?: SimplifiedPColumnSpec) {
455
+ if (!columnSpec) {
456
+ return [];
457
+ }
458
+
459
+ return Object.entries(filterUiMetadata).filter(([_, metadata]) => metadata.supportedFor(columnSpec)).map(([type, metadata]) => ({
460
+ label: metadata.label,
461
+ value: type,
462
+ }));
463
+ }
464
+
465
+ export function getFilterUiMetadata(type: FilterUiType) {
466
+ return filterUiMetadata[type];
467
+ }
468
+
469
+ export function compileFilter(ui: FilterUi): AnnotationFilter {
470
+ if (ui.type === 'or') {
471
+ return {
472
+ type: 'or' as const,
473
+ filters: compileFilters(ui.filters),
474
+ };
475
+ }
476
+
477
+ if (ui.type === 'and') {
478
+ return {
479
+ type: 'and' as const,
480
+ filters: compileFilters(ui.filters),
481
+ };
482
+ }
483
+
484
+ if (ui.type === 'not') {
485
+ return {
486
+ type: 'not' as const,
487
+ filter: compileFilter(ui.filter),
488
+ };
489
+ }
490
+
491
+ if (ui.type === 'isNA') {
492
+ return {
493
+ type: 'isNA' as const,
494
+ column: ui.column,
495
+ };
496
+ }
497
+
498
+ if (ui.type === 'isNotNA') {
499
+ const isNAFilter: IsNA = { type: 'isNA', column: ui.column };
500
+ const notFilter: NotFilter = { type: 'not', filter: isNAFilter };
501
+ return notFilter;
502
+ }
503
+
504
+ if (ui.type === 'patternEquals') {
505
+ return {
506
+ type: 'pattern' as const,
507
+ column: ui.column,
508
+ predicate: {
509
+ type: 'equals' as const,
510
+ value: ui.value,
511
+ },
512
+ };
513
+ }
514
+
515
+ if (ui.type === 'patternNotEquals') {
516
+ const patternFilter: PatternFilter = {
517
+ type: 'pattern',
518
+ column: ui.column,
519
+ predicate: { type: 'equals', value: ui.value },
520
+ };
521
+ const notFilter: NotFilter = { type: 'not', filter: patternFilter };
522
+ return notFilter;
523
+ }
524
+
525
+ if (ui.type === 'patternContainSubsequence') {
526
+ return {
527
+ type: 'pattern' as const,
528
+ column: ui.column,
529
+ predicate: {
530
+ type: 'containSubsequence' as const,
531
+ value: ui.value,
532
+ },
533
+ };
534
+ }
535
+
536
+ if (ui.type === 'patternNotContainSubsequence') {
537
+ const patternFilter: PatternFilter = {
538
+ type: 'pattern',
539
+ column: ui.column,
540
+ predicate: { type: 'containSubsequence', value: ui.value },
541
+ };
542
+ const notFilter: NotFilter = { type: 'not', filter: patternFilter };
543
+ return notFilter;
544
+ }
545
+
546
+ if (ui.type === 'topN') {
547
+ const rankTransform: ValueRank = {
548
+ transformer: 'rank',
549
+ column: ui.column,
550
+ descending: true,
551
+ };
552
+ const comparisonFilter: NumericalComparisonFilter = {
553
+ type: 'numericalComparison',
554
+ lhs: rankTransform,
555
+ rhs: ui.n,
556
+ allowEqual: true,
557
+ };
558
+ return comparisonFilter;
559
+ }
560
+
561
+ if (ui.type === 'bottomN') {
562
+ const rankTransform: ValueRank = {
563
+ transformer: 'rank',
564
+ column: ui.column,
565
+ };
566
+ const comparisonFilter: NumericalComparisonFilter = {
567
+ type: 'numericalComparison',
568
+ lhs: rankTransform,
569
+ rhs: ui.n,
570
+ allowEqual: true,
571
+ };
572
+ return comparisonFilter;
573
+ }
574
+
575
+ if (ui.type === 'lessThan') {
576
+ return {
577
+ type: 'numericalComparison' as const,
578
+ lhs: ui.column,
579
+ rhs: ui.x,
580
+ };
581
+ }
582
+
583
+ if (ui.type === 'greaterThan') {
584
+ return {
585
+ type: 'numericalComparison' as const,
586
+ rhs: ui.column,
587
+ lhs: ui.x,
588
+ };
589
+ }
590
+
591
+ if (ui.type === 'greaterThanOrEqual') {
592
+ return {
593
+ type: 'numericalComparison' as const,
594
+ rhs: ui.column,
595
+ lhs: ui.x,
596
+ allowEqual: true,
597
+ };
598
+ }
599
+
600
+ if (ui.type === 'lessThanOrEqual') {
601
+ return {
602
+ type: 'numericalComparison' as const,
603
+ lhs: ui.column,
604
+ rhs: ui.x,
605
+ allowEqual: true,
606
+ };
607
+ }
608
+
609
+ if (ui.type === 'lessThanColumn') {
610
+ return {
611
+ type: 'numericalComparison' as const,
612
+ lhs: ui.column,
613
+ rhs: ui.rhs,
614
+ minDiff: ui.minDiff,
615
+ allowEqual: undefined,
616
+ };
617
+ }
618
+
619
+ if (ui.type === 'lessThanColumnOrEqual') {
620
+ return {
621
+ type: 'numericalComparison' as const,
622
+ lhs: ui.column,
623
+ rhs: ui.rhs,
624
+ minDiff: ui.minDiff,
625
+ allowEqual: true,
626
+ };
627
+ }
628
+
629
+ if (ui.type === undefined) {
630
+ throw new Error('Filter type is undefined, this should not happen');
631
+ }
632
+
633
+ unreachable(ui);
634
+ }
635
+
636
+ export function compileFilters(uiFilters: FilterUi[]): AnnotationFilter[] {
637
+ return uiFilters.filter((f) => f.type !== undefined).map(compileFilter);
638
+ }
639
+
640
+ export type AnnotationStepUi = {
641
+ id?: number;
642
+ label: string;
643
+ filter: Extract<FilterUi, { type: 'and' | 'or' }>;
644
+ };
645
+
646
+ export type AnnotationScriptUi = {
647
+ isCreated?: boolean;
648
+ title: string;
649
+ mode: AnnotationMode;
650
+ steps: AnnotationStepUi[];
651
+ };
652
+
653
+ export function compileAnnotationScript(uiScript: AnnotationScriptUi): AnnotationScript {
654
+ return {
655
+ title: uiScript.title,
656
+ mode: uiScript.mode,
657
+ steps: uiScript.steps
658
+ .filter((step) => {
659
+ // No need to compile empty steps
660
+ if (step.filter.type == null) {
661
+ return false;
662
+ }
663
+
664
+ if (step.filter.type === 'or') {
665
+ return step.filter.filters.length > 0;
666
+ }
667
+
668
+ if (step.filter.type === 'and') {
669
+ return step.filter.filters.length > 0;
670
+ }
671
+
672
+ return false;
673
+ })
674
+ .map((step) => ({
675
+ label: step.label.trim(),
676
+ filter: compileFilter(step.filter),
677
+ })),
678
+ };
679
+ }
@@ -0,0 +1,2 @@
1
+ export * from './filter';
2
+ export * from './filters_ui';
@@ -0,0 +1,3 @@
1
+ import type { PColumnSpec } from '@milaboratories/pl-model-common';
2
+
3
+ export type SimplifiedPColumnSpec = Pick<PColumnSpec, 'valueType' | 'annotations'>;
@@ -2,3 +2,4 @@ export * from './PFrameForGraphs';
2
2
  export * from './PlDataTable';
3
3
  export * from './PlMultiSequenceAlignment';
4
4
  export * from './PlSelectionModel';
5
+ export * from './PlAnnotations';