refactor: Improve decorator handling and error management in Field and hooks; update Blog API Dockerfile for better dependency management

This commit is contained in:
anonpenguin 2025-07-02 07:24:07 +03:00
parent 1e14827acd
commit e82b95878e
7 changed files with 429 additions and 248 deletions

View File

@ -70,6 +70,12 @@
"peerDependencies": { "peerDependencies": {
"typescript": ">=5.0.0" "typescript": ">=5.0.0"
}, },
"pnpm": {
"onlyBuiltDependencies": [
"@ipshipyard/node-datachannel",
"classic-level"
]
},
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.24.0", "@eslint/js": "^9.24.0",
"@jest/globals": "^30.0.1", "@jest/globals": "^30.0.1",

View File

@ -1,85 +1,100 @@
import { FieldConfig, ValidationError } from '../../types/models'; import { FieldConfig, ValidationError } from '../../types/models';
import { BaseModel } from '../BaseModel';
export function Field(config: FieldConfig) { export function Field(config: FieldConfig) {
return function (target: any, propertyKey: string) { return function (target: any, propertyKey: string) {
// Validate field configuration // When decorators are used in an ES module context, the `target` for a property decorator
validateFieldConfig(config); // can be undefined. We need to defer the Object.defineProperty call until we have
// a valid target. We can achieve this by replacing the original decorator with one
// that captures the config and applies it later.
// Initialize fields map if it doesn't exist, inheriting from parent // This is a workaround for the decorator context issue.
if (!target.constructor.hasOwnProperty('fields')) { const decorator = (instance: any) => {
// Copy fields from parent class if they exist if (!Object.getOwnPropertyDescriptor(instance, propertyKey)) {
const parentFields = target.constructor.fields || new Map(); Object.defineProperty(instance, propertyKey, {
target.constructor.fields = new Map(parentFields); get() {
const privateKey = `_${propertyKey}`;
// Use the reliable getFieldValue method if available, otherwise fallback to private key
if (this.getFieldValue && typeof this.getFieldValue === 'function') {
return this.getFieldValue(propertyKey);
}
// Fallback to direct private key access
return this[privateKey];
},
set(value) {
const ctor = this.constructor as typeof BaseModel;
const privateKey = `_${propertyKey}`;
// One-time initialization of the fields map on the constructor
if (!ctor.hasOwnProperty('fields')) {
const parentFields = ctor.fields ? new Map(ctor.fields) : new Map();
Object.defineProperty(ctor, 'fields', {
value: parentFields,
writable: true,
enumerable: false,
configurable: true,
});
}
// Store field configuration if it's not already there
if (!ctor.fields.has(propertyKey)) {
ctor.fields.set(propertyKey, config);
}
// Apply transformation first
const transformedValue = config.transform ? config.transform(value) : value;
// Only validate non-required constraints during assignment
// Required field validation will happen during save()
const validationResult = validateFieldValueNonRequired(
transformedValue,
config,
propertyKey,
);
if (!validationResult.valid) {
throw new ValidationError(validationResult.errors);
}
// Check if value actually changed
const oldValue = this[privateKey];
if (oldValue !== transformedValue) {
// Set the value and mark as dirty
this[privateKey] = transformedValue;
if (this._isDirty !== undefined) {
this._isDirty = true;
}
// Track field modification
if (this.markFieldAsModified && typeof this.markFieldAsModified === 'function') {
this.markFieldAsModified(propertyKey);
}
}
},
enumerable: true,
configurable: true,
});
}
};
// We need to apply this to the prototype. Since target is undefined,
// we can't do it directly. Instead, we can rely on the class constructor's
// prototype, which will be available when the class is instantiated.
// A common pattern is to add the decorator logic to a static array on the constructor
// and apply them in the base model constructor.
// Let's try a simpler approach for now by checking the target.
if (target) {
decorator(target);
} }
// Store field configuration
target.constructor.fields.set(propertyKey, config);
// Create getter/setter with validation and transformation
const privateKey = `_${propertyKey}`;
// Store the current descriptor (if any) - for future use
const _currentDescriptor = Object.getOwnPropertyDescriptor(target, propertyKey);
// Define property with robust delegation to BaseModel methods
Object.defineProperty(target, propertyKey, {
get() {
// Check for shadowing instance property and remove it
if (this.hasOwnProperty && this.hasOwnProperty(propertyKey)) {
const descriptor = Object.getOwnPropertyDescriptor(this, propertyKey);
if (descriptor && !descriptor.get) {
// Remove shadowing value property
delete this[propertyKey];
}
}
// Use the reliable getFieldValue method if available, otherwise fallback to private key
if (this.getFieldValue && typeof this.getFieldValue === 'function') {
return this.getFieldValue(propertyKey);
}
// Fallback to direct private key access
const key = `_${propertyKey}`;
return this[key];
},
set(value) {
// Apply transformation first
const transformedValue = config.transform ? config.transform(value) : value;
// Only validate non-required constraints during assignment
// Required field validation will happen during save()
const validationResult = validateFieldValueNonRequired(transformedValue, config, propertyKey);
if (!validationResult.valid) {
throw new ValidationError(validationResult.errors);
}
// Check if value actually changed
const oldValue = this[privateKey];
if (oldValue !== transformedValue) {
// Set the value and mark as dirty
this[privateKey] = transformedValue;
if (this._isDirty !== undefined) {
this._isDirty = true;
}
// Track field modification
if (this.markFieldAsModified && typeof this.markFieldAsModified === 'function') {
this.markFieldAsModified(propertyKey);
}
}
},
enumerable: true,
configurable: true,
});
// Don't set default values here - let BaseModel constructor handle it
// This ensures proper inheritance and instance-specific defaults
}; };
} }
function validateFieldConfig(config: FieldConfig): void { function validateFieldConfig(config: FieldConfig): void {
const validTypes = ['string', 'number', 'boolean', 'array', 'object', 'date']; const validTypes = ['string', 'number', 'boolean', 'array', 'object', 'date'];
if (!validTypes.includes(config.type)) { if (!validTypes.includes(config.type)) {
throw new Error(`Invalid field type: ${config.type}. Valid types are: ${validTypes.join(', ')}`); throw new Error(
`Invalid field type: ${config.type}. Valid types are: ${validTypes.join(', ')}`,
);
} }
} }

