@wearesage/schema
Version:
A flexible schema definition and validation system for TypeScript with multi-database support
713 lines (597 loc) • 22.8 kB
text/typescript
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
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
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
class BaseEntity {
id: string;
baseField: string;
}
// Define entity that extends base entity
class ChildEntity extends BaseEntity {
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
class TestEntity {
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
class TestEntity {
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
class CompositeKeyEntity {
id1: string;
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
class TestEntity {
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
class TestEntity {
name: string;
email: string;
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", () => {
class TestEntity {
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
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
class Profile {}
class User {
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
class Post {}
class User {
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
class User {}
class Post {
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
class Tag {}
class Post {
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
class Tag {}
class Post {
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
class User {}
class Post {
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
class User {}
class Post {
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
class User {}
class Post {
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
class TestEntity {
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
class Profile {}
class 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
class Profile {}
class User {
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
class Post {}
class User {
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
class User {}
class Post {
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
class Tag {}
class Post {
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
class User {}
class Post {
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
class Tag {}
class Post {
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
class MockPost {}
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
class TreeNode {
id: string;
parent: TreeNode;
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);
});
});
});