@classytic/arc-next 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/api.d.ts +59 -75
- package/dist/api.js +25 -6
- package/dist/cache.d.ts +20 -3
- package/dist/cache.js +31 -17
- package/dist/client.d.ts +64 -69
- package/dist/client.js +115 -82
- package/dist/hooks.d.ts +72 -8
- package/dist/hooks.js +54 -8
- package/dist/prefetch.d.ts +23 -0
- package/dist/prefetch.js +20 -0
- package/dist/presets/bulk.d.ts +5 -4
- package/dist/presets/search.d.ts +5 -4
- package/dist/presets/slug.d.ts +2 -2
- package/dist/presets/soft-delete.d.ts +4 -3
- package/dist/presets/tree.d.ts +4 -3
- package/dist/query.d.ts +20 -20
- package/dist/query.js +12 -22
- package/package.json +3 -1
package/dist/client.d.ts
CHANGED
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { ErrorDetail } from "@classytic/repo-core/errors";
|
|
2
|
+
|
|
1
3
|
//#region src/client.d.ts
|
|
2
4
|
interface ToastHandler {
|
|
3
5
|
success: (message: string) => void;
|
|
@@ -12,39 +14,33 @@ type UseRouterHook = () => {
|
|
|
12
14
|
}) => void;
|
|
13
15
|
};
|
|
14
16
|
/**
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
17
|
+
* Canonical error codes arc and repo-core emit on `json.code`. Single
|
|
18
|
+
* top-level slot — arc 2.13's `createError` lifts business codes from
|
|
19
|
+
* `details` to top-level so `repo-core`'s `toErrorContract` round-trips
|
|
20
|
+
* them on the wire. There is no separate `detailsCode` slot; everything
|
|
21
|
+
* lives at `error.code`.
|
|
18
22
|
*
|
|
19
|
-
*
|
|
20
|
-
*
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
*
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
*
|
|
31
|
-
* `
|
|
32
|
-
* still satisfy it.
|
|
33
|
-
*/
|
|
34
|
-
type ArcTopLevelErrorCode = (typeof KNOWN_TOP_LEVEL_CODES)[number] | (string & {});
|
|
35
|
-
/**
|
|
36
|
-
* Nested business-logic codes arc's mixins / preset routes / org guards emit
|
|
37
|
-
* on `json.details.code`. These are distinct from the HTTP-status code above —
|
|
38
|
-
* `error.code` (top-level) vs `error.detailsCode` (nested). When a route
|
|
39
|
-
* returns 403 with `details.code: 'ORG_CONTEXT_REQUIRED'`, hosts read
|
|
40
|
-
* `detailsCode` to disambiguate "missing org context" from "permission denied".
|
|
23
|
+
* Three families compose this list:
|
|
24
|
+
* 1. **repo-core canonical** (`validation_error`, `not_found`, ...) — RFC 7807
|
|
25
|
+
* / Stripe-shaped lowercase + snake_case. Cross-package universals.
|
|
26
|
+
* 2. **arc hierarchical** (`arc.forbidden`, `arc.validation_error`,
|
|
27
|
+
* `arc.org.access_denied`, ...) — what arc's `errorHandlerPlugin`
|
|
28
|
+
* emits for HTTP-status throws + arc-classified errors.
|
|
29
|
+
* 3. **arc business** (`ORG_CONTEXT_REQUIRED`, `ALL_FIELDS_STRIPPED`,
|
|
30
|
+
* `OWNERSHIP_DENIED`, ...) — emitted by mixins / org guards via
|
|
31
|
+
* `createError(status, msg, { code })`. The UPPER_SNAKE form is
|
|
32
|
+
* intentional: these are reason codes, not HTTP-status codes.
|
|
33
|
+
*
|
|
34
|
+
* `(string & {})` keeps the type open so domain packages and custom
|
|
35
|
+
* `errorMappers` codes still satisfy it.
|
|
41
36
|
*/
|
|
42
|
-
declare const
|
|
37
|
+
declare const KNOWN_ARC_ERROR_CODES: readonly ["validation_error", "not_found", "conflict", "unauthorized", "forbidden", "rate_limited", "idempotency_conflict", "precondition_failed", "internal_error", "service_unavailable", "timeout", "arc.bad_request", "arc.unauthorized", "arc.forbidden", "arc.not_found", "arc.conflict", "arc.unprocessable_entity", "arc.rate_limited", "arc.internal_error", "arc.bad_gateway", "arc.service_unavailable", "arc.gateway_timeout", "arc.validation_error", "arc.invalid_id", "arc.org.selection_required", "arc.org.access_denied", "ORG_CONTEXT_REQUIRED", "ORG_ROLE_REQUIRED", "OWNERSHIP_DENIED", "MIXED_UPDATE_SHAPE", "ALL_FIELDS_STRIPPED", "BEFORE_RESTORE_HOOK_ERROR", "duplicate_key"];
|
|
43
38
|
/**
|
|
44
|
-
*
|
|
45
|
-
*
|
|
39
|
+
* Canonical arc error code union. `(string & {})` keeps the type open so
|
|
40
|
+
* domain packages can extend hierarchically (`'order.cart.locked'`,
|
|
41
|
+
* `'payment.gateway.timeout'`) and still satisfy the type.
|
|
46
42
|
*/
|
|
47
|
-
type
|
|
43
|
+
type ArcErrorCode = (typeof KNOWN_ARC_ERROR_CODES)[number] | (string & {});
|
|
48
44
|
interface ArcApiErrorOptions {
|
|
49
45
|
status: number;
|
|
50
46
|
statusText: string;
|
|
@@ -74,39 +70,36 @@ declare class ArcApiError extends Error {
|
|
|
74
70
|
readonly method: HttpMethod;
|
|
75
71
|
constructor(message: string, options: ArcApiErrorOptions);
|
|
76
72
|
/**
|
|
77
|
-
*
|
|
73
|
+
* Canonical error code from arc's wire envelope (`json.code`).
|
|
78
74
|
*
|
|
79
|
-
*
|
|
80
|
-
* `
|
|
81
|
-
*
|
|
75
|
+
* Arc 2.13 + `repo-core` 0.4 emit one canonical {@link ErrorContract}
|
|
76
|
+
* shape — `{ code, message, status, details? }` — with the business
|
|
77
|
+
* code at top-level. Hosts switch on `error.code` directly:
|
|
82
78
|
*
|
|
83
79
|
* @example
|
|
84
|
-
* if (error.code === '
|
|
80
|
+
* if (error.code === 'ORG_CONTEXT_REQUIRED') promptOrgSelector();
|
|
81
|
+
* if (error.code === 'arc.not_found') router.replace('/404');
|
|
82
|
+
* if (error.code === 'duplicate_key') showRetryAsAdmin();
|
|
85
83
|
*/
|
|
86
|
-
get code():
|
|
84
|
+
get code(): ArcErrorCode | null;
|
|
87
85
|
/**
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
*
|
|
91
|
-
*
|
|
92
|
-
*
|
|
93
|
-
* caller's `request.scope.organizationId` is missing).
|
|
94
|
-
*
|
|
95
|
-
* @example
|
|
96
|
-
* if (error.detailsCode === 'ORG_CONTEXT_REQUIRED') {
|
|
97
|
-
* alert('Configure auth before bulk operations: configureAuth({ getOrgId })');
|
|
98
|
-
* }
|
|
86
|
+
* Canonical structured details — populated for validation failures
|
|
87
|
+
* (one entry per offending field) and duplicate-key conflicts (one entry
|
|
88
|
+
* per offending field). Shape matches `repo-core`'s {@link ErrorDetail}:
|
|
89
|
+
* `{ path?, code, message, meta? }`. Returns `null` for non-arc backends
|
|
90
|
+
* or responses without details.
|
|
99
91
|
*/
|
|
100
|
-
get
|
|
92
|
+
get details(): readonly ErrorDetail[] | null;
|
|
101
93
|
/**
|
|
102
94
|
* Extract field-level validation errors as `{ field: message }` map.
|
|
103
95
|
*
|
|
104
|
-
* Reads
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
96
|
+
* Reads the canonical `ErrorContract.details: ErrorDetail[]` shape first
|
|
97
|
+
* (what arc 2.13 + repo-core emit), then falls back to legacy shapes for
|
|
98
|
+
* non-arc backends:
|
|
99
|
+
* 1. `details: [{ path, code, message }]` — canonical (arc / repo-core).
|
|
100
|
+
* 2. `errors: { email: 'invalid' }` — record form (legacy app handlers).
|
|
101
|
+
* 3. `details: { errors: [{ field|instancePath, message }] }` — pre-2.13 AJV.
|
|
102
|
+
* 4. `errors: [...]` at the top level — third-party frameworks.
|
|
110
103
|
*/
|
|
111
104
|
get fieldErrors(): Record<string, string> | null;
|
|
112
105
|
}
|
|
@@ -137,23 +130,24 @@ declare function isArcApiError(error: unknown): error is ArcApiError;
|
|
|
137
130
|
*/
|
|
138
131
|
declare function isAbortError(error: unknown): boolean;
|
|
139
132
|
/**
|
|
140
|
-
* Generic check: is this an `ArcApiError` carrying a specific
|
|
141
|
-
*
|
|
142
|
-
*
|
|
143
|
-
* `
|
|
133
|
+
* Generic check: is this an `ArcApiError` carrying a specific `code`?
|
|
134
|
+
* Single-slot — arc 2.13 + repo-core 0.4 emit one canonical `code` at
|
|
135
|
+
* top-level. Pass either the canonical lowercase form (`'arc.not_found'`,
|
|
136
|
+
* `'validation_error'`) or arc's UPPER_SNAKE business form
|
|
137
|
+
* (`'ORG_CONTEXT_REQUIRED'`).
|
|
144
138
|
*
|
|
145
139
|
* @example
|
|
146
|
-
* if (isArcErrorCode(error, '
|
|
140
|
+
* if (isArcErrorCode(error, 'duplicate_key')) showRetryUI();
|
|
147
141
|
* if (isArcErrorCode(error, 'ORG_CONTEXT_REQUIRED')) promptOrgSelector();
|
|
148
142
|
*/
|
|
149
|
-
declare function isArcErrorCode(error: unknown, code:
|
|
143
|
+
declare function isArcErrorCode(error: unknown, code: ArcErrorCode): error is ArcApiError;
|
|
150
144
|
/**
|
|
151
145
|
* Specific predicate for arc's bulk-preset + orgGuard safety code.
|
|
152
146
|
*
|
|
153
147
|
* Arc's bulk endpoints (`POST/PATCH/DELETE /:resource/bulk`) reject any call
|
|
154
148
|
* where `request.scope.organizationId` is missing — the wire signal is
|
|
155
|
-
* `403 {
|
|
156
|
-
* need to call `configureAuth({ getOrgId })` before retrying.
|
|
149
|
+
* `403 { code: 'ORG_CONTEXT_REQUIRED', message, status: 403 }`. Hosts hitting
|
|
150
|
+
* this need to call `configureAuth({ getOrgId })` before retrying.
|
|
157
151
|
*
|
|
158
152
|
* @example
|
|
159
153
|
* try { await api.bulkCreate({ data: [...] }); }
|
|
@@ -165,15 +159,16 @@ declare function isArcErrorCode(error: unknown, code: ArcTopLevelErrorCode | Arc
|
|
|
165
159
|
*/
|
|
166
160
|
declare function isOrgContextRequiredError(error: unknown): error is ArcApiError;
|
|
167
161
|
/**
|
|
168
|
-
* Specific predicate for
|
|
162
|
+
* Specific predicate for validation failures (Fastify AJV + Mongoose
|
|
169
163
|
* ValidationError). When true, `error.fieldErrors` is populated with the
|
|
170
|
-
* `{ field: message }` map.
|
|
164
|
+
* `{ field: message }` map. Matches arc's `arc.validation_error` and the
|
|
165
|
+
* canonical `validation_error` from repo-core.
|
|
171
166
|
*/
|
|
172
167
|
declare function isValidationError(error: unknown): error is ArcApiError;
|
|
173
168
|
/**
|
|
174
|
-
* Specific predicate for
|
|
175
|
-
*
|
|
176
|
-
*
|
|
169
|
+
* Specific predicate for unique-constraint violations. Arc's errorHandler
|
|
170
|
+
* classifies these uniformly across MongoDB E11000, Postgres 23505,
|
|
171
|
+
* Prisma P2002 → `arc.conflict` (with `details[].code === 'duplicate_key'`).
|
|
177
172
|
*/
|
|
178
173
|
declare function isDuplicateKeyError(error: unknown): error is ArcApiError;
|
|
179
174
|
interface ClientConfig {
|
|
@@ -505,8 +500,8 @@ declare function getClientAuthContext(client?: ArcClient): {
|
|
|
505
500
|
* Handles JSON, binary (PDF, images), CSV, and text responses.
|
|
506
501
|
*
|
|
507
502
|
* @example
|
|
508
|
-
* const
|
|
509
|
-
* const response = await handleApiRequest<
|
|
503
|
+
* const user = await handleApiRequest<User>('GET', '/users/me');
|
|
504
|
+
* const response = await handleApiRequest<PaginatedResult<Product>>('GET', '/products?page=1');
|
|
510
505
|
*/
|
|
511
506
|
declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: string, options?: ApiRequestOptions): Promise<T>;
|
|
512
507
|
/**
|
|
@@ -527,4 +522,4 @@ declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: str
|
|
|
527
522
|
*/
|
|
528
523
|
declare function createQueryString<T extends Record<string, unknown>>(params?: T): string;
|
|
529
524
|
//#endregion
|
|
530
|
-
export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig,
|
|
525
|
+
export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcErrorCode, AuthConfig, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, HttpMethod, KNOWN_ARC_ERROR_CODES, RetryConfig, StreamUrlProtocol, TextResponse, ToastHandler, UseRouterHook, _resetAuthWarnings, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };
|
package/dist/client.js
CHANGED
|
@@ -1,49 +1,59 @@
|
|
|
1
1
|
//#region src/client.ts
|
|
2
2
|
/**
|
|
3
|
-
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
3
|
+
* Canonical error codes arc and repo-core emit on `json.code`. Single
|
|
4
|
+
* top-level slot — arc 2.13's `createError` lifts business codes from
|
|
5
|
+
* `details` to top-level so `repo-core`'s `toErrorContract` round-trips
|
|
6
|
+
* them on the wire. There is no separate `detailsCode` slot; everything
|
|
7
|
+
* lives at `error.code`.
|
|
6
8
|
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
"NOT_FOUND",
|
|
21
|
-
"METHOD_NOT_ALLOWED",
|
|
22
|
-
"CONFLICT",
|
|
23
|
-
"UNPROCESSABLE_ENTITY",
|
|
24
|
-
"RATE_LIMITED",
|
|
25
|
-
"INTERNAL_ERROR",
|
|
26
|
-
"BAD_GATEWAY",
|
|
27
|
-
"SERVICE_UNAVAILABLE",
|
|
28
|
-
"GATEWAY_TIMEOUT",
|
|
29
|
-
"VALIDATION_ERROR",
|
|
30
|
-
"DUPLICATE_KEY",
|
|
31
|
-
"DOMAIN_ERROR"
|
|
32
|
-
];
|
|
33
|
-
/**
|
|
34
|
-
* Nested business-logic codes arc's mixins / preset routes / org guards emit
|
|
35
|
-
* on `json.details.code`. These are distinct from the HTTP-status code above —
|
|
36
|
-
* `error.code` (top-level) vs `error.detailsCode` (nested). When a route
|
|
37
|
-
* returns 403 with `details.code: 'ORG_CONTEXT_REQUIRED'`, hosts read
|
|
38
|
-
* `detailsCode` to disambiguate "missing org context" from "permission denied".
|
|
9
|
+
* Three families compose this list:
|
|
10
|
+
* 1. **repo-core canonical** (`validation_error`, `not_found`, ...) — RFC 7807
|
|
11
|
+
* / Stripe-shaped lowercase + snake_case. Cross-package universals.
|
|
12
|
+
* 2. **arc hierarchical** (`arc.forbidden`, `arc.validation_error`,
|
|
13
|
+
* `arc.org.access_denied`, ...) — what arc's `errorHandlerPlugin`
|
|
14
|
+
* emits for HTTP-status throws + arc-classified errors.
|
|
15
|
+
* 3. **arc business** (`ORG_CONTEXT_REQUIRED`, `ALL_FIELDS_STRIPPED`,
|
|
16
|
+
* `OWNERSHIP_DENIED`, ...) — emitted by mixins / org guards via
|
|
17
|
+
* `createError(status, msg, { code })`. The UPPER_SNAKE form is
|
|
18
|
+
* intentional: these are reason codes, not HTTP-status codes.
|
|
19
|
+
*
|
|
20
|
+
* `(string & {})` keeps the type open so domain packages and custom
|
|
21
|
+
* `errorMappers` codes still satisfy it.
|
|
39
22
|
*/
|
|
40
|
-
const
|
|
23
|
+
const KNOWN_ARC_ERROR_CODES = [
|
|
24
|
+
"validation_error",
|
|
25
|
+
"not_found",
|
|
26
|
+
"conflict",
|
|
27
|
+
"unauthorized",
|
|
28
|
+
"forbidden",
|
|
29
|
+
"rate_limited",
|
|
30
|
+
"idempotency_conflict",
|
|
31
|
+
"precondition_failed",
|
|
32
|
+
"internal_error",
|
|
33
|
+
"service_unavailable",
|
|
34
|
+
"timeout",
|
|
35
|
+
"arc.bad_request",
|
|
36
|
+
"arc.unauthorized",
|
|
37
|
+
"arc.forbidden",
|
|
38
|
+
"arc.not_found",
|
|
39
|
+
"arc.conflict",
|
|
40
|
+
"arc.unprocessable_entity",
|
|
41
|
+
"arc.rate_limited",
|
|
42
|
+
"arc.internal_error",
|
|
43
|
+
"arc.bad_gateway",
|
|
44
|
+
"arc.service_unavailable",
|
|
45
|
+
"arc.gateway_timeout",
|
|
46
|
+
"arc.validation_error",
|
|
47
|
+
"arc.invalid_id",
|
|
48
|
+
"arc.org.selection_required",
|
|
49
|
+
"arc.org.access_denied",
|
|
41
50
|
"ORG_CONTEXT_REQUIRED",
|
|
42
51
|
"ORG_ROLE_REQUIRED",
|
|
43
52
|
"OWNERSHIP_DENIED",
|
|
44
53
|
"MIXED_UPDATE_SHAPE",
|
|
45
54
|
"ALL_FIELDS_STRIPPED",
|
|
46
|
-
"BEFORE_RESTORE_HOOK_ERROR"
|
|
55
|
+
"BEFORE_RESTORE_HOOK_ERROR",
|
|
56
|
+
"duplicate_key"
|
|
47
57
|
];
|
|
48
58
|
/**
|
|
49
59
|
* Rich API error with status code, response payload, and request metadata.
|
|
@@ -75,51 +85,59 @@ var ArcApiError = class extends Error {
|
|
|
75
85
|
this.method = options.method;
|
|
76
86
|
}
|
|
77
87
|
/**
|
|
78
|
-
*
|
|
88
|
+
* Canonical error code from arc's wire envelope (`json.code`).
|
|
79
89
|
*
|
|
80
|
-
*
|
|
81
|
-
* `
|
|
82
|
-
*
|
|
90
|
+
* Arc 2.13 + `repo-core` 0.4 emit one canonical {@link ErrorContract}
|
|
91
|
+
* shape — `{ code, message, status, details? }` — with the business
|
|
92
|
+
* code at top-level. Hosts switch on `error.code` directly:
|
|
83
93
|
*
|
|
84
94
|
* @example
|
|
85
|
-
* if (error.code === '
|
|
95
|
+
* if (error.code === 'ORG_CONTEXT_REQUIRED') promptOrgSelector();
|
|
96
|
+
* if (error.code === 'arc.not_found') router.replace('/404');
|
|
97
|
+
* if (error.code === 'duplicate_key') showRetryAsAdmin();
|
|
86
98
|
*/
|
|
87
99
|
get code() {
|
|
88
100
|
const j = this.json;
|
|
89
101
|
return j && typeof j.code === "string" ? j.code : null;
|
|
90
102
|
}
|
|
91
103
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
*
|
|
95
|
-
*
|
|
96
|
-
*
|
|
97
|
-
* caller's `request.scope.organizationId` is missing).
|
|
98
|
-
*
|
|
99
|
-
* @example
|
|
100
|
-
* if (error.detailsCode === 'ORG_CONTEXT_REQUIRED') {
|
|
101
|
-
* alert('Configure auth before bulk operations: configureAuth({ getOrgId })');
|
|
102
|
-
* }
|
|
104
|
+
* Canonical structured details — populated for validation failures
|
|
105
|
+
* (one entry per offending field) and duplicate-key conflicts (one entry
|
|
106
|
+
* per offending field). Shape matches `repo-core`'s {@link ErrorDetail}:
|
|
107
|
+
* `{ path?, code, message, meta? }`. Returns `null` for non-arc backends
|
|
108
|
+
* or responses without details.
|
|
103
109
|
*/
|
|
104
|
-
get
|
|
105
|
-
const
|
|
106
|
-
return
|
|
110
|
+
get details() {
|
|
111
|
+
const j = this.json;
|
|
112
|
+
return Array.isArray(j?.details) ? j.details : null;
|
|
107
113
|
}
|
|
108
114
|
/**
|
|
109
115
|
* Extract field-level validation errors as `{ field: message }` map.
|
|
110
116
|
*
|
|
111
|
-
* Reads
|
|
112
|
-
*
|
|
113
|
-
*
|
|
114
|
-
*
|
|
115
|
-
*
|
|
116
|
-
*
|
|
117
|
+
* Reads the canonical `ErrorContract.details: ErrorDetail[]` shape first
|
|
118
|
+
* (what arc 2.13 + repo-core emit), then falls back to legacy shapes for
|
|
119
|
+
* non-arc backends:
|
|
120
|
+
* 1. `details: [{ path, code, message }]` — canonical (arc / repo-core).
|
|
121
|
+
* 2. `errors: { email: 'invalid' }` — record form (legacy app handlers).
|
|
122
|
+
* 3. `details: { errors: [{ field|instancePath, message }] }` — pre-2.13 AJV.
|
|
123
|
+
* 4. `errors: [...]` at the top level — third-party frameworks.
|
|
117
124
|
*/
|
|
118
125
|
get fieldErrors() {
|
|
119
126
|
const j = this.json;
|
|
120
127
|
if (!j) return null;
|
|
128
|
+
if (Array.isArray(j.details) && j.details.length > 0) {
|
|
129
|
+
const map = {};
|
|
130
|
+
for (const d of j.details) {
|
|
131
|
+
if (!d || typeof d !== "object") continue;
|
|
132
|
+
const key = typeof d.path === "string" ? d.path : "";
|
|
133
|
+
const msg = typeof d.message === "string" ? d.message : "Invalid value";
|
|
134
|
+
if (key && !(key in map)) map[key] = msg;
|
|
135
|
+
}
|
|
136
|
+
if (Object.keys(map).length > 0) return map;
|
|
137
|
+
}
|
|
121
138
|
if (j.errors && !Array.isArray(j.errors) && typeof j.errors === "object") return j.errors;
|
|
122
|
-
const
|
|
139
|
+
const detailsObj = j.details;
|
|
140
|
+
const errorList = Array.isArray(j.errors) ? j.errors : Array.isArray(detailsObj?.errors) ? detailsObj.errors : null;
|
|
123
141
|
if (!errorList) return null;
|
|
124
142
|
const map = {};
|
|
125
143
|
for (const item of errorList) {
|
|
@@ -169,26 +187,26 @@ function isAbortError(error) {
|
|
|
169
187
|
return false;
|
|
170
188
|
}
|
|
171
189
|
/**
|
|
172
|
-
* Generic check: is this an `ArcApiError` carrying a specific
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
* `
|
|
190
|
+
* Generic check: is this an `ArcApiError` carrying a specific `code`?
|
|
191
|
+
* Single-slot — arc 2.13 + repo-core 0.4 emit one canonical `code` at
|
|
192
|
+
* top-level. Pass either the canonical lowercase form (`'arc.not_found'`,
|
|
193
|
+
* `'validation_error'`) or arc's UPPER_SNAKE business form
|
|
194
|
+
* (`'ORG_CONTEXT_REQUIRED'`).
|
|
176
195
|
*
|
|
177
196
|
* @example
|
|
178
|
-
* if (isArcErrorCode(error, '
|
|
197
|
+
* if (isArcErrorCode(error, 'duplicate_key')) showRetryUI();
|
|
179
198
|
* if (isArcErrorCode(error, 'ORG_CONTEXT_REQUIRED')) promptOrgSelector();
|
|
180
199
|
*/
|
|
181
200
|
function isArcErrorCode(error, code) {
|
|
182
|
-
|
|
183
|
-
return error.code === code || error.detailsCode === code;
|
|
201
|
+
return isArcApiError(error) && error.code === code;
|
|
184
202
|
}
|
|
185
203
|
/**
|
|
186
204
|
* Specific predicate for arc's bulk-preset + orgGuard safety code.
|
|
187
205
|
*
|
|
188
206
|
* Arc's bulk endpoints (`POST/PATCH/DELETE /:resource/bulk`) reject any call
|
|
189
207
|
* where `request.scope.organizationId` is missing — the wire signal is
|
|
190
|
-
* `403 {
|
|
191
|
-
* need to call `configureAuth({ getOrgId })` before retrying.
|
|
208
|
+
* `403 { code: 'ORG_CONTEXT_REQUIRED', message, status: 403 }`. Hosts hitting
|
|
209
|
+
* this need to call `configureAuth({ getOrgId })` before retrying.
|
|
192
210
|
*
|
|
193
211
|
* @example
|
|
194
212
|
* try { await api.bulkCreate({ data: [...] }); }
|
|
@@ -202,20 +220,27 @@ function isOrgContextRequiredError(error) {
|
|
|
202
220
|
return isArcErrorCode(error, "ORG_CONTEXT_REQUIRED");
|
|
203
221
|
}
|
|
204
222
|
/**
|
|
205
|
-
* Specific predicate for
|
|
223
|
+
* Specific predicate for validation failures (Fastify AJV + Mongoose
|
|
206
224
|
* ValidationError). When true, `error.fieldErrors` is populated with the
|
|
207
|
-
* `{ field: message }` map.
|
|
225
|
+
* `{ field: message }` map. Matches arc's `arc.validation_error` and the
|
|
226
|
+
* canonical `validation_error` from repo-core.
|
|
208
227
|
*/
|
|
209
228
|
function isValidationError(error) {
|
|
210
|
-
|
|
229
|
+
if (!isArcApiError(error)) return false;
|
|
230
|
+
return error.code === "arc.validation_error" || error.code === "validation_error";
|
|
211
231
|
}
|
|
212
232
|
/**
|
|
213
|
-
* Specific predicate for
|
|
214
|
-
*
|
|
215
|
-
*
|
|
233
|
+
* Specific predicate for unique-constraint violations. Arc's errorHandler
|
|
234
|
+
* classifies these uniformly across MongoDB E11000, Postgres 23505,
|
|
235
|
+
* Prisma P2002 → `arc.conflict` (with `details[].code === 'duplicate_key'`).
|
|
216
236
|
*/
|
|
217
237
|
function isDuplicateKeyError(error) {
|
|
218
|
-
|
|
238
|
+
if (!isArcApiError(error)) return false;
|
|
239
|
+
if (error.code === "arc.conflict" || error.code === "duplicate_key" || error.code === "conflict") {
|
|
240
|
+
if (error.code === "arc.conflict") return error.details?.some((d) => d.code === "duplicate_key") ?? false;
|
|
241
|
+
return true;
|
|
242
|
+
}
|
|
243
|
+
return false;
|
|
219
244
|
}
|
|
220
245
|
let clientConfig = null;
|
|
221
246
|
/**
|
|
@@ -609,8 +634,8 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
609
634
|
* Handles JSON, binary (PDF, images), CSV, and text responses.
|
|
610
635
|
*
|
|
611
636
|
* @example
|
|
612
|
-
* const
|
|
613
|
-
* const response = await handleApiRequest<
|
|
637
|
+
* const user = await handleApiRequest<User>('GET', '/users/me');
|
|
638
|
+
* const response = await handleApiRequest<PaginatedResult<Product>>('GET', '/products?page=1');
|
|
614
639
|
*/
|
|
615
640
|
async function handleApiRequest(method, endpoint, options = {}) {
|
|
616
641
|
if (!clientConfig) throw new Error("arc-next: Client not configured. Call configureClient({ baseUrl }) before making API requests.");
|
|
@@ -655,10 +680,18 @@ function createQueryString(params = {}) {
|
|
|
655
680
|
if (value.length > 1) searchParams.append(`${key}[in]`, value.join(","));
|
|
656
681
|
else if (value.length === 1) searchParams.append(key, String(value[0]));
|
|
657
682
|
} else if (value === null) searchParams.append(key, "null");
|
|
683
|
+
else if (typeof value === "object") for (const [op, opValue] of Object.entries(value)) {
|
|
684
|
+
if (opValue === void 0 || opValue === "") continue;
|
|
685
|
+
const bracketKey = `${key}[${op}]`;
|
|
686
|
+
if (Array.isArray(opValue)) {
|
|
687
|
+
if (opValue.length > 0) searchParams.append(bracketKey, opValue.join(","));
|
|
688
|
+
} else if (opValue === null) searchParams.append(bracketKey, "null");
|
|
689
|
+
else searchParams.append(bracketKey, String(opValue));
|
|
690
|
+
}
|
|
658
691
|
else searchParams.append(key, String(value));
|
|
659
692
|
});
|
|
660
693
|
return searchParams.toString();
|
|
661
694
|
}
|
|
662
695
|
|
|
663
696
|
//#endregion
|
|
664
|
-
export { ArcApiError,
|
|
697
|
+
export { ArcApiError, KNOWN_ARC_ERROR_CODES, _resetAuthWarnings, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };
|
package/dist/hooks.d.ts
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import { ArcClient, UseRouterHook } from "./client.js";
|
|
2
|
-
import {
|
|
2
|
+
import { AggResult, AggRow, BaseApi } from "./api.js";
|
|
3
3
|
import { CacheUtils, QueryKeys } from "./cache.js";
|
|
4
4
|
import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
|
|
5
|
-
import { DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult } from "./query.js";
|
|
5
|
+
import { DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, RequestPassthrough } from "./query.js";
|
|
6
6
|
import { SoftDeleteMethods } from "./presets/soft-delete.js";
|
|
7
7
|
import { BulkMethods } from "./presets/bulk.js";
|
|
8
8
|
import { SlugLookupMethods } from "./presets/slug.js";
|
|
9
9
|
import { TreeMethods } from "./presets/tree.js";
|
|
10
10
|
import { SearchPresetMethods } from "./presets/search.js";
|
|
11
|
-
import { QueryKey } from "@tanstack/react-query";
|
|
11
|
+
import { QueryKey, UseQueryResult } from "@tanstack/react-query";
|
|
12
12
|
|
|
13
13
|
//#region src/hooks.d.ts
|
|
14
14
|
/**
|
|
@@ -24,8 +24,50 @@ import { QueryKey } from "@tanstack/react-query";
|
|
|
24
24
|
type CrudApi<T = unknown, TCreate = Partial<T>, TUpdate = Partial<T>> = Pick<BaseApi<T, TCreate, TUpdate>, 'getAll' | 'getById' | 'create' | 'update' | 'delete'> & {
|
|
25
25
|
upload?: BaseApi<T, TCreate, TUpdate>['upload'];
|
|
26
26
|
dispatchAction?: BaseApi<T, TCreate, TUpdate>['dispatchAction'];
|
|
27
|
-
invokeRoute?: BaseApi<T, TCreate, TUpdate>['invokeRoute'];
|
|
27
|
+
invokeRoute?: BaseApi<T, TCreate, TUpdate>['invokeRoute']; /** Declared aggregations (arc 2.13+). Always available on BaseApi. */
|
|
28
|
+
aggregate?: BaseApi<T, TCreate, TUpdate>['aggregate'];
|
|
28
29
|
} & Partial<SoftDeleteMethods<T>> & Partial<BulkMethods<T, TCreate, TUpdate>> & Partial<SlugLookupMethods<T>> & Partial<TreeMethods<T>> & Partial<SearchPresetMethods<T>>;
|
|
30
|
+
/** Args + options for `useAggregation`. Mirrors `ListQueryOptions` for DX consistency. */
|
|
31
|
+
interface AggregationQueryOptions<TRow extends AggRow = AggRow, TData = AggResult<TRow>> {
|
|
32
|
+
/** Bypass auth gate (for public dashboards behind `allowPublic` permissions). */
|
|
33
|
+
public?: boolean;
|
|
34
|
+
/** Skip the query (e.g. while a parent filter is being assembled). */
|
|
35
|
+
enabled?: boolean;
|
|
36
|
+
/** Per-call staleTime override. Defaults to factory's `defaults.staleTime`. */
|
|
37
|
+
staleTime?: number;
|
|
38
|
+
/** Per-call gcTime override. */
|
|
39
|
+
gcTime?: number;
|
|
40
|
+
/**
|
|
41
|
+
* Refetch on window focus. Aggregations / dashboards usually want this OFF —
|
|
42
|
+
* long-running compute. Defaults to `false` regardless of factory config.
|
|
43
|
+
*/
|
|
44
|
+
refetchOnWindowFocus?: boolean;
|
|
45
|
+
/** Periodic refetch (ms or `false`). Set for live dashboards. */
|
|
46
|
+
refetchInterval?: number | false;
|
|
47
|
+
/** Continue polling while the tab is in the background. */
|
|
48
|
+
refetchIntervalInBackground?: boolean;
|
|
49
|
+
/**
|
|
50
|
+
* Transform `AggResult<TRow>` before exposing it. Common pattern is to
|
|
51
|
+
* pluck the rows array directly:
|
|
52
|
+
* `select: (r) => r.rows`
|
|
53
|
+
* Or compute a derived value:
|
|
54
|
+
* `select: (r) => r.rows.reduce((acc, x) => acc + x.total, 0)`
|
|
55
|
+
* Runs on each render after structural-sharing dedupe.
|
|
56
|
+
*/
|
|
57
|
+
select?: (data: AggResult<TRow>) => TData;
|
|
58
|
+
/**
|
|
59
|
+
* Pass-through to the underlying fetch call. Use for Next.js ISR — pass
|
|
60
|
+
* `request: { revalidate: 60, tags: ['orders'] }` so the server-side fetch
|
|
61
|
+
* cache participates in `revalidateTag('orders')` invalidations alongside
|
|
62
|
+
* TanStack's client cache.
|
|
63
|
+
*/
|
|
64
|
+
request?: RequestPassthrough;
|
|
65
|
+
/**
|
|
66
|
+
* Show this data while the real query is loading / re-fetching with
|
|
67
|
+
* different filters. Smooths fast filter switches on dashboards.
|
|
68
|
+
*/
|
|
69
|
+
placeholderData?: AggResult<TRow> | ((prev: AggResult<TRow> | undefined) => AggResult<TRow> | undefined);
|
|
70
|
+
}
|
|
29
71
|
interface CrudHooksConfig<T, TCreate = Partial<T>, TUpdate = Partial<T>> {
|
|
30
72
|
api: CrudApi<T, TCreate, TUpdate>;
|
|
31
73
|
entityKey: string;
|
|
@@ -157,7 +199,7 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
157
199
|
invalidateQueries?: QueryKey[]; /** Default action name. Can be overridden per-call via `mutate({ action })`. */
|
|
158
200
|
action?: string;
|
|
159
201
|
messages?: MutationMessages;
|
|
160
|
-
onSuccess?: (data:
|
|
202
|
+
onSuccess?: (data: TResult, variables: {
|
|
161
203
|
id: string;
|
|
162
204
|
action: string;
|
|
163
205
|
data?: TBody;
|
|
@@ -167,12 +209,12 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
167
209
|
action: string;
|
|
168
210
|
data?: TBody;
|
|
169
211
|
}) => void;
|
|
170
|
-
onSettled?: (data:
|
|
212
|
+
onSettled?: (data: TResult | undefined, error: Error | null, variables: {
|
|
171
213
|
id: string;
|
|
172
214
|
action: string;
|
|
173
215
|
data?: TBody;
|
|
174
216
|
}) => void;
|
|
175
|
-
}) => TransitionMutationReturn<
|
|
217
|
+
}) => TransitionMutationReturn<TResult, {
|
|
176
218
|
id: string;
|
|
177
219
|
action?: string;
|
|
178
220
|
data?: TBody;
|
|
@@ -205,6 +247,28 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
205
247
|
input: string | string[];
|
|
206
248
|
body?: Record<string, unknown>;
|
|
207
249
|
}>;
|
|
250
|
+
/**
|
|
251
|
+
* Query against arc's declarative aggregations (arc v2.13+).
|
|
252
|
+
* `GET /:resource/aggregations/:name` — wire shape `{ rows: TRow[] }`.
|
|
253
|
+
*
|
|
254
|
+
* Cached under `KEYS.aggregation(name, filter)` so multiple call sites with
|
|
255
|
+
* the same args share one cache entry. Mutation hooks
|
|
256
|
+
* (`useActions`/`useBulkActions`) auto-invalidate `KEYS.aggregations()` so
|
|
257
|
+
* dashboards refresh after CRUD writes — opt out via the mutation's
|
|
258
|
+
* `invalidateQueries` override if you want stale-on-write.
|
|
259
|
+
*
|
|
260
|
+
* @example
|
|
261
|
+
* const { data } = useAggregation<{ day: string; total: number }>({
|
|
262
|
+
* name: 'salesByDay',
|
|
263
|
+
* filter: { from: '2025-01-01' },
|
|
264
|
+
* refetchOnWindowFocus: false,
|
|
265
|
+
* });
|
|
266
|
+
* // data.rows: Array<{ day: string; total: number }>
|
|
267
|
+
*/
|
|
268
|
+
useAggregation: <TRow extends AggRow = AggRow, TData = AggResult<TRow>>(args: {
|
|
269
|
+
name: string;
|
|
270
|
+
filter?: Record<string, unknown>;
|
|
271
|
+
} & AggregationQueryOptions<TRow, TData>) => UseQueryResult<TData>;
|
|
208
272
|
useUpload: (options?: {
|
|
209
273
|
invalidateQueries?: QueryKey[];
|
|
210
274
|
messages?: MutationMessages;
|
|
@@ -269,4 +333,4 @@ declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>(
|
|
|
269
333
|
client
|
|
270
334
|
}: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
|
|
271
335
|
//#endregion
|
|
272
|
-
export { BulkActions, CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };
|
|
336
|
+
export { AggregationQueryOptions, BulkActions, CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };
|