@classytic/arc-next 0.2.0 → 0.3.1

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 CHANGED
@@ -30,13 +30,13 @@ import { useRouter } from "next/navigation";
30
30
  // Required — sets the API base URL and auth mode
31
31
  configureClient({
32
32
  baseUrl: process.env.NEXT_PUBLIC_API_URL!,
33
- authMode: "cookie", // 'cookie' for Better Auth, 'bearer' for token auth (default)
34
- internalApiKey: process.env.NEXT_PUBLIC_INTERNAL_API_KEY, // optional
33
+ authMode: "cookie", // 'cookie' for Better Auth, 'bearer' for token auth (default)
34
+ // credentials: 'omit', // override if you don't want cookies sent cross-origin
35
35
  });
36
36
 
37
- // Optional — auto-inject org context into queries/mutations
37
+ // Optional — auto-inject tenant context into queries/mutations
38
38
  configureAuth({
39
- getOrgId: () => activeOrgId, // return current org ID
39
+ getOrgId: () => activeTenantId, // return current tenant/org/workspace ID
40
40
  getToken: () => null, // null for cookie auth (token only for bearer)
41
41
  });
42
42
 
@@ -51,10 +51,11 @@ configureNavigation(useRouter);
51
51
 
52
52
  | Import | Purpose | `"use client"` |
53
53
  | ----------------------------------- | ---------------------------------------------------------------------------- | :-------------: |
54
+ | `@classytic/arc-next` | Root — same as `/hooks` (`createCrudHooks`, `configureNavigation`) | Yes |
54
55
  | `@classytic/arc-next/client` | `configureClient`, `configureAuth`, `createClient`, `handleApiRequest`, `createQueryString`, `ArcApiError`, `isArcApiError`, `getAuthMode`, `getAuthContext` | No |
55
56
  | `@classytic/arc-next/api` | `BaseApi`, `createCrudApi`, response types, type guards | No |
56
- | `@classytic/arc-next/query` | `createQueryKeys`, `createCacheUtils`, `createListQuery`, `createDetailQuery`| Yes |
57
- | `@classytic/arc-next/mutation` | `configureToast`, `useMutationWithTransition`, `createOptimisticMutation` | Yes |
57
+ | `@classytic/arc-next/query` | `createQueryKeys`, `createCacheUtils`, `useListQuery`, `useDetailQuery` | Yes |
58
+ | `@classytic/arc-next/mutation` | `configureToast`, `useMutationWithTransition`, `useOptimisticMutation` | Yes |
58
59
  | `@classytic/arc-next/hooks` | `createCrudHooks`, `configureNavigation` | Yes |
59
60
  | `@classytic/arc-next/query-client` | `getQueryClient` (SSR-safe singleton) | No |
60
61
  | `@classytic/arc-next/prefetch` | `createCrudPrefetcher`, `dehydrate` (SSR prefetch) | No |
