@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.
- package/CHANGELOG.md +269 -0
- package/DEVELOPER_GUIDE.md +227 -0
- package/LICENSE +21 -0
- package/README.md +126 -0
- package/bun.lock +22 -0
- package/example/README.md +200 -0
- package/example/create-user.interactor.ts +88 -0
- package/example/dto/user.dto.ts +34 -0
- package/example/entity/user.entity.ts +54 -0
- package/example/index.ts +18 -0
- package/example/interface/create-user.usecase.ts +93 -0
- package/example/interface/user.repository.ts +23 -0
- package/mod.ts +1 -0
- package/package.json +32 -0
- package/src/common/abstract.actor.ts +59 -0
- package/src/common/abstract.entity.ts +59 -0
- package/src/common/abstract.interceptor.ts +17 -0
- package/src/common/abstract.repository.ts +162 -0
- package/src/common/abstract.usecase.ts +113 -0
- package/src/common/config/config-registry.ts +190 -0
- package/src/common/config/config-section.ts +106 -0
- package/src/common/exception/authorization-exception.ts +28 -0
- package/src/common/exception/repository-exception.ts +46 -0
- package/src/common/interceptor/audit-log.interceptor.ts +181 -0
- package/src/common/interceptor/authorization.interceptor.ts +252 -0
- package/src/common/interceptor/performance.interceptor.ts +101 -0
- package/src/common/llm/api-definition.type.ts +185 -0
- package/src/common/pattern/unit-of-work.ts +78 -0
- package/src/common/platform/env.ts +38 -0
- package/src/common/registry/usecase-registry.ts +80 -0
- package/src/common/type/interceptor-context.type.ts +25 -0
- package/src/common/type/json-schema.type.ts +80 -0
- package/src/common/type/json.type.ts +5 -0
- package/src/common/type/lowercase.type.ts +48 -0
- package/src/common/type/metadata.type.ts +5 -0
- package/src/common/type/money.class.ts +384 -0
- package/src/common/type/permission.type.ts +43 -0
- package/src/common/validation/validation-result.ts +52 -0
- package/src/common/validation/validators.ts +441 -0
- package/src/index.ts +95 -0
- package/test/unit/abstract-actor.test.ts +608 -0
- package/test/unit/actor.test.ts +89 -0
- package/test/unit/api-definition.test.ts +628 -0
- package/test/unit/authorization.test.ts +101 -0
- package/test/unit/entity.test.ts +95 -0
- package/test/unit/money.test.ts +480 -0
- package/test/unit/validation.test.ts +138 -0
- package/tsconfig.json +18 -0
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
import type { Interceptor } from "../abstract.interceptor.ts";
|
|
2
|
+
import type { InterceptorContext } from "../type/interceptor-context.type.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Audit log entry structure for compliance and tracking
|
|
6
|
+
*/
|
|
7
|
+
export interface AuditLogEntry {
|
|
8
|
+
timestamp: string;
|
|
9
|
+
actorId: string;
|
|
10
|
+
businessId: string;
|
|
11
|
+
action: string;
|
|
12
|
+
resource: string;
|
|
13
|
+
status: "started" | "completed" | "failed";
|
|
14
|
+
duration?: number;
|
|
15
|
+
error?: string;
|
|
16
|
+
metadata?: Record<string, unknown>;
|
|
17
|
+
requiredPermissions?: string[];
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Audit log interceptor for compliance and security tracking.
|
|
22
|
+
* Creates detailed audit trails of all usecase executions.
|
|
23
|
+
*/
|
|
24
|
+
export class AuditLogInterceptor<Input, Output>
|
|
25
|
+
implements Interceptor<Input, Output> {
|
|
26
|
+
readonly name = "AuditLogInterceptor";
|
|
27
|
+
|
|
28
|
+
private readonly startTimes = new Map<string, number>();
|
|
29
|
+
|
|
30
|
+
constructor(
|
|
31
|
+
private readonly logHandler: (
|
|
32
|
+
entry: AuditLogEntry,
|
|
33
|
+
) => void | Promise<void> = (entry) =>
|
|
34
|
+
console.log("[Audit]", JSON.stringify(entry)),
|
|
35
|
+
) {}
|
|
36
|
+
|
|
37
|
+
private getExecutionKey(context: InterceptorContext): string {
|
|
38
|
+
return `${context.actor.id}_${context.usecaseName.join("/")}`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async beforeExecute(
|
|
42
|
+
input: Input,
|
|
43
|
+
context: InterceptorContext,
|
|
44
|
+
): Promise<Input> {
|
|
45
|
+
const key = this.getExecutionKey(context);
|
|
46
|
+
this.startTimes.set(key, Date.now());
|
|
47
|
+
|
|
48
|
+
const entry: AuditLogEntry = {
|
|
49
|
+
timestamp: new Date().toISOString(),
|
|
50
|
+
actorId: context.actor.id,
|
|
51
|
+
businessId: context.actor.businessId,
|
|
52
|
+
action: context.usecaseName[0],
|
|
53
|
+
resource: context.usecaseName[1],
|
|
54
|
+
status: "started",
|
|
55
|
+
...(context.requiredPermissions.length > 0 && {
|
|
56
|
+
requiredPermissions: context.requiredPermissions.map((p) =>
|
|
57
|
+
`${p.action}:${p.resource}`
|
|
58
|
+
),
|
|
59
|
+
}),
|
|
60
|
+
...(Object.keys(context.metadata).length > 0 && {
|
|
61
|
+
metadata: context.metadata,
|
|
62
|
+
}),
|
|
63
|
+
};
|
|
64
|
+
|
|
65
|
+
await this.logHandler(entry);
|
|
66
|
+
|
|
67
|
+
return input;
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
async afterExecute(
|
|
71
|
+
output: Output,
|
|
72
|
+
context: InterceptorContext,
|
|
73
|
+
): Promise<Output> {
|
|
74
|
+
const key = this.getExecutionKey(context);
|
|
75
|
+
const startTime = this.startTimes.get(key);
|
|
76
|
+
this.startTimes.delete(key);
|
|
77
|
+
|
|
78
|
+
const entry: AuditLogEntry = {
|
|
79
|
+
timestamp: new Date().toISOString(),
|
|
80
|
+
actorId: context.actor.id,
|
|
81
|
+
businessId: context.actor.businessId,
|
|
82
|
+
action: context.usecaseName[0],
|
|
83
|
+
resource: context.usecaseName[1],
|
|
84
|
+
status: "completed",
|
|
85
|
+
...(startTime !== undefined && {
|
|
86
|
+
duration: Date.now() - startTime,
|
|
87
|
+
}),
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await this.logHandler(entry);
|
|
91
|
+
|
|
92
|
+
return output;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async onError(
|
|
96
|
+
error: Error,
|
|
97
|
+
context: InterceptorContext,
|
|
98
|
+
): Promise<void> {
|
|
99
|
+
const key = this.getExecutionKey(context);
|
|
100
|
+
const startTime = this.startTimes.get(key);
|
|
101
|
+
this.startTimes.delete(key);
|
|
102
|
+
|
|
103
|
+
const entry: AuditLogEntry = {
|
|
104
|
+
timestamp: new Date().toISOString(),
|
|
105
|
+
actorId: context.actor.id,
|
|
106
|
+
businessId: context.actor.businessId,
|
|
107
|
+
action: context.usecaseName[0],
|
|
108
|
+
resource: context.usecaseName[1],
|
|
109
|
+
status: "failed",
|
|
110
|
+
error: `${error.name}: ${error.message}`,
|
|
111
|
+
...(startTime !== undefined && {
|
|
112
|
+
duration: Date.now() - startTime,
|
|
113
|
+
}),
|
|
114
|
+
};
|
|
115
|
+
|
|
116
|
+
await this.logHandler(entry);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* File-based audit logger that writes to a file
|
|
122
|
+
* This is an example implementation - in production you'd likely use a database or log aggregation service
|
|
123
|
+
*/
|
|
124
|
+
export class FileAuditLogger {
|
|
125
|
+
private entries: AuditLogEntry[] = [];
|
|
126
|
+
|
|
127
|
+
constructor(
|
|
128
|
+
private readonly maxEntries: number = 10000,
|
|
129
|
+
private readonly rotateCallback?: (
|
|
130
|
+
entries: AuditLogEntry[],
|
|
131
|
+
) => void | Promise<void>,
|
|
132
|
+
) {}
|
|
133
|
+
|
|
134
|
+
async log(entry: AuditLogEntry): Promise<void> {
|
|
135
|
+
this.entries.push(entry);
|
|
136
|
+
|
|
137
|
+
// Rotate log when it gets too large
|
|
138
|
+
if (this.entries.length >= this.maxEntries) {
|
|
139
|
+
const entriesToRotate = this.entries.splice(0, this.maxEntries / 2);
|
|
140
|
+
if (this.rotateCallback) {
|
|
141
|
+
await this.rotateCallback(entriesToRotate);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
getEntries(
|
|
147
|
+
filter?: {
|
|
148
|
+
actorId?: string;
|
|
149
|
+
businessId?: string;
|
|
150
|
+
action?: string;
|
|
151
|
+
resource?: string;
|
|
152
|
+
status?: AuditLogEntry["status"];
|
|
153
|
+
from?: Date;
|
|
154
|
+
to?: Date;
|
|
155
|
+
},
|
|
156
|
+
): AuditLogEntry[] {
|
|
157
|
+
if (!filter) {
|
|
158
|
+
return [...this.entries];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
return this.entries.filter((entry) => {
|
|
162
|
+
if (filter.actorId && entry.actorId !== filter.actorId) return false;
|
|
163
|
+
if (filter.businessId && entry.businessId !== filter.businessId) {
|
|
164
|
+
return false;
|
|
165
|
+
}
|
|
166
|
+
if (filter.action && entry.action !== filter.action) return false;
|
|
167
|
+
if (filter.resource && entry.resource !== filter.resource) return false;
|
|
168
|
+
if (filter.status && entry.status !== filter.status) return false;
|
|
169
|
+
|
|
170
|
+
const entryTime = new Date(entry.timestamp);
|
|
171
|
+
if (filter.from && entryTime < filter.from) return false;
|
|
172
|
+
if (filter.to && entryTime > filter.to) return false;
|
|
173
|
+
|
|
174
|
+
return true;
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
clear(): void {
|
|
179
|
+
this.entries = [];
|
|
180
|
+
}
|
|
181
|
+
}
|
|
@@ -0,0 +1,252 @@
|
|
|
1
|
+
import type { Interceptor } from "../abstract.interceptor.ts";
|
|
2
|
+
import type { InterceptorContext } from "../type/interceptor-context.type.ts";
|
|
3
|
+
import {
|
|
4
|
+
type Permission,
|
|
5
|
+
type ResourceConstraints,
|
|
6
|
+
ResourceScope,
|
|
7
|
+
} from "../type/permission.type.ts";
|
|
8
|
+
import { AuthorizationException } from "../exception/authorization-exception.ts";
|
|
9
|
+
|
|
10
|
+
export class AuthorizationInterceptor<Input, Output>
|
|
11
|
+
implements Interceptor<Input, Output> {
|
|
12
|
+
readonly name = "AuthorizationInterceptor";
|
|
13
|
+
|
|
14
|
+
// Pre-computed scope satisfaction matrix for O(1) lookups
|
|
15
|
+
// PROJECT and TEAM are parallel scopes under BUSINESS
|
|
16
|
+
private static readonly SCOPE_SATISFIES = new Map<
|
|
17
|
+
ResourceScope,
|
|
18
|
+
Set<ResourceScope>
|
|
19
|
+
>([
|
|
20
|
+
[
|
|
21
|
+
ResourceScope.ALL,
|
|
22
|
+
new Set([
|
|
23
|
+
ResourceScope.ALL,
|
|
24
|
+
ResourceScope.BUSINESS,
|
|
25
|
+
ResourceScope.PROJECT,
|
|
26
|
+
ResourceScope.TEAM,
|
|
27
|
+
ResourceScope.OWNED,
|
|
28
|
+
]),
|
|
29
|
+
],
|
|
30
|
+
[
|
|
31
|
+
ResourceScope.BUSINESS,
|
|
32
|
+
new Set([
|
|
33
|
+
ResourceScope.BUSINESS,
|
|
34
|
+
ResourceScope.PROJECT,
|
|
35
|
+
ResourceScope.TEAM,
|
|
36
|
+
ResourceScope.OWNED,
|
|
37
|
+
]),
|
|
38
|
+
],
|
|
39
|
+
[
|
|
40
|
+
ResourceScope.PROJECT,
|
|
41
|
+
new Set([ResourceScope.PROJECT, ResourceScope.OWNED]),
|
|
42
|
+
],
|
|
43
|
+
[ResourceScope.TEAM, new Set([ResourceScope.TEAM, ResourceScope.OWNED])],
|
|
44
|
+
[ResourceScope.OWNED, new Set([ResourceScope.OWNED])],
|
|
45
|
+
]);
|
|
46
|
+
|
|
47
|
+
beforeExecute(
|
|
48
|
+
input: Input,
|
|
49
|
+
context: InterceptorContext,
|
|
50
|
+
): Input {
|
|
51
|
+
this._checkAuthorization(context);
|
|
52
|
+
return input;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
private _checkAuthorization(context: InterceptorContext): void {
|
|
56
|
+
const requiredPerms = context.requiredPermissions;
|
|
57
|
+
|
|
58
|
+
if (requiredPerms.length === 0) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// Index actor permissions for O(1) lookups
|
|
63
|
+
const actorPermIndex = this._indexPermissions(context.actor.permissions);
|
|
64
|
+
|
|
65
|
+
if (!this._hasPermissionsIndexed(actorPermIndex, requiredPerms)) {
|
|
66
|
+
const missingPermissions = this._findMissingPermissions(
|
|
67
|
+
context.actor.permissions,
|
|
68
|
+
requiredPerms,
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
throw new AuthorizationException(
|
|
72
|
+
`Insufficient permissions for actor ${context.actor.id}`,
|
|
73
|
+
{
|
|
74
|
+
actorId: context.actor.id,
|
|
75
|
+
resource: context.usecaseName[1],
|
|
76
|
+
action: context.usecaseName[0],
|
|
77
|
+
missingPermissions,
|
|
78
|
+
},
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
private _indexPermissions(
|
|
84
|
+
permissions: Permission[],
|
|
85
|
+
): Map<string, Permission[]> {
|
|
86
|
+
const index = new Map<string, Permission[]>();
|
|
87
|
+
|
|
88
|
+
for (const perm of permissions) {
|
|
89
|
+
const key = `${perm.resource}:${perm.action}`;
|
|
90
|
+
if (!index.has(key)) {
|
|
91
|
+
index.set(key, []);
|
|
92
|
+
}
|
|
93
|
+
index.get(key)!.push(perm);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
return index;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
private _hasPermissionsIndexed(
|
|
100
|
+
actorPermIndex: Map<string, Permission[]>,
|
|
101
|
+
requiredPermissions: Permission[],
|
|
102
|
+
): boolean {
|
|
103
|
+
if (requiredPermissions.length === 0) {
|
|
104
|
+
return true;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const required of requiredPermissions) {
|
|
108
|
+
if (!this._hasPermissionIndexed(actorPermIndex, required)) {
|
|
109
|
+
return false;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
return true;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private _hasPermissionIndexed(
|
|
117
|
+
actorPermIndex: Map<string, Permission[]>,
|
|
118
|
+
required: Permission,
|
|
119
|
+
): boolean {
|
|
120
|
+
const key = `${required.resource}:${required.action}`;
|
|
121
|
+
const candidates = actorPermIndex.get(key) || [];
|
|
122
|
+
|
|
123
|
+
// Only check constraints for exact resource:action matches (O(1) lookup)
|
|
124
|
+
for (const permission of candidates) {
|
|
125
|
+
if (
|
|
126
|
+
this._constraintsSatisfied(permission.constraints, required.constraints)
|
|
127
|
+
) {
|
|
128
|
+
return true;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
return false;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
private _hasPermission(
|
|
135
|
+
actorPermissions: Permission[],
|
|
136
|
+
required: Permission,
|
|
137
|
+
): boolean {
|
|
138
|
+
for (const permission of actorPermissions) {
|
|
139
|
+
if (this._permissionMatches(permission, required)) {
|
|
140
|
+
return true;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
private _permissionMatches(
|
|
147
|
+
granted: Permission,
|
|
148
|
+
required: Permission,
|
|
149
|
+
): boolean {
|
|
150
|
+
if (
|
|
151
|
+
granted.resource === required.resource &&
|
|
152
|
+
granted.action === required.action
|
|
153
|
+
) {
|
|
154
|
+
return this._constraintsSatisfied(
|
|
155
|
+
granted.constraints,
|
|
156
|
+
required.constraints,
|
|
157
|
+
);
|
|
158
|
+
}
|
|
159
|
+
return false;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
private _constraintsSatisfied(
|
|
163
|
+
granted?: ResourceConstraints,
|
|
164
|
+
required?: ResourceConstraints,
|
|
165
|
+
): boolean {
|
|
166
|
+
if (!required) {
|
|
167
|
+
return true;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
if (!granted) {
|
|
171
|
+
return false;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
// Check scope constraints
|
|
175
|
+
if (required.scope) {
|
|
176
|
+
if (!granted.scope) return false;
|
|
177
|
+
if (!this._scopeSatisfied(granted.scope, required.scope)) return false;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
// Check status constraints
|
|
181
|
+
if (required.statuses) {
|
|
182
|
+
if (!granted.statuses) return false;
|
|
183
|
+
const requiredSet = new Set(required.statuses);
|
|
184
|
+
const grantedSet = new Set(granted.statuses);
|
|
185
|
+
for (const status of requiredSet) {
|
|
186
|
+
if (!grantedSet.has(status)) return false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// Check time constraints
|
|
191
|
+
if (required.timeRestriction) {
|
|
192
|
+
if (!granted.timeRestriction) return false;
|
|
193
|
+
const now = new Date();
|
|
194
|
+
if (
|
|
195
|
+
now < granted.timeRestriction.from ||
|
|
196
|
+
now > granted.timeRestriction.to
|
|
197
|
+
) {
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// Check businessId constraints
|
|
203
|
+
if (required.businessId) {
|
|
204
|
+
if (!granted.businessId) return false;
|
|
205
|
+
if (granted.businessId !== required.businessId) return false;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// Check projectId constraints
|
|
209
|
+
if (required.projectId) {
|
|
210
|
+
if (!granted.projectId) return false;
|
|
211
|
+
if (granted.projectId !== required.projectId) return false;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
// Check team constraints
|
|
215
|
+
if (required.teamId) {
|
|
216
|
+
if (!granted.teamId) return false;
|
|
217
|
+
if (granted.teamId !== required.teamId) return false;
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Check resourceId constraints
|
|
221
|
+
if (required.resourceId) {
|
|
222
|
+
if (!granted.resourceId) return false;
|
|
223
|
+
if (granted.resourceId !== required.resourceId) return false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return true;
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
private _scopeSatisfied(
|
|
230
|
+
granted: ResourceScope,
|
|
231
|
+
required: ResourceScope,
|
|
232
|
+
): boolean {
|
|
233
|
+
return AuthorizationInterceptor.SCOPE_SATISFIES.get(granted)?.has(
|
|
234
|
+
required,
|
|
235
|
+
) ?? false;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
private _findMissingPermissions(
|
|
239
|
+
actorPermissions: Permission[],
|
|
240
|
+
requiredPermissions: Permission[],
|
|
241
|
+
): string[] {
|
|
242
|
+
const missing: string[] = [];
|
|
243
|
+
|
|
244
|
+
for (const required of requiredPermissions) {
|
|
245
|
+
if (!this._hasPermission(actorPermissions, required)) {
|
|
246
|
+
missing.push(`${required.action}:${required.resource}`);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
return missing;
|
|
251
|
+
}
|
|
252
|
+
}
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import type { Interceptor } from "../abstract.interceptor.ts";
|
|
2
|
+
import type { InterceptorContext } from "../type/interceptor-context.type.ts";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Performance interceptor that measures usecase execution time.
|
|
6
|
+
* Tracks slow operations and warns when they exceed thresholds.
|
|
7
|
+
*/
|
|
8
|
+
export class PerformanceInterceptor<Input, Output>
|
|
9
|
+
implements Interceptor<Input, Output> {
|
|
10
|
+
readonly name = "PerformanceInterceptor";
|
|
11
|
+
|
|
12
|
+
private readonly startTimes = new Map<string, number>();
|
|
13
|
+
|
|
14
|
+
constructor(
|
|
15
|
+
private readonly slowThresholdMs: number = 1000,
|
|
16
|
+
private readonly logger: (message: string) => void = console.log,
|
|
17
|
+
) {}
|
|
18
|
+
|
|
19
|
+
private getExecutionKey(context: InterceptorContext): string {
|
|
20
|
+
return `${context.actor.id}_${context.usecaseName.join("/")}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
beforeExecute(
|
|
24
|
+
input: Input,
|
|
25
|
+
context: InterceptorContext,
|
|
26
|
+
): Input {
|
|
27
|
+
const key = this.getExecutionKey(context);
|
|
28
|
+
this.startTimes.set(key, performance.now());
|
|
29
|
+
|
|
30
|
+
return input;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
afterExecute(
|
|
34
|
+
output: Output,
|
|
35
|
+
context: InterceptorContext,
|
|
36
|
+
): Output {
|
|
37
|
+
const key = this.getExecutionKey(context);
|
|
38
|
+
const startTime = this.startTimes.get(key);
|
|
39
|
+
this.startTimes.delete(key);
|
|
40
|
+
|
|
41
|
+
if (startTime !== undefined) {
|
|
42
|
+
const duration = performance.now() - startTime;
|
|
43
|
+
const action = context.usecaseName[0];
|
|
44
|
+
const resource = context.usecaseName[1];
|
|
45
|
+
|
|
46
|
+
this.logger(
|
|
47
|
+
`[Performance] ${action}/${resource} executed in ${
|
|
48
|
+
duration.toFixed(2)
|
|
49
|
+
}ms`,
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// Warn if execution was slow
|
|
53
|
+
if (duration > this.slowThresholdMs) {
|
|
54
|
+
this.logger(
|
|
55
|
+
`[Performance WARNING] ${action}/${resource} exceeded threshold ` +
|
|
56
|
+
`(${duration.toFixed(2)}ms > ${this.slowThresholdMs}ms)`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return output;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
onError(
|
|
65
|
+
_error: Error,
|
|
66
|
+
context: InterceptorContext,
|
|
67
|
+
): void {
|
|
68
|
+
const key = this.getExecutionKey(context);
|
|
69
|
+
const startTime = this.startTimes.get(key);
|
|
70
|
+
this.startTimes.delete(key);
|
|
71
|
+
|
|
72
|
+
if (startTime !== undefined) {
|
|
73
|
+
const duration = performance.now() - startTime;
|
|
74
|
+
const action = context.usecaseName[0];
|
|
75
|
+
const resource = context.usecaseName[1];
|
|
76
|
+
|
|
77
|
+
this.logger(
|
|
78
|
+
`[Performance] ${action}/${resource} failed after ${
|
|
79
|
+
duration.toFixed(2)
|
|
80
|
+
}ms`,
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Get current performance metrics (useful for monitoring)
|
|
87
|
+
*/
|
|
88
|
+
getMetrics(): { activeExecutions: number; keys: string[] } {
|
|
89
|
+
return {
|
|
90
|
+
activeExecutions: this.startTimes.size,
|
|
91
|
+
keys: Array.from(this.startTimes.keys()),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Clear all tracked executions (useful for cleanup)
|
|
97
|
+
*/
|
|
98
|
+
clear(): void {
|
|
99
|
+
this.startTimes.clear();
|
|
100
|
+
}
|
|
101
|
+
}
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
JsonSchema,
|
|
3
|
+
MutableJsonSchema,
|
|
4
|
+
} from "../type/json-schema.type.ts";
|
|
5
|
+
|
|
6
|
+
// Base interface for all parameters
|
|
7
|
+
interface LlmBaseParameter {
|
|
8
|
+
readonly name: string;
|
|
9
|
+
readonly description: string;
|
|
10
|
+
readonly isRequired?: boolean;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Primitive type parameters
|
|
14
|
+
interface LlmStringParameter extends LlmBaseParameter {
|
|
15
|
+
readonly type: "string";
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface LlmNumberParameter extends LlmBaseParameter {
|
|
19
|
+
readonly type: "number" | "integer";
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface LlmBooleanParameter extends LlmBaseParameter {
|
|
23
|
+
readonly type: "boolean";
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
interface LlmNullParameter extends LlmBaseParameter {
|
|
27
|
+
readonly type: "null";
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// Complex type parameters
|
|
31
|
+
interface LlmArrayParameter extends LlmBaseParameter {
|
|
32
|
+
readonly type: "array";
|
|
33
|
+
readonly items: {
|
|
34
|
+
readonly type: string;
|
|
35
|
+
readonly description?: string;
|
|
36
|
+
readonly properties?: JsonSchema;
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
interface LlmObjectParameter extends LlmBaseParameter {
|
|
41
|
+
readonly type: "object";
|
|
42
|
+
readonly properties?: Record<string, JsonSchema>;
|
|
43
|
+
readonly required?: readonly string[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Discriminated union for parameters
|
|
47
|
+
export type LlmParameter =
|
|
48
|
+
| LlmStringParameter
|
|
49
|
+
| LlmNumberParameter
|
|
50
|
+
| LlmBooleanParameter
|
|
51
|
+
| LlmNullParameter
|
|
52
|
+
| LlmArrayParameter
|
|
53
|
+
| LlmObjectParameter;
|
|
54
|
+
|
|
55
|
+
// Base interface for all return values
|
|
56
|
+
interface LlmBaseReturnValue {
|
|
57
|
+
readonly description: string;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// Primitive type returns
|
|
61
|
+
interface LlmStringReturnValue extends LlmBaseReturnValue {
|
|
62
|
+
readonly type: "string";
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface LlmNumberReturnValue extends LlmBaseReturnValue {
|
|
66
|
+
readonly type: "number" | "integer";
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
interface LlmBooleanReturnValue extends LlmBaseReturnValue {
|
|
70
|
+
readonly type: "boolean";
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
interface LlmNullReturnValue extends LlmBaseReturnValue {
|
|
74
|
+
readonly type: "null";
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
// Complex type returns
|
|
78
|
+
interface LlmArrayReturnValue extends LlmBaseReturnValue {
|
|
79
|
+
readonly type: "array";
|
|
80
|
+
readonly items: {
|
|
81
|
+
readonly type: string;
|
|
82
|
+
readonly description?: string;
|
|
83
|
+
readonly properties?: JsonSchema;
|
|
84
|
+
};
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
interface LlmObjectReturnValue extends LlmBaseReturnValue {
|
|
88
|
+
readonly type: "object";
|
|
89
|
+
readonly properties?: Record<string, JsonSchema>;
|
|
90
|
+
readonly required?: readonly string[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// Discriminated union for return values
|
|
94
|
+
export type LlmReturnValue =
|
|
95
|
+
| LlmStringReturnValue
|
|
96
|
+
| LlmNumberReturnValue
|
|
97
|
+
| LlmBooleanReturnValue
|
|
98
|
+
| LlmNullReturnValue
|
|
99
|
+
| LlmArrayReturnValue
|
|
100
|
+
| LlmObjectReturnValue;
|
|
101
|
+
|
|
102
|
+
export interface ApiDefinition {
|
|
103
|
+
readonly name: string;
|
|
104
|
+
readonly description: string;
|
|
105
|
+
readonly parameters: LlmParameter[];
|
|
106
|
+
readonly returns: LlmReturnValue;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
export function apiDefinitionToJsonSchema(
|
|
110
|
+
definition: ApiDefinition,
|
|
111
|
+
): JsonSchema {
|
|
112
|
+
const properties: Record<string, MutableJsonSchema> = {};
|
|
113
|
+
const required: string[] = [];
|
|
114
|
+
|
|
115
|
+
for (const param of definition.parameters) {
|
|
116
|
+
const paramSchema: MutableJsonSchema = {
|
|
117
|
+
type: param.type,
|
|
118
|
+
description: param.description,
|
|
119
|
+
};
|
|
120
|
+
|
|
121
|
+
// Add items for array types (JSON Schema 2020-12 compliance)
|
|
122
|
+
if (param.type === "array" && "items" in param) {
|
|
123
|
+
const arrayParam = param as LlmArrayParameter;
|
|
124
|
+
paramSchema.items = arrayParam.items as MutableJsonSchema;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Add properties and required for object types (JSON Schema 2020-12 compliance)
|
|
128
|
+
if (param.type === "object" && "properties" in param) {
|
|
129
|
+
const objectParam = param as LlmObjectParameter;
|
|
130
|
+
if (objectParam.properties) {
|
|
131
|
+
paramSchema.properties = objectParam.properties as Record<
|
|
132
|
+
string,
|
|
133
|
+
MutableJsonSchema
|
|
134
|
+
>;
|
|
135
|
+
}
|
|
136
|
+
if (objectParam.required) {
|
|
137
|
+
paramSchema.required = [...objectParam.required];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
properties[param.name] = paramSchema;
|
|
142
|
+
|
|
143
|
+
if (param.isRequired !== false) {
|
|
144
|
+
required.push(param.name);
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const returnsSchema: MutableJsonSchema = {
|
|
149
|
+
type: definition.returns.type,
|
|
150
|
+
description: definition.returns.description,
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
// Add items for array return types (JSON Schema 2020-12 compliance)
|
|
154
|
+
if (definition.returns.type === "array" && "items" in definition.returns) {
|
|
155
|
+
const arrayReturn = definition.returns as LlmArrayReturnValue;
|
|
156
|
+
returnsSchema.items = arrayReturn.items as MutableJsonSchema;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
// Add properties and required for object return types (JSON Schema 2020-12 compliance)
|
|
160
|
+
if (
|
|
161
|
+
definition.returns.type === "object" && "properties" in definition.returns
|
|
162
|
+
) {
|
|
163
|
+
const objectReturn = definition.returns as LlmObjectReturnValue;
|
|
164
|
+
if (objectReturn.properties) {
|
|
165
|
+
returnsSchema.properties = objectReturn.properties as Record<
|
|
166
|
+
string,
|
|
167
|
+
MutableJsonSchema
|
|
168
|
+
>;
|
|
169
|
+
}
|
|
170
|
+
if (objectReturn.required) {
|
|
171
|
+
returnsSchema.required = [...objectReturn.required];
|
|
172
|
+
}
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return {
|
|
176
|
+
name: definition.name,
|
|
177
|
+
description: definition.description,
|
|
178
|
+
parameters: {
|
|
179
|
+
type: "object",
|
|
180
|
+
properties,
|
|
181
|
+
required,
|
|
182
|
+
},
|
|
183
|
+
returns: returnsSchema,
|
|
184
|
+
} as JsonSchema;
|
|
185
|
+
}
|