@ethlete/query 5.15.0 → 5.16.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.
Files changed (184) hide show
  1. package/.eslintrc.json +40 -0
  2. package/CHANGELOG.md +14 -0
  3. package/jest.config.ts +23 -0
  4. package/ng-package.json +8 -0
  5. package/package.json +6 -23
  6. package/project.json +37 -0
  7. package/{index.d.ts → src/index.ts} +2 -0
  8. package/src/lib/auth/auth-provider.types.ts +160 -0
  9. package/src/lib/auth/auth-provider.utils.ts +39 -0
  10. package/src/lib/auth/basic-auth-provider.ts +17 -0
  11. package/src/lib/auth/bearer-auth-provider.ts +243 -0
  12. package/src/lib/auth/custom-header-auth-provider.ts +13 -0
  13. package/{lib/class-validator/class-validator.types.d.ts → src/lib/class-validator/class-validator.types.ts} +4 -3
  14. package/src/lib/class-validator/class-validator.utils.ts +5 -0
  15. package/src/lib/devtools/components/query-devtools/query-devtools.component.html +402 -0
  16. package/src/lib/devtools/components/query-devtools/query-devtools.component.scss +360 -0
  17. package/src/lib/devtools/components/query-devtools/query-devtools.component.ts +427 -0
  18. package/src/lib/devtools/pipes/query-short-name.pipe.ts +24 -0
  19. package/src/lib/devtools/utils/query-client.utils.ts +25 -0
  20. package/src/lib/directives/infinity-query-trigger.directive.ts +58 -0
  21. package/src/lib/directives/infinity-query.directive.ts +208 -0
  22. package/src/lib/directives/query.directive.ts +201 -0
  23. package/src/lib/entity/entity-store.ts +180 -0
  24. package/src/lib/entity/entity.types.ts +34 -0
  25. package/src/lib/entity/entity.utils.ts +51 -0
  26. package/src/lib/gql/gql.ts +22 -0
  27. package/src/lib/infinite-query/infinity-query.ts +245 -0
  28. package/src/lib/infinite-query/infinity-query.types.ts +149 -0
  29. package/src/lib/infinite-query/infinity-query.utils.ts +14 -0
  30. package/src/lib/logger/logger.ts +30 -0
  31. package/src/lib/query/query.ts +631 -0
  32. package/src/lib/query/query.types.ts +532 -0
  33. package/src/lib/query/query.utils.spec.ts +92 -0
  34. package/src/lib/query/query.utils.ts +450 -0
  35. package/src/lib/query-client/query-client.spec.ts +77 -0
  36. package/src/lib/query-client/query-client.ts +195 -0
  37. package/src/lib/query-client/query-client.types.ts +101 -0
  38. package/src/lib/query-client/query-client.utils.spec.ts +61 -0
  39. package/src/lib/query-client/query-client.utils.ts +28 -0
  40. package/src/lib/query-creator/query-creator.ts +103 -0
  41. package/src/lib/query-creator/query-creator.types.ts +74 -0
  42. package/src/lib/query-form/query-form.ts +526 -0
  43. package/src/lib/query-form/query-form.types.ts +169 -0
  44. package/src/lib/query-form/query-form.utils.ts +116 -0
  45. package/src/lib/query-store/query-store.ts +192 -0
  46. package/src/lib/request/request.ts +253 -0
  47. package/src/lib/request/request.types.ts +164 -0
  48. package/src/lib/request/request.util.spec.ts +177 -0
  49. package/src/lib/request/request.util.ts +399 -0
  50. package/src/lib/stories/angular-example-page.mdx +85 -0
  51. package/src/lib/stories/authentication-page.mdx +35 -0
  52. package/src/lib/stories/changelog-page.mdx +9 -0
  53. package/src/lib/stories/client-page.mdx +100 -0
  54. package/src/lib/stories/queries-page.mdx +117 -0
  55. package/src/lib/symfony/symfony.types.ts +21 -0
  56. package/src/lib/symfony/symfony.utils.ts +43 -0
  57. package/src/lib/utils/data.utils.ts +280 -0
  58. package/src/lib/utils/deep-freeze.util.ts +21 -0
  59. package/{lib/utils/def.util.d.ts → src/lib/utils/def.util.ts} +1 -1
  60. package/src/test-setup.ts +1 -0
  61. package/tsconfig.json +29 -0
  62. package/tsconfig.lib.json +12 -0
  63. package/tsconfig.lib.prod.json +10 -0
  64. package/tsconfig.spec.json +11 -0
  65. package/esm2022/ethlete-query.mjs +0 -5
  66. package/esm2022/index.mjs +0 -20
  67. package/esm2022/lib/auth/auth-provider.types.mjs +0 -2
  68. package/esm2022/lib/auth/auth-provider.utils.mjs +0 -25
  69. package/esm2022/lib/auth/basic-auth-provider.mjs +0 -15
  70. package/esm2022/lib/auth/bearer-auth-provider.mjs +0 -168
  71. package/esm2022/lib/auth/custom-header-auth-provider.mjs +0 -12
  72. package/esm2022/lib/auth/index.mjs +0 -6
  73. package/esm2022/lib/class-validator/class-validator.types.mjs +0 -2
  74. package/esm2022/lib/class-validator/class-validator.utils.mjs +0 -4
  75. package/esm2022/lib/class-validator/index.mjs +0 -3
  76. package/esm2022/lib/devtools/components/query-devtools/index.mjs +0 -2
  77. package/esm2022/lib/devtools/components/query-devtools/query-devtools.component.mjs +0 -297
  78. package/esm2022/lib/devtools/pipes/index.mjs +0 -2
  79. package/esm2022/lib/devtools/pipes/query-short-name.pipe.mjs +0 -24
  80. package/esm2022/lib/devtools/utils/index.mjs +0 -2
  81. package/esm2022/lib/devtools/utils/query-client.utils.mjs +0 -13
  82. package/esm2022/lib/directives/index.mjs +0 -4
  83. package/esm2022/lib/directives/infinity-query-trigger.directive.mjs +0 -56
  84. package/esm2022/lib/directives/infinity-query.directive.mjs +0 -147
  85. package/esm2022/lib/directives/query.directive.mjs +0 -123
  86. package/esm2022/lib/entity/entity-store.mjs +0 -127
  87. package/esm2022/lib/entity/entity.types.mjs +0 -2
  88. package/esm2022/lib/entity/entity.utils.mjs +0 -29
  89. package/esm2022/lib/entity/index.mjs +0 -4
  90. package/esm2022/lib/gql/gql.mjs +0 -23
  91. package/esm2022/lib/gql/index.mjs +0 -2
  92. package/esm2022/lib/infinite-query/index.mjs +0 -4
  93. package/esm2022/lib/infinite-query/infinity-query.mjs +0 -171
  94. package/esm2022/lib/infinite-query/infinity-query.types.mjs +0 -2
  95. package/esm2022/lib/infinite-query/infinity-query.utils.mjs +0 -3
  96. package/esm2022/lib/logger/index.mjs +0 -2
  97. package/esm2022/lib/logger/logger.mjs +0 -16
  98. package/esm2022/lib/query/index.mjs +0 -4
  99. package/esm2022/lib/query/query.mjs +0 -428
  100. package/esm2022/lib/query/query.types.mjs +0 -2
  101. package/esm2022/lib/query/query.utils.mjs +0 -253
  102. package/esm2022/lib/query-client/index.mjs +0 -4
  103. package/esm2022/lib/query-client/query-client.mjs +0 -80
  104. package/esm2022/lib/query-client/query-client.types.mjs +0 -2
  105. package/esm2022/lib/query-client/query-client.utils.mjs +0 -20
  106. package/esm2022/lib/query-creator/index.mjs +0 -3
  107. package/esm2022/lib/query-creator/query-creator.mjs +0 -55
  108. package/esm2022/lib/query-creator/query-creator.types.mjs +0 -2
  109. package/esm2022/lib/query-form/index.mjs +0 -4
  110. package/esm2022/lib/query-form/query-form.mjs +0 -368
  111. package/esm2022/lib/query-form/query-form.types.mjs +0 -2
  112. package/esm2022/lib/query-form/query-form.utils.mjs +0 -97
  113. package/esm2022/lib/query-store/index.mjs +0 -2
  114. package/esm2022/lib/query-store/query-store.mjs +0 -144
  115. package/esm2022/lib/request/index.mjs +0 -4
  116. package/esm2022/lib/request/request.mjs +0 -196
  117. package/esm2022/lib/request/request.types.mjs +0 -2
  118. package/esm2022/lib/request/request.util.mjs +0 -252
  119. package/esm2022/lib/symfony/index.mjs +0 -3
  120. package/esm2022/lib/symfony/symfony.types.mjs +0 -2
  121. package/esm2022/lib/symfony/symfony.utils.mjs +0 -31
  122. package/esm2022/lib/utils/data.utils.mjs +0 -138
  123. package/esm2022/lib/utils/deep-freeze.util.mjs +0 -17
  124. package/esm2022/lib/utils/def.util.mjs +0 -8
  125. package/esm2022/lib/utils/index.mjs +0 -4
  126. package/fesm2022/ethlete-query.mjs +0 -3276
  127. package/fesm2022/ethlete-query.mjs.map +0 -1
  128. package/lib/auth/auth-provider.types.d.ts +0 -133
  129. package/lib/auth/auth-provider.utils.d.ts +0 -9
  130. package/lib/auth/basic-auth-provider.d.ts +0 -10
  131. package/lib/auth/bearer-auth-provider.d.ts +0 -23
  132. package/lib/auth/custom-header-auth-provider.d.ts +0 -9
  133. package/lib/class-validator/class-validator.utils.d.ts +0 -2
  134. package/lib/devtools/components/query-devtools/query-devtools.component.d.ts +0 -151
  135. package/lib/devtools/pipes/query-short-name.pipe.d.ts +0 -8
  136. package/lib/devtools/utils/query-client.utils.d.ts +0 -12
  137. package/lib/directives/infinity-query-trigger.directive.d.ts +0 -15
  138. package/lib/directives/infinity-query.directive.d.ts +0 -49
  139. package/lib/directives/query.directive.d.ts +0 -63
  140. package/lib/entity/entity-store.d.ts +0 -25
  141. package/lib/entity/entity.types.d.ts +0 -25
  142. package/lib/entity/entity.utils.d.ts +0 -12
  143. package/lib/gql/gql.d.ts +0 -6
  144. package/lib/infinite-query/infinity-query.d.ts +0 -29
  145. package/lib/infinite-query/infinity-query.types.d.ts +0 -104
  146. package/lib/infinite-query/infinity-query.utils.d.ts +0 -4
  147. package/lib/logger/logger.d.ts +0 -12
  148. package/lib/query/query.d.ts +0 -60
  149. package/lib/query/query.types.d.ts +0 -388
  150. package/lib/query/query.utils.d.ts +0 -91
  151. package/lib/query-client/query-client.d.ts +0 -29
  152. package/lib/query-client/query-client.types.d.ts +0 -82
  153. package/lib/query-client/query-client.utils.d.ts +0 -4
  154. package/lib/query-creator/query-creator.d.ts +0 -20
  155. package/lib/query-creator/query-creator.types.d.ts +0 -13
  156. package/lib/query-form/query-form.d.ts +0 -89
  157. package/lib/query-form/query-form.types.d.ts +0 -138
  158. package/lib/query-form/query-form.utils.d.ts +0 -13
  159. package/lib/query-store/query-store.d.ts +0 -31
  160. package/lib/request/request.d.ts +0 -3
  161. package/lib/request/request.types.d.ts +0 -126
  162. package/lib/request/request.util.d.ts +0 -92
  163. package/lib/symfony/symfony.types.d.ts +0 -19
  164. package/lib/symfony/symfony.utils.d.ts +0 -6
  165. package/lib/utils/data.utils.d.ts +0 -59
  166. package/lib/utils/deep-freeze.util.d.ts +0 -1
  167. /package/{lib/auth/index.d.ts → src/lib/auth/index.ts} +0 -0
  168. /package/{lib/class-validator/index.d.ts → src/lib/class-validator/index.ts} +0 -0
  169. /package/{lib/devtools/components/query-devtools/index.d.ts → src/lib/devtools/components/query-devtools/index.ts} +0 -0
  170. /package/{lib/devtools/pipes/index.d.ts → src/lib/devtools/pipes/index.ts} +0 -0
  171. /package/{lib/devtools/utils/index.d.ts → src/lib/devtools/utils/index.ts} +0 -0
  172. /package/{lib/directives/index.d.ts → src/lib/directives/index.ts} +0 -0
  173. /package/{lib/entity/index.d.ts → src/lib/entity/index.ts} +0 -0
  174. /package/{lib/gql/index.d.ts → src/lib/gql/index.ts} +0 -0
  175. /package/{lib/infinite-query/index.d.ts → src/lib/infinite-query/index.ts} +0 -0
  176. /package/{lib/logger/index.d.ts → src/lib/logger/index.ts} +0 -0
  177. /package/{lib/query/index.d.ts → src/lib/query/index.ts} +0 -0
  178. /package/{lib/query-client/index.d.ts → src/lib/query-client/index.ts} +0 -0
  179. /package/{lib/query-creator/index.d.ts → src/lib/query-creator/index.ts} +0 -0
  180. /package/{lib/query-form/index.d.ts → src/lib/query-form/index.ts} +0 -0
  181. /package/{lib/query-store/index.d.ts → src/lib/query-store/index.ts} +0 -0
  182. /package/{lib/request/index.d.ts → src/lib/request/index.ts} +0 -0
  183. /package/{lib/symfony/index.d.ts → src/lib/symfony/index.ts} +0 -0
  184. /package/{lib/utils/index.d.ts → src/lib/utils/index.ts} +0 -0
