@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/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.replace(`:${key}`, encodeURIComponent(value));
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(parentId) {
55
- const url = parent && parentId ? `${basePath}${parent.path}/${parentId}${path}` : `${basePath}${path}`;
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
- export { ApiError, buildPath, camelToKebab, camelToSnake, defaultFetch, defineApi, deriveClient, deriveQueryKeys };
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.1",
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": ["dist"],
21
- "scripts": {
22
- "build": "tsup",
23
- "dev": "tsup --watch",
24
- "typecheck": "tsc --noEmit",
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
+ }
@@ -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 };
@@ -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 };