@emkodev/emkore 1.0.3

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.
Files changed (48) hide show
  1. package/CHANGELOG.md +269 -0
  2. package/DEVELOPER_GUIDE.md +227 -0
  3. package/LICENSE +21 -0
  4. package/README.md +126 -0
  5. package/bun.lock +22 -0
  6. package/example/README.md +200 -0
  7. package/example/create-user.interactor.ts +88 -0
  8. package/example/dto/user.dto.ts +34 -0
  9. package/example/entity/user.entity.ts +54 -0
  10. package/example/index.ts +18 -0
  11. package/example/interface/create-user.usecase.ts +93 -0
  12. package/example/interface/user.repository.ts +23 -0
  13. package/mod.ts +1 -0
  14. package/package.json +32 -0
  15. package/src/common/abstract.actor.ts +59 -0
  16. package/src/common/abstract.entity.ts +59 -0
  17. package/src/common/abstract.interceptor.ts +17 -0
  18. package/src/common/abstract.repository.ts +162 -0
  19. package/src/common/abstract.usecase.ts +113 -0
  20. package/src/common/config/config-registry.ts +190 -0
  21. package/src/common/config/config-section.ts +106 -0
  22. package/src/common/exception/authorization-exception.ts +28 -0
  23. package/src/common/exception/repository-exception.ts +46 -0
  24. package/src/common/interceptor/audit-log.interceptor.ts +181 -0
  25. package/src/common/interceptor/authorization.interceptor.ts +252 -0
  26. package/src/common/interceptor/performance.interceptor.ts +101 -0
  27. package/src/common/llm/api-definition.type.ts +185 -0
  28. package/src/common/pattern/unit-of-work.ts +78 -0
  29. package/src/common/platform/env.ts +38 -0
  30. package/src/common/registry/usecase-registry.ts +80 -0
  31. package/src/common/type/interceptor-context.type.ts +25 -0
  32. package/src/common/type/json-schema.type.ts +80 -0
  33. package/src/common/type/json.type.ts +5 -0
  34. package/src/common/type/lowercase.type.ts +48 -0
  35. package/src/common/type/metadata.type.ts +5 -0
  36. package/src/common/type/money.class.ts +384 -0
  37. package/src/common/type/permission.type.ts +43 -0
  38. package/src/common/validation/validation-result.ts +52 -0
  39. package/src/common/validation/validators.ts +441 -0
  40. package/src/index.ts +95 -0
  41. package/test/unit/abstract-actor.test.ts +608 -0
  42. package/test/unit/actor.test.ts +89 -0
  43. package/test/unit/api-definition.test.ts +628 -0
  44. package/test/unit/authorization.test.ts +101 -0
  45. package/test/unit/entity.test.ts +95 -0
  46. package/test/unit/money.test.ts +480 -0
  47. package/test/unit/validation.test.ts +138 -0
  48. package/tsconfig.json +18 -0
