feat: Improve database query handling and slug generation in BlogAPIServer
This commit is contained in:
parent
0ebdb7cbbf
commit
6e1cc2cbf0
@ -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);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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) {
|
||||||
|
@ -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))}`);
|
||||||
}
|
}
|
||||||
|
@ -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',
|
||||||
|
Reference in New Issue
Block a user