From 7a1c6c0041ef1d8fa45ab170f15397dcccceb82e Mon Sep 17 00:00:00 2001 From: Ajay Ghanwat Date: Mon, 13 Jul 2026 20:34:36 +0530 Subject: [PATCH] feat: support custom required validation messages --- .publish/ai/package.json | 2 +- .publish/authz/package.json | 2 +- .publish/cli/package.json | 22 ++++----- .publish/compiler/package.json | 2 +- .publish/core/package.json | 2 +- .publish/csr/package.json | 4 +- .publish/db/package.json | 2 +- .publish/dev-server/package.json | 26 +++++------ .publish/encryption/package.json | 2 +- .publish/helpers/package.json | 4 +- .publish/i18n/package.json | 4 +- .publish/jwt/package.json | 2 +- .publish/mobile/package.json | 4 +- .publish/native/package.json | 2 +- .publish/oauth/package.json | 2 +- .publish/pubsub/package.json | 2 +- .publish/queue/package.json | 2 +- .publish/reactive/package.json | 2 +- .publish/router/package.json | 6 +-- .publish/ssr/package.json | 4 +- .publish/styles/package.json | 4 +- .publish/test/package.json | 2 +- .publish/tracking/package.json | 2 +- .publish/ui/package.json | 4 +- .publish/uploader/package.json | 4 +- .publish/validation/README.md | 5 +- .publish/validation/package.json | 2 +- packages/ai/package.json | 2 +- packages/authz/package.json | 2 +- packages/cli/package.json | 2 +- packages/cli/src/update.ts | 8 ++++ packages/compiler/package.json | 2 +- packages/core/package.json | 2 +- packages/csr/package.json | 2 +- packages/db/package.json | 2 +- packages/dev-server/package.json | 2 +- packages/encryption/package.json | 2 +- packages/helpers/package.json | 2 +- packages/i18n/package.json | 2 +- packages/jwt/package.json | 2 +- packages/mobile/package.json | 2 +- packages/native/package.json | 2 +- packages/oauth/package.json | 2 +- packages/pubsub/package.json | 2 +- packages/queue/package.json | 2 +- packages/reactive/package.json | 2 +- packages/router/package.json | 2 +- packages/ssr/package.json | 2 +- packages/styles/package.json | 2 +- packages/test/package.json | 2 +- packages/tracking/package.json | 2 +- packages/ui/package.json | 2 +- packages/uploader/package.json | 2 +- packages/validation/README.md | 5 +- packages/validation/package.json | 2 +- packages/validation/src/index.ts | 21 ++++++++- packages/validation/src/runtime.ts | 4 +- packages/validation/test/validation.test.ts | 51 ++++++++++++++++++++- 58 files changed, 169 insertions(+), 93 deletions(-) diff --git a/.publish/ai/package.json b/.publish/ai/package.json index 6eea4bec..44df6a2b 100644 --- a/.publish/ai/package.json +++ b/.publish/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", "license": "MIT", diff --git a/.publish/authz/package.json b/.publish/authz/package.json index 0471de73..b658d459 100644 --- a/.publish/authz/package.json +++ b/.publish/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/authz — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/cli/package.json b/.publish/cli/package.json index 62f1f6e9..deeed02c 100644 --- a/.publish/cli/package.json +++ b/.publish/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/cli — part of the WrNexus framework.", "license": "MIT", @@ -28,16 +28,16 @@ "wrnexus": "./dist/index.js" }, "dependencies": { - "@wrnexus/core": "^0.2.22", - "@wrnexus/router": "^0.2.22", - "@wrnexus/csr": "^0.2.22", - "@wrnexus/compiler": "^0.2.22", - "@wrnexus/styles": "^0.2.22", - "@wrnexus/dev-server": "^0.2.22", - "@wrnexus/ui": "^0.2.22", - "@wrnexus/validation": "^0.2.22", - "@wrnexus/i18n": "^0.2.22", - "@wrnexus/db": "^0.2.22" + "@wrnexus/core": "^0.2.23", + "@wrnexus/router": "^0.2.23", + "@wrnexus/csr": "^0.2.23", + "@wrnexus/compiler": "^0.2.23", + "@wrnexus/styles": "^0.2.23", + "@wrnexus/dev-server": "^0.2.23", + "@wrnexus/ui": "^0.2.23", + "@wrnexus/validation": "^0.2.23", + "@wrnexus/i18n": "^0.2.23", + "@wrnexus/db": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/compiler/package.json b/.publish/compiler/package.json index 6007b8b0..df213ba9 100644 --- a/.publish/compiler/package.json +++ b/.publish/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/compiler — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/core/package.json b/.publish/core/package.json index 2b702f9a..b67ec151 100644 --- a/.publish/core/package.json +++ b/.publish/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/core — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/csr/package.json b/.publish/csr/package.json index 19afbb2a..28876cb9 100644 --- a/.publish/csr/package.json +++ b/.publish/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/csr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.22" + "@wrnexus/core": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/db/package.json b/.publish/db/package.json index 4837baa8..4235c75e 100644 --- a/.publish/db/package.json +++ b/.publish/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/db — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/dev-server/package.json b/.publish/dev-server/package.json index 1320fa42..26a219ba 100644 --- a/.publish/dev-server/package.json +++ b/.publish/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/dev-server — part of the WrNexus framework.", "license": "MIT", @@ -25,18 +25,18 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.22", - "@wrnexus/router": "^0.2.22", - "@wrnexus/ssr": "^0.2.22", - "@wrnexus/csr": "^0.2.22", - "@wrnexus/compiler": "^0.2.22", - "@wrnexus/styles": "^0.2.22", - "@wrnexus/ui": "^0.2.22", - "@wrnexus/validation": "^0.2.22", - "@wrnexus/i18n": "^0.2.22", - "@wrnexus/db": "^0.2.22", - "@wrnexus/pubsub": "^0.2.22", - "@wrnexus/uploader": "^0.2.22" + "@wrnexus/core": "^0.2.23", + "@wrnexus/router": "^0.2.23", + "@wrnexus/ssr": "^0.2.23", + "@wrnexus/csr": "^0.2.23", + "@wrnexus/compiler": "^0.2.23", + "@wrnexus/styles": "^0.2.23", + "@wrnexus/ui": "^0.2.23", + "@wrnexus/validation": "^0.2.23", + "@wrnexus/i18n": "^0.2.23", + "@wrnexus/db": "^0.2.23", + "@wrnexus/pubsub": "^0.2.23", + "@wrnexus/uploader": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/encryption/package.json b/.publish/encryption/package.json index ab727a9c..a7d6cfc9 100644 --- a/.publish/encryption/package.json +++ b/.publish/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/encryption — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/helpers/package.json b/.publish/helpers/package.json index 15722442..dda71bc1 100644 --- a/.publish/helpers/package.json +++ b/.publish/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.22" + "@wrnexus/core": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/i18n/package.json b/.publish/i18n/package.json index 4bd406e2..48f2aa57 100644 --- a/.publish/i18n/package.json +++ b/.publish/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/i18n — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.22" + "@wrnexus/core": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/jwt/package.json b/.publish/jwt/package.json index c49a5cfd..fc3b9c64 100644 --- a/.publish/jwt/package.json +++ b/.publish/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/jwt — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/mobile/package.json b/.publish/mobile/package.json index a1b54170..b553992b 100644 --- a/.publish/mobile/package.json +++ b/.publish/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/mobile — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/native": "^0.2.22" + "@wrnexus/native": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/native/package.json b/.publish/native/package.json index b261c015..f6350ff8 100644 --- a/.publish/native/package.json +++ b/.publish/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/native — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/oauth/package.json b/.publish/oauth/package.json index 7deb01af..9c9ade66 100644 --- a/.publish/oauth/package.json +++ b/.publish/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/oauth — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/pubsub/package.json b/.publish/pubsub/package.json index cb81d132..6409a231 100644 --- a/.publish/pubsub/package.json +++ b/.publish/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/pubsub — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/queue/package.json b/.publish/queue/package.json index d12ae92e..ff3f3d63 100644 --- a/.publish/queue/package.json +++ b/.publish/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/queue — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/reactive/package.json b/.publish/reactive/package.json index ec8838d9..184cc942 100644 --- a/.publish/reactive/package.json +++ b/.publish/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/reactive — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/router/package.json b/.publish/router/package.json index 1c72dfc8..bfcec654 100644 --- a/.publish/router/package.json +++ b/.publish/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/router — part of the WrNexus framework.", "license": "MIT", @@ -21,8 +21,8 @@ } }, "dependencies": { - "@wrnexus/compiler": "^0.2.22", - "@wrnexus/core": "^0.2.22" + "@wrnexus/compiler": "^0.2.23", + "@wrnexus/core": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/ssr/package.json b/.publish/ssr/package.json index 42cffdd2..15d58dcf 100644 --- a/.publish/ssr/package.json +++ b/.publish/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/ssr — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.22" + "@wrnexus/core": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/styles/package.json b/.publish/styles/package.json index 7482e197..85028db7 100644 --- a/.publish/styles/package.json +++ b/.publish/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/styles — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/uploader": "^0.2.22" + "@wrnexus/uploader": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/test/package.json b/.publish/test/package.json index 83a419a3..a19e9e68 100644 --- a/.publish/test/package.json +++ b/.publish/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/test — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/tracking/package.json b/.publish/tracking/package.json index 59ea3edf..b2dff52d 100644 --- a/.publish/tracking/package.json +++ b/.publish/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/tracking — part of the WrNexus framework.", "license": "MIT", diff --git a/.publish/ui/package.json b/.publish/ui/package.json index a69a289a..cf884a9c 100644 --- a/.publish/ui/package.json +++ b/.publish/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/ui — part of the WrNexus framework.", "license": "MIT", @@ -22,7 +22,7 @@ "./ui.css": "./ui.css" }, "dependencies": { - "@wrnexus/core": "^0.2.22" + "@wrnexus/core": "^0.2.23" }, "files": [ "dist", diff --git a/.publish/uploader/package.json b/.publish/uploader/package.json index ade40922..f978e7a9 100644 --- a/.publish/uploader/package.json +++ b/.publish/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/uploader — part of the WrNexus framework.", "license": "MIT", @@ -21,7 +21,7 @@ } }, "dependencies": { - "@wrnexus/core": "^0.2.22" + "@wrnexus/core": "^0.2.23" }, "files": [ "dist" diff --git a/.publish/validation/README.md b/.publish/validation/README.md index c9e6ed3b..fdbfacf5 100644 --- a/.publish/validation/README.md +++ b/.publish/validation/README.md @@ -35,6 +35,7 @@ import { v } from "@wrnexus/validation"; Every field schema is chainable and shares these base methods: - `min(n, message?)` / `max(n, message?)` — for strings, bounds the length; for numbers, bounds the value. +- `required(message?)` — require a non-empty value and optionally replace the default `"Required"` message on both server and browser validation. - `optional()` — an empty/missing value passes instead of erroring `"Required"`. - `label(text)` — human label carried into the descriptor. - `default(value)` — value substituted when the field is absent (implies `optional`). @@ -121,8 +122,8 @@ Define a schema and validate an API body: import { v, parseBody } from "@wrnexus/validation"; export const signupSchema = v.object({ - email: v.string().trim().email(), - password: v.string().min(8).max(200), + email: v.string().required("Enter your email address").trim().email(), + password: v.string().required("Enter your password").min(8).max(200), age: v.number().integer().min(13).max(120).optional(), role: v.string().oneOf(["user", "admin"]).default("user"), agree: v.boolean(), diff --git a/.publish/validation/package.json b/.publish/validation/package.json index 50ed774c..f547411a 100644 --- a/.publish/validation/package.json +++ b/.publish/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "description": "@wrnexus/validation — part of the WrNexus framework.", "license": "MIT", diff --git a/packages/ai/package.json b/packages/ai/package.json index e63b68fb..71ab35a0 100644 --- a/packages/ai/package.json +++ b/packages/ai/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ai", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "description": "Zero-dependency Claude (Anthropic) client for WrNexus apps.", diff --git a/packages/authz/package.json b/packages/authz/package.json index 7e42e596..a303915a 100644 --- a/packages/authz/package.json +++ b/packages/authz/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/authz", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/cli/package.json b/packages/cli/package.json index aa6931de..83510e06 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/cli", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/cli/src/update.ts b/packages/cli/src/update.ts index 7f6bf215..a8c82d3f 100644 --- a/packages/cli/src/update.ts +++ b/packages/cli/src/update.ts @@ -280,6 +280,14 @@ const MIGRATIONS: Migration[] = [ // Explicit no-op: updating @wrnexus/dev-server and helpers is sufficient. }, }, + { + version: "0.2.23", + id: "custom-required-validation-message", + description: "No project-file changes; custom required messages are provided at runtime", + apply() { + // Explicit no-op: updating @wrnexus/validation is sufficient. + }, + }, ]; /** Release tooling uses this to require an explicit migration entry per version. */ diff --git a/packages/compiler/package.json b/packages/compiler/package.json index d7155f4d..1935a9ac 100644 --- a/packages/compiler/package.json +++ b/packages/compiler/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/compiler", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/core/package.json b/packages/core/package.json index dc20c57a..e3300f59 100644 --- a/packages/core/package.json +++ b/packages/core/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/core", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/csr/package.json b/packages/csr/package.json index 0151c4b2..385275e3 100644 --- a/packages/csr/package.json +++ b/packages/csr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/csr", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/db/package.json b/packages/db/package.json index e0828b40..04da56cc 100644 --- a/packages/db/package.json +++ b/packages/db/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/db", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/dev-server/package.json b/packages/dev-server/package.json index f94cfd25..0589f906 100644 --- a/packages/dev-server/package.json +++ b/packages/dev-server/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/dev-server", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/encryption/package.json b/packages/encryption/package.json index 52274f91..fbb1087f 100644 --- a/packages/encryption/package.json +++ b/packages/encryption/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/encryption", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/helpers/package.json b/packages/helpers/package.json index 0bf9059b..c56fdcdc 100644 --- a/packages/helpers/package.json +++ b/packages/helpers/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/helpers", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "description": "Safe convenience helpers for WrNexus request contexts and common application flows.", diff --git a/packages/i18n/package.json b/packages/i18n/package.json index 996dbeb1..89146c2b 100644 --- a/packages/i18n/package.json +++ b/packages/i18n/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/i18n", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/jwt/package.json b/packages/jwt/package.json index 4ac551a0..580ad28f 100644 --- a/packages/jwt/package.json +++ b/packages/jwt/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/jwt", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/mobile/package.json b/packages/mobile/package.json index 121c7e9e..d1084f8b 100644 --- a/packages/mobile/package.json +++ b/packages/mobile/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/mobile", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/native/package.json b/packages/native/package.json index 9d318170..c62b5ff7 100644 --- a/packages/native/package.json +++ b/packages/native/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/native", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/oauth/package.json b/packages/oauth/package.json index 948a0e64..24efef1e 100644 --- a/packages/oauth/package.json +++ b/packages/oauth/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/oauth", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/pubsub/package.json b/packages/pubsub/package.json index a6acaf9a..d3709e6b 100644 --- a/packages/pubsub/package.json +++ b/packages/pubsub/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/pubsub", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/queue/package.json b/packages/queue/package.json index eda0510e..6e4363d2 100644 --- a/packages/queue/package.json +++ b/packages/queue/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/queue", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/reactive/package.json b/packages/reactive/package.json index 6afd3a87..ae0a13cd 100644 --- a/packages/reactive/package.json +++ b/packages/reactive/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/reactive", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/router/package.json b/packages/router/package.json index a4dd40d8..613595c3 100644 --- a/packages/router/package.json +++ b/packages/router/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/router", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/ssr/package.json b/packages/ssr/package.json index 68bd730c..969ecd5f 100644 --- a/packages/ssr/package.json +++ b/packages/ssr/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ssr", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/styles/package.json b/packages/styles/package.json index ef8530b8..f45a3f30 100644 --- a/packages/styles/package.json +++ b/packages/styles/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/styles", - "version": "0.2.22", + "version": "0.2.23", "type": "module", "main": "src/index.ts", "exports": { diff --git a/packages/test/package.json b/packages/test/package.json index ef41f695..a01f5fc2 100644 --- a/packages/test/package.json +++ b/packages/test/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/test", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/tracking/package.json b/packages/tracking/package.json index 44047517..5ed09626 100644 --- a/packages/tracking/package.json +++ b/packages/tracking/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/tracking", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/ui/package.json b/packages/ui/package.json index 13ad50dc..6918c40a 100644 --- a/packages/ui/package.json +++ b/packages/ui/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/ui", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/uploader/package.json b/packages/uploader/package.json index 147144b2..aee79d3b 100644 --- a/packages/uploader/package.json +++ b/packages/uploader/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/uploader", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/README.md b/packages/validation/README.md index c9e6ed3b..fdbfacf5 100644 --- a/packages/validation/README.md +++ b/packages/validation/README.md @@ -35,6 +35,7 @@ import { v } from "@wrnexus/validation"; Every field schema is chainable and shares these base methods: - `min(n, message?)` / `max(n, message?)` — for strings, bounds the length; for numbers, bounds the value. +- `required(message?)` — require a non-empty value and optionally replace the default `"Required"` message on both server and browser validation. - `optional()` — an empty/missing value passes instead of erroring `"Required"`. - `label(text)` — human label carried into the descriptor. - `default(value)` — value substituted when the field is absent (implies `optional`). @@ -121,8 +122,8 @@ Define a schema and validate an API body: import { v, parseBody } from "@wrnexus/validation"; export const signupSchema = v.object({ - email: v.string().trim().email(), - password: v.string().min(8).max(200), + email: v.string().required("Enter your email address").trim().email(), + password: v.string().required("Enter your password").min(8).max(200), age: v.number().integer().min(13).max(120).optional(), role: v.string().oneOf(["user", "admin"]).default("user"), agree: v.boolean(), diff --git a/packages/validation/package.json b/packages/validation/package.json index 7253ea55..3f75f28e 100644 --- a/packages/validation/package.json +++ b/packages/validation/package.json @@ -1,6 +1,6 @@ { "name": "@wrnexus/validation", - "version": "0.2.22", + "version": "0.2.23", "private": true, "type": "module", "main": "src/index.ts", diff --git a/packages/validation/src/index.ts b/packages/validation/src/index.ts index eb0c505b..de8b8d78 100644 --- a/packages/validation/src/index.ts +++ b/packages/validation/src/index.ts @@ -22,6 +22,8 @@ export type RuleDescriptor = export interface FieldDescriptor { type: "string" | "number" | "boolean"; optional?: boolean; + /** Message used when a required field is empty. Defaults to "Required". */ + requiredMessage?: string; label?: string; /** Trim string input before validating. */ trim?: boolean; @@ -103,13 +105,20 @@ export function checkField( ): { value: unknown; error: string | null } { if (desc.type === "boolean") { const value = raw === true || raw === "true" || raw === "on"; - if (!desc.optional && !value) return { value, error: "Required" }; + if (!desc.optional && !value) { + return { value, error: desc.requiredMessage || "Required" }; + } return { value, error: null }; } const pre = desc.trim && typeof raw === "string" ? raw.trim() : raw; const empty = pre === undefined || pre === null || pre === ""; - if (empty) return { value: undefined, error: desc.optional ? null : "Required" }; + if (empty) { + return { + value: undefined, + error: desc.optional ? null : desc.requiredMessage || "Required", + }; + } let value: unknown; if (desc.type === "number") { @@ -133,6 +142,7 @@ type Refinement = { fn: (value: unknown) => boolean | string; message?: string } abstract class FieldSchema { abstract readonly type: "string" | "number" | "boolean"; protected _optional = false; + protected _requiredMessage?: string; protected _label?: string; protected _default?: unknown; protected rules: RuleDescriptor[] = []; @@ -142,6 +152,12 @@ abstract class FieldSchema { this._optional = true; return this; } + /** Require a non-empty value and optionally replace the default message. */ + required(message = "Required"): this { + this._optional = false; + this._requiredMessage = message; + return this; + } label(label: string): this { this._label = label; return this; @@ -183,6 +199,7 @@ abstract class FieldSchema { return { type: this.type, optional: this._optional || undefined, + requiredMessage: this._requiredMessage, label: this._label, rules: this.rules, }; diff --git a/packages/validation/src/runtime.ts b/packages/validation/src/runtime.ts index 64c1cc8a..ba043d16 100644 --- a/packages/validation/src/runtime.ts +++ b/packages/validation/src/runtime.ts @@ -42,11 +42,11 @@ export const VALIDATE_RUNTIME = String.raw` function checkField(desc, raw) { if (desc.type === "boolean") { var b = raw === true || raw === "true" || raw === "on"; - return (!desc.optional && !b) ? "Required" : null; + return (!desc.optional && !b) ? (desc.requiredMessage || "Required") : null; } var pre = desc.trim && typeof raw === "string" ? raw.trim() : raw; var empty = pre === undefined || pre === null || pre === ""; - if (empty) return desc.optional ? null : "Required"; + if (empty) return desc.optional ? null : (desc.requiredMessage || "Required"); var value; if (desc.type === "number") { value = Number(pre); if (isNaN(value)) return "Must be a number"; } else value = String(pre); diff --git a/packages/validation/test/validation.test.ts b/packages/validation/test/validation.test.ts index 65eaaa09..792386ff 100644 --- a/packages/validation/test/validation.test.ts +++ b/packages/validation/test/validation.test.ts @@ -1,5 +1,6 @@ import { test, expect } from "bun:test"; -import { v, invalid, parseBody, checkField, parseEnv } from "../src/index.ts"; +import { Window } from "happy-dom"; +import { v, invalid, parseBody, checkField, parseEnv, VALIDATE_RUNTIME } from "../src/index.ts"; const login = v.object({ email: v.string().email(), @@ -33,6 +34,51 @@ test("required (non-optional) empty fields fail", () => { expect(r.errors.password).toBe("Required"); }); +test("required(message) customizes empty-field errors and overrides optional", () => { + const schema = v.object({ + email: v.string().required("Enter your email or username"), + password: v.string().optional().required("Enter your password"), + accepted: v.boolean().required("Accept the terms to continue"), + }); + const result = schema.parse({ accepted: false }); + + expect(result.errors).toEqual({ + email: "Enter your email or username", + password: "Enter your password", + accepted: "Accept the terms to continue", + }); + expect(schema.describe().fields.email.requiredMessage).toBe("Enter your email or username"); +}); + +test("browser validation displays the serialized custom required message", () => { + const schema = v.object({ + email: v.string().required("Enter your email or username"), + }); + const win = new Window() as unknown as Window & Record; + win.document.body.innerHTML = ` +
+ + +
`; + win.__wireSchemas = { login: schema.describe() }; + (globalThis as Record).window = win; + (globalThis as Record).document = win.document; + + try { + (0, eval)(VALIDATE_RUNTIME); + const runtime = win.__wireValidate as { init(root: Document): void }; + runtime.init(win.document as unknown as Document); + win.document.querySelector("input")!.dispatchEvent(new win.Event("blur", { bubbles: true })); + + expect(win.document.querySelector("[data-error=email]")!.textContent).toBe( + "Enter your email or username", + ); + } finally { + delete (globalThis as Record).window; + delete (globalThis as Record).document; + } +}); + test("describe emits a JSON descriptor", () => { const d = login.describe(); expect(d.type).toBe("object"); @@ -46,6 +92,9 @@ test("checkField coerces and applies rules", () => { ); expect(checkField({ type: "number", rules: [{ kind: "min", n: 18 }] }, "20").value).toBe(20); expect(checkField({ type: "string", optional: true, rules: [] }, "").error).toBeNull(); + expect( + checkField({ type: "string", requiredMessage: "Email is required", rules: [] }, "").error, + ).toBe("Email is required"); }); test("invalid() returns a 400 with errors", async () => {