@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,608 @@
1
+ import { test, expect, describe } from "bun:test";
2
+ import { Actor } from "../../src/common/abstract.actor.ts";
3
+ import type { Permission } from "../../src/common/type/permission.type.ts";
4
+ import { ResourceScope } from "../../src/common/type/permission.type.ts";
5
+
6
+ /**
7
+ * Concrete Actor implementation for testing.
8
+ * The abstract Actor class requires concrete implementations of id, businessId, and token.
9
+ */
10
+ class TestActor extends Actor {
11
+ constructor(
12
+ private _id: string,
13
+ private _businessId: string,
14
+ private _token: string,
15
+ permissions: Permission[] = [],
16
+ ) {
17
+ super();
18
+ this.permissions = permissions;
19
+ }
20
+
21
+ get id(): string {
22
+ return this._id;
23
+ }
24
+
25
+ get businessId(): string {
26
+ return this._businessId;
27
+ }
28
+
29
+ get token(): string {
30
+ return this._token;
31
+ }
32
+ }
33
+
34
+ describe("Actor.getTeamIds() - Team extraction from permissions", () => {
35
+ test("should return empty array when no permissions", () => {
36
+ const actor = new TestActor(
37
+ "1001-user-1",
38
+ "0002-business-1",
39
+ "token-123",
40
+ [],
41
+ );
42
+
43
+ const teamIds = actor.getTeamIds();
44
+
45
+ expect(teamIds).toEqual([]);
46
+ });
47
+
48
+ test("should return empty array when no team permissions", () => {
49
+ const actor = new TestActor(
50
+ "1001-user-1",
51
+ "0002-business-1",
52
+ "token-123",
53
+ [
54
+ {
55
+ resource: "product",
56
+ action: "create",
57
+ constraints: { scope: ResourceScope.OWNED },
58
+ },
59
+ {
60
+ resource: "document",
61
+ action: "retrieve",
62
+ constraints: { scope: ResourceScope.BUSINESS },
63
+ },
64
+ ],
65
+ );
66
+
67
+ const teamIds = actor.getTeamIds();
68
+
69
+ expect(teamIds).toEqual([]);
70
+ });
71
+
72
+ test("should extract single team ID from permissions", () => {
73
+ const actor = new TestActor(
74
+ "1001-user-1",
75
+ "0002-business-1",
76
+ "token-123",
77
+ [
78
+ {
79
+ resource: "document",
80
+ action: "retrieve",
81
+ constraints: {
82
+ scope: ResourceScope.TEAM,
83
+ teamId: "1100-legal",
84
+ },
85
+ },
86
+ ],
87
+ );
88
+
89
+ const teamIds = actor.getTeamIds();
90
+
91
+ expect(teamIds).toEqual(["1100-legal"]);
92
+ });
93
+
94
+ test("should extract multiple unique team IDs", () => {
95
+ const actor = new TestActor(
96
+ "1001-user-1",
97
+ "0002-business-1",
98
+ "token-123",
99
+ [
100
+ {
101
+ resource: "document",
102
+ action: "retrieve",
103
+ constraints: {
104
+ scope: ResourceScope.TEAM,
105
+ teamId: "1100-legal",
106
+ },
107
+ },
108
+ {
109
+ resource: "customer",
110
+ action: "create",
111
+ constraints: {
112
+ scope: ResourceScope.TEAM,
113
+ teamId: "1100-sales",
114
+ },
115
+ },
116
+ {
117
+ resource: "product",
118
+ action: "update",
119
+ constraints: {
120
+ scope: ResourceScope.TEAM,
121
+ teamId: "1100-engineering",
122
+ },
123
+ },
124
+ ],
125
+ );
126
+
127
+ const teamIds = actor.getTeamIds();
128
+
129
+ expect(teamIds.length).toEqual(3);
130
+ expect(teamIds.includes("1100-legal")).toEqual(true);
131
+ expect(teamIds.includes("1100-sales")).toEqual(true);
132
+ expect(teamIds.includes("1100-engineering")).toEqual(true);
133
+ });
134
+
135
+ test("should deduplicate team IDs from multiple permissions", () => {
136
+ const actor = new TestActor(
137
+ "1001-user-1",
138
+ "0002-business-1",
139
+ "token-123",
140
+ [
141
+ {
142
+ resource: "document",
143
+ action: "retrieve",
144
+ constraints: {
145
+ scope: ResourceScope.TEAM,
146
+ teamId: "1100-legal",
147
+ },
148
+ },
149
+ {
150
+ resource: "document",
151
+ action: "create",
152
+ constraints: {
153
+ scope: ResourceScope.TEAM,
154
+ teamId: "1100-legal",
155
+ },
156
+ },
157
+ {
158
+ resource: "document",
159
+ action: "update",
160
+ constraints: {
161
+ scope: ResourceScope.TEAM,
162
+ teamId: "1100-legal",
163
+ },
164
+ },
165
+ {
166
+ resource: "customer",
167
+ action: "retrieve",
168
+ constraints: {
169
+ scope: ResourceScope.TEAM,
170
+ teamId: "1100-sales",
171
+ },
172
+ },
173
+ {
174
+ resource: "customer",
175
+ action: "create",
176
+ constraints: {
177
+ scope: ResourceScope.TEAM,
178
+ teamId: "1100-sales",
179
+ },
180
+ },
181
+ ],
182
+ );
183
+
184
+ const teamIds = actor.getTeamIds();
185
+
186
+ // Should have exactly 2 unique team IDs despite 5 permissions
187
+ expect(teamIds.length).toEqual(2);
188
+ expect(teamIds.includes("1100-legal")).toEqual(true);
189
+ expect(teamIds.includes("1100-sales")).toEqual(true);
190
+ });
191
+
192
+ test("should ignore permissions without teamId constraint", () => {
193
+ const actor = new TestActor(
194
+ "1001-user-1",
195
+ "0002-business-1",
196
+ "token-123",
197
+ [
198
+ {
199
+ resource: "document",
200
+ action: "retrieve",
201
+ constraints: {
202
+ scope: ResourceScope.TEAM,
203
+ teamId: "1100-legal",
204
+ },
205
+ },
206
+ {
207
+ resource: "product",
208
+ action: "create",
209
+ constraints: { scope: ResourceScope.OWNED },
210
+ },
211
+ {
212
+ resource: "order",
213
+ action: "list",
214
+ constraints: { scope: ResourceScope.BUSINESS },
215
+ },
216
+ {
217
+ resource: "invoice",
218
+ action: "retrieve",
219
+ // No constraints at all
220
+ },
221
+ ],
222
+ );
223
+
224
+ const teamIds = actor.getTeamIds();
225
+
226
+ expect(teamIds).toEqual(["1100-legal"]);
227
+ });
228
+
229
+ test("should handle permissions with teamId but no scope", () => {
230
+ // Edge case: permission has teamId but scope is not TEAM
231
+ const actor = new TestActor(
232
+ "1001-user-1",
233
+ "0002-business-1",
234
+ "token-123",
235
+ [
236
+ {
237
+ resource: "document",
238
+ action: "retrieve",
239
+ constraints: {
240
+ scope: ResourceScope.OWNED,
241
+ teamId: "1100-legal", // Has teamId despite OWNED scope
242
+ },
243
+ },
244
+ {
245
+ resource: "customer",
246
+ action: "create",
247
+ constraints: {
248
+ teamId: "1100-sales", // Has teamId with no scope
249
+ },
250
+ },
251
+ ],
252
+ );
253
+
254
+ const teamIds = actor.getTeamIds();
255
+
256
+ // Should extract teamIds regardless of scope value
257
+ expect(teamIds.length).toEqual(2);
258
+ expect(teamIds.includes("1100-legal")).toEqual(true);
259
+ expect(teamIds.includes("1100-sales")).toEqual(true);
260
+ });
261
+ });
262
+
263
+ describe("Actor.getTeamIds() - Caching behavior", () => {
264
+ test("should cache result on first call", () => {
265
+ const actor = new TestActor(
266
+ "1001-user-1",
267
+ "0002-business-1",
268
+ "token-123",
269
+ [
270
+ {
271
+ resource: "document",
272
+ action: "retrieve",
273
+ constraints: {
274
+ scope: ResourceScope.TEAM,
275
+ teamId: "1100-legal",
276
+ },
277
+ },
278
+ ],
279
+ );
280
+
281
+ const firstCall = actor.getTeamIds();
282
+ const secondCall = actor.getTeamIds();
283
+
284
+ // Should return same array reference (cached)
285
+ expect(firstCall).toEqual(secondCall);
286
+ expect(firstCall === secondCall).toEqual(true);
287
+ });
288
+
289
+ test("should invalidate cache when permissions are set", () => {
290
+ const actor = new TestActor(
291
+ "1001-user-1",
292
+ "0002-business-1",
293
+ "token-123",
294
+ [
295
+ {
296
+ resource: "document",
297
+ action: "retrieve",
298
+ constraints: {
299
+ scope: ResourceScope.TEAM,
300
+ teamId: "1100-legal",
301
+ },
302
+ },
303
+ ],
304
+ );
305
+
306
+ const firstCall = actor.getTeamIds();
307
+ expect(firstCall).toEqual(["1100-legal"]);
308
+
309
+ // Update permissions
310
+ actor.permissions = [
311
+ {
312
+ resource: "customer",
313
+ action: "create",
314
+ constraints: {
315
+ scope: ResourceScope.TEAM,
316
+ teamId: "1100-sales",
317
+ },
318
+ },
319
+ ];
320
+
321
+ const secondCall = actor.getTeamIds();
322
+
323
+ // Should return new team IDs after permissions change
324
+ expect(secondCall).toEqual(["1100-sales"]);
325
+ expect(firstCall).not.toEqual(secondCall);
326
+ });
327
+
328
+ test("should invalidate cache when permission is added", () => {
329
+ const actor = new TestActor(
330
+ "1001-user-1",
331
+ "0002-business-1",
332
+ "token-123",
333
+ [
334
+ {
335
+ resource: "document",
336
+ action: "retrieve",
337
+ constraints: {
338
+ scope: ResourceScope.TEAM,
339
+ teamId: "1100-legal",
340
+ },
341
+ },
342
+ ],
343
+ );
344
+
345
+ const firstCall = actor.getTeamIds();
346
+ expect(firstCall).toEqual(["1100-legal"]);
347
+
348
+ // Add new permission
349
+ actor.addPermission({
350
+ resource: "customer",
351
+ action: "create",
352
+ constraints: {
353
+ scope: ResourceScope.TEAM,
354
+ teamId: "1100-sales",
355
+ },
356
+ });
357
+
358
+ const secondCall = actor.getTeamIds();
359
+
360
+ // Should include new team
361
+ expect(secondCall.length).toEqual(2);
362
+ expect(secondCall.includes("1100-legal")).toEqual(true);
363
+ expect(secondCall.includes("1100-sales")).toEqual(true);
364
+ });
365
+
366
+ test("should invalidate cache when permissions are added", () => {
367
+ const actor = new TestActor(
368
+ "1001-user-1",
369
+ "0002-business-1",
370
+ "token-123",
371
+ [
372
+ {
373
+ resource: "document",
374
+ action: "retrieve",
375
+ constraints: {
376
+ scope: ResourceScope.TEAM,
377
+ teamId: "1100-legal",
378
+ },
379
+ },
380
+ ],
381
+ );
382
+
383
+ const firstCall = actor.getTeamIds();
384
+ expect(firstCall).toEqual(["1100-legal"]);
385
+
386
+ // Add multiple permissions
387
+ actor.addPermissions([
388
+ {
389
+ resource: "customer",
390
+ action: "create",
391
+ constraints: {
392
+ scope: ResourceScope.TEAM,
393
+ teamId: "1100-sales",
394
+ },
395
+ },
396
+ {
397
+ resource: "product",
398
+ action: "update",
399
+ constraints: {
400
+ scope: ResourceScope.TEAM,
401
+ teamId: "1100-engineering",
402
+ },
403
+ },
404
+ ]);
405
+
406
+ const secondCall = actor.getTeamIds();
407
+
408
+ // Should include all teams
409
+ expect(secondCall.length).toEqual(3);
410
+ expect(secondCall.includes("1100-legal")).toEqual(true);
411
+ expect(secondCall.includes("1100-sales")).toEqual(true);
412
+ expect(secondCall.includes("1100-engineering")).toEqual(true);
413
+ });
414
+ });
415
+
416
+ describe("Actor.getTeamIds() - Real-world scenarios", () => {
417
+ test("Legal team member with document permissions", () => {
418
+ const actor = new TestActor(
419
+ "1001-alice",
420
+ "0002-techcorp",
421
+ "token-alice",
422
+ [
423
+ {
424
+ resource: "document",
425
+ action: "retrieve",
426
+ constraints: {
427
+ scope: ResourceScope.TEAM,
428
+ teamId: "1100-legal",
429
+ },
430
+ },
431
+ {
432
+ resource: "document",
433
+ action: "create",
434
+ constraints: {
435
+ scope: ResourceScope.TEAM,
436
+ teamId: "1100-legal",
437
+ },
438
+ },
439
+ {
440
+ resource: "document",
441
+ action: "update",
442
+ constraints: {
443
+ scope: ResourceScope.TEAM,
444
+ teamId: "1100-legal",
445
+ },
446
+ },
447
+ // Also has personal permissions
448
+ {
449
+ resource: "product",
450
+ action: "retrieve",
451
+ constraints: {
452
+ scope: ResourceScope.OWNED,
453
+ },
454
+ },
455
+ ],
456
+ );
457
+
458
+ const teamIds = actor.getTeamIds();
459
+
460
+ expect(teamIds).toEqual(["1100-legal"]);
461
+ });
462
+
463
+ test("Multi-team member (Legal + Sales)", () => {
464
+ const actor = new TestActor(
465
+ "1001-bob",
466
+ "0002-techcorp",
467
+ "token-bob",
468
+ [
469
+ // Legal team permissions
470
+ {
471
+ resource: "document",
472
+ action: "retrieve",
473
+ constraints: {
474
+ scope: ResourceScope.TEAM,
475
+ teamId: "1100-legal",
476
+ },
477
+ },
478
+ {
479
+ resource: "document",
480
+ action: "create",
481
+ constraints: {
482
+ scope: ResourceScope.TEAM,
483
+ teamId: "1100-legal",
484
+ },
485
+ },
486
+ // Sales team permissions
487
+ {
488
+ resource: "customer",
489
+ action: "retrieve",
490
+ constraints: {
491
+ scope: ResourceScope.TEAM,
492
+ teamId: "1100-sales",
493
+ },
494
+ },
495
+ {
496
+ resource: "customer",
497
+ action: "create",
498
+ constraints: {
499
+ scope: ResourceScope.TEAM,
500
+ teamId: "1100-sales",
501
+ },
502
+ },
503
+ {
504
+ resource: "quote",
505
+ action: "create",
506
+ constraints: {
507
+ scope: ResourceScope.TEAM,
508
+ teamId: "1100-sales",
509
+ },
510
+ },
511
+ ],
512
+ );
513
+
514
+ const teamIds = actor.getTeamIds();
515
+
516
+ expect(teamIds.length).toEqual(2);
517
+ expect(teamIds.includes("1100-legal")).toEqual(true);
518
+ expect(teamIds.includes("1100-sales")).toEqual(true);
519
+ });
520
+
521
+ test("Business owner with BUSINESS scope (no teams)", () => {
522
+ const actor = new TestActor(
523
+ "1001-owner",
524
+ "0002-techcorp",
525
+ "token-owner",
526
+ [
527
+ {
528
+ resource: "product",
529
+ action: "create",
530
+ constraints: {
531
+ scope: ResourceScope.BUSINESS,
532
+ },
533
+ },
534
+ {
535
+ resource: "product",
536
+ action: "retrieve",
537
+ constraints: {
538
+ scope: ResourceScope.BUSINESS,
539
+ },
540
+ },
541
+ {
542
+ resource: "user",
543
+ action: "create",
544
+ constraints: {
545
+ scope: ResourceScope.BUSINESS,
546
+ },
547
+ },
548
+ {
549
+ resource: "team",
550
+ action: "create",
551
+ constraints: {
552
+ scope: ResourceScope.BUSINESS,
553
+ },
554
+ },
555
+ ],
556
+ );
557
+
558
+ const teamIds = actor.getTeamIds();
559
+
560
+ expect(teamIds).toEqual([]);
561
+ });
562
+
563
+ test("Employee with mixed scope permissions", () => {
564
+ const actor = new TestActor(
565
+ "1001-carol",
566
+ "0002-techcorp",
567
+ "token-carol",
568
+ [
569
+ // Team permissions
570
+ {
571
+ resource: "document",
572
+ action: "retrieve",
573
+ constraints: {
574
+ scope: ResourceScope.TEAM,
575
+ teamId: "1100-engineering",
576
+ },
577
+ },
578
+ // Personal permissions
579
+ {
580
+ resource: "task",
581
+ action: "create",
582
+ constraints: {
583
+ scope: ResourceScope.OWNED,
584
+ },
585
+ },
586
+ {
587
+ resource: "task",
588
+ action: "retrieve",
589
+ constraints: {
590
+ scope: ResourceScope.OWNED,
591
+ },
592
+ },
593
+ // Business-wide read permissions
594
+ {
595
+ resource: "product",
596
+ action: "retrieve",
597
+ constraints: {
598
+ scope: ResourceScope.ALL,
599
+ },
600
+ },
601
+ ],
602
+ );
603
+
604
+ const teamIds = actor.getTeamIds();
605
+
606
+ expect(teamIds).toEqual(["1100-engineering"]);
607
+ });
608
+ });
@@ -0,0 +1,89 @@
1
+ import { test, expect } from "bun:test";
2
+ import { Actor, type Permission } from "../../mod.ts";
3
+
4
+ class TestActor extends Actor {
5
+ constructor(
6
+ public readonly id: string,
7
+ public readonly businessId: string,
8
+ public readonly token: string,
9
+ ) {
10
+ super();
11
+ }
12
+ }
13
+
14
+ test("Actor - basic properties", () => {
15
+ const actor = new TestActor("user-123", "business-456", "token-789");
16
+
17
+ expect(actor.id).toEqual("user-123");
18
+ expect(actor.businessId).toEqual("business-456");
19
+ expect(actor.token).toEqual("token-789");
20
+ expect(actor.permissions).toEqual([]);
21
+ });
22
+
23
+ test("Actor - set permissions", () => {
24
+ const actor = new TestActor("user-123", "business-456", "token-789");
25
+
26
+ const permissions: Permission[] = [
27
+ { resource: "user", action: "create" },
28
+ { resource: "user", action: "read" },
29
+ { resource: "post", action: "write" },
30
+ ];
31
+
32
+ actor.permissions = permissions;
33
+ expect(actor.permissions.length).toEqual(3);
34
+ expect(actor.permissions[0].resource).toEqual("user");
35
+ expect(actor.permissions[0].action).toEqual("create");
36
+ });
37
+
38
+ test("Actor - add single permission", () => {
39
+ const actor = new TestActor("user-123", "business-456", "token-789");
40
+
41
+ actor.addPermission({ resource: "user", action: "create" });
42
+ expect(actor.permissions.length).toEqual(1);
43
+
44
+ actor.addPermission({ resource: "user", action: "read" });
45
+ expect(actor.permissions.length).toEqual(2);
46
+
47
+ // Adding a new object with same values will add it (Set compares by reference)
48
+ actor.addPermission({ resource: "user", action: "create" });
49
+ expect(actor.permissions.length).toEqual(3);
50
+ });
51
+
52
+ test("Actor - add multiple permissions", () => {
53
+ const actor = new TestActor("user-123", "business-456", "token-789");
54
+
55
+ const permissions: Permission[] = [
56
+ { resource: "user", action: "create" },
57
+ { resource: "user", action: "read" },
58
+ { resource: "post", action: "write" },
59
+ ];
60
+
61
+ actor.addPermissions(permissions);
62
+ expect(actor.permissions.length).toEqual(3);
63
+
64
+ // Adding more permissions (new objects will be added even with same values)
65
+ actor.addPermissions([
66
+ { resource: "comment", action: "delete" },
67
+ { resource: "user", action: "create" }, // New object, will be added
68
+ ]);
69
+
70
+ expect(actor.permissions.length).toEqual(5);
71
+ });
72
+
73
+ test("Actor - permissions setter replaces all", () => {
74
+ const actor = new TestActor("user-123", "business-456", "token-789");
75
+
76
+ // Set initial permissions
77
+ actor.permissions = [
78
+ { resource: "user", action: "create" },
79
+ { resource: "user", action: "read" },
80
+ ];
81
+ expect(actor.permissions.length).toEqual(2);
82
+
83
+ // Replace with new set
84
+ actor.permissions = [
85
+ { resource: "post", action: "write" },
86
+ ];
87
+ expect(actor.permissions.length).toEqual(1);
88
+ expect(actor.permissions[0].resource).toEqual("post");
89
+ });