@jskit-ai/crud-core 0.1.99 → 0.1.101
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/package.descriptor.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
export default Object.freeze({
|
|
2
2
|
packageVersion: 1,
|
|
3
3
|
packageId: "@jskit-ai/crud-core",
|
|
4
|
-
version: "0.1.
|
|
4
|
+
version: "0.1.101",
|
|
5
5
|
kind: "runtime",
|
|
6
6
|
description: "Shared CRUD helpers used by CRUD modules.",
|
|
7
7
|
dependsOn: [
|
|
@@ -28,7 +28,7 @@ export default Object.freeze({
|
|
|
28
28
|
mutations: {
|
|
29
29
|
dependencies: {
|
|
30
30
|
runtime: {
|
|
31
|
-
"@jskit-ai/crud-core": "0.1.
|
|
31
|
+
"@jskit-ai/crud-core": "0.1.101"
|
|
32
32
|
},
|
|
33
33
|
dev: {}
|
|
34
34
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@jskit-ai/crud-core",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.101",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"scripts": {
|
|
6
6
|
"test": "node --test"
|
|
@@ -27,15 +27,15 @@
|
|
|
27
27
|
"./server/routeContracts": "./src/server/routeContracts.js"
|
|
28
28
|
},
|
|
29
29
|
"dependencies": {
|
|
30
|
-
"@jskit-ai/database-runtime": "0.1.
|
|
31
|
-
"@jskit-ai/http-runtime": "0.1.
|
|
32
|
-
"@jskit-ai/kernel": "0.1.
|
|
30
|
+
"@jskit-ai/database-runtime": "0.1.93",
|
|
31
|
+
"@jskit-ai/http-runtime": "0.1.92",
|
|
32
|
+
"@jskit-ai/kernel": "0.1.93",
|
|
33
33
|
"json-rest-schema": "1.x.x",
|
|
34
|
-
"@jskit-ai/realtime": "0.1.
|
|
35
|
-
"@jskit-ai/resource-crud-core": "0.1.
|
|
36
|
-
"@jskit-ai/shell-web": "0.1.
|
|
37
|
-
"@jskit-ai/users-core": "0.1.
|
|
38
|
-
"@jskit-ai/users-web": "0.1.
|
|
34
|
+
"@jskit-ai/realtime": "0.1.92",
|
|
35
|
+
"@jskit-ai/resource-crud-core": "0.1.38",
|
|
36
|
+
"@jskit-ai/shell-web": "0.1.92",
|
|
37
|
+
"@jskit-ai/users-core": "0.1.103",
|
|
38
|
+
"@jskit-ai/users-web": "0.1.108"
|
|
39
39
|
},
|
|
40
40
|
"peerDependencies": {
|
|
41
41
|
"@tanstack/vue-query": "^5.90.5",
|
|
@@ -21,6 +21,8 @@ import {
|
|
|
21
21
|
CRUD_LIST_FILTER_TYPE_DATE_RANGE,
|
|
22
22
|
CRUD_LIST_FILTER_TYPE_NUMBER_RANGE,
|
|
23
23
|
CRUD_LIST_FILTER_TYPE_PRESENCE,
|
|
24
|
+
CRUD_LIST_FILTER_PRESENCE_PRESENT,
|
|
25
|
+
CRUD_LIST_FILTER_PRESENCE_MISSING,
|
|
24
26
|
INVALID_CRUD_LIST_FILTER_QUERY_VALUE,
|
|
25
27
|
normalizeCrudListFilterInvalidValues,
|
|
26
28
|
parseCrudListFilterQueryValue
|
|
@@ -34,6 +36,7 @@ const NUMBER_FILTER_VALUE_PATTERN_SOURCE = "[+-]?(?:\\d+(?:\\.\\d*)?|\\.\\d+)(?:
|
|
|
34
36
|
const NUMBER_RANGE_FILTER_PATTERN_SOURCE =
|
|
35
37
|
`^(?:${NUMBER_FILTER_VALUE_PATTERN_SOURCE}(?:\\.\\.(?:${NUMBER_FILTER_VALUE_PATTERN_SOURCE})?)?|\\.\\.${NUMBER_FILTER_VALUE_PATTERN_SOURCE})$`;
|
|
36
38
|
const CRUD_LIST_FILTER_QUERY_TYPE = "crudListFilterQuery";
|
|
39
|
+
const JSON_REST_INTERNAL_FILTER_PREFIX = "__jskit";
|
|
37
40
|
const crudListFilterSchemaFactory = createSchema.createFactory();
|
|
38
41
|
const looseTextTransportSchema = Object.freeze({
|
|
39
42
|
type: "string",
|
|
@@ -462,6 +465,276 @@ function normalizeColumnsMap(columns = {}) {
|
|
|
462
465
|
return Object.freeze(normalized);
|
|
463
466
|
}
|
|
464
467
|
|
|
468
|
+
function resolveJsonRestFilterActualField(filter = {}, columns = {}) {
|
|
469
|
+
const meta = isPlainObject(filter.meta?.jsonRest) ? filter.meta.jsonRest : {};
|
|
470
|
+
return normalizeText(meta.actualField || meta.column || columns[filter.key] || filter.key);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
function renderInternalJsonRestFilterKey(filter = {}, suffix = "") {
|
|
474
|
+
return [
|
|
475
|
+
JSON_REST_INTERNAL_FILTER_PREFIX,
|
|
476
|
+
filter.queryKey,
|
|
477
|
+
suffix
|
|
478
|
+
]
|
|
479
|
+
.map((entry) => normalizeText(entry))
|
|
480
|
+
.filter(Boolean)
|
|
481
|
+
.join("_");
|
|
482
|
+
}
|
|
483
|
+
|
|
484
|
+
function createJsonRestSearchSchemaEntry({
|
|
485
|
+
type = "string",
|
|
486
|
+
actualField = "",
|
|
487
|
+
filterOperator = "=",
|
|
488
|
+
extra = {}
|
|
489
|
+
} = {}) {
|
|
490
|
+
return {
|
|
491
|
+
type,
|
|
492
|
+
...(actualField ? { actualField } : {}),
|
|
493
|
+
...(filterOperator ? { filterOperator } : {}),
|
|
494
|
+
...extra
|
|
495
|
+
};
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
function buildJsonRestFilterItemSchema(filter = {}) {
|
|
499
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY) {
|
|
500
|
+
return {
|
|
501
|
+
type: "string",
|
|
502
|
+
enum: (Array.isArray(filter.options) ? filter.options : []).map((entry) => entry.value)
|
|
503
|
+
};
|
|
504
|
+
}
|
|
505
|
+
|
|
506
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
507
|
+
return cloneTransportSchema(recordIdTransportSchema);
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
return {};
|
|
511
|
+
}
|
|
512
|
+
|
|
513
|
+
function buildJsonRestSimpleFilterSearchSchemaEntry(filter = {}, { actualField = "" } = {}) {
|
|
514
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_FLAG) {
|
|
515
|
+
return createJsonRestSearchSchemaEntry({
|
|
516
|
+
type: "boolean",
|
|
517
|
+
actualField,
|
|
518
|
+
filterOperator: "="
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
|
|
522
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM) {
|
|
523
|
+
return createJsonRestSearchSchemaEntry({
|
|
524
|
+
type: "string",
|
|
525
|
+
actualField,
|
|
526
|
+
filterOperator: "=",
|
|
527
|
+
extra: {
|
|
528
|
+
enum: (Array.isArray(filter.options) ? filter.options : []).map((entry) => entry.value)
|
|
529
|
+
}
|
|
530
|
+
});
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_ENUM_MANY || filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID_MANY) {
|
|
534
|
+
return createJsonRestSearchSchemaEntry({
|
|
535
|
+
type: "array",
|
|
536
|
+
actualField,
|
|
537
|
+
filterOperator: "in",
|
|
538
|
+
extra: {
|
|
539
|
+
items: buildJsonRestFilterItemSchema(filter)
|
|
540
|
+
}
|
|
541
|
+
});
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_RECORD_ID) {
|
|
545
|
+
return createJsonRestSearchSchemaEntry({
|
|
546
|
+
type: "id",
|
|
547
|
+
actualField,
|
|
548
|
+
filterOperator: "="
|
|
549
|
+
});
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
return null;
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function buildJsonRestRangeFilterSearchSchemaEntries(filter = {}, { actualField = "" } = {}) {
|
|
556
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE) {
|
|
557
|
+
return {
|
|
558
|
+
[renderInternalJsonRestFilterKey(filter, "from")]: createJsonRestSearchSchemaEntry({
|
|
559
|
+
type: "dateTime",
|
|
560
|
+
actualField,
|
|
561
|
+
filterOperator: ">="
|
|
562
|
+
}),
|
|
563
|
+
[renderInternalJsonRestFilterKey(filter, "toExclusive")]: createJsonRestSearchSchemaEntry({
|
|
564
|
+
type: "dateTime",
|
|
565
|
+
actualField,
|
|
566
|
+
filterOperator: "<"
|
|
567
|
+
})
|
|
568
|
+
};
|
|
569
|
+
}
|
|
570
|
+
|
|
571
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
572
|
+
return {
|
|
573
|
+
[renderInternalJsonRestFilterKey(filter, "from")]: createJsonRestSearchSchemaEntry({
|
|
574
|
+
type: "dateTime",
|
|
575
|
+
actualField,
|
|
576
|
+
filterOperator: ">="
|
|
577
|
+
}),
|
|
578
|
+
[renderInternalJsonRestFilterKey(filter, "toExclusive")]: createJsonRestSearchSchemaEntry({
|
|
579
|
+
type: "dateTime",
|
|
580
|
+
actualField,
|
|
581
|
+
filterOperator: "<"
|
|
582
|
+
})
|
|
583
|
+
};
|
|
584
|
+
}
|
|
585
|
+
|
|
586
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
587
|
+
return {
|
|
588
|
+
[renderInternalJsonRestFilterKey(filter, "min")]: createJsonRestSearchSchemaEntry({
|
|
589
|
+
type: "number",
|
|
590
|
+
actualField,
|
|
591
|
+
filterOperator: ">="
|
|
592
|
+
}),
|
|
593
|
+
[renderInternalJsonRestFilterKey(filter, "max")]: createJsonRestSearchSchemaEntry({
|
|
594
|
+
type: "number",
|
|
595
|
+
actualField,
|
|
596
|
+
filterOperator: "<="
|
|
597
|
+
})
|
|
598
|
+
};
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
return {};
|
|
602
|
+
}
|
|
603
|
+
|
|
604
|
+
function buildJsonRestPresenceFilterSearchSchemaEntry(filter = {}, { actualField = "" } = {}) {
|
|
605
|
+
return {
|
|
606
|
+
type: "string",
|
|
607
|
+
enum: [CRUD_LIST_FILTER_PRESENCE_PRESENT, CRUD_LIST_FILTER_PRESENCE_MISSING],
|
|
608
|
+
applyFilter(queryBuilder, value) {
|
|
609
|
+
applyDefaultFilterQuery(queryBuilder, filter, value, actualField);
|
|
610
|
+
}
|
|
611
|
+
};
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
function buildJsonRestCustomFilterSearchSchemaEntry(filter = {}, customApply = null) {
|
|
615
|
+
return {
|
|
616
|
+
type: "none",
|
|
617
|
+
applyFilter(queryBuilder, value) {
|
|
618
|
+
customApply(queryBuilder, value, {
|
|
619
|
+
filter,
|
|
620
|
+
filters: Object.freeze({
|
|
621
|
+
[filter.key]: value
|
|
622
|
+
})
|
|
623
|
+
});
|
|
624
|
+
}
|
|
625
|
+
};
|
|
626
|
+
}
|
|
627
|
+
|
|
628
|
+
function resolveCrudListFilterEntries(value = {}) {
|
|
629
|
+
if (isPlainObject(value?.filters)) {
|
|
630
|
+
return Object.values(normalizeObject(value.filters));
|
|
631
|
+
}
|
|
632
|
+
|
|
633
|
+
return Object.values(defineCrudListFilters(value));
|
|
634
|
+
}
|
|
635
|
+
|
|
636
|
+
function createCrudListFilterJsonRestSearchSchema(runtime = {}, { columns = {}, apply = {} } = {}) {
|
|
637
|
+
const filterEntries = resolveCrudListFilterEntries(runtime);
|
|
638
|
+
const normalizedColumns = normalizeColumnsMap(columns);
|
|
639
|
+
const schema = {};
|
|
640
|
+
|
|
641
|
+
for (const filter of filterEntries) {
|
|
642
|
+
const customApply = typeof apply?.[filter.key] === "function" ? apply[filter.key] : null;
|
|
643
|
+
if (customApply) {
|
|
644
|
+
schema[filter.queryKey] = buildJsonRestCustomFilterSearchSchemaEntry(filter, customApply);
|
|
645
|
+
continue;
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
const actualField = resolveJsonRestFilterActualField(filter, normalizedColumns);
|
|
649
|
+
if (!actualField) {
|
|
650
|
+
continue;
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
const simpleEntry = buildJsonRestSimpleFilterSearchSchemaEntry(filter, { actualField });
|
|
654
|
+
if (simpleEntry) {
|
|
655
|
+
schema[filter.queryKey] = simpleEntry;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_PRESENCE) {
|
|
660
|
+
schema[filter.queryKey] = buildJsonRestPresenceFilterSearchSchemaEntry(filter, { actualField });
|
|
661
|
+
continue;
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
Object.assign(schema, buildJsonRestRangeFilterSearchSchemaEntries(filter, { actualField }));
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
return Object.freeze(schema);
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
function addJsonRestDateFilterValues(target = {}, filter = {}, value = "") {
|
|
671
|
+
const nextDate = addDaysToDateFilterValue(value, 1);
|
|
672
|
+
if (value) {
|
|
673
|
+
target[renderInternalJsonRestFilterKey(filter, "from")] = `${value} 00:00:00`;
|
|
674
|
+
}
|
|
675
|
+
if (nextDate) {
|
|
676
|
+
target[renderInternalJsonRestFilterKey(filter, "toExclusive")] = `${nextDate} 00:00:00`;
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
|
|
680
|
+
function addJsonRestFilterValue(target = {}, filter = {}, value) {
|
|
681
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE) {
|
|
682
|
+
addJsonRestDateFilterValues(target, filter, value);
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
|
|
686
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_DATE_RANGE) {
|
|
687
|
+
if (value?.from) {
|
|
688
|
+
target[renderInternalJsonRestFilterKey(filter, "from")] = `${value.from} 00:00:00`;
|
|
689
|
+
}
|
|
690
|
+
if (value?.to) {
|
|
691
|
+
const nextDate = addDaysToDateFilterValue(value.to, 1);
|
|
692
|
+
if (nextDate) {
|
|
693
|
+
target[renderInternalJsonRestFilterKey(filter, "toExclusive")] = `${nextDate} 00:00:00`;
|
|
694
|
+
}
|
|
695
|
+
}
|
|
696
|
+
return;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (filter.type === CRUD_LIST_FILTER_TYPE_NUMBER_RANGE) {
|
|
700
|
+
if (value?.min != null) {
|
|
701
|
+
target[renderInternalJsonRestFilterKey(filter, "min")] = value.min;
|
|
702
|
+
}
|
|
703
|
+
if (value?.max != null) {
|
|
704
|
+
target[renderInternalJsonRestFilterKey(filter, "max")] = value.max;
|
|
705
|
+
}
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
|
|
709
|
+
target[filter.queryKey] = value;
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
function normalizeCrudListFilterJsonRestQuery(runtime = {}, query = {}) {
|
|
713
|
+
const source = normalizeObjectInput(query);
|
|
714
|
+
const filterEntries = Object.values(normalizeObject(runtime?.filters));
|
|
715
|
+
const filterQueryKeys = new Set(filterEntries.map((filter) => filter.queryKey));
|
|
716
|
+
const parsedFilters = typeof runtime.parseQuery === "function"
|
|
717
|
+
? runtime.parseQuery(source)
|
|
718
|
+
: {};
|
|
719
|
+
const normalized = {};
|
|
720
|
+
|
|
721
|
+
for (const [key, value] of Object.entries(source)) {
|
|
722
|
+
if (!filterQueryKeys.has(key)) {
|
|
723
|
+
normalized[key] = value;
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
for (const filter of filterEntries) {
|
|
728
|
+
if (!Object.hasOwn(parsedFilters, filter.key)) {
|
|
729
|
+
continue;
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
addJsonRestFilterValue(normalized, filter, parsedFilters[filter.key]);
|
|
733
|
+
}
|
|
734
|
+
|
|
735
|
+
return normalized;
|
|
736
|
+
}
|
|
737
|
+
|
|
465
738
|
function applyDefaultFilterQuery(queryBuilder, filter = {}, value, column = "") {
|
|
466
739
|
const handler = FILTER_TYPE_SERVER_HANDLERS[filter.type];
|
|
467
740
|
return handler
|
|
@@ -529,14 +802,46 @@ function createCrudListFilters(definitions = {}, { columns = {}, apply = {} } =
|
|
|
529
802
|
return Object.freeze({
|
|
530
803
|
filters: normalizedFilters,
|
|
531
804
|
createQueryValidator,
|
|
805
|
+
parseQuery: parseFilterPayload,
|
|
806
|
+
toJsonRestQuery(query = {}) {
|
|
807
|
+
return normalizeCrudListFilterJsonRestQuery({
|
|
808
|
+
filters: normalizedFilters,
|
|
809
|
+
parseQuery: parseFilterPayload
|
|
810
|
+
}, query);
|
|
811
|
+
},
|
|
812
|
+
jsonRestSearchSchema: createCrudListFilterJsonRestSearchSchema({
|
|
813
|
+
filters: normalizedFilters
|
|
814
|
+
}, {
|
|
815
|
+
columns: normalizedColumns,
|
|
816
|
+
apply
|
|
817
|
+
}),
|
|
532
818
|
applyQuery
|
|
533
819
|
});
|
|
534
820
|
}
|
|
535
821
|
|
|
822
|
+
function createCrudListFilterContract(definitions = {}, {
|
|
823
|
+
columns = {},
|
|
824
|
+
apply = {},
|
|
825
|
+
invalidValues = CRUD_LIST_FILTER_INVALID_VALUES_REJECT
|
|
826
|
+
} = {}) {
|
|
827
|
+
const runtime = createCrudListFilters(definitions, { columns, apply });
|
|
828
|
+
return Object.freeze({
|
|
829
|
+
filters: runtime.filters,
|
|
830
|
+
queryValidator: runtime.createQueryValidator({ invalidValues }),
|
|
831
|
+
createQueryValidator: runtime.createQueryValidator,
|
|
832
|
+
parseQuery: runtime.parseQuery,
|
|
833
|
+
toJsonRestQuery: runtime.toJsonRestQuery,
|
|
834
|
+
jsonRestSearchSchema: runtime.jsonRestSearchSchema,
|
|
835
|
+
applyQuery: runtime.applyQuery
|
|
836
|
+
});
|
|
837
|
+
}
|
|
838
|
+
|
|
536
839
|
export {
|
|
537
840
|
CRUD_LIST_FILTER_INVALID_VALUES_REJECT,
|
|
538
841
|
CRUD_LIST_FILTER_INVALID_VALUES_DISCARD,
|
|
539
842
|
createCrudListFilterQueryField,
|
|
540
843
|
createCrudListFilterQuerySchema,
|
|
541
|
-
|
|
844
|
+
createCrudListFilterJsonRestSearchSchema,
|
|
845
|
+
createCrudListFilters,
|
|
846
|
+
createCrudListFilterContract
|
|
542
847
|
};
|
|
@@ -441,16 +441,19 @@ function createCrudJsonApiRouteContracts({
|
|
|
441
441
|
resource = {},
|
|
442
442
|
routeParamsValidator = null,
|
|
443
443
|
listSearchQueryValidator = defaultListSearchQueryValidator,
|
|
444
|
-
lookupIncludeQueryValidator = defaultLookupIncludeQueryValidator
|
|
444
|
+
lookupIncludeQueryValidator = defaultLookupIncludeQueryValidator,
|
|
445
|
+
listFilterQueryValidator = null
|
|
445
446
|
} = {}) {
|
|
446
447
|
const listCursorPaginationQueryValidator = createCrudCursorPaginationQueryValidator({
|
|
447
448
|
orderBy: resource?.defaultSort
|
|
448
449
|
});
|
|
449
450
|
const listParentFilterQueryValidator = createCrudParentFilterQueryValidator(resource);
|
|
451
|
+
const resolvedListFilterQueryValidator = listFilterQueryValidator || resource?.contract?.listFilters?.queryValidator || null;
|
|
450
452
|
const listRouteQueryValidator = composeSchemaDefinitions([
|
|
451
453
|
listCursorPaginationQueryValidator,
|
|
452
454
|
listSearchQueryValidator,
|
|
453
455
|
listParentFilterQueryValidator,
|
|
456
|
+
...(resolvedListFilterQueryValidator ? [resolvedListFilterQueryValidator] : []),
|
|
454
457
|
lookupIncludeQueryValidator
|
|
455
458
|
]);
|
|
456
459
|
const recordRouteParamsValidator = routeParamsValidator
|
package/test/listFilters.test.js
CHANGED
|
@@ -13,7 +13,9 @@ import {
|
|
|
13
13
|
CRUD_LIST_FILTER_INVALID_VALUES_DISCARD,
|
|
14
14
|
createCrudListFilterQueryField,
|
|
15
15
|
createCrudListFilterQuerySchema,
|
|
16
|
-
|
|
16
|
+
createCrudListFilterJsonRestSearchSchema,
|
|
17
|
+
createCrudListFilters,
|
|
18
|
+
createCrudListFilterContract
|
|
17
19
|
} from "../src/server/listFilters.js";
|
|
18
20
|
|
|
19
21
|
function composeSchemaDefinition(...definitions) {
|
|
@@ -28,6 +30,8 @@ test("crud-core exposes createCrudListFilters through the public package export"
|
|
|
28
30
|
assert.equal(typeof module.createCrudListFilters, "function");
|
|
29
31
|
assert.equal(typeof module.createCrudListFilterQueryField, "function");
|
|
30
32
|
assert.equal(typeof module.createCrudListFilterQuerySchema, "function");
|
|
33
|
+
assert.equal(typeof module.createCrudListFilterJsonRestSearchSchema, "function");
|
|
34
|
+
assert.equal(typeof module.createCrudListFilterContract, "function");
|
|
31
35
|
assert.equal(module.CRUD_LIST_FILTER_INVALID_VALUES_REJECT, CRUD_LIST_FILTER_INVALID_VALUES_REJECT);
|
|
32
36
|
assert.equal(module.CRUD_LIST_FILTER_INVALID_VALUES_DISCARD, CRUD_LIST_FILTER_INVALID_VALUES_DISCARD);
|
|
33
37
|
});
|
|
@@ -526,6 +530,154 @@ test("createCrudListFilters treats exact range filter values as exact bounds", (
|
|
|
526
530
|
});
|
|
527
531
|
});
|
|
528
532
|
|
|
533
|
+
test("createCrudListFilterContract builds route validators and JSON REST search schema from one definition", () => {
|
|
534
|
+
const contract = createCrudListFilterContract(
|
|
535
|
+
{
|
|
536
|
+
status: {
|
|
537
|
+
type: "enumMany",
|
|
538
|
+
label: "Status",
|
|
539
|
+
options: [
|
|
540
|
+
{ value: "active", label: "Active" },
|
|
541
|
+
{ value: "archived", label: "Archived" }
|
|
542
|
+
]
|
|
543
|
+
},
|
|
544
|
+
supplierContactId: {
|
|
545
|
+
type: "recordIdMany",
|
|
546
|
+
label: "Supplier"
|
|
547
|
+
},
|
|
548
|
+
arrivalDate: {
|
|
549
|
+
type: "dateRange",
|
|
550
|
+
label: "Arrival Date"
|
|
551
|
+
},
|
|
552
|
+
weight: {
|
|
553
|
+
type: "numberRange",
|
|
554
|
+
label: "Weight"
|
|
555
|
+
},
|
|
556
|
+
locationAssignment: {
|
|
557
|
+
type: "presence",
|
|
558
|
+
label: "Storage"
|
|
559
|
+
}
|
|
560
|
+
},
|
|
561
|
+
{
|
|
562
|
+
columns: {
|
|
563
|
+
status: "status",
|
|
564
|
+
supplierContactId: "supplier_contact_id",
|
|
565
|
+
arrivalDate: "arrival_datetime",
|
|
566
|
+
weight: "weight_received",
|
|
567
|
+
locationAssignment: "location_id"
|
|
568
|
+
},
|
|
569
|
+
invalidValues: CRUD_LIST_FILTER_INVALID_VALUES_REJECT
|
|
570
|
+
}
|
|
571
|
+
);
|
|
572
|
+
|
|
573
|
+
assert.deepEqual(contract.queryValidator.schema.patch({
|
|
574
|
+
status: ["active", "archived"],
|
|
575
|
+
supplierContactId: ["7", "4"],
|
|
576
|
+
arrivalDate: "2026-04-01..2026-04-30",
|
|
577
|
+
weight: "12.5..18",
|
|
578
|
+
locationAssignment: "missing"
|
|
579
|
+
}), {
|
|
580
|
+
validatedObject: {
|
|
581
|
+
status: ["active", "archived"],
|
|
582
|
+
supplierContactId: ["7", "4"],
|
|
583
|
+
arrivalDate: {
|
|
584
|
+
from: "2026-04-01",
|
|
585
|
+
to: "2026-04-30"
|
|
586
|
+
},
|
|
587
|
+
weight: {
|
|
588
|
+
min: 12.5,
|
|
589
|
+
max: 18
|
|
590
|
+
},
|
|
591
|
+
locationAssignment: "missing"
|
|
592
|
+
},
|
|
593
|
+
errors: {}
|
|
594
|
+
});
|
|
595
|
+
|
|
596
|
+
assert.equal(contract.jsonRestSearchSchema.status.filterOperator, "in");
|
|
597
|
+
assert.equal(contract.jsonRestSearchSchema.status.actualField, "status");
|
|
598
|
+
assert.equal(contract.jsonRestSearchSchema.supplierContactId.filterOperator, "in");
|
|
599
|
+
assert.equal(contract.jsonRestSearchSchema.__jskit_arrivalDate_from.filterOperator, ">=");
|
|
600
|
+
assert.equal(contract.jsonRestSearchSchema.__jskit_arrivalDate_toExclusive.filterOperator, "<");
|
|
601
|
+
assert.equal(contract.jsonRestSearchSchema.__jskit_weight_min.filterOperator, ">=");
|
|
602
|
+
assert.equal(contract.jsonRestSearchSchema.__jskit_weight_max.filterOperator, "<=");
|
|
603
|
+
assert.equal(typeof contract.jsonRestSearchSchema.locationAssignment.applyFilter, "function");
|
|
604
|
+
|
|
605
|
+
assert.deepEqual(contract.toJsonRestQuery({
|
|
606
|
+
q: "Merc",
|
|
607
|
+
status: ["active", "archived"],
|
|
608
|
+
supplierContactId: ["7", "4"],
|
|
609
|
+
arrivalDate: "2026-04-01..2026-04-30",
|
|
610
|
+
weight: "12.5..",
|
|
611
|
+
locationAssignment: "missing"
|
|
612
|
+
}), {
|
|
613
|
+
q: "Merc",
|
|
614
|
+
status: ["active", "archived"],
|
|
615
|
+
supplierContactId: ["7", "4"],
|
|
616
|
+
locationAssignment: "missing",
|
|
617
|
+
__jskit_arrivalDate_from: "2026-04-01 00:00:00",
|
|
618
|
+
__jskit_arrivalDate_toExclusive: "2026-05-01 00:00:00",
|
|
619
|
+
__jskit_weight_min: 12.5
|
|
620
|
+
});
|
|
621
|
+
|
|
622
|
+
const { query, calls } = createQueryDouble();
|
|
623
|
+
contract.jsonRestSearchSchema.locationAssignment.applyFilter(query, "missing");
|
|
624
|
+
assert.deepEqual(calls, [["whereNull", "location_id"]]);
|
|
625
|
+
});
|
|
626
|
+
|
|
627
|
+
test("createCrudListFilterContract defaults route validation to reject invalid values", () => {
|
|
628
|
+
const contract = createCrudListFilterContract({
|
|
629
|
+
status: {
|
|
630
|
+
type: "enum",
|
|
631
|
+
label: "Status",
|
|
632
|
+
options: [
|
|
633
|
+
{ value: "active", label: "Active" }
|
|
634
|
+
]
|
|
635
|
+
}
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
assert.deepEqual(contract.queryValidator.schema.patch({
|
|
639
|
+
status: "unexpected"
|
|
640
|
+
}), {
|
|
641
|
+
validatedObject: {
|
|
642
|
+
status: "unexpected"
|
|
643
|
+
},
|
|
644
|
+
errors: {
|
|
645
|
+
status: {
|
|
646
|
+
field: "status",
|
|
647
|
+
code: "TYPE_CAST_FAILED",
|
|
648
|
+
message: "Value could not be cast to the required type.",
|
|
649
|
+
params: {}
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
});
|
|
653
|
+
});
|
|
654
|
+
|
|
655
|
+
test("createCrudListFilterJsonRestSearchSchema accepts normalized filter definitions directly", () => {
|
|
656
|
+
const filters = defineCrudListFilters({
|
|
657
|
+
status: {
|
|
658
|
+
type: "enum",
|
|
659
|
+
label: "Status",
|
|
660
|
+
options: [
|
|
661
|
+
{ value: "active", label: "Active" }
|
|
662
|
+
],
|
|
663
|
+
meta: {
|
|
664
|
+
jsonRest: {
|
|
665
|
+
actualField: "status_sid"
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
});
|
|
670
|
+
|
|
671
|
+
assert.deepEqual(createCrudListFilterJsonRestSearchSchema(filters), {
|
|
672
|
+
status: {
|
|
673
|
+
type: "string",
|
|
674
|
+
actualField: "status_sid",
|
|
675
|
+
filterOperator: "=",
|
|
676
|
+
enum: ["active"]
|
|
677
|
+
}
|
|
678
|
+
});
|
|
679
|
+
});
|
|
680
|
+
|
|
529
681
|
test("createCrudListFilters exposes no default query validator alias", () => {
|
|
530
682
|
const runtime = createCrudListFilters({
|
|
531
683
|
arrivalDate: {
|
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
validateSchemaPayload
|
|
6
6
|
} from "@jskit-ai/kernel/shared/validators";
|
|
7
7
|
import { returnJsonApiData } from "@jskit-ai/http-runtime/shared/validators/jsonApiResult";
|
|
8
|
+
import { createCrudListFilterContract } from "../src/server/listFilters.js";
|
|
8
9
|
import { createCrudJsonApiRouteContracts } from "../src/server/routeContracts.js";
|
|
9
10
|
|
|
10
11
|
function createSchemaDefinition(structure = {}, mode = "patch") {
|
|
@@ -304,6 +305,57 @@ test("createCrudJsonApiRouteContracts builds default CRUD JSON:API contracts", a
|
|
|
304
305
|
]);
|
|
305
306
|
});
|
|
306
307
|
|
|
308
|
+
test("createCrudJsonApiRouteContracts includes configured list filter validators", async () => {
|
|
309
|
+
const listFilterContract = createCrudListFilterContract({
|
|
310
|
+
status: {
|
|
311
|
+
type: "enumMany",
|
|
312
|
+
label: "Status",
|
|
313
|
+
options: [
|
|
314
|
+
{ value: "active", label: "Active" },
|
|
315
|
+
{ value: "archived", label: "Archived" }
|
|
316
|
+
]
|
|
317
|
+
}
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
const contracts = createCrudJsonApiRouteContracts({
|
|
321
|
+
resource: createCrudResource(),
|
|
322
|
+
listFilterQueryValidator: listFilterContract.queryValidator
|
|
323
|
+
});
|
|
324
|
+
|
|
325
|
+
assert.deepEqual(await validateSchemaPayload(contracts.listRouteContract.query, {
|
|
326
|
+
status: ["active", "archived"]
|
|
327
|
+
}, { phase: "input" }), {
|
|
328
|
+
status: ["active", "archived"]
|
|
329
|
+
});
|
|
330
|
+
});
|
|
331
|
+
|
|
332
|
+
test("createCrudJsonApiRouteContracts reads list filter validators from the resource contract", async () => {
|
|
333
|
+
const listFilterContract = createCrudListFilterContract({
|
|
334
|
+
status: {
|
|
335
|
+
type: "enum",
|
|
336
|
+
label: "Status",
|
|
337
|
+
options: [
|
|
338
|
+
{ value: "active", label: "Active" },
|
|
339
|
+
{ value: "archived", label: "Archived" }
|
|
340
|
+
]
|
|
341
|
+
}
|
|
342
|
+
});
|
|
343
|
+
const resource = {
|
|
344
|
+
...createCrudResource(),
|
|
345
|
+
contract: {
|
|
346
|
+
listFilters: listFilterContract
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const contracts = createCrudJsonApiRouteContracts({ resource });
|
|
351
|
+
|
|
352
|
+
assert.deepEqual(await validateSchemaPayload(contracts.listRouteContract.query, {
|
|
353
|
+
status: "archived"
|
|
354
|
+
}, { phase: "input" }), {
|
|
355
|
+
status: "archived"
|
|
356
|
+
});
|
|
357
|
+
});
|
|
358
|
+
|
|
307
359
|
test("createCrudJsonApiRouteContracts serializes collection relationships from hydrated lookups", () => {
|
|
308
360
|
const output = createSchemaDefinition({
|
|
309
361
|
id: {
|