nuxt-api-contract 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +385 -0
- package/dist/cli.d.mts +1 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.mjs +99 -0
- package/dist/client.d.mts +70 -0
- package/dist/client.d.ts +70 -0
- package/dist/client.mjs +3 -0
- package/dist/composables.d.mts +45 -0
- package/dist/composables.d.ts +45 -0
- package/dist/composables.mjs +97 -0
- package/dist/module.d.mts +35 -0
- package/dist/module.d.ts +35 -0
- package/dist/module.mjs +145 -0
- package/dist/openapi.d.mts +46 -0
- package/dist/openapi.d.ts +46 -0
- package/dist/openapi.mjs +312 -0
- package/dist/runtime/server/devtoolsRoute.mjs +6 -0
- package/dist/runtime/server/openapiRoute.mjs +6 -0
- package/dist/runtime/shared/contract.mjs +77 -0
- package/dist/runtime/shared/errors.mjs +71 -0
- package/dist/runtime/shared/format.mjs +32 -0
- package/dist/runtime/shared/serialization.mjs +36 -0
- package/dist/runtime/shared/types.mjs +1 -0
- package/dist/server.d.mts +41 -0
- package/dist/server.d.ts +41 -0
- package/dist/server.mjs +94 -0
- package/dist/shared/nuxt-api-contract.B9JBCRk8.d.mts +37 -0
- package/dist/shared/nuxt-api-contract.CPm9WbWA.d.mts +165 -0
- package/dist/shared/nuxt-api-contract.CPm9WbWA.d.ts +165 -0
- package/dist/shared/nuxt-api-contract.D31EDcwH.d.mts +109 -0
- package/dist/shared/nuxt-api-contract.D31EDcwH.d.ts +109 -0
- package/dist/shared/nuxt-api-contract.DCAU2j7t.mjs +34 -0
- package/dist/shared/nuxt-api-contract.DDpgZj2g.mjs +66 -0
- package/dist/shared/nuxt-api-contract.QDGSGaVY.mjs +117 -0
- package/dist/shared/nuxt-api-contract.S1zqCiJX.d.ts +37 -0
- package/dist/shared/nuxt-api-contract.obS6uV8A.mjs +73 -0
- package/dist/shared.d.mts +4 -0
- package/dist/shared.d.ts +4 -0
- package/dist/shared.mjs +3 -0
- package/dist/testing.d.mts +55 -0
- package/dist/testing.d.ts +55 -0
- package/dist/testing.mjs +49 -0
- package/package.json +104 -0
package/dist/openapi.mjs
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
1
|
+
function zodDef(schema) {
|
|
2
|
+
return schema._def;
|
|
3
|
+
}
|
|
4
|
+
function toOpenApiPath(path) {
|
|
5
|
+
return path.replace(/:([A-Za-z_][A-Za-z0-9_]*)/g, "{$1}");
|
|
6
|
+
}
|
|
7
|
+
function pathParamNames(path) {
|
|
8
|
+
return [...path.matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)].map((match) => match[1]);
|
|
9
|
+
}
|
|
10
|
+
function zodToJsonSchema(schema, warnings, contractLabel) {
|
|
11
|
+
const def = zodDef(schema);
|
|
12
|
+
const kind = def.typeName ?? "unknown";
|
|
13
|
+
const description = def.description;
|
|
14
|
+
const base = (schema2) => description ? { ...schema2, description } : schema2;
|
|
15
|
+
switch (kind) {
|
|
16
|
+
case "ZodString": {
|
|
17
|
+
const result = { type: "string" };
|
|
18
|
+
for (const check of def.checks ?? []) {
|
|
19
|
+
switch (check.kind) {
|
|
20
|
+
case "email":
|
|
21
|
+
result.format = "email";
|
|
22
|
+
break;
|
|
23
|
+
case "uuid":
|
|
24
|
+
result.format = "uuid";
|
|
25
|
+
break;
|
|
26
|
+
case "datetime":
|
|
27
|
+
result.format = "date-time";
|
|
28
|
+
break;
|
|
29
|
+
case "url":
|
|
30
|
+
result.format = "uri";
|
|
31
|
+
break;
|
|
32
|
+
case "min":
|
|
33
|
+
result.minLength = check.value;
|
|
34
|
+
break;
|
|
35
|
+
case "max":
|
|
36
|
+
result.maxLength = check.value;
|
|
37
|
+
break;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
return base(result);
|
|
41
|
+
}
|
|
42
|
+
case "ZodNumber": {
|
|
43
|
+
const result = { type: "number" };
|
|
44
|
+
for (const check of def.checks ?? []) {
|
|
45
|
+
switch (check.kind) {
|
|
46
|
+
case "int":
|
|
47
|
+
result.type = "integer";
|
|
48
|
+
break;
|
|
49
|
+
case "min":
|
|
50
|
+
result.minimum = check.value;
|
|
51
|
+
break;
|
|
52
|
+
case "max":
|
|
53
|
+
result.maximum = check.value;
|
|
54
|
+
break;
|
|
55
|
+
case "multipleOf":
|
|
56
|
+
result.multipleOf = check.value;
|
|
57
|
+
break;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
return base(result);
|
|
61
|
+
}
|
|
62
|
+
case "ZodBoolean":
|
|
63
|
+
return base({ type: "boolean" });
|
|
64
|
+
case "ZodNull":
|
|
65
|
+
return base({ type: "null" });
|
|
66
|
+
case "ZodDate":
|
|
67
|
+
return base({ type: "string", format: "date-time" });
|
|
68
|
+
case "ZodLiteral":
|
|
69
|
+
return base({ enum: [def.value], type: typeof def.value });
|
|
70
|
+
case "ZodEnum":
|
|
71
|
+
return base({ enum: def.values, type: typeof def.values?.[0] });
|
|
72
|
+
case "ZodArray": {
|
|
73
|
+
const element = def.type ?? def.innerType;
|
|
74
|
+
return base({
|
|
75
|
+
type: "array",
|
|
76
|
+
items: element ? zodToJsonSchema(element, warnings, contractLabel) : {}
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
case "ZodObject": {
|
|
80
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
81
|
+
const properties = {};
|
|
82
|
+
const required = [];
|
|
83
|
+
for (const [key, value] of Object.entries(shape ?? {})) {
|
|
84
|
+
properties[key] = zodToJsonSchema(value, warnings, contractLabel);
|
|
85
|
+
if (!isOptional(value)) required.push(key);
|
|
86
|
+
}
|
|
87
|
+
const result = { type: "object", properties };
|
|
88
|
+
if (required.length > 0) result.required = required;
|
|
89
|
+
return base(result);
|
|
90
|
+
}
|
|
91
|
+
case "ZodUnion":
|
|
92
|
+
case "ZodDiscriminatedUnion": {
|
|
93
|
+
return base({
|
|
94
|
+
anyOf: (def.options ?? []).map((option) => zodToJsonSchema(option, warnings, contractLabel))
|
|
95
|
+
});
|
|
96
|
+
}
|
|
97
|
+
case "ZodIntersection": {
|
|
98
|
+
return base({
|
|
99
|
+
allOf: [def.left, def.right].filter((part) => Boolean(part)).map((part) => zodToJsonSchema(part, warnings, contractLabel))
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
case "ZodRecord": {
|
|
103
|
+
return base({
|
|
104
|
+
type: "object",
|
|
105
|
+
additionalProperties: def.valueType ? zodToJsonSchema(def.valueType, warnings, contractLabel) : {}
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
case "ZodTuple": {
|
|
109
|
+
return base({
|
|
110
|
+
type: "array",
|
|
111
|
+
items: { anyOf: (def.items ?? []).map((item) => zodToJsonSchema(item, warnings, contractLabel)) }
|
|
112
|
+
});
|
|
113
|
+
}
|
|
114
|
+
case "ZodOptional": {
|
|
115
|
+
if (def.innerType) return zodToJsonSchema(def.innerType, warnings, contractLabel);
|
|
116
|
+
return base({});
|
|
117
|
+
}
|
|
118
|
+
case "ZodNullable": {
|
|
119
|
+
const inner = def.innerType ? zodToJsonSchema(def.innerType, warnings, contractLabel) : {};
|
|
120
|
+
return base({ ...inner, nullable: true });
|
|
121
|
+
}
|
|
122
|
+
case "ZodDefault": {
|
|
123
|
+
const inner = def.innerType ? zodToJsonSchema(def.innerType, warnings, contractLabel) : {};
|
|
124
|
+
let defaultValue;
|
|
125
|
+
try {
|
|
126
|
+
defaultValue = typeof def.defaultValue === "function" ? def.defaultValue() : def.defaultValue;
|
|
127
|
+
} catch {
|
|
128
|
+
defaultValue = void 0;
|
|
129
|
+
}
|
|
130
|
+
return base({ ...inner, default: defaultValue });
|
|
131
|
+
}
|
|
132
|
+
case "ZodCatch":
|
|
133
|
+
case "ZodBranded": {
|
|
134
|
+
return def.innerType ? zodToJsonSchema(def.innerType, warnings, contractLabel) : base({});
|
|
135
|
+
}
|
|
136
|
+
case "ZodEffects": {
|
|
137
|
+
warnings.push({
|
|
138
|
+
contract: contractLabel,
|
|
139
|
+
message: `Zod effects (${def.effect?.type ?? "unknown"}) cannot be represented in OpenAPI; the inner schema is used.`
|
|
140
|
+
});
|
|
141
|
+
const inner = def.schema ?? def.innerType;
|
|
142
|
+
return inner ? zodToJsonSchema(inner, warnings, contractLabel) : base({});
|
|
143
|
+
}
|
|
144
|
+
default: {
|
|
145
|
+
warnings.push({
|
|
146
|
+
contract: contractLabel,
|
|
147
|
+
message: `Unsupported Zod kind "${kind}" cannot be represented in OpenAPI; an empty schema is emitted.`
|
|
148
|
+
});
|
|
149
|
+
return base({});
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
function isOptional(schema) {
|
|
154
|
+
const typeName = zodDef(schema).typeName;
|
|
155
|
+
return typeName === "ZodOptional" || typeName === "ZodDefault";
|
|
156
|
+
}
|
|
157
|
+
function extractShapeProperty(schema, name, warnings, label) {
|
|
158
|
+
const def = zodDef(schema);
|
|
159
|
+
const shape = typeof def.shape === "function" ? def.shape() : def.shape;
|
|
160
|
+
if (def.typeName === "ZodObject" && shape?.[name]) {
|
|
161
|
+
return zodToJsonSchema(shape[name], warnings, label);
|
|
162
|
+
}
|
|
163
|
+
warnings.push({
|
|
164
|
+
contract: label,
|
|
165
|
+
message: `Params schema is not a plain ZodObject; path parameter "${name}" defaults to { type: string }.`
|
|
166
|
+
});
|
|
167
|
+
return void 0;
|
|
168
|
+
}
|
|
169
|
+
function hasOptionalTopLevel(schema) {
|
|
170
|
+
const json = zodToJsonSchema(schema, [], "(internal)");
|
|
171
|
+
return !json.required?.length;
|
|
172
|
+
}
|
|
173
|
+
function contractToOperation(contract, warnings) {
|
|
174
|
+
const label = contract.name ?? `${contract.method} ${contract.path}`;
|
|
175
|
+
const parameters = [];
|
|
176
|
+
const operation = {
|
|
177
|
+
operationId: contract.name ?? `${contract.method.toLowerCase()}_${toOpenApiPath(contract.path).replace(/[^A-Za-z0-9]/g, "_")}`,
|
|
178
|
+
summary: contract.summary ?? label,
|
|
179
|
+
parameters
|
|
180
|
+
};
|
|
181
|
+
if (contract.description) operation.description = contract.description;
|
|
182
|
+
if (contract.tags?.length) operation.tags = [...contract.tags];
|
|
183
|
+
for (const name of pathParamNames(contract.path)) {
|
|
184
|
+
const schema = contract.params ? extractShapeProperty(contract.params, name, warnings, label) : void 0;
|
|
185
|
+
parameters.push({
|
|
186
|
+
name,
|
|
187
|
+
in: "path",
|
|
188
|
+
required: true,
|
|
189
|
+
schema: schema ?? { type: "string" }
|
|
190
|
+
});
|
|
191
|
+
}
|
|
192
|
+
if (contract.query) {
|
|
193
|
+
const querySchema = zodToJsonSchema(contract.query, warnings, label);
|
|
194
|
+
const properties = querySchema.properties ?? {};
|
|
195
|
+
const required = querySchema.required ?? [];
|
|
196
|
+
for (const [name, schema] of Object.entries(properties)) {
|
|
197
|
+
parameters.push({ name, in: "query", required: required.includes(name), schema });
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (contract.headers) {
|
|
201
|
+
const headersSchema = zodToJsonSchema(contract.headers, warnings, label);
|
|
202
|
+
const properties = headersSchema.properties ?? {};
|
|
203
|
+
const required = headersSchema.required ?? [];
|
|
204
|
+
for (const [name, schema] of Object.entries(properties)) {
|
|
205
|
+
parameters.push({ name, in: "header", required: required.includes(name), schema });
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (contract.body) {
|
|
209
|
+
operation.requestBody = {
|
|
210
|
+
required: !hasOptionalTopLevel(contract.body),
|
|
211
|
+
content: { "application/json": { schema: zodToJsonSchema(contract.body, warnings, label) } }
|
|
212
|
+
};
|
|
213
|
+
}
|
|
214
|
+
const responses = {};
|
|
215
|
+
responses["200"] = contract.response ? {
|
|
216
|
+
description: contract.summary ?? "Successful response",
|
|
217
|
+
content: { "application/json": { schema: zodToJsonSchema(contract.response, warnings, label) } }
|
|
218
|
+
} : { description: "Successful response" };
|
|
219
|
+
if (contract.params || contract.query || contract.body || contract.headers) {
|
|
220
|
+
responses["400"] = {
|
|
221
|
+
description: "Request validation error",
|
|
222
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ApiValidationError" } } }
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
responses.default = {
|
|
226
|
+
description: "Unified API error",
|
|
227
|
+
content: { "application/json": { schema: { $ref: "#/components/schemas/ApiError" } } }
|
|
228
|
+
};
|
|
229
|
+
operation.responses = responses;
|
|
230
|
+
if (contract.errors) {
|
|
231
|
+
operation["x-error-codes"] = Object.keys(contract.errors);
|
|
232
|
+
}
|
|
233
|
+
return operation;
|
|
234
|
+
}
|
|
235
|
+
function generateOpenApiDocument(contracts, options = {}) {
|
|
236
|
+
const warnings = [];
|
|
237
|
+
const paths = {};
|
|
238
|
+
for (const contract of contracts) {
|
|
239
|
+
const openApiPath = toOpenApiPath(contract.path);
|
|
240
|
+
paths[openApiPath] ??= {};
|
|
241
|
+
const key = contract.method.toLowerCase();
|
|
242
|
+
if (paths[openApiPath][key]) {
|
|
243
|
+
warnings.push({
|
|
244
|
+
contract: contract.name ?? contract.path,
|
|
245
|
+
message: `Duplicate operation ${contract.method} ${openApiPath}; the previous definition is overwritten.`
|
|
246
|
+
});
|
|
247
|
+
}
|
|
248
|
+
paths[openApiPath][key] = contractToOperation(contract, warnings);
|
|
249
|
+
}
|
|
250
|
+
const document = {
|
|
251
|
+
openapi: "3.0.3",
|
|
252
|
+
info: {
|
|
253
|
+
title: options.title ?? "API Contracts",
|
|
254
|
+
version: options.version ?? "0.1.0",
|
|
255
|
+
...options.description ? { description: options.description } : {}
|
|
256
|
+
},
|
|
257
|
+
paths,
|
|
258
|
+
components: {
|
|
259
|
+
schemas: {
|
|
260
|
+
ApiError: {
|
|
261
|
+
type: "object",
|
|
262
|
+
properties: {
|
|
263
|
+
error: {
|
|
264
|
+
type: "object",
|
|
265
|
+
properties: {
|
|
266
|
+
code: { type: "string" },
|
|
267
|
+
message: { type: "string" },
|
|
268
|
+
statusCode: { type: "number" }
|
|
269
|
+
},
|
|
270
|
+
required: ["code", "message"]
|
|
271
|
+
}
|
|
272
|
+
},
|
|
273
|
+
required: ["error"]
|
|
274
|
+
},
|
|
275
|
+
ApiValidationError: {
|
|
276
|
+
type: "object",
|
|
277
|
+
properties: {
|
|
278
|
+
error: {
|
|
279
|
+
type: "object",
|
|
280
|
+
properties: {
|
|
281
|
+
code: { type: "string", enum: ["VALIDATION_ERROR"] },
|
|
282
|
+
message: { type: "string" },
|
|
283
|
+
issues: {
|
|
284
|
+
type: "array",
|
|
285
|
+
items: {
|
|
286
|
+
type: "object",
|
|
287
|
+
properties: {
|
|
288
|
+
path: { type: "string" },
|
|
289
|
+
message: { type: "string" },
|
|
290
|
+
expected: { type: "string" }
|
|
291
|
+
},
|
|
292
|
+
required: ["path", "message"]
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
required: ["code", "message"]
|
|
297
|
+
}
|
|
298
|
+
},
|
|
299
|
+
required: ["error"]
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
};
|
|
304
|
+
return { document, warnings };
|
|
305
|
+
}
|
|
306
|
+
function pickContracts(values) {
|
|
307
|
+
return values.filter(
|
|
308
|
+
(value) => typeof value === "object" && value !== null && value.kind === "api-contract"
|
|
309
|
+
);
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
export { contractToOperation, generateOpenApiDocument, pickContracts, toOpenApiPath, zodToJsonSchema };
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { API_CONTRACT_KIND } from "./types.mjs";
|
|
2
|
+
const REGISTRY_KEY = Symbol.for("nuxt-api-contract.registry");
|
|
3
|
+
function getStore() {
|
|
4
|
+
const globalThis_ = globalThis;
|
|
5
|
+
if (!globalThis_[REGISTRY_KEY]) {
|
|
6
|
+
globalThis_[REGISTRY_KEY] = { contracts: /* @__PURE__ */ new Map() };
|
|
7
|
+
}
|
|
8
|
+
return globalThis_[REGISTRY_KEY];
|
|
9
|
+
}
|
|
10
|
+
export function registerContract(contract) {
|
|
11
|
+
if (!contract.name) return;
|
|
12
|
+
const store = getStore();
|
|
13
|
+
const existing = store.contracts.get(contract.name);
|
|
14
|
+
if (existing && existing !== contract) {
|
|
15
|
+
console.warn(
|
|
16
|
+
`[nuxt-api-contract] Duplicate contract name "${contract.name}" registered. The latest definition wins.`
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
store.contracts.set(contract.name, contract);
|
|
20
|
+
}
|
|
21
|
+
export function getContractByName(name) {
|
|
22
|
+
return getStore().contracts.get(name);
|
|
23
|
+
}
|
|
24
|
+
export function listRegisteredContracts() {
|
|
25
|
+
return [...getStore().contracts.values()];
|
|
26
|
+
}
|
|
27
|
+
export function clearContractRegistry() {
|
|
28
|
+
getStore().contracts.clear();
|
|
29
|
+
}
|
|
30
|
+
const MOCK_STORE_KEY = Symbol.for("nuxt-api-contract.mocks");
|
|
31
|
+
const mockStore = globalThis[MOCK_STORE_KEY] ??= /* @__PURE__ */ new Map();
|
|
32
|
+
export function mockContract(contract, mock) {
|
|
33
|
+
mockStore.set(contract, mock);
|
|
34
|
+
}
|
|
35
|
+
export function getContractMock(contract) {
|
|
36
|
+
return mockStore.get(contract);
|
|
37
|
+
}
|
|
38
|
+
export function defineApiContract(definition) {
|
|
39
|
+
const contract = {
|
|
40
|
+
kind: API_CONTRACT_KIND,
|
|
41
|
+
name: definition.name,
|
|
42
|
+
version: definition.version,
|
|
43
|
+
method: definition.method,
|
|
44
|
+
path: definition.path,
|
|
45
|
+
params: definition.params,
|
|
46
|
+
query: definition.query,
|
|
47
|
+
body: definition.body,
|
|
48
|
+
headers: definition.headers,
|
|
49
|
+
response: definition.response,
|
|
50
|
+
errors: definition.errors ? Object.freeze({ ...definition.errors }) : void 0,
|
|
51
|
+
summary: definition.summary,
|
|
52
|
+
description: definition.description,
|
|
53
|
+
tags: definition.tags ? Object.freeze([...definition.tags]) : void 0,
|
|
54
|
+
auth: definition.auth,
|
|
55
|
+
metadata: definition.metadata ? Object.freeze({ ...definition.metadata }) : void 0
|
|
56
|
+
};
|
|
57
|
+
Object.freeze(contract);
|
|
58
|
+
if (definition.name) {
|
|
59
|
+
registerContract(contract);
|
|
60
|
+
}
|
|
61
|
+
return contract;
|
|
62
|
+
}
|
|
63
|
+
export function isApiContract(value) {
|
|
64
|
+
return typeof value === "object" && value !== null && value.kind === API_CONTRACT_KIND;
|
|
65
|
+
}
|
|
66
|
+
export function buildRequestPath(path, params) {
|
|
67
|
+
let url = path;
|
|
68
|
+
for (const match of path.matchAll(/:([A-Za-z_][A-Za-z0-9_]*)/g)) {
|
|
69
|
+
const name = match[1];
|
|
70
|
+
const value = params?.[name];
|
|
71
|
+
if (value === void 0 || value === null) {
|
|
72
|
+
throw new Error(`[nuxt-api-contract] Missing path parameter ":${name}" for ${path}`);
|
|
73
|
+
}
|
|
74
|
+
url = url.replace(`:${name}`, encodeURIComponent(String(value)));
|
|
75
|
+
}
|
|
76
|
+
return url;
|
|
77
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
export class ApiError extends Error {
|
|
2
|
+
code;
|
|
3
|
+
statusCode;
|
|
4
|
+
details;
|
|
5
|
+
issues;
|
|
6
|
+
constructor(options) {
|
|
7
|
+
super(options.message);
|
|
8
|
+
this.name = "ApiError";
|
|
9
|
+
this.code = options.code;
|
|
10
|
+
this.statusCode = options.statusCode ?? 500;
|
|
11
|
+
this.details = options.details;
|
|
12
|
+
this.issues = options.issues;
|
|
13
|
+
}
|
|
14
|
+
toJSON() {
|
|
15
|
+
return serializeApiError(this);
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
export function createApiError(codeOrOptions, message, statusCode, details) {
|
|
19
|
+
if (typeof codeOrOptions === "string") {
|
|
20
|
+
return new ApiError({ code: codeOrOptions, message: message ?? codeOrOptions, statusCode, details });
|
|
21
|
+
}
|
|
22
|
+
return new ApiError({
|
|
23
|
+
code: codeOrOptions.code,
|
|
24
|
+
message: codeOrOptions.message ?? codeOrOptions.code,
|
|
25
|
+
statusCode: codeOrOptions.statusCode,
|
|
26
|
+
details: codeOrOptions.details,
|
|
27
|
+
issues: codeOrOptions.issues
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
export function isApiError(value) {
|
|
31
|
+
return value instanceof ApiError || typeof value === "object" && value !== null && value.name === "ApiError" && typeof value.code === "string";
|
|
32
|
+
}
|
|
33
|
+
export function serializeApiError(error) {
|
|
34
|
+
return {
|
|
35
|
+
error: {
|
|
36
|
+
code: error.code,
|
|
37
|
+
message: error.message,
|
|
38
|
+
statusCode: error.statusCode,
|
|
39
|
+
details: error.details,
|
|
40
|
+
issues: error.issues
|
|
41
|
+
}
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
export function parseApiErrorPayload(value) {
|
|
45
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
46
|
+
const error = value.error;
|
|
47
|
+
if (typeof error !== "object" || error === null) return void 0;
|
|
48
|
+
const { code, message, statusCode, details, issues } = error;
|
|
49
|
+
if (typeof code !== "string" || typeof message !== "string") return void 0;
|
|
50
|
+
return {
|
|
51
|
+
code,
|
|
52
|
+
message,
|
|
53
|
+
statusCode: typeof statusCode === "number" ? statusCode : void 0,
|
|
54
|
+
details,
|
|
55
|
+
issues: Array.isArray(issues) ? issues : void 0
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
export function toApiError(value, fallbackMessage = "Internal server error") {
|
|
59
|
+
if (isApiError(value)) return value;
|
|
60
|
+
if (value instanceof Error) {
|
|
61
|
+
return new ApiError({ code: "INTERNAL_ERROR", message: value.message || fallbackMessage, statusCode: 500 });
|
|
62
|
+
}
|
|
63
|
+
return new ApiError({ code: "INTERNAL_ERROR", message: fallbackMessage, statusCode: 500 });
|
|
64
|
+
}
|
|
65
|
+
export const BUILT_IN_ERROR_CODES = {
|
|
66
|
+
validation: "VALIDATION_ERROR",
|
|
67
|
+
responseValidation: "API_CONTRACT_RESPONSE_VALIDATION_ERROR",
|
|
68
|
+
internal: "INTERNAL_ERROR",
|
|
69
|
+
notFound: "NOT_FOUND",
|
|
70
|
+
methodNotAllowed: "METHOD_NOT_ALLOWED"
|
|
71
|
+
};
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
export function toValidationIssues(error) {
|
|
2
|
+
return error.issues.map((issue) => ({
|
|
3
|
+
path: issue.path.length > 0 ? issue.path.join(".") : "(root)",
|
|
4
|
+
message: issue.message,
|
|
5
|
+
expected: "expected" in issue ? String(issue.expected) : void 0,
|
|
6
|
+
received: "received" in issue ? truncateReceived(String(issue.received)) : void 0
|
|
7
|
+
}));
|
|
8
|
+
}
|
|
9
|
+
export function formatValidationMessage(options) {
|
|
10
|
+
const lines = [
|
|
11
|
+
"[nuxt-api-contract]",
|
|
12
|
+
"",
|
|
13
|
+
`Invalid ${options.subject} for ${options.method} ${options.path}`,
|
|
14
|
+
""
|
|
15
|
+
];
|
|
16
|
+
for (const issue of options.issues) {
|
|
17
|
+
const path = issue.path === "(root)" ? options.subject : `${options.subject}.${issue.path}`;
|
|
18
|
+
lines.push(`${path}:`);
|
|
19
|
+
lines.push(` Expected ${issue.expected ?? issue.message}`);
|
|
20
|
+
if (options.includeReceived !== false && issue.received !== void 0) {
|
|
21
|
+
lines.push(` Received ${issue.received}`);
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
return lines.join("\n");
|
|
25
|
+
}
|
|
26
|
+
function truncateReceived(value, max = 80) {
|
|
27
|
+
const serialized = value.length > max ? `${value.slice(0, max)}\u2026` : value;
|
|
28
|
+
return serialized;
|
|
29
|
+
}
|
|
30
|
+
export function sanitizeIssues(issues) {
|
|
31
|
+
return issues.map((issue) => ({ ...issue, received: void 0 }));
|
|
32
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
export function serializeQueryValue(value) {
|
|
2
|
+
if (value instanceof Date) return value.toISOString();
|
|
3
|
+
if (typeof value === "bigint") return value.toString();
|
|
4
|
+
if (Array.isArray(value)) return value.map((item) => serializePrimitive(item));
|
|
5
|
+
if (value === void 0 || value === null) return "";
|
|
6
|
+
if (typeof value === "object") return JSON.stringify(value);
|
|
7
|
+
return value;
|
|
8
|
+
}
|
|
9
|
+
function serializePrimitive(value) {
|
|
10
|
+
if (value instanceof Date) return value.toISOString();
|
|
11
|
+
if (typeof value === "bigint") return value.toString();
|
|
12
|
+
if (value === void 0 || value === null || typeof value === "object") return JSON.stringify(value) ?? "";
|
|
13
|
+
return value;
|
|
14
|
+
}
|
|
15
|
+
export function serializeQuery(query) {
|
|
16
|
+
if (!query) return void 0;
|
|
17
|
+
const result = {};
|
|
18
|
+
for (const [key, value] of Object.entries(query)) {
|
|
19
|
+
if (value === void 0) continue;
|
|
20
|
+
result[key] = serializeQueryValue(value);
|
|
21
|
+
}
|
|
22
|
+
return result;
|
|
23
|
+
}
|
|
24
|
+
export function stableStringify(value) {
|
|
25
|
+
return JSON.stringify(sortValue(value));
|
|
26
|
+
}
|
|
27
|
+
function sortValue(value) {
|
|
28
|
+
if (Array.isArray(value)) return value.map((item) => sortValue(item));
|
|
29
|
+
if (value instanceof Date) return value.toISOString();
|
|
30
|
+
if (typeof value === "bigint") return value.toString();
|
|
31
|
+
if (value !== null && typeof value === "object") {
|
|
32
|
+
const entries = Object.entries(value).filter(([, item]) => item !== void 0).sort(([a], [b]) => a < b ? -1 : a > b ? 1 : 0);
|
|
33
|
+
return Object.fromEntries(entries.map(([key, item]) => [key, sortValue(item)]));
|
|
34
|
+
}
|
|
35
|
+
return value;
|
|
36
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const API_CONTRACT_KIND = "api-contract";
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export { ContractMock, MockResponseInput, buildRequestPath, clearContractRegistry, defineApiContract, getContractByName, getContractMock, isApiContract, listRegisteredContracts, mockContract, registerContract, serializeQuery, serializeQueryValue, stableStringify } from './client.mjs';
|
|
2
|
+
export { A as ApiError, a as ApiErrorPayload, B as BUILT_IN_ERROR_CODES, C as CreateApiErrorInput, V as ValidationIssue, c as createApiError, f as formatValidationMessage, i as isApiError, p as parseApiErrorPayload, s as sanitizeIssues, b as serializeApiError, t as toApiError, d as toValidationIssues } from './shared/nuxt-api-contract.D31EDcwH.mjs';
|
|
3
|
+
import { A as AnyApiContract } from './shared/nuxt-api-contract.CPm9WbWA.mjs';
|
|
4
|
+
export { a as API_CONTRACT_KIND, b as ApiContract, c as ApiContractDefinition, d as ApiRequestOptions, e as AuthConfig, C as ContractBodyInput, f as ContractClientResponse, g as ContractErrorCode, h as ContractFromDefinition, i as ContractHandlerResponse, j as ContractHeadersInput, k as ContractParamsInput, l as ContractPathParams, m as ContractQueryInput, E as EmptyObject, n as ExtractSchemaInput, o as ExtractSchemaOutput, H as HttpMethod, I as IsEmptyObject, M as MaybePromise, P as PathParams, R as ResolvedApiRequestOptions, S as SplitPath } from './shared/nuxt-api-contract.CPm9WbWA.mjs';
|
|
5
|
+
export { C as ContractHandler, a as ContractHandlerContext, d as defineContractHandler } from './shared/nuxt-api-contract.B9JBCRk8.mjs';
|
|
6
|
+
import { z, ZodType } from 'zod';
|
|
7
|
+
import 'h3';
|
|
8
|
+
|
|
9
|
+
type ResponseValidationMode = 'never' | 'development' | 'always';
|
|
10
|
+
interface RuntimeApiContractConfig {
|
|
11
|
+
validateResponse?: ResponseValidationMode;
|
|
12
|
+
mocks?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Reads runtime contract config from the current Nitro runtime config. */
|
|
15
|
+
declare function readRuntimeConfig(getConfig: () => unknown): RuntimeApiContractConfig;
|
|
16
|
+
/** Whether response validation is active for the current environment. */
|
|
17
|
+
declare function shouldValidateResponse(mode: ResponseValidationMode | undefined): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Validates an input (params / query / body / headers) against a schema and
|
|
20
|
+
* throws a typed, developer-friendly `VALIDATION_ERROR` on failure.
|
|
21
|
+
*
|
|
22
|
+
* In production the "received" values are stripped from issues so that
|
|
23
|
+
* sensitive data (passwords, tokens) is never echoed back.
|
|
24
|
+
*/
|
|
25
|
+
declare function validateContractInput<S extends ZodType>(contract: AnyApiContract, subject: 'params' | 'query' | 'body' | 'headers', schema: S, value: unknown): z.output<S>;
|
|
26
|
+
/**
|
|
27
|
+
* Validates a handler response against the contract's response schema.
|
|
28
|
+
* Failures produce `API_CONTRACT_RESPONSE_VALIDATION_ERROR` with contract,
|
|
29
|
+
* path, method and issues — but never expose the received payload in
|
|
30
|
+
* production logs or responses.
|
|
31
|
+
*/
|
|
32
|
+
declare function validateContractResponse<S extends ZodType>(contract: AnyApiContract, schema: S, value: unknown): z.output<S>;
|
|
33
|
+
/**
|
|
34
|
+
* Coerces a raw query object (all string values) before Zod validation is
|
|
35
|
+
* applied by the caller. Kept as an explicit seam so `z.coerce` usage stays
|
|
36
|
+
* documented and predictable.
|
|
37
|
+
*/
|
|
38
|
+
declare function rawQuerySchema(): z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
39
|
+
|
|
40
|
+
export { AnyApiContract, rawQuerySchema, readRuntimeConfig, shouldValidateResponse, validateContractInput, validateContractResponse };
|
|
41
|
+
export type { ResponseValidationMode, RuntimeApiContractConfig };
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
export { ContractMock, MockResponseInput, buildRequestPath, clearContractRegistry, defineApiContract, getContractByName, getContractMock, isApiContract, listRegisteredContracts, mockContract, registerContract, serializeQuery, serializeQueryValue, stableStringify } from './client.js';
|
|
2
|
+
export { A as ApiError, a as ApiErrorPayload, B as BUILT_IN_ERROR_CODES, C as CreateApiErrorInput, V as ValidationIssue, c as createApiError, f as formatValidationMessage, i as isApiError, p as parseApiErrorPayload, s as sanitizeIssues, b as serializeApiError, t as toApiError, d as toValidationIssues } from './shared/nuxt-api-contract.D31EDcwH.js';
|
|
3
|
+
import { A as AnyApiContract } from './shared/nuxt-api-contract.CPm9WbWA.js';
|
|
4
|
+
export { a as API_CONTRACT_KIND, b as ApiContract, c as ApiContractDefinition, d as ApiRequestOptions, e as AuthConfig, C as ContractBodyInput, f as ContractClientResponse, g as ContractErrorCode, h as ContractFromDefinition, i as ContractHandlerResponse, j as ContractHeadersInput, k as ContractParamsInput, l as ContractPathParams, m as ContractQueryInput, E as EmptyObject, n as ExtractSchemaInput, o as ExtractSchemaOutput, H as HttpMethod, I as IsEmptyObject, M as MaybePromise, P as PathParams, R as ResolvedApiRequestOptions, S as SplitPath } from './shared/nuxt-api-contract.CPm9WbWA.js';
|
|
5
|
+
export { C as ContractHandler, a as ContractHandlerContext, d as defineContractHandler } from './shared/nuxt-api-contract.S1zqCiJX.js';
|
|
6
|
+
import { z, ZodType } from 'zod';
|
|
7
|
+
import 'h3';
|
|
8
|
+
|
|
9
|
+
type ResponseValidationMode = 'never' | 'development' | 'always';
|
|
10
|
+
interface RuntimeApiContractConfig {
|
|
11
|
+
validateResponse?: ResponseValidationMode;
|
|
12
|
+
mocks?: boolean;
|
|
13
|
+
}
|
|
14
|
+
/** Reads runtime contract config from the current Nitro runtime config. */
|
|
15
|
+
declare function readRuntimeConfig(getConfig: () => unknown): RuntimeApiContractConfig;
|
|
16
|
+
/** Whether response validation is active for the current environment. */
|
|
17
|
+
declare function shouldValidateResponse(mode: ResponseValidationMode | undefined): boolean;
|
|
18
|
+
/**
|
|
19
|
+
* Validates an input (params / query / body / headers) against a schema and
|
|
20
|
+
* throws a typed, developer-friendly `VALIDATION_ERROR` on failure.
|
|
21
|
+
*
|
|
22
|
+
* In production the "received" values are stripped from issues so that
|
|
23
|
+
* sensitive data (passwords, tokens) is never echoed back.
|
|
24
|
+
*/
|
|
25
|
+
declare function validateContractInput<S extends ZodType>(contract: AnyApiContract, subject: 'params' | 'query' | 'body' | 'headers', schema: S, value: unknown): z.output<S>;
|
|
26
|
+
/**
|
|
27
|
+
* Validates a handler response against the contract's response schema.
|
|
28
|
+
* Failures produce `API_CONTRACT_RESPONSE_VALIDATION_ERROR` with contract,
|
|
29
|
+
* path, method and issues — but never expose the received payload in
|
|
30
|
+
* production logs or responses.
|
|
31
|
+
*/
|
|
32
|
+
declare function validateContractResponse<S extends ZodType>(contract: AnyApiContract, schema: S, value: unknown): z.output<S>;
|
|
33
|
+
/**
|
|
34
|
+
* Coerces a raw query object (all string values) before Zod validation is
|
|
35
|
+
* applied by the caller. Kept as an explicit seam so `z.coerce` usage stays
|
|
36
|
+
* documented and predictable.
|
|
37
|
+
*/
|
|
38
|
+
declare function rawQuerySchema(): z.ZodRecord<z.ZodString, z.ZodUnknown>;
|
|
39
|
+
|
|
40
|
+
export { AnyApiContract, rawQuerySchema, readRuntimeConfig, shouldValidateResponse, validateContractInput, validateContractResponse };
|
|
41
|
+
export type { ResponseValidationMode, RuntimeApiContractConfig };
|