enlace-openapi 0.0.1-beta.3 → 0.0.1-beta.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -135,13 +135,11 @@ This generates:
135
135
  }
136
136
  ```
137
137
 
138
- ## Endpoint Type
138
+ ## Endpoint Types
139
139
 
140
- The `Endpoint` type accepts three generic parameters:
140
+ ### `Endpoint<TData, TBody?, TError?>`
141
141
 
142
- ```typescript
143
- Endpoint<TData, TBody?, TError?>
144
- ```
142
+ For endpoints with JSON body:
145
143
 
146
144
  | Parameter | Description |
147
145
  |-----------|-------------|
@@ -149,6 +147,112 @@ Endpoint<TData, TBody?, TError?>
149
147
  | `TBody` | Request body type (optional) |
150
148
  | `TError` | Error response type (optional) |
151
149
 
150
+ ### `EndpointWithQuery<TData, TQuery, TError?>`
151
+
152
+ For endpoints with typed query parameters:
153
+
154
+ ```typescript
155
+ import { EndpointWithQuery } from "enlace-core";
156
+
157
+ type ApiSchema = {
158
+ users: {
159
+ $get: EndpointWithQuery<User[], { page: number; limit: number; search?: string }>;
160
+ };
161
+ };
162
+ ```
163
+
164
+ Generated OpenAPI:
165
+
166
+ ```json
167
+ {
168
+ "/users": {
169
+ "get": {
170
+ "parameters": [
171
+ { "name": "page", "in": "query", "required": true, "schema": { "type": "number" } },
172
+ { "name": "limit", "in": "query", "required": true, "schema": { "type": "number" } },
173
+ { "name": "search", "in": "query", "required": false, "schema": { "type": "string" } }
174
+ ],
175
+ "responses": { "200": { "..." } }
176
+ }
177
+ }
178
+ }
179
+ ```
180
+
181
+ ### `EndpointWithFormData<TData, TFormData, TError?>`
182
+
183
+ For file upload endpoints (multipart/form-data):
184
+
185
+ ```typescript
186
+ import { EndpointWithFormData } from "enlace-core";
187
+
188
+ type ApiSchema = {
189
+ uploads: {
190
+ $post: EndpointWithFormData<Upload, { file: Blob | File; name: string }>;
191
+ };
192
+ };
193
+ ```
194
+
195
+ Generated OpenAPI:
196
+
197
+ ```json
198
+ {
199
+ "/uploads": {
200
+ "post": {
201
+ "requestBody": {
202
+ "required": true,
203
+ "content": {
204
+ "multipart/form-data": {
205
+ "schema": {
206
+ "type": "object",
207
+ "properties": {
208
+ "file": { "type": "string", "format": "binary" },
209
+ "name": { "type": "string" }
210
+ },
211
+ "required": ["file", "name"]
212
+ }
213
+ }
214
+ }
215
+ },
216
+ "responses": { "200": { "..." } }
217
+ }
218
+ }
219
+ }
220
+ ```
221
+
222
+ ### `EndpointFull<T>`
223
+
224
+ Object-style for complex endpoints with multiple options:
225
+
226
+ ```typescript
227
+ import { EndpointFull } from "enlace-core";
228
+
229
+ type ApiSchema = {
230
+ products: {
231
+ $post: EndpointFull<{
232
+ data: Product;
233
+ body: CreateProduct;
234
+ query: { categoryId: string };
235
+ error: ValidationError;
236
+ }>;
237
+ };
238
+ files: {
239
+ $post: EndpointFull<{
240
+ data: FileUpload;
241
+ formData: { file: File; description: string };
242
+ query: { folder: string };
243
+ }>;
244
+ };
245
+ };
246
+ ```
247
+
248
+ | Property | Description | OpenAPI Mapping |
249
+ |----------|-------------|-----------------|
250
+ | `data` | Response data type | `responses.200.content` |
251
+ | `body` | JSON request body | `requestBody` with `application/json` |
252
+ | `query` | Query parameters | `parameters` with `in: "query"` |
253
+ | `formData` | FormData fields | `requestBody` with `multipart/form-data` |
254
+ | `error` | Error response type | `responses.400.content` |
255
+
152
256
  ## Path Parameters
153
257
 
154
258
  Use `_` to define dynamic path segments:
package/dist/cli.js CHANGED
@@ -33,14 +33,19 @@ var import_path = __toESM(require("path"));
33
33
 
34
34
  // src/type-to-schema.ts
35
35
  var import_typescript = __toESM(require("typescript"));
36
+ var MAX_DEPTH = 50;
36
37
  function createSchemaContext(checker) {
37
38
  return {
38
39
  checker,
39
40
  schemas: /* @__PURE__ */ new Map(),
40
- visitedTypes: /* @__PURE__ */ new Set()
41
+ visitedTypes: /* @__PURE__ */ new Set(),
42
+ depth: 0
41
43
  };
42
44
  }
43
45
  function typeToSchema(type, ctx) {
46
+ if (ctx.depth > MAX_DEPTH) {
47
+ return {};
48
+ }
44
49
  const { checker } = ctx;
45
50
  if (type.flags & import_typescript.default.TypeFlags.String) {
46
51
  return { type: "string" };
@@ -79,7 +84,7 @@ function typeToSchema(type, ctx) {
79
84
  );
80
85
  const hasNull = type.types.some((t) => t.flags & import_typescript.default.TypeFlags.Null);
81
86
  if (nonNullTypes.length === 1 && hasNull) {
82
- const schema = typeToSchema(nonNullTypes[0], ctx);
87
+ const schema = typeToSchema(nonNullTypes[0], { ...ctx, depth: ctx.depth + 1 });
83
88
  return { ...schema, nullable: true };
84
89
  }
85
90
  if (nonNullTypes.every((t) => t.isStringLiteral())) {
@@ -94,19 +99,20 @@ function typeToSchema(type, ctx) {
94
99
  enum: nonNullTypes.map((t) => t.value)
95
100
  };
96
101
  }
102
+ const nextCtx = { ...ctx, depth: ctx.depth + 1 };
97
103
  return {
98
- oneOf: nonNullTypes.map((t) => typeToSchema(t, ctx))
104
+ oneOf: nonNullTypes.map((t) => typeToSchema(t, nextCtx))
99
105
  };
100
106
  }
101
107
  if (type.isIntersection()) {
102
- return intersectionTypeToSchema(type, ctx);
108
+ return intersectionTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
103
109
  }
104
110
  if (checker.isArrayType(type)) {
105
111
  const typeArgs = type.typeArguments;
106
112
  if (typeArgs?.[0]) {
107
113
  return {
108
114
  type: "array",
109
- items: typeToSchema(typeArgs[0], ctx)
115
+ items: typeToSchema(typeArgs[0], { ...ctx, depth: ctx.depth + 1 })
110
116
  };
111
117
  }
112
118
  return { type: "array" };
@@ -118,18 +124,18 @@ function typeToSchema(type, ctx) {
118
124
  return { type: "string", format: "date-time" };
119
125
  }
120
126
  if (typeName && typeName !== "__type" && typeName !== "Array" && !typeName.startsWith("__")) {
121
- if (ctx.visitedTypes.has(type)) {
127
+ if (ctx.visitedTypes.has(typeName)) {
122
128
  return { $ref: `#/components/schemas/${typeName}` };
