n8n-nodes-semble
Version:
n8n community node for Semble practice management system - automate bookings, patients, and product/service catalog management
494 lines (493 loc) • 17.9 kB
JavaScript
;
/**
* @fileoverview SchemaRegistry.ts
* @description Central schema registry for field definitions, validation rules, and schema versioning. Provides schema management, lookup, and validation capabilities.
* @author Mike Hatcher
* @website https://progenious.com
* @namespace N8nNodesSemble.Core.SchemaRegistry
* @since 2.0.0
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.SchemaRegistryUtils = exports.schemaRegistry = exports.SchemaRegistry = void 0;
const SembleError_1 = require("./SembleError");
/**
* Schema registry implementation
*/
class SchemaRegistry {
constructor() {
this.schemas = new Map();
this.latestVersions = new Map();
this.schemaHistory = new Map();
this.dependencies = new Map();
}
/**
* Register a new schema version
*/
registerSchema(schema) {
const key = this.getSchemaKey(schema.type);
const version = schema.version.version;
// Validate schema before registration
const validation = this.validateSchema(schema);
if (!validation.isValid) {
throw new SembleError_1.SembleError(`Schema validation failed: ${validation.errors.join(', ')}`);
}
// Initialize maps if needed
if (!this.schemas.has(key)) {
this.schemas.set(key, new Map());
this.schemaHistory.set(key, []);
}
// Check for version conflicts
if (this.schemas.get(key).has(version)) {
throw new SembleError_1.SembleError(`Schema version ${version} already exists for ${schema.type}`);
}
// Analyze change impact if not the first version
const existingVersions = this.schemaHistory.get(key);
if (existingVersions.length > 0) {
const latestSchema = this.getLatestSchema(schema.type);
if (latestSchema) {
const impact = this.analyzeSchemaChanges(latestSchema, schema);
if (impact.breaking && !schema.version.breaking) {
console.warn(`Schema ${schema.type} v${version} introduces breaking changes but is not marked as breaking`);
}
}
}
// Register schema
this.schemas.get(key).set(version, schema);
this.schemaHistory.get(key).push(schema.version);
this.latestVersions.set(key, version);
// Update dependencies
this.updateDependencies(schema);
console.log(`Registered schema ${schema.type} v${version}`);
}
/**
* Get schema by resource type and version
*/
getSchema(resourceType, version) {
const key = this.getSchemaKey(resourceType);
const schemaMap = this.schemas.get(key);
if (!schemaMap) {
return undefined;
}
if (version) {
return schemaMap.get(version);
}
// Return latest version
const latestVersion = this.latestVersions.get(key);
return latestVersion ? schemaMap.get(latestVersion) : undefined;
}
/**
* Get latest schema version
*/
getLatestSchema(resourceType) {
return this.getSchema(resourceType);
}
/**
* Get all schemas
*/
getAllSchemas() {
const result = [];
for (const schemaMap of this.schemas.values()) {
for (const schema of schemaMap.values()) {
result.push(schema);
}
}
return result;
}
/**
* Get all versions of a schema
*/
getSchemaVersions(resourceType) {
const key = this.getSchemaKey(resourceType);
return [...(this.schemaHistory.get(key) || [])];
}
/**
* Get schema by version pattern (e.g., "1.x", ">=2.0.0")
*/
getSchemaByPattern(resourceType, pattern) {
const versions = this.getSchemaVersions(resourceType);
// Simple pattern matching - can be extended with semver
for (const version of versions.reverse()) {
if (this.matchesPattern(version.version, pattern)) {
return this.getSchema(resourceType, version.version);
}
}
return undefined;
}
/**
* Validate a schema definition
*/
validateSchema(schema) {
const errors = [];
const warnings = [];
// Basic schema validation
if (!schema.name) {
errors.push('Schema name is required');
}
if (!schema.type) {
errors.push('Schema type is required');
}
if (!schema.version) {
errors.push('Schema version is required');
}
if (!Array.isArray(schema.fields)) {
errors.push('Schema fields must be an array');
}
else {
// Validate fields
const fieldNames = new Set();
for (const field of schema.fields) {
// Check for duplicate field names
if (fieldNames.has(field.name)) {
errors.push(`Duplicate field name: ${field.name}`);
}
else {
fieldNames.add(field.name);
}
// Validate field structure
if (!field.name) {
errors.push('Field name is required');
}
if (!field.type) {
errors.push(`Field type is required for field: ${field.name}`);
}
// Validate field dependencies
if (field.dependencies) {
for (const dep of field.dependencies) {
if (!fieldNames.has(dep)) {
warnings.push(`Field ${field.name} depends on undefined field: ${dep}`);
}
}
}
}
}
// Validate actions
if (schema.actions && Array.isArray(schema.actions)) {
for (const action of schema.actions) {
if (!this.isValidAction(action)) {
errors.push(`Invalid action: ${action}`);
}
}
}
return {
isValid: errors.length === 0,
errors,
warnings
};
}
/**
* Generate n8n node properties from schema
*/
generateNodeProperties(schema, action) {
const properties = [];
for (const field of schema.fields) {
// Skip fields not relevant to the action
if (action && !this.isFieldRelevantForAction(field, action)) {
continue;
}
const property = {
displayName: this.formatDisplayName(field.name),
name: field.name,
type: this.mapFieldTypeToNodeType(field.type),
default: this.getFieldDefault(field),
description: field.description || `${field.name} field`,
required: field.required
};
// Add validation rules
if (field.validation && field.validation.length > 0) {
this.applyValidationRules(property, field.validation);
}
// Add conditional display rules
if (field.conditional && field.conditional.length > 0) {
this.applyConditionalRules(property, field.conditional);
}
properties.push(property);
}
return properties;
}
/**
* Analyze changes between schema versions
*/
analyzeSchemaChanges(oldSchema, newSchema) {
const oldFields = new Map(oldSchema.fields.map(f => [f.name, f]));
const newFields = new Map(newSchema.fields.map(f => [f.name, f]));
const addedFields = [];
const removedFields = [];
const modifiedFields = [];
const compatibilityIssues = [];
// Find added fields
for (const fieldName of newFields.keys()) {
if (!oldFields.has(fieldName)) {
addedFields.push(fieldName);
}
}
// Find removed fields
for (const fieldName of oldFields.keys()) {
if (!newFields.has(fieldName)) {
removedFields.push(fieldName);
compatibilityIssues.push(`Field removed: ${fieldName}`);
}
}
// Find modified fields
for (const [fieldName, newField] of newFields) {
const oldField = oldFields.get(fieldName);
if (oldField && this.isFieldModified(oldField, newField)) {
modifiedFields.push(fieldName);
// Check for breaking changes
if (oldField.required !== newField.required && newField.required) {
compatibilityIssues.push(`Field ${fieldName} is now required`);
}
if (oldField.type !== newField.type) {
compatibilityIssues.push(`Field ${fieldName} type changed from ${oldField.type} to ${newField.type}`);
}
}
}
const breaking = removedFields.length > 0 || compatibilityIssues.length > 0;
return {
breaking,
addedFields,
removedFields,
modifiedFields,
compatibilityIssues
};
}
/**
* Clear all schemas
*/
clear() {
this.schemas.clear();
this.latestVersions.clear();
this.schemaHistory.clear();
this.dependencies.clear();
}
/**
* Get schema dependencies
*/
getSchemaDependencies(resourceType) {
const key = this.getSchemaKey(resourceType);
return Array.from(this.dependencies.get(key) || []);
}
/**
* Export schema to JSON
*/
exportSchema(resourceType, version) {
const schema = this.getSchema(resourceType, version);
if (!schema) {
throw new SembleError_1.SembleError(`Schema not found: ${resourceType}${version ? ` v${version}` : ''}`);
}
return JSON.stringify(schema, null, 2);
}
/**
* Import schema from JSON
*/
importSchema(jsonString) {
try {
const schema = JSON.parse(jsonString);
this.registerSchema(schema);
}
catch (error) {
throw new SembleError_1.SembleError(`Failed to import schema: ${error}`);
}
}
/**
* Get schema statistics
*/
getStatistics() {
const allSchemas = this.getAllSchemas();
const schemasByType = {};
let totalFields = 0;
for (const schema of allSchemas) {
schemasByType[schema.type] = (schemasByType[schema.type] || 0) + 1;
totalFields += schema.fields.length;
}
return {
totalSchemas: allSchemas.length,
schemasByType,
totalFields,
averageFieldsPerSchema: allSchemas.length > 0 ? totalFields / allSchemas.length : 0
};
}
// Private helper methods
getSchemaKey(resourceType) {
return resourceType.toLowerCase();
}
matchesPattern(version, pattern) {
// Simple pattern matching - could be extended with proper semver
if (pattern.includes('x')) {
const regex = new RegExp(pattern.replace('x', '\\d+'));
return regex.test(version);
}
return version === pattern;
}
isValidAction(action) {
const validActions = ['create', 'get', 'getMany', 'update', 'delete'];
return validActions.includes(action);
}
isFieldRelevantForAction(field, action) {
// Logic to determine if field is relevant for the action
// For example, 'id' field might not be relevant for 'create' action
if (action === 'create' && field.name === 'id') {
return false;
}
return true;
}
formatDisplayName(fieldName) {
return fieldName
.split(/(?=[A-Z])/)
.map(word => word.charAt(0).toUpperCase() + word.slice(1))
.join(' ');
}
mapFieldTypeToNodeType(fieldType) {
const typeMap = {
'string': 'string',
'number': 'number',
'boolean': 'boolean',
'date': 'dateTime',
'enum': 'options',
'array': 'multiOptions',
'object': 'json'
};
return typeMap[fieldType] || 'string';
}
getFieldDefault(field) {
var _a;
if (((_a = field.metadata) === null || _a === void 0 ? void 0 : _a.default) !== undefined) {
return field.metadata.default;
}
switch (field.type) {
case 'boolean': return false;
case 'number': return 0;
case 'array': return [];
case 'object': return {};
default: return '';
}
}
applyValidationRules(property, rules) {
for (const rule of rules) {
switch (rule.type) {
case 'pattern':
// @ts-ignore - n8n property extension
property.typeOptions = {
...property.typeOptions,
regex: rule.params.pattern
};
break;
case 'range':
if (property.type === 'number') {
// @ts-ignore - n8n property extension
property.typeOptions = {
...property.typeOptions,
minValue: rule.params.min,
maxValue: rule.params.max
};
}
break;
}
}
}
applyConditionalRules(property, rules) {
for (const rule of rules) {
if (rule.action === 'show' || rule.action === 'hide') {
property.displayOptions = {
...property.displayOptions,
[rule.action]: {
// Convert condition to n8n format
// This is a simplified implementation
'@version': [1]
}
};
}
}
}
isFieldModified(oldField, newField) {
return (oldField.type !== newField.type ||
oldField.required !== newField.required ||
JSON.stringify(oldField.validation) !== JSON.stringify(newField.validation) ||
JSON.stringify(oldField.dependencies) !== JSON.stringify(newField.dependencies));
}
updateDependencies(schema) {
const key = this.getSchemaKey(schema.type);
const deps = new Set();
for (const field of schema.fields) {
if (field.dependencies) {
for (const dep of field.dependencies) {
deps.add(dep);
}
}
}
this.dependencies.set(key, deps);
}
}
exports.SchemaRegistry = SchemaRegistry;
/**
* Default schema registry instance
*/
exports.schemaRegistry = new SchemaRegistry();
/**
* Utility functions for schema management
*/
class SchemaRegistryUtils {
/**
* Create schema from field definitions
*/
static createSchema(name, type, fields, version = '1.0.0') {
const processedFields = fields.map(field => ({
name: field.name || '',
type: field.type || 'string',
required: field.required || false,
description: field.description,
validation: field.validation || [],
dependencies: field.dependencies || [],
conditional: field.conditional || [],
metadata: field.metadata || {}
}));
return {
name,
type,
version: {
version,
timestamp: Date.now(),
author: 'system',
description: `Schema for ${name}`,
breaking: false
},
fields: processedFields,
actions: this.getDefaultActions(type),
permissions: [],
metadata: {}
};
}
/**
* Get default actions for a resource type
*/
static getDefaultActions(type) {
// All resource types support CRUD operations by default
return ['create', 'get', 'getMany', 'update', 'delete'];
}
/**
* Register common Semble schemas
*/
static registerCommonSchemas(registry) {
// Patient schema
const patientSchema = SchemaRegistryUtils.createSchema('Patient', 'patient', [
{ name: 'id', type: 'string', description: 'Patient ID' },
{ name: 'firstName', type: 'string', required: true, description: 'First name' },
{ name: 'lastName', type: 'string', required: true, description: 'Last name' },
{ name: 'email', type: 'string', description: 'Email address' },
{ name: 'phone', type: 'string', description: 'Phone number' },
{ name: 'dateOfBirth', type: 'date', description: 'Date of birth' }
]);
// Booking schema
const bookingSchema = SchemaRegistryUtils.createSchema('Booking', 'booking', [
{ name: 'id', type: 'string', description: 'Booking ID' },
{ name: 'patientId', type: 'string', required: true, description: 'Patient ID' },
{ name: 'doctorId', type: 'string', required: true, description: 'Doctor ID' },
{ name: 'startTime', type: 'date', required: true, description: 'Start time' },
{ name: 'endTime', type: 'date', required: true, description: 'End time' },
{ name: 'status', type: 'enum', description: 'Booking status' }
]);
registry.registerSchema(patientSchema);
registry.registerSchema(bookingSchema);
console.log('Common Semble schemas registered');
}
}
exports.SchemaRegistryUtils = SchemaRegistryUtils;