diff --git a/package.json b/package.json index 68c06a4..a14eacf 100644 --- a/package.json +++ b/package.json @@ -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", diff --git a/src/framework/models/decorators/Field.ts b/src/framework/models/decorators/Field.ts index f684613..183d926 100644 --- a/src/framework/models/decorators/Field.ts +++ b/src/framework/models/decorators/Field.ts @@ -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; } diff --git a/src/framework/models/decorators/hooks.ts b/src/framework/models/decorators/hooks.ts index f3ec7d1..e6bb498 100644 --- a/src/framework/models/decorators/hooks.ts +++ b/src/framework/models/decorators/hooks.ts @@ -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 = {}; - + 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 ${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 { diff --git a/tests/real-integration/blog-scenario/docker/Dockerfile.blog-api b/tests/real-integration/blog-scenario/docker/Dockerfile.blog-api index 60045b1..b7f93d0 100644 --- a/tests/real-integration/blog-scenario/docker/Dockerfile.blog-api +++ b/tests/real-integration/blog-scenario/docker/Dockerfile.blog-api @@ -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"] diff --git a/tests/real-integration/blog-scenario/docker/blog-api-server.ts b/tests/real-integration/blog-scenario/docker/blog-api-server.ts index c39e4cb..37e653a 100644 --- a/tests/real-integration/blog-scenario/docker/blog-api-server.ts +++ b/tests/real-integration/blog-scenario/docker/blog-api-server.ts @@ -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 { // 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(); \ No newline at end of file +server.start(); diff --git a/tests/real-integration/blog-scenario/models/BlogModels.ts b/tests/real-integration/blog-scenario/models/BlogModels.ts index 8fbdc7c..4ef1e07 100644 --- a/tests/real-integration/blog-scenario/models/BlogModels.ts +++ b/tests/real-integration/blog-scenario/models/BlogModels.ts @@ -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;