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}\"",
|
"format": "prettier --write \"**/*.{ts,js,json,md}\"",
|
||||||
"lint:fix": "npx eslint src --fix",
|
"lint:fix": "npx eslint src --fix",
|
||||||
"test:unit": "jest tests/unit",
|
"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"
|
"test:real": "docker-compose -f tests/real-integration/blog-scenario/docker/docker-compose.blog.yml up --build --abort-on-container-exit"
|
||||||
},
|
},
|
||||||
"keywords": [
|
"keywords": [
|
||||||
@ -76,6 +76,7 @@
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
|
"axios": "^1.6.0",
|
||||||
"@eslint/js": "^9.24.0",
|
"@eslint/js": "^9.24.0",
|
||||||
"@jest/globals": "^30.0.1",
|
"@jest/globals": "^30.0.1",
|
||||||
"@orbitdb/core-types": "^1.0.14",
|
"@orbitdb/core-types": "^1.0.14",
|
||||||
|
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@ -111,6 +111,9 @@ importers:
|
|||||||
'@typescript-eslint/parser':
|
'@typescript-eslint/parser':
|
||||||
specifier: ^8.29.0
|
specifier: ^8.29.0
|
||||||
version: 8.29.0(eslint@9.24.0)(typescript@5.8.2)
|
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:
|
eslint:
|
||||||
specifier: ^9.24.0
|
specifier: ^9.24.0
|
||||||
version: 9.24.0
|
version: 9.24.0
|
||||||
|
@ -22,14 +22,19 @@ export abstract class BaseModel {
|
|||||||
constructor(data: any = {}) {
|
constructor(data: any = {}) {
|
||||||
// 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();
|
||||||
|
|
||||||
// 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') {
|
||||||
Object.keys(data).forEach((key) => {
|
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
|
// Always set directly - the Field decorator's setter will handle validation and transformation
|
||||||
try {
|
try {
|
||||||
(this as any)[key] = data[key];
|
(this as any)[key] = data[key];
|
||||||
@ -41,28 +46,27 @@ export abstract class BaseModel {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// Mark as existing if it has an ID in the data
|
// Mark as existing if it has an ID in the data
|
||||||
if (data.id) {
|
if (data.id) {
|
||||||
this._isNew = false;
|
this._isNew = false;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove any instance properties that might shadow prototype getters
|
// Remove any instance properties that might shadow prototype getters
|
||||||
this.cleanupShadowingProperties();
|
this.cleanupShadowingProperties();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private cleanupShadowingProperties(): void {
|
private cleanupShadowingProperties(): void {
|
||||||
const modelClass = this.constructor as typeof BaseModel;
|
const modelClass = this.constructor as typeof BaseModel;
|
||||||
|
|
||||||
// For each field, ensure no instance properties are shadowing prototype getters
|
// For each field, ensure no instance properties are shadowing prototype getters
|
||||||
for (const [fieldName] of modelClass.fields) {
|
for (const [fieldName] of modelClass.fields) {
|
||||||
// If there's an instance property, remove it and create a working getter
|
// If there's an instance property, remove it and create a working getter
|
||||||
if (this.hasOwnProperty(fieldName)) {
|
if (this.hasOwnProperty(fieldName)) {
|
||||||
const _oldValue = (this as any)[fieldName];
|
const _oldValue = (this as any)[fieldName];
|
||||||
delete (this as any)[fieldName];
|
delete (this as any)[fieldName];
|
||||||
|
|
||||||
// Define a working getter directly on the instance
|
// Define a working getter directly on the instance
|
||||||
Object.defineProperty(this, fieldName, {
|
Object.defineProperty(this, fieldName, {
|
||||||
get: () => {
|
get: () => {
|
||||||
@ -75,21 +79,20 @@ export abstract class BaseModel {
|
|||||||
this.markFieldAsModified(fieldName);
|
this.markFieldAsModified(fieldName);
|
||||||
},
|
},
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: true
|
configurable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
// Core CRUD operations
|
// Core CRUD operations
|
||||||
async save(): Promise<this> {
|
async save(): Promise<this> {
|
||||||
if (this._isNew) {
|
if (this._isNew) {
|
||||||
// Clean up any instance properties before hooks run
|
// Clean up any instance properties before hooks run
|
||||||
this.cleanupShadowingProperties();
|
this.cleanupShadowingProperties();
|
||||||
|
|
||||||
await this.beforeCreate();
|
await this.beforeCreate();
|
||||||
|
|
||||||
// Clean up any instance properties created by hooks
|
// Clean up any instance properties created by hooks
|
||||||
this.cleanupShadowingProperties();
|
this.cleanupShadowingProperties();
|
||||||
|
|
||||||
@ -102,11 +105,10 @@ export abstract class BaseModel {
|
|||||||
const now = Date.now();
|
const now = Date.now();
|
||||||
this.setFieldValue('createdAt', now);
|
this.setFieldValue('createdAt', now);
|
||||||
this.setFieldValue('updatedAt', now);
|
this.setFieldValue('updatedAt', now);
|
||||||
|
|
||||||
// Clean up any additional shadowing properties after setting timestamps
|
// Clean up any additional shadowing properties after setting timestamps
|
||||||
this.cleanupShadowingProperties();
|
this.cleanupShadowingProperties();
|
||||||
|
|
||||||
|
|
||||||
// Validate after all field generation is complete
|
// Validate after all field generation is complete
|
||||||
await this.validate();
|
await this.validate();
|
||||||
|
|
||||||
@ -117,7 +119,7 @@ export abstract class BaseModel {
|
|||||||
this.clearModifications();
|
this.clearModifications();
|
||||||
|
|
||||||
await this.afterCreate();
|
await this.afterCreate();
|
||||||
|
|
||||||
// Clean up any shadowing properties created during save
|
// Clean up any shadowing properties created during save
|
||||||
this.cleanupShadowingProperties();
|
this.cleanupShadowingProperties();
|
||||||
} else if (this._isDirty) {
|
} else if (this._isDirty) {
|
||||||
@ -125,7 +127,7 @@ export abstract class BaseModel {
|
|||||||
|
|
||||||
// Set timestamp using Field setter
|
// Set timestamp using Field setter
|
||||||
this.setFieldValue('updatedAt', Date.now());
|
this.setFieldValue('updatedAt', Date.now());
|
||||||
|
|
||||||
// Validate after hooks have run
|
// Validate after hooks have run
|
||||||
await this.validate();
|
await this.validate();
|
||||||
|
|
||||||
@ -135,7 +137,7 @@ export abstract class BaseModel {
|
|||||||
this.clearModifications();
|
this.clearModifications();
|
||||||
|
|
||||||
await this.afterUpdate();
|
await this.afterUpdate();
|
||||||
|
|
||||||
// Clean up any shadowing properties created during save
|
// Clean up any shadowing properties created during save
|
||||||
this.cleanupShadowingProperties();
|
this.cleanupShadowingProperties();
|
||||||
}
|
}
|
||||||
@ -168,21 +170,32 @@ export abstract class BaseModel {
|
|||||||
try {
|
try {
|
||||||
const modelClass = this as any;
|
const modelClass = this as any;
|
||||||
let data = null;
|
let data = null;
|
||||||
|
|
||||||
if (modelClass.scope === 'user') {
|
if (modelClass.scope === 'user') {
|
||||||
// For user-scoped models, we would need userId - for now, try global
|
// 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) {
|
if (database && framework.databaseManager?.getDocument) {
|
||||||
data = await framework.databaseManager.getDocument(database, modelClass.storeType, id);
|
data = await framework.databaseManager.getDocument(database, modelClass.storeType, id);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (modelClass.sharding) {
|
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) {
|
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 {
|
} 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) {
|
if (database && framework.databaseManager?.getDocument) {
|
||||||
data = await framework.databaseManager.getDocument(database, modelClass.storeType, id);
|
data = await framework.databaseManager.getDocument(database, modelClass.storeType, id);
|
||||||
}
|
}
|
||||||
@ -195,7 +208,7 @@ export abstract class BaseModel {
|
|||||||
instance.clearModifications();
|
instance.clearModifications();
|
||||||
return instance;
|
return instance;
|
||||||
}
|
}
|
||||||
|
|
||||||
return null;
|
return null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error('Failed to find by ID:', error);
|
console.error('Failed to find by ID:', error);
|
||||||
@ -283,12 +296,12 @@ export abstract class BaseModel {
|
|||||||
criteria: any,
|
criteria: any,
|
||||||
): Promise<T | null> {
|
): Promise<T | null> {
|
||||||
const query = new QueryBuilder<T>(this as any);
|
const query = new QueryBuilder<T>(this as any);
|
||||||
|
|
||||||
// Apply criteria as where clauses
|
// Apply criteria as where clauses
|
||||||
Object.keys(criteria).forEach(key => {
|
Object.keys(criteria).forEach((key) => {
|
||||||
query.where(key, '=', criteria[key]);
|
query.where(key, '=', criteria[key]);
|
||||||
});
|
});
|
||||||
|
|
||||||
const results = await query.limit(1).exec();
|
const results = await query.limit(1).exec();
|
||||||
return results.length > 0 ? results[0] : null;
|
return results.length > 0 ? results[0] : null;
|
||||||
}
|
}
|
||||||
@ -385,6 +398,11 @@ export abstract class BaseModel {
|
|||||||
// Include basic properties
|
// Include basic properties
|
||||||
result.id = this.id;
|
result.id = this.id;
|
||||||
|
|
||||||
|
// For OrbitDB docstore compatibility, also include _id field
|
||||||
|
if (modelClass.storeType === 'docstore') {
|
||||||
|
result._id = this.id;
|
||||||
|
}
|
||||||
|
|
||||||
// Include loaded relations
|
// Include loaded relations
|
||||||
this._loadedRelations.forEach((value, key) => {
|
this._loadedRelations.forEach((value, key) => {
|
||||||
result[key] = value;
|
result[key] = value;
|
||||||
@ -396,7 +414,7 @@ export abstract class BaseModel {
|
|||||||
fromJSON(data: any): this {
|
fromJSON(data: any): this {
|
||||||
if (!data) return this;
|
if (!data) return this;
|
||||||
|
|
||||||
// Set basic properties
|
// Set basic properties
|
||||||
Object.keys(data).forEach((key) => {
|
Object.keys(data).forEach((key) => {
|
||||||
if (key !== '_loadedRelations' && key !== '_isDirty' && key !== '_isNew') {
|
if (key !== '_loadedRelations' && key !== '_isDirty' && key !== '_isNew') {
|
||||||
(this as any)[key] = data[key];
|
(this as any)[key] = data[key];
|
||||||
@ -416,12 +434,11 @@ export abstract class BaseModel {
|
|||||||
const errors: string[] = [];
|
const errors: string[] = [];
|
||||||
const modelClass = this.constructor as typeof BaseModel;
|
const modelClass = this.constructor as typeof BaseModel;
|
||||||
|
|
||||||
|
|
||||||
// Validate each field using private keys (more reliable)
|
// Validate each field using private keys (more reliable)
|
||||||
for (const [fieldName, fieldConfig] of modelClass.fields) {
|
for (const [fieldName, fieldConfig] of modelClass.fields) {
|
||||||
const privateKey = `_${fieldName}`;
|
const privateKey = `_${fieldName}`;
|
||||||
const value = (this as any)[privateKey];
|
const value = (this as any)[privateKey];
|
||||||
|
|
||||||
const fieldErrors = await this.validateField(fieldName, value, fieldConfig);
|
const fieldErrors = await this.validateField(fieldName, value, fieldConfig);
|
||||||
errors.push(...fieldErrors);
|
errors.push(...fieldErrors);
|
||||||
}
|
}
|
||||||
@ -435,7 +452,11 @@ export abstract class BaseModel {
|
|||||||
return result;
|
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[] = [];
|
const errors: string[] = [];
|
||||||
|
|
||||||
// Required validation
|
// Required validation
|
||||||
@ -544,18 +565,18 @@ export abstract class BaseModel {
|
|||||||
|
|
||||||
private applyFieldDefaults(): void {
|
private applyFieldDefaults(): void {
|
||||||
const modelClass = this.constructor as typeof BaseModel;
|
const modelClass = this.constructor as typeof BaseModel;
|
||||||
|
|
||||||
// Ensure we have fields map
|
// Ensure we have fields map
|
||||||
if (!modelClass.fields) {
|
if (!modelClass.fields) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const [fieldName, fieldConfig] of modelClass.fields) {
|
for (const [fieldName, fieldConfig] of modelClass.fields) {
|
||||||
if (fieldConfig.default !== undefined) {
|
if (fieldConfig.default !== undefined) {
|
||||||
const privateKey = `_${fieldName}`;
|
const privateKey = `_${fieldName}`;
|
||||||
const hasProperty = (this as any).hasOwnProperty(privateKey);
|
const hasProperty = (this as any).hasOwnProperty(privateKey);
|
||||||
const currentValue = (this as any)[privateKey];
|
const currentValue = (this as any)[privateKey];
|
||||||
|
|
||||||
// Always apply default value to private field if it's not set
|
// Always apply default value to private field if it's not set
|
||||||
if (!hasProperty || currentValue === undefined) {
|
if (!hasProperty || currentValue === undefined) {
|
||||||
// Apply default value to private field
|
// Apply default value to private field
|
||||||
@ -594,16 +615,29 @@ export abstract class BaseModel {
|
|||||||
getFieldValue(fieldName: string): any {
|
getFieldValue(fieldName: string): any {
|
||||||
// Always ensure this field's getter works properly
|
// Always ensure this field's getter works properly
|
||||||
this.ensureFieldGetter(fieldName);
|
this.ensureFieldGetter(fieldName);
|
||||||
|
|
||||||
|
// Try private key first
|
||||||
const privateKey = `_${fieldName}`;
|
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 {
|
private ensureFieldGetter(fieldName: string): void {
|
||||||
// If there's a shadowing instance property, remove it and create a working getter
|
// If there's a shadowing instance property, remove it and create a working getter
|
||||||
if (this.hasOwnProperty(fieldName)) {
|
if (this.hasOwnProperty(fieldName)) {
|
||||||
delete (this as any)[fieldName];
|
delete (this as any)[fieldName];
|
||||||
|
|
||||||
// Define a working getter directly on the instance
|
// Define a working getter directly on the instance
|
||||||
Object.defineProperty(this, fieldName, {
|
Object.defineProperty(this, fieldName, {
|
||||||
get: () => {
|
get: () => {
|
||||||
@ -616,7 +650,7 @@ export abstract class BaseModel {
|
|||||||
this.markFieldAsModified(fieldName);
|
this.markFieldAsModified(fieldName);
|
||||||
},
|
},
|
||||||
enumerable: true,
|
enumerable: true,
|
||||||
configurable: true
|
configurable: true,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -636,14 +670,14 @@ export abstract class BaseModel {
|
|||||||
getAllFieldValues(): Record<string, any> {
|
getAllFieldValues(): Record<string, any> {
|
||||||
const modelClass = this.constructor as typeof BaseModel;
|
const modelClass = this.constructor as typeof BaseModel;
|
||||||
const values: Record<string, any> = {};
|
const values: Record<string, any> = {};
|
||||||
|
|
||||||
for (const [fieldName] of modelClass.fields) {
|
for (const [fieldName] of modelClass.fields) {
|
||||||
const value = this.getFieldValue(fieldName);
|
const value = this.getFieldValue(fieldName);
|
||||||
if (value !== undefined) {
|
if (value !== undefined) {
|
||||||
values[fieldName] = value;
|
values[fieldName] = value;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -676,17 +710,20 @@ export abstract class BaseModel {
|
|||||||
try {
|
try {
|
||||||
if (modelClass.scope === 'user') {
|
if (modelClass.scope === 'user') {
|
||||||
// For user-scoped models, we need a userId (check common field names)
|
// 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) {
|
if (!userId) {
|
||||||
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user databases exist before accessing them
|
// Ensure user databases exist before accessing them
|
||||||
await this.ensureUserDatabasesExist(framework, userId);
|
await this.ensureUserDatabasesExist(framework, userId);
|
||||||
|
|
||||||
// Ensure user databases exist before accessing them
|
// Ensure user databases exist before accessing them
|
||||||
await this.ensureUserDatabasesExist(framework, userId);
|
await this.ensureUserDatabasesExist(framework, userId);
|
||||||
|
|
||||||
const database = await framework.databaseManager.getUserDatabase(
|
const database = await framework.databaseManager.getUserDatabase(
|
||||||
userId,
|
userId,
|
||||||
modelClass.modelName,
|
modelClass.modelName,
|
||||||
@ -705,7 +742,11 @@ export abstract class BaseModel {
|
|||||||
} else {
|
} else {
|
||||||
// Use single global database
|
// Use single global database
|
||||||
const database = await framework.databaseManager.getGlobalDatabase(modelClass.modelName);
|
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) {
|
} catch (error) {
|
||||||
@ -725,14 +766,17 @@ export abstract class BaseModel {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (modelClass.scope === 'user') {
|
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) {
|
if (!userId) {
|
||||||
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user databases exist before accessing them
|
// Ensure user databases exist before accessing them
|
||||||
await this.ensureUserDatabasesExist(framework, userId);
|
await this.ensureUserDatabasesExist(framework, userId);
|
||||||
|
|
||||||
const database = await framework.databaseManager.getUserDatabase(
|
const database = await framework.databaseManager.getUserDatabase(
|
||||||
userId,
|
userId,
|
||||||
modelClass.modelName,
|
modelClass.modelName,
|
||||||
@ -779,14 +823,17 @@ export abstract class BaseModel {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
if (modelClass.scope === 'user') {
|
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) {
|
if (!userId) {
|
||||||
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
throw new Error('User-scoped models must have a userId, authorId, or ownerId field');
|
||||||
}
|
}
|
||||||
|
|
||||||
// Ensure user databases exist before accessing them
|
// Ensure user databases exist before accessing them
|
||||||
await this.ensureUserDatabasesExist(framework, userId);
|
await this.ensureUserDatabasesExist(framework, userId);
|
||||||
|
|
||||||
const database = await framework.databaseManager.getUserDatabase(
|
const database = await framework.databaseManager.getUserDatabase(
|
||||||
userId,
|
userId,
|
||||||
modelClass.modelName,
|
modelClass.modelName,
|
||||||
@ -858,7 +905,7 @@ export abstract class BaseModel {
|
|||||||
if (!(globalThis as any).__mockDatabase) {
|
if (!(globalThis as any).__mockDatabase) {
|
||||||
(globalThis as any).__mockDatabase = new Map();
|
(globalThis as any).__mockDatabase = new Map();
|
||||||
}
|
}
|
||||||
|
|
||||||
const mockDatabase = {
|
const mockDatabase = {
|
||||||
_data: (globalThis as any).__mockDatabase,
|
_data: (globalThis as any).__mockDatabase,
|
||||||
async get(id: string) {
|
async get(id: string) {
|
||||||
@ -874,7 +921,7 @@ export abstract class BaseModel {
|
|||||||
},
|
},
|
||||||
async all() {
|
async all() {
|
||||||
return Array.from(this._data.values());
|
return Array.from(this._data.values());
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return {
|
return {
|
||||||
@ -908,13 +955,13 @@ export abstract class BaseModel {
|
|||||||
},
|
},
|
||||||
async getAllDocuments(_database: any, _type: string) {
|
async getAllDocuments(_database: any, _type: string) {
|
||||||
return await mockDatabase.all();
|
return await mockDatabase.all();
|
||||||
}
|
},
|
||||||
},
|
},
|
||||||
shardManager: {
|
shardManager: {
|
||||||
getShardForKey(_modelName: string, _key: string) {
|
getShardForKey(_modelName: string, _key: string) {
|
||||||
return { database: mockDatabase };
|
return { database: mockDatabase };
|
||||||
}
|
},
|
||||||
}
|
},
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
|
@ -7,18 +7,8 @@ export function Field(config: FieldConfig) {
|
|||||||
validateFieldConfig(config);
|
validateFieldConfig(config);
|
||||||
|
|
||||||
// Handle ESM case where target might be undefined
|
// Handle ESM case where target might be undefined
|
||||||
if (!target) {
|
if (!target || typeof target !== 'object') {
|
||||||
// In ESM environment, defer the decorator application
|
// Skip the decorator if target is not available - the field will be handled later
|
||||||
// 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);
|
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -112,8 +112,8 @@ class BlogAPIServer {
|
|||||||
|
|
||||||
const user = await User.create(sanitizedData);
|
const user = await User.create(sanitizedData);
|
||||||
|
|
||||||
console.log(`[${this.nodeId}] Created user: ${user.username} (${user.id})`);
|
console.log(`[${this.nodeId}] Created user: ${user.getFieldValue('username')} (${user.id})`);
|
||||||
res.status(201).json(user.toJSON());
|
res.status(201).json(user);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
}
|
}
|
||||||
@ -226,7 +226,7 @@ class BlogAPIServer {
|
|||||||
|
|
||||||
const category = await Category.create(sanitizedData);
|
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);
|
res.status(201).json(category);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
@ -276,7 +276,7 @@ class BlogAPIServer {
|
|||||||
|
|
||||||
const post = await Post.create(sanitizedData);
|
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);
|
res.status(201).json(post);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
next(error);
|
next(error);
|
||||||
|
@ -2,6 +2,79 @@ import 'reflect-metadata';
|
|||||||
import { BaseModel } from '../../../../src/framework/models/BaseModel';
|
import { BaseModel } from '../../../../src/framework/models/BaseModel';
|
||||||
import { Model, Field, HasMany, BelongsTo, HasOne, BeforeCreate, AfterCreate } from '../../../../src/framework/models/decorators';
|
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
|
// User Profile Model
|
||||||
@Model({
|
@Model({
|
||||||
scope: 'global',
|
scope: 'global',
|
||||||
@ -368,4 +441,7 @@ export interface UpdatePostRequest {
|
|||||||
tags?: string[];
|
tags?: string[];
|
||||||
featuredImage?: string;
|
featuredImage?: string;
|
||||||
isFeatured?: boolean;
|
isFeatured?: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Initialize field configurations after all models are defined
|
||||||
|
setupFieldConfigurations();
|
||||||
|
Loading…
x
Reference in New Issue
Block a user