typebox-utils
Version:
TypeBox utilities with MongoDB ObjectId support and common validation types
175 lines (174 loc) • 7.79 kB
JavaScript
/**
* @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];
}
}
;