The ValidationManager is GeoHazardWatch's central system for ensuring data integrity and consistency across the file-based storage.
ValidationManager enforces UUID-based naming conventions, validates YAML frontmatter metadata, and provides utilities for generating compliant files. This manager is crucial for maintaining architectural consistency and preventing malformed content from entering the system.
ValidationManager - Validates User Keywords and system-category.
The ValidationManager follows JSPWiki's manager pattern with clean separation between:
All pages must follow the format: {uuid}.md where UUID is a valid RFC 4122 version 4 UUID.
Example Valid Filenames:
3463c02f-5c84-4a42-a574-a56077ff8162.md749e0fc7-0f71-483a-ab80-538d9c598352.mdExample Invalid Filenames:
MyPage.md (not a UUID)3463c02f-5c84-4a42-a574-a56077ff8162.txt (wrong extension)invalid-uuid-format.md (invalid UUID)Every page must include these YAML frontmatter fields:
| Field | Type | Description | Validation Rules |
|---|---|---|---|
title | string | Page display title | Non-empty string |
uuid | string | Unique identifier | Valid RFC 4122 UUID v4 |
slug | string | URL-safe identifier | Lowercase, alphanumeric, hyphens only: /^[a-z0-9]+(?:-[a-z0-9]+)*$/ |
system-category | string | System category | Must match valid category list (case-insensitive warning) |
user-keywords | array | User-defined tags | Array of non-empty strings, max 5 items (configurable) |
lastModified | string | Last modification timestamp | Valid ISO 8601 date string |
System categories are now configurable via app-default-config.json under ngdpbase.systemCategories. Each category has:
| Property | Type | Description |
|---|---|---|
label | string | Display name for the category |
description | string | Human-readable description |
default | boolean | Whether this is the default category |
storageLocation | string | Where pages go: regular (/pages) or required (/required-pages) |
enabled | boolean | Whether the category is active |
Default System Categories:
general - General pages (default, regular storage)system - System configuration pages (required storage)system-admin - Administrative system pages (required storage)documentation - User documentation (regular storage)user - User-generated content (regular storage)test - Testing pages (regular storage)developer - Developer documentation (regular storage)Custom categories can be added to configuration. Categories not in the configuration will generate validation warnings (but are still allowed).
Configuration is loaded from ConfigurationManager via config/app-default-config.json and config/app-custom-config.json:
{
"ngdpbase.maximum.user-keywords": 5,
"ngdpbase.default.system-category": "general"
}
{
"ngdpbase.systemCategories": {
"general": {
"label": "General",
"description": "General pages",
"default": true,
"storageLocation": "regular",
"enabled": true
},
"system": {
"label": "System",
"description": "System configuration and infrastructure pages",
"default": false,
"storageLocation": "required",
"enabled": true
},
"documentation": {
"label": "Documentation",
"description": "User and technical documentation",
"default": false,
"storageLocation": "regular",
"enabled": true
}
}
}
Configuration Properties:
label (string, required) - Display name used in metadata validationdescription (string, optional) - Human-readable descriptiondefault (boolean, optional) - Mark as default category (only one should be true)storageLocation (string, optional) - regular for /pages or required for /required-pagesenabled (boolean, optional) - Set to false to disable categoryAdding Custom Categories:
To add a custom category, add it to app-custom-config.json:
{
"ngdpbase.systemCategories": {
"custom-category": {
"label": "Custom Category",
"description": "My custom category",
"default": false,
"storageLocation": "regular",
"enabled": true
}
}
}
The configuration is hierarchical - custom config merges with and overrides default config.
await validationManager.initialize({
maxUserKeywords: 5,
maxCategories: 3
});
const validationManager = engine.getManager('ValidationManager');
const result = validationManager.validateFilename('3463c02f-5c84-4a42-a574-a56077ff8162.md');
if (result.success) {
console.log('Filename is valid');
} else {
console.error(`Validation failed: ${result.error}`);
}
Result Object:
{
success: boolean,
error: string | null
}
const metadata = {
title: 'My Sample Page',
uuid: '3463c02f-5c84-4a42-a574-a56077ff8162',
slug: 'my-sample-page',
'system-category': 'Documentation',
'user-keywords': ['guide', 'tutorial'],
lastModified: '2025-10-05T12:00:00.000Z'
};
const result = validationManager.validateMetadata(metadata);
if (result.success) {
console.log('Metadata is valid');
if (result.warnings.length > 0) {
console.warn('Warnings:', result.warnings);
}
} else {
console.error(`Validation failed: ${result.error}`);
}
Result Object:
{
success: boolean,
error: string | null,
warnings: string[]
}
const filename = '3463c02f-5c84-4a42-a574-a56077ff8162.md';
const metadata = { /* metadata object */ };
const content = '# My Page\n\nContent here...';
const result = validationManager.validatePage(filename, metadata, content);
if (result.success) {
console.log('Page is valid');
} else {
console.error(`Validation failed: ${result.error}`);
}
Result Object:
{
success: boolean,
error: string | null,
warnings: string[],
filenameValid: boolean,
metadataValid: boolean
}
const metadata = validationManager.generateValidMetadata('My New Page', {
'system-category': 'Documentation',
'user-keywords': ['guide', 'howto']
});
// Result:
{
title: 'My New Page',
uuid: 'generated-uuid-v4',
slug: 'my-new-page',
'system-category': 'Documentation',
'user-keywords': ['guide', 'howto'],
lastModified: '2025-10-05T12:00:00.000Z'
}
const filename = validationManager.generateFilename(metadata);
// Returns: '3463c02f-5c84-4a42-a574-a56077ff8162.md'
const slug = validationManager.generateSlug('My Amazing Page!');
// Returns: 'my-amazing-page'
const slug2 = validationManager.generateSlug('Hello World---Test');
// Returns: 'hello-world-test'
const fs = require('fs-extra');
const matter = require('gray-matter');
const filePath = '/pages/old-page.md';
const fileContent = await fs.readFile(filePath, 'utf8');
const fileData = matter(fileContent);
const result = validationManager.validateExistingFile(filePath, fileData);
if (!result.success) {
console.error(`Validation failed: ${result.error}`);
if (result.fixes) {
console.log('Suggested fixes:');
if (result.fixes.filename) {
console.log(`- Rename file to: ${result.fixes.filename}`);
}
console.log('- Updated metadata:', result.fixes.metadata);
}
}
const fixes = validationManager.generateFixSuggestions('old-page.md', {
title: 'Old Page'
// Missing: uuid, slug, system-category, user-keywords, lastModified
});
// Result:
{
filename: '{generated-uuid}.md',
metadata: {
title: 'Old Page',
uuid: '{generated-uuid-v4}',
slug: 'old-page',
'system-category': 'general',
'user-keywords': [],
lastModified: '2025-10-05T12:00:00.000Z'
}
}
const content = '# My Page\n\nVery short content';
const result = validationManager.validateContent(content);
// Result:
{
warnings: ['Content is very short (less than 10 characters)']
}
Extends BaseManager and provides comprehensive validation services.
requiredMetadataFields: string[]Array of required metadata field names:
['title', 'uuid', 'slug', 'system-category', 'user-keywords', 'lastModified']
validSystemCategories: string[]Array of standard system category values:
['System', 'System/Admin', 'Documentation', 'General', 'User', 'Test', 'Developer']
maxUserKeywords: numberMaximum allowed user keywords per page (default: 5, configurable via ngdpbase.maximum.user-keywords).
maxCategories: numberMaximum allowed categories per page (default: 3, configurable).
initialize(config?: object): Promise<void>Initializes the ValidationManager with configuration from ConfigurationManager.
Parameters:
config (optional): Configuration overridesmaxUserKeywords (number): Max keywords per pagemaxCategories (number): Max categories per pageReturns: Promise<void>
Example:
await validationManager.initialize({
maxUserKeywords: 10,
maxCategories: 5
});
validateFilename(filename: string): objectValidates that a filename follows UUID naming convention.
Parameters:
filename (string): The filename to validate (e.g., '{uuid}.md')Returns: Validation result object
{
success: boolean,
error: string | null
}
Example:
const result = validationManager.validateFilename('3463c02f-5c84-4a42-a574-a56077ff8162.md');
// { success: true, error: null }
const invalid = validationManager.validateFilename('MyPage.md');
// { success: false, error: "Filename 'MyPage.md' does not follow UUID naming convention..." }
validateMetadata(metadata: object): objectValidates page metadata contains all required fields with proper values.
Parameters:
metadata (object): The metadata object from YAML frontmatterReturns: Validation result object
{
success: boolean,
error: string | null,
warnings: string[]
}
Validation Rules:
title: Non-empty stringuuid: Valid RFC 4122 UUID v4slug: URL-safe string matching /^[a-z0-9]+(?:-[a-z0-9]+)*$/system-category: String (warning if not in standard list)user-keywords: Array of non-empty strings, max maxUserKeywords itemslastModified: Valid ISO 8601 date stringExample:
const result = validationManager.validateMetadata({
title: 'My Page',
uuid: '3463c02f-5c84-4a42-a574-a56077ff8162',
slug: 'my-page',
'system-category': 'Documentation',
'user-keywords': ['guide'],
lastModified: '2025-10-05T12:00:00.000Z'
});
// { success: true, error: null, warnings: [] }
validatePage(filename: string, metadata: object, content?: string): objectValidates a complete page before saving, including UUID consistency check.
Parameters:
filename (string): The target filenamemetadata (object): The page metadatacontent (string, optional): The page content for optional validationReturns: Comprehensive validation result
{
success: boolean,
error: string | null,
warnings: string[],
filenameValid: boolean,
metadataValid: boolean
}
Validation Checks:
Example:
const result = validationManager.validatePage(
'3463c02f-5c84-4a42-a574-a56077ff8162.md',
{ title: 'Test', uuid: '3463c02f-5c84-4a42-a574-a56077ff8162', /* ... */ },
'# Test Page\n\nContent...'
);
validateContent(content: string): objectPerforms optional content quality checks.
Parameters:
content (string): The page contentReturns: Content validation result
{
warnings: string[]
}
Checks:
#)Example:
const result = validationManager.validateContent('# Title\n\nContent here...');
// { warnings: [] }
const short = validationManager.validateContent('Hi');
// { warnings: ['Content is very short (less than 10 characters)', 'Content appears to lack markdown headers'] }
isValidSlug(slug: string): booleanValidates slug format (URL-safe).
Parameters:
slug (string): The slug to validateReturns: boolean - True if valid
Rules: Must be lowercase, alphanumeric, and hyphens only: /^[a-z0-9]+(?:-[a-z0-9]+)*$/
Example:
validationManager.isValidSlug('my-sample-page'); // true
validationManager.isValidSlug('My-Sample-Page'); // false (uppercase)
validationManager.isValidSlug('my--page'); // false (double hyphen)
validationManager.isValidSlug('-my-page'); // false (leading hyphen)
generateValidMetadata(title: string, options?: object): objectGenerates properly formatted metadata for a new page with all required fields.
Parameters:
title (string): Page titleoptions (object, optional): Additional metadata optionsuuid (string): Use specific UUID (generates new one if not provided)slug (string): Use specific slug (generates from title if not provided)system-category (string): System category (uses default if not provided)user-keywords (array): User keywords arrayReturns: Complete metadata object
{
title: string,
uuid: string,
slug: string,
'system-category': string,
'user-keywords': string[],
lastModified: string
}
Example:
const metadata = validationManager.generateValidMetadata('My New Page', {
'system-category': 'Documentation',
'user-keywords': ['guide', 'tutorial'],
customField: 'custom value'
});
// Result:
{
title: 'My New Page',
uuid: '3463c02f-5c84-4a42-a574-a56077ff8162', // auto-generated
slug: 'my-new-page', // auto-generated from title
'system-category': 'Documentation',
'user-keywords': ['guide', 'tutorial'],
lastModified: '2025-10-05T12:00:00.000Z', // current timestamp
customField: 'custom value'
}
generateSlug(title: string): stringGenerates URL-safe slug from title.
Parameters:
title (string): Page titleReturns: string - URL-safe slug
Transformation Rules:
Example:
validationManager.generateSlug('Hello World'); // 'hello-world'
validationManager.generateSlug('My Amazing Page!!!'); // 'my-amazing-page'
validationManager.generateSlug('Test---Multiple Spaces'); // 'test-multiple-spaces'
validationManager.generateSlug(' Trim Me '); // 'trim-me'
generateFilename(metadata: object): stringGenerates UUID-based filename from metadata.
Parameters:
metadata (object): Page metadata containing UUIDReturns: string - Filename in {uuid}.md format
Throws: Error if metadata.uuid is missing
Example:
const filename = validationManager.generateFilename({
uuid: '3463c02f-5c84-4a42-a574-a56077ff8162',
title: 'My Page'
});
// Returns: '3463c02f-5c84-4a42-a574-a56077ff8162.md'
validateExistingFile(filePath: string, fileData: object): objectValidates an existing file and provides fix suggestions if validation fails.
Parameters:
filePath (string): Path to the existing filefileData (object): Object with content and data properties from gray-mattercontent (string): File content without frontmatterdata (object): Parsed YAML frontmatterReturns: Validation result with optional fix suggestions
{
success: boolean,
error: string | null,
warnings: string[],
filenameValid: boolean,
metadataValid: boolean,
fixes?: {
filename: string | null,
metadata: object
}
}
Example:
const matter = require('gray-matter');
const fileContent = await fs.readFile('/pages/old-page.md', 'utf8');
const fileData = matter(fileContent);
const result = validationManager.validateExistingFile('/pages/old-page.md', fileData);
if (!result.success && result.fixes) {
console.log('Rename file to:', result.fixes.filename);
console.log('Update metadata to:', result.fixes.metadata);
}
getCategoryConfig(label: string): object|nullGets system category configuration by label.
Parameters:
label (string): Category label (e.g., "General", "System")Returns: Category configuration object or null
{
key: string, // Category key from config
label: string, // Display label
description: string, // Category description
default: boolean, // Is default category
storageLocation: string, // 'regular' or 'required'
enabled: boolean // Is enabled
}
Example:
const categoryConfig = validationManager.getCategoryConfig('System');
if (categoryConfig) {
console.log('Storage location:', categoryConfig.storageLocation); // 'required'
console.log('Description:', categoryConfig.description);
}
getCategoryStorageLocation(category: string): stringGets storage location for a category.
Parameters:
category (string): Category labelReturns: string - Storage location ('regular' or 'required')
Example:
const location = validationManager.getCategoryStorageLocation('System');
// Returns: 'required'
const location2 = validationManager.getCategoryStorageLocation('Documentation');
// Returns: 'regular'
Use Case:
const category = metadata['system-category'];
const storageLocation = validationManager.getCategoryStorageLocation(category);
if (storageLocation === 'required') {
targetPath = path.join(requiredPagesDir, filename);
} else {
targetPath = path.join(pagesDir, filename);
}
getAllSystemCategories(): Array<object>Gets all enabled system categories with their configuration.
Parameters: None
Returns: Array of category configuration objects
[
{
key: 'general',
label: 'General',
description: 'General pages',
default: true,
storageLocation: 'regular',
enabled: true
},
// ... more categories
]
Example:
const categories = validationManager.getAllSystemCategories();
// Display in UI dropdown
categories.forEach(cat => {
console.log(`${cat.label}: ${cat.description}`);
});
// Filter by storage location
const requiredCategories = categories.filter(c => c.storageLocation === 'required');
getDefaultSystemCategory(): stringGets the default system category label.
Parameters: None
Returns: string - Default category label (e.g., "General")
Example:
const defaultCategory = validationManager.getDefaultSystemCategory();
// Returns: 'General'
// Use in metadata generation
const metadata = {
title: 'New Page',
'system-category': validationManager.getDefaultSystemCategory(),
// ...
};
generateFixSuggestions(filename: string, metadata: object): objectGenerates suggestions to fix validation issues in existing pages.
Parameters:
filename (string): Current filenamemetadata (object): Current metadataReturns: Fix suggestions object
{
filename: string | null, // Suggested new filename (if change needed)
metadata: object // Complete corrected metadata
}
Fix Logic:
Example:
const fixes = validationManager.generateFixSuggestions('old-name.md', {
title: 'Old Page'
// Missing: uuid, slug, system-category, user-keywords, lastModified
});
// Result:
{
filename: '3463c02f-5c84-4a42-a574-a56077ff8162.md', // new UUID-based filename
metadata: {
title: 'Old Page',
uuid: '3463c02f-5c84-4a42-a574-a56077ff8162',
slug: 'old-page',
'system-category': 'general',
'user-keywords': [],
lastModified: '2025-10-05T12:00:00.000Z'
}
}
PageManager should use ValidationManager before creating or updating pages:
const pageManager = engine.getManager('PageManager');
const validationManager = engine.getManager('ValidationManager');
const path = require('path');
async function createPage(title, content, options) {
// Generate valid metadata
const metadata = validationManager.generateValidMetadata(title, options);
// Generate filename
const filename = validationManager.generateFilename(metadata);
// Validate before saving
const validation = validationManager.validatePage(filename, metadata, content);
if (!validation.success) {
throw new Error(`Page validation failed: ${validation.error}`);
}
// Determine storage location based on category
const category = metadata['system-category'];
const storageLocation = validationManager.getCategoryStorageLocation(category);
let targetPath;
if (storageLocation === 'required') {
targetPath = path.join(requiredPagesDir, filename);
} else {
targetPath = path.join(pagesDir, filename);
}
// Save via PageManager
await pageManager.savePage(targetPath, { metadata, content });
}
Category-Based Storage Example:
// System pages go to /required-pages
const systemMetadata = validationManager.generateValidMetadata('System Config', {
'system-category': 'System'
});
const systemLocation = validationManager.getCategoryStorageLocation('System');
console.log(systemLocation); // 'required'
// General pages go to /pages
const generalMetadata = validationManager.generateValidMetadata('My Page', {
'system-category': 'General'
});
const generalLocation = validationManager.getCategoryStorageLocation('General');
console.log(generalLocation); // 'regular'
ValidationManager reads configuration from ConfigurationManager:
const configManager = engine.getManager('ConfigurationManager');
// Configure in app-custom-config.json:
{
"amdwiki": {
"maximum": {
"user-keywords": 10
},
"default": {
"system-category": "Documentation"
}
}
}
1. User provides title and options
2. ValidationManager.generateValidMetadata(title, options)
3. ValidationManager.generateFilename(metadata)
4. ValidationManager.validatePage(filename, metadata, content)
5. If valid: PageManager.savePage()
6. If invalid: Display errors and prevent save
1. Read existing file with gray-matter
2. ValidationManager.validateExistingFile(path, fileData)
3. If invalid:
- Get fix suggestions from result.fixes
- Rename file to result.fixes.filename
- Update metadata to result.fixes.metadata
- Save corrected page
4. If valid: No action needed
async function validateAllPages() {
const pageManager = engine.getManager('PageManager');
const validationManager = engine.getManager('ValidationManager');
const matter = require('gray-matter');
const pages = await pageManager.getAllPages();
const results = [];
for (const page of pages) {
const filePath = pageManager.getPagePath(page);
const fileContent = await fs.readFile(filePath, 'utf8');
const fileData = matter(fileContent);
const result = validationManager.validateExistingFile(filePath, fileData);
if (!result.success) {
results.push({
page: page,
error: result.error,
fixes: result.fixes
});
}
}
return results;
}
| Error | Cause | Solution |
|---|---|---|
Filename does not follow UUID naming convention | Filename is not a valid UUID | Rename file using generateFilename() |
UUID mismatch | Filename UUID ≠ metadata UUID | Update filename or metadata UUID to match |
Required metadata field 'X' is missing | Missing required field in YAML | Add missing field using generateValidMetadata() |
uuid must be a valid RFC 4122 UUID v4 | Invalid UUID format | Generate new UUID with uuidv4() |
slug must be a URL-safe string | Invalid slug format | Use generateSlug() to create valid slug |
Maximum N user keywords are allowed | Too many keywords | Reduce keywords array to max allowed |
{
success: true,
error: null,
warnings: ['System category "Custom" is not in the standard list...']
}
{
success: false,
error: "Required metadata field 'uuid' is missing",
warnings: []
}
Always Validate Before Saving
javascript
const validation = validationManager.validatePage(filename, metadata, content);
if (!validation.success) {
throw new Error(validation.error);
}
await pageManager.savePage(filename, pageData);
Use Metadata Generation for New Pages
javascript
// DON'T manually create metadata
const metadata = { title: 'New Page', uuid: generateUUID(), /* ... */ };
// DO use generateValidMetadata
const metadata = validationManager.generateValidMetadata('New Page', options);
Handle Warnings Appropriately
javascript
if (result.warnings.length > 0) {
console.warn('Validation warnings:', result.warnings);
// Log or notify but allow operation to continue
}
Use Fix Suggestions for Migration
javascript
if (!result.success && result.fixes) {
// Apply automated fixes
await renameFile(oldPath, result.fixes.filename);
await updateMetadata(result.fixes.metadata);
}
Validate in Development/Testing
javascript
if (process.env.NODE_ENV === 'development') {
const validation = validationManager.validatePage(filename, metadata);
if (!validation.success) {
throw new Error(`DEV: Validation failed - ${validation.error}`);
}
}
const ValidationManager = require('./ValidationManager');
const WikiEngine = require('../WikiEngine');
describe('ValidationManager', () => {
let engine, validationManager;
beforeAll(async () => {
engine = new WikiEngine();
await engine.initialize();
validationManager = engine.getManager('ValidationManager');
});
test('validates correct filename', () => {
const result = validationManager.validateFilename(
'3463c02f-5c84-4a42-a574-a56077ff8162.md'
);
expect(result.success).toBe(true);
expect(result.error).toBeNull();
});
test('rejects invalid filename', () => {
const result = validationManager.validateFilename('MyPage.md');
expect(result.success).toBe(false);
expect(result.error).toContain('UUID naming convention');
});
test('generates valid metadata', () => {
const metadata = validationManager.generateValidMetadata('Test Page', {
'system-category': 'Documentation'
});
const validation = validationManager.validateMetadata(metadata);
expect(validation.success).toBe(true);
});
test('generates valid slug', () => {
expect(validationManager.generateSlug('Hello World')).toBe('hello-world');
expect(validationManager.generateSlug('Test!@#$%Page')).toBe('test-page');
});
});
Source: src/managers/ValidationManager.js
Dependencies:
BaseManager - Base manager classuuid - UUID generation and validationpath - Path operationsRelated Managers:
PageManager - Uses ValidationManager for page operationsConfigurationManager - Provides configuration valuesSee Documentation for Developers for technical documentation on extending or integrating with the ValidationManager.