webspresso 0.0.111 → 0.0.113
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/core/client/adapter/datastar.js +700 -50
- package/core/client/api.js +147 -7
- package/core/client/controller.js +254 -22
- package/core/client/devtool.js +59 -33
- package/core/client/forms.js +598 -38
- package/core/client/fragments.js +644 -112
- package/core/client/host.js +5 -1
- package/core/client/index.js +13 -1
- package/core/client/lite.js +1 -1
- package/core/client/server-fragments.js +145 -37
- package/core/client/utils.js +72 -17
- package/core/http/pipeline.js +82 -1
- package/core/openapi/build-from-api-routes.js +5 -5
- package/core/orm/json-fields.js +7 -6
- package/core/orm/schema-helpers.js +18 -15
- package/core/orm/utils/nanoid.js +7 -8
- package/core/services/validator.js +12 -1
- package/core/validation/apply.js +3 -0
- package/core/validation/index.js +25 -0
- package/core/validation/zod-introspect.js +187 -0
- package/index.d.ts +110 -2
- package/package.json +9 -8
- package/plugins/mcp/providers/services.js +2 -2
- package/src/api/define-api.js +2 -0
- package/src/api/validation-i18n.js +6 -2
- package/src/bundle/index.js +67 -3
- package/src/dev/api-typegen.js +79 -22
- package/src/dev/typegen.js +61 -35
- package/src/file-router.js +24 -6
- package/src/routing/mount-discovered-routes.js +24 -6
|
@@ -5,6 +5,13 @@
|
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
7
|
const { safeJsonParse } = require('../utils/json');
|
|
8
|
+
const {
|
|
9
|
+
getZodDef,
|
|
10
|
+
getZodTypeName,
|
|
11
|
+
getZodInner,
|
|
12
|
+
getZodEnumValues,
|
|
13
|
+
isZodInt,
|
|
14
|
+
} = require('../validation/zod-introspect');
|
|
8
15
|
|
|
9
16
|
const METADATA_MARKER = '__wdb__';
|
|
10
17
|
const metaWeakMap = new WeakMap();
|
|
@@ -665,10 +672,7 @@ function createSchemaHelpers(z) {
|
|
|
665
672
|
*/
|
|
666
673
|
json(options = {}) {
|
|
667
674
|
const { nullable = false, ...rest } = options;
|
|
668
|
-
let schema = z.unknown();
|
|
669
|
-
if (nullable) {
|
|
670
|
-
schema = schema.nullable().optional();
|
|
671
|
-
}
|
|
675
|
+
let schema = nullable ? z.unknown().nullable().optional() : z.unknown().optional();
|
|
672
676
|
return createSchemaBuilder(schema, {
|
|
673
677
|
type: 'json',
|
|
674
678
|
nullable,
|
|
@@ -888,14 +892,14 @@ function extractColumnsFromSchema(schema) {
|
|
|
888
892
|
// Unwrap optional/nullable wrappers to get to the base schema
|
|
889
893
|
let current = schemaToCheck;
|
|
890
894
|
let isNullable = false;
|
|
891
|
-
while (current && current
|
|
892
|
-
|
|
895
|
+
while (current && getZodDef(current)) {
|
|
896
|
+
const currentType = getZodTypeName(current);
|
|
897
|
+
if (currentType === 'ZodNullable' || currentType === 'ZodOptional') {
|
|
893
898
|
isNullable = true;
|
|
894
899
|
}
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
current = current._def.schema;
|
|
900
|
+
const inner = getZodInner(current);
|
|
901
|
+
if (inner) {
|
|
902
|
+
current = inner;
|
|
899
903
|
} else {
|
|
900
904
|
break;
|
|
901
905
|
}
|
|
@@ -905,16 +909,15 @@ function extractColumnsFromSchema(schema) {
|
|
|
905
909
|
const meta = getColumnMeta(schemaToCheck) || (current ? getColumnMeta(current) : null);
|
|
906
910
|
if (meta) {
|
|
907
911
|
columns.set(key, meta);
|
|
908
|
-
} else if (current && current
|
|
909
|
-
const typeName = current
|
|
912
|
+
} else if (current && getZodTypeName(current)) {
|
|
913
|
+
const typeName = getZodTypeName(current);
|
|
910
914
|
let inferred = null;
|
|
911
915
|
switch (typeName) {
|
|
912
916
|
case 'ZodString':
|
|
913
917
|
inferred = { type: 'string', maxLength: 255, nullable: isNullable };
|
|
914
918
|
break;
|
|
915
919
|
case 'ZodNumber': {
|
|
916
|
-
|
|
917
|
-
inferred = { type: isInt ? 'integer' : 'float', nullable: isNullable };
|
|
920
|
+
inferred = { type: isZodInt(current) ? 'integer' : 'float', nullable: isNullable };
|
|
918
921
|
break;
|
|
919
922
|
}
|
|
920
923
|
case 'ZodBoolean':
|
|
@@ -924,7 +927,7 @@ function extractColumnsFromSchema(schema) {
|
|
|
924
927
|
inferred = { type: 'timestamp', nullable: isNullable };
|
|
925
928
|
break;
|
|
926
929
|
case 'ZodEnum':
|
|
927
|
-
inferred = { type: 'enum', enumValues: current
|
|
930
|
+
inferred = { type: 'enum', enumValues: getZodEnumValues(current), nullable: isNullable };
|
|
928
931
|
break;
|
|
929
932
|
case 'ZodArray':
|
|
930
933
|
inferred = { type: 'files', nullable: isNullable };
|
package/core/orm/utils/nanoid.js
CHANGED
|
@@ -63,14 +63,13 @@ function resolveNanoidSize(arg) {
|
|
|
63
63
|
* @param {typeof import('zod').z} zod
|
|
64
64
|
*/
|
|
65
65
|
function extendZ(zod) {
|
|
66
|
-
return
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
return
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
return Reflect.get(target, prop, receiver);
|
|
66
|
+
return Object.create(zod, {
|
|
67
|
+
nanoid: {
|
|
68
|
+
value: function nanoid(arg) {
|
|
69
|
+
return zodNanoid(zod, resolveNanoidSize(arg));
|
|
70
|
+
},
|
|
71
|
+
enumerable: true,
|
|
72
|
+
configurable: true,
|
|
74
73
|
},
|
|
75
74
|
});
|
|
76
75
|
}
|
|
@@ -293,7 +293,18 @@ function validateServiceInput(schema, input, serviceName, options = {}) {
|
|
|
293
293
|
// 1. Zod schema validation
|
|
294
294
|
if (compiled instanceof z.ZodType || typeof compiled.safeParse === 'function') {
|
|
295
295
|
try {
|
|
296
|
-
|
|
296
|
+
let result;
|
|
297
|
+
if (compiled._zod && typeof compiled._zod.run === 'function') {
|
|
298
|
+
const rawRun = compiled._zod.run({ value: sanitized, issues: [] }, { async: true });
|
|
299
|
+
if (rawRun instanceof Promise) {
|
|
300
|
+
// Attach no-op catch so async refinements rejected during sync preflight don't emit UnhandledRejection
|
|
301
|
+
rawRun.catch(() => {});
|
|
302
|
+
throw new Error('Encountered Promise during synchronous parse');
|
|
303
|
+
}
|
|
304
|
+
result = rawRun.issues.length ? compiled.safeParse(sanitized) : { success: true, data: rawRun.value };
|
|
305
|
+
} else {
|
|
306
|
+
result = compiled.safeParse(sanitized);
|
|
307
|
+
}
|
|
297
308
|
if (!result.success) {
|
|
298
309
|
const { message, fields, fieldErrors } = formatZodErrors(result.error, serviceName);
|
|
299
310
|
throw createServiceValidationError(message, {
|
package/core/validation/apply.js
CHANGED
package/core/validation/index.js
CHANGED
|
@@ -28,6 +28,31 @@ if (z.ZodType && z.ZodType.prototype && typeof z.ZodType.prototype._refinement =
|
|
|
28
28
|
};
|
|
29
29
|
wrappedRefinement.__webspresso_wrapped = true;
|
|
30
30
|
z.ZodType.prototype._refinement = wrappedRefinement;
|
|
31
|
+
} else if (z.ZodType && z.ZodType.prototype && typeof z.ZodType.prototype.refine === 'function' && !z.ZodType.prototype.refine.__webspresso_wrapped) {
|
|
32
|
+
const origRefine = z.ZodType.prototype.refine;
|
|
33
|
+
const wrappedRefine = function (check, params) {
|
|
34
|
+
const wrappedCheck = typeof check === 'function'
|
|
35
|
+
? (val) => {
|
|
36
|
+
const res = check(val);
|
|
37
|
+
if (res && typeof res.catch === 'function') {
|
|
38
|
+
res.catch(() => {});
|
|
39
|
+
}
|
|
40
|
+
return res;
|
|
41
|
+
}
|
|
42
|
+
: check;
|
|
43
|
+
return origRefine.call(this, wrappedCheck, params);
|
|
44
|
+
};
|
|
45
|
+
wrappedRefine.__webspresso_wrapped = true;
|
|
46
|
+
z.ZodType.prototype.refine = wrappedRefine;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (z.ZodError && z.ZodError.prototype && !Object.getOwnPropertyDescriptor(z.ZodError.prototype, 'errors')) {
|
|
50
|
+
Object.defineProperty(z.ZodError.prototype, 'errors', {
|
|
51
|
+
get() {
|
|
52
|
+
return this.issues;
|
|
53
|
+
},
|
|
54
|
+
configurable: true,
|
|
55
|
+
});
|
|
31
56
|
}
|
|
32
57
|
|
|
33
58
|
/**
|
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Zod v3 & v4 Introspection Helpers
|
|
3
|
+
* Provides a unified abstraction over Zod v3 (`_def.typeName`) and Zod v4 (`_zod.def.type`)
|
|
4
|
+
* for ORM column inference, TypeGen, API TypeGen, and OpenAPI/MCP JSON Schema generation.
|
|
5
|
+
* @module core/validation/zod-introspect
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const z = require('zod');
|
|
9
|
+
|
|
10
|
+
const V4_TYPE_MAP = Object.freeze({
|
|
11
|
+
string: 'ZodString',
|
|
12
|
+
number: 'ZodNumber',
|
|
13
|
+
boolean: 'ZodBoolean',
|
|
14
|
+
date: 'ZodDate',
|
|
15
|
+
bigint: 'ZodBigInt',
|
|
16
|
+
any: 'ZodAny',
|
|
17
|
+
unknown: 'ZodUnknown',
|
|
18
|
+
never: 'ZodNever',
|
|
19
|
+
undefined: 'ZodUndefined',
|
|
20
|
+
void: 'ZodVoid',
|
|
21
|
+
null: 'ZodNull',
|
|
22
|
+
literal: 'ZodLiteral',
|
|
23
|
+
enum: 'ZodEnum',
|
|
24
|
+
array: 'ZodArray',
|
|
25
|
+
optional: 'ZodOptional',
|
|
26
|
+
nullable: 'ZodNullable',
|
|
27
|
+
default: 'ZodDefault',
|
|
28
|
+
prefault: 'ZodDefault',
|
|
29
|
+
transform: 'ZodEffects',
|
|
30
|
+
pipe: 'ZodPipeline',
|
|
31
|
+
union: 'ZodUnion',
|
|
32
|
+
object: 'ZodObject',
|
|
33
|
+
record: 'ZodRecord',
|
|
34
|
+
tuple: 'ZodTuple',
|
|
35
|
+
set: 'ZodSet',
|
|
36
|
+
map: 'ZodMap',
|
|
37
|
+
nonoptional: 'ZodNonOptional',
|
|
38
|
+
readonly: 'ZodReadonly',
|
|
39
|
+
catch: 'ZodCatch',
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
function getZodDef(schema) {
|
|
43
|
+
if (!schema || typeof schema !== 'object') return null;
|
|
44
|
+
return (schema._zod && schema._zod.def) || schema._def || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function getZodTypeName(schema) {
|
|
48
|
+
const def = getZodDef(schema);
|
|
49
|
+
if (!def) return null;
|
|
50
|
+
if (def.typeName) return def.typeName;
|
|
51
|
+
if (typeof def.type === 'string') {
|
|
52
|
+
return V4_TYPE_MAP[def.type] || null;
|
|
53
|
+
}
|
|
54
|
+
return null;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function isZodSchema(val) {
|
|
58
|
+
return Boolean(val && typeof val === 'object' && (val._zod || val._def) && typeof val.safeParse === 'function');
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function getZodInner(schema) {
|
|
62
|
+
const def = getZodDef(schema);
|
|
63
|
+
if (!def) return null;
|
|
64
|
+
return def.innerType || def.schema || def.in || null;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function getZodPipeOut(schema) {
|
|
68
|
+
const def = getZodDef(schema);
|
|
69
|
+
if (!def) return null;
|
|
70
|
+
return def.out || null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getZodArrayElement(schema) {
|
|
74
|
+
const def = getZodDef(schema);
|
|
75
|
+
if (!def) return null;
|
|
76
|
+
if (def.element) return def.element;
|
|
77
|
+
if (def.type && typeof def.type === 'object') return def.type;
|
|
78
|
+
return null;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function getZodObjectShape(schema) {
|
|
82
|
+
if (!schema || typeof schema !== 'object') return {};
|
|
83
|
+
if (typeof schema.shape === 'object' && schema.shape !== null) return schema.shape;
|
|
84
|
+
const def = getZodDef(schema);
|
|
85
|
+
if (!def) return {};
|
|
86
|
+
if (typeof def.shape === 'function') return def.shape() || {};
|
|
87
|
+
if (typeof def.shape === 'object' && def.shape !== null) return def.shape;
|
|
88
|
+
return {};
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getZodEnumValues(schema) {
|
|
92
|
+
const def = getZodDef(schema);
|
|
93
|
+
if (!def) return [];
|
|
94
|
+
if (Array.isArray(def.values)) return def.values;
|
|
95
|
+
if (def.entries && typeof def.entries === 'object') return Object.values(def.entries);
|
|
96
|
+
if (def.values && typeof def.values === 'object') return Object.values(def.values);
|
|
97
|
+
return [];
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function getZodLiteralValue(schema) {
|
|
101
|
+
const def = getZodDef(schema);
|
|
102
|
+
if (!def) return undefined;
|
|
103
|
+
if (def.value !== undefined) return def.value;
|
|
104
|
+
if (Array.isArray(def.values)) return def.values[0];
|
|
105
|
+
return undefined;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function isZodInt(schema) {
|
|
109
|
+
const def = getZodDef(schema);
|
|
110
|
+
if (!def) return false;
|
|
111
|
+
if (def.format === 'safeint' || def.format === 'int32' || def.format === 'uint32') return true;
|
|
112
|
+
if (!Array.isArray(def.checks)) return false;
|
|
113
|
+
return def.checks.some((c) => {
|
|
114
|
+
if (!c) return false;
|
|
115
|
+
if (c.kind === 'int') return true;
|
|
116
|
+
const cDef = c._zod && c._zod.def;
|
|
117
|
+
return Boolean(cDef && cDef.check === 'number_format' && (cDef.format === 'safeint' || cDef.format === 'int32'));
|
|
118
|
+
});
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
function isZodCoerced(schema) {
|
|
122
|
+
const def = getZodDef(schema);
|
|
123
|
+
return Boolean(def && def.coerce);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
function zodToJsonSchemaCompat(schema, options = {}) {
|
|
127
|
+
if (typeof z.toJSONSchema === 'function') {
|
|
128
|
+
try {
|
|
129
|
+
const target = options.target === 'openApi3' ? 'openapi-3.0' : (options.target || 'draft-7');
|
|
130
|
+
const jsonSchema = z.toJSONSchema(schema, {
|
|
131
|
+
target,
|
|
132
|
+
unrepresentable: 'any',
|
|
133
|
+
});
|
|
134
|
+
if (jsonSchema && typeof jsonSchema === 'object') {
|
|
135
|
+
delete jsonSchema.$schema;
|
|
136
|
+
}
|
|
137
|
+
return jsonSchema;
|
|
138
|
+
} catch (_) {
|
|
139
|
+
// Fall through to legacy helper if needed
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
const { zodToJsonSchema } = require('zod-to-json-schema');
|
|
143
|
+
return zodToJsonSchema(schema, options);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Apply Zod v3 compatibility shims onto Zod v4 prototypes (idempotent).
|
|
148
|
+
* - Exposes `ZodError.prototype.errors` -> `this.issues`
|
|
149
|
+
*/
|
|
150
|
+
function ensureZodCompat(zodModule = z) {
|
|
151
|
+
if (!zodModule) return;
|
|
152
|
+
|
|
153
|
+
const sampleError = typeof zodModule.string === 'function'
|
|
154
|
+
? zodModule.string().safeParse(123)?.error
|
|
155
|
+
: null;
|
|
156
|
+
const errProtos = [
|
|
157
|
+
sampleError ? Object.getPrototypeOf(sampleError) : null,
|
|
158
|
+
zodModule.ZodError?.prototype,
|
|
159
|
+
];
|
|
160
|
+
for (const errProto of errProtos) {
|
|
161
|
+
if (errProto && !Object.getOwnPropertyDescriptor(errProto, 'errors')) {
|
|
162
|
+
Object.defineProperty(errProto, 'errors', {
|
|
163
|
+
get() {
|
|
164
|
+
return this.issues;
|
|
165
|
+
},
|
|
166
|
+
configurable: true,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
module.exports = {
|
|
173
|
+
getZodDef,
|
|
174
|
+
getZodTypeName,
|
|
175
|
+
isZodSchema,
|
|
176
|
+
getZodInner,
|
|
177
|
+
getZodPipeOut,
|
|
178
|
+
getZodArrayElement,
|
|
179
|
+
getZodObjectShape,
|
|
180
|
+
getZodEnumValues,
|
|
181
|
+
getZodLiteralValue,
|
|
182
|
+
isZodInt,
|
|
183
|
+
isZodCoerced,
|
|
184
|
+
zodToJsonSchemaCompat,
|
|
185
|
+
ensureZodCompat,
|
|
186
|
+
};
|
|
187
|
+
|
package/index.d.ts
CHANGED
|
@@ -5075,6 +5075,11 @@ export interface DefineApiOptions<
|
|
|
5075
5075
|
middleware?: (string | import('express').RequestHandler | [string, Record<string, unknown>])[];
|
|
5076
5076
|
flag?: string | { key: string; statusCode?: number; message?: string };
|
|
5077
5077
|
schema?: TSchema;
|
|
5078
|
+
validateFields?: boolean | string[];
|
|
5079
|
+
validate?: (
|
|
5080
|
+
req: ApiRequest<TBody, TParams, TQuery, THeaders>,
|
|
5081
|
+
ctx: { body: TBody; fields: string[]; req: ApiRequest<TBody, TParams, TQuery, THeaders>; res: ApiResponse<TBody, TParams, TQuery, THeaders> }
|
|
5082
|
+
) => Promise<Record<string, string> | null | undefined> | Record<string, string> | null | undefined;
|
|
5078
5083
|
response?: Record<number | string, unknown>;
|
|
5079
5084
|
handler: (
|
|
5080
5085
|
req: ApiRequest<TBody, TParams, TQuery, THeaders>,
|
|
@@ -5094,6 +5099,16 @@ export function defineApi<
|
|
|
5094
5099
|
>(
|
|
5095
5100
|
config: {
|
|
5096
5101
|
schema: TSchema;
|
|
5102
|
+
validateFields?: boolean | string[];
|
|
5103
|
+
validate?: (
|
|
5104
|
+
req: ApiRequest<ApiNoInfer<TBody>, ApiNoInfer<TParams>, ApiNoInfer<TQuery>, ApiNoInfer<THeaders>>,
|
|
5105
|
+
ctx: {
|
|
5106
|
+
body: ApiNoInfer<TBody>;
|
|
5107
|
+
fields: string[];
|
|
5108
|
+
req: ApiRequest<ApiNoInfer<TBody>, ApiNoInfer<TParams>, ApiNoInfer<TQuery>, ApiNoInfer<THeaders>>;
|
|
5109
|
+
res: ApiResponse<ApiNoInfer<TBody>, ApiNoInfer<TParams>, ApiNoInfer<TQuery>, ApiNoInfer<THeaders>>;
|
|
5110
|
+
}
|
|
5111
|
+
) => Promise<Record<string, string> | null | undefined> | Record<string, string> | null | undefined;
|
|
5097
5112
|
description?: string;
|
|
5098
5113
|
tags?: string[];
|
|
5099
5114
|
middleware?: (string | import('express').RequestHandler | [string, Record<string, unknown>])[];
|
|
@@ -6367,6 +6382,9 @@ export class ClientRequestError<TData = unknown> extends Error {
|
|
|
6367
6382
|
readonly isTimedOut: boolean;
|
|
6368
6383
|
readonly isDropped: boolean;
|
|
6369
6384
|
readonly isSuperseded: boolean;
|
|
6385
|
+
readonly dispatched: boolean;
|
|
6386
|
+
readonly outcomeUnknown: boolean;
|
|
6387
|
+
readonly outcome: 'unknown' | 'failed' | 'not_sent';
|
|
6370
6388
|
readonly cause?: unknown;
|
|
6371
6389
|
constructor(options: {
|
|
6372
6390
|
message: string;
|
|
@@ -6383,6 +6401,9 @@ export class ClientRequestError<TData = unknown> extends Error {
|
|
|
6383
6401
|
isTimedOut?: boolean;
|
|
6384
6402
|
isDropped?: boolean;
|
|
6385
6403
|
isSuperseded?: boolean;
|
|
6404
|
+
dispatched?: boolean;
|
|
6405
|
+
outcomeUnknown?: boolean;
|
|
6406
|
+
outcome?: 'unknown' | 'failed' | 'not_sent';
|
|
6386
6407
|
});
|
|
6387
6408
|
}
|
|
6388
6409
|
|
|
@@ -6411,6 +6432,12 @@ export interface ClientRequestCallOptions<
|
|
|
6411
6432
|
timeout?: number;
|
|
6412
6433
|
retry?: boolean | number | ClientRetryOptions;
|
|
6413
6434
|
retryMutations?: boolean;
|
|
6435
|
+
onUploadProgress?: (progress: {
|
|
6436
|
+
loaded: number;
|
|
6437
|
+
total: number;
|
|
6438
|
+
percent: number | null;
|
|
6439
|
+
lengthComputable: boolean;
|
|
6440
|
+
}) => void;
|
|
6414
6441
|
}
|
|
6415
6442
|
|
|
6416
6443
|
export interface AppApiRouteMap {
|
|
@@ -6429,6 +6456,7 @@ export interface ClientTask<TData = any, TArgs extends unknown[] = any[]> {
|
|
|
6429
6456
|
readonly error: ClientRequestError | Error | null;
|
|
6430
6457
|
readonly stale: boolean;
|
|
6431
6458
|
readonly lastUpdatedAt: number;
|
|
6459
|
+
readonly lastArgs: TArgs;
|
|
6432
6460
|
isStale(): boolean;
|
|
6433
6461
|
run(...args: TArgs): Promise<TData | undefined>;
|
|
6434
6462
|
cancel(): void;
|
|
@@ -6439,6 +6467,8 @@ export interface ClientTaskOptions {
|
|
|
6439
6467
|
concurrency?: ClientConcurrencyPolicy;
|
|
6440
6468
|
policy?: ClientConcurrencyPolicy;
|
|
6441
6469
|
key?: string;
|
|
6470
|
+
mutation?: boolean;
|
|
6471
|
+
kind?: 'query' | 'mutation';
|
|
6442
6472
|
staleTime?: number | string;
|
|
6443
6473
|
refetchOnMount?: ClientRefetchPolicy;
|
|
6444
6474
|
refetchOnVisit?: ClientRefetchPolicy;
|
|
@@ -6713,6 +6743,8 @@ export interface ClientFormSubmitResult<TData = any> {
|
|
|
6713
6743
|
hookError?: Error | null;
|
|
6714
6744
|
error?: ClientRequestError | Error;
|
|
6715
6745
|
aborted?: boolean;
|
|
6746
|
+
outcomeUnknown?: boolean;
|
|
6747
|
+
outcome?: 'unknown' | 'failed' | 'not_sent';
|
|
6716
6748
|
}
|
|
6717
6749
|
|
|
6718
6750
|
export interface ClientFormHookMeta<
|
|
@@ -6737,10 +6769,45 @@ export interface ClientFormHandle<
|
|
|
6737
6769
|
readonly services: ClientServicesRegistry<TServices>;
|
|
6738
6770
|
readonly pending: boolean;
|
|
6739
6771
|
readonly dirty: boolean;
|
|
6772
|
+
readonly touched: boolean;
|
|
6773
|
+
readonly touchedFields: string[];
|
|
6774
|
+
readonly submitted: boolean;
|
|
6775
|
+
readonly inputRevision: number;
|
|
6776
|
+
readonly draftKey: string;
|
|
6777
|
+
readonly baseline: TValues;
|
|
6740
6778
|
readonly errors: Record<string, string>;
|
|
6741
|
-
readonly formError: string;
|
|
6779
|
+
readonly formError: string | null;
|
|
6780
|
+
readonly lastError: ClientRequestError | Error | null;
|
|
6742
6781
|
readonly fieldErrors: Partial<Record<keyof TValues & string, string>> & Record<string, string>;
|
|
6743
6782
|
disabled: boolean;
|
|
6783
|
+
isTouched(fieldName?: keyof TValues & string | string): boolean;
|
|
6784
|
+
touch(fieldName?: keyof TValues & string | string): this;
|
|
6785
|
+
untouch(fieldName?: keyof TValues & string | string): this;
|
|
6786
|
+
validateField(
|
|
6787
|
+
fieldName: (keyof TValues & string) | string,
|
|
6788
|
+
options?: { url?: string; method?: string; contextFields?: string[] | '*' }
|
|
6789
|
+
): Promise<{
|
|
6790
|
+
ok: boolean;
|
|
6791
|
+
valid?: boolean;
|
|
6792
|
+
stale?: boolean;
|
|
6793
|
+
canceled?: boolean;
|
|
6794
|
+
networkError?: boolean;
|
|
6795
|
+
errors?: Record<string, string>;
|
|
6796
|
+
error?: Error;
|
|
6797
|
+
}>;
|
|
6798
|
+
readonly uploads: Array<{
|
|
6799
|
+
field: string;
|
|
6800
|
+
name: string;
|
|
6801
|
+
size: number;
|
|
6802
|
+
status: 'uploading' | 'done' | 'error' | 'canceled';
|
|
6803
|
+
loaded: number;
|
|
6804
|
+
total: number;
|
|
6805
|
+
percent: number | null;
|
|
6806
|
+
error: string | null;
|
|
6807
|
+
}>;
|
|
6808
|
+
cancelUpload(): boolean;
|
|
6809
|
+
createPreviewUrl(file: File | Blob, fieldName?: string): string | null;
|
|
6810
|
+
revokePreviewUrls(fieldName?: string): void;
|
|
6744
6811
|
values(): TValues;
|
|
6745
6812
|
read(submitter?: HTMLElement | null): TValues;
|
|
6746
6813
|
fill(values: Partial<TValues> & Record<string, unknown>, options?: ClientFormFillOptions): this;
|
|
@@ -6754,8 +6821,12 @@ export interface ClientFormHandle<
|
|
|
6754
6821
|
saveDraft(): boolean;
|
|
6755
6822
|
restoreDraft(): boolean;
|
|
6756
6823
|
clearDraft(): boolean;
|
|
6824
|
+
setDraftKey(nextKey: string, options?: { restore?: boolean }): this;
|
|
6757
6825
|
readonly hasDraft: boolean;
|
|
6758
6826
|
submit(submitter?: HTMLElement | null | Record<string, unknown>): Promise<ClientFormSubmitResult<TResponse>>;
|
|
6827
|
+
beforeSubmit(
|
|
6828
|
+
callback: (values: TValues, meta: { form: ClientFormHandle<TValues, TResponse, TServices>; submitter?: HTMLElement | null; action: string; method: string }) => boolean | Promise<boolean>
|
|
6829
|
+
): () => void;
|
|
6759
6830
|
onSuccess(
|
|
6760
6831
|
callback: (data: TResponse, meta: ClientFormHookMeta<TValues, TResponse, TServices>) => void | Promise<void>
|
|
6761
6832
|
): () => void;
|
|
@@ -6792,10 +6863,12 @@ export interface ControllerFormsRegistry<TServices = AppClientServices> {
|
|
|
6792
6863
|
|
|
6793
6864
|
export interface FragmentRefreshResult {
|
|
6794
6865
|
ok: boolean;
|
|
6866
|
+
partial?: boolean;
|
|
6795
6867
|
refreshed: string[];
|
|
6796
6868
|
errors?: Error[];
|
|
6797
6869
|
skipped?: boolean;
|
|
6798
6870
|
canceled?: boolean;
|
|
6871
|
+
superseded?: boolean;
|
|
6799
6872
|
error?: Error | null;
|
|
6800
6873
|
timings?: {
|
|
6801
6874
|
loadMs: number;
|
|
@@ -6807,6 +6880,12 @@ export interface FragmentRefreshOptions {
|
|
|
6807
6880
|
replaceInput?: boolean;
|
|
6808
6881
|
timeout?: number;
|
|
6809
6882
|
throwOnError?: boolean;
|
|
6883
|
+
mode?: 'replace' | 'append' | 'prepend';
|
|
6884
|
+
limit?: number;
|
|
6885
|
+
target?: string;
|
|
6886
|
+
removeKeys?: string[];
|
|
6887
|
+
onBeforePatch?: (el: Element, inst: any) => void;
|
|
6888
|
+
onAfterPatch?: (el: Element, inst: any) => void;
|
|
6810
6889
|
onSuccess?: (result: FragmentRefreshResult) => void | Promise<void>;
|
|
6811
6890
|
onFailure?: (error: Error) => void | Promise<void>;
|
|
6812
6891
|
onError?: (error: Error) => void | Promise<void>;
|
|
@@ -6816,6 +6895,7 @@ export interface FragmentRefreshOptions {
|
|
|
6816
6895
|
|
|
6817
6896
|
export interface ControllerFragmentHandle<TInput extends Record<string, any> = Record<string, any>> {
|
|
6818
6897
|
readonly input: TInput;
|
|
6898
|
+
readonly inputs: Record<string, TInput>;
|
|
6819
6899
|
refresh(
|
|
6820
6900
|
nextInput?: Partial<TInput> & Record<string, unknown>,
|
|
6821
6901
|
options?: FragmentRefreshOptions
|
|
@@ -6827,10 +6907,17 @@ export interface ControllerErrorMeta {
|
|
|
6827
6907
|
task?: string;
|
|
6828
6908
|
event?: Event;
|
|
6829
6909
|
actionEl?: HTMLElement | null;
|
|
6830
|
-
phase?: 'action' | 'task' | 'connect' | 'disconnect';
|
|
6910
|
+
phase?: 'action' | 'task' | 'connect' | 'disconnect' | 'beforePatch' | 'afterPatch';
|
|
6831
6911
|
[key: string]: unknown;
|
|
6832
6912
|
}
|
|
6833
6913
|
|
|
6914
|
+
export interface FragmentPatchHookMeta {
|
|
6915
|
+
fragmentId?: string | null;
|
|
6916
|
+
fragmentName?: string | null;
|
|
6917
|
+
isFragmentRoot?: boolean;
|
|
6918
|
+
isDescendant?: boolean;
|
|
6919
|
+
}
|
|
6920
|
+
|
|
6834
6921
|
export type WatchSource<T = any> =
|
|
6835
6922
|
| (() => T)
|
|
6836
6923
|
| string
|
|
@@ -6886,7 +6973,9 @@ export interface ControllerContext<
|
|
|
6886
6973
|
readonly lastError: Error | null;
|
|
6887
6974
|
clearError(): void;
|
|
6888
6975
|
readonly storage: ClientStorage & { readonly global: ClientStorage; readonly root: ClientStorage };
|
|
6976
|
+
readonly persistenceConflicts: Array<Record<string, unknown>>;
|
|
6889
6977
|
clearPersistedState(): void;
|
|
6978
|
+
retryFailedFragments(): Promise<FragmentRefreshResult>;
|
|
6890
6979
|
onError(listener: (err: Error, meta?: ControllerErrorMeta) => void): () => void;
|
|
6891
6980
|
task<TData = any, TArgs extends unknown[] = any[]>(
|
|
6892
6981
|
runFn: (...args: TArgs) => Promise<TData>,
|
|
@@ -6977,6 +7066,22 @@ export interface ControllerDefinition<
|
|
|
6977
7066
|
TServices
|
|
6978
7067
|
>
|
|
6979
7068
|
) => void;
|
|
7069
|
+
beforePatch?: (
|
|
7070
|
+
ctx: ControllerContext<
|
|
7071
|
+
[TState][TState extends any ? 0 : never] & { readonly [K in keyof TComputed]: ReturnType<TComputed[K]> },
|
|
7072
|
+
Record<string, ClientTask>,
|
|
7073
|
+
TServices
|
|
7074
|
+
>,
|
|
7075
|
+
meta: FragmentPatchHookMeta
|
|
7076
|
+
) => void;
|
|
7077
|
+
afterPatch?: (
|
|
7078
|
+
ctx: ControllerContext<
|
|
7079
|
+
[TState][TState extends any ? 0 : never] & { readonly [K in keyof TComputed]: ReturnType<TComputed[K]> },
|
|
7080
|
+
Record<string, ClientTask>,
|
|
7081
|
+
TServices
|
|
7082
|
+
>,
|
|
7083
|
+
meta: FragmentPatchHookMeta
|
|
7084
|
+
) => void | Promise<void>;
|
|
6980
7085
|
onError?: ControllerErrorHandler<
|
|
6981
7086
|
[TState][TState extends any ? 0 : never] & { readonly [K in keyof TComputed]: ReturnType<TComputed[K]> },
|
|
6982
7087
|
Record<string, ClientTask>,
|
|
@@ -7041,6 +7146,9 @@ export interface FragmentDefinition<TInput = Record<string, unknown>, TData = Re
|
|
|
7041
7146
|
refetchOnVisit?: ClientRefetchPolicy;
|
|
7042
7147
|
refetchInterval?: number | string | false;
|
|
7043
7148
|
refetchIntervalInBackground?: boolean;
|
|
7149
|
+
lazy?: boolean;
|
|
7150
|
+
placeholder?: string | ((ctx: { input: TInput; instanceId: string; name: string }) => string);
|
|
7151
|
+
placeholderTemplate?: string;
|
|
7044
7152
|
load?: (context: FragmentLoaderContext<TInput>) => TData | Promise<TData>;
|
|
7045
7153
|
}
|
|
7046
7154
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "webspresso",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.113",
|
|
4
4
|
"description": "Minimal, production-ready SSR framework for Node.js with file-based routing, Nunjucks templating, built-in i18n, and CLI tooling",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"types": "index.d.ts",
|
|
@@ -100,7 +100,7 @@
|
|
|
100
100
|
"bench:ci:local": "npm run bench:baseline && npm run bench:ci",
|
|
101
101
|
"bench:profile": "softscope --top 30 node scripts/profile-bench.mjs",
|
|
102
102
|
"bench:profile:ai": "softscope --top 40 -o softscope-bench.ai node scripts/profile-bench.mjs",
|
|
103
|
-
"check:types": "tsc --project tests/ts-smoke/tsconfig.json && tsc --noEmit --skipLibCheck false --types node index.d.ts",
|
|
103
|
+
"check:types": "tsc --project tests/ts-smoke/tsconfig.json && tsc --noEmit --skipLibCheck false --esModuleInterop --types node index.d.ts",
|
|
104
104
|
"rebuild:native": "npm rebuild better-sqlite3 bcrypt sharp",
|
|
105
105
|
"docs:dev": "docboot dev ./docs",
|
|
106
106
|
"docs:build": "docboot build ./docs",
|
|
@@ -148,7 +148,7 @@
|
|
|
148
148
|
"qs": "^6.17.1",
|
|
149
149
|
"sanitize-html": "^2.18.1",
|
|
150
150
|
"yaml": "^2.8.3",
|
|
151
|
-
"zod": "^
|
|
151
|
+
"zod": "^4.6.7",
|
|
152
152
|
"zod-to-json-schema": "^3.25.2"
|
|
153
153
|
},
|
|
154
154
|
"peerDependencies": {
|
|
@@ -218,7 +218,7 @@
|
|
|
218
218
|
"@swup/scripts-plugin": "^2.1.0",
|
|
219
219
|
"@types/express": "^5.0.0",
|
|
220
220
|
"@types/node": "^20.14.0",
|
|
221
|
-
"@vitest/coverage-v8": "^4.1.
|
|
221
|
+
"@vitest/coverage-v8": "^4.1.12",
|
|
222
222
|
"bcrypt": "^6.0.0",
|
|
223
223
|
"better-sqlite3": "^11.10.0",
|
|
224
224
|
"chokidar": "^4.0.3",
|
|
@@ -226,7 +226,7 @@
|
|
|
226
226
|
"dotenv": "^16.3.1",
|
|
227
227
|
"exceljs": "^4.4.0",
|
|
228
228
|
"express-rate-limit": "^8.4.1",
|
|
229
|
-
"hono": "^4.13.
|
|
229
|
+
"hono": "^4.13.16",
|
|
230
230
|
"mjml": "^5.4.0",
|
|
231
231
|
"nodemailer": "^10.0.10",
|
|
232
232
|
"release-it": "^20.2.1",
|
|
@@ -235,14 +235,14 @@
|
|
|
235
235
|
"supertest": "^6.3.4",
|
|
236
236
|
"swup": "^4.8.3",
|
|
237
237
|
"typescript": "~5.6.3",
|
|
238
|
-
"vitest": "^4.1.
|
|
238
|
+
"vitest": "^4.1.12",
|
|
239
239
|
"vitest-doctor": "^0.3.0"
|
|
240
240
|
},
|
|
241
241
|
"engines": {
|
|
242
242
|
"node": ">=20.0.0"
|
|
243
243
|
},
|
|
244
244
|
"overrides": {
|
|
245
|
-
"basic-ftp": "^6.2.
|
|
245
|
+
"basic-ftp": "^6.2.4",
|
|
246
246
|
"chokidar": "^4.0.3",
|
|
247
247
|
"katex": "^0.18.2",
|
|
248
248
|
"tar": "^7.5.11",
|
|
@@ -251,7 +251,8 @@
|
|
|
251
251
|
"js-yaml": "^5.4.2",
|
|
252
252
|
"qs": "^6.17.1",
|
|
253
253
|
"sanitize-html": "^2.18.1",
|
|
254
|
-
"vite": "^8.3.5"
|
|
254
|
+
"vite": "^8.3.5",
|
|
255
|
+
"minimatch@>=7.0.0 <=11.0.0": "^11.0.8"
|
|
255
256
|
},
|
|
256
257
|
"allowScripts": {
|
|
257
258
|
"bcrypt@6.0.0": true,
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* @module plugins/mcp/providers/services
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
const {
|
|
7
|
+
const { zodToJsonSchemaCompat } = require('../../../core/validation/zod-introspect');
|
|
8
8
|
|
|
9
9
|
/**
|
|
10
10
|
* Normalizes a dot-separated service name to an MCP-safe tool name
|
|
@@ -99,7 +99,7 @@ function convertZodToInputSchema(schema) {
|
|
|
99
99
|
}
|
|
100
100
|
|
|
101
101
|
try {
|
|
102
|
-
const rawSchema =
|
|
102
|
+
const rawSchema = zodToJsonSchemaCompat(schema, {
|
|
103
103
|
$refStrategy: 'none',
|
|
104
104
|
removeAdditionalStrategy: 'strict',
|
|
105
105
|
});
|
package/src/api/define-api.js
CHANGED
|
@@ -13,6 +13,8 @@
|
|
|
13
13
|
* @param {Array<string|Function>} [config.middleware] - Named or functional middlewares
|
|
14
14
|
* @param {string|{ key: string, statusCode?: number, message?: string }} [config.flag] - Feature flag required to access this endpoint
|
|
15
15
|
* @param {Object|Function} [config.schema] - Request schemas (params, query, body, headers)
|
|
16
|
+
* @param {boolean|string[]} [config.validateFields] - Opt-in field-level validation without running mutation handler
|
|
17
|
+
* @param {Function} [config.validate] - Optional async read-only validation hook: async (ctx, fields) => ({ field: 'msg' })
|
|
16
18
|
* @param {Object} [config.response] - Expected response schemas by status code
|
|
17
19
|
* @param {Function} config.handler - Request handler: async (req, ctx) => data
|
|
18
20
|
* @returns {Object}
|