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.
@@ -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._def) {
892
- if (current._def.typeName === 'ZodNullable' || current._def.typeName === 'ZodOptional') {
895
+ while (current && getZodDef(current)) {
896
+ const currentType = getZodTypeName(current);
897
+ if (currentType === 'ZodNullable' || currentType === 'ZodOptional') {
893
898
  isNullable = true;
894
899
  }
895
- if (current._def.innerType) {
896
- current = current._def.innerType;
897
- } else if (current._def.schema) {
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._def && current._def.typeName) {
909
- const typeName = current._def.typeName;
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
- const isInt = current._def.checks?.some((c) => c.kind === 'int');
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._def.values || [], nullable: isNullable };
930
+ inferred = { type: 'enum', enumValues: getZodEnumValues(current), nullable: isNullable };
928
931
  break;
929
932
  case 'ZodArray':
930
933
  inferred = { type: 'files', nullable: isNullable };
@@ -63,14 +63,13 @@ function resolveNanoidSize(arg) {
63
63
  * @param {typeof import('zod').z} zod
64
64
  */
65
65
  function extendZ(zod) {
66
- return new Proxy(zod, {
67
- get(target, prop, receiver) {
68
- if (prop === 'nanoid') {
69
- return function nanoid(arg) {
70
- return zodNanoid(zod, resolveNanoidSize(arg));
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
- const result = compiled.safeParse(sanitized);
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, {
@@ -6,6 +6,9 @@
6
6
  * @module core/validation/apply
7
7
  */
8
8
 
9
+ const { ensureZodCompat } = require('./zod-introspect');
10
+ ensureZodCompat();
11
+
9
12
  const EMPTY_INPUT = Object.freeze({
10
13
  body: undefined,
11
14
  params: undefined,
@@ -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.111",
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": "^3.23.0",
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.11",
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.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.11",
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.1",
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 { zodToJsonSchema } = require('zod-to-json-schema');
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 = zodToJsonSchema(schema, {
102
+ const rawSchema = zodToJsonSchemaCompat(schema, {
103
103
  $refStrategy: 'none',
104
104
  removeAdditionalStrategy: 'strict',
105
105
  });
@@ -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}