@classytic/arc-next 0.2.1 → 0.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/client.d.ts CHANGED
@@ -55,11 +55,38 @@ interface ClientConfig {
55
55
  * - 'bearer' (default): Requires a token for authenticated requests. Queries are disabled until a token is provided.
56
56
  * - 'cookie': Auth is handled via HTTP-only cookies (e.g. Better Auth). Queries are always enabled — no token needed.
57
57
  */
58
- authMode?: 'bearer' | 'cookie';
58
+ authMode?: 'bearer' | 'cookie' | 'header';
59
+ /**
60
+ * Fetch credentials policy.
61
+ * - 'include': Always send cookies cross-origin (required for cookie-based auth).
62
+ * - 'same-origin': Only send cookies to same-origin requests (browser default).
63
+ * - 'omit': Never send cookies.
64
+ *
65
+ * When not set, derived from `authMode`:
66
+ * - `authMode: 'cookie'` → `'include'`
67
+ * - `authMode: 'bearer'` (default) → `'same-origin'`
68
+ */
69
+ credentials?: RequestCredentials;
70
+ /**
71
+ * API version sent as `Accept-Version` header.
72
+ * Use when the Arc backend has versioning enabled.
73
+ * @example '2' // sends Accept-Version: 2
74
+ */
75
+ apiVersion?: string;
76
+ /**
77
+ * Auto-generate `Idempotency-Key` header for POST/PUT/PATCH requests.
78
+ * Prevents duplicate mutations on network retries.
79
+ * Default: false — opt-in per-request via `idempotencyKey` option.
80
+ */
81
+ autoIdempotency?: boolean;
59
82
  }
60
83
  /**
61
84
  * Configure the API client. Call once at app init before any API requests.
62
85
  *
86
+ * **SSR safety:** This sets module-level state. In Next.js, call this only in
87
+ * client-side code (e.g. a `"use client"` provider or `useEffect`).
88
+ * Calling on the server risks leaking state between requests.
89
+ *
63
90
  * @example
64
91
  * // Bearer token auth (default)
65
92
  * configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! });
@@ -71,15 +98,21 @@ declare function configureClient(config: ClientConfig): void;
71
98
  /**
72
99
  * Get the configured auth mode. Returns 'bearer' if not configured.
73
100
  */
74
- declare function getAuthMode(): 'bearer' | 'cookie';
101
+ declare function getAuthMode(): 'bearer' | 'cookie' | 'header';
102
+ /** Whether auto-idempotency is enabled on the global client. */
103
+ declare function isAutoIdempotency(): boolean;
75
104
  interface AuthConfig {
76
105
  getToken?: () => string | null;
77
106
  getOrgId?: () => string | null;
107
+ /** Custom auth header name. Used when authMode is 'header'. Default: 'x-api-key' */
108
+ headerName?: string;
78
109
  }
79
110
  /**
80
111
  * Configure auth context for automatic token/orgId injection.
81
112
  * When configured, hooks auto-inject these values so you don't need to pass them manually.
82
113
  *
114
+ * **SSR safety:** This sets module-level state. Call only in client-side code.
115
+ *
83
116
  * @example
84
117
  * // Cookie auth (no token needed)
85
118
  * configureAuth({ getOrgId: () => currentOrg.id });
@@ -108,14 +141,8 @@ interface ApiRequestOptions {
108
141
  tags?: string[];
109
142
  cache?: RequestCache;
110
143
  signal?: AbortSignal;
111
- }
112
- interface BlobResponse {
113
- data: Blob;
114
- response: Response;
115
- }
116
- interface TextResponse {
117
- data: string;
118
- response: Response;
144
+ /** Explicit idempotency key for this request. Sent as `Idempotency-Key` header. */
145
+ idempotencyKey?: string;
119
146
  }