@@ -148,13 +149,15 @@ export function ProductsPage() {
148
149
  configureClient({
149
150
  baseUrl: string; // Required — API base URL
150
151
  authMode?: 'cookie' | 'bearer'; // Default: 'bearer'
152
+ credentials?: RequestCredentials; // Default: derived from authMode
151
153
  internalApiKey?: string; // Optional — sent as x-internal-api-key header
152
154
  defaultHeaders?: Record<string, string>; // Optional — merged into every request
153
155
  });
154
156
  ```
155
157
 
156
- - `authMode: 'bearer'` (default) — requires a token for authenticated requests; queries are disabled until a token is provided
157
- - `authMode: 'cookie'` — auth via HTTP-only cookies (e.g. Better Auth); queries are always enabled, no token needed
158
+ - `authMode: 'bearer'` (default) — requires a token; queries disabled until token provided; `credentials: 'same-origin'`
159
+ - `authMode: 'cookie'` — HTTP-only cookies (e.g. Better Auth); queries always enabled; `credentials: 'include'`
160
+ - `credentials` — explicit override: `'include'` (send cookies cross-origin), `'same-origin'` (same-origin only), `'omit'` (never send cookies)
158
161
 
159
162
  Must be called before any API requests. Throws if not configured.
160
163
 
@@ -167,7 +170,7 @@ configureAuth({
167
170
  });
168
171
  ```
169
172
 
170
- Auto-injects `token` and `organizationId` into queries/mutations. Hooks use the new signature (no explicit token param) — legacy signature still works.
173
+ Auto-injects `token` and tenant ID (sent as `x-organization-id` header) into queries/mutations. The header name is a convention — your backend controls how it's read and which field it maps to (`organizationId`, `workspaceId`, `teamId`, etc.). Hooks use the new signature (no explicit token param) — legacy signature still works.
171
174
 
172
175
  ### `handleApiRequest<T>(method, endpoint, options?)`
173
176
 
@@ -226,7 +229,7 @@ const api = createCrudApi<Product, CreateProduct>("products", {
226
229
 
227
230
  ### `createCrudHooks<T, TCreate, TUpdate>(config)`
228
231
 
229
- Factory that returns everything you need:
232
+ Factory that returns everything you need. The `api` parameter accepts any `createCrudApi()` result directly — no casts needed. Types are derived from `BaseApi` via `Pick`, so generics thread through automatically:
230
233
 
231
234
  ```ts
232
235
  const {
@@ -235,7 +238,7 @@ const {
235
238
  useActions, useUpload, useSearch, useCustomMutation,
236
239
  useNavigation,
237
240
  } = createCrudHooks<Product, CreateProduct>({
238
- api: productsApi, // from createCrudApi()
241
+ api: productsApi, // from createCrudApi() — types inferred, no cast
239
242
  entityKey: "products", // TanStack Query key prefix
240
243
  singular: "Product", // for toast messages
241
244
  defaults: { // optional
@@ -263,7 +266,7 @@ const { items, pagination, isLoading, isFetching, refetch } = useList(
263
266
  );
264
267
  ```
265
268
 
266
- - Auto-scopes query keys by `organizationId` (tenant vs super-admin)
269
+ - Auto-scopes list query keys by tenant context (when present → `tenant` scope, otherwise → `super-admin`)
267
270
  - Normalizes pagination from `docs`/`data`/`items`/`results` formats
268
271
  - Prefills detail cache from list results (skips re-fetch on navigate)
269
272
  - `options.public: true` — enables query without token
@@ -534,7 +537,7 @@ useProducts(token, {}, { ...QUERY_CONFIGS.realtime });
534
537
 
535
538
  ### `updateListCache(listData, updater)`
536
539
 
537
- Transforms list cache regardless of format (`docs[]`, `data[]`, `items[]`, `results[]`, or raw array).
540
+ Transforms list cache regardless of format — well-known keys (`docs[]`, `data[]`, `items[]`, `results[]`), custom keys (`products[]`, `users[]`, etc.), or raw arrays.
538
541
  Automatically adjusts `total`/`totalDocs` counts when items are added or removed (optimistic add/delete).
539
542
 
540
543
  ```ts
@@ -551,11 +554,11 @@ Extracts `_id` or `id` from any item. Returns `string | null`.
551
554
 
552
555
  ### `normalizePagination(data)`
553
556
 
554
- Converts any pagination response format to a normalized `PaginationData` object. Handles `total`/`totalDocs`, `pages`/`totalPages`, `page`/`currentPage`, `hasNext`/`hasNextPage`/`hasMore`, `hasPrev`/`hasPrevPage`.
557
+ Converts any pagination response format to a normalized `PaginationData` object. Detects pagination method (`offset`, `keyset`, `aggregate`) and normalizes all fields: `total`/`totalDocs`, `pages`/`totalPages`, `page`/`currentPage`, `hasNext`/`hasNextPage`/`hasMore`, `hasPrev`/`hasPrevPage`, `next` (keyset cursor).
555
558
 
556
559
  ### `extractItems<T>(data)`
557
560
 
558
- Extracts the items array from any response format looks for `docs`, `data`, `items`, `results` fields, or returns the data directly if it's already an array.
561
+ Extracts the items array from any response format. Checks well-known keys first (`docs`, `data`, `items`, `results`), then falls back to finding the first top-level array so `{ products: [...] }` or `{ users: [...] }` works without configuration.
559
562
 
560
563
  ## Multi-Client (Multiple APIs)
561
564
 
@@ -654,8 +657,10 @@ try {
654
657
  ### Multi-tenant data fetching
655
658
 
656
659
  ```ts
657
- // organizationId in params → scoped query key → isolated cache per tenant
658
- const { items } = useProducts(token, { organizationId: currentOrg });
660
+ // Tenant ID in params → scoped query key → isolated cache per tenant
661
+ // The param name is up to you arc-next sends it as x-organization-id header,
662
+ // your backend maps it to whatever tenant field your schema uses.
663
+ const { items } = useProducts(token, { organizationId: currentTenantId });
659
664
  ```
660
665
 
661
666
  ### Public endpoints (no auth)
@@ -704,11 +709,11 @@ const adminApi = createCrudApi("users", {
704
709
 
705
710
  - **CRUD Factory** — `createCrudApi` + `createCrudHooks` generates typed API clients and React Query hooks
706
711
  - **Optimistic Updates** — Create, update, delete with instant UI feedback and automatic rollback
707
- - **Multi-Tenant Scoping** — `organizationId` in headers + scoped query keys
708
- - **Pagination Normalization** — Handles `docs`/`data`/`items`/`results` response formats, offset/keyset/aggregate pagination
712
+ - **Multi-Tenant Scoping** — Tenant ID sent via `x-organization-id` header + scoped list query keys. Backend controls the tenant field name and access enforcement.
713
+ - **Pagination Normalization** — Handles `docs`/`data`/`items`/`results` + any custom key, offset/keyset/aggregate pagination
709
714
  - **Detail Cache Prefilling** — List results auto-populate detail query cache
710
715
  - **React 19 Transitions** — `useMutationWithTransition` wraps invalidation in `startTransition`
711
- - **Cookie & Bearer Auth** — `authMode: 'cookie'` for Better Auth, `'bearer'` for token auth
716
+ - **Cookie & Bearer Auth** — `authMode: 'cookie'` for Better Auth, `'bearer'` for token auth, configurable `credentials` policy
712
717
  - **SSR Prefetch** — `createCrudPrefetcher` + `dehydrate` for server component data loading
713
718
  - **Multi-Client** — `createClient()` for multiple API backends side by side
714
719
  - **Pluggable Toast** — `configureToast()` — use sonner, react-hot-toast, or anything
package/dist/api.d.ts CHANGED
@@ -225,5 +225,4 @@ declare function isOffsetPagination<T>(response: PaginatedResponse<T>): response
225
225
  declare function isKeysetPagination<T>(response: PaginatedResponse<T>): response is KeysetPaginationResponse<T>;
226
226
  declare function isAggregatePagination<T>(response: PaginatedResponse<T>): response is AggregatePaginationResponse<T>;
227
227
  //#endregion
228
- export { AggregatePaginationResponse, ApiResponse, BaseApi, BaseApiConfig, DeleteResponse, ExtractDoc, FilterOperator, KeysetPaginationResponse, OffsetPaginationResponse, PaginatedResponse, PopulateOption, QueryParams, RequestOptions, SortDirection, SortSpec, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
229
- //# sourceMappingURL=api.d.ts.map
228
+ export { AggregatePaginationResponse, ApiResponse, BaseApi, BaseApiConfig, DeleteResponse, ExtractDoc, FilterOperator, KeysetPaginationResponse, OffsetPaginationResponse, PaginatedResponse, PopulateOption, QueryParams, RequestOptions, SortDirection, SortSpec, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
package/dist/api.js CHANGED
@@ -57,7 +57,11 @@ var BaseApi = class {
57
57
  return result;
58
58
  }
59
59
  async getAll({ token = null, organizationId = null, params = {}, options = {} } = {}) {
60
- const processedParams = this.prepareParams(params);
60
+ const mergedParams = {
61
+ ...this.config.defaultParams,
62
+ ...params
63
+ };
64
+ const processedParams = this.prepareParams(mergedParams);
61
65
  const queryString = this.createQueryString(processedParams);
62
66
  const requestOptions = {
63
67
  cache: this.config.cache,
@@ -118,6 +122,7 @@ var BaseApi = class {
118
122
  }
119
123
  async search({ token = null, organizationId = null, searchParams = {}, params = {}, options = {} } = {}) {
120
124
  const queryParams = {
125
+ ...this.config.defaultParams,
121
126
  ...params,
122
127
  ...searchParams
123
128
  };
@@ -133,7 +138,10 @@ var BaseApi = class {
133
138
  }
134
139
  async findBy({ token = null, organizationId = null, field, value, operator, params = {}, options = {} }) {
135
140
  if (!field || value === void 0) throw new Error("Field and value are required");
136
- const queryParams = { ...params };
141
+ const queryParams = {
142
+ ...this.config.defaultParams,
143
+ ...params
144
+ };
137
145
  if (operator) queryParams[`${field}[${operator}]`] = Array.isArray(value) ? value.join(",") : value;
138
146
  else queryParams[field] = value;
139
147
  const processedParams = this.prepareParams(queryParams);
@@ -176,5 +184,4 @@ function isAggregatePagination(response) {
176
184
  }
177
185
 
178
186
  //#endregion
179
- export { BaseApi, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
180
- //# sourceMappingURL=api.js.map
187
+ export { BaseApi, createCrudApi, isAggregatePagination, isKeysetPagination, isOffsetPagination };
package/dist/client.d.ts CHANGED
@@ -56,10 +56,25 @@ interface ClientConfig {
56
56
  * - 'cookie': Auth is handled via HTTP-only cookies (e.g. Better Auth). Queries are always enabled — no token needed.
57
57
  */
58
58
  authMode?: 'bearer' | 'cookie';
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;
59
70
  }
60
71
  /**
61
72
  * Configure the API client. Call once at app init before any API requests.
62
73
  *
74
+ * **SSR safety:** This sets module-level state. In Next.js, call this only in
75
+ * client-side code (e.g. a `"use client"` provider or `useEffect`).
76
+ * Calling on the server risks leaking state between requests.
77
+ *
63
78
  * @example
64
79
  * // Bearer token auth (default)
65
80
  * configureClient({ baseUrl: process.env.NEXT_PUBLIC_API_URL! });
@@ -80,6 +95,8 @@ interface AuthConfig {
80
95
  * Configure auth context for automatic token/orgId injection.
81
96
  * When configured, hooks auto-inject these values so you don't need to pass them manually.
82
97
  *
98
+ * **SSR safety:** This sets module-level state. Call only in client-side code.
99
+ *
83
100
  * @example
84
101
  * // Cookie auth (no token needed)
85
102
  * configureAuth({ getOrgId: () => currentOrg.id });
@@ -168,5 +185,4 @@ declare function handleApiRequest<T = unknown>(method: HttpMethod, endpoint: str
168
185
  */
169
186
  declare function createQueryString<T extends Record<string, unknown>>(params?: T): string;
170
187
  //#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
188
+ export { ApiRequestOptions, ArcApiError, ArcApiErrorOptions, ArcClient, ArcClientConfig, AuthConfig, BlobResponse, ClientConfig, HttpMethod, TextResponse, ToastHandler, UseRouterHook, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError };
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! });
@@ -64,6 +68,8 @@ let authConfig = null;
64
68
  * Configure auth context for automatic token/orgId injection.
65
69
  * When configured, hooks auto-inject these values so you don't need to pass them manually.
66
70
  *
71
+ * **SSR safety:** This sets module-level state. Call only in client-side code.
72
+ *
67
73
  * @example
68
74
  * // Cookie auth (no token needed)
69
75
  * configureAuth({ getOrgId: () => currentOrg.id });
@@ -122,10 +128,11 @@ async function executeRequest(config, method, endpoint, options = {}) {
122
128
  ...headers,
123
129
  ...headerOptions
124
130
  };
131
+ const credentials = config.credentials ?? (config.authMode === "cookie" ? "include" : "same-origin");
125
132
  const fetchOptions = {
126
133
  method,
127
134
  headers,
128
- credentials: "include",
135
+ credentials,
129
136
  ...signal ? { signal } : {}
130
137
  };
131
138
  if (body !== void 0 && body !== null) fetchOptions.body = body instanceof FormData ? body : JSON.stringify(body);
@@ -177,8 +184,8 @@ async function executeRequest(config, method, endpoint, options = {}) {
177
184
  }
178
185
  return data;
179
186
  } catch (error) {
180
- if (error instanceof ArcApiError) throw error;
181
- throw new Error(error instanceof Error ? error.message : "An error occurred while fetching data.");
187
+ if (error instanceof Error) throw error;
188
+ throw new Error("An error occurred while fetching data.");
182
189
  }
183
190
  }
184
191
  /**
@@ -238,5 +245,4 @@ function createQueryString(params = {}) {
238
245
  }
239
246
 
240
247
  //#endregion
241
- export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError };
242
- //# sourceMappingURL=client.js.map
248
+ export { ArcApiError, configureAuth, configureClient, createClient, createQueryString, getAuthContext, getAuthMode, handleApiRequest, isArcApiError };
package/dist/hooks.d.ts CHANGED
@@ -1,65 +1,21 @@
1
1
  import { ArcClient, UseRouterHook } from "./client.js";
2
+ import { BaseApi } from "./api.js";
2
3
  import { MutationCallbacks, MutationMessages, TransitionMutationReturn } from "./mutation.js";
3
4
  import { CacheUtils, DetailQueryOptions, DetailQueryResult, InfiniteListQueryOptions, InfiniteListQueryResult, ListQueryOptions, ListQueryResult, QueryKeys } from "./query.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
- getAll: (options: {
9
- token?: string | null;
10
- organizationId?: string | null;
11
- params?: Record<string, unknown>;
12
- options?: {
13
- signal?: AbortSignal;
14
- [key: string]: unknown;
15
- };
16
- }) => Promise<unknown>;
17
- getById: (options: {
18
- id: string;
19
- token?: string | null;
20
- organizationId?: string | null;
21
- params?: {
22
- select?: string;
23
- populate?: string | string[];
24
- };
25
- options?: {
26
- signal?: AbortSignal;
27
- [key: string]: unknown;
28
- };
29
- }) => Promise<unknown>;
30
- create: (options: {
31
- token?: string | null;
32
- organizationId?: string | null;
33
- data: TCreate;
34
- }) => Promise<unknown>;
35
- update: (options: {
36
- token?: string | null;
37
- organizationId?: string | null;
38
- id: string;
39
- data: TUpdate;
40
- }) => Promise<unknown>;
41
- delete: (options: {
42
- token?: string | null;
43
- organizationId?: string | null;
44
- id: string;
45
- }) => Promise<unknown>;
46
- upload?: (options: {
47
- token?: string | null;
48
- organizationId?: string | null;
49
- data: FormData;
50
- id?: string;
51
- path?: string;
52
- }) => Promise<unknown>;
53
- search?: (options: {
54
- token?: string | null;
55
- organizationId?: string | null;
56
- params?: Record<string, unknown>;
57
- options?: {
58
- signal?: AbortSignal;
59
- [key: string]: unknown;
60
- };
61
- }) => Promise<unknown>;
62
- }
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
+ };
63
19
  interface CrudHooksConfig<T, TCreate = Partial<T>, TUpdate = Partial<T>> {
64
20
  api: CrudApi<T, TCreate, TUpdate>;
65
21
  entityKey: string;
@@ -167,6 +123,8 @@ interface CrudHooksReturn<T, TCreate, TUpdate> {
167
123
  /**
168
124
  * Configure the router hook for useNavigation. Call once at app init.
169
125
  *
126
+ * **SSR safety:** This sets module-level state. Call only in client-side code.
127
+ *
170
128
  * @example
171
129
  * import { useRouter } from "next/navigation";
172
130
  * configureNavigation(useRouter);
@@ -181,5 +139,4 @@ declare function createCrudHooks<T, TCreate = Partial<T>, TUpdate = Partial<T>>(
181
139
  client
182
140
  }: CrudHooksConfig<T, TCreate, TUpdate>): CrudHooksReturn<T, TCreate, TUpdate>;
183
141
  //#endregion
184
- export { CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, type UseRouterHook, configureNavigation, createCrudHooks };
185
- //# sourceMappingURL=hooks.d.ts.map
142
+ export { CallOptions, CrudActions, CrudApi, CrudHooksConfig, CrudHooksReturn, DeleteParams, MutationParams, NavigateFn, NavigationOptions, UpdateParams, configureNavigation, createCrudHooks };