@ulfblk/admin 0.1.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 ADDED
@@ -0,0 +1,70 @@
1
+ # @ulfblk/admin
2
+
3
+ React Admin DataProvider and AuthProvider for the ulfblk ecosystem.
4
+
5
+ ## Installation
6
+
7
+ ```bash
8
+ pnpm add @ulfblk/admin react-admin
9
+ ```
10
+
11
+ ## Quick Start
12
+
13
+ ```tsx
14
+ import { Admin, Resource, ListGuesser, EditGuesser } from "react-admin";
15
+ import { BloqueClient } from "@ulfblk/api-client";
16
+ import { createDataProvider, createAuthProvider } from "@ulfblk/admin";
17
+
18
+ const client = new BloqueClient({ baseUrl: "http://localhost:8000" });
19
+ const dataProvider = createDataProvider(client);
20
+ const authProvider = createAuthProvider(client);
21
+
22
+ function App() {
23
+ return (
24
+ <Admin dataProvider={dataProvider} authProvider={authProvider}>
25
+ <Resource name="users" list={ListGuesser} edit={EditGuesser} />
26
+ <Resource name="orders" list={ListGuesser} edit={EditGuesser} />
27
+ </Admin>
28
+ );
29
+ }
30
+ ```
31
+
32
+ ## Features
33
+
34
+ - **DataProvider**: CRUD operations via BloqueClient with FastAPI-compatible query serialization (`qs` with `arrayFormat: 'repeat'`)
35
+ - **AuthProvider**: JWT login/logout with client-side token validation via `jwt-decode`
36
+ - **Tenant-aware**: BloqueClient's tenant interceptor handles tenant context automatically
37
+ - **Configurable**: Custom `basePath`, query serializer, login path
38
+ - **Error handling**: Maps HTTP errors to react-admin's `HttpError` (401 -> re-login, 403 -> access denied)
39
+ - **Batch operations**: `deleteMany`/`updateMany` execute in batches of 5 to avoid overwhelming the backend
40
+
41
+ ## Options
42
+
43
+ ```tsx
44
+ // Custom base path
45
+ const dataProvider = createDataProvider(client, { basePath: "/v1" });
46
+
47
+ // Custom login endpoint
48
+ const authProvider = createAuthProvider(client, { loginPath: "/auth/login" });
49
+
50
+ // Multi-tenant: switch tenants dynamically
51
+ client.setTenantId("acme");
52
+ // All subsequent requests include tenant context via interceptor
53
+ ```
54
+
55
+ ## Backend Requirements
56
+
57
+ The DataProvider expects standard REST endpoints:
58
+
59
+ | Method | Endpoint | react-admin method |
60
+ |--------|----------|-------------------|
61
+ | GET | `{basePath}/{resource}` | getList, getMany |
62
+ | GET | `{basePath}/{resource}/{id}` | getOne |
63
+ | POST | `{basePath}/{resource}` | create |
64
+ | PUT | `{basePath}/{resource}/{id}` | update |
65
+ | DELETE | `{basePath}/{resource}/{id}` | delete |
66
+
67
+ List responses must match `PaginatedResponse`:
68
+ ```json
69
+ { "items": [...], "total": 100 }
70
+ ```
@@ -0,0 +1,10 @@
1
+ /**
2
+ * React Admin AuthProvider using BloqueClient.
3
+ *
4
+ * Handles login/logout, token validation via jwt-decode,
5
+ * and permission extraction from JWT payload.
6
+ */
7
+ import type { AuthProvider } from "react-admin";
8
+ import type { BloqueClient } from "@ulfblk/api-client";
9
+ import type { AuthProviderOptions } from "./types.js";
10
+ export declare function createAuthProvider(client: BloqueClient, options?: AuthProviderOptions): AuthProvider;
@@ -0,0 +1,81 @@
1
+ /**
2
+ * React Admin AuthProvider using BloqueClient.
3
+ *
4
+ * Handles login/logout, token validation via jwt-decode,
5
+ * and permission extraction from JWT payload.
6
+ */
7
+ import { jwtDecode } from "jwt-decode";
8
+ function isTokenExpired(token) {
9
+ try {
10
+ const decoded = jwtDecode(token);
11
+ return decoded.exp * 1000 < Date.now();
12
+ }
13
+ catch {
14
+ return true;
15
+ }
16
+ }
17
+ function getPayload(token) {
18
+ try {
19
+ return jwtDecode(token);
20
+ }
21
+ catch {
22
+ return null;
23
+ }
24
+ }
25
+ export function createAuthProvider(client, options) {
26
+ const loginPath = options?.loginPath ?? "/auth/login";
27
+ let lastToken = null;
28
+ return {
29
+ login: async ({ username, password }) => {
30
+ const response = await client.post(loginPath, { email: username, password });
31
+ client.setTokens({
32
+ accessToken: response.access_token,
33
+ refreshToken: response.refresh_token,
34
+ });
35
+ lastToken = response.access_token;
36
+ },
37
+ logout: async () => {
38
+ client.logout();
39
+ lastToken = null;
40
+ },
41
+ checkAuth: async () => {
42
+ if (!lastToken || isTokenExpired(lastToken)) {
43
+ throw new Error("Token expired or missing");
44
+ }
45
+ },
46
+ checkError: async (error) => {
47
+ if (error.status === 401) {
48
+ client.logout();
49
+ lastToken = null;
50
+ throw new Error("Unauthorized");
51
+ }
52
+ // 403 = valid token but no permission - don't logout
53
+ if (error.status === 403) {
54
+ throw new Error("Forbidden");
55
+ }
56
+ },
57
+ getPermissions: async () => {
58
+ if (!lastToken)
59
+ return { roles: [], permissions: [] };
60
+ const payload = getPayload(lastToken);
61
+ if (!payload)
62
+ return { roles: [], permissions: [] };
63
+ return {
64
+ roles: payload.roles ?? [],
65
+ permissions: payload.permissions ?? [],
66
+ };
67
+ },
68
+ getIdentity: async () => {
69
+ if (!lastToken)
70
+ throw new Error("Not authenticated");
71
+ const payload = getPayload(lastToken);
72
+ if (!payload)
73
+ throw new Error("Invalid token");
74
+ return {
75
+ id: payload.sub,
76
+ fullName: payload.sub,
77
+ tenant: payload.tenant,
78
+ };
79
+ },
80
+ };
81
+ }
@@ -0,0 +1,13 @@
1
+ /**
2
+ * React Admin DataProvider using BloqueClient as HTTP transport.
3
+ *
4
+ * Translates react-admin CRUD calls to REST endpoints.
5
+ * Uses `qs` for query string serialization (FastAPI-compatible).
6
+ *
7
+ * Tenant context is handled automatically via BloqueClient's
8
+ * tenant interceptor (call client.setTenantId() to switch tenants).
9
+ */
10
+ import type { DataProvider } from "react-admin";
11
+ import type { BloqueClient } from "@ulfblk/api-client";
12
+ import type { DataProviderOptions } from "./types.js";
13
+ export declare function createDataProvider(client: BloqueClient, options?: DataProviderOptions): DataProvider;
@@ -0,0 +1,110 @@
1
+ /**
2
+ * React Admin DataProvider using BloqueClient as HTTP transport.
3
+ *
4
+ * Translates react-admin CRUD calls to REST endpoints.
5
+ * Uses `qs` for query string serialization (FastAPI-compatible).
6
+ *
7
+ * Tenant context is handled automatically via BloqueClient's
8
+ * tenant interceptor (call client.setTenantId() to switch tenants).
9
+ */
10
+ import { HttpError } from "react-admin";
11
+ import { stringify } from "qs";
12
+ const BATCH_SIZE = 5;
13
+ function defaultSerializer(params) {
14
+ return stringify(params, { arrayFormat: "repeat", skipNulls: true });
15
+ }
16
+ async function batchExecute(ids, fn) {
17
+ const results = [];
18
+ for (let i = 0; i < ids.length; i += BATCH_SIZE) {
19
+ const batch = ids.slice(i, i + BATCH_SIZE);
20
+ const batchResults = await Promise.all(batch.map(fn));
21
+ results.push(...batchResults);
22
+ }
23
+ return results;
24
+ }
25
+ export function createDataProvider(client, options) {
26
+ const base = (options?.basePath ?? "/api").replace(/\/+$/, "");
27
+ const serialize = options?.querySerializer ?? defaultSerializer;
28
+ async function request(method, path, body) {
29
+ try {
30
+ switch (method) {
31
+ case "GET":
32
+ return await client.get(path);
33
+ case "POST":
34
+ return await client.post(path, body);
35
+ case "PUT":
36
+ return await client.put(path, body);
37
+ case "DELETE":
38
+ return await client.delete(path);
39
+ default:
40
+ throw new Error(`Unsupported method: ${method}`);
41
+ }
42
+ }
43
+ catch (error) {
44
+ if (error instanceof Error && "status" in error) {
45
+ const e = error;
46
+ throw new HttpError(e.message, e.status, e.body);
47
+ }
48
+ throw error;
49
+ }
50
+ }
51
+ return {
52
+ getList: async (resource, params) => {
53
+ const { page, perPage } = params.pagination ?? { page: 1, perPage: 20 };
54
+ const { field, order } = params.sort ?? { field: "id", order: "ASC" };
55
+ const query = serialize({
56
+ page,
57
+ size: perPage,
58
+ sort: field,
59
+ order,
60
+ ...params.filter,
61
+ });
62
+ const url = `${base}/${resource}?${query}`;
63
+ const response = await request("GET", url);
64
+ return { data: response.items, total: response.total };
65
+ },
66
+ getOne: async (resource, params) => {
67
+ const data = await request("GET", `${base}/${resource}/${params.id}`);
68
+ return { data };
69
+ },
70
+ getMany: async (resource, params) => {
71
+ const query = serialize({ id: params.ids });
72
+ const response = await request("GET", `${base}/${resource}?${query}`);
73
+ return { data: response.items };
74
+ },
75
+ getManyReference: async (resource, params) => {
76
+ const { page, perPage } = params.pagination ?? { page: 1, perPage: 20 };
77
+ const { field, order } = params.sort ?? { field: "id", order: "ASC" };
78
+ const query = serialize({
79
+ page,
80
+ size: perPage,
81
+ sort: field,
82
+ order,
83
+ [params.target]: params.id,
84
+ ...params.filter,
85
+ });
86
+ const response = await request("GET", `${base}/${resource}?${query}`);
87
+ return { data: response.items, total: response.total };
88
+ },
89
+ create: async (resource, params) => {
90
+ const data = await request("POST", `${base}/${resource}`, params.data);
91
+ return { data };
92
+ },
93
+ update: async (resource, params) => {
94
+ const data = await request("PUT", `${base}/${resource}/${params.id}`, params.data);
95
+ return { data };
96
+ },
97
+ delete: async (resource, params) => {
98
+ const data = await request("DELETE", `${base}/${resource}/${params.id}`);
99
+ return { data };
100
+ },
101
+ deleteMany: async (resource, params) => {
102
+ await batchExecute(params.ids, (id) => request("DELETE", `${base}/${resource}/${id}`));
103
+ return { data: params.ids };
104
+ },
105
+ updateMany: async (resource, params) => {
106
+ await batchExecute(params.ids, (id) => request("PUT", `${base}/${resource}/${id}`, params.data));
107
+ return { data: params.ids };
108
+ },
109
+ };
110
+ }
@@ -0,0 +1,10 @@
1
+ /**
2
+ * @ulfblk/admin - React Admin providers for the ulfblk ecosystem.
3
+ *
4
+ * Provides DataProvider and AuthProvider that use BloqueClient
5
+ * as the HTTP transport layer, with automatic tenant context,
6
+ * JWT auth, and FastAPI-compatible query serialization.
7
+ */
8
+ export { createDataProvider } from "./data-provider.js";
9
+ export { createAuthProvider } from "./auth-provider.js";
10
+ export type { DataProviderOptions, AuthProviderOptions, ListResponse } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,9 @@
1
+ /**
2
+ * @ulfblk/admin - React Admin providers for the ulfblk ecosystem.
3
+ *
4
+ * Provides DataProvider and AuthProvider that use BloqueClient
5
+ * as the HTTP transport layer, with automatic tenant context,
6
+ * JWT auth, and FastAPI-compatible query serialization.
7
+ */
8
+ export { createDataProvider } from "./data-provider.js";
9
+ export { createAuthProvider } from "./auth-provider.js";
@@ -0,0 +1,30 @@
1
+ /**
2
+ * Configuration options for the ulfblk DataProvider.
3
+ */
4
+ export interface DataProviderOptions {
5
+ /** Base path for API endpoints. Default: "/api" */
6
+ basePath?: string;
7
+ /**
8
+ * Custom query string serializer.
9
+ * Default uses `qs` with arrayFormat: 'repeat' (FastAPI-compatible).
10
+ */
11
+ querySerializer?: (params: Record<string, unknown>) => string;
12
+ }
13
+ /**
14
+ * Configuration options for the ulfblk AuthProvider.
15
+ */
16
+ export interface AuthProviderOptions {
17
+ /** Path for the login endpoint. Default: "/auth/login" */
18
+ loginPath?: string;
19
+ }
20
+ /**
21
+ * Expected response format from list endpoints.
22
+ * Must match PaginatedResponse from the Python backend.
23
+ */
24
+ export interface ListResponse<T = Record<string, unknown>> {
25
+ items: T[];
26
+ total: number;
27
+ page?: number;
28
+ page_size?: number;
29
+ pages?: number;
30
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,50 @@
1
+ {
2
+ "name": "@ulfblk/admin",
3
+ "version": "0.1.0",
4
+ "description": "React Admin DataProvider and AuthProvider for the ulfblk ecosystem",
5
+ "license": "MIT",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "https://github.com/abelardodiaz/web25-991-bloques-reciclables",
9
+ "directory": "packages/typescript/ulfblk-admin"
10
+ },
11
+ "type": "module",
12
+ "main": "dist/index.js",
13
+ "types": "dist/index.d.ts",
14
+ "exports": {
15
+ ".": {
16
+ "types": "./dist/index.d.ts",
17
+ "import": "./dist/index.js"
18
+ }
19
+ },
20
+ "files": ["dist", "src", "README.md"],
21
+ "scripts": {
22
+ "build": "tsc",
23
+ "lint": "biome check src/",
24
+ "test": "vitest run"
25
+ },
26
+ "dependencies": {
27
+ "@ulfblk/api-client": "^0.1.0",
28
+ "@ulfblk/types": "^0.1.0",
29
+ "qs": "^6.14.0"
30
+ },
31
+ "devDependencies": {
32
+ "@types/qs": "^6.9.0",
33
+ "jwt-decode": "^4.0.0",
34
+ "react-admin": "^5.0.0",
35
+ "react": "^19.0.0",
36
+ "react-dom": "^19.0.0",
37
+ "typescript": "^5.7.0",
38
+ "vitest": "^3.0.0"
39
+ },
40
+ "peerDependencies": {
41
+ "react-admin": "^5.0.0",
42
+ "react": "^18 || ^19"
43
+ },
44
+ "peerDependenciesMeta": {
45
+ "react-dom": {
46
+ "optional": true
47
+ }
48
+ },
49
+ "keywords": ["react-admin", "admin", "data-provider", "auth-provider", "ulfblk", "saas"]
50
+ }
@@ -0,0 +1,103 @@
1
+ /**
2
+ * React Admin AuthProvider using BloqueClient.
3
+ *
4
+ * Handles login/logout, token validation via jwt-decode,
5
+ * and permission extraction from JWT payload.
6
+ */
7
+
8
+ import type { AuthProvider } from "react-admin";
9
+ import type { BloqueClient } from "@ulfblk/api-client";
10
+ import { jwtDecode } from "jwt-decode";
11
+ import type { AuthProviderOptions } from "./types.js";
12
+
13
+ interface JwtPayload {
14
+ sub: string;
15
+ tenant: string;
16
+ exp: number;
17
+ type: string;
18
+ roles?: string[];
19
+ permissions?: string[];
20
+ }
21
+
22
+ function isTokenExpired(token: string): boolean {
23
+ try {
24
+ const decoded = jwtDecode<JwtPayload>(token);
25
+ return decoded.exp * 1000 < Date.now();
26
+ } catch {
27
+ return true;
28
+ }
29
+ }
30
+
31
+ function getPayload(token: string): JwtPayload | null {
32
+ try {
33
+ return jwtDecode<JwtPayload>(token);
34
+ } catch {
35
+ return null;
36
+ }
37
+ }
38
+
39
+ export function createAuthProvider(
40
+ client: BloqueClient,
41
+ options?: AuthProviderOptions,
42
+ ): AuthProvider {
43
+ const loginPath = options?.loginPath ?? "/auth/login";
44
+ let lastToken: string | null = null;
45
+
46
+ return {
47
+ login: async ({ username, password }: { username: string; password: string }) => {
48
+ const response = await client.post<{ access_token: string; refresh_token: string }>(
49
+ loginPath,
50
+ { email: username, password },
51
+ );
52
+ client.setTokens({
53
+ accessToken: response.access_token,
54
+ refreshToken: response.refresh_token,
55
+ });
56
+ lastToken = response.access_token;
57
+ },
58
+
59
+ logout: async () => {
60
+ client.logout();
61
+ lastToken = null;
62
+ },
63
+
64
+ checkAuth: async () => {
65
+ if (!lastToken || isTokenExpired(lastToken)) {
66
+ throw new Error("Token expired or missing");
67
+ }
68
+ },
69
+
70
+ checkError: async (error: { status?: number }) => {
71
+ if (error.status === 401) {
72
+ client.logout();
73
+ lastToken = null;
74
+ throw new Error("Unauthorized");
75
+ }
76
+ // 403 = valid token but no permission - don't logout
77
+ if (error.status === 403) {
78
+ throw new Error("Forbidden");
79
+ }
80
+ },
81
+
82
+ getPermissions: async () => {
83
+ if (!lastToken) return { roles: [], permissions: [] };
84
+ const payload = getPayload(lastToken);
85
+ if (!payload) return { roles: [], permissions: [] };
86
+ return {
87
+ roles: payload.roles ?? [],
88
+ permissions: payload.permissions ?? [],
89
+ };
90
+ },
91
+
92
+ getIdentity: async () => {
93
+ if (!lastToken) throw new Error("Not authenticated");
94
+ const payload = getPayload(lastToken);
95
+ if (!payload) throw new Error("Invalid token");
96
+ return {
97
+ id: payload.sub,
98
+ fullName: payload.sub,
99
+ tenant: payload.tenant,
100
+ };
101
+ },
102
+ };
103
+ }
@@ -0,0 +1,140 @@
1
+ /**
2
+ * React Admin DataProvider using BloqueClient as HTTP transport.
3
+ *
4
+ * Translates react-admin CRUD calls to REST endpoints.
5
+ * Uses `qs` for query string serialization (FastAPI-compatible).
6
+ *
7
+ * Tenant context is handled automatically via BloqueClient's
8
+ * tenant interceptor (call client.setTenantId() to switch tenants).
9
+ */
10
+
11
+ import type { DataProvider } from "react-admin";
12
+ import { HttpError } from "react-admin";
13
+ import { stringify } from "qs";
14
+ import type { BloqueClient } from "@ulfblk/api-client";
15
+ import type { DataProviderOptions, ListResponse } from "./types.js";
16
+
17
+ const BATCH_SIZE = 5;
18
+
19
+ function defaultSerializer(params: Record<string, unknown>): string {
20
+ return stringify(params, { arrayFormat: "repeat", skipNulls: true });
21
+ }
22
+
23
+ async function batchExecute<T>(
24
+ ids: (string | number)[],
25
+ fn: (id: string | number) => Promise<T>,
26
+ ): Promise<T[]> {
27
+ const results: T[] = [];
28
+ for (let i = 0; i < ids.length; i += BATCH_SIZE) {
29
+ const batch = ids.slice(i, i + BATCH_SIZE);
30
+ const batchResults = await Promise.all(batch.map(fn));
31
+ results.push(...batchResults);
32
+ }
33
+ return results;
34
+ }
35
+
36
+ export function createDataProvider(
37
+ client: BloqueClient,
38
+ options?: DataProviderOptions,
39
+ ): DataProvider {
40
+ const base = (options?.basePath ?? "/api").replace(/\/+$/, "");
41
+ const serialize = options?.querySerializer ?? defaultSerializer;
42
+
43
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -- react-admin generics are complex
44
+ type AnyRecord = any;
45
+
46
+ async function request<T>(method: string, path: string, body?: unknown): Promise<T> {
47
+ try {
48
+ switch (method) {
49
+ case "GET":
50
+ return await client.get<T>(path);
51
+ case "POST":
52
+ return await client.post<T>(path, body);
53
+ case "PUT":
54
+ return await client.put<T>(path, body);
55
+ case "DELETE":
56
+ return await client.delete<T>(path);
57
+ default:
58
+ throw new Error(`Unsupported method: ${method}`);
59
+ }
60
+ } catch (error: unknown) {
61
+ if (error instanceof Error && "status" in error) {
62
+ const e = error as Error & { status: number; body?: unknown };
63
+ throw new HttpError(e.message, e.status, e.body);
64
+ }
65
+ throw error;
66
+ }
67
+ }
68
+
69
+ return {
70
+ getList: async (resource: string, params: any) => {
71
+ const { page, perPage } = params.pagination ?? { page: 1, perPage: 20 };
72
+ const { field, order } = params.sort ?? { field: "id", order: "ASC" };
73
+ const query = serialize({
74
+ page,
75
+ size: perPage,
76
+ sort: field,
77
+ order,
78
+ ...params.filter,
79
+ });
80
+ const url = `${base}/${resource}?${query}`;
81
+ const response = await request<AnyRecord>("GET", url);
82
+ return { data: response.items, total: response.total };
83
+ },
84
+
85
+ getOne: async (resource: string, params: any) => {
86
+ const data = await request<AnyRecord>("GET", `${base}/${resource}/${params.id}`);
87
+ return { data };
88
+ },
89
+
90
+ getMany: async (resource: string, params: any) => {
91
+ const query = serialize({ id: params.ids });
92
+ const response = await request<AnyRecord>("GET", `${base}/${resource}?${query}`);
93
+ return { data: response.items };
94
+ },
95
+
96
+ getManyReference: async (resource: string, params: any) => {
97
+ const { page, perPage } = params.pagination ?? { page: 1, perPage: 20 };
98
+ const { field, order } = params.sort ?? { field: "id", order: "ASC" };
99
+ const query = serialize({
100
+ page,
101
+ size: perPage,
102
+ sort: field,
103
+ order,
104
+ [params.target]: params.id,
105
+ ...params.filter,
106
+ });
107
+ const response = await request<AnyRecord>("GET", `${base}/${resource}?${query}`);
108
+ return { data: response.items, total: response.total };
109
+ },
110
+
111
+ create: async (resource: string, params: any) => {
112
+ const data = await request<AnyRecord>("POST", `${base}/${resource}`, params.data);
113
+ return { data };
114
+ },
115
+
116
+ update: async (resource: string, params: any) => {
117
+ const data = await request<AnyRecord>("PUT", `${base}/${resource}/${params.id}`, params.data);
118
+ return { data };
119
+ },
120
+
121
+ delete: async (resource: string, params: any) => {
122
+ const data = await request<AnyRecord>("DELETE", `${base}/${resource}/${params.id}`);
123
+ return { data };
124
+ },
125
+
126
+ deleteMany: async (resource: string, params: any) => {
127
+ await batchExecute(params.ids, (id) =>
128
+ request("DELETE", `${base}/${resource}/${id}`),
129
+ );
130
+ return { data: params.ids };
131
+ },
132
+
133
+ updateMany: async (resource: string, params: any) => {
134
+ await batchExecute(params.ids, (id) =>
135
+ request("PUT", `${base}/${resource}/${id}`, params.data),
136
+ );
137
+ return { data: params.ids };
138
+ },
139
+ };
140
+ }
package/src/index.ts ADDED
@@ -0,0 +1,11 @@
1
+ /**
2
+ * @ulfblk/admin - React Admin providers for the ulfblk ecosystem.
3
+ *
4
+ * Provides DataProvider and AuthProvider that use BloqueClient
5
+ * as the HTTP transport layer, with automatic tenant context,
6
+ * JWT auth, and FastAPI-compatible query serialization.
7
+ */
8
+
9
+ export { createDataProvider } from "./data-provider.js";
10
+ export { createAuthProvider } from "./auth-provider.js";
11
+ export type { DataProviderOptions, AuthProviderOptions, ListResponse } from "./types.js";
package/src/types.ts ADDED
@@ -0,0 +1,33 @@
1
+ /**
2
+ * Configuration options for the ulfblk DataProvider.
3
+ */
4
+ export interface DataProviderOptions {
5
+ /** Base path for API endpoints. Default: "/api" */
6
+ basePath?: string;
7
+
8
+ /**
9
+ * Custom query string serializer.
10
+ * Default uses `qs` with arrayFormat: 'repeat' (FastAPI-compatible).
11
+ */
12
+ querySerializer?: (params: Record<string, unknown>) => string;
13
+ }
14
+
15
+ /**
16
+ * Configuration options for the ulfblk AuthProvider.
17
+ */
18
+ export interface AuthProviderOptions {
19
+ /** Path for the login endpoint. Default: "/auth/login" */
20
+ loginPath?: string;
21
+ }
22
+
23
+ /**
24
+ * Expected response format from list endpoints.
25
+ * Must match PaginatedResponse from the Python backend.
26
+ */
27
+ export interface ListResponse<T = Record<string, unknown>> {
28
+ items: T[];
29
+ total: number;
30
+ page?: number;
31
+ page_size?: number;
32
+ pages?: number;
33
+ }