120
147
  interface ArcClientConfig extends ClientConfig {
121
148
  toast?: ToastHandler;
@@ -168,5 +195,4 @@ declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: str
168
195
  */
169
196
  declare function createQueryString<T extends Record<string, unknown>>(params?: T): string;
170
197
  //#endregion
171
- export { ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, AuthConfig, BlobResponse, ClientConfig, HttpMethod, TextResponse, ToastHandler, UseRouterHook, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError };
172
- //# sourceMappingURL=client.d.ts.map
198
+ export { ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, AuthConfig, ClientConfig, HttpMethod, ToastHandler, UseRouterHook, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError, isAutoIdempotency };
package/dist/client.js CHANGED
@@ -43,6 +43,10 @@ let clientConfig = null;
43
43
  /**
44
44
  * Configure the API client. Call once at app init before any API requests.
45
45
  *
46
+ * **SSR safety:** This sets module-level state. In Next.js, call this only in
47
+ * client-side code (e.g. a `"use client"` provider or `useEffect`).
48
+ * Calling on the server risks leaking state between requests.
49
+ *
46
50
  * @example
47
51
  * // Bearer token auth (default)
48
52
  * configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! });
@@ -51,6 +55,7 @@ let clientConfig = null;
51
55
  * configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL!, authMode: 'cookie' });
52
56
  */
53
57
  function configureClient(config) {
58
+ if (typeof window === "undefined") console.warn("[arc-next] configureClient() called on the server. This sets module-level state that persists across requests. Call only in client-side code (e.g., a 'use client' provider).");
54
59
  clientConfig = config;
55
60
  }
56
61
  /**
@@ -59,11 +64,17 @@ function configureClient(config) {
59
64
  function getAuthMode() {
60
65
  return clientConfig?.authMode ?? "bearer";
61
66
  }
67
+ /** Whether auto-idempotency is enabled on the global client. */
68
+ function isAutoIdempotency() {
69
+ return clientConfig?.autoIdempotency ?? false;
70
+ }
62
71
  let authConfig = null;
63
72
  /**
64
73
  * Configure auth context for automatic token/orgId injection.
65
74
  * When configured, hooks auto-inject these values so you don't need to pass them manually.
66
75
  *
76
+ * **SSR safety:** This sets module-level state. Call only in client-side code.
77
+ *
67
78
  * @example
68
79
  * // Cookie auth (no token needed)
69
80
  * configureAuth({ getOrgId: () => currentOrg.id });
@@ -75,6 +86,7 @@ let authConfig = null;
75
86
  * });
76
87
  */
77
88
  function configureAuth(config) {
89
+ if (typeof window === "undefined") console.warn("[arc-next] configureAuth() called on the server. This sets module-level state that persists across requests. Call only in client-side code (e.g., a 'use client' provider).");
78
90
  authConfig = config;
79
91
  }
80
92
  /**
@@ -109,23 +121,29 @@ function createClient(config) {
109
121
  };
110
122
  }
111
123
  async function executeRequest(config, method, endpoint, options = {}) {
112
- const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal } = options;
124
+ const { body, token, organizationId, revalidate, headerOptions, tags, cache, signal, idempotencyKey } = options;
113
125
  try {
114
126
  let headers = {
115
127
  ...organizationId ? { "x-organization-id": organizationId } : {},
116
128
  ...config.defaultHeaders ?? {}
117
129
  };
118
130
  if (config.internalApiKey) headers["x-internal-api-key"] = config.internalApiKey;
119
- if (token) headers["Authorization"] = `Bearer ${token}`;
131
+ if (token) if (config.authMode === "header") {
132
+ const headerName = authConfig?.headerName ?? "x-api-key";
133
+ headers[headerName] = token;
134
+ } else headers["Authorization"] = `Bearer ${token}`;
135
+ if (config.apiVersion) headers["Accept-Version"] = config.apiVersion;
136
+ if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
120
137
  if (body !== void 0 && body !== null && !(body instanceof FormData)) headers["Content-Type"] = "application/json";
121
138
  if (headerOptions) headers = {
122
139
  ...headers,
123
140
  ...headerOptions
124
141
  };
142
+ const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
125
143
  const fetchOptions = {
126
144
  method,
127
145
  headers,
128
- credentials: "include",
146
+ credentials,
129
147
  ...signal ? { signal } : {}
130
148
  };
131
149
  if (body !== void 0 && body !== null) fetchOptions.body = body instanceof FormData ? body : JSON.stringify(body);
@@ -177,8 +195,8 @@ async function executeRequest(config, method, endpoint, options = {}) {
177
195
  }
178
196
  return data;
179
197
  } catch (error) {
180
- if (error instanceof ArcApiError) throw error;
181
- throw new Error(error instanceof Error ? error.message : "An error occurred while fetching data.");
198
+ if (error instanceof Error) throw error;
199
+ throw new Error("An error occurred while fetching data.");
182
200
  }
183
201
  }
184
202
  /**
@@ -238,5 +256,4 @@ function createQueryString(params = {}) {
238
256
  }
239
257
 
240
258
  //#endregion
241
- export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError };
242
- //# sourceMappingURL=client.js.map
259
+ export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError, isAutoIdempotency };
package/dist/hooks.d.ts CHANGED
@@ -1,84 +1,46 @@
1
1
  import { ArcClient, UseRouterHook } from "./client.js";
2
- import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
2
+ import { BaseApi, FilterOperator } from "./api.js";
3
3
  import { CacheUtils, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, QueryKeys } from "./query.js";
4
+ import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
4
5
  import { QueryKey } from "@tanstack/react-query";
5
6
 
6
7
  //#region src/hooks.d.ts
7
- interface CrudApi<T, TCreate = Partial<T>, TUpdate = Partial<T>> {
8
- /** Scope from BaseApi used to auto-detect platformScoped */
9
- scope?: 'tenant' | 'platform';
10
- getAll: (options: {
11
- token?: string | null;
12
- organizationId?: string | null;
13
- params?: Record<string, unknown>;
14
- options?: {
15
- signal?: AbortSignal;
16
- [key: string]: unknown;
17
- };
18
- }) => Promise<unknown>;
19
- getById: (options: {
20
- id: string;
21
- token?: string | null;
22
- organizationId?: string | null;
23
- params?: {
24
- select?: string;
25
- populate?: string | string[];
26
- };
27
- options?: {
28
- signal?: AbortSignal;
29
- [key: string]: unknown;
30
- };
31
- }) => Promise<unknown>;
32
- create: (options: {
33
- token?: string | null;
34
- organizationId?: string | null;
35
- data: TCreate;
36
- }) => Promise<unknown>;
37
- update: (options: {
38
- token?: string | null;
39
- organizationId?: string | null;
40
- id: string;
41
- data: TUpdate;
42
- }) => Promise<unknown>;
43
- delete: (options: {
44
- token?: string | null;
45
- organizationId?: string | null;
46
- id: string;
47
- }) => Promise<unknown>;
48
- upload?: (options: {
49
- token?: string | null;
50
- organizationId?: string | null;
51
- data: FormData;
52
- id?: string;
53
- path?: string;
54
- }) => Promise<unknown>;
55
- search?: (options: {
56
- token?: string | null;
57
- organizationId?: string | null;
58
- params?: Record<string, unknown>;
59
- options?: {
60
- signal?: AbortSignal;
61
- [key: string]: unknown;
62
- };
63
- }) => Promise<unknown>;
64
- }
8
+ /**
9
+ * CRUD API interface accepted by createCrudHooks.
10
+ *
11
+ * Derived from BaseApi via Pick so the types are always in sync.
12
+ * BaseApi instances satisfy this exactly (same source of truth).
13
+ * Custom implementations just need to match BaseApi's method signatures.
14
+ */
15
+ type CrudApi<T = unknown, TCreate = Partial<T>, TUpdate = Partial<T>> = Pick<BaseApi<T, TCreate, TUpdate>, 'getAll' | 'getById' | 'create' | 'update' | 'delete'> & {
16
+ upload?: BaseApi<T, TCreate, TUpdate>['upload'];
17
+ search?: BaseApi<T, TCreate, TUpdate>['search'];
18
+ getDeleted?: BaseApi<T, TCreate, TUpdate>['getDeleted'];
19
+ restore?: BaseApi<T, TCreate, TUpdate>['restore'];
20
+ bulkCreate?: BaseApi<T, TCreate, TUpdate>['bulkCreate'];
21
+ bulkUpdate?: BaseApi<T, TCreate, TUpdate>['bulkUpdate'];
22
+ bulkDelete?: BaseApi<T, TCreate, TUpdate>['bulkDelete'];
23
+ getBySlug?: BaseApi<T, TCreate, TUpdate>['getBySlug'];
24
+ getTree?: BaseApi<T, TCreate, TUpdate>['getTree'];
25
+ getChildren?: BaseApi<T, TCreate, TUpdate>['getChildren'];
26
+ findBy?: BaseApi<T, TCreate, TUpdate>['findBy'];
27
+ };
65
28
  interface CrudHooksConfig<T, TCreate = Partial<T>, TUpdate = Partial<T>> {
66
29
  api: CrudApi<T, TCreate, TUpdate>;
67
30
  entityKey: string;
68
31
  singular: string;
69
32
  plural?: string;
70
33
  /**
71
- * Platform-scoped hooks skip automatic organizationId injection.
34
+ * Primary key field used to extract item IDs from response data.
35
+ * Used for cache key resolution, optimistic updates, and detail cache prefill.
72
36
  *
73
- * Use this for admin/superadmin hooks that query across all orgs
74
- * (e.g., APIs configured with `x-arc-scope: platform`).
37
+ * Default lookup order: `_id` `id`.
38
+ * Set this when your resource uses a custom ID field (e.g., `'sku'`, `'slug'`, `'code'`).
75
39
  *
76
- * When true, the hooks will NOT auto-inject the user's active
77
- * organizationId into requests the API's own headers control scoping.
78
- *
79
- * @default false
40
+ * @example
41
+ * createCrudHooks({ idField: 'sku', ... }) // GET /products/:sku
80
42
  */
81
- platformScoped?: boolean;
43
+ idField?: string;
82
44
  defaults?: {
83
45
  staleTime?: number;
84
46
  gcTime?: number;
@@ -130,11 +92,35 @@ interface CrudActions<T, TCreate, TUpdate> {
130
92
  create: (params: MutationParams<TCreate>, options?: CallOptions<T>) => Promise<T>;
131
93
  update: (params: UpdateParams<TUpdate>, options?: CallOptions<T>) => Promise<T>;
132
94
  remove: (params: DeleteParams, options?: CallOptions) => Promise<unknown>;
95
+ /** Restore a soft-deleted item. Only available when backend has softDelete preset. */
96
+ restore: (params: DeleteParams, options?: CallOptions<T>) => Promise<T>;
133
97
  isCreating: boolean;
134
98
  isUpdating: boolean;
135
99
  isDeleting: boolean;
100
+ isRestoring: boolean;
136
101
  isMutating: boolean;
137
102
  }
103
+ interface BulkActions<T, TCreate> {
104
+ bulkCreate: (params: {
105
+ data: TCreate[];
106
+ token?: string | null;
107
+ organizationId?: string | null;
108
+ }, options?: CallOptions<T[]>) => Promise<T[]>;
109
+ bulkUpdate: (params: {
110
+ filter: Record<string, unknown>;
111
+ data: Partial<T>;
112
+ token?: string | null;
113
+ organizationId?: string | null;
114
+ }, options?: CallOptions) => Promise<unknown>;
115
+ bulkRemove: (params: {
116
+ filter: Record<string, unknown>;
117
+ token?: string | null;
118
+ organizationId?: string | null;
119
+ }, options?: CallOptions) => Promise<unknown>;
120
+ isBulkCreating: boolean;
121
+ isBulkUpdating: boolean;
122
+ isBulkDeleting: boolean;
123
+ }
138
124
  interface NavigationOptions {
139
125
  scroll?: boolean;
140
126
  replace?: boolean;
@@ -156,6 +142,14 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
156
142
  (token: string | null, params?: Record<string, unknown>, options?: InfiniteListQueryOptions): InfiniteListQueryResult<T>;
157
143
  };
158
144
  useActions: () => CrudActions<T, TCreate, TUpdate>;
145
+ useBulkActions: () => BulkActions<T, TCreate>;
146
+ useDeleted: (params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
147
+ useDetailBySlug: (slug: string | null, options?: DetailQueryOptions<T>) => DetailQueryResult<T>;
148
+ useTree: (params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
149
+ useChildren: (parentId: string | null, params?: Record<string, unknown>, options?: ListQueryOptions<T>) => ListQueryResult<T>;
150
+ useFindBy: (field: string, value: unknown, options?: ListQueryOptions<T> & {
151
+ operator?: FilterOperator;
152
+ }) => ListQueryResult<T>;
159
153
  useUpload: (options?: {
160
154
  invalidateQueries?: QueryKey[];
161
155
  messages?: MutationMessages;
@@ -181,6 +175,8 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
181
175
  /**
182
176
  * Configure the router hook for useNavigation. Call once at app init.
183
177
  *
178
+ * **SSR safety:** This sets module-level state. Call only in client-side code.
179
+ *
184
180
  * @example
185
181
  * import { useRouter } from "next/navigation";
186
182
  * configureNavigation(useRouter);
@@ -190,11 +186,11 @@ declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>(
190
186
  api,
191
187
  entityKey,
192
188
  singular,
193
- platformScoped,
189
+ plural,
190
+ idField,
194
191
  defaults,
195
192
  callbacks,
196
193
  client
197
194
  }: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
198
195
  //#endregion
199
- export { CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, type UseRouterHook, configureNavigation, createCrudHooks };
200
- //# sourceMappingURL=hooks.d.ts.map
196
+ export { BulkActions, CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };