@classytic/arc-next 0.5.0 → 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +7 -5
- package/dist/api.d.ts +59 -75
- package/dist/api.js +25 -6
- package/dist/cache.d.ts +20 -3
- package/dist/cache.js +31 -17
- package/dist/client.d.ts +64 -69
- package/dist/client.js +115 -82
- package/dist/hooks.d.ts +72 -8
- package/dist/hooks.js +54 -8
- package/dist/prefetch.d.ts +23 -0
- package/dist/prefetch.js +20 -0
- package/dist/presets/bulk.d.ts +5 -4
- package/dist/presets/search.d.ts +5 -4
- package/dist/presets/slug.d.ts +2 -2
- package/dist/presets/soft-delete.d.ts +4 -3
- package/dist/presets/tree.d.ts +4 -3
- package/dist/query.d.ts +20 -20
- package/dist/query.js +12 -22
- package/package.json +3 -1
package/dist/hooks.js
CHANGED
|
@@ -7,7 +7,7 @@ import { useDetailQuery, useInfiniteListQuery, useListQuery } 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";
|
|
10
|
-
import { useQueryClient } from "@tanstack/react-query";
|
|
10
|
+
import { useQuery, useQueryClient } from "@tanstack/react-query";
|
|
11
11
|
import { useCallback, useEffect, useRef, useState } from "react";
|
|
12
12
|
|
|
13
13
|
//#region src/hooks.ts
|
|
@@ -166,7 +166,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
166
166
|
data
|
|
167
167
|
}),
|
|
168
168
|
queryClient,
|
|
169
|
-
queryKeys: [KEYS.lists()],
|
|
169
|
+
queryKeys: [KEYS.lists(), KEYS.aggregations()],
|
|
170
170
|
shouldToast,
|
|
171
171
|
optimisticUpdate: (oldData, { data }) => {
|
|
172
172
|
const optimisticItem = {
|
|
@@ -199,7 +199,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
199
199
|
data
|
|
200
200
|
}),
|
|
201
201
|
queryClient,
|
|
202
|
-
queryKeys: [
|
|
202
|
+
queryKeys: [
|
|
203
|
+
KEYS.lists(),
|
|
204
|
+
KEYS.details(),
|
|
205
|
+
KEYS.aggregations()
|
|
206
|
+
],
|
|
203
207
|
shouldToast,
|
|
204
208
|
optimisticUpdate: (oldData, { id, data }) => {
|
|
205
209
|
const updated = updateListCache(oldData, (arr) => (arr || []).map((item) => resolveItemId(item) === id ? {
|
|
@@ -250,7 +254,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
250
254
|
id
|
|
251
255
|
}),
|
|
252
256
|
queryClient,
|
|
253
|
-
queryKeys: [KEYS.lists()],
|
|
257
|
+
queryKeys: [KEYS.lists(), KEYS.aggregations()],
|
|
254
258
|
shouldToast,
|
|
255
259
|
optimisticUpdate: (oldData, { id }) => {
|
|
256
260
|
return updateListCache(oldData, (arr) => (arr || []).filter((item) => resolveItemId(item) !== id));
|
|
@@ -280,7 +284,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
280
284
|
id
|
|
281
285
|
});
|
|
282
286
|
},
|
|
283
|
-
invalidateQueries: [
|
|
287
|
+
invalidateQueries: [
|
|
288
|
+
KEYS.lists(),
|
|
289
|
+
KEYS.custom("deleted"),
|
|
290
|
+
KEYS.aggregations()
|
|
291
|
+
],
|
|
284
292
|
shouldToast,
|
|
285
293
|
messages: {
|
|
286
294
|
success: `${singular} restored successfully`,
|
|
@@ -608,7 +616,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
608
616
|
data: vars.data
|
|
609
617
|
});
|
|
610
618
|
},
|
|
611
|
-
invalidateQueries: [KEYS.lists()],
|
|
619
|
+
invalidateQueries: [KEYS.lists(), KEYS.aggregations()],
|
|
612
620
|
messages: {
|
|
613
621
|
success: `${pluralName} created successfully`,
|
|
614
622
|
error: `Failed to create ${pluralName.toLowerCase()}`
|
|
@@ -626,7 +634,11 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
626
634
|
data: vars.data
|
|
627
635
|
});
|
|
628
636
|
},
|
|
629
|
-
invalidateQueries: [
|
|
637
|
+
invalidateQueries: [
|
|
638
|
+
KEYS.lists(),
|
|
639
|
+
KEYS.details(),
|
|
640
|
+
KEYS.aggregations()
|
|
641
|
+
],
|
|
630
642
|
messages: {
|
|
631
643
|
success: `${pluralName} updated successfully`,
|
|
632
644
|
error: `Failed to update ${pluralName.toLowerCase()}`
|
|
@@ -643,7 +655,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
643
655
|
filter: vars.filter
|
|
644
656
|
});
|
|
645
657
|
},
|
|
646
|
-
invalidateQueries: [KEYS.lists()],
|
|
658
|
+
invalidateQueries: [KEYS.lists(), KEYS.aggregations()],
|
|
647
659
|
messages: {
|
|
648
660
|
success: `${pluralName} deleted successfully`,
|
|
649
661
|
error: `Failed to delete ${pluralName.toLowerCase()}`
|
|
@@ -772,6 +784,38 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
772
784
|
toastHandler: instanceToast
|
|
773
785
|
});
|
|
774
786
|
}
|
|
787
|
+
function useAggregation(args) {
|
|
788
|
+
const { name, filter, public: isPublic, enabled = true, staleTime = config.staleTime, gcTime = config.gcTime, refetchOnWindowFocus = false, refetchInterval, refetchIntervalInBackground, select, request, placeholderData } = args;
|
|
789
|
+
const auth = resolveAuth();
|
|
790
|
+
const filterKey = auth.organizationId ? {
|
|
791
|
+
_org: auth.organizationId,
|
|
792
|
+
...filter ?? {}
|
|
793
|
+
} : filter ?? {};
|
|
794
|
+
return useQuery({
|
|
795
|
+
queryKey: KEYS.aggregation(name, filterKey),
|
|
796
|
+
queryFn: () => {
|
|
797
|
+
if (!api.aggregate) return Promise.reject(/* @__PURE__ */ new Error(`[arc-next] "${entityKey}" api does not define an aggregate method (requires arc 2.13+)`));
|
|
798
|
+
return api.aggregate({
|
|
799
|
+
name,
|
|
800
|
+
filter,
|
|
801
|
+
token: auth.token,
|
|
802
|
+
organizationId: auth.organizationId,
|
|
803
|
+
...request ? { options: request } : {}
|
|
804
|
+
});
|
|
805
|
+
},
|
|
806
|
+
enabled: !!name && !!api.aggregate && createEnabledRule(auth.token, {
|
|
807
|
+
public: isPublic,
|
|
808
|
+
enabled
|
|
809
|
+
}, resolveAuthMode(), hasStaticAuth),
|
|
810
|
+
staleTime,
|
|
811
|
+
gcTime,
|
|
812
|
+
refetchOnWindowFocus,
|
|
813
|
+
...select ? { select } : {},
|
|
814
|
+
...refetchInterval !== void 0 ? { refetchInterval } : {},
|
|
815
|
+
...refetchIntervalInBackground !== void 0 ? { refetchIntervalInBackground } : {},
|
|
816
|
+
...placeholderData !== void 0 ? { placeholderData } : {}
|
|
817
|
+
});
|
|
818
|
+
}
|
|
775
819
|
/**
|
|
776
820
|
* Subscribe to live `<resource>.<operation>` broadcasts from arc and
|
|
777
821
|
* auto-invalidate this entity's TanStack Query cache.
|
|
@@ -813,6 +857,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
813
857
|
return raw != null ? String(raw) : void 0;
|
|
814
858
|
})() : void 0;
|
|
815
859
|
queryClient.invalidateQueries({ queryKey: KEYS.lists() });
|
|
860
|
+
queryClient.invalidateQueries({ queryKey: KEYS.aggregations() });
|
|
816
861
|
if (id && (operation === "updated" || operation === "deleted")) queryClient.invalidateQueries({ queryKey: KEYS.detail(id) });
|
|
817
862
|
onEventRef.current?.({
|
|
818
863
|
operation,
|
|
@@ -894,6 +939,7 @@ function createCrudHooks({ api, entityKey, singular, plural, idField, defaults =
|
|
|
894
939
|
useSearchEngine,
|
|
895
940
|
useSearchSimilar,
|
|
896
941
|
useEmbed,
|
|
942
|
+
useAggregation,
|
|
897
943
|
useResourceSync,
|
|
898
944
|
useNavigation
|
|
899
945
|
};
|
package/dist/prefetch.d.ts
CHANGED
|
@@ -61,6 +61,20 @@ interface CrudPrefetcher {
|
|
|
61
61
|
* @example
|
|
62
62
|
* await productsPrefetcher.prefetchInfiniteList(queryClient, { limit: 20 });
|
|
63
63
|
*/
|
|
64
|
+
/**
|
|
65
|
+
* Prefetch a declared aggregation (arc 2.13+). Uses the same query key as
|
|
66
|
+
* `useAggregation` so RSC-pre-rendered dashboard rows hydrate without a
|
|
67
|
+
* client refetch.
|
|
68
|
+
*
|
|
69
|
+
* @example
|
|
70
|
+
* await ordersPrefetcher.prefetchAggregation(
|
|
71
|
+
* queryClient,
|
|
72
|
+
* 'salesByDay',
|
|
73
|
+
* { from: '2025-01-01', to: '2025-12-31' },
|
|
74
|
+
* { token: jwt, organizationId: orgId, staleTime: 60_000 },
|
|
75
|
+
* );
|
|
76
|
+
*/
|
|
77
|
+
prefetchAggregation: (queryClient: QueryClient, name: string, filter?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
|
|
64
78
|
prefetchInfiniteList: (queryClient: QueryClient, params?: Record<string, unknown>, options?: PrefetchOptions) => Promise<void>;
|
|
65
79
|
}
|
|
66
80
|
/**
|
|
@@ -122,6 +136,15 @@ declare function createCrudPrefetcher(api: {
|
|
|
122
136
|
headerOptions?: Record<string, string>;
|
|
123
137
|
};
|
|
124
138
|
}) => Promise<unknown>;
|
|
139
|
+
aggregate?: (opts: {
|
|
140
|
+
name: string;
|
|
141
|
+
filter?: Record<string, unknown>;
|
|
142
|
+
token?: string | null;
|
|
143
|
+
organizationId?: string | null;
|
|
144
|
+
options?: {
|
|
145
|
+
headerOptions?: Record<string, string>;
|
|
146
|
+
};
|
|
147
|
+
}) => Promise<unknown>;
|
|
125
148
|
getTree?: (opts: {
|
|
126
149
|
params?: Record<string, unknown>;
|
|
127
150
|
token?: string | null;
|
package/dist/prefetch.js
CHANGED
|
@@ -100,6 +100,26 @@ function createCrudPrefetcher(api, entityKey) {
|
|
|
100
100
|
staleTime: options.staleTime
|
|
101
101
|
});
|
|
102
102
|
},
|
|
103
|
+
async prefetchAggregation(queryClient, name, filter, options = {}) {
|
|
104
|
+
if (!api.aggregate) throw new Error(`[arc-next] prefetchAggregation requires an api with aggregate (arc 2.13+)`);
|
|
105
|
+
if (!name) throw new Error("[arc-next] prefetchAggregation: aggregation name is required");
|
|
106
|
+
const orgId = options.organizationId ?? null;
|
|
107
|
+
const filterKey = orgId ? {
|
|
108
|
+
_org: orgId,
|
|
109
|
+
...filter ?? {}
|
|
110
|
+
} : filter ?? {};
|
|
111
|
+
await queryClient.prefetchQuery({
|
|
112
|
+
queryKey: KEYS.aggregation(name, filterKey),
|
|
113
|
+
queryFn: () => api.aggregate({
|
|
114
|
+
name,
|
|
115
|
+
filter,
|
|
116
|
+
token: options.token ?? null,
|
|
117
|
+
organizationId: orgId,
|
|
118
|
+
...options.headers ? { options: { headerOptions: options.headers } } : {}
|
|
119
|
+
}),
|
|
120
|
+
staleTime: options.staleTime
|
|
121
|
+
});
|
|
122
|
+
},
|
|
103
123
|
async prefetchTree(queryClient, params = {}, options = {}) {
|
|
104
124
|
if (!api.getTree) throw new Error(`[arc-next] prefetchTree requires an api with getTree (tree preset)`);
|
|
105
125
|
const { organizationId: paramOrgId, ...restParams } = params;
|
package/dist/presets/bulk.d.ts
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
|
-
import { BaseApi,
|
|
1
|
+
import { BaseApi, ScopedArgs } from "../api.js";
|
|
2
|
+
import { BulkCreateResult, DeleteManyResult, UpdateManyResult } from "@classytic/repo-core/repository";
|
|
2
3
|
|
|
3
4
|
//#region src/presets/bulk.d.ts
|
|
4
5
|
interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
|
|
5
6
|
/** Insert many docs in one round-trip. Backend mounts `POST /:resource/bulk`. */
|
|
6
7
|
bulkCreate(args: ScopedArgs & {
|
|
7
8
|
data: TCreate[];
|
|
8
|
-
}): Promise<
|
|
9
|
+
}): Promise<BulkCreateResult<TDoc>>;
|
|
9
10
|
/**
|
|
10
11
|
* Update all docs matching `filter` with `data`.
|
|
11
12
|
* Backend mounts `PATCH /:resource/bulk`.
|
|
@@ -13,14 +14,14 @@ interface BulkMethods<TDoc, TCreate = Partial<TDoc>, TUpdate = Partial<TDoc>> {
|
|
|
13
14
|
bulkUpdate(args: ScopedArgs & {
|
|
14
15
|
filter: Record<string, unknown>;
|
|
15
16
|
data: TUpdate;
|
|
16
|
-
}): Promise<
|
|
17
|
+
}): Promise<UpdateManyResult>;
|
|
17
18
|
/**
|
|
18
19
|
* Delete all docs matching `filter`.
|
|
19
20
|
* Backend mounts `DELETE /:resource/bulk`.
|
|
20
21
|
*/
|
|
21
22
|
bulkDelete(args: ScopedArgs & {
|
|
22
23
|
filter: Record<string, unknown>;
|
|
23
|
-
}): Promise<
|
|
24
|
+
}): Promise<DeleteManyResult>;
|
|
24
25
|
}
|
|
25
26
|
/**
|
|
26
27
|
* Adds bulk preset methods to a BaseApi.
|
package/dist/presets/search.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseApi, ScopedArgs } from "../api.js";
|
|
2
|
+
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
2
3
|
|
|
3
4
|
//#region src/presets/search.d.ts
|
|
4
5
|
interface SearchPresetMethods<TDoc> {
|
|
@@ -10,7 +11,7 @@ interface SearchPresetMethods<TDoc> {
|
|
|
10
11
|
/** Free-text query forwarded as `body.query`. */query?: string; /** Engine-specific options merged into the request body. */
|
|
11
12
|
body?: TBody; /** Override path (default `/search`). */
|
|
12
13
|
path?: string;
|
|
13
|
-
}): Promise<
|
|
14
|
+
}): Promise<TResult[] | PaginatedResult<TResult>>;
|
|
14
15
|
/**
|
|
15
16
|
* Vector / semantic similarity (Atlas, Pinecone, Qdrant...).
|
|
16
17
|
* Backend mounts `POST /:resource/search-similar`.
|
|
@@ -20,7 +21,7 @@ interface SearchPresetMethods<TDoc> {
|
|
|
20
21
|
vector?: number[]; /** Vector-engine options (`topK`, `filter`, `index`, ...). */
|
|
21
22
|
body?: TBody; /** Override path (default `/search-similar`). */
|
|
22
23
|
path?: string;
|
|
23
|
-
}): Promise<
|
|
24
|
+
}): Promise<TResult[]>;
|
|
24
25
|
/**
|
|
25
26
|
* Convert text/media to a vector embedding via the engine the resource is wired to.
|
|
26
27
|
* Backend mounts `POST /:resource/embed`.
|
|
@@ -29,7 +30,7 @@ interface SearchPresetMethods<TDoc> {
|
|
|
29
30
|
/** Text or array of texts to embed. */input: string | string[]; /** Embed-engine options (`model`, `dimensions`, ...). */
|
|
30
31
|
body?: Record<string, unknown>; /** Override path (default `/embed`). */
|
|
31
32
|
path?: string;
|
|
32
|
-
}): Promise<
|
|
33
|
+
}): Promise<number[] | number[][]>;
|
|
33
34
|
}
|
|
34
35
|
/**
|
|
35
36
|
* Adds search preset methods to a BaseApi.
|
package/dist/presets/slug.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseApi, ScopedArgs } from "../api.js";
|
|
2
2
|
|
|
3
3
|
//#region src/presets/slug.d.ts
|
|
4
4
|
interface SlugLookupMethods<TDoc> {
|
|
@@ -9,7 +9,7 @@ interface SlugLookupMethods<TDoc> {
|
|
|
9
9
|
select?: string;
|
|
10
10
|
populate?: string | string[];
|
|
11
11
|
};
|
|
12
|
-
}): Promise<
|
|
12
|
+
}): Promise<TDoc>;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
15
|
* Adds slug-lookup preset methods to a BaseApi.
|
|
@@ -1,15 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseApi, QueryParams, ScopedArgs } from "../api.js";
|
|
2
|
+
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
2
3
|
|
|
3
4
|
//#region src/presets/soft-delete.d.ts
|
|
4
5
|
interface SoftDeleteMethods<TDoc> {
|
|
5
6
|
/** List soft-deleted docs. Backend mounts `GET /:resource/deleted`. */
|
|
6
7
|
getDeleted(args?: ScopedArgs & {
|
|
7
8
|
params?: QueryParams;
|
|
8
|
-
}): Promise<
|
|
9
|
+
}): Promise<PaginatedResult<TDoc>>;
|
|
9
10
|
/** Undo a soft-delete. Backend mounts `POST /:resource/:id/restore`. */
|
|
10
11
|
restore(args: ScopedArgs & {
|
|
11
12
|
id: string;
|
|
12
|
-
}): Promise<
|
|
13
|
+
}): Promise<TDoc>;
|
|
13
14
|
}
|
|
14
15
|
/**
|
|
15
16
|
* Adds soft-delete preset methods to a BaseApi.
|
package/dist/presets/tree.d.ts
CHANGED
|
@@ -1,16 +1,17 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { BaseApi, QueryParams, ScopedArgs } from "../api.js";
|
|
2
|
+
import { PaginatedResult } from "@classytic/repo-core/pagination";
|
|
2
3
|
|
|
3
4
|
//#region src/presets/tree.d.ts
|
|
4
5
|
interface TreeMethods<TDoc> {
|
|
5
6
|
/** Fetch the full hierarchy. Backend mounts `GET /:resource/tree`. */
|
|
6
7
|
getTree(args?: ScopedArgs & {
|
|
7
8
|
params?: QueryParams;
|
|
8
|
-
}): Promise<
|
|
9
|
+
}): Promise<TDoc[]>;
|
|
9
10
|
/** Fetch direct children of a node. Backend mounts `GET /:resource/:id/children`. */
|
|
10
11
|
getChildren(args: ScopedArgs & {
|
|
11
12
|
parentId: string;
|
|
12
13
|
params?: QueryParams;
|
|
13
|
-
}): Promise<
|
|
14
|
+
}): Promise<PaginatedResult<TDoc>>;
|
|
14
15
|
}
|
|
15
16
|
/**
|
|
16
17
|
* Adds tree preset methods to a BaseApi.
|
package/dist/query.d.ts
CHANGED
|
@@ -169,13 +169,13 @@ declare function useInfiniteListQuery<T>({
|
|
|
169
169
|
/** Recognized data-freshness presets. Maps to QUERY_CONFIGS. */
|
|
170
170
|
type QueryFreshness = keyof typeof QUERY_CONFIGS;
|
|
171
171
|
/**
|
|
172
|
-
*
|
|
173
|
-
*
|
|
172
|
+
* Identity passthrough — arc emits raw data on success (no envelope; HTTP
|
|
173
|
+
* status discriminates errors via thrown `ArcApiError`). Kept as a named
|
|
174
|
+
* type for the `useApiQuery` default param so the public signature stays
|
|
175
|
+
* `useApiQuery<TResponse, TData = ExtractData<TResponse>>` for callers
|
|
176
|
+
* that want a custom select projection.
|
|
174
177
|
*/
|
|
175
|
-
type ExtractData<T> = T
|
|
176
|
-
success: unknown;
|
|
177
|
-
data: infer D;
|
|
178
|
-
} ? D : T;
|
|
178
|
+
type ExtractData<T> = T;
|
|
179
179
|
/** Per-call request pass-through and TanStack Query overrides. */
|
|
180
180
|
interface UseApiQueryOptions {
|
|
181
181
|
staleTime?: number;
|
|
@@ -196,9 +196,9 @@ interface UseApiQueryConfig<TResponse, TData> {
|
|
|
196
196
|
/** Freshness preset name (`realtime` | `frequent` | `stable` | `static`). */
|
|
197
197
|
freshness?: QueryFreshness;
|
|
198
198
|
/**
|
|
199
|
-
* Custom projection from the raw response. When omitted, the
|
|
200
|
-
*
|
|
201
|
-
*
|
|
199
|
+
* Custom projection from the raw response. When omitted, the response is
|
|
200
|
+
* returned unchanged — arc 2.13+ has no wire envelope, so the response
|
|
201
|
+
* already IS the data.
|
|
202
202
|
*/
|
|
203
203
|
select?: (response: TResponse) => TData;
|
|
204
204
|
/** TanStack Query overrides (staleTime, refetchInterval, retry, etc.). */
|
|
@@ -216,29 +216,29 @@ interface UseApiQueryResult<TData> {
|
|
|
216
216
|
}
|
|
217
217
|
/**
|
|
218
218
|
* Generic query hook for non-CRUD reads (reports, aggregates, lookups, RPC-style
|
|
219
|
-
* endpoints). Wraps TanStack Query with
|
|
219
|
+
* endpoints). Wraps TanStack Query with two ergonomics on top:
|
|
220
220
|
*
|
|
221
|
-
* 1. **
|
|
222
|
-
* `T` automatically. Override with a custom `select` for shape changes or
|
|
223
|
-
* multi-field projections.
|
|
224
|
-
* 2. **Freshness presets.** Pass `freshness: 'realtime' | 'frequent' | 'stable' |
|
|
221
|
+
* 1. **Freshness presets.** Pass `freshness: 'realtime' | 'frequent' | 'stable' |
|
|
225
222
|
* 'static'` to map onto `QUERY_CONFIGS`. Per-option overrides still win.
|
|
226
|
-
*
|
|
223
|
+
* 2. **Standardized result shape.** `{ data, isLoading, isFetching, isError,
|
|
227
224
|
* isSuccess, isStale, error, refetch }` — same contract as the CRUD hooks.
|
|
228
225
|
*
|
|
226
|
+
* Arc emits raw data on success (no envelope), so the response IS the data.
|
|
227
|
+
* Use `select` for shape transformations / multi-field projections.
|
|
228
|
+
*
|
|
229
229
|
* @example
|
|
230
|
-
* //
|
|
231
|
-
* const { data } = useApiQuery<
|
|
230
|
+
* // Direct typing — response IS the data
|
|
231
|
+
* const { data } = useApiQuery<DashboardStats>({
|
|
232
232
|
* queryKey: ['dashboard', 'stats'],
|
|
233
233
|
* queryFn: ({ signal }) => api.request('GET', '/dashboard/stats', { options: { signal } }),
|
|
234
234
|
* freshness: 'realtime',
|
|
235
235
|
* });
|
|
236
236
|
*
|
|
237
|
-
* // Custom projection
|
|
237
|
+
* // Custom projection (e.g. extract a sub-field)
|
|
238
238
|
* const { data } = useApiQuery({
|
|
239
239
|
* queryKey: ['ledger', accountId],
|
|
240
|
-
* queryFn: ({ signal }) => api.request('GET', `/ledger/${accountId}`, { options: { signal } }),
|
|
241
|
-
* select: (res) => res.
|
|
240
|
+
* queryFn: ({ signal }) => api.request<{ entries: Entry[] }>('GET', `/ledger/${accountId}`, { options: { signal } }),
|
|
241
|
+
* select: (res) => res.entries,
|
|
242
242
|
* });
|
|
243
243
|
*/
|
|
244
244
|
declare function useApiQuery<TResponse = unknown, TData = ExtractData<TResponse>>({
|
package/dist/query.js
CHANGED
|
@@ -98,40 +98,30 @@ function useInfiniteListQuery({ queryKey, queryFn, enabled = true, options = {},
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
/**
|
|
101
|
-
* Strict Arc envelope detector — requires BOTH `success` and `data` keys.
|
|
102
|
-
*
|
|
103
|
-
* Stricter than checking only `data` so we don't mis-unwrap responses that happen
|
|
104
|
-
* to use a `data` field for unrelated semantics (e.g. a chart that returns
|
|
105
|
-
* `{ data: number[], labels: [] }`).
|
|
106
|
-
*/
|
|
107
|
-
function isArcEnvelope(value) {
|
|
108
|
-
return typeof value === "object" && value !== null && "success" in value && "data" in value;
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
101
|
* Generic query hook for non-CRUD reads (reports, aggregates, lookups, RPC-style
|
|
112
|
-
* endpoints). Wraps TanStack Query with
|
|
102
|
+
* endpoints). Wraps TanStack Query with two ergonomics on top:
|
|
113
103
|
*
|
|
114
|
-
* 1. **
|
|
115
|
-
* `T` automatically. Override with a custom `select` for shape changes or
|
|
116
|
-
* multi-field projections.
|
|
117
|
-
* 2. **Freshness presets.** Pass `freshness: 'realtime' | 'frequent' | 'stable' |
|
|
104
|
+
* 1. **Freshness presets.** Pass `freshness: 'realtime' | 'frequent' | 'stable' |
|
|
118
105
|
* 'static'` to map onto `QUERY_CONFIGS`. Per-option overrides still win.
|
|
119
|
-
*
|
|
106
|
+
* 2. **Standardized result shape.** `{ data, isLoading, isFetching, isError,
|
|
120
107
|
* isSuccess, isStale, error, refetch }` — same contract as the CRUD hooks.
|
|
121
108
|
*
|
|
109
|
+
* Arc emits raw data on success (no envelope), so the response IS the data.
|
|
110
|
+
* Use `select` for shape transformations / multi-field projections.
|
|
111
|
+
*
|
|
122
112
|
* @example
|
|
123
|
-
* //
|
|
124
|
-
* const { data } = useApiQuery<
|
|
113
|
+
* // Direct typing — response IS the data
|
|
114
|
+
* const { data } = useApiQuery<DashboardStats>({
|
|
125
115
|
* queryKey: ['dashboard', 'stats'],
|
|
126
116
|
* queryFn: ({ signal }) => api.request('GET', '/dashboard/stats', { options: { signal } }),
|
|
127
117
|
* freshness: 'realtime',
|
|
128
118
|
* });
|
|
129
119
|
*
|
|
130
|
-
* // Custom projection
|
|
120
|
+
* // Custom projection (e.g. extract a sub-field)
|
|
131
121
|
* const { data } = useApiQuery({
|
|
132
122
|
* queryKey: ['ledger', accountId],
|
|
133
|
-
* queryFn: ({ signal }) => api.request('GET', `/ledger/${accountId}`, { options: { signal } }),
|
|
134
|
-
* select: (res) => res.
|
|
123
|
+
* queryFn: ({ signal }) => api.request<{ entries: Entry[] }>('GET', `/ledger/${accountId}`, { options: { signal } }),
|
|
124
|
+
* select: (res) => res.entries,
|
|
135
125
|
* });
|
|
136
126
|
*/
|
|
137
127
|
function useApiQuery({ queryKey, queryFn, enabled = true, freshness, select, options = {} }) {
|
|
@@ -140,7 +130,7 @@ function useApiQuery({ queryKey, queryFn, enabled = true, freshness, select, opt
|
|
|
140
130
|
const projection = useCallback((response) => {
|
|
141
131
|
const fn = selectRef.current;
|
|
142
132
|
if (fn) return fn(response);
|
|
143
|
-
return
|
|
133
|
+
return response;
|
|
144
134
|
}, []);
|
|
145
135
|
const preset = freshness ? QUERY_CONFIGS[freshness] : void 0;
|
|
146
136
|
const query = useQuery({
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@classytic/arc-next",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"description": "React + TanStack Query SDK for Arc resources",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"sideEffects": false,
|
|
@@ -122,11 +122,13 @@
|
|
|
122
122
|
"prepublishOnly": "npm run typecheck && npm test && npm run build"
|
|
123
123
|
},
|
|
124
124
|
"peerDependencies": {
|
|
125
|
+
"@classytic/repo-core": ">=0.4.0",
|
|
125
126
|
"@tanstack/react-query": ">=5.0.0",
|
|
126
127
|
"react": ">=19.0.0"
|
|
127
128
|
},
|
|
128
129
|
"devDependencies": {
|
|
129
130
|
"@classytic/dev-tools": "^0.2.0",
|
|
131
|
+
"@classytic/repo-core": "^0.4.0",
|
|
130
132
|
"@tanstack/react-query": "^5.97.0",
|
|
131
133
|
"@testing-library/jest-dom": "^6.9.1",
|
|
132
134
|
"@testing-library/react": "^16.3.2",
|