simple-task-master
Version:
A simple command-line task management tool
264 lines • 10.5 kB
JavaScript
;
/**
* Schema validation for Simple Task Master
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaValidationError = exports.CURRENT_SCHEMA_VERSION = exports.isLockFile = exports.isConfig = exports.isTask = void 0;
exports.validateTask = validateTask;
exports.validateConfig = validateConfig;
exports.validateLockFile = validateLockFile;
exports.createDefaultConfig = createDefaultConfig;
const types_1 = require("./types");
Object.defineProperty(exports, "isTask", { enumerable: true, get: function () { return types_1.isTask; } });
Object.defineProperty(exports, "isConfig", { enumerable: true, get: function () { return types_1.isConfig; } });
Object.defineProperty(exports, "isLockFile", { enumerable: true, get: function () { return types_1.isLockFile; } });
const constants_1 = require("./constants");
/**
* Current schema version
*/
exports.CURRENT_SCHEMA_VERSION = 1;
/**
* Error thrown when schema validation fails
*/
class SchemaValidationError extends Error {
field;
constructor(message, field) {
super(message);
this.field = field;
this.name = 'SchemaValidationError';
}
}
exports.SchemaValidationError = SchemaValidationError;
/**
* Core STM fields with their expected types for validation
*/
const STM_CORE_FIELDS = {
id: 'number',
title: 'string',
status: 'string',
schema: 'number',
created: 'string',
updated: 'string',
tags: 'array',
dependencies: 'array'
};
/**
* Required fields for Task schema
*/
const REQUIRED_TASK_FIELDS = [
'schema',
'id',
'title',
'status',
'created',
'updated',
'tags',
'dependencies'
];
/**
* Required fields for Config schema
*/
const REQUIRED_CONFIG_FIELDS = ['schema', 'lockTimeoutMs', 'maxTaskSizeBytes'];
/**
* Required fields for LockFile schema
*/
const REQUIRED_LOCK_FIELDS = ['pid', 'command', 'timestamp'];
/**
* Validates that an object has all required core fields and validates types for known core fields
* Unknown fields are preserved without validation
*/
function validateFields(obj, requiredFields, coreFields, schemaName) {
// Check for required core fields
for (const field of requiredFields) {
if (!(field in obj)) {
throw new SchemaValidationError(`Missing required core field '${field}' in ${schemaName}`, field);
}
}
// Validate types for known core fields (unknown fields are preserved)
for (const [field, expectedType] of Object.entries(coreFields)) {
if (field in obj) {
const value = obj[field];
let isValidType = false;
switch (expectedType) {
case 'number':
isValidType = typeof value === 'number';
break;
case 'string':
isValidType = typeof value === 'string';
break;
case 'array':
isValidType = Array.isArray(value);
break;
default:
isValidType = true; // Unknown type, skip validation
}
if (!isValidType) {
throw new SchemaValidationError(`Core field '${field}' in ${schemaName} must be of type ${expectedType}, got ${Array.isArray(value) ? 'array' : typeof value}`, field);
}
}
}
}
/**
* Validates a Task object against the schema
*/
function validateTask(obj) {
if (typeof obj !== 'object' || obj === null) {
throw new SchemaValidationError('Task must be an object');
}
const task = obj;
// Validate field count limit
const fieldCount = Object.keys(task).length;
if (fieldCount > constants_1.FILE_LIMITS.MAX_TOTAL_FIELDS) {
throw new SchemaValidationError(`${constants_1.ERROR_MESSAGES.TOO_MANY_FIELDS} (found ${fieldCount} fields)`);
}
// Validate required core fields and validate types for known core fields
validateFields(task, REQUIRED_TASK_FIELDS, STM_CORE_FIELDS, 'Task');
// Validate schema version
if (task.schema !== exports.CURRENT_SCHEMA_VERSION) {
throw new SchemaValidationError(`Unsupported schema version ${task.schema}. Expected version ${exports.CURRENT_SCHEMA_VERSION}`, 'schema');
}
// Validate field types
if (!(0, types_1.isTask)(task)) {
// Provide specific error messages for type mismatches
if (typeof task.id !== 'number') {
throw new SchemaValidationError('Task id must be a number', 'id');
}
if (typeof task.title !== 'string') {
throw new SchemaValidationError('Task title must be a string', 'title');
}
if (typeof task.status !== 'string' ||
!['pending', 'in-progress', 'done'].includes(task.status)) {
throw new SchemaValidationError('Task status must be one of: pending, in-progress, done', 'status');
}
if (typeof task.created !== 'string') {
throw new SchemaValidationError('Task created must be an ISO 8601 string', 'created');
}
if (typeof task.updated !== 'string') {
throw new SchemaValidationError('Task updated must be an ISO 8601 string', 'updated');
}
if (!Array.isArray(task.tags)) {
throw new SchemaValidationError('Task tags must be an array', 'tags');
}
if (!task.tags.every((tag) => typeof tag === 'string')) {
throw new SchemaValidationError('All task tags must be strings', 'tags');
}
if (!Array.isArray(task.dependencies)) {
throw new SchemaValidationError('Task dependencies must be an array', 'dependencies');
}
if (!task.dependencies.every((dep) => typeof dep === 'number')) {
throw new SchemaValidationError('All task dependencies must be numbers', 'dependencies');
}
// Generic fallback (shouldn't reach here)
throw new SchemaValidationError('Invalid task object');
}
// Validate ISO 8601 timestamps
validateISO8601(task.created, 'created');
validateISO8601(task.updated, 'updated');
return task;
}
/**
* Validates a Config object against the schema
*/
function validateConfig(obj) {
if (typeof obj !== 'object' || obj === null) {
throw new SchemaValidationError('Config must be an object');
}
const config = obj;
// Validate required fields (Config validation unchanged - still strict)
validateFields(config, REQUIRED_CONFIG_FIELDS, {}, 'Config');
// Additional validation for unknown fields in Config (maintain strict validation)
const allowedConfigFields = new Set(REQUIRED_CONFIG_FIELDS);
for (const field of Object.keys(config)) {
if (!allowedConfigFields.has(field)) {
throw new SchemaValidationError(`Unknown field '${field}' in Config. Allowed fields: ${Array.from(allowedConfigFields).join(', ')}`, field);
}
}
// Validate schema version
if (config.schema !== exports.CURRENT_SCHEMA_VERSION) {
throw new SchemaValidationError(`Unsupported schema version ${config.schema}. Expected version ${exports.CURRENT_SCHEMA_VERSION}`, 'schema');
}
// Validate field types
if (!(0, types_1.isConfig)(config)) {
if (typeof config.lockTimeoutMs !== 'number') {
throw new SchemaValidationError('Config lockTimeoutMs must be a number', 'lockTimeoutMs');
}
if (typeof config.maxTaskSizeBytes !== 'number') {
throw new SchemaValidationError('Config maxTaskSizeBytes must be a number', 'maxTaskSizeBytes');
}
// Generic fallback
throw new SchemaValidationError('Invalid config object');
}
// Validate ranges
if (config.lockTimeoutMs <= 0) {
throw new SchemaValidationError('Config lockTimeoutMs must be positive', 'lockTimeoutMs');
}
if (config.maxTaskSizeBytes <= 0) {
throw new SchemaValidationError('Config maxTaskSizeBytes must be positive', 'maxTaskSizeBytes');
}
return config;
}
/**
* Validates a LockFile object against the schema
*/
function validateLockFile(obj) {
if (typeof obj !== 'object' || obj === null) {
throw new SchemaValidationError('LockFile must be an object');
}
const lock = obj;
// Validate required fields (LockFile validation unchanged - still strict)
validateFields(lock, REQUIRED_LOCK_FIELDS, {}, 'LockFile');
// Additional validation for unknown fields in LockFile (maintain strict validation)
const allowedLockFields = new Set(REQUIRED_LOCK_FIELDS);
for (const field of Object.keys(lock)) {
if (!allowedLockFields.has(field)) {
throw new SchemaValidationError(`Unknown field '${field}' in LockFile. Allowed fields: ${Array.from(allowedLockFields).join(', ')}`, field);
}
}
// Validate field types
if (!(0, types_1.isLockFile)(lock)) {
if (typeof lock.pid !== 'number') {
throw new SchemaValidationError('LockFile pid must be a number', 'pid');
}
if (typeof lock.command !== 'string') {
throw new SchemaValidationError('LockFile command must be a string', 'command');
}
if (typeof lock.timestamp !== 'number') {
throw new SchemaValidationError('LockFile timestamp must be a number', 'timestamp');
}
// Generic fallback
throw new SchemaValidationError('Invalid lock file object');
}
// Validate ranges
if (lock.pid <= 0) {
throw new SchemaValidationError('LockFile pid must be positive', 'pid');
}
if (lock.timestamp <= 0) {
throw new SchemaValidationError('LockFile timestamp must be positive', 'timestamp');
}
return lock;
}
/**
* Validates an ISO 8601 timestamp string
*/
function validateISO8601(timestamp, field) {
const iso8601Regex = /^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(\.\d{3})?Z$/;
if (!iso8601Regex.test(timestamp)) {
throw new SchemaValidationError(`Invalid ISO 8601 timestamp in field '${field}': ${timestamp}`, field);
}
// Verify it's a valid date
const date = new Date(timestamp);
if (isNaN(date.getTime())) {
throw new SchemaValidationError(`Invalid date in field '${field}': ${timestamp}`, field);
}
}
/**
* Creates a default Config object
*/
function createDefaultConfig() {
return {
schema: exports.CURRENT_SCHEMA_VERSION,
lockTimeoutMs: 30000,
maxTaskSizeBytes: 1048576
};
}
//# sourceMappingURL=schema.js.map