@classytic/arc-next 0.7.1 → 0.9.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/api.d.ts +34 -3
- package/dist/api.js +26 -10
- package/dist/client.d.ts +74 -5
- package/dist/client.js +19 -12
- package/dist/encryption.d.ts +36 -0
- package/dist/encryption.js +75 -0
- package/dist/hooks.d.ts +12 -0
- package/dist/hooks.js +79 -1
- package/dist/prefetch.d.ts +23 -18
- package/dist/prefetch.js +15 -7
- package/dist/presets/bulk.d.ts +2 -2
- package/dist/presets/search.d.ts +2 -2
- package/dist/presets/slug.d.ts +2 -2
- package/dist/presets/soft-delete.d.ts +2 -2
- package/dist/presets/tree.d.ts +2 -2
- package/dist/query.d.ts +15 -1
- package/dist/query.js +44 -2
- package/package.json +21 -5
package/dist/api.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { ArcClient } from "./client.js";
|
|
1
|
+
import { ArcClient, NextFetchOptions } from "./client.js";
|
|
2
2
|
import { AggregatePaginationResult, KeysetPaginationResult, OffsetPaginationResult, PaginatedResult } from "@classytic/repo-core/pagination";
|
|
3
3
|
import { AggResult, AggRow, BulkCreateResult, DeleteManyResult, DeleteResult, UpdateManyResult } from "@classytic/repo-core/repository";
|
|
4
4
|
import { BracketOperator, BracketOperator as BracketOperator$1 } from "@classytic/repo-core/query-parser";
|
|
@@ -50,8 +50,14 @@ interface RequestOptions {
|
|
|
50
50
|
token?: string | null;
|
|
51
51
|
organizationId?: string | null;
|
|
52
52
|
cache?: RequestCache;
|
|
53
|
-
revalidate
|
|
53
|
+
/** Flattened Next `revalidate`. `false` = cache indefinitely. */
|
|
54
|
+
revalidate?: number | false;
|
|
54
55
|
tags?: string[];
|
|
56
|
+
/**
|
|
57
|
+
* Idiomatic Next App Router fetch config — passed to `fetch(url, { next })`.
|
|
58
|
+
* Merged with the flattened `revalidate`/`tags`. Prefer this from Next hosts.
|
|
59
|
+
*/
|
|
60
|
+
next?: NextFetchOptions;
|
|
55
61
|
headerOptions?: Record<string, string>;
|
|
56
62
|
responseType?: 'json' | 'blob' | 'text';
|
|
57
63
|
signal?: AbortSignal;
|
|
@@ -87,6 +93,23 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
87
93
|
constructor(entity: string, config?: BaseApiConfig);
|
|
88
94
|
/** Merge per-instance headers into request options */
|
|
89
95
|
private withHeaders;
|
|
96
|
+
/**
|
|
97
|
+
* Apply the instance's default `cache` to a per-call options object — but
|
|
98
|
+
* ONLY when the caller expressed no caching intent of their own.
|
|
99
|
+
*
|
|
100
|
+
* A per-call `revalidate`/`next` (time-based ISR) or an explicit `cache`
|
|
101
|
+
* takes precedence. The default is `no-store`; forcing it alongside a
|
|
102
|
+
* `revalidate` makes Next.js throw ("cache: 'no-store' and revalidate are
|
|
103
|
+
* contradictory") and otherwise silently pins the route to dynamic
|
|
104
|
+
* rendering — so a consumer can never opt a single read into ISR. Gating the
|
|
105
|
+
* default here lets `getBySlug({ slug, options: { revalidate: 60 } })` become
|
|
106
|
+
* statically cacheable without every other call losing its no-store default.
|
|
107
|
+
*
|
|
108
|
+
* Framework-agnostic: `cache` / `revalidate` / `next` are inert pass-throughs
|
|
109
|
+
* on runtimes (React Native, plain browser fetch) that don't implement the
|
|
110
|
+
* Next.js fetch extensions, so this never assumes a Next host.
|
|
111
|
+
*/
|
|
112
|
+
private withCacheDefault;
|
|
90
113
|
createQueryString(params?: Record<string, unknown>): string;
|
|
91
114
|
prepareParams(params?: QueryParams): Record<string, unknown>;
|
|
92
115
|
getAll({
|
|
@@ -259,8 +282,16 @@ declare class BaseApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, T
|
|
|
259
282
|
}
|
|
260
283
|
declare function createCrudApi<TDoc = Record<string, unknown>, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>>(entity: string, config?: BaseApiConfig): BaseApi<TDoc, TCreate, TUpdate>;
|
|
261
284
|
type ExtractDoc<T> = T extends PaginatedResult<infer D> ? D : never;
|
|
285
|
+
/** Any preset-augmented BaseApi — the constraint preset factories accept. */
|
|
286
|
+
type AnyBaseApi = BaseApi<any, any, any>;
|
|
287
|
+
/** Document type of a (possibly augmented) BaseApi. */
|
|
288
|
+
type DocOf<A> = A extends BaseApi<infer D, any, any> ? D : never;
|
|
289
|
+
/** Create-payload type of a (possibly augmented) BaseApi. */
|
|
290
|
+
type CreateOf<A> = A extends BaseApi<any, infer C, any> ? C : never;
|
|
291
|
+
/** Update-payload type of a (possibly augmented) BaseApi. */
|
|
292
|
+
type UpdateOf<A> = A extends BaseApi<any, any, infer U> ? U : never;
|
|
262
293
|
declare function isOffsetPagination<T>(response: PaginatedResult<T>): response is OffsetPaginationResult<T>;
|
|
263
294
|
declare function isKeysetPagination<T>(response: PaginatedResult<T>): response is KeysetPaginationResult<T>;
|
|
264
295
|
declare function isAggregatePagination<T>(response: PaginatedResult<T>): response is AggregatePaginationResult<T>;
|
|
265
296
|
//#endregion
|
|
266
|
-
export { type AggResult, type AggRow, BaseApi, BaseApiConfig, type BracketOperator, type BulkCreateResult, type DeleteManyResult, type DeleteResult, ExtractDoc, FilterOperator, PopulateOption, QueryParams, RequestOptions, ScopedArgs, SortDirection, SortSpec, type UpdateManyResult, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
|
|
297
|
+
export { type AggResult, type AggRow, AnyBaseApi, BaseApi, BaseApiConfig, type BracketOperator, type BulkCreateResult, CreateOf, type DeleteManyResult, type DeleteResult, DocOf, ExtractDoc, FilterOperator, PopulateOption, QueryParams, RequestOptions, ScopedArgs, SortDirection, SortSpec, type UpdateManyResult, UpdateOf, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
|
package/dist/api.js
CHANGED
|
@@ -33,6 +33,29 @@ var BaseApi = class {
|
|
|
33
33
|
}
|
|
34
34
|
};
|
|
35
35
|
}
|
|
36
|
+
/**
|
|
37
|
+
* Apply the instance's default `cache` to a per-call options object — but
|
|
38
|
+
* ONLY when the caller expressed no caching intent of their own.
|
|
39
|
+
*
|
|
40
|
+
* A per-call `revalidate`/`next` (time-based ISR) or an explicit `cache`
|
|
41
|
+
* takes precedence. The default is `no-store`; forcing it alongside a
|
|
42
|
+
* `revalidate` makes Next.js throw ("cache: 'no-store' and revalidate are
|
|
43
|
+
* contradictory") and otherwise silently pins the route to dynamic
|
|
44
|
+
* rendering — so a consumer can never opt a single read into ISR. Gating the
|
|
45
|
+
* default here lets `getBySlug({ slug, options: { revalidate: 60 } })` become
|
|
46
|
+
* statically cacheable without every other call losing its no-store default.
|
|
47
|
+
*
|
|
48
|
+
* Framework-agnostic: `cache` / `revalidate` / `next` are inert pass-throughs
|
|
49
|
+
* on runtimes (React Native, plain browser fetch) that don't implement the
|
|
50
|
+
* Next.js fetch extensions, so this never assumes a Next host.
|
|
51
|
+
*/
|
|
52
|
+
withCacheDefault(options = {}) {
|
|
53
|
+
if (options.cache !== void 0 || options.revalidate !== void 0 || options.next !== void 0) return options;
|
|
54
|
+
return {
|
|
55
|
+
cache: this.config.cache,
|
|
56
|
+
...options
|
|
57
|
+
};
|
|
58
|
+
}
|
|
36
59
|
createQueryString(params = {}) {
|
|
37
60
|
return createQueryString(params);
|
|
38
61
|
}
|
|
@@ -77,10 +100,7 @@ var BaseApi = class {
|
|
|
77
100
|
};
|
|
78
101
|
const processedParams = this.prepareParams(mergedParams);
|
|
79
102
|
const queryString = this.createQueryString(processedParams);
|
|
80
|
-
const requestOptions = {
|
|
81
|
-
cache: this.config.cache,
|
|
82
|
-
...options
|
|
83
|
-
};
|
|
103
|
+
const requestOptions = { ...this.withCacheDefault(options) };
|
|
84
104
|
if (token) requestOptions.token = token;
|
|
85
105
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
86
106
|
return this.requestFn("GET", `${this.baseUrl}?${queryString}`, this.withHeaders(requestOptions));
|
|
@@ -89,10 +109,7 @@ var BaseApi = class {
|
|
|
89
109
|
if (!id) throw new Error("ID is required");
|
|
90
110
|
const queryString = this.createQueryString(params);
|
|
91
111
|
const url = queryString ? `${this.baseUrl}/${id}?${queryString}` : `${this.baseUrl}/${id}`;
|
|
92
|
-
const requestOptions = {
|
|
93
|
-
cache: this.config.cache,
|
|
94
|
-
...options
|
|
95
|
-
};
|
|
112
|
+
const requestOptions = { ...this.withCacheDefault(options) };
|
|
96
113
|
if (token) requestOptions.token = token;
|
|
97
114
|
if (organizationId) requestOptions.organizationId = organizationId;
|
|
98
115
|
return this.requestFn("GET", url, this.withHeaders(requestOptions));
|
|
@@ -142,8 +159,7 @@ var BaseApi = class {
|
|
|
142
159
|
}
|
|
143
160
|
const requestOptions = {
|
|
144
161
|
body: data,
|
|
145
|
-
|
|
146
|
-
...options
|
|
162
|
+
...this.withCacheDefault(options)
|
|
147
163
|
};
|
|
148
164
|
if (token) requestOptions.token = token;
|
|
149
165
|
if (organizationId) requestOptions.organizationId = organizationId;
|
package/dist/client.d.ts
CHANGED
|
@@ -171,6 +171,39 @@ declare function isValidationError(error: unknown): error is ArcApiError;
|
|
|
171
171
|
* Prisma P2002 → `arc.conflict` (with `details[].code === 'duplicate_key'`).
|
|
172
172
|
*/
|
|
173
173
|
declare function isDuplicateKeyError(error: unknown): error is ArcApiError;
|
|
174
|
+
/**
|
|
175
|
+
* Application-Layer Encryption (ALE) config — the client half of
|
|
176
|
+
* `@classytic/arc/encryption`. Lets the SDK decrypt JWE response bodies and
|
|
177
|
+
* (optionally) encrypt JSON request bodies, transparently to callers and to
|
|
178
|
+
* the repo-core wire contracts (decryption happens BEFORE error / pagination
|
|
179
|
+
* parsing, so the parsed shape is identical to an unencrypted response).
|
|
180
|
+
*
|
|
181
|
+
* `decrypt` / `encrypt` are host-supplied so the core SDK pulls no crypto
|
|
182
|
+
* dependency. Use `createJoseEncryption()` from `@classytic/arc-next/encryption`
|
|
183
|
+
* for a ready-made JWE implementation, or wire your own (KMS proxy, WebCrypto).
|
|
184
|
+
*/
|
|
185
|
+
interface ClientEncryptionConfig {
|
|
186
|
+
/**
|
|
187
|
+
* Decrypt an encrypted response body (JWE compact string) → plaintext JSON
|
|
188
|
+
* string. Required — this is what makes encrypted responses readable.
|
|
189
|
+
*/
|
|
190
|
+
decrypt: (payload: string) => string | Promise<string>;
|
|
191
|
+
/**
|
|
192
|
+
* Encrypt a serialized JSON request body string → JWE compact string.
|
|
193
|
+
* Required only when `encryptRequests` is true.
|
|
194
|
+
*/
|
|
195
|
+
encrypt?: (json: string) => string | Promise<string>;
|
|
196
|
+
/**
|
|
197
|
+
* Encrypt outbound JSON request bodies. Default `false` — responses are
|
|
198
|
+
* still decrypted regardless (the common case: server encrypts responses,
|
|
199
|
+
* client posts plaintext over TLS).
|
|
200
|
+
*/
|
|
201
|
+
encryptRequests?: boolean;
|
|
202
|
+
/** Response media type signalling an encrypted body. Default `'application/jose'`. */
|
|
203
|
+
responseContentType?: string;
|
|
204
|
+
/** Content-Type set on encrypted outbound requests. Default `'application/jose'`. */
|
|
205
|
+
requestContentType?: string;
|
|
206
|
+
}
|
|
174
207
|
interface ClientConfig {
|
|
175
208
|
baseUrl: string;
|
|
176
209
|
internalApiKey?: string;
|
|
@@ -253,6 +286,13 @@ interface ClientConfig {
|
|
|
253
286
|
* });
|
|
254
287
|
*/
|
|
255
288
|
afterResponse?: AfterResponseInterceptor;
|
|
289
|
+
/**
|
|
290
|
+
* Application-Layer Encryption. When set, the SDK decrypts `application/jose`
|
|
291
|
+
* response bodies (and, with `encryptRequests`, encrypts JSON request bodies)
|
|
292
|
+
* — the client counterpart to `@classytic/arc/encryption` on the backend.
|
|
293
|
+
* Transparent to repo-core wire contracts. See {@link ClientEncryptionConfig}.
|
|
294
|
+
*/
|
|
295
|
+
encryption?: ClientEncryptionConfig;
|
|
256
296
|
}
|
|
257
297
|
interface RetryConfig {
|
|
258
298
|
/** Total attempts including the first. `attempts: 3` means 1 try + 2 retries. Default: 0 (off). */
|
|
@@ -562,13 +602,40 @@ interface TextResponse {
|
|
|
562
602
|
data: string;
|
|
563
603
|
response: Response;
|
|
564
604
|
}
|
|
605
|
+
/**
|
|
606
|
+
* Next.js App Router `fetch` cache directives — forwarded verbatim as
|
|
607
|
+
* `fetch(url, { next })`. This is the idiomatic Next shape; a host using the
|
|
608
|
+
* App Router can pass it 1:1 with what they'd hand to `fetch`. The flattened
|
|
609
|
+
* `revalidate` / `tags` options remain for back-compat and are merged into
|
|
610
|
+
* this object before the request (flattened values win / append).
|
|
611
|
+
*
|
|
612
|
+
* @see https://nextjs.org/docs/app/api-reference/functions/fetch
|
|
613
|
+
*/
|
|
614
|
+
interface NextFetchOptions {
|
|
615
|
+
/**
|
|
616
|
+
* Seconds before the cached response is considered stale and revalidated.
|
|
617
|
+
* `false` caches indefinitely (Next's `force-cache` semantics); `0` opts
|
|
618
|
+
* out of caching. Omit to inherit the route's default.
|
|
619
|
+
*/
|
|
620
|
+
revalidate?: number | false;
|
|
621
|
+
/** Cache tags for on-demand `revalidateTag(tag)` invalidation. */
|
|
622
|
+
tags?: string[];
|
|
623
|
+
}
|
|
565
624
|
interface ApiRequestOptions {
|
|
566
625
|
body?: unknown;
|
|
567
626
|
token?: string | null;
|
|
568
627
|
organizationId?: string | null;
|
|
569
|
-
revalidate
|
|
628
|
+
/** Flattened Next `revalidate` (see `next`). `false` = cache indefinitely. */
|
|
629
|
+
revalidate?: number | false;
|
|
570
630
|
headerOptions?: Record<string, string>;
|
|
631
|
+
/** Flattened Next cache `tags` (see `next`). */
|
|
571
632
|
tags?: string[];
|
|
633
|
+
/**
|
|
634
|
+
* Idiomatic Next App Router fetch cache config — passed straight to
|
|
635
|
+
* `fetch(url, { next })`. Prefer this over the flattened `revalidate`/`tags`
|
|
636
|
+
* when a Next host wants full control; both are merged.
|
|
637
|
+
*/
|
|
638
|
+
next?: NextFetchOptions;
|
|
572
639
|
cache?: RequestCache;
|
|
573
640
|
signal?: AbortSignal;
|
|
574
641
|
/** Explicit idempotency key for this request. Sent as `Idempotency-Key` header. */
|
|
@@ -725,10 +792,12 @@ interface ArcFetchOptions extends Omit<RequestInit, 'body' | 'headers'> {
|
|
|
725
792
|
elevated?: boolean;
|
|
726
793
|
/** Per-call `Idempotency-Key` header. */
|
|
727
794
|
idempotencyKey?: string;
|
|
728
|
-
/** Next
|
|
729
|
-
revalidate?: number;
|
|
730
|
-
/** Next
|
|
795
|
+
/** Flattened Next `revalidate` pass-through. `false` = cache indefinitely. */
|
|
796
|
+
revalidate?: number | false;
|
|
797
|
+
/** Flattened Next cache `tags` pass-through. */
|
|
731
798
|
tags?: string[];
|
|
799
|
+
/** Idiomatic Next App Router fetch config — merged with `revalidate`/`tags`. */
|
|
800
|
+
next?: NextFetchOptions;
|
|
732
801
|
/** `cache` pass-through (browser fetch + Next.js). */
|
|
733
802
|
cache?: RequestCache;
|
|
734
803
|
/**
|
|
@@ -795,4 +864,4 @@ declare const arc: {
|
|
|
795
864
|
delete: <T = unknown>(path: string, opts?: ArcFetchOptions) => Promise<T>;
|
|
796
865
|
};
|
|
797
866
|
//#endregion
|
|
798
|
-
export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcErrorCode, ArcFetchOptions, AuthConfig, AuthErrorContext, AuthErrorHandler, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, HttpMethod, KNOWN_ARC_ERROR_CODES, RetryConfig, StreamUrlProtocol, TextResponse, ToastHandler, UseRouterHook, _getAuthErrorHandler, _isAuthRecoverable, _resetArcFetchClient, _resetAuthRecovery, _resetAuthWarnings, _resolveRefreshedToken, _runAuthRecovery, arc, arcAuthHeaders, arcFetch, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createAuthRefreshHandler, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, hasGlobalStaticAuth, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };
|
|
867
|
+
export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcErrorCode, ArcFetchOptions, AuthConfig, AuthErrorContext, AuthErrorHandler, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, ClientEncryptionConfig, HttpMethod, KNOWN_ARC_ERROR_CODES, NextFetchOptions, RetryConfig, StreamUrlProtocol, TextResponse, ToastHandler, UseRouterHook, _getAuthErrorHandler, _isAuthRecoverable, _resetArcFetchClient, _resetAuthRecovery, _resetAuthWarnings, _resolveRefreshedToken, _runAuthRecovery, arc, arcAuthHeaders, arcFetch, buildStreamUrl, configureAuth, configureClient, createAuthAwareClient, createAuthRefreshHandler, createClient, createQueryString, getAuthContext, getAuthMode, getBaseUrl, getClientAuthContext, handleApiRequest, hasGlobalStaticAuth, isAbortError, isArcApiError, isArcErrorCode, isAutoIdempotency, isDuplicateKeyError, isOrgContextRequiredError, isValidationError };
|
package/dist/client.js
CHANGED
|
@@ -738,7 +738,7 @@ function sleepAbortable(ms, signal) {
|
|
|
738
738
|
}
|
|
739
739
|
/** A single fetch attempt — used by executeRequest's retry loop. */
|
|
740
740
|
async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
741
|
-
const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal, idempotencyKey, elevated } = options;
|
|
741
|
+
const { body, token, organizationId, revalidate, headerOptions, tags, next, cache, signal, idempotencyKey, elevated } = options;
|
|
742
742
|
const startTime = Date.now();
|
|
743
743
|
try {
|
|
744
744
|
let headers = {
|
|
@@ -763,6 +763,11 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
763
763
|
const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
|
|
764
764
|
let serializedBody = void 0;
|
|
765
765
|
if (body !== void 0 && body !== null) serializedBody = isNonJsonBody(body) ? body : JSON.stringify(body);
|
|
766
|
+
const encryption = config.encryption;
|
|
767
|
+
if (encryption?.encryptRequests && encryption.encrypt && typeof serializedBody === "string") {
|
|
768
|
+
serializedBody = await encryption.encrypt(serializedBody);
|
|
769
|
+
headers["Content-Type"] = encryption.requestContentType ?? "application/jose";
|
|
770
|
+
}
|
|
766
771
|
if (config.beforeRequest) {
|
|
767
772
|
const ctx = await config.beforeRequest({
|
|
768
773
|
method,
|
|
@@ -783,14 +788,10 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
783
788
|
};
|
|
784
789
|
if (serializedBody !== void 0) fetchOptions.body = serializedBody;
|
|
785
790
|
if (cache) fetchOptions.cache = cache;
|
|
786
|
-
|
|
787
|
-
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
if (tags) fetchOptions.next = {
|
|
791
|
-
...fetchOptions.next,
|
|
792
|
-
tags
|
|
793
|
-
};
|
|
791
|
+
const nextCfg = { ...next };
|
|
792
|
+
if (revalidate !== void 0) nextCfg.revalidate = revalidate;
|
|
793
|
+
if (tags) nextCfg.tags = nextCfg.tags ? [...nextCfg.tags, ...tags] : tags;
|
|
794
|
+
if (nextCfg.revalidate !== void 0 || nextCfg.tags) fetchOptions.next = nextCfg;
|
|
794
795
|
if (!/^https?:\/\//i.test(endpoint) && !config.baseUrl) throw new Error(`[arc-next] handleApiRequest(${method} ${endpoint}): baseUrl is empty. Call configureClient({ baseUrl: '...' }) BEFORE the first request. If you use createAuthAwareClient() at module top-level, make sure the Providers component runs configureClient() first (e.g. in a useState() initializer, before the children render).`);
|
|
795
796
|
const response = await fetch(`${config.baseUrl}${endpoint}`, fetchOptions);
|
|
796
797
|
if (!response.ok) {
|
|
@@ -799,7 +800,7 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
799
800
|
try {
|
|
800
801
|
json = await response.clone().json();
|
|
801
802
|
const j = json;
|
|
802
|
-
errorMessage = typeof j?.error === "string" && j.error || typeof j?.message === "string" && j.message || response.statusText;
|
|
803
|
+
errorMessage = typeof j?.error === "string" && j.error || typeof j?.meta?.message === "string" && j.meta.message || typeof j?.message === "string" && j.message || response.statusText;
|
|
803
804
|
} catch {
|
|
804
805
|
try {
|
|
805
806
|
const text = await response.text();
|
|
@@ -819,7 +820,12 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
819
820
|
}
|
|
820
821
|
const contentType = response.headers.get("Content-Type");
|
|
821
822
|
let data;
|
|
822
|
-
|
|
823
|
+
const decryptCt = encryption ? encryption.responseContentType ?? "application/jose" : void 0;
|
|
824
|
+
if (encryption && decryptCt && contentType?.includes(decryptCt)) {
|
|
825
|
+
const cipher = await response.text();
|
|
826
|
+
const plaintext = cipher.length > 0 ? await encryption.decrypt(cipher) : "";
|
|
827
|
+
data = plaintext.length > 0 ? JSON.parse(plaintext) : void 0;
|
|
828
|
+
} else if (contentType?.includes("application/json")) data = await response.json();
|
|
823
829
|
else if (contentType?.includes("application/pdf") || contentType?.includes("image/")) data = {
|
|
824
830
|
data: await response.blob(),
|
|
825
831
|
response
|
|
@@ -1052,7 +1058,7 @@ function sanitizeUserHeaders(headers) {
|
|
|
1052
1058
|
* const result = await arc.post<{ ok: true }>('/api/statements', statements);
|
|
1053
1059
|
*/
|
|
1054
1060
|
function arcFetch(path, options = {}) {
|
|
1055
|
-
const { method = "GET", body, headers, signal, elevated, idempotencyKey, revalidate, tags, cache, client } = options;
|
|
1061
|
+
const { method = "GET", body, headers, signal, elevated, idempotencyKey, revalidate, tags, next, cache, client } = options;
|
|
1056
1062
|
const transport = client ?? getDefaultArcFetchClient();
|
|
1057
1063
|
const apiOptions = {
|
|
1058
1064
|
body,
|
|
@@ -1062,6 +1068,7 @@ function arcFetch(path, options = {}) {
|
|
|
1062
1068
|
...idempotencyKey ? { idempotencyKey } : {},
|
|
1063
1069
|
...revalidate !== void 0 ? { revalidate } : {},
|
|
1064
1070
|
...tags ? { tags } : {},
|
|
1071
|
+
...next ? { next } : {},
|
|
1065
1072
|
...cache ? { cache } : {}
|
|
1066
1073
|
};
|
|
1067
1074
|
return transport.request(method, path, apiOptions);
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { ClientEncryptionConfig } from "./client.js";
|
|
2
|
+
|
|
3
|
+
//#region src/encryption.d.ts
|
|
4
|
+
/** Key material accepted by jose in browser + Node (Web Crypto). */
|
|
5
|
+
type JoseKey = CryptoKey | Uint8Array;
|
|
6
|
+
interface JoseEncryptionOptions {
|
|
7
|
+
/** Single decryption key (private/secret) used for every response. */
|
|
8
|
+
decryptionKey?: JoseKey;
|
|
9
|
+
/**
|
|
10
|
+
* Decryption keys indexed by `kid` (rotation). When set, the inbound JWE's
|
|
11
|
+
* `kid` header selects the key — keep 2–3 entries across a rotation so
|
|
12
|
+
* in-flight responses signed with the previous `kid` still decrypt.
|
|
13
|
+
*/
|
|
14
|
+
decryptionKeys?: Record<string, JoseKey>;
|
|
15
|
+
/** Public/secret key + `kid` used to encrypt outbound requests. */
|
|
16
|
+
encryptionKey?: {
|
|
17
|
+
kid: string;
|
|
18
|
+
key: JoseKey;
|
|
19
|
+
alg?: string;
|
|
20
|
+
};
|
|
21
|
+
/** Key-management algorithm. Default `'RSA-OAEP-256'`. */
|
|
22
|
+
alg?: string;
|
|
23
|
+
/** Content-encryption algorithm. Default `'A256GCM'`. */
|
|
24
|
+
enc?: string;
|
|
25
|
+
/** Inbound-decryption allowlists (anti-downgrade). Default `[alg]` / `[enc]`. */
|
|
26
|
+
allowedAlgs?: string[];
|
|
27
|
+
allowedEncs?: string[];
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Build the `encrypt` / `decrypt` half of a {@link ClientEncryptionConfig}
|
|
31
|
+
* using `jose`. Spread the result into `encryption` and add `encryptRequests`
|
|
32
|
+
* / content-type overrides as needed.
|
|
33
|
+
*/
|
|
34
|
+
declare function createJoseEncryption(options: JoseEncryptionOptions): Pick<ClientEncryptionConfig, 'encrypt' | 'decrypt'>;
|
|
35
|
+
//#endregion
|
|
36
|
+
export { JoseEncryptionOptions, createJoseEncryption };
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { CompactEncrypt, compactDecrypt } from "jose";
|
|
2
|
+
|
|
3
|
+
//#region src/encryption.ts
|
|
4
|
+
/**
|
|
5
|
+
* `@classytic/arc-next/encryption` — JWE helper for the client SDK.
|
|
6
|
+
*
|
|
7
|
+
* A ready-made `ClientEncryptionConfig` backed by `jose` (panva) — the same
|
|
8
|
+
* library `@classytic/arc/encryption` uses server-side. Works in the browser
|
|
9
|
+
* and Node (jose runs on Web Crypto). `jose` is an OPTIONAL peer: import this
|
|
10
|
+
* subpath only when you want JWE; the core SDK pulls no crypto dependency.
|
|
11
|
+
*
|
|
12
|
+
* Asymmetric key model (mirrors Visa MLE / the arc backend plugin):
|
|
13
|
+
* - Decrypt RESPONSES with the client's PRIVATE key (the recipient).
|
|
14
|
+
* - Encrypt REQUESTS with the SERVER's PUBLIC key, tagged by `kid`.
|
|
15
|
+
*
|
|
16
|
+
* @example
|
|
17
|
+
* ```ts
|
|
18
|
+
* import { configureClient } from "@classytic/arc-next/client";
|
|
19
|
+
* import { createJoseEncryption } from "@classytic/arc-next/encryption";
|
|
20
|
+
* import { importPKCS8, importSPKI } from "jose";
|
|
21
|
+
*
|
|
22
|
+
* configureClient({
|
|
23
|
+
* baseUrl: "https://api.example.com",
|
|
24
|
+
* encryption: {
|
|
25
|
+
* ...createJoseEncryption({
|
|
26
|
+
* decryptionKeys: { "client-1": await importPKCS8(clientPrivPem, "RSA-OAEP-256") },
|
|
27
|
+
* encryptionKey: { kid: "server-1", key: await importSPKI(serverPubPem, "RSA-OAEP-256") },
|
|
28
|
+
* }),
|
|
29
|
+
* encryptRequests: true,
|
|
30
|
+
* },
|
|
31
|
+
* });
|
|
32
|
+
* ```
|
|
33
|
+
*/
|
|
34
|
+
/**
|
|
35
|
+
* Build the `encrypt` / `decrypt` half of a {@link ClientEncryptionConfig}
|
|
36
|
+
* using `jose`. Spread the result into `encryption` and add `encryptRequests`
|
|
37
|
+
* / content-type overrides as needed.
|
|
38
|
+
*/
|
|
39
|
+
function createJoseEncryption(options) {
|
|
40
|
+
const alg = options.alg ?? "RSA-OAEP-256";
|
|
41
|
+
const enc = options.enc ?? "A256GCM";
|
|
42
|
+
const allowedAlgs = options.allowedAlgs ?? [alg];
|
|
43
|
+
const allowedEncs = options.allowedEncs ?? [enc];
|
|
44
|
+
const decoder = new TextDecoder();
|
|
45
|
+
const encoder = new TextEncoder();
|
|
46
|
+
const decrypt = async (payload) => {
|
|
47
|
+
const { plaintext } = await compactDecrypt(payload, async (header) => {
|
|
48
|
+
if (options.decryptionKeys) {
|
|
49
|
+
const key = header.kid ? options.decryptionKeys[header.kid] : void 0;
|
|
50
|
+
if (!key) throw new Error(`[arc-next/encryption] no decryption key for kid '${header.kid ?? "<none>"}'`);
|
|
51
|
+
return key;
|
|
52
|
+
}
|
|
53
|
+
if (!options.decryptionKey) throw new Error("[arc-next/encryption] no decryption key configured");
|
|
54
|
+
return options.decryptionKey;
|
|
55
|
+
}, {
|
|
56
|
+
keyManagementAlgorithms: allowedAlgs,
|
|
57
|
+
contentEncryptionAlgorithms: allowedEncs
|
|
58
|
+
});
|
|
59
|
+
return decoder.decode(plaintext);
|
|
60
|
+
};
|
|
61
|
+
const encryptionKey = options.encryptionKey;
|
|
62
|
+
if (!encryptionKey) return { decrypt };
|
|
63
|
+
const encrypt = async (json) => new CompactEncrypt(encoder.encode(json)).setProtectedHeader({
|
|
64
|
+
alg: encryptionKey.alg ?? alg,
|
|
65
|
+
enc,
|
|
66
|
+
kid: encryptionKey.kid
|
|
67
|
+
}).encrypt(encryptionKey.key);
|
|
68
|
+
return {
|
|
69
|
+
encrypt,
|
|
70
|
+
decrypt
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
//#endregion
|
|
75
|
+
export { createJoseEncryption };
|
package/dist/hooks.d.ts
CHANGED
|
@@ -180,6 +180,18 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
|
|
|
180
180
|
/** New signature — auto-injects token from configureAuth() context */(id: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>; /** Legacy signature — explicit token */
|
|
181
181
|
(id: string | null, token: string | null, options?: DetailQueryOptions<T>): DetailQueryResult<T>;
|
|
182
182
|
};
|
|
183
|
+
/**
|
|
184
|
+
* Suspense variant of `useList` for Suspense / streaming-SSR routes. Always
|
|
185
|
+
* fetches and SUSPENDS until resolved — wrap the subtree in `<Suspense>` and
|
|
186
|
+
* an error boundary. No `enabled` gate and no `keepPreviousData` preview
|
|
187
|
+
* (TanStack forbids both on suspense queries); `isLoading` is always `false`.
|
|
188
|
+
*/
|
|
189
|
+
useSuspenseList: (params?: Record<string, unknown>, options?: Omit<ListQueryOptions<T>, "enabled">) => ListQueryResult<T>;
|
|
190
|
+
/**
|
|
191
|
+
* Suspense variant of `useDetail`. `id` must be present (it always fetches).
|
|
192
|
+
* Suspends until resolved; no list→detail `placeholderData` preview.
|
|
193
|
+
*/
|
|
194
|
+
useSuspenseDetail: (id: string, options?: Omit<DetailQueryOptions<T>, "enabled">) => DetailQueryResult<T>;
|
|
183
195
|
useInfiniteList: {
|
|
184
196
|
/** New signature — auto-injects token/orgId from configureAuth() context */(params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>; /** Legacy signature — explicit token */
|
|
185
197
|
(token: string | null, params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>;
|
package/dist/hooks.js
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
import { getAuthMode, getClientAuthContext, hasGlobalStaticAuth } from "./client.js";
|
|
4
4
|
import { isKeysetPagination, isOffsetPagination } from "./api.js";
|
|
5
5
|
import { DEFAULT_QUERY_CONFIG, createCacheUtils, createQueryKeys, extractItem, getItemId, syncDetailToLists, updateListCache } from "./cache.js";
|
|
6
|
-
import { findItemInListCache, useDetailQuery, useInfiniteListQuery, useListQuery } from "./query.js";
|
|
6
|
+
import { findItemInListCache, useDetailQuery, useInfiniteListQuery, useListQuery, useSuspenseDetailQuery, useSuspenseListQuery } from "./query.js";
|
|
7
7
|
import { useMutationWithTransition, useOptimisticMutation } from "./mutation.js";
|
|
8
8
|
import { subscribeToEvents } from "./sse.js";
|
|
9
9
|
import { connectWs } from "./ws.js";
|
|
@@ -176,6 +176,82 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
176
176
|
]);
|
|
177
177
|
return detailResult;
|
|
178
178
|
}
|
|
179
|
+
function useSuspenseList(params = {}, options = {}) {
|
|
180
|
+
const auth = resolveAuth();
|
|
181
|
+
const token = auth.token;
|
|
182
|
+
let resolvedParams = params;
|
|
183
|
+
if (auth.organizationId && !resolvedParams.organizationId) resolvedParams = {
|
|
184
|
+
...resolvedParams,
|
|
185
|
+
organizationId: auth.organizationId
|
|
186
|
+
};
|
|
187
|
+
const { organizationId, ...restParams } = resolvedParams;
|
|
188
|
+
const scope = options._scope || (organizationId ? "tenant" : "super-admin");
|
|
189
|
+
const { request: requestOpts, ...queryOpts } = options;
|
|
190
|
+
return useSuspenseListQuery({
|
|
191
|
+
queryKey: KEYS.scopedList(scope, {
|
|
192
|
+
organizationId,
|
|
193
|
+
...restParams
|
|
194
|
+
}),
|
|
195
|
+
queryFn: ({ signal }) => api.getAll({
|
|
196
|
+
token,
|
|
197
|
+
organizationId,
|
|
198
|
+
params: restParams,
|
|
199
|
+
options: {
|
|
200
|
+
signal,
|
|
201
|
+
...requestOpts
|
|
202
|
+
}
|
|
203
|
+
}),
|
|
204
|
+
options: {
|
|
205
|
+
staleTime: queryOpts.staleTime ?? config.staleTime,
|
|
206
|
+
gcTime: queryOpts.gcTime ?? config.gcTime,
|
|
207
|
+
refetchOnWindowFocus: queryOpts.refetchOnWindowFocus ?? config.refetchOnWindowFocus,
|
|
208
|
+
structuralSharing: queryOpts.structuralSharing ?? config.structuralSharing,
|
|
209
|
+
refetchInterval: queryOpts.refetchInterval,
|
|
210
|
+
refetchIntervalInBackground: queryOpts.refetchIntervalInBackground
|
|
211
|
+
},
|
|
212
|
+
select: queryOpts.select
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
function useSuspenseDetail(id, options = {}) {
|
|
216
|
+
const auth = resolveAuth();
|
|
217
|
+
const token = auth.token;
|
|
218
|
+
let opts = options;
|
|
219
|
+
if (auth.organizationId && !opts.organizationId) opts = {
|
|
220
|
+
...opts,
|
|
221
|
+
organizationId: auth.organizationId
|
|
222
|
+
};
|
|
223
|
+
const { organizationId, params: queryParams, request: requestOpts, ...restOptions } = opts;
|
|
224
|
+
const detailKey = KEYS.scopedDetail(id, organizationId ?? null);
|
|
225
|
+
const fullDetailKey = queryParams ? [...detailKey, queryParams] : detailKey;
|
|
226
|
+
const queryClient = useQueryClient();
|
|
227
|
+
const detailResult = useSuspenseDetailQuery({
|
|
228
|
+
queryKey: fullDetailKey,
|
|
229
|
+
queryFn: ({ signal }) => api.getById({
|
|
230
|
+
id,
|
|
231
|
+
token,
|
|
232
|
+
organizationId,
|
|
233
|
+
params: queryParams,
|
|
234
|
+
options: {
|
|
235
|
+
signal,
|
|
236
|
+
...requestOpts
|
|
237
|
+
}
|
|
238
|
+
}),
|
|
239
|
+
options: {
|
|
240
|
+
staleTime: restOptions.staleTime ?? config.staleTime,
|
|
241
|
+
gcTime: restOptions.gcTime ?? config.gcTime,
|
|
242
|
+
refetchOnWindowFocus: restOptions.refetchOnWindowFocus ?? config.refetchOnWindowFocus,
|
|
243
|
+
structuralSharing: restOptions.structuralSharing ?? config.structuralSharing,
|
|
244
|
+
refetchInterval: restOptions.refetchInterval,
|
|
245
|
+
refetchIntervalInBackground: restOptions.refetchIntervalInBackground
|
|
246
|
+
},
|
|
247
|
+
select: restOptions.select
|
|
248
|
+
});
|
|
249
|
+
useEffect(() => {
|
|
250
|
+
if (!detailResult.item) return;
|
|
251
|
+
syncDetailToLists(queryClient, KEYS.lists(), detailResult.item, idField ? { idField } : {});
|
|
252
|
+
}, [detailResult.item, queryClient]);
|
|
253
|
+
return detailResult;
|
|
254
|
+
}
|
|
179
255
|
function useActions() {
|
|
180
256
|
const queryClient = useQueryClient();
|
|
181
257
|
const silentRef = useRef(false);
|
|
@@ -956,6 +1032,8 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
956
1032
|
cache,
|
|
957
1033
|
useList,
|
|
958
1034
|
useDetail,
|
|
1035
|
+
useSuspenseList,
|
|
1036
|
+
useSuspenseDetail,
|
|
959
1037
|
useInfiniteList,
|
|
960
1038
|
useActions,
|
|
961
1039
|
useBulkActions,
|
package/dist/prefetch.d.ts
CHANGED
|
@@ -11,7 +11,24 @@ interface PrefetchAuthContext {
|
|
|
11
11
|
}
|
|
12
12
|
interface PrefetchOptions extends PrefetchAuthContext {
|
|
13
13
|
staleTime?: number;
|
|
14
|
+
/**
|
|
15
|
+
* Next.js fetch caching forwarded to the underlying API call. A Server
|
|
16
|
+
* Component that prefetches with `revalidate` lets that data participate in
|
|
17
|
+
* ISR — without it the SDK's default `no-store` pins the whole route to
|
|
18
|
+
* dynamic rendering (e.g. a shared layout prefetch silently disables ISR on
|
|
19
|
+
* every page under it). Inert off-Next (React Native / plain browser fetch).
|
|
20
|
+
*/
|
|
21
|
+
cache?: RequestCache;
|
|
22
|
+
revalidate?: number | false;
|
|
23
|
+
tags?: string[];
|
|
14
24
|
}
|
|
25
|
+
/** Per-call API `options` the prefetcher forwards (caching + custom headers). */
|
|
26
|
+
type ForwardedApiOptions = {
|
|
27
|
+
headerOptions?: Record<string, string>;
|
|
28
|
+
cache?: RequestCache;
|
|
29
|
+
revalidate?: number | false;
|
|
30
|
+
tags?: string[];
|
|
31
|
+
};
|
|
15
32
|
interface PrefetchDetailOptions extends PrefetchOptions {
|
|
16
33
|
/** Query params (select, populate) — key must match useDetail's params to share cache */
|
|
17
34
|
params?: {
|
|
@@ -107,51 +124,39 @@ declare function createCrudPrefetcher(api: {
|
|
|
107
124
|
params?: Record<string, unknown>;
|
|
108
125
|
token?: string | null;
|
|
109
126
|
organizationId?: string | null;
|
|
110
|
-
options?:
|
|
111
|
-
headerOptions?: Record<string, string>;
|
|
112
|
-
};
|
|
127
|
+
options?: ForwardedApiOptions;
|
|
113
128
|
}) => Promise<unknown>;
|
|
114
129
|
getById: (opts: {
|
|
115
130
|
id: string;
|
|
116
131
|
token?: string | null;
|
|
117
132
|
organizationId?: string | null;
|
|
118
|
-
options?:
|
|
119
|
-
headerOptions?: Record<string, string>;
|
|
120
|
-
};
|
|
133
|
+
options?: ForwardedApiOptions;
|
|
121
134
|
}) => Promise<unknown>;
|
|
122
135
|
getBySlug?: (opts: {
|
|
123
136
|
slug: string;
|
|
124
137
|
token?: string | null;
|
|
125
138
|
organizationId?: string | null;
|
|
126
139
|
params?: Record<string, unknown>;
|
|
127
|
-
options?:
|
|
128
|
-
headerOptions?: Record<string, string>;
|
|
129
|
-
};
|
|
140
|
+
options?: ForwardedApiOptions;
|
|
130
141
|
}) => Promise<unknown>;
|
|
131
142
|
getDeleted?: (opts: {
|
|
132
143
|
params?: Record<string, unknown>;
|
|
133
144
|
token?: string | null;
|
|
134
145
|
organizationId?: string | null;
|
|
135
|
-
options?:
|
|
136
|
-
headerOptions?: Record<string, string>;
|
|
137
|
-
};
|
|
146
|
+
options?: ForwardedApiOptions;
|
|
138
147
|
}) => Promise<unknown>;
|
|
139
148
|
aggregate?: (opts: {
|
|
140
149
|
name: string;
|
|
141
150
|
filter?: Record<string, unknown>;
|
|
142
151
|
token?: string | null;
|
|
143
152
|
organizationId?: string | null;
|
|
144
|
-
options?:
|
|
145
|
-
headerOptions?: Record<string, string>;
|
|
146
|
-
};
|
|
153
|
+
options?: ForwardedApiOptions;
|
|
147
154
|
}) => Promise<unknown>;
|
|
148
155
|
getTree?: (opts: {
|
|
149
156
|
params?: Record<string, unknown>;
|
|
150
157
|
token?: string | null;
|
|
151
158
|
organizationId?: string | null;
|
|
152
|
-
options?:
|
|
153
|
-
headerOptions?: Record<string, string>;
|
|
154
|
-
};
|
|
159
|
+
options?: ForwardedApiOptions;
|
|
155
160
|
}) => Promise<unknown>;
|
|
156
161
|
}, entityKey: string): CrudPrefetcher;
|
|
157
162
|
//#endregion
|
package/dist/prefetch.js
CHANGED
|
@@ -29,6 +29,14 @@ import { HydrationBoundary, dehydrate } from "@tanstack/react-query";
|
|
|
29
29
|
*/
|
|
30
30
|
function createCrudPrefetcher(api, entityKey) {
|
|
31
31
|
const KEYS = createQueryKeys(entityKey);
|
|
32
|
+
const apiOptions = (o) => {
|
|
33
|
+
const opt = {};
|
|
34
|
+
if (o.headers) opt.headerOptions = o.headers;
|
|
35
|
+
if (o.cache !== void 0) opt.cache = o.cache;
|
|
36
|
+
if (o.revalidate !== void 0) opt.revalidate = o.revalidate;
|
|
37
|
+
if (o.tags !== void 0) opt.tags = o.tags;
|
|
38
|
+
return Object.keys(opt).length ? { options: opt } : {};
|
|
39
|
+
};
|
|
32
40
|
return {
|
|
33
41
|
async prefetchList(queryClient, params = {}, options = {}) {
|
|
34
42
|
const { organizationId: paramOrgId, ...restParams } = params;
|
|
@@ -44,7 +52,7 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
44
52
|
params: restParams,
|
|
45
53
|
token: options.token ?? null,
|
|
46
54
|
organizationId: orgId,
|
|
47
|
-
...options
|
|
55
|
+
...apiOptions(options)
|
|
48
56
|
}),
|
|
49
57
|
staleTime: options.staleTime
|
|
50
58
|
});
|
|
@@ -60,7 +68,7 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
60
68
|
token: token ?? null,
|
|
61
69
|
organizationId: organizationId ?? null,
|
|
62
70
|
...params ? { params } : {},
|
|
63
|
-
...options
|
|
71
|
+
...apiOptions(options)
|
|
64
72
|
}),
|
|
65
73
|
staleTime
|
|
66
74
|
});
|
|
@@ -76,7 +84,7 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
76
84
|
token: token ?? null,
|
|
77
85
|
organizationId: organizationId ?? null,
|
|
78
86
|
...params ? { params } : {},
|
|
79
|
-
...options
|
|
87
|
+
...apiOptions(options)
|
|
80
88
|
}),
|
|
81
89
|
staleTime
|
|
82
90
|
});
|
|
@@ -95,7 +103,7 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
95
103
|
params: restParams,
|
|
96
104
|
token: options.token ?? null,
|
|
97
105
|
organizationId: orgId,
|
|
98
|
-
...options
|
|
106
|
+
...apiOptions(options)
|
|
99
107
|
}),
|
|
100
108
|
staleTime: options.staleTime
|
|
101
109
|
});
|
|
@@ -115,7 +123,7 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
115
123
|
filter,
|
|
116
124
|
token: options.token ?? null,
|
|
117
125
|
organizationId: orgId,
|
|
118
|
-
...options
|
|
126
|
+
...apiOptions(options)
|
|
119
127
|
}),
|
|
120
128
|
staleTime: options.staleTime
|
|
121
129
|
});
|
|
@@ -134,7 +142,7 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
134
142
|
params: restParams,
|
|
135
143
|
token: options.token ?? null,
|
|
136
144
|
organizationId: orgId,
|
|
137
|
-
...options
|
|
145
|
+
...apiOptions(options)
|
|
138
146
|
}),
|
|
139
147
|
staleTime: options.staleTime
|
|
140
148
|
});
|
|
@@ -156,7 +164,7 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
156
164
|
},
|
|
157
165
|
token: options.token ?? null,
|
|
158
166
|
organizationId: orgId,
|
|
159
|
-
...options
|
|
167
|
+
...apiOptions(options)
|
|
160
168
|
}),
|
|
161
169
|
initialPageParam: 1,
|
|
162
170
|
getNextPageParam: () => void 0,
|
package/dist/presets/bulk.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnyBaseApi, CreateOf, DocOf, ScopedArgs, UpdateOf } from "../api.js";
|
|
2
2
|
import { BulkCreateResult, DeleteManyResult, UpdateManyResult } from "@classytic/repo-core/repository";
|
|
3
3
|
|
|
4
4
|
//#region src/presets/bulk.d.ts
|
|
@@ -38,6 +38,6 @@ interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
|
|
|
38
38
|
* await todos.bulkUpdate({ filter: { status: 'pending' }, data: { status: 'archived' } });
|
|
39
39
|
* await todos.bulkDelete({ filter: { archivedBefore: '2024-01-01' } });
|
|
40
40
|
*/
|
|
41
|
-
declare function withBulk<
|
|
41
|
+
declare function withBulk<TApi extends AnyBaseApi>(api: TApi): TApi & BulkMethods<DocOf<TApi>, CreateOf<TApi>, UpdateOf<TApi>>;
|
|
42
42
|
//#endregion
|
|
43
43
|
export { BulkMethods, withBulk };
|
package/dist/presets/search.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnyBaseApi, DocOf, ScopedArgs } from "../api.js";
|
|
2
2
|
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
3
3
|
|
|
4
4
|
//#region src/presets/search.d.ts
|
|
@@ -51,6 +51,6 @@ interface SearchPresetMethods<TDoc> {
|
|
|
51
51
|
* await places.searchSimilar({ vector: [0.1, 0.2, ...], body: { topK: 5 } });
|
|
52
52
|
* await places.embed({ input: 'hello world' });
|
|
53
53
|
*/
|
|
54
|
-
declare function withSearchPreset<
|
|
54
|
+
declare function withSearchPreset<TApi extends AnyBaseApi>(api: TApi): TApi & SearchPresetMethods<DocOf<TApi>>;
|
|
55
55
|
//#endregion
|
|
56
56
|
export { SearchPresetMethods, withSearchPreset };
|
package/dist/presets/slug.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnyBaseApi, DocOf, ScopedArgs } from "../api.js";
|
|
2
2
|
|
|
3
3
|
//#region src/presets/slug.d.ts
|
|
4
4
|
interface SlugLookupMethods<TDoc> {
|
|
@@ -23,6 +23,6 @@ interface SlugLookupMethods<TDoc> {
|
|
|
23
23
|
*
|
|
24
24
|
* const cat = await categories.getBySlug({ slug: 'engineering' });
|
|
25
25
|
*/
|
|
26
|
-
declare function withSlugLookup<
|
|
26
|
+
declare function withSlugLookup<TApi extends AnyBaseApi>(api: TApi): TApi & SlugLookupMethods<DocOf<TApi>>;
|
|
27
27
|
//#endregion
|
|
28
28
|
export { SlugLookupMethods, withSlugLookup };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnyBaseApi, DocOf, QueryParams, ScopedArgs } from "../api.js";
|
|
2
2
|
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
3
3
|
|
|
4
4
|
//#region src/presets/soft-delete.d.ts
|
|
@@ -29,6 +29,6 @@ interface SoftDeleteMethods<TDoc> {
|
|
|
29
29
|
* await todos.restore({ id }); // undo
|
|
30
30
|
* const trash = await todos.getDeleted();
|
|
31
31
|
*/
|
|
32
|
-
declare function withSoftDelete<
|
|
32
|
+
declare function withSoftDelete<TApi extends AnyBaseApi>(api: TApi): TApi & SoftDeleteMethods<DocOf<TApi>>;
|
|
33
33
|
//#endregion
|
|
34
34
|
export { SoftDeleteMethods, withSoftDelete };
|
package/dist/presets/tree.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { AnyBaseApi, DocOf, QueryParams, ScopedArgs } from "../api.js";
|
|
2
2
|
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
3
3
|
|
|
4
4
|
//#region src/presets/tree.d.ts
|
|
@@ -27,6 +27,6 @@ interface TreeMethods<TDoc> {
|
|
|
27
27
|
* const root = await categories.getTree();
|
|
28
28
|
* const kids = await categories.getChildren({ parentId: 'engineering' });
|
|
29
29
|
*/
|
|
30
|
-
declare function withTree<
|
|
30
|
+
declare function withTree<TApi extends AnyBaseApi>(api: TApi): TApi & TreeMethods<DocOf<TApi>>;
|
|
31
31
|
//#endregion
|
|
32
32
|
export { TreeMethods, withTree };
|
package/dist/query.d.ts
CHANGED
|
@@ -195,6 +195,20 @@ declare function useDetailQuery<T>({
|
|
|
195
195
|
select,
|
|
196
196
|
placeholderData
|
|
197
197
|
}: CreateDetailQueryConfig<T>): DetailQueryResult<T>;
|
|
198
|
+
/** Suspense variant of {@link useListQuery}. Always enabled; suspends until resolved. */
|
|
199
|
+
declare function useSuspenseListQuery<T>({
|
|
200
|
+
queryKey,
|
|
201
|
+
queryFn,
|
|
202
|
+
options,
|
|
203
|
+
select
|
|
204
|
+
}: Omit<CreateListQueryConfig, "enabled">): ListQueryResult<T>;
|
|
205
|
+
/** Suspense variant of {@link useDetailQuery}. Always enabled; suspends until resolved. */
|
|
206
|
+
declare function useSuspenseDetailQuery<T>({
|
|
207
|
+
queryKey,
|
|
208
|
+
queryFn,
|
|
209
|
+
options,
|
|
210
|
+
select
|
|
211
|
+
}: Omit<CreateDetailQueryConfig<T>, "enabled" | "placeholderData">): DetailQueryResult<T>;
|
|
198
212
|
interface InfiniteListQueryOptions {
|
|
199
213
|
public?: boolean;
|
|
200
214
|
enabled?: boolean;
|
|
@@ -337,4 +351,4 @@ declare function useApiQuery<TResponse = unknown, TData = ExtractData<TResponse>
|
|
|
337
351
|
options
|
|
338
352
|
}: UseApiQueryConfig<TResponse, TData>): UseApiQueryResult<TData>;
|
|
339
353
|
//#endregion
|
|
340
|
-
export { type CacheUtils, CreateDetailQueryConfig, CreateInfiniteListQueryConfig, CreateListQueryConfig, DEFAULT_QUERY_CONFIG, DetailQueryOptions, DetailQueryResult, ExtractData, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, type PaginationData, QUERY_CONFIGS, QueryFreshness, type QueryKeys, RequestPassthrough, UseApiQueryConfig, UseApiQueryOptions, UseApiQueryResult, createCacheUtils, createQueryKeys, extractItem, extractItems, findItemInListCache, getItemId, normalizePagination, updateListCache, useApiQuery, useDetailQuery, useInfiniteListQuery, useListQuery };
|
|
354
|
+
export { type CacheUtils, CreateDetailQueryConfig, CreateInfiniteListQueryConfig, CreateListQueryConfig, DEFAULT_QUERY_CONFIG, DetailQueryOptions, DetailQueryResult, ExtractData, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, type PaginationData, QUERY_CONFIGS, QueryFreshness, type QueryKeys, RequestPassthrough, UseApiQueryConfig, UseApiQueryOptions, UseApiQueryResult, createCacheUtils, createQueryKeys, extractItem, extractItems, findItemInListCache, getItemId, normalizePagination, updateListCache, useApiQuery, useDetailQuery, useInfiniteListQuery, useListQuery, useSuspenseDetailQuery, useSuspenseListQuery };
|
package/dist/query.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
|
|
3
3
|
import { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, extractItems, getItemId, normalizePagination, updateListCache } from "./cache.js";
|
|
4
|
-
import { keepPreviousData, useInfiniteQuery, useQuery } from "@tanstack/react-query";
|
|
4
|
+
import { keepPreviousData, useInfiniteQuery, useQuery, useSuspenseQuery } from "@tanstack/react-query";
|
|
5
5
|
import { useCallback, useMemo, useRef } from "react";
|
|
6
6
|
|
|
7
7
|
//#region src/query.ts
|
|
@@ -99,6 +99,48 @@ function useDetailQuery({ queryKey, queryFn, enabled = true, options = {}, selec
|
|
|
99
99
|
refetch: query.refetch
|
|
100
100
|
};
|
|
101
101
|
}
|
|
102
|
+
/** Suspense variant of {@link useListQuery}. Always enabled; suspends until resolved. */
|
|
103
|
+
function useSuspenseListQuery({ queryKey, queryFn, options = {}, select }) {
|
|
104
|
+
const query = useSuspenseQuery({
|
|
105
|
+
queryKey,
|
|
106
|
+
queryFn: ({ signal }) => queryFn({ signal }),
|
|
107
|
+
...DEFAULT_QUERY_CONFIG,
|
|
108
|
+
...options,
|
|
109
|
+
...select ? { select } : {}
|
|
110
|
+
});
|
|
111
|
+
return {
|
|
112
|
+
items: useMemo(() => extractItems(query.data), [query.data]),
|
|
113
|
+
pagination: useMemo(() => normalizePagination(query.data), [query.data]),
|
|
114
|
+
isLoading: false,
|
|
115
|
+
isFetching: query.isFetching,
|
|
116
|
+
isError: query.isError,
|
|
117
|
+
isSuccess: query.isSuccess,
|
|
118
|
+
isStale: query.isStale,
|
|
119
|
+
error: query.error,
|
|
120
|
+
refetch: query.refetch
|
|
121
|
+
};
|
|
122
|
+
}
|
|
123
|
+
/** Suspense variant of {@link useDetailQuery}. Always enabled; suspends until resolved. */
|
|
124
|
+
function useSuspenseDetailQuery({ queryKey, queryFn, options = {}, select }) {
|
|
125
|
+
const query = useSuspenseQuery({
|
|
126
|
+
queryKey,
|
|
127
|
+
queryFn: ({ signal }) => queryFn({ signal }),
|
|
128
|
+
...DEFAULT_QUERY_CONFIG,
|
|
129
|
+
...options,
|
|
130
|
+
...select ? { select } : {}
|
|
131
|
+
});
|
|
132
|
+
return {
|
|
133
|
+
item: extractItem(query.data),
|
|
134
|
+
isLoading: false,
|
|
135
|
+
isFetching: query.isFetching,
|
|
136
|
+
isError: query.isError,
|
|
137
|
+
isSuccess: query.isSuccess,
|
|
138
|
+
isStale: query.isStale,
|
|
139
|
+
isPlaceholderData: false,
|
|
140
|
+
error: query.error,
|
|
141
|
+
refetch: query.refetch
|
|
142
|
+
};
|
|
143
|
+
}
|
|
102
144
|
function useInfiniteListQuery({ queryKey, queryFn, enabled = true, options = {}, initialPageParam = 1, getNextPageParam, getPreviousPageParam, maxPages }) {
|
|
103
145
|
const query = useInfiniteQuery({
|
|
104
146
|
queryKey,
|
|
@@ -189,4 +231,4 @@ function useApiQuery({ queryKey, queryFn, enabled = true, freshness, select, opt
|
|
|
189
231
|
}
|
|
190
232
|
|
|
191
233
|
//#endregion
|
|
192
|
-
export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, extractItems, findItemInListCache, getItemId, normalizePagination, updateListCache, useApiQuery, useDetailQuery, useInfiniteListQuery, useListQuery };
|
|
234
|
+
export { DEFAULT_QUERY_CONFIG, QUERY_CONFIGS, createCacheUtils, createQueryKeys, extractItem, extractItems, findItemInListCache, getItemId, normalizePagination, updateListCache, useApiQuery, useDetailQuery, useInfiniteListQuery, useListQuery, useSuspenseDetailQuery, useSuspenseListQuery };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc-next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "React + TanStack Query SDK for Arc resources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -38,6 +38,10 @@
|
|
|
38
38
|
"types": "./dist/client.d.ts",
|
|
39
39
|
"default": "./dist/client.js"
|
|
40
40
|
},
|
|
41
|
+
"./encryption": {
|
|
42
|
+
"types": "./dist/encryption.d.ts",
|
|
43
|
+
"default": "./dist/encryption.js"
|
|
44
|
+
},
|
|
41
45
|
"./api": {
|
|
42
46
|
"types": "./dist/api.d.ts",
|
|
43
47
|
"default": "./dist/api.js"
|
|
@@ -119,17 +123,28 @@
|
|
|
119
123
|
"push": "classytic-push",
|
|
120
124
|
"release:tag": "node -e \"require('child_process').execSync('npm run push -- v'+require('./package.json').version,{stdio:'inherit'})\"",
|
|
121
125
|
"release": "npm run push -- main && npm run release:tag && npm publish",
|
|
122
|
-
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
126
|
+
"prepublishOnly": "npm run typecheck && npm test && npm run build",
|
|
127
|
+
"typecheck:tests": "tsc --noEmit -p tsconfig.test.json"
|
|
123
128
|
},
|
|
124
129
|
"peerDependencies": {
|
|
125
130
|
"@classytic/repo-core": ">=0.4.0",
|
|
126
131
|
"@tanstack/react-query": ">=5.62.0",
|
|
132
|
+
"jose": ">=5.0.0",
|
|
127
133
|
"react": ">=19.0.0"
|
|
128
134
|
},
|
|
129
135
|
"peerDependenciesMeta": {
|
|
130
|
-
"react": {
|
|
131
|
-
|
|
132
|
-
|
|
136
|
+
"react": {
|
|
137
|
+
"optional": false
|
|
138
|
+
},
|
|
139
|
+
"@tanstack/react-query": {
|
|
140
|
+
"optional": false
|
|
141
|
+
},
|
|
142
|
+
"@classytic/repo-core": {
|
|
143
|
+
"optional": false
|
|
144
|
+
},
|
|
145
|
+
"jose": {
|
|
146
|
+
"optional": true
|
|
147
|
+
}
|
|
133
148
|
},
|
|
134
149
|
"devDependencies": {
|
|
135
150
|
"@classytic/dev-tools": "^0.2.0",
|
|
@@ -139,6 +154,7 @@
|
|
|
139
154
|
"@testing-library/react": "^16.3.2",
|
|
140
155
|
"@types/react": "^19.2.14",
|
|
141
156
|
"@types/react-dom": "^19.2.3",
|
|
157
|
+
"jose": "^6.2.3",
|
|
142
158
|
"jsdom": "^29.0.2",
|
|
143
159
|
"react": "^19.2.5",
|
|
144
160
|
"react-dom": "^19.2.5",
|