UNPKG

@wearesage/schema

Version:

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

713 lines (597 loc) 22.8 kB
import "reflect-metadata"; import { Entity, Property, Id, OneToOne, OneToMany, ManyToOne, ManyToMany } from "../../core/decorators"; describe("Decorators", () => { beforeEach(() => { // Clear any existing metadata for tests jest.clearAllMocks(); }); describe("@Entity", () => { test("should store entity metadata on the class", () => { // Define entity with decorator @Entity({ name: "test_entity" }) class TestEntity {} // Check if metadata was stored correctly const metadata = Reflect.getMetadata("entity:options", TestEntity); expect(metadata).toBeDefined(); expect(metadata).toEqual({ name: "test_entity" }); }); test("should initialize empty arrays for properties and relationships", () => { // Define entity with decorator @Entity() class TestEntity {} // Check if property and relationship arrays were initialized const properties = Reflect.getMetadata("entity:properties", TestEntity); const relationships = Reflect.getMetadata("entity:relationships", TestEntity); expect(properties).toEqual([]); expect(relationships).toEqual([]); }); test("should work with inheritance", () => { // Define base entity @Entity({ name: "base_entity" }) class BaseEntity { @Id() id: string; @Property() baseField: string; } // Define entity that extends base entity @Entity({ name: "child_entity" }) class ChildEntity extends BaseEntity { @Property() childField: string; } // Check that each class has its own metadata const baseOptions = Reflect.getMetadata("entity:options", BaseEntity); const childOptions = Reflect.getMetadata("entity:options", ChildEntity); expect(baseOptions).toEqual({ name: "base_entity" }); expect(childOptions).toEqual({ name: "child_entity" }); // Check that properties are registered for each class const baseProps = Reflect.getMetadata("entity:properties", BaseEntity); const childProps = Reflect.getMetadata("entity:properties", ChildEntity); expect(baseProps).toContain("id"); expect(baseProps).toContain("baseField"); expect(childProps).toContain("childField"); }); }); describe("@Id", () => { test("should mark property as ID and register it", () => { // Define entity class with ID property @Entity() class TestEntity { @Id() id: string; } // Check if ID was registered correctly const isId = Reflect.getMetadata("property:id", TestEntity.prototype, "id"); expect(isId).toBe(true); // Check if property was also registered with enhanced options const propertyOptions = Reflect.getMetadata("property:options", TestEntity.prototype, "id"); expect(propertyOptions).toEqual({ required: true, unique: true }); // Check if property was added to the entity's property list const properties = Reflect.getMetadata("entity:properties", TestEntity); expect(properties).toContain("id"); }); test("should merge custom options with default ID options", () => { // Define entity class with ID property and custom options @Entity() class TestEntity { @Id({ description: "Primary key" }) id: string; } // Check if property was registered with merged options const propertyOptions = Reflect.getMetadata("property:options", TestEntity.prototype, "id"); expect(propertyOptions).toEqual({ required: true, unique: true, description: "Primary key" }); }); test("should support multiple ID fields", () => { // Define entity class with multiple ID properties @Entity() class CompositeKeyEntity { @Id() id1: string; @Id() id2: string; } // Check if both IDs are marked correctly expect(Reflect.getMetadata("property:id", CompositeKeyEntity.prototype, "id1")).toBe(true); expect(Reflect.getMetadata("property:id", CompositeKeyEntity.prototype, "id2")).toBe(true); // Check if both properties are in the properties list const properties = Reflect.getMetadata("entity:properties", CompositeKeyEntity); expect(properties).toContain("id1"); expect(properties).toContain("id2"); }); }); describe("@Property", () => { test("should register property metadata", () => { // Define entity class with property @Entity() class TestEntity { @Property({ required: true }) name: string; } // Check if property was registered correctly const propertyOptions = Reflect.getMetadata("property:options", TestEntity.prototype, "name"); expect(propertyOptions).toEqual({ required: true }); // Check if property was added to the entity's property list const properties = Reflect.getMetadata("entity:properties", TestEntity); expect(properties).toContain("name"); }); test("should handle multiple properties", () => { // Define entity class with multiple properties @Entity() class TestEntity { @Property({ required: true }) name: string; @Property({ unique: true }) email: string; @Property({ default: 0 }) count: number; } // Check if all properties were added to the entity's property list const properties = Reflect.getMetadata("entity:properties", TestEntity); expect(properties).toContain("name"); expect(properties).toContain("email"); expect(properties).toContain("count"); expect(properties.length).toBe(3); // Check specific property options expect(Reflect.getMetadata("property:options", TestEntity.prototype, "name")) .toEqual({ required: true }); expect(Reflect.getMetadata("property:options", TestEntity.prototype, "email")) .toEqual({ unique: true }); expect(Reflect.getMetadata("property:options", TestEntity.prototype, "count")) .toEqual({ default: 0 }); }); test("should handle property decorator with empty options", () => { @Entity() class TestEntity { @Property() name: string; } // Check if property was registered with empty options const propertyOptions = Reflect.getMetadata("property:options", TestEntity.prototype, "name"); expect(propertyOptions).toEqual({}); }); test("should handle property decorator applied multiple times to the same property", () => { // Define entity class @Entity() class TestEntity {} // Apply Property decorator multiple times to the same property Property({ required: true })(TestEntity.prototype, "name"); Property({ unique: true })(TestEntity.prototype, "name"); // The second application should overwrite the first one const propertyOptions = Reflect.getMetadata("property:options", TestEntity.prototype, "name"); expect(propertyOptions).toEqual({ unique: true }); // The property should be in the list only once const properties = Reflect.getMetadata("entity:properties", TestEntity); expect(properties.filter(p => p === "name").length).toBe(1); }); }); describe("Relationship Decorators", () => { test("@OneToOne should store correct relationship metadata", () => { // Define related entity classes @Entity() class Profile {} @Entity() class User { @OneToOne({ target: () => Profile, inverse: "user", name: "HAS_PROFILE" }) profile: Profile; } // Check if relationship was registered correctly const relationshipType = Reflect.getMetadata("relationship:type", User.prototype, "profile"); expect(relationshipType).toBe("one-to-one"); const options = Reflect.getMetadata("relationship:options", User.prototype, "profile"); expect(options).toEqual({ name: "HAS_PROFILE", target: Profile, inverse: "user" }); // Check if relationship was added to the entity's relationship list const relationships = Reflect.getMetadata("entity:relationships", User); expect(relationships).toContain("profile"); }); test("@OneToMany should store correct relationship metadata", () => { // Define related entity classes @Entity() class Post {} @Entity() class User { @OneToMany({ target: () => Post, inverse: "author", name: "AUTHORED" }) posts: Post[]; } // Check if relationship was registered correctly const relationshipType = Reflect.getMetadata("relationship:type", User.prototype, "posts"); expect(relationshipType).toBe("one-to-many"); const options = Reflect.getMetadata("relationship:options", User.prototype, "posts"); expect(options).toEqual({ name: "AUTHORED", target: Post, inverse: "author" }); // Check if relationship was added to the entity's relationship list const relationships = Reflect.getMetadata("entity:relationships", User); expect(relationships).toContain("posts"); }); test("@ManyToOne should store correct relationship metadata", () => { // Define related entity classes @Entity() class User {} @Entity() class Post { @ManyToOne({ target: () => User, inverse: "posts", name: "AUTHORED" }) author: User; } // Check if relationship was registered correctly const relationshipType = Reflect.getMetadata("relationship:type", Post.prototype, "author"); expect(relationshipType).toBe("many-to-one"); const options = Reflect.getMetadata("relationship:options", Post.prototype, "author"); expect(options).toEqual({ name: "AUTHORED", target: User, inverse: "posts" }); // Check if relationship was added to the entity's relationship list const relationships = Reflect.getMetadata("entity:relationships", Post); expect(relationships).toContain("author"); }); test("@ManyToMany should store correct relationship metadata", () => { // Define related entity classes @Entity() class Tag {} @Entity() class Post { @ManyToMany({ target: () => Tag, inverse: "posts", name: "HAS_TAG" }) tags: Tag[]; } // Check if relationship was registered correctly const relationshipType = Reflect.getMetadata("relationship:type", Post.prototype, "tags"); expect(relationshipType).toBe("many-to-many"); const options = Reflect.getMetadata("relationship:options", Post.prototype, "tags"); expect(options).toEqual({ name: "HAS_TAG", target: Tag, inverse: "posts" }); // Check if relationship was added to the entity's relationship list const relationships = Reflect.getMetadata("entity:relationships", Post); expect(relationships).toContain("tags"); }); test("should use property name as relationship name if not provided", () => { // Define related entity classes @Entity() class Tag {} @Entity() class Post { @ManyToMany({ target: () => Tag, inverse: "posts" }) tags: Tag[]; } // Check if default name was used (uppercase property name) const options = Reflect.getMetadata("relationship:options", Post.prototype, "tags"); expect(options.name).toBe("TAGS"); }); test("should store database-specific options for Neo4j", () => { // Define related entity classes @Entity() class User {} @Entity() class Post { @ManyToOne({ target: () => User, inverse: "posts", neo4j: { direction: "OUT", properties: { createdAt: "timestamp" } } }) author: User; } // Check if Neo4j-specific options were stored const neo4jOptions = Reflect.getMetadata("neo4j:relationship", Post.prototype, "author"); expect(neo4jOptions).toEqual({ direction: "OUT", properties: { createdAt: "timestamp" } }); }); test("should store database-specific options for MongoDB", () => { // Define related entity classes @Entity() class User {} @Entity() class Post { @ManyToOne({ target: () => User, inverse: "posts", mongodb: { embedded: false, collection: "users" } }) author: User; } // Check if MongoDB-specific options were stored const mongodbOptions = Reflect.getMetadata("mongodb:relationship", Post.prototype, "author"); expect(mongodbOptions).toEqual({ embedded: false, collection: "users" }); }); test("should store database-specific options for PostgreSQL", () => { // Define related entity classes @Entity() class User {} @Entity() class Post { @ManyToOne({ target: () => User, inverse: "posts", postgresql: { joinTable: "post_authors", foreignKeyColumn: "user_id" } }) author: User; } // Check if PostgreSQL-specific options were stored const postgresqlOptions = Reflect.getMetadata("postgresql:relationship", Post.prototype, "author"); expect(postgresqlOptions).toEqual({ joinTable: "post_authors", foreignKeyColumn: "user_id" }); }); test("Entity decorator should handle existing property and relationship metadata", () => { // Define a mock class that extends a constructor class MockTarget {} // Add existing metadata Reflect.defineMetadata("entity:properties", ["existingProp"], MockTarget); Reflect.defineMetadata("entity:relationships", ["existingRel"], MockTarget); // Apply decorator to the mock class Entity({ name: "mockEntity" })(MockTarget); // Check if options were set const options = Reflect.getMetadata("entity:options", MockTarget); expect(options).toEqual({ name: "mockEntity" }); // Verify existing metadata was preserved const props = Reflect.getMetadata("entity:properties", MockTarget); const rels = Reflect.getMetadata("entity:relationships", MockTarget); expect(props).toContain("existingProp"); expect(rels).toContain("existingRel"); }); test("Property decorator should work with existing properties list", () => { // Define entity with existing properties metadata @Entity() class TestEntity { @Property({ required: true }) existing: string; } // Get the properties before adding another one const existingProps = Reflect.getMetadata("entity:properties", TestEntity); // Add another property to the class prototype directly Property({ unique: true })(TestEntity.prototype, "addedLater"); // Check if both properties are in the list const updatedProps = Reflect.getMetadata("entity:properties", TestEntity); expect(updatedProps).toContain("existing"); expect(updatedProps).toContain("addedLater"); expect(updatedProps.length).toBe(2); }); test("OneToOne decorator without database-specific options", () => { // Define related entity classes @Entity() class Profile {} @Entity() class User { @OneToOne({ target: () => Profile, inverse: "user", }) profile: Profile; } // Check relationship name defaults to uppercase property name const options = Reflect.getMetadata("relationship:options", User.prototype, "profile"); expect(options.name).toBe("PROFILE"); // Check no database options were set expect(Reflect.getMetadata("neo4j:relationship", User.prototype, "profile")).toBeUndefined(); expect(Reflect.getMetadata("mongodb:relationship", User.prototype, "profile")).toBeUndefined(); expect(Reflect.getMetadata("postgresql:relationship", User.prototype, "profile")).toBeUndefined(); }); test("OneToOne decorator with required option", () => { // Define related entity classes @Entity() class Profile {} @Entity() class User { @OneToOne({ target: () => Profile, inverse: "user", required: true }) profile: Profile; } // Check if required option is stored const options = Reflect.getMetadata("relationship:options", User.prototype, "profile"); expect(options.required).toBe(true); }); test("OneToMany decorator with required option", () => { // Define related entity classes with required relationship @Entity() class Post {} @Entity() class User { @OneToMany({ target: () => Post, inverse: "author", required: true }) posts: Post[]; } // Check if required option is stored const options = Reflect.getMetadata("relationship:options", User.prototype, "posts"); expect(options.required).toBe(true); }); test("ManyToOne decorator with required option", () => { // Define related entity classes @Entity() class User {} @Entity() class Post { @ManyToOne({ target: () => User, inverse: "posts", required: true }) author: User; } // Check if required option is stored const options = Reflect.getMetadata("relationship:options", Post.prototype, "author"); expect(options.required).toBe(true); }); test("ManyToMany decorator with required option", () => { // Define related entity classes @Entity() class Tag {} @Entity() class Post { @ManyToMany({ target: () => Tag, inverse: "posts", required: true }) tags: Tag[]; } // Check if required option is stored const options = Reflect.getMetadata("relationship:options", Post.prototype, "tags"); expect(options.required).toBe(true); }); test("ManyToOne decorator with custom relationship name", () => { // Define related entity classes @Entity() class User {} @Entity() class Post { @ManyToOne({ target: () => User, inverse: "posts", name: "WRITTEN_BY" // Custom name }) author: User; } // Check if custom name is stored const options = Reflect.getMetadata("relationship:options", Post.prototype, "author"); expect(options.name).toBe("WRITTEN_BY"); }); test("ManyToMany decorator with all database options", () => { // Define related entity classes @Entity() class Tag {} @Entity() class Post { @ManyToMany({ target: () => Tag, inverse: "posts", neo4j: { type: "TAGGED_WITH" }, mongodb: { embedded: true }, postgresql: { joinTable: "post_tags" } }) tags: Tag[]; } // Check all database options are stored expect(Reflect.getMetadata("neo4j:relationship", Post.prototype, "tags")) .toEqual({ type: "TAGGED_WITH" }); expect(Reflect.getMetadata("mongodb:relationship", Post.prototype, "tags")) .toEqual({ embedded: true }); expect(Reflect.getMetadata("postgresql:relationship", Post.prototype, "tags")) .toEqual({ joinTable: "post_tags" }); }); test("OneToMany decorator with existing relationships list", () => { // Define entity and target classes @Entity() class MockPost {} @Entity() class MockUser { // Will be added manually below posts: MockPost[]; } // Generate a relationship list and set it on the class const existingRels = ["comments"]; Reflect.defineMetadata("entity:relationships", existingRels, MockUser); // Now apply the OneToMany decorator OneToMany({ target: () => MockPost, inverse: "author" })(MockUser.prototype, "posts"); // Check if both relationships are present const relationships = Reflect.getMetadata("entity:relationships", MockUser); expect(relationships).toContain("comments"); expect(relationships).toContain("posts"); expect(relationships.length).toBe(2); }); test("should handle relationships with circular references", () => { // First define the classes to avoid reference issues class TestUser {} class TestPost {} // Now apply decorators Entity()(TestUser); Entity()(TestPost); // Apply relationship decorators with circular references OneToMany({ target: () => TestPost, inverse: "author" })(TestUser.prototype, "posts"); ManyToOne({ target: () => TestUser, inverse: "posts" })(TestPost.prototype, "author"); // Check that relationship metadata was stored correctly const userPostsOptions = Reflect.getMetadata("relationship:options", TestUser.prototype, "posts"); const postAuthorOptions = Reflect.getMetadata("relationship:options", TestPost.prototype, "author"); expect(userPostsOptions.target).toBe(TestPost); expect(postAuthorOptions.target).toBe(TestUser); }); test("should handle self-referential relationships", () => { // Define an entity that references itself @Entity() class TreeNode { @Id() id: string; @ManyToOne({ target: () => TreeNode, inverse: "children" }) parent: TreeNode; @OneToMany({ target: () => TreeNode, inverse: "parent" }) children: TreeNode[]; } // Both relationships should point to the same class const parentTarget = Reflect.getMetadata("relationship:options", TreeNode.prototype, "parent").target; const childrenTarget = Reflect.getMetadata("relationship:options", TreeNode.prototype, "children").target; expect(parentTarget).toBe(TreeNode); expect(childrenTarget).toBe(TreeNode); }); }); });