@simplix-react/contract 0.0.1 → 0.0.3
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 +606 -11
- package/dist/index.js +99 -12
- package/package.json +19 -20
- 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/dist/index.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
function buildPath(template, params = {}) {
|
|
3
3
|
let result = template;
|
|
4
4
|
for (const [key, value] of Object.entries(params)) {
|
|
5
|
-
result = result.
|
|
5
|
+
result = result.replaceAll(`:${key}`, encodeURIComponent(value));
|
|
6
6
|
}
|
|
7
7
|
return result;
|
|
8
8
|
}
|
|
@@ -17,12 +17,15 @@ var ApiError = class extends Error {
|
|
|
17
17
|
}
|
|
18
18
|
};
|
|
19
19
|
async function defaultFetch(path, options) {
|
|
20
|
+
const method = options?.method?.toUpperCase();
|
|
21
|
+
const hasBody = method === "POST" || method === "PUT" || method === "PATCH";
|
|
22
|
+
const headers = {
|
|
23
|
+
...hasBody ? { "Content-Type": "application/json" } : {},
|
|
24
|
+
...options?.headers
|
|
25
|
+
};
|
|
20
26
|
const res = await fetch(path, {
|
|
21
27
|
...options,
|
|
22
|
-
headers
|
|
23
|
-
"Content-Type": "application/json",
|
|
24
|
-
...options?.headers
|
|
25
|
-
}
|
|
28
|
+
headers
|
|
26
29
|
});
|
|
27
30
|
if (!res.ok) {
|
|
28
31
|
throw new ApiError(res.status, await res.text());
|
|
@@ -36,10 +39,10 @@ async function defaultFetch(path, options) {
|
|
|
36
39
|
|
|
37
40
|
// src/derive/client.ts
|
|
38
41
|
function deriveClient(config, fetchFn = defaultFetch) {
|
|
39
|
-
const { basePath, entities, operations } = config;
|
|
42
|
+
const { basePath, entities, operations, queryBuilder } = config;
|
|
40
43
|
const result = {};
|
|
41
44
|
for (const [name, entity] of Object.entries(entities)) {
|
|
42
|
-
result[name] = createEntityClient(basePath, entity, fetchFn);
|
|
45
|
+
result[name] = createEntityClient(basePath, entity, fetchFn, queryBuilder);
|
|
43
46
|
}
|
|
44
47
|
if (operations) {
|
|
45
48
|
for (const [name, operation] of Object.entries(operations)) {
|
|
@@ -48,11 +51,24 @@ function deriveClient(config, fetchFn = defaultFetch) {
|
|
|
48
51
|
}
|
|
49
52
|
return result;
|
|
50
53
|
}
|
|
51
|
-
function createEntityClient(basePath, entity, fetchFn) {
|
|
54
|
+
function createEntityClient(basePath, entity, fetchFn, queryBuilder) {
|
|
52
55
|
const { path, parent } = entity;
|
|
53
56
|
return {
|
|
54
|
-
list(
|
|
55
|
-
|
|
57
|
+
list(parentIdOrParams, params) {
|
|
58
|
+
let parentId;
|
|
59
|
+
let listParams;
|
|
60
|
+
if (typeof parentIdOrParams === "string") {
|
|
61
|
+
parentId = parentIdOrParams;
|
|
62
|
+
listParams = params;
|
|
63
|
+
} else {
|
|
64
|
+
listParams = parentIdOrParams;
|
|
65
|
+
}
|
|
66
|
+
let url = parent && parentId ? `${basePath}${parent.path}/${parentId}${path}` : `${basePath}${path}`;
|
|
67
|
+
if (listParams && queryBuilder) {
|
|
68
|
+
const sp = queryBuilder.buildSearchParams(listParams);
|
|
69
|
+
const qs = sp.toString();
|
|
70
|
+
if (qs) url += `?${qs}`;
|
|
71
|
+
}
|
|
56
72
|
return fetchFn(url);
|
|
57
73
|
},
|
|
58
74
|
get(id) {
|
|
@@ -101,6 +117,12 @@ function createOperationClient(basePath, operation, fetchFn) {
|
|
|
101
117
|
inputData = args[argIndex];
|
|
102
118
|
}
|
|
103
119
|
const url = `${basePath}${buildPath(operation.path, pathParams)}`;
|
|
120
|
+
if (operation.contentType === "multipart" && inputData !== void 0) {
|
|
121
|
+
return multipartFetch(url, operation.method, toFormData(inputData), operation.responseType);
|
|
122
|
+
}
|
|
123
|
+
if (operation.responseType === "blob") {
|
|
124
|
+
return blobFetch(url, operation.method, inputData);
|
|
125
|
+
}
|
|
104
126
|
const options = { method: operation.method };
|
|
105
127
|
if (inputData !== void 0 && operation.method !== "GET") {
|
|
106
128
|
options.body = JSON.stringify(inputData);
|
|
@@ -108,6 +130,42 @@ function createOperationClient(basePath, operation, fetchFn) {
|
|
|
108
130
|
return fetchFn(url, options);
|
|
109
131
|
};
|
|
110
132
|
}
|
|
133
|
+
function toFormData(data) {
|
|
134
|
+
const formData = new FormData();
|
|
135
|
+
if (data && typeof data === "object") {
|
|
136
|
+
for (const [key, value] of Object.entries(data)) {
|
|
137
|
+
if (value instanceof File || value instanceof Blob) {
|
|
138
|
+
formData.append(key, value);
|
|
139
|
+
} else if (value !== void 0 && value !== null) {
|
|
140
|
+
formData.append(key, String(value));
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
return formData;
|
|
145
|
+
}
|
|
146
|
+
async function multipartFetch(url, method, formData, responseType) {
|
|
147
|
+
const res = await fetch(url, { method, body: formData });
|
|
148
|
+
if (!res.ok) {
|
|
149
|
+
throw new ApiError(res.status, await res.text());
|
|
150
|
+
}
|
|
151
|
+
if (responseType === "blob") {
|
|
152
|
+
return res.blob();
|
|
153
|
+
}
|
|
154
|
+
const json = await res.json();
|
|
155
|
+
return json.data !== void 0 ? json.data : json;
|
|
156
|
+
}
|
|
157
|
+
async function blobFetch(url, method, inputData) {
|
|
158
|
+
const options = { method };
|
|
159
|
+
if (inputData !== void 0 && method !== "GET") {
|
|
160
|
+
options.body = JSON.stringify(inputData);
|
|
161
|
+
options.headers = { "Content-Type": "application/json" };
|
|
162
|
+
}
|
|
163
|
+
const res = await fetch(url, options);
|
|
164
|
+
if (!res.ok) {
|
|
165
|
+
throw new ApiError(res.status, await res.text());
|
|
166
|
+
}
|
|
167
|
+
return res.blob();
|
|
168
|
+
}
|
|
111
169
|
|
|
112
170
|
// src/derive/query-keys.ts
|
|
113
171
|
function deriveQueryKeys(config) {
|
|
@@ -142,7 +200,36 @@ function camelToKebab(str) {
|
|
|
142
200
|
return str.replace(/([a-z0-9])([A-Z])/g, "$1-$2").toLowerCase();
|
|
143
201
|
}
|
|
144
202
|
function camelToSnake(str) {
|
|
145
|
-
return str.replace(/([a-z0-9])([A-Z])/g, "$1_$2").toLowerCase();
|
|
203
|
+
return str.replace(/([a-z0-9])([A-Z])/g, "$1_$2").replace(/[-\s]+/g, "_").toLowerCase();
|
|
146
204
|
}
|
|
147
205
|
|
|
148
|
-
|
|
206
|
+
// src/helpers/query-builders.ts
|
|
207
|
+
var simpleQueryBuilder = {
|
|
208
|
+
buildSearchParams(params) {
|
|
209
|
+
const sp = new URLSearchParams();
|
|
210
|
+
if (params.filters) {
|
|
211
|
+
for (const [k, v] of Object.entries(params.filters)) {
|
|
212
|
+
if (v !== void 0 && v !== null) sp.set(k, String(v));
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
if (params.sort) {
|
|
216
|
+
const sorts = Array.isArray(params.sort) ? params.sort : [params.sort];
|
|
217
|
+
sp.set(
|
|
218
|
+
"sort",
|
|
219
|
+
sorts.map((s) => `${s.field}:${s.direction}`).join(",")
|
|
220
|
+
);
|
|
221
|
+
}
|
|
222
|
+
if (params.pagination) {
|
|
223
|
+
if (params.pagination.type === "offset") {
|
|
224
|
+
sp.set("page", String(params.pagination.page));
|
|
225
|
+
sp.set("limit", String(params.pagination.limit));
|
|
226
|
+
} else {
|
|
227
|
+
sp.set("cursor", params.pagination.cursor);
|
|
228
|
+
sp.set("limit", String(params.pagination.limit));
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
return sp;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
export { ApiError, buildPath, camelToKebab, camelToSnake, defaultFetch, defineApi, deriveClient, deriveQueryKeys, simpleQueryBuilder };
|
package/package.json
CHANGED
|
@@ -1,40 +1,39 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@simplix-react/contract",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.3",
|
|
4
4
|
"description": "Define type-safe API contracts with Zod schemas",
|
|
5
5
|
"type": "module",
|
|
6
|
+
"sideEffects": false,
|
|
6
7
|
"exports": {
|
|
7
8
|
".": {
|
|
8
9
|
"types": "./dist/index.d.ts",
|
|
9
10
|
"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
11
|
}
|
|
19
12
|
},
|
|
20
|
-
"files": [
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
"
|
|
25
|
-
"lint": "eslint src",
|
|
26
|
-
"test": "vitest run --passWithNoTests",
|
|
27
|
-
"clean": "rm -rf dist .turbo"
|
|
13
|
+
"files": [
|
|
14
|
+
"dist"
|
|
15
|
+
],
|
|
16
|
+
"publishConfig": {
|
|
17
|
+
"access": "public"
|
|
28
18
|
},
|
|
29
19
|
"peerDependencies": {
|
|
30
20
|
"zod": ">=4.0.0"
|
|
31
21
|
},
|
|
32
22
|
"devDependencies": {
|
|
33
|
-
"@simplix-react/config-typescript": "workspace:*",
|
|
34
23
|
"eslint": "^9.39.2",
|
|
35
24
|
"tsup": "^8.5.1",
|
|
36
25
|
"typescript": "^5.9.3",
|
|
37
26
|
"vitest": "^3.0.0",
|
|
38
|
-
"zod": "^4.0.0"
|
|
27
|
+
"zod": "^4.0.0",
|
|
28
|
+
"@simplix-react/config-eslint": "0.0.1",
|
|
29
|
+
"@simplix-react/config-typescript": "0.0.1"
|
|
30
|
+
},
|
|
31
|
+
"scripts": {
|
|
32
|
+
"build": "tsup",
|
|
33
|
+
"dev": "tsup --watch",
|
|
34
|
+
"typecheck": "tsc --noEmit",
|
|
35
|
+
"lint": "eslint src",
|
|
36
|
+
"test": "vitest run --passWithNoTests",
|
|
37
|
+
"clean": "rm -rf dist .turbo"
|
|
39
38
|
}
|
|
40
|
-
}
|
|
39
|
+
}
|
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 };
|