@simplix-react/contract 0.0.1 → 0.0.2
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 +309 -0
- package/dist/index.d.ts +600 -11
- package/dist/index.js +90 -6
- package/package.json +8 -10
- package/dist/mcp-tools.d.ts +0 -14
- package/dist/mcp-tools.js +0 -62
- package/dist/openapi.d.ts +0 -21
- package/dist/openapi.js +0 -54
- package/dist/types-tFXBXgJP.d.ts +0 -41
package/README.md
ADDED
|
@@ -0,0 +1,309 @@
|
|
|
1
|
+
# @simplix-react/contract
|
|
2
|
+
|
|
3
|
+
Define type-safe API contracts with Zod schemas. A single contract drives your HTTP client, React Query hooks, and MSW mock handlers.
|
|
4
|
+
|
|
5
|
+
## Installation
|
|
6
|
+
|
|
7
|
+
```bash
|
|
8
|
+
pnpm add @simplix-react/contract
|
|
9
|
+
```
|
|
10
|
+
|
|
11
|
+
**Peer dependency:** `zod >= 4.0.0`
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pnpm add zod
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
## Quick Example
|
|
18
|
+
|
|
19
|
+
```ts
|
|
20
|
+
import { z } from "zod";
|
|
21
|
+
import { defineApi, simpleQueryBuilder } from "@simplix-react/contract";
|
|
22
|
+
|
|
23
|
+
// 1. Define your contract
|
|
24
|
+
const projectApi = defineApi({
|
|
25
|
+
domain: "project",
|
|
26
|
+
basePath: "/api/v1",
|
|
27
|
+
entities: {
|
|
28
|
+
task: {
|
|
29
|
+
path: "/tasks",
|
|
30
|
+
schema: z.object({
|
|
31
|
+
id: z.string(),
|
|
32
|
+
title: z.string(),
|
|
33
|
+
status: z.enum(["open", "closed"]),
|
|
34
|
+
}),
|
|
35
|
+
createSchema: z.object({
|
|
36
|
+
title: z.string(),
|
|
37
|
+
}),
|
|
38
|
+
updateSchema: z.object({
|
|
39
|
+
title: z.string().optional(),
|
|
40
|
+
status: z.enum(["open", "closed"]).optional(),
|
|
41
|
+
}),
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
queryBuilder: simpleQueryBuilder,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// 2. Use the type-safe client
|
|
48
|
+
const tasks = await projectApi.client.task.list();
|
|
49
|
+
const task = await projectApi.client.task.get("task-1");
|
|
50
|
+
const created = await projectApi.client.task.create({ title: "New task" });
|
|
51
|
+
const updated = await projectApi.client.task.update("task-1", { status: "closed" });
|
|
52
|
+
await projectApi.client.task.delete("task-1");
|
|
53
|
+
|
|
54
|
+
// 3. Use query keys for cache management
|
|
55
|
+
projectApi.queryKeys.task.all; // ["project", "task"]
|
|
56
|
+
projectApi.queryKeys.task.lists(); // ["project", "task", "list"]
|
|
57
|
+
projectApi.queryKeys.task.list({ status: "open" }); // ["project", "task", "list", { status: "open" }]
|
|
58
|
+
projectApi.queryKeys.task.detail("task-1"); // ["project", "task", "detail", "task-1"]
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
## API Overview
|
|
62
|
+
|
|
63
|
+
| Export | Kind | Description |
|
|
64
|
+
| --- | --- | --- |
|
|
65
|
+
| `defineApi` | Function | Creates a contract with client and query keys from a config |
|
|
66
|
+
| `deriveClient` | Function | Generates a type-safe HTTP client from a contract config |
|
|
67
|
+
| `deriveQueryKeys` | Function | Generates TanStack Query key factories from a contract config |
|
|
68
|
+
| `buildPath` | Function | Substitutes `:param` placeholders in URL templates |
|
|
69
|
+
| `defaultFetch` | Function | Built-in fetch with JSON content-type and `{ data }` envelope unwrapping |
|
|
70
|
+
| `ApiError` | Class | Error type for non-2xx HTTP responses |
|
|
71
|
+
| `simpleQueryBuilder` | Object | Ready-made `QueryBuilder` for common REST query string patterns |
|
|
72
|
+
| `camelToKebab` | Function | Converts camelCase to kebab-case |
|
|
73
|
+
| `camelToSnake` | Function | Converts camelCase to snake_case |
|
|
74
|
+
|
|
75
|
+
### Type Exports
|
|
76
|
+
|
|
77
|
+
| Export | Description |
|
|
78
|
+
| --- | --- |
|
|
79
|
+
| `EntityDefinition` | Describes a CRUD entity with Zod schemas |
|
|
80
|
+
| `EntityParent` | Parent resource for nested URL construction |
|
|
81
|
+
| `EntityQuery` | Named query scope filtering by parent relationship |
|
|
82
|
+
| `OperationDefinition` | Describes a custom non-CRUD API operation |
|
|
83
|
+
| `HttpMethod` | Union of supported HTTP methods |
|
|
84
|
+
| `ApiContractConfig` | Full configuration input for `defineApi` |
|
|
85
|
+
| `ApiContract` | Return type of `defineApi` |
|
|
86
|
+
| `EntityClient` | CRUD client interface for a single entity |
|
|
87
|
+
| `QueryKeyFactory` | Structured query key generators for TanStack Query |
|
|
88
|
+
| `FetchFn` | Custom fetch function signature |
|
|
89
|
+
| `ListParams` | Filters, sort, and pagination parameters for list queries |
|
|
90
|
+
| `SortParam` | Sort field and direction |
|
|
91
|
+
| `PaginationParam` | Offset-based or cursor-based pagination |
|
|
92
|
+
| `PageInfo` | Server-returned pagination metadata |
|
|
93
|
+
| `QueryBuilder` | Interface for serializing list params to URL search params |
|
|
94
|
+
|
|
95
|
+
## Key Concepts
|
|
96
|
+
|
|
97
|
+
### Zod Schema → Type Inference
|
|
98
|
+
|
|
99
|
+
Every contract type is inferred from Zod schemas at compile time. You define schemas once; TypeScript infers the rest:
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
import { z } from "zod";
|
|
103
|
+
|
|
104
|
+
const taskSchema = z.object({
|
|
105
|
+
id: z.string(),
|
|
106
|
+
title: z.string(),
|
|
107
|
+
status: z.enum(["open", "closed"]),
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
// z.infer<typeof taskSchema> → { id: string; title: string; status: "open" | "closed" }
|
|
111
|
+
```
|
|
112
|
+
|
|
113
|
+
The framework uses these inferred types throughout the client, hooks, and mock handlers, so your API types are always in sync with validation logic.
|
|
114
|
+
|
|
115
|
+
### EntityDefinition
|
|
116
|
+
|
|
117
|
+
An `EntityDefinition` is the building block for CRUD resources. It bundles three schemas:
|
|
118
|
+
|
|
119
|
+
- **`schema`** — The full entity shape returned by the API
|
|
120
|
+
- **`createSchema`** — The payload required to create a new entity
|
|
121
|
+
- **`updateSchema`** — The payload for partial updates
|
|
122
|
+
|
|
123
|
+
```ts
|
|
124
|
+
const taskEntity = {
|
|
125
|
+
path: "/tasks",
|
|
126
|
+
schema: taskSchema,
|
|
127
|
+
createSchema: z.object({ title: z.string() }),
|
|
128
|
+
updateSchema: z.object({ title: z.string().optional() }),
|
|
129
|
+
};
|
|
130
|
+
```
|
|
131
|
+
|
|
132
|
+
This single definition drives `list`, `get`, `create`, `update`, and `delete` methods on the client.
|
|
133
|
+
|
|
134
|
+
### OperationDefinition
|
|
135
|
+
|
|
136
|
+
For endpoints that don't fit the CRUD pattern (file uploads, RPC-style calls, batch operations), use `OperationDefinition`:
|
|
137
|
+
|
|
138
|
+
```ts
|
|
139
|
+
import { z } from "zod";
|
|
140
|
+
import { defineApi } from "@simplix-react/contract";
|
|
141
|
+
|
|
142
|
+
const api = defineApi({
|
|
143
|
+
domain: "project",
|
|
144
|
+
basePath: "/api/v1",
|
|
145
|
+
entities: { task: taskEntity },
|
|
146
|
+
operations: {
|
|
147
|
+
assignTask: {
|
|
148
|
+
method: "POST",
|
|
149
|
+
path: "/tasks/:taskId/assign",
|
|
150
|
+
input: z.object({ userId: z.string() }),
|
|
151
|
+
output: z.object({ id: z.string(), assigneeId: z.string() }),
|
|
152
|
+
},
|
|
153
|
+
exportReport: {
|
|
154
|
+
method: "GET",
|
|
155
|
+
path: "/projects/:projectId/export",
|
|
156
|
+
input: z.object({}),
|
|
157
|
+
output: z.any(),
|
|
158
|
+
responseType: "blob",
|
|
159
|
+
},
|
|
160
|
+
},
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
// Path params are positional arguments, input is the last argument
|
|
164
|
+
await api.client.assignTask("task-1", { userId: "user-42" });
|
|
165
|
+
```
|
|
166
|
+
|
|
167
|
+
### Nested Entities (Parent Relationships)
|
|
168
|
+
|
|
169
|
+
Entities can declare a `parent` for nested URL construction:
|
|
170
|
+
|
|
171
|
+
```ts
|
|
172
|
+
const taskEntity = {
|
|
173
|
+
path: "/tasks",
|
|
174
|
+
schema: taskSchema,
|
|
175
|
+
createSchema: createTaskSchema,
|
|
176
|
+
updateSchema: updateTaskSchema,
|
|
177
|
+
parent: { param: "projectId", path: "/projects" },
|
|
178
|
+
};
|
|
179
|
+
|
|
180
|
+
// Client adjusts URLs based on parent
|
|
181
|
+
await api.client.task.list("project-1");
|
|
182
|
+
// GET /api/v1/projects/project-1/tasks
|
|
183
|
+
|
|
184
|
+
await api.client.task.create("project-1", { title: "New task" });
|
|
185
|
+
// POST /api/v1/projects/project-1/tasks
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
### Query Keys
|
|
189
|
+
|
|
190
|
+
The contract automatically generates TanStack Query-compatible key factories with hierarchical structure:
|
|
191
|
+
|
|
192
|
+
```
|
|
193
|
+
task.all → ["project", "task"] (broadest)
|
|
194
|
+
task.lists() → ["project", "task", "list"]
|
|
195
|
+
task.list({ ... }) → ["project", "task", "list", { ... }]
|
|
196
|
+
task.details() → ["project", "task", "detail"]
|
|
197
|
+
task.detail("id") → ["project", "task", "detail", "id"] (most specific)
|
|
198
|
+
```
|
|
199
|
+
|
|
200
|
+
Invalidating a broader key automatically invalidates all more-specific keys beneath it.
|
|
201
|
+
|
|
202
|
+
## Guides
|
|
203
|
+
|
|
204
|
+
### Custom Fetch Function
|
|
205
|
+
|
|
206
|
+
Replace the built-in HTTP client with custom logic for authentication, logging, or retry:
|
|
207
|
+
|
|
208
|
+
```ts
|
|
209
|
+
import { defineApi, defaultFetch } from "@simplix-react/contract";
|
|
210
|
+
|
|
211
|
+
const api = defineApi(config, {
|
|
212
|
+
fetchFn: async (path, options) => {
|
|
213
|
+
const token = await getAuthToken();
|
|
214
|
+
return defaultFetch(path, {
|
|
215
|
+
...options,
|
|
216
|
+
headers: {
|
|
217
|
+
...options?.headers,
|
|
218
|
+
Authorization: `Bearer ${token}`,
|
|
219
|
+
},
|
|
220
|
+
});
|
|
221
|
+
},
|
|
222
|
+
});
|
|
223
|
+
```
|
|
224
|
+
|
|
225
|
+
### Custom Query Builder
|
|
226
|
+
|
|
227
|
+
Implement the `QueryBuilder` interface to match your API's query string conventions:
|
|
228
|
+
|
|
229
|
+
```ts
|
|
230
|
+
import { defineApi } from "@simplix-react/contract";
|
|
231
|
+
import type { QueryBuilder } from "@simplix-react/contract";
|
|
232
|
+
|
|
233
|
+
const springQueryBuilder: QueryBuilder = {
|
|
234
|
+
buildSearchParams(params) {
|
|
235
|
+
const sp = new URLSearchParams();
|
|
236
|
+
if (params.pagination?.type === "offset") {
|
|
237
|
+
// Spring uses 0-based page indexing
|
|
238
|
+
sp.set("page", String(params.pagination.page - 1));
|
|
239
|
+
sp.set("size", String(params.pagination.limit));
|
|
240
|
+
}
|
|
241
|
+
if (params.sort) {
|
|
242
|
+
const sorts = Array.isArray(params.sort) ? params.sort : [params.sort];
|
|
243
|
+
for (const s of sorts) {
|
|
244
|
+
sp.append("sort", `${s.field},${s.direction}`);
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
return sp;
|
|
248
|
+
},
|
|
249
|
+
};
|
|
250
|
+
|
|
251
|
+
const api = defineApi({
|
|
252
|
+
domain: "project",
|
|
253
|
+
basePath: "/api/v1",
|
|
254
|
+
entities: { task: taskEntity },
|
|
255
|
+
queryBuilder: springQueryBuilder,
|
|
256
|
+
});
|
|
257
|
+
```
|
|
258
|
+
|
|
259
|
+
### Multipart File Uploads
|
|
260
|
+
|
|
261
|
+
Use `contentType: "multipart"` in an operation definition for file upload endpoints:
|
|
262
|
+
|
|
263
|
+
```ts
|
|
264
|
+
const api = defineApi({
|
|
265
|
+
domain: "project",
|
|
266
|
+
basePath: "/api/v1",
|
|
267
|
+
entities: {},
|
|
268
|
+
operations: {
|
|
269
|
+
uploadAvatar: {
|
|
270
|
+
method: "POST",
|
|
271
|
+
path: "/users/:userId/avatar",
|
|
272
|
+
input: z.object({ file: z.instanceof(File) }),
|
|
273
|
+
output: z.object({ url: z.string() }),
|
|
274
|
+
contentType: "multipart",
|
|
275
|
+
},
|
|
276
|
+
},
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
await api.client.uploadAvatar("user-1", { file: selectedFile });
|
|
280
|
+
```
|
|
281
|
+
|
|
282
|
+
### Error Handling
|
|
283
|
+
|
|
284
|
+
All client methods throw `ApiError` for non-2xx responses:
|
|
285
|
+
|
|
286
|
+
```ts
|
|
287
|
+
import { ApiError } from "@simplix-react/contract";
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await api.client.task.get("nonexistent");
|
|
291
|
+
} catch (error) {
|
|
292
|
+
if (error instanceof ApiError) {
|
|
293
|
+
console.log(error.status); // 404
|
|
294
|
+
console.log(error.body); // Raw response body
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
```
|
|
298
|
+
|
|
299
|
+
## Related Packages
|
|
300
|
+
|
|
301
|
+
| Package | Description |
|
|
302
|
+
| --- | --- |
|
|
303
|
+
| `@simplix-react/react` | Derives React Query hooks from the contract |
|
|
304
|
+
| `@simplix-react/mock` | Generates MSW handlers and PGlite repositories from the contract |
|
|
305
|
+
| `@simplix-react/testing` | Test utilities built on top of the contract and mock packages |
|
|
306
|
+
|
|
307
|
+
---
|
|
308
|
+
|
|
309
|
+
**Next Step** → [`@simplix-react/react`](../react/README.md) — Turn your contract into React Query hooks.
|
package/dist/index.d.ts
CHANGED
|
@@ -1,62 +1,470 @@
|
|
|
1
1
|
import { z } from 'zod';
|
|
2
2
|
|
|
3
|
+
/**
|
|
4
|
+
* Describes a single sort directive with field name and direction.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* ```ts
|
|
8
|
+
* import type { SortParam } from "@simplix-react/contract";
|
|
9
|
+
*
|
|
10
|
+
* const sort: SortParam = { field: "createdAt", direction: "desc" };
|
|
11
|
+
* ```
|
|
12
|
+
*/
|
|
13
|
+
interface SortParam {
|
|
14
|
+
/** The field name to sort by. */
|
|
15
|
+
field: string;
|
|
16
|
+
/** Sort direction: ascending or descending. */
|
|
17
|
+
direction: "asc" | "desc";
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Describes pagination strategy, supporting both offset-based and cursor-based patterns.
|
|
21
|
+
*
|
|
22
|
+
* @example
|
|
23
|
+
* ```ts
|
|
24
|
+
* import type { PaginationParam } from "@simplix-react/contract";
|
|
25
|
+
*
|
|
26
|
+
* // Offset-based
|
|
27
|
+
* const offset: PaginationParam = { type: "offset", page: 1, limit: 20 };
|
|
28
|
+
*
|
|
29
|
+
* // Cursor-based
|
|
30
|
+
* const cursor: PaginationParam = { type: "cursor", cursor: "abc123", limit: 20 };
|
|
31
|
+
* ```
|
|
32
|
+
*/
|
|
33
|
+
type PaginationParam = {
|
|
34
|
+
type: "offset";
|
|
35
|
+
page: number;
|
|
36
|
+
limit: number;
|
|
37
|
+
} | {
|
|
38
|
+
type: "cursor";
|
|
39
|
+
cursor: string;
|
|
40
|
+
limit: number;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Encapsulates all list query parameters: filters, sorting, and pagination.
|
|
44
|
+
*
|
|
45
|
+
* Passed to entity `list()` methods and serialized into URL search params
|
|
46
|
+
* by a {@link QueryBuilder}.
|
|
47
|
+
*
|
|
48
|
+
* @typeParam TFilters - Shape of the filter object, defaults to `Record<string, unknown>`.
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```ts
|
|
52
|
+
* import type { ListParams } from "@simplix-react/contract";
|
|
53
|
+
*
|
|
54
|
+
* const params: ListParams = {
|
|
55
|
+
* filters: { status: "active" },
|
|
56
|
+
* sort: { field: "title", direction: "asc" },
|
|
57
|
+
* pagination: { type: "offset", page: 1, limit: 10 },
|
|
58
|
+
* };
|
|
59
|
+
*
|
|
60
|
+
* await api.client.task.list(params);
|
|
61
|
+
* ```
|
|
62
|
+
*/
|
|
63
|
+
interface ListParams<TFilters = Record<string, unknown>> {
|
|
64
|
+
/** Optional filter criteria applied to the list query. */
|
|
65
|
+
filters?: TFilters;
|
|
66
|
+
/** Single sort directive or array of sort directives. */
|
|
67
|
+
sort?: SortParam | SortParam[];
|
|
68
|
+
/** Pagination strategy and parameters. */
|
|
69
|
+
pagination?: PaginationParam;
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Describes pagination metadata returned from the server.
|
|
73
|
+
*
|
|
74
|
+
* Used by {@link QueryBuilder.parsePageInfo} to extract pagination state
|
|
75
|
+
* from API responses, enabling infinite scroll and paginated UIs.
|
|
76
|
+
*
|
|
77
|
+
* @example
|
|
78
|
+
* ```ts
|
|
79
|
+
* import type { PageInfo } from "@simplix-react/contract";
|
|
80
|
+
*
|
|
81
|
+
* const pageInfo: PageInfo = {
|
|
82
|
+
* total: 42,
|
|
83
|
+
* hasNextPage: true,
|
|
84
|
+
* nextCursor: "cursor-xyz",
|
|
85
|
+
* };
|
|
86
|
+
* ```
|
|
87
|
+
*/
|
|
88
|
+
interface PageInfo {
|
|
89
|
+
/** Total number of items across all pages (if the server provides it). */
|
|
90
|
+
total?: number;
|
|
91
|
+
/** Whether more items exist beyond the current page. */
|
|
92
|
+
hasNextPage: boolean;
|
|
93
|
+
/** Cursor value to fetch the next page (cursor-based pagination only). */
|
|
94
|
+
nextCursor?: string;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Defines how list parameters are serialized to URL search params and how
|
|
98
|
+
* pagination metadata is extracted from API responses.
|
|
99
|
+
*
|
|
100
|
+
* Implement this interface to adapt the framework to your API's query string
|
|
101
|
+
* conventions. Use {@link simpleQueryBuilder} as a ready-made implementation
|
|
102
|
+
* for common REST patterns.
|
|
103
|
+
*
|
|
104
|
+
* @example
|
|
105
|
+
* ```ts
|
|
106
|
+
* import type { QueryBuilder } from "@simplix-react/contract";
|
|
107
|
+
*
|
|
108
|
+
* const customQueryBuilder: QueryBuilder = {
|
|
109
|
+
* buildSearchParams(params) {
|
|
110
|
+
* const sp = new URLSearchParams();
|
|
111
|
+
* if (params.pagination?.type === "offset") {
|
|
112
|
+
* sp.set("offset", String((params.pagination.page - 1) * params.pagination.limit));
|
|
113
|
+
* sp.set("limit", String(params.pagination.limit));
|
|
114
|
+
* }
|
|
115
|
+
* return sp;
|
|
116
|
+
* },
|
|
117
|
+
* parsePageInfo(response) {
|
|
118
|
+
* const { total, nextCursor } = response as any;
|
|
119
|
+
* return { total, hasNextPage: !!nextCursor, nextCursor };
|
|
120
|
+
* },
|
|
121
|
+
* };
|
|
122
|
+
* ```
|
|
123
|
+
*
|
|
124
|
+
* @see {@link simpleQueryBuilder} for the built-in implementation.
|
|
125
|
+
*/
|
|
126
|
+
interface QueryBuilder {
|
|
127
|
+
/** Converts structured list parameters into URL search params. */
|
|
128
|
+
buildSearchParams(params: ListParams): URLSearchParams;
|
|
129
|
+
/** Extracts pagination metadata from an API response (optional). */
|
|
130
|
+
parsePageInfo?(response: unknown): PageInfo;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Describes the parent resource in a nested entity relationship.
|
|
135
|
+
*
|
|
136
|
+
* Enables hierarchical URL construction such as `/projects/:projectId/tasks`.
|
|
137
|
+
*
|
|
138
|
+
* @example
|
|
139
|
+
* ```ts
|
|
140
|
+
* const parent: EntityParent = {
|
|
141
|
+
* param: "projectId",
|
|
142
|
+
* path: "/projects",
|
|
143
|
+
* };
|
|
144
|
+
* // Produces: /projects/:projectId/tasks
|
|
145
|
+
* ```
|
|
146
|
+
*
|
|
147
|
+
* @see {@link EntityDefinition} for the entity that references this parent.
|
|
148
|
+
*/
|
|
3
149
|
interface EntityParent {
|
|
150
|
+
/** Route parameter name used to identify the parent resource (e.g. `"projectId"`). */
|
|
4
151
|
param: string;
|
|
152
|
+
/** Base path segment for the parent resource (e.g. `"/projects"`). */
|
|
5
153
|
path: string;
|
|
6
154
|
}
|
|
155
|
+
/**
|
|
156
|
+
* Represents a named query scope that filters entities by a parent relationship.
|
|
157
|
+
*
|
|
158
|
+
* Allows defining reusable query patterns like "tasks by project" that can be
|
|
159
|
+
* referenced throughout the application.
|
|
160
|
+
*
|
|
161
|
+
* @see {@link EntityDefinition.queries} where these are declared.
|
|
162
|
+
*/
|
|
7
163
|
interface EntityQuery {
|
|
164
|
+
/** Name of the parent entity this query filters by (e.g. `"project"`). */
|
|
8
165
|
parent: string;
|
|
166
|
+
/** Route parameter name used to scope the query (e.g. `"projectId"`). */
|
|
9
167
|
param: string;
|
|
10
168
|
}
|
|
169
|
+
/**
|
|
170
|
+
* Defines a CRUD-capable API entity with Zod schemas for type-safe validation.
|
|
171
|
+
*
|
|
172
|
+
* Serves as the single source of truth for an entity's shape, creation payload,
|
|
173
|
+
* update payload, and URL structure. The framework derives API clients, React Query
|
|
174
|
+
* hooks, and MSW handlers from this definition.
|
|
175
|
+
*
|
|
176
|
+
* @typeParam TSchema - Zod schema for the entity's response shape.
|
|
177
|
+
* @typeParam TCreate - Zod schema for the creation payload.
|
|
178
|
+
* @typeParam TUpdate - Zod schema for the update (partial) payload.
|
|
179
|
+
*
|
|
180
|
+
* @example
|
|
181
|
+
* ```ts
|
|
182
|
+
* import { z } from "zod";
|
|
183
|
+
* import type { EntityDefinition } from "@simplix-react/contract";
|
|
184
|
+
*
|
|
185
|
+
* const taskEntity: EntityDefinition = {
|
|
186
|
+
* path: "/tasks",
|
|
187
|
+
* schema: z.object({ id: z.string(), title: z.string() }),
|
|
188
|
+
* createSchema: z.object({ title: z.string() }),
|
|
189
|
+
* updateSchema: z.object({ title: z.string().optional() }),
|
|
190
|
+
* parent: { param: "projectId", path: "/projects" },
|
|
191
|
+
* };
|
|
192
|
+
* ```
|
|
193
|
+
*
|
|
194
|
+
* @see {@link OperationDefinition} for non-CRUD custom operations.
|
|
195
|
+
* @see {@link @simplix-react/react!deriveHooks | deriveHooks} for deriving React Query hooks.
|
|
196
|
+
* @see {@link @simplix-react/mock!deriveMockHandlers | deriveMockHandlers} for deriving MSW handlers.
|
|
197
|
+
*/
|
|
11
198
|
interface EntityDefinition<TSchema extends z.ZodType = z.ZodType, TCreate extends z.ZodType = z.ZodType, TUpdate extends z.ZodType = z.ZodType> {
|
|
199
|
+
/** URL path segment for this entity (e.g. `"/tasks"`). */
|
|
12
200
|
path: string;
|
|
201
|
+
/** Zod schema describing the full entity shape returned by the API. */
|
|
13
202
|
schema: TSchema;
|
|
203
|
+
/** Zod schema describing the payload required to create a new entity. */
|
|
14
204
|
createSchema: TCreate;
|
|
205
|
+
/** Zod schema describing the payload for updating an existing entity. */
|
|
15
206
|
updateSchema: TUpdate;
|
|
207
|
+
/** Optional parent resource for nested URL construction. */
|
|
16
208
|
parent?: EntityParent;
|
|
209
|
+
/** Named query scopes for filtering entities by parent relationships. */
|
|
17
210
|
queries?: Record<string, EntityQuery>;
|
|
211
|
+
/** Optional Zod schema for validating list filter parameters. */
|
|
212
|
+
filterSchema?: z.ZodType;
|
|
18
213
|
}
|
|
214
|
+
/**
|
|
215
|
+
* Supported HTTP methods for API operations.
|
|
216
|
+
*/
|
|
19
217
|
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
218
|
+
/**
|
|
219
|
+
* Defines a custom (non-CRUD) API operation with typed input and output.
|
|
220
|
+
*
|
|
221
|
+
* Covers endpoints that do not fit the standard entity CRUD pattern, such as
|
|
222
|
+
* file uploads, batch operations, or RPC-style calls. Path parameters use
|
|
223
|
+
* the `:paramName` syntax and are positionally mapped to function arguments.
|
|
224
|
+
*
|
|
225
|
+
* @typeParam TInput - Zod schema for the request payload.
|
|
226
|
+
* @typeParam TOutput - Zod schema for the response payload.
|
|
227
|
+
*
|
|
228
|
+
* @example
|
|
229
|
+
* ```ts
|
|
230
|
+
* import { z } from "zod";
|
|
231
|
+
* import type { OperationDefinition } from "@simplix-react/contract";
|
|
232
|
+
*
|
|
233
|
+
* const assignTask: OperationDefinition = {
|
|
234
|
+
* method: "POST",
|
|
235
|
+
* path: "/tasks/:taskId/assign",
|
|
236
|
+
* input: z.object({ userId: z.string() }),
|
|
237
|
+
* output: z.object({ id: z.string(), assigneeId: z.string() }),
|
|
238
|
+
* };
|
|
239
|
+
* ```
|
|
240
|
+
*
|
|
241
|
+
* @see {@link EntityDefinition} for standard CRUD entities.
|
|
242
|
+
*/
|
|
20
243
|
interface OperationDefinition<TInput extends z.ZodType = z.ZodType, TOutput extends z.ZodType = z.ZodType> {
|
|
244
|
+
/** HTTP method for this operation. */
|
|
21
245
|
method: HttpMethod;
|
|
246
|
+
/** URL path with optional `:paramName` placeholders (e.g. `"/tasks/:taskId/assign"`). */
|
|
22
247
|
path: string;
|
|
248
|
+
/** Zod schema validating the request payload. */
|
|
23
249
|
input: TInput;
|
|
250
|
+
/** Zod schema validating the response payload. */
|
|
24
251
|
output: TOutput;
|
|
252
|
+
/** Content type for the request body. Defaults to `"json"`. */
|
|
253
|
+
contentType?: "json" | "multipart";
|
|
254
|
+
/** Expected response format. Defaults to `"json"`. */
|
|
255
|
+
responseType?: "json" | "blob";
|
|
256
|
+
/**
|
|
257
|
+
* Returns query key arrays that should be invalidated after this operation succeeds.
|
|
258
|
+
* Enables automatic cache invalidation in `@simplix-react/react`.
|
|
259
|
+
*/
|
|
25
260
|
invalidates?: (queryKeys: Record<string, QueryKeyFactory>, params: Record<string, string>) => readonly unknown[][];
|
|
26
261
|
}
|
|
262
|
+
/**
|
|
263
|
+
* Configures a complete API contract with entities, operations, and shared settings.
|
|
264
|
+
*
|
|
265
|
+
* Serves as the input to {@link defineApi}, grouping all entity and operation
|
|
266
|
+
* definitions under a single domain namespace with a shared base path.
|
|
267
|
+
*
|
|
268
|
+
* @typeParam TEntities - Map of entity names to their definitions.
|
|
269
|
+
* @typeParam TOperations - Map of operation names to their definitions.
|
|
270
|
+
*
|
|
271
|
+
* @example
|
|
272
|
+
* ```ts
|
|
273
|
+
* import { z } from "zod";
|
|
274
|
+
* import { simpleQueryBuilder } from "@simplix-react/contract";
|
|
275
|
+
* import type { ApiContractConfig } from "@simplix-react/contract";
|
|
276
|
+
*
|
|
277
|
+
* const config: ApiContractConfig = {
|
|
278
|
+
* domain: "project",
|
|
279
|
+
* basePath: "/api/v1",
|
|
280
|
+
* entities: {
|
|
281
|
+
* task: {
|
|
282
|
+
* path: "/tasks",
|
|
283
|
+
* schema: z.object({ id: z.string(), title: z.string() }),
|
|
284
|
+
* createSchema: z.object({ title: z.string() }),
|
|
285
|
+
* updateSchema: z.object({ title: z.string().optional() }),
|
|
286
|
+
* },
|
|
287
|
+
* },
|
|
288
|
+
* queryBuilder: simpleQueryBuilder,
|
|
289
|
+
* };
|
|
290
|
+
* ```
|
|
291
|
+
*
|
|
292
|
+
* @see {@link defineApi} for constructing a contract from this config.
|
|
293
|
+
*/
|
|
27
294
|
interface ApiContractConfig<TEntities extends Record<string, EntityDefinition<any, any, any>> = Record<string, EntityDefinition>, TOperations extends Record<string, OperationDefinition<any, any>> = Record<string, OperationDefinition>> {
|
|
295
|
+
/** Logical domain name used as the root segment in query keys (e.g. `"project"`). */
|
|
28
296
|
domain: string;
|
|
297
|
+
/** Base URL path prepended to all entity and operation paths (e.g. `"/api/v1"`). */
|
|
29
298
|
basePath: string;
|
|
299
|
+
/** Map of entity names to their CRUD definitions. */
|
|
30
300
|
entities: TEntities;
|
|
301
|
+
/** Optional map of custom operation names to their definitions. */
|
|
31
302
|
operations?: TOperations;
|
|
303
|
+
/** Strategy for serializing list parameters (filters, sort, pagination) into URL search params. */
|
|
304
|
+
queryBuilder?: QueryBuilder;
|
|
32
305
|
}
|
|
306
|
+
/**
|
|
307
|
+
* Provides structured query key generators for a single entity, following the
|
|
308
|
+
* query key factory pattern recommended by TanStack Query.
|
|
309
|
+
*
|
|
310
|
+
* Generated automatically by {@link deriveQueryKeys} and used by `@simplix-react/react`
|
|
311
|
+
* to manage cache granularity and invalidation.
|
|
312
|
+
*
|
|
313
|
+
* @example
|
|
314
|
+
* ```ts
|
|
315
|
+
* import { defineApi } from "@simplix-react/contract";
|
|
316
|
+
*
|
|
317
|
+
* const api = defineApi(config);
|
|
318
|
+
*
|
|
319
|
+
* api.queryKeys.task.all; // ["project", "task"]
|
|
320
|
+
* api.queryKeys.task.lists(); // ["project", "task", "list"]
|
|
321
|
+
* api.queryKeys.task.list({ status: "open" }); // ["project", "task", "list", { status: "open" }]
|
|
322
|
+
* api.queryKeys.task.details(); // ["project", "task", "detail"]
|
|
323
|
+
* api.queryKeys.task.detail("abc"); // ["project", "task", "detail", "abc"]
|
|
324
|
+
* ```
|
|
325
|
+
*
|
|
326
|
+
* @see {@link deriveQueryKeys} for the factory function.
|
|
327
|
+
*/
|
|
33
328
|
interface QueryKeyFactory {
|
|
329
|
+
/** Root key matching all queries for this entity: `[domain, entity]`. */
|
|
34
330
|
all: readonly unknown[];
|
|
331
|
+
/** Returns key matching all list queries: `[domain, entity, "list"]`. */
|
|
35
332
|
lists: () => readonly unknown[];
|
|
333
|
+
/** Returns key matching a specific list query with parameters. */
|
|
36
334
|
list: (params: Record<string, unknown>) => readonly unknown[];
|
|
335
|
+
/** Returns key matching all detail queries: `[domain, entity, "detail"]`. */
|
|
37
336
|
details: () => readonly unknown[];
|
|
337
|
+
/** Returns key matching a specific detail query by ID. */
|
|
38
338
|
detail: (id: string) => readonly unknown[];
|
|
39
339
|
}
|
|
340
|
+
/**
|
|
341
|
+
* Provides a type-safe CRUD client for a single entity, derived from its
|
|
342
|
+
* {@link EntityDefinition} schemas.
|
|
343
|
+
*
|
|
344
|
+
* All methods infer request/response types directly from the Zod schemas,
|
|
345
|
+
* ensuring compile-time safety without manual type annotations.
|
|
346
|
+
*
|
|
347
|
+
* @typeParam TSchema - Zod schema for the entity's response shape.
|
|
348
|
+
* @typeParam TCreate - Zod schema for the creation payload.
|
|
349
|
+
* @typeParam TUpdate - Zod schema for the update payload.
|
|
350
|
+
*
|
|
351
|
+
* @example
|
|
352
|
+
* ```ts
|
|
353
|
+
* import { defineApi } from "@simplix-react/contract";
|
|
354
|
+
*
|
|
355
|
+
* const api = defineApi(config);
|
|
356
|
+
*
|
|
357
|
+
* // All methods are fully typed based on entity schemas
|
|
358
|
+
* const tasks = await api.client.task.list();
|
|
359
|
+
* const task = await api.client.task.get("task-1");
|
|
360
|
+
* const created = await api.client.task.create({ title: "New task" });
|
|
361
|
+
* const updated = await api.client.task.update("task-1", { title: "Updated" });
|
|
362
|
+
* await api.client.task.delete("task-1");
|
|
363
|
+
* ```
|
|
364
|
+
*
|
|
365
|
+
* @see {@link deriveClient} for the factory function.
|
|
366
|
+
*/
|
|
40
367
|
interface EntityClient<TSchema extends z.ZodType, TCreate extends z.ZodType, TUpdate extends z.ZodType> {
|
|
41
|
-
list
|
|
368
|
+
/** Fetches a list of entities, optionally scoped by parent ID and/or list parameters. */
|
|
369
|
+
list: (parentIdOrParams?: string | ListParams, params?: ListParams) => Promise<z.infer<TSchema>[]>;
|
|
370
|
+
/** Fetches a single entity by its ID. */
|
|
42
371
|
get: (id: string) => Promise<z.infer<TSchema>>;
|
|
372
|
+
/** Creates a new entity, optionally under a parent resource. */
|
|
43
373
|
create: (parentIdOrDto: string | z.infer<TCreate>, dto?: z.infer<TCreate>) => Promise<z.infer<TSchema>>;
|
|
374
|
+
/** Partially updates an existing entity by ID. */
|
|
44
375
|
update: (id: string, dto: z.infer<TUpdate>) => Promise<z.infer<TSchema>>;
|
|
376
|
+
/** Deletes an entity by its ID. */
|
|
45
377
|
delete: (id: string) => Promise<void>;
|
|
46
378
|
}
|
|
379
|
+
/**
|
|
380
|
+
* Represents a customizable fetch function signature.
|
|
381
|
+
*
|
|
382
|
+
* Allows replacing the default HTTP client with a custom implementation
|
|
383
|
+
* (e.g. for authentication headers, retry logic, or testing).
|
|
384
|
+
*
|
|
385
|
+
* @typeParam T - The expected response type after deserialization.
|
|
386
|
+
*
|
|
387
|
+
* @see {@link defaultFetch} for the built-in implementation.
|
|
388
|
+
* @see {@link defineApi} where this is provided via `options.fetchFn`.
|
|
389
|
+
*/
|
|
47
390
|
type FetchFn = <T>(path: string, options?: RequestInit) => Promise<T>;
|
|
391
|
+
/**
|
|
392
|
+
* Represents the fully constructed API contract returned by {@link defineApi}.
|
|
393
|
+
*
|
|
394
|
+
* Contains the original configuration, a type-safe HTTP client, and query key
|
|
395
|
+
* factories for all registered entities. This is the primary interface consumed
|
|
396
|
+
* by `@simplix-react/react` and `@simplix-react/mock`.
|
|
397
|
+
*
|
|
398
|
+
* @typeParam TEntities - Map of entity names to their definitions.
|
|
399
|
+
* @typeParam TOperations - Map of operation names to their definitions.
|
|
400
|
+
*
|
|
401
|
+
* @see {@link defineApi} for constructing this contract.
|
|
402
|
+
* @see {@link @simplix-react/react!deriveHooks | deriveHooks} for deriving React hooks.
|
|
403
|
+
* @see {@link @simplix-react/mock!deriveMockHandlers | deriveMockHandlers} for deriving mock handlers.
|
|
404
|
+
*/
|
|
48
405
|
interface ApiContract<TEntities extends Record<string, EntityDefinition<any, any, any>>, TOperations extends Record<string, OperationDefinition<any, any>>> {
|
|
406
|
+
/** The original contract configuration. */
|
|
49
407
|
config: ApiContractConfig<TEntities, TOperations>;
|
|
408
|
+
/** Type-safe HTTP client with methods for each entity and operation. */
|
|
50
409
|
client: {
|
|
51
410
|
[K in keyof TEntities]: EntityClient<TEntities[K]["schema"], TEntities[K]["createSchema"], TEntities[K]["updateSchema"]>;
|
|
52
411
|
} & {
|
|
53
412
|
[K in keyof TOperations]: TOperations[K] extends OperationDefinition<infer _TInput, infer TOutput> ? (...args: unknown[]) => Promise<z.infer<TOutput>> : never;
|
|
54
413
|
};
|
|
414
|
+
/** Query key factories for cache management, one per entity. */
|
|
55
415
|
queryKeys: {
|
|
56
416
|
[K in keyof TEntities]: QueryKeyFactory;
|
|
57
417
|
};
|
|
58
418
|
}
|
|
59
419
|
|
|
420
|
+
/**
|
|
421
|
+
* Creates a fully-typed API contract from an {@link ApiContractConfig}.
|
|
422
|
+
*
|
|
423
|
+
* Serves as the main entry point for `@simplix-react/contract`. Takes a
|
|
424
|
+
* declarative config of entities and operations, then derives a type-safe
|
|
425
|
+
* HTTP client and query key factories. The returned contract is consumed
|
|
426
|
+
* by `@simplix-react/react` for hooks and `@simplix-react/mock` for MSW handlers.
|
|
427
|
+
*
|
|
428
|
+
* @typeParam TEntities - Map of entity names to their {@link EntityDefinition}s.
|
|
429
|
+
* @typeParam TOperations - Map of operation names to their {@link OperationDefinition}s.
|
|
430
|
+
*
|
|
431
|
+
* @param config - The API contract configuration defining entities, operations, and shared settings.
|
|
432
|
+
* @param options - Optional settings for customizing the contract.
|
|
433
|
+
* @param options.fetchFn - Custom fetch function replacing the built-in {@link defaultFetch}.
|
|
434
|
+
* @returns An {@link ApiContract} containing `config`, `client`, and `queryKeys`.
|
|
435
|
+
*
|
|
436
|
+
* @example
|
|
437
|
+
* ```ts
|
|
438
|
+
* import { z } from "zod";
|
|
439
|
+
* import { defineApi, simpleQueryBuilder } from "@simplix-react/contract";
|
|
440
|
+
*
|
|
441
|
+
* const projectApi = defineApi({
|
|
442
|
+
* domain: "project",
|
|
443
|
+
* basePath: "/api/v1",
|
|
444
|
+
* entities: {
|
|
445
|
+
* task: {
|
|
446
|
+
* path: "/tasks",
|
|
447
|
+
* schema: z.object({ id: z.string(), title: z.string() }),
|
|
448
|
+
* createSchema: z.object({ title: z.string() }),
|
|
449
|
+
* updateSchema: z.object({ title: z.string().optional() }),
|
|
450
|
+
* },
|
|
451
|
+
* },
|
|
452
|
+
* queryBuilder: simpleQueryBuilder,
|
|
453
|
+
* });
|
|
454
|
+
*
|
|
455
|
+
* // Type-safe client usage
|
|
456
|
+
* const tasks = await projectApi.client.task.list();
|
|
457
|
+
* const task = await projectApi.client.task.get("task-1");
|
|
458
|
+
*
|
|
459
|
+
* // Query keys for TanStack Query
|
|
460
|
+
* projectApi.queryKeys.task.all; // ["project", "task"]
|
|
461
|
+
* projectApi.queryKeys.task.detail("task-1"); // ["project", "task", "detail", "task-1"]
|
|
462
|
+
* ```
|
|
463
|
+
*
|
|
464
|
+
* @see {@link ApiContractConfig} for the full config shape.
|
|
465
|
+
* @see {@link @simplix-react/react!deriveHooks | deriveHooks} for deriving React Query hooks.
|
|
466
|
+
* @see {@link @simplix-react/mock!deriveMockHandlers | deriveMockHandlers} for deriving MSW handlers.
|
|
467
|
+
*/
|
|
60
468
|
declare function defineApi<TEntities extends Record<string, EntityDefinition<any, any, any>>, TOperations extends Record<string, OperationDefinition<any, any>> = Record<string, never>>(config: ApiContractConfig<TEntities, TOperations>, options?: {
|
|
61
469
|
fetchFn?: FetchFn;
|
|
62
470
|
}): {
|
|
@@ -65,38 +473,219 @@ declare function defineApi<TEntities extends Record<string, EntityDefinition<any
|
|
|
65
473
|
queryKeys: { [K in keyof TEntities]: QueryKeyFactory; };
|
|
66
474
|
};
|
|
67
475
|
|
|
476
|
+
/**
|
|
477
|
+
* Derives a type-safe HTTP client from an {@link ApiContractConfig}.
|
|
478
|
+
*
|
|
479
|
+
* Iterates over all entities and operations in the config and generates
|
|
480
|
+
* corresponding CRUD methods and operation functions. Each entity produces
|
|
481
|
+
* `list`, `get`, `create`, `update`, and `delete` methods. Each operation
|
|
482
|
+
* produces a callable function with positional path parameter arguments.
|
|
483
|
+
*
|
|
484
|
+
* Typically called internally by {@link defineApi} rather than used directly.
|
|
485
|
+
*
|
|
486
|
+
* @typeParam TEntities - Map of entity names to their definitions.
|
|
487
|
+
* @typeParam TOperations - Map of operation names to their definitions.
|
|
488
|
+
* @param config - The API contract configuration.
|
|
489
|
+
* @param fetchFn - Custom fetch function; defaults to {@link defaultFetch}.
|
|
490
|
+
* @returns A client object with typed methods for each entity and operation.
|
|
491
|
+
*
|
|
492
|
+
* @example
|
|
493
|
+
* ```ts
|
|
494
|
+
* import { deriveClient } from "@simplix-react/contract";
|
|
495
|
+
*
|
|
496
|
+
* const client = deriveClient(config);
|
|
497
|
+
* const tasks = await client.task.list();
|
|
498
|
+
* ```
|
|
499
|
+
*
|
|
500
|
+
* @see {@link defineApi} for the recommended high-level API.
|
|
501
|
+
*/
|
|
68
502
|
declare function deriveClient<TEntities extends Record<string, EntityDefinition<any, any, any>>, TOperations extends Record<string, OperationDefinition<any, any>>>(config: ApiContractConfig<TEntities, TOperations>, fetchFn?: FetchFn): Record<string, unknown>;
|
|
69
503
|
|
|
504
|
+
/**
|
|
505
|
+
* Derives a set of {@link QueryKeyFactory} instances for all entities in a contract.
|
|
506
|
+
*
|
|
507
|
+
* Generates structured query keys following the factory pattern recommended
|
|
508
|
+
* by TanStack Query. Each entity receives keys scoped by `[domain, entityName]`,
|
|
509
|
+
* enabling granular cache invalidation (e.g. invalidate all task lists without
|
|
510
|
+
* affecting task details).
|
|
511
|
+
*
|
|
512
|
+
* Typically called internally by {@link defineApi} rather than used directly.
|
|
513
|
+
*
|
|
514
|
+
* @typeParam TEntities - Map of entity names to their definitions.
|
|
515
|
+
* @param config - Subset of the API contract config containing `domain` and `entities`.
|
|
516
|
+
* @returns A map of entity names to their {@link QueryKeyFactory} instances.
|
|
517
|
+
*
|
|
518
|
+
* @example
|
|
519
|
+
* ```ts
|
|
520
|
+
* import { deriveQueryKeys } from "@simplix-react/contract";
|
|
521
|
+
*
|
|
522
|
+
* const queryKeys = deriveQueryKeys({ domain: "project", entities: { task: taskEntity } });
|
|
523
|
+
*
|
|
524
|
+
* queryKeys.task.all; // ["project", "task"]
|
|
525
|
+
* queryKeys.task.lists(); // ["project", "task", "list"]
|
|
526
|
+
* queryKeys.task.detail("abc"); // ["project", "task", "detail", "abc"]
|
|
527
|
+
* ```
|
|
528
|
+
*
|
|
529
|
+
* @see {@link defineApi} for the recommended high-level API.
|
|
530
|
+
* @see {@link QueryKeyFactory} for the generated key structure.
|
|
531
|
+
*/
|
|
70
532
|
declare function deriveQueryKeys<TEntities extends Record<string, EntityDefinition<any, any, any>>>(config: Pick<ApiContractConfig<TEntities>, "domain" | "entities">): {
|
|
71
533
|
[K in keyof TEntities]: QueryKeyFactory;
|
|
72
534
|
};
|
|
73
535
|
|
|
74
536
|
/**
|
|
75
|
-
*
|
|
76
|
-
*
|
|
77
|
-
*
|
|
537
|
+
* Substitutes `:paramName` placeholders in a URL path template with actual values.
|
|
538
|
+
*
|
|
539
|
+
* Values are URI-encoded to ensure safe inclusion in URLs. Used internally by
|
|
540
|
+
* {@link deriveClient} for building operation URLs, and available as a public
|
|
541
|
+
* utility for custom URL construction.
|
|
542
|
+
*
|
|
543
|
+
* @param template - URL path template with `:paramName` placeholders.
|
|
544
|
+
* @param params - Map of parameter names to their string values.
|
|
545
|
+
* @returns The resolved URL path with all placeholders replaced.
|
|
546
|
+
*
|
|
547
|
+
* @example
|
|
548
|
+
* ```ts
|
|
549
|
+
* import { buildPath } from "@simplix-react/contract";
|
|
550
|
+
*
|
|
551
|
+
* buildPath("/projects/:projectId/tasks", { projectId: "abc" });
|
|
552
|
+
* // "/projects/abc/tasks"
|
|
553
|
+
*
|
|
554
|
+
* buildPath("/tasks/:taskId/assign", { taskId: "task-1" });
|
|
555
|
+
* // "/tasks/task-1/assign"
|
|
556
|
+
* ```
|
|
78
557
|
*/
|
|
79
558
|
declare function buildPath(template: string, params?: Record<string, string>): string;
|
|
80
559
|
|
|
560
|
+
/**
|
|
561
|
+
* Represents an HTTP error response from the API.
|
|
562
|
+
*
|
|
563
|
+
* Thrown by {@link defaultFetch} and the internal multipart/blob fetchers when
|
|
564
|
+
* the server responds with a non-2xx status code. Captures both the HTTP status
|
|
565
|
+
* and the raw response body for debugging.
|
|
566
|
+
*
|
|
567
|
+
* @example
|
|
568
|
+
* ```ts
|
|
569
|
+
* import { ApiError } from "@simplix-react/contract";
|
|
570
|
+
*
|
|
571
|
+
* try {
|
|
572
|
+
* await api.client.task.get("nonexistent");
|
|
573
|
+
* } catch (error) {
|
|
574
|
+
* if (error instanceof ApiError) {
|
|
575
|
+
* console.log(error.status); // 404
|
|
576
|
+
* console.log(error.body); // "Not Found"
|
|
577
|
+
* }
|
|
578
|
+
* }
|
|
579
|
+
* ```
|
|
580
|
+
*/
|
|
81
581
|
declare class ApiError extends Error {
|
|
582
|
+
/** HTTP status code of the failed response. */
|
|
82
583
|
readonly status: number;
|
|
584
|
+
/** Raw response body text. */
|
|
83
585
|
readonly body: string;
|
|
84
|
-
constructor(
|
|
586
|
+
constructor(
|
|
587
|
+
/** HTTP status code of the failed response. */
|
|
588
|
+
status: number,
|
|
589
|
+
/** Raw response body text. */
|
|
590
|
+
body: string);
|
|
85
591
|
}
|
|
86
592
|
/**
|
|
87
|
-
*
|
|
593
|
+
* Performs an HTTP request with automatic JSON content-type headers and
|
|
594
|
+
* `{ data: T }` envelope unwrapping.
|
|
595
|
+
*
|
|
596
|
+
* Serves as the built-in fetch implementation used by {@link deriveClient}
|
|
597
|
+
* when no custom `fetchFn` is provided. Returns `undefined` for 204 No Content
|
|
598
|
+
* responses and throws {@link ApiError} for non-2xx status codes.
|
|
599
|
+
*
|
|
600
|
+
* @typeParam T - The expected deserialized response type.
|
|
601
|
+
* @param path - The full URL path to fetch.
|
|
602
|
+
* @param options - Standard `RequestInit` options forwarded to the native `fetch`.
|
|
603
|
+
* @returns The unwrapped response data.
|
|
604
|
+
* @throws {@link ApiError} When the response status is not OK.
|
|
605
|
+
*
|
|
606
|
+
* @example
|
|
607
|
+
* ```ts
|
|
608
|
+
* import { defineApi, defaultFetch } from "@simplix-react/contract";
|
|
609
|
+
*
|
|
610
|
+
* // Use as-is (default behavior)
|
|
611
|
+
* const api = defineApi(config);
|
|
612
|
+
*
|
|
613
|
+
* // Or provide a custom wrapper
|
|
614
|
+
* const api = defineApi(config, {
|
|
615
|
+
* fetchFn: async (path, options) => {
|
|
616
|
+
* // Add auth header, then delegate to defaultFetch
|
|
617
|
+
* return defaultFetch(path, {
|
|
618
|
+
* ...options,
|
|
619
|
+
* headers: { ...options?.headers, Authorization: `Bearer ${token}` },
|
|
620
|
+
* });
|
|
621
|
+
* },
|
|
622
|
+
* });
|
|
623
|
+
* ```
|
|
88
624
|
*/
|
|
89
625
|
declare function defaultFetch<T>(path: string, options?: RequestInit): Promise<T>;
|
|
90
626
|
|
|
91
627
|
/**
|
|
92
|
-
*
|
|
93
|
-
*
|
|
628
|
+
* Converts a camelCase string to kebab-case.
|
|
629
|
+
*
|
|
630
|
+
* Used internally for transforming entity names into URL-friendly path segments.
|
|
631
|
+
*
|
|
632
|
+
* @param str - The camelCase string to convert.
|
|
633
|
+
* @returns The kebab-case equivalent.
|
|
634
|
+
*
|
|
635
|
+
* @example
|
|
636
|
+
* ```ts
|
|
637
|
+
* import { camelToKebab } from "@simplix-react/contract";
|
|
638
|
+
*
|
|
639
|
+
* camelToKebab("doorReader"); // "door-reader"
|
|
640
|
+
* camelToKebab("myEntity"); // "my-entity"
|
|
641
|
+
* ```
|
|
94
642
|
*/
|
|
95
643
|
declare function camelToKebab(str: string): string;
|
|
96
644
|
/**
|
|
97
|
-
*
|
|
98
|
-
*
|
|
645
|
+
* Converts a camelCase string to snake_case.
|
|
646
|
+
*
|
|
647
|
+
* Used internally for transforming entity names into database-friendly column names.
|
|
648
|
+
*
|
|
649
|
+
* @param str - The camelCase string to convert.
|
|
650
|
+
* @returns The snake_case equivalent.
|
|
651
|
+
*
|
|
652
|
+
* @example
|
|
653
|
+
* ```ts
|
|
654
|
+
* import { camelToSnake } from "@simplix-react/contract";
|
|
655
|
+
*
|
|
656
|
+
* camelToSnake("doorReader"); // "door_reader"
|
|
657
|
+
* camelToSnake("myEntity"); // "my_entity"
|
|
658
|
+
* ```
|
|
99
659
|
*/
|
|
100
660
|
declare function camelToSnake(str: string): string;
|
|
101
661
|
|
|
102
|
-
|
|
662
|
+
/**
|
|
663
|
+
* Provides a straightforward {@link QueryBuilder} implementation for common REST APIs.
|
|
664
|
+
*
|
|
665
|
+
* Serializes filters as flat key-value pairs, sort as `field:direction` comma-separated
|
|
666
|
+
* values, and pagination as `page`/`limit` (offset) or `cursor`/`limit` (cursor-based).
|
|
667
|
+
*
|
|
668
|
+
* @example
|
|
669
|
+
* ```ts
|
|
670
|
+
* import { defineApi, simpleQueryBuilder } from "@simplix-react/contract";
|
|
671
|
+
*
|
|
672
|
+
* const api = defineApi({
|
|
673
|
+
* domain: "project",
|
|
674
|
+
* basePath: "/api/v1",
|
|
675
|
+
* entities: { task: taskEntity },
|
|
676
|
+
* queryBuilder: simpleQueryBuilder,
|
|
677
|
+
* });
|
|
678
|
+
*
|
|
679
|
+
* // Produces: /api/v1/tasks?status=pending&sort=name:asc&page=1&limit=10
|
|
680
|
+
* await api.client.task.list({
|
|
681
|
+
* filters: { status: "pending" },
|
|
682
|
+
* sort: { field: "name", direction: "asc" },
|
|
683
|
+
* pagination: { type: "offset", page: 1, limit: 10 },
|
|
684
|
+
* });
|
|
685
|
+
* ```
|
|
686
|
+
*
|
|
687
|
+
* @see {@link QueryBuilder} for implementing custom serialization strategies.
|
|
688
|
+
*/
|
|
689
|
+
declare const simpleQueryBuilder: QueryBuilder;
|
|
690
|
+
|
|
691
|
+
export { type ApiContract, type ApiContractConfig, ApiError, type EntityClient, type EntityDefinition, type EntityParent, type EntityQuery, type FetchFn, type HttpMethod, type ListParams, type OperationDefinition, type PageInfo, type PaginationParam, type QueryBuilder, type QueryKeyFactory, type SortParam, buildPath, camelToKebab, camelToSnake, defaultFetch, defineApi, deriveClient, deriveQueryKeys, simpleQueryBuilder };
|
package/dist/index.js
CHANGED
|
@@ -36,10 +36,10 @@ async function defaultFetch(path, options) {
|
|
|
36
36
|
|
|
37
37
|
// src/derive/client.ts
|
|
38
38
|
function deriveClient(config, fetchFn = defaultFetch) {
|
|
39
|
-
const { basePath, entities, operations } = config;
|
|
39
|
+
const { basePath, entities, operations, queryBuilder } = config;
|
|
40
40
|
const result = {};
|
|
41
41
|
for (const [name, entity] of Object.entries(entities)) {
|
|
42
|
-
result[name] = createEntityClient(basePath, entity, fetchFn);
|
|
42
|
+
result[name] = createEntityClient(basePath, entity, fetchFn, queryBuilder);
|
|
43
43
|
}
|
|
44
44
|
if (operations) {
|
|
45
45
|
for (const [name, operation] of Object.entries(operations)) {
|
|
@@ -48,11 +48,24 @@ function deriveClient(config, fetchFn = defaultFetch) {
|
|
|
48
48
|
}
|
|
49
49
|
return result;
|
|
50
50
|
}
|
|
51
|
-
function createEntityClient(basePath, entity, fetchFn) {
|
|
51
|
+
function createEntityClient(basePath, entity, fetchFn, queryBuilder) {
|
|
52
52
|
const { path, parent } = entity;
|
|
53
53
|
return {
|
|
54
|
-
list(
|
|
55
|
-
|
|
54
|
+
list(parentIdOrParams, params) {
|
|
55
|
+
let parentId;
|
|
56
|
+
let listParams;
|
|
57
|
+
if (typeof parentIdOrParams === "string") {
|
|
58
|
+
parentId = parentIdOrParams;
|
|
59
|
+
listParams = params;
|
|
60
|
+
} else {
|
|
61
|
+
listParams = parentIdOrParams;
|
|
62
|
+
}
|
|
63
|
+
let url = parent && parentId ? `${basePath}${parent.path}/${parentId}${path}` : `${basePath}${path}`;
|
|
64
|
+
if (listParams && queryBuilder) {
|
|
65
|
+
const sp = queryBuilder.buildSearchParams(listParams);
|
|
66
|
+
const qs = sp.toString();
|
|
67
|
+
if (qs) url += `?${qs}`;
|
|
68
|
+
}
|
|
56
69
|
return fetchFn(url);
|
|
57
70
|
},
|
|
58
71
|
get(id) {
|
|
@@ -101,6 +114,12 @@ function createOperationClient(basePath, operation, fetchFn) {
|
|
|
101
114
|
inputData = args[argIndex];
|
|
102
115
|
}
|
|
103
116
|
const url = `${basePath}${buildPath(operation.path, pathParams)}`;
|
|
117
|
+
if (operation.contentType === "multipart" && inputData !== void 0) {
|
|
118
|
+
return multipartFetch(url, operation.method, toFormData(inputData), operation.responseType);
|
|
119
|
+
}
|
|
120
|
+
if (operation.responseType === "blob") {
|
|
121
|
+
return blobFetch(url, operation.method, inputData);
|
|
122
|
+
}
|
|
104
123
|
const options = { method: operation.method };
|
|
105
124
|
if (inputData !== void 0 && operation.method !== "GET") {
|
|
106
125
|
options.body = JSON.stringify(inputData);
|
|
@@ -108,6 +127,42 @@ function createOperationClient(basePath, operation, fetchFn) {
|
|
|
108
127
|
return fetchFn(url, options);
|
|
109
128
|
};
|
|
110
129
|
}
|
|
130
|
+
function toFormData(data) {
|
|
131
|
+
const formData = new FormData();
|
|
132
|
+
if (data && typeof data === "object") {
|
|
133
|
+
for (const [key, value] of Object.entries(data)) {
|
|
134
|
+
if (value instanceof File || value instanceof Blob) {
|
|
135
|
+
formData.append(key, value);
|
|
136
|
+
} else if (value !== void 0 && value !== null) {
|
|
137
|
+
formData.append(key, String(value));
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
return formData;
|
|
142
|
+
}
|
|
143
|
+
async function multipartFetch(url, method, formData, responseType) {
|
|
144
|
+
const res = await fetch(url, { method, body: formData });
|
|
145
|
+
if (!res.ok) {
|
|
146
|
+
throw new ApiError(res.status, await res.text());
|
|
147
|
+
}
|
|
148
|
+
if (responseType === "blob") {
|
|
149
|
+
return res.blob();
|
|
150
|
+
}
|
|
151
|
+
const json = await res.json();
|
|
152
|
+
return json.data !== void 0 ? json.data : json;
|
|
153
|
+
}
|
|
154
|
+
async function blobFetch(url, method, inputData) {
|
|
155
|
+
const options = { method };
|
|
156
|
+
if (inputData !== void 0 && method !== "GET") {
|
|
157
|
+
options.body = JSON.stringify(inputData);
|
|
158
|
+
options.headers = { "Content-Type": "application/json" };
|
|
159
|
+
}
|
|
160
|
+
const res = await fetch(url, options);
|
|
161
|
+
if (!res.ok) {
|
|
162
|
+
throw new ApiError(res.status, await res.text());
|
|
163
|
+
}
|
|
164
|
+
return res.blob();
|
|
165
|
+
}
|
|
111
166
|
|
|
112
167
|
// src/derive/query-keys.ts
|
|
113
168
|
function deriveQueryKeys(config) {
|
|
@@ -145,4 +200,33 @@ function camelToSnake(str) {
|
|
|
145
200
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
146
201
|
}
|
|
147
202
|
|
|
148
|
-
|
|
203
|
+
// src/helpers/query-builders.ts
|
|
204
|
+
var simpleQueryBuilder = {
|
|
205
|
+
buildSearchParams(params) {
|
|
206
|
+
const sp = new URLSearchParams();
|
|
207
|
+
if (params.filters) {
|
|
208
|
+
for (const [k, v] of Object.entries(params.filters)) {
|
|
209
|
+
if (v !== void 0 && v !== null) sp.set(k, String(v));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
if (params.sort) {
|
|
213
|
+
const sorts = Array.isArray(params.sort) ? params.sort : [params.sort];
|
|
214
|
+
sp.set(
|
|
215
|
+
"sort",
|
|
216
|
+
sorts.map((s) => `${s.field}:${s.direction}`).join(",")
|
|
217
|
+
);
|
|
218
|
+
}
|
|
219
|
+
if (params.pagination) {
|
|
220
|
+
if (params.pagination.type === "offset") {
|
|
221
|
+
sp.set("page", String(params.pagination.page));
|
|
222
|
+
sp.set("limit", String(params.pagination.limit));
|
|
223
|
+
} else {
|
|
224
|
+
sp.set("cursor", params.pagination.cursor);
|
|
225
|
+
sp.set("limit", String(params.pagination.limit));
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
return sp;
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
export { ApiError, buildPath, camelToKebab, camelToSnake, defaultFetch, defineApi, deriveClient, deriveQueryKeys, simpleQueryBuilder };
|
package/package.json
CHANGED
|
@@ -1,23 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplix-react/contract",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.2",
|
|
4
4
|
"description": "Define type-safe API contracts with Zod schemas",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"exports": {
|
|
7
7
|
".": {
|
|
8
8
|
"types": "./dist/index.d.ts",
|
|
9
9
|
"import": "./dist/index.js"
|
|
10
|
-
},
|
|
11
|
-
"./openapi": {
|
|
12
|
-
"types": "./dist/openapi.d.ts",
|
|
13
|
-
"import": "./dist/openapi.js"
|
|
14
|
-
},
|
|
15
|
-
"./mcp": {
|
|
16
|
-
"types": "./dist/mcp-tools.d.ts",
|
|
17
|
-
"import": "./dist/mcp-tools.js"
|
|
18
10
|
}
|
|
19
11
|
},
|
|
20
|
-
"files": [
|
|
12
|
+
"files": [
|
|
13
|
+
"dist"
|
|
14
|
+
],
|
|
15
|
+
"publishConfig": {
|
|
16
|
+
"access": "public"
|
|
17
|
+
},
|
|
21
18
|
"scripts": {
|
|
22
19
|
"build": "tsup",
|
|
23
20
|
"dev": "tsup --watch",
|
|
@@ -30,6 +27,7 @@
|
|
|
30
27
|
"zod": ">=4.0.0"
|
|
31
28
|
},
|
|
32
29
|
"devDependencies": {
|
|
30
|
+
"@simplix-react/config-eslint": "workspace:*",
|
|
33
31
|
"@simplix-react/config-typescript": "workspace:*",
|
|
34
32
|
"eslint": "^9.39.2",
|
|
35
33
|
"tsup": "^8.5.1",
|
package/dist/mcp-tools.d.ts
DELETED
|
@@ -1,14 +0,0 @@
|
|
|
1
|
-
import { E as EntityDefinition, O as OperationDefinition, A as ApiContractConfig } from './types-tFXBXgJP.js';
|
|
2
|
-
import 'zod';
|
|
3
|
-
|
|
4
|
-
interface MCPToolDefinition {
|
|
5
|
-
name: string;
|
|
6
|
-
description: string;
|
|
7
|
-
inputSchema: Record<string, unknown>;
|
|
8
|
-
}
|
|
9
|
-
/**
|
|
10
|
-
* Derive MCP tool definitions from contract config.
|
|
11
|
-
*/
|
|
12
|
-
declare function deriveMCPTools<TEntities extends Record<string, EntityDefinition<any, any, any>>, TOperations extends Record<string, OperationDefinition<any, any>>>(config: ApiContractConfig<TEntities, TOperations>): MCPToolDefinition[];
|
|
13
|
-
|
|
14
|
-
export { type MCPToolDefinition, deriveMCPTools };
|
package/dist/mcp-tools.js
DELETED
|
@@ -1,62 +0,0 @@
|
|
|
1
|
-
// src/derive/mcp-tools.ts
|
|
2
|
-
function deriveMCPTools(config) {
|
|
3
|
-
const tools = [];
|
|
4
|
-
for (const [name, entity] of Object.entries(config.entities)) {
|
|
5
|
-
const capitalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
6
|
-
tools.push(
|
|
7
|
-
{
|
|
8
|
-
name: `list${capitalName}s`,
|
|
9
|
-
description: `List all ${name} entities`,
|
|
10
|
-
inputSchema: entity.parent ? {
|
|
11
|
-
type: "object",
|
|
12
|
-
properties: { [entity.parent.param]: { type: "string" } },
|
|
13
|
-
required: [entity.parent.param]
|
|
14
|
-
} : { type: "object", properties: {} }
|
|
15
|
-
},
|
|
16
|
-
{
|
|
17
|
-
name: `get${capitalName}`,
|
|
18
|
-
description: `Get a single ${name} by ID`,
|
|
19
|
-
inputSchema: {
|
|
20
|
-
type: "object",
|
|
21
|
-
properties: { id: { type: "string" } },
|
|
22
|
-
required: ["id"]
|
|
23
|
-
}
|
|
24
|
-
},
|
|
25
|
-
{
|
|
26
|
-
name: `create${capitalName}`,
|
|
27
|
-
description: `Create a new ${name}`,
|
|
28
|
-
inputSchema: { type: "object", properties: {} }
|
|
29
|
-
},
|
|
30
|
-
{
|
|
31
|
-
name: `update${capitalName}`,
|
|
32
|
-
description: `Update an existing ${name}`,
|
|
33
|
-
inputSchema: {
|
|
34
|
-
type: "object",
|
|
35
|
-
properties: { id: { type: "string" } },
|
|
36
|
-
required: ["id"]
|
|
37
|
-
}
|
|
38
|
-
},
|
|
39
|
-
{
|
|
40
|
-
name: `delete${capitalName}`,
|
|
41
|
-
description: `Delete a ${name}`,
|
|
42
|
-
inputSchema: {
|
|
43
|
-
type: "object",
|
|
44
|
-
properties: { id: { type: "string" } },
|
|
45
|
-
required: ["id"]
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
);
|
|
49
|
-
}
|
|
50
|
-
if (config.operations) {
|
|
51
|
-
for (const [name, op] of Object.entries(config.operations)) {
|
|
52
|
-
tools.push({
|
|
53
|
-
name,
|
|
54
|
-
description: `${op.method} ${op.path}`,
|
|
55
|
-
inputSchema: { type: "object", properties: {} }
|
|
56
|
-
});
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
return tools;
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
export { deriveMCPTools };
|
package/dist/openapi.d.ts
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
import { E as EntityDefinition, O as OperationDefinition, A as ApiContractConfig } from './types-tFXBXgJP.js';
|
|
2
|
-
import 'zod';
|
|
3
|
-
|
|
4
|
-
interface OpenAPISpec {
|
|
5
|
-
openapi: "3.1.0";
|
|
6
|
-
info: {
|
|
7
|
-
title: string;
|
|
8
|
-
version: string;
|
|
9
|
-
};
|
|
10
|
-
paths: Record<string, unknown>;
|
|
11
|
-
}
|
|
12
|
-
/**
|
|
13
|
-
* Derive OpenAPI 3.1 specification from contract config.
|
|
14
|
-
* Full implementation requires zod-to-json-schema.
|
|
15
|
-
*/
|
|
16
|
-
declare function deriveOpenAPI<TEntities extends Record<string, EntityDefinition<any, any, any>>, TOperations extends Record<string, OperationDefinition<any, any>>>(config: ApiContractConfig<TEntities, TOperations>, options?: {
|
|
17
|
-
title?: string;
|
|
18
|
-
version?: string;
|
|
19
|
-
}): OpenAPISpec;
|
|
20
|
-
|
|
21
|
-
export { type OpenAPISpec, deriveOpenAPI };
|
package/dist/openapi.js
DELETED
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
// src/derive/openapi.ts
|
|
2
|
-
function deriveOpenAPI(config, options) {
|
|
3
|
-
const title = options?.title ?? config.domain;
|
|
4
|
-
const version = options?.version ?? "0.1.0";
|
|
5
|
-
const paths = {};
|
|
6
|
-
for (const [name, entity] of Object.entries(config.entities)) {
|
|
7
|
-
const entityPath = entity.parent ? `${config.basePath}${entity.parent.path}/{${entity.parent.param}}${entity.path}` : `${config.basePath}${entity.path}`;
|
|
8
|
-
const capitalName = name.charAt(0).toUpperCase() + name.slice(1);
|
|
9
|
-
paths[entityPath] = {
|
|
10
|
-
get: {
|
|
11
|
-
summary: `List ${name}`,
|
|
12
|
-
operationId: `list${capitalName}`,
|
|
13
|
-
responses: { "200": { description: "Success" } }
|
|
14
|
-
},
|
|
15
|
-
post: {
|
|
16
|
-
summary: `Create ${name}`,
|
|
17
|
-
operationId: `create${capitalName}`,
|
|
18
|
-
responses: { "201": { description: "Created" } }
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
paths[`${config.basePath}${entity.path}/{id}`] = {
|
|
22
|
-
get: {
|
|
23
|
-
summary: `Get ${name}`,
|
|
24
|
-
operationId: `get${capitalName}`,
|
|
25
|
-
responses: { "200": { description: "Success" } }
|
|
26
|
-
},
|
|
27
|
-
patch: {
|
|
28
|
-
summary: `Update ${name}`,
|
|
29
|
-
operationId: `update${capitalName}`,
|
|
30
|
-
responses: { "200": { description: "Success" } }
|
|
31
|
-
},
|
|
32
|
-
delete: {
|
|
33
|
-
summary: `Delete ${name}`,
|
|
34
|
-
operationId: `delete${capitalName}`,
|
|
35
|
-
responses: { "204": { description: "Deleted" } }
|
|
36
|
-
}
|
|
37
|
-
};
|
|
38
|
-
}
|
|
39
|
-
if (config.operations) {
|
|
40
|
-
for (const [name, op] of Object.entries(config.operations)) {
|
|
41
|
-
const opPath = `${config.basePath}${op.path}`;
|
|
42
|
-
paths[opPath] = {
|
|
43
|
-
[op.method.toLowerCase()]: {
|
|
44
|
-
summary: name,
|
|
45
|
-
operationId: name,
|
|
46
|
-
responses: { "200": { description: "Success" } }
|
|
47
|
-
}
|
|
48
|
-
};
|
|
49
|
-
}
|
|
50
|
-
}
|
|
51
|
-
return { openapi: "3.1.0", info: { title, version }, paths };
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
export { deriveOpenAPI };
|
package/dist/types-tFXBXgJP.d.ts
DELETED
|
@@ -1,41 +0,0 @@
|
|
|
1
|
-
import { z } from 'zod';
|
|
2
|
-
|
|
3
|
-
interface EntityParent {
|
|
4
|
-
param: string;
|
|
5
|
-
path: string;
|
|
6
|
-
}
|
|
7
|
-
interface EntityQuery {
|
|
8
|
-
parent: string;
|
|
9
|
-
param: string;
|
|
10
|
-
}
|
|
11
|
-
interface EntityDefinition<TSchema extends z.ZodType = z.ZodType, TCreate extends z.ZodType = z.ZodType, TUpdate extends z.ZodType = z.ZodType> {
|
|
12
|
-
path: string;
|
|
13
|
-
schema: TSchema;
|
|
14
|
-
createSchema: TCreate;
|
|
15
|
-
updateSchema: TUpdate;
|
|
16
|
-
parent?: EntityParent;
|
|
17
|
-
queries?: Record<string, EntityQuery>;
|
|
18
|
-
}
|
|
19
|
-
type HttpMethod = "GET" | "POST" | "PUT" | "PATCH" | "DELETE";
|
|
20
|
-
interface OperationDefinition<TInput extends z.ZodType = z.ZodType, TOutput extends z.ZodType = z.ZodType> {
|
|
21
|
-
method: HttpMethod;
|
|
22
|
-
path: string;
|
|
23
|
-
input: TInput;
|
|
24
|
-
output: TOutput;
|
|
25
|
-
invalidates?: (queryKeys: Record<string, QueryKeyFactory>, params: Record<string, string>) => readonly unknown[][];
|
|
26
|
-
}
|
|
27
|
-
interface ApiContractConfig<TEntities extends Record<string, EntityDefinition<any, any, any>> = Record<string, EntityDefinition>, TOperations extends Record<string, OperationDefinition<any, any>> = Record<string, OperationDefinition>> {
|
|
28
|
-
domain: string;
|
|
29
|
-
basePath: string;
|
|
30
|
-
entities: TEntities;
|
|
31
|
-
operations?: TOperations;
|
|
32
|
-
}
|
|
33
|
-
interface QueryKeyFactory {
|
|
34
|
-
all: readonly unknown[];
|
|
35
|
-
lists: () => readonly unknown[];
|
|
36
|
-
list: (params: Record<string, unknown>) => readonly unknown[];
|
|
37
|
-
details: () => readonly unknown[];
|
|
38
|
-
detail: (id: string) => readonly unknown[];
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export type { ApiContractConfig as A, EntityDefinition as E, OperationDefinition as O };
|