@cobre-npm/library-portal-core 0.53.0 → 0.54.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 CHANGED
@@ -485,8 +485,9 @@ import type {
485
485
 
486
486
  ### `exports`
487
487
 
488
- Interfaces only (pure types) — the export engine's (`util-platform-exports`) request/response contract,
489
- verified against its real DTO and entity, not against what any consumer had guessed:
488
+ The export engine's (`util-platform-exports`) request/response contract, verified against its real DTO and
489
+ entity, not against what any consumer had guessed, plus its operator constants and a pure search → export filter
490
+ translator:
490
491
 
491
492
  ```ts
492
493
  import {
@@ -494,6 +495,7 @@ import {
494
495
  type ExportResponse, type ExportStatus, type ExportCreator, type ExportStatusState,
495
496
  EXPORT_FORMATS, type ExportFormat,
496
497
  EXPORT_QUERY_OPERATORS, type ExportQueryOperator,
498
+ toExportFilterConditions, type ExportFilterTranslation, type ExportFilterTranslationOptions,
497
499
  } from "@cobre-npm/library-portal-core/exports"
498
500
  ```
499
501
 
@@ -502,10 +504,26 @@ import {
502
504
  `status`/`updated_at`/`export_path` in place as it processes. Both the exports list/status view in
503
505
  `portal` and any widget that creates exports read/write this one shape; keeping two hand-typed copies
504
506
  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`) — narrower
506
- than what a layout's catalog is allowed to declare (`between`, `in`, `like` are valid catalog
507
- entries the engine still rejects at request time). A consumer validating a filter before sending it
508
- needs the intersection of both, not just the catalog's.
507
+ - `EXPORT_QUERY_OPERATORS` is the engine's **real** allow-list (`eq`, `neq`, `gt`, `lt`, `lte`, `gte`, `in`, `like`,
508
+ `between`); `in` and `between` take comma-separated values. A layout's catalog declares which ones each column
509
+ accepts, so a consumer validating a filter before sending it needs the intersection of both.
510
+ - `toExportFilterConditions(filter, { fieldMap })` translates the `SearchFilter` a table sends to the search API into
511
+ `ExportFilterCondition[]`, so the file holds exactly the rows the table shows. `eq` stays, `not_eq` → `neq`,
512
+ `contains` → `like` (both case-insensitive substring matches), `gte`/`lte` stay, and an OR of `eq` on one field (a
513
+ multi-select filter) → `in`. Date conditions are translated like any other; checking them against the export's
514
+ 31-day limit is the caller's job. What has no export equivalent — `exists`, a blank `contains`, an OR across
515
+ fields or operators (a table's text search), an `in` value holding a comma, an empty OR (which matches nothing in
516
+ the search) — comes back in `untranslatable` instead of being dropped, so the caller can block the export rather
517
+ than ship a wider file. `fieldMap` renames only the fields whose layout column differs:
518
+
519
+ ```ts
520
+ // filter: the same SearchFilter the view sends to the search API
521
+ toExportFilterConditions(filter, { fieldMap: { currency: "currency_pair" } })
522
+ // { conditions: [
523
+ // { column: "currency_pair", operator: "in", value: "usd/cop,usd/mxn" },
524
+ // { column: "created_at", operator: "gte", value: "2026-10-01T00:00:00" }
525
+ // ], untranslatable: [] }
526
+ ```
509
527
  - `ExportFilterCondition` is an `ExportQueryFilter` before its `type` is resolved
510
528
  (`{ column: "state", operator: "neq", value: "pending_funds" }`). The type belongs to the layout's catalog,
511
529
  so whoever reads the catalog fills it in and the column's type keeps one source of truth.
@@ -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 can declare more operators than this
12
- * ("between", "in", "like") — those are rejected by the engine at request time.
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 can declare more operators than this
28
- * ("between", "in", "like") — those are rejected by the engine at request time.
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,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -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,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 can declare more operators than this
11
- * ("between", "in", "like") — those are rejected by the engine at request time.
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,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
+ };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cobre-npm/library-portal-core",
3
- "version": "0.53.0",
3
+ "version": "0.54.0",
4
4
  "description": "Shared configurations and resources for Portal MFEs",
5
5
  "main": "./dist/cjs/index.js",
6
6
  "module": "./dist/esm/index.js",