feat: Improve database query handling and slug generation in BlogAPIServer

This commit is contained in:
anonpenguin 2025-07-10 05:28:45 +03:00
parent 0ebdb7cbbf
commit 6e1cc2cbf0
4 changed files with 136 additions and 39 deletions

View File

@ -20,14 +20,11 @@ export abstract class BaseModel {
static hooks: Map<string, Function[]> = new Map(); static hooks: Map<string, Function[]> = new Map();
constructor(data: any = {}) { constructor(data: any = {}) {
console.log(`[DEBUG] Constructing ${this.constructor.name} with data:`, data);
// Generate ID first // Generate ID first
this.id = this.generateId(); this.id = this.generateId();
// Apply field defaults first // Apply field defaults first
this.applyFieldDefaults(); this.applyFieldDefaults();
console.log(`[DEBUG] After applying defaults, instance properties:`, Object.getOwnPropertyNames(this));
// Then apply provided data, but only for properties that are explicitly provided // Then apply provided data, but only for properties that are explicitly provided
if (data && typeof data === 'object') { if (data && typeof data === 'object') {
@ -44,12 +41,10 @@ export abstract class BaseModel {
// For model fields, store in private field // For model fields, store in private field
const privateKey = `_${key}`; const privateKey = `_${key}`;
(this as any)[privateKey] = data[key]; (this as any)[privateKey] = data[key];
console.log(`[DEBUG] Set private field ${privateKey} = ${data[key]}`);
} else { } else {
// For non-field properties, set directly // For non-field properties, set directly
try { try {
(this as any)[key] = data[key]; (this as any)[key] = data[key];
console.log(`[DEBUG] Set property ${key} = ${data[key]}`);
} catch (error) { } catch (error) {
console.error(`Error setting property ${key}:`, error); console.error(`Error setting property ${key}:`, error);
} }
@ -65,7 +60,6 @@ export abstract class BaseModel {
// Remove any instance properties that might shadow prototype getters // Remove any instance properties that might shadow prototype getters
this.cleanupShadowingProperties(); this.cleanupShadowingProperties();
console.log(`[DEBUG] After cleanup, instance properties:`, Object.getOwnPropertyNames(this));
} }
private cleanupShadowingProperties(): void { private cleanupShadowingProperties(): void {
@ -460,10 +454,6 @@ export abstract class BaseModel {
const errors: string[] = []; const errors: string[] = [];
const modelClass = this.constructor as typeof BaseModel; const modelClass = this.constructor as typeof BaseModel;
console.log(`[DEBUG] Validating model ${modelClass.name}`);
console.log(`[DEBUG] Available fields:`, Array.from(modelClass.fields.keys()));
console.log(`[DEBUG] Instance properties:`, Object.getOwnPropertyNames(this));
// Validate each field using getter values (more reliable) // Validate each field using getter values (more reliable)
for (const [fieldName, fieldConfig] of modelClass.fields) { for (const [fieldName, fieldConfig] of modelClass.fields) {
const privateKey = `_${fieldName}`; const privateKey = `_${fieldName}`;
@ -473,8 +463,6 @@ export abstract class BaseModel {
// Use the property value (getter) if available, otherwise use private value // Use the property value (getter) if available, otherwise use private value
const value = propertyValue !== undefined ? propertyValue : privateValue; const value = propertyValue !== undefined ? propertyValue : privateValue;
console.log(`[DEBUG] Field ${fieldName}: privateKey=${privateKey}, privateValue=${privateValue}, propertyValue=${propertyValue}, finalValue=${value}, config=`, fieldConfig);
const fieldErrors = await this.validateField(fieldName, value, fieldConfig); const fieldErrors = await this.validateField(fieldName, value, fieldConfig);
errors.push(...fieldErrors); errors.push(...fieldErrors);
} }
@ -482,7 +470,6 @@ export abstract class BaseModel {
const result = { valid: errors.length === 0, errors }; const result = { valid: errors.length === 0, errors };
if (!result.valid) { if (!result.valid) {
console.log(`[DEBUG] Validation failed:`, errors);
throw new ValidationError(errors); throw new ValidationError(errors);
} }
@ -931,9 +918,7 @@ export abstract class BaseModel {
} }
static query<T extends BaseModel>(this: typeof BaseModel & (new (data?: any) => T)): any { static query<T extends BaseModel>(this: typeof BaseModel & (new (data?: any) => T)): any {
// Import dynamically to avoid circular dependency // Use the imported QueryBuilder directly
const QueryBuilderModule = require('../query/QueryBuilder');
const QueryBuilder = QueryBuilderModule.QueryBuilder;
return new QueryBuilder(this); return new QueryBuilder(this);
} }

View File

@ -113,8 +113,6 @@ export class QueryExecutor<T extends BaseModel> {
private async executeUserSpecificQuery(userFilter: QueryCondition): Promise<T[]> { private async executeUserSpecificQuery(userFilter: QueryCondition): Promise<T[]> {
const userIds = userFilter.operator === 'userIn' ? userFilter.value : [userFilter.value]; const userIds = userFilter.operator === 'userIn' ? userFilter.value : [userFilter.value];
console.log(`👤 Querying user databases for ${userIds.length} users`);
const results: T[] = []; const results: T[] = [];
// Query each user's database in parallel // Query each user's database in parallel
@ -127,7 +125,7 @@ export class QueryExecutor<T extends BaseModel> {
return await this.queryDatabase(userDB, this.model.storeType); return await this.queryDatabase(userDB, this.model.storeType);
} catch (error) { } catch (error) {
console.warn(`Failed to query user ${userId} database:`, error); // Silently handle user database query failures
return []; return [];
} }
}); });
@ -143,8 +141,6 @@ export class QueryExecutor<T extends BaseModel> {
} }
private async executeGlobalIndexQuery(): Promise<T[]> { private async executeGlobalIndexQuery(): Promise<T[]> {
console.log(`📇 Querying global index for ${this.model.name}`);
// Query global index for user-scoped models // Query global index for user-scoped models
const globalIndexName = `${this.model.modelName}GlobalIndex`; const globalIndexName = `${this.model.modelName}GlobalIndex`;
const indexShards = this.framework.shardManager.getAllShards(globalIndexName); const indexShards = this.framework.shardManager.getAllShards(globalIndexName);
@ -175,10 +171,45 @@ export class QueryExecutor<T extends BaseModel> {
// It's expensive but ensures completeness // It's expensive but ensures completeness
console.warn(`⚠️ Executing expensive all-users query for ${this.model.name}`); console.warn(`⚠️ Executing expensive all-users query for ${this.model.name}`);
// This would require getting all user IDs from the directory try {
// For now, return empty array and log warning // Get all entity IDs from the directory shards
console.warn('All-users query not implemented - please ensure global indexes are set up'); const entityIds = await this.getAllEntityIdsFromDirectory();
return [];
if (entityIds.length === 0) {
console.warn('No entities found in directory shards');
return [];
}
const results: T[] = [];
// Query each entity's database in parallel (in batches to avoid overwhelming the system)
const batchSize = 10;
for (let i = 0; i < entityIds.length; i += batchSize) {
const batch = entityIds.slice(i, i + batchSize);
const batchPromises = batch.map(async (entityId: string) => {
try {
const entityDB = await this.framework.databaseManager.getUserDatabase(
entityId,
this.model.modelName,
);
return await this.queryDatabase(entityDB, this.model.storeType);
} catch (error) {
// Silently handle entity database query failures
return [];
}
});
const batchResults = await Promise.all(batchPromises);
for (const entityResult of batchResults) {
results.push(...entityResult);
}
}
return this.postProcessResults(results);
} catch (error) {
console.error('Error executing all-entities query:', error);
return [];
}
} }
private async executeGlobalQuery(): Promise<T[]> { private async executeGlobalQuery(): Promise<T[]> {
@ -192,8 +223,6 @@ export class QueryExecutor<T extends BaseModel> {
} }
private async executeShardedQuery(): Promise<T[]> { private async executeShardedQuery(): Promise<T[]> {
console.log(`🔀 Executing sharded query for ${this.model.name}`);
const conditions = this.query.getConditions(); const conditions = this.query.getConditions();
const shardingConfig = this.model.sharding!; const shardingConfig = this.model.sharding!;
@ -616,6 +645,68 @@ export class QueryExecutor<T extends BaseModel> {
}; };
} }
private async getAllEntityIdsFromDirectory(): Promise<string[]> {
const maxRetries = 3;
const baseDelay = 100; // ms
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const directoryShards = await this.framework.databaseManager.getGlobalDirectoryShards();
const entityIds: string[] = [];
// Query all directory shards in parallel
const shardPromises = directoryShards.map(async (shard: any, index: number) => {
try {
// For keyvalue stores, we need to get the keys (entity IDs), not values
const shardData = shard.all();
const keys = Object.keys(shardData);
return keys;
} catch (error) {
console.warn(`Failed to read directory shard ${index}:`, error);
return [];
}
});
const shardResults = await Promise.all(shardPromises);
// Flatten all entity IDs from all shards
for (const shardEntityIds of shardResults) {
entityIds.push(...shardEntityIds);
}
// If we found entities, return them
if (entityIds.length > 0) {
console.log(`📂 Found ${entityIds.length} entities in directory shards`);
return entityIds;
}
// If this is our last attempt, return empty array
if (attempt === maxRetries) {
console.warn('📂 No entities found in directory shards after all attempts');
return [];
}
// Wait before retry with exponential backoff
const delay = baseDelay * Math.pow(2, attempt);
console.log(`📂 No entities found, retrying in ${delay}ms (attempt ${attempt + 1}/${maxRetries + 1})`);
await new Promise(resolve => setTimeout(resolve, delay));
} catch (error) {
console.error(`Error getting entity IDs from directory (attempt ${attempt + 1}):`, error);
if (attempt === maxRetries) {
return [];
}
// Wait before retry
const delay = baseDelay * Math.pow(2, attempt);
await new Promise(resolve => setTimeout(resolve, delay));
}
}
return [];
}
private getFrameworkInstance(): any { private getFrameworkInstance(): any {
const framework = (globalThis as any).__debrosFramework; const framework = (globalThis as any).__debrosFramework;
if (!framework) { if (!framework) {

View File

@ -36,11 +36,6 @@ export class FrameworkOrbitDBService {
} }
async openDatabase(name: string, type: StoreType): Promise<any> { async openDatabase(name: string, type: StoreType): Promise<any> {
console.log('FrameworkOrbitDBService.openDatabase called with:', { name, type });
console.log('this.orbitDBService:', this.orbitDBService);
console.log('typeof this.orbitDBService.openDB:', typeof this.orbitDBService.openDB);
console.log('this.orbitDBService methods:', Object.getOwnPropertyNames(Object.getPrototypeOf(this.orbitDBService)));
if (typeof this.orbitDBService.openDB !== 'function') { if (typeof this.orbitDBService.openDB !== 'function') {
throw new Error(`openDB is not a function. Service type: ${typeof this.orbitDBService}, methods: ${Object.getOwnPropertyNames(Object.getPrototypeOf(this.orbitDBService))}`); throw new Error(`openDB is not a function. Service type: ${typeof this.orbitDBService}, methods: ${Object.getOwnPropertyNames(Object.getPrototypeOf(this.orbitDBService))}`);
} }

View File

@ -300,6 +300,18 @@ class BlogAPIServer {
this.app.post('/api/posts', async (req, res, next) => { this.app.post('/api/posts', async (req, res, next) => {
try { try {
const sanitizedData = BlogValidation.sanitizePostInput(req.body); const sanitizedData = BlogValidation.sanitizePostInput(req.body);
// Generate slug if not provided
if (!sanitizedData.slug && sanitizedData.title) {
sanitizedData.slug = sanitizedData.title
.toLowerCase()
.replace(/\s+/g, '-')
.replace(/[^a-z0-9-]/g, '')
.replace(/--+/g, '-')
.replace(/^-|-$/g, '');
console.log(`[${this.nodeId}] Generated slug: ${sanitizedData.slug}`);
}
BlogValidation.validatePost(sanitizedData); BlogValidation.validatePost(sanitizedData);
const post = await Post.create(sanitizedData); const post = await Post.create(sanitizedData);
@ -380,7 +392,9 @@ class BlogAPIServer {
// Update post // Update post
this.app.put('/api/posts/:id', async (req, res, next) => { this.app.put('/api/posts/:id', async (req, res, next) => {
try { try {
const post = await Post.findById(req.params.id); const post = await Post.query()
.where('id', req.params.id)
.first();
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
@ -404,7 +418,9 @@ class BlogAPIServer {
// Publish post // Publish post
this.app.post('/api/posts/:id/publish', async (req, res, next) => { this.app.post('/api/posts/:id/publish', async (req, res, next) => {
try { try {
const post = await Post.findById(req.params.id); const post = await Post.query()
.where('id', req.params.id)
.first();
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
@ -423,7 +439,9 @@ class BlogAPIServer {
// Unpublish post // Unpublish post
this.app.post('/api/posts/:id/unpublish', async (req, res, next) => { this.app.post('/api/posts/:id/unpublish', async (req, res, next) => {
try { try {
const post = await Post.findById(req.params.id); const post = await Post.query()
.where('id', req.params.id)
.first();
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
@ -442,7 +460,9 @@ class BlogAPIServer {
// Like post // Like post
this.app.post('/api/posts/:id/like', async (req, res, next) => { this.app.post('/api/posts/:id/like', async (req, res, next) => {
try { try {
const post = await Post.findById(req.params.id); const post = await Post.query()
.where('id', req.params.id)
.first();
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
@ -460,7 +480,9 @@ class BlogAPIServer {
// View post (increment view count) // View post (increment view count)
this.app.post('/api/posts/:id/view', async (req, res, next) => { this.app.post('/api/posts/:id/view', async (req, res, next) => {
try { try {
const post = await Post.findById(req.params.id); const post = await Post.query()
.where('id', req.params.id)
.first();
if (!post) { if (!post) {
return res.status(404).json({ return res.status(404).json({
error: 'Post not found', error: 'Post not found',
@ -516,7 +538,9 @@ class BlogAPIServer {
// Approve comment // Approve comment
this.app.post('/api/comments/:id/approve', async (req, res, next) => { this.app.post('/api/comments/:id/approve', async (req, res, next) => {
try { try {
const comment = await Comment.findById(req.params.id); const comment = await Comment.query()
.where('id', req.params.id)
.first();
if (!comment) { if (!comment) {
return res.status(404).json({ return res.status(404).json({
error: 'Comment not found', error: 'Comment not found',
@ -535,7 +559,9 @@ class BlogAPIServer {
// Like comment // Like comment
this.app.post('/api/comments/:id/like', async (req, res, next) => { this.app.post('/api/comments/:id/like', async (req, res, next) => {
try { try {
const comment = await Comment.findById(req.params.id); const comment = await Comment.query()
.where('id', req.params.id)
.first();
if (!comment) { if (!comment) {
return res.status(404).json({ return res.status(404).json({
error: 'Comment not found', error: 'Comment not found',