@nhtio/validation 1.20250911.1 → 1.20251029.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nhtio/validation",
3
- "version": "1.20250911.1",
3
+ "version": "1.20251029.0",
4
4
  "description": "A powerful schema description language and data validator",
5
5
  "keywords": [],
6
6
  "author": "Jak Giveon <jak@nht.io>",
@@ -21,7 +21,8 @@
21
21
  },
22
22
  "onlyBuiltDependencies": [
23
23
  "es5-ext",
24
- "esbuild"
24
+ "esbuild",
25
+ "sqlite3"
25
26
  ]
26
27
  },
27
28
  "packageManager": "pnpm@10.8.0+sha512.0e82714d1b5b43c74610193cb20734897c1d00de89d0e18420aebc5977fa13d780a9cb05734624e81ebd81cc876cd464794850641c48b9544326b5622ca29971",
@@ -0,0 +1,63 @@
1
+ /**
2
+ * Modern ES6 + TypeScript reimplementation of Joi's root factory.
3
+ *
4
+ * Based on: https://github.com/hapijs/joi/blob/master/lib/index.js
5
+ *
6
+ * This module recreates Joi's internal root factory to enable deeper
7
+ * customization through schema type modifiers and shortcuts modifiers.
8
+ */
9
+ import type { Root } from 'joi';
10
+ import type { Schema } from '../index';
11
+ export type SchemaTypeDefinition = Schema;
12
+ export type SchemaTypeDefinititionMiddlewareFn = (ctx: SchemaTypeDefinition, root: Root, args: any[]) => SchemaTypeDefinition;
13
+ export type ShortcutsMiddlewareFn = (ctx: string[]) => string[];
14
+ export interface RootFactoryOptions {
15
+ schemaTypeModifiers?: SchemaTypeDefinititionMiddlewareFn[];
16
+ shortcutsModifiers?: ShortcutsMiddlewareFn[];
17
+ }
18
+ export declare class RootFactory {
19
+ static readonly types: {
20
+ alternatives: any;
21
+ any: any;
22
+ array: any;
23
+ boolean: any;
24
+ date: any;
25
+ function: any;
26
+ link: any;
27
+ number: any;
28
+ object: any;
29
+ string: any;
30
+ symbol: any;
31
+ binary: any;
32
+ };
33
+ static readonly aliases: {
34
+ alt: string;
35
+ bool: string;
36
+ func: string;
37
+ };
38
+ static assertValue(value: any, schema: any, annotate: boolean, args: any[]): any;
39
+ static generate(root: any, schema: any, args: any[]): any;
40
+ static expandExtension(extension: any, joi: any): any[];
41
+ static getMethods(): {
42
+ ValidationError: any;
43
+ version: any;
44
+ cache: any;
45
+ assert(value: any, schema: any, ...args: any[]): void;
46
+ attempt(value: any, schema: any, ...args: any[]): any;
47
+ build(this: Root, desc: any): any;
48
+ checkPreferences(prefs: any): void;
49
+ compile(this: Root, schema: any, options: any): any;
50
+ defaults(this: Root, modifier: any): Root;
51
+ expression(...args: any[]): any;
52
+ extend(this: Root, ...extensions: any[]): Root;
53
+ isError: any;
54
+ isExpression: any;
55
+ isRef: any;
56
+ isSchema: any;
57
+ in(...args: any[]): any;
58
+ override: any;
59
+ ref(...args: any[]): any;
60
+ types(this: any): any;
61
+ };
62
+ static create(options?: RootFactoryOptions): Root;
63
+ }
@@ -1,12 +1,14 @@
1
- import { default as Joi } from 'joi';
2
1
  import type { DateTime } from 'luxon';
3
2
  import type { PhoneSchema } from './schemas/phone';
4
3
  import type { BigIntSchema } from './schemas/bigint';
5
4
  import type { DatetimeSchema } from './schemas/datetime';
6
5
  import type { CountryOrUnknown } from '@nhtio/phone-object';
7
- import type { Root, Reference, SchemaMap, SchemaLike } from 'joi';
8
6
  import type { I18nCallback, SetI18nCallback } from './patches/i18n';
