UNPKG

typebox-utils

Version:

TypeBox utilities with MongoDB ObjectId support and common validation types

175 lines (174 loc) 7.79 kB
"use strict"; /** * @package */ Object.defineProperty(exports, "__esModule", { value: true }); exports.Utils = exports.Type = void 0; exports.Encode = Encode; exports.Decode = Decode; const typebox_1 = require("@sinclair/typebox"); Object.defineProperty(exports, "Type", { enumerable: true, get: function () { return typebox_1.Type; } }); const value_1 = require("@sinclair/typebox/value"); const mongodb_1 = require("mongodb"); /** * Register custom formats for validation */ typebox_1.FormatRegistry.Set('email', (v) => /^[^@]+@[^@]+\.[^@]+$/.test(v)); typebox_1.FormatRegistry.Set('mobile', (v) => /^[0-9]{10}$/.test(v)); typebox_1.FormatRegistry.Set('uuid', (v) => /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(v)); /** * Common reusable schema types */ const Utils = { /** * Unix timestamp type (milliseconds since * @param {Object} config - Configuration object for the Timestamp type * @param {number} [config.default] - Optional default value for the timestamp * @param {number} [config.minimum] - Optional minimum value (defaults to 0) * @param {number} [config.maximum] - Optional maximum value * @returns {import('@sinclair/typebox').TNumber} A TypeBox schema for Unix timestamp */ Timestamp: (config) => typebox_1.Type.Number({ minimum: config?.minimum || 0, maximum: config?.maximum, default: config?.default, description: 'Unix timestamp (milliseconds since epoch)' }), /** * UUID v4 format type * @param {Object} config - Configuration object for the UUID type * @param {string} [config.default] - Optional default UUID string * @returns {import('@sinclair/typebox').TString} A TypeBox schema for UUID v4 */ UUID: (config) => { if (config?.default) { if (!/^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$/.test(config.default)) { throw new Error('Invalid default value for UUID ' + config.default); } } return typebox_1.Type.String({ pattern: '^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-4[0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}$', description: 'UUID v4 format', default: config?.default }); }, /** * Email format type * @param {Object} config - Configuration object for the Email type * @param {string} [config.default] - Optional default email address * @returns {import('@sinclair/typebox').TString} A TypeBox schema for email format */ Email: (config) => { if (config?.default) { if (!/^[^@]+@[^@]+\.[^@]+$/.test(config.default)) { throw new Error('Invalid default value for Email ' + config.default); } } return typebox_1.Type.String({ format: 'email', description: 'Email format: example@domain.com', default: config?.default }); }, /** * Mobile number format type (10 digits) * @param {Object} config - Configuration object for the Mobile type * @param {string} [config.default] - Optional default mobile number 1 * @returns {import('@sinclair/typebox').TString} A TypeBox schema for mobile number format */ Mobile: (config) => { if (config?.default) { if (!/^[0-9]{10}$/.test(config.default)) { throw new Error('Invalid default value for Mobile ' + config.default); } } return typebox_1.Type.String({ format: 'mobile', description: '10-digit mobile number', default: config?.default }); }, /** * ObjectId type * @param {Object} config - Configuration object for the ObjectId type * @param {string} [config.default] - Optional default value for the ObjectId type * @returns {import('@sinclair/typebox').TString & { static: ObjectId }} A TypeBox schema for MongoDB ObjectId */ ObjectId: (config) => { const raw = typebox_1.Type.Union([ typebox_1.Type.RegExp(/^[0-9a-fA-F]{24}$/), typebox_1.Type.Object({ _bsontype: typebox_1.Type.Literal('ObjectID'), generationTime: typebox_1.Type.Integer(), id: typebox_1.Type.Any() }) ], { errorMessage: 'Expected either a 24-character hex string or an ObjectID' }); const transformed = typebox_1.Type.Transform(raw) .Decode(value => (typeof value === 'string' ? new mongodb_1.ObjectId(value) : new mongodb_1.ObjectId(value.id))) .Encode(value => (value instanceof mongodb_1.ObjectId ? value.toString() : String(value))); if (config?.default) transformed.default = config.default; return transformed; } }; exports.Utils = Utils; /** * Validates a value against a schema * @param value The value to validate * @param schema The schema to validate against (preferably pre-compiled) * @param containsObjectId Whether the schema contains ObjectId fields * @param skipOperations Array of operations to skip during validation. default performed operations: ['Clean', 'Default', 'Convert', 'ConvertOID'] * @returns Tuple of [error message or null, validated value] * @note * - When nothing is specified then it will perform all operations * - When `Convert` is specified then it will convert the ObjectId string to ObjectId instance * - When `Default` is specified then it will set the default value * - When `Clean` is specified then it will remove the extra spaces and trim the string * - Validate will never mutate the original value as it creates a clone of the value 1st * @example * const [error, value] = validate(data, schema); * if (error) { * console.error(error); * } else { * // use validated value * } */ // ------------------------------------------------------------------ // Encode and Decode Pipeline // ------------------------------------------------------------------ function Encode(value, type, applyDefault = true, removeExcessProperties = false) { try { const pipelines = applyDefault ? ['Encode', 'Assert', 'Convert', 'Default'] : ['Encode', 'Assert', 'Convert']; if (removeExcessProperties) pipelines.push('Clean'); const result = value_1.Value.Parse(pipelines, type, value); return [null, result]; } catch (e) { const msg = e?.error?.schema?.errorMessage || e?.error?.message || 'Unknown encoding error'; const path = e?.error?.path || ''; const passedValue = e?.error?.value || undefined; const generatedMessage = msg + ` at path "${path}" but got "${passedValue}"`; // console.error('Encoding error:', generatedMessage); // throw new Error(generatedMessage); return [generatedMessage, undefined]; } } function Decode(value, type, applyDefault = true, removeExcessProperties = false) { try { const pipelines = applyDefault ? ['Default', 'Convert', 'Assert', 'Decode'] : ['Convert', 'Assert', 'Decode']; if (removeExcessProperties) pipelines.unshift('Clean'); const result = value_1.Value.Parse(pipelines, type, value); return [null, result]; } catch (e) { const msg = e?.error?.schema?.errorMessage || e?.error?.message || 'Unknown decoding error'; const path = e?.error?.path || ''; const passedValue = e?.error?.value || undefined; const generatedMessage = msg + ` at path "${path}" but got "${passedValue}"`; // console.error('Decoding error:', generatedMessage); // throw new Error(generatedMessage); return [generatedMessage, undefined]; } }