sequelize-api-query-parser 0.0.0-stage → 0.1.1

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 (54) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +234 -2
  3. package/dist/cjs/index.js +6 -0
  4. package/dist/cjs/index.js.map +1 -0
  5. package/dist/cjs/package.json +3 -0
  6. package/dist/cjs/parsers/include.js +111 -0
  7. package/dist/cjs/parsers/include.js.map +1 -0
  8. package/dist/cjs/parsers/order.js +49 -0
  9. package/dist/cjs/parsers/order.js.map +1 -0
  10. package/dist/cjs/parsers/search.js +85 -0
  11. package/dist/cjs/parsers/search.js.map +1 -0
  12. package/dist/cjs/parsers/select.js +29 -0
  13. package/dist/cjs/parsers/select.js.map +1 -0
  14. package/dist/cjs/parsers/where.js +173 -0
  15. package/dist/cjs/parsers/where.js.map +1 -0
  16. package/dist/cjs/query-parser.js +81 -0
  17. package/dist/cjs/query-parser.js.map +1 -0
  18. package/dist/cjs/types.js +3 -0
  19. package/dist/cjs/types.js.map +1 -0
  20. package/dist/cjs/utils/fields.js +76 -0
  21. package/dist/cjs/utils/fields.js.map +1 -0
  22. package/dist/cjs/utils/values.js +149 -0
  23. package/dist/cjs/utils/values.js.map +1 -0
  24. package/dist/esm/index.js +2 -0
  25. package/dist/esm/index.js.map +1 -0
  26. package/dist/esm/parsers/include.js +107 -0
  27. package/dist/esm/parsers/include.js.map +1 -0
  28. package/dist/esm/parsers/order.js +45 -0
  29. package/dist/esm/parsers/order.js.map +1 -0
  30. package/dist/esm/parsers/search.js +80 -0
  31. package/dist/esm/parsers/search.js.map +1 -0
  32. package/dist/esm/parsers/select.js +25 -0
  33. package/dist/esm/parsers/select.js.map +1 -0
  34. package/dist/esm/parsers/where.js +169 -0
  35. package/dist/esm/parsers/where.js.map +1 -0
  36. package/dist/esm/query-parser.js +77 -0
  37. package/dist/esm/query-parser.js.map +1 -0
  38. package/dist/esm/types.js +2 -0
  39. package/dist/esm/types.js.map +1 -0
  40. package/dist/esm/utils/fields.js +68 -0
  41. package/dist/esm/utils/fields.js.map +1 -0
  42. package/dist/esm/utils/values.js +138 -0
  43. package/dist/esm/utils/values.js.map +1 -0
  44. package/dist/types/index.d.ts +2 -0
  45. package/dist/types/parsers/include.d.ts +12 -0
  46. package/dist/types/parsers/order.d.ts +2 -0
  47. package/dist/types/parsers/search.d.ts +13 -0
  48. package/dist/types/parsers/select.d.ts +2 -0
  49. package/dist/types/parsers/where.d.ts +13 -0
  50. package/dist/types/query-parser.d.ts +7 -0
  51. package/dist/types/types.d.ts +52 -0
  52. package/dist/types/utils/fields.d.ts +12 -0
  53. package/dist/types/utils/values.d.ts +12 -0
  54. package/package.json +60 -4
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Sequelize Query Parser contributors
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,235 @@
1
- # Temporary Holding Version
1
+ # sequelize-api-query-parser
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
+ Turn HTTP-style query input into validated Sequelize v6 query options. The package is framework-independent, written in TypeScript, and ships ESM, CommonJS, source maps, and declaration files.
4
+
5
+ ## Requirements
6
+
7
+ - Node.js 18 or newer
8
+ - Sequelize `>=6.37.8 <7`
9
+ - PostgreSQL for JSON text search, array containment, `iLike`, and formatted date search
10
+
11
+ These are runtime requirements for package consumers. The repository's Vitest/Vite development toolchain requires Node.js 20.19+, 22.12+, or 24+.
12
+
13
+ ## Installation
14
+
15
+ ```bash
16
+ npm install sequelize-api-query-parser sequelize
17
+ ```
18
+
19
+ ## TypeScript and ESM
20
+
21
+ ```ts
22
+ import { QueryParser } from 'sequelize-api-query-parser';
23
+ import type { ColumnDetail, QueryParserQuery } from 'sequelize-api-query-parser';
24
+ import { Customer } from './models/customer.js';
25
+
26
+ const query: QueryParserQuery = {
27
+ q: { active: 'b|true', age: { gte: 'n|18' } },
28
+ searchText: 'Ada',
29
+ searchFields: 'name,address.city',
30
+ select: 'id,name,age',
31
+ sort: '-createdAt',
32
+ limit: '25',
33
+ page: '2',
34
+ };
35
+
36
+ const columns: Readonly<Partial<Record<string, ColumnDetail>>> = {
37
+ age: { fieldName: 'age', type: 'INTEGER' },
38
+ name: { fieldName: 'name', type: 'STRING' },
39
+ };
40
+
41
+ const options = new QueryParser({
42
+ request: {
43
+ query,
44
+ headers: { 'client-tz': 'Asia/Kolkata' },
45
+ },
46
+ model: Customer,
47
+ columns,
48
+ }).getFullQuery();
49
+
50
+ const customers = await Customer.findAll(options);
51
+ ```
52
+
53
+ The `request` option is structural: Express, Fastify, NestJS, another framework, or a plain object can provide `query` and `headers`. The package does not depend on a web framework or `sequelize-typescript`.
54
+
55
+ ## CommonJS
56
+
57
+ ```js
58
+ const { QueryParser } = require('sequelize-api-query-parser');
59
+ const { Customer } = require('./models/customer');
60
+
61
+ const options = new QueryParser({
62
+ request: { query: req.query, headers: req.headers },
63
+ model: Customer,
64
+ }).getFullQuery();
65
+ ```
66
+
67
+ ## Query input
68
+
69
+ `getFullQuery(target?)` returns a new object and preserves extra options from `target`. It never mutates the request, query, headers, target, column metadata, or model metadata.
70
+
71
+ | Input | Behavior |
72
+ | --- | --- |
73
+ | `q` | Field filters and nested `and`, `or`, or `not` groups |
74
+ | `searchText` + `searchFields` | Comma-separated model or association fields searched with an `Op.or` group |
75
+ | `checkYesNo` | Enables `yes`/`no` matching for boolean search fields |
76
+ | `select` | Comma-separated attributes; `all` selects all and `notAll` selects none |
77
+ | `sort` | Comma-separated fields; prefix a field with `-` for descending order |
78
+ | `include` | Association include map described below |
79
+ | `limit` + `page` | Positive pagination values |
80
+ | `paranoid` | Boolean or `"true"`/`"false"` override |
81
+ | `subQuery` | Boolean or `"true"`/`"false"` override |
82
+
83
+ Fields may use validated association paths such as `address.city`. Sorting that field produces a Sequelize order path, while filtering and searching use Sequelize's `$address.city$` syntax.
84
+
85
+ ### Filters
86
+
87
+ ```ts
88
+ const q = {
89
+ active: 'b|true',
90
+ age: { between: 'n|18,65' },
91
+ score: { gte: 'f|8.5' },
92
+ createdAt: { gt: 'd|2026-01-01T00:00:00.000Z' },
93
+ or: [
94
+ { name: { iLike: 'ada' } },
95
+ { 'address.city': { eq: 'Pune' } },
96
+ ],
97
+ };
98
+ ```
99
+
100
+ Supported operators are:
101
+
102
+ - Equality and comparison: `eq`, `ne`, `gt`, `gte`, `lt`, `lte`
103
+ - Text and pattern: `like`, `notLike`, `iLike`, `notILike`, `iEq`, `startsWith`, `endsWith`
104
+ - Collections and ranges: `contains`, `between`, `notBetween`, `in`, `notIn`
105
+ - Regular expressions: `regexp`, `notRegexp`, `iRegexp`, `notIRegexp`
106
+ - Logical groups: `and`, `or`, `not`
107
+
108
+ An empty scalar string becomes `null`. Array-field `eq` and `contains` use PostgreSQL array containment; `ne` uses not-containment. JSON equality and pattern filters cast validated JSON columns to text without putting user values into the SQL literal.
109
+
110
+ Typed string prefixes are useful when query-string parsers produce only strings:
111
+
112
+ | Prefix | Value |
113
+ | --- | --- |
114
+ | `n|42` | Number |
115
+ | `f|9.5` | Floating-point number |
116
+ | `b|true` | Boolean |
117
+ | `d|2026-10-22` | Date |
118
+ | `s|0042` | String |
119
+
120
+ List operators accept comma-separated values after the prefix, for example `n|18,65`. Untagged values are also coerced from Sequelize attribute metadata for integer, floating-point, boolean, and date fields. Untagged `BIGINT` and `DECIMAL` values remain validated strings so JavaScript number conversion cannot lose precision.
121
+
122
+ When `columns` metadata is supplied, `whereFields` contains each configured field referenced by `q`, once, in traversal order.
123
+
124
+ ### Search
125
+
126
+ ```ts
127
+ const query = {
128
+ searchText: 'Ada',
129
+ searchFields: 'name,notes,address.city',
130
+ checkYesNo: true,
131
+ };
132
+ ```
133
+
134
+ String and text fields use `Op.iLike`. Boolean fields accept yes/no matching only when `checkYesNo` is enabled. Other scalar, JSON, and array fields are searched through a validated PostgreSQL text cast. Date fields use `TO_CHAR(..., 'MM/DD/YYYY')`; a valid IANA timezone from the `client-tz` header adds `AT TIME ZONE`.
135
+
136
+ Search predicates are appended to an existing top-level filter `Op.or` array without mutating either expression.
137
+
138
+ ### Includes
139
+
140
+ Includes resolve `model.associations[name].target` directly; no application model registry is required.
141
+
142
+ ```ts
143
+ const include = {
144
+ address: {
145
+ select: 'city,state',
146
+ required: 'true',
147
+ separate: false,
148
+ paranoid: 'false',
149
+ q: { city: { iLike: 'pun' } },
150
+ sort: '-city',
151
+ limit: '10',
152
+ page: '2',
153
+ include: {
154
+ country: 'code,name',
155
+ },
156
+ },
157
+ orders: 'all',
158
+ };
159
+ ```
160
+
161
+ A string value is either `all`, `notAll`, or a comma-separated selection. Object values can configure `select`, `required`, `separate`, `paranoid`, `q`, `sort`, `limit`, `page`, and nested `include`. Include recursion is capped at five association levels and circular input objects terminate safely. Passing `isGroupBy: true` to the constructor gives every include an empty attribute list.
162
+
163
+ Sequelize supports `separate: true` only on `HasMany` associations; use it when a per-association limit must be applied by a separate child query.
164
+
165
+ ## Defaults
166
+
167
+ - `limit`: `10`
168
+ - `page`: `1`
169
+ - maximum root and include limit: `100` (change with constructor option `maxLimit`)
170
+ - `order`: `id DESC` when the model has an `id` attribute
171
+ - `attributes`: all model attributes
172
+ - `where`: empty
173
+ - `include`: empty
174
+ - `subQuery` and `paranoid`: `undefined` unless explicitly provided
175
+
176
+ Explicit include objects default to `required: false`, `separate: false`, and `paranoid: true`.
177
+
178
+ ## Validation and security
179
+
180
+ - Every selected, sorted, filtered, searched, and included path must resolve through supplied Sequelize model metadata.
181
+ - Operator names come from an allowlist; dynamic access to Sequelize operators is not used.
182
+ - Prototype-pollution keys and punctuation-bearing identifiers are rejected.
183
+ - SQL fragments contain only validated model, attribute, association-alias, format, and timezone data. Search/filter values remain Sequelize comparison values.
184
+ - Malformed optional fields and unknown associations are omitted instead of causing incidental parser errors.
185
+ - Logical-filter and include recursion are bounded.
186
+
187
+ Always keep normal database authorization, tenant scoping, and row-level access controls in the caller. A query parser does not replace those controls.
188
+
189
+ ## PostgreSQL-specific behavior
190
+
191
+ Projection, ordering, pagination, basic comparisons, and includes are broadly dialect-neutral. `iLike`, regular expressions, JSON text casts, array containment, and `TO_CHAR` date search use PostgreSQL behavior and may not work on other Sequelize dialects.
192
+
193
+ ## Public API
194
+
195
+ The package exports `QueryParser` and these TypeScript types:
196
+
197
+ - `ColumnDetail`
198
+ - `ParsedQuery`
199
+ - `QueryParserOptions`
200
+ - `QueryParserQuery`
201
+ - `QueryParserRequest`
202
+
203
+ Internal parser helpers are not part of the package export map.
204
+
205
+ ## Development
206
+
207
+ ```bash
208
+ npm ci
209
+ npm run check
210
+ npm run test:coverage
211
+ npm pack --dry-run
212
+ ```
213
+
214
+ `npm run check` runs linting, strict type-checking, unit tests, both builds, ESM/CommonJS smoke imports, and the TypeScript consumer check.
215
+
216
+ To verify an actual archive after `npm pack`:
217
+
218
+ ```bash
219
+ node scripts/verify-tarball.mjs sequelize-api-query-parser-0.1.1.tgz
220
+ ```
221
+
222
+ ## Publishing
223
+
224
+ Publishing is intentionally a separate authenticated step. Confirm that the unscoped registry name is available and that the version is correct, then run:
225
+
226
+ ```bash
227
+ npm login
228
+ npm run check
229
+ npm pack --dry-run
230
+ npm publish --access public
231
+ ```
232
+
233
+ ## License
234
+
235
+ MIT
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.QueryParser = void 0;
4
+ var query_parser_js_1 = require("./query-parser.js");
5
+ Object.defineProperty(exports, "QueryParser", { enumerable: true, get: function () { return query_parser_js_1.QueryParser; } });
6
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/index.ts"],"names":[],"mappings":";;;AAAA,qDAAgD;AAAvC,8GAAA,WAAW,OAAA","sourcesContent":["export { QueryParser } from './query-parser.js';\nexport type {\n ColumnDetail,\n ParsedQuery,\n QueryParserOptions,\n QueryParserQuery,\n QueryParserRequest,\n} from './types.js';\n"]}
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "commonjs"
3
+ }
@@ -0,0 +1,111 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseIncludes = void 0;
4
+ const fields_js_1 = require("../utils/fields.js");
5
+ const values_js_1 = require("../utils/values.js");
6
+ const order_js_1 = require("./order.js");
7
+ const select_js_1 = require("./select.js");
8
+ const where_js_1 = require("./where.js");
9
+ const MAX_INCLUDE_DEPTH = 5;
10
+ const DEFAULT_MAX_LIMIT = 100;
11
+ const getAssociationTarget = (model, associationName) => {
12
+ if (!(0, fields_js_1.isSafeIdentifier)(associationName)) {
13
+ return null;
14
+ }
15
+ const associations = model.associations;
16
+ if (!associations || !Object.prototype.hasOwnProperty.call(associations, associationName)) {
17
+ return null;
18
+ }
19
+ const target = associations[associationName]?.target;
20
+ return target && typeof target.getAttributes === 'function' ? target : null;
21
+ };
22
+ const parseOptionalPagination = (value, maxLimit) => {
23
+ if (value.limit === undefined) {
24
+ return {};
25
+ }
26
+ const limit = (0, values_js_1.parsePositiveInteger)(value.limit, 0, maxLimit);
27
+ if (limit === 0) {
28
+ return {};
29
+ }
30
+ const page = (0, values_js_1.parsePositiveInteger)(value.page, 1, Number.MAX_SAFE_INTEGER);
31
+ return {
32
+ limit,
33
+ offset: Math.min(limit * (page - 1), Number.MAX_SAFE_INTEGER),
34
+ };
35
+ };
36
+ const parseStringInclude = (associationName, target, value, isGroupBy) => ({
37
+ as: associationName,
38
+ model: target,
39
+ required: false,
40
+ attributes: isGroupBy ? [] : value === 'all' ? undefined : (0, select_js_1.parseSelection)(target, value),
41
+ });
42
+ const parseIncludes = ({ input, model, isGroupBy = false, depth = 0, seen = new WeakSet(), associationPath = [], maxLimit = DEFAULT_MAX_LIMIT, }) => {
43
+ if (!(0, values_js_1.isRecord)(input) || depth >= MAX_INCLUDE_DEPTH || seen.has(input)) {
44
+ return [];
45
+ }
46
+ seen.add(input);
47
+ try {
48
+ const normalizedMaxLimit = (0, values_js_1.parsePositiveInteger)(maxLimit, DEFAULT_MAX_LIMIT, Number.MAX_SAFE_INTEGER);
49
+ const result = [];
50
+ for (const associationName of Object.keys(input)) {
51
+ const target = getAssociationTarget(model, associationName);
52
+ if (!target) {
53
+ continue;
54
+ }
55
+ const value = input[associationName];
56
+ if (typeof value === 'string') {
57
+ result.push(parseStringInclude(associationName, target, value, isGroupBy));
58
+ continue;
59
+ }
60
+ if (!(0, values_js_1.isRecord)(value)) {
61
+ continue;
62
+ }
63
+ const currentAssociationPath = [...associationPath, associationName];
64
+ const required = (0, values_js_1.parseBoolean)(value.required, false) ?? false;
65
+ const separate = (0, values_js_1.parseBoolean)(value.separate, false) ?? false;
66
+ const paranoid = (0, values_js_1.parseBoolean)(value.paranoid, true) ?? true;
67
+ const queryAssociationPath = separate ? [] : currentAssociationPath;
68
+ const parsedWhere = Object.prototype.hasOwnProperty.call(value, 'q')
69
+ ? (0, where_js_1.parseWhere)({ input: value.q, model: target, associationPath: queryAssociationPath }).where
70
+ : undefined;
71
+ const where = parsedWhere && Reflect.ownKeys(parsedWhere).length > 0 ? parsedWhere : undefined;
72
+ const order = typeof value.sort === 'string' && value.sort.trim()
73
+ ? (0, order_js_1.parseOrder)(target, value.sort)
74
+ : undefined;
75
+ const attributes = isGroupBy
76
+ ? []
77
+ : typeof value.select === 'string'
78
+ ? (0, select_js_1.parseSelection)(target, value.select)
79
+ : undefined;
80
+ const nestedIncludes = Object.prototype.hasOwnProperty.call(value, 'include')
81
+ ? (0, exports.parseIncludes)({
82
+ input: value.include,
83
+ model: target,
84
+ isGroupBy,
85
+ depth: depth + 1,
86
+ seen,
87
+ associationPath: queryAssociationPath,
88
+ maxLimit: normalizedMaxLimit,
89
+ })
90
+ : undefined;
91
+ result.push({
92
+ as: associationName,
93
+ model: target,
94
+ required,
95
+ separate,
96
+ paranoid,
97
+ where,
98
+ order,
99
+ ...parseOptionalPagination(value, normalizedMaxLimit),
100
+ attributes,
101
+ include: nestedIncludes,
102
+ });
103
+ }
104
+ return result;
105
+ }
106
+ finally {
107
+ seen.delete(input);
108
+ }
109
+ };
110
+ exports.parseIncludes = parseIncludes;
111
+ //# sourceMappingURL=include.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"include.js","sourceRoot":"","sources":["../../../src/parsers/include.ts"],"names":[],"mappings":"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","sourcesContent":["import type { Includeable, Model, ModelStatic, Order, WhereOptions } from 'sequelize';\n\nimport { isSafeIdentifier } from '../utils/fields.js';\nimport { isRecord, parseBoolean, parsePositiveInteger } from '../utils/values.js';\nimport { parseOrder } from './order.js';\nimport { parseSelection } from './select.js';\nimport { parseWhere } from './where.js';\n\nconst MAX_INCLUDE_DEPTH = 5;\nconst DEFAULT_MAX_LIMIT = 100;\n\ninterface ParseIncludesOptions {\n readonly input: unknown;\n readonly model: ModelStatic<Model>;\n readonly isGroupBy?: boolean;\n readonly depth?: number;\n readonly seen?: WeakSet<object>;\n readonly associationPath?: readonly string[];\n readonly maxLimit?: number;\n}\n\ninterface AssociationTarget {\n readonly target?: ModelStatic<Model>;\n}\n\ninterface ParsedInclude {\n readonly as: string;\n readonly model: ModelStatic<Model>;\n readonly required: boolean;\n readonly separate?: boolean;\n readonly paranoid?: boolean;\n readonly where?: WhereOptions;\n readonly order?: Order;\n readonly limit?: number;\n readonly offset?: number;\n readonly attributes?: string[];\n readonly include?: Includeable[];\n}\n\nconst getAssociationTarget = (\n model: ModelStatic<Model>,\n associationName: string,\n): ModelStatic<Model> | null => {\n if (!isSafeIdentifier(associationName)) {\n return null;\n }\n\n const associations = model.associations as Record<string, AssociationTarget> | undefined;\n if (!associations || !Object.prototype.hasOwnProperty.call(associations, associationName)) {\n return null;\n }\n\n const target = associations[associationName]?.target;\n return target && typeof target.getAttributes === 'function' ? target : null;\n};\n\nconst parseOptionalPagination = (value: Record<string, unknown>, maxLimit: number) => {\n if (value.limit === undefined) {\n return {};\n }\n\n const limit = parsePositiveInteger(value.limit, 0, maxLimit);\n if (limit === 0) {\n return {};\n }\n\n const page = parsePositiveInteger(value.page, 1, Number.MAX_SAFE_INTEGER);\n return {\n limit,\n offset: Math.min(limit * (page - 1), Number.MAX_SAFE_INTEGER),\n };\n};\n\nconst parseStringInclude = (\n associationName: string,\n target: ModelStatic<Model>,\n value: string,\n isGroupBy: boolean,\n): ParsedInclude => ({\n as: associationName,\n model: target,\n required: false,\n attributes: isGroupBy ? [] : value === 'all' ? undefined : parseSelection(target, value),\n});\n\nexport const parseIncludes = ({\n input,\n model,\n isGroupBy = false,\n depth = 0,\n seen = new WeakSet<object>(),\n associationPath = [],\n maxLimit = DEFAULT_MAX_LIMIT,\n}: ParseIncludesOptions): Includeable[] => {\n if (!isRecord(input) || depth >= MAX_INCLUDE_DEPTH || seen.has(input)) {\n return [];\n }\n\n seen.add(input);\n try {\n const normalizedMaxLimit = parsePositiveInteger(\n maxLimit,\n DEFAULT_MAX_LIMIT,\n Number.MAX_SAFE_INTEGER,\n );\n const result: Includeable[] = [];\n for (const associationName of Object.keys(input)) {\n const target = getAssociationTarget(model, associationName);\n if (!target) {\n continue;\n }\n\n const value = input[associationName];\n if (typeof value === 'string') {\n result.push(parseStringInclude(associationName, target, value, isGroupBy) as Includeable);\n continue;\n }\n if (!isRecord(value)) {\n continue;\n }\n\n const currentAssociationPath = [...associationPath, associationName];\n const required = parseBoolean(value.required, false) ?? false;\n const separate = parseBoolean(value.separate, false) ?? false;\n const paranoid = parseBoolean(value.paranoid, true) ?? true;\n const queryAssociationPath = separate ? [] : currentAssociationPath;\n const parsedWhere = Object.prototype.hasOwnProperty.call(value, 'q')\n ? parseWhere({ input: value.q, model: target, associationPath: queryAssociationPath }).where\n : undefined;\n const where = parsedWhere && Reflect.ownKeys(parsedWhere).length > 0 ? parsedWhere : undefined;\n const order = typeof value.sort === 'string' && value.sort.trim()\n ? parseOrder(target, value.sort)\n : undefined;\n const attributes = isGroupBy\n ? []\n : typeof value.select === 'string'\n ? parseSelection(target, value.select)\n : undefined;\n const nestedIncludes = Object.prototype.hasOwnProperty.call(value, 'include')\n ? parseIncludes({\n input: value.include,\n model: target,\n isGroupBy,\n depth: depth + 1,\n seen,\n associationPath: queryAssociationPath,\n maxLimit: normalizedMaxLimit,\n })\n : undefined;\n\n result.push({\n as: associationName,\n model: target,\n required,\n separate,\n paranoid,\n where,\n order,\n ...parseOptionalPagination(value, normalizedMaxLimit),\n attributes,\n include: nestedIncludes,\n } as Includeable);\n }\n return result;\n } finally {\n seen.delete(input);\n }\n};\n"]}
@@ -0,0 +1,49 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseOrder = void 0;
4
+ const fields_js_1 = require("../utils/fields.js");
5
+ const tokenizeSort = (input) => {
6
+ const tokens = [];
7
+ let token = '';
8
+ let bracketDepth = 0;
9
+ const pushToken = () => {
10
+ const trimmed = token.trim();
11
+ if (trimmed) {
12
+ tokens.push(trimmed);
13
+ }
14
+ token = '';
15
+ };
16
+ for (const character of input) {
17
+ if (character === ',' && bracketDepth === 0) {
18
+ pushToken();
19
+ continue;
20
+ }
21
+ token += character;
22
+ if (character === '[') {
23
+ bracketDepth += 1;
24
+ }
25
+ else if (character === ']') {
26
+ bracketDepth = Math.max(0, bracketDepth - 1);
27
+ }
28
+ }
29
+ pushToken();
30
+ return tokens;
31
+ };
32
+ const defaultOrder = (model) => (0, fields_js_1.resolveField)(model, 'id') ? [['id', 'DESC']] : [];
33
+ const parseOrder = (model, input) => {
34
+ if (typeof input !== 'string' || input.trim() === '') {
35
+ return defaultOrder(model);
36
+ }
37
+ const order = tokenizeSort(input).flatMap((token) => {
38
+ const descending = token.startsWith('-');
39
+ const fieldPath = descending ? token.slice(1) : token;
40
+ const resolved = (0, fields_js_1.resolveField)(model, fieldPath);
41
+ if (!resolved) {
42
+ return [];
43
+ }
44
+ return [[...resolved.path, descending ? 'DESC' : 'ASC']];
45
+ });
46
+ return order.length > 0 ? order : defaultOrder(model);
47
+ };
48
+ exports.parseOrder = parseOrder;
49
+ //# sourceMappingURL=order.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"order.js","sourceRoot":"","sources":["../../../src/parsers/order.ts"],"names":[],"mappings":";;;AAEA,kDAAkD;AAElD,MAAM,YAAY,GAAG,CAAC,KAAa,EAAY,EAAE;IAC/C,MAAM,MAAM,GAAa,EAAE,CAAC;IAC5B,IAAI,KAAK,GAAG,EAAE,CAAC;IACf,IAAI,YAAY,GAAG,CAAC,CAAC;IAErB,MAAM,SAAS,GAAG,GAAG,EAAE;QACrB,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC;QAC7B,IAAI,OAAO,EAAE,CAAC;YACZ,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QACvB,CAAC;QACD,KAAK,GAAG,EAAE,CAAC;IACb,CAAC,CAAC;IAEF,KAAK,MAAM,SAAS,IAAI,KAAK,EAAE,CAAC;QAC9B,IAAI,SAAS,KAAK,GAAG,IAAI,YAAY,KAAK,CAAC,EAAE,CAAC;YAC5C,SAAS,EAAE,CAAC;YACZ,SAAS;QACX,CAAC;QAED,KAAK,IAAI,SAAS,CAAC;QACnB,IAAI,SAAS,KAAK,GAAG,EAAE,CAAC;YACtB,YAAY,IAAI,CAAC,CAAC;QACpB,CAAC;aAAM,IAAI,SAAS,KAAK,GAAG,EAAE,CAAC;YAC7B,YAAY,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,YAAY,GAAG,CAAC,CAAC,CAAC;QAC/C,CAAC;IACH,CAAC;IACD,SAAS,EAAE,CAAC;IAEZ,OAAO,MAAM,CAAC;AAChB,CAAC,CAAC;AAEF,MAAM,YAAY,GAAG,CAAC,KAAyB,EAAS,EAAE,CACxD,IAAA,wBAAY,EAAC,KAAK,EAAE,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;AAE7C,MAAM,UAAU,GAAG,CAAC,KAAyB,EAAE,KAAc,EAAS,EAAE;IAC7E,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,EAAE,CAAC;QACrD,OAAO,YAAY,CAAC,KAAK,CAAC,CAAC;IAC7B,CAAC;IAED,MAAM,KAAK,GAAG,YAAY,CAAC,KAAK,CAAC,CAAC,OAAO,CAAC,CAAC,KAAK,EAAE,EAAE;QAClD,MAAM,UAAU,GAAG,KAAK,CAAC,UAAU,CAAC,GAAG,CAAC,CAAC;QACzC,MAAM,SAAS,GAAG,UAAU,CAAC,CAAC,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC;QACtD,MAAM,QAAQ,GAAG,IAAA,wBAAY,EAAC,KAAK,EAAE,SAAS,CAAC,CAAC;QAChD,IAAI,CAAC,QAAQ,EAAE,CAAC;YACd,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,OAAO,CAAC,CAAC,GAAG,QAAQ,CAAC,IAAI,EAAE,UAAU,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC;IAC3D,CAAC,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,CAAE,KAAe,CAAC,CAAC,CAAC,YAAY,CAAC,KAAK,CAAC,CAAC;AACnE,CAAC,CAAC;AAjBW,QAAA,UAAU,cAiBrB","sourcesContent":["import type { Model, ModelStatic, Order } from 'sequelize';\n\nimport { resolveField } from '../utils/fields.js';\n\nconst tokenizeSort = (input: string): string[] => {\n const tokens: string[] = [];\n let token = '';\n let bracketDepth = 0;\n\n const pushToken = () => {\n const trimmed = token.trim();\n if (trimmed) {\n tokens.push(trimmed);\n }\n token = '';\n };\n\n for (const character of input) {\n if (character === ',' && bracketDepth === 0) {\n pushToken();\n continue;\n }\n\n token += character;\n if (character === '[') {\n bracketDepth += 1;\n } else if (character === ']') {\n bracketDepth = Math.max(0, bracketDepth - 1);\n }\n }\n pushToken();\n\n return tokens;\n};\n\nconst defaultOrder = (model: ModelStatic<Model>): Order =>\n resolveField(model, 'id') ? [['id', 'DESC']] : [];\n\nexport const parseOrder = (model: ModelStatic<Model>, input: unknown): Order => {\n if (typeof input !== 'string' || input.trim() === '') {\n return defaultOrder(model);\n }\n\n const order = tokenizeSort(input).flatMap((token) => {\n const descending = token.startsWith('-');\n const fieldPath = descending ? token.slice(1) : token;\n const resolved = resolveField(model, fieldPath);\n if (!resolved) {\n return [];\n }\n\n return [[...resolved.path, descending ? 'DESC' : 'ASC']];\n });\n\n return order.length > 0 ? (order as Order) : defaultOrder(model);\n};\n"]}
@@ -0,0 +1,85 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.mergeSearchWhere = exports.parseSearch = void 0;
4
+ const sequelize_1 = require("sequelize");
5
+ const fields_js_1 = require("../utils/fields.js");
6
+ const values_js_1 = require("../utils/values.js");
7
+ const STRING_TYPES = new Set(['CHAR', 'CITEXT', 'STRING', 'TEXT', 'VARCHAR']);
8
+ const DATE_TYPES = new Set(['DATE', 'DATEONLY', 'DATETIME']);
9
+ const getOutputKey = (path) => path.length > 1 ? `$${path.join('.')}$` : path[0] ?? '';
10
+ const parseSearchFields = (value) => {
11
+ if (typeof value !== 'string') {
12
+ return [];
13
+ }
14
+ return [...new Set(value.split(',').map((field) => field.trim()).filter(Boolean))];
15
+ };
16
+ const parseYesNo = (value) => {
17
+ const normalized = value.trim().toLowerCase();
18
+ if (normalized && 'yes'.includes(normalized)) {
19
+ return true;
20
+ }
21
+ if (normalized && 'no'.includes(normalized)) {
22
+ return false;
23
+ }
24
+ return null;
25
+ };
26
+ const buildTextCast = (reference, searchText) => (0, sequelize_1.where)((0, sequelize_1.literal)(`${reference}::text`), sequelize_1.Op.iLike, `%${searchText}%`);
27
+ const parseSearch = ({ searchText, searchFields, checkYesNo, timezone, model, columns = {}, }) => {
28
+ if (typeof searchText !== 'string' || !searchText.trim()) {
29
+ return null;
30
+ }
31
+ const fields = parseSearchFields(searchFields);
32
+ const clauses = [];
33
+ const yesNo = (0, values_js_1.parseBoolean)(checkYesNo, false) ? parseYesNo(searchText) : null;
34
+ const validTimezone = (0, values_js_1.isValidTimeZone)(timezone) ? timezone : undefined;
35
+ for (const field of fields) {
36
+ const resolved = (0, fields_js_1.resolveField)(model, field);
37
+ if (!resolved) {
38
+ continue;
39
+ }
40
+ const typeName = resolved.typeName ?? columns[field]?.type;
41
+ const outputKey = getOutputKey(resolved.path);
42
+ if (typeName === 'BOOLEAN') {
43
+ if (yesNo !== null) {
44
+ clauses.push({ [outputKey]: yesNo });
45
+ }
46
+ continue;
47
+ }
48
+ if (STRING_TYPES.has(typeName ?? '')) {
49
+ clauses.push({ [outputKey]: { [sequelize_1.Op.iLike]: `%${searchText}%` } });
50
+ continue;
51
+ }
52
+ const reference = (0, fields_js_1.getSqlFieldReference)(model, resolved.path);
53
+ if (!reference) {
54
+ continue;
55
+ }
56
+ if (DATE_TYPES.has(typeName ?? '')) {
57
+ if (!(0, values_js_1.isValidDateText)(searchText)) {
58
+ continue;
59
+ }
60
+ const timezoneSql = validTimezone ? ` AT TIME ZONE '${validTimezone}'` : '';
61
+ clauses.push((0, sequelize_1.where)((0, sequelize_1.literal)(`TO_CHAR(${reference}${timezoneSql}, 'MM/DD/YYYY')`), sequelize_1.Op.iLike, `%${searchText}%`));
62
+ continue;
63
+ }
64
+ clauses.push(buildTextCast(reference, searchText));
65
+ }
66
+ return clauses.length > 0 ? { [sequelize_1.Op.or]: clauses } : null;
67
+ };
68
+ exports.parseSearch = parseSearch;
69
+ const mergeSearchWhere = (filterWhere, searchWhere) => {
70
+ if (!searchWhere) {
71
+ return filterWhere;
72
+ }
73
+ const filterRecord = filterWhere;
74
+ const searchRecord = searchWhere;
75
+ const filterOrValue = filterRecord[sequelize_1.Op.or];
76
+ const searchOrValue = searchRecord[sequelize_1.Op.or];
77
+ const filterOr = Array.isArray(filterOrValue) ? filterOrValue : [];
78
+ const searchOr = Array.isArray(searchOrValue) ? searchOrValue : [];
79
+ return {
80
+ ...filterRecord,
81
+ [sequelize_1.Op.or]: [...filterOr, ...searchOr],
82
+ };
83
+ };
84
+ exports.mergeSearchWhere = mergeSearchWhere;
85
+ //# sourceMappingURL=search.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"search.js","sourceRoot":"","sources":["../../../src/parsers/search.ts"],"names":[],"mappings":"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","sourcesContent":["import { Op, literal, where as sequelizeWhere } from 'sequelize';\nimport type { Model, ModelStatic, WhereOptions } from 'sequelize';\n\nimport type { ColumnDetail } from '../types.js';\nimport { getSqlFieldReference, resolveField } from '../utils/fields.js';\nimport { isValidDateText, isValidTimeZone, parseBoolean } from '../utils/values.js';\n\nconst STRING_TYPES = new Set(['CHAR', 'CITEXT', 'STRING', 'TEXT', 'VARCHAR']);\nconst DATE_TYPES = new Set(['DATE', 'DATEONLY', 'DATETIME']);\n\ninterface ParseSearchOptions {\n readonly searchText: unknown;\n readonly searchFields: unknown;\n readonly checkYesNo?: unknown;\n readonly timezone?: unknown;\n readonly model: ModelStatic<Model>;\n readonly columns?: Readonly<Partial<Record<string, ColumnDetail>>>;\n}\n\nconst getOutputKey = (path: readonly string[]): string =>\n path.length > 1 ? `$${path.join('.')}$` : path[0] ?? '';\n\nconst parseSearchFields = (value: unknown): string[] => {\n if (typeof value !== 'string') {\n return [];\n }\n\n return [...new Set(value.split(',').map((field) => field.trim()).filter(Boolean))];\n};\n\nconst parseYesNo = (value: string): boolean | null => {\n const normalized = value.trim().toLowerCase();\n if (normalized && 'yes'.includes(normalized)) {\n return true;\n }\n if (normalized && 'no'.includes(normalized)) {\n return false;\n }\n return null;\n};\n\nconst buildTextCast = (reference: string, searchText: string) =>\n sequelizeWhere(literal(`${reference}::text`), Op.iLike, `%${searchText}%`);\n\nexport const parseSearch = ({\n searchText,\n searchFields,\n checkYesNo,\n timezone,\n model,\n columns = {},\n}: ParseSearchOptions): WhereOptions | null => {\n if (typeof searchText !== 'string' || !searchText.trim()) {\n return null;\n }\n\n const fields = parseSearchFields(searchFields);\n const clauses: unknown[] = [];\n const yesNo = parseBoolean(checkYesNo, false) ? parseYesNo(searchText) : null;\n const validTimezone = isValidTimeZone(timezone) ? timezone : undefined;\n\n for (const field of fields) {\n const resolved = resolveField(model, field);\n if (!resolved) {\n continue;\n }\n\n const typeName = resolved.typeName ?? columns[field]?.type;\n const outputKey = getOutputKey(resolved.path);\n if (typeName === 'BOOLEAN') {\n if (yesNo !== null) {\n clauses.push({ [outputKey]: yesNo });\n }\n continue;\n }\n\n if (STRING_TYPES.has(typeName ?? '')) {\n clauses.push({ [outputKey]: { [Op.iLike]: `%${searchText}%` } });\n continue;\n }\n\n const reference = getSqlFieldReference(model, resolved.path);\n if (!reference) {\n continue;\n }\n\n if (DATE_TYPES.has(typeName ?? '')) {\n if (!isValidDateText(searchText)) {\n continue;\n }\n\n const timezoneSql = validTimezone ? ` AT TIME ZONE '${validTimezone}'` : '';\n clauses.push(\n sequelizeWhere(\n literal(`TO_CHAR(${reference}${timezoneSql}, 'MM/DD/YYYY')`),\n Op.iLike,\n `%${searchText}%`,\n ),\n );\n continue;\n }\n\n clauses.push(buildTextCast(reference, searchText));\n }\n\n return clauses.length > 0 ? ({ [Op.or]: clauses } as WhereOptions) : null;\n};\n\nexport const mergeSearchWhere = (\n filterWhere: WhereOptions,\n searchWhere: WhereOptions | null,\n): WhereOptions => {\n if (!searchWhere) {\n return filterWhere;\n }\n\n const filterRecord = filterWhere as Record<PropertyKey, unknown>;\n const searchRecord = searchWhere as Record<PropertyKey, unknown>;\n const filterOrValue = filterRecord[Op.or];\n const searchOrValue = searchRecord[Op.or];\n const filterOr = Array.isArray(filterOrValue) ? filterOrValue : [];\n const searchOr = Array.isArray(searchOrValue) ? searchOrValue : [];\n\n return {\n ...filterRecord,\n [Op.or]: [...filterOr, ...searchOr],\n } as WhereOptions;\n};\n"]}
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.parseSelection = void 0;
4
+ const fields_js_1 = require("../utils/fields.js");
5
+ const IDENTITY_FIELDS = ['id', 'deleted_at'];
6
+ const parseSelection = (model, input) => {
7
+ if (typeof input !== 'string' || input.trim() === '' || input === 'all') {
8
+ return (0, fields_js_1.getAttributeNames)(model);
9
+ }
10
+ if (input === 'notAll') {
11
+ return [];
12
+ }
13
+ const selected = [];
14
+ for (const candidate of input.split(',')) {
15
+ const field = candidate.trim();
16
+ const resolved = (0, fields_js_1.resolveField)(model, field);
17
+ if (resolved?.path.length === 1 && !selected.includes(resolved.attribute)) {
18
+ selected.push(resolved.attribute);
19
+ }
20
+ }
21
+ for (const identityField of IDENTITY_FIELDS) {
22
+ if ((0, fields_js_1.resolveField)(model, identityField) && !selected.includes(identityField)) {
23
+ selected.push(identityField);
24
+ }
25
+ }
26
+ return selected;
27
+ };
28
+ exports.parseSelection = parseSelection;
29
+ //# sourceMappingURL=select.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"select.js","sourceRoot":"","sources":["../../../src/parsers/select.ts"],"names":[],"mappings":";;;AAEA,kDAAqE;AAErE,MAAM,eAAe,GAAG,CAAC,IAAI,EAAE,YAAY,CAAU,CAAC;AAE/C,MAAM,cAAc,GAAG,CAAC,KAAyB,EAAE,KAAc,EAAY,EAAE;IACpF,IAAI,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,IAAI,EAAE,KAAK,EAAE,IAAI,KAAK,KAAK,KAAK,EAAE,CAAC;QACxE,OAAO,IAAA,6BAAiB,EAAC,KAAK,CAAC,CAAC;IAClC,CAAC;IAED,IAAI,KAAK,KAAK,QAAQ,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,QAAQ,GAAa,EAAE,CAAC;IAC9B,KAAK,MAAM,SAAS,IAAI,KAAK,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,CAAC;QACzC,MAAM,KAAK,GAAG,SAAS,CAAC,IAAI,EAAE,CAAC;QAC/B,MAAM,QAAQ,GAAG,IAAA,wBAAY,EAAC,KAAK,EAAE,KAAK,CAAC,CAAC;QAC5C,IAAI,QAAQ,EAAE,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,QAAQ,CAAC,SAAS,CAAC,EAAE,CAAC;YAC1E,QAAQ,CAAC,IAAI,CAAC,QAAQ,CAAC,SAAS,CAAC,CAAC;QACpC,CAAC;IACH,CAAC;IAED,KAAK,MAAM,aAAa,IAAI,eAAe,EAAE,CAAC;QAC5C,IAAI,IAAA,wBAAY,EAAC,KAAK,EAAE,aAAa,CAAC,IAAI,CAAC,QAAQ,CAAC,QAAQ,CAAC,aAAa,CAAC,EAAE,CAAC;YAC5E,QAAQ,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC;QAC/B,CAAC;IACH,CAAC;IAED,OAAO,QAAQ,CAAC;AAClB,CAAC,CAAC;AAzBW,QAAA,cAAc,kBAyBzB","sourcesContent":["import type { Model, ModelStatic } from 'sequelize';\n\nimport { getAttributeNames, resolveField } from '../utils/fields.js';\n\nconst IDENTITY_FIELDS = ['id', 'deleted_at'] as const;\n\nexport const parseSelection = (model: ModelStatic<Model>, input: unknown): string[] => {\n if (typeof input !== 'string' || input.trim() === '' || input === 'all') {\n return getAttributeNames(model);\n }\n\n if (input === 'notAll') {\n return [];\n }\n\n const selected: string[] = [];\n for (const candidate of input.split(',')) {\n const field = candidate.trim();\n const resolved = resolveField(model, field);\n if (resolved?.path.length === 1 && !selected.includes(resolved.attribute)) {\n selected.push(resolved.attribute);\n }\n }\n\n for (const identityField of IDENTITY_FIELDS) {\n if (resolveField(model, identityField) && !selected.includes(identityField)) {\n selected.push(identityField);\n }\n }\n\n return selected;\n};\n"]}