7
+ import type { ValidationOptions, Root, Reference as JoiReference, SchemaMap, SchemaLike, WhenSchemaOptions, WhenOptions, State, ReferenceOptions } from 'joi';
9
8
  import type { AnySchema, StringSchema, BinarySchema, NumberSchema, BooleanSchema, ObjectSchema, ArraySchema, DateSchema, AlternativesSchema, FunctionSchema, LinkSchema, SymbolSchema, Schema } from './schemas';
9
+ export type Reference = JoiReference & {
10
+ resolve: (value: any, state: State, prefs: ValidationOptions, local?: any, options?: ReferenceOptions) => any;
11
+ };
10
12
  /**
11
13
  * Extended Joi root interface that includes custom schema types for
12
14
  * additional validation scenarios.
@@ -26,7 +28,7 @@ import type { AnySchema, StringSchema, BinarySchema, NumberSchema, BooleanSchema
26
28
  *
27
29
  * @public
28
30
  */
29
- export interface ValidationRoot extends Omit<Root, 'allow' | 'alt' | 'alternatives' | 'any' | 'array' | 'binary' | 'bool' | 'boolean' | 'date' | 'disallow' | 'equal' | 'exist' | 'forbidden' | 'func' | 'function' | 'invalid' | 'link' | 'not' | 'number' | 'object' | 'optional' | 'preferences' | 'prefs' | 'required' | 'string' | 'symbol' | 'types' | 'valid' | 'when'> {
31
+ export interface ValidationRoot extends Omit<Root, 'allow' | 'alt' | 'alternatives' | 'any' | 'array' | 'binary' | 'bool' | 'boolean' | 'date' | 'disallow' | 'equal' | 'exist' | 'forbidden' | 'func' | 'function' | 'invalid' | 'link' | 'not' | 'number' | 'object' | 'optional' | 'preferences' | 'prefs' | 'required' | 'string' | 'symbol' | 'types' | 'valid' | 'when' | 'ref'> {
30
32
  /**
31
33
  * Generates a schema object that matches any data type.
32
34
  */
@@ -165,16 +167,20 @@ export interface ValidationRoot extends Omit<Root, 'allow' | 'alt' | 'alternativ
165
167
  /**
166
168
  * Overrides the global validate() options for the current key and any sub-key.
167
169
  */
168
- preferences(options: Joi.ValidationOptions): Schema;
170
+ preferences(options: ValidationOptions): Schema;
169
171
  /**
170
172
  * Overrides the global validate() options for the current key and any sub-key.
171
173
  */
172
- prefs(options: Joi.ValidationOptions): Schema;
174
+ prefs(options: ValidationOptions): Schema;
173
175
  /**
174
176
  * Converts the type into an alternatives type where the conditions are merged into the type definition where:
175
177
  */
176
- when(ref: string | Reference, options: Joi.WhenOptions | Joi.WhenOptions[]): AlternativesSchema;
177
- when(ref: Schema, options: Joi.WhenSchemaOptions): AlternativesSchema;
178
+ when(ref: string | Reference, options: WhenOptions | WhenOptions[]): AlternativesSchema;
179
+ when(ref: Schema, options: WhenSchemaOptions): AlternativesSchema;
180
+ /**
181
+ * Creates a reference to another schema key.
182
+ */
183
+ ref(key: string, options?: ReferenceOptions): Reference;
178
184
  /**
179
185
  * Sets a global internationalization callback that applies to all validator instances.
180
186
  *
@@ -232,27 +238,8 @@ export interface ValidationRoot extends Omit<Root, 'allow' | 'alt' | 'alternativ
232
238
  $clearI18n: () => ValidationRoot;
233
239
  $i18n: I18nCallback;
234
240
  }
235
- /**
236
- * Extended Joi instance with custom schema types.
237
- *
238
- * This instance includes all standard Joi functionality plus additional
239
- * schema types for additional validation scenarios.
240
- *
241
- * @example
242
- * ```typescript
243
- * import { validator } from '@nhtio/validation'
244
- *
245
- * // Standard Joi usage
246
- * const userSchema = validator.object({
247
- * name: validator.string().required(),
248
- * age: validator.number().min(0),
249
- * balance: validator.bigint().positive() // Custom BigInt type
250
- * })
251
- * ```
252
- *
253
- * @public
254
- */
255
241
  export declare const validator: ValidationRoot;
256
242
  export type { BigIntSchema, DatetimeSchema, PhoneSchema, SetI18nCallback, I18nCallback };
257
243
  export type * from './schemas';
258
244
  export { encode, decode } from './utils';
245
+ export type { Knex } from 'knex';
@@ -0,0 +1,16 @@
1
+ import type { Knex } from 'knex';
2
+ import type { SchemaTypeDefinititionMiddlewareFn } from '../core/root';
3
+ import type { KnexSchemaConnection } from '../schemas';
4
+ export declare const knexMessages: {
5
+ 'knex.unique': string;
6
+ 'knex.exists': string;
7
+ 'knex.missingConnection': string;
8
+ 'knex.invalidTable': string;
9
+ 'knex.invalidColumn': string;
10
+ 'knex.internal': string;
11
+ };
12
+ export declare const resolveQueryBuilder: (connection: KnexSchemaConnection, table: string) => {
13
+ client: Knex;
14
+ query: Knex.QueryBuilder;
15
+ };
16
+ export declare const knex: SchemaTypeDefinititionMiddlewareFn;
@@ -1,5 +1,5 @@
1
1
  import { DateTime } from 'luxon';
2
- import { Dayjs } from 'dayjs';
2
+ import type { Dayjs } from 'dayjs';
3
3
  import type { Tokens } from '../types';
4
4
  import type { AnySchema } from '../schemas';
5
5
  import type { ExtensionFactory, CustomHelpers, Reference } from 'joi';
@@ -1,9 +1,40 @@
1
+ import type { Knex } from 'knex';
2
+ import type { Reference } from '..';
1
3
  import type { DateTime } from 'luxon';
2
4
  import type { PhoneSchema } from './schemas/phone';
3
5
  import type { BigIntSchema } from './schemas/bigint';
4
6
  import type { DatetimeSchema } from './schemas/datetime';
5
- import type { default as Joi, AnySchema as JoiAnySchema, Reference, BasicType, CustomHelpers } from 'joi';
7
+ import type { QueryClientContract } from '@adonisjs/lucid/types/database';
8
+ import type { DatabaseQueryBuilderContract } from '@adonisjs/lucid/types/querybuilder';
9
+ import type { default as Joi, AnySchema as JoiAnySchema, BasicType, CustomHelpers, ExternalHelpers } from 'joi';
10
+ export type { Knex, QueryClientContract, DatabaseQueryBuilderContract };
11
+ export type KnexTransaction = Knex.Transaction;
12
+ export type KnexQueryBuilder = Knex.QueryBuilder;
13
+ export type QueryClient = Knex | KnexTransaction | QueryClientContract;
14
+ export type QueryBuilder = KnexQueryBuilder | DatabaseQueryBuilderContract;
15
+ export interface KnexConnectionConfigurations {
16
+ client: NonNullable<Knex.Config['client']>;
17
+ connection: NonNullable<Knex.Config['connection']>;
18
+ [key: string | number | symbol]: any;
19
+ }
20
+ export type KnexSchemaConnection = QueryClient | KnexConnectionConfigurations;
6
21
  export type DefaultableValue = Reference | BasicType | DateTime | bigint | ((parent: any, helpers: CustomHelpers) => Reference | BasicType | DateTime | bigint);
22
+ /**
23
+ * Options for database-backed validation helpers such as `uniqueInDb` and `existsInDb`.
24
+ */
25
+ export interface DbValidationOptions {
26
+ /**
27
+ * Perform case-insensitive comparisons when querying the database.
28
+ * Defaults to `false`.
29
+ */
30
+ caseInsensitive?: boolean;
31
+ /**
32
+ * Optional function that receives the Knex query builder (or equivalent) so
33
+ * the caller can append additional WHERE clauses (or joins). Can be async.
34
+ * Example: `async (query, value, column, helpers) => query.where('tenant_id', tenantId)`
35
+ */
36
+ filter?: (queryBuilder: QueryBuilder, value: any, column: string, helpers: Omit<ExternalHelpers, 'warn' | 'error' | 'message'>) => void | Promise<void>;
37
+ }
7
38
  /**
8
39
  * Base schema type for all validation schemas.
9
40
  *
@@ -100,6 +131,70 @@ export interface AnySchema<TSchema = any> extends Omit<JoiAnySchema<TSchema>, '$
100
131
  warning(code: string, context: Joi.Context): this;
101
132
  when(ref: string | Reference, options: Joi.WhenOptions | Joi.WhenOptions[]): this;
102
133
  when(ref: Schema, options: Joi.WhenSchemaOptions): this;
134
+ $_knex: KnexSchemaConnection | undefined;
135
+ /**
136
+ * Sets the database connection for database validation rules.
137
+ * This must be called before using `.uniqueInDb()` or `.existsInDb()`.
138
+ *
139
+ * @param connection - A Knex instance, transaction, or connection configuration
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * import knex from 'knex'
144
+ * const db = knex({ client: 'pg', connection: {...} })
145
+ * const schema = joi.string().knex(db).uniqueInDb('users', 'email')
146
+ * ```
147
+ */
148
+ knex(connection: KnexSchemaConnection): this;
149
+ /**
150
+ * Alias for `.knex()`. Sets the database connection for database validation rules.
151
+ *
152
+ * @param connection - A Knex instance, transaction, or connection configuration
153
+ */
154
+ db(connection: KnexSchemaConnection): this;
155
+ /**
156
+ * Validates that the value is unique in the specified database table and column.
157
+ * Requires `.knex()` or `.db()` to be called first to set the database connection.
158
+ *
159
+ * @param table - The database table name (can be a Joi reference)
160
+ * @param column - The column name to check (can be a Joi reference)
161
+ * @param options - Optional configuration:
162
+ * - `caseInsensitive`: Perform case-insensitive comparison (default: false)
163
+ * - `filter`: Async function to add additional WHERE clauses to the query
164
+ *
165
+ * @example
166
+ * ```typescript
167
+ * const schema = joi.object({
168
+ * email: joi.string().email().knex(db).uniqueInDb('users', 'email'),
169
+ * username: joi.string().knex(db).uniqueInDb('users', 'username', {
170
+ * caseInsensitive: true,
171
+ * filter: async (query) => query.where('tenant_id', tenantId)
172
+ * })
173
+ * })
174
+ * ```
175
+ */
176
+ uniqueInDb(table: string | Reference, column: string | Reference, options?: DbValidationOptions): this;
177
+ /**
178
+ * Validates that the value exists in the specified database table and column.
179
+ * Requires `.knex()` or `.db()` to be called first to set the database connection.
180
+ *
181
+ * @param table - The database table name (can be a Joi reference)
182
+ * @param column - The column name to check (can be a Joi reference)
183
+ * @param options - Optional configuration:
184
+ * - `caseInsensitive`: Perform case-insensitive comparison (default: false)
185
+ * - `filter`: Async function to add additional WHERE clauses to the query
186
+ *
187
+ * @example
188
+ * ```typescript
189
+ * const schema = joi.object({
190
+ * country_id: joi.number().knex(db).existsInDb('countries', 'id'),
191
+ * category: joi.string().knex(db).existsInDb('categories', 'name', {
192
+ * caseInsensitive: true
193
+ * })
194
+ * })
195
+ * ```
196
+ */
197
+ existsInDb(table: string | Reference, column: string | Reference, options?: DbValidationOptions): this;
103
198
  /**
104
199
  * Sets a default value if the original value is `undefined`.
105
200
  *
@@ -1,4 +1,71 @@
1
1
  import type { Schema } from './schemas';
2
+ /**
3
+ * Options for encoding and decoding validation schemas.
4
+ *
5
+ * These options control what gets serialized when encoding schemas and how
6
+ * they're reconstructed during decoding. The defaults prioritize security
7
+ * and are designed for the most common use case: sharing validation rules
8
+ * between frontend and backend environments.
9
+ */
10
+ export type EncoderOptions = {
11
+ /**
12
+ * Include database connections and database validation rules in the encoded schema.
13
+ *
14
+ * When `false` (default):
15
+ * - Database connections (`.knex()`, `.db()`) are stripped from the encoded schema
16
+ * - Database validation rules (`.uniqueInDb()`, `.existsInDb()`) are converted to no-ops
17
+ * - Filter functions in database rules are replaced with empty functions
18
+ * - This prevents credential leakage and reduces payload size
19
+ *
20
+ * When `true`:
21
+ * - Database connection configurations are serialized (if they're plain objects)
22
+ * - Database validation rules and their filter functions are preserved
23
+ * - **Security warning**: Only use this in trusted backend-to-backend communication
24
+ * - Encoded schemas will require re-attaching database connections after decoding
25
+ *
26
+ * **Why this defaults to `false`:**
27
+ * 1. **Security**: Prevents accidental exposure of database credentials in client-side code
28
+ * 2. **Use case alignment**: Frontend validation typically doesn't need database rules
29
+ * 3. **Credential safety**: Database connections often contain sensitive connection strings
30
+ * 4. **Function security**: Custom filter functions may contain business logic or credentials
31
+ *
32
+ * @default false
33
+ *
34
+ * @example Frontend/Backend Schema Sharing (default)
35
+ * ```typescript
36
+ * // Backend
37
+ * const schema = joi.object({
38
+ * email: joi.string().email().uniqueInDb('users', 'email')
39
+ * }).knex(db)
40
+ *
41
+ * const encoded = encode(schema) // withDatabase defaults to false
42
+ * // Database rules are stripped, safe to send to frontend
43
+ *
44
+ * // Frontend receives and decodes
45
+ * const frontendSchema = decode(encoded)
46
+ * // Only gets email format validation, no database checks
47
+ * ```
48
+ *
49
+ * @example Backend-to-Backend Schema Transfer
50
+ * ```typescript
51
+ * // Service A
52
+ * const schema = joi.object({
53
+ * username: joi.string().uniqueInDb('users', 'username', {
54
+ * filter: async (query) => query.where('tenant_id', tenantId)
55
+ * })
56
+ * }).knex({ client: 'pg', connection: { host: 'localhost', ... } })
57
+ *
58
+ * const encoded = encode(schema, { withDatabase: true })
59
+ * // Sends to trusted Service B
60
+ *
61
+ * // Service B
62
+ * const decoded = decode(encoded, { withDatabase: true })
63
+ * const rehydrated = decoded.knex(serviceB_db_connection)
64
+ * // Now has full database validation capabilities
65
+ * ```
66
+ */
67
+ withDatabase?: boolean;
68
+ };
2
69
  /**
3
70
  * Encodes a validation schema into a compressed, base64-encoded string for transport between environments.
4
71
  *
@@ -46,7 +113,7 @@ import type { Schema } from './schemas';
46
113
  *
47
114
  * @see {@link decode} For decoding schemas from encoded strings
48
115
  */
49
- export declare const encode: (schema: Schema) => string;
116
+ export declare const encode: (schema: Schema, options?: EncoderOptions) => string;
50
117
  /**
51
118
  * Decodes a base64-encoded schema string back into a usable validation schema.
52
119
  *
@@ -63,6 +130,7 @@ export declare const encode: (schema: Schema) => string;
63
130
  * - Schema reconstruction using Joi's build method
64
131
  *
65
132
  * @param base64 - The base64-encoded schema string
133
+ * @param options - Decoding options
66
134
  * @returns A reconstructed validation schema ready for validation
67
135
  * @throws {TypeError} When the encoded string is not a valid schema
68
136
  * @throws {TypeError} When the schema version is invalid or incompatible
@@ -101,6 +169,18 @@ export declare const encode: (schema: Schema) => string;
101
169
  * const schema = decode(oldSchema) // Works - backward compatible
102
170
  * ```
103
171
  *
172
+ * @security
173
+ * **IMPORTANT: Only decode schemas from trusted sources.**
174
+ *
175
+ * Decoded schemas may contain serialized functions that are evaluated using the
176
+ * Function constructor. Never decode schemas from:
177
+ * - User input or user-controlled data
178
+ * - Untrusted third-party APIs
179
+ * - Any source you do not control
180
+ *
181
+ * If the schema source is not under your direct control, validate its integrity
182
+ * using cryptographic signatures or checksums before decoding.
183
+ *
104
184
  * @see {@link encode} For encoding schemas into transportable strings
105
185
  */
106
- export declare const decode: (base64: string) => Schema;
186
+ export declare const decode: (base64: string, options?: EncoderOptions) => Schema;