ng-qubee 3.2.0 → 3.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +116 -1
- package/fesm2022/ng-qubee.mjs +2060 -1717
- package/fesm2022/ng-qubee.mjs.map +1 -1
- package/package.json +4 -2
- package/types/ng-qubee.d.ts +614 -260
package/fesm2022/ng-qubee.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
2
|
import { signal, computed, Injectable, InjectionToken, Inject, makeEnvironmentProviders, NgModule } from '@angular/core';
|
|
3
|
-
import { BehaviorSubject, filter, throwError } from 'rxjs';
|
|
4
3
|
import * as qs from 'qs';
|
|
4
|
+
import { BehaviorSubject, filter, throwError } from 'rxjs';
|
|
5
5
|
|
|
6
6
|
class KeyNotFoundError extends Error {
|
|
7
7
|
constructor(key) {
|
|
@@ -77,40 +77,10 @@ var DriverEnum;
|
|
|
77
77
|
DriverEnum["JSON_API"] = "json-api";
|
|
78
78
|
DriverEnum["LARAVEL"] = "laravel";
|
|
79
79
|
DriverEnum["NESTJS"] = "nestjs";
|
|
80
|
+
DriverEnum["POSTGREST"] = "postgrest";
|
|
80
81
|
DriverEnum["SPATIE"] = "spatie";
|
|
81
82
|
})(DriverEnum || (DriverEnum = {}));
|
|
82
83
|
|
|
83
|
-
/**
|
|
84
|
-
* Resolved query parameter key names with defaults applied
|
|
85
|
-
*
|
|
86
|
-
* Maps logical query concepts to the actual query parameter names
|
|
87
|
-
* used in the generated URI. Unset values fall back to defaults.
|
|
88
|
-
*/
|
|
89
|
-
class QueryBuilderOptions {
|
|
90
|
-
appends;
|
|
91
|
-
fields;
|
|
92
|
-
filters;
|
|
93
|
-
includes;
|
|
94
|
-
limit;
|
|
95
|
-
page;
|
|
96
|
-
search;
|
|
97
|
-
select;
|
|
98
|
-
sort;
|
|
99
|
-
sortBy;
|
|
100
|
-
constructor(options) {
|
|
101
|
-
this.appends = options.appends || 'append';
|
|
102
|
-
this.fields = options.fields || 'fields';
|
|
103
|
-
this.filters = options.filters || 'filter';
|
|
104
|
-
this.includes = options.includes || 'include';
|
|
105
|
-
this.limit = options.limit || 'limit';
|
|
106
|
-
this.page = options.page || 'page';
|
|
107
|
-
this.search = options.search || 'search';
|
|
108
|
-
this.select = options.select || 'select';
|
|
109
|
-
this.sort = options.sort || 'sort';
|
|
110
|
-
this.sortBy = options.sortBy || 'sortBy';
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
84
|
/**
|
|
115
85
|
* Resolved response field key names with defaults applied
|
|
116
86
|
*
|
|
@@ -202,2155 +172,2550 @@ class NestjsResponseOptions extends ResponseOptions {
|
|
|
202
172
|
}
|
|
203
173
|
}
|
|
204
174
|
|
|
175
|
+
var SortEnum;
|
|
176
|
+
(function (SortEnum) {
|
|
177
|
+
SortEnum["ASC"] = "asc";
|
|
178
|
+
SortEnum["DESC"] = "desc";
|
|
179
|
+
})(SortEnum || (SortEnum = {}));
|
|
180
|
+
|
|
181
|
+
class UnselectableModelError extends Error {
|
|
182
|
+
constructor(model) {
|
|
183
|
+
super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
205
187
|
/**
|
|
206
|
-
*
|
|
188
|
+
* Thrown when a limit value does not satisfy the active driver's constraints
|
|
207
189
|
*
|
|
208
|
-
*
|
|
190
|
+
* Validation is driver-scoped: most drivers require an integer `>= 1`, while
|
|
191
|
+
* the NestJS driver additionally accepts `-1` as a "fetch all items" sentinel
|
|
192
|
+
* (as documented by nestjs-paginate). The message is tailored accordingly so
|
|
193
|
+
* the caller understands which values are permitted.
|
|
209
194
|
*/
|
|
210
|
-
class
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
195
|
+
class InvalidLimitError extends Error {
|
|
196
|
+
/**
|
|
197
|
+
* @param limit - The rejected limit value
|
|
198
|
+
* @param allowFetchAll - Whether the active driver accepts `-1` (fetch all)
|
|
199
|
+
*/
|
|
200
|
+
constructor(limit, allowFetchAll = false) {
|
|
201
|
+
const allowed = allowFetchAll
|
|
202
|
+
? 'a positive integer greater than 0, or -1 to fetch all items'
|
|
203
|
+
: 'a positive integer greater than 0';
|
|
204
|
+
super(`Invalid limit value: Limit must be ${allowed}. Received: ${limit}`);
|
|
205
|
+
this.name = 'InvalidLimitError';
|
|
214
206
|
}
|
|
215
207
|
}
|
|
216
208
|
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
209
|
+
/**
|
|
210
|
+
* Base class for request strategies
|
|
211
|
+
*
|
|
212
|
+
* Concentrates the glue every concrete strategy used to copy: the
|
|
213
|
+
* resource-required guard, the `?`/`&` URL composition, and the default
|
|
214
|
+
* positive-integer `validateLimit`. Concrete strategies override only
|
|
215
|
+
* the parts that differ — the per-driver wire format goes into a single
|
|
216
|
+
* `protected parts(state, options): string[]` method that returns the
|
|
217
|
+
* ordered query-string segments the base then joins.
|
|
218
|
+
*
|
|
219
|
+
* Drivers that need a non-default `validateLimit` (e.g. NestJS, which
|
|
220
|
+
* accepts `-1` as a fetch-all sentinel) override that method directly.
|
|
221
|
+
*/
|
|
222
|
+
class AbstractRequestStrategy {
|
|
223
|
+
/**
|
|
224
|
+
* Compose the full request URI from the given state
|
|
225
|
+
*
|
|
226
|
+
* Template method: validates the resource, computes the base path,
|
|
227
|
+
* delegates the per-driver query-string segments to `parts(...)`, and
|
|
228
|
+
* joins them with the conventional `?`/`&` separators.
|
|
229
|
+
*
|
|
230
|
+
* @param state - The current query builder state
|
|
231
|
+
* @param options - The query parameter key name configuration
|
|
232
|
+
* @returns The composed URI string
|
|
233
|
+
* @throws Error if the resource is not set
|
|
234
|
+
*/
|
|
235
|
+
buildUri(state, options) {
|
|
236
|
+
this.assertResource(state);
|
|
237
|
+
const segments = this.parts(state, options);
|
|
238
|
+
return this.join(this.baseUri(state), segments);
|
|
221
239
|
}
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
const INITIAL_STATE = {
|
|
225
|
-
baseUrl: '',
|
|
226
|
-
fields: {},
|
|
227
|
-
filters: {},
|
|
228
|
-
includes: [],
|
|
229
|
-
isLastPageKnown: false,
|
|
230
|
-
lastPage: 1,
|
|
231
|
-
limit: 15,
|
|
232
|
-
operatorFilters: [],
|
|
233
|
-
page: 1,
|
|
234
|
-
resource: '',
|
|
235
|
-
search: '',
|
|
236
|
-
select: [],
|
|
237
|
-
sorts: []
|
|
238
|
-
};
|
|
239
|
-
class NestService {
|
|
240
240
|
/**
|
|
241
|
-
*
|
|
241
|
+
* Validate that a limit value is acceptable for this driver
|
|
242
242
|
*
|
|
243
|
-
*
|
|
243
|
+
* Default policy: positive integer. Drivers that recognise a sentinel
|
|
244
|
+
* (NestJS treats `-1` as "fetch all") override this method.
|
|
245
|
+
*
|
|
246
|
+
* @param limit - The limit value to validate
|
|
247
|
+
* @throws {InvalidLimitError} If the value is not a positive integer
|
|
244
248
|
*/
|
|
245
|
-
|
|
249
|
+
validateLimit(limit) {
|
|
250
|
+
if (Number.isInteger(limit) && limit >= 1) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
throw new InvalidLimitError(limit);
|
|
254
|
+
}
|
|
246
255
|
/**
|
|
247
|
-
*
|
|
256
|
+
* Throw if the resource is not set on the state
|
|
248
257
|
*
|
|
249
|
-
*
|
|
258
|
+
* Centralises the message that was previously copy-pasted across four
|
|
259
|
+
* of the five concrete strategies.
|
|
260
|
+
*
|
|
261
|
+
* @param state - The current query builder state
|
|
262
|
+
* @throws Error if `state.resource` is empty
|
|
250
263
|
*/
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
264
|
+
assertResource(state) {
|
|
265
|
+
if (!state.resource) {
|
|
266
|
+
throw new Error('Set the resource property BEFORE adding filters or calling the url() / get() methods');
|
|
267
|
+
}
|
|
254
268
|
}
|
|
255
269
|
/**
|
|
256
|
-
*
|
|
270
|
+
* Compute the base path (no query string)
|
|
257
271
|
*
|
|
258
|
-
* @param
|
|
259
|
-
* @example
|
|
260
|
-
* service.baseUrl = 'https://api.example.com';
|
|
272
|
+
* @param state - The current query builder state
|
|
273
|
+
* @returns The base URI without the query separator (e.g. `/users` or `https://api.example.com/users`)
|
|
261
274
|
*/
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
...nest,
|
|
265
|
-
baseUrl
|
|
266
|
-
}));
|
|
275
|
+
baseUri(state) {
|
|
276
|
+
return state.baseUrl ? `${state.baseUrl}/${state.resource}` : `/${state.resource}`;
|
|
267
277
|
}
|
|
268
278
|
/**
|
|
269
|
-
*
|
|
279
|
+
* Glue the base URI and the per-driver query-string segments
|
|
270
280
|
*
|
|
271
|
-
*
|
|
272
|
-
*
|
|
273
|
-
* by `NgQubeeService.setLimit()`, because the accepted range depends on
|
|
274
|
-
* the driver (e.g. nestjs-paginate accepts `-1` for "fetch all").
|
|
281
|
+
* Returns the bare base when no segments were emitted (e.g. PostgREST
|
|
282
|
+
* in RANGE mode with no filters), otherwise joins with `?` + `&`.
|
|
275
283
|
*
|
|
276
|
-
* @param
|
|
277
|
-
* @
|
|
278
|
-
*
|
|
284
|
+
* @param base - The base URI from `_baseUri`
|
|
285
|
+
* @param segments - The query-string fragments from `parts(...)`
|
|
286
|
+
* @returns The full URI
|
|
279
287
|
*/
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
...nest,
|
|
283
|
-
limit
|
|
284
|
-
}));
|
|
288
|
+
join(base, segments) {
|
|
289
|
+
return segments.length ? `${base}?${segments.join('&')}` : base;
|
|
285
290
|
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Request strategy for the JSON:API driver
|
|
295
|
+
*
|
|
296
|
+
* Generates URIs in the JSON:API format:
|
|
297
|
+
* - Fields: `fields[articles]=title,body&fields[people]=name`
|
|
298
|
+
* - Filters: `filter[status]=active`
|
|
299
|
+
* - Includes: `include=author,comments.author`
|
|
300
|
+
* - Pagination: `page[number]=1&page[size]=15`
|
|
301
|
+
* - Sort: `sort=-created_at,name` (- prefix = DESC)
|
|
302
|
+
*
|
|
303
|
+
* @see https://jsonapi.org/format/
|
|
304
|
+
*/
|
|
305
|
+
class JsonApiRequestStrategy extends AbstractRequestStrategy {
|
|
286
306
|
/**
|
|
287
|
-
*
|
|
288
|
-
*
|
|
307
|
+
* Filters, sorts, includes, per-model fields — same shape as Spatie
|
|
308
|
+
* but with bracket-style pagination
|
|
309
|
+
*/
|
|
310
|
+
capabilities = {
|
|
311
|
+
fields: true,
|
|
312
|
+
filters: true,
|
|
313
|
+
includes: true,
|
|
314
|
+
operatorFilters: false,
|
|
315
|
+
search: false,
|
|
316
|
+
select: false,
|
|
317
|
+
sort: true
|
|
318
|
+
};
|
|
319
|
+
/**
|
|
320
|
+
* Emit JSON:API-format query-string segments in canonical order:
|
|
321
|
+
* include → fields → filters → pagination → sort
|
|
289
322
|
*
|
|
290
|
-
* @param
|
|
291
|
-
* @
|
|
292
|
-
* @
|
|
293
|
-
* service.page = 2;
|
|
323
|
+
* @param state - The current query builder state
|
|
324
|
+
* @param options - The query parameter key name configuration
|
|
325
|
+
* @returns Ordered query-string fragments
|
|
294
326
|
*/
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
this.
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
327
|
+
parts(state, options) {
|
|
328
|
+
const out = [];
|
|
329
|
+
this._appendIncludes(state, options, out);
|
|
330
|
+
this._appendFields(state, options, out);
|
|
331
|
+
this._appendFilters(state, options, out);
|
|
332
|
+
this._appendPagination(state, options, out);
|
|
333
|
+
this._appendSort(state, options, out);
|
|
334
|
+
return out;
|
|
301
335
|
}
|
|
302
336
|
/**
|
|
303
|
-
*
|
|
304
|
-
* Must be a non-empty string
|
|
337
|
+
* Append per-type field selection in bracket notation
|
|
305
338
|
*
|
|
306
|
-
* @param
|
|
307
|
-
* @
|
|
308
|
-
* @
|
|
309
|
-
*
|
|
339
|
+
* @param state - The current query builder state
|
|
340
|
+
* @param options - The query parameter key name configuration
|
|
341
|
+
* @param out - The accumulator the caller joins into the URI
|
|
342
|
+
* @throws Error if the resource is missing from the fields object
|
|
343
|
+
* @throws UnselectableModelError if a field type is not the resource or in includes
|
|
310
344
|
*/
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
345
|
+
_appendFields(state, options, out) {
|
|
346
|
+
if (!Object.keys(state.fields).length) {
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (!(state.resource in state.fields)) {
|
|
350
|
+
throw new Error(`Key ${state.resource} is missing in the fields object`);
|
|
351
|
+
}
|
|
352
|
+
const grouped = {};
|
|
353
|
+
for (const type in state.fields) {
|
|
354
|
+
if (!state.fields.hasOwnProperty(type)) {
|
|
355
|
+
continue;
|
|
356
|
+
}
|
|
357
|
+
if (type !== state.resource && !state.includes.includes(type)) {
|
|
358
|
+
throw new UnselectableModelError(type);
|
|
359
|
+
}
|
|
360
|
+
grouped[`${options.fields}[${type}]`] = state.fields[type].join(',');
|
|
361
|
+
}
|
|
362
|
+
out.push(qs.stringify(grouped, { encode: false }));
|
|
320
363
|
}
|
|
321
364
|
/**
|
|
322
|
-
*
|
|
365
|
+
* Append filter parameters in bracket notation: `filter[key]=value`
|
|
323
366
|
*
|
|
324
|
-
* @param
|
|
325
|
-
* @
|
|
326
|
-
* @
|
|
367
|
+
* @param state - The current query builder state
|
|
368
|
+
* @param options - The query parameter key name configuration
|
|
369
|
+
* @param out - The accumulator the caller joins into the URI
|
|
327
370
|
*/
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
371
|
+
_appendFilters(state, options, out) {
|
|
372
|
+
const keys = Object.keys(state.filters);
|
|
373
|
+
if (!keys.length) {
|
|
374
|
+
return;
|
|
331
375
|
}
|
|
376
|
+
const wrapper = {
|
|
377
|
+
[options.filters]: keys.reduce((acc, key) => {
|
|
378
|
+
return Object.assign(acc, { [key]: state.filters[key].join(',') });
|
|
379
|
+
}, {})
|
|
380
|
+
};
|
|
381
|
+
out.push(qs.stringify(wrapper, { encode: false }));
|
|
332
382
|
}
|
|
333
383
|
/**
|
|
334
|
-
*
|
|
384
|
+
* Append include parameter as `include=author,comments.author`
|
|
335
385
|
*
|
|
336
|
-
* @param
|
|
337
|
-
* @
|
|
338
|
-
* @
|
|
386
|
+
* @param state - The current query builder state
|
|
387
|
+
* @param options - The query parameter key name configuration
|
|
388
|
+
* @param out - The accumulator the caller joins into the URI
|
|
339
389
|
*/
|
|
340
|
-
|
|
341
|
-
if (!
|
|
342
|
-
|
|
390
|
+
_appendIncludes(state, options, out) {
|
|
391
|
+
if (!state.includes.length) {
|
|
392
|
+
return;
|
|
343
393
|
}
|
|
394
|
+
out.push(`${options.includes}=${state.includes}`);
|
|
344
395
|
}
|
|
345
396
|
/**
|
|
346
|
-
*
|
|
347
|
-
* Automatically prevents duplicate fields for each model
|
|
397
|
+
* Append JSON:API bracket pagination as `page[number]=1&page[size]=15`
|
|
348
398
|
*
|
|
349
|
-
*
|
|
350
|
-
*
|
|
351
|
-
*
|
|
352
|
-
*
|
|
353
|
-
*
|
|
399
|
+
* `qs.stringify` already returns the two segments joined with `&`, so we
|
|
400
|
+
* push the whole string as one accumulator entry — `_join` will glue
|
|
401
|
+
* it onto the rest with the same separator.
|
|
402
|
+
*
|
|
403
|
+
* @param state - The current query builder state
|
|
404
|
+
* @param options - The query parameter key name configuration
|
|
405
|
+
* @param out - The accumulator the caller joins into the URI
|
|
354
406
|
*/
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
Object.keys(fields).forEach(model => {
|
|
359
|
-
const existingFields = mergedFields[model] || [];
|
|
360
|
-
const newFields = fields[model];
|
|
361
|
-
// Use Set to prevent duplicates
|
|
362
|
-
const uniqueFields = Array.from(new Set([...existingFields, ...newFields]));
|
|
363
|
-
mergedFields[model] = uniqueFields;
|
|
364
|
-
});
|
|
365
|
-
return {
|
|
366
|
-
...nest,
|
|
367
|
-
fields: mergedFields
|
|
368
|
-
};
|
|
369
|
-
});
|
|
407
|
+
_appendPagination(state, options, out) {
|
|
408
|
+
const pagination = qs.stringify({ [options.page]: { number: state.page, size: state.limit } }, { encode: false });
|
|
409
|
+
out.push(pagination);
|
|
370
410
|
}
|
|
371
411
|
/**
|
|
372
|
-
*
|
|
373
|
-
* Automatically prevents duplicate filter values for each filter key
|
|
412
|
+
* Append sort parameter as `sort=-field1,field2` (`-` prefix = DESC)
|
|
374
413
|
*
|
|
375
|
-
* @param
|
|
376
|
-
* @
|
|
377
|
-
* @
|
|
378
|
-
* service.addFilters({ id: [1, 2, 3] });
|
|
379
|
-
* service.addFilters({ status: ['active', 'pending'] });
|
|
414
|
+
* @param state - The current query builder state
|
|
415
|
+
* @param options - The query parameter key name configuration
|
|
416
|
+
* @param out - The accumulator the caller joins into the URI
|
|
380
417
|
*/
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
// Use Set to prevent duplicates
|
|
388
|
-
const uniqueValues = Array.from(new Set([...existingValues, ...newValues]));
|
|
389
|
-
mergedFilters[key] = uniqueValues;
|
|
390
|
-
});
|
|
391
|
-
return {
|
|
392
|
-
...nest,
|
|
393
|
-
filters: mergedFilters
|
|
394
|
-
};
|
|
395
|
-
});
|
|
418
|
+
_appendSort(state, options, out) {
|
|
419
|
+
if (!state.sorts.length) {
|
|
420
|
+
return;
|
|
421
|
+
}
|
|
422
|
+
const pairs = state.sorts.map(sort => `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`);
|
|
423
|
+
out.push(`${options.sort}=${pairs.join(',')}`);
|
|
396
424
|
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
/**
|
|
428
|
+
* Base class for response strategies whose pagination metadata lives at
|
|
429
|
+
* dot-notation paths inside the response body
|
|
430
|
+
*
|
|
431
|
+
* JSON:API and NestJS share an identical body-traversal algorithm: the
|
|
432
|
+
* total / current-page / etc. live at nested keys like `meta.total`, and
|
|
433
|
+
* `from`/`to` are either present directly or must be derived from
|
|
434
|
+
* `currentPage` × `perPage`. Both strategies were duplicating this
|
|
435
|
+
* verbatim before this base existed; concrete classes now extend and
|
|
436
|
+
* provide only the docstring describing their driver's specific path
|
|
437
|
+
* conventions (see `JsonApiResponseStrategy`, `NestjsResponseStrategy`).
|
|
438
|
+
*
|
|
439
|
+
* Drivers whose pagination metadata travels via HTTP headers (PostgREST)
|
|
440
|
+
* or whose body has a flat shape with no dot paths (Laravel, Spatie) do
|
|
441
|
+
* not extend this class — they implement `IResponseStrategy` directly.
|
|
442
|
+
*/
|
|
443
|
+
class AbstractDotPathResponseStrategy {
|
|
397
444
|
/**
|
|
398
|
-
*
|
|
399
|
-
* Automatically prevents duplicate includes
|
|
445
|
+
* Parse a nested-envelope pagination response into a PaginatedCollection
|
|
400
446
|
*
|
|
401
|
-
* @param
|
|
402
|
-
* @
|
|
403
|
-
* @
|
|
404
|
-
* service.addIncludes(['profile', 'posts']);
|
|
405
|
-
* service.addIncludes(['comments']);
|
|
447
|
+
* @param response - The raw API response object
|
|
448
|
+
* @param options - The response key name configuration (dot-notation paths supported)
|
|
449
|
+
* @returns A typed PaginatedCollection instance
|
|
406
450
|
*/
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
451
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
452
|
+
paginate(response, options) {
|
|
453
|
+
const data = this.resolve(response, options.data);
|
|
454
|
+
const currentPage = this.resolve(response, options.currentPage);
|
|
455
|
+
const total = this.resolve(response, options.total);
|
|
456
|
+
const perPage = this.resolve(response, options.perPage);
|
|
457
|
+
const lastPage = this.resolve(response, options.lastPage);
|
|
458
|
+
// Compute from/to if not directly available
|
|
459
|
+
const from = this.resolveFrom(response, options, currentPage, perPage);
|
|
460
|
+
const to = this.resolveTo(response, options, currentPage, perPage, total);
|
|
461
|
+
const prevPageUrl = this.resolve(response, options.prevPageUrl);
|
|
462
|
+
const nextPageUrl = this.resolve(response, options.nextPageUrl);
|
|
463
|
+
const firstPageUrl = this.resolve(response, options.firstPageUrl);
|
|
464
|
+
const lastPageUrl = this.resolve(response, options.lastPageUrl);
|
|
465
|
+
return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl, nextPageUrl, lastPage, firstPageUrl, lastPageUrl);
|
|
416
466
|
}
|
|
417
467
|
/**
|
|
418
|
-
*
|
|
419
|
-
* Automatically prevents duplicate operator filters for the same field + operator combination
|
|
468
|
+
* Resolve a value from a response object using a dot-notation path
|
|
420
469
|
*
|
|
421
|
-
*
|
|
422
|
-
*
|
|
423
|
-
* @
|
|
424
|
-
*
|
|
425
|
-
*
|
|
470
|
+
* Supports both flat keys (`'data'`) and nested paths (`'meta.totalItems'`).
|
|
471
|
+
*
|
|
472
|
+
* @param response - The raw response object
|
|
473
|
+
* @param path - The dot-notation path to resolve
|
|
474
|
+
* @returns The resolved value, or undefined if any segment is missing
|
|
426
475
|
*/
|
|
427
|
-
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
filters.forEach(newFilter => {
|
|
431
|
-
const existingIdx = merged.findIndex(f => f.field === newFilter.field && f.operator === newFilter.operator);
|
|
432
|
-
if (existingIdx > -1) {
|
|
433
|
-
const existingValues = merged[existingIdx].values;
|
|
434
|
-
merged[existingIdx] = {
|
|
435
|
-
...merged[existingIdx],
|
|
436
|
-
values: Array.from(new Set([...existingValues, ...newFilter.values]))
|
|
437
|
-
};
|
|
438
|
-
}
|
|
439
|
-
else {
|
|
440
|
-
merged.push({ ...newFilter });
|
|
441
|
-
}
|
|
442
|
-
});
|
|
443
|
-
return {
|
|
444
|
-
...nest,
|
|
445
|
-
operatorFilters: merged
|
|
446
|
-
};
|
|
447
|
-
});
|
|
476
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
477
|
+
resolve(response, path) {
|
|
478
|
+
return path.split('.').reduce((obj, key) => obj?.[key], response);
|
|
448
479
|
}
|
|
449
480
|
/**
|
|
450
|
-
*
|
|
451
|
-
* Automatically prevents duplicate select fields
|
|
481
|
+
* Resolve the "from" index value
|
|
452
482
|
*
|
|
453
|
-
*
|
|
454
|
-
*
|
|
455
|
-
*
|
|
456
|
-
*
|
|
483
|
+
* If `options.from` resolves to a value in the response, use it.
|
|
484
|
+
* Otherwise compute `(currentPage - 1) * perPage + 1` when both are known.
|
|
485
|
+
*
|
|
486
|
+
* @param response - The raw response object
|
|
487
|
+
* @param options - The response key name configuration
|
|
488
|
+
* @param currentPage - The current page number
|
|
489
|
+
* @param perPage - The number of items per page
|
|
490
|
+
* @returns The "from" index, or `undefined` when neither path nor inputs suffice
|
|
457
491
|
*/
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
492
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
493
|
+
resolveFrom(response, options, currentPage, perPage) {
|
|
494
|
+
const direct = this.resolve(response, options.from);
|
|
495
|
+
if (direct !== undefined) {
|
|
496
|
+
return direct;
|
|
497
|
+
}
|
|
498
|
+
if (currentPage && perPage) {
|
|
499
|
+
return (currentPage - 1) * perPage + 1;
|
|
500
|
+
}
|
|
501
|
+
return undefined;
|
|
466
502
|
}
|
|
467
503
|
/**
|
|
468
|
-
*
|
|
504
|
+
* Resolve the "to" index value
|
|
469
505
|
*
|
|
470
|
-
*
|
|
471
|
-
*
|
|
472
|
-
*
|
|
473
|
-
*
|
|
474
|
-
*
|
|
475
|
-
*
|
|
506
|
+
* If `options.to` resolves to a value in the response, use it.
|
|
507
|
+
* Otherwise compute `Math.min(currentPage * perPage, total)` when all
|
|
508
|
+
* three are known.
|
|
509
|
+
*
|
|
510
|
+
* @param response - The raw response object
|
|
511
|
+
* @param options - The response key name configuration
|
|
512
|
+
* @param currentPage - The current page number
|
|
513
|
+
* @param perPage - The number of items per page
|
|
514
|
+
* @param total - The total number of items
|
|
515
|
+
* @returns The "to" index, or `undefined` when neither path nor inputs suffice
|
|
476
516
|
*/
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
517
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
518
|
+
resolveTo(response, options, currentPage, perPage, total) {
|
|
519
|
+
const direct = this.resolve(response, options.to);
|
|
520
|
+
if (direct !== undefined) {
|
|
521
|
+
return direct;
|
|
522
|
+
}
|
|
523
|
+
if (currentPage && perPage && total) {
|
|
524
|
+
return Math.min(currentPage * perPage, total);
|
|
525
|
+
}
|
|
526
|
+
return undefined;
|
|
482
527
|
}
|
|
528
|
+
}
|
|
529
|
+
|
|
530
|
+
/**
|
|
531
|
+
* Response strategy for the JSON:API driver
|
|
532
|
+
*
|
|
533
|
+
* Parses JSON:API pagination responses:
|
|
534
|
+
* ```json
|
|
535
|
+
* {
|
|
536
|
+
* "data": [...],
|
|
537
|
+
* "meta": {
|
|
538
|
+
* "current-page": 1,
|
|
539
|
+
* "per-page": 10,
|
|
540
|
+
* "total": 100,
|
|
541
|
+
* "page-count": 10,
|
|
542
|
+
* "from": 1,
|
|
543
|
+
* "to": 10
|
|
544
|
+
* },
|
|
545
|
+
* "links": {
|
|
546
|
+
* "first": "url",
|
|
547
|
+
* "prev": "url",
|
|
548
|
+
* "next": "url",
|
|
549
|
+
* "last": "url"
|
|
550
|
+
* }
|
|
551
|
+
* }
|
|
552
|
+
* ```
|
|
553
|
+
*
|
|
554
|
+
* Default key paths are configured in `JsonApiResponseOptions`. The
|
|
555
|
+
* traversal algorithm (dot-notation resolution + computed `from`/`to`) is
|
|
556
|
+
* inherited from `AbstractDotPathResponseStrategy`; this class exists so
|
|
557
|
+
* `DriverEnum.JSON_API` resolves to a distinct identity at the DI layer
|
|
558
|
+
* even though the parsing logic is shared with NestJS.
|
|
559
|
+
*
|
|
560
|
+
* @see https://jsonapi.org/format/
|
|
561
|
+
*/
|
|
562
|
+
class JsonApiResponseStrategy extends AbstractDotPathResponseStrategy {
|
|
563
|
+
}
|
|
564
|
+
|
|
565
|
+
/**
|
|
566
|
+
* Request strategy for the Laravel (pagination-only) driver
|
|
567
|
+
*
|
|
568
|
+
* Generates simple pagination URIs:
|
|
569
|
+
* - `/{resource}?limit=N&page=N`
|
|
570
|
+
*
|
|
571
|
+
* Filters, sorts, fields, includes, search, and select in state are ignored.
|
|
572
|
+
*/
|
|
573
|
+
class LaravelRequestStrategy extends AbstractRequestStrategy {
|
|
483
574
|
/**
|
|
484
|
-
*
|
|
485
|
-
|
|
575
|
+
* Pagination-only driver — no filtering, sorting, or column selection
|
|
576
|
+
*/
|
|
577
|
+
capabilities = {
|
|
578
|
+
fields: false,
|
|
579
|
+
filters: false,
|
|
580
|
+
includes: false,
|
|
581
|
+
operatorFilters: false,
|
|
582
|
+
search: false,
|
|
583
|
+
select: false,
|
|
584
|
+
sort: false
|
|
585
|
+
};
|
|
586
|
+
/**
|
|
587
|
+
* Emit only the pagination params; filters/sorts/etc. are ignored
|
|
486
588
|
*
|
|
487
|
-
* @param
|
|
488
|
-
* @
|
|
489
|
-
* @
|
|
490
|
-
* service.deleteFields({ users: ['email'] });
|
|
491
|
-
* service.deleteFields({ posts: ['content', 'body'] });
|
|
589
|
+
* @param state - The current query builder state
|
|
590
|
+
* @param options - The query parameter key name configuration
|
|
591
|
+
* @returns The two pagination query-string fragments
|
|
492
592
|
*/
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
return;
|
|
499
|
-
}
|
|
500
|
-
f[k] = f[k].filter(v => !fields[k].includes(v));
|
|
501
|
-
});
|
|
502
|
-
this._nest.update(nest => ({
|
|
503
|
-
...nest,
|
|
504
|
-
fields: f
|
|
505
|
-
}));
|
|
593
|
+
parts(state, options) {
|
|
594
|
+
return [
|
|
595
|
+
`${options.limit}=${state.limit}`,
|
|
596
|
+
`${options.page}=${state.page}`
|
|
597
|
+
];
|
|
506
598
|
}
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Response strategy for the Laravel (pagination-only) driver
|
|
603
|
+
*
|
|
604
|
+
* Parses flat Laravel pagination responses:
|
|
605
|
+
* ```json
|
|
606
|
+
* {
|
|
607
|
+
* "data": [...],
|
|
608
|
+
* "current_page": 1,
|
|
609
|
+
* "total": 100,
|
|
610
|
+
* "per_page": 15,
|
|
611
|
+
* "from": 1,
|
|
612
|
+
* "to": 15,
|
|
613
|
+
* ...
|
|
614
|
+
* }
|
|
615
|
+
* ```
|
|
616
|
+
*/
|
|
617
|
+
class LaravelResponseStrategy {
|
|
507
618
|
/**
|
|
508
|
-
*
|
|
509
|
-
* Uses deep cloning to prevent mutations to the original state
|
|
619
|
+
* Parse a flat Laravel pagination response into a PaginatedCollection
|
|
510
620
|
*
|
|
511
|
-
* @param
|
|
512
|
-
* @
|
|
513
|
-
* @
|
|
514
|
-
* service.deleteFilters('id');
|
|
515
|
-
* service.deleteFilters('status', 'type');
|
|
621
|
+
* @param response - The raw API response object
|
|
622
|
+
* @param options - The response key name configuration
|
|
623
|
+
* @returns A typed PaginatedCollection instance
|
|
516
624
|
*/
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
filters.forEach(k => delete f[k]);
|
|
521
|
-
this._nest.update(nest => ({
|
|
522
|
-
...nest,
|
|
523
|
-
filters: f
|
|
524
|
-
}));
|
|
625
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
626
|
+
paginate(response, options) {
|
|
627
|
+
return new PaginatedCollection(response[options.data], response[options.currentPage], response[options.from], response[options.to], response[options.total], response[options.perPage], response[options.prevPageUrl], response[options.nextPageUrl], response[options.lastPage], response[options.firstPageUrl], response[options.lastPageUrl]);
|
|
525
628
|
}
|
|
629
|
+
}
|
|
630
|
+
|
|
631
|
+
/**
|
|
632
|
+
* Request strategy for the NestJS (nestjs-paginate) driver
|
|
633
|
+
*
|
|
634
|
+
* Generates URIs in the NestJS paginate format:
|
|
635
|
+
* - Simple filters: `filter.field=value`
|
|
636
|
+
* - Operator filters: `filter.field=$operator:value`
|
|
637
|
+
* - Sorts: `sortBy=field1:DESC,field2:ASC`
|
|
638
|
+
* - Select: `select=col1,col2`
|
|
639
|
+
* - Search: `search=term`
|
|
640
|
+
* - Pagination: `limit=N&page=N`
|
|
641
|
+
*
|
|
642
|
+
* @see https://github.com/ppetzold/nestjs-paginate
|
|
643
|
+
*/
|
|
644
|
+
class NestjsRequestStrategy extends AbstractRequestStrategy {
|
|
526
645
|
/**
|
|
527
|
-
*
|
|
646
|
+
* Filters, operator filters, sorts, flat select, global search — no
|
|
647
|
+
* per-model fields, no includes
|
|
648
|
+
*/
|
|
649
|
+
capabilities = {
|
|
650
|
+
fields: false,
|
|
651
|
+
filters: true,
|
|
652
|
+
includes: false,
|
|
653
|
+
operatorFilters: true,
|
|
654
|
+
search: true,
|
|
655
|
+
select: true,
|
|
656
|
+
sort: true
|
|
657
|
+
};
|
|
658
|
+
/**
|
|
659
|
+
* Validate that the given limit is accepted by nestjs-paginate
|
|
528
660
|
*
|
|
529
|
-
*
|
|
530
|
-
*
|
|
531
|
-
*
|
|
532
|
-
*
|
|
533
|
-
*
|
|
661
|
+
* Accepts any integer `>= 1` as a page size, plus `-1` which nestjs-paginate
|
|
662
|
+
* interprets as "fetch all items" (server must opt-in via `maxLimit: -1`).
|
|
663
|
+
*
|
|
664
|
+
* @param limit - The limit value to validate
|
|
665
|
+
* @throws {InvalidLimitError} If the value is not an integer, or is 0, or is a negative number other than -1
|
|
534
666
|
*/
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
667
|
+
validateLimit(limit) {
|
|
668
|
+
if (Number.isInteger(limit) && (limit === -1 || limit >= 1)) {
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
671
|
+
throw new InvalidLimitError(limit, true);
|
|
540
672
|
}
|
|
541
673
|
/**
|
|
542
|
-
*
|
|
674
|
+
* Emit NestJS-format query-string segments in canonical order:
|
|
675
|
+
* filters → operator filters → sortBy → select → search → limit → page
|
|
543
676
|
*
|
|
544
|
-
* @param
|
|
545
|
-
* @
|
|
546
|
-
* @
|
|
547
|
-
* service.deleteOperatorFilters('age');
|
|
548
|
-
* service.deleteOperatorFilters('price', 'quantity');
|
|
677
|
+
* @param state - The current query builder state
|
|
678
|
+
* @param options - The query parameter key name configuration
|
|
679
|
+
* @returns Ordered query-string fragments
|
|
549
680
|
*/
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
681
|
+
parts(state, options) {
|
|
682
|
+
const out = [];
|
|
683
|
+
this._appendFilters(state, options, out);
|
|
684
|
+
this._appendOperatorFilters(state, options, out);
|
|
685
|
+
this._appendSort(state, options, out);
|
|
686
|
+
this._appendSelect(state, options, out);
|
|
687
|
+
this._appendSearch(state, options, out);
|
|
688
|
+
this._appendLimit(state, options, out);
|
|
689
|
+
this._appendPage(state, options, out);
|
|
690
|
+
return out;
|
|
555
691
|
}
|
|
556
692
|
/**
|
|
557
|
-
*
|
|
693
|
+
* Append simple filter parameters as `filter.field=value1,value2`
|
|
558
694
|
*
|
|
559
|
-
* @
|
|
560
|
-
* @
|
|
561
|
-
*
|
|
695
|
+
* @param state - The current query builder state
|
|
696
|
+
* @param options - The query parameter key name configuration
|
|
697
|
+
* @param out - The accumulator the caller joins into the URI
|
|
562
698
|
*/
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
}
|
|
699
|
+
_appendFilters(state, options, out) {
|
|
700
|
+
const keys = Object.keys(state.filters);
|
|
701
|
+
if (!keys.length) {
|
|
702
|
+
return;
|
|
703
|
+
}
|
|
704
|
+
keys.forEach(key => {
|
|
705
|
+
const values = state.filters[key].join(',');
|
|
706
|
+
out.push(`${options.filters}.${key}=${values}`);
|
|
707
|
+
});
|
|
568
708
|
}
|
|
569
709
|
/**
|
|
570
|
-
*
|
|
710
|
+
* Append the limit parameter
|
|
571
711
|
*
|
|
572
|
-
* @param
|
|
573
|
-
* @
|
|
574
|
-
* @
|
|
575
|
-
* service.deleteSelect('email');
|
|
576
|
-
* service.deleteSelect('name', 'email');
|
|
712
|
+
* @param state - The current query builder state
|
|
713
|
+
* @param options - The query parameter key name configuration
|
|
714
|
+
* @param out - The accumulator the caller joins into the URI
|
|
577
715
|
*/
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
...nest,
|
|
581
|
-
select: nest.select.filter(f => !fields.includes(f))
|
|
582
|
-
}));
|
|
716
|
+
_appendLimit(state, options, out) {
|
|
717
|
+
out.push(`${options.limit}=${state.limit}`);
|
|
583
718
|
}
|
|
584
719
|
/**
|
|
585
|
-
*
|
|
720
|
+
* Append operator-filter parameters as `filter.field=$op:value`
|
|
586
721
|
*
|
|
587
|
-
*
|
|
588
|
-
*
|
|
589
|
-
* @
|
|
590
|
-
*
|
|
591
|
-
*
|
|
722
|
+
* Groups by field; multi-value operators ($in, $btw) join values with commas.
|
|
723
|
+
*
|
|
724
|
+
* @param state - The current query builder state
|
|
725
|
+
* @param options - The query parameter key name configuration
|
|
726
|
+
* @param out - The accumulator the caller joins into the URI
|
|
592
727
|
*/
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
}
|
|
728
|
+
_appendOperatorFilters(state, options, out) {
|
|
729
|
+
if (!state.operatorFilters.length) {
|
|
730
|
+
return;
|
|
731
|
+
}
|
|
732
|
+
state.operatorFilters.forEach((opFilter) => {
|
|
733
|
+
const values = opFilter.values.join(',');
|
|
734
|
+
out.push(`${options.filters}.${opFilter.field}=${opFilter.operator}:${values}`);
|
|
600
735
|
});
|
|
601
|
-
this._nest.update(nest => ({
|
|
602
|
-
...nest,
|
|
603
|
-
sorts: s
|
|
604
|
-
}));
|
|
605
736
|
}
|
|
606
737
|
/**
|
|
607
|
-
*
|
|
738
|
+
* Append the page parameter
|
|
608
739
|
*
|
|
609
|
-
* @param
|
|
610
|
-
* @
|
|
611
|
-
* @
|
|
612
|
-
* service.setSearch('john doe');
|
|
740
|
+
* @param state - The current query builder state
|
|
741
|
+
* @param options - The query parameter key name configuration
|
|
742
|
+
* @param out - The accumulator the caller joins into the URI
|
|
613
743
|
*/
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
...nest,
|
|
617
|
-
search
|
|
618
|
-
}));
|
|
744
|
+
_appendPage(state, options, out) {
|
|
745
|
+
out.push(`${options.page}=${state.page}`);
|
|
619
746
|
}
|
|
620
747
|
/**
|
|
621
|
-
*
|
|
622
|
-
* flip `isLastPageKnown` to `true`
|
|
748
|
+
* Append the search parameter as `search=term`
|
|
623
749
|
*
|
|
624
|
-
*
|
|
625
|
-
*
|
|
626
|
-
*
|
|
627
|
-
* drift out of sync.
|
|
628
|
-
*
|
|
629
|
-
* @param {number} lastPage - The last page number parsed from the most recent paginated response
|
|
630
|
-
* @return {void}
|
|
750
|
+
* @param state - The current query builder state
|
|
751
|
+
* @param options - The query parameter key name configuration
|
|
752
|
+
* @param out - The accumulator the caller joins into the URI
|
|
631
753
|
*/
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
}));
|
|
754
|
+
_appendSearch(state, options, out) {
|
|
755
|
+
if (!state.search) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
out.push(`${options.search}=${state.search}`);
|
|
638
759
|
}
|
|
639
760
|
/**
|
|
640
|
-
*
|
|
641
|
-
* Clears all fields, filters, includes, sorts, and resets pagination
|
|
761
|
+
* Append the select parameter as `select=col1,col2`
|
|
642
762
|
*
|
|
643
|
-
* @
|
|
644
|
-
* @
|
|
645
|
-
*
|
|
763
|
+
* @param state - The current query builder state
|
|
764
|
+
* @param options - The query parameter key name configuration
|
|
765
|
+
* @param out - The accumulator the caller joins into the URI
|
|
646
766
|
*/
|
|
647
|
-
|
|
648
|
-
|
|
767
|
+
_appendSelect(state, options, out) {
|
|
768
|
+
if (!state.select.length) {
|
|
769
|
+
return;
|
|
770
|
+
}
|
|
771
|
+
out.push(`${options.select}=${state.select.join(',')}`);
|
|
649
772
|
}
|
|
650
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
651
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService });
|
|
652
|
-
}
|
|
653
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, decorators: [{
|
|
654
|
-
type: Injectable
|
|
655
|
-
}], ctorParameters: () => [] });
|
|
656
|
-
|
|
657
|
-
/**
|
|
658
|
-
* Thrown when a pagination helper that needs `state.lastPage` is called
|
|
659
|
-
* before `PaginationService.paginate()` has ever synced a value.
|
|
660
|
-
*
|
|
661
|
-
* Examples: `NgQubeeService.lastPage()`, `NgQubeeService.totalPages()`.
|
|
662
|
-
*
|
|
663
|
-
* Safe-for-templates predicates (`isLastPage`, `hasNextPage`, etc.) do not
|
|
664
|
-
* throw and return conservative defaults instead.
|
|
665
|
-
*/
|
|
666
|
-
class PaginationNotSyncedError extends Error {
|
|
667
773
|
/**
|
|
668
|
-
*
|
|
669
|
-
*
|
|
670
|
-
*
|
|
774
|
+
* Append sort parameter as `sortBy=field1:DESC,field2:ASC`
|
|
775
|
+
*
|
|
776
|
+
* @param state - The current query builder state
|
|
777
|
+
* @param options - The query parameter key name configuration
|
|
778
|
+
* @param out - The accumulator the caller joins into the URI
|
|
671
779
|
*/
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
}
|
|
677
|
-
|
|
678
|
-
/**
|
|
679
|
-
* Error thrown when per-model field selection is attempted with a driver that does not support it
|
|
680
|
-
*
|
|
681
|
-
* Per-model field selection is only supported by the Spatie driver.
|
|
682
|
-
* Use `addSelect()` for NestJS flat field selection.
|
|
683
|
-
*/
|
|
684
|
-
class UnsupportedFieldSelectionError extends Error {
|
|
685
|
-
constructor() {
|
|
686
|
-
super('Per-model field selection is only supported by the Spatie driver. Use addSelect() for NestJS.');
|
|
687
|
-
this.name = 'UnsupportedFieldSelectionError';
|
|
780
|
+
_appendSort(state, options, out) {
|
|
781
|
+
if (!state.sorts.length) {
|
|
782
|
+
return;
|
|
783
|
+
}
|
|
784
|
+
const pairs = state.sorts.map(sort => `${sort.field}:${sort.order === SortEnum.DESC ? 'DESC' : 'ASC'}`);
|
|
785
|
+
out.push(`${options.sortBy}=${pairs.join(',')}`);
|
|
688
786
|
}
|
|
689
787
|
}
|
|
690
788
|
|
|
691
789
|
/**
|
|
692
|
-
*
|
|
790
|
+
* Response strategy for the NestJS (nestjs-paginate) driver
|
|
693
791
|
*
|
|
694
|
-
*
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
*
|
|
792
|
+
* Parses nested NestJS pagination responses:
|
|
793
|
+
* ```json
|
|
794
|
+
* {
|
|
795
|
+
* "data": [...],
|
|
796
|
+
* "meta": {
|
|
797
|
+
* "currentPage": 1,
|
|
798
|
+
* "totalItems": 100,
|
|
799
|
+
* "itemsPerPage": 10,
|
|
800
|
+
* "totalPages": 10
|
|
801
|
+
* },
|
|
802
|
+
* "links": {
|
|
803
|
+
* "first": "url",
|
|
804
|
+
* "previous": "url",
|
|
805
|
+
* "next": "url",
|
|
806
|
+
* "last": "url",
|
|
807
|
+
* "current": "url"
|
|
808
|
+
* }
|
|
809
|
+
* }
|
|
810
|
+
* ```
|
|
705
811
|
*
|
|
706
|
-
*
|
|
707
|
-
*
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
|
|
712
|
-
this.name = 'UnsupportedFilterOperatorError';
|
|
713
|
-
}
|
|
714
|
-
}
|
|
715
|
-
|
|
716
|
-
/**
|
|
717
|
-
* Error thrown when includes are attempted with a driver that does not support them
|
|
812
|
+
* Default key paths are configured in `NestjsResponseOptions`. The
|
|
813
|
+
* traversal algorithm (dot-notation resolution + computed `from`/`to`) is
|
|
814
|
+
* inherited from `AbstractDotPathResponseStrategy`; this class exists so
|
|
815
|
+
* `DriverEnum.NESTJS` resolves to a distinct identity at the DI layer
|
|
816
|
+
* even though the parsing logic is shared with JSON:API.
|
|
718
817
|
*
|
|
719
|
-
*
|
|
818
|
+
* @see https://github.com/ppetzold/nestjs-paginate
|
|
720
819
|
*/
|
|
721
|
-
class
|
|
722
|
-
constructor() {
|
|
723
|
-
super('Includes are only supported by the Spatie driver.');
|
|
724
|
-
this.name = 'UnsupportedIncludesError';
|
|
725
|
-
}
|
|
820
|
+
class NestjsResponseStrategy extends AbstractDotPathResponseStrategy {
|
|
726
821
|
}
|
|
727
822
|
|
|
728
823
|
/**
|
|
729
|
-
*
|
|
824
|
+
* Enum representing the available filter operators for explicit operator
|
|
825
|
+
* filters
|
|
730
826
|
*
|
|
731
|
-
*
|
|
827
|
+
* NestJS encodes these with the `$` prefix at the wire level
|
|
828
|
+
* (`filter.field=$operator:value`); PostgREST translates them to its own
|
|
829
|
+
* prefix notation (`col=eq.val`, `col=is.null`, etc.). The enum values are
|
|
830
|
+
* intentionally the NestJS form; each driver's request strategy is
|
|
831
|
+
* responsible for mapping them into its own shape.
|
|
832
|
+
*
|
|
833
|
+
* `FTS`, `PLFTS`, `PHFTS`, `WFTS` are PostgREST-native full-text search
|
|
834
|
+
* variants; they throw `UnsupportedFilterOperatorError` on every other
|
|
835
|
+
* driver that does not recognise them.
|
|
836
|
+
*
|
|
837
|
+
* @see https://github.com/ppetzold/nestjs-paginate
|
|
838
|
+
* @see https://postgrest.org/en/stable/api.html#operators
|
|
732
839
|
*/
|
|
733
|
-
|
|
734
|
-
|
|
735
|
-
|
|
736
|
-
|
|
737
|
-
|
|
738
|
-
|
|
840
|
+
var FilterOperatorEnum;
|
|
841
|
+
(function (FilterOperatorEnum) {
|
|
842
|
+
FilterOperatorEnum["BTW"] = "$btw";
|
|
843
|
+
FilterOperatorEnum["CONTAINS"] = "$contains";
|
|
844
|
+
FilterOperatorEnum["EQ"] = "$eq";
|
|
845
|
+
FilterOperatorEnum["FTS"] = "$fts";
|
|
846
|
+
FilterOperatorEnum["GT"] = "$gt";
|
|
847
|
+
FilterOperatorEnum["GTE"] = "$gte";
|
|
848
|
+
FilterOperatorEnum["ILIKE"] = "$ilike";
|
|
849
|
+
FilterOperatorEnum["IN"] = "$in";
|
|
850
|
+
FilterOperatorEnum["LT"] = "$lt";
|
|
851
|
+
FilterOperatorEnum["LTE"] = "$lte";
|
|
852
|
+
FilterOperatorEnum["NOT"] = "$not";
|
|
853
|
+
FilterOperatorEnum["NULL"] = "$null";
|
|
854
|
+
FilterOperatorEnum["PHFTS"] = "$phfts";
|
|
855
|
+
FilterOperatorEnum["PLFTS"] = "$plfts";
|
|
856
|
+
FilterOperatorEnum["SW"] = "$sw";
|
|
857
|
+
FilterOperatorEnum["WFTS"] = "$wfts";
|
|
858
|
+
})(FilterOperatorEnum || (FilterOperatorEnum = {}));
|
|
739
859
|
|
|
740
860
|
/**
|
|
741
|
-
*
|
|
861
|
+
* Enum representing the wire-level pagination mechanism
|
|
742
862
|
*
|
|
743
|
-
*
|
|
744
|
-
*
|
|
863
|
+
* `QUERY` (default) — the request strategy emits `limit` and `offset` (or
|
|
864
|
+
* equivalent) query parameters on the URL.
|
|
865
|
+
*
|
|
866
|
+
* `RANGE` — the request strategy omits URL-based pagination and the
|
|
867
|
+
* consumer instead applies HTTP request headers returned by
|
|
868
|
+
* `NgQubeeService.paginationHeaders()`. Currently honoured only by the
|
|
869
|
+
* PostgREST driver, which maps it to `Range-Unit: items` + `Range: 0-9`.
|
|
870
|
+
* Other drivers ignore the setting.
|
|
745
871
|
*/
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
}
|
|
872
|
+
var PaginationModeEnum;
|
|
873
|
+
(function (PaginationModeEnum) {
|
|
874
|
+
PaginationModeEnum["QUERY"] = "query";
|
|
875
|
+
PaginationModeEnum["RANGE"] = "range";
|
|
876
|
+
})(PaginationModeEnum || (PaginationModeEnum = {}));
|
|
752
877
|
|
|
753
878
|
/**
|
|
754
|
-
*
|
|
879
|
+
* Thrown when a filter operator receives a value array of the wrong shape
|
|
755
880
|
*
|
|
756
|
-
*
|
|
881
|
+
* Some operators have arity or type constraints that the library enforces
|
|
882
|
+
* at call time so misuse fails loudly instead of silently emitting invalid
|
|
883
|
+
* server requests:
|
|
884
|
+
*
|
|
885
|
+
* - `BTW` requires exactly two values (min, max).
|
|
886
|
+
* - `NULL` requires exactly one boolean value (`true` for `IS NULL`,
|
|
887
|
+
* `false` for `IS NOT NULL`).
|
|
888
|
+
*
|
|
889
|
+
* Operators with looser shape rules leave validation to the server; this
|
|
890
|
+
* error is reserved for cases where the library itself can detect the
|
|
891
|
+
* problem unambiguously from the call site.
|
|
757
892
|
*/
|
|
758
|
-
class
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
893
|
+
class InvalidFilterOperatorValueError extends Error {
|
|
894
|
+
/**
|
|
895
|
+
* @param operator - The operator that rejected the values
|
|
896
|
+
* @param reason - Short human-readable explanation of the constraint
|
|
897
|
+
*/
|
|
898
|
+
constructor(operator, reason) {
|
|
899
|
+
super(`Invalid values for filter operator ${operator}: ${reason}`);
|
|
900
|
+
this.name = 'InvalidFilterOperatorValueError';
|
|
762
901
|
}
|
|
763
902
|
}
|
|
764
903
|
|
|
765
904
|
/**
|
|
766
|
-
*
|
|
767
|
-
*
|
|
768
|
-
* Provided by `provideNgQubee()` / `NgQubeeModule.forRoot()` from the
|
|
769
|
-
* user-supplied `IConfig.driver`. Services read it to gate driver-specific
|
|
770
|
-
* behavior (e.g. `NgQubeeService._assertDriver`).
|
|
771
|
-
*/
|
|
772
|
-
const NG_QUBEE_DRIVER = new InjectionToken('NG_QUBEE_DRIVER');
|
|
773
|
-
/**
|
|
774
|
-
* Injection token for the resolved request URI strategy
|
|
905
|
+
* Request strategy for the PostgREST driver
|
|
775
906
|
*
|
|
776
|
-
*
|
|
777
|
-
*
|
|
778
|
-
|
|
779
|
-
const NG_QUBEE_REQUEST_STRATEGY = new InjectionToken('NG_QUBEE_REQUEST_STRATEGY');
|
|
780
|
-
/**
|
|
781
|
-
* Injection token for the resolved request query-parameter key options
|
|
907
|
+
* PostgREST auto-generates REST APIs from PostgreSQL schemas and is the
|
|
908
|
+
* backbone of Supabase's data API. This strategy produces URIs in
|
|
909
|
+
* PostgREST's native query-string format:
|
|
782
910
|
*
|
|
783
|
-
*
|
|
784
|
-
*
|
|
785
|
-
*
|
|
786
|
-
|
|
787
|
-
const NG_QUBEE_REQUEST_OPTIONS = new InjectionToken('NG_QUBEE_REQUEST_OPTIONS');
|
|
788
|
-
/**
|
|
789
|
-
* Injection token for the resolved response parsing strategy
|
|
911
|
+
* - Filters: `col=eq.val` (single value) / `col=in.(v1,v2,v3)` (multi-value)
|
|
912
|
+
* - Order: `order=col1.asc,col2.desc`
|
|
913
|
+
* - Select: `select=col1,col2`
|
|
914
|
+
* - Pagination: `limit=N&offset=M` (offset derived from state.page)
|
|
790
915
|
*
|
|
791
|
-
*
|
|
792
|
-
*
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
/**
|
|
796
|
-
* Injection token for the resolved response field-key options
|
|
916
|
+
* The `order` and `offset` query-parameter names are PostgREST conventions
|
|
917
|
+
* and are intentionally not configurable via `QueryBuilderOptions` (see
|
|
918
|
+
* issue #50 MVP scope). `limit`, `select`, and `filters` (per-column name)
|
|
919
|
+
* honour the existing option keys.
|
|
797
920
|
*
|
|
798
|
-
*
|
|
799
|
-
*
|
|
800
|
-
* `provideNgQubee()` constructs the correct variant from `IConfig.response`.
|
|
921
|
+
* @see https://postgrest.org/en/stable/api.html
|
|
922
|
+
* @see https://supabase.com/docs/reference/javascript/select
|
|
801
923
|
*/
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
class NgQubeeService {
|
|
805
|
-
_nestService;
|
|
924
|
+
class PostgrestRequestStrategy extends AbstractRequestStrategy {
|
|
806
925
|
/**
|
|
807
|
-
*
|
|
926
|
+
* Filters, operator filters (incl. FTS), sorts, flat select — no
|
|
927
|
+
* per-model fields, no JSON:API/Spatie-style includes, no global
|
|
928
|
+
* search (per-column FTS via the operator family covers it)
|
|
808
929
|
*/
|
|
809
|
-
|
|
930
|
+
capabilities = {
|
|
931
|
+
fields: false,
|
|
932
|
+
filters: true,
|
|
933
|
+
includes: false,
|
|
934
|
+
operatorFilters: true,
|
|
935
|
+
search: false,
|
|
936
|
+
select: true,
|
|
937
|
+
sort: true
|
|
938
|
+
};
|
|
939
|
+
static _offsetKey = 'offset';
|
|
940
|
+
static _orderKey = 'order';
|
|
810
941
|
/**
|
|
811
|
-
*
|
|
942
|
+
* Active pagination mode
|
|
943
|
+
*
|
|
944
|
+
* QUERY (default) → URL emits limit/offset.
|
|
945
|
+
* RANGE → URL omits them; `buildPaginationHeaders()` returns the
|
|
946
|
+
* `Range-Unit` / `Range` HTTP headers instead.
|
|
812
947
|
*/
|
|
813
|
-
|
|
948
|
+
_paginationMode;
|
|
814
949
|
/**
|
|
815
|
-
*
|
|
950
|
+
* @param paginationMode - Wire-level pagination mechanism. Defaults to
|
|
951
|
+
* `PaginationModeEnum.QUERY`; `provideNgQubee` wires this from
|
|
952
|
+
* `IConfig.pagination`.
|
|
816
953
|
*/
|
|
817
|
-
|
|
954
|
+
constructor(paginationMode = PaginationModeEnum.QUERY) {
|
|
955
|
+
super();
|
|
956
|
+
this._paginationMode = paginationMode;
|
|
957
|
+
}
|
|
818
958
|
/**
|
|
819
|
-
*
|
|
959
|
+
* Compute `Range-Unit` / `Range` HTTP headers for RANGE pagination mode
|
|
960
|
+
*
|
|
961
|
+
* In QUERY mode this returns `null` so `NgQubeeService.paginationHeaders()`
|
|
962
|
+
* conveys "no headers needed" to the consumer. In RANGE mode the method
|
|
963
|
+
* converts the 1-indexed `state.page` + `state.limit` into PostgREST's
|
|
964
|
+
* 0-indexed inclusive range (`from = (page - 1) * limit`,
|
|
965
|
+
* `to = from + limit - 1`) and returns both header values.
|
|
966
|
+
*
|
|
967
|
+
* @param state - The current query builder state
|
|
968
|
+
* @returns `{ 'Range-Unit': 'items', 'Range': 'from-to' }` or `null`
|
|
820
969
|
*/
|
|
821
|
-
|
|
970
|
+
buildPaginationHeaders(state) {
|
|
971
|
+
if (this._paginationMode !== PaginationModeEnum.RANGE) {
|
|
972
|
+
return null;
|
|
973
|
+
}
|
|
974
|
+
const from = (state.page - 1) * state.limit;
|
|
975
|
+
const to = from + state.limit - 1;
|
|
976
|
+
/* eslint-disable @typescript-eslint/naming-convention */
|
|
977
|
+
return {
|
|
978
|
+
'Range-Unit': 'items',
|
|
979
|
+
'Range': `${from}-${to}`
|
|
980
|
+
};
|
|
981
|
+
/* eslint-enable @typescript-eslint/naming-convention */
|
|
982
|
+
}
|
|
822
983
|
/**
|
|
823
|
-
*
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
984
|
+
* Emit PostgREST-format query-string segments in canonical order:
|
|
985
|
+
* filters → operator filters → order → select → (limit + offset in
|
|
986
|
+
* QUERY mode only — RANGE mode passes pagination via headers instead)
|
|
987
|
+
*
|
|
988
|
+
* @param state - The current query builder state
|
|
989
|
+
* @param options - The query parameter key name configuration
|
|
990
|
+
* @returns Ordered query-string fragments
|
|
991
|
+
*/
|
|
992
|
+
parts(state, options) {
|
|
993
|
+
const out = [];
|
|
994
|
+
this._appendFilters(state, out);
|
|
995
|
+
this._appendOperatorFilters(state, out);
|
|
996
|
+
this._appendOrder(state, out);
|
|
997
|
+
this._appendSelect(state, options, out);
|
|
998
|
+
if (this._paginationMode === PaginationModeEnum.QUERY) {
|
|
999
|
+
this._appendLimit(state, options, out);
|
|
1000
|
+
this._appendOffset(state, out);
|
|
1001
|
+
}
|
|
1002
|
+
return out;
|
|
831
1003
|
}
|
|
832
1004
|
/**
|
|
833
|
-
*
|
|
1005
|
+
* Append filter parameters in PostgREST format
|
|
834
1006
|
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
1007
|
+
* Every filter is operator-prefixed (PostgREST has no implicit equality):
|
|
1008
|
+
* a single value yields `col=eq.val`; multiple values collapse into
|
|
1009
|
+
* PostgREST's native IN-list syntax `col=in.(v1,v2,v3)`.
|
|
1010
|
+
*
|
|
1011
|
+
* @param state - The current query builder state
|
|
1012
|
+
* @param out - The accumulator the caller joins into the URI
|
|
838
1013
|
*/
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
1014
|
+
_appendFilters(state, out) {
|
|
1015
|
+
const keys = Object.keys(state.filters);
|
|
1016
|
+
if (!keys.length) {
|
|
1017
|
+
return;
|
|
842
1018
|
}
|
|
1019
|
+
keys.forEach(key => {
|
|
1020
|
+
const values = state.filters[key];
|
|
1021
|
+
if (!values.length) {
|
|
1022
|
+
return;
|
|
1023
|
+
}
|
|
1024
|
+
// single-value → eq.<val>
|
|
1025
|
+
// multi-value → in.(v1,v2,v3)
|
|
1026
|
+
const rhs = values.length === 1
|
|
1027
|
+
? `eq.${values[0]}`
|
|
1028
|
+
: `in.(${values.join(',')})`;
|
|
1029
|
+
out.push(`${key}=${rhs}`);
|
|
1030
|
+
});
|
|
843
1031
|
}
|
|
844
1032
|
/**
|
|
845
|
-
*
|
|
1033
|
+
* Append the limit parameter
|
|
846
1034
|
*
|
|
847
|
-
* @param
|
|
848
|
-
* @param
|
|
849
|
-
* @
|
|
850
|
-
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
1035
|
+
* @param state - The current query builder state
|
|
1036
|
+
* @param options - The query parameter key name configuration
|
|
1037
|
+
* @param out - The accumulator the caller joins into the URI
|
|
851
1038
|
*/
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
1039
|
+
_appendLimit(state, options, out) {
|
|
1040
|
+
out.push(`${options.limit}=${state.limit}`);
|
|
1041
|
+
}
|
|
1042
|
+
/**
|
|
1043
|
+
* Append the offset parameter, derived from state.page
|
|
1044
|
+
*
|
|
1045
|
+
* PostgREST uses offset-based pagination, not page-based. The offset is
|
|
1046
|
+
* computed as `(page - 1) * limit`. Omitted when offset would be 0
|
|
1047
|
+
* (i.e. page 1) since PostgREST defaults to offset=0 anyway and dropping
|
|
1048
|
+
* it keeps the URI shorter.
|
|
1049
|
+
*
|
|
1050
|
+
* @param state - The current query builder state
|
|
1051
|
+
* @param out - The accumulator the caller joins into the URI
|
|
1052
|
+
*/
|
|
1053
|
+
_appendOffset(state, out) {
|
|
1054
|
+
const offset = (state.page - 1) * state.limit;
|
|
1055
|
+
if (offset <= 0) {
|
|
1056
|
+
return;
|
|
856
1057
|
}
|
|
857
|
-
|
|
858
|
-
return this;
|
|
1058
|
+
out.push(`${PostgrestRequestStrategy._offsetKey}=${offset}`);
|
|
859
1059
|
}
|
|
860
1060
|
/**
|
|
861
|
-
*
|
|
1061
|
+
* Append explicit operator filters
|
|
862
1062
|
*
|
|
863
|
-
*
|
|
1063
|
+
* Maps each `FilterOperatorEnum` value to PostgREST's prefix-operator
|
|
1064
|
+
* syntax. `BTW` expands to two query params (`gte` + `lte`); `NULL`
|
|
1065
|
+
* emits `is.null` / `is.not.null` based on the boolean value; `NOT`
|
|
1066
|
+
* picks its inner operator by arity (`not.eq.val` for single values,
|
|
1067
|
+
* `not.in.(v1,v2)` for multi-value).
|
|
864
1068
|
*
|
|
865
|
-
* @param
|
|
866
|
-
* @param
|
|
867
|
-
* @
|
|
868
|
-
* @throws {UnsupportedFilterError} If the active driver does not support filters
|
|
1069
|
+
* @param state - The current query builder state
|
|
1070
|
+
* @param out - The accumulator the caller joins into the URI
|
|
1071
|
+
* @throws {InvalidFilterOperatorValueError} If `BTW` does not receive exactly 2 values, or `NULL` does not receive exactly 1 boolean
|
|
869
1072
|
*/
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
return this;
|
|
1073
|
+
_appendOperatorFilters(state, out) {
|
|
1074
|
+
if (!state.operatorFilters.length) {
|
|
1075
|
+
return;
|
|
874
1076
|
}
|
|
875
|
-
|
|
876
|
-
|
|
1077
|
+
state.operatorFilters.forEach(filter => {
|
|
1078
|
+
// BTW expands to two segments: col=gte.min and col=lte.max
|
|
1079
|
+
if (filter.operator === FilterOperatorEnum.BTW) {
|
|
1080
|
+
this._appendBetweenFilter(filter, out);
|
|
1081
|
+
return;
|
|
1082
|
+
}
|
|
1083
|
+
const rhs = this._formatOperatorRhs(filter);
|
|
1084
|
+
out.push(`${filter.field}=${rhs}`);
|
|
877
1085
|
});
|
|
878
|
-
this._nestService.page = 1;
|
|
879
|
-
return this;
|
|
880
1086
|
}
|
|
881
1087
|
/**
|
|
882
|
-
*
|
|
1088
|
+
* Append a `BTW` operator filter as two PostgREST segments
|
|
883
1089
|
*
|
|
884
|
-
* Produces: `
|
|
1090
|
+
* Produces: `col=gte.min` and `col=lte.max`. Values must be exactly
|
|
1091
|
+
* `[min, max]`.
|
|
885
1092
|
*
|
|
886
|
-
* @param
|
|
887
|
-
* @param
|
|
888
|
-
* @
|
|
889
|
-
* @returns {this}
|
|
890
|
-
* @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
|
|
1093
|
+
* @param filter - The operator filter carrying the BTW bounds
|
|
1094
|
+
* @param out - The accumulator the caller joins into the URI
|
|
1095
|
+
* @throws {InvalidFilterOperatorValueError} If values.length !== 2
|
|
891
1096
|
*/
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
1097
|
+
_appendBetweenFilter(filter, out) {
|
|
1098
|
+
if (filter.values.length !== 2) {
|
|
1099
|
+
throw new InvalidFilterOperatorValueError(filter.operator, 'BTW requires exactly 2 values (min, max)');
|
|
1100
|
+
}
|
|
1101
|
+
const [min, max] = filter.values;
|
|
1102
|
+
out.push(`${filter.field}=gte.${min}`);
|
|
1103
|
+
out.push(`${filter.field}=lte.${max}`);
|
|
1104
|
+
}
|
|
1105
|
+
/**
|
|
1106
|
+
* Build the right-hand-side of a PostgREST filter param for the given operator
|
|
1107
|
+
*
|
|
1108
|
+
* Kept as a separate helper so each operator's shape is visible in one
|
|
1109
|
+
* place and the dispatch is exhaustively typed against
|
|
1110
|
+
* `FilterOperatorEnum`.
|
|
1111
|
+
*
|
|
1112
|
+
* @param filter - The operator filter (field, operator, values)
|
|
1113
|
+
* @returns The PostgREST-formatted value portion (right of the `=` sign)
|
|
1114
|
+
* @throws {InvalidFilterOperatorValueError} If NULL receives a non-boolean or wrong arity
|
|
1115
|
+
*/
|
|
1116
|
+
_formatOperatorRhs(filter) {
|
|
1117
|
+
const { operator, values } = filter;
|
|
1118
|
+
const first = values[0];
|
|
1119
|
+
switch (operator) {
|
|
1120
|
+
case FilterOperatorEnum.EQ: return `eq.${first}`;
|
|
1121
|
+
case FilterOperatorEnum.GT: return `gt.${first}`;
|
|
1122
|
+
case FilterOperatorEnum.GTE: return `gte.${first}`;
|
|
1123
|
+
case FilterOperatorEnum.LT: return `lt.${first}`;
|
|
1124
|
+
case FilterOperatorEnum.LTE: return `lte.${first}`;
|
|
1125
|
+
case FilterOperatorEnum.ILIKE: return `ilike.${first}`;
|
|
1126
|
+
case FilterOperatorEnum.IN: return `in.(${values.join(',')})`;
|
|
1127
|
+
case FilterOperatorEnum.SW: return `like.${first}*`;
|
|
1128
|
+
case FilterOperatorEnum.CONTAINS: return `ilike.%${first}%`;
|
|
1129
|
+
case FilterOperatorEnum.FTS: return `fts.${first}`;
|
|
1130
|
+
case FilterOperatorEnum.PLFTS: return `plfts.${first}`;
|
|
1131
|
+
case FilterOperatorEnum.PHFTS: return `phfts.${first}`;
|
|
1132
|
+
case FilterOperatorEnum.WFTS: return `wfts.${first}`;
|
|
1133
|
+
case FilterOperatorEnum.NOT:
|
|
1134
|
+
return values.length === 1
|
|
1135
|
+
? `not.eq.${first}`
|
|
1136
|
+
: `not.in.(${values.join(',')})`;
|
|
1137
|
+
case FilterOperatorEnum.NULL: {
|
|
1138
|
+
if (values.length !== 1 || typeof first !== 'boolean') {
|
|
1139
|
+
throw new InvalidFilterOperatorValueError(operator, 'NULL requires exactly 1 boolean value (true → IS NULL, false → IS NOT NULL)');
|
|
1140
|
+
}
|
|
1141
|
+
return first ? 'is.null' : 'is.not.null';
|
|
1142
|
+
}
|
|
1143
|
+
// BTW is dispatched by _appendOperatorFilters; falling through would be a bug
|
|
1144
|
+
case FilterOperatorEnum.BTW:
|
|
1145
|
+
throw new InvalidFilterOperatorValueError(operator, 'BTW should be dispatched to _appendBetweenFilter — this indicates a bug');
|
|
1146
|
+
}
|
|
1147
|
+
}
|
|
1148
|
+
/**
|
|
1149
|
+
* Append the order parameter as `order=col1.asc,col2.desc`
|
|
1150
|
+
*
|
|
1151
|
+
* @param state - The current query builder state
|
|
1152
|
+
* @param out - The accumulator the caller joins into the URI
|
|
1153
|
+
*/
|
|
1154
|
+
_appendOrder(state, out) {
|
|
1155
|
+
if (!state.sorts.length) {
|
|
1156
|
+
return;
|
|
1157
|
+
}
|
|
1158
|
+
const pairs = state.sorts.map(sort => `${sort.field}.${sort.order === SortEnum.DESC ? 'desc' : 'asc'}`);
|
|
1159
|
+
out.push(`${PostgrestRequestStrategy._orderKey}=${pairs.join(',')}`);
|
|
1160
|
+
}
|
|
1161
|
+
/**
|
|
1162
|
+
* Append the select parameter as `select=col1,col2`
|
|
1163
|
+
*
|
|
1164
|
+
* PostgREST uses a `select` query param for column pruning, matching
|
|
1165
|
+
* NestJS semantics.
|
|
1166
|
+
*
|
|
1167
|
+
* @param state - The current query builder state
|
|
1168
|
+
* @param options - The query parameter key name configuration
|
|
1169
|
+
* @param out - The accumulator the caller joins into the URI
|
|
1170
|
+
*/
|
|
1171
|
+
_appendSelect(state, options, out) {
|
|
1172
|
+
if (!state.select.length) {
|
|
1173
|
+
return;
|
|
1174
|
+
}
|
|
1175
|
+
out.push(`${options.select}=${state.select.join(',')}`);
|
|
1176
|
+
}
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Read a header value by name from a `HeaderBag`, regardless of whether the
|
|
1181
|
+
* bag exposes a `.get()` accessor or plain property access.
|
|
1182
|
+
*
|
|
1183
|
+
* @param bag - The header bag to read from
|
|
1184
|
+
* @param name - The header name (case-sensitivity follows the underlying bag)
|
|
1185
|
+
* @returns The header value, or `null` if absent or the bag itself is falsy
|
|
1186
|
+
*/
|
|
1187
|
+
function readHeader(bag, name) {
|
|
1188
|
+
if (!bag) {
|
|
1189
|
+
return null;
|
|
1190
|
+
}
|
|
1191
|
+
const accessor = bag;
|
|
1192
|
+
if (typeof accessor.get === 'function') {
|
|
1193
|
+
return accessor.get(name);
|
|
1194
|
+
}
|
|
1195
|
+
const value = bag[name];
|
|
1196
|
+
return value ?? null;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Response strategy for the PostgREST driver
|
|
1201
|
+
*
|
|
1202
|
+
* PostgREST (and Supabase, which wraps it) returns a bare array body for
|
|
1203
|
+
* collection endpoints. Pagination metadata is carried in the
|
|
1204
|
+
* `Content-Range` HTTP response header, e.g. `0-9/50` meaning "items 0–9
|
|
1205
|
+
* out of 50 total". Consumers opt into totals by sending the
|
|
1206
|
+
* `Prefer: count=exact` request header.
|
|
1207
|
+
*
|
|
1208
|
+
* This strategy expects the consumer to pass the array body as `response`
|
|
1209
|
+
* (or a plain object with `response[options.data]` pointing at the array)
|
|
1210
|
+
* and the response headers via the optional `headers` bag. See
|
|
1211
|
+
* `PaginationService.paginate()` for the call-site shape.
|
|
1212
|
+
*
|
|
1213
|
+
* @see https://postgrest.org/en/stable/references/api/pagination_count.html
|
|
1214
|
+
*/
|
|
1215
|
+
class PostgrestResponseStrategy {
|
|
1216
|
+
static _contentRangeHeader = 'Content-Range';
|
|
1217
|
+
static _contentRangeRegex = /^(\d+)-(\d+)\/(\*|\d+)$/;
|
|
1218
|
+
/**
|
|
1219
|
+
* Parse a PostgREST response into a typed PaginatedCollection
|
|
1220
|
+
*
|
|
1221
|
+
* @param response - The raw response. Either the array body directly, or
|
|
1222
|
+
* an object with the array at `response[options.data]`.
|
|
1223
|
+
* @param options - The response key configuration (only `options.data` is
|
|
1224
|
+
* consulted; all pagination metadata comes from the Content-Range header).
|
|
1225
|
+
* @param headers - Optional HTTP response headers. The `Content-Range`
|
|
1226
|
+
* header drives page/total derivation; omission is tolerated and yields
|
|
1227
|
+
* a collection with `undefined` bounds (auto-sync will leave
|
|
1228
|
+
* `isLastPageKnown` at `false`).
|
|
1229
|
+
* @returns A typed PaginatedCollection instance
|
|
1230
|
+
*/
|
|
1231
|
+
paginate(response, options, headers) {
|
|
1232
|
+
// Body may be a bare array or an envelope with the array at options.data
|
|
1233
|
+
const data = (Array.isArray(response) ? response : response[options.data]);
|
|
1234
|
+
// Header-driven pagination metadata
|
|
1235
|
+
const contentRange = readHeader(headers, PostgrestResponseStrategy._contentRangeHeader);
|
|
1236
|
+
const { from, to, total } = this._parseContentRange(contentRange);
|
|
1237
|
+
// Per-page can only be derived from the from/to range; fall back to undefined
|
|
1238
|
+
const perPage = (from !== undefined && to !== undefined) ? (to - from + 1) : undefined;
|
|
1239
|
+
// Page is 1-based in ng-qubee state; PostgREST reports 0-based indices
|
|
1240
|
+
const page = (perPage && from !== undefined) ? Math.floor(from / perPage) + 1 : 1;
|
|
1241
|
+
const lastPage = (total !== undefined && perPage) ? Math.ceil(total / perPage) : undefined;
|
|
1242
|
+
// Library convention: from/to are 1-indexed and inclusive; PostgREST emits 0-indexed
|
|
1243
|
+
const fromOneIndexed = from !== undefined ? from + 1 : undefined;
|
|
1244
|
+
const toOneIndexed = to !== undefined ? to + 1 : undefined;
|
|
1245
|
+
// PostgREST does not emit page URLs, so prev/next/first/last URLs stay undefined
|
|
1246
|
+
return new PaginatedCollection(data, page, fromOneIndexed, toOneIndexed, total, perPage, undefined, undefined, lastPage);
|
|
1247
|
+
}
|
|
1248
|
+
/**
|
|
1249
|
+
* Extract `{from, to, total}` from a PostgREST `Content-Range` value
|
|
1250
|
+
*
|
|
1251
|
+
* Expected format: `<from>-<to>/<total|*>`. Any shape mismatch returns
|
|
1252
|
+
* an empty object; `*` as the total yields `total: undefined`.
|
|
1253
|
+
*
|
|
1254
|
+
* @param value - Raw header value (possibly null/undefined)
|
|
1255
|
+
* @returns Parsed integers; missing fields indicate an unparseable header
|
|
1256
|
+
*/
|
|
1257
|
+
_parseContentRange(value) {
|
|
1258
|
+
if (!value) {
|
|
1259
|
+
return {};
|
|
896
1260
|
}
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
1261
|
+
const match = value.trim().match(PostgrestResponseStrategy._contentRangeRegex);
|
|
1262
|
+
if (!match) {
|
|
1263
|
+
return {};
|
|
1264
|
+
}
|
|
1265
|
+
const from = parseInt(match[1], 10);
|
|
1266
|
+
const to = parseInt(match[2], 10);
|
|
1267
|
+
const total = match[3] === '*' ? undefined : parseInt(match[3], 10);
|
|
1268
|
+
return { from, to, total };
|
|
900
1269
|
}
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
/**
|
|
1273
|
+
* Request strategy for the Spatie Query Builder driver
|
|
1274
|
+
*
|
|
1275
|
+
* Generates URIs in the Spatie format:
|
|
1276
|
+
* - Fields: `fields[model]=col1,col2`
|
|
1277
|
+
* - Filters: `filter[field]=value`
|
|
1278
|
+
* - Includes: `include=model1,model2`
|
|
1279
|
+
* - Sorts: `sort=-field1,field2` (- prefix = DESC)
|
|
1280
|
+
* - Pagination: `limit=N&page=N`
|
|
1281
|
+
*
|
|
1282
|
+
* @see https://spatie.be/docs/laravel-query-builder
|
|
1283
|
+
*/
|
|
1284
|
+
class SpatieRequestStrategy extends AbstractRequestStrategy {
|
|
901
1285
|
/**
|
|
902
|
-
*
|
|
1286
|
+
* Filters, sorts, includes, per-model fields — no operators, no flat
|
|
1287
|
+
* select, no global search
|
|
1288
|
+
*/
|
|
1289
|
+
capabilities = {
|
|
1290
|
+
fields: true,
|
|
1291
|
+
filters: true,
|
|
1292
|
+
includes: true,
|
|
1293
|
+
operatorFilters: false,
|
|
1294
|
+
search: false,
|
|
1295
|
+
select: false,
|
|
1296
|
+
sort: true
|
|
1297
|
+
};
|
|
1298
|
+
/**
|
|
1299
|
+
* Emit Spatie-format query-string segments in canonical order:
|
|
1300
|
+
* include → fields → filters → limit → page → sort
|
|
903
1301
|
*
|
|
904
|
-
* @param
|
|
905
|
-
* @
|
|
906
|
-
* @
|
|
1302
|
+
* @param state - The current query builder state
|
|
1303
|
+
* @param options - The query parameter key name configuration
|
|
1304
|
+
* @returns Ordered query-string fragments
|
|
907
1305
|
*/
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
this.
|
|
914
|
-
|
|
1306
|
+
parts(state, options) {
|
|
1307
|
+
const out = [];
|
|
1308
|
+
this._appendIncludes(state, options, out);
|
|
1309
|
+
this._appendFields(state, options, out);
|
|
1310
|
+
this._appendFilters(state, options, out);
|
|
1311
|
+
this._appendLimit(state, options, out);
|
|
1312
|
+
this._appendPage(state, options, out);
|
|
1313
|
+
this._appendSort(state, options, out);
|
|
1314
|
+
return out;
|
|
915
1315
|
}
|
|
916
1316
|
/**
|
|
917
|
-
*
|
|
1317
|
+
* Append per-model field selection in bracket notation
|
|
918
1318
|
*
|
|
919
|
-
*
|
|
1319
|
+
* Validates that each field model exists either as the main resource
|
|
1320
|
+
* or in the includes list.
|
|
920
1321
|
*
|
|
921
|
-
* @param
|
|
922
|
-
* @
|
|
923
|
-
* @
|
|
1322
|
+
* @param state - The current query builder state
|
|
1323
|
+
* @param options - The query parameter key name configuration
|
|
1324
|
+
* @param out - The accumulator the caller joins into the URI
|
|
1325
|
+
* @throws Error if the resource is required but not set
|
|
1326
|
+
* @throws UnselectableModelError if a field model is not in resource or includes
|
|
924
1327
|
*/
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
return this;
|
|
1328
|
+
_appendFields(state, options, out) {
|
|
1329
|
+
if (!Object.keys(state.fields).length) {
|
|
1330
|
+
return;
|
|
929
1331
|
}
|
|
930
|
-
|
|
931
|
-
|
|
1332
|
+
if (!(state.resource in state.fields)) {
|
|
1333
|
+
throw new Error(`Key ${state.resource} is missing in the fields object`);
|
|
1334
|
+
}
|
|
1335
|
+
const grouped = {};
|
|
1336
|
+
for (const model in state.fields) {
|
|
1337
|
+
if (!state.fields.hasOwnProperty(model)) {
|
|
1338
|
+
continue;
|
|
1339
|
+
}
|
|
1340
|
+
if (model !== state.resource && !state.includes.includes(model)) {
|
|
1341
|
+
throw new UnselectableModelError(model);
|
|
1342
|
+
}
|
|
1343
|
+
grouped[`${options.fields}[${model}]`] = state.fields[model].join(',');
|
|
1344
|
+
}
|
|
1345
|
+
out.push(qs.stringify(grouped, { encode: false }));
|
|
932
1346
|
}
|
|
933
1347
|
/**
|
|
934
|
-
*
|
|
1348
|
+
* Append filter parameters in bracket notation: `filter[key]=value`
|
|
935
1349
|
*
|
|
936
|
-
* @param
|
|
937
|
-
* @param
|
|
938
|
-
* @
|
|
939
|
-
* @throws {UnsupportedSortError} If the active driver does not support sorts
|
|
1350
|
+
* @param state - The current query builder state
|
|
1351
|
+
* @param options - The query parameter key name configuration
|
|
1352
|
+
* @param out - The accumulator the caller joins into the URI
|
|
940
1353
|
*/
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
1354
|
+
_appendFilters(state, options, out) {
|
|
1355
|
+
const keys = Object.keys(state.filters);
|
|
1356
|
+
if (!keys.length) {
|
|
1357
|
+
return;
|
|
1358
|
+
}
|
|
1359
|
+
const wrapper = {
|
|
1360
|
+
[options.filters]: keys.reduce((acc, key) => {
|
|
1361
|
+
return Object.assign(acc, { [key]: state.filters[key].join(',') });
|
|
1362
|
+
}, {})
|
|
1363
|
+
};
|
|
1364
|
+
out.push(qs.stringify(wrapper, { encode: false }));
|
|
949
1365
|
}
|
|
950
1366
|
/**
|
|
951
|
-
*
|
|
1367
|
+
* Append include parameter as `include=model1,model2`
|
|
952
1368
|
*
|
|
953
|
-
* @
|
|
954
|
-
* @
|
|
1369
|
+
* @param state - The current query builder state
|
|
1370
|
+
* @param options - The query parameter key name configuration
|
|
1371
|
+
* @param out - The accumulator the caller joins into the URI
|
|
955
1372
|
*/
|
|
956
|
-
|
|
957
|
-
|
|
1373
|
+
_appendIncludes(state, options, out) {
|
|
1374
|
+
if (!state.includes.length) {
|
|
1375
|
+
return;
|
|
1376
|
+
}
|
|
1377
|
+
out.push(`${options.includes}=${state.includes}`);
|
|
958
1378
|
}
|
|
959
1379
|
/**
|
|
960
|
-
*
|
|
961
|
-
*
|
|
962
|
-
* ```
|
|
963
|
-
* ngQubeeService.deleteFields({
|
|
964
|
-
* users: ['email', 'password'],
|
|
965
|
-
* address: ['zipcode']
|
|
966
|
-
* });
|
|
967
|
-
* ```
|
|
1380
|
+
* Append the limit parameter
|
|
968
1381
|
*
|
|
969
|
-
* @param
|
|
970
|
-
* @
|
|
971
|
-
* @
|
|
1382
|
+
* @param state - The current query builder state
|
|
1383
|
+
* @param options - The query parameter key name configuration
|
|
1384
|
+
* @param out - The accumulator the caller joins into the URI
|
|
972
1385
|
*/
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
this._nestService.deleteFields(fields);
|
|
976
|
-
return this;
|
|
1386
|
+
_appendLimit(state, options, out) {
|
|
1387
|
+
out.push(`${options.limit}=${state.limit}`);
|
|
977
1388
|
}
|
|
978
1389
|
/**
|
|
979
|
-
*
|
|
980
|
-
*
|
|
981
|
-
* ```
|
|
982
|
-
* ngQubeeService.deleteFieldsByModel('users', 'email', 'password');
|
|
983
|
-
* ```
|
|
1390
|
+
* Append the page parameter
|
|
984
1391
|
*
|
|
985
|
-
* @param
|
|
986
|
-
* @param
|
|
987
|
-
* @
|
|
988
|
-
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
1392
|
+
* @param state - The current query builder state
|
|
1393
|
+
* @param options - The query parameter key name configuration
|
|
1394
|
+
* @param out - The accumulator the caller joins into the URI
|
|
989
1395
|
*/
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
if (!fields.length) {
|
|
993
|
-
return this;
|
|
994
|
-
}
|
|
995
|
-
this._nestService.deleteFields({
|
|
996
|
-
[model]: fields
|
|
997
|
-
});
|
|
998
|
-
return this;
|
|
1396
|
+
_appendPage(state, options, out) {
|
|
1397
|
+
out.push(`${options.page}=${state.page}`);
|
|
999
1398
|
}
|
|
1000
1399
|
/**
|
|
1001
|
-
*
|
|
1400
|
+
* Append sort parameter as `sort=-field1,field2` (`-` prefix = DESC)
|
|
1002
1401
|
*
|
|
1003
|
-
* @param
|
|
1004
|
-
* @
|
|
1005
|
-
* @
|
|
1402
|
+
* @param state - The current query builder state
|
|
1403
|
+
* @param options - The query parameter key name configuration
|
|
1404
|
+
* @param out - The accumulator the caller joins into the URI
|
|
1006
1405
|
*/
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
1010
|
-
return this;
|
|
1406
|
+
_appendSort(state, options, out) {
|
|
1407
|
+
if (!state.sorts.length) {
|
|
1408
|
+
return;
|
|
1011
1409
|
}
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
return this;
|
|
1410
|
+
const pairs = state.sorts.map(sort => `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`);
|
|
1411
|
+
out.push(`${options.sort}=${pairs.join(',')}`);
|
|
1015
1412
|
}
|
|
1413
|
+
}
|
|
1414
|
+
|
|
1415
|
+
/**
|
|
1416
|
+
* Response strategy for the Spatie Query Builder driver
|
|
1417
|
+
*
|
|
1418
|
+
* Parses flat Laravel pagination responses:
|
|
1419
|
+
* ```json
|
|
1420
|
+
* {
|
|
1421
|
+
* "data": [...],
|
|
1422
|
+
* "current_page": 1,
|
|
1423
|
+
* "total": 100,
|
|
1424
|
+
* "per_page": 15,
|
|
1425
|
+
* "from": 1,
|
|
1426
|
+
* "to": 15,
|
|
1427
|
+
* ...
|
|
1428
|
+
* }
|
|
1429
|
+
* ```
|
|
1430
|
+
*
|
|
1431
|
+
* @see https://spatie.be/docs/laravel-query-builder
|
|
1432
|
+
*/
|
|
1433
|
+
class SpatieResponseStrategy {
|
|
1016
1434
|
/**
|
|
1017
|
-
*
|
|
1435
|
+
* Parse a flat Laravel pagination response into a PaginatedCollection
|
|
1018
1436
|
*
|
|
1019
|
-
* @param
|
|
1020
|
-
* @
|
|
1021
|
-
* @
|
|
1437
|
+
* @param response - The raw API response object
|
|
1438
|
+
* @param options - The response key name configuration
|
|
1439
|
+
* @returns A typed PaginatedCollection instance
|
|
1022
1440
|
*/
|
|
1023
|
-
|
|
1024
|
-
|
|
1025
|
-
|
|
1026
|
-
return this;
|
|
1027
|
-
}
|
|
1028
|
-
this._nestService.deleteIncludes(...includes);
|
|
1029
|
-
return this;
|
|
1441
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1442
|
+
paginate(response, options) {
|
|
1443
|
+
return new PaginatedCollection(response[options.data], response[options.currentPage], response[options.from], response[options.to], response[options.total], response[options.perPage], response[options.prevPageUrl], response[options.nextPageUrl], response[options.lastPage], response[options.firstPageUrl], response[options.lastPageUrl]);
|
|
1030
1444
|
}
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1041
|
-
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1045
|
-
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
/**
|
|
1448
|
+
* Driver registry — single source of truth for what each `DriverEnum`
|
|
1449
|
+
* value resolves to
|
|
1450
|
+
*
|
|
1451
|
+
* `Record<DriverEnum, IDriverDefinition>` gives compile-time
|
|
1452
|
+
* exhaustiveness: adding a new value to `DriverEnum` fails to compile
|
|
1453
|
+
* until its definition is added here. `provideNgQubee` looks up the
|
|
1454
|
+
* definition by driver and calls the three factories — no more parallel
|
|
1455
|
+
* `switch` blocks.
|
|
1456
|
+
*/
|
|
1457
|
+
const DRIVERS = {
|
|
1458
|
+
[DriverEnum.JSON_API]: {
|
|
1459
|
+
createRequestStrategy: () => new JsonApiRequestStrategy(),
|
|
1460
|
+
createResponseStrategy: () => new JsonApiResponseStrategy(),
|
|
1461
|
+
createResponseOptions: (config) => new JsonApiResponseOptions(config)
|
|
1462
|
+
},
|
|
1463
|
+
[DriverEnum.LARAVEL]: {
|
|
1464
|
+
createRequestStrategy: () => new LaravelRequestStrategy(),
|
|
1465
|
+
createResponseStrategy: () => new LaravelResponseStrategy(),
|
|
1466
|
+
createResponseOptions: (config) => new ResponseOptions(config)
|
|
1467
|
+
},
|
|
1468
|
+
[DriverEnum.NESTJS]: {
|
|
1469
|
+
createRequestStrategy: () => new NestjsRequestStrategy(),
|
|
1470
|
+
createResponseStrategy: () => new NestjsResponseStrategy(),
|
|
1471
|
+
createResponseOptions: (config) => new NestjsResponseOptions(config)
|
|
1472
|
+
},
|
|
1473
|
+
[DriverEnum.POSTGREST]: {
|
|
1474
|
+
createRequestStrategy: (mode) => new PostgrestRequestStrategy(mode),
|
|
1475
|
+
createResponseStrategy: () => new PostgrestResponseStrategy(),
|
|
1476
|
+
createResponseOptions: (config) => new ResponseOptions(config)
|
|
1477
|
+
},
|
|
1478
|
+
[DriverEnum.SPATIE]: {
|
|
1479
|
+
createRequestStrategy: () => new SpatieRequestStrategy(),
|
|
1480
|
+
createResponseStrategy: () => new SpatieResponseStrategy(),
|
|
1481
|
+
createResponseOptions: (config) => new ResponseOptions(config)
|
|
1482
|
+
}
|
|
1483
|
+
};
|
|
1484
|
+
|
|
1485
|
+
/**
|
|
1486
|
+
* Resolved query parameter key names with defaults applied
|
|
1487
|
+
*
|
|
1488
|
+
* Maps logical query concepts to the actual query parameter names
|
|
1489
|
+
* used in the generated URI. Unset values fall back to defaults.
|
|
1490
|
+
*/
|
|
1491
|
+
class QueryBuilderOptions {
|
|
1492
|
+
appends;
|
|
1493
|
+
fields;
|
|
1494
|
+
filters;
|
|
1495
|
+
includes;
|
|
1496
|
+
limit;
|
|
1497
|
+
page;
|
|
1498
|
+
search;
|
|
1499
|
+
select;
|
|
1500
|
+
sort;
|
|
1501
|
+
sortBy;
|
|
1502
|
+
constructor(options) {
|
|
1503
|
+
this.appends = options.appends || 'append';
|
|
1504
|
+
this.fields = options.fields || 'fields';
|
|
1505
|
+
this.filters = options.filters || 'filter';
|
|
1506
|
+
this.includes = options.includes || 'include';
|
|
1507
|
+
this.limit = options.limit || 'limit';
|
|
1508
|
+
this.page = options.page || 'page';
|
|
1509
|
+
this.search = options.search || 'search';
|
|
1510
|
+
this.select = options.select || 'select';
|
|
1511
|
+
this.sort = options.sort || 'sort';
|
|
1512
|
+
this.sortBy = options.sortBy || 'sortBy';
|
|
1513
|
+
}
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
/**
|
|
1517
|
+
* Error thrown when an invalid resource name is provided
|
|
1518
|
+
*
|
|
1519
|
+
* Resource name must be a non-empty string.
|
|
1520
|
+
*/
|
|
1521
|
+
class InvalidResourceNameError extends Error {
|
|
1522
|
+
constructor(resource) {
|
|
1523
|
+
super(`Invalid resource name: Resource name must be a non-empty string. Received: ${JSON.stringify(resource)}`);
|
|
1524
|
+
this.name = 'InvalidResourceNameError';
|
|
1525
|
+
}
|
|
1526
|
+
}
|
|
1527
|
+
|
|
1528
|
+
class InvalidPageNumberError extends Error {
|
|
1529
|
+
constructor(page) {
|
|
1530
|
+
super(`Invalid page number: Page must be a positive integer greater than 0. Received: ${page}`);
|
|
1531
|
+
this.name = 'InvalidPageNumberError';
|
|
1046
1532
|
}
|
|
1533
|
+
}
|
|
1534
|
+
|
|
1535
|
+
const INITIAL_STATE = {
|
|
1536
|
+
baseUrl: '',
|
|
1537
|
+
fields: {},
|
|
1538
|
+
filters: {},
|
|
1539
|
+
includes: [],
|
|
1540
|
+
isLastPageKnown: false,
|
|
1541
|
+
lastPage: 1,
|
|
1542
|
+
limit: 15,
|
|
1543
|
+
operatorFilters: [],
|
|
1544
|
+
page: 1,
|
|
1545
|
+
resource: '',
|
|
1546
|
+
search: '',
|
|
1547
|
+
select: [],
|
|
1548
|
+
sorts: []
|
|
1549
|
+
};
|
|
1550
|
+
class NestService {
|
|
1047
1551
|
/**
|
|
1048
|
-
*
|
|
1552
|
+
* Private writable signal that holds the Query Builder state
|
|
1049
1553
|
*
|
|
1050
|
-
* @
|
|
1051
|
-
* @throws {UnsupportedSearchError} If the active driver does not support search
|
|
1554
|
+
* @type {IQueryBuilderState}
|
|
1052
1555
|
*/
|
|
1053
|
-
|
|
1054
|
-
this._assertDriver([DriverEnum.NESTJS], new UnsupportedSearchError());
|
|
1055
|
-
this._nestService.deleteSearch();
|
|
1056
|
-
this._nestService.page = 1;
|
|
1057
|
-
return this;
|
|
1058
|
-
}
|
|
1556
|
+
_nest = signal(this._clone(INITIAL_STATE), ...(ngDevMode ? [{ debugName: "_nest" }] : []));
|
|
1059
1557
|
/**
|
|
1060
|
-
*
|
|
1558
|
+
* A computed signal that makes readonly the writable signal _nest
|
|
1061
1559
|
*
|
|
1062
|
-
* @
|
|
1063
|
-
* @returns {this}
|
|
1064
|
-
* @throws {UnsupportedSelectError} If the active driver does not support flat field selection
|
|
1560
|
+
* @type {Signal<IQueryBuilderState>}
|
|
1065
1561
|
*/
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
return this;
|
|
1070
|
-
}
|
|
1071
|
-
this._nestService.deleteSelect(...fields);
|
|
1072
|
-
return this;
|
|
1562
|
+
nest = computed(() => this._clone(this._nest()), ...(ngDevMode ? [{ debugName: "nest" }] : []));
|
|
1563
|
+
constructor() {
|
|
1564
|
+
// Nothing to see here
|
|
1073
1565
|
}
|
|
1074
1566
|
/**
|
|
1075
|
-
*
|
|
1567
|
+
* Set the base URL for the API
|
|
1076
1568
|
*
|
|
1077
|
-
* @param
|
|
1078
|
-
* @
|
|
1079
|
-
*
|
|
1569
|
+
* @param {string} baseUrl - The base URL to prepend to generated URIs
|
|
1570
|
+
* @example
|
|
1571
|
+
* service.baseUrl = 'https://api.example.com';
|
|
1080
1572
|
*/
|
|
1081
|
-
|
|
1082
|
-
this.
|
|
1083
|
-
|
|
1084
|
-
|
|
1085
|
-
|
|
1573
|
+
set baseUrl(baseUrl) {
|
|
1574
|
+
this._nest.update(nest => ({
|
|
1575
|
+
...nest,
|
|
1576
|
+
baseUrl
|
|
1577
|
+
}));
|
|
1086
1578
|
}
|
|
1087
1579
|
/**
|
|
1088
|
-
*
|
|
1580
|
+
* Set the limit for paginated results
|
|
1089
1581
|
*
|
|
1090
|
-
*
|
|
1091
|
-
*
|
|
1582
|
+
* This setter performs a raw state write. Validation of the value is the
|
|
1583
|
+
* responsibility of the active request strategy and is enforced upstream
|
|
1584
|
+
* by `NgQubeeService.setLimit()`, because the accepted range depends on
|
|
1585
|
+
* the driver (e.g. nestjs-paginate accepts `-1` for "fetch all").
|
|
1586
|
+
*
|
|
1587
|
+
* @param {number} limit - The number of items per page
|
|
1588
|
+
* @example
|
|
1589
|
+
* service.limit = 25;
|
|
1092
1590
|
*/
|
|
1093
|
-
|
|
1094
|
-
this.
|
|
1095
|
-
|
|
1591
|
+
set limit(limit) {
|
|
1592
|
+
this._nest.update(nest => ({
|
|
1593
|
+
...nest,
|
|
1594
|
+
limit
|
|
1595
|
+
}));
|
|
1096
1596
|
}
|
|
1097
1597
|
/**
|
|
1098
|
-
*
|
|
1598
|
+
* Set the page number for pagination
|
|
1599
|
+
* Must be a positive integer greater than 0
|
|
1099
1600
|
*
|
|
1100
|
-
* @
|
|
1601
|
+
* @param {number} page - The page number to fetch
|
|
1602
|
+
* @throws {InvalidPageNumberError} If page is not a positive integer
|
|
1603
|
+
* @example
|
|
1604
|
+
* service.page = 2;
|
|
1101
1605
|
*/
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
return throwError(() => error);
|
|
1109
|
-
}
|
|
1606
|
+
set page(page) {
|
|
1607
|
+
this._validatePageNumber(page);
|
|
1608
|
+
this._nest.update(nest => ({
|
|
1609
|
+
...nest,
|
|
1610
|
+
page
|
|
1611
|
+
}));
|
|
1110
1612
|
}
|
|
1111
1613
|
/**
|
|
1112
|
-
*
|
|
1113
|
-
*
|
|
1114
|
-
* Validates integer/positive via the existing `setPage` path, and
|
|
1115
|
-
* additionally rejects values that exceed `state.lastPage` when
|
|
1116
|
-
* pagination bounds are known.
|
|
1614
|
+
* Set the resource name for the query
|
|
1615
|
+
* Must be a non-empty string
|
|
1117
1616
|
*
|
|
1118
|
-
* @param
|
|
1119
|
-
* @
|
|
1120
|
-
* @
|
|
1617
|
+
* @param {string} resource - The API resource name (e.g., 'users', 'posts')
|
|
1618
|
+
* @throws {InvalidResourceNameError} If resource is not a non-empty string
|
|
1619
|
+
* @example
|
|
1620
|
+
* service.resource = 'users';
|
|
1121
1621
|
*/
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1622
|
+
set resource(resource) {
|
|
1623
|
+
this._validateResourceName(resource);
|
|
1624
|
+
this._nest.update(nest => ({
|
|
1625
|
+
...nest,
|
|
1626
|
+
resource
|
|
1627
|
+
}));
|
|
1628
|
+
}
|
|
1629
|
+
_clone(obj) {
|
|
1630
|
+
return JSON.parse(JSON.stringify(obj));
|
|
1129
1631
|
}
|
|
1130
1632
|
/**
|
|
1131
|
-
*
|
|
1633
|
+
* Validates that the page number is a positive integer
|
|
1132
1634
|
*
|
|
1133
|
-
* @
|
|
1134
|
-
* @
|
|
1635
|
+
* @param {number} page - The page number to validate
|
|
1636
|
+
* @throws {InvalidPageNumberError} If page is not a positive integer
|
|
1637
|
+
* @private
|
|
1135
1638
|
*/
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
1639
|
+
_validatePageNumber(page) {
|
|
1640
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
1641
|
+
throw new InvalidPageNumberError(page);
|
|
1642
|
+
}
|
|
1139
1643
|
}
|
|
1140
1644
|
/**
|
|
1141
|
-
*
|
|
1645
|
+
* Validates that the resource name is a non-empty string
|
|
1142
1646
|
*
|
|
1143
|
-
* @
|
|
1144
|
-
* @
|
|
1647
|
+
* @param {string} resource - The resource name to validate
|
|
1648
|
+
* @throws {InvalidResourceNameError} If resource is not a non-empty string
|
|
1649
|
+
* @private
|
|
1145
1650
|
*/
|
|
1146
|
-
|
|
1147
|
-
|
|
1651
|
+
_validateResourceName(resource) {
|
|
1652
|
+
if (!resource || typeof resource !== 'string' || resource.trim().length === 0) {
|
|
1653
|
+
throw new InvalidResourceNameError(resource);
|
|
1654
|
+
}
|
|
1148
1655
|
}
|
|
1149
1656
|
/**
|
|
1150
|
-
*
|
|
1657
|
+
* Add selectable fields for the given model to the request
|
|
1658
|
+
* Automatically prevents duplicate fields for each model
|
|
1151
1659
|
*
|
|
1152
|
-
* @
|
|
1153
|
-
* @
|
|
1660
|
+
* @param {IFields} fields - Object mapping model names to arrays of field names
|
|
1661
|
+
* @return {void}
|
|
1662
|
+
* @example
|
|
1663
|
+
* service.addFields({ users: ['id', 'email', 'username'] });
|
|
1664
|
+
* service.addFields({ posts: ['title', 'content'] });
|
|
1154
1665
|
*/
|
|
1155
|
-
|
|
1156
|
-
|
|
1666
|
+
addFields(fields) {
|
|
1667
|
+
this._nest.update(nest => {
|
|
1668
|
+
const mergedFields = { ...nest.fields };
|
|
1669
|
+
Object.keys(fields).forEach(model => {
|
|
1670
|
+
const existingFields = mergedFields[model] || [];
|
|
1671
|
+
const newFields = fields[model];
|
|
1672
|
+
// Use Set to prevent duplicates
|
|
1673
|
+
const uniqueFields = Array.from(new Set([...existingFields, ...newFields]));
|
|
1674
|
+
mergedFields[model] = uniqueFields;
|
|
1675
|
+
});
|
|
1676
|
+
return {
|
|
1677
|
+
...nest,
|
|
1678
|
+
fields: mergedFields
|
|
1679
|
+
};
|
|
1680
|
+
});
|
|
1157
1681
|
}
|
|
1158
1682
|
/**
|
|
1159
|
-
*
|
|
1683
|
+
* Add filters to the request
|
|
1684
|
+
* Automatically prevents duplicate filter values for each filter key
|
|
1160
1685
|
*
|
|
1161
|
-
* @
|
|
1162
|
-
* @
|
|
1686
|
+
* @param {IFilters} filters - Object mapping filter keys to arrays of values
|
|
1687
|
+
* @return {void}
|
|
1688
|
+
* @example
|
|
1689
|
+
* service.addFilters({ id: [1, 2, 3] });
|
|
1690
|
+
* service.addFilters({ status: ['active', 'pending'] });
|
|
1163
1691
|
*/
|
|
1164
|
-
|
|
1165
|
-
|
|
1166
|
-
|
|
1692
|
+
addFilters(filters) {
|
|
1693
|
+
this._nest.update(nest => {
|
|
1694
|
+
const mergedFilters = { ...nest.filters };
|
|
1695
|
+
Object.keys(filters).forEach(key => {
|
|
1696
|
+
const existingValues = mergedFilters[key] || [];
|
|
1697
|
+
const newValues = filters[key];
|
|
1698
|
+
// Use Set to prevent duplicates
|
|
1699
|
+
const uniqueValues = Array.from(new Set([...existingValues, ...newValues]));
|
|
1700
|
+
mergedFilters[key] = uniqueValues;
|
|
1701
|
+
});
|
|
1702
|
+
return {
|
|
1703
|
+
...nest,
|
|
1704
|
+
filters: mergedFilters
|
|
1705
|
+
};
|
|
1706
|
+
});
|
|
1167
1707
|
}
|
|
1168
1708
|
/**
|
|
1169
|
-
*
|
|
1709
|
+
* Add resources to include with the request
|
|
1710
|
+
* Automatically prevents duplicate includes
|
|
1170
1711
|
*
|
|
1171
|
-
* @
|
|
1172
|
-
* @
|
|
1173
|
-
* @
|
|
1712
|
+
* @param {string[]} includes - Array of resource names to include in the response
|
|
1713
|
+
* @return {void}
|
|
1714
|
+
* @example
|
|
1715
|
+
* service.addIncludes(['profile', 'posts']);
|
|
1716
|
+
* service.addIncludes(['comments']);
|
|
1174
1717
|
*/
|
|
1175
|
-
|
|
1176
|
-
|
|
1177
|
-
|
|
1178
|
-
|
|
1179
|
-
|
|
1180
|
-
|
|
1181
|
-
|
|
1718
|
+
addIncludes(includes) {
|
|
1719
|
+
this._nest.update(nest => {
|
|
1720
|
+
// Use Set to prevent duplicates
|
|
1721
|
+
const uniqueIncludes = Array.from(new Set([...nest.includes, ...includes]));
|
|
1722
|
+
return {
|
|
1723
|
+
...nest,
|
|
1724
|
+
includes: uniqueIncludes
|
|
1725
|
+
};
|
|
1726
|
+
});
|
|
1182
1727
|
}
|
|
1183
1728
|
/**
|
|
1184
|
-
*
|
|
1729
|
+
* Add filters with explicit operators (NestJS only)
|
|
1730
|
+
* Automatically prevents duplicate operator filters for the same field + operator combination
|
|
1185
1731
|
*
|
|
1186
|
-
* @
|
|
1187
|
-
* @
|
|
1732
|
+
* @param {IOperatorFilter[]} filters - Array of operator filter configurations
|
|
1733
|
+
* @return {void}
|
|
1734
|
+
* @example
|
|
1735
|
+
* import { FilterOperatorEnum } from 'ng-qubee';
|
|
1736
|
+
* service.addOperatorFilters([{ field: 'age', operator: FilterOperatorEnum.GTE, values: [18] }]);
|
|
1188
1737
|
*/
|
|
1189
|
-
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
1738
|
+
addOperatorFilters(filters) {
|
|
1739
|
+
this._nest.update(nest => {
|
|
1740
|
+
const merged = [...nest.operatorFilters];
|
|
1741
|
+
filters.forEach(newFilter => {
|
|
1742
|
+
const existingIdx = merged.findIndex(f => f.field === newFilter.field && f.operator === newFilter.operator);
|
|
1743
|
+
if (existingIdx > -1) {
|
|
1744
|
+
const existingValues = merged[existingIdx].values;
|
|
1745
|
+
merged[existingIdx] = {
|
|
1746
|
+
...merged[existingIdx],
|
|
1747
|
+
values: Array.from(new Set([...existingValues, ...newFilter.values]))
|
|
1748
|
+
};
|
|
1749
|
+
}
|
|
1750
|
+
else {
|
|
1751
|
+
merged.push({ ...newFilter });
|
|
1752
|
+
}
|
|
1753
|
+
});
|
|
1754
|
+
return {
|
|
1755
|
+
...nest,
|
|
1756
|
+
operatorFilters: merged
|
|
1757
|
+
};
|
|
1758
|
+
});
|
|
1196
1759
|
}
|
|
1197
1760
|
/**
|
|
1198
|
-
*
|
|
1761
|
+
* Add flat field selection columns (NestJS only)
|
|
1762
|
+
* Automatically prevents duplicate select fields
|
|
1199
1763
|
*
|
|
1200
|
-
* @
|
|
1201
|
-
* @
|
|
1764
|
+
* @param {string[]} fields - Array of column names to select
|
|
1765
|
+
* @return {void}
|
|
1766
|
+
* @example
|
|
1767
|
+
* service.addSelect(['id', 'name', 'email']);
|
|
1202
1768
|
*/
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
return
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1769
|
+
addSelect(fields) {
|
|
1770
|
+
this._nest.update(nest => {
|
|
1771
|
+
const uniqueSelect = Array.from(new Set([...nest.select, ...fields]));
|
|
1772
|
+
return {
|
|
1773
|
+
...nest,
|
|
1774
|
+
select: uniqueSelect
|
|
1775
|
+
};
|
|
1776
|
+
});
|
|
1210
1777
|
}
|
|
1211
1778
|
/**
|
|
1212
|
-
*
|
|
1779
|
+
* Add a field that should be used for sorting data
|
|
1213
1780
|
*
|
|
1214
|
-
* @
|
|
1781
|
+
* @param {ISort} sort - Sort configuration with field name and order (ASC/DESC)
|
|
1782
|
+
* @return {void}
|
|
1783
|
+
* @example
|
|
1784
|
+
* import { SortEnum } from 'ng-qubee';
|
|
1785
|
+
* service.addSort({ field: 'created_at', order: SortEnum.DESC });
|
|
1786
|
+
* service.addSort({ field: 'name', order: SortEnum.ASC });
|
|
1215
1787
|
*/
|
|
1216
|
-
|
|
1217
|
-
this.
|
|
1218
|
-
|
|
1788
|
+
addSort(sort) {
|
|
1789
|
+
this._nest.update(nest => ({
|
|
1790
|
+
...nest,
|
|
1791
|
+
sorts: [...nest.sorts, sort]
|
|
1792
|
+
}));
|
|
1219
1793
|
}
|
|
1220
1794
|
/**
|
|
1221
|
-
*
|
|
1795
|
+
* Remove fields for the given model
|
|
1796
|
+
* Uses deep cloning to prevent mutations to the original state
|
|
1222
1797
|
*
|
|
1223
|
-
* @param {
|
|
1224
|
-
* @
|
|
1798
|
+
* @param {IFields} fields - Object mapping model names to arrays of field names to remove
|
|
1799
|
+
* @return {void}
|
|
1800
|
+
* @example
|
|
1801
|
+
* service.deleteFields({ users: ['email'] });
|
|
1802
|
+
* service.deleteFields({ posts: ['content', 'body'] });
|
|
1225
1803
|
*/
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
1804
|
+
deleteFields(fields) {
|
|
1805
|
+
// Deep clone the fields object to prevent mutations
|
|
1806
|
+
const f = this._clone(this._nest().fields);
|
|
1807
|
+
Object.keys(fields).forEach(k => {
|
|
1808
|
+
if (!(k in f)) {
|
|
1809
|
+
return;
|
|
1810
|
+
}
|
|
1811
|
+
f[k] = f[k].filter(v => !fields[k].includes(v));
|
|
1812
|
+
});
|
|
1813
|
+
this._nest.update(nest => ({
|
|
1814
|
+
...nest,
|
|
1815
|
+
fields: f
|
|
1816
|
+
}));
|
|
1229
1817
|
}
|
|
1230
1818
|
/**
|
|
1231
|
-
*
|
|
1232
|
-
*
|
|
1233
|
-
* Validation is delegated to the active request strategy because the
|
|
1234
|
-
* accepted range is driver-specific: nestjs-paginate additionally accepts
|
|
1235
|
-
* `-1` as a "fetch all" sentinel, while Laravel, Spatie, and JSON:API
|
|
1236
|
-
* require a positive integer.
|
|
1819
|
+
* Remove filters from the request
|
|
1820
|
+
* Uses deep cloning to prevent mutations to the original state
|
|
1237
1821
|
*
|
|
1238
|
-
* @param
|
|
1239
|
-
* @
|
|
1240
|
-
* @
|
|
1822
|
+
* @param {...string[]} filters - Filter keys to remove
|
|
1823
|
+
* @return {void}
|
|
1824
|
+
* @example
|
|
1825
|
+
* service.deleteFilters('id');
|
|
1826
|
+
* service.deleteFilters('status', 'type');
|
|
1241
1827
|
*/
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
this.
|
|
1245
|
-
|
|
1246
|
-
|
|
1828
|
+
deleteFilters(...filters) {
|
|
1829
|
+
// Deep clone the filters object to prevent mutations
|
|
1830
|
+
const f = this._clone(this._nest().filters);
|
|
1831
|
+
filters.forEach(k => delete f[k]);
|
|
1832
|
+
this._nest.update(nest => ({
|
|
1833
|
+
...nest,
|
|
1834
|
+
filters: f
|
|
1835
|
+
}));
|
|
1247
1836
|
}
|
|
1248
1837
|
/**
|
|
1249
|
-
*
|
|
1838
|
+
* Remove includes from the request
|
|
1250
1839
|
*
|
|
1251
|
-
* @param
|
|
1252
|
-
* @
|
|
1840
|
+
* @param {...string[]} includes - Include names to remove
|
|
1841
|
+
* @return {void}
|
|
1842
|
+
* @example
|
|
1843
|
+
* service.deleteIncludes('profile');
|
|
1844
|
+
* service.deleteIncludes('posts', 'comments');
|
|
1253
1845
|
*/
|
|
1254
|
-
|
|
1255
|
-
this.
|
|
1256
|
-
|
|
1846
|
+
deleteIncludes(...includes) {
|
|
1847
|
+
this._nest.update(nest => ({
|
|
1848
|
+
...nest,
|
|
1849
|
+
includes: nest.includes.filter(v => !includes.includes(v))
|
|
1850
|
+
}));
|
|
1257
1851
|
}
|
|
1258
1852
|
/**
|
|
1259
|
-
*
|
|
1853
|
+
* Remove operator filters by field name (NestJS only)
|
|
1260
1854
|
*
|
|
1261
|
-
* @param {string}
|
|
1262
|
-
* @
|
|
1855
|
+
* @param {...string[]} fields - Field names of operator filters to remove
|
|
1856
|
+
* @return {void}
|
|
1857
|
+
* @example
|
|
1858
|
+
* service.deleteOperatorFilters('age');
|
|
1859
|
+
* service.deleteOperatorFilters('price', 'quantity');
|
|
1263
1860
|
*/
|
|
1264
|
-
|
|
1265
|
-
this.
|
|
1266
|
-
|
|
1267
|
-
|
|
1861
|
+
deleteOperatorFilters(...fields) {
|
|
1862
|
+
this._nest.update(nest => ({
|
|
1863
|
+
...nest,
|
|
1864
|
+
operatorFilters: nest.operatorFilters.filter(f => !fields.includes(f.field))
|
|
1865
|
+
}));
|
|
1268
1866
|
}
|
|
1269
1867
|
/**
|
|
1270
|
-
*
|
|
1271
|
-
*
|
|
1272
|
-
* Produces: `search=term`
|
|
1868
|
+
* Remove the search term from the state (NestJS only)
|
|
1273
1869
|
*
|
|
1274
|
-
* @
|
|
1275
|
-
* @
|
|
1276
|
-
*
|
|
1870
|
+
* @return {void}
|
|
1871
|
+
* @example
|
|
1872
|
+
* service.deleteSearch();
|
|
1277
1873
|
*/
|
|
1278
|
-
|
|
1279
|
-
this.
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1874
|
+
deleteSearch() {
|
|
1875
|
+
this._nest.update(nest => ({
|
|
1876
|
+
...nest,
|
|
1877
|
+
search: ''
|
|
1878
|
+
}));
|
|
1283
1879
|
}
|
|
1284
1880
|
/**
|
|
1285
|
-
*
|
|
1881
|
+
* Remove flat field selections from the state (NestJS only)
|
|
1286
1882
|
*
|
|
1287
|
-
* @
|
|
1288
|
-
* @
|
|
1289
|
-
* @
|
|
1883
|
+
* @param {...string[]} fields - Field names to remove from selection
|
|
1884
|
+
* @return {void}
|
|
1885
|
+
* @example
|
|
1886
|
+
* service.deleteSelect('email');
|
|
1887
|
+
* service.deleteSelect('name', 'email');
|
|
1290
1888
|
*/
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1294
|
-
|
|
1295
|
-
}
|
|
1296
|
-
return state.lastPage;
|
|
1889
|
+
deleteSelect(...fields) {
|
|
1890
|
+
this._nest.update(nest => ({
|
|
1891
|
+
...nest,
|
|
1892
|
+
select: nest.select.filter(f => !fields.includes(f))
|
|
1893
|
+
}));
|
|
1297
1894
|
}
|
|
1298
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService, deps: [{ token: NestService }, { token: NG_QUBEE_REQUEST_STRATEGY }, { token: NG_QUBEE_DRIVER }, { token: NG_QUBEE_REQUEST_OPTIONS }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1299
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService });
|
|
1300
|
-
}
|
|
1301
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService, decorators: [{
|
|
1302
|
-
type: Injectable
|
|
1303
|
-
}], ctorParameters: () => [{ type: NestService }, { type: undefined, decorators: [{
|
|
1304
|
-
type: Inject,
|
|
1305
|
-
args: [NG_QUBEE_REQUEST_STRATEGY]
|
|
1306
|
-
}] }, { type: DriverEnum, decorators: [{
|
|
1307
|
-
type: Inject,
|
|
1308
|
-
args: [NG_QUBEE_DRIVER]
|
|
1309
|
-
}] }, { type: QueryBuilderOptions, decorators: [{
|
|
1310
|
-
type: Inject,
|
|
1311
|
-
args: [NG_QUBEE_REQUEST_OPTIONS]
|
|
1312
|
-
}] }] });
|
|
1313
|
-
|
|
1314
|
-
class PaginationService {
|
|
1315
1895
|
/**
|
|
1316
|
-
*
|
|
1317
|
-
*
|
|
1318
|
-
*
|
|
1319
|
-
|
|
1320
|
-
|
|
1321
|
-
|
|
1322
|
-
*
|
|
1896
|
+
* Remove sorts from the request by field name
|
|
1897
|
+
*
|
|
1898
|
+
* @param {...string[]} sorts - Field names of sorts to remove
|
|
1899
|
+
* @return {void}
|
|
1900
|
+
* @example
|
|
1901
|
+
* service.deleteSorts('created_at');
|
|
1902
|
+
* service.deleteSorts('name', 'created_at');
|
|
1323
1903
|
*/
|
|
1324
|
-
|
|
1904
|
+
deleteSorts(...sorts) {
|
|
1905
|
+
const s = [...this._nest().sorts];
|
|
1906
|
+
sorts.forEach(field => {
|
|
1907
|
+
const p = s.findIndex(sort => sort.field === field);
|
|
1908
|
+
if (p > -1) {
|
|
1909
|
+
s.splice(p, 1);
|
|
1910
|
+
}
|
|
1911
|
+
});
|
|
1912
|
+
this._nest.update(nest => ({
|
|
1913
|
+
...nest,
|
|
1914
|
+
sorts: s
|
|
1915
|
+
}));
|
|
1916
|
+
}
|
|
1325
1917
|
/**
|
|
1326
|
-
*
|
|
1918
|
+
* Set the full-text search term (NestJS only)
|
|
1919
|
+
*
|
|
1920
|
+
* @param {string} search - The search term
|
|
1921
|
+
* @return {void}
|
|
1922
|
+
* @example
|
|
1923
|
+
* service.setSearch('john doe');
|
|
1327
1924
|
*/
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1925
|
+
setSearch(search) {
|
|
1926
|
+
this._nest.update(nest => ({
|
|
1927
|
+
...nest,
|
|
1928
|
+
search
|
|
1929
|
+
}));
|
|
1333
1930
|
}
|
|
1334
1931
|
/**
|
|
1335
|
-
*
|
|
1932
|
+
* Atomically record the `lastPage` value from a paginated response and
|
|
1933
|
+
* flip `isLastPageKnown` to `true`
|
|
1336
1934
|
*
|
|
1337
|
-
*
|
|
1338
|
-
* auto-
|
|
1339
|
-
*
|
|
1340
|
-
*
|
|
1935
|
+
* Called exclusively by `PaginationService.paginate()` as part of the
|
|
1936
|
+
* auto-sync contract; not intended to be invoked by consumers directly.
|
|
1937
|
+
* Keeping the two fields under a single write guarantees they cannot
|
|
1938
|
+
* drift out of sync.
|
|
1341
1939
|
*
|
|
1342
|
-
* @
|
|
1343
|
-
*
|
|
1344
|
-
|
|
1345
|
-
|
|
1940
|
+
* @param {number} lastPage - The last page number parsed from the most recent paginated response
|
|
1941
|
+
* @return {void}
|
|
1942
|
+
*/
|
|
1943
|
+
syncLastPage(lastPage) {
|
|
1944
|
+
this._nest.update(nest => ({
|
|
1945
|
+
...nest,
|
|
1946
|
+
isLastPageKnown: true,
|
|
1947
|
+
lastPage
|
|
1948
|
+
}));
|
|
1949
|
+
}
|
|
1950
|
+
/**
|
|
1951
|
+
* Reset the query builder state to initial values
|
|
1952
|
+
* Clears all fields, filters, includes, sorts, and resets pagination
|
|
1346
1953
|
*
|
|
1347
|
-
* @
|
|
1348
|
-
* @
|
|
1954
|
+
* @return {void}
|
|
1955
|
+
* @example
|
|
1956
|
+
* service.reset();
|
|
1349
1957
|
*/
|
|
1350
|
-
|
|
1351
|
-
|
|
1352
|
-
const collection = this._responseStrategy.paginate(response, this._options);
|
|
1353
|
-
this._nestService.page = collection.page;
|
|
1354
|
-
if (typeof collection.lastPage === 'number' && Number.isInteger(collection.lastPage) && collection.lastPage > 0) {
|
|
1355
|
-
this._nestService.syncLastPage(collection.lastPage);
|
|
1356
|
-
}
|
|
1357
|
-
return collection;
|
|
1958
|
+
reset() {
|
|
1959
|
+
this._nest.update(_ => this._clone(INITIAL_STATE));
|
|
1358
1960
|
}
|
|
1359
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type:
|
|
1360
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type:
|
|
1961
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1962
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService });
|
|
1361
1963
|
}
|
|
1362
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type:
|
|
1964
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, decorators: [{
|
|
1363
1965
|
type: Injectable
|
|
1364
|
-
}], ctorParameters: () => [
|
|
1365
|
-
type: Inject,
|
|
1366
|
-
args: [NG_QUBEE_RESPONSE_STRATEGY]
|
|
1367
|
-
}] }, { type: ResponseOptions, decorators: [{
|
|
1368
|
-
type: Inject,
|
|
1369
|
-
args: [NG_QUBEE_RESPONSE_OPTIONS]
|
|
1370
|
-
}] }] });
|
|
1371
|
-
|
|
1372
|
-
var SortEnum;
|
|
1373
|
-
(function (SortEnum) {
|
|
1374
|
-
SortEnum["ASC"] = "asc";
|
|
1375
|
-
SortEnum["DESC"] = "desc";
|
|
1376
|
-
})(SortEnum || (SortEnum = {}));
|
|
1966
|
+
}], ctorParameters: () => [] });
|
|
1377
1967
|
|
|
1378
1968
|
/**
|
|
1379
|
-
* Thrown when a
|
|
1969
|
+
* Thrown when a pagination helper that needs `state.lastPage` is called
|
|
1970
|
+
* before `PaginationService.paginate()` has ever synced a value.
|
|
1380
1971
|
*
|
|
1381
|
-
*
|
|
1382
|
-
*
|
|
1383
|
-
*
|
|
1384
|
-
*
|
|
1972
|
+
* Examples: `NgQubeeService.lastPage()`, `NgQubeeService.totalPages()`.
|
|
1973
|
+
*
|
|
1974
|
+
* Safe-for-templates predicates (`isLastPage`, `hasNextPage`, etc.) do not
|
|
1975
|
+
* throw and return conservative defaults instead.
|
|
1385
1976
|
*/
|
|
1386
|
-
class
|
|
1977
|
+
class PaginationNotSyncedError extends Error {
|
|
1387
1978
|
/**
|
|
1388
|
-
* @param
|
|
1389
|
-
*
|
|
1979
|
+
* @param action - Short imperative describing what the caller was trying
|
|
1980
|
+
* to do (e.g. "navigate to last page", "read totalPages"). Surfaced in
|
|
1981
|
+
* the error message so the cause is obvious at the call site.
|
|
1390
1982
|
*/
|
|
1391
|
-
constructor(
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
: 'a positive integer greater than 0';
|
|
1395
|
-
super(`Invalid limit value: Limit must be ${allowed}. Received: ${limit}`);
|
|
1396
|
-
this.name = 'InvalidLimitError';
|
|
1983
|
+
constructor(action) {
|
|
1984
|
+
super(`Cannot ${action}: no paginated response has been synced yet. Call PaginationService.paginate() at least once first.`);
|
|
1985
|
+
this.name = 'PaginationNotSyncedError';
|
|
1397
1986
|
}
|
|
1398
1987
|
}
|
|
1399
1988
|
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1989
|
+
/**
|
|
1990
|
+
* Error thrown when per-model field selection is attempted with a driver that does not support it
|
|
1991
|
+
*
|
|
1992
|
+
* Per-model field selection is only supported by the Spatie driver.
|
|
1993
|
+
* Use `addSelect()` for NestJS flat field selection.
|
|
1994
|
+
*/
|
|
1995
|
+
class UnsupportedFieldSelectionError extends Error {
|
|
1996
|
+
constructor() {
|
|
1997
|
+
super('Per-model field selection is only supported by the Spatie driver. Use addSelect() for NestJS.');
|
|
1998
|
+
this.name = 'UnsupportedFieldSelectionError';
|
|
1403
1999
|
}
|
|
1404
2000
|
}
|
|
1405
2001
|
|
|
1406
2002
|
/**
|
|
1407
|
-
*
|
|
2003
|
+
* Error thrown when filters are attempted with a driver that does not support them
|
|
1408
2004
|
*
|
|
1409
|
-
*
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
2005
|
+
* Filters are only supported by the Spatie and NestJS drivers.
|
|
2006
|
+
*/
|
|
2007
|
+
class UnsupportedFilterError extends Error {
|
|
2008
|
+
constructor() {
|
|
2009
|
+
super('Filters are only supported by the Spatie and NestJS drivers.');
|
|
2010
|
+
this.name = 'UnsupportedFilterError';
|
|
2011
|
+
}
|
|
2012
|
+
}
|
|
2013
|
+
|
|
2014
|
+
/**
|
|
2015
|
+
* Error thrown when filter operators are attempted with a driver that does not support them
|
|
1415
2016
|
*
|
|
1416
|
-
*
|
|
2017
|
+
* Filter operators are only supported by the NestJS driver.
|
|
2018
|
+
* Use `addFilter()` for Spatie implicit equality filters.
|
|
1417
2019
|
*/
|
|
1418
|
-
class
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
_uri = '';
|
|
1423
|
-
/**
|
|
1424
|
-
* Build a URI string from the given state using the JSON:API format
|
|
1425
|
-
*
|
|
1426
|
-
* @param state - The current query builder state
|
|
1427
|
-
* @param options - The query parameter key name configuration
|
|
1428
|
-
* @returns The composed URI string
|
|
1429
|
-
* @throws Error if resource is not set
|
|
1430
|
-
*/
|
|
1431
|
-
buildUri(state, options) {
|
|
1432
|
-
if (!state.resource) {
|
|
1433
|
-
throw new Error('Set the resource property BEFORE adding filters or calling the url() / get() methods');
|
|
1434
|
-
}
|
|
1435
|
-
this._uri = '';
|
|
1436
|
-
this._parseIncludes(state, options);
|
|
1437
|
-
this._parseFields(state, options);
|
|
1438
|
-
this._parseFilters(state, options);
|
|
1439
|
-
this._parsePagination(state, options);
|
|
1440
|
-
this._parseSort(state, options);
|
|
1441
|
-
return this._uri;
|
|
2020
|
+
class UnsupportedFilterOperatorError extends Error {
|
|
2021
|
+
constructor() {
|
|
2022
|
+
super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
|
|
2023
|
+
this.name = 'UnsupportedFilterOperatorError';
|
|
1442
2024
|
}
|
|
1443
|
-
|
|
1444
|
-
|
|
1445
|
-
|
|
1446
|
-
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
if (Number.isInteger(limit) && limit >= 1) {
|
|
1455
|
-
return;
|
|
1456
|
-
}
|
|
1457
|
-
throw new InvalidLimitError(limit);
|
|
2025
|
+
}
|
|
2026
|
+
|
|
2027
|
+
/**
|
|
2028
|
+
* Error thrown when includes are attempted with a driver that does not support them
|
|
2029
|
+
*
|
|
2030
|
+
* Includes are only supported by the Spatie driver.
|
|
2031
|
+
*/
|
|
2032
|
+
class UnsupportedIncludesError extends Error {
|
|
2033
|
+
constructor() {
|
|
2034
|
+
super('Includes are only supported by the Spatie driver.');
|
|
2035
|
+
this.name = 'UnsupportedIncludesError';
|
|
1458
2036
|
}
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
|
|
1488
|
-
|
|
1489
|
-
|
|
1490
|
-
|
|
1491
|
-
|
|
1492
|
-
|
|
2037
|
+
}
|
|
2038
|
+
|
|
2039
|
+
/**
|
|
2040
|
+
* Error thrown when search is attempted with a driver that does not support it
|
|
2041
|
+
*
|
|
2042
|
+
* Search is only supported by the NestJS driver.
|
|
2043
|
+
*/
|
|
2044
|
+
class UnsupportedSearchError extends Error {
|
|
2045
|
+
constructor() {
|
|
2046
|
+
super('Search is only supported by the NestJS driver.');
|
|
2047
|
+
this.name = 'UnsupportedSearchError';
|
|
2048
|
+
}
|
|
2049
|
+
}
|
|
2050
|
+
|
|
2051
|
+
/**
|
|
2052
|
+
* Error thrown when flat field selection is attempted with a driver that does not support it
|
|
2053
|
+
*
|
|
2054
|
+
* Flat field selection is only supported by the NestJS driver.
|
|
2055
|
+
* Use `addFields()` for Spatie per-model field selection.
|
|
2056
|
+
*/
|
|
2057
|
+
class UnsupportedSelectError extends Error {
|
|
2058
|
+
constructor() {
|
|
2059
|
+
super('Flat field selection is only supported by the NestJS driver. Use addFields() for Spatie.');
|
|
2060
|
+
this.name = 'UnsupportedSelectError';
|
|
2061
|
+
}
|
|
2062
|
+
}
|
|
2063
|
+
|
|
2064
|
+
/**
|
|
2065
|
+
* Error thrown when sorts are attempted with a driver that does not support them
|
|
2066
|
+
*
|
|
2067
|
+
* Sorts are only supported by the Spatie and NestJS drivers.
|
|
2068
|
+
*/
|
|
2069
|
+
class UnsupportedSortError extends Error {
|
|
2070
|
+
constructor() {
|
|
2071
|
+
super('Sorts are only supported by the Spatie and NestJS drivers.');
|
|
2072
|
+
this.name = 'UnsupportedSortError';
|
|
1493
2073
|
}
|
|
2074
|
+
}
|
|
2075
|
+
|
|
2076
|
+
/**
|
|
2077
|
+
* Injection token for the active pagination driver
|
|
2078
|
+
*
|
|
2079
|
+
* Provided by `provideNgQubee()` / `NgQubeeModule.forRoot()` from the
|
|
2080
|
+
* user-supplied `IConfig.driver`. Services read it to gate driver-specific
|
|
2081
|
+
* behavior (e.g. `NgQubeeService._assertDriver`).
|
|
2082
|
+
*/
|
|
2083
|
+
const NG_QUBEE_DRIVER = new InjectionToken('NG_QUBEE_DRIVER');
|
|
2084
|
+
/**
|
|
2085
|
+
* Injection token for the resolved request URI strategy
|
|
2086
|
+
*
|
|
2087
|
+
* Provided by `provideNgQubee()` / `NgQubeeModule.forRoot()` based on the
|
|
2088
|
+
* active driver. Used by `NgQubeeService` to build request URIs.
|
|
2089
|
+
*/
|
|
2090
|
+
const NG_QUBEE_REQUEST_STRATEGY = new InjectionToken('NG_QUBEE_REQUEST_STRATEGY');
|
|
2091
|
+
/**
|
|
2092
|
+
* Injection token for the resolved request query-parameter key options
|
|
2093
|
+
*
|
|
2094
|
+
* Provided as a fully-built `QueryBuilderOptions` instance. `provideNgQubee()`
|
|
2095
|
+
* constructs it from `IConfig.request`; consumers don't interact with this
|
|
2096
|
+
* token directly.
|
|
2097
|
+
*/
|
|
2098
|
+
const NG_QUBEE_REQUEST_OPTIONS = new InjectionToken('NG_QUBEE_REQUEST_OPTIONS');
|
|
2099
|
+
/**
|
|
2100
|
+
* Injection token for the resolved response parsing strategy
|
|
2101
|
+
*
|
|
2102
|
+
* Provided by `provideNgQubee()` / `NgQubeeModule.forRoot()` based on the
|
|
2103
|
+
* active driver. Used by `PaginationService` to parse paginated responses.
|
|
2104
|
+
*/
|
|
2105
|
+
const NG_QUBEE_RESPONSE_STRATEGY = new InjectionToken('NG_QUBEE_RESPONSE_STRATEGY');
|
|
2106
|
+
/**
|
|
2107
|
+
* Injection token for the resolved response field-key options
|
|
2108
|
+
*
|
|
2109
|
+
* Provided as a fully-built `ResponseOptions` instance (or a driver-specific
|
|
2110
|
+
* subclass like `JsonApiResponseOptions` / `NestjsResponseOptions`).
|
|
2111
|
+
* `provideNgQubee()` constructs the correct variant from `IConfig.response`.
|
|
2112
|
+
*/
|
|
2113
|
+
const NG_QUBEE_RESPONSE_OPTIONS = new InjectionToken('NG_QUBEE_RESPONSE_OPTIONS');
|
|
2114
|
+
|
|
2115
|
+
class NgQubeeService {
|
|
2116
|
+
_nestService;
|
|
1494
2117
|
/**
|
|
1495
|
-
*
|
|
1496
|
-
*
|
|
1497
|
-
* Generates filter parameters in bracket notation: `filter[key]=value1,value2`
|
|
1498
|
-
*
|
|
1499
|
-
* @param state - The current query builder state
|
|
1500
|
-
* @param options - The query parameter key name configuration
|
|
1501
|
-
* @returns The generated filter parameter string
|
|
2118
|
+
* The active pagination driver
|
|
1502
2119
|
*/
|
|
1503
|
-
|
|
1504
|
-
const keys = Object.keys(state.filters);
|
|
1505
|
-
if (!keys.length) {
|
|
1506
|
-
return this._uri;
|
|
1507
|
-
}
|
|
1508
|
-
const f = {
|
|
1509
|
-
[`${options.filters}`]: keys.reduce((acc, key) => {
|
|
1510
|
-
return Object.assign(acc, { [key]: state.filters[key].join(',') });
|
|
1511
|
-
}, {})
|
|
1512
|
-
};
|
|
1513
|
-
const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
|
|
1514
|
-
this._uri += param;
|
|
1515
|
-
return param;
|
|
1516
|
-
}
|
|
2120
|
+
_driver;
|
|
1517
2121
|
/**
|
|
1518
|
-
*
|
|
1519
|
-
*
|
|
1520
|
-
* Generates: `include=author,comments.author`
|
|
1521
|
-
*
|
|
1522
|
-
* @param state - The current query builder state
|
|
1523
|
-
* @param options - The query parameter key name configuration
|
|
1524
|
-
* @returns The generated include parameter string
|
|
2122
|
+
* Resolved query parameter key name options
|
|
1525
2123
|
*/
|
|
1526
|
-
|
|
1527
|
-
if (!state.includes.length) {
|
|
1528
|
-
return this._uri;
|
|
1529
|
-
}
|
|
1530
|
-
const param = `${this._prepend(state)}${options.includes}=${state.includes}`;
|
|
1531
|
-
this._uri += param;
|
|
1532
|
-
return param;
|
|
1533
|
-
}
|
|
2124
|
+
_options;
|
|
1534
2125
|
/**
|
|
1535
|
-
*
|
|
1536
|
-
*
|
|
1537
|
-
* Generates: `page[number]=1&page[size]=15`
|
|
1538
|
-
*
|
|
1539
|
-
* @param state - The current query builder state
|
|
1540
|
-
* @param options - The query parameter key name configuration
|
|
1541
|
-
* @returns The generated pagination parameter string
|
|
2126
|
+
* The request strategy that builds URIs for the active driver
|
|
1542
2127
|
*/
|
|
1543
|
-
|
|
1544
|
-
const pagination = qs.stringify({ [options.page]: { number: state.page, size: state.limit } }, { encode: false });
|
|
1545
|
-
const param = `${this._prepend(state)}${pagination}`;
|
|
1546
|
-
this._uri += param;
|
|
1547
|
-
return param;
|
|
1548
|
-
}
|
|
2128
|
+
_requestStrategy;
|
|
1549
2129
|
/**
|
|
1550
|
-
*
|
|
1551
|
-
*
|
|
1552
|
-
* Generates: `sort=-field1,field2` where `-` prefix indicates DESC order
|
|
1553
|
-
*
|
|
1554
|
-
* @param state - The current query builder state
|
|
1555
|
-
* @param options - The query parameter key name configuration
|
|
1556
|
-
* @returns The generated sort parameter string
|
|
2130
|
+
* Internal BehaviorSubject that holds the latest generated URI
|
|
1557
2131
|
*/
|
|
1558
|
-
|
|
1559
|
-
let param = '';
|
|
1560
|
-
if (!state.sorts.length) {
|
|
1561
|
-
return param;
|
|
1562
|
-
}
|
|
1563
|
-
param = `${this._prepend(state)}${options.sort}=`;
|
|
1564
|
-
state.sorts.forEach((sort, idx) => {
|
|
1565
|
-
param += `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`;
|
|
1566
|
-
if (idx < state.sorts.length - 1) {
|
|
1567
|
-
param += ',';
|
|
1568
|
-
}
|
|
1569
|
-
});
|
|
1570
|
-
this._uri += param;
|
|
1571
|
-
return param;
|
|
1572
|
-
}
|
|
2132
|
+
_uri$ = new BehaviorSubject('');
|
|
1573
2133
|
/**
|
|
1574
|
-
*
|
|
1575
|
-
*
|
|
1576
|
-
* Returns the full base path with `?` for the first parameter,
|
|
1577
|
-
* or `&` for subsequent parameters.
|
|
1578
|
-
*
|
|
1579
|
-
* @param state - The current query builder state
|
|
1580
|
-
* @returns The prefix string to prepend to the next parameter
|
|
2134
|
+
* Observable that emits non-empty generated URIs
|
|
1581
2135
|
*/
|
|
1582
|
-
|
|
1583
|
-
|
|
1584
|
-
|
|
1585
|
-
|
|
1586
|
-
|
|
2136
|
+
uri$ = this._uri$.asObservable().pipe(filter(uri => !!uri));
|
|
2137
|
+
constructor(_nestService, requestStrategy, driver, options = new QueryBuilderOptions({})) {
|
|
2138
|
+
this._nestService = _nestService;
|
|
2139
|
+
this._driver = driver;
|
|
2140
|
+
this._options = options;
|
|
2141
|
+
this._requestStrategy = requestStrategy;
|
|
1587
2142
|
}
|
|
1588
|
-
}
|
|
1589
|
-
|
|
1590
|
-
/**
|
|
1591
|
-
* Response strategy for the JSON:API driver
|
|
1592
|
-
*
|
|
1593
|
-
* Parses JSON:API pagination responses:
|
|
1594
|
-
* ```json
|
|
1595
|
-
* {
|
|
1596
|
-
* "data": [...],
|
|
1597
|
-
* "meta": {
|
|
1598
|
-
* "current-page": 1,
|
|
1599
|
-
* "per-page": 10,
|
|
1600
|
-
* "total": 100,
|
|
1601
|
-
* "page-count": 10,
|
|
1602
|
-
* "from": 1,
|
|
1603
|
-
* "to": 10
|
|
1604
|
-
* },
|
|
1605
|
-
* "links": {
|
|
1606
|
-
* "first": "url",
|
|
1607
|
-
* "prev": "url",
|
|
1608
|
-
* "next": "url",
|
|
1609
|
-
* "last": "url"
|
|
1610
|
-
* }
|
|
1611
|
-
* }
|
|
1612
|
-
* ```
|
|
1613
|
-
*
|
|
1614
|
-
* @see https://jsonapi.org/format/
|
|
1615
|
-
*/
|
|
1616
|
-
class JsonApiResponseStrategy {
|
|
1617
2143
|
/**
|
|
1618
|
-
*
|
|
2144
|
+
* Assert that the active strategy declares support for a capability
|
|
1619
2145
|
*
|
|
1620
|
-
*
|
|
1621
|
-
*
|
|
1622
|
-
*
|
|
2146
|
+
* Reads from `IRequestStrategy.capabilities` rather than the driver
|
|
2147
|
+
* enum so adding a new driver only requires declaring its capability
|
|
2148
|
+
* map — this method does not change.
|
|
1623
2149
|
*
|
|
1624
|
-
* @param
|
|
1625
|
-
* @param
|
|
1626
|
-
* @
|
|
2150
|
+
* @param flag - The capability key to check
|
|
2151
|
+
* @param error - The error to throw if the capability is unsupported
|
|
2152
|
+
* @throws The provided error if the active strategy lacks the capability
|
|
1627
2153
|
*/
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
const total = this._resolve(response, options.total);
|
|
1633
|
-
const perPage = this._resolve(response, options.perPage);
|
|
1634
|
-
const lastPage = this._resolve(response, options.lastPage);
|
|
1635
|
-
// Compute from/to if not directly available
|
|
1636
|
-
const from = this._resolveFrom(response, options, currentPage, perPage);
|
|
1637
|
-
const to = this._resolveTo(response, options, currentPage, perPage, total);
|
|
1638
|
-
const prevPageUrl = this._resolve(response, options.prevPageUrl);
|
|
1639
|
-
const nextPageUrl = this._resolve(response, options.nextPageUrl);
|
|
1640
|
-
const firstPageUrl = this._resolve(response, options.firstPageUrl);
|
|
1641
|
-
const lastPageUrl = this._resolve(response, options.lastPageUrl);
|
|
1642
|
-
return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl, nextPageUrl, lastPage, firstPageUrl, lastPageUrl);
|
|
2154
|
+
_assertCapability(flag, error) {
|
|
2155
|
+
if (!this._requestStrategy.capabilities[flag]) {
|
|
2156
|
+
throw error;
|
|
2157
|
+
}
|
|
1643
2158
|
}
|
|
1644
2159
|
/**
|
|
1645
|
-
*
|
|
1646
|
-
*
|
|
1647
|
-
* Supports both flat keys ('data') and nested paths ('meta.current-page').
|
|
2160
|
+
* Add fields to the select statement for the given model (JSON:API and Spatie only)
|
|
1648
2161
|
*
|
|
1649
|
-
* @param
|
|
1650
|
-
* @param
|
|
1651
|
-
* @returns
|
|
2162
|
+
* @param model - Model that holds the fields
|
|
2163
|
+
* @param fields - Fields to select
|
|
2164
|
+
* @returns {this}
|
|
2165
|
+
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
1652
2166
|
*/
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
2167
|
+
addFields(model, fields) {
|
|
2168
|
+
this._assertCapability('fields', new UnsupportedFieldSelectionError());
|
|
2169
|
+
if (!fields.length) {
|
|
2170
|
+
return this;
|
|
2171
|
+
}
|
|
2172
|
+
this._nestService.addFields({ [model]: fields });
|
|
2173
|
+
return this;
|
|
1656
2174
|
}
|
|
1657
2175
|
/**
|
|
1658
|
-
*
|
|
2176
|
+
* Add a filter with the given value(s) (JSON:API, NestJS, PostgREST, and Spatie)
|
|
1659
2177
|
*
|
|
1660
|
-
*
|
|
1661
|
-
* Otherwise, compute it from currentPage and perPage:
|
|
1662
|
-
* `(currentPage - 1) * perPage + 1`
|
|
2178
|
+
* Produces: `filter[field]=value` (JSON:API / Spatie) or `filter.field=value` (NestJS)
|
|
1663
2179
|
*
|
|
1664
|
-
* @param
|
|
1665
|
-
* @param
|
|
1666
|
-
* @
|
|
1667
|
-
* @
|
|
1668
|
-
* @returns The computed "from" index
|
|
2180
|
+
* @param {string} field - Name of the field to filter
|
|
2181
|
+
* @param {(string | number | boolean)[]} values - The needle(s)
|
|
2182
|
+
* @returns {this}
|
|
2183
|
+
* @throws {UnsupportedFilterError} If the active driver does not support filters
|
|
1669
2184
|
*/
|
|
1670
|
-
|
|
1671
|
-
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
return direct;
|
|
1675
|
-
}
|
|
1676
|
-
if (currentPage && perPage) {
|
|
1677
|
-
return (currentPage - 1) * perPage + 1;
|
|
2185
|
+
addFilter(field, ...values) {
|
|
2186
|
+
this._assertCapability('filters', new UnsupportedFilterError());
|
|
2187
|
+
if (!values.length) {
|
|
2188
|
+
return this;
|
|
1678
2189
|
}
|
|
1679
|
-
|
|
2190
|
+
this._nestService.addFilters({
|
|
2191
|
+
[field]: values
|
|
2192
|
+
});
|
|
2193
|
+
this._nestService.page = 1;
|
|
2194
|
+
return this;
|
|
1680
2195
|
}
|
|
1681
2196
|
/**
|
|
1682
|
-
*
|
|
2197
|
+
* Add a filter with an explicit operator (NestJS and PostgREST)
|
|
1683
2198
|
*
|
|
1684
|
-
*
|
|
1685
|
-
* Otherwise, compute it from currentPage, perPage, and total:
|
|
1686
|
-
* `Math.min(currentPage * perPage, total)`
|
|
2199
|
+
* Produces: `filter.field=$operator:value`
|
|
1687
2200
|
*
|
|
1688
|
-
* @param
|
|
1689
|
-
* @param
|
|
1690
|
-
* @param
|
|
1691
|
-
* @
|
|
1692
|
-
* @
|
|
1693
|
-
* @returns The computed "to" index
|
|
2201
|
+
* @param {string} field - Name of the field to filter
|
|
2202
|
+
* @param {FilterOperatorEnum} operator - The filter operator to apply
|
|
2203
|
+
* @param {(string | number | boolean)[]} values - The value(s) for the filter
|
|
2204
|
+
* @returns {this}
|
|
2205
|
+
* @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
|
|
1694
2206
|
*/
|
|
1695
|
-
|
|
1696
|
-
|
|
1697
|
-
|
|
1698
|
-
|
|
1699
|
-
return direct;
|
|
1700
|
-
}
|
|
1701
|
-
if (currentPage && perPage && total) {
|
|
1702
|
-
return Math.min(currentPage * perPage, total);
|
|
2207
|
+
addFilterOperator(field, operator, ...values) {
|
|
2208
|
+
this._assertCapability('operatorFilters', new UnsupportedFilterOperatorError());
|
|
2209
|
+
if (!values.length) {
|
|
2210
|
+
return this;
|
|
1703
2211
|
}
|
|
1704
|
-
|
|
2212
|
+
this._nestService.addOperatorFilters([{ field, operator, values }]);
|
|
2213
|
+
this._nestService.page = 1;
|
|
2214
|
+
return this;
|
|
1705
2215
|
}
|
|
1706
|
-
}
|
|
1707
|
-
|
|
1708
|
-
/**
|
|
1709
|
-
* Request strategy for the Laravel (pagination-only) driver
|
|
1710
|
-
*
|
|
1711
|
-
* Generates simple pagination URIs:
|
|
1712
|
-
* - `/{resource}?limit=N&page=N`
|
|
1713
|
-
*
|
|
1714
|
-
* Filters, sorts, fields, includes, search, and select in state are ignored.
|
|
1715
|
-
*/
|
|
1716
|
-
class LaravelRequestStrategy {
|
|
1717
2216
|
/**
|
|
1718
|
-
*
|
|
2217
|
+
* Add related entities to include in the request (JSON:API and Spatie only)
|
|
1719
2218
|
*
|
|
1720
|
-
* @param
|
|
1721
|
-
* @
|
|
1722
|
-
* @
|
|
1723
|
-
* @throws Error if resource is not set
|
|
2219
|
+
* @param {string[]} models - Models to include
|
|
2220
|
+
* @returns {this}
|
|
2221
|
+
* @throws {UnsupportedIncludesError} If the active driver does not support includes
|
|
1724
2222
|
*/
|
|
1725
|
-
|
|
1726
|
-
|
|
1727
|
-
|
|
2223
|
+
addIncludes(...models) {
|
|
2224
|
+
this._assertCapability('includes', new UnsupportedIncludesError());
|
|
2225
|
+
if (!models.length) {
|
|
2226
|
+
return this;
|
|
1728
2227
|
}
|
|
1729
|
-
|
|
1730
|
-
return
|
|
2228
|
+
this._nestService.addIncludes(models);
|
|
2229
|
+
return this;
|
|
1731
2230
|
}
|
|
1732
2231
|
/**
|
|
1733
|
-
*
|
|
2232
|
+
* Add flat field selection (NestJS and PostgREST)
|
|
1734
2233
|
*
|
|
1735
|
-
*
|
|
1736
|
-
* so only positive integers are accepted.
|
|
2234
|
+
* Produces: `select=col1,col2`
|
|
1737
2235
|
*
|
|
1738
|
-
* @param
|
|
1739
|
-
* @
|
|
2236
|
+
* @param {string[]} fields - Fields to select
|
|
2237
|
+
* @returns {this}
|
|
2238
|
+
* @throws {UnsupportedSelectError} If the active driver does not support flat field selection
|
|
1740
2239
|
*/
|
|
1741
|
-
|
|
1742
|
-
|
|
1743
|
-
|
|
2240
|
+
addSelect(...fields) {
|
|
2241
|
+
this._assertCapability('select', new UnsupportedSelectError());
|
|
2242
|
+
if (!fields.length) {
|
|
2243
|
+
return this;
|
|
1744
2244
|
}
|
|
1745
|
-
|
|
2245
|
+
this._nestService.addSelect(fields);
|
|
2246
|
+
return this;
|
|
1746
2247
|
}
|
|
1747
|
-
}
|
|
1748
|
-
|
|
1749
|
-
/**
|
|
1750
|
-
* Response strategy for the Laravel (pagination-only) driver
|
|
1751
|
-
*
|
|
1752
|
-
* Parses flat Laravel pagination responses:
|
|
1753
|
-
* ```json
|
|
1754
|
-
* {
|
|
1755
|
-
* "data": [...],
|
|
1756
|
-
* "current_page": 1,
|
|
1757
|
-
* "total": 100,
|
|
1758
|
-
* "per_page": 15,
|
|
1759
|
-
* "from": 1,
|
|
1760
|
-
* "to": 15,
|
|
1761
|
-
* ...
|
|
1762
|
-
* }
|
|
1763
|
-
* ```
|
|
1764
|
-
*/
|
|
1765
|
-
class LaravelResponseStrategy {
|
|
1766
2248
|
/**
|
|
1767
|
-
*
|
|
2249
|
+
* Add a field with a sort criteria (JSON:API, NestJS, PostgREST, and Spatie)
|
|
1768
2250
|
*
|
|
1769
|
-
* @param
|
|
1770
|
-
* @param
|
|
1771
|
-
* @returns
|
|
2251
|
+
* @param field - Field to use for sorting
|
|
2252
|
+
* @param {SortEnum} order - A value from the SortEnum enumeration
|
|
2253
|
+
* @returns {this}
|
|
2254
|
+
* @throws {UnsupportedSortError} If the active driver does not support sorts
|
|
1772
2255
|
*/
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
2256
|
+
addSort(field, order) {
|
|
2257
|
+
this._assertCapability('sort', new UnsupportedSortError());
|
|
2258
|
+
this._nestService.addSort({
|
|
2259
|
+
field,
|
|
2260
|
+
order
|
|
2261
|
+
});
|
|
2262
|
+
this._nestService.page = 1;
|
|
2263
|
+
return this;
|
|
1776
2264
|
}
|
|
1777
|
-
}
|
|
1778
|
-
|
|
1779
|
-
/**
|
|
1780
|
-
* Request strategy for the NestJS (nestjs-paginate) driver
|
|
1781
|
-
*
|
|
1782
|
-
* Generates URIs in the NestJS paginate format:
|
|
1783
|
-
* - Simple filters: `filter.field=value`
|
|
1784
|
-
* - Operator filters: `filter.field=$operator:value`
|
|
1785
|
-
* - Sorts: `sortBy=field1:DESC,field2:ASC`
|
|
1786
|
-
* - Select: `select=col1,col2`
|
|
1787
|
-
* - Search: `search=term`
|
|
1788
|
-
* - Pagination: `limit=N&page=N`
|
|
1789
|
-
*
|
|
1790
|
-
* @see https://github.com/ppetzold/nestjs-paginate
|
|
1791
|
-
*/
|
|
1792
|
-
class NestjsRequestStrategy {
|
|
1793
2265
|
/**
|
|
1794
|
-
*
|
|
2266
|
+
* Get the current page number
|
|
2267
|
+
*
|
|
2268
|
+
* @remarks Always safe to call. Thin accessor over the internal state's `page` field.
|
|
2269
|
+
* @returns The current page number
|
|
1795
2270
|
*/
|
|
1796
|
-
|
|
2271
|
+
currentPage() {
|
|
2272
|
+
return this._nestService.nest().page;
|
|
2273
|
+
}
|
|
1797
2274
|
/**
|
|
1798
|
-
*
|
|
2275
|
+
* Delete selected fields for the given models in the current query builder state (JSON:API and Spatie only)
|
|
1799
2276
|
*
|
|
1800
|
-
*
|
|
1801
|
-
*
|
|
1802
|
-
*
|
|
1803
|
-
*
|
|
2277
|
+
* ```
|
|
2278
|
+
* ngQubeeService.deleteFields({
|
|
2279
|
+
* users: ['email', 'password'],
|
|
2280
|
+
* address: ['zipcode']
|
|
2281
|
+
* });
|
|
2282
|
+
* ```
|
|
2283
|
+
*
|
|
2284
|
+
* @param {IFields} fields - Object mapping model names to field arrays to remove
|
|
2285
|
+
* @returns {this}
|
|
2286
|
+
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
1804
2287
|
*/
|
|
1805
|
-
|
|
1806
|
-
|
|
1807
|
-
|
|
1808
|
-
|
|
1809
|
-
this._uri = '';
|
|
1810
|
-
this._parseFilters(state, options);
|
|
1811
|
-
this._parseOperatorFilters(state, options);
|
|
1812
|
-
this._parseSort(state, options);
|
|
1813
|
-
this._parseSelect(state, options);
|
|
1814
|
-
this._parseSearch(state, options);
|
|
1815
|
-
this._parseLimit(state, options);
|
|
1816
|
-
this._parsePage(state, options);
|
|
1817
|
-
return this._uri;
|
|
2288
|
+
deleteFields(fields) {
|
|
2289
|
+
this._assertCapability('fields', new UnsupportedFieldSelectionError());
|
|
2290
|
+
this._nestService.deleteFields(fields);
|
|
2291
|
+
return this;
|
|
1818
2292
|
}
|
|
1819
2293
|
/**
|
|
1820
|
-
*
|
|
2294
|
+
* Delete selected fields for the given model in the current query builder state (JSON:API and Spatie only)
|
|
1821
2295
|
*
|
|
1822
|
-
*
|
|
1823
|
-
*
|
|
2296
|
+
* ```
|
|
2297
|
+
* ngQubeeService.deleteFieldsByModel('users', 'email', 'password');
|
|
2298
|
+
* ```
|
|
1824
2299
|
*
|
|
1825
|
-
* @param
|
|
1826
|
-
* @
|
|
2300
|
+
* @param model - Model that holds the fields
|
|
2301
|
+
* @param {string[]} fields - Fields to delete from the state
|
|
2302
|
+
* @returns {this}
|
|
2303
|
+
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
1827
2304
|
*/
|
|
1828
|
-
|
|
1829
|
-
|
|
1830
|
-
|
|
2305
|
+
deleteFieldsByModel(model, ...fields) {
|
|
2306
|
+
this._assertCapability('fields', new UnsupportedFieldSelectionError());
|
|
2307
|
+
if (!fields.length) {
|
|
2308
|
+
return this;
|
|
1831
2309
|
}
|
|
1832
|
-
|
|
2310
|
+
this._nestService.deleteFields({
|
|
2311
|
+
[model]: fields
|
|
2312
|
+
});
|
|
2313
|
+
return this;
|
|
1833
2314
|
}
|
|
1834
2315
|
/**
|
|
1835
|
-
*
|
|
2316
|
+
* Remove given filters from the query builder state (JSON:API, NestJS, PostgREST, and Spatie)
|
|
1836
2317
|
*
|
|
1837
|
-
*
|
|
1838
|
-
*
|
|
1839
|
-
* @
|
|
1840
|
-
* @param options - The query parameter key name configuration
|
|
2318
|
+
* @param {string[]} filters - Filters to remove
|
|
2319
|
+
* @returns {this}
|
|
2320
|
+
* @throws {UnsupportedFilterError} If the active driver does not support filters
|
|
1841
2321
|
*/
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
if (!
|
|
1845
|
-
return;
|
|
2322
|
+
deleteFilters(...filters) {
|
|
2323
|
+
this._assertCapability('filters', new UnsupportedFilterError());
|
|
2324
|
+
if (!filters.length) {
|
|
2325
|
+
return this;
|
|
1846
2326
|
}
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
this._uri += param;
|
|
1851
|
-
});
|
|
2327
|
+
this._nestService.deleteFilters(...filters);
|
|
2328
|
+
this._nestService.page = 1;
|
|
2329
|
+
return this;
|
|
1852
2330
|
}
|
|
1853
2331
|
/**
|
|
1854
|
-
*
|
|
2332
|
+
* Remove selected related models from the query builder state (JSON:API and Spatie only)
|
|
1855
2333
|
*
|
|
1856
|
-
* @param
|
|
1857
|
-
* @
|
|
2334
|
+
* @param {string[]} includes - Models to remove
|
|
2335
|
+
* @returns {this}
|
|
2336
|
+
* @throws {UnsupportedIncludesError} If the active driver does not support includes
|
|
1858
2337
|
*/
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
2338
|
+
deleteIncludes(...includes) {
|
|
2339
|
+
this._assertCapability('includes', new UnsupportedIncludesError());
|
|
2340
|
+
if (!includes.length) {
|
|
2341
|
+
return this;
|
|
2342
|
+
}
|
|
2343
|
+
this._nestService.deleteIncludes(...includes);
|
|
2344
|
+
return this;
|
|
1862
2345
|
}
|
|
1863
2346
|
/**
|
|
1864
|
-
*
|
|
2347
|
+
* Remove operator filters by field name (NestJS and PostgREST)
|
|
1865
2348
|
*
|
|
1866
|
-
*
|
|
1867
|
-
*
|
|
1868
|
-
*
|
|
1869
|
-
*
|
|
1870
|
-
* @param state - The current query builder state
|
|
1871
|
-
* @param options - The query parameter key name configuration
|
|
2349
|
+
* @param {string[]} fields - Field names of operator filters to remove
|
|
2350
|
+
* @returns {this}
|
|
2351
|
+
* @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
|
|
1872
2352
|
*/
|
|
1873
|
-
|
|
1874
|
-
|
|
1875
|
-
|
|
2353
|
+
deleteOperatorFilters(...fields) {
|
|
2354
|
+
this._assertCapability('operatorFilters', new UnsupportedFilterOperatorError());
|
|
2355
|
+
if (!fields.length) {
|
|
2356
|
+
return this;
|
|
1876
2357
|
}
|
|
1877
|
-
|
|
1878
|
-
|
|
1879
|
-
|
|
1880
|
-
this._uri += param;
|
|
1881
|
-
});
|
|
2358
|
+
this._nestService.deleteOperatorFilters(...fields);
|
|
2359
|
+
this._nestService.page = 1;
|
|
2360
|
+
return this;
|
|
1882
2361
|
}
|
|
1883
2362
|
/**
|
|
1884
|
-
*
|
|
2363
|
+
* Remove search term from the query builder state (NestJS only)
|
|
1885
2364
|
*
|
|
1886
|
-
* @
|
|
1887
|
-
* @
|
|
2365
|
+
* @returns {this}
|
|
2366
|
+
* @throws {UnsupportedSearchError} If the active driver does not support search
|
|
1888
2367
|
*/
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
this.
|
|
2368
|
+
deleteSearch() {
|
|
2369
|
+
this._assertCapability('search', new UnsupportedSearchError());
|
|
2370
|
+
this._nestService.deleteSearch();
|
|
2371
|
+
this._nestService.page = 1;
|
|
2372
|
+
return this;
|
|
1892
2373
|
}
|
|
1893
2374
|
/**
|
|
1894
|
-
*
|
|
2375
|
+
* Remove flat field selections from the query builder state (NestJS and PostgREST)
|
|
1895
2376
|
*
|
|
1896
|
-
*
|
|
1897
|
-
*
|
|
1898
|
-
* @
|
|
1899
|
-
* @param options - The query parameter key name configuration
|
|
2377
|
+
* @param {string[]} fields - Fields to remove from selection
|
|
2378
|
+
* @returns {this}
|
|
2379
|
+
* @throws {UnsupportedSelectError} If the active driver does not support flat field selection
|
|
1900
2380
|
*/
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
2381
|
+
deleteSelect(...fields) {
|
|
2382
|
+
this._assertCapability('select', new UnsupportedSelectError());
|
|
2383
|
+
if (!fields.length) {
|
|
2384
|
+
return this;
|
|
1904
2385
|
}
|
|
1905
|
-
|
|
1906
|
-
this
|
|
2386
|
+
this._nestService.deleteSelect(...fields);
|
|
2387
|
+
return this;
|
|
1907
2388
|
}
|
|
1908
2389
|
/**
|
|
1909
|
-
*
|
|
1910
|
-
*
|
|
1911
|
-
* Generates: `select=col1,col2`
|
|
2390
|
+
* Remove sort rules from the query builder state (JSON:API, NestJS, PostgREST, and Spatie)
|
|
1912
2391
|
*
|
|
1913
|
-
* @param
|
|
1914
|
-
* @
|
|
2392
|
+
* @param sorts - Fields used for sorting to remove
|
|
2393
|
+
* @returns {this}
|
|
2394
|
+
* @throws {UnsupportedSortError} If the active driver does not support sorts
|
|
1915
2395
|
*/
|
|
1916
|
-
|
|
1917
|
-
|
|
1918
|
-
|
|
1919
|
-
|
|
1920
|
-
|
|
1921
|
-
this._uri += param;
|
|
2396
|
+
deleteSorts(...sorts) {
|
|
2397
|
+
this._assertCapability('sort', new UnsupportedSortError());
|
|
2398
|
+
this._nestService.deleteSorts(...sorts);
|
|
2399
|
+
this._nestService.page = 1;
|
|
2400
|
+
return this;
|
|
1922
2401
|
}
|
|
1923
2402
|
/**
|
|
1924
|
-
*
|
|
2403
|
+
* Navigate to the first page (page 1)
|
|
1925
2404
|
*
|
|
1926
|
-
*
|
|
2405
|
+
* @remarks Never throws. Idempotent when already on page 1.
|
|
2406
|
+
* @returns {this}
|
|
2407
|
+
*/
|
|
2408
|
+
firstPage() {
|
|
2409
|
+
this._nestService.page = 1;
|
|
2410
|
+
return this;
|
|
2411
|
+
}
|
|
2412
|
+
/**
|
|
2413
|
+
* Generate a URI accordingly to the given data and active driver
|
|
1927
2414
|
*
|
|
1928
|
-
* @
|
|
1929
|
-
* @param options - The query parameter key name configuration
|
|
2415
|
+
* @returns {Observable<string>} An observable that emits the generated URI
|
|
1930
2416
|
*/
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
2417
|
+
generateUri() {
|
|
2418
|
+
try {
|
|
2419
|
+
this._uri$.next(this._requestStrategy.buildUri(this._nestService.nest(), this._options));
|
|
2420
|
+
return this.uri$;
|
|
2421
|
+
}
|
|
2422
|
+
catch (error) {
|
|
2423
|
+
return throwError(() => error);
|
|
1934
2424
|
}
|
|
1935
|
-
const sortPairs = state.sorts.map(sort => `${sort.field}:${sort.order === SortEnum.DESC ? 'DESC' : 'ASC'}`);
|
|
1936
|
-
const param = `${this._prepend(state)}${options.sortBy}=${sortPairs.join(',')}`;
|
|
1937
|
-
this._uri += param;
|
|
1938
2425
|
}
|
|
1939
2426
|
/**
|
|
1940
|
-
*
|
|
2427
|
+
* Navigate directly to the specified page
|
|
1941
2428
|
*
|
|
1942
|
-
*
|
|
1943
|
-
*
|
|
2429
|
+
* Validates integer/positive via the existing `setPage` path, and
|
|
2430
|
+
* additionally rejects values that exceed `state.lastPage` when
|
|
2431
|
+
* pagination bounds are known.
|
|
1944
2432
|
*
|
|
1945
|
-
* @param
|
|
1946
|
-
* @returns
|
|
2433
|
+
* @param n - Target page number
|
|
2434
|
+
* @returns {this}
|
|
2435
|
+
* @throws {InvalidPageNumberError} If `n` is not a positive integer, or if `n > state.lastPage` when `state.isLastPageKnown` is true
|
|
1947
2436
|
*/
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
2437
|
+
goToPage(n) {
|
|
2438
|
+
const state = this._nestService.nest();
|
|
2439
|
+
if (state.isLastPageKnown && n > state.lastPage) {
|
|
2440
|
+
throw new InvalidPageNumberError(n);
|
|
1951
2441
|
}
|
|
1952
|
-
|
|
2442
|
+
this._nestService.page = n;
|
|
2443
|
+
return this;
|
|
1953
2444
|
}
|
|
1954
|
-
}
|
|
1955
|
-
|
|
1956
|
-
/**
|
|
1957
|
-
* Response strategy for the NestJS (nestjs-paginate) driver
|
|
1958
|
-
*
|
|
1959
|
-
* Parses nested NestJS pagination responses:
|
|
1960
|
-
* ```json
|
|
1961
|
-
* {
|
|
1962
|
-
* "data": [...],
|
|
1963
|
-
* "meta": {
|
|
1964
|
-
* "currentPage": 1,
|
|
1965
|
-
* "totalItems": 100,
|
|
1966
|
-
* "itemsPerPage": 10,
|
|
1967
|
-
* "totalPages": 10
|
|
1968
|
-
* },
|
|
1969
|
-
* "links": {
|
|
1970
|
-
* "first": "url",
|
|
1971
|
-
* "previous": "url",
|
|
1972
|
-
* "next": "url",
|
|
1973
|
-
* "last": "url",
|
|
1974
|
-
* "current": "url"
|
|
1975
|
-
* }
|
|
1976
|
-
* }
|
|
1977
|
-
* ```
|
|
1978
|
-
*
|
|
1979
|
-
* @see https://github.com/ppetzold/nestjs-paginate
|
|
1980
|
-
*/
|
|
1981
|
-
class NestjsResponseStrategy {
|
|
1982
2445
|
/**
|
|
1983
|
-
*
|
|
1984
|
-
*
|
|
1985
|
-
* Supports dot-notation key paths for accessing nested values.
|
|
1986
|
-
* Computes `from` and `to` from `currentPage` and `itemsPerPage` when
|
|
1987
|
-
* they are not directly available in the response.
|
|
2446
|
+
* Check whether a next page exists
|
|
1988
2447
|
*
|
|
1989
|
-
* @
|
|
1990
|
-
* @
|
|
1991
|
-
* @returns A typed PaginatedCollection instance
|
|
2448
|
+
* @remarks Template-safe. Returns `true` when pagination bounds are unknown (conservative default — keeps a "Next" button enabled before the first `paginate()` call).
|
|
2449
|
+
* @returns `true` if `state.page < state.lastPage` when bounds are known, or `true` when bounds are unknown
|
|
1992
2450
|
*/
|
|
1993
|
-
|
|
1994
|
-
|
|
1995
|
-
|
|
1996
|
-
const currentPage = this._resolve(response, options.currentPage);
|
|
1997
|
-
const total = this._resolve(response, options.total);
|
|
1998
|
-
const perPage = this._resolve(response, options.perPage);
|
|
1999
|
-
const lastPage = this._resolve(response, options.lastPage);
|
|
2000
|
-
// Compute from/to if not directly available
|
|
2001
|
-
const from = this._resolveFrom(response, options, currentPage, perPage);
|
|
2002
|
-
const to = this._resolveTo(response, options, currentPage, perPage, total);
|
|
2003
|
-
const prevPageUrl = this._resolve(response, options.prevPageUrl);
|
|
2004
|
-
const nextPageUrl = this._resolve(response, options.nextPageUrl);
|
|
2005
|
-
const firstPageUrl = this._resolve(response, options.firstPageUrl);
|
|
2006
|
-
const lastPageUrl = this._resolve(response, options.lastPageUrl);
|
|
2007
|
-
return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl, nextPageUrl, lastPage, firstPageUrl, lastPageUrl);
|
|
2451
|
+
hasNextPage() {
|
|
2452
|
+
const state = this._nestService.nest();
|
|
2453
|
+
return !state.isLastPageKnown || state.page < state.lastPage;
|
|
2008
2454
|
}
|
|
2009
2455
|
/**
|
|
2010
|
-
*
|
|
2011
|
-
*
|
|
2012
|
-
* Supports both flat keys ('data') and nested paths ('meta.currentPage').
|
|
2456
|
+
* Check whether a previous page exists
|
|
2013
2457
|
*
|
|
2014
|
-
* @
|
|
2015
|
-
* @
|
|
2016
|
-
* @returns The resolved value, or undefined if not found
|
|
2458
|
+
* @remarks Always safe. Does not require a synced paginated response.
|
|
2459
|
+
* @returns `true` if `state.page > 1`
|
|
2017
2460
|
*/
|
|
2018
|
-
|
|
2019
|
-
|
|
2020
|
-
return path.split('.').reduce((obj, key) => obj?.[key], response);
|
|
2461
|
+
hasPreviousPage() {
|
|
2462
|
+
return this._nestService.nest().page > 1;
|
|
2021
2463
|
}
|
|
2022
2464
|
/**
|
|
2023
|
-
*
|
|
2024
|
-
*
|
|
2025
|
-
* If the path resolves to a value in the response, use it.
|
|
2026
|
-
* Otherwise, compute it from currentPage and perPage:
|
|
2027
|
-
* `(currentPage - 1) * perPage + 1`
|
|
2465
|
+
* Check whether the current page is the first page
|
|
2028
2466
|
*
|
|
2029
|
-
* @
|
|
2030
|
-
* @
|
|
2031
|
-
* @param currentPage - The current page number
|
|
2032
|
-
* @param perPage - The number of items per page
|
|
2033
|
-
* @returns The computed "from" index
|
|
2467
|
+
* @remarks Always safe. Does not require a synced paginated response.
|
|
2468
|
+
* @returns `true` if `state.page === 1`
|
|
2034
2469
|
*/
|
|
2035
|
-
|
|
2036
|
-
|
|
2037
|
-
const direct = this._resolve(response, options.from);
|
|
2038
|
-
if (direct !== undefined) {
|
|
2039
|
-
return direct;
|
|
2040
|
-
}
|
|
2041
|
-
if (currentPage && perPage) {
|
|
2042
|
-
return (currentPage - 1) * perPage + 1;
|
|
2043
|
-
}
|
|
2044
|
-
return undefined;
|
|
2470
|
+
isFirstPage() {
|
|
2471
|
+
return this._nestService.nest().page === 1;
|
|
2045
2472
|
}
|
|
2046
2473
|
/**
|
|
2047
|
-
*
|
|
2048
|
-
*
|
|
2049
|
-
* If the path resolves to a value in the response, use it.
|
|
2050
|
-
* Otherwise, compute it from currentPage, perPage, and total:
|
|
2051
|
-
* `Math.min(currentPage * perPage, total)`
|
|
2474
|
+
* Check whether the current page is the last page
|
|
2052
2475
|
*
|
|
2053
|
-
* @
|
|
2054
|
-
* @
|
|
2055
|
-
* @param currentPage - The current page number
|
|
2056
|
-
* @param perPage - The number of items per page
|
|
2057
|
-
* @param total - The total number of items
|
|
2058
|
-
* @returns The computed "to" index
|
|
2476
|
+
* @remarks Template-safe. Returns `false` when pagination bounds are unknown (no paginated response has been synced yet) — keeps "Next" navigation unblocked until the first `paginate()` call syncs.
|
|
2477
|
+
* @returns `true` only when `state.isLastPageKnown` and `state.page === state.lastPage`
|
|
2059
2478
|
*/
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
if (direct !== undefined) {
|
|
2064
|
-
return direct;
|
|
2065
|
-
}
|
|
2066
|
-
if (currentPage && perPage && total) {
|
|
2067
|
-
return Math.min(currentPage * perPage, total);
|
|
2068
|
-
}
|
|
2069
|
-
return undefined;
|
|
2479
|
+
isLastPage() {
|
|
2480
|
+
const state = this._nestService.nest();
|
|
2481
|
+
return state.isLastPageKnown && state.page === state.lastPage;
|
|
2070
2482
|
}
|
|
2071
|
-
}
|
|
2072
|
-
|
|
2073
|
-
/**
|
|
2074
|
-
* Request strategy for the Spatie Query Builder driver
|
|
2075
|
-
*
|
|
2076
|
-
* Generates URIs in the Spatie format:
|
|
2077
|
-
* - Fields: `fields[model]=col1,col2`
|
|
2078
|
-
* - Filters: `filter[field]=value`
|
|
2079
|
-
* - Includes: `include=model1,model2`
|
|
2080
|
-
* - Sorts: `sort=-field1,field2` (- prefix = DESC)
|
|
2081
|
-
* - Pagination: `limit=N&page=N`
|
|
2082
|
-
*
|
|
2083
|
-
* @see https://spatie.be/docs/laravel-query-builder
|
|
2084
|
-
*/
|
|
2085
|
-
class SpatieRequestStrategy {
|
|
2086
2483
|
/**
|
|
2087
|
-
*
|
|
2484
|
+
* Navigate to the last page known from the most recent paginated response
|
|
2485
|
+
*
|
|
2486
|
+
* @remarks Requires at least one `PaginationService.paginate()` call to have synced `state.lastPage`. Before that, the bound is unknown and this method throws.
|
|
2487
|
+
* @returns {this}
|
|
2488
|
+
* @throws {PaginationNotSyncedError} If `state.isLastPageKnown` is false (no paginated response has been synced yet)
|
|
2088
2489
|
*/
|
|
2089
|
-
|
|
2490
|
+
lastPage() {
|
|
2491
|
+
const state = this._nestService.nest();
|
|
2492
|
+
if (!state.isLastPageKnown) {
|
|
2493
|
+
throw new PaginationNotSyncedError('navigate to last page');
|
|
2494
|
+
}
|
|
2495
|
+
this._nestService.page = state.lastPage;
|
|
2496
|
+
return this;
|
|
2497
|
+
}
|
|
2090
2498
|
/**
|
|
2091
|
-
*
|
|
2499
|
+
* Navigate to the next page
|
|
2092
2500
|
*
|
|
2093
|
-
* @
|
|
2094
|
-
* @
|
|
2095
|
-
* @returns The composed URI string
|
|
2096
|
-
* @throws Error if resource is not set
|
|
2501
|
+
* @remarks Never throws. Idempotent at the known last page (no-op). Pair with `hasNextPage()` for a disable-state binding.
|
|
2502
|
+
* @returns {this}
|
|
2097
2503
|
*/
|
|
2098
|
-
|
|
2099
|
-
|
|
2100
|
-
|
|
2504
|
+
nextPage() {
|
|
2505
|
+
const state = this._nestService.nest();
|
|
2506
|
+
if (state.isLastPageKnown && state.page >= state.lastPage) {
|
|
2507
|
+
return this;
|
|
2101
2508
|
}
|
|
2102
|
-
this.
|
|
2103
|
-
this
|
|
2104
|
-
this._parseFields(state, options);
|
|
2105
|
-
this._parseFilters(state, options);
|
|
2106
|
-
this._parseLimit(state, options);
|
|
2107
|
-
this._parsePage(state, options);
|
|
2108
|
-
this._parseSort(state, options);
|
|
2109
|
-
return this._uri;
|
|
2509
|
+
this._nestService.page = state.page + 1;
|
|
2510
|
+
return this;
|
|
2110
2511
|
}
|
|
2111
2512
|
/**
|
|
2112
|
-
*
|
|
2513
|
+
* HTTP request headers the active driver wants the consumer to apply
|
|
2113
2514
|
*
|
|
2114
|
-
*
|
|
2115
|
-
*
|
|
2515
|
+
* Returns `null` for drivers that pass all pagination metadata on the
|
|
2516
|
+
* URL (Laravel, Spatie, JSON:API, NestJS, and PostgREST in its default
|
|
2517
|
+
* QUERY mode). Returns a map of header name → value when the active
|
|
2518
|
+
* driver uses HTTP headers instead — today, only the PostgREST driver
|
|
2519
|
+
* configured with `PaginationModeEnum.RANGE`, which yields
|
|
2520
|
+
* `{ 'Range-Unit': 'items', 'Range': 'from-to' }`.
|
|
2116
2521
|
*
|
|
2117
|
-
* @
|
|
2118
|
-
* @throws {InvalidLimitError} If the value is not a positive integer
|
|
2522
|
+
* @returns Map of headers to apply to the HTTP request, or `null` when not needed
|
|
2119
2523
|
*/
|
|
2120
|
-
|
|
2121
|
-
if (
|
|
2122
|
-
return;
|
|
2524
|
+
paginationHeaders() {
|
|
2525
|
+
if (typeof this._requestStrategy.buildPaginationHeaders !== 'function') {
|
|
2526
|
+
return null;
|
|
2123
2527
|
}
|
|
2124
|
-
|
|
2528
|
+
return this._requestStrategy.buildPaginationHeaders(this._nestService.nest());
|
|
2125
2529
|
}
|
|
2126
2530
|
/**
|
|
2127
|
-
*
|
|
2128
|
-
*
|
|
2129
|
-
* Validates that each field model exists either as the main resource
|
|
2130
|
-
* or in the includes list. Fields are grouped by model in bracket notation.
|
|
2531
|
+
* Navigate to the previous page
|
|
2131
2532
|
*
|
|
2132
|
-
* @
|
|
2133
|
-
* @
|
|
2134
|
-
* @returns The generated field selection parameter string
|
|
2135
|
-
* @throws Error if resource is required but not set
|
|
2136
|
-
* @throws UnselectableModelError if a field model is not in resource or includes
|
|
2533
|
+
* @remarks Never throws. Idempotent at page 1 (floored). Pair with `hasPreviousPage()` for a disable-state binding.
|
|
2534
|
+
* @returns {this}
|
|
2137
2535
|
*/
|
|
2138
|
-
|
|
2139
|
-
|
|
2140
|
-
|
|
2141
|
-
|
|
2142
|
-
if (!state.resource) {
|
|
2143
|
-
throw new Error('While selecting fields, the -> resource <- is required');
|
|
2144
|
-
}
|
|
2145
|
-
if (!(state.resource in state.fields)) {
|
|
2146
|
-
throw new Error(`Key ${state.resource} is missing in the fields object`);
|
|
2147
|
-
}
|
|
2148
|
-
const f = {};
|
|
2149
|
-
for (const k in state.fields) {
|
|
2150
|
-
if (state.fields.hasOwnProperty(k)) {
|
|
2151
|
-
if (k !== state.resource && !state.includes.includes(k)) {
|
|
2152
|
-
throw new UnselectableModelError(k);
|
|
2153
|
-
}
|
|
2154
|
-
Object.assign(f, { [`${options.fields}[${k}]`]: state.fields[k].join(',') });
|
|
2155
|
-
}
|
|
2536
|
+
previousPage() {
|
|
2537
|
+
const state = this._nestService.nest();
|
|
2538
|
+
if (state.page <= 1) {
|
|
2539
|
+
return this;
|
|
2156
2540
|
}
|
|
2157
|
-
|
|
2158
|
-
this
|
|
2159
|
-
return param;
|
|
2541
|
+
this._nestService.page = state.page - 1;
|
|
2542
|
+
return this;
|
|
2160
2543
|
}
|
|
2161
2544
|
/**
|
|
2162
|
-
*
|
|
2545
|
+
* Clear the current state and reset the Query Builder to a fresh, clean condition
|
|
2163
2546
|
*
|
|
2164
|
-
*
|
|
2547
|
+
* @returns {this}
|
|
2548
|
+
*/
|
|
2549
|
+
reset() {
|
|
2550
|
+
this._nestService.reset();
|
|
2551
|
+
return this;
|
|
2552
|
+
}
|
|
2553
|
+
/**
|
|
2554
|
+
* Set the base URL to use for composing the address
|
|
2165
2555
|
*
|
|
2166
|
-
* @param
|
|
2167
|
-
* @
|
|
2168
|
-
* @returns The generated filter parameter string
|
|
2556
|
+
* @param {string} baseUrl - The base URL
|
|
2557
|
+
* @returns {this}
|
|
2169
2558
|
*/
|
|
2170
|
-
|
|
2171
|
-
|
|
2172
|
-
|
|
2173
|
-
return this._uri;
|
|
2174
|
-
}
|
|
2175
|
-
const f = {
|
|
2176
|
-
[`${options.filters}`]: keys.reduce((acc, key) => {
|
|
2177
|
-
return Object.assign(acc, { [key]: state.filters[key].join(',') });
|
|
2178
|
-
}, {})
|
|
2179
|
-
};
|
|
2180
|
-
const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
|
|
2181
|
-
this._uri += param;
|
|
2182
|
-
return param;
|
|
2559
|
+
setBaseUrl(baseUrl) {
|
|
2560
|
+
this._nestService.baseUrl = baseUrl;
|
|
2561
|
+
return this;
|
|
2183
2562
|
}
|
|
2184
2563
|
/**
|
|
2185
|
-
*
|
|
2564
|
+
* Set the items per page number
|
|
2186
2565
|
*
|
|
2187
|
-
*
|
|
2566
|
+
* Validation is delegated to the active request strategy because the
|
|
2567
|
+
* accepted range is driver-specific: nestjs-paginate additionally accepts
|
|
2568
|
+
* `-1` as a "fetch all" sentinel, while Laravel, Spatie, and JSON:API
|
|
2569
|
+
* require a positive integer.
|
|
2188
2570
|
*
|
|
2189
|
-
* @param
|
|
2190
|
-
* @
|
|
2191
|
-
* @
|
|
2571
|
+
* @param limit - Number of items per page (or `-1` to fetch all, NestJS only)
|
|
2572
|
+
* @returns {this}
|
|
2573
|
+
* @throws {import('../errors/invalid-limit.error').InvalidLimitError} If the value is not accepted by the active driver
|
|
2192
2574
|
*/
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
this._uri += param;
|
|
2199
|
-
return param;
|
|
2575
|
+
setLimit(limit) {
|
|
2576
|
+
this._requestStrategy.validateLimit(limit);
|
|
2577
|
+
this._nestService.limit = limit;
|
|
2578
|
+
this._nestService.page = 1;
|
|
2579
|
+
return this;
|
|
2200
2580
|
}
|
|
2201
2581
|
/**
|
|
2202
|
-
*
|
|
2582
|
+
* Set the page that the backend will use to paginate the result set
|
|
2203
2583
|
*
|
|
2204
|
-
* @param
|
|
2205
|
-
* @
|
|
2206
|
-
* @returns The generated limit parameter string
|
|
2584
|
+
* @param page - Page number
|
|
2585
|
+
* @returns {this}
|
|
2207
2586
|
*/
|
|
2208
|
-
|
|
2209
|
-
|
|
2210
|
-
this
|
|
2211
|
-
return param;
|
|
2587
|
+
setPage(page) {
|
|
2588
|
+
this._nestService.page = page;
|
|
2589
|
+
return this;
|
|
2212
2590
|
}
|
|
2213
2591
|
/**
|
|
2214
|
-
*
|
|
2592
|
+
* Set the API resource to run the query against
|
|
2215
2593
|
*
|
|
2216
|
-
* @param
|
|
2217
|
-
* @
|
|
2218
|
-
* @returns The generated page parameter string
|
|
2594
|
+
* @param {string} resource - Resource name (e.g. 'users' produces /users)
|
|
2595
|
+
* @returns {this}
|
|
2219
2596
|
*/
|
|
2220
|
-
|
|
2221
|
-
|
|
2222
|
-
this.
|
|
2223
|
-
return
|
|
2597
|
+
setResource(resource) {
|
|
2598
|
+
this._nestService.resource = resource;
|
|
2599
|
+
this._nestService.page = 1;
|
|
2600
|
+
return this;
|
|
2224
2601
|
}
|
|
2225
2602
|
/**
|
|
2226
|
-
*
|
|
2603
|
+
* Set the search term for full-text search (NestJS only)
|
|
2227
2604
|
*
|
|
2228
|
-
*
|
|
2605
|
+
* Produces: `search=term`
|
|
2229
2606
|
*
|
|
2230
|
-
* @param
|
|
2231
|
-
* @
|
|
2232
|
-
* @
|
|
2607
|
+
* @param {string} search - The search term
|
|
2608
|
+
* @returns {this}
|
|
2609
|
+
* @throws {UnsupportedSearchError} If the active driver does not support search
|
|
2233
2610
|
*/
|
|
2234
|
-
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2238
|
-
|
|
2239
|
-
param = `${this._prepend(state)}${options.sort}=`;
|
|
2240
|
-
state.sorts.forEach((sort, idx) => {
|
|
2241
|
-
param += `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`;
|
|
2242
|
-
if (idx < state.sorts.length - 1) {
|
|
2243
|
-
param += ',';
|
|
2244
|
-
}
|
|
2245
|
-
});
|
|
2246
|
-
this._uri += param;
|
|
2247
|
-
return param;
|
|
2611
|
+
setSearch(search) {
|
|
2612
|
+
this._assertCapability('search', new UnsupportedSearchError());
|
|
2613
|
+
this._nestService.setSearch(search);
|
|
2614
|
+
this._nestService.page = 1;
|
|
2615
|
+
return this;
|
|
2248
2616
|
}
|
|
2249
2617
|
/**
|
|
2250
|
-
*
|
|
2251
|
-
*
|
|
2252
|
-
* Returns the full base path with `?` for the first parameter,
|
|
2253
|
-
* or `&` for subsequent parameters.
|
|
2618
|
+
* Get the total number of pages reported by the most recent paginated response
|
|
2254
2619
|
*
|
|
2255
|
-
* @
|
|
2256
|
-
* @returns The
|
|
2620
|
+
* @remarks Throws when called before any `paginate()` has synced a value. For a non-throwing read in a template, read `nest().isLastPageKnown` first as a guard.
|
|
2621
|
+
* @returns The last page number
|
|
2622
|
+
* @throws {PaginationNotSyncedError} If `state.isLastPageKnown` is false (no paginated response has been synced yet)
|
|
2257
2623
|
*/
|
|
2258
|
-
|
|
2259
|
-
|
|
2260
|
-
|
|
2624
|
+
totalPages() {
|
|
2625
|
+
const state = this._nestService.nest();
|
|
2626
|
+
if (!state.isLastPageKnown) {
|
|
2627
|
+
throw new PaginationNotSyncedError('read totalPages');
|
|
2261
2628
|
}
|
|
2262
|
-
return state.
|
|
2629
|
+
return state.lastPage;
|
|
2263
2630
|
}
|
|
2631
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService, deps: [{ token: NestService }, { token: NG_QUBEE_REQUEST_STRATEGY }, { token: NG_QUBEE_DRIVER }, { token: NG_QUBEE_REQUEST_OPTIONS }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2632
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService });
|
|
2264
2633
|
}
|
|
2634
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeService, decorators: [{
|
|
2635
|
+
type: Injectable
|
|
2636
|
+
}], ctorParameters: () => [{ type: NestService }, { type: undefined, decorators: [{
|
|
2637
|
+
type: Inject,
|
|
2638
|
+
args: [NG_QUBEE_REQUEST_STRATEGY]
|
|
2639
|
+
}] }, { type: DriverEnum, decorators: [{
|
|
2640
|
+
type: Inject,
|
|
2641
|
+
args: [NG_QUBEE_DRIVER]
|
|
2642
|
+
}] }, { type: QueryBuilderOptions, decorators: [{
|
|
2643
|
+
type: Inject,
|
|
2644
|
+
args: [NG_QUBEE_REQUEST_OPTIONS]
|
|
2645
|
+
}] }] });
|
|
2265
2646
|
|
|
2266
|
-
|
|
2267
|
-
* Response strategy for the Spatie Query Builder driver
|
|
2268
|
-
*
|
|
2269
|
-
* Parses flat Laravel pagination responses:
|
|
2270
|
-
* ```json
|
|
2271
|
-
* {
|
|
2272
|
-
* "data": [...],
|
|
2273
|
-
* "current_page": 1,
|
|
2274
|
-
* "total": 100,
|
|
2275
|
-
* "per_page": 15,
|
|
2276
|
-
* "from": 1,
|
|
2277
|
-
* "to": 15,
|
|
2278
|
-
* ...
|
|
2279
|
-
* }
|
|
2280
|
-
* ```
|
|
2281
|
-
*
|
|
2282
|
-
* @see https://spatie.be/docs/laravel-query-builder
|
|
2283
|
-
*/
|
|
2284
|
-
class SpatieResponseStrategy {
|
|
2647
|
+
class PaginationService {
|
|
2285
2648
|
/**
|
|
2286
|
-
*
|
|
2649
|
+
* The NestService instance that owns the query-builder state for this
|
|
2650
|
+
* PaginationService's scope (environment-level by default, or
|
|
2651
|
+
* component-level when used via `provideNgQubeeInstance()`)
|
|
2652
|
+
*/
|
|
2653
|
+
_nestService;
|
|
2654
|
+
/**
|
|
2655
|
+
* Resolved response key name options
|
|
2656
|
+
*/
|
|
2657
|
+
_options;
|
|
2658
|
+
/**
|
|
2659
|
+
* The response strategy that parses responses for the active driver
|
|
2660
|
+
*/
|
|
2661
|
+
_responseStrategy;
|
|
2662
|
+
constructor(nestService, responseStrategy, options = new ResponseOptions({})) {
|
|
2663
|
+
this._nestService = nestService;
|
|
2664
|
+
this._options = options;
|
|
2665
|
+
this._responseStrategy = responseStrategy;
|
|
2666
|
+
}
|
|
2667
|
+
/**
|
|
2668
|
+
* Transform a raw API response into a typed PaginatedCollection
|
|
2287
2669
|
*
|
|
2288
|
-
*
|
|
2289
|
-
*
|
|
2670
|
+
* Delegates to the active driver's response strategy for parsing, then
|
|
2671
|
+
* auto-syncs the parsed `page` and `lastPage` back into `NestService`
|
|
2672
|
+
* so pagination navigation helpers on `NgQubeeService` can operate
|
|
2673
|
+
* against the live server-reported bounds without consumer bookkeeping.
|
|
2674
|
+
*
|
|
2675
|
+
* @remarks
|
|
2676
|
+
* `lastPage` is only synced when the response yields a positive integer.
|
|
2677
|
+
* Server-emitted `0` (empty collection edge case) and absent fields are
|
|
2678
|
+
* treated as "no useful info" and leave `isLastPageKnown: false`.
|
|
2679
|
+
*
|
|
2680
|
+
* @param response - The raw API response body. For drivers that emit a
|
|
2681
|
+
* bare array (PostgREST), pass the array.
|
|
2682
|
+
* @param headers - Optional HTTP response headers. Required by the
|
|
2683
|
+
* PostgREST driver (reads `Content-Range` for pagination metadata);
|
|
2684
|
+
* body-only drivers ignore it. Accepts Angular's `HttpHeaders`, the
|
|
2685
|
+
* native `Headers` class, or a plain `Record<string, string>`.
|
|
2290
2686
|
* @returns A typed PaginatedCollection instance
|
|
2291
2687
|
*/
|
|
2292
2688
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2293
|
-
paginate(response,
|
|
2294
|
-
|
|
2689
|
+
paginate(response, headers) {
|
|
2690
|
+
const collection = this._responseStrategy.paginate(response, this._options, headers);
|
|
2691
|
+
this._nestService.page = collection.page;
|
|
2692
|
+
if (typeof collection.lastPage === 'number' && Number.isInteger(collection.lastPage) && collection.lastPage > 0) {
|
|
2693
|
+
this._nestService.syncLastPage(collection.lastPage);
|
|
2694
|
+
}
|
|
2695
|
+
return collection;
|
|
2295
2696
|
}
|
|
2697
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService, deps: [{ token: NestService }, { token: NG_QUBEE_RESPONSE_STRATEGY }, { token: NG_QUBEE_RESPONSE_OPTIONS }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
2698
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService });
|
|
2296
2699
|
}
|
|
2700
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: PaginationService, decorators: [{
|
|
2701
|
+
type: Injectable
|
|
2702
|
+
}], ctorParameters: () => [{ type: NestService }, { type: undefined, decorators: [{
|
|
2703
|
+
type: Inject,
|
|
2704
|
+
args: [NG_QUBEE_RESPONSE_STRATEGY]
|
|
2705
|
+
}] }, { type: ResponseOptions, decorators: [{
|
|
2706
|
+
type: Inject,
|
|
2707
|
+
args: [NG_QUBEE_RESPONSE_OPTIONS]
|
|
2708
|
+
}] }] });
|
|
2297
2709
|
|
|
2298
|
-
/**
|
|
2299
|
-
* Resolve the request strategy instance for the given driver
|
|
2300
|
-
*
|
|
2301
|
-
* @param driver - The pagination driver
|
|
2302
|
-
* @returns The corresponding request strategy
|
|
2303
|
-
*/
|
|
2304
|
-
function resolveRequestStrategy(driver) {
|
|
2305
|
-
switch (driver) {
|
|
2306
|
-
case DriverEnum.JSON_API:
|
|
2307
|
-
return new JsonApiRequestStrategy();
|
|
2308
|
-
case DriverEnum.NESTJS:
|
|
2309
|
-
return new NestjsRequestStrategy();
|
|
2310
|
-
case DriverEnum.SPATIE:
|
|
2311
|
-
return new SpatieRequestStrategy();
|
|
2312
|
-
case DriverEnum.LARAVEL:
|
|
2313
|
-
return new LaravelRequestStrategy();
|
|
2314
|
-
}
|
|
2315
|
-
}
|
|
2316
|
-
/**
|
|
2317
|
-
* Resolve the response strategy instance for the given driver
|
|
2318
|
-
*
|
|
2319
|
-
* @param driver - The pagination driver
|
|
2320
|
-
* @returns The corresponding response strategy
|
|
2321
|
-
*/
|
|
2322
|
-
function resolveResponseStrategy(driver) {
|
|
2323
|
-
switch (driver) {
|
|
2324
|
-
case DriverEnum.JSON_API:
|
|
2325
|
-
return new JsonApiResponseStrategy();
|
|
2326
|
-
case DriverEnum.NESTJS:
|
|
2327
|
-
return new NestjsResponseStrategy();
|
|
2328
|
-
case DriverEnum.SPATIE:
|
|
2329
|
-
return new SpatieResponseStrategy();
|
|
2330
|
-
case DriverEnum.LARAVEL:
|
|
2331
|
-
return new LaravelResponseStrategy();
|
|
2332
|
-
}
|
|
2333
|
-
}
|
|
2334
|
-
/**
|
|
2335
|
-
* Resolve the driver-specific `ResponseOptions` instance
|
|
2336
|
-
*
|
|
2337
|
-
* @param driver - The pagination driver
|
|
2338
|
-
* @param responseConfig - User-supplied response key overrides
|
|
2339
|
-
* @returns A pre-built ResponseOptions (or driver-specific subclass)
|
|
2340
|
-
*/
|
|
2341
|
-
function resolveResponseOptions(driver, responseConfig) {
|
|
2342
|
-
if (driver === DriverEnum.JSON_API) {
|
|
2343
|
-
return new JsonApiResponseOptions(responseConfig);
|
|
2344
|
-
}
|
|
2345
|
-
if (driver === DriverEnum.NESTJS) {
|
|
2346
|
-
return new NestjsResponseOptions(responseConfig);
|
|
2347
|
-
}
|
|
2348
|
-
return new ResponseOptions(responseConfig);
|
|
2349
|
-
}
|
|
2350
2710
|
/**
|
|
2351
2711
|
* Build the core provider list shared by `provideNgQubee()` and
|
|
2352
2712
|
* `NgQubeeModule.forRoot()`
|
|
2353
2713
|
*
|
|
2714
|
+
* Looks up the driver definition from the registry and calls its three
|
|
2715
|
+
* factories — request strategy, response strategy, response options.
|
|
2716
|
+
* Adding a driver means adding one entry to `DRIVERS`; this function
|
|
2717
|
+
* does not change.
|
|
2718
|
+
*
|
|
2354
2719
|
* Exposes the driver, strategies, and options via injection tokens so that
|
|
2355
2720
|
* consumers can request a component-scoped instance of the services through
|
|
2356
2721
|
* `provideNgQubeeInstance()`.
|
|
@@ -2360,13 +2725,15 @@ function resolveResponseOptions(driver, responseConfig) {
|
|
|
2360
2725
|
*/
|
|
2361
2726
|
function buildNgQubeeProviders(config) {
|
|
2362
2727
|
const driver = config.driver;
|
|
2728
|
+
const paginationMode = config.pagination ?? PaginationModeEnum.QUERY;
|
|
2729
|
+
const definition = DRIVERS[driver];
|
|
2363
2730
|
const requestOptions = new QueryBuilderOptions(Object.assign({}, config.request));
|
|
2364
|
-
const responseOptions =
|
|
2731
|
+
const responseOptions = definition.createResponseOptions(Object.assign({}, config.response));
|
|
2365
2732
|
return [
|
|
2366
2733
|
{ provide: NG_QUBEE_DRIVER, useValue: driver },
|
|
2367
|
-
{ provide: NG_QUBEE_REQUEST_STRATEGY, useValue:
|
|
2734
|
+
{ provide: NG_QUBEE_REQUEST_STRATEGY, useValue: definition.createRequestStrategy(paginationMode) },
|
|
2368
2735
|
{ provide: NG_QUBEE_REQUEST_OPTIONS, useValue: requestOptions },
|
|
2369
|
-
{ provide: NG_QUBEE_RESPONSE_STRATEGY, useValue:
|
|
2736
|
+
{ provide: NG_QUBEE_RESPONSE_STRATEGY, useValue: definition.createResponseStrategy() },
|
|
2370
2737
|
{ provide: NG_QUBEE_RESPONSE_OPTIONS, useValue: responseOptions },
|
|
2371
2738
|
NestService,
|
|
2372
2739
|
NgQubeeService,
|
|
@@ -2473,30 +2840,6 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImpor
|
|
|
2473
2840
|
args: [{}]
|
|
2474
2841
|
}] });
|
|
2475
2842
|
|
|
2476
|
-
/**
|
|
2477
|
-
* Enum representing the available filter operators for the NestJS driver
|
|
2478
|
-
*
|
|
2479
|
-
* These operators map to the nestjs-paginate filter syntax:
|
|
2480
|
-
* `filter.field=$operator:value`
|
|
2481
|
-
*
|
|
2482
|
-
* @see https://github.com/ppetzold/nestjs-paginate
|
|
2483
|
-
*/
|
|
2484
|
-
var FilterOperatorEnum;
|
|
2485
|
-
(function (FilterOperatorEnum) {
|
|
2486
|
-
FilterOperatorEnum["BTW"] = "$btw";
|
|
2487
|
-
FilterOperatorEnum["CONTAINS"] = "$contains";
|
|
2488
|
-
FilterOperatorEnum["EQ"] = "$eq";
|
|
2489
|
-
FilterOperatorEnum["GT"] = "$gt";
|
|
2490
|
-
FilterOperatorEnum["GTE"] = "$gte";
|
|
2491
|
-
FilterOperatorEnum["ILIKE"] = "$ilike";
|
|
2492
|
-
FilterOperatorEnum["IN"] = "$in";
|
|
2493
|
-
FilterOperatorEnum["LT"] = "$lt";
|
|
2494
|
-
FilterOperatorEnum["LTE"] = "$lte";
|
|
2495
|
-
FilterOperatorEnum["NOT"] = "$not";
|
|
2496
|
-
FilterOperatorEnum["NULL"] = "$null";
|
|
2497
|
-
FilterOperatorEnum["SW"] = "$sw";
|
|
2498
|
-
})(FilterOperatorEnum || (FilterOperatorEnum = {}));
|
|
2499
|
-
|
|
2500
2843
|
/*
|
|
2501
2844
|
* Public API Surface of angular-query-builder
|
|
2502
2845
|
*/
|
|
@@ -2505,5 +2848,5 @@ var FilterOperatorEnum;
|
|
|
2505
2848
|
* Generated bundle index. Do not edit.
|
|
2506
2849
|
*/
|
|
2507
2850
|
|
|
2508
|
-
export { DriverEnum, FilterOperatorEnum, InvalidLimitError, InvalidPageNumberError, InvalidResourceNameError, JsonApiRequestStrategy, JsonApiResponseStrategy, KeyNotFoundError, LaravelRequestStrategy, LaravelResponseStrategy, NG_QUBEE_DRIVER, NG_QUBEE_REQUEST_OPTIONS, NG_QUBEE_REQUEST_STRATEGY, NG_QUBEE_RESPONSE_OPTIONS, NG_QUBEE_RESPONSE_STRATEGY, NestjsRequestStrategy, NestjsResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationNotSyncedError, PaginationService, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, buildNgQubeeProviders, provideNgQubee, provideNgQubeeInstance };
|
|
2851
|
+
export { DriverEnum, FilterOperatorEnum, InvalidFilterOperatorValueError, InvalidLimitError, InvalidPageNumberError, InvalidResourceNameError, JsonApiRequestStrategy, JsonApiResponseStrategy, KeyNotFoundError, LaravelRequestStrategy, LaravelResponseStrategy, NG_QUBEE_DRIVER, NG_QUBEE_REQUEST_OPTIONS, NG_QUBEE_REQUEST_STRATEGY, NG_QUBEE_RESPONSE_OPTIONS, NG_QUBEE_RESPONSE_STRATEGY, NestjsRequestStrategy, NestjsResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationModeEnum, PaginationNotSyncedError, PaginationService, PostgrestRequestStrategy, PostgrestResponseStrategy, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, buildNgQubeeProviders, provideNgQubee, provideNgQubeeInstance, readHeader };
|
|
2509
2852
|
//# sourceMappingURL=ng-qubee.mjs.map
|