@classytic/arc-next 0.7.1 → 0.8.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 +41 -1
- package/dist/client.js +12 -2
- 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/query.d.ts +15 -1
- package/dist/query.js +44 -2
- package/package.json +9 -2
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). */
|
|
@@ -795,4 +835,4 @@ declare const arc: {
|
|
|
795
835
|
delete: <T = unknown>(path: string, opts?: ArcFetchOptions) => Promise<T>;
|
|
796
836
|
};
|
|
797
837
|
//#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 };
|
|
838
|
+
export { AfterResponseContext, AfterResponseInterceptor, ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, ArcErrorCode, ArcFetchOptions, AuthConfig, AuthErrorContext, AuthErrorHandler, BeforeRequestContext, BeforeRequestInterceptor, BlobResponse, ClientConfig, ClientEncryptionConfig, 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 };
|
package/dist/client.js
CHANGED
|
@@ -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,
|
|
@@ -799,7 +804,7 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
799
804
|
try {
|
|
800
805
|
json = await response.clone().json();
|
|
801
806
|
const j = json;
|
|
802
|
-
errorMessage = typeof j?.error === "string" && j.error || typeof j?.message === "string" && j.message || response.statusText;
|
|
807
|
+
errorMessage = typeof j?.error === "string" && j.error || typeof j?.meta?.message === "string" && j.meta.message || typeof j?.message === "string" && j.message || response.statusText;
|
|
803
808
|
} catch {
|
|
804
809
|
try {
|
|
805
810
|
const text = await response.text();
|
|
@@ -819,7 +824,12 @@ async function executeAttempt(config, method, endpoint, options, attempt) {
|
|
|
819
824
|
}
|
|
820
825
|
const contentType = response.headers.get("Content-Type");
|
|
821
826
|
let data;
|
|
822
|
-
|
|
827
|
+
const decryptCt = encryption ? encryption.responseContentType ?? "application/jose" : void 0;
|
|
828
|
+
if (encryption && decryptCt && contentType?.includes(decryptCt)) {
|
|
829
|
+
const cipher = await response.text();
|
|
830
|
+
const plaintext = cipher.length > 0 ? await encryption.decrypt(cipher) : "";
|
|
831
|
+
data = plaintext.length > 0 ? JSON.parse(plaintext) : void 0;
|
|
832
|
+
} else if (contentType?.includes("application/json")) data = await response.json();
|
|
823
833
|
else if (contentType?.includes("application/pdf") || contentType?.includes("image/")) data = {
|
|
824
834
|
data: await response.blob(),
|
|
825
835
|
response
|
|
@@ -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/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.8.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"
|
|
@@ -124,12 +128,14 @@
|
|
|
124
128
|
"peerDependencies": {
|
|
125
129
|
"@classytic/repo-core": ">=0.4.0",
|
|
126
130
|
"@tanstack/react-query": ">=5.62.0",
|
|
131
|
+
"jose": ">=5.0.0",
|
|
127
132
|
"react": ">=19.0.0"
|
|
128
133
|
},
|
|
129
134
|
"peerDependenciesMeta": {
|
|
130
135
|
"react": { "optional": false },
|
|
131
136
|
"@tanstack/react-query": { "optional": false },
|
|
132
|
-
"@classytic/repo-core": { "optional": false }
|
|
137
|
+
"@classytic/repo-core": { "optional": false },
|
|
138
|
+
"jose": { "optional": true }
|
|
133
139
|
},
|
|
134
140
|
"devDependencies": {
|
|
135
141
|
"@classytic/dev-tools": "^0.2.0",
|
|
@@ -139,6 +145,7 @@
|
|
|
139
145
|
"@testing-library/react": "^16.3.2",
|
|
140
146
|
"@types/react": "^19.2.14",
|
|
141
147
|
"@types/react-dom": "^19.2.3",
|
|
148
|
+
"jose": "^6.2.3",
|
|
142
149
|
"jsdom": "^29.0.2",
|
|
143
150
|
"react": "^19.2.5",
|
|
144
151
|
"react-dom": "^19.2.5",
|