@ballistix.digital/odata-query-builder 0.0.0-stage → 0.1.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.
Files changed (92) hide show
  1. package/README.md +166 -2
  2. package/dist/index.d.ts +31 -0
  3. package/dist/index.js +31 -0
  4. package/dist/index.js.map +1 -0
  5. package/dist/module/common/domain/exceptionCode.model.d.ts +12 -0
  6. package/dist/module/common/domain/exceptionCode.model.js +17 -0
  7. package/dist/module/common/domain/exceptionCode.model.js.map +1 -0
  8. package/dist/module/common/domain/exceptionDetail.model.d.ts +14 -0
  9. package/dist/module/common/domain/exceptionDetail.model.js +11 -0
  10. package/dist/module/common/domain/exceptionDetail.model.js.map +1 -0
  11. package/dist/module/common/domain/scalar.model.d.ts +15 -0
  12. package/dist/module/common/domain/scalar.model.js +14 -0
  13. package/dist/module/common/domain/scalar.model.js.map +1 -0
  14. package/dist/module/common/exception/queryBuilder.exception.d.ts +27 -0
  15. package/dist/module/common/exception/queryBuilder.exception.js +37 -0
  16. package/dist/module/common/exception/queryBuilder.exception.js.map +1 -0
  17. package/dist/module/common/util/escape.util.d.ts +55 -0
  18. package/dist/module/common/util/escape.util.js +83 -0
  19. package/dist/module/common/util/escape.util.js.map +1 -0
  20. package/dist/module/query/domain/chain/chain.model.d.ts +33 -0
  21. package/dist/module/query/domain/chain/chain.model.js +3 -0
  22. package/dist/module/query/domain/chain/chain.model.js.map +1 -0
  23. package/dist/module/query/domain/chain/expandNamespace.model.d.ts +56 -0
  24. package/dist/module/query/domain/chain/expandNamespace.model.js +3 -0
  25. package/dist/module/query/domain/chain/expandNamespace.model.js.map +1 -0
  26. package/dist/module/query/domain/chain/itemChain.model.d.ts +19 -0
  27. package/dist/module/query/domain/chain/itemChain.model.js +3 -0
  28. package/dist/module/query/domain/chain/itemChain.model.js.map +1 -0
  29. package/dist/module/query/domain/chain/projectChain.model.d.ts +28 -0
  30. package/dist/module/query/domain/chain/projectChain.model.js +3 -0
  31. package/dist/module/query/domain/chain/projectChain.model.js.map +1 -0
  32. package/dist/module/query/domain/filter/booleanExpr.model.d.ts +13 -0
  33. package/dist/module/query/domain/filter/booleanExpr.model.js +3 -0
  34. package/dist/module/query/domain/filter/booleanExpr.model.js.map +1 -0
  35. package/dist/module/query/domain/filter/castable.model.d.ts +17 -0
  36. package/dist/module/query/domain/filter/castable.model.js +3 -0
  37. package/dist/module/query/domain/filter/castable.model.js.map +1 -0
  38. package/dist/module/query/domain/filter/comparable.model.d.ts +32 -0
  39. package/dist/module/query/domain/filter/comparable.model.js +3 -0
  40. package/dist/module/query/domain/filter/comparable.model.js.map +1 -0
  41. package/dist/module/query/domain/filter/dateExpr.model.d.ts +35 -0
  42. package/dist/module/query/domain/filter/dateExpr.model.js +3 -0
  43. package/dist/module/query/domain/filter/dateExpr.model.js.map +1 -0
  44. package/dist/module/query/domain/filter/node.model.d.ts +21 -0
  45. package/dist/module/query/domain/filter/node.model.js +10 -0
  46. package/dist/module/query/domain/filter/node.model.js.map +1 -0
  47. package/dist/module/query/domain/filter/numberExpr.model.d.ts +22 -0
  48. package/dist/module/query/domain/filter/numberExpr.model.js +3 -0
  49. package/dist/module/query/domain/filter/numberExpr.model.js.map +1 -0
  50. package/dist/module/query/domain/filter/operators.model.d.ts +25 -0
  51. package/dist/module/query/domain/filter/operators.model.js +3 -0
  52. package/dist/module/query/domain/filter/operators.model.js.map +1 -0
  53. package/dist/module/query/domain/filter/scope.model.d.ts +94 -0
  54. package/dist/module/query/domain/filter/scope.model.js +3 -0
  55. package/dist/module/query/domain/filter/scope.model.js.map +1 -0
  56. package/dist/module/query/domain/filter/stringExpr.model.d.ts +33 -0
  57. package/dist/module/query/domain/filter/stringExpr.model.js +3 -0
  58. package/dist/module/query/domain/filter/stringExpr.model.js.map +1 -0
  59. package/dist/module/query/domain/queryState.model.d.ts +37 -0
  60. package/dist/module/query/domain/queryState.model.js +3 -0
  61. package/dist/module/query/domain/queryState.model.js.map +1 -0
  62. package/dist/module/query/exception/invalidFilter.exception.d.ts +4 -0
  63. package/dist/module/query/exception/invalidFilter.exception.js +21 -0
  64. package/dist/module/query/exception/invalidFilter.exception.js.map +1 -0
  65. package/dist/module/query/exception/reservedName.exception.d.ts +5 -0
  66. package/dist/module/query/exception/reservedName.exception.js +27 -0
  67. package/dist/module/query/exception/reservedName.exception.js.map +1 -0
  68. package/dist/module/query/service/emit/emit.facade.d.ts +8 -0
  69. package/dist/module/query/service/emit/emit.facade.js +31 -0
  70. package/dist/module/query/service/emit/emit.facade.js.map +1 -0
  71. package/dist/module/query/service/filter/filter.facade.d.ts +22 -0
  72. package/dist/module/query/service/filter/filter.facade.js +79 -0
  73. package/dist/module/query/service/filter/filter.facade.js.map +1 -0
  74. package/dist/module/query/service/queryChain.service.d.ts +53 -0
  75. package/dist/module/query/service/queryChain.service.js +97 -0
  76. package/dist/module/query/service/queryChain.service.js.map +1 -0
  77. package/dist/module/query/util/emit/queryParts.util.d.ts +16 -0
  78. package/dist/module/query/util/emit/queryParts.util.js +56 -0
  79. package/dist/module/query/util/emit/queryParts.util.js.map +1 -0
  80. package/dist/module/query/util/filter/filterFunction.util.d.ts +9 -0
  81. package/dist/module/query/util/filter/filterFunction.util.js +111 -0
  82. package/dist/module/query/util/filter/filterFunction.util.js.map +1 -0
  83. package/dist/module/query/util/filter/literal.util.d.ts +23 -0
  84. package/dist/module/query/util/filter/literal.util.js +81 -0
  85. package/dist/module/query/util/filter/literal.util.js.map +1 -0
  86. package/dist/module/query/util/filter/predicate.util.d.ts +21 -0
  87. package/dist/module/query/util/filter/predicate.util.js +35 -0
  88. package/dist/module/query/util/filter/predicate.util.js.map +1 -0
  89. package/dist/module/query/util/filter/scopeProxy.util.d.ts +22 -0
  90. package/dist/module/query/util/filter/scopeProxy.util.js +93 -0
  91. package/dist/module/query/util/filter/scopeProxy.util.js.map +1 -0
  92. package/package.json +65 -4