@@ -0,0 +1,200 @@
1
+ # Emkore Example - Modern Production Patterns
2
+
3
+ This example demonstrates the recommended patterns for using emkore in
4
+ production, based on real-world usage in the hardkore codebase.
5
+
6
+ ## Structure
7
+
8
+ ```
9
+ example/
10
+ ├── interface/ # Use case interfaces (contracts)
11
+ │ ├── create-user.usecase.ts
12
+ │ └── user.repository.ts
13
+ ├── dto/ # Data Transfer Objects
14
+ │ └── user.dto.ts
15
+ ├── entity/ # Business entities
16
+ │ └── user.entity.ts
17
+ └── create-user.interactor.ts # Use case implementation
18
+ ```
19
+
20
+ ## Key Patterns
21
+
22
+ ### 1. Import Style
23
+
24
+ When using emkore in your project, import as a namespace from the published
25
+ package:
26
+
27
+ ```typescript
28
+ import * as emkore from "@emkodev/emkore";
29
+ ```
30
+
31
+ Within the emkore package examples, we use relative imports:
32
+
33
+ ```typescript
34
+ import * as emkore from "../mod.ts"; // For interactor files
35
+ import * as emkore from "../../mod.ts"; // For nested files
36
+ ```
37
+
38
+ ### 2. Input/Output Types
39
+
40
+ - **Input**: Use `interface` for input types
41
+ - **Output**: Use type alias pointing to the DTO
42
+ - **Validation**: Use separate validation functions
43
+
44
+ ```typescript
45
+ export interface CreateUserInput {
46
+ readonly name: string;
47
+ readonly email: string;
48
+ readonly owner: emkore.ResourceScope;
49
+ }
50
+
51
+ export type CreateUserOutput = UserDto;
52
+
53
+ export const validateCreateUserInput = (
54
+ input: CreateUserInput,
55
+ ): emkore.ValidationResult =>
56
+ new emkore.ValidatorBuilder()
57
+ .string("name", input.name, { required: true })
58
+ .email("email", input.email, true)
59
+ .build();
60
+ ```
61
+
62
+ ### 3. Entity Pattern
63
+
64
+ Entities extend `emkore.Entity` and provide:
65
+
66
+ - Business logic methods
67
+ - `toDto()` for persistence
68
+ - `isValid()` for validation
69
+
70
+ ```typescript
71
+ export class UserEntity extends emkore.Entity {
72
+ readonly type = "user";
73
+
74
+ override toDto(): UserDto {
75
+ return {
76
+ ...super.toDto(),
77
+ name: this.name,
78
+ email: this.email,
79
+ };
80
+ }
81
+ }
82
+ ```
83
+
84
+ ### 4. DTO Pattern
85
+
86
+ DTOs extend `emkore.Dto` and represent the persistence structure:
87
+
88
+ ```typescript
89
+ export interface UserDto extends emkore.Dto {
90
+ readonly name: string;
91
+ readonly email: string;
92
+ readonly role?: string;
93
+ }
94
+ ```
95
+
96
+ ### 5. Use Case Pattern
97
+
98
+ Use cases define the contract with:
99
+
100
+ - `override` keyword for abstract properties
101
+ - Proper API definitions for RPC/documentation
102
+ - Resource scope support
103
+
104
+ ```typescript
105
+ export abstract class CreateUserUsecase extends emkore.Usecase<
106
+ CreateUserInput,
107
+ CreateUserOutput
108
+ > {
109
+ override usecaseName: emkore.UsecaseName = ["create", "user"];
110
+
111
+ override apiDefinition: emkore.ApiDefinition = {
112
+ name: this.usecaseName.join("_"),
113
+ description: "Create a new user in the system",
114
+ // ...
115
+ };
116
+
117
+ override get requiredPermissions(): emkore.Permission[] {
118
+ return [{ action: "create", resource: "user" }];
119
+ }
120
+ }
121
+ ```
122
+
123
+ ### 6. Interactor Pattern
124
+
125
+ Interactors implement the business logic with:
126
+
127
+ - Input validation
128
+ - Entity creation
129
+ - Repository operations within transactions
130
+ - DTO validation
131
+
132
+ ```typescript
133
+ export class CreateUserInteractor extends CreateUserUsecase {
134
+ protected async _execute(
135
+ actor: emkore.Actor,
136
+ input: CreateUserInput,
137
+ ): Promise<CreateUserOutput> {
138
+ // 1. Validate input
139
+ const validation = validateCreateUserInput(input);
140
+ if (!validation.isValid) {
141
+ throw new emkore.ValidationException(validation.errors.join(", "));
142
+ }
143
+
144
+ // 2. Create entity
145
+ const entity = new UserEntity({
146
+ id: emkore.Entity.generateId("user"),
147
+ ownerId: input.owner === emkore.ResourceScope.BUSINESS
148
+ ? actor.businessId
149
+ : actor.id,
150
+ // ...
151
+ });
152
+
153
+ // 3. Persist with transaction
154
+ return await this._uowFactory.transaction(actor.businessId, async (uow) => {
155
+ const createdDto = await uow.userRepository.create(entity.toDto());
156
+
157
+ // 4. Validate returned DTO
158
+ const dtoValidation = validateUserDto(createdDto);
159
+ if (!dtoValidation.isValid) {
160
+ throw new emkore.ValidationException("Invalid DTO");
161
+ }
162
+
163
+ return createdDto;
164
+ });
165
+ }
166
+ }
167
+ ```
168
+
169
+ ## Best Practices
170
+
171
+ 1. **Always validate input** before processing
172
+ 2. **Use ResourceScope** for proper ownership (USER vs BUSINESS)
173
+ 3. **Validate DTOs** returned from repositories
174
+ 4. **Use transactions** for all database operations
175
+ 5. **Generate IDs** with proper prefixes using `Entity.generateId("prefix")`
176
+ 6. **Keep entities pure** - business logic only, no persistence concerns
177
+ 7. **Use the override keyword** for all abstract properties
178
+
179
+ ## Running the Example
180
+
181
+ This example is meant to demonstrate patterns. To use it in your application:
182
+
183
+ 1. Implement a concrete `UnitOfWorkFactory` for your database
184
+ 2. Implement repositories for your storage layer
185
+ 3. Wire up dependencies with your DI container
186
+ 4. Register use cases in your registry for RPC exposure
187
+
188
+ ## Migration from Old Patterns
189
+
190
+ If you're using older patterns:
191
+
192
+ | Old Pattern | New Pattern |
193
+ | ------------------------- | ------------------------------------------- |
194
+ | Relative imports | `import * as emkore from "@emkodev/emkore"` |
195
+ | Abstract class for Input | `interface` for Input |
196
+ | Abstract class for Output | Type alias to DTO |
197
+ | Static validation methods | Separate validation functions |
198
+ | No Entity class | Entity extends `emkore.Entity` |
199
+ | Direct DTO manipulation | Entity with `toDto()` method |
200
+ | No `override` keyword | Use `override` for all abstract properties |
@@ -0,0 +1,88 @@
1
+ import * as emkore from "../mod.ts";
2
+ import { UserEntity } from "./entity/user.entity.ts";
3
+ import { validateUserDto } from "./dto/user.dto.ts";
4
+ import {
5
+ type CreateUserInput,
6
+ type CreateUserOutput,
7
+ CreateUserUsecase,
8
+ validateCreateUserInput,
9
+ } from "./interface/create-user.usecase.ts";
10
+ import type { UserRepository } from "./interface/user.repository.ts";
11
+
12
+ // Example concrete UnitOfWorkFactory implementation
13
+ interface ExampleUnitOfWork extends emkore.UnitOfWork {
14
+ userRepository: UserRepository;
15
+ }
16
+
17
+ interface ExampleUnitOfWorkFactory extends emkore.UnitOfWorkFactory {
18
+ transaction<T>(
19
+ businessId: string,
20
+ callback: (uow: ExampleUnitOfWork) => Promise<T>,
21
+ ): Promise<T>;
22
+ }
23
+
24
+ /**
25
+ * Concrete implementation of CreateUserUsecase.
26
+ *
27
+ * This Interactor:
28
+ * 1. Extends the abstract CreateUserUsecase (inherits all metadata)
29
+ * 2. Accepts dependencies through constructor (UnitOfWorkFactory)
30
+ * 3. Implements only the _execute method with HOW to create a user
31
+ * 4. Uses UoW pattern for transaction management
32
+ * 5. Follows production patterns from hardkore codebase
33
+ */
34
+ export class CreateUserInteractor extends CreateUserUsecase {
35
+ constructor(private readonly _uowFactory: ExampleUnitOfWorkFactory) {
36
+ super();
37
+ }
38
+
39
+ protected async _execute(
40
+ actor: emkore.Actor,
41
+ input: CreateUserInput,
42
+ ): Promise<CreateUserOutput> {
43
+ // Validate input
44
+ const validation = validateCreateUserInput(input);
45
+ if (!validation.isValid) {
46
+ throw new emkore.ValidationException(validation.errors.join(", "));
47
+ }
48
+
49
+ // Create entity
50
+ const entity = new UserEntity({
51
+ id: emkore.Entity.generateId("user"),
52
+ ownerId: input.owner === emkore.ResourceScope.BUSINESS
53
+ ? actor.businessId
54
+ : actor.id,
55
+ createdAt: new Date().toISOString(),
56
+ updatedAt: new Date().toISOString(),
57
+ name: input.name,
58
+ email: input.email,
59
+ ...(input.role && { role: input.role }),
60
+ });
61
+
62
+ // Persist via repository using transaction
63
+ return await this._uowFactory.transaction(actor.businessId, async (uow) => {
64
+ // Check for existing email
65
+ const existingUser = await uow.userRepository.findByEmail(input.email);
66
+ if (existingUser) {
67
+ throw new emkore.ValidationException(
68
+ `Email ${input.email} already exists`,
69
+ );
70
+ }
71
+
72
+ // Create the user
73
+ const createdDto = await uow.userRepository.create(entity.toDto());
74
+
75
+ // Validate the returned DTO
76
+ const dtoValidation = validateUserDto(createdDto);
77
+ if (!dtoValidation.isValid) {
78
+ throw new emkore.ValidationException(
79
+ `Invalid DTO returned from repository: ${
80
+ dtoValidation.errors.join(", ")
81
+ }`,
82
+ );
83
+ }
84
+
85
+ return createdDto;
86
+ });
87
+ }
88
+ }
@@ -0,0 +1,34 @@
1
+ import * as emkore from "../../mod.ts";
2
+
3
+ /**
4
+ * User Data Transfer Object.
5
+ * This represents the data structure for persistence.
6
+ */
7
+ export interface UserDto extends emkore.Dto {
8
+ readonly name: string;
9
+ readonly email: string;
10
+ readonly role?: string;
11
+ }
12
+
13
+ /**
14
+ * Validates a User DTO.
15
+ */
16
+ export const validateUserDto = (dto: UserDto): emkore.ValidationResult =>
17
+ new emkore.ValidatorBuilder()
18
+ .entityId("id", dto.id, "user", true)
19
+ .string("ownerId", dto.ownerId, { required: true })
20
+ .string("name", dto.name, {
21
+ required: true,
22
+ minLength: 1,
23
+ maxLength: 100,
24
+ })
25
+ .email("email", dto.email, true)
26
+ .string("role", dto.role, { required: false })
27
+ .dateTime("createdAt", new Date(dto.createdAt), { required: true })
28
+ .dateTime("updatedAt", new Date(dto.updatedAt), { required: true })
29
+ .dateTime(
30
+ "deletedAt",
31
+ dto.deletedAt ? new Date(dto.deletedAt) : undefined,
32
+ { required: false },
33
+ )
34
+ .build();
@@ -0,0 +1,54 @@
1
+ import * as emkore from "../../mod.ts";
2
+ import { type UserDto, validateUserDto } from "../dto/user.dto.ts";
3
+
4
+ /**
5
+ * User Entity class.
6
+ * This represents the business object with behavior.
7
+ */
8
+ export class UserEntity extends emkore.Entity {
9
+ readonly type = "user";
10
+
11
+ readonly name: string;
12
+ readonly email: string;
13
+ readonly role?: string;
14
+
15
+ constructor(dto: UserDto) {
16
+ super(dto);
17
+ this.name = dto.name;
18
+ this.email = dto.email;
19
+ if (dto.role) this.role = dto.role;
20
+ }
21
+
22
+ /**
23
+ * Converts the entity back to a DTO for persistence.
24
+ */
25
+ override toDto(): UserDto {
26
+ return {
27
+ ...super.toDto(),
28
+ name: this.name,
29
+ email: this.email,
30
+ ...(this.role && { role: this.role }),
31
+ };
32
+ }
33
+
34
+ /**
35
+ * Validates the entity's data.
36
+ */
37
+ isValid(): boolean {
38
+ return validateUserDto(this.toDto()).isValid;
39
+ }
40
+
41
+ /**
42
+ * Business logic example: Check if user has admin role.
43
+ */
44
+ isAdmin(): boolean {
45
+ return this.role === "admin";
46
+ }
47
+
48
+ /**
49
+ * Business logic example: Format display name.
50
+ */
51
+ getDisplayName(): string {
52
+ return this.name || this.email.split("@")[0];
53
+ }
54
+ }
@@ -0,0 +1,18 @@
1
+ /**
2
+ * Example exports demonstrating modern emkore patterns.
3
+ *
4
+ * This example shows production-ready patterns based on real-world usage.
5
+ */
6
+
7
+ // DTOs
8
+ export * from "./dto/user.dto.ts";
9
+
10
+ // Entities
11
+ export * from "./entity/user.entity.ts";
12
+
13
+ // Interfaces
14
+ export * from "./interface/create-user.usecase.ts";
15
+ export * from "./interface/user.repository.ts";
16
+
17
+ // Interactors
18
+ export * from "./create-user.interactor.ts";
@@ -0,0 +1,93 @@
1
+ import * as emkore from "../../mod.ts";
2
+ import type { UserDto } from "../dto/user.dto.ts";
3
+
4
+ /**
5
+ * Input data for creating a new user.
6
+ */
7
+ export interface CreateUserInput {
8
+ readonly name: string;
9
+ readonly email: string;
10
+ readonly role?: string;
11
+ readonly owner: emkore.ResourceScope;
12
+ }
13
+
14
+ /**
15
+ * Output data for create operation.
16
+ * Returns plain DTO object, not Entity class.
17
+ */
18
+ export type CreateUserOutput = UserDto;
19
+
20
+ /**
21
+ * Validates the create user input.
22
+ */
23
+ export const validateCreateUserInput = (
24
+ input: CreateUserInput,
25
+ ): emkore.ValidationResult =>
26
+ new emkore.ValidatorBuilder()
27
+ .string("name", input.name, {
28
+ required: true,
29
+ minLength: 1,
30
+ maxLength: 100,
31
+ })
32
+ .email("email", input.email, true)
33
+ .string("role", input.role, { required: false })
34
+ .enum("owner", input.owner, Object.values(emkore.ResourceScope))
35
+ .build();
36
+
37
+ /**
38
+ * Abstract Usecase defining the contract for creating a user.
39
+ * This defines WHAT the usecase does, not HOW it does it.
40
+ */
41
+ export abstract class CreateUserUsecase extends emkore.Usecase<
42
+ CreateUserInput,
43
+ CreateUserOutput
44
+ > {
45
+ override usecaseName: emkore.UsecaseName = ["create", "user"];
46
+
47
+ override apiDefinition: emkore.ApiDefinition = {
48
+ name: this.usecaseName.join("_"),
49
+ description: "Create a new user in the system",
50
+ parameters: [
51
+ {
52
+ name: "name",
53
+ type: "string",
54
+ description: "User full name",
55
+ isRequired: true,
56
+ },
57
+ {
58
+ name: "email",
59
+ type: "string",
60
+ description: "User email address",
61
+ isRequired: true,
62
+ },
63
+ {
64
+ name: "role",
65
+ type: "string",
66
+ description: "Optional user role",
67
+ isRequired: false,
68
+ },
69
+ {
70
+ name: "owner",
71
+ type: "string",
72
+ description: `Resource scope owner (${
73
+ Object.values(emkore.ResourceScope).join(", ")
74
+ })`,
75
+ isRequired: true,
76
+ },
77
+ ],
78
+ returns: {
79
+ type: "object",
80
+ description:
81
+ "Result containing the created user with id, name, email, and timestamps",
82
+ },
83
+ };
84
+
85
+ override get requiredPermissions(): emkore.Permission[] {
86
+ return [
87
+ {
88
+ action: "create",
89
+ resource: "user",
90
+ },
91
+ ];
92
+ }
93
+ }
@@ -0,0 +1,23 @@
1
+ import type * as emkore from "../../mod.ts";
2
+ import type { UserDto } from "../dto/user.dto.ts";
3
+
4
+ /**
5
+ * User Repository interface.
6
+ * Extends the base repository with user-specific methods.
7
+ */
8
+ export interface UserRepository extends emkore.Repository<UserDto> {
9
+ /**
10
+ * Find a user by email address.
11
+ */
12
+ findByEmail(email: string): Promise<UserDto | null>;
13
+
14
+ /**
15
+ * Find users by role.
16
+ */
17
+ findByRole(role: string): Promise<UserDto[]>;
18
+
19
+ /**
20
+ * Check if email exists in the system.
21
+ */
22
+ emailExists(email: string): Promise<boolean>;
23
+ }
package/mod.ts ADDED
@@ -0,0 +1 @@
1
+ export * from "./src/index.ts";
package/package.json ADDED
@@ -0,0 +1,32 @@
1
+ {
2
+ "name": "@emkodev/emkore",
3
+ "version": "1.0.3",
4
+ "description": "A TypeScript foundation framework for building robust, type-safe business applications with clean architecture patterns",
5
+ "license": "MIT",
6
+ "author": "emko.dev",
7
+ "type": "module",
8
+ "keywords": [
9
+ "typescript",
10
+ "clean-architecture",
11
+ "business-logic",
12
+ "framework",
13
+ "patterns"
14
+ ],
15
+ "repository": {
16
+ "type": "git",
17
+ "url": "git+https://github.com/emkore/emkore.git"
18
+ },
19
+ "exports": {
20
+ ".": "./mod.ts"
21
+ },
22
+ "scripts": {
23
+ "test": "bun test",
24
+ "check": "bun run tsc --noEmit",
25
+ "lint": "biome lint ./src ./test",
26
+ "fmt": "biome format --write ./src ./test"
27
+ },
28
+ "devDependencies": {
29
+ "bun-types": "^1.3.9",
30
+ "typescript": "^5.9.3"
31
+ }
32
+ }
@@ -0,0 +1,59 @@
1
+ import type { Permission } from "./type/permission.type.ts";
2
+
3
+ export abstract class Actor {
4
+ private _permissions = new Set<Permission>();
5
+ private _cachedTeamIds?: string[] | undefined;
6
+
7
+ abstract get id(): string;
8
+ abstract get businessId(): string;
9
+ abstract get token(): string;
10
+
11
+ get permissions(): Permission[] {
12
+ return Array.from(this._permissions);
13
+ }
14
+
15
+ set permissions(permissions: Permission[]) {
16
+ this._permissions = new Set(permissions);
17
+ this._cachedTeamIds = undefined; // Invalidate cache when permissions change
18
+ }
19
+
20
+ addPermission(permission: Permission): void {
21
+ this._permissions.add(permission);
22
+ this._cachedTeamIds = undefined; // Invalidate cache
23
+ }
24
+
25
+ addPermissions(permissions: Permission[]): void {
26
+ permissions.forEach((permission) => this._permissions.add(permission));
27
+ this._cachedTeamIds = undefined; // Invalidate cache
28
+ }
29
+
30
+ /**
31
+ * Extract unique team IDs from user permissions.
32
+ *
33
+ * Team membership is derived from permissions with `constraints.teamId`.
34
+ * A user is a member of a team if they have ANY permission with that teamId.
35
+ *
36
+ * Result is cached for performance within the actor's lifetime (request scope).
37
+ *
38
+ * @returns Array of team IDs the actor belongs to
39
+ * @example
40
+ * ```typescript
41
+ * const teamIds = actor.getTeamIds(); // ["1100-legal", "1100-sales"]
42
+ * ```
43
+ */
44
+ getTeamIds(): string[] {
45
+ if (this._cachedTeamIds) {
46
+ return this._cachedTeamIds;
47
+ }
48
+
49
+ const teamIds = new Set<string>();
50
+ for (const permission of this._permissions) {
51
+ if (permission.constraints?.teamId) {
52
+ teamIds.add(permission.constraints.teamId);
53
+ }
54
+ }
55
+
56
+ this._cachedTeamIds = Array.from(teamIds);
57
+ return this._cachedTeamIds;
58
+ }
59
+ }
@@ -0,0 +1,59 @@
1
+ import type { ValidationResult } from "./validation/validation-result.ts";
2
+ import { ValidatorBuilder } from "./validation/validators.ts";
3
+
4
+ export interface Dto {
5
+ readonly id: string;
6
+ readonly ownerId: string;
7
+ readonly createdAt: string;
8
+ readonly updatedAt: string;
9
+ readonly deletedAt?: string;
10
+ }
11
+
12
+ export const validateDto = (): ValidationResult =>
13
+ new ValidatorBuilder()
14
+ .string("id", "id")
15
+ .string("ownerId", "")
16
+ .dateTime("createdAt", new Date())
17
+ .build();
18
+
19
+ export abstract class Entity {
20
+ readonly id: string;
21
+ readonly ownerId: string;
22
+ readonly createdAt: Date;
23
+ readonly updatedAt: Date;
24
+ readonly deletedAt?: Date;
25
+
26
+ constructor(dto: Dto) {
27
+ this.id = dto.id;
28
+ this.ownerId = dto.ownerId;
29
+ this.createdAt = new Date(dto.createdAt);
30
+ this.updatedAt = new Date(dto.updatedAt);
31
+ if (dto.deletedAt) {
32
+ this.deletedAt = new Date(dto.deletedAt);
33
+ }
34
+ }
35
+
36
+ get isDeleted(): boolean {
37
+ return this.deletedAt !== undefined;
38
+ }
39
+
40
+ toDto(): Dto {
41
+ return {
42
+ id: this.id,
43
+ ownerId: this.ownerId,
44
+ createdAt: this.createdAt.toISOString(),
45
+ updatedAt: this.updatedAt.toISOString(),
46
+ ...(this.deletedAt && { deletedAt: this.deletedAt.toISOString() }),
47
+ };
48
+ }
49
+
50
+ static generateId(prefix: string): string {
51
+ const validSymbols = /^[0-9a-fA-F]{4}$/;
52
+ if (!validSymbols.test(prefix)) {
53
+ throw new Error(
54
+ "Prefix must be exactly four hexadecimal characters (0-9, a-f, A-F), no hyphens.",
55
+ );
56
+ }
57
+ return `${prefix}-${crypto.randomUUID()}`;
58
+ }
59
+ }
@@ -0,0 +1,17 @@
1
+ import type { InterceptorContext } from "./type/interceptor-context.type.ts";
2
+
3
+ export interface Interceptor<Input, Output> {
4
+ readonly name: string;
5
+
6
+ beforeExecute?(
7
+ input: Input,
8
+ context: InterceptorContext,
9
+ ): Input | Promise<Input>;
10
+
11
+ afterExecute?(
12
+ result: Output,
13
+ context: InterceptorContext,
14
+ ): Output | Promise<Output>;
15
+
16
+ onError?(error: Error, context: InterceptorContext): void | Promise<void>;
17
+ }