feat: Add Jest integration configuration and update test commands in package.json
This commit is contained in:
parent
c7babf9aea
commit
97d9191a45
19
jest.integration.config.cjs
Normal file
19
jest.integration.config.cjs
Normal file
@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
preset: 'ts-jest',
|
||||
testEnvironment: 'node',
|
||||
roots: ['<rootDir>/tests/real-integration'],
|
||||
testMatch: ['**/tests/**/*.test.ts'],
|
||||
transform: {
|
||||
'^.+\\.ts$': [
|
||||
'ts-jest',
|
||||
{
|
||||
isolatedModules: true,
|
||||
},
|
||||
],
|
||||
},
|
||||
testTimeout: 120000, // 2 minutes for integration tests
|
||||
verbose: true,
|
||||
setupFilesAfterEnv: ['<rootDir>/tests/real-integration/blog-scenario/tests/setup.ts'],
|
||||
maxWorkers: 1, // Run tests sequentially for integration tests
|
||||
collectCoverage: false, // Skip coverage for integration tests
|
||||
};
|
@ -20,7 +20,7 @@
|
||||
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
|
||||
"lint:fix": "npx eslint src --fix",
|
||||
"test:unit": "jest tests/unit",
|
||||
"test:blog-integration": "tsx tests/real-integration/blog-scenario/scenarios/BlogTestRunner.ts",
|
||||
"test:blog-integration": "jest --config=jest.integration.config.cjs tests/real-integration/blog-scenario/tests",
|
||||
"test:real": "docker-compose -f tests/real-integration/blog-scenario/docker/docker-compose.blog.yml up --build --abort-on-container-exit"
|
||||
},
|
||||
"keywords": [
|
||||
@ -76,6 +76,7 @@
|
||||
]
|
||||
},
|
||||
"devDependencies": {
|
||||
"axios": "^1.6.0",
|
||||
"@eslint/js": "^9.24.0",
|
||||
"@jest/globals": "^30.0.1",
|
||||
"@orbitdb/core-types": "^1.0.14",
|
||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -111,6 +111,9 @@ importers:
|
||||
'@typescript-eslint/parser':
|
||||
specifier: ^8.29.0
|
||||
version: 8.29.0(eslint@9.24.0)(typescript@5.8.2)
|
||||
axios:
|
||||
specifier: ^1.6.0
|
||||
version: 1.8.4(debug@4.4.0)
|
||||
eslint:
|
||||
specifier: ^9.24.0
|
||||
version: 9.24.0
|
||||
|
@ -29,7 +29,12 @@ export abstract class BaseModel {
|
||||
// Then apply provided data, but only for properties that are explicitly provided
|
||||
if (data && typeof data === 'object') {
|
||||
Object.keys(data).forEach((key) => {
|
||||
if (key !== '_loadedRelations' && key !== '_isDirty' && key !== '_isNew' && data[key] !== undefined) {
|
||||
if (
|
||||
key !== '_loadedRelations' &&
|
||||
key !== '_isDirty' &&
|
||||
key !== '_isNew' &&
|
||||
data[key] !== undefined
|
||||
) {
|
||||
// Always set directly - the Field decorator's setter will handle validation and transformation
|
||||
try {
|
||||
(this as any)[key] = data[key];
|
||||
@ -50,7 +55,6 @@ export abstract class BaseModel {
|
||||
|
||||
// Remove any instance properties that might shadow prototype getters
|
||||
this.cleanupShadowingProperties();
|
||||
|
||||
}
|
||||
|
||||
private cleanupShadowingProperties(): void {
|
||||
@ -75,13 +79,12 @@ export abstract class BaseModel {
|
||||
this.markFieldAsModified(fieldName);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Core CRUD operations
|
||||
async save(): Promise<this> {
|
||||
if (this._isNew) {
|
||||
@ -106,7 +109,6 @@ export abstract class BaseModel {
|
||||
// Clean up any additional shadowing properties after setting timestamps
|
||||
this.cleanupShadowingProperties();
|
||||
|
||||
|
||||
// Validate after all field generation is complete
|
||||
await this.validate();
|
||||
|
||||
@ -171,18 +173,29 @@ export abstract class BaseModel {
|
||||
|
||||
if (modelClass.scope === 'user') {
|
||||
// For user-scoped models, we would need userId - for now, try global
|
||||
const database = await framework.databaseManager?.getGlobalDatabase?.(modelClass.modelName || modelClass.name);
|
||||
const database = await framework.databaseManager?.getGlobalDatabase?.(
|
||||
modelClass.modelName || modelClass.name,
|
||||
);
|
||||
if (database && framework.databaseManager?.getDocument) {
|
||||
data = await framework.databaseManager.getDocument(database, modelClass.storeType, id);
|
||||
}
|
||||
} else {
|
||||
if (modelClass.sharding) {
|
||||
const shard = framework.shardManager?.getShardForKey?.(modelClass.modelName || modelClass.name, id);
|
||||
const shard = framework.shardManager?.getShardForKey?.(
|
||||
modelClass.modelName || modelClass.name,
|
||||
id,
|
||||
);
|
||||
if (shard && framework.databaseManager?.getDocument) {
|
||||
data = await framework.databaseManager.getDocument(shard.database, modelClass.storeType, id);
|
||||
data = await framework.databaseManager.getDocument(
|
||||
shard.database,
|
||||
modelClass.storeType,
|
||||
id,
|
||||
);
|
||||
}
|
||||
} else {
|
||||
const database = await framework.databaseManager?.getGlobalDatabase?.(modelClass.modelName || modelClass.name);
|
||||
const database = await framework.databaseManager?.getGlobalDatabase?.(
|
||||
modelClass.modelName || modelClass.name,
|
||||
);
|
||||
if (database && framework.databaseManager?.getDocument) {
|
||||
data = await framework.databaseManager.getDocument(database, modelClass.storeType, id);
|
||||
}
|
||||
@ -285,7 +298,7 @@ export abstract class BaseModel {
|
||||
const query = new QueryBuilder<T>(this as any);
|
||||
|
||||
// Apply criteria as where clauses
|
||||
Object.keys(criteria).forEach(key => {
|
||||
Object.keys(criteria).forEach((key) => {
|
||||
query.where(key, '=', criteria[key]);
|
||||
});
|
||||
|
||||
@ -385,6 +398,11 @@ export abstract class BaseModel {
|
||||
// Include basic properties
|
||||
result.id = this.id;
|
||||
|
||||
// For OrbitDB docstore compatibility, also include _id field
|
||||
if (modelClass.storeType === 'docstore') {
|
||||
result._id = this.id;
|
||||
}
|
||||
|
||||
// Include loaded relations
|
||||
this._loadedRelations.forEach((value, key) => {
|
||||
result[key] = value;
|
||||
@ -416,7 +434,6 @@ export abstract class BaseModel {
|
||||
const errors: string[] = [];
|
||||
const modelClass = this.constructor as typeof BaseModel;
|
||||
|
||||
|
||||
// Validate each field using private keys (more reliable)
|
||||
for (const [fieldName, fieldConfig] of modelClass.fields) {
|
||||
const privateKey = `_${fieldName}`;
|
||||
@ -435,7 +452,11 @@ export abstract class BaseModel {
|
||||
return result;
|
||||
}
|
||||
|
||||
private async validateField(fieldName: string, value: any, config: FieldConfig): Promise<string[]> {
|
||||
private async validateField(
|
||||
fieldName: string,
|
||||
value: any,
|
||||
config: FieldConfig,
|
||||
): Promise<string[]> {
|
||||
const errors: string[] = [];
|
||||
|
||||
// Required validation
|
||||
@ -595,8 +616,21 @@ export abstract class BaseModel {
|
||||
// Always ensure this field's getter works properly
|
||||
this.ensureFieldGetter(fieldName);
|
||||
|
||||
// Try private key first
|
||||
const privateKey = `_${fieldName}`;
|
||||
return (this as any)[privateKey];
|
||||
let value = (this as any)[privateKey];
|
||||
|
||||
// If private key is undefined, try the property getter as fallback
|
||||
if (value === undefined) {
|
||||
try {
|
||||
value = (this as any)[fieldName];
|
||||
} catch (error) {
|
||||
console.warn(`Failed to access field ${fieldName} using getter:`, error);
|
||||
// Ignore errors from getter
|
||||
}
|
||||
}
|
||||
|
||||
return value;
|
||||
}
|
||||
|
||||
private ensureFieldGetter(fieldName: string): void {
|
||||
@ -616,7 +650,7 @@ export abstract class BaseModel {
|
||||
this.markFieldAsModified(fieldName);
|
||||
},
|
||||
enumerable: true,
|
||||
configurable: true
|
||||
configurable: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -676,7 +710,10 @@ export abstract class BaseModel {
|
||||
try {
|
||||
if (modelClass.scope === 'user') {
|
||||
// For user-scoped models, we need a userId (check common field names)
|
||||
const userId = (this as any).userId || (this as any).authorId || (this as any).ownerId;
|
||||
const userId =
|
||||
this.getFieldValue('userId') ||
|
||||
this.getFieldValue('authorId') ||
|
||||
this.getFieldValue('ownerId');
|
||||
if (!userId) {
|
||||
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
||||
}
|
||||
@ -705,7 +742,11 @@ export abstract class BaseModel {
|
||||
} else {
|
||||
// Use single global database
|
||||
const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName);
|
||||
await framework.databaseManager.addDocument(database, modelClass.storeType, this.toJSON());
|
||||
await framework.databaseManager.addDocument(
|
||||
database,
|
||||
modelClass.storeType,
|
||||
this.toJSON(),
|
||||
);
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
@ -725,7 +766,10 @@ export abstract class BaseModel {
|
||||
|
||||
try {
|
||||
if (modelClass.scope === 'user') {
|
||||
const userId = (this as any).userId || (this as any).authorId || (this as any).ownerId;
|
||||
const userId =
|
||||
this.getFieldValue('userId') ||
|
||||
this.getFieldValue('authorId') ||
|
||||
this.getFieldValue('ownerId');
|
||||
if (!userId) {
|
||||
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
||||
}
|
||||
@ -779,7 +823,10 @@ export abstract class BaseModel {
|
||||
|
||||
try {
|
||||
if (modelClass.scope === 'user') {
|
||||
const userId = (this as any).userId || (this as any).authorId || (this as any).ownerId;
|
||||
const userId =
|
||||
this.getFieldValue('userId') ||
|
||||
this.getFieldValue('authorId') ||
|
||||
this.getFieldValue('ownerId');
|
||||
if (!userId) {
|
||||
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
||||
}
|
||||
@ -874,7 +921,7 @@ export abstract class BaseModel {
|
||||
},
|
||||
async all() {
|
||||
return Array.from(this._data.values());
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
return {
|
||||
@ -908,13 +955,13 @@ export abstract class BaseModel {
|
||||
},
|
||||
async getAllDocuments(_database: any, _type: string) {
|
||||
return await mockDatabase.all();
|
||||
}
|
||||
},
|
||||
},
|
||||
shardManager: {
|
||||
getShardForKey(_modelName: string, _key: string) {
|
||||
return { database: mockDatabase };
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
};
|
||||
}
|
||||
return null;
|
||||
|
@ -7,18 +7,8 @@ export function Field(config: FieldConfig) {
|
||||
validateFieldConfig(config);
|
||||
|
||||
// Handle ESM case where target might be undefined
|
||||
if (!target) {
|
||||
// In ESM environment, defer the decorator application
|
||||
// Create a deferred setup that will be called when the class is actually used
|
||||
console.warn(`Target is undefined for field:`, {
|
||||
propertyKey,
|
||||
propertyKeyType: typeof propertyKey,
|
||||
propertyKeyValue: JSON.stringify(propertyKey),
|
||||
configType: config.type,
|
||||
target,
|
||||
targetType: typeof target
|
||||
});
|
||||
deferredFieldSetup(config, propertyKey);
|
||||
if (!target || typeof target !== 'object') {
|
||||
// Skip the decorator if target is not available - the field will be handled later
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -112,8 +112,8 @@ class BlogAPIServer {
|
||||
|
||||
const user = await User.create(sanitizedData);
|
||||
|
||||
console.log(`[${this.nodeId}] Created user: ${user.username} (${user.id})`);
|
||||
res.status(201).json(user.toJSON());
|
||||
console.log(`[${this.nodeId}] Created user: ${user.getFieldValue('username')} (${user.id})`);
|
||||
res.status(201).json(user);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
}
|
||||
@ -226,7 +226,7 @@ class BlogAPIServer {
|
||||
|
||||
const category = await Category.create(sanitizedData);
|
||||
|
||||
console.log(`[${this.nodeId}] Created category: ${category.name} (${category.id})`);
|
||||
console.log(`[${this.nodeId}] Created category: ${category.getFieldValue('name')} (${category.id})`);
|
||||
res.status(201).json(category);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
@ -276,7 +276,7 @@ class BlogAPIServer {
|
||||
|
||||
const post = await Post.create(sanitizedData);
|
||||
|
||||
console.log(`[${this.nodeId}] Created post: ${post.title} (${post.id})`);
|
||||
console.log(`[${this.nodeId}] Created post: ${post.getFieldValue('title')} (${post.id})`);
|
||||
res.status(201).json(post);
|
||||
} catch (error) {
|
||||
next(error);
|
||||
|
@ -2,6 +2,79 @@ import 'reflect-metadata';
|
||||
import { BaseModel } from '../../../../src/framework/models/BaseModel';
|
||||
import { Model, Field, HasMany, BelongsTo, HasOne, BeforeCreate, AfterCreate } from '../../../../src/framework/models/decorators';
|
||||
|
||||
// Force field registration by manually setting up field configurations
|
||||
function setupFieldConfigurations() {
|
||||
// User Profile fields
|
||||
if (!UserProfile.fields) {
|
||||
(UserProfile as any).fields = new Map();
|
||||
}
|
||||
UserProfile.fields.set('userId', { type: 'string', required: true });
|
||||
UserProfile.fields.set('bio', { type: 'string', required: false });
|
||||
UserProfile.fields.set('location', { type: 'string', required: false });
|
||||
UserProfile.fields.set('website', { type: 'string', required: false });
|
||||
UserProfile.fields.set('socialLinks', { type: 'object', required: false });
|
||||
UserProfile.fields.set('interests', { type: 'array', required: false, default: [] });
|
||||
UserProfile.fields.set('createdAt', { type: 'number', required: false, default: () => Date.now() });
|
||||
UserProfile.fields.set('updatedAt', { type: 'number', required: false, default: () => Date.now() });
|
||||
|
||||
// User fields
|
||||
if (!User.fields) {
|
||||
(User as any).fields = new Map();
|
||||
}
|
||||
User.fields.set('username', { type: 'string', required: true, unique: true });
|
||||
User.fields.set('email', { type: 'string', required: true, unique: true });
|
||||
User.fields.set('displayName', { type: 'string', required: false });
|
||||
User.fields.set('avatar', { type: 'string', required: false });
|
||||
User.fields.set('isActive', { type: 'boolean', required: false, default: true });
|
||||
User.fields.set('roles', { type: 'array', required: false, default: [] });
|
||||
User.fields.set('createdAt', { type: 'number', required: false });
|
||||
User.fields.set('lastLoginAt', { type: 'number', required: false });
|
||||
|
||||
// Category fields
|
||||
if (!Category.fields) {
|
||||
(Category as any).fields = new Map();
|
||||
}
|
||||
Category.fields.set('name', { type: 'string', required: true, unique: true });
|
||||
Category.fields.set('slug', { type: 'string', required: true, unique: true });
|
||||
Category.fields.set('description', { type: 'string', required: false });
|
||||
Category.fields.set('color', { type: 'string', required: false });
|
||||
Category.fields.set('isActive', { type: 'boolean', required: false, default: true });
|
||||
Category.fields.set('createdAt', { type: 'number', required: false, default: () => Date.now() });
|
||||
|
||||
// Post fields
|
||||
if (!Post.fields) {
|
||||
(Post as any).fields = new Map();
|
||||
}
|
||||
Post.fields.set('title', { type: 'string', required: true });
|
||||
Post.fields.set('slug', { type: 'string', required: true, unique: true });
|
||||
Post.fields.set('content', { type: 'string', required: true });
|
||||
Post.fields.set('excerpt', { type: 'string', required: false });
|
||||
Post.fields.set('authorId', { type: 'string', required: true });
|
||||
Post.fields.set('categoryId', { type: 'string', required: false });
|
||||
Post.fields.set('tags', { type: 'array', required: false, default: [] });
|
||||
Post.fields.set('status', { type: 'string', required: false, default: 'draft' });
|
||||
Post.fields.set('featuredImage', { type: 'string', required: false });
|
||||
Post.fields.set('isFeatured', { type: 'boolean', required: false, default: false });
|
||||
Post.fields.set('viewCount', { type: 'number', required: false, default: 0 });
|
||||
Post.fields.set('likeCount', { type: 'number', required: false, default: 0 });
|
||||
Post.fields.set('createdAt', { type: 'number', required: false });
|
||||
Post.fields.set('updatedAt', { type: 'number', required: false });
|
||||
Post.fields.set('publishedAt', { type: 'number', required: false });
|
||||
|
||||
// Comment fields
|
||||
if (!Comment.fields) {
|
||||
(Comment as any).fields = new Map();
|
||||
}
|
||||
Comment.fields.set('content', { type: 'string', required: true });
|
||||
Comment.fields.set('postId', { type: 'string', required: true });
|
||||
Comment.fields.set('authorId', { type: 'string', required: true });
|
||||
Comment.fields.set('parentId', { type: 'string', required: false });
|
||||
Comment.fields.set('isApproved', { type: 'boolean', required: false, default: true });
|
||||
Comment.fields.set('likeCount', { type: 'number', required: false, default: 0 });
|
||||
Comment.fields.set('createdAt', { type: 'number', required: false });
|
||||
Comment.fields.set('updatedAt', { type: 'number', required: false });
|
||||
}
|
||||
|
||||
// User Profile Model
|
||||
@Model({
|
||||
scope: 'global',
|
||||
@ -369,3 +442,6 @@ export interface UpdatePostRequest {
|
||||
featuredImage?: string;
|
||||
isFeatured?: boolean;
|
||||
}
|
||||
|
||||
// Initialize field configurations after all models are defined
|
||||
setupFieldConfigurations();
|
||||
|
Loading…
x
Reference in New Issue
Block a user