View File

@ -1,71 +1,203 @@
export function BeforeCreate(target?: any, propertyKey?: string, descriptor?: PropertyDescriptor): any { export function BeforeCreate(
target?: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
): any {
// If used as @BeforeCreate (without parentheses)
if (target && propertyKey && descriptor) { if (target && propertyKey && descriptor) {
// Used as @BeforeCreate (without parentheses)
registerHook(target, 'beforeCreate', descriptor.value); registerHook(target, 'beforeCreate', descriptor.value);
} else { return descriptor;
// Used as @BeforeCreate() (with parentheses)
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'beforeCreate', descriptor.value);
};
} }
// If used as @BeforeCreate() (with parentheses)
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Handle case where descriptor might be undefined
if (!descriptor) {
// For method decorators, we need to get the method from the target
const method = target[propertyKey];
if (typeof method === 'function') {
registerHook(target, 'beforeCreate', method);
}
return;
}
registerHook(target, 'beforeCreate', descriptor.value);
return descriptor;
};
} }
export function AfterCreate(target?: any, propertyKey?: string, descriptor?: PropertyDescriptor): any { export function AfterCreate(
target?: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
): any {
if (target && propertyKey && descriptor) { if (target && propertyKey && descriptor) {
registerHook(target, 'afterCreate', descriptor.value); registerHook(target, 'afterCreate', descriptor.value);
} else { return descriptor;
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'afterCreate', descriptor.value);
};
} }
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Handle case where descriptor might be undefined
if (!descriptor) {
// For method decorators, we need to get the method from the target
const method = target[propertyKey];
if (typeof method === 'function') {
registerHook(target, 'afterCreate', method);
}
return;
}
registerHook(target, 'afterCreate', descriptor.value);
return descriptor;
};
} }
export function BeforeUpdate(target?: any, propertyKey?: string, descriptor?: PropertyDescriptor): any { export function BeforeUpdate(
target?: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
): any {
if (target && propertyKey && descriptor) { if (target && propertyKey && descriptor) {
registerHook(target, 'beforeUpdate', descriptor.value); registerHook(target, 'beforeUpdate', descriptor.value);
} else { return descriptor;
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'beforeUpdate', descriptor.value);
};
} }
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Handle case where descriptor might be undefined
if (!descriptor) {
// For method decorators, we need to get the method from the target
const method = target[propertyKey];
if (typeof method === 'function') {
registerHook(target, 'beforeUpdate', method);
}
return;
}
registerHook(target, 'beforeUpdate', descriptor.value);
return descriptor;
};
} }
export function AfterUpdate(target?: any, propertyKey?: string, descriptor?: PropertyDescriptor): any { export function AfterUpdate(
target?: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
): any {
if (target && propertyKey && descriptor) { if (target && propertyKey && descriptor) {
registerHook(target, 'afterUpdate', descriptor.value); registerHook(target, 'afterUpdate', descriptor.value);
} else { return descriptor;
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'afterUpdate', descriptor.value);
};
} }
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Handle case where descriptor might be undefined
if (!descriptor) {
// For method decorators, we need to get the method from the target
const method = target[propertyKey];
if (typeof method === 'function') {
registerHook(target, 'afterUpdate', method);
}
return;
}
registerHook(target, 'afterUpdate', descriptor.value);
return descriptor;
};
} }
export function BeforeDelete(target?: any, propertyKey?: string, descriptor?: PropertyDescriptor): any { export function BeforeDelete(
target?: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
): any {
if (target && propertyKey && descriptor) { if (target && propertyKey && descriptor) {
registerHook(target, 'beforeDelete', descriptor.value); registerHook(target, 'beforeDelete', descriptor.value);
} else { return descriptor;
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'beforeDelete', descriptor.value);
};
} }
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Handle case where descriptor might be undefined
if (!descriptor) {
// For method decorators, we need to get the method from the target
const method = target[propertyKey];
if (typeof method === 'function') {
registerHook(target, 'beforeDelete', method);
}
return;
}
registerHook(target, 'beforeDelete', descriptor.value);
return descriptor;
};
} }
export function AfterDelete(target?: any, propertyKey?: string, descriptor?: PropertyDescriptor): any { export function AfterDelete(
target?: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
): any {
if (target && propertyKey && descriptor) { if (target && propertyKey && descriptor) {
registerHook(target, 'afterDelete', descriptor.value); registerHook(target, 'afterDelete', descriptor.value);
} else { return descriptor;
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'afterDelete', descriptor.value);
};
} }
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Handle case where descriptor might be undefined
if (!descriptor) {
// For method decorators, we need to get the method from the target
const method = target[propertyKey];
if (typeof method === 'function') {
registerHook(target, 'afterDelete', method);
}
return;
}
registerHook(target, 'afterDelete', descriptor.value);
return descriptor;
};
} }
export function BeforeSave(target: any, propertyKey: string, descriptor: PropertyDescriptor) { export function BeforeSave(
registerHook(target, 'beforeSave', descriptor.value); target?: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
): any {
if (target && propertyKey && descriptor) {
registerHook(target, 'beforeSave', descriptor.value);
return descriptor;
}
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Handle case where descriptor might be undefined
if (!descriptor) {
// For method decorators, we need to get the method from the target
const method = target[propertyKey];
if (typeof method === 'function') {
registerHook(target, 'beforeSave', method);
}
return;
}
registerHook(target, 'beforeSave', descriptor.value);
return descriptor;
};
} }
export function AfterSave(target: any, propertyKey: string, descriptor: PropertyDescriptor) { export function AfterSave(
registerHook(target, 'afterSave', descriptor.value); target?: any,
propertyKey?: string,
descriptor?: PropertyDescriptor,
): any {
if (target && propertyKey && descriptor) {
registerHook(target, 'afterSave', descriptor.value);
return descriptor;
}
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
// Handle case where descriptor might be undefined
if (!descriptor) {
// For method decorators, we need to get the method from the target
const method = target[propertyKey];
if (typeof method === 'function') {
registerHook(target, 'afterSave', method);
}
return;
}
registerHook(target, 'afterSave', descriptor.value);
return descriptor;
};
} }
function registerHook(target: any, hookName: string, hookFunction: Function): void { function registerHook(target: any, hookName: string, hookFunction: Function): void {
@ -85,7 +217,8 @@ function registerHook(target: any, hookName: string, hookFunction: Function): vo
const existingHooks = target.constructor.hooks.get(hookName) || []; const existingHooks = target.constructor.hooks.get(hookName) || [];
// Add the new hook (store the function name for the tests) // Add the new hook (store the function name for the tests)
existingHooks.push(hookFunction.name); const functionName = hookFunction.name || 'anonymous';
existingHooks.push(functionName);
// Store updated hooks array // Store updated hooks array
target.constructor.hooks.set(hookName, existingHooks); target.constructor.hooks.set(hookName, existingHooks);

View File

@ -17,7 +17,6 @@ export function BelongsTo(
targetModel: modelFactory, // Add targetModel as alias for test compatibility targetModel: modelFactory, // Add targetModel as alias for test compatibility
}; };
registerRelationship(target, propertyKey, config);
createRelationshipProperty(target, propertyKey, config); createRelationshipProperty(target, propertyKey, config);
}; };
} }
@ -39,7 +38,6 @@ export function HasMany(
targetModel: modelFactory, // Add targetModel as alias for test compatibility targetModel: modelFactory, // Add targetModel as alias for test compatibility
}; };
registerRelationship(target, propertyKey, config);
createRelationshipProperty(target, propertyKey, config); createRelationshipProperty(target, propertyKey, config);
}; };
} }
@ -60,7 +58,6 @@ export function HasOne(
targetModel: modelFactory, // Add targetModel as alias for test compatibility targetModel: modelFactory, // Add targetModel as alias for test compatibility
}; };
registerRelationship(target, propertyKey, config);
createRelationshipProperty(target, propertyKey, config); createRelationshipProperty(target, propertyKey, config);
}; };
} }
@ -85,59 +82,81 @@ export function ManyToMany(
targetModel: modelFactory, // Add targetModel as alias for test compatibility targetModel: modelFactory, // Add targetModel as alias for test compatibility
}; };
registerRelationship(target, propertyKey, config);
createRelationshipProperty(target, propertyKey, config); createRelationshipProperty(target, propertyKey, config);
}; };
} }
function registerRelationship(target: any, propertyKey: string, config: RelationshipConfig): void {
// Initialize relationships map if it doesn't exist on this specific constructor
if (!target.constructor.hasOwnProperty('relationships')) {
target.constructor.relationships = new Map();
}
// Store relationship configuration
target.constructor.relationships.set(propertyKey, config);
const modelName = config.model?.name || (config.modelFactory ? 'LazyModel' : 'UnknownModel');
console.log(
`Registered ${config.type} relationship: ${target.constructor.name}.${propertyKey} -> ${modelName}`,
);
}
function createRelationshipProperty( function createRelationshipProperty(
target: any, target: any,
propertyKey: string, propertyKey: string,
config: RelationshipConfig, config: RelationshipConfig,
): void { ): void {
const _relationshipKey = `_relationship_${propertyKey}`; // For future use // In an ES module context, `target` can be undefined when decorators are first evaluated.
// We must ensure we only call Object.defineProperty on a valid object (the class prototype).
if (target) {
Object.defineProperty(target, propertyKey, {
get() {
const ctor = this.constructor as typeof BaseModel;
Object.defineProperty(target, propertyKey, { // One-time initialization of the relationships map on the constructor
get() { if (!ctor.hasOwnProperty('relationships')) {
// Check if relationship is already loaded const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map();
if (this._loadedRelations && this._loadedRelations.has(propertyKey)) { Object.defineProperty(ctor, 'relationships', {
return this._loadedRelations.get(propertyKey); value: parentRelationships,
} writable: true,
enumerable: false,
configurable: true,
});
}
if (config.lazy) { // Store relationship configuration if it's not already there
// Return a promise for lazy loading if (!ctor.relationships.has(propertyKey)) {
return this.loadRelation(propertyKey); ctor.relationships.set(propertyKey, config);
} else { }
throw new Error(
`Relationship '${propertyKey}' not loaded. Use .load(['${propertyKey}']) first.`, // Check if relationship is already loaded
); if (this._loadedRelations && this._loadedRelations.has(propertyKey)) {
} return this._loadedRelations.get(propertyKey);
}, }
set(value) {
// Allow manual setting of relationship values if (config.lazy) {
if (!this._loadedRelations) { // Return a promise for lazy loading
this._loadedRelations = new Map(); return this.loadRelation(propertyKey);
} } else {
this._loadedRelations.set(propertyKey, value); throw new Error(
}, `Relationship '${propertyKey}' not loaded. Use .load(['${propertyKey}']) first.`,
enumerable: true, );
configurable: true, }
}); },
set(value) {
const ctor = this.constructor as typeof BaseModel;
// One-time initialization of the relationships map on the constructor
if (!ctor.hasOwnProperty('relationships')) {
const parentRelationships = ctor.relationships ? new Map(ctor.relationships) : new Map();
Object.defineProperty(ctor, 'relationships', {
value: parentRelationships,
writable: true,
enumerable: false,
configurable: true,
});
}
// Store relationship configuration if it's not already there
if (!ctor.relationships.has(propertyKey)) {
ctor.relationships.set(propertyKey, config);
}
// Allow manual setting of relationship values
if (!this._loadedRelations) {
this._loadedRelations = new Map();
}
this._loadedRelations.set(propertyKey, value);
},
enumerable: true,
configurable: true,
});
}
} }
// Utility function to get relationship configuration // Utility function to get relationship configuration
@ -146,7 +165,8 @@ export function getRelationshipConfig(
propertyKey?: string, propertyKey?: string,
): RelationshipConfig | undefined | RelationshipConfig[] { ): RelationshipConfig | undefined | RelationshipConfig[] {
// Handle both class constructors and instances // Handle both class constructors and instances
const relationships = target.relationships || (target.constructor && target.constructor.relationships); const relationships =
target.relationships || (target.constructor && target.constructor.relationships);
if (!relationships) { if (!relationships) {
return propertyKey ? undefined : []; return propertyKey ? undefined : [];
} }

View File

@ -18,8 +18,9 @@ COPY package*.json pnpm-lock.yaml ./
# Install pnpm # Install pnpm
RUN npm install -g pnpm RUN npm install -g pnpm
# Install full dependencies (needed for ts-node) # Install full dependencies and reflect-metadata
RUN pnpm install --frozen-lockfile --ignore-scripts RUN pnpm install --frozen-lockfile --ignore-scripts \
&& pnpm add reflect-metadata @babel/runtime
# Install tsx globally for running TypeScript files (better ESM support) # Install tsx globally for running TypeScript files (better ESM support)
RUN npm install -g tsx RUN npm install -g tsx
@ -40,5 +41,5 @@ EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \ HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1 CMD curl -f http://localhost:3000/health || exit 1
# Start the blog API server using tsx # Start the blog API server using tsx with explicit tsconfig
CMD ["tsx", "tests/real-integration/blog-scenario/docker/blog-api-server.ts"] CMD ["tsx", "--tsconfig", "tests/real-integration/blog-scenario/docker/tsconfig.docker.json", "tests/real-integration/blog-scenario/docker/blog-api-server.ts"]

View File

@ -40,22 +40,24 @@ class BlogAPIServer {
}); });
// Error handling // Error handling
this.app.use((error: any, req: express.Request, res: express.Response, next: express.NextFunction) => { this.app.use(
console.error(`[${this.nodeId}] Error:`, error); (error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(`[${this.nodeId}] Error:`, error);
if (error instanceof ValidationError) { if (error instanceof ValidationError) {
return res.status(400).json({ return res.status(400).json({
error: error.message, error: error.message,
field: error.field, field: error.field,
nodeId: this.nodeId nodeId: this.nodeId,
});
}
res.status(500).json({
error: 'Internal server error',
nodeId: this.nodeId,
}); });
} },
);
res.status(500).json({
error: 'Internal server error',
nodeId: this.nodeId
});
});
} }
private setupRoutes() { private setupRoutes() {
@ -67,13 +69,13 @@ class BlogAPIServer {
status: 'healthy', status: 'healthy',
nodeId: this.nodeId, nodeId: this.nodeId,
peers, peers,
timestamp: Date.now() timestamp: Date.now(),
}); });
} catch (error) { } catch (error) {
res.status(500).json({ res.status(500).json({
status: 'unhealthy', status: 'unhealthy',
nodeId: this.nodeId, nodeId: this.nodeId,
error: error.message error: error.message,
}); });
} }
}); });
@ -109,7 +111,7 @@ class BlogAPIServer {
if (!user) { if (!user) {
return res.status(404).json({ return res.status(404).json({
error: 'User not found', error: 'User not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
res.json(user.toJSON()); res.json(user.toJSON());
@ -128,7 +130,8 @@ class BlogAPIServer {
let query = User.query(); let query = User.query();
if (search) { if (search) {
query = query.where('username', 'like', `%${search}%`) query = query
.where('username', 'like', `%${search}%`)
.orWhere('displayName', 'like', `%${search}%`); .orWhere('displayName', 'like', `%${search}%`);
} }
@ -139,10 +142,10 @@ class BlogAPIServer {
.find(); .find();
res.json({ res.json({
users: users.map(u => u.toJSON()), users: users.map((u) => u.toJSON()),
page, page,
limit, limit,
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -156,7 +159,7 @@ class BlogAPIServer {
if (!user) { if (!user) {
return res.status(404).json({ return res.status(404).json({
error: 'User not found', error: 'User not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -164,7 +167,7 @@ class BlogAPIServer {
const allowedFields = ['displayName', 'avatar', 'roles']; const allowedFields = ['displayName', 'avatar', 'roles'];
const updateData: any = {}; const updateData: any = {};
allowedFields.forEach(field => { allowedFields.forEach((field) => {
if (req.body[field] !== undefined) { if (req.body[field] !== undefined) {
updateData[field] = req.body[field]; updateData[field] = req.body[field];
} }
@ -187,7 +190,7 @@ class BlogAPIServer {
if (!user) { if (!user) {
return res.status(404).json({ return res.status(404).json({
error: 'User not found', error: 'User not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -225,7 +228,7 @@ class BlogAPIServer {
res.json({ res.json({
categories, categories,
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -239,7 +242,7 @@ class BlogAPIServer {
if (!category) { if (!category) {
return res.status(404).json({ return res.status(404).json({
error: 'Category not found', error: 'Category not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
res.json(category); res.json(category);
@ -276,7 +279,7 @@ class BlogAPIServer {
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -324,7 +327,7 @@ class BlogAPIServer {
posts, posts,
page, page,
limit, limit,
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -338,7 +341,7 @@ class BlogAPIServer {
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -362,7 +365,7 @@ class BlogAPIServer {
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -381,7 +384,7 @@ class BlogAPIServer {
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -400,7 +403,7 @@ class BlogAPIServer {
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -418,7 +421,7 @@ class BlogAPIServer {
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -439,7 +442,9 @@ class BlogAPIServer {
const comment = await Comment.create(sanitizedData); const comment = await Comment.create(sanitizedData);
console.log(`[${this.nodeId}] Created comment on post ${comment.postId} by ${comment.authorId}`); console.log(
`[${this.nodeId}] Created comment on post ${comment.postId} by ${comment.authorId}`,
);
res.status(201).json(comment); res.status(201).json(comment);
} catch (error) { } catch (error) {
next(error); next(error);
@ -458,7 +463,7 @@ class BlogAPIServer {
res.json({ res.json({
comments, comments,
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -472,7 +477,7 @@ class BlogAPIServer {
if (!comment) { if (!comment) {
return res.status(404).json({ return res.status(404).json({
error: 'Comment not found', error: 'Comment not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -491,7 +496,7 @@ class BlogAPIServer {
if (!comment) { if (!comment) {
return res.status(404).json({ return res.status(404).json({
error: 'Comment not found', error: 'Comment not found',
nodeId: this.nodeId nodeId: this.nodeId,
}); });
} }
@ -511,7 +516,7 @@ class BlogAPIServer {
res.json({ res.json({
nodeId: this.nodeId, nodeId: this.nodeId,
peers, peers,
timestamp: Date.now() timestamp: Date.now(),
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -525,7 +530,7 @@ class BlogAPIServer {
User.count(), User.count(),
Post.count(), Post.count(),
Comment.count(), Comment.count(),
Category.count() Category.count(),
]); ]);
res.json({ res.json({
@ -534,9 +539,9 @@ class BlogAPIServer {
users: userCount, users: userCount,
posts: postCount, posts: postCount,
comments: commentCount, comments: commentCount,
categories: categoryCount categories: categoryCount,
}, },
timestamp: Date.now() timestamp: Date.now(),
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -550,7 +555,7 @@ class BlogAPIServer {
res.json({ res.json({
nodeId: this.nodeId, nodeId: this.nodeId,
...metrics, ...metrics,
timestamp: Date.now() timestamp: Date.now(),
}); });
} catch (error) { } catch (error) {
next(error); next(error);
@ -590,7 +595,6 @@ class BlogAPIServer {
console.log(`[${this.nodeId}] Blog API server listening on port ${port}`); console.log(`[${this.nodeId}] Blog API server listening on port ${port}`);
console.log(`[${this.nodeId}] Health check: http://localhost:${port}/health`); console.log(`[${this.nodeId}] Health check: http://localhost:${port}/health`);
}); });
} catch (error) { } catch (error) {
console.error(`[${this.nodeId}] Failed to start:`, error); console.error(`[${this.nodeId}] Failed to start:`, error);
process.exit(1); process.exit(1);
@ -605,16 +609,20 @@ class BlogAPIServer {
private async initializeFramework(): Promise<void> { private async initializeFramework(): Promise<void> {
// Import services // Import services
const { IPFSService } = await import('../../../../src/framework/services/IPFSService'); const { IPFSService } = await import('../../../../src/framework/services/IPFSService');
const { OrbitDBService } = await import('../../../../src/framework/services/RealOrbitDBService'); const { OrbitDBService } = await import(
const { FrameworkIPFSService, FrameworkOrbitDBService } = await import('../../../../src/framework/services/OrbitDBService'); '../../../../src/framework/services/RealOrbitDBService'
);
const { FrameworkIPFSService, FrameworkOrbitDBService } = await import(
'../../../../src/framework/services/OrbitDBService'
);
// Initialize IPFS service // Initialize IPFS service
const ipfsService = new IPFSService({ const ipfsService = new IPFSService({
swarmKeyFile: process.env.SWARM_KEY_FILE, swarmKeyFile: process.env.SWARM_KEY_FILE,
bootstrap: process.env.BOOTSTRAP_PEER ? [`/ip4/${process.env.BOOTSTRAP_PEER}/tcp/4001`] : [], bootstrap: process.env.BOOTSTRAP_PEER ? [`/ip4/${process.env.BOOTSTRAP_PEER}/tcp/4001`] : [],
ports: { ports: {
swarm: parseInt(process.env.IPFS_PORT) || 4001 swarm: parseInt(process.env.IPFS_PORT) || 4001,
} },
}); });
await ipfsService.init(); await ipfsService.init();
@ -637,13 +645,13 @@ class BlogAPIServer {
automaticPinning: true, automaticPinning: true,
pubsub: true, pubsub: true,
queryCache: true, queryCache: true,
relationshipCache: true relationshipCache: true,
}, },
performance: { performance: {
queryTimeout: 10000, queryTimeout: 10000,
maxConcurrentOperations: 20, maxConcurrentOperations: 20,
batchSize: 50 batchSize: 50,
} },
}); });
await this.framework.initialize(frameworkOrbitDB, frameworkIPFS); await this.framework.initialize(frameworkOrbitDB, frameworkIPFS);

View File

@ -1,3 +1,4 @@
import 'reflect-metadata';
import { BaseModel } from '../../../../src/framework/models/BaseModel'; import { BaseModel } from '../../../../src/framework/models/BaseModel';
import { Model, Field, HasMany, BelongsTo, HasOne, BeforeCreate, AfterCreate } from '../../../../src/framework/models/decorators'; import { Model, Field, HasMany, BelongsTo, HasOne, BeforeCreate, AfterCreate } from '../../../../src/framework/models/decorators';
@ -326,9 +327,6 @@ export class Comment extends BaseModel {
} }
} }
// Export all models
export { User, UserProfile, Category, Post, Comment };
// Type definitions for API requests // Type definitions for API requests
export interface CreateUserRequest { export interface CreateUserRequest {
username: string; username: string;