@cobre-npm/library-portal-core 0.53.0 → 0.55.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -13
- package/dist/cjs/countries/base/utils/country.utils.d.ts +7 -0
- package/dist/cjs/countries/base/utils/country.utils.js +9 -1
- package/dist/cjs/countries/iso/utils/country-iso.utils.d.ts +7 -0
- package/dist/cjs/countries/iso/utils/country-iso.utils.js +9 -1
- package/dist/cjs/exports/index.d.ts +7 -2
- package/dist/cjs/exports/index.js +8 -3
- package/dist/cjs/exports/interfaces/translation.interface.d.ts +11 -0
- package/dist/cjs/exports/interfaces/translation.interface.js +2 -0
- package/dist/cjs/exports/utils/search-filter.utils.d.ts +19 -0
- package/dist/cjs/exports/utils/search-filter.utils.js +68 -0
- package/dist/esm/countries/base/utils/country.utils.js +7 -0
- package/dist/esm/countries/iso/utils/country-iso.utils.js +7 -0
- package/dist/esm/exports/index.js +8 -3
- package/dist/esm/exports/interfaces/translation.interface.js +1 -0
- package/dist/esm/exports/utils/search-filter.utils.js +64 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -221,19 +221,34 @@ Groups two catalogs:
|
|
|
221
221
|
|
|
222
222
|
```ts
|
|
223
223
|
import {
|
|
224
|
-
getCountries, type Country, type CountryOption,
|
|
225
|
-
getCountriesISO, getIndicativesWithCodes, hasCountryISOFlag,
|
|
226
|
-
type CountryISO, type CountryISOOption, type IndicativeWithCode,
|
|
224
|
+
getCountries, queryCountries, type Country, type CountryOption, type CountryQuery,
|
|
225
|
+
getCountriesISO, queryCountriesISO, getIndicativesWithCodes, hasCountryISOFlag,
|
|
226
|
+
type CountryISO, type CountryISOOption, type CountryISOQuery, type IndicativeWithCode,
|
|
227
227
|
} from "@cobre-npm/library-portal-core/countries"
|
|
228
228
|
|
|
229
|
-
getCountries("en").col.label
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
229
|
+
getCountries("en").col.label // "Colombia"
|
|
230
|
+
queryCountries({ lang: "en", code: ["mex", "col"] }) // [ mex, col ] → the list a picker needs
|
|
231
|
+
getCountriesISO("en").col // { code:"col", isoCode:"COL", indicative:"+57", label:"Colombia" }
|
|
232
|
+
queryCountriesISO({ lang: "en", isoCode: "MEX" }) // [ mex ] → filter the full ISO catalog
|
|
233
|
+
getIndicativesWithCodes("en") // view-model for a phone dialing-code selector
|
|
234
|
+
hasCountryISOFlag("col") // true — whether a flag asset is available
|
|
233
235
|
```
|
|
234
236
|
|
|
235
237
|
- **base** (i18n): business countries (`col`, `mex`, `usa`, `global`, …).
|
|
238
|
+
`queryCountries({ lang, ... })` returns the same catalog as a **list**, narrowed by `code` — same
|
|
239
|
+
matching rules as [`queryAccountTypes`](#accounts): `lang` is required (i18n domain, no default) inside
|
|
240
|
+
the query object, and there is no default field.
|
|
236
241
|
- **iso** (i18n): full ISO catalog (~249 countries) with `isoCode` and `indicative`.
|
|
242
|
+
`queryCountriesISO({ lang, ... })` returns the same catalog as a **list**, narrowed by `code`/`isoCode`
|
|
243
|
+
— same rules as above, over the full ISO catalog instead of the small business one.
|
|
244
|
+
|
|
245
|
+
As with every `queryX()`, **a key that is present always filters** — it's the absence of the key that
|
|
246
|
+
doesn't. So `queryCountries({ lang, code: undefined })` returns `[]`, not "no filter". An optional UI
|
|
247
|
+
filter has to be spread in conditionally:
|
|
248
|
+
|
|
249
|
+
```ts
|
|
250
|
+
queryCountries({ lang, ...(codes ? { code: codes } : {}) })
|
|
251
|
+
```
|
|
237
252
|
|
|
238
253
|
### `movements`
|
|
239
254
|
|
|
@@ -485,8 +500,9 @@ import type {
|
|
|
485
500
|
|
|
486
501
|
### `exports`
|
|
487
502
|
|
|
488
|
-
|
|
489
|
-
|
|
503
|
+
The export engine's (`util-platform-exports`) request/response contract, verified against its real DTO and
|
|
504
|
+
entity, not against what any consumer had guessed, plus its operator constants and a pure search → export filter
|
|
505
|
+
translator:
|
|
490
506
|
|
|
491
507
|
```ts
|
|
492
508
|
import {
|
|
@@ -494,6 +510,7 @@ import {
|
|
|
494
510
|
type ExportResponse, type ExportStatus, type ExportCreator, type ExportStatusState,
|
|
495
511
|
EXPORT_FORMATS, type ExportFormat,
|
|
496
512
|
EXPORT_QUERY_OPERATORS, type ExportQueryOperator,
|
|
513
|
+
toExportFilterConditions, type ExportFilterTranslation, type ExportFilterTranslationOptions,
|
|
497
514
|
} from "@cobre-npm/library-portal-core/exports"
|
|
498
515
|
```
|
|
499
516
|
|
|
@@ -502,10 +519,26 @@ import {
|
|
|
502
519
|
`status`/`updated_at`/`export_path` in place as it processes. Both the exports list/status view in
|
|
503
520
|
`portal` and any widget that creates exports read/write this one shape; keeping two hand-typed copies
|
|
504
521
|
is what let each one drift from the real contract in its own way (see the two fixes below).
|
|
505
|
-
- `EXPORT_QUERY_OPERATORS` is the engine's **real** allow-list (`eq`, `neq`, `gt`, `lt`, `lte`, `gte`
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
522
|
+
- `EXPORT_QUERY_OPERATORS` is the engine's **real** allow-list (`eq`, `neq`, `gt`, `lt`, `lte`, `gte`, `in`, `like`,
|
|
523
|
+
`between`); `in` and `between` take comma-separated values. A layout's catalog declares which ones each column
|
|
524
|
+
accepts, so a consumer validating a filter before sending it needs the intersection of both.
|
|
525
|
+
- `toExportFilterConditions(filter, { fieldMap })` translates the `SearchFilter` a table sends to the search API into
|
|
526
|
+
`ExportFilterCondition[]`, so the file holds exactly the rows the table shows. `eq` stays, `not_eq` → `neq`,
|
|
527
|
+
`contains` → `like` (both case-insensitive substring matches), `gte`/`lte` stay, and an OR of `eq` on one field (a
|
|
528
|
+
multi-select filter) → `in`. Date conditions are translated like any other; checking them against the export's
|
|
529
|
+
31-day limit is the caller's job. What has no export equivalent — `exists`, a blank `contains`, an OR across
|
|
530
|
+
fields or operators (a table's text search), an `in` value holding a comma, an empty OR (which matches nothing in
|
|
531
|
+
the search) — comes back in `untranslatable` instead of being dropped, so the caller can block the export rather
|
|
532
|
+
than ship a wider file. `fieldMap` renames only the fields whose layout column differs:
|
|
533
|
+
|
|
534
|
+
```ts
|
|
535
|
+
// filter: the same SearchFilter the view sends to the search API
|
|
536
|
+
toExportFilterConditions(filter, { fieldMap: { currency: "currency_pair" } })
|
|
537
|
+
// { conditions: [
|
|
538
|
+
// { column: "currency_pair", operator: "in", value: "usd/cop,usd/mxn" },
|
|
539
|
+
// { column: "created_at", operator: "gte", value: "2026-10-01T00:00:00" }
|
|
540
|
+
// ], untranslatable: [] }
|
|
541
|
+
```
|
|
509
542
|
- `ExportFilterCondition` is an `ExportQueryFilter` before its `type` is resolved
|
|
510
543
|
(`{ column: "state", operator: "neq", value: "pending_funds" }`). The type belongs to the layout's catalog,
|
|
511
544
|
so whoever reads the catalog fills it in and the column's type keeps one source of truth.
|
|
@@ -1,6 +1,13 @@
|
|
|
1
1
|
import type { Country, CountryRecord } from "../catalog";
|
|
2
2
|
import type { Locale } from "../../../lang";
|
|
3
|
+
import type { CatalogQuery } from "../../../utils/catalog-query.utils";
|
|
3
4
|
export type CountryOption = CountryRecord & {
|
|
4
5
|
label: string;
|
|
5
6
|
};
|
|
6
7
|
export declare const getCountries: (lang: Locale) => Record<Country, CountryOption>;
|
|
8
|
+
/** `lang` is required (i18n domain, no default) and travels inside the query object, never positional. */
|
|
9
|
+
export type CountryQuery = CatalogQuery<CountryRecord> & {
|
|
10
|
+
lang: Locale;
|
|
11
|
+
};
|
|
12
|
+
/** The catalog as a list, narrowed by the query — see `matchesQuery` for the matching rules. */
|
|
13
|
+
export declare const queryCountries: (query: CountryQuery) => CountryOption[];
|
|
@@ -3,10 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getCountries = void 0;
|
|
6
|
+
exports.queryCountries = exports.getCountries = void 0;
|
|
7
7
|
const catalog_1 = require("../catalog");
|
|
8
8
|
const en_json_1 = __importDefault(require("../locales/en.json"));
|
|
9
9
|
const es_mex_json_1 = __importDefault(require("../locales/es-mex.json"));
|
|
10
|
+
const catalog_query_utils_1 = require("../../../utils/catalog-query.utils");
|
|
10
11
|
const locales = { en: en_json_1.default, "es-mex": es_mex_json_1.default };
|
|
11
12
|
const getCountries = (lang) => {
|
|
12
13
|
const locale = locales[lang];
|
|
@@ -17,3 +18,10 @@ const getCountries = (lang) => {
|
|
|
17
18
|
return result;
|
|
18
19
|
};
|
|
19
20
|
exports.getCountries = getCountries;
|
|
21
|
+
/** The catalog as a list, narrowed by the query — see `matchesQuery` for the matching rules. */
|
|
22
|
+
const queryCountries = (query) => {
|
|
23
|
+
const { lang, ...criteria } = query;
|
|
24
|
+
const countries = (0, exports.getCountries)(lang);
|
|
25
|
+
return Object.values(countries).filter(country => (0, catalog_query_utils_1.matchesQuery)(country, criteria));
|
|
26
|
+
};
|
|
27
|
+
exports.queryCountries = queryCountries;
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
import type { CountryISO, CountryISORecord } from "../catalog";
|
|
2
2
|
import type { Locale } from "../../../lang";
|
|
3
|
+
import type { CatalogQuery } from "../../../utils/catalog-query.utils";
|
|
3
4
|
export declare const hasCountryISOFlag: (code: CountryISO) => boolean;
|
|
4
5
|
export type CountryISOOption = CountryISORecord & {
|
|
5
6
|
label: string;
|
|
@@ -12,4 +13,10 @@ export interface IndicativeWithCode {
|
|
|
12
13
|
isoCode: string | undefined;
|
|
13
14
|
}
|
|
14
15
|
export declare const getCountriesISO: (lang: Locale) => Record<CountryISO, CountryISOOption>;
|
|
16
|
+
/** `lang` is required (i18n domain, no default) and travels inside the query object, never positional. */
|
|
17
|
+
export type CountryISOQuery = CatalogQuery<CountryISORecord> & {
|
|
18
|
+
lang: Locale;
|
|
19
|
+
};
|
|
20
|
+
/** The catalog as a list, narrowed by the query — see `matchesQuery` for the matching rules. */
|
|
21
|
+
export declare const queryCountriesISO: (query: CountryISOQuery) => CountryISOOption[];
|
|
15
22
|
export declare const getIndicativesWithCodes: (lang: Locale) => IndicativeWithCode[];
|
|
@@ -3,10 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.getIndicativesWithCodes = exports.getCountriesISO = exports.hasCountryISOFlag = void 0;
|
|
6
|
+
exports.getIndicativesWithCodes = exports.queryCountriesISO = exports.getCountriesISO = exports.hasCountryISOFlag = void 0;
|
|
7
7
|
const catalog_1 = require("../catalog");
|
|
8
8
|
const en_json_1 = __importDefault(require("../locales/en.json"));
|
|
9
9
|
const es_mex_json_1 = __importDefault(require("../locales/es-mex.json"));
|
|
10
|
+
const catalog_query_utils_1 = require("../../../utils/catalog-query.utils");
|
|
10
11
|
const locales = { en: en_json_1.default, "es-mex": es_mex_json_1.default };
|
|
11
12
|
const AVAILABLE_FLAG_CODES = new Set(["col", "mex", "usa"]);
|
|
12
13
|
const hasCountryISOFlag = (code) => AVAILABLE_FLAG_CODES.has(code);
|
|
@@ -20,6 +21,13 @@ const getCountriesISO = (lang) => {
|
|
|
20
21
|
return result;
|
|
21
22
|
};
|
|
22
23
|
exports.getCountriesISO = getCountriesISO;
|
|
24
|
+
/** The catalog as a list, narrowed by the query — see `matchesQuery` for the matching rules. */
|
|
25
|
+
const queryCountriesISO = (query) => {
|
|
26
|
+
const { lang, ...criteria } = query;
|
|
27
|
+
const countries = (0, exports.getCountriesISO)(lang);
|
|
28
|
+
return Object.values(countries).filter(country => (0, catalog_query_utils_1.matchesQuery)(country, criteria));
|
|
29
|
+
};
|
|
30
|
+
exports.queryCountriesISO = queryCountriesISO;
|
|
23
31
|
const getIndicativesWithCodes = (lang) => {
|
|
24
32
|
const locale = locales[lang];
|
|
25
33
|
const seenDialCodes = new Set();
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
export * from "./interfaces/common.interface";
|
|
2
|
+
export * from "./interfaces/translation.interface";
|
|
2
3
|
export * from "./interfaces/request.interface";
|
|
3
4
|
export * from "./interfaces/response.interface";
|
|
5
|
+
export * from "./utils/search-filter.utils";
|
|
4
6
|
export declare const EXPORT_FORMATS: {
|
|
5
7
|
readonly CSV: "csv";
|
|
6
8
|
readonly JSON: "json";
|
|
@@ -8,8 +10,8 @@ export declare const EXPORT_FORMATS: {
|
|
|
8
10
|
export type ExportFormat = typeof EXPORT_FORMATS[keyof typeof EXPORT_FORMATS];
|
|
9
11
|
/**
|
|
10
12
|
* The export engine's real allow-list (verified against util-platform-exports'
|
|
11
|
-
* QueryOperator enum). A layout's catalog
|
|
12
|
-
*
|
|
13
|
+
* QueryOperator enum). `in` and `between` take comma-separated values. A layout's catalog declares which of
|
|
14
|
+
* them each column accepts, so a consumer validating a filter needs both lists.
|
|
13
15
|
*/
|
|
14
16
|
export declare const EXPORT_QUERY_OPERATORS: {
|
|
15
17
|
readonly EQ: "eq";
|
|
@@ -18,5 +20,8 @@ export declare const EXPORT_QUERY_OPERATORS: {
|
|
|
18
20
|
readonly LT: "lt";
|
|
19
21
|
readonly LTE: "lte";
|
|
20
22
|
readonly GTE: "gte";
|
|
23
|
+
readonly IN: "in";
|
|
24
|
+
readonly LIKE: "like";
|
|
25
|
+
readonly BETWEEN: "between";
|
|
21
26
|
};
|
|
22
27
|
export type ExportQueryOperator = typeof EXPORT_QUERY_OPERATORS[keyof typeof EXPORT_QUERY_OPERATORS];
|
|
@@ -16,16 +16,18 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
|
|
|
16
16
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
17
17
|
exports.EXPORT_QUERY_OPERATORS = exports.EXPORT_FORMATS = void 0;
|
|
18
18
|
__exportStar(require("./interfaces/common.interface"), exports);
|
|
19
|
+
__exportStar(require("./interfaces/translation.interface"), exports);
|
|
19
20
|
__exportStar(require("./interfaces/request.interface"), exports);
|
|
20
21
|
__exportStar(require("./interfaces/response.interface"), exports);
|
|
22
|
+
__exportStar(require("./utils/search-filter.utils"), exports);
|
|
21
23
|
exports.EXPORT_FORMATS = {
|
|
22
24
|
CSV: "csv",
|
|
23
25
|
JSON: "json"
|
|
24
26
|
};
|
|
25
27
|
/**
|
|
26
28
|
* The export engine's real allow-list (verified against util-platform-exports'
|
|
27
|
-
* QueryOperator enum). A layout's catalog
|
|
28
|
-
*
|
|
29
|
+
* QueryOperator enum). `in` and `between` take comma-separated values. A layout's catalog declares which of
|
|
30
|
+
* them each column accepts, so a consumer validating a filter needs both lists.
|
|
29
31
|
*/
|
|
30
32
|
exports.EXPORT_QUERY_OPERATORS = {
|
|
31
33
|
EQ: "eq",
|
|
@@ -33,5 +35,8 @@ exports.EXPORT_QUERY_OPERATORS = {
|
|
|
33
35
|
GT: "gt",
|
|
34
36
|
LT: "lt",
|
|
35
37
|
LTE: "lte",
|
|
36
|
-
GTE: "gte"
|
|
38
|
+
GTE: "gte",
|
|
39
|
+
IN: "in",
|
|
40
|
+
LIKE: "like",
|
|
41
|
+
BETWEEN: "between"
|
|
37
42
|
};
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import type { SearchCondition, SearchOrFilter } from "../../search/interfaces/filters.interface";
|
|
2
|
+
import type { ExportFilterCondition } from "./common.interface";
|
|
3
|
+
export interface ExportFilterTranslationOptions {
|
|
4
|
+
/** Search field → layout column, only where the two names differ (e.g. `{ currency: "currency_pair" }`). */
|
|
5
|
+
fieldMap?: Readonly<Record<string, string>>;
|
|
6
|
+
}
|
|
7
|
+
export interface ExportFilterTranslation {
|
|
8
|
+
conditions: ExportFilterCondition[];
|
|
9
|
+
/** Parts of the search filter with no export equivalent, so the caller can block or warn instead of dropping them. */
|
|
10
|
+
untranslatable: Array<SearchCondition | SearchOrFilter>;
|
|
11
|
+
}
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { SearchFilter } from "../../search/interfaces/filters.interface";
|
|
2
|
+
import type { ExportFilterTranslationOptions, ExportFilterTranslation } from "../interfaces/translation.interface";
|
|
3
|
+
/**
|
|
4
|
+
* Translates the filter a table sends to the search API into the conditions the exports API takes, so an
|
|
5
|
+
* export can match what the table shows. What has no export equivalent comes back in `untranslatable`.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* toExportFilterConditions({
|
|
9
|
+
* and: [
|
|
10
|
+
* { or: [{ field: "state", operator: "eq", value: "completed" }, { field: "state", operator: "eq", value: "failed" }] },
|
|
11
|
+
* { field: "created_at", operator: "gte", value: "2026-10-01T00:00:00" },
|
|
12
|
+
* ],
|
|
13
|
+
* })
|
|
14
|
+
* // { conditions: [
|
|
15
|
+
* // { column: "state", operator: "in", value: "completed,failed" },
|
|
16
|
+
* // { column: "created_at", operator: "gte", value: "2026-10-01T00:00:00" }
|
|
17
|
+
* // ], untranslatable: [] }
|
|
18
|
+
*/
|
|
19
|
+
export declare const toExportFilterConditions: (filter: SearchFilter, options?: ExportFilterTranslationOptions) => ExportFilterTranslation;
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.toExportFilterConditions = void 0;
|
|
4
|
+
/* Literals, not EXPORT_QUERY_OPERATORS: a value import from the barrel that re-exports this file is a runtime cycle. */
|
|
5
|
+
const SEARCH_TO_EXPORT_OPERATORS = {
|
|
6
|
+
eq: "eq",
|
|
7
|
+
not_eq: "neq",
|
|
8
|
+
contains: "like",
|
|
9
|
+
gte: "gte",
|
|
10
|
+
lte: "lte",
|
|
11
|
+
};
|
|
12
|
+
/* The export splits an `in` value on commas, so a value that holds one can't travel in it. */
|
|
13
|
+
const IN_SEPARATOR = ",";
|
|
14
|
+
const isAndFilter = (filter) => "and" in filter;
|
|
15
|
+
const isOrFilter = (part) => "or" in part;
|
|
16
|
+
const toColumn = (field, { fieldMap = {} }) => fieldMap[field] ?? field;
|
|
17
|
+
const toCondition = ({ field, operator, value }, options) => {
|
|
18
|
+
const exportOperator = SEARCH_TO_EXPORT_OPERATORS[operator];
|
|
19
|
+
const exportValue = String(value);
|
|
20
|
+
/* A blank contains matches every row with a value in the search; the export rejects a blank like and has no exists. */
|
|
21
|
+
if (!exportOperator || (exportOperator === "like" && exportValue.trim() === ""))
|
|
22
|
+
return null;
|
|
23
|
+
return { column: toColumn(field, options), operator: exportOperator, value: exportValue };
|
|
24
|
+
};
|
|
25
|
+
/* A multi-select filter reaches the search as an OR of eq on one field; the export reads it as in. */
|
|
26
|
+
const toInCondition = ({ or }, options) => {
|
|
27
|
+
const field = or[0]?.field;
|
|
28
|
+
const values = or.map(({ value }) => String(value));
|
|
29
|
+
const isSingleFieldEq = field !== undefined && or.every(part => part.operator === "eq" && part.field === field);
|
|
30
|
+
if (!isSingleFieldEq || values.some(value => value.includes(IN_SEPARATOR)))
|
|
31
|
+
return null;
|
|
32
|
+
return { column: toColumn(field, options), operator: "in", value: values.join(IN_SEPARATOR) };
|
|
33
|
+
};
|
|
34
|
+
const toPartCondition = (part, options) => {
|
|
35
|
+
if (!isOrFilter(part))
|
|
36
|
+
return toCondition(part, options);
|
|
37
|
+
const [singleCondition] = part.or;
|
|
38
|
+
return part.or.length === 1 && singleCondition ? toCondition(singleCondition, options) : toInCondition(part, options);
|
|
39
|
+
};
|
|
40
|
+
/**
|
|
41
|
+
* Translates the filter a table sends to the search API into the conditions the exports API takes, so an
|
|
42
|
+
* export can match what the table shows. What has no export equivalent comes back in `untranslatable`.
|
|
43
|
+
*
|
|
44
|
+
* @example
|
|
45
|
+
* toExportFilterConditions({
|
|
46
|
+
* and: [
|
|
47
|
+
* { or: [{ field: "state", operator: "eq", value: "completed" }, { field: "state", operator: "eq", value: "failed" }] },
|
|
48
|
+
* { field: "created_at", operator: "gte", value: "2026-10-01T00:00:00" },
|
|
49
|
+
* ],
|
|
50
|
+
* })
|
|
51
|
+
* // { conditions: [
|
|
52
|
+
* // { column: "state", operator: "in", value: "completed,failed" },
|
|
53
|
+
* // { column: "created_at", operator: "gte", value: "2026-10-01T00:00:00" }
|
|
54
|
+
* // ], untranslatable: [] }
|
|
55
|
+
*/
|
|
56
|
+
const toExportFilterConditions = (filter, options = {}) => {
|
|
57
|
+
const parts = isAndFilter(filter) ? filter.and : [filter];
|
|
58
|
+
const translation = { conditions: [], untranslatable: [] };
|
|
59
|
+
parts.forEach(part => {
|
|
60
|
+
const condition = toPartCondition(part, options);
|
|
61
|
+
if (condition)
|
|
62
|
+
translation.conditions.push(condition);
|
|
63
|
+
else
|
|
64
|
+
translation.untranslatable.push(part);
|
|
65
|
+
});
|
|
66
|
+
return translation;
|
|
67
|
+
};
|
|
68
|
+
exports.toExportFilterConditions = toExportFilterConditions;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { COUNTRIES } from "../catalog";
|
|
2
2
|
import enLocale from "../locales/en.json";
|
|
3
3
|
import esMexLocale from "../locales/es-mex.json";
|
|
4
|
+
import { matchesQuery } from "../../../utils/catalog-query.utils";
|
|
4
5
|
const locales = { en: enLocale, "es-mex": esMexLocale };
|
|
5
6
|
export const getCountries = (lang) => {
|
|
6
7
|
const locale = locales[lang];
|
|
@@ -10,3 +11,9 @@ export const getCountries = (lang) => {
|
|
|
10
11
|
}
|
|
11
12
|
return result;
|
|
12
13
|
};
|
|
14
|
+
/** The catalog as a list, narrowed by the query — see `matchesQuery` for the matching rules. */
|
|
15
|
+
export const queryCountries = (query) => {
|
|
16
|
+
const { lang, ...criteria } = query;
|
|
17
|
+
const countries = getCountries(lang);
|
|
18
|
+
return Object.values(countries).filter(country => matchesQuery(country, criteria));
|
|
19
|
+
};
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { COUNTRIES_ISO } from "../catalog";
|
|
2
2
|
import enLocale from "../locales/en.json";
|
|
3
3
|
import esMexLocale from "../locales/es-mex.json";
|
|
4
|
+
import { matchesQuery } from "../../../utils/catalog-query.utils";
|
|
4
5
|
const locales = { en: enLocale, "es-mex": esMexLocale };
|
|
5
6
|
const AVAILABLE_FLAG_CODES = new Set(["col", "mex", "usa"]);
|
|
6
7
|
export const hasCountryISOFlag = (code) => AVAILABLE_FLAG_CODES.has(code);
|
|
@@ -12,6 +13,12 @@ export const getCountriesISO = (lang) => {
|
|
|
12
13
|
}
|
|
13
14
|
return result;
|
|
14
15
|
};
|
|
16
|
+
/** The catalog as a list, narrowed by the query — see `matchesQuery` for the matching rules. */
|
|
17
|
+
export const queryCountriesISO = (query) => {
|
|
18
|
+
const { lang, ...criteria } = query;
|
|
19
|
+
const countries = getCountriesISO(lang);
|
|
20
|
+
return Object.values(countries).filter(country => matchesQuery(country, criteria));
|
|
21
|
+
};
|
|
15
22
|
export const getIndicativesWithCodes = (lang) => {
|
|
16
23
|
const locale = locales[lang];
|
|
17
24
|
const seenDialCodes = new Set();
|
|
@@ -1,14 +1,16 @@
|
|
|
1
1
|
export * from "./interfaces/common.interface";
|
|
2
|
+
export * from "./interfaces/translation.interface";
|
|
2
3
|
export * from "./interfaces/request.interface";
|
|
3
4
|
export * from "./interfaces/response.interface";
|
|
5
|
+
export * from "./utils/search-filter.utils";
|
|
4
6
|
export const EXPORT_FORMATS = {
|
|
5
7
|
CSV: "csv",
|
|
6
8
|
JSON: "json"
|
|
7
9
|
};
|
|
8
10
|
/**
|
|
9
11
|
* The export engine's real allow-list (verified against util-platform-exports'
|
|
10
|
-
* QueryOperator enum). A layout's catalog
|
|
11
|
-
*
|
|
12
|
+
* QueryOperator enum). `in` and `between` take comma-separated values. A layout's catalog declares which of
|
|
13
|
+
* them each column accepts, so a consumer validating a filter needs both lists.
|
|
12
14
|
*/
|
|
13
15
|
export const EXPORT_QUERY_OPERATORS = {
|
|
14
16
|
EQ: "eq",
|
|
@@ -16,5 +18,8 @@ export const EXPORT_QUERY_OPERATORS = {
|
|
|
16
18
|
GT: "gt",
|
|
17
19
|
LT: "lt",
|
|
18
20
|
LTE: "lte",
|
|
19
|
-
GTE: "gte"
|
|
21
|
+
GTE: "gte",
|
|
22
|
+
IN: "in",
|
|
23
|
+
LIKE: "like",
|
|
24
|
+
BETWEEN: "between"
|
|
20
25
|
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/* Literals, not EXPORT_QUERY_OPERATORS: a value import from the barrel that re-exports this file is a runtime cycle. */
|
|
2
|
+
const SEARCH_TO_EXPORT_OPERATORS = {
|
|
3
|
+
eq: "eq",
|
|
4
|
+
not_eq: "neq",
|
|
5
|
+
contains: "like",
|
|
6
|
+
gte: "gte",
|
|
7
|
+
lte: "lte",
|
|
8
|
+
};
|
|
9
|
+
/* The export splits an `in` value on commas, so a value that holds one can't travel in it. */
|
|
10
|
+
const IN_SEPARATOR = ",";
|
|
11
|
+
const isAndFilter = (filter) => "and" in filter;
|
|
12
|
+
const isOrFilter = (part) => "or" in part;
|
|
13
|
+
const toColumn = (field, { fieldMap = {} }) => fieldMap[field] ?? field;
|
|
14
|
+
const toCondition = ({ field, operator, value }, options) => {
|
|
15
|
+
const exportOperator = SEARCH_TO_EXPORT_OPERATORS[operator];
|
|
16
|
+
const exportValue = String(value);
|
|
17
|
+
/* A blank contains matches every row with a value in the search; the export rejects a blank like and has no exists. */
|
|
18
|
+
if (!exportOperator || (exportOperator === "like" && exportValue.trim() === ""))
|
|
19
|
+
return null;
|
|
20
|
+
return { column: toColumn(field, options), operator: exportOperator, value: exportValue };
|
|
21
|
+
};
|
|
22
|
+
/* A multi-select filter reaches the search as an OR of eq on one field; the export reads it as in. */
|
|
23
|
+
const toInCondition = ({ or }, options) => {
|
|
24
|
+
const field = or[0]?.field;
|
|
25
|
+
const values = or.map(({ value }) => String(value));
|
|
26
|
+
const isSingleFieldEq = field !== undefined && or.every(part => part.operator === "eq" && part.field === field);
|
|
27
|
+
if (!isSingleFieldEq || values.some(value => value.includes(IN_SEPARATOR)))
|
|
28
|
+
return null;
|
|
29
|
+
return { column: toColumn(field, options), operator: "in", value: values.join(IN_SEPARATOR) };
|
|
30
|
+
};
|
|
31
|
+
const toPartCondition = (part, options) => {
|
|
32
|
+
if (!isOrFilter(part))
|
|
33
|
+
return toCondition(part, options);
|
|
34
|
+
const [singleCondition] = part.or;
|
|
35
|
+
return part.or.length === 1 && singleCondition ? toCondition(singleCondition, options) : toInCondition(part, options);
|
|
36
|
+
};
|
|
37
|
+
/**
|
|
38
|
+
* Translates the filter a table sends to the search API into the conditions the exports API takes, so an
|
|
39
|
+
* export can match what the table shows. What has no export equivalent comes back in `untranslatable`.
|
|
40
|
+
*
|
|
41
|
+
* @example
|
|
42
|
+
* toExportFilterConditions({
|
|
43
|
+
* and: [
|
|
44
|
+
* { or: [{ field: "state", operator: "eq", value: "completed" }, { field: "state", operator: "eq", value: "failed" }] },
|
|
45
|
+
* { field: "created_at", operator: "gte", value: "2026-10-01T00:00:00" },
|
|
46
|
+
* ],
|
|
47
|
+
* })
|
|
48
|
+
* // { conditions: [
|
|
49
|
+
* // { column: "state", operator: "in", value: "completed,failed" },
|
|
50
|
+
* // { column: "created_at", operator: "gte", value: "2026-10-01T00:00:00" }
|
|
51
|
+
* // ], untranslatable: [] }
|
|
52
|
+
*/
|
|
53
|
+
export const toExportFilterConditions = (filter, options = {}) => {
|
|
54
|
+
const parts = isAndFilter(filter) ? filter.and : [filter];
|
|
55
|
+
const translation = { conditions: [], untranslatable: [] };
|
|
56
|
+
parts.forEach(part => {
|
|
57
|
+
const condition = toPartCondition(part, options);
|
|
58
|
+
if (condition)
|
|
59
|
+
translation.conditions.push(condition);
|
|
60
|
+
else
|
|
61
|
+
translation.untranslatable.push(part);
|
|
62
|
+
});
|
|
63
|
+
return translation;
|
|
64
|
+
};
|