@rangka/core 0.1.1 → 0.1.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +6 -2
- package/.claude/skills/extend-core/SKILL.md +0 -133
- package/.turbo/turbo-build.log +0 -4
- package/CHANGELOG.md +0 -25
- package/CLAUDE.md +0 -180
- package/src/__tests__/coerce.test.ts +0 -154
- package/src/__tests__/context.test.ts +0 -111
- package/src/__tests__/helpers.ts +0 -21
- package/src/__tests__/index.test.ts +0 -7
- package/src/__tests__/widgets.test.ts +0 -197
- package/src/api/__tests__/handlers.test.ts +0 -389
- package/src/api/__tests__/include-resolver.test.ts +0 -393
- package/src/api/__tests__/middleware.test.ts +0 -100
- package/src/api/__tests__/openapi-schema.test.ts +0 -210
- package/src/api/__tests__/query-parser.test.ts +0 -291
- package/src/api/__tests__/route-generator.test.ts +0 -137
- package/src/api/__tests__/server.test.ts +0 -73
- package/src/api/__tests__/swagger.test.ts +0 -166
- package/src/api/handlers.ts +0 -274
- package/src/api/include-resolver.ts +0 -27
- package/src/api/index.ts +0 -4
- package/src/api/meta-handler.ts +0 -254
- package/src/api/openapi-schema.ts +0 -99
- package/src/api/query-parser.ts +0 -315
- package/src/api/route-generator.ts +0 -448
- package/src/api/server.ts +0 -147
- package/src/api/types.ts +0 -16
- package/src/audit/__tests__/audit.test.ts +0 -144
- package/src/audit/index.ts +0 -3
- package/src/audit/record.ts +0 -69
- package/src/audit/tables.ts +0 -48
- package/src/audit/types.ts +0 -26
- package/src/auth/__tests__/core-module.test.ts +0 -54
- package/src/auth/__tests__/debug.test.ts +0 -47
- package/src/auth/__tests__/field-permissions.test.ts +0 -245
- package/src/auth/__tests__/integration.test.ts +0 -208
- package/src/auth/__tests__/meta-boot.test.ts +0 -538
- package/src/auth/__tests__/model-permissions.test.ts +0 -205
- package/src/auth/__tests__/password.test.ts +0 -29
- package/src/auth/__tests__/permission-registry.test.ts +0 -313
- package/src/auth/__tests__/scope-hook.test.ts +0 -509
- package/src/auth/__tests__/scope-registry.test.ts +0 -297
- package/src/auth/__tests__/scopes.test.ts +0 -66
- package/src/auth/__tests__/session.test.ts +0 -214
- package/src/auth/core-models.ts +0 -52
- package/src/auth/core-module.ts +0 -59
- package/src/auth/debug.ts +0 -157
- package/src/auth/field-permissions.ts +0 -116
- package/src/auth/index.ts +0 -37
- package/src/auth/model-permissions.ts +0 -59
- package/src/auth/password.ts +0 -22
- package/src/auth/permission-registry.ts +0 -171
- package/src/auth/scope-filters.ts +0 -11
- package/src/auth/scope-registry.ts +0 -121
- package/src/auth/scopes.ts +0 -146
- package/src/auth/seed.ts +0 -44
- package/src/auth/session.ts +0 -178
- package/src/auth/types.ts +0 -50
- package/src/boot/__tests__/page-scanning.test.ts +0 -170
- package/src/boot/__tests__/page-utils.test.ts +0 -225
- package/src/boot/__tests__/project-scanner.test.ts +0 -88
- package/src/boot/dependency-sort.ts +0 -82
- package/src/boot/discovery.ts +0 -85
- package/src/boot/index.ts +0 -457
- package/src/boot/page-utils.ts +0 -110
- package/src/boot/project-scanner.ts +0 -397
- package/src/boot/schema-loader.ts +0 -26
- package/src/boot/schema-merger.ts +0 -125
- package/src/boot/traits.ts +0 -25
- package/src/boot/types.ts +0 -73
- package/src/context.ts +0 -105
- package/src/db/__tests__/cascade-delete.test.ts +0 -182
- package/src/db/__tests__/desired-state.test.ts +0 -136
- package/src/db/__tests__/diff-engine.test.ts +0 -635
- package/src/db/__tests__/field-mapper.test.ts +0 -355
- package/src/db/__tests__/introspect.test.ts +0 -70
- package/src/db/__tests__/search-filter.test.ts +0 -45
- package/src/db/__tests__/sequence.test.ts +0 -221
- package/src/db/auto-sync.ts +0 -133
- package/src/db/client.ts +0 -147
- package/src/db/desired-state.ts +0 -98
- package/src/db/diff-engine.ts +0 -305
- package/src/db/field-mapper.ts +0 -504
- package/src/db/filter-applier.ts +0 -89
- package/src/db/include-resolver.ts +0 -40
- package/src/db/index.ts +0 -23
- package/src/db/introspect.ts +0 -265
- package/src/db/model-include-resolver.ts +0 -327
- package/src/db/model-ops.ts +0 -281
- package/src/db/scope-enforcer.ts +0 -37
- package/src/db/types.ts +0 -98
- package/src/errors.ts +0 -41
- package/src/events/__tests__/bus.test.ts +0 -105
- package/src/events/bus.ts +0 -89
- package/src/events/index.ts +0 -2
- package/src/events/types.ts +0 -9
- package/src/external-model/__tests__/computed-fields.test.ts +0 -106
- package/src/external-model/__tests__/field-mapper.test.ts +0 -160
- package/src/external-model/__tests__/in-memory-ops.test.ts +0 -247
- package/src/external-model/__tests__/mutation-executor.test.ts +0 -160
- package/src/external-model/__tests__/query-executor.test.ts +0 -284
- package/src/external-model/__tests__/schema-converter.test.ts +0 -174
- package/src/external-model/computed-fields.ts +0 -15
- package/src/external-model/define.ts +0 -5
- package/src/external-model/external-model-ops.ts +0 -108
- package/src/external-model/field-mapper.ts +0 -66
- package/src/external-model/in-memory-ops.ts +0 -107
- package/src/external-model/index.ts +0 -7
- package/src/external-model/mutation-executor.ts +0 -71
- package/src/external-model/query-executor.ts +0 -100
- package/src/external-model/schema-converter.ts +0 -53
- package/src/external-model/types.ts +0 -32
- package/src/fixtures/__tests__/fixtures.test.ts +0 -203
- package/src/fixtures/index.ts +0 -10
- package/src/fixtures/loader.ts +0 -196
- package/src/fixtures/registry.ts +0 -125
- package/src/fixtures/types.ts +0 -33
- package/src/helpers/assert-ownership.ts +0 -19
- package/src/helpers/coerce.ts +0 -28
- package/src/helpers/stamping.ts +0 -28
- package/src/helpers/validation.ts +0 -14
- package/src/hooks/__tests__/context.test.ts +0 -73
- package/src/hooks/__tests__/executor.test.ts +0 -433
- package/src/hooks/__tests__/middleware.test.ts +0 -224
- package/src/hooks/__tests__/registry.test.ts +0 -50
- package/src/hooks/context.ts +0 -89
- package/src/hooks/errors.ts +0 -11
- package/src/hooks/executor.ts +0 -115
- package/src/hooks/index.ts +0 -10
- package/src/hooks/middleware.ts +0 -220
- package/src/hooks/registry.ts +0 -20
- package/src/hooks/types.ts +0 -32
- package/src/index.ts +0 -172
- package/src/jobs/__tests__/enqueue.test.ts +0 -77
- package/src/jobs/__tests__/integration.test.ts +0 -71
- package/src/jobs/__tests__/registry.test.ts +0 -103
- package/src/jobs/__tests__/scheduler.test.ts +0 -92
- package/src/jobs/__tests__/worker-execution.test.ts +0 -202
- package/src/jobs/__tests__/worker.test.ts +0 -119
- package/src/jobs/enqueue.ts +0 -93
- package/src/jobs/index.ts +0 -14
- package/src/jobs/registry.ts +0 -92
- package/src/jobs/scheduler.ts +0 -205
- package/src/jobs/tables.ts +0 -132
- package/src/jobs/types.ts +0 -62
- package/src/jobs/worker.ts +0 -272
- package/src/model-api/__tests__/cross-boundary-includes.test.ts +0 -366
- package/src/model-api/__tests__/extended-api.test.ts +0 -244
- package/src/model-api/__tests__/filter-applier.test.ts +0 -177
- package/src/model-api/__tests__/filter-translator.test.ts +0 -186
- package/src/model-api/__tests__/include-resolver.test.ts +0 -226
- package/src/model-api/__tests__/model-access.test.ts +0 -284
- package/src/model-api/__tests__/query-builder.test.ts +0 -224
- package/src/model-api/__tests__/scope-enforcer.test.ts +0 -268
- package/src/model-api/field-access.ts +0 -28
- package/src/model-api/filter-applier.ts +0 -1
- package/src/model-api/filter-translator.ts +0 -67
- package/src/model-api/include-resolver.ts +0 -2
- package/src/model-api/index.ts +0 -86
- package/src/model-api/query-builder.ts +0 -155
- package/src/model-api/scope-enforcer.ts +0 -3
- package/src/model-api/types.ts +0 -139
- package/src/plugins/__tests__/adapter-registry.test.ts +0 -92
- package/src/plugins/__tests__/lifecycle.test.ts +0 -96
- package/src/plugins/__tests__/loader.test.ts +0 -273
- package/src/plugins/__tests__/validator.test.ts +0 -275
- package/src/plugins/adapter-registry.ts +0 -42
- package/src/plugins/define.ts +0 -5
- package/src/plugins/index.ts +0 -28
- package/src/plugins/lifecycle.ts +0 -27
- package/src/plugins/loader.ts +0 -126
- package/src/plugins/types.ts +0 -76
- package/src/plugins/validator.ts +0 -141
- package/src/schema/__tests__/registry-models-by-module.test.ts +0 -58
- package/src/schema/registry.ts +0 -93
- package/src/schema/relationships.ts +0 -93
- package/src/schema/types.ts +0 -43
- package/src/services/__tests__/integration.test.ts +0 -63
- package/src/services/__tests__/registry.test.ts +0 -175
- package/src/services/index.ts +0 -13
- package/src/services/registry.ts +0 -156
- package/src/services/types.ts +0 -27
- package/src/validation/__tests__/field-validator.test.ts +0 -195
- package/src/validation/field-validator.ts +0 -113
- package/src/validation/index.ts +0 -1
- package/src/widgets/index.ts +0 -3
- package/src/widgets/slot-validator.ts +0 -87
- package/src/widgets/widget-registry.ts +0 -32
- package/tests/boot.test.ts +0 -323
- package/tests/dependency-sort.test.ts +0 -99
- package/tests/discovery.test.ts +0 -126
- package/tests/registry.test.ts +0 -216
- package/tests/schema-loader.test.ts +0 -52
- package/tests/schema-merger.test.ts +0 -180
- package/tsconfig.json +0 -9
- package/tsconfig.tsbuildinfo +0 -1
- package/vitest.config.ts +0 -14
package/src/api/query-parser.ts
DELETED
|
@@ -1,315 +0,0 @@
|
|
|
1
|
-
import type { ResolvedField } from '../schema/types.js';
|
|
2
|
-
import type { FieldConfig } from '@rangka/shared';
|
|
3
|
-
import { toBool, toInt, isNil } from '../helpers/coerce.js';
|
|
4
|
-
|
|
5
|
-
// --- Result types returned by the parser ---
|
|
6
|
-
|
|
7
|
-
export interface ParsedFilter {
|
|
8
|
-
field: string;
|
|
9
|
-
operator: string;
|
|
10
|
-
value: unknown;
|
|
11
|
-
}
|
|
12
|
-
|
|
13
|
-
export interface ParsedSort {
|
|
14
|
-
field: string;
|
|
15
|
-
direction: 'asc' | 'desc';
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
export interface ParsedPagination {
|
|
19
|
-
page: number;
|
|
20
|
-
limit: number;
|
|
21
|
-
offset: number;
|
|
22
|
-
}
|
|
23
|
-
|
|
24
|
-
export interface ParsedInclude {
|
|
25
|
-
relation: string;
|
|
26
|
-
nested?: ParsedInclude[];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export interface ParsedQuery {
|
|
30
|
-
filters: ParsedFilter[];
|
|
31
|
-
sort: ParsedSort[];
|
|
32
|
-
pagination: ParsedPagination;
|
|
33
|
-
includes: ParsedInclude[];
|
|
34
|
-
fields: string[];
|
|
35
|
-
search?: string;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
// --- Type classification sets for operator validation ---
|
|
39
|
-
|
|
40
|
-
const NUMERIC_TYPES = new Set(['int', 'decimal', 'money']);
|
|
41
|
-
const DATE_TYPES = new Set(['date', 'datetime']);
|
|
42
|
-
/** Types that support comparison operators (gt, gte, lt, lte) */
|
|
43
|
-
const ORDERED_TYPES = new Set([...NUMERIC_TYPES, ...DATE_TYPES]);
|
|
44
|
-
|
|
45
|
-
const COMPARISON_OPERATORS = new Set(['gt', 'gte', 'lt', 'lte']);
|
|
46
|
-
const ALL_OPERATORS = new Set(['eq', 'neq', 'gt', 'gte', 'lt', 'lte', 'like', 'in', 'isnull']);
|
|
47
|
-
|
|
48
|
-
// --- Pagination defaults ---
|
|
49
|
-
|
|
50
|
-
const DEFAULT_PAGE = 1;
|
|
51
|
-
const DEFAULT_LIMIT = 25;
|
|
52
|
-
const MIN_LIMIT = 1;
|
|
53
|
-
const MAX_LIMIT = 100;
|
|
54
|
-
const MAX_INCLUDE_DEPTH = 2;
|
|
55
|
-
|
|
56
|
-
// --- Helpers ---
|
|
57
|
-
|
|
58
|
-
/** Split a comma-separated string into trimmed, non-empty parts. */
|
|
59
|
-
function splitCommaSeparated(input: string): string[] {
|
|
60
|
-
return input
|
|
61
|
-
.split(',')
|
|
62
|
-
.map((segment) => segment.trim())
|
|
63
|
-
.filter(Boolean);
|
|
64
|
-
}
|
|
65
|
-
|
|
66
|
-
/** Clamp a numeric value to a [min, max] range. */
|
|
67
|
-
function clamp(value: number, min: number, max: number): number {
|
|
68
|
-
return Math.max(min, Math.min(max, value));
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
/**
|
|
72
|
-
* Parses incoming query-string parameters into a structured, validated query object.
|
|
73
|
-
* Validates field names, operators, and types against the entity schema.
|
|
74
|
-
*/
|
|
75
|
-
export class QueryParser {
|
|
76
|
-
private readonly fieldMap: Map<string, FieldConfig>;
|
|
77
|
-
private readonly relationNames: Set<string>;
|
|
78
|
-
|
|
79
|
-
constructor(fields: ResolvedField[], relationNames: string[]) {
|
|
80
|
-
this.fieldMap = new Map();
|
|
81
|
-
for (const resolved of fields) {
|
|
82
|
-
this.fieldMap.set(resolved.name, resolved.config);
|
|
83
|
-
}
|
|
84
|
-
this.relationNames = new Set(relationNames);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/** Parse all query sections at once. */
|
|
88
|
-
parse(query: Record<string, unknown>): ParsedQuery {
|
|
89
|
-
return {
|
|
90
|
-
filters: this.parseFilters(query),
|
|
91
|
-
sort: this.parseSort(query),
|
|
92
|
-
pagination: this.parsePagination(query),
|
|
93
|
-
includes: this.parseIncludes(query),
|
|
94
|
-
fields: this.parseFields(query),
|
|
95
|
-
search: this.parseSearch(query),
|
|
96
|
-
};
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
/**
|
|
100
|
-
* Parse filter conditions from `query.filter`.
|
|
101
|
-
* Expected shape: { filter: { fieldName: { operator: value } } }
|
|
102
|
-
*/
|
|
103
|
-
parseFilters(query: Record<string, unknown>): ParsedFilter[] {
|
|
104
|
-
const filterParam = query['filter'];
|
|
105
|
-
if (!filterParam || typeof filterParam !== 'object') return [];
|
|
106
|
-
|
|
107
|
-
const filters: ParsedFilter[] = [];
|
|
108
|
-
const filtersByField = filterParam as Record<string, unknown>;
|
|
109
|
-
|
|
110
|
-
for (const [fieldName, operatorMap] of Object.entries(filtersByField)) {
|
|
111
|
-
const fieldConfig = this.getFieldConfigOrThrow(fieldName);
|
|
112
|
-
|
|
113
|
-
if (!operatorMap || typeof operatorMap !== 'object') continue;
|
|
114
|
-
const operatorEntries = operatorMap as Record<string, unknown>;
|
|
115
|
-
|
|
116
|
-
for (const [operator, rawValue] of Object.entries(operatorEntries)) {
|
|
117
|
-
if (!ALL_OPERATORS.has(operator)) {
|
|
118
|
-
throw new QueryValidationError(`Unknown filter operator: ${operator}`);
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
this.validateOperatorForType(fieldName, operator, fieldConfig);
|
|
122
|
-
|
|
123
|
-
filters.push({
|
|
124
|
-
field: fieldName,
|
|
125
|
-
operator,
|
|
126
|
-
value: this.coerceValue(operator, rawValue, fieldConfig),
|
|
127
|
-
});
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
return filters;
|
|
132
|
-
}
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Parse sort directives from `query.sort`.
|
|
136
|
-
* Expected format: "field1,-field2" (prefix with `-` for descending).
|
|
137
|
-
*/
|
|
138
|
-
parseSort(query: Record<string, unknown>): ParsedSort[] {
|
|
139
|
-
const sortParam = query['sort'];
|
|
140
|
-
if (!sortParam || typeof sortParam !== 'string') return [];
|
|
141
|
-
|
|
142
|
-
const sortFields = splitCommaSeparated(sortParam);
|
|
143
|
-
const result: ParsedSort[] = [];
|
|
144
|
-
|
|
145
|
-
for (const entry of sortFields) {
|
|
146
|
-
const isDescending = entry.startsWith('-');
|
|
147
|
-
const fieldName = isDescending ? entry.slice(1) : entry;
|
|
148
|
-
|
|
149
|
-
if (!this.fieldMap.has(fieldName)) {
|
|
150
|
-
throw new QueryValidationError(`Unknown sort field: ${fieldName}`);
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
result.push({ field: fieldName, direction: isDescending ? 'desc' : 'asc' });
|
|
154
|
-
}
|
|
155
|
-
|
|
156
|
-
return result;
|
|
157
|
-
}
|
|
158
|
-
|
|
159
|
-
/** Parse page/limit from the query, applying defaults and bounds. */
|
|
160
|
-
parsePagination(query: Record<string, unknown>): ParsedPagination {
|
|
161
|
-
const page = this.parsePageNumber(query['page']);
|
|
162
|
-
const limit = this.parseLimit(query['limit']);
|
|
163
|
-
const offset = (page - 1) * limit;
|
|
164
|
-
|
|
165
|
-
return { page, limit, offset };
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* Parse relation includes from `query.include`.
|
|
170
|
-
* Supports dot notation for nested relations up to depth 2: "posts.comments"
|
|
171
|
-
*/
|
|
172
|
-
parseIncludes(query: Record<string, unknown>): ParsedInclude[] {
|
|
173
|
-
const includeParam = query['include'];
|
|
174
|
-
if (!includeParam || typeof includeParam !== 'string') return [];
|
|
175
|
-
|
|
176
|
-
const includePaths = splitCommaSeparated(includeParam);
|
|
177
|
-
const result: ParsedInclude[] = [];
|
|
178
|
-
|
|
179
|
-
for (const path of includePaths) {
|
|
180
|
-
const segments = path.split('.');
|
|
181
|
-
|
|
182
|
-
if (segments.length > MAX_INCLUDE_DEPTH) {
|
|
183
|
-
throw new QueryValidationError(
|
|
184
|
-
`Max include depth of ${MAX_INCLUDE_DEPTH} exceeded: ${path}`,
|
|
185
|
-
);
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
if (!this.relationNames.has(segments[0])) {
|
|
189
|
-
throw new QueryValidationError(`Unknown relation: ${segments[0]}`);
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
const parsed: ParsedInclude = { relation: segments[0] };
|
|
193
|
-
if (segments.length === 2) {
|
|
194
|
-
parsed.nested = [{ relation: segments[1] }];
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
result.push(parsed);
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
return result;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Parse sparse fieldsets from `query.fields`.
|
|
205
|
-
* Always ensures "id" is included.
|
|
206
|
-
*/
|
|
207
|
-
parseFields(query: Record<string, unknown>): string[] {
|
|
208
|
-
const fieldsParam = query['fields'];
|
|
209
|
-
if (!fieldsParam || typeof fieldsParam !== 'string') return [];
|
|
210
|
-
|
|
211
|
-
const requestedFields = splitCommaSeparated(fieldsParam);
|
|
212
|
-
|
|
213
|
-
for (const fieldName of requestedFields) {
|
|
214
|
-
if (fieldName === 'id') continue;
|
|
215
|
-
if (!this.fieldMap.has(fieldName)) {
|
|
216
|
-
throw new QueryValidationError(`Unknown field: ${fieldName}`);
|
|
217
|
-
}
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// Ensure "id" is always present at the start
|
|
221
|
-
if (!requestedFields.includes('id')) {
|
|
222
|
-
requestedFields.unshift('id');
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
return requestedFields;
|
|
226
|
-
}
|
|
227
|
-
|
|
228
|
-
// --- Private helpers ---
|
|
229
|
-
|
|
230
|
-
/** Look up a field config, throwing if the field is not recognized. */
|
|
231
|
-
private getFieldConfigOrThrow(fieldName: string): FieldConfig {
|
|
232
|
-
const config = this.fieldMap.get(fieldName);
|
|
233
|
-
if (!config) {
|
|
234
|
-
throw new QueryValidationError(`Unknown filter field: ${fieldName}`);
|
|
235
|
-
}
|
|
236
|
-
return config;
|
|
237
|
-
}
|
|
238
|
-
|
|
239
|
-
/** Validate that the operator is allowed for the field's type. */
|
|
240
|
-
private validateOperatorForType(fieldName: string, operator: string, config: FieldConfig): void {
|
|
241
|
-
if (COMPARISON_OPERATORS.has(operator) && !ORDERED_TYPES.has(config.type)) {
|
|
242
|
-
throw new QueryValidationError(
|
|
243
|
-
`Operator '${operator}' is not valid for ${config.type} field '${fieldName}'`,
|
|
244
|
-
);
|
|
245
|
-
}
|
|
246
|
-
|
|
247
|
-
if (operator === 'like' && config.type !== 'string' && config.type !== 'text') {
|
|
248
|
-
throw new QueryValidationError(
|
|
249
|
-
`Operator 'like' is not valid for ${config.type} field '${fieldName}'`,
|
|
250
|
-
);
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
|
|
254
|
-
/** Coerce a raw query value to the appropriate JS type based on the operator and field config. */
|
|
255
|
-
private coerceValue(operator: string, rawValue: unknown, config: FieldConfig): unknown {
|
|
256
|
-
const stringValue = String(rawValue);
|
|
257
|
-
|
|
258
|
-
if (operator === 'isnull') {
|
|
259
|
-
return toBool(rawValue);
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
if (operator === 'in') {
|
|
263
|
-
return stringValue.split(',').map((item) => this.coerceSingleValue(item.trim(), config));
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
return this.coerceSingleValue(stringValue, config);
|
|
267
|
-
}
|
|
268
|
-
|
|
269
|
-
/** Coerce a single string value to its typed representation (number, boolean, or string). */
|
|
270
|
-
private coerceSingleValue(value: string, config: FieldConfig): unknown {
|
|
271
|
-
if (NUMERIC_TYPES.has(config.type)) {
|
|
272
|
-
const parsed = Number(value);
|
|
273
|
-
if (isNaN(parsed)) return value;
|
|
274
|
-
return parsed;
|
|
275
|
-
}
|
|
276
|
-
if (config.type === 'boolean') {
|
|
277
|
-
return toBool(value);
|
|
278
|
-
}
|
|
279
|
-
return value;
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
/** Parse and validate the page number, defaulting to 1 if invalid. */
|
|
283
|
-
private parsePageNumber(raw: unknown): number {
|
|
284
|
-
const parsed = toInt(raw, -1);
|
|
285
|
-
if (parsed < 1) return DEFAULT_PAGE;
|
|
286
|
-
return parsed;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
/** Parse and validate the limit, clamping to [MIN_LIMIT, MAX_LIMIT]. */
|
|
290
|
-
private parseLimit(raw: unknown): number {
|
|
291
|
-
if (isNil(raw) || raw === '') return DEFAULT_LIMIT;
|
|
292
|
-
const parsed = toInt(raw, -1);
|
|
293
|
-
if (parsed < 0) return DEFAULT_LIMIT;
|
|
294
|
-
return clamp(parsed, MIN_LIMIT, MAX_LIMIT);
|
|
295
|
-
}
|
|
296
|
-
|
|
297
|
-
/** Parse the search term from the query, trimming whitespace. */
|
|
298
|
-
parseSearch(query: Record<string, unknown>): string | undefined {
|
|
299
|
-
const raw = query['search'];
|
|
300
|
-
if (!raw || typeof raw !== 'string') return undefined;
|
|
301
|
-
const trimmed = raw.trim();
|
|
302
|
-
return trimmed.length > 0 ? trimmed : undefined;
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
|
|
306
|
-
/** Thrown when a query string contains invalid field names, operators, or values. */
|
|
307
|
-
export class QueryValidationError extends Error {
|
|
308
|
-
public readonly statusCode = 400;
|
|
309
|
-
public readonly code = 'QUERY_VALIDATION_ERROR';
|
|
310
|
-
|
|
311
|
-
constructor(message: string) {
|
|
312
|
-
super(message);
|
|
313
|
-
this.name = 'QueryValidationError';
|
|
314
|
-
}
|
|
315
|
-
}
|