@woltz/rich-domain 0.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/ci.yml +40 -0
- package/.husky/commit-msg +1 -0
- package/.husky/pre-commit +1 -0
- package/.versionrc.json +21 -0
- package/.vscode/settings.json +3 -0
- package/CHANGELOG.md +81 -0
- package/LICENSE +21 -0
- package/README.md +712 -0
- package/commitlint.config.js +23 -0
- package/dist/base-entity.d.ts +67 -0
- package/dist/base-entity.d.ts.map +1 -0
- package/dist/base-entity.js +309 -0
- package/dist/base-entity.js.map +1 -0
- package/dist/constants.d.ts +3 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +6 -0
- package/dist/constants.js.map +1 -0
- package/dist/criteria.d.ts +60 -0
- package/dist/criteria.d.ts.map +1 -0
- package/dist/criteria.js +214 -0
- package/dist/criteria.js.map +1 -0
- package/dist/deep-proxy.d.ts +34 -0
- package/dist/deep-proxy.d.ts.map +1 -0
- package/dist/deep-proxy.js +297 -0
- package/dist/deep-proxy.js.map +1 -0
- package/dist/domain-event-bus.d.ts +57 -0
- package/dist/domain-event-bus.d.ts.map +1 -0
- package/dist/domain-event-bus.js +112 -0
- package/dist/domain-event-bus.js.map +1 -0
- package/dist/domain-event.d.ts +55 -0
- package/dist/domain-event.d.ts.map +1 -0
- package/dist/domain-event.js +42 -0
- package/dist/domain-event.js.map +1 -0
- package/dist/entity.d.ts +13 -0
- package/dist/entity.d.ts.map +1 -0
- package/dist/entity.js +15 -0
- package/dist/entity.js.map +1 -0
- package/dist/filtering.d.ts +107 -0
- package/dist/filtering.d.ts.map +1 -0
- package/dist/filtering.js +202 -0
- package/dist/filtering.js.map +1 -0
- package/dist/id.d.ts +51 -0
- package/dist/id.d.ts.map +1 -0
- package/dist/id.js +84 -0
- package/dist/id.js.map +1 -0
- package/dist/index.d.ts +15 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +25 -0
- package/dist/index.js.map +1 -0
- package/dist/ordering.d.ts +93 -0
- package/dist/ordering.d.ts.map +1 -0
- package/dist/ordering.js +154 -0
- package/dist/ordering.js.map +1 -0
- package/dist/paginated-result.d.ts +62 -0
- package/dist/paginated-result.d.ts.map +1 -0
- package/dist/paginated-result.js +201 -0
- package/dist/paginated-result.js.map +1 -0
- package/dist/pagination.d.ts +218 -0
- package/dist/pagination.d.ts.map +1 -0
- package/dist/pagination.js +281 -0
- package/dist/pagination.js.map +1 -0
- package/dist/repository/base-repository.d.ts +77 -0
- package/dist/repository/base-repository.d.ts.map +1 -0
- package/dist/repository/base-repository.js +80 -0
- package/dist/repository/base-repository.js.map +1 -0
- package/dist/repository/in-memory-repository.d.ts +46 -0
- package/dist/repository/in-memory-repository.d.ts.map +1 -0
- package/dist/repository/in-memory-repository.js +85 -0
- package/dist/repository/in-memory-repository.js.map +1 -0
- package/dist/repository/index.d.ts +42 -0
- package/dist/repository/index.d.ts.map +1 -0
- package/dist/repository/index.js +47 -0
- package/dist/repository/index.js.map +1 -0
- package/dist/repository/mapper.d.ts +56 -0
- package/dist/repository/mapper.d.ts.map +1 -0
- package/dist/repository/mapper.js +15 -0
- package/dist/repository/mapper.js.map +1 -0
- package/dist/repository/types.d.ts +87 -0
- package/dist/repository/types.d.ts.map +1 -0
- package/dist/repository/types.js +6 -0
- package/dist/repository/types.js.map +1 -0
- package/dist/repository/unit-of-work.d.ts +70 -0
- package/dist/repository/unit-of-work.d.ts.map +1 -0
- package/dist/repository/unit-of-work.js +122 -0
- package/dist/repository/unit-of-work.js.map +1 -0
- package/dist/repository.d.ts +2 -0
- package/dist/repository.d.ts.map +1 -0
- package/dist/repository.js +21 -0
- package/dist/repository.js.map +1 -0
- package/dist/specification.d.ts +102 -0
- package/dist/specification.d.ts.map +1 -0
- package/dist/specification.js +187 -0
- package/dist/specification.js.map +1 -0
- package/dist/types/criteria.d.ts +35 -0
- package/dist/types/criteria.d.ts.map +1 -0
- package/dist/types/criteria.js +17 -0
- package/dist/types/criteria.js.map +1 -0
- package/dist/types/domain.d.ts +30 -0
- package/dist/types/domain.d.ts.map +1 -0
- package/dist/types/domain.js +2 -0
- package/dist/types/domain.js.map +1 -0
- package/dist/types/history-tracker.d.ts +36 -0
- package/dist/types/history-tracker.d.ts.map +1 -0
- package/dist/types/history-tracker.js +2 -0
- package/dist/types/history-tracker.js.map +1 -0
- package/dist/types/index.d.ts +8 -0
- package/dist/types/index.d.ts.map +1 -0
- package/dist/types/index.js +8 -0
- package/dist/types/index.js.map +1 -0
- package/dist/types/repository.d.ts +43 -0
- package/dist/types/repository.d.ts.map +1 -0
- package/dist/types/repository.js +2 -0
- package/dist/types/repository.js.map +1 -0
- package/dist/types/standard-schema.d.ts +15 -0
- package/dist/types/standard-schema.d.ts.map +1 -0
- package/dist/types/standard-schema.js +2 -0
- package/dist/types/standard-schema.js.map +1 -0
- package/dist/types/unit-of-work.d.ts +39 -0
- package/dist/types/unit-of-work.d.ts.map +1 -0
- package/dist/types/unit-of-work.js +2 -0
- package/dist/types/unit-of-work.js.map +1 -0
- package/dist/types/utils.d.ts +14 -0
- package/dist/types/utils.d.ts.map +1 -0
- package/dist/types/utils.js +2 -0
- package/dist/types/utils.js.map +1 -0
- package/dist/types.d.ts +88 -0
- package/dist/types.d.ts.map +1 -0
- package/dist/types.js +12 -0
- package/dist/types.js.map +1 -0
- package/dist/validation-error.d.ts +42 -0
- package/dist/validation-error.d.ts.map +1 -0
- package/dist/validation-error.js +73 -0
- package/dist/validation-error.js.map +1 -0
- package/dist/value-object.d.ts +47 -0
- package/dist/value-object.d.ts.map +1 -0
- package/dist/value-object.js +136 -0
- package/dist/value-object.js.map +1 -0
- package/eslint.config.js +51 -0
- package/jest.config.js +21 -0
- package/package.json +58 -0
- package/src/base-entity.ts +401 -0
- package/src/constants.ts +7 -0
- package/src/criteria.ts +291 -0
- package/src/deep-proxy.ts +339 -0
- package/src/domain-event-bus.ts +166 -0
- package/src/domain-event.ts +90 -0
- package/src/entity.ts +16 -0
- package/src/id.ts +94 -0
- package/src/index.ts +33 -0
- package/src/paginated-result.ts +274 -0
- package/src/repository/base-repository.ts +152 -0
- package/src/repository/in-memory-repository.ts +104 -0
- package/src/repository/index.ts +55 -0
- package/src/repository/mapper.ts +74 -0
- package/src/repository/unit-of-work.ts +148 -0
- package/src/types/criteria.ts +79 -0
- package/src/types/domain.ts +37 -0
- package/src/types/history-tracker.ts +45 -0
- package/src/types/index.ts +7 -0
- package/src/types/repository.ts +51 -0
- package/src/types/standard-schema.ts +19 -0
- package/src/types/unit-of-work.ts +46 -0
- package/src/types/utils.ts +29 -0
- package/src/validation-error.ts +97 -0
- package/src/value-object.ts +187 -0
- package/tests/criteria.test.ts +432 -0
- package/tests/domain-events.test.ts +445 -0
- package/tests/entity-equality.test.ts +487 -0
- package/tests/entity-validation.test.ts +339 -0
- package/tests/entity.test.ts +33 -0
- package/tests/history-tracker.spec.ts +667 -0
- package/tests/id.test.ts +341 -0
- package/tests/repository.test.ts +641 -0
- package/tests/to-json.test.ts +91 -0
- package/tests/utils.ts +151 -0
- package/tests/value-object-validation.test.ts +228 -0
- package/tests/value-objects.test.ts +52 -0
- package/tsconfig.json +31 -0
|
@@ -0,0 +1,187 @@
|
|
|
1
|
+
// ============================================================================
|
|
2
|
+
// Value Object - Immutable Domain Objects
|
|
3
|
+
// ============================================================================
|
|
4
|
+
|
|
5
|
+
import { ValidationError } from "./validation-error";
|
|
6
|
+
import { IDomainEvent } from "./domain-event";
|
|
7
|
+
import {
|
|
8
|
+
VOHooks,
|
|
9
|
+
ValidationConfig,
|
|
10
|
+
StandardSchema,
|
|
11
|
+
EntityValidation,
|
|
12
|
+
} from "./types";
|
|
13
|
+
import { DEFAULT_VALIDATION_CONFIG } from "./constants";
|
|
14
|
+
|
|
15
|
+
// Helper to get static properties from constructor
|
|
16
|
+
function getStaticProperty<T>(
|
|
17
|
+
instance: any,
|
|
18
|
+
propertyName: string
|
|
19
|
+
): T | undefined {
|
|
20
|
+
return instance.constructor[propertyName];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export abstract class ValueObject<T> {
|
|
24
|
+
protected readonly props!: T;
|
|
25
|
+
private validationConfig: Required<ValidationConfig>;
|
|
26
|
+
private domainHooks?: VOHooks<T, any>;
|
|
27
|
+
private domainSchema?: StandardSchema<T>;
|
|
28
|
+
private domainEvents: IDomainEvent[] = [];
|
|
29
|
+
|
|
30
|
+
// Static properties that subclasses can override
|
|
31
|
+
protected static validation?: EntityValidation<any>;
|
|
32
|
+
protected static hooks?: VOHooks<any, any>;
|
|
33
|
+
|
|
34
|
+
constructor(props: T) {
|
|
35
|
+
// Get static configuration from subclass
|
|
36
|
+
const validation = getStaticProperty<EntityValidation<T>>(
|
|
37
|
+
this,
|
|
38
|
+
"validation"
|
|
39
|
+
);
|
|
40
|
+
const hooks = getStaticProperty<VOHooks<T, any>>(this, "hooks");
|
|
41
|
+
|
|
42
|
+
this.domainHooks = hooks;
|
|
43
|
+
|
|
44
|
+
if (validation?.schema) {
|
|
45
|
+
this.domainSchema = validation.schema;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
this.validationConfig = {
|
|
49
|
+
...DEFAULT_VALIDATION_CONFIG,
|
|
50
|
+
...validation?.config,
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
// Apply defaultValues
|
|
54
|
+
let finalProps = { ...props } as T;
|
|
55
|
+
if (hooks?.defaultValues) {
|
|
56
|
+
finalProps = { ...hooks.defaultValues, ...props } as T;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
// Validate schema on creation
|
|
60
|
+
if (this.domainSchema && this.validationConfig.onCreate) {
|
|
61
|
+
this.validateProps(finalProps);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// Set props (not frozen yet) so rules can access them
|
|
65
|
+
(this as any).props = finalProps;
|
|
66
|
+
|
|
67
|
+
// Execute rules (custom validations) - after props is set but before freezing
|
|
68
|
+
if (hooks?.rules) {
|
|
69
|
+
hooks.rules(this as any);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Now freeze the props for immutability
|
|
73
|
+
Object.freeze(this.props);
|
|
74
|
+
|
|
75
|
+
// Hook onCreate
|
|
76
|
+
if (hooks?.onCreate) {
|
|
77
|
+
hooks.onCreate(this as any);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
private validateProps(props: T): void {
|
|
82
|
+
if (!this.domainSchema) return;
|
|
83
|
+
|
|
84
|
+
const result = this.domainSchema["~standard"].validate(props);
|
|
85
|
+
|
|
86
|
+
if (result instanceof Promise) {
|
|
87
|
+
throw new Error(
|
|
88
|
+
"Async validation not supported in constructor. Use sync validation schema."
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
if (result.issues && result.issues.length > 0) {
|
|
93
|
+
const validationError = new ValidationError(
|
|
94
|
+
result.issues.map((issue) => ({
|
|
95
|
+
path: issue.path?.map((p) => this.extractPathKey(p)) || [],
|
|
96
|
+
message: issue.message,
|
|
97
|
+
}))
|
|
98
|
+
);
|
|
99
|
+
|
|
100
|
+
if (this.validationConfig.throwOnError) {
|
|
101
|
+
throw validationError;
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// If not throwing, store error for later retrieval
|
|
105
|
+
(this as any)._validationError = validationError;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
private extractPathKey(pathSegment: unknown): string {
|
|
110
|
+
if (pathSegment === null || pathSegment === undefined) {
|
|
111
|
+
return "";
|
|
112
|
+
}
|
|
113
|
+
// Handle PropertyKey (string | number | symbol)
|
|
114
|
+
if (typeof pathSegment === "string" || typeof pathSegment === "number") {
|
|
115
|
+
return String(pathSegment);
|
|
116
|
+
}
|
|
117
|
+
if (typeof pathSegment === "symbol") {
|
|
118
|
+
return pathSegment.toString();
|
|
119
|
+
}
|
|
120
|
+
// Handle object with 'key' property (Zod's PathSegment)
|
|
121
|
+
if (typeof pathSegment === "object" && "key" in pathSegment) {
|
|
122
|
+
return String((pathSegment as { key: unknown }).key);
|
|
123
|
+
}
|
|
124
|
+
// Fallback
|
|
125
|
+
return String(pathSegment);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Check if value object has validation errors (when throwOnError is false)
|
|
130
|
+
*/
|
|
131
|
+
get hasValidationErrors(): boolean {
|
|
132
|
+
return !!(this as any)._validationError;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Get validation errors (when throwOnError is false)
|
|
137
|
+
*/
|
|
138
|
+
get validationErrors(): ValidationError | undefined {
|
|
139
|
+
return (this as any)._validationError;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
equals(other: ValueObject<T>): boolean {
|
|
143
|
+
if (!other || !(other instanceof ValueObject)) return false;
|
|
144
|
+
return JSON.stringify(this.props) === JSON.stringify(other.props);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Add a domain event to this value object
|
|
149
|
+
*/
|
|
150
|
+
protected addDomainEvent(event: IDomainEvent): void {
|
|
151
|
+
this.domainEvents.push(event);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Get all uncommitted domain events
|
|
156
|
+
*/
|
|
157
|
+
getUncommittedEvents(): IDomainEvent[] {
|
|
158
|
+
return [...this.domainEvents];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Clear all domain events (call after publishing)
|
|
163
|
+
*/
|
|
164
|
+
clearEvents(): void {
|
|
165
|
+
this.domainEvents = [];
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Check if value object has uncommitted events
|
|
170
|
+
*/
|
|
171
|
+
hasUncommittedEvents(): boolean {
|
|
172
|
+
return this.domainEvents.length > 0;
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
toJson(): T {
|
|
176
|
+
return { ...this.props };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Create a new ValueObject with updated properties
|
|
181
|
+
* Since ValueObjects are immutable, this returns a new instance
|
|
182
|
+
*/
|
|
183
|
+
protected clone(updates: Partial<T>): this {
|
|
184
|
+
const Constructor = this.constructor as new (props: T) => this;
|
|
185
|
+
return new Constructor({ ...this.props, ...updates });
|
|
186
|
+
}
|
|
187
|
+
}
|
|
@@ -0,0 +1,432 @@
|
|
|
1
|
+
import { Pagination } from "../src";
|
|
2
|
+
import { Criteria } from "../src/criteria";
|
|
3
|
+
import { PaginatedResult } from "../src/paginated-result";
|
|
4
|
+
import { Post } from "./utils";
|
|
5
|
+
|
|
6
|
+
interface TestUser {
|
|
7
|
+
id: string;
|
|
8
|
+
name: string;
|
|
9
|
+
email: string;
|
|
10
|
+
age: number;
|
|
11
|
+
status: "active" | "inactive";
|
|
12
|
+
createdAt: Date;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const testUsers: TestUser[] = [
|
|
16
|
+
{
|
|
17
|
+
id: "1",
|
|
18
|
+
name: "Alice",
|
|
19
|
+
email: "alice@example.com",
|
|
20
|
+
age: 25,
|
|
21
|
+
status: "active",
|
|
22
|
+
createdAt: new Date("2024-01-01"),
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
id: "2",
|
|
26
|
+
name: "Bob",
|
|
27
|
+
email: "bob@example.com",
|
|
28
|
+
age: 30,
|
|
29
|
+
status: "active",
|
|
30
|
+
createdAt: new Date("2024-02-01"),
|
|
31
|
+
},
|
|
32
|
+
{
|
|
33
|
+
id: "3",
|
|
34
|
+
name: "Charlie",
|
|
35
|
+
email: "charlie@test.com",
|
|
36
|
+
age: 35,
|
|
37
|
+
status: "inactive",
|
|
38
|
+
createdAt: new Date("2024-03-01"),
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
id: "4",
|
|
42
|
+
name: "Diana",
|
|
43
|
+
email: "diana@example.com",
|
|
44
|
+
age: 28,
|
|
45
|
+
status: "active",
|
|
46
|
+
createdAt: new Date("2024-04-01"),
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
id: "5",
|
|
50
|
+
name: "Eve",
|
|
51
|
+
email: "eve@test.com",
|
|
52
|
+
age: 22,
|
|
53
|
+
status: "inactive",
|
|
54
|
+
createdAt: new Date("2024-05-01"),
|
|
55
|
+
},
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
describe("Criteria", () => {
|
|
59
|
+
describe("Fluent API", () => {
|
|
60
|
+
it("should create empty criteria", () => {
|
|
61
|
+
const criteria = Criteria.create<TestUser>();
|
|
62
|
+
expect(criteria.hasFilters()).toBe(false);
|
|
63
|
+
expect(criteria.hasOrders()).toBe(false);
|
|
64
|
+
expect(criteria.hasPagination()).toBe(true); // Default pagination is set
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should chain methods fluently", () => {
|
|
68
|
+
const criteria = Criteria.create<TestUser>()
|
|
69
|
+
.where("status", "equals", "active")
|
|
70
|
+
.where("age", "greaterThan", 18)
|
|
71
|
+
.orderBy("name", "asc")
|
|
72
|
+
.paginate(1, 10);
|
|
73
|
+
|
|
74
|
+
expect(criteria.hasFilters()).toBe(true);
|
|
75
|
+
expect(criteria.hasOrders()).toBe(true);
|
|
76
|
+
expect(criteria.hasPagination()).toBe(true);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it("should use shorthand methods", () => {
|
|
80
|
+
const criteria = Criteria.create<TestUser>()
|
|
81
|
+
.whereEquals("status", "active")
|
|
82
|
+
.whereContains("name", "ali")
|
|
83
|
+
.whereIn("age", [25, 30, 35])
|
|
84
|
+
.orderByDesc("createdAt");
|
|
85
|
+
|
|
86
|
+
const filters = criteria.getFilters();
|
|
87
|
+
expect(filters).toHaveLength(3);
|
|
88
|
+
expect(filters[0].operator).toBe("equals");
|
|
89
|
+
expect(filters[1].operator).toBe("contains");
|
|
90
|
+
expect(filters[2].operator).toBe("in");
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("Filtering", () => {
|
|
95
|
+
it("should filter by equals", () => {
|
|
96
|
+
const criteria = Criteria.create<TestUser>().whereEquals(
|
|
97
|
+
"status",
|
|
98
|
+
"active"
|
|
99
|
+
);
|
|
100
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
101
|
+
expect(result.data).toHaveLength(3);
|
|
102
|
+
expect(result.data.every((u) => u.status === "active")).toBe(true);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it("should filter by notEquals", () => {
|
|
106
|
+
const criteria = Criteria.create<TestUser>().where(
|
|
107
|
+
"status",
|
|
108
|
+
"notEquals",
|
|
109
|
+
"active"
|
|
110
|
+
);
|
|
111
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
112
|
+
expect(result.data).toHaveLength(2);
|
|
113
|
+
expect(result.data.every((u) => u.status === "inactive")).toBe(true);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("should filter by greaterThan", () => {
|
|
117
|
+
const criteria = Criteria.create<TestUser>().where(
|
|
118
|
+
"age",
|
|
119
|
+
"greaterThan",
|
|
120
|
+
28
|
|
121
|
+
);
|
|
122
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
123
|
+
expect(result.data).toHaveLength(2);
|
|
124
|
+
expect(result.data.map((u) => u.name)).toEqual(["Bob", "Charlie"]);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it("should filter by lessThan", () => {
|
|
128
|
+
const criteria = Criteria.create<TestUser>().where("age", "lessThan", 26);
|
|
129
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
130
|
+
expect(result.data).toHaveLength(2);
|
|
131
|
+
expect(result.data.map((u) => u.name)).toEqual(["Alice", "Eve"]);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should filter by contains", () => {
|
|
135
|
+
const criteria = Criteria.create<TestUser>().whereContains(
|
|
136
|
+
"email",
|
|
137
|
+
"example"
|
|
138
|
+
);
|
|
139
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
140
|
+
expect(result.data).toHaveLength(3);
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
it("should filter by startsWith", () => {
|
|
144
|
+
const criteria = Criteria.create<TestUser>().where(
|
|
145
|
+
"name",
|
|
146
|
+
"startsWith",
|
|
147
|
+
"A"
|
|
148
|
+
);
|
|
149
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
150
|
+
expect(result.data).toHaveLength(1);
|
|
151
|
+
expect(result.data[0].name).toBe("Alice");
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should filter by endsWith", () => {
|
|
155
|
+
const criteria = Criteria.create<TestUser>().where(
|
|
156
|
+
"email",
|
|
157
|
+
"endsWith",
|
|
158
|
+
".com"
|
|
159
|
+
);
|
|
160
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
161
|
+
expect(result.data).toHaveLength(5);
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
it("should filter by in", () => {
|
|
165
|
+
const criteria = Criteria.create<TestUser>().whereIn("age", [25, 35]);
|
|
166
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
167
|
+
expect(result.data).toHaveLength(2);
|
|
168
|
+
expect(result.data.map((u) => u.name)).toEqual(["Alice", "Charlie"]);
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("should filter by notIn", () => {
|
|
172
|
+
const criteria = Criteria.create<TestUser>().where(
|
|
173
|
+
"age",
|
|
174
|
+
"notIn",
|
|
175
|
+
[25, 35]
|
|
176
|
+
);
|
|
177
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
178
|
+
expect(result.data).toHaveLength(3);
|
|
179
|
+
});
|
|
180
|
+
|
|
181
|
+
it("should filter by between", () => {
|
|
182
|
+
const criteria = Criteria.create<TestUser>().whereBetween("age", 25, 30);
|
|
183
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
184
|
+
expect(result.data).toHaveLength(3);
|
|
185
|
+
expect(result.data.map((u) => u.name)).toEqual(["Alice", "Bob", "Diana"]);
|
|
186
|
+
});
|
|
187
|
+
|
|
188
|
+
it("should combine multiple filters", () => {
|
|
189
|
+
const criteria = Criteria.create<TestUser>()
|
|
190
|
+
.whereEquals("status", "active")
|
|
191
|
+
.where("age", "greaterThan", 25);
|
|
192
|
+
|
|
193
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
194
|
+
expect(result.data).toHaveLength(2);
|
|
195
|
+
expect(result.data.map((u) => u.name)).toEqual(["Bob", "Diana"]);
|
|
196
|
+
});
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
describe("Ordering", () => {
|
|
200
|
+
it("should order by ascending", () => {
|
|
201
|
+
const criteria = Criteria.create<TestUser>().orderByAsc("age");
|
|
202
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
203
|
+
const ages = result.data.map((u) => u.age);
|
|
204
|
+
expect(ages).toEqual([22, 25, 28, 30, 35]);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it("should order by descending", () => {
|
|
208
|
+
const criteria = Criteria.create<TestUser>().orderByDesc("age");
|
|
209
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
210
|
+
const ages = result.data.map((u) => u.age);
|
|
211
|
+
expect(ages).toEqual([35, 30, 28, 25, 22]);
|
|
212
|
+
});
|
|
213
|
+
|
|
214
|
+
it("should order by string field", () => {
|
|
215
|
+
const criteria = Criteria.create<TestUser>().orderByAsc("name");
|
|
216
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
217
|
+
const names = result.data.map((u) => u.name);
|
|
218
|
+
expect(names).toEqual(["Alice", "Bob", "Charlie", "Diana", "Eve"]);
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("should order by date field", () => {
|
|
222
|
+
const criteria = Criteria.create<TestUser>().orderByDesc("createdAt");
|
|
223
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
224
|
+
const names = result.data.map((u) => u.name);
|
|
225
|
+
expect(names).toEqual(["Eve", "Diana", "Charlie", "Bob", "Alice"]);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe("Pagination", () => {
|
|
230
|
+
it("should paginate results", () => {
|
|
231
|
+
const criteria = Criteria.create<TestUser>().paginate(1, 2);
|
|
232
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
233
|
+
|
|
234
|
+
expect(result.data).toHaveLength(2);
|
|
235
|
+
expect(result.meta.page).toBe(1);
|
|
236
|
+
expect(result.meta.limit).toBe(2);
|
|
237
|
+
expect(result.meta.total).toBe(5);
|
|
238
|
+
expect(result.meta.totalPages).toBe(3);
|
|
239
|
+
expect(result.meta.hasNext).toBe(true);
|
|
240
|
+
expect(result.meta.hasPrevious).toBe(false);
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it("should get second page", () => {
|
|
244
|
+
const criteria = Criteria.create<TestUser>().paginate(2, 2);
|
|
245
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
246
|
+
|
|
247
|
+
expect(result.data).toHaveLength(2);
|
|
248
|
+
expect(result.data.map((u) => u.name)).toEqual(["Charlie", "Diana"]);
|
|
249
|
+
expect(result.meta.page).toBe(2);
|
|
250
|
+
expect(result.meta.hasNext).toBe(true);
|
|
251
|
+
expect(result.meta.hasPrevious).toBe(true);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it("should get last page", () => {
|
|
255
|
+
const criteria = Criteria.create<TestUser>().paginate(3, 2);
|
|
256
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
257
|
+
|
|
258
|
+
expect(result.data).toHaveLength(1);
|
|
259
|
+
expect(result.data[0].name).toBe("Eve");
|
|
260
|
+
expect(result.meta.hasNext).toBe(false);
|
|
261
|
+
expect(result.meta.hasPrevious).toBe(true);
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("should handle empty page", () => {
|
|
265
|
+
const criteria = Criteria.create<TestUser>().paginate(10, 2);
|
|
266
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
267
|
+
|
|
268
|
+
expect(result.data).toHaveLength(0);
|
|
269
|
+
expect(result.meta.total).toBe(5);
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
it("should apply limit shorthand", () => {
|
|
273
|
+
const criteria = Criteria.create<TestUser>().limit(3);
|
|
274
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
275
|
+
|
|
276
|
+
expect(result.data).toHaveLength(3);
|
|
277
|
+
expect(result.meta.page).toBe(1);
|
|
278
|
+
});
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
describe("Combined Operations", () => {
|
|
282
|
+
it("should filter, order, and paginate", () => {
|
|
283
|
+
const criteria = Criteria.create<TestUser>()
|
|
284
|
+
.whereEquals("status", "active")
|
|
285
|
+
.orderByDesc("age")
|
|
286
|
+
.paginate(1, 2);
|
|
287
|
+
|
|
288
|
+
const result = PaginatedResult.fromArray(testUsers, criteria);
|
|
289
|
+
|
|
290
|
+
expect(result.data).toHaveLength(2);
|
|
291
|
+
expect(result.data.map((u) => u.name)).toEqual(["Bob", "Diana"]);
|
|
292
|
+
expect(result.meta.total).toBe(3); // Total active users
|
|
293
|
+
expect(result.meta.totalPages).toBe(2);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe("Serialization", () => {
|
|
298
|
+
it("should convert to object", () => {
|
|
299
|
+
const criteria = Criteria.create<TestUser>()
|
|
300
|
+
.whereEquals("status", "active")
|
|
301
|
+
.orderByDesc("age")
|
|
302
|
+
.paginate(1, 10);
|
|
303
|
+
|
|
304
|
+
const obj = criteria.toJSON();
|
|
305
|
+
|
|
306
|
+
expect(obj.filters).toHaveLength(1);
|
|
307
|
+
expect(obj.orders).toHaveLength(1);
|
|
308
|
+
expect(obj.pagination).toBeDefined();
|
|
309
|
+
expect(obj.pagination?.page).toBe(1);
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it("should create from object", () => {
|
|
313
|
+
const criteria = Criteria.fromObject<TestUser>({
|
|
314
|
+
filters: [{ field: "status", operator: "equals", value: "active" }],
|
|
315
|
+
orders: [{ field: "age", direction: "desc" }],
|
|
316
|
+
pagination: { page: 1, limit: 10, offset: 0 },
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
expect(criteria.getFilters()).toHaveLength(1);
|
|
320
|
+
expect(criteria.getOrders()).toHaveLength(1);
|
|
321
|
+
expect(criteria.getPagination()?.page).toBe(1);
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("should clone criteria", () => {
|
|
325
|
+
const original = Criteria.create<TestUser>()
|
|
326
|
+
.whereEquals("status", "active")
|
|
327
|
+
.orderByDesc("age");
|
|
328
|
+
|
|
329
|
+
const cloned = original.clone();
|
|
330
|
+
cloned.whereEquals("age", 30);
|
|
331
|
+
|
|
332
|
+
expect(original.getFilters()).toHaveLength(1);
|
|
333
|
+
expect(cloned.getFilters()).toHaveLength(2);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it("should deserialize pagination result with entities", () => {
|
|
337
|
+
const pagination: Pagination = { page: 1, limit: 10, offset: 0 };
|
|
338
|
+
|
|
339
|
+
const data = [
|
|
340
|
+
new Post({
|
|
341
|
+
title: "Post 1",
|
|
342
|
+
content: "Content 1",
|
|
343
|
+
likes: 1,
|
|
344
|
+
}),
|
|
345
|
+
new Post({
|
|
346
|
+
title: "Post 2",
|
|
347
|
+
content: "Content 2",
|
|
348
|
+
likes: 2,
|
|
349
|
+
}),
|
|
350
|
+
];
|
|
351
|
+
|
|
352
|
+
const total = data.length;
|
|
353
|
+
|
|
354
|
+
const paginationResult = PaginatedResult.create(data, pagination, total);
|
|
355
|
+
|
|
356
|
+
const result = paginationResult.toJSON();
|
|
357
|
+
|
|
358
|
+
expect(result.data).toHaveLength(2);
|
|
359
|
+
expect(result.data[0].title).toBe("Post 1");
|
|
360
|
+
expect(result.data[1].title).toBe("Post 2");
|
|
361
|
+
expect(result.meta.total).toBe(total);
|
|
362
|
+
expect(result.meta.totalPages).toBe(1);
|
|
363
|
+
expect(result.meta.page).toBe(1);
|
|
364
|
+
expect(result.meta.limit).toBe(10);
|
|
365
|
+
});
|
|
366
|
+
|
|
367
|
+
it("should deserialize pagination result with plain objects", () => {
|
|
368
|
+
const pagination: Pagination = { page: 1, limit: 10, offset: 0 };
|
|
369
|
+
const total = testUsers.length;
|
|
370
|
+
|
|
371
|
+
const paginationResult = PaginatedResult.create(
|
|
372
|
+
testUsers,
|
|
373
|
+
pagination,
|
|
374
|
+
total
|
|
375
|
+
);
|
|
376
|
+
|
|
377
|
+
const result = paginationResult.toJSON();
|
|
378
|
+
|
|
379
|
+
expect(result.data).toHaveLength(testUsers.length);
|
|
380
|
+
expect(result.data.map((u) => u.name)).toEqual(
|
|
381
|
+
testUsers.map((u) => u.name)
|
|
382
|
+
);
|
|
383
|
+
expect(result.meta.total).toBe(total);
|
|
384
|
+
expect(result.meta.totalPages).toBe(1);
|
|
385
|
+
expect(result.meta.page).toBe(1);
|
|
386
|
+
expect(result.meta.limit).toBe(10);
|
|
387
|
+
});
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
describe("Helper Functions", () => {
|
|
391
|
+
it("should create pagination meta", () => {
|
|
392
|
+
const pagination = { page: 2, limit: 10, offset: 10 };
|
|
393
|
+
const meta = PaginatedResult.createMeta(pagination, 45);
|
|
394
|
+
|
|
395
|
+
expect(meta.page).toBe(2);
|
|
396
|
+
expect(meta.limit).toBe(10);
|
|
397
|
+
expect(meta.total).toBe(45);
|
|
398
|
+
expect(meta.totalPages).toBe(5);
|
|
399
|
+
expect(meta.hasNext).toBe(true);
|
|
400
|
+
expect(meta.hasPrevious).toBe(true);
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
it("should create paginated result", () => {
|
|
404
|
+
const data = [{ id: "1" }, { id: "2" }];
|
|
405
|
+
const pagination = { page: 1, limit: 2, offset: 0 };
|
|
406
|
+
const result = PaginatedResult.create(data, pagination, data.length);
|
|
407
|
+
|
|
408
|
+
expect(result.data).toEqual(data);
|
|
409
|
+
expect(result.meta.total).toBe(data.length);
|
|
410
|
+
expect(result.meta.totalPages).toBe(1);
|
|
411
|
+
});
|
|
412
|
+
});
|
|
413
|
+
|
|
414
|
+
describe("Criteria from Query Params", () => {
|
|
415
|
+
it("should create criteria from query params", () => {
|
|
416
|
+
const queryParams = {
|
|
417
|
+
"status:equals": "active",
|
|
418
|
+
"age:greaterThan": "25",
|
|
419
|
+
orderBy: "age",
|
|
420
|
+
orderDirection: "desc",
|
|
421
|
+
page: "1",
|
|
422
|
+
limit: "2",
|
|
423
|
+
};
|
|
424
|
+
|
|
425
|
+
const criteria = Criteria.fromQueryParams<TestUser>(queryParams);
|
|
426
|
+
expect(criteria.getFilters()).toHaveLength(2);
|
|
427
|
+
expect(criteria.getOrders()).toHaveLength(1);
|
|
428
|
+
expect(criteria.getPagination()?.page).toBe(1);
|
|
429
|
+
expect(criteria.getPagination()?.limit).toBe(2);
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
});
|