123
129
  }
124
130
  if (!ctx.schemas.has(typeName)) {
125
- ctx.visitedTypes.add(type);
126
- const schema = objectTypeToSchema(type, ctx);
131
+ ctx.visitedTypes.add(typeName);
132
+ const schema = objectTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
127
133
  ctx.schemas.set(typeName, schema);
128
- ctx.visitedTypes.delete(type);
134
+ ctx.visitedTypes.delete(typeName);
129
135
  }
130
136
  return { $ref: `#/components/schemas/${typeName}` };
131
137
  }
132
- return objectTypeToSchema(type, ctx);
138
+ return objectTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
133
139
  }
134
140
  return {};
135
141
  }
@@ -142,7 +148,7 @@ function objectTypeToSchema(type, ctx) {
142
148
  const propName = prop.getName();
143
149
  const propType = checker.getTypeOfSymbol(prop);
144
150
  const isOptional = prop.flags & import_typescript.default.SymbolFlags.Optional;
145
- properties[propName] = typeToSchema(propType, ctx);
151
+ properties[propName] = typeToSchema(propType, { ...ctx, depth: ctx.depth + 1 });
146
152
  if (!isOptional) {
147
153
  required.push(propName);
148
154
  }
@@ -167,7 +173,7 @@ function intersectionTypeToSchema(type, ctx) {
167
173
  if (propName.startsWith("__")) continue;
168
174
  const propType = checker.getTypeOfSymbol(prop);
169
175
  const isOptional = prop.flags & import_typescript.default.SymbolFlags.Optional;
170
- properties[propName] = typeToSchema(propType, ctx);
176
+ properties[propName] = typeToSchema(propType, { ...ctx, depth: ctx.depth + 1 });
171
177
  if (!isOptional && !required.includes(propName)) {
172
178
  required.push(propName);
173
179
  }
package/dist/cli.mjs CHANGED
@@ -10,14 +10,19 @@ import path from "path";
10
10
 
11
11
  // src/type-to-schema.ts
12
12
  import ts from "typescript";
13
+ var MAX_DEPTH = 50;
13
14
  function createSchemaContext(checker) {
14
15
  return {
15
16
  checker,
16
17
  schemas: /* @__PURE__ */ new Map(),
17
- visitedTypes: /* @__PURE__ */ new Set()
18
+ visitedTypes: /* @__PURE__ */ new Set(),
19
+ depth: 0
18
20
  };
19
21
  }
20
22
  function typeToSchema(type, ctx) {
23
+ if (ctx.depth > MAX_DEPTH) {
24
+ return {};
25
+ }
21
26
  const { checker } = ctx;
22
27
  if (type.flags & ts.TypeFlags.String) {
23
28
  return { type: "string" };
@@ -56,7 +61,7 @@ function typeToSchema(type, ctx) {
56
61
  );
57
62
  const hasNull = type.types.some((t) => t.flags & ts.TypeFlags.Null);
58
63
  if (nonNullTypes.length === 1 && hasNull) {
59
- const schema = typeToSchema(nonNullTypes[0], ctx);
64
+ const schema = typeToSchema(nonNullTypes[0], { ...ctx, depth: ctx.depth + 1 });
60
65
  return { ...schema, nullable: true };
61
66
  }
62
67
  if (nonNullTypes.every((t) => t.isStringLiteral())) {
@@ -71,19 +76,20 @@ function typeToSchema(type, ctx) {
71
76
  enum: nonNullTypes.map((t) => t.value)
72
77
  };
73
78
  }
79
+ const nextCtx = { ...ctx, depth: ctx.depth + 1 };
74
80
  return {
75
- oneOf: nonNullTypes.map((t) => typeToSchema(t, ctx))
81
+ oneOf: nonNullTypes.map((t) => typeToSchema(t, nextCtx))
76
82
  };
77
83
  }
78
84
  if (type.isIntersection()) {
79
- return intersectionTypeToSchema(type, ctx);
85
+ return intersectionTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
80
86
  }
81
87
  if (checker.isArrayType(type)) {
82
88
  const typeArgs = type.typeArguments;
83
89
  if (typeArgs?.[0]) {
84
90
  return {
85
91
  type: "array",
86
- items: typeToSchema(typeArgs[0], ctx)
92
+ items: typeToSchema(typeArgs[0], { ...ctx, depth: ctx.depth + 1 })
87
93
  };
88
94
  }
89
95
  return { type: "array" };
@@ -95,18 +101,18 @@ function typeToSchema(type, ctx) {
95
101
  return { type: "string", format: "date-time" };
96
102
  }
97
103
  if (typeName && typeName !== "__type" && typeName !== "Array" && !typeName.startsWith("__")) {
98
- if (ctx.visitedTypes.has(type)) {
104
+ if (ctx.visitedTypes.has(typeName)) {
99
105
  return { $ref: `#/components/schemas/${typeName}` };
100
106
  }
101
107
  if (!ctx.schemas.has(typeName)) {
102
- ctx.visitedTypes.add(type);
103
- const schema = objectTypeToSchema(type, ctx);
108
+ ctx.visitedTypes.add(typeName);
109
+ const schema = objectTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
104
110
  ctx.schemas.set(typeName, schema);
105
- ctx.visitedTypes.delete(type);
111
+ ctx.visitedTypes.delete(typeName);
106
112
  }
107
113
  return { $ref: `#/components/schemas/${typeName}` };
108
114
  }
109
- return objectTypeToSchema(type, ctx);
115
+ return objectTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
110
116
  }
111
117
  return {};
112
118
  }
@@ -119,7 +125,7 @@ function objectTypeToSchema(type, ctx) {
119
125
  const propName = prop.getName();
120
126
  const propType = checker.getTypeOfSymbol(prop);
121
127
  const isOptional = prop.flags & ts.SymbolFlags.Optional;
122
- properties[propName] = typeToSchema(propType, ctx);
128
+ properties[propName] = typeToSchema(propType, { ...ctx, depth: ctx.depth + 1 });
123
129
  if (!isOptional) {
124
130
  required.push(propName);
125
131
  }
@@ -144,7 +150,7 @@ function intersectionTypeToSchema(type, ctx) {
144
150
  if (propName.startsWith("__")) continue;
145
151
  const propType = checker.getTypeOfSymbol(prop);
146
152
  const isOptional = prop.flags & ts.SymbolFlags.Optional;
147
- properties[propName] = typeToSchema(propType, ctx);
153
+ properties[propName] = typeToSchema(propType, { ...ctx, depth: ctx.depth + 1 });
148
154
  if (!isOptional && !required.includes(propName)) {
149
155
  required.push(propName);
150
156
  }
package/dist/index.js CHANGED
@@ -41,14 +41,19 @@ var import_path = __toESM(require("path"));
41
41
 
42
42
  // src/type-to-schema.ts
43
43
  var import_typescript = __toESM(require("typescript"));
44
+ var MAX_DEPTH = 50;
44
45
  function createSchemaContext(checker) {
45
46
  return {
46
47
  checker,
47
48
  schemas: /* @__PURE__ */ new Map(),
48
- visitedTypes: /* @__PURE__ */ new Set()
49
+ visitedTypes: /* @__PURE__ */ new Set(),
50
+ depth: 0
49
51
  };
50
52
  }
51
53
  function typeToSchema(type, ctx) {
54
+ if (ctx.depth > MAX_DEPTH) {
55
+ return {};
56
+ }
52
57
  const { checker } = ctx;
53
58
  if (type.flags & import_typescript.default.TypeFlags.String) {
54
59
  return { type: "string" };
@@ -87,7 +92,7 @@ function typeToSchema(type, ctx) {
87
92
  );
88
93
  const hasNull = type.types.some((t) => t.flags & import_typescript.default.TypeFlags.Null);
89
94
  if (nonNullTypes.length === 1 && hasNull) {
90
- const schema = typeToSchema(nonNullTypes[0], ctx);
95
+ const schema = typeToSchema(nonNullTypes[0], { ...ctx, depth: ctx.depth + 1 });
91
96
  return { ...schema, nullable: true };
92
97
  }
93
98
  if (nonNullTypes.every((t) => t.isStringLiteral())) {
@@ -102,19 +107,20 @@ function typeToSchema(type, ctx) {
102
107
  enum: nonNullTypes.map((t) => t.value)
103
108
  };
104
109
  }
110
+ const nextCtx = { ...ctx, depth: ctx.depth + 1 };
105
111
  return {
106
- oneOf: nonNullTypes.map((t) => typeToSchema(t, ctx))
112
+ oneOf: nonNullTypes.map((t) => typeToSchema(t, nextCtx))
107
113
  };
108
114
  }
109
115
  if (type.isIntersection()) {
110
- return intersectionTypeToSchema(type, ctx);
116
+ return intersectionTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
111
117
  }
112
118
  if (checker.isArrayType(type)) {
113
119
  const typeArgs = type.typeArguments;
114
120
  if (typeArgs?.[0]) {
115
121
  return {
116
122
  type: "array",
117
- items: typeToSchema(typeArgs[0], ctx)
123
+ items: typeToSchema(typeArgs[0], { ...ctx, depth: ctx.depth + 1 })
118
124
  };
119
125
  }
120
126
  return { type: "array" };
@@ -126,18 +132,18 @@ function typeToSchema(type, ctx) {
126
132
  return { type: "string", format: "date-time" };
127
133
  }
128
134
  if (typeName && typeName !== "__type" && typeName !== "Array" && !typeName.startsWith("__")) {
129
- if (ctx.visitedTypes.has(type)) {
135
+ if (ctx.visitedTypes.has(typeName)) {
130
136
  return { $ref: `#/components/schemas/${typeName}` };
131
137
  }
132
138
  if (!ctx.schemas.has(typeName)) {
133
- ctx.visitedTypes.add(type);
134
- const schema = objectTypeToSchema(type, ctx);
139
+ ctx.visitedTypes.add(typeName);
140
+ const schema = objectTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
135
141
  ctx.schemas.set(typeName, schema);
136
- ctx.visitedTypes.delete(type);
142
+ ctx.visitedTypes.delete(typeName);
137
143
  }
138
144
  return { $ref: `#/components/schemas/${typeName}` };
139
145
  }
140
- return objectTypeToSchema(type, ctx);
146
+ return objectTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
141
147
  }
142
148
  return {};
143
149
  }
@@ -150,7 +156,7 @@ function objectTypeToSchema(type, ctx) {
150
156
  const propName = prop.getName();
151
157
  const propType = checker.getTypeOfSymbol(prop);
152
158
  const isOptional = prop.flags & import_typescript.default.SymbolFlags.Optional;
153
- properties[propName] = typeToSchema(propType, ctx);
159
+ properties[propName] = typeToSchema(propType, { ...ctx, depth: ctx.depth + 1 });
154
160
  if (!isOptional) {
155
161
  required.push(propName);
156
162
  }
@@ -175,7 +181,7 @@ function intersectionTypeToSchema(type, ctx) {
175
181
  if (propName.startsWith("__")) continue;
176
182
  const propType = checker.getTypeOfSymbol(prop);
177
183
  const isOptional = prop.flags & import_typescript.default.SymbolFlags.Optional;
178
- properties[propName] = typeToSchema(propType, ctx);
184
+ properties[propName] = typeToSchema(propType, { ...ctx, depth: ctx.depth + 1 });
179
185
  if (!isOptional && !required.includes(propName)) {
180
186
  required.push(propName);
181
187
  }
package/dist/index.mjs CHANGED
@@ -4,14 +4,19 @@ import path from "path";
4
4
 
5
5
  // src/type-to-schema.ts
6
6
  import ts from "typescript";
7
+ var MAX_DEPTH = 50;
7
8
  function createSchemaContext(checker) {
8
9
  return {
9
10
  checker,
10
11
  schemas: /* @__PURE__ */ new Map(),
11
- visitedTypes: /* @__PURE__ */ new Set()
12
+ visitedTypes: /* @__PURE__ */ new Set(),
13
+ depth: 0
12
14
  };
13
15
  }
14
16
  function typeToSchema(type, ctx) {
17
+ if (ctx.depth > MAX_DEPTH) {
18
+ return {};
19
+ }
15
20
  const { checker } = ctx;
16
21
  if (type.flags & ts.TypeFlags.String) {
17
22
  return { type: "string" };
@@ -50,7 +55,7 @@ function typeToSchema(type, ctx) {
50
55
  );
51
56
  const hasNull = type.types.some((t) => t.flags & ts.TypeFlags.Null);
52
57
  if (nonNullTypes.length === 1 && hasNull) {
53
- const schema = typeToSchema(nonNullTypes[0], ctx);
58
+ const schema = typeToSchema(nonNullTypes[0], { ...ctx, depth: ctx.depth + 1 });
54
59
  return { ...schema, nullable: true };
55
60
  }
56
61
  if (nonNullTypes.every((t) => t.isStringLiteral())) {
@@ -65,19 +70,20 @@ function typeToSchema(type, ctx) {
65
70
  enum: nonNullTypes.map((t) => t.value)
66
71
  };
67
72
  }
73
+ const nextCtx = { ...ctx, depth: ctx.depth + 1 };
68
74
  return {
69
- oneOf: nonNullTypes.map((t) => typeToSchema(t, ctx))
75
+ oneOf: nonNullTypes.map((t) => typeToSchema(t, nextCtx))
70
76
  };
71
77
  }
72
78
  if (type.isIntersection()) {
73
- return intersectionTypeToSchema(type, ctx);
79
+ return intersectionTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
74
80
  }
75
81
  if (checker.isArrayType(type)) {
76
82
  const typeArgs = type.typeArguments;
77
83
  if (typeArgs?.[0]) {
78
84
  return {
79
85
  type: "array",
80
- items: typeToSchema(typeArgs[0], ctx)
86
+ items: typeToSchema(typeArgs[0], { ...ctx, depth: ctx.depth + 1 })
81
87
  };
82
88
  }
83
89
  return { type: "array" };
@@ -89,18 +95,18 @@ function typeToSchema(type, ctx) {
89
95
  return { type: "string", format: "date-time" };
90
96
  }
91
97
  if (typeName && typeName !== "__type" && typeName !== "Array" && !typeName.startsWith("__")) {
92
- if (ctx.visitedTypes.has(type)) {
98
+ if (ctx.visitedTypes.has(typeName)) {
93
99
  return { $ref: `#/components/schemas/${typeName}` };
94
100
  }
95
101
  if (!ctx.schemas.has(typeName)) {
96
- ctx.visitedTypes.add(type);
97
- const schema = objectTypeToSchema(type, ctx);
102
+ ctx.visitedTypes.add(typeName);
103
+ const schema = objectTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
98
104
  ctx.schemas.set(typeName, schema);
99
- ctx.visitedTypes.delete(type);
105
+ ctx.visitedTypes.delete(typeName);
100
106
  }
101
107
  return { $ref: `#/components/schemas/${typeName}` };
102
108
  }
103
- return objectTypeToSchema(type, ctx);
109
+ return objectTypeToSchema(type, { ...ctx, depth: ctx.depth + 1 });
104
110
  }
105
111
  return {};
106
112
  }
@@ -113,7 +119,7 @@ function objectTypeToSchema(type, ctx) {
113
119
  const propName = prop.getName();
114
120
  const propType = checker.getTypeOfSymbol(prop);
115
121
  const isOptional = prop.flags & ts.SymbolFlags.Optional;
116
- properties[propName] = typeToSchema(propType, ctx);
122
+ properties[propName] = typeToSchema(propType, { ...ctx, depth: ctx.depth + 1 });
117
123
  if (!isOptional) {
118
124
  required.push(propName);
119
125
  }
@@ -138,7 +144,7 @@ function intersectionTypeToSchema(type, ctx) {
138
144
  if (propName.startsWith("__")) continue;
139
145
  const propType = checker.getTypeOfSymbol(prop);
140
146
  const isOptional = prop.flags & ts.SymbolFlags.Optional;
141
- properties[propName] = typeToSchema(propType, ctx);
147
+ properties[propName] = typeToSchema(propType, { ...ctx, depth: ctx.depth + 1 });
142
148
  if (!isOptional && !required.includes(propName)) {
143
149
  required.push(propName);
144
150
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "enlace-openapi",
3
- "version": "0.0.1-beta.3",
3
+ "version": "0.0.1-beta.5",
4
4
  "license": "MIT",
5
5
  "bin": {
6
6
  "enlace-openapi": "./dist/cli.mjs"