@wearesage/schema
Version:
A flexible schema definition and validation system for TypeScript with multi-database support
294 lines (251 loc) • 9.31 kB
text/typescript
import "reflect-metadata";
import * as fc from "fast-check";
import {
MetadataRegistry,
SchemaBuilder,
Entity,
Property,
Id,
OneToOne,
OneToMany,
ManyToOne,
ManyToMany
} from "../../core";
describe("Property-based Metadata Tests", () => {
let registry: MetadataRegistry;
let builder: SchemaBuilder;
beforeEach(() => {
registry = new MetadataRegistry();
builder = new SchemaBuilder(registry);
});
test("should correctly register entities with randomly generated properties", () => {
// Generate random class names
const classNameArb = fc.string({
minLength: 3,
maxLength: 20
}).map(s => s.replace(/[^a-zA-Z]/g, 'a')); // Replace non-letters with 'a'
// Generate random property names
const propNameArb = fc.string({
minLength: 2,
maxLength: 15
}).map(s => s.replace(/[^a-zA-Z]/g, 'a')); // Replace non-letters with 'a'
// Generate random property options
const propOptionsArb = fc.record({
required: fc.boolean(),
unique: fc.boolean(),
default: fc.oneof(fc.string(), fc.integer(), fc.constant(null))
});
// Generate arrays of property definitions with unique names
const propertiesArb = fc.uniqueArray(
fc.tuple(propNameArb, propOptionsArb),
{
minLength: 1,
maxLength: 10,
comparator: (a, b) => a[0] === b[0] // Compare by property name
}
);
// Generate random entity metadata
const entityMetadataArb = fc.record({
className: classNameArb,
properties: propertiesArb
});
// Run the test
fc.assert(
fc.property(entityMetadataArb, (entityMetadata) => {
// Create a dynamic class with the entity decorator
const entityClass = createDynamicEntity(
entityMetadata.className,
entityMetadata.properties
);
// Register the entity
builder.registerEntity(entityClass);
// Verify registration was successful
const registeredMetadata = registry.getEntityMetadata(entityClass);
if (!registeredMetadata) return false;
// Verify all properties were registered
let allPropertiesRegistered = true;
for (const [propName, options] of entityMetadata.properties) {
const propMetadata = registry.getPropertyMetadata(entityClass, propName);
if (!propMetadata) {
allPropertiesRegistered = false;
break;
}
// Check if required and unique options match
if (propMetadata.required !== options.required ||
propMetadata.unique !== options.unique) {
allPropertiesRegistered = false;
break;
}
}
return allPropertiesRegistered;
}),
{ numRuns: 50 }
);
});
test("should handle all relationship types correctly", () => {
// Define types of relationships to test
const relationshipTypes = ["one-to-one", "one-to-many", "many-to-one", "many-to-many"];
// Generate random relationship metadata
const relationshipArb = fc.record({
sourceClass: fc.string({ minLength: 3, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z]/g, 'a')),
targetClass: fc.string({ minLength: 3, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z]/g, 'a')),
relType: fc.constantFrom(...relationshipTypes),
relName: fc.string({ minLength: 2, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z]/g, 'a')),
inverseProp: fc.string({ minLength: 2, maxLength: 10 }).map(s => s.replace(/[^a-zA-Z]/g, 'a')),
required: fc.boolean()
});
// Run the test
fc.assert(
fc.property(relationshipArb, (relData) => {
// Create source and target classes
const TargetClass = createDynamicEntity(relData.targetClass, []);
const SourceClass = createDynamicEntityWithRelationship(
relData.sourceClass,
relData.relName,
TargetClass,
relData.relType,
relData.inverseProp,
relData.required
);
// Register classes
builder.registerEntities([SourceClass, TargetClass]);
// Verify relationship registration
const relationship = registry.getRelationshipMetadata(SourceClass, relData.relName);
if (!relationship) return false;
// Check relationship properties
const correctTarget = relationship.target === TargetClass;
const correctInverse = relationship.inverse === relData.inverseProp;
const correctRequired = relationship.required === relData.required;
// Check cardinality based on relationship type
let correctCardinality = false;
if (relData.relType === "one-to-one" || relData.relType === "many-to-one") {
correctCardinality = relationship.cardinality === "one";
} else if (relData.relType === "one-to-many" || relData.relType === "many-to-many") {
correctCardinality = relationship.cardinality === "many";
}
return correctTarget && correctInverse && correctRequired && correctCardinality;
}),
{ numRuns: 50 }
);
});
test("should handle cloning and clearing registry with random data", () => {
// Generate a random set of entities to register
const entitiesCountArb = fc.integer({ min: 1, max: 10 });
fc.assert(
fc.property(entitiesCountArb, (count) => {
// Create multiple random entities
const entities = [];
for (let i = 0; i < count; i++) {
const className = `TestEntity${i}`;
const entityClass = createDynamicEntity(className, [
[`prop${i}`, { required: true }]
]);
entities.push(entityClass);
}
// Register all entities
builder.registerEntities(entities);
// Verify all entities are registered
const allRegistered = entities.every(e =>
registry.getEntityMetadata(e) !== undefined
);
if (!allRegistered) return false;
// Clone the registry
const clonedRegistry = registry.clone();
// Verify all entities exist in the clone
const allCloned = entities.every(e =>
clonedRegistry.getEntityMetadata(e) !== undefined
);
if (!allCloned) return false;
// Clear the original registry
registry.clear();
// Verify original registry is empty but clone still has data
const originalEmpty = entities.every(e =>
registry.getEntityMetadata(e) === undefined
);
const cloneIntact = entities.every(e =>
clonedRegistry.getEntityMetadata(e) !== undefined
);
return originalEmpty && cloneIntact;
}),
{ numRuns: 10 }
);
});
});
// Helper functions to dynamically create entity classes
function createDynamicEntity(
className: string,
properties: [string, any][]
): any {
// Create a class with the specified name
const entityClass = class {};
Object.defineProperty(entityClass, 'name', { value: className });
// Apply entity decorator
Entity()(entityClass);
// Add properties with decorators
properties.forEach(([propName, options]) => {
// Define the property on the prototype
Object.defineProperty(entityClass.prototype, propName, {
writable: true,
enumerable: true,
configurable: true
});
// Apply property decorator
Property(options)(entityClass.prototype, propName);
});
// Add an ID property
Object.defineProperty(entityClass.prototype, 'id', {
writable: true,
enumerable: true,
configurable: true
});
Id()(entityClass.prototype, 'id');
return entityClass;
}
function createDynamicEntityWithRelationship(
className: string,
relationshipName: string,
targetClass: any,
relationType: string,
inverseProp: string,
required: boolean
): any {
// Create a class with the specified name
const entityClass = class {};
Object.defineProperty(entityClass, 'name', { value: className });
// Apply entity decorator
Entity()(entityClass);
// Define the relationship property
Object.defineProperty(entityClass.prototype, relationshipName, {
writable: true,
enumerable: true,
configurable: true
});
// Apply the appropriate relationship decorator
const relationshipOptions = {
target: () => targetClass,
inverse: inverseProp,
required
};
switch (relationType) {
case "one-to-one":
OneToOne(relationshipOptions)(entityClass.prototype, relationshipName);
break;
case "one-to-many":
OneToMany(relationshipOptions)(entityClass.prototype, relationshipName);
break;
case "many-to-one":
ManyToOne(relationshipOptions)(entityClass.prototype, relationshipName);
break;
case "many-to-many":
ManyToMany(relationshipOptions)(entityClass.prototype, relationshipName);
break;
}
// Add an ID property
Object.defineProperty(entityClass.prototype, 'id', {
writable: true,
enumerable: true,
configurable: true
});
Id()(entityClass.prototype, 'id');
return entityClass;
}