@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/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
- * Top-level error codes arc's `errorHandlerPlugin` emits on the response root
16
- * (`json.code`). Mirrors `statusCodeToCode()` + the special `VALIDATION_ERROR`
17
- * / `DUPLICATE_KEY` / `INTERNAL_ERROR` branches.
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
- * Open via intersection with `(string & {})` so unknown codes from custom
20
- * `errorMappers` / `errorMap` still satisfy the type.
21
- */
22
- /**
23
- * Top-level HTTP-status error codes arc's `errorHandlerPlugin` emits on
24
- * `json.code`. Exported as a `const` array first so callers get both
25
- * compile-time exhaustiveness AND runtime iteration (e.g. building a
26
- * client-side i18n lookup, or whitelisting which codes get retried).
27
- */
28
- declare const KNOWN_TOP_LEVEL_CODES: readonly ["BAD_REQUEST", "UNAUTHORIZED", "FORBIDDEN", "NOT_FOUND", "METHOD_NOT_ALLOWED", "CONFLICT", "UNPROCESSABLE_ENTITY", "RATE_LIMITED", "INTERNAL_ERROR", "BAD_GATEWAY", "SERVICE_UNAVAILABLE", "GATEWAY_TIMEOUT", "VALIDATION_ERROR", "DUPLICATE_KEY", "DOMAIN_ERROR"];
29
- /**
30
- * Top-level HTTP-status error code union derived from {@link KNOWN_TOP_LEVEL_CODES}.
31
- * `(string & {})` keeps the type open so non-arc backends or custom error mappers
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 KNOWN_DETAILS_CODES: readonly ["ORG_CONTEXT_REQUIRED", "ORG_ROLE_REQUIRED", "OWNERSHIP_DENIED", "MIXED_UPDATE_SHAPE", "ALL_FIELDS_STRIPPED", "BEFORE_RESTORE_HOOK_ERROR"];
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
- * Nested business-logic error code union derived from {@link KNOWN_DETAILS_CODES}.
45
- * Same `(string & {})` escape hatch as {@link ArcTopLevelErrorCode}.
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 ArcDetailsErrorCode = (typeof KNOWN_DETAILS_CODES)[number] | (string & {});
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
- * Top-level error code from arc's response envelope (`json.code`).
73
+ * Canonical error code from arc's wire envelope (`json.code`).
78
74
  *
79
- * One of {@link ArcTopLevelErrorCode} covers HTTP-status codes (`FORBIDDEN`,
80
- * `CONFLICT`...) plus the dedicated `VALIDATION_ERROR` / `DUPLICATE_KEY`
81
- * branches arc emits. Returns `null` if the response carries no envelope.
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 === 'DUPLICATE_KEY') showRetryAsAdmin();
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(): ArcTopLevelErrorCode | null;
84
+ get code(): ArcErrorCode | null;
87
85
  /**
88
- * Nested business-logic code from arc's `json.details.code` slot.
89
- *
90
- * Arc's preset mixins (bulk, softDelete) and org guards emit specific reason
91
- * codes here distinct from the HTTP-status `code` above. The most common
92
- * is `ORG_CONTEXT_REQUIRED` (bulk operations + orgGuard reject when the
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 detailsCode(): ArcDetailsErrorCode | null;
92
+ get details(): readonly ErrorDetail[] | null;
101
93
  /**
102
94
  * Extract field-level validation errors as `{ field: message }` map.
103
95
  *
104
- * Reads three response shapes used by Arc:
105
- * 1. `{ errors: { email: 'invalid' } }` record form (legacy / app-level handlers).
106
- * 2. `{ details: { errors: [{ field, message }] } }` — Fastify AJV + Arc errorHandler emit this.
107
- * 3. `{ details: { errors: [{ instancePath, message, params }] } }` — raw AJV passthrough.
108
- *
109
- * Returns null when none of the shapes match.
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 top-level OR
141
- * nested `details.code`? Matches whichever slot the code lives in — saves
142
- * call sites from having to know whether arc emitted it at the root or under
143
- * `details`.
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, 'DUPLICATE_KEY')) showRetryUI();
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: ArcTopLevelErrorCode | ArcDetailsErrorCode): error is ArcApiError;
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 { details: { code: 'ORG_CONTEXT_REQUIRED' } }`. Hosts hitting this
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 arc's `VALIDATION_ERROR` (Fastify AJV + Mongoose
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 arc's `DUPLICATE_KEY` (unique-constraint violation).
175
- * Arc's errorHandler classifies these uniformly across MongoDB E11000,
176
- * Postgres 23505, and Prisma P2002.
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 { success, data } = await handleApiRequest<ApiResponse<User>>('GET', '/users/me');
509
- * const response = await handleApiRequest<PaginatedResponse<Product>>('GET', '/products?page=1');
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, ArcDetailsErrorCode, ArcTopLevelErrorCode, AuthConfig, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, HttpMethod, KNOWN_DETAILS_CODES, KNOWN_TOP_LEVEL_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 };
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
- * Top-level error codes arc's `errorHandlerPlugin` emits on the response root
4
- * (`json.code`). Mirrors `statusCodeToCode()` + the special `VALIDATION_ERROR`
5
- * / `DUPLICATE_KEY` / `INTERNAL_ERROR` branches.
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
- * Open via intersection with `(string & {})` so unknown codes from custom
8
- * `errorMappers` / `errorMap` still satisfy the type.
9
- */
10
- /**
11
- * Top-level HTTP-status error codes arc's `errorHandlerPlugin` emits on
12
- * `json.code`. Exported as a `const` array first so callers get both
13
- * compile-time exhaustiveness AND runtime iteration (e.g. building a
14
- * client-side i18n lookup, or whitelisting which codes get retried).
15
- */
16
- const KNOWN_TOP_LEVEL_CODES = [
17
- "BAD_REQUEST",
18
- "UNAUTHORIZED",
19
- "FORBIDDEN",
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 KNOWN_DETAILS_CODES = [
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
- * Top-level error code from arc's response envelope (`json.code`).
88
+ * Canonical error code from arc's wire envelope (`json.code`).
79
89
  *
80
- * One of {@link ArcTopLevelErrorCode} covers HTTP-status codes (`FORBIDDEN`,
81
- * `CONFLICT`...) plus the dedicated `VALIDATION_ERROR` / `DUPLICATE_KEY`
82
- * branches arc emits. Returns `null` if the response carries no envelope.
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 === 'DUPLICATE_KEY') showRetryAsAdmin();
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
- * Nested business-logic code from arc's `json.details.code` slot.
93
- *
94
- * Arc's preset mixins (bulk, softDelete) and org guards emit specific reason
95
- * codes here distinct from the HTTP-status `code` above. The most common
96
- * is `ORG_CONTEXT_REQUIRED` (bulk operations + orgGuard reject when the
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 detailsCode() {
105
- const c = this.json?.details?.code;
106
- return typeof c === "string" ? c : null;
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 three response shapes used by Arc:
112
- * 1. `{ errors: { email: 'invalid' } }` record form (legacy / app-level handlers).
113
- * 2. `{ details: { errors: [{ field, message }] } }` — Fastify AJV + Arc errorHandler emit this.
114
- * 3. `{ details: { errors: [{ instancePath, message, params }] } }` — raw AJV passthrough.
115
- *
116
- * Returns null when none of the shapes match.
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 errorList = Array.isArray(j.errors) ? j.errors : Array.isArray(j.details?.errors) ? j.details.errors : null;
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 top-level OR
173
- * nested `details.code`? Matches whichever slot the code lives in — saves
174
- * call sites from having to know whether arc emitted it at the root or under
175
- * `details`.
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, 'DUPLICATE_KEY')) showRetryUI();
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
- if (!isArcApiError(error)) return false;
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 { details: { code: 'ORG_CONTEXT_REQUIRED' } }`. Hosts hitting this
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 arc's `VALIDATION_ERROR` (Fastify AJV + Mongoose
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
- return isArcErrorCode(error, "VALIDATION_ERROR");
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 arc's `DUPLICATE_KEY` (unique-constraint violation).
214
- * Arc's errorHandler classifies these uniformly across MongoDB E11000,
215
- * Postgres 23505, and Prisma P2002.
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
- return isArcErrorCode(error, "DUPLICATE_KEY");
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 { success, data } = await handleApiRequest<ApiResponse<User>>('GET', '/users/me');
613
- * const response = await handleApiRequest<PaginatedResponse<Product>>('GET', '/products?page=1');
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, KNOWN_DETAILS_CODES, KNOWN_TOP_LEVEL_CODES, _resetAuthWarnings, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };
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 { ApiResponse, BaseApi } from "./api.js";
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: ApiResponse<TResult>, variables: {
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: ApiResponse<TResult> | undefined, error: Error | null, variables: {
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<ApiResponse<TResult>, {
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 };