@@ -0,0 +1,208 @@
1
+ /* eslint-disable @typescript-eslint/no-explicit-any */
2
+ import {
3
+ ChangeDetectorRef,
4
+ Directive,
5
+ ErrorHandler,
6
+ inject,
7
+ InjectionToken,
8
+ Input,
9
+ TemplateRef,
10
+ ViewContainerRef,
11
+ } from '@angular/core';
12
+ import { toSignal } from '@angular/core/rxjs-interop';
13
+ import { createDestroy, DelayableDirective } from '@ethlete/core';
14
+ import { BehaviorSubject, combineLatest, Subject, takeUntil, tap, withLatestFrom } from 'rxjs';
15
+ import { InfinityQuery, InfinityQueryConfig, InfinityQueryOf } from '../infinite-query';
16
+ import {
17
+ BaseArguments,
18
+ isQueryStateFailure,
19
+ isQueryStateLoading,
20
+ isQueryStateSuccess,
21
+ switchQueryState,
22
+ } from '../query';
23
+ import { AnyQueryCreator, ConstructQuery } from '../query-creator';
24
+ import { RequestError } from '../request';
25
+
26
+ interface InfinityQueryContext<
27
+ Q extends InfinityQueryConfig<AnyQueryCreator, BaseArguments | undefined, any, unknown[]>,
28
+ > {
29
+ $implicit: Q['response']['arrayType'] | null;
30
+ etInfinityQuery: Q['response']['arrayType'] | null;
31
+ loading: boolean;
32
+ error: RequestError<unknown> | null;
33
+
34
+ isFirstLoad: boolean;
35
+ canLoadMore: boolean;
36
+ currentPage: number | null;
37
+ currentCalculatedPage: number | null;
38
+ totalPages: number | null;
39
+ itemsPerPage: number | null;
40
+
41
+ currentQuery: ConstructQuery<Q['queryCreator']> | null;
42
+ }
43
+
44
+ export const INFINITY_QUERY_TOKEN = new InjectionToken<InfinityQueryDirective<any>>('INFINITY_QUERY_TOKEN');
45
+
46
+ @Directive({
47
+ // eslint-disable-next-line @angular-eslint/directive-selector
48
+ selector: '[etInfinityQuery]',
49
+ exportAs: 'etInfinityQuery',
50
+ standalone: true,
51
+ providers: [{ provide: INFINITY_QUERY_TOKEN, useExisting: InfinityQueryDirective }],
52
+ hostDirectives: [DelayableDirective],
53
+ })
54
+ export class InfinityQueryDirective<
55
+ Q extends InfinityQueryConfig<AnyQueryCreator, BaseArguments | undefined, any, unknown[]>,
56
+ > {
57
+ private readonly _queryConfigChanged$ = new Subject<boolean>();
58
+ private readonly _viewContext: InfinityQueryContext<Q> = {
59
+ $implicit: null,
60
+ etInfinityQuery: null,
61
+ loading: false,
62
+ error: null,
63
+
64
+ isFirstLoad: false,
65
+ canLoadMore: false,
66
+ currentPage: null,
67
+ currentCalculatedPage: null,
68
+ totalPages: null,
69
+ itemsPerPage: null,
70
+
71
+ currentQuery: null,
72
+ };
73
+ private _infinityQueryInstance: InfinityQueryOf<Q> | null = null;
74
+
75
+ private readonly _destroy$ = createDestroy();
76
+ private readonly _cdr = inject(ChangeDetectorRef);
77
+ private readonly _viewContainerRef = inject(ViewContainerRef);
78
+ private readonly _mainTemplateRef = inject(TemplateRef<InfinityQueryContext<Q>>);
79
+ private readonly _errorHandler = inject(ErrorHandler);
80
+ private readonly _delayable = inject(DelayableDirective, { host: true });
81
+
82
+ private readonly _data$ = new BehaviorSubject<Q['response']['arrayType']>([]);
83
+
84
+ @Input('etInfinityQuery')
85
+ get infinityQuery(): Q {
86
+ return this._infinityQuery;
87
+ }
88
+ set infinityQuery(v: Q) {
89
+ this._cleanup();
90
+
91
+ this._infinityQuery = v;
92
+ this._infinityQueryInstance = this._setupInfinityQuery(v);
93
+ this._infinityQueryInstance.nextPage();
94
+ }
95
+ private _infinityQuery!: Q;
96
+
97
+ get context() {
98
+ return this._viewContext;
99
+ }
100
+
101
+ get instance() {
102
+ return this._infinityQueryInstance;
103
+ }
104
+
105
+ get data$() {
106
+ return this._data$.asObservable();
107
+ }
108
+
109
+ data = toSignal(this._data$, { requireSync: true });
110
+
111
+ static ngTemplateContextGuard<
112
+ Q extends InfinityQueryConfig<AnyQueryCreator, BaseArguments | undefined, any, unknown[]>,
113
+ >(dir: InfinityQueryDirective<Q>, ctx: unknown): ctx is InfinityQueryContext<Q> {
114
+ return true;
115
+ }
116
+
117
+ constructor() {
118
+ this._viewContainerRef.createEmbeddedView(this._mainTemplateRef, this._viewContext);
119
+ }
120
+
121
+ private _setupInfinityQuery(config: Q) {
122
+ const instance = new InfinityQuery(config, this._destroy$) as InfinityQueryOf<Q>;
123
+
124
+ combineLatest([
125
+ instance.currentQuery$.pipe(switchQueryState(), withLatestFrom(instance.currentQuery$)),
126
+ this._delayable.isDelayed$,
127
+ instance.data$,
128
+ ])
129
+ .pipe(
130
+ tap(([[state, currentQuery], isDelayed, infinityArray]) => {
131
+ this._viewContext.currentPage = instance.currentPage;
132
+ this._viewContext.totalPages = instance.totalPages;
133
+ this._viewContext.itemsPerPage = instance.itemsPerPage;
134
+ this._viewContext.canLoadMore =
135
+ (instance.totalPages !== null &&
136
+ instance.currentPage !== null &&
137
+ instance.totalPages > instance.currentPage) ||
138
+ false;
139
+ this._viewContext.currentCalculatedPage = instance.currentCalculatedPage;
140
+ this._viewContext.currentQuery = currentQuery;
141
+
142
+ if (isQueryStateLoading(state) || isDelayed || !infinityArray) {
143
+ this._viewContext.loading = state ? state.meta.triggeredVia !== 'poll' : true;
144
+ this._viewContext.error = null;
145
+ this._viewContext.isFirstLoad = this.context.etInfinityQuery === null;
146
+ } else if (isQueryStateFailure(state)) {
147
+ this._viewContext.loading = false;
148
+ this._viewContext.error = state.error;
149
+ this._viewContext.isFirstLoad = false;
150
+ this._errorHandler.handleError(state.error);
151
+ } else if (isQueryStateSuccess(state)) {
152
+ this._viewContext.loading = false;
153
+ this._viewContext.error = null;
154
+ this._viewContext.isFirstLoad = false;
155
+ this._viewContext.etInfinityQuery = this._viewContext.$implicit =
156
+ infinityArray as Q['response']['arrayType'];
157
+ this._data$.next(infinityArray);
158
+ }
159
+
160
+ this._cdr.markForCheck();
161
+ }),
162
+ takeUntil(this._destroy$),
163
+ takeUntil(this._queryConfigChanged$),
164
+ )
165
+ .subscribe();
166
+
167
+ return instance;
168
+ }
169
+
170
+ loadNextPage() {
171
+ if (!this._infinityQueryInstance) {
172
+ return;
173
+ }
174
+
175
+ if (this._viewContext.loading) {
176
+ return;
177
+ }
178
+
179
+ this._infinityQueryInstance.nextPage();
180
+ }
181
+
182
+ reset(newConfig?: Omit<Q, 'queryCreator' | 'response'>) {
183
+ if (!this._infinityQueryInstance) {
184
+ return;
185
+ }
186
+
187
+ this._infinityQueryInstance.reset(newConfig as any);
188
+ }
189
+
190
+ private _cleanup() {
191
+ this._queryConfigChanged$.next(true);
192
+
193
+ this._viewContext.loading = false;
194
+ this._viewContext.error = null;
195
+ this._viewContext.etInfinityQuery = null;
196
+ this._viewContext.$implicit = null;
197
+
198
+ this._viewContext.isFirstLoad = false;
199
+ this._viewContext.canLoadMore = false;
200
+ this._viewContext.currentPage = null;
201
+ this._viewContext.itemsPerPage = null;
202
+ this._viewContext.totalPages = null;
203
+
204
+ this._viewContext.currentQuery = null;
205
+
206
+ this._data$.next([]);
207
+ }
208
+ }
@@ -0,0 +1,201 @@
1
+ import {
2
+ ChangeDetectorRef,
3
+ Directive,
4
+ ErrorHandler,
5
+ Input,
6
+ OnDestroy,
7
+ OnInit,
8
+ TemplateRef,
9
+ ViewContainerRef,
10
+ inject,
11
+ } from '@angular/core';
12
+ import { Subscription, tap } from 'rxjs';
13
+ import {
14
+ AnyQuery,
15
+ AnyQueryCollection,
16
+ QueryCollectionKeysOf,
17
+ QueryOf,
18
+ QueryState,
19
+ extractQuery,
20
+ isQueryCollection,
21
+ isQueryStateCancelled,
22
+ isQueryStateFailure,
23
+ isQueryStateLoading,
24
+ isQueryStatePrepared,
25
+ isQueryStateSuccess,
26
+ } from '../query';
27
+ import { QueryDataOf } from '../query-creator';
28
+ import { RequestError, RequestProgress } from '../request';
29
+
30
+ interface QueryContext<Q extends AnyQuery | AnyQueryCollection | null> {
31
+ /**
32
+ * The queries's response data.
33
+ */
34
+ $implicit: QueryDataOf<QueryOf<Q>> | null;
35
+
36
+ /**
37
+ * The queries's response data.
38
+ */
39
+ etQuery: QueryDataOf<QueryOf<Q>> | null;
40
+
41
+ /**
42
+ * The query used inside this directive. Useful if for instance the query gets unwrapped via async pipe inside the directive.
43
+ */
44
+ query: QueryOf<Q> | null;
45
+
46
+ /**
47
+ * Is true when the query is triggered by user interaction.
48
+ */
49
+ loading: boolean;
50
+
51
+ /**
52
+ * Is true when the query is triggered by either polling or auto refresh event.
53
+ */
54
+ refreshing: boolean;
55
+
56
+ /**
57
+ * The query's progress state.
58
+ */
59
+ progress: RequestProgress | null;
60
+
61
+ /**
62
+ * The query's error state.
63
+ */
64
+ error: RequestError<unknown> | null;
65
+
66
+ /**
67
+ * The query's scope (only available when the query is a collection)
68
+ */
69
+ scope: QueryCollectionKeysOf<Q> | null;
70
+ }
71
+
72
+ @Directive({
73
+ // eslint-disable-next-line @angular-eslint/directive-selector
74
+ selector: '[etQuery]',
75
+ standalone: true,
76
+ })
77
+ export class QueryDirective<Q extends AnyQuery | AnyQueryCollection | null> implements OnInit, OnDestroy {
78
+ private _mainTemplateRef = inject<TemplateRef<QueryContext<Q>>>(TemplateRef);
79
+ private _viewContainerRef = inject(ViewContainerRef);
80
+ private _errorHandler = inject(ErrorHandler);
81
+ private _cdr = inject(ChangeDetectorRef);
82
+
83
+ private _isMainViewCreated = false;
84
+ private _subscription: Subscription | null = null;
85
+
86
+ private readonly _viewContext: QueryContext<Q> = {
87
+ $implicit: null,
88
+ etQuery: null,
89
+ loading: false,
90
+ refreshing: false,
91
+ error: null,
92
+ progress: null,
93
+ scope: null,
94
+ query: null,
95
+ };
96
+
97
+ @Input('etQuery')
98
+ get query(): Q {
99
+ return this._query;
100
+ }
101
+ set query(v: Q) {
102
+ this._query = v;
103
+
104
+ this._subscribeToQuery();
105
+ }
106
+ private _query!: Q;
107
+
108
+ @Input('etQueryCache')
109
+ get cache(): boolean {
110
+ return this._cache;
111
+ }
112
+ set cache(v: boolean) {
113
+ this._cache = v;
114
+ }
115
+ private _cache = false;
116
+
117
+ static ngTemplateContextGuard<Q extends AnyQuery | AnyQueryCollection | null>(
118
+ dir: QueryDirective<Q>,
119
+ ctx: unknown,
120
+ ): ctx is QueryContext<Q> {
121
+ return true;
122
+ }
123
+
124
+ ngOnInit(): void {
125
+ this._renderMainView();
126
+ }
127
+
128
+ ngOnDestroy(): void {
129
+ this._subscription?.unsubscribe();
130
+ }
131
+
132
+ private _subscribeToQuery(): void {
133
+ this._subscription?.unsubscribe();
134
+ this._subscription = null;
135
+
136
+ const query = extractQuery(this.query);
137
+
138
+ if (!query) {
139
+ this._viewContext.$implicit = null;
140
+ this._viewContext.etQuery = null;
141
+ this._viewContext.loading = false;
142
+ this._viewContext.refreshing = false;
143
+ this._viewContext.error = null;
144
+ this._viewContext.progress = null;
145
+ this._viewContext.scope = null;
146
+ this._viewContext.query = null;
147
+ return;
148
+ }
149
+
150
+ if (isQueryStatePrepared(query.rawState) || isQueryStateCancelled(query.rawState)) {
151
+ query.execute();
152
+ }
153
+
154
+ const sub = query.state$.pipe(tap((state) => this._updateView(state))).subscribe();
155
+
156
+ this._viewContext.scope = isQueryCollection(query) ? (query.type as QueryCollectionKeysOf<Q>) : null;
157
+ this._viewContext.query = query as QueryOf<Q>;
158
+
159
+ this._subscription = sub;
160
+ }
161
+
162
+ private _updateView(state: QueryState) {
163
+ if (isQueryStateLoading(state)) {
164
+ this._viewContext.progress = state.progress ?? null;
165
+ this._viewContext.refreshing = state.meta.triggeredVia === 'auto' || state.meta.triggeredVia === 'poll';
166
+
167
+ if (!this._viewContext.refreshing) {
168
+ this._viewContext.loading = true;
169
+ }
170
+ } else {
171
+ this._viewContext.loading = false;
172
+ this._viewContext.refreshing = false;
173
+ this._viewContext.progress = null;
174
+ }
175
+
176
+ if (isQueryStateSuccess(state)) {
177
+ this._viewContext.etQuery = state.response as QueryDataOf<QueryOf<Q>>;
178
+ this._viewContext.$implicit = state.response as QueryDataOf<QueryOf<Q>>;
179
+ } else if (!this.cache) {
180
+ this._viewContext.etQuery = null;
181
+ this._viewContext.$implicit = null;
182
+ }
183
+
184
+ if (isQueryStateFailure(state)) {
185
+ this._viewContext.error = state.error;
186
+
187
+ this._errorHandler.handleError(state.error);
188
+ } else {
189
+ this._viewContext.error = null;
190
+ }
191
+
192
+ this._cdr.markForCheck();
193
+ }
194
+
195
+ private _renderMainView(): void {
196
+ if (!this._isMainViewCreated) {
197
+ this._isMainViewCreated = true;
198
+ this._viewContainerRef.createEmbeddedView(this._mainTemplateRef, this._viewContext);
199
+ }
200
+ }
201
+ }
@@ -0,0 +1,180 @@
1
+ import { Observable, Subject, debounceTime, filter, map, shareReplay, startWith } from 'rxjs';
2
+ import { EntityKey, EntityStoreConfig } from './entity.types';
3
+
4
+ class EntityStoreError extends Error {
5
+ constructor(
6
+ message: string,
7
+ public detail?: unknown,
8
+ ) {
9
+ super(message);
10
+ this.name = 'EntityStoreError';
11
+ }
12
+ }
13
+
14
+ function flatten<T>(arr: T[][]): T[] {
15
+ return arr.reduce((acc, val) => acc.concat(val), []);
16
+ }
17
+
18
+ export class EntityStore<T> {
19
+ /**
20
+ * @internal
21
+ */
22
+ readonly _dictionary = new Map<EntityKey, T>();
23
+
24
+ /**
25
+ * @internal
26
+ */
27
+ readonly _change$ = new Subject<EntityKey | EntityKey[]>();
28
+
29
+ constructor(private readonly _config: EntityStoreConfig) {}
30
+
31
+ select(key: EntityKey): Observable<T | null>;
32
+ select(keys: EntityKey[]): Observable<T[]>;
33
+ select(keys: EntityKey[][]): Observable<T[]>;
34
+ select(keyOrKeys: EntityKey | EntityKey[] | EntityKey[][]): Observable<T | null> | Observable<T[]> {
35
+ if (Array.isArray(keyOrKeys)) {
36
+ const _keys = this._normalizeKeys(keyOrKeys);
37
+ return this._selectMany(_keys as EntityKey[]);
38
+ }
39
+
40
+ return this._select(keyOrKeys);
41
+ }
42
+
43
+ /**
44
+ * **Note:** Prefer `select` over `selectWhere` when possible.
45
+ */
46
+ selectWhere(predicate: (value: T) => boolean): Observable<T[]> {
47
+ return this._change$.pipe(
48
+ startWith(null),
49
+ debounceTime(0),
50
+ map(() => Array.from(this._dictionary.values()).filter(predicate)),
51
+ shareReplay({ bufferSize: 1, refCount: true }),
52
+ );
53
+ }
54
+
55
+ set(key: EntityKey, value: T): void;
56
+ set(keys: EntityKey[], values: T[]): void;
57
+ set(keys: EntityKey[][], values: T[][]): void;
58
+ set(keyOrKeys: EntityKey | EntityKey[] | EntityKey[][], valueOrValues: T | T[] | T[][]) {
59
+ const _keys = this._normalizeKeys(keyOrKeys);
60
+
61
+ if (Array.isArray(_keys)) {
62
+ if (!Array.isArray(valueOrValues)) {
63
+ throw new EntityStoreError('When setting multiple keys, values must be an array', {
64
+ keyOrValues: _keys,
65
+ valueOrValues,
66
+ });
67
+ }
68
+
69
+ const _values = (Array.isArray(valueOrValues[0]) ? flatten(valueOrValues as T[][]) : valueOrValues) as T[];
70
+
71
+ for (let i = 0; i < _keys.length; i++) {
72
+ const k = _keys[i];
73
+ const v = _values[i];
74
+
75
+ if (k === undefined || v === undefined) continue;
76
+
77
+ this._set(k, v);
78
+ }
79
+ } else {
80
+ this._set(_keys, valueOrValues as T);
81
+ }
82
+
83
+ this._change$.next(_keys);
84
+ }
85
+
86
+ remove(key: EntityKey): void;
87
+ remove(keys: EntityKey[]): void;
88
+ remove(keys: EntityKey[][]): void;
89
+ remove(keyOrKeys: EntityKey | EntityKey[] | EntityKey[][]) {
90
+ const _keys = this._normalizeKeys(keyOrKeys);
91
+
92
+ if (Array.isArray(_keys)) {
93
+ for (const key of _keys) {
94
+ this._remove(key);
95
+ }
96
+ } else {
97
+ this._remove(_keys);
98
+ }
99
+
100
+ this._change$.next(_keys);
101
+ }
102
+
103
+ logState() {
104
+ console.log('EntityStore:', this._config.name, {
105
+ dictionary: this._dictionary,
106
+ });
107
+ }
108
+
109
+ private _select(key: EntityKey): Observable<T | null> {
110
+ if (this._config.logActions) {
111
+ console.log('EntityStore: select', this._config.name, key);
112
+ }
113
+
114
+ return this._change$.pipe(
115
+ startWith(null),
116
+ filter((v) => {
117
+ if (v === null) {
118
+ return true;
119
+ }
120
+
121
+ if (Array.isArray(v)) {
122
+ return v.includes(key);
123
+ }
124
+
125
+ return v === key;
126
+ }),
127
+ map(() => this._dictionary.get(key) ?? null),
128
+ );
129
+ }
130
+
131
+ private _selectMany(keys: EntityKey[]): Observable<T[]> {
132
+ if (this._config.logActions) {
133
+ console.log('EntityStore: selectMany', this._config.name, keys);
134
+ }
135
+
136
+ return this._change$.pipe(
137
+ startWith(null),
138
+ filter((v) => {
139
+ if (v === null) {
140
+ return true;
141
+ }
142
+
143
+ if (Array.isArray(v)) {
144
+ return keys.some((key) => v.includes(key));
145
+ }
146
+
147
+ return keys.includes(v);
148
+ }),
149
+ map(() => keys.map((key) => this._dictionary.get(key)).filter((value): value is T => value !== undefined)),
150
+ );
151
+ }
152
+
153
+ private _set(key: EntityKey, value: T) {
154
+ if (this._config.logActions) {
155
+ console.log('EntityStore: set', this._config.name, key, value);
156
+ }
157
+
158
+ this._dictionary.set(key, value);
159
+ }
160
+
161
+ private _remove(key: EntityKey) {
162
+ if (this._config.logActions) {
163
+ console.log('EntityStore: remove', this._config.name, key);
164
+ }
165
+
166
+ this._dictionary.delete(key);
167
+ }
168
+
169
+ private _normalizeKeys(keys: EntityKey | EntityKey[] | EntityKey[][]): EntityKey | EntityKey[] {
170
+ if (Array.isArray(keys)) {
171
+ if (Array.isArray(keys[0])) {
172
+ return flatten(keys as EntityKey[][]);
173
+ }
174
+
175
+ return keys as EntityKey[];
176
+ }
177
+
178
+ return keys;
179
+ }
180
+ }
@@ -0,0 +1,34 @@
1
+ export type EntityKey = string | number;
2
+
3
+ export interface EntityStoreConfig {
4
+ name: string;
5
+ logActions?: boolean;
6
+ }
7
+
8
+ export interface InsertFromConfig<
9
+ OriginStoreType,
10
+ OriginKeys extends keyof NonNullable<OriginStoreType>,
11
+ IdFn extends (value: NonNullable<OriginStoreType>) => EntityKey | EntityKey[],
12
+ > {
13
+ /**
14
+ * The property key of which the value should be replaced.
15
+ */
16
+ for: OriginKeys;
17
+
18
+ /**
19
+ * A function that returns the id of the entity. Can also return an array of ids.
20
+ */
21
+ id: IdFn;
22
+ }
23
+
24
+ export interface RemoveFromConfig<T> {
25
+ /**
26
+ * The where function to select the entities to remove.
27
+ */
28
+ where: (entity: T) => boolean;
29
+
30
+ /**
31
+ * A function that returns the id of the entity.
32
+ */
33
+ id: (entity: T) => EntityKey;
34
+ }
@@ -0,0 +1,51 @@
1
+ import { Paginated } from '@ethlete/types';
2
+ import { map, of, switchMap, take, tap } from 'rxjs';
3
+ import { EntityStore } from './entity-store';
4
+ import { EntityKey, InsertFromConfig, RemoveFromConfig } from './entity.types';
5
+
6
+ export function mapToPaginated<T>(response: Paginated<T>) {
7
+ return map((items) => ({ ...response, items }) as Paginated<T>);
8
+ }
9
+
10
+ // FIXME: This breaks if SubStoreType is an array
11
+ /**
12
+ * Use an other store to represent a property of the current entity.
13
+ */
14
+ export const insertFrom = <
15
+ OriginStoreType,
16
+ OriginKeys extends keyof NonNullable<OriginStoreType>,
17
+ SubStoreType extends NonNullable<OriginStoreType>[OriginKeys] extends Array<infer U>
18
+ ? U
19
+ : NonNullable<OriginStoreType>[OriginKeys],
20
+ IdFn extends (value: NonNullable<OriginStoreType>) => EntityKey | EntityKey[],
21
+ ComputedEntityType extends ReturnType<IdFn> extends Array<unknown>
22
+ ? OriginStoreType
23
+ : Omit<OriginStoreType, OriginKeys> & { [K in OriginKeys]: SubStoreType | null },
24
+ >(
25
+ store: EntityStore<SubStoreType>,
26
+ { for: key, id }: InsertFromConfig<OriginStoreType, OriginKeys, IdFn>,
27
+ ) =>
28
+ switchMap((value: OriginStoreType) => {
29
+ if (!value) return of(null);
30
+
31
+ const ids = id(value);
32
+
33
+ if (Array.isArray(ids)) {
34
+ return store.select(ids).pipe(map((data) => ({ ...value, [key]: data }) as ComputedEntityType));
35
+ } else {
36
+ return store.select(ids).pipe(map((data) => ({ ...value, [key]: data }) as ComputedEntityType));
37
+ }
38
+ });
39
+
40
+ /**
41
+ * Remove entities from a store based on a where function.
42
+ */
43
+ export const removeFrom = <T>(store: EntityStore<T>, { where, id }: RemoveFromConfig<T>) => {
44
+ store
45
+ .selectWhere(where)
46
+ .pipe(
47
+ tap((entities) => store.remove(entities.map(id))),
48
+ take(1),
49
+ )
50
+ .subscribe();
51
+ };
@@ -0,0 +1,22 @@
1
+ const getOpName = /(query|mutation) ?([\w\d-_]+)? ?\(.*?\)? \{/;
2
+
3
+ export const transformGql = (str: string) => {
4
+ str = Array.isArray(str) ? str.join('') : str;
5
+ const name = getOpName.exec(str);
6
+ return function (variables: Record<string, unknown> | null | undefined) {
7
+ const data: Record<string, unknown> = { query: str };
8
+ if (variables) data['variables'] = JSON.stringify(variables);
9
+ if (name && name.length) {
10
+ const operationName = name[2];
11
+ if (operationName) data['operationName'] = name[2];
12
+ }
13
+ return data as { query: string; variables?: string; operationName?: string };
14
+ };
15
+ };
16
+
17
+ export const gql = (strings: TemplateStringsArray, ...values: string[]) => {
18
+ const str = strings.reduce((acc, cur, i) => {
19
+ return acc + cur + (values[i] || '');
20
+ }, '');
21
+ return str;
22
+ };