ng-qubee 2.1.0 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +378 -54
- package/fesm2022/ng-qubee.mjs +1791 -419
- package/fesm2022/ng-qubee.mjs.map +1 -1
- package/package.json +4 -4
- package/types/ng-qubee.d.ts +1544 -0
- package/index.d.ts +0 -5
- package/lib/enums/sort.enum.d.ts +0 -4
- package/lib/errors/invalid-limit.error.d.ts +0 -3
- package/lib/errors/invalid-model-name.error.d.ts +0 -3
- package/lib/errors/invalid-page-number.error.d.ts +0 -3
- package/lib/errors/key-not-found.error.d.ts +0 -3
- package/lib/errors/unselectable-model.error.d.ts +0 -3
- package/lib/interfaces/config.interface.d.ts +0 -6
- package/lib/interfaces/fields.interface.d.ts +0 -3
- package/lib/interfaces/filters.interface.d.ts +0 -3
- package/lib/interfaces/nest-state.interface.d.ts +0 -4
- package/lib/interfaces/normalized.interface.d.ts +0 -3
- package/lib/interfaces/page.interface.d.ts +0 -2
- package/lib/interfaces/paginated-object.interface.d.ts +0 -3
- package/lib/interfaces/pagination-config.interface.d.ts +0 -14
- package/lib/interfaces/query-builder-config.interface.d.ts +0 -9
- package/lib/interfaces/query-builder-state.interface.d.ts +0 -13
- package/lib/interfaces/sort.interface.d.ts +0 -5
- package/lib/models/paginated-collection.d.ts +0 -30
- package/lib/models/query-builder-options.d.ts +0 -11
- package/lib/models/response-options.d.ts +0 -16
- package/lib/ng-qubee.module.d.ts +0 -9
- package/lib/provide-ngqubee.d.ts +0 -21
- package/lib/services/nest.service.d.ts +0 -182
- package/lib/services/ng-qubee.service.d.ts +0 -147
- package/lib/services/pagination.service.d.ts +0 -13
- package/public-api.d.ts +0 -20
package/fesm2022/ng-qubee.mjs
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { signal, computed, Injectable,
|
|
3
|
-
import * as qs from 'qs';
|
|
2
|
+
import { signal, computed, Injectable, NgModule, makeEnvironmentProviders } from '@angular/core';
|
|
4
3
|
import { BehaviorSubject, filter, throwError } from 'rxjs';
|
|
4
|
+
import * as qs from 'qs';
|
|
5
5
|
|
|
6
6
|
class KeyNotFoundError extends Error {
|
|
7
7
|
constructor(key) {
|
|
@@ -66,18 +66,204 @@ class PaginatedCollection {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
69
|
+
/**
|
|
70
|
+
* Enum representing the available pagination driver types
|
|
71
|
+
*
|
|
72
|
+
* Each driver encapsulates the full format knowledge for both
|
|
73
|
+
* request building (URI generation) and response parsing.
|
|
74
|
+
*/
|
|
75
|
+
var DriverEnum;
|
|
76
|
+
(function (DriverEnum) {
|
|
77
|
+
DriverEnum["JSON_API"] = "json-api";
|
|
78
|
+
DriverEnum["LARAVEL"] = "laravel";
|
|
79
|
+
DriverEnum["NESTJS"] = "nestjs";
|
|
80
|
+
DriverEnum["SPATIE"] = "spatie";
|
|
81
|
+
})(DriverEnum || (DriverEnum = {}));
|
|
74
82
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
83
|
+
/**
|
|
84
|
+
* Resolved response field key names with defaults applied
|
|
85
|
+
*
|
|
86
|
+
* Maps logical pagination concepts to the actual key names
|
|
87
|
+
* used in the API response. Unset values fall back to Laravel defaults.
|
|
88
|
+
*
|
|
89
|
+
* For NestJS responses, use dot-notation paths:
|
|
90
|
+
* ```typescript
|
|
91
|
+
* new ResponseOptions({
|
|
92
|
+
* currentPage: 'meta.currentPage',
|
|
93
|
+
* total: 'meta.totalItems'
|
|
94
|
+
* });
|
|
95
|
+
* ```
|
|
96
|
+
*/
|
|
97
|
+
class ResponseOptions {
|
|
98
|
+
currentPage;
|
|
99
|
+
data;
|
|
100
|
+
firstPageUrl;
|
|
101
|
+
from;
|
|
102
|
+
lastPage;
|
|
103
|
+
lastPageUrl;
|
|
104
|
+
nextPageUrl;
|
|
105
|
+
path;
|
|
106
|
+
perPage;
|
|
107
|
+
prevPageUrl;
|
|
108
|
+
to;
|
|
109
|
+
total;
|
|
110
|
+
constructor(options) {
|
|
111
|
+
this.currentPage = options.currentPage || 'current_page';
|
|
112
|
+
this.data = options.data || 'data';
|
|
113
|
+
this.firstPageUrl = options.firstPageUrl || 'first_page_url';
|
|
114
|
+
this.from = options.from || 'from';
|
|
115
|
+
this.lastPage = options.lastPage || 'last_page';
|
|
116
|
+
this.lastPageUrl = options.lastPageUrl || 'last_page_url';
|
|
117
|
+
this.nextPageUrl = options.nextPageUrl || 'next_page_url';
|
|
118
|
+
this.path = options.path || 'path';
|
|
119
|
+
this.perPage = options.perPage || 'per_page';
|
|
120
|
+
this.prevPageUrl = options.prevPageUrl || 'prev_page_url';
|
|
121
|
+
this.to = options.to || 'to';
|
|
122
|
+
this.total = options.total || 'total';
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
/**
|
|
126
|
+
* Pre-configured ResponseOptions for the JSON:API driver
|
|
127
|
+
*
|
|
128
|
+
* Uses dot-notation paths to access nested values in the JSON:API response format.
|
|
129
|
+
* JSON:API meta key names vary by implementation; these defaults cover the most
|
|
130
|
+
* common conventions and can be fully customised via `IPaginationConfig`.
|
|
131
|
+
*/
|
|
132
|
+
class JsonApiResponseOptions extends ResponseOptions {
|
|
133
|
+
constructor(options) {
|
|
134
|
+
super({
|
|
135
|
+
currentPage: options.currentPage || 'meta.current-page',
|
|
136
|
+
data: options.data || 'data',
|
|
137
|
+
firstPageUrl: options.firstPageUrl || 'links.first',
|
|
138
|
+
from: options.from || 'meta.from',
|
|
139
|
+
lastPage: options.lastPage || 'meta.page-count',
|
|
140
|
+
lastPageUrl: options.lastPageUrl || 'links.last',
|
|
141
|
+
nextPageUrl: options.nextPageUrl || 'links.next',
|
|
142
|
+
path: options.path || 'path',
|
|
143
|
+
perPage: options.perPage || 'meta.per-page',
|
|
144
|
+
prevPageUrl: options.prevPageUrl || 'links.prev',
|
|
145
|
+
to: options.to || 'meta.to',
|
|
146
|
+
total: options.total || 'meta.total'
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Pre-configured ResponseOptions for the NestJS driver
|
|
152
|
+
*
|
|
153
|
+
* Uses dot-notation paths to access nested values in the NestJS response format.
|
|
154
|
+
*/
|
|
155
|
+
class NestjsResponseOptions extends ResponseOptions {
|
|
156
|
+
constructor(options) {
|
|
157
|
+
super({
|
|
158
|
+
currentPage: options.currentPage || 'meta.currentPage',
|
|
159
|
+
data: options.data || 'data',
|
|
160
|
+
firstPageUrl: options.firstPageUrl || 'links.first',
|
|
161
|
+
from: options.from || 'meta.from',
|
|
162
|
+
lastPage: options.lastPage || 'meta.totalPages',
|
|
163
|
+
lastPageUrl: options.lastPageUrl || 'links.last',
|
|
164
|
+
nextPageUrl: options.nextPageUrl || 'links.next',
|
|
165
|
+
path: options.path || 'path',
|
|
166
|
+
perPage: options.perPage || 'meta.itemsPerPage',
|
|
167
|
+
prevPageUrl: options.prevPageUrl || 'links.previous',
|
|
168
|
+
to: options.to || 'meta.to',
|
|
169
|
+
total: options.total || 'meta.totalItems'
|
|
170
|
+
});
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* Error thrown when per-model field selection is attempted with a driver that does not support it
|
|
176
|
+
*
|
|
177
|
+
* Per-model field selection is only supported by the Spatie driver.
|
|
178
|
+
* Use `addSelect()` for NestJS flat field selection.
|
|
179
|
+
*/
|
|
180
|
+
class UnsupportedFieldSelectionError extends Error {
|
|
181
|
+
constructor() {
|
|
182
|
+
super('Per-model field selection is only supported by the Spatie driver. Use addSelect() for NestJS.');
|
|
183
|
+
this.name = 'UnsupportedFieldSelectionError';
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* Error thrown when filters are attempted with a driver that does not support them
|
|
189
|
+
*
|
|
190
|
+
* Filters are only supported by the Spatie and NestJS drivers.
|
|
191
|
+
*/
|
|
192
|
+
class UnsupportedFilterError extends Error {
|
|
193
|
+
constructor() {
|
|
194
|
+
super('Filters are only supported by the Spatie and NestJS drivers.');
|
|
195
|
+
this.name = 'UnsupportedFilterError';
|
|
196
|
+
}
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Error thrown when filter operators are attempted with a driver that does not support them
|
|
201
|
+
*
|
|
202
|
+
* Filter operators are only supported by the NestJS driver.
|
|
203
|
+
* Use `addFilter()` for Spatie implicit equality filters.
|
|
204
|
+
*/
|
|
205
|
+
class UnsupportedFilterOperatorError extends Error {
|
|
206
|
+
constructor() {
|
|
207
|
+
super('Filter operators are only supported by the NestJS driver. Use addFilter() for Spatie.');
|
|
208
|
+
this.name = 'UnsupportedFilterOperatorError';
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* Error thrown when includes are attempted with a driver that does not support them
|
|
214
|
+
*
|
|
215
|
+
* Includes are only supported by the Spatie driver.
|
|
216
|
+
*/
|
|
217
|
+
class UnsupportedIncludesError extends Error {
|
|
218
|
+
constructor() {
|
|
219
|
+
super('Includes are only supported by the Spatie driver.');
|
|
220
|
+
this.name = 'UnsupportedIncludesError';
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Error thrown when search is attempted with a driver that does not support it
|
|
226
|
+
*
|
|
227
|
+
* Search is only supported by the NestJS driver.
|
|
228
|
+
*/
|
|
229
|
+
class UnsupportedSearchError extends Error {
|
|
230
|
+
constructor() {
|
|
231
|
+
super('Search is only supported by the NestJS driver.');
|
|
232
|
+
this.name = 'UnsupportedSearchError';
|
|
233
|
+
}
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* Error thrown when flat field selection is attempted with a driver that does not support it
|
|
238
|
+
*
|
|
239
|
+
* Flat field selection is only supported by the NestJS driver.
|
|
240
|
+
* Use `addFields()` for Spatie per-model field selection.
|
|
241
|
+
*/
|
|
242
|
+
class UnsupportedSelectError extends Error {
|
|
243
|
+
constructor() {
|
|
244
|
+
super('Flat field selection is only supported by the NestJS driver. Use addFields() for Spatie.');
|
|
245
|
+
this.name = 'UnsupportedSelectError';
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
/**
|
|
250
|
+
* Error thrown when sorts are attempted with a driver that does not support them
|
|
251
|
+
*
|
|
252
|
+
* Sorts are only supported by the Spatie and NestJS drivers.
|
|
253
|
+
*/
|
|
254
|
+
class UnsupportedSortError extends Error {
|
|
255
|
+
constructor() {
|
|
256
|
+
super('Sorts are only supported by the Spatie and NestJS drivers.');
|
|
257
|
+
this.name = 'UnsupportedSortError';
|
|
78
258
|
}
|
|
79
259
|
}
|
|
80
260
|
|
|
261
|
+
/**
|
|
262
|
+
* Resolved query parameter key names with defaults applied
|
|
263
|
+
*
|
|
264
|
+
* Maps logical query concepts to the actual query parameter names
|
|
265
|
+
* used in the generated URI. Unset values fall back to defaults.
|
|
266
|
+
*/
|
|
81
267
|
class QueryBuilderOptions {
|
|
82
268
|
appends;
|
|
83
269
|
fields;
|
|
@@ -85,7 +271,10 @@ class QueryBuilderOptions {
|
|
|
85
271
|
includes;
|
|
86
272
|
limit;
|
|
87
273
|
page;
|
|
274
|
+
search;
|
|
275
|
+
select;
|
|
88
276
|
sort;
|
|
277
|
+
sortBy;
|
|
89
278
|
constructor(options) {
|
|
90
279
|
this.appends = options.appends || 'append';
|
|
91
280
|
this.fields = options.fields || 'fields';
|
|
@@ -93,176 +282,523 @@ class QueryBuilderOptions {
|
|
|
93
282
|
this.includes = options.includes || 'include';
|
|
94
283
|
this.limit = options.limit || 'limit';
|
|
95
284
|
this.page = options.page || 'page';
|
|
285
|
+
this.search = options.search || 'search';
|
|
286
|
+
this.select = options.select || 'select';
|
|
96
287
|
this.sort = options.sort || 'sort';
|
|
288
|
+
this.sortBy = options.sortBy || 'sortBy';
|
|
97
289
|
}
|
|
98
290
|
}
|
|
99
291
|
|
|
100
|
-
class
|
|
101
|
-
|
|
102
|
-
super(`Invalid model name: Model name must be a non-empty string. Received: ${JSON.stringify(model)}`);
|
|
103
|
-
this.name = 'InvalidModelNameError';
|
|
104
|
-
}
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
class InvalidPageNumberError extends Error {
|
|
108
|
-
constructor(page) {
|
|
109
|
-
super(`Invalid page number: Page must be a positive integer greater than 0. Received: ${page}`);
|
|
110
|
-
this.name = 'InvalidPageNumberError';
|
|
111
|
-
}
|
|
112
|
-
}
|
|
113
|
-
|
|
114
|
-
class InvalidLimitError extends Error {
|
|
115
|
-
constructor(limit) {
|
|
116
|
-
super(`Invalid limit value: Limit must be a positive integer greater than 0. Received: ${limit}`);
|
|
117
|
-
this.name = 'InvalidLimitError';
|
|
118
|
-
}
|
|
119
|
-
}
|
|
120
|
-
|
|
121
|
-
const INITIAL_STATE = {
|
|
122
|
-
baseUrl: '',
|
|
123
|
-
fields: {},
|
|
124
|
-
filters: {},
|
|
125
|
-
includes: [],
|
|
126
|
-
limit: 15,
|
|
127
|
-
model: '',
|
|
128
|
-
page: 1,
|
|
129
|
-
sorts: []
|
|
130
|
-
};
|
|
131
|
-
class NestService {
|
|
292
|
+
class NgQubeeService {
|
|
293
|
+
_nestService;
|
|
132
294
|
/**
|
|
133
|
-
*
|
|
134
|
-
*
|
|
135
|
-
* @type {IQueryBuilderState}
|
|
295
|
+
* The active pagination driver
|
|
136
296
|
*/
|
|
137
|
-
|
|
297
|
+
_driver;
|
|
138
298
|
/**
|
|
139
|
-
*
|
|
140
|
-
*
|
|
141
|
-
* @type {Signal<IQueryBuilderState>}
|
|
299
|
+
* Resolved query parameter key name options
|
|
142
300
|
*/
|
|
143
|
-
|
|
144
|
-
constructor() {
|
|
145
|
-
// Nothing to see here 👮🏻♀️
|
|
146
|
-
}
|
|
301
|
+
_options;
|
|
147
302
|
/**
|
|
148
|
-
*
|
|
149
|
-
*
|
|
150
|
-
* @param {string} baseUrl - The base URL to prepend to generated URIs
|
|
151
|
-
* @example
|
|
152
|
-
* service.baseUrl = 'https://api.example.com';
|
|
303
|
+
* The request strategy that builds URIs for the active driver
|
|
153
304
|
*/
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
305
|
+
_requestStrategy;
|
|
306
|
+
/**
|
|
307
|
+
* Internal BehaviorSubject that holds the latest generated URI
|
|
308
|
+
*/
|
|
309
|
+
_uri$ = new BehaviorSubject('');
|
|
310
|
+
/**
|
|
311
|
+
* Observable that emits non-empty generated URIs
|
|
312
|
+
*/
|
|
313
|
+
uri$ = this._uri$.asObservable().pipe(filter(uri => !!uri));
|
|
314
|
+
constructor(_nestService, requestStrategy, driver, options = {}) {
|
|
315
|
+
this._nestService = _nestService;
|
|
316
|
+
this._driver = driver;
|
|
317
|
+
this._options = new QueryBuilderOptions(options);
|
|
318
|
+
this._requestStrategy = requestStrategy;
|
|
159
319
|
}
|
|
160
320
|
/**
|
|
161
|
-
*
|
|
162
|
-
* Must be a positive integer greater than 0
|
|
321
|
+
* Assert that the active driver is one of the allowed drivers
|
|
163
322
|
*
|
|
164
|
-
* @param
|
|
165
|
-
* @
|
|
166
|
-
* @
|
|
167
|
-
* service.limit = 25;
|
|
323
|
+
* @param allowed - The allowed drivers
|
|
324
|
+
* @param error - The error to throw if the driver is not allowed
|
|
325
|
+
* @throws The provided error if the active driver is not in the allowed list
|
|
168
326
|
*/
|
|
169
|
-
|
|
170
|
-
this.
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
limit
|
|
174
|
-
}));
|
|
327
|
+
_assertDriver(allowed, error) {
|
|
328
|
+
if (!allowed.includes(this._driver)) {
|
|
329
|
+
throw error;
|
|
330
|
+
}
|
|
175
331
|
}
|
|
176
332
|
/**
|
|
177
|
-
*
|
|
178
|
-
* Must be a non-empty string
|
|
333
|
+
* Add fields to the select statement for the given model (JSON:API and Spatie only)
|
|
179
334
|
*
|
|
180
|
-
* @param
|
|
181
|
-
* @
|
|
182
|
-
* @
|
|
183
|
-
*
|
|
335
|
+
* @param model - Model that holds the fields
|
|
336
|
+
* @param fields - Fields to select
|
|
337
|
+
* @returns {this}
|
|
338
|
+
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
184
339
|
*/
|
|
185
|
-
|
|
186
|
-
this.
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
})
|
|
340
|
+
addFields(model, fields) {
|
|
341
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedFieldSelectionError());
|
|
342
|
+
if (!fields.length) {
|
|
343
|
+
return this;
|
|
344
|
+
}
|
|
345
|
+
this._nestService.addFields({ [model]: fields });
|
|
346
|
+
return this;
|
|
191
347
|
}
|
|
192
348
|
/**
|
|
193
|
-
*
|
|
194
|
-
* Must be a positive integer greater than 0
|
|
349
|
+
* Add a filter with the given value(s) (JSON:API, NestJS, and Spatie)
|
|
195
350
|
*
|
|
196
|
-
*
|
|
197
|
-
*
|
|
198
|
-
* @
|
|
199
|
-
*
|
|
351
|
+
* Produces: `filter[field]=value` (JSON:API / Spatie) or `filter.field=value` (NestJS)
|
|
352
|
+
*
|
|
353
|
+
* @param {string} field - Name of the field to filter
|
|
354
|
+
* @param {(string | number | boolean)[]} values - The needle(s)
|
|
355
|
+
* @returns {this}
|
|
356
|
+
* @throws {UnsupportedFilterError} If the active driver does not support filters
|
|
200
357
|
*/
|
|
201
|
-
|
|
202
|
-
this.
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
return
|
|
358
|
+
addFilter(field, ...values) {
|
|
359
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedFilterError());
|
|
360
|
+
if (!values.length) {
|
|
361
|
+
return this;
|
|
362
|
+
}
|
|
363
|
+
this._nestService.addFilters({
|
|
364
|
+
[field]: values
|
|
365
|
+
});
|
|
366
|
+
return this;
|
|
210
367
|
}
|
|
211
368
|
/**
|
|
212
|
-
*
|
|
369
|
+
* Add a filter with an explicit operator (NestJS only)
|
|
213
370
|
*
|
|
214
|
-
*
|
|
215
|
-
*
|
|
216
|
-
* @
|
|
371
|
+
* Produces: `filter.field=$operator:value`
|
|
372
|
+
*
|
|
373
|
+
* @param {string} field - Name of the field to filter
|
|
374
|
+
* @param {FilterOperatorEnum} operator - The filter operator to apply
|
|
375
|
+
* @param {(string | number | boolean)[]} values - The value(s) for the filter
|
|
376
|
+
* @returns {this}
|
|
377
|
+
* @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
|
|
217
378
|
*/
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
379
|
+
addFilterOperator(field, operator, ...values) {
|
|
380
|
+
this._assertDriver([DriverEnum.NESTJS], new UnsupportedFilterOperatorError());
|
|
381
|
+
if (!values.length) {
|
|
382
|
+
return this;
|
|
221
383
|
}
|
|
384
|
+
this._nestService.addOperatorFilters([{ field, operator, values }]);
|
|
385
|
+
return this;
|
|
222
386
|
}
|
|
223
387
|
/**
|
|
224
|
-
*
|
|
388
|
+
* Add related entities to include in the request (JSON:API and Spatie only)
|
|
225
389
|
*
|
|
226
|
-
* @param {
|
|
227
|
-
* @
|
|
228
|
-
* @
|
|
390
|
+
* @param {string[]} models - Models to include
|
|
391
|
+
* @returns {this}
|
|
392
|
+
* @throws {UnsupportedIncludesError} If the active driver does not support includes
|
|
229
393
|
*/
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
394
|
+
addIncludes(...models) {
|
|
395
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedIncludesError());
|
|
396
|
+
if (!models.length) {
|
|
397
|
+
return this;
|
|
233
398
|
}
|
|
399
|
+
this._nestService.addIncludes(models);
|
|
400
|
+
return this;
|
|
234
401
|
}
|
|
235
402
|
/**
|
|
236
|
-
*
|
|
403
|
+
* Add flat field selection (NestJS only)
|
|
237
404
|
*
|
|
238
|
-
*
|
|
239
|
-
*
|
|
240
|
-
* @
|
|
405
|
+
* Produces: `select=col1,col2`
|
|
406
|
+
*
|
|
407
|
+
* @param {string[]} fields - Fields to select
|
|
408
|
+
* @returns {this}
|
|
409
|
+
* @throws {UnsupportedSelectError} If the active driver does not support flat field selection
|
|
241
410
|
*/
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
411
|
+
addSelect(...fields) {
|
|
412
|
+
this._assertDriver([DriverEnum.NESTJS], new UnsupportedSelectError());
|
|
413
|
+
if (!fields.length) {
|
|
414
|
+
return this;
|
|
245
415
|
}
|
|
416
|
+
this._nestService.addSelect(fields);
|
|
417
|
+
return this;
|
|
246
418
|
}
|
|
247
419
|
/**
|
|
248
|
-
* Add
|
|
249
|
-
* Automatically prevents duplicate fields for each model
|
|
420
|
+
* Add a field with a sort criteria (JSON:API, NestJS, and Spatie)
|
|
250
421
|
*
|
|
251
|
-
* @param
|
|
252
|
-
* @
|
|
253
|
-
* @
|
|
254
|
-
*
|
|
255
|
-
* service.addFields({ posts: ['title', 'content'] });
|
|
422
|
+
* @param field - Field to use for sorting
|
|
423
|
+
* @param {SortEnum} order - A value from the SortEnum enumeration
|
|
424
|
+
* @returns {this}
|
|
425
|
+
* @throws {UnsupportedSortError} If the active driver does not support sorts
|
|
256
426
|
*/
|
|
257
|
-
|
|
258
|
-
this.
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
427
|
+
addSort(field, order) {
|
|
428
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedSortError());
|
|
429
|
+
this._nestService.addSort({
|
|
430
|
+
field,
|
|
431
|
+
order
|
|
432
|
+
});
|
|
433
|
+
return this;
|
|
434
|
+
}
|
|
435
|
+
/**
|
|
436
|
+
* Delete selected fields for the given models in the current query builder state (JSON:API and Spatie only)
|
|
437
|
+
*
|
|
438
|
+
* ```
|
|
439
|
+
* ngQubeeService.deleteFields({
|
|
440
|
+
* users: ['email', 'password'],
|
|
441
|
+
* address: ['zipcode']
|
|
442
|
+
* });
|
|
443
|
+
* ```
|
|
444
|
+
*
|
|
445
|
+
* @param {IFields} fields - Object mapping model names to field arrays to remove
|
|
446
|
+
* @returns {this}
|
|
447
|
+
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
448
|
+
*/
|
|
449
|
+
deleteFields(fields) {
|
|
450
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedFieldSelectionError());
|
|
451
|
+
this._nestService.deleteFields(fields);
|
|
452
|
+
return this;
|
|
453
|
+
}
|
|
454
|
+
/**
|
|
455
|
+
* Delete selected fields for the given model in the current query builder state (JSON:API and Spatie only)
|
|
456
|
+
*
|
|
457
|
+
* ```
|
|
458
|
+
* ngQubeeService.deleteFieldsByModel('users', 'email', 'password');
|
|
459
|
+
* ```
|
|
460
|
+
*
|
|
461
|
+
* @param model - Model that holds the fields
|
|
462
|
+
* @param {string[]} fields - Fields to delete from the state
|
|
463
|
+
* @returns {this}
|
|
464
|
+
* @throws {UnsupportedFieldSelectionError} If the active driver does not support per-model field selection
|
|
465
|
+
*/
|
|
466
|
+
deleteFieldsByModel(model, ...fields) {
|
|
467
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedFieldSelectionError());
|
|
468
|
+
if (!fields.length) {
|
|
469
|
+
return this;
|
|
470
|
+
}
|
|
471
|
+
this._nestService.deleteFields({
|
|
472
|
+
[model]: fields
|
|
473
|
+
});
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
/**
|
|
477
|
+
* Remove given filters from the query builder state (JSON:API, NestJS, and Spatie)
|
|
478
|
+
*
|
|
479
|
+
* @param {string[]} filters - Filters to remove
|
|
480
|
+
* @returns {this}
|
|
481
|
+
* @throws {UnsupportedFilterError} If the active driver does not support filters
|
|
482
|
+
*/
|
|
483
|
+
deleteFilters(...filters) {
|
|
484
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedFilterError());
|
|
485
|
+
if (!filters.length) {
|
|
486
|
+
return this;
|
|
487
|
+
}
|
|
488
|
+
this._nestService.deleteFilters(...filters);
|
|
489
|
+
return this;
|
|
490
|
+
}
|
|
491
|
+
/**
|
|
492
|
+
* Remove selected related models from the query builder state (JSON:API and Spatie only)
|
|
493
|
+
*
|
|
494
|
+
* @param {string[]} includes - Models to remove
|
|
495
|
+
* @returns {this}
|
|
496
|
+
* @throws {UnsupportedIncludesError} If the active driver does not support includes
|
|
497
|
+
*/
|
|
498
|
+
deleteIncludes(...includes) {
|
|
499
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.SPATIE], new UnsupportedIncludesError());
|
|
500
|
+
if (!includes.length) {
|
|
501
|
+
return this;
|
|
502
|
+
}
|
|
503
|
+
this._nestService.deleteIncludes(...includes);
|
|
504
|
+
return this;
|
|
505
|
+
}
|
|
506
|
+
/**
|
|
507
|
+
* Remove operator filters by field name (NestJS only)
|
|
508
|
+
*
|
|
509
|
+
* @param {string[]} fields - Field names of operator filters to remove
|
|
510
|
+
* @returns {this}
|
|
511
|
+
* @throws {UnsupportedFilterOperatorError} If the active driver does not support filter operators
|
|
512
|
+
*/
|
|
513
|
+
deleteOperatorFilters(...fields) {
|
|
514
|
+
this._assertDriver([DriverEnum.NESTJS], new UnsupportedFilterOperatorError());
|
|
515
|
+
if (!fields.length) {
|
|
516
|
+
return this;
|
|
517
|
+
}
|
|
518
|
+
this._nestService.deleteOperatorFilters(...fields);
|
|
519
|
+
return this;
|
|
520
|
+
}
|
|
521
|
+
/**
|
|
522
|
+
* Remove search term from the query builder state (NestJS only)
|
|
523
|
+
*
|
|
524
|
+
* @returns {this}
|
|
525
|
+
* @throws {UnsupportedSearchError} If the active driver does not support search
|
|
526
|
+
*/
|
|
527
|
+
deleteSearch() {
|
|
528
|
+
this._assertDriver([DriverEnum.NESTJS], new UnsupportedSearchError());
|
|
529
|
+
this._nestService.deleteSearch();
|
|
530
|
+
return this;
|
|
531
|
+
}
|
|
532
|
+
/**
|
|
533
|
+
* Remove flat field selections from the query builder state (NestJS only)
|
|
534
|
+
*
|
|
535
|
+
* @param {string[]} fields - Fields to remove from selection
|
|
536
|
+
* @returns {this}
|
|
537
|
+
* @throws {UnsupportedSelectError} If the active driver does not support flat field selection
|
|
538
|
+
*/
|
|
539
|
+
deleteSelect(...fields) {
|
|
540
|
+
this._assertDriver([DriverEnum.NESTJS], new UnsupportedSelectError());
|
|
541
|
+
if (!fields.length) {
|
|
542
|
+
return this;
|
|
543
|
+
}
|
|
544
|
+
this._nestService.deleteSelect(...fields);
|
|
545
|
+
return this;
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Remove sort rules from the query builder state (JSON:API, NestJS, and Spatie)
|
|
549
|
+
*
|
|
550
|
+
* @param sorts - Fields used for sorting to remove
|
|
551
|
+
* @returns {this}
|
|
552
|
+
* @throws {UnsupportedSortError} If the active driver does not support sorts
|
|
553
|
+
*/
|
|
554
|
+
deleteSorts(...sorts) {
|
|
555
|
+
this._assertDriver([DriverEnum.JSON_API, DriverEnum.NESTJS, DriverEnum.SPATIE], new UnsupportedSortError());
|
|
556
|
+
this._nestService.deleteSorts(...sorts);
|
|
557
|
+
return this;
|
|
558
|
+
}
|
|
559
|
+
/**
|
|
560
|
+
* Generate a URI accordingly to the given data and active driver
|
|
561
|
+
*
|
|
562
|
+
* @returns {Observable<string>} An observable that emits the generated URI
|
|
563
|
+
*/
|
|
564
|
+
generateUri() {
|
|
565
|
+
try {
|
|
566
|
+
this._uri$.next(this._requestStrategy.buildUri(this._nestService.nest(), this._options));
|
|
567
|
+
return this.uri$;
|
|
568
|
+
}
|
|
569
|
+
catch (error) {
|
|
570
|
+
return throwError(() => error);
|
|
571
|
+
}
|
|
572
|
+
}
|
|
573
|
+
/**
|
|
574
|
+
* Clear the current state and reset the Query Builder to a fresh, clean condition
|
|
575
|
+
*
|
|
576
|
+
* @returns {this}
|
|
577
|
+
*/
|
|
578
|
+
reset() {
|
|
579
|
+
this._nestService.reset();
|
|
580
|
+
return this;
|
|
581
|
+
}
|
|
582
|
+
/**
|
|
583
|
+
* Set the base URL to use for composing the address
|
|
584
|
+
*
|
|
585
|
+
* @param {string} baseUrl - The base URL
|
|
586
|
+
* @returns {this}
|
|
587
|
+
*/
|
|
588
|
+
setBaseUrl(baseUrl) {
|
|
589
|
+
this._nestService.baseUrl = baseUrl;
|
|
590
|
+
return this;
|
|
591
|
+
}
|
|
592
|
+
/**
|
|
593
|
+
* Set the items per page number
|
|
594
|
+
*
|
|
595
|
+
* Validation is delegated to the active request strategy because the
|
|
596
|
+
* accepted range is driver-specific: nestjs-paginate additionally accepts
|
|
597
|
+
* `-1` as a "fetch all" sentinel, while Laravel, Spatie, and JSON:API
|
|
598
|
+
* require a positive integer.
|
|
599
|
+
*
|
|
600
|
+
* @param limit - Number of items per page (or `-1` to fetch all, NestJS only)
|
|
601
|
+
* @returns {this}
|
|
602
|
+
* @throws {import('../errors/invalid-limit.error').InvalidLimitError} If the value is not accepted by the active driver
|
|
603
|
+
*/
|
|
604
|
+
setLimit(limit) {
|
|
605
|
+
this._requestStrategy.validateLimit(limit);
|
|
606
|
+
this._nestService.limit = limit;
|
|
607
|
+
return this;
|
|
608
|
+
}
|
|
609
|
+
/**
|
|
610
|
+
* Set the page that the backend will use to paginate the result set
|
|
611
|
+
*
|
|
612
|
+
* @param page - Page number
|
|
613
|
+
* @returns {this}
|
|
614
|
+
*/
|
|
615
|
+
setPage(page) {
|
|
616
|
+
this._nestService.page = page;
|
|
617
|
+
return this;
|
|
618
|
+
}
|
|
619
|
+
/**
|
|
620
|
+
* Set the API resource to run the query against
|
|
621
|
+
*
|
|
622
|
+
* @param {string} resource - Resource name (e.g. 'users' produces /users)
|
|
623
|
+
* @returns {this}
|
|
624
|
+
*/
|
|
625
|
+
setResource(resource) {
|
|
626
|
+
this._nestService.resource = resource;
|
|
627
|
+
return this;
|
|
628
|
+
}
|
|
629
|
+
/**
|
|
630
|
+
* Set the search term for full-text search (NestJS only)
|
|
631
|
+
*
|
|
632
|
+
* Produces: `search=term`
|
|
633
|
+
*
|
|
634
|
+
* @param {string} search - The search term
|
|
635
|
+
* @returns {this}
|
|
636
|
+
* @throws {UnsupportedSearchError} If the active driver does not support search
|
|
637
|
+
*/
|
|
638
|
+
setSearch(search) {
|
|
639
|
+
this._assertDriver([DriverEnum.NESTJS], new UnsupportedSearchError());
|
|
640
|
+
this._nestService.setSearch(search);
|
|
641
|
+
return this;
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
|
|
645
|
+
/**
|
|
646
|
+
* Error thrown when an invalid resource name is provided
|
|
647
|
+
*
|
|
648
|
+
* Resource name must be a non-empty string.
|
|
649
|
+
*/
|
|
650
|
+
class InvalidResourceNameError extends Error {
|
|
651
|
+
constructor(resource) {
|
|
652
|
+
super(`Invalid resource name: Resource name must be a non-empty string. Received: ${JSON.stringify(resource)}`);
|
|
653
|
+
this.name = 'InvalidResourceNameError';
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
|
|
657
|
+
class InvalidPageNumberError extends Error {
|
|
658
|
+
constructor(page) {
|
|
659
|
+
super(`Invalid page number: Page must be a positive integer greater than 0. Received: ${page}`);
|
|
660
|
+
this.name = 'InvalidPageNumberError';
|
|
661
|
+
}
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
const INITIAL_STATE = {
|
|
665
|
+
baseUrl: '',
|
|
666
|
+
fields: {},
|
|
667
|
+
filters: {},
|
|
668
|
+
includes: [],
|
|
669
|
+
limit: 15,
|
|
670
|
+
operatorFilters: [],
|
|
671
|
+
page: 1,
|
|
672
|
+
resource: '',
|
|
673
|
+
search: '',
|
|
674
|
+
select: [],
|
|
675
|
+
sorts: []
|
|
676
|
+
};
|
|
677
|
+
class NestService {
|
|
678
|
+
/**
|
|
679
|
+
* Private writable signal that holds the Query Builder state
|
|
680
|
+
*
|
|
681
|
+
* @type {IQueryBuilderState}
|
|
682
|
+
*/
|
|
683
|
+
_nest = signal(this._clone(INITIAL_STATE), ...(ngDevMode ? [{ debugName: "_nest" }] : []));
|
|
684
|
+
/**
|
|
685
|
+
* A computed signal that makes readonly the writable signal _nest
|
|
686
|
+
*
|
|
687
|
+
* @type {Signal<IQueryBuilderState>}
|
|
688
|
+
*/
|
|
689
|
+
nest = computed(() => this._clone(this._nest()), ...(ngDevMode ? [{ debugName: "nest" }] : []));
|
|
690
|
+
constructor() {
|
|
691
|
+
// Nothing to see here
|
|
692
|
+
}
|
|
693
|
+
/**
|
|
694
|
+
* Set the base URL for the API
|
|
695
|
+
*
|
|
696
|
+
* @param {string} baseUrl - The base URL to prepend to generated URIs
|
|
697
|
+
* @example
|
|
698
|
+
* service.baseUrl = 'https://api.example.com';
|
|
699
|
+
*/
|
|
700
|
+
set baseUrl(baseUrl) {
|
|
701
|
+
this._nest.update(nest => ({
|
|
702
|
+
...nest,
|
|
703
|
+
baseUrl
|
|
704
|
+
}));
|
|
705
|
+
}
|
|
706
|
+
/**
|
|
707
|
+
* Set the limit for paginated results
|
|
708
|
+
*
|
|
709
|
+
* This setter performs a raw state write. Validation of the value is the
|
|
710
|
+
* responsibility of the active request strategy and is enforced upstream
|
|
711
|
+
* by `NgQubeeService.setLimit()`, because the accepted range depends on
|
|
712
|
+
* the driver (e.g. nestjs-paginate accepts `-1` for "fetch all").
|
|
713
|
+
*
|
|
714
|
+
* @param {number} limit - The number of items per page
|
|
715
|
+
* @example
|
|
716
|
+
* service.limit = 25;
|
|
717
|
+
*/
|
|
718
|
+
set limit(limit) {
|
|
719
|
+
this._nest.update(nest => ({
|
|
720
|
+
...nest,
|
|
721
|
+
limit
|
|
722
|
+
}));
|
|
723
|
+
}
|
|
724
|
+
/**
|
|
725
|
+
* Set the page number for pagination
|
|
726
|
+
* Must be a positive integer greater than 0
|
|
727
|
+
*
|
|
728
|
+
* @param {number} page - The page number to fetch
|
|
729
|
+
* @throws {InvalidPageNumberError} If page is not a positive integer
|
|
730
|
+
* @example
|
|
731
|
+
* service.page = 2;
|
|
732
|
+
*/
|
|
733
|
+
set page(page) {
|
|
734
|
+
this._validatePageNumber(page);
|
|
735
|
+
this._nest.update(nest => ({
|
|
736
|
+
...nest,
|
|
737
|
+
page
|
|
738
|
+
}));
|
|
739
|
+
}
|
|
740
|
+
/**
|
|
741
|
+
* Set the resource name for the query
|
|
742
|
+
* Must be a non-empty string
|
|
743
|
+
*
|
|
744
|
+
* @param {string} resource - The API resource name (e.g., 'users', 'posts')
|
|
745
|
+
* @throws {InvalidResourceNameError} If resource is not a non-empty string
|
|
746
|
+
* @example
|
|
747
|
+
* service.resource = 'users';
|
|
748
|
+
*/
|
|
749
|
+
set resource(resource) {
|
|
750
|
+
this._validateResourceName(resource);
|
|
751
|
+
this._nest.update(nest => ({
|
|
752
|
+
...nest,
|
|
753
|
+
resource
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
_clone(obj) {
|
|
757
|
+
return JSON.parse(JSON.stringify(obj));
|
|
758
|
+
}
|
|
759
|
+
/**
|
|
760
|
+
* Validates that the page number is a positive integer
|
|
761
|
+
*
|
|
762
|
+
* @param {number} page - The page number to validate
|
|
763
|
+
* @throws {InvalidPageNumberError} If page is not a positive integer
|
|
764
|
+
* @private
|
|
765
|
+
*/
|
|
766
|
+
_validatePageNumber(page) {
|
|
767
|
+
if (!Number.isInteger(page) || page < 1) {
|
|
768
|
+
throw new InvalidPageNumberError(page);
|
|
769
|
+
}
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Validates that the resource name is a non-empty string
|
|
773
|
+
*
|
|
774
|
+
* @param {string} resource - The resource name to validate
|
|
775
|
+
* @throws {InvalidResourceNameError} If resource is not a non-empty string
|
|
776
|
+
* @private
|
|
777
|
+
*/
|
|
778
|
+
_validateResourceName(resource) {
|
|
779
|
+
if (!resource || typeof resource !== 'string' || resource.trim().length === 0) {
|
|
780
|
+
throw new InvalidResourceNameError(resource);
|
|
781
|
+
}
|
|
782
|
+
}
|
|
783
|
+
/**
|
|
784
|
+
* Add selectable fields for the given model to the request
|
|
785
|
+
* Automatically prevents duplicate fields for each model
|
|
786
|
+
*
|
|
787
|
+
* @param {IFields} fields - Object mapping model names to arrays of field names
|
|
788
|
+
* @return {void}
|
|
789
|
+
* @example
|
|
790
|
+
* service.addFields({ users: ['id', 'email', 'username'] });
|
|
791
|
+
* service.addFields({ posts: ['title', 'content'] });
|
|
792
|
+
*/
|
|
793
|
+
addFields(fields) {
|
|
794
|
+
this._nest.update(nest => {
|
|
795
|
+
const mergedFields = { ...nest.fields };
|
|
796
|
+
Object.keys(fields).forEach(model => {
|
|
797
|
+
const existingFields = mergedFields[model] || [];
|
|
798
|
+
const newFields = fields[model];
|
|
799
|
+
// Use Set to prevent duplicates
|
|
800
|
+
const uniqueFields = Array.from(new Set([...existingFields, ...newFields]));
|
|
801
|
+
mergedFields[model] = uniqueFields;
|
|
266
802
|
});
|
|
267
803
|
return {
|
|
268
804
|
...nest,
|
|
@@ -292,27 +828,77 @@ class NestService {
|
|
|
292
828
|
});
|
|
293
829
|
return {
|
|
294
830
|
...nest,
|
|
295
|
-
filters: mergedFilters
|
|
831
|
+
filters: mergedFilters
|
|
832
|
+
};
|
|
833
|
+
});
|
|
834
|
+
}
|
|
835
|
+
/**
|
|
836
|
+
* Add resources to include with the request
|
|
837
|
+
* Automatically prevents duplicate includes
|
|
838
|
+
*
|
|
839
|
+
* @param {string[]} includes - Array of resource names to include in the response
|
|
840
|
+
* @return {void}
|
|
841
|
+
* @example
|
|
842
|
+
* service.addIncludes(['profile', 'posts']);
|
|
843
|
+
* service.addIncludes(['comments']);
|
|
844
|
+
*/
|
|
845
|
+
addIncludes(includes) {
|
|
846
|
+
this._nest.update(nest => {
|
|
847
|
+
// Use Set to prevent duplicates
|
|
848
|
+
const uniqueIncludes = Array.from(new Set([...nest.includes, ...includes]));
|
|
849
|
+
return {
|
|
850
|
+
...nest,
|
|
851
|
+
includes: uniqueIncludes
|
|
852
|
+
};
|
|
853
|
+
});
|
|
854
|
+
}
|
|
855
|
+
/**
|
|
856
|
+
* Add filters with explicit operators (NestJS only)
|
|
857
|
+
* Automatically prevents duplicate operator filters for the same field + operator combination
|
|
858
|
+
*
|
|
859
|
+
* @param {IOperatorFilter[]} filters - Array of operator filter configurations
|
|
860
|
+
* @return {void}
|
|
861
|
+
* @example
|
|
862
|
+
* import { FilterOperatorEnum } from 'ng-qubee';
|
|
863
|
+
* service.addOperatorFilters([{ field: 'age', operator: FilterOperatorEnum.GTE, values: [18] }]);
|
|
864
|
+
*/
|
|
865
|
+
addOperatorFilters(filters) {
|
|
866
|
+
this._nest.update(nest => {
|
|
867
|
+
const merged = [...nest.operatorFilters];
|
|
868
|
+
filters.forEach(newFilter => {
|
|
869
|
+
const existingIdx = merged.findIndex(f => f.field === newFilter.field && f.operator === newFilter.operator);
|
|
870
|
+
if (existingIdx > -1) {
|
|
871
|
+
const existingValues = merged[existingIdx].values;
|
|
872
|
+
merged[existingIdx] = {
|
|
873
|
+
...merged[existingIdx],
|
|
874
|
+
values: Array.from(new Set([...existingValues, ...newFilter.values]))
|
|
875
|
+
};
|
|
876
|
+
}
|
|
877
|
+
else {
|
|
878
|
+
merged.push({ ...newFilter });
|
|
879
|
+
}
|
|
880
|
+
});
|
|
881
|
+
return {
|
|
882
|
+
...nest,
|
|
883
|
+
operatorFilters: merged
|
|
296
884
|
};
|
|
297
885
|
});
|
|
298
886
|
}
|
|
299
887
|
/**
|
|
300
|
-
* Add
|
|
301
|
-
* Automatically prevents duplicate
|
|
888
|
+
* Add flat field selection columns (NestJS only)
|
|
889
|
+
* Automatically prevents duplicate select fields
|
|
302
890
|
*
|
|
303
|
-
* @param {string[]}
|
|
891
|
+
* @param {string[]} fields - Array of column names to select
|
|
304
892
|
* @return {void}
|
|
305
893
|
* @example
|
|
306
|
-
* service.
|
|
307
|
-
* service.addIncludes(['comments']);
|
|
894
|
+
* service.addSelect(['id', 'name', 'email']);
|
|
308
895
|
*/
|
|
309
|
-
|
|
896
|
+
addSelect(fields) {
|
|
310
897
|
this._nest.update(nest => {
|
|
311
|
-
|
|
312
|
-
const uniqueIncludes = Array.from(new Set([...nest.includes, ...includes]));
|
|
898
|
+
const uniqueSelect = Array.from(new Set([...nest.select, ...fields]));
|
|
313
899
|
return {
|
|
314
900
|
...nest,
|
|
315
|
-
|
|
901
|
+
select: uniqueSelect
|
|
316
902
|
};
|
|
317
903
|
});
|
|
318
904
|
}
|
|
@@ -390,6 +976,49 @@ class NestService {
|
|
|
390
976
|
includes: nest.includes.filter(v => !includes.includes(v))
|
|
391
977
|
}));
|
|
392
978
|
}
|
|
979
|
+
/**
|
|
980
|
+
* Remove operator filters by field name (NestJS only)
|
|
981
|
+
*
|
|
982
|
+
* @param {...string[]} fields - Field names of operator filters to remove
|
|
983
|
+
* @return {void}
|
|
984
|
+
* @example
|
|
985
|
+
* service.deleteOperatorFilters('age');
|
|
986
|
+
* service.deleteOperatorFilters('price', 'quantity');
|
|
987
|
+
*/
|
|
988
|
+
deleteOperatorFilters(...fields) {
|
|
989
|
+
this._nest.update(nest => ({
|
|
990
|
+
...nest,
|
|
991
|
+
operatorFilters: nest.operatorFilters.filter(f => !fields.includes(f.field))
|
|
992
|
+
}));
|
|
993
|
+
}
|
|
994
|
+
/**
|
|
995
|
+
* Remove the search term from the state (NestJS only)
|
|
996
|
+
*
|
|
997
|
+
* @return {void}
|
|
998
|
+
* @example
|
|
999
|
+
* service.deleteSearch();
|
|
1000
|
+
*/
|
|
1001
|
+
deleteSearch() {
|
|
1002
|
+
this._nest.update(nest => ({
|
|
1003
|
+
...nest,
|
|
1004
|
+
search: ''
|
|
1005
|
+
}));
|
|
1006
|
+
}
|
|
1007
|
+
/**
|
|
1008
|
+
* Remove flat field selections from the state (NestJS only)
|
|
1009
|
+
*
|
|
1010
|
+
* @param {...string[]} fields - Field names to remove from selection
|
|
1011
|
+
* @return {void}
|
|
1012
|
+
* @example
|
|
1013
|
+
* service.deleteSelect('email');
|
|
1014
|
+
* service.deleteSelect('name', 'email');
|
|
1015
|
+
*/
|
|
1016
|
+
deleteSelect(...fields) {
|
|
1017
|
+
this._nest.update(nest => ({
|
|
1018
|
+
...nest,
|
|
1019
|
+
select: nest.select.filter(f => !fields.includes(f))
|
|
1020
|
+
}));
|
|
1021
|
+
}
|
|
393
1022
|
/**
|
|
394
1023
|
* Remove sorts from the request by field name
|
|
395
1024
|
*
|
|
@@ -412,6 +1041,20 @@ class NestService {
|
|
|
412
1041
|
sorts: s
|
|
413
1042
|
}));
|
|
414
1043
|
}
|
|
1044
|
+
/**
|
|
1045
|
+
* Set the full-text search term (NestJS only)
|
|
1046
|
+
*
|
|
1047
|
+
* @param {string} search - The search term
|
|
1048
|
+
* @return {void}
|
|
1049
|
+
* @example
|
|
1050
|
+
* service.setSearch('john doe');
|
|
1051
|
+
*/
|
|
1052
|
+
setSearch(search) {
|
|
1053
|
+
this._nest.update(nest => ({
|
|
1054
|
+
...nest,
|
|
1055
|
+
search
|
|
1056
|
+
}));
|
|
1057
|
+
}
|
|
415
1058
|
/**
|
|
416
1059
|
* Reset the query builder state to initial values
|
|
417
1060
|
* Clears all fields, filters, includes, sorts, and resets pagination
|
|
@@ -419,385 +1062,1018 @@ class NestService {
|
|
|
419
1062
|
* @return {void}
|
|
420
1063
|
* @example
|
|
421
1064
|
* service.reset();
|
|
422
|
-
* // State is now: { baseUrl: '', fields: {}, filters: {}, includes: [], limit: 15, model: '', page: 1, sorts: [] }
|
|
423
1065
|
*/
|
|
424
1066
|
reset() {
|
|
425
1067
|
this._nest.update(_ => this._clone(INITIAL_STATE));
|
|
426
1068
|
}
|
|
427
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
428
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "
|
|
1069
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
|
|
1070
|
+
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService });
|
|
429
1071
|
}
|
|
430
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
1072
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NestService, decorators: [{
|
|
431
1073
|
type: Injectable
|
|
432
1074
|
}], ctorParameters: () => [] });
|
|
433
1075
|
|
|
434
|
-
class
|
|
435
|
-
|
|
1076
|
+
class PaginationService {
|
|
1077
|
+
/**
|
|
1078
|
+
* Resolved response key name options
|
|
1079
|
+
*/
|
|
436
1080
|
_options;
|
|
437
1081
|
/**
|
|
438
|
-
*
|
|
1082
|
+
* The response strategy that parses responses for the active driver
|
|
1083
|
+
*/
|
|
1084
|
+
_responseStrategy;
|
|
1085
|
+
constructor(responseStrategy, options = {}) {
|
|
1086
|
+
this._options = new ResponseOptions(options);
|
|
1087
|
+
this._responseStrategy = responseStrategy;
|
|
1088
|
+
}
|
|
1089
|
+
/**
|
|
1090
|
+
* Transform a raw API response into a typed PaginatedCollection
|
|
1091
|
+
*
|
|
1092
|
+
* Delegates to the active driver's response strategy for parsing.
|
|
1093
|
+
*
|
|
1094
|
+
* @param response - The raw API response object
|
|
1095
|
+
* @returns A typed PaginatedCollection instance
|
|
1096
|
+
*/
|
|
1097
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1098
|
+
paginate(response) {
|
|
1099
|
+
return this._responseStrategy.paginate(response, this._options);
|
|
1100
|
+
}
|
|
1101
|
+
}
|
|
1102
|
+
|
|
1103
|
+
var SortEnum;
|
|
1104
|
+
(function (SortEnum) {
|
|
1105
|
+
SortEnum["ASC"] = "asc";
|
|
1106
|
+
SortEnum["DESC"] = "desc";
|
|
1107
|
+
})(SortEnum || (SortEnum = {}));
|
|
1108
|
+
|
|
1109
|
+
/**
|
|
1110
|
+
* Thrown when a limit value does not satisfy the active driver's constraints
|
|
1111
|
+
*
|
|
1112
|
+
* Validation is driver-scoped: most drivers require an integer `>= 1`, while
|
|
1113
|
+
* the NestJS driver additionally accepts `-1` as a "fetch all items" sentinel
|
|
1114
|
+
* (as documented by nestjs-paginate). The message is tailored accordingly so
|
|
1115
|
+
* the caller understands which values are permitted.
|
|
1116
|
+
*/
|
|
1117
|
+
class InvalidLimitError extends Error {
|
|
1118
|
+
/**
|
|
1119
|
+
* @param limit - The rejected limit value
|
|
1120
|
+
* @param allowFetchAll - Whether the active driver accepts `-1` (fetch all)
|
|
1121
|
+
*/
|
|
1122
|
+
constructor(limit, allowFetchAll = false) {
|
|
1123
|
+
const allowed = allowFetchAll
|
|
1124
|
+
? 'a positive integer greater than 0, or -1 to fetch all items'
|
|
1125
|
+
: 'a positive integer greater than 0';
|
|
1126
|
+
super(`Invalid limit value: Limit must be ${allowed}. Received: ${limit}`);
|
|
1127
|
+
this.name = 'InvalidLimitError';
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
|
|
1131
|
+
class UnselectableModelError extends Error {
|
|
1132
|
+
constructor(model) {
|
|
1133
|
+
super(`Unselectable Model: the selected model (${model}) is not present neither in the "model" property, nor in the includes object.`);
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
/**
|
|
1138
|
+
* Request strategy for the JSON:API driver
|
|
1139
|
+
*
|
|
1140
|
+
* Generates URIs in the JSON:API format:
|
|
1141
|
+
* - Fields: `fields[articles]=title,body&fields[people]=name`
|
|
1142
|
+
* - Filters: `filter[status]=active`
|
|
1143
|
+
* - Includes: `include=author,comments.author`
|
|
1144
|
+
* - Pagination: `page[number]=1&page[size]=15`
|
|
1145
|
+
* - Sort: `sort=-created_at,name` (- prefix = DESC)
|
|
1146
|
+
*
|
|
1147
|
+
* @see https://jsonapi.org/format/
|
|
1148
|
+
*/
|
|
1149
|
+
class JsonApiRequestStrategy {
|
|
1150
|
+
/**
|
|
1151
|
+
* Accumulator for composing the URI string
|
|
439
1152
|
*/
|
|
440
1153
|
_uri = '';
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
1154
|
+
/**
|
|
1155
|
+
* Build a URI string from the given state using the JSON:API format
|
|
1156
|
+
*
|
|
1157
|
+
* @param state - The current query builder state
|
|
1158
|
+
* @param options - The query parameter key name configuration
|
|
1159
|
+
* @returns The composed URI string
|
|
1160
|
+
* @throws Error if resource is not set
|
|
1161
|
+
*/
|
|
1162
|
+
buildUri(state, options) {
|
|
1163
|
+
if (!state.resource) {
|
|
1164
|
+
throw new Error('Set the resource property BEFORE adding filters or calling the url() / get() methods');
|
|
1165
|
+
}
|
|
1166
|
+
this._uri = '';
|
|
1167
|
+
this._parseIncludes(state, options);
|
|
1168
|
+
this._parseFields(state, options);
|
|
1169
|
+
this._parseFilters(state, options);
|
|
1170
|
+
this._parsePagination(state, options);
|
|
1171
|
+
this._parseSort(state, options);
|
|
1172
|
+
return this._uri;
|
|
1173
|
+
}
|
|
1174
|
+
/**
|
|
1175
|
+
* Validate that the given limit is accepted by the JSON:API driver
|
|
1176
|
+
*
|
|
1177
|
+
* The JSON:API specification leaves pagination semantics to the server and
|
|
1178
|
+
* does not define a "fetch all" sentinel, so only positive integers are
|
|
1179
|
+
* accepted.
|
|
1180
|
+
*
|
|
1181
|
+
* @param limit - The limit value to validate
|
|
1182
|
+
* @throws {InvalidLimitError} If the value is not a positive integer
|
|
1183
|
+
*/
|
|
1184
|
+
validateLimit(limit) {
|
|
1185
|
+
if (Number.isInteger(limit) && limit >= 1) {
|
|
1186
|
+
return;
|
|
1187
|
+
}
|
|
1188
|
+
throw new InvalidLimitError(limit);
|
|
446
1189
|
}
|
|
447
|
-
|
|
448
|
-
|
|
1190
|
+
/**
|
|
1191
|
+
* Parse and append field selection parameters
|
|
1192
|
+
*
|
|
1193
|
+
* Validates that each field model exists either as the main resource
|
|
1194
|
+
* or in the includes list. Fields are grouped by type in bracket notation.
|
|
1195
|
+
*
|
|
1196
|
+
* @param state - The current query builder state
|
|
1197
|
+
* @param options - The query parameter key name configuration
|
|
1198
|
+
* @returns The generated field selection parameter string
|
|
1199
|
+
* @throws Error if resource is required but not set
|
|
1200
|
+
* @throws UnselectableModelError if a field model is not in resource or includes
|
|
1201
|
+
*/
|
|
1202
|
+
_parseFields(state, options) {
|
|
1203
|
+
if (!Object.keys(state.fields).length) {
|
|
449
1204
|
return this._uri;
|
|
450
1205
|
}
|
|
451
|
-
if (!
|
|
452
|
-
throw new Error('While selecting fields, the ->
|
|
1206
|
+
if (!state.resource) {
|
|
1207
|
+
throw new Error('While selecting fields, the -> resource <- is required');
|
|
453
1208
|
}
|
|
454
|
-
if (!(
|
|
455
|
-
throw new Error(`Key ${
|
|
1209
|
+
if (!(state.resource in state.fields)) {
|
|
1210
|
+
throw new Error(`Key ${state.resource} is missing in the fields object`);
|
|
456
1211
|
}
|
|
457
1212
|
const f = {};
|
|
458
|
-
for (const k in
|
|
459
|
-
if (
|
|
460
|
-
|
|
461
|
-
// If not, it means that has not been selected anywhere and that will cause an error on the API
|
|
462
|
-
if (k !== s.model && !s.includes.includes(k)) {
|
|
1213
|
+
for (const k in state.fields) {
|
|
1214
|
+
if (state.fields.hasOwnProperty(k)) {
|
|
1215
|
+
if (k !== state.resource && !state.includes.includes(k)) {
|
|
463
1216
|
throw new UnselectableModelError(k);
|
|
464
1217
|
}
|
|
465
|
-
Object.assign(f, { [`${
|
|
1218
|
+
Object.assign(f, { [`${options.fields}[${k}]`]: state.fields[k].join(',') });
|
|
466
1219
|
}
|
|
467
1220
|
}
|
|
468
|
-
const param = `${this._prepend(
|
|
1221
|
+
const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
|
|
469
1222
|
this._uri += param;
|
|
470
1223
|
return param;
|
|
471
1224
|
}
|
|
472
|
-
|
|
473
|
-
|
|
1225
|
+
/**
|
|
1226
|
+
* Parse and append filter parameters
|
|
1227
|
+
*
|
|
1228
|
+
* Generates filter parameters in bracket notation: `filter[key]=value1,value2`
|
|
1229
|
+
*
|
|
1230
|
+
* @param state - The current query builder state
|
|
1231
|
+
* @param options - The query parameter key name configuration
|
|
1232
|
+
* @returns The generated filter parameter string
|
|
1233
|
+
*/
|
|
1234
|
+
_parseFilters(state, options) {
|
|
1235
|
+
const keys = Object.keys(state.filters);
|
|
474
1236
|
if (!keys.length) {
|
|
475
1237
|
return this._uri;
|
|
476
1238
|
}
|
|
477
1239
|
const f = {
|
|
478
|
-
[`${
|
|
479
|
-
return Object.assign(acc, { [key]:
|
|
1240
|
+
[`${options.filters}`]: keys.reduce((acc, key) => {
|
|
1241
|
+
return Object.assign(acc, { [key]: state.filters[key].join(',') });
|
|
480
1242
|
}, {})
|
|
481
1243
|
};
|
|
482
|
-
const param = `${this._prepend(
|
|
1244
|
+
const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
|
|
483
1245
|
this._uri += param;
|
|
484
1246
|
return param;
|
|
485
1247
|
}
|
|
486
|
-
|
|
487
|
-
|
|
1248
|
+
/**
|
|
1249
|
+
* Parse and append include parameters
|
|
1250
|
+
*
|
|
1251
|
+
* Generates: `include=author,comments.author`
|
|
1252
|
+
*
|
|
1253
|
+
* @param state - The current query builder state
|
|
1254
|
+
* @param options - The query parameter key name configuration
|
|
1255
|
+
* @returns The generated include parameter string
|
|
1256
|
+
*/
|
|
1257
|
+
_parseIncludes(state, options) {
|
|
1258
|
+
if (!state.includes.length) {
|
|
488
1259
|
return this._uri;
|
|
489
1260
|
}
|
|
490
|
-
const param = `${this._prepend(
|
|
491
|
-
this._uri += param;
|
|
492
|
-
return param;
|
|
493
|
-
}
|
|
494
|
-
_parseLimit(s) {
|
|
495
|
-
const param = `${this._prepend(s.model)}${this._options.limit}=${s.limit}`;
|
|
1261
|
+
const param = `${this._prepend(state)}${options.includes}=${state.includes}`;
|
|
496
1262
|
this._uri += param;
|
|
497
1263
|
return param;
|
|
498
1264
|
}
|
|
499
|
-
|
|
500
|
-
|
|
1265
|
+
/**
|
|
1266
|
+
* Parse and append pagination parameters in JSON:API bracket notation
|
|
1267
|
+
*
|
|
1268
|
+
* Generates: `page[number]=1&page[size]=15`
|
|
1269
|
+
*
|
|
1270
|
+
* @param state - The current query builder state
|
|
1271
|
+
* @param options - The query parameter key name configuration
|
|
1272
|
+
* @returns The generated pagination parameter string
|
|
1273
|
+
*/
|
|
1274
|
+
_parsePagination(state, options) {
|
|
1275
|
+
const pagination = qs.stringify({ [options.page]: { number: state.page, size: state.limit } }, { encode: false });
|
|
1276
|
+
const param = `${this._prepend(state)}${pagination}`;
|
|
501
1277
|
this._uri += param;
|
|
502
1278
|
return param;
|
|
503
1279
|
}
|
|
504
|
-
|
|
1280
|
+
/**
|
|
1281
|
+
* Parse and append sort parameters
|
|
1282
|
+
*
|
|
1283
|
+
* Generates: `sort=-field1,field2` where `-` prefix indicates DESC order
|
|
1284
|
+
*
|
|
1285
|
+
* @param state - The current query builder state
|
|
1286
|
+
* @param options - The query parameter key name configuration
|
|
1287
|
+
* @returns The generated sort parameter string
|
|
1288
|
+
*/
|
|
1289
|
+
_parseSort(state, options) {
|
|
505
1290
|
let param = '';
|
|
506
|
-
if (!
|
|
1291
|
+
if (!state.sorts.length) {
|
|
507
1292
|
return param;
|
|
508
1293
|
}
|
|
509
|
-
param = `${this._prepend(
|
|
510
|
-
|
|
1294
|
+
param = `${this._prepend(state)}${options.sort}=`;
|
|
1295
|
+
state.sorts.forEach((sort, idx) => {
|
|
511
1296
|
param += `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`;
|
|
512
|
-
if (idx <
|
|
1297
|
+
if (idx < state.sorts.length - 1) {
|
|
513
1298
|
param += ',';
|
|
514
1299
|
}
|
|
515
1300
|
});
|
|
516
1301
|
this._uri += param;
|
|
517
1302
|
return param;
|
|
518
1303
|
}
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
1304
|
+
/**
|
|
1305
|
+
* Determine the appropriate URI prefix based on the current accumulator state
|
|
1306
|
+
*
|
|
1307
|
+
* Returns the full base path with `?` for the first parameter,
|
|
1308
|
+
* or `&` for subsequent parameters.
|
|
1309
|
+
*
|
|
1310
|
+
* @param state - The current query builder state
|
|
1311
|
+
* @returns The prefix string to prepend to the next parameter
|
|
1312
|
+
*/
|
|
1313
|
+
_prepend(state) {
|
|
1314
|
+
if (this._uri) {
|
|
1315
|
+
return '&';
|
|
1316
|
+
}
|
|
1317
|
+
return state.baseUrl ? `${state.baseUrl}/${state.resource}?` : `/${state.resource}?`;
|
|
1318
|
+
}
|
|
1319
|
+
}
|
|
1320
|
+
|
|
1321
|
+
/**
|
|
1322
|
+
* Response strategy for the JSON:API driver
|
|
1323
|
+
*
|
|
1324
|
+
* Parses JSON:API pagination responses:
|
|
1325
|
+
* ```json
|
|
1326
|
+
* {
|
|
1327
|
+
* "data": [...],
|
|
1328
|
+
* "meta": {
|
|
1329
|
+
* "current-page": 1,
|
|
1330
|
+
* "per-page": 10,
|
|
1331
|
+
* "total": 100,
|
|
1332
|
+
* "page-count": 10,
|
|
1333
|
+
* "from": 1,
|
|
1334
|
+
* "to": 10
|
|
1335
|
+
* },
|
|
1336
|
+
* "links": {
|
|
1337
|
+
* "first": "url",
|
|
1338
|
+
* "prev": "url",
|
|
1339
|
+
* "next": "url",
|
|
1340
|
+
* "last": "url"
|
|
1341
|
+
* }
|
|
1342
|
+
* }
|
|
1343
|
+
* ```
|
|
1344
|
+
*
|
|
1345
|
+
* @see https://jsonapi.org/format/
|
|
1346
|
+
*/
|
|
1347
|
+
class JsonApiResponseStrategy {
|
|
1348
|
+
/**
|
|
1349
|
+
* Parse a JSON:API pagination response into a PaginatedCollection
|
|
1350
|
+
*
|
|
1351
|
+
* Supports dot-notation key paths for accessing nested values.
|
|
1352
|
+
* Computes `from` and `to` from `currentPage` and `perPage` when
|
|
1353
|
+
* they are not directly available in the response.
|
|
1354
|
+
*
|
|
1355
|
+
* @param response - The raw API response object
|
|
1356
|
+
* @param options - The response key name configuration
|
|
1357
|
+
* @returns A typed PaginatedCollection instance
|
|
1358
|
+
*/
|
|
1359
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1360
|
+
paginate(response, options) {
|
|
1361
|
+
const data = this._resolve(response, options.data);
|
|
1362
|
+
const currentPage = this._resolve(response, options.currentPage);
|
|
1363
|
+
const total = this._resolve(response, options.total);
|
|
1364
|
+
const perPage = this._resolve(response, options.perPage);
|
|
1365
|
+
const lastPage = this._resolve(response, options.lastPage);
|
|
1366
|
+
// Compute from/to if not directly available
|
|
1367
|
+
const from = this._resolveFrom(response, options, currentPage, perPage);
|
|
1368
|
+
const to = this._resolveTo(response, options, currentPage, perPage, total);
|
|
1369
|
+
const prevPageUrl = this._resolve(response, options.prevPageUrl);
|
|
1370
|
+
const nextPageUrl = this._resolve(response, options.nextPageUrl);
|
|
1371
|
+
const firstPageUrl = this._resolve(response, options.firstPageUrl);
|
|
1372
|
+
const lastPageUrl = this._resolve(response, options.lastPageUrl);
|
|
1373
|
+
return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl, nextPageUrl, lastPage, firstPageUrl, lastPageUrl);
|
|
1374
|
+
}
|
|
1375
|
+
/**
|
|
1376
|
+
* Resolve a value from a response object using a dot-notation path
|
|
1377
|
+
*
|
|
1378
|
+
* Supports both flat keys ('data') and nested paths ('meta.current-page').
|
|
1379
|
+
*
|
|
1380
|
+
* @param response - The raw response object
|
|
1381
|
+
* @param path - The dot-notation path to resolve
|
|
1382
|
+
* @returns The resolved value, or undefined if not found
|
|
1383
|
+
*/
|
|
1384
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1385
|
+
_resolve(response, path) {
|
|
1386
|
+
return path.split('.').reduce((obj, key) => obj?.[key], response);
|
|
1387
|
+
}
|
|
1388
|
+
/**
|
|
1389
|
+
* Resolve the "from" index value
|
|
1390
|
+
*
|
|
1391
|
+
* If the path resolves to a value in the response, use it.
|
|
1392
|
+
* Otherwise, compute it from currentPage and perPage:
|
|
1393
|
+
* `(currentPage - 1) * perPage + 1`
|
|
1394
|
+
*
|
|
1395
|
+
* @param response - The raw response object
|
|
1396
|
+
* @param options - The response key name configuration
|
|
1397
|
+
* @param currentPage - The current page number
|
|
1398
|
+
* @param perPage - The number of items per page
|
|
1399
|
+
* @returns The computed "from" index
|
|
1400
|
+
*/
|
|
1401
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1402
|
+
_resolveFrom(response, options, currentPage, perPage) {
|
|
1403
|
+
const direct = this._resolve(response, options.from);
|
|
1404
|
+
if (direct !== undefined) {
|
|
1405
|
+
return direct;
|
|
1406
|
+
}
|
|
1407
|
+
if (currentPage && perPage) {
|
|
1408
|
+
return (currentPage - 1) * perPage + 1;
|
|
1409
|
+
}
|
|
1410
|
+
return undefined;
|
|
1411
|
+
}
|
|
1412
|
+
/**
|
|
1413
|
+
* Resolve the "to" index value
|
|
1414
|
+
*
|
|
1415
|
+
* If the path resolves to a value in the response, use it.
|
|
1416
|
+
* Otherwise, compute it from currentPage, perPage, and total:
|
|
1417
|
+
* `Math.min(currentPage * perPage, total)`
|
|
1418
|
+
*
|
|
1419
|
+
* @param response - The raw response object
|
|
1420
|
+
* @param options - The response key name configuration
|
|
1421
|
+
* @param currentPage - The current page number
|
|
1422
|
+
* @param perPage - The number of items per page
|
|
1423
|
+
* @param total - The total number of items
|
|
1424
|
+
* @returns The computed "to" index
|
|
1425
|
+
*/
|
|
1426
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1427
|
+
_resolveTo(response, options, currentPage, perPage, total) {
|
|
1428
|
+
const direct = this._resolve(response, options.to);
|
|
1429
|
+
if (direct !== undefined) {
|
|
1430
|
+
return direct;
|
|
1431
|
+
}
|
|
1432
|
+
if (currentPage && perPage && total) {
|
|
1433
|
+
return Math.min(currentPage * perPage, total);
|
|
1434
|
+
}
|
|
1435
|
+
return undefined;
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1438
|
+
|
|
1439
|
+
/**
|
|
1440
|
+
* Request strategy for the Laravel (pagination-only) driver
|
|
1441
|
+
*
|
|
1442
|
+
* Generates simple pagination URIs:
|
|
1443
|
+
* - `/{resource}?limit=N&page=N`
|
|
1444
|
+
*
|
|
1445
|
+
* Filters, sorts, fields, includes, search, and select in state are ignored.
|
|
1446
|
+
*/
|
|
1447
|
+
class LaravelRequestStrategy {
|
|
1448
|
+
/**
|
|
1449
|
+
* Build a pagination-only URI from the given state
|
|
1450
|
+
*
|
|
1451
|
+
* @param state - The current query builder state
|
|
1452
|
+
* @param options - The query parameter key name configuration
|
|
1453
|
+
* @returns The composed URI string
|
|
1454
|
+
* @throws Error if resource is not set
|
|
1455
|
+
*/
|
|
1456
|
+
buildUri(state, options) {
|
|
1457
|
+
if (!state.resource) {
|
|
1458
|
+
throw new Error('Set the resource property BEFORE calling the url() / get() methods');
|
|
1459
|
+
}
|
|
1460
|
+
const base = state.baseUrl ? `${state.baseUrl}/${state.resource}` : `/${state.resource}`;
|
|
1461
|
+
return `${base}?${options.limit}=${state.limit}&${options.page}=${state.page}`;
|
|
1462
|
+
}
|
|
1463
|
+
/**
|
|
1464
|
+
* Validate that the given limit is accepted by the Laravel driver
|
|
1465
|
+
*
|
|
1466
|
+
* Laravel pagination does not recognize `-1` as a "fetch all" sentinel,
|
|
1467
|
+
* so only positive integers are accepted.
|
|
1468
|
+
*
|
|
1469
|
+
* @param limit - The limit value to validate
|
|
1470
|
+
* @throws {InvalidLimitError} If the value is not a positive integer
|
|
1471
|
+
*/
|
|
1472
|
+
validateLimit(limit) {
|
|
1473
|
+
if (Number.isInteger(limit) && limit >= 1) {
|
|
1474
|
+
return;
|
|
1475
|
+
}
|
|
1476
|
+
throw new InvalidLimitError(limit);
|
|
1477
|
+
}
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
/**
|
|
1481
|
+
* Response strategy for the Laravel (pagination-only) driver
|
|
1482
|
+
*
|
|
1483
|
+
* Parses flat Laravel pagination responses:
|
|
1484
|
+
* ```json
|
|
1485
|
+
* {
|
|
1486
|
+
* "data": [...],
|
|
1487
|
+
* "current_page": 1,
|
|
1488
|
+
* "total": 100,
|
|
1489
|
+
* "per_page": 15,
|
|
1490
|
+
* "from": 1,
|
|
1491
|
+
* "to": 15,
|
|
1492
|
+
* ...
|
|
1493
|
+
* }
|
|
1494
|
+
* ```
|
|
1495
|
+
*/
|
|
1496
|
+
class LaravelResponseStrategy {
|
|
1497
|
+
/**
|
|
1498
|
+
* Parse a flat Laravel pagination response into a PaginatedCollection
|
|
1499
|
+
*
|
|
1500
|
+
* @param response - The raw API response object
|
|
1501
|
+
* @param options - The response key name configuration
|
|
1502
|
+
* @returns A typed PaginatedCollection instance
|
|
1503
|
+
*/
|
|
1504
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1505
|
+
paginate(response, options) {
|
|
1506
|
+
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]);
|
|
1507
|
+
}
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
/**
|
|
1511
|
+
* Request strategy for the NestJS (nestjs-paginate) driver
|
|
1512
|
+
*
|
|
1513
|
+
* Generates URIs in the NestJS paginate format:
|
|
1514
|
+
* - Simple filters: `filter.field=value`
|
|
1515
|
+
* - Operator filters: `filter.field=$operator:value`
|
|
1516
|
+
* - Sorts: `sortBy=field1:DESC,field2:ASC`
|
|
1517
|
+
* - Select: `select=col1,col2`
|
|
1518
|
+
* - Search: `search=term`
|
|
1519
|
+
* - Pagination: `limit=N&page=N`
|
|
1520
|
+
*
|
|
1521
|
+
* @see https://github.com/ppetzold/nestjs-paginate
|
|
1522
|
+
*/
|
|
1523
|
+
class NestjsRequestStrategy {
|
|
1524
|
+
/**
|
|
1525
|
+
* Accumulator for composing the URI string
|
|
1526
|
+
*/
|
|
1527
|
+
_uri = '';
|
|
1528
|
+
/**
|
|
1529
|
+
* Build a URI string from the given state using the NestJS paginate format
|
|
1530
|
+
*
|
|
1531
|
+
* @param state - The current query builder state
|
|
1532
|
+
* @param options - The query parameter key name configuration
|
|
1533
|
+
* @returns The composed URI string
|
|
1534
|
+
* @throws Error if model is not set
|
|
1535
|
+
*/
|
|
1536
|
+
buildUri(state, options) {
|
|
1537
|
+
if (!state.resource) {
|
|
1538
|
+
throw new Error('Set the resource property BEFORE adding filters or calling the url() / get() methods');
|
|
522
1539
|
}
|
|
523
|
-
// Cleanup the previously generated URI
|
|
524
1540
|
this._uri = '';
|
|
525
|
-
this.
|
|
526
|
-
this.
|
|
527
|
-
this.
|
|
528
|
-
this.
|
|
529
|
-
this.
|
|
530
|
-
this.
|
|
1541
|
+
this._parseFilters(state, options);
|
|
1542
|
+
this._parseOperatorFilters(state, options);
|
|
1543
|
+
this._parseSort(state, options);
|
|
1544
|
+
this._parseSelect(state, options);
|
|
1545
|
+
this._parseSearch(state, options);
|
|
1546
|
+
this._parseLimit(state, options);
|
|
1547
|
+
this._parsePage(state, options);
|
|
531
1548
|
return this._uri;
|
|
532
1549
|
}
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
1550
|
+
/**
|
|
1551
|
+
* Validate that the given limit is accepted by nestjs-paginate
|
|
1552
|
+
*
|
|
1553
|
+
* Accepts any integer `>= 1` as a page size, plus `-1` which nestjs-paginate
|
|
1554
|
+
* interprets as "fetch all items" (server must opt-in via `maxLimit: -1`).
|
|
1555
|
+
*
|
|
1556
|
+
* @param limit - The limit value to validate
|
|
1557
|
+
* @throws {InvalidLimitError} If the value is not an integer, or is 0, or is a negative number other than -1
|
|
1558
|
+
*/
|
|
1559
|
+
validateLimit(limit) {
|
|
1560
|
+
if (Number.isInteger(limit) && (limit === -1 || limit >= 1)) {
|
|
1561
|
+
return;
|
|
1562
|
+
}
|
|
1563
|
+
throw new InvalidLimitError(limit, true);
|
|
1564
|
+
}
|
|
1565
|
+
/**
|
|
1566
|
+
* Parse and append simple filter parameters
|
|
1567
|
+
*
|
|
1568
|
+
* Generates: `filter.field=value1,value2` for each filter
|
|
1569
|
+
*
|
|
1570
|
+
* @param state - The current query builder state
|
|
1571
|
+
* @param options - The query parameter key name configuration
|
|
1572
|
+
*/
|
|
1573
|
+
_parseFilters(state, options) {
|
|
1574
|
+
const keys = Object.keys(state.filters);
|
|
1575
|
+
if (!keys.length) {
|
|
1576
|
+
return;
|
|
1577
|
+
}
|
|
1578
|
+
keys.forEach(key => {
|
|
1579
|
+
const values = state.filters[key].join(',');
|
|
1580
|
+
const param = `${this._prepend(state)}${options.filters}.${key}=${values}`;
|
|
1581
|
+
this._uri += param;
|
|
1582
|
+
});
|
|
1583
|
+
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Parse and append the limit parameter
|
|
1586
|
+
*
|
|
1587
|
+
* @param state - The current query builder state
|
|
1588
|
+
* @param options - The query parameter key name configuration
|
|
1589
|
+
*/
|
|
1590
|
+
_parseLimit(state, options) {
|
|
1591
|
+
const param = `${this._prepend(state)}${options.limit}=${state.limit}`;
|
|
1592
|
+
this._uri += param;
|
|
1593
|
+
}
|
|
1594
|
+
/**
|
|
1595
|
+
* Parse and append operator filter parameters
|
|
1596
|
+
*
|
|
1597
|
+
* Groups operator filters by field and generates:
|
|
1598
|
+
* - Single value: `filter.field=$operator:value`
|
|
1599
|
+
* - Multiple values ($in, $btw): `filter.field=$operator:val1,val2`
|
|
1600
|
+
*
|
|
1601
|
+
* @param state - The current query builder state
|
|
1602
|
+
* @param options - The query parameter key name configuration
|
|
1603
|
+
*/
|
|
1604
|
+
_parseOperatorFilters(state, options) {
|
|
1605
|
+
if (!state.operatorFilters.length) {
|
|
1606
|
+
return;
|
|
1607
|
+
}
|
|
1608
|
+
state.operatorFilters.forEach((opFilter) => {
|
|
1609
|
+
const values = opFilter.values.join(',');
|
|
1610
|
+
const param = `${this._prepend(state)}${options.filters}.${opFilter.field}=${opFilter.operator}:${values}`;
|
|
1611
|
+
this._uri += param;
|
|
1612
|
+
});
|
|
1613
|
+
}
|
|
1614
|
+
/**
|
|
1615
|
+
* Parse and append the page parameter
|
|
1616
|
+
*
|
|
1617
|
+
* @param state - The current query builder state
|
|
1618
|
+
* @param options - The query parameter key name configuration
|
|
1619
|
+
*/
|
|
1620
|
+
_parsePage(state, options) {
|
|
1621
|
+
const param = `${this._prepend(state)}${options.page}=${state.page}`;
|
|
1622
|
+
this._uri += param;
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Parse and append the search parameter
|
|
1626
|
+
*
|
|
1627
|
+
* Generates: `search=term`
|
|
1628
|
+
*
|
|
1629
|
+
* @param state - The current query builder state
|
|
1630
|
+
* @param options - The query parameter key name configuration
|
|
1631
|
+
*/
|
|
1632
|
+
_parseSearch(state, options) {
|
|
1633
|
+
if (!state.search) {
|
|
1634
|
+
return;
|
|
1635
|
+
}
|
|
1636
|
+
const param = `${this._prepend(state)}${options.search}=${state.search}`;
|
|
1637
|
+
this._uri += param;
|
|
1638
|
+
}
|
|
1639
|
+
/**
|
|
1640
|
+
* Parse and append the select parameter
|
|
1641
|
+
*
|
|
1642
|
+
* Generates: `select=col1,col2`
|
|
1643
|
+
*
|
|
1644
|
+
* @param state - The current query builder state
|
|
1645
|
+
* @param options - The query parameter key name configuration
|
|
1646
|
+
*/
|
|
1647
|
+
_parseSelect(state, options) {
|
|
1648
|
+
if (!state.select.length) {
|
|
1649
|
+
return;
|
|
538
1650
|
}
|
|
539
|
-
|
|
1651
|
+
const param = `${this._prepend(state)}${options.select}=${state.select.join(',')}`;
|
|
1652
|
+
this._uri += param;
|
|
540
1653
|
}
|
|
541
1654
|
/**
|
|
542
|
-
*
|
|
1655
|
+
* Parse and append sort parameters
|
|
543
1656
|
*
|
|
544
|
-
*
|
|
545
|
-
*
|
|
546
|
-
* @
|
|
1657
|
+
* Generates: `sortBy=field1:DESC,field2:ASC`
|
|
1658
|
+
*
|
|
1659
|
+
* @param state - The current query builder state
|
|
1660
|
+
* @param options - The query parameter key name configuration
|
|
547
1661
|
*/
|
|
548
|
-
|
|
549
|
-
if (!
|
|
550
|
-
return
|
|
1662
|
+
_parseSort(state, options) {
|
|
1663
|
+
if (!state.sorts.length) {
|
|
1664
|
+
return;
|
|
551
1665
|
}
|
|
552
|
-
|
|
553
|
-
|
|
1666
|
+
const sortPairs = state.sorts.map(sort => `${sort.field}:${sort.order === SortEnum.DESC ? 'DESC' : 'ASC'}`);
|
|
1667
|
+
const param = `${this._prepend(state)}${options.sortBy}=${sortPairs.join(',')}`;
|
|
1668
|
+
this._uri += param;
|
|
554
1669
|
}
|
|
555
1670
|
/**
|
|
556
|
-
*
|
|
557
|
-
* I.e. filter[field]=1 or filter[field]=1,2,3
|
|
1671
|
+
* Determine the appropriate URI prefix based on the current accumulator state
|
|
558
1672
|
*
|
|
559
|
-
*
|
|
560
|
-
*
|
|
561
|
-
*
|
|
1673
|
+
* Returns the full base path with `?` for the first parameter,
|
|
1674
|
+
* or `&` for subsequent parameters.
|
|
1675
|
+
*
|
|
1676
|
+
* @param state - The current query builder state
|
|
1677
|
+
* @returns The prefix string to prepend to the next parameter
|
|
562
1678
|
*/
|
|
563
|
-
|
|
564
|
-
if (
|
|
565
|
-
return
|
|
1679
|
+
_prepend(state) {
|
|
1680
|
+
if (this._uri) {
|
|
1681
|
+
return '&';
|
|
566
1682
|
}
|
|
567
|
-
|
|
568
|
-
[field]: values
|
|
569
|
-
});
|
|
570
|
-
return this;
|
|
1683
|
+
return state.baseUrl ? `${state.baseUrl}/${state.resource}?` : `/${state.resource}?`;
|
|
571
1684
|
}
|
|
1685
|
+
}
|
|
1686
|
+
|
|
1687
|
+
/**
|
|
1688
|
+
* Response strategy for the NestJS (nestjs-paginate) driver
|
|
1689
|
+
*
|
|
1690
|
+
* Parses nested NestJS pagination responses:
|
|
1691
|
+
* ```json
|
|
1692
|
+
* {
|
|
1693
|
+
* "data": [...],
|
|
1694
|
+
* "meta": {
|
|
1695
|
+
* "currentPage": 1,
|
|
1696
|
+
* "totalItems": 100,
|
|
1697
|
+
* "itemsPerPage": 10,
|
|
1698
|
+
* "totalPages": 10
|
|
1699
|
+
* },
|
|
1700
|
+
* "links": {
|
|
1701
|
+
* "first": "url",
|
|
1702
|
+
* "previous": "url",
|
|
1703
|
+
* "next": "url",
|
|
1704
|
+
* "last": "url",
|
|
1705
|
+
* "current": "url"
|
|
1706
|
+
* }
|
|
1707
|
+
* }
|
|
1708
|
+
* ```
|
|
1709
|
+
*
|
|
1710
|
+
* @see https://github.com/ppetzold/nestjs-paginate
|
|
1711
|
+
*/
|
|
1712
|
+
class NestjsResponseStrategy {
|
|
572
1713
|
/**
|
|
573
|
-
*
|
|
1714
|
+
* Parse a nested NestJS pagination response into a PaginatedCollection
|
|
574
1715
|
*
|
|
575
|
-
*
|
|
576
|
-
*
|
|
1716
|
+
* Supports dot-notation key paths for accessing nested values.
|
|
1717
|
+
* Computes `from` and `to` from `currentPage` and `itemsPerPage` when
|
|
1718
|
+
* they are not directly available in the response.
|
|
1719
|
+
*
|
|
1720
|
+
* @param response - The raw API response object
|
|
1721
|
+
* @param options - The response key name configuration
|
|
1722
|
+
* @returns A typed PaginatedCollection instance
|
|
577
1723
|
*/
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
this.
|
|
583
|
-
|
|
1724
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1725
|
+
paginate(response, options) {
|
|
1726
|
+
const data = this._resolve(response, options.data);
|
|
1727
|
+
const currentPage = this._resolve(response, options.currentPage);
|
|
1728
|
+
const total = this._resolve(response, options.total);
|
|
1729
|
+
const perPage = this._resolve(response, options.perPage);
|
|
1730
|
+
const lastPage = this._resolve(response, options.lastPage);
|
|
1731
|
+
// Compute from/to if not directly available
|
|
1732
|
+
const from = this._resolveFrom(response, options, currentPage, perPage);
|
|
1733
|
+
const to = this._resolveTo(response, options, currentPage, perPage, total);
|
|
1734
|
+
const prevPageUrl = this._resolve(response, options.prevPageUrl);
|
|
1735
|
+
const nextPageUrl = this._resolve(response, options.nextPageUrl);
|
|
1736
|
+
const firstPageUrl = this._resolve(response, options.firstPageUrl);
|
|
1737
|
+
const lastPageUrl = this._resolve(response, options.lastPageUrl);
|
|
1738
|
+
return new PaginatedCollection(data, currentPage, from, to, total, perPage, prevPageUrl, nextPageUrl, lastPage, firstPageUrl, lastPageUrl);
|
|
584
1739
|
}
|
|
585
1740
|
/**
|
|
586
|
-
*
|
|
1741
|
+
* Resolve a value from a response object using a dot-notation path
|
|
587
1742
|
*
|
|
588
|
-
*
|
|
589
|
-
*
|
|
590
|
-
* @
|
|
1743
|
+
* Supports both flat keys ('data') and nested paths ('meta.currentPage').
|
|
1744
|
+
*
|
|
1745
|
+
* @param response - The raw response object
|
|
1746
|
+
* @param path - The dot-notation path to resolve
|
|
1747
|
+
* @returns The resolved value, or undefined if not found
|
|
591
1748
|
*/
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
order
|
|
596
|
-
});
|
|
597
|
-
return this;
|
|
1749
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1750
|
+
_resolve(response, path) {
|
|
1751
|
+
return path.split('.').reduce((obj, key) => obj?.[key], response);
|
|
598
1752
|
}
|
|
599
1753
|
/**
|
|
600
|
-
*
|
|
1754
|
+
* Resolve the "from" index value
|
|
601
1755
|
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
* address: ['zipcode']
|
|
606
|
-
* });
|
|
607
|
-
* ```
|
|
1756
|
+
* If the path resolves to a value in the response, use it.
|
|
1757
|
+
* Otherwise, compute it from currentPage and perPage:
|
|
1758
|
+
* `(currentPage - 1) * perPage + 1`
|
|
608
1759
|
*
|
|
609
|
-
* @param
|
|
610
|
-
* @
|
|
1760
|
+
* @param response - The raw response object
|
|
1761
|
+
* @param options - The response key name configuration
|
|
1762
|
+
* @param currentPage - The current page number
|
|
1763
|
+
* @param perPage - The number of items per page
|
|
1764
|
+
* @returns The computed "from" index
|
|
611
1765
|
*/
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
1766
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1767
|
+
_resolveFrom(response, options, currentPage, perPage) {
|
|
1768
|
+
const direct = this._resolve(response, options.from);
|
|
1769
|
+
if (direct !== undefined) {
|
|
1770
|
+
return direct;
|
|
1771
|
+
}
|
|
1772
|
+
if (currentPage && perPage) {
|
|
1773
|
+
return (currentPage - 1) * perPage + 1;
|
|
1774
|
+
}
|
|
1775
|
+
return undefined;
|
|
615
1776
|
}
|
|
616
1777
|
/**
|
|
617
|
-
*
|
|
1778
|
+
* Resolve the "to" index value
|
|
618
1779
|
*
|
|
619
|
-
*
|
|
620
|
-
*
|
|
621
|
-
*
|
|
1780
|
+
* If the path resolves to a value in the response, use it.
|
|
1781
|
+
* Otherwise, compute it from currentPage, perPage, and total:
|
|
1782
|
+
* `Math.min(currentPage * perPage, total)`
|
|
622
1783
|
*
|
|
623
|
-
* @param
|
|
624
|
-
* @param
|
|
625
|
-
* @
|
|
1784
|
+
* @param response - The raw response object
|
|
1785
|
+
* @param options - The response key name configuration
|
|
1786
|
+
* @param currentPage - The current page number
|
|
1787
|
+
* @param perPage - The number of items per page
|
|
1788
|
+
* @param total - The total number of items
|
|
1789
|
+
* @returns The computed "to" index
|
|
626
1790
|
*/
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
1791
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1792
|
+
_resolveTo(response, options, currentPage, perPage, total) {
|
|
1793
|
+
const direct = this._resolve(response, options.to);
|
|
1794
|
+
if (direct !== undefined) {
|
|
1795
|
+
return direct;
|
|
630
1796
|
}
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
}
|
|
634
|
-
return
|
|
1797
|
+
if (currentPage && perPage && total) {
|
|
1798
|
+
return Math.min(currentPage * perPage, total);
|
|
1799
|
+
}
|
|
1800
|
+
return undefined;
|
|
635
1801
|
}
|
|
1802
|
+
}
|
|
1803
|
+
|
|
1804
|
+
/**
|
|
1805
|
+
* Request strategy for the Spatie Query Builder driver
|
|
1806
|
+
*
|
|
1807
|
+
* Generates URIs in the Spatie format:
|
|
1808
|
+
* - Fields: `fields[model]=col1,col2`
|
|
1809
|
+
* - Filters: `filter[field]=value`
|
|
1810
|
+
* - Includes: `include=model1,model2`
|
|
1811
|
+
* - Sorts: `sort=-field1,field2` (- prefix = DESC)
|
|
1812
|
+
* - Pagination: `limit=N&page=N`
|
|
1813
|
+
*
|
|
1814
|
+
* @see https://spatie.be/docs/laravel-query-builder
|
|
1815
|
+
*/
|
|
1816
|
+
class SpatieRequestStrategy {
|
|
1817
|
+
/**
|
|
1818
|
+
* Accumulator for composing the URI string
|
|
1819
|
+
*/
|
|
1820
|
+
_uri = '';
|
|
636
1821
|
/**
|
|
637
|
-
*
|
|
1822
|
+
* Build a URI string from the given state using the Spatie format
|
|
638
1823
|
*
|
|
639
|
-
* @param
|
|
640
|
-
* @
|
|
1824
|
+
* @param state - The current query builder state
|
|
1825
|
+
* @param options - The query parameter key name configuration
|
|
1826
|
+
* @returns The composed URI string
|
|
1827
|
+
* @throws Error if resource is not set
|
|
641
1828
|
*/
|
|
642
|
-
|
|
643
|
-
if (!
|
|
644
|
-
|
|
1829
|
+
buildUri(state, options) {
|
|
1830
|
+
if (!state.resource) {
|
|
1831
|
+
throw new Error('Set the resource property BEFORE adding filters or calling the url() / get() methods');
|
|
645
1832
|
}
|
|
646
|
-
this.
|
|
647
|
-
|
|
1833
|
+
this._uri = '';
|
|
1834
|
+
this._parseIncludes(state, options);
|
|
1835
|
+
this._parseFields(state, options);
|
|
1836
|
+
this._parseFilters(state, options);
|
|
1837
|
+
this._parseLimit(state, options);
|
|
1838
|
+
this._parsePage(state, options);
|
|
1839
|
+
this._parseSort(state, options);
|
|
1840
|
+
return this._uri;
|
|
648
1841
|
}
|
|
649
1842
|
/**
|
|
650
|
-
*
|
|
1843
|
+
* Validate that the given limit is accepted by the Spatie driver
|
|
651
1844
|
*
|
|
652
|
-
*
|
|
653
|
-
*
|
|
1845
|
+
* Spatie query-builder does not recognize `-1` as a "fetch all" sentinel,
|
|
1846
|
+
* so only positive integers are accepted.
|
|
1847
|
+
*
|
|
1848
|
+
* @param limit - The limit value to validate
|
|
1849
|
+
* @throws {InvalidLimitError} If the value is not a positive integer
|
|
654
1850
|
*/
|
|
655
|
-
|
|
656
|
-
if (
|
|
657
|
-
return
|
|
1851
|
+
validateLimit(limit) {
|
|
1852
|
+
if (Number.isInteger(limit) && limit >= 1) {
|
|
1853
|
+
return;
|
|
658
1854
|
}
|
|
659
|
-
|
|
660
|
-
return this;
|
|
1855
|
+
throw new InvalidLimitError(limit);
|
|
661
1856
|
}
|
|
662
1857
|
/**
|
|
663
|
-
*
|
|
1858
|
+
* Parse and append field selection parameters
|
|
664
1859
|
*
|
|
665
|
-
*
|
|
666
|
-
*
|
|
1860
|
+
* Validates that each field model exists either as the main resource
|
|
1861
|
+
* or in the includes list. Fields are grouped by model in bracket notation.
|
|
1862
|
+
*
|
|
1863
|
+
* @param state - The current query builder state
|
|
1864
|
+
* @param options - The query parameter key name configuration
|
|
1865
|
+
* @returns The generated field selection parameter string
|
|
1866
|
+
* @throws Error if resource is required but not set
|
|
1867
|
+
* @throws UnselectableModelError if a field model is not in resource or includes
|
|
667
1868
|
*/
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
1869
|
+
_parseFields(state, options) {
|
|
1870
|
+
if (!Object.keys(state.fields).length) {
|
|
1871
|
+
return this._uri;
|
|
1872
|
+
}
|
|
1873
|
+
if (!state.resource) {
|
|
1874
|
+
throw new Error('While selecting fields, the -> resource <- is required');
|
|
1875
|
+
}
|
|
1876
|
+
if (!(state.resource in state.fields)) {
|
|
1877
|
+
throw new Error(`Key ${state.resource} is missing in the fields object`);
|
|
1878
|
+
}
|
|
1879
|
+
const f = {};
|
|
1880
|
+
for (const k in state.fields) {
|
|
1881
|
+
if (state.fields.hasOwnProperty(k)) {
|
|
1882
|
+
if (k !== state.resource && !state.includes.includes(k)) {
|
|
1883
|
+
throw new UnselectableModelError(k);
|
|
1884
|
+
}
|
|
1885
|
+
Object.assign(f, { [`${options.fields}[${k}]`]: state.fields[k].join(',') });
|
|
1886
|
+
}
|
|
1887
|
+
}
|
|
1888
|
+
const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
|
|
1889
|
+
this._uri += param;
|
|
1890
|
+
return param;
|
|
671
1891
|
}
|
|
672
1892
|
/**
|
|
673
|
-
*
|
|
1893
|
+
* Parse and append filter parameters
|
|
674
1894
|
*
|
|
675
|
-
*
|
|
1895
|
+
* Generates filter parameters in bracket notation: `filter[key]=value1,value2`
|
|
1896
|
+
*
|
|
1897
|
+
* @param state - The current query builder state
|
|
1898
|
+
* @param options - The query parameter key name configuration
|
|
1899
|
+
* @returns The generated filter parameter string
|
|
676
1900
|
*/
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
return this.
|
|
681
|
-
}
|
|
682
|
-
catch (error) {
|
|
683
|
-
return throwError(() => error);
|
|
1901
|
+
_parseFilters(state, options) {
|
|
1902
|
+
const keys = Object.keys(state.filters);
|
|
1903
|
+
if (!keys.length) {
|
|
1904
|
+
return this._uri;
|
|
684
1905
|
}
|
|
1906
|
+
const f = {
|
|
1907
|
+
[`${options.filters}`]: keys.reduce((acc, key) => {
|
|
1908
|
+
return Object.assign(acc, { [key]: state.filters[key].join(',') });
|
|
1909
|
+
}, {})
|
|
1910
|
+
};
|
|
1911
|
+
const param = `${this._prepend(state)}${qs.stringify(f, { encode: false })}`;
|
|
1912
|
+
this._uri += param;
|
|
1913
|
+
return param;
|
|
685
1914
|
}
|
|
686
1915
|
/**
|
|
687
|
-
*
|
|
1916
|
+
* Parse and append include parameters
|
|
688
1917
|
*
|
|
689
|
-
*
|
|
1918
|
+
* Generates: `include=model1,model2`
|
|
1919
|
+
*
|
|
1920
|
+
* @param state - The current query builder state
|
|
1921
|
+
* @param options - The query parameter key name configuration
|
|
1922
|
+
* @returns The generated include parameter string
|
|
690
1923
|
*/
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
|
|
1924
|
+
_parseIncludes(state, options) {
|
|
1925
|
+
if (!state.includes.length) {
|
|
1926
|
+
return this._uri;
|
|
1927
|
+
}
|
|
1928
|
+
const param = `${this._prepend(state)}${options.includes}=${state.includes}`;
|
|
1929
|
+
this._uri += param;
|
|
1930
|
+
return param;
|
|
694
1931
|
}
|
|
695
1932
|
/**
|
|
696
|
-
*
|
|
1933
|
+
* Parse and append the limit parameter
|
|
697
1934
|
*
|
|
698
|
-
* @param
|
|
699
|
-
* @
|
|
1935
|
+
* @param state - The current query builder state
|
|
1936
|
+
* @param options - The query parameter key name configuration
|
|
1937
|
+
* @returns The generated limit parameter string
|
|
700
1938
|
*/
|
|
701
|
-
|
|
702
|
-
this.
|
|
703
|
-
|
|
1939
|
+
_parseLimit(state, options) {
|
|
1940
|
+
const param = `${this._prepend(state)}${options.limit}=${state.limit}`;
|
|
1941
|
+
this._uri += param;
|
|
1942
|
+
return param;
|
|
704
1943
|
}
|
|
705
1944
|
/**
|
|
706
|
-
*
|
|
1945
|
+
* Parse and append the page parameter
|
|
707
1946
|
*
|
|
708
|
-
* @param
|
|
709
|
-
* @
|
|
1947
|
+
* @param state - The current query builder state
|
|
1948
|
+
* @param options - The query parameter key name configuration
|
|
1949
|
+
* @returns The generated page parameter string
|
|
710
1950
|
*/
|
|
711
|
-
|
|
712
|
-
this.
|
|
713
|
-
|
|
1951
|
+
_parsePage(state, options) {
|
|
1952
|
+
const param = `${this._prepend(state)}${options.page}=${state.page}`;
|
|
1953
|
+
this._uri += param;
|
|
1954
|
+
return param;
|
|
714
1955
|
}
|
|
715
1956
|
/**
|
|
716
|
-
*
|
|
717
|
-
* - I.e. the model "users" will return /users
|
|
1957
|
+
* Parse and append sort parameters
|
|
718
1958
|
*
|
|
719
|
-
*
|
|
720
|
-
*
|
|
1959
|
+
* Generates: `sort=-field1,field2` where `-` prefix indicates DESC order
|
|
1960
|
+
*
|
|
1961
|
+
* @param state - The current query builder state
|
|
1962
|
+
* @param options - The query parameter key name configuration
|
|
1963
|
+
* @returns The generated sort parameter string
|
|
721
1964
|
*/
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
1965
|
+
_parseSort(state, options) {
|
|
1966
|
+
let param = '';
|
|
1967
|
+
if (!state.sorts.length) {
|
|
1968
|
+
return param;
|
|
1969
|
+
}
|
|
1970
|
+
param = `${this._prepend(state)}${options.sort}=`;
|
|
1971
|
+
state.sorts.forEach((sort, idx) => {
|
|
1972
|
+
param += `${sort.order === SortEnum.DESC ? '-' : ''}${sort.field}`;
|
|
1973
|
+
if (idx < state.sorts.length - 1) {
|
|
1974
|
+
param += ',';
|
|
1975
|
+
}
|
|
1976
|
+
});
|
|
1977
|
+
this._uri += param;
|
|
1978
|
+
return param;
|
|
725
1979
|
}
|
|
726
1980
|
/**
|
|
727
|
-
*
|
|
1981
|
+
* Determine the appropriate URI prefix based on the current accumulator state
|
|
728
1982
|
*
|
|
729
|
-
*
|
|
730
|
-
*
|
|
1983
|
+
* Returns the full base path with `?` for the first parameter,
|
|
1984
|
+
* or `&` for subsequent parameters.
|
|
1985
|
+
*
|
|
1986
|
+
* @param state - The current query builder state
|
|
1987
|
+
* @returns The prefix string to prepend to the next parameter
|
|
731
1988
|
*/
|
|
732
|
-
|
|
733
|
-
this.
|
|
734
|
-
|
|
1989
|
+
_prepend(state) {
|
|
1990
|
+
if (this._uri) {
|
|
1991
|
+
return '&';
|
|
1992
|
+
}
|
|
1993
|
+
return state.baseUrl ? `${state.baseUrl}/${state.resource}?` : `/${state.resource}?`;
|
|
735
1994
|
}
|
|
736
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeService, deps: [{ token: NestService }, { token: 'QUERY_PARAMS_CONFIG', optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
737
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeService });
|
|
738
1995
|
}
|
|
739
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: NgQubeeService, decorators: [{
|
|
740
|
-
type: Injectable
|
|
741
|
-
}], ctorParameters: () => [{ type: NestService }, { type: undefined, decorators: [{
|
|
742
|
-
type: Inject,
|
|
743
|
-
args: ['QUERY_PARAMS_CONFIG']
|
|
744
|
-
}, {
|
|
745
|
-
type: Optional
|
|
746
|
-
}] }] });
|
|
747
1996
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
1997
|
+
/**
|
|
1998
|
+
* Response strategy for the Spatie Query Builder driver
|
|
1999
|
+
*
|
|
2000
|
+
* Parses flat Laravel pagination responses:
|
|
2001
|
+
* ```json
|
|
2002
|
+
* {
|
|
2003
|
+
* "data": [...],
|
|
2004
|
+
* "current_page": 1,
|
|
2005
|
+
* "total": 100,
|
|
2006
|
+
* "per_page": 15,
|
|
2007
|
+
* "from": 1,
|
|
2008
|
+
* "to": 15,
|
|
2009
|
+
* ...
|
|
2010
|
+
* }
|
|
2011
|
+
* ```
|
|
2012
|
+
*
|
|
2013
|
+
* @see https://spatie.be/docs/laravel-query-builder
|
|
2014
|
+
*/
|
|
2015
|
+
class SpatieResponseStrategy {
|
|
2016
|
+
/**
|
|
2017
|
+
* Parse a flat Laravel pagination response into a PaginatedCollection
|
|
2018
|
+
*
|
|
2019
|
+
* @param response - The raw API response object
|
|
2020
|
+
* @param options - The response key name configuration
|
|
2021
|
+
* @returns A typed PaginatedCollection instance
|
|
2022
|
+
*/
|
|
2023
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
2024
|
+
paginate(response, options) {
|
|
2025
|
+
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]);
|
|
774
2026
|
}
|
|
775
2027
|
}
|
|
776
2028
|
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
2029
|
+
/**
|
|
2030
|
+
* Resolve the request strategy instance for the given driver
|
|
2031
|
+
*
|
|
2032
|
+
* @param driver - The pagination driver
|
|
2033
|
+
* @returns The corresponding request strategy
|
|
2034
|
+
*/
|
|
2035
|
+
function resolveRequestStrategy$1(driver) {
|
|
2036
|
+
switch (driver) {
|
|
2037
|
+
case DriverEnum.JSON_API:
|
|
2038
|
+
return new JsonApiRequestStrategy();
|
|
2039
|
+
case DriverEnum.NESTJS:
|
|
2040
|
+
return new NestjsRequestStrategy();
|
|
2041
|
+
case DriverEnum.SPATIE:
|
|
2042
|
+
return new SpatieRequestStrategy();
|
|
2043
|
+
case DriverEnum.LARAVEL:
|
|
2044
|
+
return new LaravelRequestStrategy();
|
|
781
2045
|
}
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
2046
|
+
}
|
|
2047
|
+
/**
|
|
2048
|
+
* Resolve the response strategy instance for the given driver
|
|
2049
|
+
*
|
|
2050
|
+
* @param driver - The pagination driver
|
|
2051
|
+
* @returns The corresponding response strategy
|
|
2052
|
+
*/
|
|
2053
|
+
function resolveResponseStrategy$1(driver) {
|
|
2054
|
+
switch (driver) {
|
|
2055
|
+
case DriverEnum.JSON_API:
|
|
2056
|
+
return new JsonApiResponseStrategy();
|
|
2057
|
+
case DriverEnum.NESTJS:
|
|
2058
|
+
return new NestjsResponseStrategy();
|
|
2059
|
+
case DriverEnum.SPATIE:
|
|
2060
|
+
return new SpatieResponseStrategy();
|
|
2061
|
+
case DriverEnum.LARAVEL:
|
|
2062
|
+
return new LaravelResponseStrategy();
|
|
785
2063
|
}
|
|
786
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PaginationService, deps: [{ token: 'RESPONSE_OPTIONS', optional: true }], target: i0.ɵɵFactoryTarget.Injectable });
|
|
787
|
-
static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PaginationService });
|
|
788
2064
|
}
|
|
789
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.17", ngImport: i0, type: PaginationService, decorators: [{
|
|
790
|
-
type: Injectable
|
|
791
|
-
}], ctorParameters: () => [{ type: undefined, decorators: [{
|
|
792
|
-
type: Inject,
|
|
793
|
-
args: ['RESPONSE_OPTIONS']
|
|
794
|
-
}, {
|
|
795
|
-
type: Optional
|
|
796
|
-
}] }] });
|
|
797
|
-
|
|
798
2065
|
// @dynamic
|
|
799
2066
|
class NgQubeeModule {
|
|
800
|
-
|
|
2067
|
+
/**
|
|
2068
|
+
* Configure NgQubee for the root module
|
|
2069
|
+
*
|
|
2070
|
+
* @param config - Configuration object with driver, and optional request and response settings
|
|
2071
|
+
* @returns Module with providers configured for the specified driver
|
|
2072
|
+
*/
|
|
2073
|
+
static forRoot(config) {
|
|
2074
|
+
const driver = config.driver;
|
|
2075
|
+
const requestStrategy = resolveRequestStrategy$1(driver);
|
|
2076
|
+
const responseStrategy = resolveResponseStrategy$1(driver);
|
|
801
2077
|
return {
|
|
802
2078
|
ngModule: NgQubeeModule,
|
|
803
2079
|
providers: [
|
|
@@ -805,52 +2081,115 @@ class NgQubeeModule {
|
|
|
805
2081
|
{
|
|
806
2082
|
deps: [NestService],
|
|
807
2083
|
provide: NgQubeeService,
|
|
808
|
-
useFactory: (nestService) => new NgQubeeService(nestService, Object.assign({}, config.request))
|
|
809
|
-
},
|
|
2084
|
+
useFactory: (nestService) => new NgQubeeService(nestService, requestStrategy, driver, Object.assign({}, config.request))
|
|
2085
|
+
},
|
|
2086
|
+
{
|
|
810
2087
|
provide: PaginationService,
|
|
811
|
-
useFactory: () =>
|
|
2088
|
+
useFactory: () => {
|
|
2089
|
+
const responseConfig = Object.assign({}, config.response);
|
|
2090
|
+
if (driver === DriverEnum.JSON_API) {
|
|
2091
|
+
return new PaginationService(responseStrategy, new JsonApiResponseOptions(responseConfig));
|
|
2092
|
+
}
|
|
2093
|
+
return driver === DriverEnum.NESTJS
|
|
2094
|
+
? new PaginationService(responseStrategy, new NestjsResponseOptions(responseConfig))
|
|
2095
|
+
: new PaginationService(responseStrategy, responseConfig);
|
|
2096
|
+
}
|
|
812
2097
|
}
|
|
813
2098
|
]
|
|
814
2099
|
};
|
|
815
2100
|
}
|
|
816
|
-
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "
|
|
817
|
-
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "
|
|
818
|
-
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "
|
|
819
|
-
deps: [NestService],
|
|
820
|
-
provide: NgQubeeService,
|
|
821
|
-
useFactory: (nestService) => new NgQubeeService(nestService, {})
|
|
822
|
-
}] });
|
|
2101
|
+
static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule, deps: [], target: i0.ɵɵFactoryTarget.NgModule });
|
|
2102
|
+
static ɵmod = i0.ɵɵngDeclareNgModule({ minVersion: "14.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule });
|
|
2103
|
+
static ɵinj = i0.ɵɵngDeclareInjector({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule });
|
|
823
2104
|
}
|
|
824
|
-
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "
|
|
2105
|
+
i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "21.0.3", ngImport: i0, type: NgQubeeModule, decorators: [{
|
|
825
2106
|
type: NgModule,
|
|
826
|
-
args: [{
|
|
827
|
-
providers: [{
|
|
828
|
-
deps: [NestService],
|
|
829
|
-
provide: NgQubeeService,
|
|
830
|
-
useFactory: (nestService) => new NgQubeeService(nestService, {})
|
|
831
|
-
}]
|
|
832
|
-
}]
|
|
2107
|
+
args: [{}]
|
|
833
2108
|
}] });
|
|
834
2109
|
|
|
2110
|
+
/**
|
|
2111
|
+
* Resolve the request strategy instance for the given driver
|
|
2112
|
+
*
|
|
2113
|
+
* @param driver - The pagination driver
|
|
2114
|
+
* @returns The corresponding request strategy
|
|
2115
|
+
*/
|
|
2116
|
+
function resolveRequestStrategy(driver) {
|
|
2117
|
+
switch (driver) {
|
|
2118
|
+
case DriverEnum.JSON_API:
|
|
2119
|
+
return new JsonApiRequestStrategy();
|
|
2120
|
+
case DriverEnum.NESTJS:
|
|
2121
|
+
return new NestjsRequestStrategy();
|
|
2122
|
+
case DriverEnum.SPATIE:
|
|
2123
|
+
return new SpatieRequestStrategy();
|
|
2124
|
+
case DriverEnum.LARAVEL:
|
|
2125
|
+
return new LaravelRequestStrategy();
|
|
2126
|
+
}
|
|
2127
|
+
}
|
|
2128
|
+
/**
|
|
2129
|
+
* Resolve the response strategy instance for the given driver
|
|
2130
|
+
*
|
|
2131
|
+
* @param driver - The pagination driver
|
|
2132
|
+
* @returns The corresponding response strategy
|
|
2133
|
+
*/
|
|
2134
|
+
function resolveResponseStrategy(driver) {
|
|
2135
|
+
switch (driver) {
|
|
2136
|
+
case DriverEnum.JSON_API:
|
|
2137
|
+
return new JsonApiResponseStrategy();
|
|
2138
|
+
case DriverEnum.NESTJS:
|
|
2139
|
+
return new NestjsResponseStrategy();
|
|
2140
|
+
case DriverEnum.SPATIE:
|
|
2141
|
+
return new SpatieResponseStrategy();
|
|
2142
|
+
case DriverEnum.LARAVEL:
|
|
2143
|
+
return new LaravelResponseStrategy();
|
|
2144
|
+
}
|
|
2145
|
+
}
|
|
835
2146
|
/**
|
|
836
2147
|
* Sets up providers necessary to enable `NgQubee` functionality for the application.
|
|
837
2148
|
*
|
|
838
2149
|
* @usageNotes
|
|
839
2150
|
*
|
|
840
|
-
* Basic example
|
|
2151
|
+
* Basic example with the Laravel driver:
|
|
2152
|
+
* ```
|
|
2153
|
+
* bootstrapApplication(AppComponent, {
|
|
2154
|
+
* providers: [provideNgQubee({ driver: DriverEnum.LARAVEL })]
|
|
2155
|
+
* });
|
|
2156
|
+
* ```
|
|
2157
|
+
*
|
|
2158
|
+
* Spatie driver example:
|
|
2159
|
+
* ```
|
|
2160
|
+
* import { DriverEnum } from 'ng-qubee';
|
|
2161
|
+
*
|
|
2162
|
+
* bootstrapApplication(AppComponent, {
|
|
2163
|
+
* providers: [provideNgQubee({ driver: DriverEnum.SPATIE })]
|
|
2164
|
+
* });
|
|
2165
|
+
* ```
|
|
2166
|
+
*
|
|
2167
|
+
* JSON:API driver example:
|
|
2168
|
+
* ```
|
|
2169
|
+
* import { DriverEnum } from 'ng-qubee';
|
|
2170
|
+
*
|
|
2171
|
+
* bootstrapApplication(AppComponent, {
|
|
2172
|
+
* providers: [provideNgQubee({ driver: DriverEnum.JSON_API })]
|
|
2173
|
+
* });
|
|
2174
|
+
* ```
|
|
2175
|
+
*
|
|
2176
|
+
* NestJS driver example:
|
|
841
2177
|
* ```
|
|
842
|
-
*
|
|
2178
|
+
* import { DriverEnum } from 'ng-qubee';
|
|
843
2179
|
*
|
|
844
2180
|
* bootstrapApplication(AppComponent, {
|
|
845
|
-
* providers: [provideNgQubee(
|
|
2181
|
+
* providers: [provideNgQubee({ driver: DriverEnum.NESTJS })]
|
|
846
2182
|
* });
|
|
847
2183
|
* ```
|
|
848
2184
|
*
|
|
849
2185
|
* @publicApi
|
|
850
|
-
* @param config Configuration object compliant to the IConfig interface
|
|
2186
|
+
* @param config - Configuration object compliant to the IConfig interface
|
|
851
2187
|
* @returns A set of providers to setup NgQubee
|
|
852
2188
|
*/
|
|
853
|
-
function provideNgQubee(config
|
|
2189
|
+
function provideNgQubee(config) {
|
|
2190
|
+
const driver = config.driver;
|
|
2191
|
+
const requestStrategy = resolveRequestStrategy(driver);
|
|
2192
|
+
const responseStrategy = resolveResponseStrategy(driver);
|
|
854
2193
|
return makeEnvironmentProviders([
|
|
855
2194
|
{
|
|
856
2195
|
provide: NestService,
|
|
@@ -859,14 +2198,47 @@ function provideNgQubee(config = {}) {
|
|
|
859
2198
|
{
|
|
860
2199
|
deps: [NestService],
|
|
861
2200
|
provide: NgQubeeService,
|
|
862
|
-
useFactory: (nestService) => new NgQubeeService(nestService, Object.assign({}, config.request))
|
|
863
|
-
},
|
|
2201
|
+
useFactory: (nestService) => new NgQubeeService(nestService, requestStrategy, driver, Object.assign({}, config.request))
|
|
2202
|
+
},
|
|
2203
|
+
{
|
|
864
2204
|
provide: PaginationService,
|
|
865
|
-
useFactory: () =>
|
|
2205
|
+
useFactory: () => {
|
|
2206
|
+
const responseConfig = Object.assign({}, config.response);
|
|
2207
|
+
if (driver === DriverEnum.JSON_API) {
|
|
2208
|
+
return new PaginationService(responseStrategy, new JsonApiResponseOptions(responseConfig));
|
|
2209
|
+
}
|
|
2210
|
+
return driver === DriverEnum.NESTJS
|
|
2211
|
+
? new PaginationService(responseStrategy, new NestjsResponseOptions(responseConfig))
|
|
2212
|
+
: new PaginationService(responseStrategy, responseConfig);
|
|
2213
|
+
}
|
|
866
2214
|
}
|
|
867
2215
|
]);
|
|
868
2216
|
}
|
|
869
2217
|
|
|
2218
|
+
/**
|
|
2219
|
+
* Enum representing the available filter operators for the NestJS driver
|
|
2220
|
+
*
|
|
2221
|
+
* These operators map to the nestjs-paginate filter syntax:
|
|
2222
|
+
* `filter.field=$operator:value`
|
|
2223
|
+
*
|
|
2224
|
+
* @see https://github.com/ppetzold/nestjs-paginate
|
|
2225
|
+
*/
|
|
2226
|
+
var FilterOperatorEnum;
|
|
2227
|
+
(function (FilterOperatorEnum) {
|
|
2228
|
+
FilterOperatorEnum["BTW"] = "$btw";
|
|
2229
|
+
FilterOperatorEnum["CONTAINS"] = "$contains";
|
|
2230
|
+
FilterOperatorEnum["EQ"] = "$eq";
|
|
2231
|
+
FilterOperatorEnum["GT"] = "$gt";
|
|
2232
|
+
FilterOperatorEnum["GTE"] = "$gte";
|
|
2233
|
+
FilterOperatorEnum["ILIKE"] = "$ilike";
|
|
2234
|
+
FilterOperatorEnum["IN"] = "$in";
|
|
2235
|
+
FilterOperatorEnum["LT"] = "$lt";
|
|
2236
|
+
FilterOperatorEnum["LTE"] = "$lte";
|
|
2237
|
+
FilterOperatorEnum["NOT"] = "$not";
|
|
2238
|
+
FilterOperatorEnum["NULL"] = "$null";
|
|
2239
|
+
FilterOperatorEnum["SW"] = "$sw";
|
|
2240
|
+
})(FilterOperatorEnum || (FilterOperatorEnum = {}));
|
|
2241
|
+
|
|
870
2242
|
/*
|
|
871
2243
|
* Public API Surface of angular-query-builder
|
|
872
2244
|
*/
|
|
@@ -875,5 +2247,5 @@ function provideNgQubee(config = {}) {
|
|
|
875
2247
|
* Generated bundle index. Do not edit.
|
|
876
2248
|
*/
|
|
877
2249
|
|
|
878
|
-
export {
|
|
2250
|
+
export { DriverEnum, FilterOperatorEnum, InvalidLimitError, InvalidPageNumberError, InvalidResourceNameError, JsonApiRequestStrategy, JsonApiResponseStrategy, KeyNotFoundError, LaravelRequestStrategy, LaravelResponseStrategy, NestjsRequestStrategy, NestjsResponseStrategy, NgQubeeModule, NgQubeeService, PaginatedCollection, PaginationService, SortEnum, SpatieRequestStrategy, SpatieResponseStrategy, UnselectableModelError, UnsupportedFieldSelectionError, UnsupportedFilterError, UnsupportedFilterOperatorError, UnsupportedIncludesError, UnsupportedSearchError, UnsupportedSelectError, UnsupportedSortError, provideNgQubee };
|
|
879
2251
|
//# sourceMappingURL=ng-qubee.mjs.map
|