UNPKG

@wearesage/schema

Version:

A flexible schema definition and validation system for TypeScript with multi-database support

294 lines (251 loc) 9.31 kB
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; }