@ethlete/query 6.0.0-next.16 → 6.0.0-next.17
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/CHANGELOG.md +12 -0
- package/fesm2022/ethlete-query.mjs +263 -117
- package/fesm2022/ethlete-query.mjs.map +1 -1
- package/package.json +1 -2
- package/types/ethlete-query.d.ts +110 -4
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# @ethlete/query
|
|
2
2
|
|
|
3
|
+
## 6.0.0-next.17
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- [#3030](https://github.com/ethlete-io/ethdk/pull/3030) [`47e4109`](https://github.com/ethlete-io/ethdk/commit/47e41097adf96788120840bdd03cad25306628f2) Thanks [@github-actions](https://github.com/apps/github-actions)! - Add `validateWithQuery` (v3) and `validateWithV2Query` (v2) — query-backed async
|
|
8
|
+
validators for Angular signal forms. They adapt an `@ethlete/query` query into
|
|
9
|
+
`validateAsync`, so server-side validation runs through the query client (auth,
|
|
10
|
+
base route, caching, error normalization) instead of a raw `httpResource`. A
|
|
11
|
+
`422 FormViolationListView` maps each violation onto its child field by
|
|
12
|
+
`propertyPath` (via `mapViolationsToFormErrors`); success reports no errors and
|
|
13
|
+
network/other errors surface as a form-level error.
|
|
14
|
+
|
|
3
15
|
## 6.0.0-next.16
|
|
4
16
|
|
|
5
17
|
### Minor Changes
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as i0 from '@angular/core';
|
|
2
|
-
import { isDevMode, DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, computed, runInInjectionContext, assertInInjectionContext, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, ElementRef, input, Pipe, ViewEncapsulation, Component, DOCUMENT, NgZone } from '@angular/core';
|
|
2
|
+
import { isDevMode, DestroyRef, InjectionToken, inject, Injector, EnvironmentInjector, ErrorHandler, createEnvironmentInjector, effect, untracked, signal, linkedSignal, computed, runInInjectionContext, assertInInjectionContext, ChangeDetectorRef, ViewContainerRef, TemplateRef, Input, Directive, ElementRef, input, Pipe, ViewEncapsulation, Component, resource, DOCUMENT, NgZone } from '@angular/core';
|
|
3
3
|
import { createStaticProvider, RuntimeError, clamp, createDestroy, computedTillTruthy, getCookie, setCookie, deleteCookie, createProvider, nextFrame, randomId, createRootProvider, isObject, injectRoute, getDomain, injectQueryParamChanges, injectQueryParams, clone, equal, ET_PROPERTY_REMOVED, previousSignalValue } from '@ethlete/core';
|
|
4
4
|
import { filter, Subject, Subscription, Observable, tap, retry, throwError, timer, catchError, takeUntil, startWith, pairwise, switchMap, of, BehaviorSubject, fromEvent, take, ReplaySubject, map, shareReplay, interval, skip, takeWhile, combineLatest, EMPTY, withLatestFrom, debounceTime, firstValueFrom, merge, throttleTime } from 'rxjs';
|
|
5
5
|
import { toObservable, takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop';
|
|
@@ -8,6 +8,7 @@ import { getActiveConsumer, setActiveConsumer } from '@angular/core/primitives/s
|
|
|
8
8
|
import { NgClass, AsyncPipe, JsonPipe } from '@angular/common';
|
|
9
9
|
import * as i1 from '@angular/forms';
|
|
10
10
|
import { FormControl, ReactiveFormsModule, FormGroup } from '@angular/forms';
|
|
11
|
+
import { validateAsync } from '@angular/forms/signals';
|
|
11
12
|
import { Router, ActivatedRoute } from '@angular/router';
|
|
12
13
|
import { io } from 'socket.io-client';
|
|
13
14
|
|
|
@@ -5063,6 +5064,216 @@ const removeFrom = (store, { where, id }) => {
|
|
|
5063
5064
|
.subscribe();
|
|
5064
5065
|
};
|
|
5065
5066
|
|
|
5067
|
+
/** `kind` of a `ValidationError` created from a single server violation. */
|
|
5068
|
+
const SERVER_VIOLATION_ERROR_KIND = 'etServerViolation';
|
|
5069
|
+
/** `kind` of the form-level fallback error created when a failed request carries no violations. */
|
|
5070
|
+
const SERVER_ERROR_KIND = 'etServerError';
|
|
5071
|
+
/**
|
|
5072
|
+
* Extracts the violation list from a failed request in any of the shapes it may reach you:
|
|
5073
|
+
* a `QueryErrorResponse` (from `query.error()` or a settled snapshot), a raw `HttpErrorResponse`,
|
|
5074
|
+
* an already-unwrapped error body, or a plain violation array. Returns `[]` when the error
|
|
5075
|
+
* carries no violations.
|
|
5076
|
+
*/
|
|
5077
|
+
const extractFormViolations = (error) => {
|
|
5078
|
+
if (!error)
|
|
5079
|
+
return [];
|
|
5080
|
+
const body = isQueryErrorResponse(error) ? error.raw.error : error instanceof HttpErrorResponse ? error.error : error;
|
|
5081
|
+
if (isSymfonyFormViolationListError(body)) {
|
|
5082
|
+
return body.violations;
|
|
5083
|
+
}
|
|
5084
|
+
if (isSymfonyListError(body)) {
|
|
5085
|
+
return body;
|
|
5086
|
+
}
|
|
5087
|
+
return [];
|
|
5088
|
+
};
|
|
5089
|
+
/**
|
|
5090
|
+
* Maps a failed request's violations onto a signal-forms field tree as validation errors —
|
|
5091
|
+
* the bridge between an API's violation list (`@ethlete/types` `FormViolationView`) and
|
|
5092
|
+
* signal forms.
|
|
5093
|
+
*
|
|
5094
|
+
* Each violation's `propertyPath` (e.g. `items[2].name`) is resolved against `fieldTree`; a
|
|
5095
|
+
* resolved violation becomes a `ServerViolationValidationError` bound to that field, an
|
|
5096
|
+
* unresolved one becomes a form-level error (customizable via `onUnmappedViolation`). A failed
|
|
5097
|
+
* request that carries no violations at all degrades to form-level `ServerValidationError`s
|
|
5098
|
+
* built from the normalized error message — so returning this function's result from a
|
|
5099
|
+
* `submit()` action never silently succeeds on failure.
|
|
5100
|
+
*
|
|
5101
|
+
* ```ts
|
|
5102
|
+
* await submit(this.form, async (field) => {
|
|
5103
|
+
* const snapshot = await executeUntilSettled(this.createPost, { args: { body: field().value() } });
|
|
5104
|
+
* const error = snapshot.error();
|
|
5105
|
+
*
|
|
5106
|
+
* if (!error) return;
|
|
5107
|
+
*
|
|
5108
|
+
* return mapViolationsToFormErrors({ fieldTree: field, error });
|
|
5109
|
+
* });
|
|
5110
|
+
* ```
|
|
5111
|
+
*/
|
|
5112
|
+
const mapViolationsToFormErrors = (options) => {
|
|
5113
|
+
const { fieldTree, error, rewritePath, onUnmappedViolation } = options;
|
|
5114
|
+
if (error === null || error === undefined)
|
|
5115
|
+
return [];
|
|
5116
|
+
const violations = extractFormViolations(error);
|
|
5117
|
+
if (!violations.length) {
|
|
5118
|
+
// An empty violation array is an explicit "nothing to map" — only unrecognized error
|
|
5119
|
+
// shapes degrade to the form-level fallback.
|
|
5120
|
+
if (Array.isArray(error) && !error.length)
|
|
5121
|
+
return [];
|
|
5122
|
+
return createFallbackErrors(error);
|
|
5123
|
+
}
|
|
5124
|
+
const errors = [];
|
|
5125
|
+
for (const violation of violations) {
|
|
5126
|
+
const rawPath = violation.propertyPath;
|
|
5127
|
+
const path = rawPath && rewritePath ? rewritePath(rawPath, violation) : rawPath;
|
|
5128
|
+
const resolvedFieldTree = path ? resolveFieldTreePath(fieldTree, path) : null;
|
|
5129
|
+
if (resolvedFieldTree) {
|
|
5130
|
+
const mappedError = {
|
|
5131
|
+
kind: SERVER_VIOLATION_ERROR_KIND,
|
|
5132
|
+
message: violation.message,
|
|
5133
|
+
fieldTree: resolvedFieldTree,
|
|
5134
|
+
violation,
|
|
5135
|
+
};
|
|
5136
|
+
errors.push(mappedError);
|
|
5137
|
+
}
|
|
5138
|
+
else if (onUnmappedViolation) {
|
|
5139
|
+
const customError = onUnmappedViolation(violation);
|
|
5140
|
+
if (customError) {
|
|
5141
|
+
errors.push(customError);
|
|
5142
|
+
}
|
|
5143
|
+
}
|
|
5144
|
+
else {
|
|
5145
|
+
const unmappedError = {
|
|
5146
|
+
kind: SERVER_VIOLATION_ERROR_KIND,
|
|
5147
|
+
message: violation.message,
|
|
5148
|
+
violation,
|
|
5149
|
+
};
|
|
5150
|
+
errors.push(unmappedError);
|
|
5151
|
+
}
|
|
5152
|
+
}
|
|
5153
|
+
return errors;
|
|
5154
|
+
};
|
|
5155
|
+
const createFallbackErrors = (error) => {
|
|
5156
|
+
const queryError = isQueryErrorResponse(error) ? error : createQueryErrorResponse(error);
|
|
5157
|
+
if (queryError.isList) {
|
|
5158
|
+
return queryError.errors.map((item) => ({ kind: SERVER_ERROR_KIND, message: item.message }));
|
|
5159
|
+
}
|
|
5160
|
+
return [{ kind: SERVER_ERROR_KIND, message: queryError.error.message }];
|
|
5161
|
+
};
|
|
5162
|
+
/** Splits a property path like `items[2].name` or `translations['de'].title` into its segments. */
|
|
5163
|
+
const parsePropertyPath = (path) => {
|
|
5164
|
+
return path
|
|
5165
|
+
.replace(/\[(['"]?)([^\]]*?)\1\]/g, '.$2')
|
|
5166
|
+
.split('.')
|
|
5167
|
+
.filter((segment) => segment.length > 0);
|
|
5168
|
+
};
|
|
5169
|
+
const resolveFieldTreePath = (fieldTree, path) => {
|
|
5170
|
+
let current = fieldTree;
|
|
5171
|
+
for (const segment of parsePropertyPath(path)) {
|
|
5172
|
+
if (current === null || (typeof current !== 'object' && typeof current !== 'function')) {
|
|
5173
|
+
return null;
|
|
5174
|
+
}
|
|
5175
|
+
current = current[segment];
|
|
5176
|
+
}
|
|
5177
|
+
// A field tree node is callable (calling it returns the field state) — anything else means the
|
|
5178
|
+
// path left the tree (e.g. an index past the array's current length).
|
|
5179
|
+
return typeof current === 'function' ? current : null;
|
|
5180
|
+
};
|
|
5181
|
+
|
|
5182
|
+
// Shared core for the query-backed async validators (`validateWithQuery` /
|
|
5183
|
+
// `validateWithV2Query`). Everything except how the query is executed — the `factory` that turns
|
|
5184
|
+
// the debounced params into a `Resource` — is identical between the v2 and v3 variants, so it is
|
|
5185
|
+
// factored here and imported by both. This module is intentionally NOT part of the `http` barrel;
|
|
5186
|
+
// it is internal plumbing consumed by direct path (the legacy variant already imports from `http`).
|
|
5187
|
+
/** Default debounce (ms) applied before a validate request runs, mirroring `selectOptionsFrom*Query`. */
|
|
5188
|
+
const DEFAULT_QUERY_VALIDATION_DEBOUNCE = 300;
|
|
5189
|
+
/**
|
|
5190
|
+
* Turns a failed validate request into field errors — the default `onError` mapping shared by both
|
|
5191
|
+
* validators. Reuses the existing `mapViolationsToFormErrors` bridge: a `422` violation list is
|
|
5192
|
+
* resolved onto the child fields by `propertyPath`, while a network / non-violation error degrades
|
|
5193
|
+
* to a non-swallowed form-level error. A caller-supplied `mapViolations` overrides only the
|
|
5194
|
+
* violation → error step (it receives the already-extracted violations).
|
|
5195
|
+
*/
|
|
5196
|
+
const mapQueryValidationError = (error, ctx, mapViolations) => {
|
|
5197
|
+
// Angular's `resource()` wraps a thrown non-`Error` (our `QueryErrorResponse` / `HttpErrorResponse`)
|
|
5198
|
+
// into an `Error` and stashes the original on `.cause`, so unwrap it before extraction.
|
|
5199
|
+
const cause = error instanceof Error && error.cause !== undefined ? error.cause : error;
|
|
5200
|
+
if (mapViolations) {
|
|
5201
|
+
return mapViolations(extractFormViolations(cause), ctx);
|
|
5202
|
+
}
|
|
5203
|
+
// `ctx.fieldTree` is the (read-only) tree of the validated field; `propertyPath`s resolve against
|
|
5204
|
+
// it. The cast bridges `ReadonlyFieldTree` → `FieldTree` — `mapViolationsToFormErrors` only walks
|
|
5205
|
+
// and reads the tree, never mutates it.
|
|
5206
|
+
return mapViolationsToFormErrors({ fieldTree: ctx.fieldTree, error: cause });
|
|
5207
|
+
};
|
|
5208
|
+
/**
|
|
5209
|
+
* Binds a query-backed async validator to `path` via `validateAsync`. Success (`204` / any `2xx`
|
|
5210
|
+
* with no violation body) reports no errors; a failed request is mapped by
|
|
5211
|
+
* {@link mapQueryValidationError}. Async validation only runs once the field's synchronous
|
|
5212
|
+
* validators pass and while `when` holds — signal forms feeds the `factory` `undefined` params when
|
|
5213
|
+
* the request shouldn't run, so the resource stays idle.
|
|
5214
|
+
*/
|
|
5215
|
+
const applyQueryAsyncValidator = (path, config) => {
|
|
5216
|
+
validateAsync(path, {
|
|
5217
|
+
params: config.params,
|
|
5218
|
+
debounce: config.debounce ?? DEFAULT_QUERY_VALIDATION_DEBOUNCE,
|
|
5219
|
+
factory: config.factory,
|
|
5220
|
+
when: config.when,
|
|
5221
|
+
onSuccess: () => null,
|
|
5222
|
+
onError: (error, ctx) => mapQueryValidationError(error, ctx, config.mapViolations),
|
|
5223
|
+
});
|
|
5224
|
+
};
|
|
5225
|
+
|
|
5226
|
+
/**
|
|
5227
|
+
* Binds a query-backed async validator to a signal-forms field, adapting a **legacy v2**
|
|
5228
|
+
* `@ethlete/query` query into what `validateAsync` consumes — the `V2QueryClient` counterpart of
|
|
5229
|
+
* {@link validateWithQuery}, so apps that haven't migrated yet can still validate against the
|
|
5230
|
+
* server. The query runs through the client (bearer auth, base route, caching, Symfony error
|
|
5231
|
+
* normalization), where a raw `httpResource` would fire unauthenticated and bypass the pipeline.
|
|
5232
|
+
*
|
|
5233
|
+
* The query is prepared and executed (debounced) as the field value changes, only after the
|
|
5234
|
+
* field's synchronous validators pass. A `204` / successful response reports no errors; a
|
|
5235
|
+
* `422 FormViolationListView` maps each violation onto its child field by `propertyPath`; a
|
|
5236
|
+
* network / other error degrades to a non-swallowed form-level error.
|
|
5237
|
+
*
|
|
5238
|
+
* Call it from a schema definition, the same place you'd call `validateAsync`:
|
|
5239
|
+
*
|
|
5240
|
+
* ```ts
|
|
5241
|
+
* const schema = schema<OpportunityEmail>((p) => {
|
|
5242
|
+
* validateWithV2Query(p, {
|
|
5243
|
+
* queryCreator: postOpportunityEmailValidate, // hubApiClient.post({ ... })
|
|
5244
|
+
* args: (ctx) => ({ pathParams: { opportunityId }, body: { ...ctx.value() } }),
|
|
5245
|
+
* });
|
|
5246
|
+
* });
|
|
5247
|
+
* ```
|
|
5248
|
+
*/
|
|
5249
|
+
const validateWithV2Query = (path, config) => {
|
|
5250
|
+
applyQueryAsyncValidator(path, {
|
|
5251
|
+
params: config.args,
|
|
5252
|
+
debounce: config.debounce,
|
|
5253
|
+
when: config.when,
|
|
5254
|
+
mapViolations: config.mapViolations,
|
|
5255
|
+
factory: (params) => resource({
|
|
5256
|
+
params: () => params(),
|
|
5257
|
+
loader: ({ params: prepareArgs, abortSignal }) => {
|
|
5258
|
+
// Prepared per request — v2 caches by the prepared args, so a fresh `prepare()` is the
|
|
5259
|
+
// legacy idiom (the sibling `selectOptionsFromV2Query` does the same via `queryComputed`).
|
|
5260
|
+
const query = config.queryCreator.prepare(prepareArgs);
|
|
5261
|
+
abortSignal.addEventListener('abort', () => query.abort());
|
|
5262
|
+
query.execute();
|
|
5263
|
+
// Resolve once the query settles. A failure is thrown so it lands in `onError`, where the
|
|
5264
|
+
// violations are mapped onto the fields; the underlying `HttpErrorResponse` is thrown so
|
|
5265
|
+
// `extractFormViolations` can unwrap the 422 body.
|
|
5266
|
+
return firstValueFrom(query.state$.pipe(filter((state) => isQueryStateSuccess(state) || isQueryStateFailure(state)), take(1))).then((state) => {
|
|
5267
|
+
if (isQueryStateFailure(state)) {
|
|
5268
|
+
throw state.error.httpErrorResponse;
|
|
5269
|
+
}
|
|
5270
|
+
return state.response;
|
|
5271
|
+
});
|
|
5272
|
+
},
|
|
5273
|
+
}),
|
|
5274
|
+
});
|
|
5275
|
+
};
|
|
5276
|
+
|
|
5066
5277
|
const generateUuid = () => randomId();
|
|
5067
5278
|
const createQueryRepository = (config) => {
|
|
5068
5279
|
const cache = new Map();
|
|
@@ -5261,121 +5472,6 @@ const createPatchQuery = createQueryTemplate('PATCH');
|
|
|
5261
5472
|
/** A query creator that creates a secure PATCH query (requires authentication) */
|
|
5262
5473
|
const createSecurePatchQuery = createSecureQueryTemplate('PATCH');
|
|
5263
5474
|
|
|
5264
|
-
/** `kind` of a `ValidationError` created from a single server violation. */
|
|
5265
|
-
const SERVER_VIOLATION_ERROR_KIND = 'etServerViolation';
|
|
5266
|
-
/** `kind` of the form-level fallback error created when a failed request carries no violations. */
|
|
5267
|
-
const SERVER_ERROR_KIND = 'etServerError';
|
|
5268
|
-
/**
|
|
5269
|
-
* Extracts the violation list from a failed request in any of the shapes it may reach you:
|
|
5270
|
-
* a `QueryErrorResponse` (from `query.error()` or a settled snapshot), a raw `HttpErrorResponse`,
|
|
5271
|
-
* an already-unwrapped error body, or a plain violation array. Returns `[]` when the error
|
|
5272
|
-
* carries no violations.
|
|
5273
|
-
*/
|
|
5274
|
-
const extractFormViolations = (error) => {
|
|
5275
|
-
if (!error)
|
|
5276
|
-
return [];
|
|
5277
|
-
const body = isQueryErrorResponse(error) ? error.raw.error : error instanceof HttpErrorResponse ? error.error : error;
|
|
5278
|
-
if (isSymfonyFormViolationListError(body)) {
|
|
5279
|
-
return body.violations;
|
|
5280
|
-
}
|
|
5281
|
-
if (isSymfonyListError(body)) {
|
|
5282
|
-
return body;
|
|
5283
|
-
}
|
|
5284
|
-
return [];
|
|
5285
|
-
};
|
|
5286
|
-
/**
|
|
5287
|
-
* Maps a failed request's violations onto a signal-forms field tree as validation errors —
|
|
5288
|
-
* the bridge between an API's violation list (`@ethlete/types` `FormViolationView`) and
|
|
5289
|
-
* signal forms.
|
|
5290
|
-
*
|
|
5291
|
-
* Each violation's `propertyPath` (e.g. `items[2].name`) is resolved against `fieldTree`; a
|
|
5292
|
-
* resolved violation becomes a `ServerViolationValidationError` bound to that field, an
|
|
5293
|
-
* unresolved one becomes a form-level error (customizable via `onUnmappedViolation`). A failed
|
|
5294
|
-
* request that carries no violations at all degrades to form-level `ServerValidationError`s
|
|
5295
|
-
* built from the normalized error message — so returning this function's result from a
|
|
5296
|
-
* `submit()` action never silently succeeds on failure.
|
|
5297
|
-
*
|
|
5298
|
-
* ```ts
|
|
5299
|
-
* await submit(this.form, async (field) => {
|
|
5300
|
-
* const snapshot = await executeUntilSettled(this.createPost, { args: { body: field().value() } });
|
|
5301
|
-
* const error = snapshot.error();
|
|
5302
|
-
*
|
|
5303
|
-
* if (!error) return;
|
|
5304
|
-
*
|
|
5305
|
-
* return mapViolationsToFormErrors({ fieldTree: field, error });
|
|
5306
|
-
* });
|
|
5307
|
-
* ```
|
|
5308
|
-
*/
|
|
5309
|
-
const mapViolationsToFormErrors = (options) => {
|
|
5310
|
-
const { fieldTree, error, rewritePath, onUnmappedViolation } = options;
|
|
5311
|
-
if (error === null || error === undefined)
|
|
5312
|
-
return [];
|
|
5313
|
-
const violations = extractFormViolations(error);
|
|
5314
|
-
if (!violations.length) {
|
|
5315
|
-
// An empty violation array is an explicit "nothing to map" — only unrecognized error
|
|
5316
|
-
// shapes degrade to the form-level fallback.
|
|
5317
|
-
if (Array.isArray(error) && !error.length)
|
|
5318
|
-
return [];
|
|
5319
|
-
return createFallbackErrors(error);
|
|
5320
|
-
}
|
|
5321
|
-
const errors = [];
|
|
5322
|
-
for (const violation of violations) {
|
|
5323
|
-
const rawPath = violation.propertyPath;
|
|
5324
|
-
const path = rawPath && rewritePath ? rewritePath(rawPath, violation) : rawPath;
|
|
5325
|
-
const resolvedFieldTree = path ? resolveFieldTreePath(fieldTree, path) : null;
|
|
5326
|
-
if (resolvedFieldTree) {
|
|
5327
|
-
const mappedError = {
|
|
5328
|
-
kind: SERVER_VIOLATION_ERROR_KIND,
|
|
5329
|
-
message: violation.message,
|
|
5330
|
-
fieldTree: resolvedFieldTree,
|
|
5331
|
-
violation,
|
|
5332
|
-
};
|
|
5333
|
-
errors.push(mappedError);
|
|
5334
|
-
}
|
|
5335
|
-
else if (onUnmappedViolation) {
|
|
5336
|
-
const customError = onUnmappedViolation(violation);
|
|
5337
|
-
if (customError) {
|
|
5338
|
-
errors.push(customError);
|
|
5339
|
-
}
|
|
5340
|
-
}
|
|
5341
|
-
else {
|
|
5342
|
-
const unmappedError = {
|
|
5343
|
-
kind: SERVER_VIOLATION_ERROR_KIND,
|
|
5344
|
-
message: violation.message,
|
|
5345
|
-
violation,
|
|
5346
|
-
};
|
|
5347
|
-
errors.push(unmappedError);
|
|
5348
|
-
}
|
|
5349
|
-
}
|
|
5350
|
-
return errors;
|
|
5351
|
-
};
|
|
5352
|
-
const createFallbackErrors = (error) => {
|
|
5353
|
-
const queryError = isQueryErrorResponse(error) ? error : createQueryErrorResponse(error);
|
|
5354
|
-
if (queryError.isList) {
|
|
5355
|
-
return queryError.errors.map((item) => ({ kind: SERVER_ERROR_KIND, message: item.message }));
|
|
5356
|
-
}
|
|
5357
|
-
return [{ kind: SERVER_ERROR_KIND, message: queryError.error.message }];
|
|
5358
|
-
};
|
|
5359
|
-
/** Splits a property path like `items[2].name` or `translations['de'].title` into its segments. */
|
|
5360
|
-
const parsePropertyPath = (path) => {
|
|
5361
|
-
return path
|
|
5362
|
-
.replace(/\[(['"]?)([^\]]*?)\1\]/g, '.$2')
|
|
5363
|
-
.split('.')
|
|
5364
|
-
.filter((segment) => segment.length > 0);
|
|
5365
|
-
};
|
|
5366
|
-
const resolveFieldTreePath = (fieldTree, path) => {
|
|
5367
|
-
let current = fieldTree;
|
|
5368
|
-
for (const segment of parsePropertyPath(path)) {
|
|
5369
|
-
if (current === null || (typeof current !== 'object' && typeof current !== 'function')) {
|
|
5370
|
-
return null;
|
|
5371
|
-
}
|
|
5372
|
-
current = current[segment];
|
|
5373
|
-
}
|
|
5374
|
-
// A field tree node is callable (calling it returns the field state) — anything else means the
|
|
5375
|
-
// path left the tree (e.g. an index past the array's current length).
|
|
5376
|
-
return typeof current === 'function' ? current : null;
|
|
5377
|
-
};
|
|
5378
|
-
|
|
5379
5475
|
/**
|
|
5380
5476
|
* Executes the query and resolves once that execution has settled (response or error received).
|
|
5381
5477
|
* The returned snapshot is frozen to the awaited execution, so a later execution can't swap the
|
|
@@ -5394,6 +5490,56 @@ const executeUntilSettled = async (query, executeArgs) => {
|
|
|
5394
5490
|
return snapshot;
|
|
5395
5491
|
};
|
|
5396
5492
|
|
|
5493
|
+
/**
|
|
5494
|
+
* Binds a query-backed async validator to a signal-forms field, adapting an `@ethlete/query` v3
|
|
5495
|
+
* query into what `validateAsync` consumes. The query runs through the query client — so bearer
|
|
5496
|
+
* auth, base route, caching and Symfony error normalization all apply — where a raw `httpResource`
|
|
5497
|
+
* would fire unauthenticated and bypass the pipeline.
|
|
5498
|
+
*
|
|
5499
|
+
* On the current v3 `QueryClient` / `createPostQuery` creators; the `validateWithV2Query` twin
|
|
5500
|
+
* covers the legacy `V2QueryClient`. The query is created once and re-executed (debounced) as the
|
|
5501
|
+
* field value changes, only after the field's synchronous validators pass. A `204` / successful
|
|
5502
|
+
* response reports no errors; a `422 FormViolationListView` maps each violation onto its child
|
|
5503
|
+
* field by `propertyPath`; a network / other error degrades to a non-swallowed form-level error.
|
|
5504
|
+
*
|
|
5505
|
+
* Call it from a schema definition, the same place you'd call `validateAsync`:
|
|
5506
|
+
*
|
|
5507
|
+
* ```ts
|
|
5508
|
+
* const schema = schema<OpportunityEmail>((p) => {
|
|
5509
|
+
* validateWithQuery(p, {
|
|
5510
|
+
* queryCreator: postOpportunityEmailValidate,
|
|
5511
|
+
* args: (ctx) => ({ pathParams: { opportunityId }, body: { ...ctx.value() } }),
|
|
5512
|
+
* });
|
|
5513
|
+
* });
|
|
5514
|
+
* ```
|
|
5515
|
+
*/
|
|
5516
|
+
const validateWithQuery = (path, config) => {
|
|
5517
|
+
applyQueryAsyncValidator(path, {
|
|
5518
|
+
params: config.args,
|
|
5519
|
+
debounce: config.debounce,
|
|
5520
|
+
when: config.when,
|
|
5521
|
+
mapViolations: config.mapViolations,
|
|
5522
|
+
factory: (params) => {
|
|
5523
|
+
// Created once, exactly like a query stack. A POST validate route doesn't auto-execute, so
|
|
5524
|
+
// the resource loader drives it — each params change re-executes with the new args.
|
|
5525
|
+
const query = config.queryCreator();
|
|
5526
|
+
return resource({
|
|
5527
|
+
params: () => params(),
|
|
5528
|
+
loader: async ({ params: requestArgs }) => {
|
|
5529
|
+
const snapshot = await executeUntilSettled(query, { args: requestArgs });
|
|
5530
|
+
const error = snapshot.error();
|
|
5531
|
+
// A failed request (e.g. 422) is thrown so it lands in `onError`, where the violations are
|
|
5532
|
+
// mapped onto the fields. A `204` / success settles with no error and no violations.
|
|
5533
|
+
if (error) {
|
|
5534
|
+
throw error;
|
|
5535
|
+
}
|
|
5536
|
+
return snapshot.response() ?? undefined;
|
|
5537
|
+
},
|
|
5538
|
+
});
|
|
5539
|
+
},
|
|
5540
|
+
});
|
|
5541
|
+
};
|
|
5542
|
+
|
|
5397
5543
|
const setupLeaderElection = () => {
|
|
5398
5544
|
const destroyRef = inject(DestroyRef);
|
|
5399
5545
|
const channelName = 'ethlete-auth-leader';
|
|
@@ -7657,5 +7803,5 @@ const createWebSocketClient = (options) => {
|
|
|
7657
7803
|
* Generated bundle index. Do not edit.
|
|
7658
7804
|
*/
|
|
7659
7805
|
|
|
7660
|
-
export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DateArrayQueryField, DateQueryField, EntityStore, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QueryDevtoolsComponent, QueryDirective, QueryError, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, buildErrorMessage, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, contentfulGqlLikePaginationAdapter, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createInfinityQueryConfig, createLegacyQueryCreator, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryErrorResponse, createQueryFeature, createQueryObject, createQueryRepository, createQuerySnapshotFn, createQueryStack, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, decryptBearer, deepFreeze, def, detectContentTypeHeader, dynLikePaginationAdapter, effectComputed, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isEmptyString, isGqlQueryConfig, isLegacyQuery, isNaN$1 as isNaN, isQuery, isQueryCollection, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, mapToPaginated, mapViolationsToFormErrors, maybeExecute, mergeHeaders, nestedEffect, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideQueryClientForDevtools, queryArrayComputed, queryComputed, queryComputedTillTruthy, queryExecute, queryFeatureUsedMultipleTimes, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, removeFrom, request, resetExecuteState, resetPageOnError, serializeBody, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, splitQueryConfig, switchQueryCollectionState, switchQueryState, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withErrorHandling, withInactivityLogout, withLogging, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withRefreshQuery, withResponseUpdate, withSuccessHandling, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal };
|
|
7806
|
+
export { BasicAuthProvider, BearerAuthFeatureType, BooleanArrayQueryField, CLEAR_QUERY_ARGS, CustomHeaderAuthProvider, DateArrayQueryField, DateQueryField, EntityStore, INFINITY_QUERY_TOKEN, InfinityQuery, InfinityQueryDirective, InfinityQueryTriggerDirective, LegacyQuery, LegacyQueryCreator, NumberArrayQueryField, ParseHttpErrorCodeToMessageDePipe, ParseHttpErrorCodeToMessageEnPipe, ParseHttpErrorCodeToTitleDePipe, ParseHttpErrorCodeToTitleEnPipe, QUERY_CLIENT_DEVTOOLS_TOKEN, QueryDevtoolsComponent, QueryDirective, QueryError, QueryFeatureType, QueryField, QueryForm, QueryRuntimeErrorCode, QueryShortNamePipe, QueryStore, SERVER_ERROR_KIND, SERVER_VIOLATION_ERROR_KIND, SPEED_BUFFER_TIME_IN_MS, SearchQueryField, SortQueryField, StringArrayQueryField, V2BearerAuthProvider, V2Query, V2QueryClient, V2QueryCreator, addQueryContainerHandling, applyQueryFeatures, authExtractTokensResponseMissingAccessToken, authExtractTokensResponseMissingRefreshToken, authExtractTokensResponseNotObject, authProviderFeatureUsedMultipleTimes, buildErrorMessage, buildQueryCacheKey, buildQueryString, buildRoute, buildTimestampFromSeconds, castQueryCreatorTypes, circularQueryDependency, circularQueryDependencyChecker, computeQueryAuthHeader, computeQueryBody, computeQueryHeaders, computeQueryMethod, computeQueryQueryParams, contentfulGqlLikePaginationAdapter, createBaseQuery, createBaseQueryCreator, createBearerAuthProvider, createDeleteQuery, createExecuteFn, createGetQuery, createGqlExecuteFn, createGqlMutationViaGet, createGqlMutationViaPost, createGqlQuery, createGqlQueryCreator, createGqlQueryViaGet, createGqlQueryViaPost, createHttpRequest, createInfinityQueryConfig, createLegacyQueryCreator, createPagedQueryStack, createPatchQuery, createPersistentAuthFeature, createPostQuery, createPutQuery, createQuery, createQueryClient, createQueryCollection, createQueryCollectionSignal, createQueryCollectionSubject, createQueryContext, createQueryCreator, createQueryErrorResponse, createQueryFeature, createQueryObject, createQueryRepository, createQuerySnapshotFn, createQueryStack, createSecureDeleteQuery, createSecureExecuteFactory, createSecureExecuteFn, createSecureGetQuery, createSecureGqlExecuteFn, createSecureGqlMutationViaGet, createSecureGqlMutationViaPost, createSecureGqlQuery, createSecureGqlQueryCreator, createSecureGqlQueryViaGet, createSecureGqlQueryViaPost, createSecurePatchQuery, createSecurePostQuery, createSecurePutQuery, createSecureQuery, createSecureQueryCreator, createTrackingFeature, createWebSocketClient, decryptBearer, deepFreeze, def, detectContentTypeHeader, dynLikePaginationAdapter, effectComputed, ethletePaginationAdapter, executeUntilSettled, extractExpiresInSeconds, extractFormViolations, extractQuery, fakePaginationAdapter, filterFailure, filterNull, filterQueryStates, filterSuccess, forEachHeader, getDefaultHeaders, getQueryFeatureUsage, getResponseUrl, ggLikePaginationAdapter, gql, gqlDataPropertyMissingInResponse, hasHeader, ignoreAutoRefresh, injectInfinityQueryResponseDelay, injectQueryContext, insertFrom, invalidBaseRouteError, invalidBodyError, invalidRouteError, invalidStateInsideSecureExecuteFactory, isAutoRefresh, isBasicAuthProvider, isBearerAuthProvider, isClassValidatorError, isCreateGqlQueryOptions, isCustomHeaderAuthProvider, isEmptyString, isGqlQueryConfig, isLegacyQuery, isNaN$1 as isNaN, isQuery, isQueryCollection, isQueryErrorResponse, isQueryStateCancelled, isQueryStateFailure, isQueryStateLoading, isQueryStatePrepared, isQueryStateSuccess, isRequestError, isSymfonyError, isSymfonyFormViolationListError, isSymfonyListError, isSymfonyPagerfantaOutOfRangeError, mapToPaginated, mapViolationsToFormErrors, maybeExecute, mergeHeaders, nestedEffect, pagedQueryStackNextPageCalledWithoutPreviousPage, pagedQueryStackPageBiggerThanTotalPages, pagedQueryStackPreviousPageCalledButAlreadyAtFirstPage, paginatedEntityValueUpdater, parseAllXhrResponseHeaders, parseHttpErrorCodeToMessageDe, parseHttpErrorCodeToMessageEn, parseHttpErrorCodeToTitleDe, parseHttpErrorCodeToTitleEn, pathParamsMissingInRouteFunctionError, provideInfinityQueryResponseDelay, provideQueryClientForDevtools, queryArrayComputed, queryComputed, queryComputedTillTruthy, queryExecute, queryFeatureUsedMultipleTimes, queryStackTotalQueriesAndExpectedQueriesMismatch, queryStackWithArgsUsed, queryStackWithResponseUpdateUsed, queryStateAlreadyHasKeyError, queryStateCannotTransformError, queryStateDoesNotContainKeyError, queryStateErrorSignal, queryStateLoadingSignal, queryStateResponseSignal, queryStateSignal, removeFrom, request, resetExecuteState, resetPageOnError, serializeBody, setupQueryDependencies, setupQueryExecuteState, setupQueryState, shouldAutoExecuteGqlQuery, shouldAutoExecuteQuery, shouldCacheQuery, shouldRetryRequest, silenceMissingWithArgsFeatureErrorUsedButWithArgsPresent, skipPaginationPageParamCalculator, splitQueryConfig, switchQueryCollectionState, switchQueryState, takeUntilResponse, toQuerySignal, toQuerySubject, tokensNotAvailableInsideAuthAndExec, transformArrayResponse, transformExecStateToQueryState, transformGql, transformMethod, transformPaginatedResponse, transformToBoolean, transformToBooleanArray, transformToDate, transformToDateArray, transformToNumber, transformToNumberArray, transformToSort, transformToSortQueryParam, transformToString, transformToStringArray, uncacheableRequestHasAllowCacheParam, uncacheableRequestHasCacheKeyParam, v2BuildQueryCacheKey, v2ExtractExpiresInSeconds, v2ShouldCacheQuery, v2ShouldRetryRequest, validateWithQuery, validateWithV2Query, withArgs, withArgsQueryFeatureMissingButRouteIsFunction, withAuthenticationQuery, withAutoRefresh, withAutoRefreshUsedInManualQuery, withAutoRefreshUsedOnUnsupportedHttpMethod, withErrorHandling, withInactivityLogout, withLogging, withPersistentAuth, withPolling, withPollingUsedOnUnsupportedHttpMethod, withRefreshQuery, withResponseUpdate, withSuccessHandling, withTokenExpirationWarning, withTokenRevocation, withTracking, wrapAsObservableSignal };
|
|
7661
7807
|
//# sourceMappingURL=ethlete-query.mjs.map
|