@ecodev/natural 64.0.2 → 65.0.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.
@@ -1,1218 +0,0 @@
1
- import { mergeWith, defaultsDeep, defaults } from 'es-toolkit/compat';
2
- import { pickBy, cloneDeepWith, cloneDeep, uniq, groupBy, omit, merge, pick } from 'es-toolkit';
3
- import { switchMap, take, BehaviorSubject, throwError, from, ReplaySubject, Subject, debounceTime, raceWith, mergeMap, EMPTY, shareReplay, catchError, of, Observable, forkJoin, map, first, combineLatest } from 'rxjs';
4
- import { NavigationStart, NavigationEnd } from '@angular/router';
5
- import { filter, takeWhile, map as map$1, switchMap as switchMap$1, debounceTime as debounceTime$1, tap, shareReplay as shareReplay$1, startWith } from 'rxjs/operators';
6
- import { Apollo, gql } from 'apollo-angular';
7
- import { NetworkStatus } from '@apollo/client/core';
8
- import { UntypedFormControl, UntypedFormGroup } from '@angular/forms';
9
- import * as i0 from '@angular/core';
10
- import { Injectable, inject } from '@angular/core';
11
-
12
- function formatIsoDate(date) {
13
- if (!date) {
14
- return null;
15
- }
16
- const y = date.getFullYear();
17
- const m = date.getMonth() + 1;
18
- const d = date.getDate();
19
- return y + '-' + (m < 10 ? '0' : '') + m + '-' + (d < 10 ? '0' : '') + d;
20
- }
21
- /**
22
- * Format a date and time in a way that will preserve the local time zone.
23
- * This allows the server side to know the day (without time) that was selected on client side.
24
- *
25
- * So something like: "2021-09-23T17:57:16+09:00"
26
- */
27
- function formatIsoDateTime(date) {
28
- const timezoneOffsetInMinutes = date.getTimezoneOffset();
29
- const timezoneOffsetInHours = -Math.trunc(timezoneOffsetInMinutes / 60); // UTC minus local time
30
- const sign = timezoneOffsetInHours >= 0 ? '+' : '-';
31
- const hoursLeadingZero = Math.abs(timezoneOffsetInHours) < 10 ? '0' : '';
32
- const remainderMinutes = -(timezoneOffsetInMinutes % 60);
33
- const minutesLeadingZero = Math.abs(remainderMinutes) < 10 ? '0' : '';
34
- // It's a bit unfortunate that we need to construct a new Date instance,
35
- // but we don't want the original Date instance to be modified
36
- const correctedDate = new Date(date.getFullYear(), date.getMonth(), date.getDate(), date.getHours(), date.getMinutes(), date.getSeconds(), date.getMilliseconds());
37
- correctedDate.setHours(date.getHours() + timezoneOffsetInHours);
38
- const iso = correctedDate
39
- .toISOString()
40
- .replace(/\.\d{3}Z/, '')
41
- .replace('Z', '');
42
- return (iso +
43
- sign +
44
- hoursLeadingZero +
45
- Math.abs(timezoneOffsetInHours).toString() +
46
- ':' +
47
- minutesLeadingZero +
48
- remainderMinutes);
49
- }
50
- /**
51
- * Relations to full objects are converted to their IDs only.
52
- *
53
- * So {user: {id: 123}} becomes {user: 123}
54
- */
55
- function relationsToIds(object) {
56
- const newObj = {};
57
- Object.keys(object).forEach(key => {
58
- let value = object[key];
59
- if (value === null || value === undefined) {
60
- // noop
61
- }
62
- else if (hasId(value)) {
63
- value = value.id;
64
- }
65
- else if (Array.isArray(value)) {
66
- value = value.map((i) => (hasId(i) ? i.id : i));
67
- }
68
- else if (typeof value === 'object' && !(value instanceof File) && !(value instanceof Date)) {
69
- value = pickBy(value, (v, k) => k !== '__typename'); // omit(value, ['__typename']) ?
70
- }
71
- newObj[key] = value;
72
- });
73
- return newObj;
74
- }
75
- function hasId(value) {
76
- return !!value && typeof value === 'object' && 'id' in value && !!value.id;
77
- }
78
- /**
79
- * Returns the plural form of the given name
80
- *
81
- * This is **not** necessarily valid english grammar. Its only purpose is for internal usage, not for humans.
82
- *
83
- * This **MUST** be kept in sync with `\Ecodev\Felix\Api\Plural:make()`.
84
- *
85
- * This is a bit performance-sensitive, so we should keep it fast and only cover cases that we actually need.
86
- */
87
- function makePlural(name) {
88
- // Words ending in a y preceded by a vowel form their plurals by adding -s:
89
- if (/[aeiou]y$/.exec(name)) {
90
- return name + 's';
91
- }
92
- const plural = name + 's';
93
- return plural.replace(/ys$/, 'ies').replace(/ss$/, 'ses').replace(/xs$/, 'xes');
94
- }
95
- /**
96
- * Returns the string with the first letter as capital
97
- */
98
- function upperCaseFirstLetter(term) {
99
- return term.charAt(0).toUpperCase() + term.slice(1);
100
- }
101
- /**
102
- * Replace all attributes of first object with the ones provided by the second, but keeps the reference
103
- */
104
- function replaceObjectKeepingReference(obj, newObj) {
105
- if (!obj || !newObj) {
106
- return;
107
- }
108
- Object.keys(obj).forEach(key => {
109
- delete obj[key];
110
- });
111
- Object.keys(newObj).forEach(key => {
112
- obj[key] = newObj[key];
113
- });
114
- }
115
- /**
116
- * Get contrasted color for text in the slider thumb
117
- * @param hexBgColor string in hexadecimals representing the background color
118
- */
119
- function getForegroundColor(hexBgColor) {
120
- const rgb = hexToRgb(hexBgColor.slice(0, 7)); // splice remove alpha and consider only "visible" color at 100% alpha
121
- const o = Math.round((rgb.r * 299 + rgb.g * 587 + rgb.b * 114) / 1000);
122
- return o > 125 ? 'black' : 'white';
123
- }
124
- function hexToRgb(hex) {
125
- // Expand shorthand form (e.g. "03F") to full form (e.g. "0033FF")
126
- const shorthandRegex = /^#?([a-f\d])([a-f\d])([a-f\d])$/i;
127
- hex = hex.replace(shorthandRegex, (m, r, g, b) => {
128
- return r + r + g + g + b + b;
129
- });
130
- const result = /^#?([a-f\d]{2})([a-f\d]{2})([a-f\d]{2})$/i.exec(hex);
131
- return result
132
- ? {
133
- r: parseInt(result[1], 16),
134
- g: parseInt(result[2], 16),
135
- b: parseInt(result[3], 16),
136
- }
137
- : {
138
- r: 0,
139
- g: 0,
140
- b: 0,
141
- };
142
- }
143
- /**
144
- * Convert RGB color to hexadecimal color
145
- *
146
- * ```ts
147
- * rgbToHex('rgb(255, 00, 255)'); // '#FF00FF'
148
- * ```
149
- */
150
- function rgbToHex(rgb) {
151
- const m = /^rgb\((\d+),\s*(\d+),\s*(\d+)\)$/.exec(rgb);
152
- if (!m) {
153
- return rgb;
154
- }
155
- return '#' + [m[1], m[2], m[3]].map(x => parseInt(x).toString(16).toUpperCase().padStart(2, '0')).join('');
156
- }
157
- /**
158
- * Deep clone given values except for `File` that will be referencing the original value
159
- */
160
- function cloneDeepButSkipFile(value) {
161
- return cloneDeepWith(value, v => (isFile(v) ? v : undefined));
162
- }
163
- function isFile(value) {
164
- return ((typeof File !== 'undefined' && value instanceof File) ||
165
- (typeof Blob !== 'undefined' && value instanceof Blob) ||
166
- (typeof FileList !== 'undefined' && value instanceof FileList));
167
- }
168
- /**
169
- * During lodash.mergeWith, overrides arrays
170
- */
171
- function mergeOverrideArray(destValue, source) {
172
- if (Array.isArray(source) || isFile(source)) {
173
- return source;
174
- }
175
- }
176
- /**
177
- * Copy text to clipboard.
178
- * Accepts line breaks `\n` as textarea do.
179
- */
180
- function copyToClipboard(document, text) {
181
- const input = document.createElement('textarea');
182
- document.body.append(input);
183
- input.value = text;
184
- input.select();
185
- // eslint-disable-next-line @typescript-eslint/no-deprecated
186
- document.execCommand('copy');
187
- document.body.removeChild(input);
188
- }
189
- function deepFreeze(o) {
190
- Object.values(o).forEach(v => Object.isFrozen(v) || deepFreeze(v));
191
- return Object.freeze(o);
192
- }
193
- /**
194
- * Return a valid PaginationInput from whatever is available from data. Invalid properties/types will be dropped.
195
- */
196
- function validatePagination(data) {
197
- if (!data || typeof data !== 'object' || Array.isArray(data)) {
198
- return null;
199
- }
200
- const pagination = {};
201
- if ('offset' in data && (data.offset === null || typeof data.offset === 'number')) {
202
- pagination.offset = data.offset;
203
- }
204
- if ('pageIndex' in data && (data.pageIndex === null || typeof data.pageIndex === 'number')) {
205
- pagination.pageIndex = data.pageIndex;
206
- }
207
- if ('pageSize' in data && (data.pageSize === null || typeof data.pageSize === 'number')) {
208
- pagination.pageSize = data.pageSize;
209
- }
210
- return pagination;
211
- }
212
- /**
213
- * Return a valid Sortings from whatever is available from data. Invalid properties/types will be dropped.
214
- */
215
- function validateSorting(data) {
216
- if (!Array.isArray(data)) {
217
- return null;
218
- }
219
- const result = [];
220
- data.forEach(s => {
221
- const r = validateOneSorting(s);
222
- if (r) {
223
- result.push(r);
224
- }
225
- });
226
- return result;
227
- }
228
- function validateOneSorting(data) {
229
- if (!data || typeof data !== 'object' || !('field' in data)) {
230
- return null;
231
- }
232
- const sorting = { field: data.field };
233
- if ('order' in data &&
234
- (data.order === SortingOrder.ASC || data.order === SortingOrder.DESC || data.order === null)) {
235
- sorting.order = data.order;
236
- }
237
- if ('nullAsHighest' in data && (data.nullAsHighest === null || typeof data.nullAsHighest === 'boolean')) {
238
- sorting.nullAsHighest = data.nullAsHighest;
239
- }
240
- if ('emptyStringAsHighest' in data &&
241
- (data.emptyStringAsHighest === null || typeof data.emptyStringAsHighest === 'boolean')) {
242
- sorting.emptyStringAsHighest = data.emptyStringAsHighest;
243
- }
244
- return sorting;
245
- }
246
- /**
247
- * Return valid columns from whatever is available from data. Invalid properties/types will be dropped.
248
- */
249
- function validateColumns(data) {
250
- if (typeof data !== 'string') {
251
- return null;
252
- }
253
- return data.split(',').filter(string => string);
254
- }
255
- function onHistoryEvent(router) {
256
- return router.events.pipe(filter(e => e instanceof NavigationStart && e.navigationTrigger === 'popstate'), switchMap(() => router.events.pipe(filter(e => e instanceof NavigationEnd), take(1))));
257
- }
258
-
259
- // Basic; loosely typed structure for graphql-doctrine filters
260
- // Logical operator to be used in conditions
261
- var LogicalOperator;
262
- (function (LogicalOperator) {
263
- LogicalOperator["AND"] = "AND";
264
- LogicalOperator["OR"] = "OR";
265
- })(LogicalOperator || (LogicalOperator = {}));
266
- // Join types to be used in DQL
267
- var JoinType;
268
- (function (JoinType) {
269
- JoinType["innerJoin"] = "innerJoin";
270
- JoinType["leftJoin"] = "leftJoin";
271
- })(JoinType || (JoinType = {}));
272
-
273
- function hasMixedGroupLogic(groups) {
274
- // Complete lack of definition by fallback on AND operator
275
- const completedGroups = cloneDeep(groups).map(group => {
276
- if (!group.groupLogic) {
277
- group.groupLogic = LogicalOperator.AND;
278
- }
279
- return group;
280
- });
281
- const groupLogics = uniq(Object.keys(groupBy(completedGroups.slice(1), group => group.groupLogic)));
282
- return groupLogics.length > 1;
283
- }
284
-
285
- var SortingOrder;
286
- (function (SortingOrder) {
287
- SortingOrder["ASC"] = "ASC";
288
- SortingOrder["DESC"] = "DESC";
289
- })(SortingOrder || (SortingOrder = {}));
290
- /**
291
- * During lodash merge, concat arrays
292
- */
293
- function mergeConcatArray(destValue, source) {
294
- if (isFile(source)) {
295
- return source;
296
- }
297
- if (Array.isArray(source)) {
298
- if (Array.isArray(destValue)) {
299
- return destValue.concat(source);
300
- }
301
- else {
302
- return source;
303
- }
304
- }
305
- }
306
- /**
307
- * Filter manager stores a set of channels that contain a variable object and exposes an observable "variables" that updates with the result
308
- * of all channels merged together.
309
- *
310
- * A channel is supposed to be used by a given aspect of the GUI (pagination, sorting, search, others ?).
311
- *
312
- * ```ts
313
- * const fm = new QueryVariablesManager();
314
- * fm.merge('componentA-variables', {a : [1, 2, 3]});
315
- * ```
316
- *
317
- * Variables attributes is a BehaviorSubject. That mean it's not mandatory to subscribe, we can just call getValue or value attributes on
318
- * it :
319
- *
320
- * ```ts
321
- * console.log(fm.variables.value); // {a : [1, 2, 3]}
322
- * ```
323
- *
324
- * Set new variables for 'componentA-variables':
325
- *
326
- * ```ts
327
- * fm.merge('componentA-variables', {a : [1, 2]});
328
- * console.log(fm.variables.value); // {a : [1, 2, 3]}
329
- * ```
330
- *
331
- * Set new variables for new channel:
332
- *
333
- * ```ts
334
- * fm.merge('componentB-variables', {a : [3, 4]});
335
- * console.log(fm.variables.value); // {a : [1, 2, 3, 4]}
336
- * ```
337
- */
338
- class NaturalQueryVariablesManager {
339
- variables = new BehaviorSubject(undefined);
340
- channels = new Map();
341
- constructor(queryVariablesManager) {
342
- if (queryVariablesManager) {
343
- this.channels = queryVariablesManager.getChannelsCopy();
344
- this.updateVariables();
345
- }
346
- }
347
- /**
348
- * Set or override all the variables that may exist in the given channel
349
- */
350
- set(channelName, variables) {
351
- // cloneDeep to change reference and prevent some interactions when merge
352
- if (variables) {
353
- this.channels.set(channelName, cloneDeepButSkipFile(variables));
354
- }
355
- else {
356
- this.channels.delete(channelName);
357
- }
358
- this.updateVariables();
359
- }
360
- /**
361
- * Return a deep clone of the variables for the given channel name.
362
- *
363
- * Avoid returning the same reference to prevent an attribute change, then another channel update that would
364
- * used this changed attribute without having explicitly asked QueryVariablesManager to update it.
365
- */
366
- get(channelName) {
367
- return cloneDeepButSkipFile(this.channels.get(channelName));
368
- }
369
- /**
370
- * Merge variable into a channel, overriding arrays in same channel / key
371
- */
372
- merge(channelName, newVariables) {
373
- const variables = this.channels.get(channelName);
374
- if (variables) {
375
- mergeWith(variables, cloneDeep(newVariables), mergeOverrideArray); // merge preserves references, cloneDeep prevent that
376
- this.updateVariables();
377
- }
378
- else {
379
- this.set(channelName, newVariables);
380
- }
381
- }
382
- /**
383
- * Apply default values to a channel
384
- * Note : lodash defaults only defines values on destinations keys that are undefined
385
- */
386
- defaults(channelName, newVariables) {
387
- const variables = this.channels.get(channelName);
388
- if (variables) {
389
- defaultsDeep(variables, newVariables);
390
- this.updateVariables();
391
- }
392
- else {
393
- this.set(channelName, newVariables);
394
- }
395
- }
396
- getChannelsCopy() {
397
- return new Map(this.channels);
398
- }
399
- /**
400
- * Merge channels in a single object
401
- * Arrays are concatenated
402
- * Filter groups are combined smartly (see mergeGroupList)
403
- */
404
- updateVariables() {
405
- const merged = {};
406
- this.channels.forEach(channelVariables => {
407
- if (channelVariables.filter) {
408
- // Merge filter's groups first
409
- const groups = this.mergeGroupList(merged.filter?.groups ? merged.filter.groups : [], channelVariables.filter.groups || []);
410
- // Merge filter key (that contain groups)
411
- if (groups?.length) {
412
- if (merged.filter) {
413
- merged.filter.groups = groups;
414
- }
415
- else {
416
- merged.filter = { groups: groups };
417
- }
418
- }
419
- else {
420
- mergeWith(merged, { filter: channelVariables.filter }, mergeConcatArray);
421
- }
422
- }
423
- // Merge other attributes than filter
424
- mergeWith(merged, omit(channelVariables, ['filter']), mergeConcatArray);
425
- });
426
- this.variables.next(merged);
427
- }
428
- /**
429
- * Cross merge two filters
430
- * Only accepts groups with same groupLogic (ignores the first one, because there is no groupLogic in this one)
431
- * @throws In case two non-empty lists of groups are given and at one of them mix groupLogic value, throws an error
432
- */
433
- mergeGroupList(groupsA, groupsB) {
434
- if (groupsA.length === 0 && groupsB.length === 0) {
435
- return []; // empty listings, return empty lists
436
- }
437
- if (groupsA.length === 0 && groupsB.length > 0) {
438
- return groupsB; // One list is empty, return the one that is not
439
- }
440
- if (groupsB.length === 0 && groupsA.length > 0) {
441
- return groupsA; // One list is empty, return the one that is not
442
- }
443
- const groups = [];
444
- if (hasMixedGroupLogic(groupsA) || hasMixedGroupLogic(groupsB)) {
445
- throw Error('QueryVariables groups contain mixed group logics');
446
- }
447
- groupsA.forEach(groupA => {
448
- groupsB.forEach(groupB => {
449
- groups.push(mergeWith(cloneDeep(groupA), groupB, mergeConcatArray));
450
- });
451
- });
452
- return groups;
453
- }
454
- }
455
-
456
- function bufferToHexa(hashBuffer) {
457
- const hashArray = new Uint8Array(hashBuffer); // convert buffer to byte array
458
- return hashArray.reduce((result, byte) => result + byte.toString(16).padStart(2, '0'), ''); // convert bytes to hex string
459
- }
460
- /**
461
- * Thin wrapper around browsers' native SubtleCrypto for convenience of use
462
- */
463
- async function sha256(message) {
464
- const msgUint8 = new TextEncoder().encode(message); // encode as (utf-8) Uint8Array
465
- const hashBuffer = await crypto.subtle.digest('SHA-256', msgUint8); // hash the message
466
- return bufferToHexa(hashBuffer);
467
- }
468
- /**
469
- * Thin wrapper around browsers' native SubtleCrypto for convenience of use
470
- */
471
- async function hmacSha256(secret, payload) {
472
- const encoder = new TextEncoder();
473
- const algorithm = { name: 'HMAC', hash: 'SHA-256' };
474
- const key = await crypto.subtle.importKey('raw', encoder.encode(secret), algorithm, false, ['sign']);
475
- const signature = await crypto.subtle.sign(algorithm.name, key, encoder.encode(payload));
476
- return bufferToHexa(signature);
477
- }
478
-
479
- function getOperations(req) {
480
- if (req.body instanceof FormData) {
481
- const operations = req.body.get('operations');
482
- if (typeof operations !== 'string') {
483
- throw new Error('Cannot sign a GraphQL query that is using FormData but that is missing the key `operations`');
484
- }
485
- return operations;
486
- }
487
- else {
488
- return JSON.stringify(req.body);
489
- }
490
- }
491
- /**
492
- * Sign all HTTP POST requests that are GraphQL queries against `/graphql` endpoint with a custom signature.
493
- *
494
- * The server will validate the signature before executing the GraphQL query.
495
- */
496
- function graphqlQuerySigner(key) {
497
- // Validates the configuration exactly 1 time (not for
498
- // every query), and if not reject **all** HTTP requests
499
- if (!key) {
500
- return () => throwError(() => new Error('graphqlQuerySigner requires a non-empty key. Configure it in local.php under signedQueries.'));
501
- }
502
- return (req, next) => {
503
- const mustSign = req.method === 'POST' && /\/graphql(\?|$)/.exec(req.url);
504
- if (!mustSign) {
505
- return next(req);
506
- }
507
- const operations = getOperations(req);
508
- const timestamp = Math.round(Date.now() / 1000);
509
- const payload = timestamp + operations;
510
- return from(hmacSha256(key, payload)).pipe(switchMap(hash => {
511
- const header = `v1.${timestamp}.${hash}`;
512
- const signedRequest = req.clone({
513
- headers: req.headers.set('X-Signature', header),
514
- });
515
- return next(signedRequest);
516
- }));
517
- };
518
- }
519
-
520
- /**
521
- * Debounce subscriptions to update mutations, with the possibility to cancel one, flush one, or flush all of them.
522
- *
523
- * `modelService` is also used to separate objects by their types. So User with ID 1 is not confused with Product with ID 1.
524
- *
525
- * `id` must be the ID of the object that will be updated.
526
- */
527
- class NaturalDebounceService {
528
- /**
529
- * Stores the debounced update function
530
- */
531
- allDebouncedUpdateCache = new Map();
532
- /**
533
- * Debounce the `modelService.updateNow()` mutation for a short time. If called multiple times with the same
534
- * modelService and id, it will postpone the subscription to the mutation.
535
- *
536
- * All input variables for the same object (same service and ID) will be cumulated over time. So it is possible
537
- * to update `field1`, then `field2`, and they will be batched into a single XHR including `field1` and `field2`.
538
- *
539
- * But it will always keep the same debouncing timeline.
540
- */
541
- debounce(modelService, id, object) {
542
- const debouncedUpdateCache = this.getMap(modelService);
543
- let debounced = debouncedUpdateCache.get(id);
544
- if (debounced) {
545
- debounced.object = {
546
- ...debounced.object,
547
- ...object,
548
- };
549
- }
550
- else {
551
- const debouncer = new ReplaySubject(1);
552
- let wasCancelled = false;
553
- const canceller = new Subject();
554
- canceller.subscribe(() => {
555
- wasCancelled = true;
556
- debouncer.complete();
557
- canceller.complete();
558
- this.delete(modelService, id);
559
- });
560
- const flusher = new Subject();
561
- debounced = {
562
- object,
563
- debouncer,
564
- canceller,
565
- flusher,
566
- modelService: modelService,
567
- result: debouncer.pipe(debounceTime(2000), // Wait 2 seconds...
568
- raceWith(flusher), // ...unless flusher is triggered
569
- take(1), mergeMap(() => {
570
- this.delete(modelService, id);
571
- if (wasCancelled || !debounced) {
572
- return EMPTY;
573
- }
574
- return modelService.updateNow(debounced.object);
575
- }), shareReplay()),
576
- };
577
- debouncedUpdateCache.set(id, debounced);
578
- }
579
- // Notify our debounced update each time we ask to update
580
- debounced.debouncer.next();
581
- // Return and observable that is updated when mutation is done
582
- return debounced.result;
583
- }
584
- cancelOne(modelService, id) {
585
- const debounced = this.allDebouncedUpdateCache.get(modelService)?.get(id);
586
- debounced?.canceller.next();
587
- }
588
- /**
589
- * Immediately execute the pending update, if any.
590
- *
591
- * It should typically be called before resolving the object, to mutate it before re-fetching it from server.
592
- *
593
- * The returned observable will complete when the update completes, even if it errors.
594
- */
595
- flushOne(modelService, id) {
596
- const debounced = this.allDebouncedUpdateCache.get(modelService)?.get(id);
597
- return this.internalFlush(debounced ? [debounced] : []);
598
- }
599
- /**
600
- * Immediately execute all pending updates.
601
- *
602
- * It should typically be called before login out.
603
- *
604
- * The returned observable will complete when all updates complete, even if some of them error.
605
- */
606
- flush() {
607
- const all = [];
608
- this.allDebouncedUpdateCache.forEach(map => map.forEach(debounced => all.push(debounced)));
609
- return this.internalFlush(all);
610
- }
611
- internalFlush(debounceds) {
612
- const all = [];
613
- const allFlusher = [];
614
- debounceds.forEach(debounced => {
615
- all.push(debounced.result.pipe(catchError(() => of(undefined))));
616
- allFlusher.push(debounced.flusher);
617
- });
618
- if (!all.length) {
619
- all.push(of(undefined));
620
- }
621
- return new Observable(subscriber => {
622
- const subscription = forkJoin(all)
623
- .pipe(map(() => undefined))
624
- .subscribe(subscriber);
625
- // Flush only after subscription process is finished
626
- allFlusher.forEach(flusher => flusher.next());
627
- return subscription;
628
- });
629
- }
630
- /**
631
- * Count of pending updates
632
- */
633
- get count() {
634
- let count = 0;
635
- this.allDebouncedUpdateCache.forEach(map => (count += map.size));
636
- return count;
637
- }
638
- getMap(modelService) {
639
- let debouncedUpdateCache = this.allDebouncedUpdateCache.get(modelService);
640
- if (!debouncedUpdateCache) {
641
- debouncedUpdateCache = new Map();
642
- this.allDebouncedUpdateCache.set(modelService, debouncedUpdateCache);
643
- }
644
- return debouncedUpdateCache;
645
- }
646
- delete(modelService, id) {
647
- const map = this.allDebouncedUpdateCache.get(modelService);
648
- if (!map) {
649
- return;
650
- }
651
- map.delete(id);
652
- if (!map.size) {
653
- this.allDebouncedUpdateCache.delete(modelService);
654
- }
655
- }
656
- static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NaturalDebounceService, deps: [], target: i0.ɵɵFactoryTarget.Injectable });
657
- static ɵprov = i0.ɵɵngDeclareInjectable({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NaturalDebounceService, providedIn: 'root' });
658
- }
659
- i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "20.3.0", ngImport: i0, type: NaturalDebounceService, decorators: [{
660
- type: Injectable,
661
- args: [{
662
- providedIn: 'root',
663
- }]
664
- }] });
665
-
666
- /**
667
- * Lookup a facet by its `name` and then by its `field`, or return null if not found
668
- */
669
- function getFacetFromSelection(facets, selection) {
670
- if (!facets) {
671
- return null;
672
- }
673
- return (facets.find(facet => facet.name != null && facet.name === selection.name) ||
674
- facets.find(facet => facet.field === selection.field) ||
675
- null);
676
- }
677
- /**
678
- * Deep clone a literal via JSON serializing/unserializing
679
- *
680
- * It will **not** work with:
681
- *
682
- * - functions (will be removed)
683
- * - `undefined` (will be removed)
684
- * - cyclic references (will crash)
685
- * - objects (will be converted to `{}`)
686
- */
687
- function deepClone(obj) {
688
- return JSON.parse(JSON.stringify(obj));
689
- }
690
-
691
- class NaturalAbstractModelService {
692
- name;
693
- oneQuery;
694
- allQuery;
695
- createMutation;
696
- updateMutation;
697
- deleteMutation;
698
- createName;
699
- updateName;
700
- deleteName;
701
- /**
702
- * Store the creation mutations that are pending
703
- */
704
- creatingCache = new Map();
705
- apollo = inject(Apollo);
706
- naturalDebounceService = inject(NaturalDebounceService);
707
- plural;
708
- /**
709
- *
710
- * @param name service and single object query name (eg. userForFront or user).
711
- * @param oneQuery GraphQL query to fetch a single object from ID (eg. userForCrudQuery).
712
- * @param allQuery GraphQL query to fetch a filtered list of objects (eg. usersForCrudQuery).
713
- * @param createMutation GraphQL mutation to create an object.
714
- * @param updateMutation GraphQL mutation to update an object.
715
- * @param deleteMutation GraphQL mutation to delete a list of objects.
716
- * @param plural list query name (eg. usersForFront or users).
717
- * @param createName create object mutation name (eg. createUser).
718
- * @param updateName update object mutation name (eg. updateUser).
719
- * @param deleteName delete object mutation name (eg. deleteUsers).
720
- */
721
- constructor(name, oneQuery, allQuery, createMutation, updateMutation, deleteMutation, plural = null, createName = null, updateName = null, deleteName = null) {
722
- this.name = name;
723
- this.oneQuery = oneQuery;
724
- this.allQuery = allQuery;
725
- this.createMutation = createMutation;
726
- this.updateMutation = updateMutation;
727
- this.deleteMutation = deleteMutation;
728
- this.createName = createName;
729
- this.updateName = updateName;
730
- this.deleteName = deleteName;
731
- this.plural = plural ?? makePlural(this.name);
732
- }
733
- /**
734
- * List of individual fields validators
735
- */
736
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
737
- getFormValidators(model) {
738
- return {};
739
- }
740
- /**
741
- * List of individual async fields validators
742
- */
743
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
744
- getFormAsyncValidators(model) {
745
- return {};
746
- }
747
- /**
748
- * List of grouped fields validators (like password + confirm password)
749
- */
750
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
751
- getFormGroupValidators(model) {
752
- return [];
753
- }
754
- /**
755
- * List of async group fields validators (like unique constraint on multiple columns)
756
- */
757
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
758
- getFormGroupAsyncValidators(model) {
759
- return [];
760
- }
761
- getFormConfig(model) {
762
- const values = { ...this.getDefaultForServer(), ...this.getFormExtraFieldDefaultValues() };
763
- const validators = this.getFormValidators(model);
764
- const asyncValidators = this.getFormAsyncValidators(model);
765
- const controls = {};
766
- const disabled = model.permissions ? !model.permissions.update : false;
767
- if (model.id) {
768
- controls.id = new UntypedFormControl({ value: model.id, disabled: true });
769
- }
770
- // Configure form for each field of model
771
- for (const key of Object.keys(values)) {
772
- const value = model[key] !== undefined ? model[key] : values[key];
773
- const formState = {
774
- value: value,
775
- disabled: disabled,
776
- };
777
- const validator = typeof validators[key] !== 'undefined' ? validators[key] : null;
778
- const asyncValidator = typeof asyncValidators[key] !== 'undefined' ? asyncValidators[key] : null;
779
- controls[key] = new UntypedFormControl(formState, validator, asyncValidator);
780
- }
781
- // Configure form for extra validators that are not on a specific field
782
- for (const key of Object.keys(validators)) {
783
- if (!controls[key]) {
784
- const formState = {
785
- value: model[key] ? model[key] : null,
786
- disabled: disabled,
787
- };
788
- controls[key] = new UntypedFormControl(formState, validators[key]);
789
- }
790
- }
791
- for (const key of Object.keys(asyncValidators)) {
792
- if (controls[key] && asyncValidators[key]) {
793
- controls[key].setAsyncValidators(asyncValidators[key]);
794
- }
795
- else {
796
- const formState = {
797
- value: model[key] ? model[key] : null,
798
- disabled: disabled,
799
- };
800
- controls[key] = new UntypedFormControl(formState, null, asyncValidators[key]);
801
- }
802
- }
803
- return controls;
804
- }
805
- /**
806
- * Create the final FormGroup for the object, including all validators
807
- *
808
- * This method should **not** be overridden, but instead `getFormConfig`,
809
- * `getFormGroupValidators`, `getFormGroupAsyncValidators` might be.
810
- */
811
- getFormGroup(model) {
812
- const formConfig = this.getFormConfig(deepClone(model));
813
- return new UntypedFormGroup(formConfig, {
814
- validators: this.getFormGroupValidators(model),
815
- asyncValidators: this.getFormGroupAsyncValidators(model),
816
- });
817
- }
818
- /**
819
- * Get a single object
820
- *
821
- * If available it will emit object from cache immediately, then it
822
- * will **always** fetch from network and then the observable will be completed.
823
- *
824
- * You must subscribe to start getting results (and fetch from network).
825
- */
826
- getOne(id) {
827
- return this.prepareOneQuery(id, 'cache-and-network').pipe(takeWhile(result => result.networkStatus !== NetworkStatus.ready, true), map$1(result => result.data[this.name]));
828
- }
829
- /**
830
- * Watch a single object
831
- *
832
- * If available it will emit object from cache immediately, then it
833
- * will **always** fetch from network, and then keep watching the cache forever.
834
- *
835
- * You must subscribe to start getting results (and fetch from network).
836
- *
837
- * You **MUST** unsubscribe.
838
- */
839
- watchOne(id, fetchPolicy = 'cache-and-network') {
840
- return this.prepareOneQuery(id, fetchPolicy).pipe(map$1(result => result.data[this.name]));
841
- }
842
- prepareOneQuery(id, fetchPolicy) {
843
- this.throwIfObservable(id);
844
- this.throwIfNotQuery(this.oneQuery);
845
- return this.getVariablesForOne(id).pipe(switchMap$1(variables => {
846
- this.throwIfNotQuery(this.oneQuery);
847
- return this.apollo.watchQuery({
848
- query: this.oneQuery,
849
- variables: variables,
850
- fetchPolicy: fetchPolicy,
851
- nextFetchPolicy: 'cache-only',
852
- }).valueChanges;
853
- }), filter(result => !!result.data));
854
- }
855
- /**
856
- * Get a collection of objects
857
- *
858
- * It will **always** fetch from network and then the observable will be completed.
859
- * No cache is ever used, so it's slow but correct.
860
- */
861
- getAll(queryVariablesManager) {
862
- this.throwIfNotQuery(this.allQuery);
863
- return this.getPartialVariablesForAll().pipe(first(), switchMap$1(partialVariables => {
864
- this.throwIfNotQuery(this.allQuery);
865
- // Copy manager to prevent to apply internal variables to external QueryVariablesManager
866
- const manager = new NaturalQueryVariablesManager(queryVariablesManager);
867
- manager.merge('partial-variables', partialVariables);
868
- return this.apollo.query({
869
- query: this.allQuery,
870
- variables: manager.variables.value,
871
- fetchPolicy: 'network-only',
872
- });
873
- }), this.mapAll());
874
- }
875
- /**
876
- * Get a collection of objects
877
- *
878
- * Every time the observable variables change, and they are not undefined,
879
- * it will return result from cache, then it will **always** fetch from network,
880
- * and then keep watching the cache forever.
881
- *
882
- * You must subscribe to start getting results (and fetch from network).
883
- *
884
- * You **MUST** unsubscribe.
885
- */
886
- watchAll(queryVariablesManager, fetchPolicy = 'cache-and-network') {
887
- this.throwIfNotQuery(this.allQuery);
888
- return combineLatest({
889
- variables: queryVariablesManager.variables.pipe(
890
- // Ignore very fast variable changes
891
- debounceTime$1(20),
892
- // Wait for variables to be defined to prevent duplicate query: with and without variables
893
- // Null is accepted value for "no variables"
894
- filter(variables => typeof variables !== 'undefined')),
895
- partialVariables: this.getPartialVariablesForAll(),
896
- }).pipe(switchMap$1(result => {
897
- // Apply partial variables from service
898
- // Copy manager to prevent to apply internal variables to external QueryVariablesManager
899
- const manager = new NaturalQueryVariablesManager(queryVariablesManager);
900
- manager.merge('partial-variables', result.partialVariables);
901
- this.throwIfNotQuery(this.allQuery);
902
- return this.apollo
903
- .watchQuery({
904
- query: this.allQuery,
905
- variables: manager.variables.value,
906
- fetchPolicy: fetchPolicy,
907
- })
908
- .valueChanges.pipe(catchError(() => EMPTY), filter(r => !!r.data), this.mapAll());
909
- }));
910
- }
911
- /**
912
- * This functions allow to quickly create or update objects.
913
- *
914
- * Manages a "creation is pending" status, and update when creation is ready.
915
- * Uses regular update/updateNow and create methods.
916
- * Used mainly when editing multiple objects in same controller (like in editable arrays)
917
- */
918
- createOrUpdate(object, now = false) {
919
- this.throwIfObservable(object);
920
- this.throwIfNotQuery(this.createMutation);
921
- this.throwIfNotQuery(this.updateMutation);
922
- // If creation is pending, listen to creation observable and when ready, fire update
923
- const pendingCreation = this.creatingCache.get(object);
924
- if (pendingCreation) {
925
- return pendingCreation.pipe(switchMap$1(created => {
926
- return this.update({
927
- id: created.id,
928
- ...object,
929
- });
930
- }));
931
- }
932
- // If object has Id, just save it
933
- if ('id' in object && object.id) {
934
- if (now) {
935
- // used mainly for tests, because lodash debounced used in update() does not work fine with fakeAsync and tick()
936
- return this.updateNow(object);
937
- }
938
- else {
939
- return this.update(object);
940
- }
941
- }
942
- // If object was not saving, and has no ID, create it
943
- const creation = this.create(object).pipe(tap(() => {
944
- this.creatingCache.delete(object); // remove from cache
945
- }));
946
- // stores creating observable in a cache replayable version of the observable,
947
- // so several update() can subscribe to the same creation
948
- this.creatingCache.set(object, creation.pipe(shareReplay$1()));
949
- return creation;
950
- }
951
- /**
952
- * Create an object in DB and then refetch the list of objects
953
- */
954
- create(object) {
955
- this.throwIfObservable(object);
956
- this.throwIfNotQuery(this.createMutation);
957
- const variables = merge({ input: this.getInput(object, true) }, this.getPartialVariablesForCreation(object));
958
- return this.apollo
959
- .mutate({
960
- mutation: this.createMutation,
961
- variables: variables,
962
- })
963
- .pipe(map$1(result => {
964
- this.apollo.client.reFetchObservableQueries();
965
- return this.mapCreation(result);
966
- }));
967
- }
968
- /**
969
- * Update an object, after a short debounce
970
- */
971
- update(object) {
972
- this.throwIfObservable(object);
973
- this.throwIfNotQuery(this.updateMutation);
974
- // Keep a single instance of the debounced update function
975
- const id = object.id;
976
- return this.naturalDebounceService.debounce(this, id, object);
977
- }
978
- /**
979
- * Update an object immediately when subscribing
980
- */
981
- updateNow(object) {
982
- this.throwIfObservable(object);
983
- this.throwIfNotQuery(this.updateMutation);
984
- const variables = merge({
985
- id: object.id,
986
- input: this.getInput(object, false),
987
- }, this.getPartialVariablesForUpdate(object));
988
- return this.apollo
989
- .mutate({
990
- mutation: this.updateMutation,
991
- variables: variables,
992
- })
993
- .pipe(map$1(result => {
994
- this.apollo.client.reFetchObservableQueries();
995
- return this.mapUpdate(result);
996
- }));
997
- }
998
- /**
999
- * Delete objects and then refetch the list of objects
1000
- */
1001
- delete(objects) {
1002
- this.throwIfObservable(objects);
1003
- this.throwIfNotQuery(this.deleteMutation);
1004
- const ids = objects.map(o => {
1005
- // Cancel pending update
1006
- this.naturalDebounceService.cancelOne(this, o.id);
1007
- return o.id;
1008
- });
1009
- const variables = merge({
1010
- ids: ids,
1011
- }, this.getPartialVariablesForDelete(objects));
1012
- return this.apollo
1013
- .mutate({
1014
- mutation: this.deleteMutation,
1015
- variables: variables,
1016
- })
1017
- .pipe(
1018
- // Delay the observable until Apollo refetch is completed
1019
- switchMap$1(result => {
1020
- const mappedResult = this.mapDelete(result);
1021
- return from(this.apollo.client.reFetchObservableQueries()).pipe(map$1(() => mappedResult));
1022
- }));
1023
- }
1024
- /**
1025
- * If the id is provided, resolves an observable model. The observable model will only be emitted after we are sure
1026
- * that Apollo cache is fresh and warm. Then the component can subscribe to the observable model to get the model
1027
- * immediately from Apollo cache and any subsequents future mutations that may happen to Apollo cache.
1028
- *
1029
- * Without id, returns default values, in order to show a creation form.
1030
- */
1031
- resolve(id) {
1032
- if (id) {
1033
- const onlyNetwork = this.watchOne(id, 'network-only').pipe(first());
1034
- const onlyCache = this.watchOne(id, 'cache-first');
1035
- // In theory, we can rely on Apollo Cache to return a result instantly. It is very fast indeed,
1036
- // but it is still asynchronous, so there may be a very short time when we don't have the model
1037
- // available. To fix that, we can rely on RxJS, which is able to emit synchronously the value we just
1038
- // got from server. Once Apollo Client moves to RxJS (https://github.com/apollographql/apollo-feature-requests/issues/375),
1039
- // we could try to remove `startWith()`.
1040
- return onlyNetwork.pipe(map$1(firstValue => onlyCache.pipe(startWith(firstValue))));
1041
- }
1042
- else {
1043
- return of(of(this.getDefaultForServer()));
1044
- }
1045
- }
1046
- /**
1047
- * Return an object that match the GraphQL input type.
1048
- * It creates an object with manually filled data and add uncompleted data (like required attributes that can be empty strings)
1049
- */
1050
- getInput(object, forCreation) {
1051
- // Convert relations to their IDs for mutation
1052
- object = relationsToIds(object);
1053
- // Pick only attributes that we can find in the empty object
1054
- // In other words, prevent to select data that has unwanted attributes
1055
- const emptyObject = this.getDefaultForServer();
1056
- let input = pick(object, Object.keys(emptyObject));
1057
- // Complete a potentially uncompleted object with default values
1058
- if (forCreation) {
1059
- input = defaults(input, emptyObject);
1060
- }
1061
- return input;
1062
- }
1063
- /**
1064
- * Return the number of objects matching the query. It may never complete.
1065
- *
1066
- * This is used for the unique validator
1067
- */
1068
- count(queryVariablesManager) {
1069
- const queryName = 'Count' + upperCaseFirstLetter(this.plural);
1070
- const filterType = upperCaseFirstLetter(this.name) + 'Filter';
1071
- const query = gql `
1072
- query ${queryName} ($filter: ${filterType}) {
1073
- count: ${this.plural} (filter: $filter, pagination: {pageSize: 0, pageIndex: 0}) {
1074
- length
1075
- }
1076
- }`;
1077
- return this.getPartialVariablesForAll().pipe(switchMap$1(partialVariables => {
1078
- // Copy manager to prevent to apply internal variables to external QueryVariablesManager
1079
- const manager = new NaturalQueryVariablesManager(queryVariablesManager);
1080
- manager.merge('partial-variables', partialVariables);
1081
- return this.apollo.query({
1082
- query: query,
1083
- variables: manager.variables.value,
1084
- fetchPolicy: 'network-only',
1085
- });
1086
- }), map$1(result => result.data.count.length));
1087
- }
1088
- /**
1089
- * Return empty object with some default values from server perspective
1090
- *
1091
- * This is typically useful when showing a form for creation
1092
- */
1093
- getDefaultForServer() {
1094
- return {};
1095
- }
1096
- /**
1097
- * You probably **should not** use this.
1098
- *
1099
- * If you are trying to *call* this method, instead you probably want to call `getDefaultForServer()` to get default
1100
- * values for a model, or `getFormConfig()` to get a configured form that includes extra form fields.
1101
- *
1102
- * If you are trying to *override* this method, instead you probably want to override `getDefaultForServer()`.
1103
- *
1104
- * The only and **very rare** reason to override this method is if the client needs extra form fields that cannot be
1105
- * accepted by the server (not part of `XXXInput` type) and that are strictly for the client form needs. In that case,
1106
- * then you can return default values for those extra form fields, and the form returned by `getFormConfig()` will
1107
- * include those extra fields.
1108
- */
1109
- getFormExtraFieldDefaultValues() {
1110
- return {};
1111
- }
1112
- /**
1113
- * This is used to extract only the array of fetched objects out of the entire fetched data
1114
- */
1115
- mapAll() {
1116
- return map$1(result => result.data[this.plural]); // See https://github.com/apollographql/apollo-client/issues/5662
1117
- }
1118
- /**
1119
- * This is used to extract only the created object out of the entire fetched data
1120
- */
1121
- mapCreation(result) {
1122
- const name = this.createName ?? 'create' + upperCaseFirstLetter(this.name);
1123
- return result.data[name]; // See https://github.com/apollographql/apollo-client/issues/5662
1124
- }
1125
- /**
1126
- * This is used to extract only the updated object out of the entire fetched data
1127
- */
1128
- mapUpdate(result) {
1129
- const name = this.updateName ?? 'update' + upperCaseFirstLetter(this.name);
1130
- return result.data[name]; // See https://github.com/apollographql/apollo-client/issues/5662
1131
- }
1132
- /**
1133
- * This is used to extract only flag when deleting an object
1134
- */
1135
- mapDelete(result) {
1136
- const name = this.deleteName ?? 'delete' + upperCaseFirstLetter(this.plural);
1137
- return result.data[name]; // See https://github.com/apollographql/apollo-client/issues/5662
1138
- }
1139
- /**
1140
- * Returns additional variables to be used when getting a single object
1141
- *
1142
- * This is typically a site or state ID, and is needed to get appropriate access rights
1143
- */
1144
- getPartialVariablesForOne() {
1145
- return of({});
1146
- }
1147
- /**
1148
- * Returns additional variables to be used when getting multiple objects
1149
- *
1150
- * This is typically a site or state ID, but it could be something else to further filter the query
1151
- */
1152
- getPartialVariablesForAll() {
1153
- return of({});
1154
- }
1155
- /**
1156
- * Returns additional variables to be used when creating an object
1157
- *
1158
- * This is typically a site or state ID
1159
- */
1160
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1161
- getPartialVariablesForCreation(object) {
1162
- return {};
1163
- }
1164
- /**
1165
- * Returns additional variables to be used when updating an object
1166
- *
1167
- * This is typically a site or state ID
1168
- */
1169
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1170
- getPartialVariablesForUpdate(object) {
1171
- return {};
1172
- }
1173
- /**
1174
- * Return additional variables to be used when deleting an object
1175
- *
1176
- * This is typically a site or state ID
1177
- */
1178
- // eslint-disable-next-line @typescript-eslint/no-unused-vars
1179
- getPartialVariablesForDelete(objects) {
1180
- return {};
1181
- }
1182
- /**
1183
- * Throw exception to prevent executing queries with invalid variables
1184
- */
1185
- throwIfObservable(value) {
1186
- if (value instanceof Observable) {
1187
- throw new Error('Cannot use Observable as variables. Instead you should use .subscribe() to call the method with a real value');
1188
- }
1189
- }
1190
- /**
1191
- * Merge given ID with additional partial variables if there is any
1192
- */
1193
- getVariablesForOne(id) {
1194
- return this.getPartialVariablesForOne().pipe(map$1(partialVariables => merge({ id: id }, partialVariables)));
1195
- }
1196
- /**
1197
- * Throw exception to prevent executing null queries
1198
- */
1199
- throwIfNotQuery(query) {
1200
- if (!query) {
1201
- throw new Error('GraphQL query for this method was not configured in this service constructor');
1202
- }
1203
- }
1204
- }
1205
-
1206
- /**
1207
- * **DO NOT MODIFY UNLESS STRICTLY REQUIRED FOR VANILLA**
1208
- *
1209
- * This is a minimal service specialized for Vanilla and any modification,
1210
- * including adding `import` in this file, might break https://navigations.ichtus.club.
1211
- */
1212
-
1213
- /**
1214
- * Generated bundle index. Do not edit.
1215
- */
1216
-
1217
- export { NaturalAbstractModelService, NaturalQueryVariablesManager, formatIsoDateTime, graphqlQuerySigner };
1218
- //# sourceMappingURL=ecodev-natural-vanilla.mjs.map