@nage-api/data 1.0.0-beta.2

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.
@@ -0,0 +1,67 @@
1
+ /**
2
+ * `NageDataModule` (PLAN.md §7.3, §14.1).
3
+ *
4
+ * `forRoot` publishes the unit of work and the health probe a driver supplies;
5
+ * `forFeature` registers one repository per model under the token from
6
+ * `@nage-api/core`. Application services inject the token, drivers provide it, and
7
+ * neither imports the other — which is what makes the engine a scaffold-time
8
+ * choice instead of a runtime branch.
9
+ */
10
+ import { type DynamicModule } from '@nestjs/common';
11
+ import type { DataSourceHealth, Id, RepositoryPort, UnitOfWork } from '@nage-api/contracts';
12
+ export interface DataModuleOptions {
13
+ /** Transaction boundary implemented by the selected driver. */
14
+ readonly unitOfWork: UnitOfWork;
15
+ /** Connection probe feeding `/health/ready`. */
16
+ readonly health?: DataSourceHealth;
17
+ }
18
+ /** One model's registration: a name and the repository serving it. */
19
+ export interface ModelRegistration<TEntity extends {
20
+ id: Id;
21
+ }> {
22
+ readonly model: string;
23
+ readonly repository: RepositoryPort<TEntity>;
24
+ }
25
+ declare const erasedEntity: unique symbol;
26
+ /**
27
+ * A registration whose entity type has been erased.
28
+ *
29
+ * `forFeature` takes several models at once, and TypeScript cannot spell "an
30
+ * array whose elements are each independently generic". This used to be typed
31
+ * `readonly ModelRegistration<never>[]`, which reads like the erased form but is
32
+ * inhabited by nothing — `RepositoryPort<never>` accepts no repository, so the
33
+ * method could not be called at all. The first real caller (a generated app)
34
+ * found that immediately.
35
+ *
36
+ * The brand is what keeps the erasure honest: an object literal cannot satisfy
37
+ * it, so every registration goes through `registerModel`, where the entity type
38
+ * *is* checked.
39
+ */
40
+ export interface ErasedModelRegistration {
41
+ readonly model: string;
42
+ readonly repository: unknown;
43
+ readonly [erasedEntity]: true;
44
+ }
45
+ /**
46
+ * Pair a model name with the repository serving it, for `forFeature`.
47
+ *
48
+ * ```ts
49
+ * NageDataModule.forFeature([registerModel('Product', productRepository)])
50
+ * ```
51
+ */
52
+ export declare function registerModel<TEntity extends {
53
+ id: Id;
54
+ }>(model: string, repository: RepositoryPort<TEntity>): ErasedModelRegistration;
55
+ export declare class NageDataModule {
56
+ static forRoot(options: DataModuleOptions): DynamicModule;
57
+ /**
58
+ * Register repositories for a feature module.
59
+ *
60
+ * ```ts
61
+ * @Module({ imports: [NageDataModule.forFeature([registerModel('Product', repository)])] })
62
+ * ```
63
+ */
64
+ static forFeature(registrations: readonly ErasedModelRegistration[]): DynamicModule;
65
+ }
66
+ export {};
67
+ //# sourceMappingURL=data.module.d.ts.map
@@ -0,0 +1,71 @@
1
+ "use strict";
2
+ /**
3
+ * `NageDataModule` (PLAN.md §7.3, §14.1).
4
+ *
5
+ * `forRoot` publishes the unit of work and the health probe a driver supplies;
6
+ * `forFeature` registers one repository per model under the token from
7
+ * `@nage-api/core`. Application services inject the token, drivers provide it, and
8
+ * neither imports the other — which is what makes the engine a scaffold-time
9
+ * choice instead of a runtime branch.
10
+ */
11
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
12
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
13
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
14
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
15
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
16
+ };
17
+ var NageDataModule_1;
18
+ Object.defineProperty(exports, "__esModule", { value: true });
19
+ exports.NageDataModule = void 0;
20
+ exports.registerModel = registerModel;
21
+ const common_1 = require("@nestjs/common");
22
+ const core_1 = require("@nage-api/core");
23
+ const tokens_js_1 = require("../tokens.js");
24
+ const core_2 = require("@nage-api/core");
25
+ /**
26
+ * Pair a model name with the repository serving it, for `forFeature`.
27
+ *
28
+ * ```ts
29
+ * NageDataModule.forFeature([registerModel('Product', productRepository)])
30
+ * ```
31
+ */
32
+ function registerModel(model, repository) {
33
+ // The one cast, here rather than in every caller: the module only ever hands
34
+ // the repository to `useValue`, so nothing downstream needs the entity type.
35
+ return { model, repository };
36
+ }
37
+ let NageDataModule = NageDataModule_1 = class NageDataModule {
38
+ static forRoot(options) {
39
+ const providers = [{ provide: core_2.NAGE_UNIT_OF_WORK, useValue: options.unitOfWork }];
40
+ const exports = [core_2.NAGE_UNIT_OF_WORK];
41
+ if (options.health !== undefined) {
42
+ providers.push({ provide: tokens_js_1.NAGE_DATA_HEALTH, useValue: options.health });
43
+ exports.push(tokens_js_1.NAGE_DATA_HEALTH);
44
+ }
45
+ return { module: NageDataModule_1, providers, exports };
46
+ }
47
+ /**
48
+ * Register repositories for a feature module.
49
+ *
50
+ * ```ts
51
+ * @Module({ imports: [NageDataModule.forFeature([registerModel('Product', repository)])] })
52
+ * ```
53
+ */
54
+ static forFeature(registrations) {
55
+ const providers = registrations.map((registration) => ({
56
+ provide: (0, core_1.repositoryToken)(registration.model),
57
+ useValue: registration.repository,
58
+ }));
59
+ return {
60
+ module: NageDataModule_1,
61
+ providers,
62
+ exports: providers.map((provider) => provider.provide),
63
+ };
64
+ }
65
+ };
66
+ exports.NageDataModule = NageDataModule;
67
+ exports.NageDataModule = NageDataModule = NageDataModule_1 = __decorate([
68
+ (0, common_1.Global)(),
69
+ (0, common_1.Module)({})
70
+ ], NageDataModule);
71
+ //# sourceMappingURL=data.module.js.map
@@ -0,0 +1,38 @@
1
+ /**
2
+ * Validate client input against a model's query policy (PLAN.md §12, §16.2).
3
+ *
4
+ * This is the gate between an HTTP query string and a driver. Everything that
5
+ * crosses it has been checked against an allow-list; anything else is rejected
6
+ * with `INVALID_QUERY` rather than silently dropped, so a caller learns that a
7
+ * field is not filterable instead of quietly receiving unfiltered results.
8
+ */
9
+ import type { Query, QueryPolicy, Sort, Where } from '@nage-api/contracts';
10
+ /** Raw, untrusted query input — typically `request.query`. */
11
+ export type RawQuery = Record<string, unknown>;
12
+ /**
13
+ * Enforce the page-size ceiling.
14
+ *
15
+ * `limit: -1` — the legacy way to ask for everything — has no representation
16
+ * here: a non-positive limit is rejected, and anything above the model's
17
+ * ceiling is refused rather than silently clamped, so a client that asked for
18
+ * 100 000 rows finds out instead of believing it got them.
19
+ */
20
+ export declare function resolvePagination<TEntity>(query: {
21
+ readonly offset?: unknown;
22
+ readonly limit?: unknown;
23
+ }, policy: QueryPolicy<TEntity>): {
24
+ offset: number;
25
+ limit: number;
26
+ };
27
+ /** Validate a `where` clause against the filterable fields and operator allow-list. */
28
+ export declare function parseWhere<TEntity>(raw: unknown, policy: QueryPolicy<TEntity>, depth?: number): Where<TEntity>;
29
+ /** Validate sort instructions; accepts `-price` shorthand as well as tuples. */
30
+ export declare function parseSort<TEntity>(raw: unknown, policy: QueryPolicy<TEntity>): Sort<TEntity>;
31
+ /**
32
+ * Validate a raw query against a model's policy.
33
+ *
34
+ * @throws InvalidQueryError for anything outside the allow-lists
35
+ * @throws QueryLimitExceededError when the page size exceeds the ceiling
36
+ */
37
+ export declare function parseQuery<TEntity>(raw: RawQuery, policy: QueryPolicy<TEntity>): Query<TEntity>;
38
+ //# sourceMappingURL=parse-query.d.ts.map
@@ -0,0 +1,285 @@
1
+ "use strict";
2
+ /**
3
+ * Validate client input against a model's query policy (PLAN.md §12, §16.2).
4
+ *
5
+ * This is the gate between an HTTP query string and a driver. Everything that
6
+ * crosses it has been checked against an allow-list; anything else is rejected
7
+ * with `INVALID_QUERY` rather than silently dropped, so a caller learns that a
8
+ * field is not filterable instead of quietly receiving unfiltered results.
9
+ */
10
+ Object.defineProperty(exports, "__esModule", { value: true });
11
+ exports.resolvePagination = resolvePagination;
12
+ exports.parseWhere = parseWhere;
13
+ exports.parseSort = parseSort;
14
+ exports.parseQuery = parseQuery;
15
+ const core_1 = require("@nage-api/core");
16
+ const LOGICAL_KEYS = new Set(['and', 'or', 'not']);
17
+ const SORT_DIRECTIONS = new Set(['asc', 'desc']);
18
+ function reject(message, meta = {}) {
19
+ throw new core_1.InvalidQueryError({ message, meta });
20
+ }
21
+ function isPlainObject(value) {
22
+ return typeof value === 'object' && value !== null && !Array.isArray(value);
23
+ }
24
+ /**
25
+ * Refuse a clause whose prototype the client replaced.
26
+ *
27
+ * `{"__proto__": {...}}` sets the prototype instead of adding an own property in
28
+ * every parser that honours the key, so `Object.entries` reports an empty clause
29
+ * and the allow-list waves it through — the caller asked to filter and silently
30
+ * received the unfiltered collection, with the attacker's payload still riding
31
+ * on the object handed to the driver. Checking own keys against the allow-list
32
+ * cannot see this; only the prototype can. A null prototype is the hardened
33
+ * shape and stays welcome.
34
+ */
35
+ function rejectTamperedPrototype(value, label) {
36
+ const prototype = Object.getPrototypeOf(value);
37
+ if (prototype !== Object.prototype && prototype !== null) {
38
+ reject(`${label} must not carry a modified prototype`);
39
+ }
40
+ }
41
+ /**
42
+ * Enforce the page-size ceiling.
43
+ *
44
+ * `limit: -1` — the legacy way to ask for everything — has no representation
45
+ * here: a non-positive limit is rejected, and anything above the model's
46
+ * ceiling is refused rather than silently clamped, so a client that asked for
47
+ * 100 000 rows finds out instead of believing it got them.
48
+ */
49
+ function resolvePagination(query, policy) {
50
+ const offset = query.offset === undefined ? 0 : toInteger(query.offset, 'offset');
51
+ if (offset < 0)
52
+ reject('offset must not be negative', { offset });
53
+ if (query.limit === undefined)
54
+ return { offset, limit: policy.defaultLimit };
55
+ const limit = toInteger(query.limit, 'limit');
56
+ if (limit <= 0) {
57
+ reject('limit must be a positive number; use cursor pagination for large exports', { limit });
58
+ }
59
+ if (limit > policy.maxLimit) {
60
+ throw new core_1.QueryLimitExceededError(policy.maxLimit, { meta: { requested: limit } });
61
+ }
62
+ return { offset, limit };
63
+ }
64
+ /** The keys whose values the DSL expects to arrive already parsed into objects. */
65
+ const STRUCTURED_KEYS = ['where', 'sort', 'select', 'scope', 'populate', 'search'];
66
+ /**
67
+ * Refuse a query whose bracket syntax was never parsed.
68
+ *
69
+ * Express 5's default `query parser` is `simple`, under which
70
+ * `?where[status]=active` arrives as the single flat key `'where[status]'`
71
+ * instead of `{ where: { status: 'active' } }`. Every structured key then looks
72
+ * absent, and a request that asked to filter returns the **unfiltered**
73
+ * collection with a 200 — indistinguishable, to the client, from a filter that
74
+ * matched everything.
75
+ *
76
+ * Ignoring an unrecognised top-level key is right in general (a query string
77
+ * carries analytics parameters and the like), so the check is narrow: a key that
78
+ * begins with one of the DSL's own names followed by `[` can only mean a
79
+ * misconfigured parser, and saying so is far better than answering as if no
80
+ * filter had been requested. `@nage-api/core`'s `enableQueryDsl` is the fix.
81
+ */
82
+ function rejectUnparsedBrackets(raw) {
83
+ for (const key of Object.keys(raw)) {
84
+ const bracket = key.indexOf('[');
85
+ if (bracket <= 0)
86
+ continue;
87
+ if (STRUCTURED_KEYS.includes(key.slice(0, bracket))) {
88
+ reject(`Query key "${key}" arrived unparsed; the HTTP query parser is not configured for bracket syntax`, { key, hint: 'Call enableQueryDsl(app), which bootstrap() already does.' });
89
+ }
90
+ }
91
+ }
92
+ function toInteger(value, field) {
93
+ const parsed = typeof value === 'number' ? value : Number(value);
94
+ if (!Number.isInteger(parsed))
95
+ reject(`${field} must be an integer`, { [field]: value });
96
+ return parsed;
97
+ }
98
+ /** Validate a `where` clause against the filterable fields and operator allow-list. */
99
+ function parseWhere(raw, policy, depth = 0) {
100
+ if (!isPlainObject(raw))
101
+ reject('where must be an object');
102
+ rejectTamperedPrototype(raw, 'where');
103
+ if (depth > 5)
104
+ reject('where is nested too deeply', { maxDepth: 5 });
105
+ const filterable = new Set(policy.filterable);
106
+ const operators = new Set(policy.operators);
107
+ const result = {};
108
+ for (const [key, value] of Object.entries(raw)) {
109
+ if (LOGICAL_KEYS.has(key)) {
110
+ result[key] =
111
+ key === 'not'
112
+ ? parseWhere(value, policy, depth + 1)
113
+ : asArray(value, key).map((entry) => parseWhere(entry, policy, depth + 1));
114
+ continue;
115
+ }
116
+ if (!filterable.has(key)) {
117
+ // Naming the allowed fields turns a 400 into something a developer can
118
+ // act on without reading the source.
119
+ reject(`Field "${key}" is not filterable`, {
120
+ field: key,
121
+ filterable: [...filterable],
122
+ });
123
+ }
124
+ result[key] = isPlainObject(value) ? parseCondition(key, value, operators) : value;
125
+ }
126
+ return result;
127
+ }
128
+ function asArray(value, key) {
129
+ if (!Array.isArray(value))
130
+ reject(`"${key}" must be an array of conditions`);
131
+ return value;
132
+ }
133
+ function parseCondition(field, raw, operators) {
134
+ // An empty condition matches every row, so the same trick applied one level
135
+ // down turns a filter into a no-op rather than an error.
136
+ rejectTamperedPrototype(raw, `condition on "${field}"`);
137
+ const condition = {};
138
+ for (const [operator, operand] of Object.entries(raw)) {
139
+ if (!operators.has(operator)) {
140
+ reject(`Operator "${operator}" is not allowed on "${field}"`, {
141
+ field,
142
+ operator,
143
+ allowed: [...operators],
144
+ });
145
+ }
146
+ if ((operator === 'in' || operator === 'nin') && !Array.isArray(operand)) {
147
+ reject(`Operator "${operator}" expects an array`, { field, operator });
148
+ }
149
+ if (operator === 'between' && (!Array.isArray(operand) || operand.length !== 2)) {
150
+ reject('Operator "between" expects exactly two values', { field });
151
+ }
152
+ condition[operator] = operand;
153
+ }
154
+ return condition;
155
+ }
156
+ /** Validate sort instructions; accepts `-price` shorthand as well as tuples. */
157
+ function parseSort(raw, policy) {
158
+ const sortable = new Set(policy.sortable);
159
+ const entries = [];
160
+ const push = (field, direction) => {
161
+ if (!sortable.has(field)) {
162
+ reject(`Field "${field}" is not sortable`, { field, sortable: [...sortable] });
163
+ }
164
+ entries.push([field, direction]);
165
+ };
166
+ const parseToken = (token) => {
167
+ // `-created_at` is the shorthand every list UI already sends.
168
+ if (token.startsWith('-'))
169
+ push(token.slice(1), 'desc');
170
+ else
171
+ push(token, 'asc');
172
+ };
173
+ if (typeof raw === 'string') {
174
+ for (const token of raw.split(',')) {
175
+ const trimmed = token.trim();
176
+ if (trimmed.length > 0)
177
+ parseToken(trimmed);
178
+ }
179
+ }
180
+ else if (Array.isArray(raw)) {
181
+ for (const entry of raw) {
182
+ if (typeof entry === 'string') {
183
+ parseToken(entry.trim());
184
+ continue;
185
+ }
186
+ if (!Array.isArray(entry) || entry.length !== 2)
187
+ reject('sort entries must be [field, direction]');
188
+ const [field, direction] = entry;
189
+ if (typeof field !== 'string' || typeof direction !== 'string') {
190
+ reject('sort entries must be [field, direction]');
191
+ }
192
+ if (!SORT_DIRECTIONS.has(direction)) {
193
+ reject(`"${direction}" is not a sort direction`, { direction });
194
+ }
195
+ push(field, direction);
196
+ }
197
+ }
198
+ else {
199
+ reject('sort must be a string or an array');
200
+ }
201
+ return entries;
202
+ }
203
+ function parseStringList(raw, field) {
204
+ if (typeof raw === 'string') {
205
+ return raw
206
+ .split(',')
207
+ .map((entry) => entry.trim())
208
+ .filter((entry) => entry.length > 0);
209
+ }
210
+ if (Array.isArray(raw) && raw.every((entry) => typeof entry === 'string')) {
211
+ return raw;
212
+ }
213
+ return reject(`${field} must be a string or an array of strings`);
214
+ }
215
+ function parseAllowedList(raw, allowed, field, label) {
216
+ const requested = parseStringList(raw, field);
217
+ const permitted = new Set(allowed);
218
+ for (const entry of requested) {
219
+ if (!permitted.has(entry)) {
220
+ reject(`"${entry}" is not ${label}`, { field: entry, allowed: [...permitted] });
221
+ }
222
+ }
223
+ return requested;
224
+ }
225
+ /**
226
+ * Validate a raw query against a model's policy.
227
+ *
228
+ * @throws InvalidQueryError for anything outside the allow-lists
229
+ * @throws QueryLimitExceededError when the page size exceeds the ceiling
230
+ */
231
+ function parseQuery(raw, policy) {
232
+ rejectUnparsedBrackets(raw);
233
+ const { offset, limit } = resolvePagination(raw, policy);
234
+ const query = { offset, limit };
235
+ if (raw['where'] !== undefined)
236
+ query['where'] = parseWhere(raw['where'], policy);
237
+ if (raw['sort'] !== undefined)
238
+ query['sort'] = parseSort(raw['sort'], policy);
239
+ if (raw['select'] !== undefined) {
240
+ query['select'] = parseAllowedList(raw['select'], policy.selectable, 'select', 'selectable');
241
+ }
242
+ if (raw['scope'] !== undefined) {
243
+ query['scope'] = parseAllowedList(raw['scope'], policy.scopes, 'scope', 'a known scope');
244
+ }
245
+ if (raw['populate'] !== undefined) {
246
+ const populate = parseAllowedList(raw['populate'], policy.populatable, 'populate', 'populatable');
247
+ for (const relation of populate) {
248
+ const depth = relation.split('.').length;
249
+ if (depth > policy.maxPopulateDepth) {
250
+ reject(`populate "${relation}" is nested deeper than ${policy.maxPopulateDepth}`, {
251
+ relation,
252
+ maxPopulateDepth: policy.maxPopulateDepth,
253
+ });
254
+ }
255
+ }
256
+ query['populate'] = populate;
257
+ }
258
+ if (raw['search'] !== undefined)
259
+ query['search'] = parseSearch(raw['search'], policy);
260
+ if (raw['withDeleted'] !== undefined) {
261
+ query['withDeleted'] = raw['withDeleted'] === true || raw['withDeleted'] === 'true';
262
+ }
263
+ if (raw['cursor'] !== undefined) {
264
+ if (typeof raw['cursor'] !== 'string')
265
+ reject('cursor must be a string');
266
+ query['cursor'] = raw['cursor'];
267
+ }
268
+ return query;
269
+ }
270
+ function parseSearch(raw, policy) {
271
+ if (policy.searchable.length === 0) {
272
+ reject('This model does not support search', { searchable: [] });
273
+ }
274
+ if (typeof raw === 'string')
275
+ return { term: raw };
276
+ if (!isPlainObject(raw) || typeof raw['term'] !== 'string') {
277
+ reject('search must be a string or { term, fields }');
278
+ }
279
+ const term = raw['term'];
280
+ if (raw['fields'] === undefined)
281
+ return { term };
282
+ const fields = parseAllowedList(raw['fields'], policy.searchable, 'search.fields', 'searchable');
283
+ return { term, fields };
284
+ }
285
+ //# sourceMappingURL=parse-query.js.map
@@ -0,0 +1,52 @@
1
+ /**
2
+ * Per-model query policy (PLAN.md §12, §14.2).
3
+ *
4
+ * The legacy DSL forwarded client-supplied `$`-operators straight to the ORM and
5
+ * honoured `limit: -1`. The DSL survives — it is the framework's most productive
6
+ * idea — but a model now declares exactly which fields and operators a client
7
+ * may reach, and every query is validated against that declaration before it
8
+ * reaches a driver.
9
+ *
10
+ * Nothing is filterable by default. A policy is a statement of what is exposed,
11
+ * not a list of exceptions.
12
+ */
13
+ import type { ComparisonOperator, FieldName, QueryPolicy } from '@nage-api/contracts';
14
+ /** Operators safe to expose on any model. */
15
+ export declare const DEFAULT_OPERATORS: readonly ComparisonOperator[];
16
+ export declare const DEFAULT_MAX_LIMIT = 100;
17
+ export declare const DEFAULT_LIMIT = 25;
18
+ export declare const DEFAULT_MAX_POPULATE_DEPTH = 2;
19
+ /** What a model has to state; everything else has a safe default. */
20
+ export interface QueryPolicyInput<TEntity> {
21
+ readonly filterable?: readonly FieldName<TEntity>[];
22
+ readonly sortable?: readonly FieldName<TEntity>[];
23
+ readonly selectable?: readonly FieldName<TEntity>[];
24
+ readonly searchable?: readonly FieldName<TEntity>[];
25
+ readonly populatable?: readonly string[];
26
+ readonly scopes?: readonly string[];
27
+ readonly operators?: readonly ComparisonOperator[];
28
+ readonly maxLimit?: number;
29
+ readonly defaultLimit?: number;
30
+ readonly maxPopulateDepth?: number;
31
+ }
32
+ /**
33
+ * Build a policy.
34
+ *
35
+ * ```ts
36
+ * const productPolicy = defineQueryPolicy<Product>({
37
+ * filterable: ['name', 'price', 'archived'],
38
+ * sortable: ['name', 'price', 'created_at'],
39
+ * searchable: ['name'],
40
+ * maxLimit: 200,
41
+ * });
42
+ * ```
43
+ */
44
+ export declare function defineQueryPolicy<TEntity>(input?: QueryPolicyInput<TEntity>): QueryPolicy<TEntity>;
45
+ /**
46
+ * Policy for internal callers: everything the entity has, nothing hidden.
47
+ *
48
+ * For server-side code that composes its own queries — never for a policy
49
+ * applied to client input.
50
+ */
51
+ export declare function trustedQueryPolicy<TEntity>(fields: readonly FieldName<TEntity>[], maxLimit?: number): QueryPolicy<TEntity>;
52
+ //# sourceMappingURL=policy.d.ts.map
@@ -0,0 +1,80 @@
1
+ "use strict";
2
+ /**
3
+ * Per-model query policy (PLAN.md §12, §14.2).
4
+ *
5
+ * The legacy DSL forwarded client-supplied `$`-operators straight to the ORM and
6
+ * honoured `limit: -1`. The DSL survives — it is the framework's most productive
7
+ * idea — but a model now declares exactly which fields and operators a client
8
+ * may reach, and every query is validated against that declaration before it
9
+ * reaches a driver.
10
+ *
11
+ * Nothing is filterable by default. A policy is a statement of what is exposed,
12
+ * not a list of exceptions.
13
+ */
14
+ Object.defineProperty(exports, "__esModule", { value: true });
15
+ exports.DEFAULT_MAX_POPULATE_DEPTH = exports.DEFAULT_LIMIT = exports.DEFAULT_MAX_LIMIT = exports.DEFAULT_OPERATORS = void 0;
16
+ exports.defineQueryPolicy = defineQueryPolicy;
17
+ exports.trustedQueryPolicy = trustedQueryPolicy;
18
+ /** Operators safe to expose on any model. */
19
+ exports.DEFAULT_OPERATORS = [
20
+ 'eq',
21
+ 'ne',
22
+ 'gt',
23
+ 'gte',
24
+ 'lt',
25
+ 'lte',
26
+ 'in',
27
+ 'nin',
28
+ 'like',
29
+ 'ilike',
30
+ 'between',
31
+ 'isNull',
32
+ ];
33
+ exports.DEFAULT_MAX_LIMIT = 100;
34
+ exports.DEFAULT_LIMIT = 25;
35
+ exports.DEFAULT_MAX_POPULATE_DEPTH = 2;
36
+ /**
37
+ * Build a policy.
38
+ *
39
+ * ```ts
40
+ * const productPolicy = defineQueryPolicy<Product>({
41
+ * filterable: ['name', 'price', 'archived'],
42
+ * sortable: ['name', 'price', 'created_at'],
43
+ * searchable: ['name'],
44
+ * maxLimit: 200,
45
+ * });
46
+ * ```
47
+ */
48
+ function defineQueryPolicy(input = {}) {
49
+ const maxLimit = input.maxLimit ?? exports.DEFAULT_MAX_LIMIT;
50
+ return {
51
+ filterable: input.filterable ?? [],
52
+ sortable: input.sortable ?? [],
53
+ selectable: input.selectable ?? [],
54
+ searchable: input.searchable ?? [],
55
+ populatable: input.populatable ?? [],
56
+ scopes: input.scopes ?? [],
57
+ operators: input.operators ?? exports.DEFAULT_OPERATORS,
58
+ maxLimit,
59
+ // A default page larger than the ceiling would be incoherent.
60
+ defaultLimit: Math.min(input.defaultLimit ?? exports.DEFAULT_LIMIT, maxLimit),
61
+ maxPopulateDepth: input.maxPopulateDepth ?? exports.DEFAULT_MAX_POPULATE_DEPTH,
62
+ };
63
+ }
64
+ /**
65
+ * Policy for internal callers: everything the entity has, nothing hidden.
66
+ *
67
+ * For server-side code that composes its own queries — never for a policy
68
+ * applied to client input.
69
+ */
70
+ function trustedQueryPolicy(fields, maxLimit = 1000) {
71
+ return defineQueryPolicy({
72
+ filterable: fields,
73
+ sortable: fields,
74
+ selectable: fields,
75
+ searchable: fields,
76
+ maxLimit,
77
+ defaultLimit: Math.min(exports.DEFAULT_LIMIT, maxLimit),
78
+ });
79
+ }
80
+ //# sourceMappingURL=policy.js.map
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Evaluate a `Where` clause against a plain object.
3
+ *
4
+ * Used by the in-memory driver, and by tests that need to reason about the DSL
5
+ * without a database. SQL and Mongo drivers translate the same clause into
6
+ * their own dialect instead — the semantics here are the reference the shared
7
+ * conformance suite pins down.
8
+ */
9
+ import type { UnknownRecord, Where } from '@nage-api/contracts';
10
+ /** Does `record` satisfy `where`? An empty clause matches everything. */
11
+ export declare function matchesWhere<TEntity extends UnknownRecord>(record: TEntity, where: Where<TEntity> | undefined): boolean;
12
+ //# sourceMappingURL=where-matcher.d.ts.map