package/README.md CHANGED
@@ -1,3 +1,167 @@
1
- # Temporary Holding Version
1
+ # @ballistix.digital/odata-query-builder
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ A typed, fluent builder for the OData v4 query strings that
4
+ `@ballistix.digital/typeorm-extensions` serves.
5
+
6
+ It builds a **query string**. It does not fetch, and it does not know about the
7
+ `_embedded` / `_page` response envelope. It has **zero runtime dependencies**.
8
+
9
+ Full documentation lives in [`docs/`](docs/README.md) — the developer entry
10
+ point, with a module map and one page per pipeline stage.
11
+
12
+ ```ts
13
+ import { odataQuery, ProjectStatusEnum } from '@ballistix.digital/odata-query-builder';
14
+
15
+ const query = odataQuery<ProjectDto>()
16
+ .filter((v, o) =>
17
+ o.and(
18
+ v.archived.isFalse(),
19
+ v.status.in([ProjectStatusEnum.DRAFT, ProjectStatusEnum.ACTIVE]),
20
+ v.children.any((a) => a.priority.ge(2)),
21
+ ),
22
+ )
23
+ .select('id', 'name', 'status')
24
+ .expand.parent((p) => p.select('name').expand.owner((u) => u.select('fullName')))
25
+ .orderBy('name', 'desc')
26
+ .top(25)
27
+ .count()
28
+ .toQueryString();
29
+ ```
30
+
31
+ ## What makes it typed
32
+
33
+ The scope a `filter()` callback receives is derived from the DTO type alone:
34
+
35
+ - an own scalar column becomes an expression typed by the column's own type, so
36
+ an enum column takes enum members and a number column refuses a string;
37
+ - a `_embedded` to-one relation becomes a nested scope, emitted with `/`;
38
+ - a `_embedded` to-many relation becomes a lambda namespace, so `any` and `all`
39
+ are the only things reachable on it — which is what the server accepts;
40
+ - a primitive array column (`text[]`) becomes a lambda namespace over the
41
+ element.
42
+
43
+ `expand.<relation>` lists exactly the relations `_embedded` declares. There is
44
+ no code generation, no `$metadata` document, and no configuration: the DTO
45
+ convention is `_embedded`, and the library assumes it.
46
+
47
+ ## A property read is never a method
48
+
49
+ `v.year` is ambiguous at runtime: the `year()` function, or a column called
50
+ `year`. Reading a property therefore never resolves a function. Read further
51
+ from it and it was a path segment; call it and it was a function. Both roles
52
+ work in one expression:
53
+
54
+ ```ts
55
+ v.year.eq(v.date.asDate().year());
56
+ // year eq year(date)
57
+ ```
58
+
59
+ ## The surface
60
+
61
+ | Verb | Emits |
62
+ | --- | --- |
63
+ | `filter(cb)` | `$filter`; repeated calls AND together |
64
+ | `search(term, cols)` | `$search` + the mandatory `$searchColumns` |
65
+ | `select(...)` | `$select`; the own columns plus embedded leaf paths; omit it and nothing is narrowed, at any depth |
66
+ | `expand.<rel>(cb?)` | `$expand`, with nested `$filter`/`$select`/`$expand` |
67
+ | `orderBy(key, direction?)` | `$orderby`, ascending by default; `key` may be a to-one relation path or an embedded leaf path |
68
+ | `top` / `skip` / `count` | `$top` / `$skip` / `$count=true` |
69
+ | `toQueryString()` | the whole string, already URL-safe |
70
+ | `toParams()` | the same options as a map, escaping undone |
71
+
72
+ `odataItem<T>()` is the single-row variant: `$select` and `$expand` only.
73
+
74
+ An embedded object column (a DTO property that is a plain object) reads as a
75
+ namespace of columns, so `v.address.geo.lat` filters, selects and sorts as
76
+ `address/geo/lat`.
77
+
78
+ Inside a filter callback the second argument carries the combinators —
79
+ `o.and`, `o.or`, `o.not`, `o.now`, `o.maxdatetime`, `o.mindatetime`, `o.raw` —
80
+ and the same functions are importable. A single predicate needs no combinator.
81
+
82
+ `isNull()` and `isNotNull()` are how you compare against null; `eq(null)` is a
83
+ compile error, because "no filter" and "is null" must not look alike.
84
+
85
+ ## Filtered expansions
86
+
87
+ From `@ballistix.digital/typeorm-extensions` 3.8.0 the server accepts a nested
88
+ `$filter`, so the nested chain has `filter()`:
89
+
90
+ ```ts
91
+ odataQuery<ProjectDto>()
92
+ .expand.children((c) => c.filter((x) => x.priority.ge(2023)).select('priority'));
93
+ // $expand=children($filter=priority ge 2023;$select=priority)
94
+ ```
95
+
96
+ The filter narrows the EXPANSION only. Every root row still comes back: a row
97
+ whose collection matched nothing arrives with an empty collection, a to-one
98
+ expansion that fails the filter arrives as null, and `$count` and the paging
99
+ window are unchanged.
100
+
101
+ `$orderby`, `$top`, `$skip`, `$count` and `$levels` inside `$expand` are still
102
+ rejected by the server, so they are still absent from the nested chain.
103
+
104
+ ## Escaping
105
+
106
+ `toQueryString()` is already URL-safe. **Do not** run `encodeURIComponent` over
107
+ it — that turns `%25` into `%2525`. Six rules, each measured against the
108
+ server's own parser:
109
+
110
+ | In a literal | Emitted as | Why |
111
+ | --- | --- | --- |
112
+ | `'` | `''` | the OData string escape |
113
+ | `%` | `%25` | the query string is percent-decoded before parsing |
114
+ | `+` | `%2B` | a bare `+` is read as whitespace |
115
+ | `&` | `%26` | a bare `&` starts another query option |
116
+ | `#` | `%23` | a bare `#` starts the URL fragment |
117
+ | space | `%20` | only inside literals; the spaces between tokens stay raw |
118
+
119
+ Use `toParams()` when the caller has its own serialiser (`URLSearchParams`,
120
+ ky): it hands back the raw values so that serialiser's own encoding is the only
121
+ one applied.
122
+
123
+ ## Tests
124
+
125
+ The suite is split into five Jest projects, each runnable on its own.
126
+
127
+ | Command | Needs Postgres | What it covers |
128
+ | --- | --- | --- |
129
+ | `npm run test:unit` | no | Golden strings for the whole emission surface, the combination matrix, `toParams()` equivalence, every runtime guard, and a cross-check that feeds each emitted string through the real server-side parser |
130
+ | `npm run test:types` | no | The compile gate: every `.ok` fixture compiles clean and every `// @errorline TSxxxx` marker in an `.err` fixture errors with that code, on TypeScript 5.9, 6.0 and 7.0 |
131
+ | `npm run test:packaging` | no | `npm pack --dry-run`: only `dist/**` ships |
132
+ | `npm run test:integration` | yes | The emitted strings executed against real Postgres through the published `typeorm-extensions`, asserting rows |
133
+ | `npm run test:e2e` | yes | The same strings sent to a real NestJS app, asserting the response envelope and the 400 body for each rejected construct |
134
+ | `npm run test:coverage` | no | `unit`, with the coverage gate |
135
+ | `npm test` | yes | `npm run build`, then all five projects |
136
+
137
+ ```bash
138
+ npm run db:up # postgres:17-alpine on port 5557
139
+ npm test
140
+ npm run db:down
141
+ ```
142
+
143
+ The database-backed tiers skip themselves with a clear message when the
144
+ sidecar is down. See [Testing](docs/testing.md) for the shared test setup,
145
+ the fixtures, the seed data, and how to add a test to each tier.
146
+
147
+ ## Layout
148
+
149
+ ```
150
+ src/index.ts the public API: odataQuery, odataItem, and every exported type
151
+ src/module/common/ what two or more areas share; it imports nothing
152
+ src/module/common/domain/ ExceptionCode, ExceptionDetail, Scalar and Unwrap
153
+ src/module/common/exception/ QueryBuilderException
154
+ src/module/common/util/ escape.util — the one literal-escaping mechanism
155
+ src/module/query/ the builder: three stages, QueryChainService, shared state, the two error classes
156
+ src/module/query/domain/ QueryState, and one folder per stage: chain/, filter/
157
+ src/module/query/exception/ InvalidFilterError, ReservedNameError
158
+ src/module/query/service/ queryChain.service.ts (QueryChainService) and one facade per stage: filter/, emit/
159
+ src/module/query/util/ pure-function folders for two stages: filter/, emit/
160
+ test/unit/ no database
161
+ test/types/ the compile gate, three TypeScript versions
162
+ test/integration/ executes queries against seeded Postgres
163
+ test/e2e/ a real NestJS app, the response envelope, the rejected constructs
164
+ test/packaging/ tarball guard
165
+ test/fixtures/ DTO twins, entities, the seed data
166
+ test/support/ shared test setup: datasource, app, env
167
+ ```
@@ -0,0 +1,31 @@
1
+ /**
2
+ * The one entry point of the package: the complete public API.
3
+ *
4
+ * Imports and re-exports only. Every name below is public — it keeps its name
5
+ * and signature across minor versions. A construct the server answers with a
6
+ * 400 is deliberately absent, so it is a compile error rather than a runtime
7
+ * failure.
8
+ */
9
+ import { QueryBuilderExceptionCode } from './module/common/domain/exceptionCode.model';
10
+ import type { ExceptionDetail } from './module/common/domain/exceptionDetail.model';
11
+ import type { Scalar, Unwrap } from './module/common/domain/scalar.model';
12
+ import { QueryBuilderException } from './module/common/exception/queryBuilder.exception';
13
+ import type { Chain, QueryParams } from './module/query/domain/chain/chain.model';
14
+ import type { EmbeddedPathKeys, ExpandKey, ExpandNamespace, FieldKey, OrderKey } from './module/query/domain/chain/expandNamespace.model';
15
+ import type { ItemChain } from './module/query/domain/chain/itemChain.model';
16
+ import type { ProjectChain } from './module/query/domain/chain/projectChain.model';
17
+ import type { BooleanExpr } from './module/query/domain/filter/booleanExpr.model';
18
+ import type { Castable, CastResult, EdmTarget } from './module/query/domain/filter/castable.model';
19
+ import type { Comparable } from './module/query/domain/filter/comparable.model';
20
+ import type { DateExpr, DateInput } from './module/query/domain/filter/dateExpr.model';
21
+ import type { Node, Predicate } from './module/query/domain/filter/node.model';
22
+ import type { NumberExpr } from './module/query/domain/filter/numberExpr.model';
23
+ import type { Operators } from './module/query/domain/filter/operators.model';
24
+ import type { EmbeddedObjectKeys, EmbeddedOf, ExprFor, LambdaNamespace, OwnKeys, PrimitiveArrayKeys, PrimitiveLambdaNamespace, Relation, ScalarFieldKeys, Scope, ToManyKeys, ToOneKeys } from './module/query/domain/filter/scope.model';
25
+ import type { StringExpr } from './module/query/domain/filter/stringExpr.model';
26
+ import { InvalidFilterError } from './module/query/exception/invalidFilter.exception';
27
+ import { ReservedNameError } from './module/query/exception/reservedName.exception';
28
+ import { and, maxdatetime, mindatetime, not, now, or, raw } from './module/query/service/filter/filter.facade';
29
+ import { odataItem, odataQuery } from './module/query/service/queryChain.service';
30
+ export { InvalidFilterError, QueryBuilderException, QueryBuilderExceptionCode, ReservedNameError, and, maxdatetime, mindatetime, not, now, odataItem, odataQuery, or, raw, };
31
+ export type { BooleanExpr, CastResult, Castable, Chain, Comparable, DateExpr, DateInput, EdmTarget, EmbeddedObjectKeys, EmbeddedOf, EmbeddedPathKeys, ExceptionDetail, ExpandKey, ExpandNamespace, ExprFor, FieldKey, ItemChain, LambdaNamespace, Node, NumberExpr, Operators, OrderKey, OwnKeys, Predicate, PrimitiveArrayKeys, PrimitiveLambdaNamespace, ProjectChain, QueryParams, Relation, Scalar, ScalarFieldKeys, Scope, StringExpr, ToManyKeys, ToOneKeys, Unwrap, };
package/dist/index.js ADDED
@@ -0,0 +1,31 @@
1
+ "use strict";
2
+ /**
3
+ * The one entry point of the package: the complete public API.
4
+ *
5
+ * Imports and re-exports only. Every name below is public — it keeps its name
6
+ * and signature across minor versions. A construct the server answers with a
7
+ * 400 is deliberately absent, so it is a compile error rather than a runtime
8
+ * failure.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.raw = exports.or = exports.odataQuery = exports.odataItem = exports.now = exports.not = exports.mindatetime = exports.maxdatetime = exports.and = exports.ReservedNameError = exports.QueryBuilderExceptionCode = exports.QueryBuilderException = exports.InvalidFilterError = void 0;
12
+ const exceptionCode_model_1 = require("./module/common/domain/exceptionCode.model");
13
+ Object.defineProperty(exports, "QueryBuilderExceptionCode", { enumerable: true, get: function () { return exceptionCode_model_1.QueryBuilderExceptionCode; } });
14
+ const queryBuilder_exception_1 = require("./module/common/exception/queryBuilder.exception");
15
+ Object.defineProperty(exports, "QueryBuilderException", { enumerable: true, get: function () { return queryBuilder_exception_1.QueryBuilderException; } });
16
+ const invalidFilter_exception_1 = require("./module/query/exception/invalidFilter.exception");
17
+ Object.defineProperty(exports, "InvalidFilterError", { enumerable: true, get: function () { return invalidFilter_exception_1.InvalidFilterError; } });
18
+ const reservedName_exception_1 = require("./module/query/exception/reservedName.exception");
19
+ Object.defineProperty(exports, "ReservedNameError", { enumerable: true, get: function () { return reservedName_exception_1.ReservedNameError; } });
20
+ const filter_facade_1 = require("./module/query/service/filter/filter.facade");
21
+ Object.defineProperty(exports, "and", { enumerable: true, get: function () { return filter_facade_1.and; } });
22
+ Object.defineProperty(exports, "maxdatetime", { enumerable: true, get: function () { return filter_facade_1.maxdatetime; } });
23
+ Object.defineProperty(exports, "mindatetime", { enumerable: true, get: function () { return filter_facade_1.mindatetime; } });
24
+ Object.defineProperty(exports, "not", { enumerable: true, get: function () { return filter_facade_1.not; } });
25
+ Object.defineProperty(exports, "now", { enumerable: true, get: function () { return filter_facade_1.now; } });
26
+ Object.defineProperty(exports, "or", { enumerable: true, get: function () { return filter_facade_1.or; } });
27
+ Object.defineProperty(exports, "raw", { enumerable: true, get: function () { return filter_facade_1.raw; } });
28
+ const queryChain_service_1 = require("./module/query/service/queryChain.service");
29
+ Object.defineProperty(exports, "odataItem", { enumerable: true, get: function () { return queryChain_service_1.odataItem; } });
30
+ Object.defineProperty(exports, "odataQuery", { enumerable: true, get: function () { return queryChain_service_1.odataQuery; } });
31
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,oFAAuF;AAiEtF,0GAjEQ,+CAAyB,OAiER;AA9D1B,6FAAyF;AA6DxF,sGA7DQ,8CAAqB,OA6DR;AAlBtB,8FAAsF;AAiBrF,mGAjBQ,4CAAkB,OAiBR;AAhBnB,4FAAoF;AAmBnF,kGAnBQ,0CAAiB,OAmBR;AAlBlB,+EAQqD;AAWpD,oFAlBA,mBAAG,OAkBA;AACH,4FAlBA,2BAAW,OAkBA;AACX,4FAlBA,2BAAW,OAkBA;AACX,oFAlBA,mBAAG,OAkBA;AACH,oFAlBA,mBAAG,OAkBA;AAGH,mFApBA,kBAAE,OAoBA;AACF,oFApBA,mBAAG,OAoBA;AAlBJ,kFAGmD;AAYlD,0FAdA,8BAAS,OAcA;AACT,2FAdA,+BAAU,OAcA"}
@@ -0,0 +1,12 @@
1
+ /**
2
+ * The stable machine-readable codes this library reports.
3
+ *
4
+ * A code never changes meaning: a caller may branch on it, and a value here is
5
+ * part of the public API. `QueryBuilderExceptionCode` is namespaced away from
6
+ * the server's own `ExceptionCode` on purpose — the two enums are unrelated
7
+ * even where a name looks similar.
8
+ */
9
+ export declare enum QueryBuilderExceptionCode {
10
+ RESERVED_NAME = "RESERVED_NAME",
11
+ INVALID_FILTER = "INVALID_FILTER"
12
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ /**
3
+ * The stable machine-readable codes this library reports.
4
+ *
5
+ * A code never changes meaning: a caller may branch on it, and a value here is
6
+ * part of the public API. `QueryBuilderExceptionCode` is namespaced away from
7
+ * the server's own `ExceptionCode` on purpose — the two enums are unrelated
8
+ * even where a name looks similar.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.QueryBuilderExceptionCode = void 0;
12
+ var QueryBuilderExceptionCode;
13
+ (function (QueryBuilderExceptionCode) {
14
+ QueryBuilderExceptionCode["RESERVED_NAME"] = "RESERVED_NAME";
15
+ QueryBuilderExceptionCode["INVALID_FILTER"] = "INVALID_FILTER";
16
+ })(QueryBuilderExceptionCode || (exports.QueryBuilderExceptionCode = QueryBuilderExceptionCode = {}));
17
+ //# sourceMappingURL=exceptionCode.model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exceptionCode.model.js","sourceRoot":"","sources":["../../../../src/module/common/domain/exceptionCode.model.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG;;;AAEH,IAAY,yBAGX;AAHD,WAAY,yBAAyB;IACpC,4DAA+B,CAAA;IAC/B,8DAAiC,CAAA;AAClC,CAAC,EAHW,yBAAyB,yCAAzB,yBAAyB,QAGpC"}
@@ -0,0 +1,14 @@
1
+ /**
2
+ * The machine-readable payload behind an error message. One entry of
3
+ * `QueryBuilderException.errors` is one of these, and `errors[0].reason` is
4
+ * the exception message.
5
+ *
6
+ * Plain serializable data: an adapter can put the array straight into an HTTP
7
+ * response body.
8
+ */
9
+ export interface ExceptionDetail {
10
+ /** What went wrong, in one sentence, for a human reading a log. */
11
+ reason: string;
12
+ /** The name, path or value the reason is about, where there is one. */
13
+ subject?: string;
14
+ }
@@ -0,0 +1,11 @@
1
+ "use strict";
2
+ /**
3
+ * The machine-readable payload behind an error message. One entry of
4
+ * `QueryBuilderException.errors` is one of these, and `errors[0].reason` is
5
+ * the exception message.
6
+ *
7
+ * Plain serializable data: an adapter can put the array straight into an HTTP
8
+ * response body.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ //# sourceMappingURL=exceptionDetail.model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"exceptionDetail.model.js","sourceRoot":"","sources":["../../../../src/module/common/domain/exceptionDetail.model.ts"],"names":[],"mappings":";AAAA;;;;;;;GAOG"}
@@ -0,0 +1,15 @@
1
+ /**
2
+ * The two type primitives every other module reads a DTO with.
3
+ *
4
+ * They live in `common` because both the filter module (which turns a DTO into
5
+ * a filter scope) and the query module (which turns a DTO into a select/expand
6
+ * namespace) need the same answer to "is this a column or a relation".
7
+ *
8
+ * `Scalar` admits only the five shapes the dialect can compare, sort or
9
+ * select directly; an object or array field never matches it, so those two
10
+ * modules route it through `Unwrap` and the embedding types instead.
11
+ */
12
+ /** A DTO property this dialect can compare, sort or select. */
13
+ export type Scalar = string | number | boolean | bigint | Date;
14
+ /** The element type of an array property; anything else is returned as-is. */
15
+ export type Unwrap<T> = T extends readonly (infer U)[] ? U : T;
@@ -0,0 +1,14 @@
1
+ "use strict";
2
+ /**
3
+ * The two type primitives every other module reads a DTO with.
4
+ *
5
+ * They live in `common` because both the filter module (which turns a DTO into
6
+ * a filter scope) and the query module (which turns a DTO into a select/expand
7
+ * namespace) need the same answer to "is this a column or a relation".
8
+ *
9
+ * `Scalar` admits only the five shapes the dialect can compare, sort or
10
+ * select directly; an object or array field never matches it, so those two
11
+ * modules route it through `Unwrap` and the embedding types instead.
12
+ */
13
+ Object.defineProperty(exports, "__esModule", { value: true });
14
+ //# sourceMappingURL=scalar.model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"scalar.model.js","sourceRoot":"","sources":["../../../../src/module/common/domain/scalar.model.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;GAUG"}
@@ -0,0 +1,27 @@
1
+ /**
2
+ * The base of every error this library throws.
3
+ *
4
+ * It carries the same `{status, code, errors}` SHAPE
5
+ * `@ballistix.digital/typeorm-extensions` puts on its own errors — an
6
+ * HTTP-ish `status`, a stable machine-readable `code`, and a list of details —
7
+ * so a caller's existing handling of the server's errors (reading those three
8
+ * fields) also handles the builder's. It is field-compatible, not a shared
9
+ * type: that package exports its own, unrelated `LibraryException` and
10
+ * `ExceptionCode`, so this class is never an `instanceof` that one.
11
+ *
12
+ * The envelope lives in `common` because every area raises it. Nothing here
13
+ * depends on a framework, and nothing here depends on another area.
14
+ */
15
+ import type { QueryBuilderExceptionCode } from '../domain/exceptionCode.model';
16
+ import type { ExceptionDetail } from '../domain/exceptionDetail.model';
17
+ export declare abstract class QueryBuilderException extends Error {
18
+ /** Suggested HTTP status for adapters. Always 400. */
19
+ readonly status: number;
20
+ /** The same value, under the name Nest and Axios both use. */
21
+ readonly statusCode: number;
22
+ /** Stable machine-readable code, e.g. "RESERVED_NAME". */
23
+ readonly code: QueryBuilderExceptionCode;
24
+ /** Every detail behind this error; `errors[0]` fills the message. */
25
+ readonly errors: ExceptionDetail[];
26
+ protected constructor(code: QueryBuilderExceptionCode, message: string, errors: ExceptionDetail[]);
27
+ }
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ /**
3
+ * The base of every error this library throws.
4
+ *
5
+ * It carries the same `{status, code, errors}` SHAPE
6
+ * `@ballistix.digital/typeorm-extensions` puts on its own errors — an
7
+ * HTTP-ish `status`, a stable machine-readable `code`, and a list of details —
8
+ * so a caller's existing handling of the server's errors (reading those three
9
+ * fields) also handles the builder's. It is field-compatible, not a shared
10
+ * type: that package exports its own, unrelated `LibraryException` and
11
+ * `ExceptionCode`, so this class is never an `instanceof` that one.
12
+ *
13
+ * The envelope lives in `common` because every area raises it. Nothing here
14
+ * depends on a framework, and nothing here depends on another area.
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.QueryBuilderException = void 0;
18
+ class QueryBuilderException extends Error {
19
+ /** Suggested HTTP status for adapters. Always 400. */
20
+ status;
21
+ /** The same value, under the name Nest and Axios both use. */
22
+ statusCode;
23
+ /** Stable machine-readable code, e.g. "RESERVED_NAME". */
24
+ code;
25
+ /** Every detail behind this error; `errors[0]` fills the message. */
26
+ errors;
27
+ constructor(code, message, errors) {
28
+ super(message);
29
+ this.name = new.target.name;
30
+ this.status = 400;
31
+ this.statusCode = 400;
32
+ this.code = code;
33
+ this.errors = errors;
34
+ }
35
+ }
36
+ exports.QueryBuilderException = QueryBuilderException;
37
+ //# sourceMappingURL=queryBuilder.exception.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"queryBuilder.exception.js","sourceRoot":"","sources":["../../../../src/module/common/exception/queryBuilder.exception.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;GAaG;;;AAKH,MAAsB,qBAAsB,SAAQ,KAAK;IACxD,sDAAsD;IAC7C,MAAM,CAAS;IACxB,8DAA8D;IACrD,UAAU,CAAS;IAC5B,0DAA0D;IACjD,IAAI,CAA4B;IACzC,qEAAqE;IAC5D,MAAM,CAAoB;IAEnC,YACC,IAA+B,EAC/B,OAAe,EACf,MAAyB;QAEzB,KAAK,CAAC,OAAO,CAAC,CAAC;QACf,IAAI,CAAC,IAAI,GAAG,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC;QAC5B,IAAI,CAAC,MAAM,GAAG,GAAG,CAAC;QAClB,IAAI,CAAC,UAAU,GAAG,GAAG,CAAC;QACtB,IAAI,CAAC,IAAI,GAAG,IAAI,CAAC;QACjB,IAAI,CAAC,MAAM,GAAG,MAAM,CAAC;IACtB,CAAC;CACD;AAtBD,sDAsBC"}
@@ -0,0 +1,55 @@
1
+ /**
2
+ * Literal escaping: the one mechanism that makes a value safe inside a query
3
+ * string. It sits in `common` because two areas need the same answer — filter
4
+ * quotes literals and search terms with it, and emit undoes its escapes for
5
+ * `toParams()`.
6
+ *
7
+ * These seven rules are EMITTER GUARANTEES, each one measured by feeding the
8
+ * emitted string back through the server's own parser and reading the literal
9
+ * that came out:
10
+ *
11
+ * ' -> '' OData string escape. `name eq 'It''s'` parses to `It's`.
12
+ * % -> %25 the query string is percent-DECODED before parsing, so a
13
+ * literal `a%20b` would arrive as `a b` unless the % is
14
+ * escaped first.
15
+ * + -> %2B the grammar reads a bare `+` as whitespace: `name eq 'a+b'`
16
+ * parses to the value `a b`. This is the rule that silently
17
+ * corrupts data if it is missing.
18
+ * & -> %26 a bare `&` splits the query string into another option, so
19
+ * `name eq 'a&b'` is a 400, not a wrong value.
20
+ * # -> %23 a bare `#` starts the URL fragment.
21
+ * ' ' -> %20 only inside literals; the spaces BETWEEN tokens stay raw so
22
+ * the query string is readable.
23
+ * a C0 control code point (below 0x20) or DEL (0x7F) -> %XX
24
+ * a full WHATWG URL parse (the trip a caller's own HTTP
25
+ * client makes when it builds a request URL from the
26
+ * emitted string) deletes a raw tab, LF or CR from the query
27
+ * string outright, before any percent-decoding happens. A
28
+ * literal carrying one of those bytes raw would arrive at
29
+ * the server silently shortened.
30
+ *
31
+ * `%` is replaced first, otherwise the escapes introduced afterwards would be
32
+ * escaped again. The control-code pass runs after the other percent-escapes
33
+ * and walks the string by code point rather than by regex, so it never has to
34
+ * name an unprintable character inside a pattern.
35
+ *
36
+ * The output of `toQueryString()` is therefore already URL-safe. Do NOT run
37
+ * `encodeURIComponent` over it — that turns every `%25` into `%2525`.
38
+ */
39
+ /** Every rule except the quote doubling, for values that are not quoted (`$search`). */
40
+ export declare const escapeBareValue: (value: string) => string;
41
+ /**
42
+ * Every rule: `escapeBareValue()`, then the quote doubling. No other rule adds
43
+ * or removes a quote, so the order does not change the result.
44
+ */
45
+ export declare const escapeLiteral: (value: string) => string;
46
+ export declare const quote: (value: string) => string;
47
+ /**
48
+ * Undo the percent-escapes, for `toParams()`.
49
+ *
50
+ * A caller that hands the parts to `URLSearchParams` or ky runs its own
51
+ * encoder over each value, so the OData-safety escaping has to come off first
52
+ * or every `%25` ships as `%2525`. The OData quote doubling is NOT undone —
53
+ * that one belongs to the expression grammar, not to the transport.
54
+ */
55
+ export declare const unescapeParamValue: (value: string) => string;
@@ -0,0 +1,83 @@
1
+ "use strict";
2
+ /**
3
+ * Literal escaping: the one mechanism that makes a value safe inside a query
4
+ * string. It sits in `common` because two areas need the same answer — filter
5
+ * quotes literals and search terms with it, and emit undoes its escapes for
6
+ * `toParams()`.
7
+ *
8
+ * These seven rules are EMITTER GUARANTEES, each one measured by feeding the
9
+ * emitted string back through the server's own parser and reading the literal
10
+ * that came out:
11
+ *
12
+ * ' -> '' OData string escape. `name eq 'It''s'` parses to `It's`.
13
+ * % -> %25 the query string is percent-DECODED before parsing, so a
14
+ * literal `a%20b` would arrive as `a b` unless the % is
15
+ * escaped first.
16
+ * + -> %2B the grammar reads a bare `+` as whitespace: `name eq 'a+b'`
17
+ * parses to the value `a b`. This is the rule that silently
18
+ * corrupts data if it is missing.
19
+ * & -> %26 a bare `&` splits the query string into another option, so
20
+ * `name eq 'a&b'` is a 400, not a wrong value.
21
+ * # -> %23 a bare `#` starts the URL fragment.
22
+ * ' ' -> %20 only inside literals; the spaces BETWEEN tokens stay raw so
23
+ * the query string is readable.
24
+ * a C0 control code point (below 0x20) or DEL (0x7F) -> %XX
25
+ * a full WHATWG URL parse (the trip a caller's own HTTP
26
+ * client makes when it builds a request URL from the
27
+ * emitted string) deletes a raw tab, LF or CR from the query
28
+ * string outright, before any percent-decoding happens. A
29
+ * literal carrying one of those bytes raw would arrive at
30
+ * the server silently shortened.
31
+ *
32
+ * `%` is replaced first, otherwise the escapes introduced afterwards would be
33
+ * escaped again. The control-code pass runs after the other percent-escapes
34
+ * and walks the string by code point rather than by regex, so it never has to
35
+ * name an unprintable character inside a pattern.
36
+ *
37
+ * The output of `toQueryString()` is therefore already URL-safe. Do NOT run
38
+ * `encodeURIComponent` over it — that turns every `%25` into `%2525`.
39
+ */
40
+ Object.defineProperty(exports, "__esModule", { value: true });
41
+ exports.unescapeParamValue = exports.quote = exports.escapeLiteral = exports.escapeBareValue = void 0;
42
+ const encodeControlCharacters = (value) => Array.from(value)
43
+ .map((character) => {
44
+ const code = character.charCodeAt(0);
45
+ // A C0 control code (below 0x20) or DEL (0x7F).
46
+ return code < 0x20 || code === 0x7f
47
+ ? `%${code.toString(16).toUpperCase().padStart(2, '0')}`
48
+ : character;
49
+ })
50
+ .join('');
51
+ /** Every rule except the quote doubling, for values that are not quoted (`$search`). */
52
+ const escapeBareValue = (value) => encodeControlCharacters(value
53
+ .replace(/%/g, '%25')
54
+ .replace(/\+/g, '%2B')
55
+ .replace(/&/g, '%26')
56
+ .replace(/#/g, '%23')
57
+ .replace(/ /g, '%20'));
58
+ exports.escapeBareValue = escapeBareValue;
59
+ /**
60
+ * Every rule: `escapeBareValue()`, then the quote doubling. No other rule adds
61
+ * or removes a quote, so the order does not change the result.
62
+ */
63
+ const escapeLiteral = (value) => (0, exports.escapeBareValue)(value).replace(/'/g, "''");
64
+ exports.escapeLiteral = escapeLiteral;
65
+ const quote = (value) => `'${(0, exports.escapeLiteral)(value)}'`;
66
+ exports.quote = quote;
67
+ /**
68
+ * Undo the percent-escapes, for `toParams()`.
69
+ *
70
+ * A caller that hands the parts to `URLSearchParams` or ky runs its own
71
+ * encoder over each value, so the OData-safety escaping has to come off first
72
+ * or every `%25` ships as `%2525`. The OData quote doubling is NOT undone —
73
+ * that one belongs to the expression grammar, not to the transport.
74
+ */
75
+ const unescapeParamValue = (value) => value
76
+ .replace(/%(0[0-9A-Fa-f]|1[0-9A-Fa-f]|7[Ff])/g, (_match, hexDigits) => String.fromCharCode(parseInt(hexDigits, 16)))
77
+ .replace(/%2B/g, '+')
78
+ .replace(/%26/g, '&')
79
+ .replace(/%23/g, '#')
80
+ .replace(/%20/g, ' ')
81
+ .replace(/%25/g, '%');
82
+ exports.unescapeParamValue = unescapeParamValue;
83
+ //# sourceMappingURL=escape.util.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"escape.util.js","sourceRoot":"","sources":["../../../../src/module/common/util/escape.util.ts"],"names":[],"mappings":";AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAqCG;;;AAEH,MAAM,uBAAuB,GAAG,CAAC,KAAa,EAAU,EAAE,CACzD,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC;KACf,GAAG,CAAC,CAAC,SAAS,EAAE,EAAE;IAClB,MAAM,IAAI,GAAG,SAAS,CAAC,UAAU,CAAC,CAAC,CAAC,CAAC;IACrC,gDAAgD;IAChD,OAAO,IAAI,GAAG,IAAI,IAAI,IAAI,KAAK,IAAI;QAClC,CAAC,CAAC,IAAI,IAAI,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,WAAW,EAAE,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE;QACxD,CAAC,CAAC,SAAS,CAAC;AACd,CAAC,CAAC;KACD,IAAI,CAAC,EAAE,CAAC,CAAC;AAEZ,wFAAwF;AACjF,MAAM,eAAe,GAAG,CAAC,KAAa,EAAU,EAAE,CACxD,uBAAuB,CACtB,KAAK;KACH,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;KACpB,OAAO,CAAC,KAAK,EAAE,KAAK,CAAC;KACrB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;KACpB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC;KACpB,OAAO,CAAC,IAAI,EAAE,KAAK,CAAC,CACtB,CAAC;AARU,QAAA,eAAe,mBAQzB;AAEH;;;GAGG;AACI,MAAM,aAAa,GAAG,CAAC,KAAa,EAAU,EAAE,CACtD,IAAA,uBAAe,EAAC,KAAK,CAAC,CAAC,OAAO,CAAC,IAAI,EAAE,IAAI,CAAC,CAAC;AAD/B,QAAA,aAAa,iBACkB;AAErC,MAAM,KAAK,GAAG,CAAC,KAAa,EAAU,EAAE,CAAC,IAAI,IAAA,qBAAa,EAAC,KAAK,CAAC,GAAG,CAAC;AAA/D,QAAA,KAAK,SAA0D;AAE5E;;;;;;;GAOG;AACI,MAAM,kBAAkB,GAAG,CAAC,KAAa,EAAU,EAAE,CAC3D,KAAK;KACH,OAAO,CACP,qCAAqC,EACrC,CAAC,MAAM,EAAE,SAAiB,EAAE,EAAE,CAC7B,MAAM,CAAC,YAAY,CAAC,QAAQ,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC,CAC7C;KACA,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;KACpB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;KACpB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;KACpB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC;KACpB,OAAO,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;AAXX,QAAA,kBAAkB,sBAWP"}
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The root chain: every system query option the dialect serves.
3
+ *
4
+ * The builder produces a query STRING. It does not fetch, and it does not know
5
+ * about the `_embedded` / `_page` response envelope.
6
+ */
7
+ import type { Predicate } from '../filter/node.model';
8
+ import type { Operators } from '../filter/operators.model';
9
+ import type { Scope } from '../filter/scope.model';
10
+ import type { StringExpr } from '../filter/stringExpr.model';
11
+ import type { ExpandNamespace, FieldKey, OrderKey } from './expandNamespace.model';
12
+ /** A flat `{ $option: value }` map, for a caller that serialises its own URL. */
13
+ export type QueryParams = Record<string, string>;
14
+ export interface Chain<T> {
15
+ readonly expand: ExpandNamespace<T, Chain<T>>;
16
+ select(...keys: FieldKey<T>[]): Chain<T>;
17
+ /** Repeated calls AND together. */
18
+ filter(build: (scope: Scope<T>, operators: Operators) => Predicate): Chain<T>;
19
+ /** `$search` + its mandatory `$searchColumns`, as one verb. */
20
+ search(term: string, columns: (scope: Scope<T>) => StringExpr | readonly StringExpr[]): Chain<T>;
21
+ /** Sorts on an own column, an embedded leaf, or a to-one relation path. */
22
+ orderBy(key: OrderKey<T>, direction?: 'asc' | 'desc'): Chain<T>;
23
+ top(count: number): Chain<T>;
24
+ skip(count: number): Chain<T>;
25
+ count(on?: boolean): Chain<T>;
26
+ /** The whole query string, already URL-safe. Never re-encode it. */
27
+ toQueryString(): string;
28
+ /**
29
+ * The same query as separate parts, with the transport escaping undone, for
30
+ * a caller that hands them to `URLSearchParams` or ky and lets that encode.
31
+ */
32
+ toParams(): QueryParams;
33
+ }
@@ -0,0 +1,3 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ //# sourceMappingURL=chain.model.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"chain.model.js","sourceRoot":"","sources":["../../../../../src/module/query/domain/chain/chain.model.ts"],"names":[],"mappings":""}