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