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": {
"typescript": ">=5.0.0"
},
"pnpm": {
"onlyBuiltDependencies": [
"@ipshipyard/node-datachannel",
"classic-level"
]
},
"devDependencies": {
"@eslint/js": "^9.24.0",
"@jest/globals": "^30.0.1",

View File

@ -1,85 +1,100 @@
import { FieldConfig, ValidationError } from '../../types/models';
import { BaseModel } from '../BaseModel';
export function Field(config: FieldConfig) {
return function (target: any, propertyKey: string) {
// Validate field configuration
validateFieldConfig(config);
// Initialize fields map if it doesn't exist, inheriting from parent
if (!target.constructor.hasOwnProperty('fields')) {
// Copy fields from parent class if they exist
const parentFields = target.constructor.fields || new Map();
target.constructor.fields = new Map(parentFields);
// When decorators are used in an ES module context, the `target` for a property decorator
// 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.
// This is a workaround for the decorator context issue.
const decorator = (instance: any) => {
if (!Object.getOwnPropertyDescriptor(instance, propertyKey)) {
Object.defineProperty(instance, propertyKey, {
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 {
const validTypes = ['string', 'number', 'boolean', 'array', 'object', 'date'];
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(', ')}`,
);
}
}
@ -176,7 +191,7 @@ export function getFieldConfig(target: any, propertyKey: string): FieldConfig |
if (target.constructor && target.constructor !== Function) {
current = target.constructor;
}
// Walk up the prototype chain to find field configuration
while (current && current !== Function && current !== Object) {
if (current.fields && current.fields.has(propertyKey)) {
@ -188,7 +203,7 @@ export function getFieldConfig(target: any, propertyKey: string): FieldConfig |
break;
}
}
return undefined;
}

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) {
// Used as @BeforeCreate (without parentheses)
registerHook(target, 'beforeCreate', descriptor.value);
} else {
// Used as @BeforeCreate() (with parentheses)
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'beforeCreate', descriptor.value);
};
return descriptor;
}
// 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) {
registerHook(target, 'afterCreate', descriptor.value);
} else {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'afterCreate', 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, '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) {
registerHook(target, 'beforeUpdate', descriptor.value);
} else {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'beforeUpdate', 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, '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) {
registerHook(target, 'afterUpdate', descriptor.value);
} else {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'afterUpdate', 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, '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) {
registerHook(target, 'beforeDelete', descriptor.value);
} else {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'beforeDelete', 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, '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) {
registerHook(target, 'afterDelete', descriptor.value);
} else {
return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'afterDelete', 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, 'afterDelete', method);
}
return;
}
registerHook(target, 'afterDelete', descriptor.value);
return descriptor;
};
}
export function BeforeSave(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
registerHook(target, 'beforeSave', descriptor.value);
export function BeforeSave(
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) {
registerHook(target, 'afterSave', descriptor.value);
export function AfterSave(
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 {
@ -74,7 +206,7 @@ function registerHook(target: any, hookName: string, hookFunction: Function): vo
// Copy hooks from parent class if they exist
const parentHooks = target.constructor.hooks || new Map();
target.constructor.hooks = new Map();
// Copy all parent hooks
for (const [name, hooks] of parentHooks.entries()) {
target.constructor.hooks.set(name, [...hooks]);
@ -85,7 +217,8 @@ function registerHook(target: any, hookName: string, hookFunction: Function): vo
const existingHooks = target.constructor.hooks.get(hookName) || [];
// 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
target.constructor.hooks.set(hookName, existingHooks);
@ -100,10 +233,10 @@ export function getHooks(target: any, hookName?: string): string[] | Record<stri
if (target.constructor && target.constructor !== Function) {
current = target.constructor;
}
// Collect hooks from the entire prototype chain
const allHooks: Record<string, string[]> = {};
while (current && current !== Function && current !== Object) {
if (current.hooks) {
for (const [name, hookFunctions] of current.hooks.entries()) {
@ -120,7 +253,7 @@ export function getHooks(target: any, hookName?: string): string[] | Record<stri
break;
}
}
if (hookName) {
return allHooks[hookName] || [];
} else {

View File

@ -17,7 +17,6 @@ export function BelongsTo(
targetModel: modelFactory, // Add targetModel as alias for test compatibility
};
registerRelationship(target, propertyKey, config);
createRelationshipProperty(target, propertyKey, config);
};
}
@ -39,7 +38,6 @@ export function HasMany(
targetModel: modelFactory, // Add targetModel as alias for test compatibility
};
registerRelationship(target, propertyKey, config);
createRelationshipProperty(target, propertyKey, config);
};
}
@ -60,7 +58,6 @@ export function HasOne(
targetModel: modelFactory, // Add targetModel as alias for test compatibility
};
registerRelationship(target, propertyKey, config);
createRelationshipProperty(target, propertyKey, config);
};
}
@ -85,59 +82,81 @@ export function ManyToMany(
targetModel: modelFactory, // Add targetModel as alias for test compatibility
};
registerRelationship(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(
target: any,
propertyKey: string,
config: RelationshipConfig,
): 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, {
get() {
// Check if relationship is already loaded
if (this._loadedRelations && this._loadedRelations.has(propertyKey)) {
return this._loadedRelations.get(propertyKey);
}
// 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,
});
}
if (config.lazy) {
// Return a promise for lazy loading
return this.loadRelation(propertyKey);
} else {
throw new Error(
`Relationship '${propertyKey}' not loaded. Use .load(['${propertyKey}']) first.`,
);
}
},
set(value) {
// Allow manual setting of relationship values
if (!this._loadedRelations) {
this._loadedRelations = new Map();
}
this._loadedRelations.set(propertyKey, value);
},
enumerable: true,
configurable: true,
});
// Store relationship configuration if it's not already there
if (!ctor.relationships.has(propertyKey)) {
ctor.relationships.set(propertyKey, config);
}
// Check if relationship is already loaded
if (this._loadedRelations && this._loadedRelations.has(propertyKey)) {
return this._loadedRelations.get(propertyKey);
}
if (config.lazy) {
// Return a promise for lazy loading
return this.loadRelation(propertyKey);
} else {
throw new Error(
`Relationship '${propertyKey}' not loaded. Use .load(['${propertyKey}']) first.`,
);
}
},
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
@ -146,11 +165,12 @@ export function getRelationshipConfig(
propertyKey?: string,
): RelationshipConfig | undefined | RelationshipConfig[] {
// 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) {
return propertyKey ? undefined : [];
}
if (propertyKey) {
return relationships.get(propertyKey);
} else {

View File

@ -18,8 +18,9 @@ COPY package*.json pnpm-lock.yaml ./
# Install pnpm
RUN npm install -g pnpm
# Install full dependencies (needed for ts-node)
RUN pnpm install --frozen-lockfile --ignore-scripts
# Install full dependencies and reflect-metadata
RUN pnpm install --frozen-lockfile --ignore-scripts \
&& pnpm add reflect-metadata @babel/runtime
# Install tsx globally for running TypeScript files (better ESM support)
RUN npm install -g tsx
@ -40,5 +41,5 @@ EXPOSE 3000
HEALTHCHECK --interval=30s --timeout=10s --start-period=60s --retries=3 \
CMD curl -f http://localhost:3000/health || exit 1
# Start the blog API server using tsx
CMD ["tsx", "tests/real-integration/blog-scenario/docker/blog-api-server.ts"]
# Start the blog API server using tsx with explicit tsconfig
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
this.app.use((error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(`[${this.nodeId}] Error:`, error);
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
field: error.field,
nodeId: this.nodeId
});
}
this.app.use(
(error: any, req: express.Request, res: express.Response, next: express.NextFunction) => {
console.error(`[${this.nodeId}] Error:`, error);
res.status(500).json({
error: 'Internal server error',
nodeId: this.nodeId
});
});
if (error instanceof ValidationError) {
return res.status(400).json({
error: error.message,
field: error.field,
nodeId: this.nodeId,
});
}
res.status(500).json({
error: 'Internal server error',
nodeId: this.nodeId,
});
},
);
}
private setupRoutes() {
@ -63,17 +65,17 @@ class BlogAPIServer {
this.app.get('/health', async (req, res) => {
try {
const peers = await this.getConnectedPeerCount();
res.json({
status: 'healthy',
res.json({
status: 'healthy',
nodeId: this.nodeId,
peers,
timestamp: Date.now()
timestamp: Date.now(),
});
} catch (error) {
res.status(500).json({
status: 'unhealthy',
nodeId: this.nodeId,
error: error.message
error: error.message,
});
}
});
@ -94,7 +96,7 @@ class BlogAPIServer {
BlogValidation.validateUser(sanitizedData);
const user = await User.create(sanitizedData);
console.log(`[${this.nodeId}] Created user: ${user.username} (${user.id})`);
res.status(201).json(user.toJSON());
} catch (error) {
@ -107,9 +109,9 @@ class BlogAPIServer {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
return res.status(404).json({
error: 'User not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
res.json(user.toJSON());
@ -128,7 +130,8 @@ class BlogAPIServer {
let query = User.query();
if (search) {
query = query.where('username', 'like', `%${search}%`)
query = query
.where('username', 'like', `%${search}%`)
.orWhere('displayName', 'like', `%${search}%`);
}
@ -139,10 +142,10 @@ class BlogAPIServer {
.find();
res.json({
users: users.map(u => u.toJSON()),
users: users.map((u) => u.toJSON()),
page,
limit,
nodeId: this.nodeId
nodeId: this.nodeId,
});
} catch (error) {
next(error);
@ -154,17 +157,17 @@ class BlogAPIServer {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
return res.status(404).json({
error: 'User not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
// Only allow updating certain fields
const allowedFields = ['displayName', 'avatar', 'roles'];
const updateData: any = {};
allowedFields.forEach(field => {
allowedFields.forEach((field) => {
if (req.body[field] !== undefined) {
updateData[field] = req.body[field];
}
@ -172,7 +175,7 @@ class BlogAPIServer {
Object.assign(user, updateData);
await user.save();
console.log(`[${this.nodeId}] Updated user: ${user.username}`);
res.json(user.toJSON());
} catch (error) {
@ -185,9 +188,9 @@ class BlogAPIServer {
try {
const user = await User.findById(req.params.id);
if (!user) {
return res.status(404).json({
return res.status(404).json({
error: 'User not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
@ -207,7 +210,7 @@ class BlogAPIServer {
BlogValidation.validateCategory(sanitizedData);
const category = await Category.create(sanitizedData);
console.log(`[${this.nodeId}] Created category: ${category.name} (${category.id})`);
res.status(201).json(category);
} catch (error) {
@ -225,7 +228,7 @@ class BlogAPIServer {
res.json({
categories,
nodeId: this.nodeId
nodeId: this.nodeId,
});
} catch (error) {
next(error);
@ -237,9 +240,9 @@ class BlogAPIServer {
try {
const category = await Category.findById(req.params.id);
if (!category) {
return res.status(404).json({
return res.status(404).json({
error: 'Category not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
res.json(category);
@ -257,7 +260,7 @@ class BlogAPIServer {
BlogValidation.validatePost(sanitizedData);
const post = await Post.create(sanitizedData);
console.log(`[${this.nodeId}] Created post: ${post.title} (${post.id})`);
res.status(201).json(post);
} catch (error) {
@ -272,11 +275,11 @@ class BlogAPIServer {
.where('id', req.params.id)
.with(['author', 'category', 'comments'])
.first();
if (!post) {
return res.status(404).json({
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
@ -297,7 +300,7 @@ class BlogAPIServer {
const tag = req.query.tag as string;
let query = Post.query().with(['author', 'category']);
if (status) {
query = query.where('status', status);
}
@ -324,7 +327,7 @@ class BlogAPIServer {
posts,
page,
limit,
nodeId: this.nodeId
nodeId: this.nodeId,
});
} catch (error) {
next(error);
@ -336,9 +339,9 @@ class BlogAPIServer {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
@ -347,7 +350,7 @@ class BlogAPIServer {
Object.assign(post, req.body);
post.updatedAt = Date.now();
await post.save();
console.log(`[${this.nodeId}] Updated post: ${post.title}`);
res.json(post);
} catch (error) {
@ -360,12 +363,12 @@ class BlogAPIServer {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
await post.publish();
console.log(`[${this.nodeId}] Published post: ${post.title}`);
res.json(post);
@ -379,12 +382,12 @@ class BlogAPIServer {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
await post.unpublish();
console.log(`[${this.nodeId}] Unpublished post: ${post.title}`);
res.json(post);
@ -398,12 +401,12 @@ class BlogAPIServer {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
await post.like();
res.json({ likeCount: post.likeCount });
} catch (error) {
@ -416,12 +419,12 @@ class BlogAPIServer {
try {
const post = await Post.findById(req.params.id);
if (!post) {
return res.status(404).json({
return res.status(404).json({
error: 'Post not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
await post.incrementViews();
res.json({ viewCount: post.viewCount });
} catch (error) {
@ -438,8 +441,10 @@ class BlogAPIServer {
BlogValidation.validateComment(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);
} catch (error) {
next(error);
@ -458,7 +463,7 @@ class BlogAPIServer {
res.json({
comments,
nodeId: this.nodeId
nodeId: this.nodeId,
});
} catch (error) {
next(error);
@ -470,12 +475,12 @@ class BlogAPIServer {
try {
const comment = await Comment.findById(req.params.id);
if (!comment) {
return res.status(404).json({
return res.status(404).json({
error: 'Comment not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
await comment.approve();
console.log(`[${this.nodeId}] Approved comment ${comment.id}`);
res.json(comment);
@ -489,12 +494,12 @@ class BlogAPIServer {
try {
const comment = await Comment.findById(req.params.id);
if (!comment) {
return res.status(404).json({
return res.status(404).json({
error: 'Comment not found',
nodeId: this.nodeId
nodeId: this.nodeId,
});
}
await comment.like();
res.json({ likeCount: comment.likeCount });
} catch (error) {
@ -511,7 +516,7 @@ class BlogAPIServer {
res.json({
nodeId: this.nodeId,
peers,
timestamp: Date.now()
timestamp: Date.now(),
});
} catch (error) {
next(error);
@ -525,7 +530,7 @@ class BlogAPIServer {
User.count(),
Post.count(),
Comment.count(),
Category.count()
Category.count(),
]);
res.json({
@ -534,9 +539,9 @@ class BlogAPIServer {
users: userCount,
posts: postCount,
comments: commentCount,
categories: categoryCount
categories: categoryCount,
},
timestamp: Date.now()
timestamp: Date.now(),
});
} catch (error) {
next(error);
@ -550,7 +555,7 @@ class BlogAPIServer {
res.json({
nodeId: this.nodeId,
...metrics,
timestamp: Date.now()
timestamp: Date.now(),
});
} catch (error) {
next(error);
@ -590,7 +595,6 @@ class BlogAPIServer {
console.log(`[${this.nodeId}] Blog API server listening on port ${port}`);
console.log(`[${this.nodeId}] Health check: http://localhost:${port}/health`);
});
} catch (error) {
console.error(`[${this.nodeId}] Failed to start:`, error);
process.exit(1);
@ -605,16 +609,20 @@ class BlogAPIServer {
private async initializeFramework(): Promise<void> {
// Import services
const { IPFSService } = await import('../../../../src/framework/services/IPFSService');
const { OrbitDBService } = await import('../../../../src/framework/services/RealOrbitDBService');
const { FrameworkIPFSService, FrameworkOrbitDBService } = await import('../../../../src/framework/services/OrbitDBService');
const { OrbitDBService } = await import(
'../../../../src/framework/services/RealOrbitDBService'
);
const { FrameworkIPFSService, FrameworkOrbitDBService } = await import(
'../../../../src/framework/services/OrbitDBService'
);
// Initialize IPFS service
const ipfsService = new IPFSService({
swarmKeyFile: process.env.SWARM_KEY_FILE,
bootstrap: process.env.BOOTSTRAP_PEER ? [`/ip4/${process.env.BOOTSTRAP_PEER}/tcp/4001`] : [],
ports: {
swarm: parseInt(process.env.IPFS_PORT) || 4001
}
swarm: parseInt(process.env.IPFS_PORT) || 4001,
},
});
await ipfsService.init();
@ -637,13 +645,13 @@ class BlogAPIServer {
automaticPinning: true,
pubsub: true,
queryCache: true,
relationshipCache: true
relationshipCache: true,
},
performance: {
queryTimeout: 10000,
maxConcurrentOperations: 20,
batchSize: 50
}
batchSize: 50,
},
});
await this.framework.initialize(frameworkOrbitDB, frameworkIPFS);
@ -664,4 +672,4 @@ process.on('SIGINT', () => {
// Start the server
const server = new BlogAPIServer();
server.start();
server.start();

View File

@ -1,3 +1,4 @@
import 'reflect-metadata';
import { BaseModel } from '../../../../src/framework/models/BaseModel';
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
export interface CreateUserRequest {
username: string;