@tstdl/base 0.93.94 → 0.93.96

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 (71) hide show
  1. package/api/response.js +1 -1
  2. package/authentication/client/authentication.service.js +1 -1
  3. package/document-management/api/document-management.api.d.ts +19 -1
  4. package/document-management/api/document-management.api.js +8 -4
  5. package/document-management/models/document-category.model.d.ts +1 -0
  6. package/document-management/models/document-category.model.js +7 -1
  7. package/document-management/models/document-property.model.d.ts +1 -0
  8. package/document-management/models/document-property.model.js +7 -1
  9. package/document-management/models/document-type.model.d.ts +1 -0
  10. package/document-management/models/document-type.model.js +7 -1
  11. package/document-management/models/document-workflow.model.d.ts +1 -0
  12. package/document-management/models/document-workflow.model.js +6 -1
  13. package/document-management/server/api/document-management.api.d.ts +1 -0
  14. package/document-management/server/api/document-management.api.js +8 -7
  15. package/document-management/server/drizzle/{0000_glamorous_lorna_dane.sql → 0000_needy_steel_serpent.sql} +7 -0
  16. package/document-management/server/drizzle/meta/0000_snapshot.json +49 -1
  17. package/document-management/server/drizzle/meta/_journal.json +2 -2
  18. package/document-management/server/module.d.ts +1 -0
  19. package/document-management/server/module.js +1 -0
  20. package/document-management/server/services/document-category-type.service.d.ts +8 -3
  21. package/document-management/server/services/document-category-type.service.js +49 -6
  22. package/document-management/server/services/document-management.service.js +17 -15
  23. package/document-management/server/services/document-property.service.d.ts +3 -1
  24. package/document-management/server/services/document-property.service.js +23 -2
  25. package/document-management/server/services/document-validation.service.js +2 -1
  26. package/document-management/server/services/document-workflow.service.d.ts +3 -3
  27. package/document-management/server/services/document-workflow.service.js +34 -15
  28. package/document-management/server/services/document.service.d.ts +1 -1
  29. package/document-management/server/services/document.service.js +7 -2
  30. package/document-management/service-models/categories-and-types.view-model.d.ts +6 -0
  31. package/document-management/service-models/categories-and-types.view-model.js +18 -0
  32. package/document-management/service-models/document-management.view-model.d.ts +1 -0
  33. package/document-management/service-models/document-management.view-model.js +5 -0
  34. package/document-management/service-models/document.service-model.d.ts +7 -0
  35. package/document-management/service-models/document.service-model.js +7 -1
  36. package/document-management/service-models/enriched/enriched-document-category.view.d.ts +1 -0
  37. package/document-management/service-models/enriched/enriched-document-category.view.js +2 -0
  38. package/document-management/service-models/enriched/enriched-document-type.view.d.ts +1 -0
  39. package/document-management/service-models/enriched/enriched-document-type.view.js +2 -0
  40. package/document-management/tests/document-management-core.test.d.ts +1 -0
  41. package/document-management/tests/document-management-core.test.js +162 -0
  42. package/document-management/tests/document.service.test.d.ts +1 -0
  43. package/document-management/tests/document.service.test.js +139 -0
  44. package/document-management/tests/enum-helpers.test.d.ts +1 -0
  45. package/document-management/tests/enum-helpers.test.js +452 -0
  46. package/document-management/tests/helper.d.ts +24 -0
  47. package/document-management/tests/helper.js +39 -0
  48. package/{utils/format-error.js → errors/format.js} +32 -26
  49. package/errors/index.d.ts +1 -0
  50. package/errors/index.js +1 -0
  51. package/errors/tests/format.test.d.ts +1 -0
  52. package/errors/tests/format.test.js +84 -0
  53. package/http/http.error.d.ts +1 -1
  54. package/logger/formatters/json.js +2 -2
  55. package/logger/formatters/pretty-print.js +16 -4
  56. package/logger/formatters/pretty-print.test.d.ts +1 -0
  57. package/logger/formatters/pretty-print.test.js +60 -0
  58. package/mail/mail.service.js +1 -1
  59. package/orm/decorators.d.ts +5 -5
  60. package/package.json +5 -5
  61. package/rpc/rpc.error.js +1 -1
  62. package/schema/schema.error.d.ts +1 -1
  63. package/task-queue/postgres/task-queue.js +1 -1
  64. package/test-column-builder.d.ts +22 -0
  65. package/test-column-builder.js +1 -0
  66. package/test4.d.ts +0 -22
  67. package/test4.js +8 -122
  68. package/test5.js +8 -6
  69. package/utils/index.d.ts +0 -1
  70. package/utils/index.js +0 -1
  71. /package/{utils/format-error.d.ts → errors/format.d.ts} +0 -0
@@ -0,0 +1,452 @@
1
+ import { afterAll, beforeAll, beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { AiService } from '../../ai/ai.service.js';
3
+ import { AiModuleOptions } from '../../ai/module.js';
4
+ import { NotFoundError } from '../../errors/not-found.error.js';
5
+ import { runInInjectionContext } from '../../injector/index.js';
6
+ import { ObjectStorage } from '../../object-storage/index.js';
7
+ import { TaskQueue } from '../../task-queue/index.js';
8
+ import { setupIntegrationTest, truncateTables } from '../../unit-test/index.js';
9
+ import { DocumentPropertyDataType } from '../models/document-property.model.js';
10
+ import { configureDocumentManagement } from '../server/configure.js';
11
+ import { migrateDocumentManagementSchema } from '../server/module.js';
12
+ import { DocumentCategoryTypeService } from '../server/services/document-category-type.service.js';
13
+ import { DocumentManagementService } from '../server/services/document-management.service.js';
14
+ import { DocumentPropertyService } from '../server/services/document-property.service.js';
15
+ import { TestDocumentManagementAncillaryService, TestDocumentManagementAuthorizationService } from './helper.js';
16
+ describe('Document Management Extended Suite', () => {
17
+ let injector;
18
+ let database;
19
+ let documentManagementService;
20
+ let categoryTypeService;
21
+ let propertyService;
22
+ const schema = 'document_management';
23
+ const tenantId = crypto.randomUUID();
24
+ const otherTenantId = crypto.randomUUID();
25
+ beforeAll(async () => {
26
+ ({ injector, database } = await setupIntegrationTest({
27
+ modules: { taskQueue: false, messageBus: true },
28
+ orm: { schema },
29
+ }));
30
+ injector.register(AiModuleOptions, { useValue: {} });
31
+ injector.register(AiService, { useValue: { processFile: vi.fn() } });
32
+ injector.register(ObjectStorage, {
33
+ useValue: {
34
+ uploadObject: vi.fn(),
35
+ getDownloadUrl: vi.fn(),
36
+ getContent: vi.fn(),
37
+ getContentStream: vi.fn(),
38
+ getObject: vi.fn(),
39
+ exists: vi.fn(),
40
+ },
41
+ });
42
+ injector.register(TaskQueue, {
43
+ useValue: {
44
+ enqueue: vi.fn(),
45
+ enqueueMany: vi.fn(),
46
+ process: vi.fn(),
47
+ },
48
+ });
49
+ configureDocumentManagement({
50
+ ancillaryService: TestDocumentManagementAncillaryService,
51
+ authorizationService: TestDocumentManagementAuthorizationService,
52
+ fileObjectStorageModule: 'documents',
53
+ fileUploadObjectStorageModule: 'document-uploads',
54
+ filePreviewObjectStorageModule: 'document-previews',
55
+ });
56
+ await runInInjectionContext(injector, migrateDocumentManagementSchema);
57
+ documentManagementService = await injector.resolveAsync(DocumentManagementService);
58
+ categoryTypeService = await injector.resolveAsync(DocumentCategoryTypeService);
59
+ propertyService = await injector.resolveAsync(DocumentPropertyService);
60
+ });
61
+ afterAll(async () => {
62
+ await injector?.dispose();
63
+ });
64
+ beforeEach(async () => {
65
+ await truncateTables(database, schema, ['type_property', 'property', 'type', 'category']);
66
+ categoryTypeService.invalidateCache(tenantId);
67
+ propertyService.invalidateCache(tenantId);
68
+ categoryTypeService.invalidateCache(otherTenantId);
69
+ propertyService.invalidateCache(otherTenantId);
70
+ });
71
+ // --- 1. Category Management ---
72
+ test('1. Create category with key', async () => {
73
+ await runInInjectionContext(injector, async () => {
74
+ const category = await categoryTypeService.createCategory({ tenantId, label: 'Cat Key', parentId: null, key: 'cat-key' });
75
+ expect(category.key).toBe('cat-key');
76
+ expect(await categoryTypeService.getCategoryId(tenantId, 'cat-key')).toBe(category.id);
77
+ });
78
+ });
79
+ test('2. Create category without key', async () => {
80
+ await runInInjectionContext(injector, async () => {
81
+ const category = await categoryTypeService.createCategory({ tenantId, label: 'Cat No Key', parentId: null });
82
+ expect(category.key).toBeNull();
83
+ await expect(categoryTypeService.getCategoryId(tenantId, 'cat-no-key')).rejects.toThrow(NotFoundError);
84
+ });
85
+ });
86
+ test('3. Category Hierarchy with keys', async () => {
87
+ await runInInjectionContext(injector, async () => {
88
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
89
+ categoryLabels: { root: 'Root', child: 'Child' },
90
+ categoryParents: { root: null, child: 'root' },
91
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
92
+ });
93
+ const rootId = await categoryTypeService.getCategoryId(tenantId, 'root');
94
+ const childId = await categoryTypeService.getCategoryId(tenantId, 'child');
95
+ const child = await categoryTypeService.loadCategory(tenantId, childId);
96
+ expect(child.parentId).toBe(rootId);
97
+ });
98
+ });
99
+ test('4. Update category label via initialize', async () => {
100
+ await runInInjectionContext(injector, async () => {
101
+ await categoryTypeService.createCategory({ tenantId, label: 'Old Label', parentId: null, key: 'cat-update' });
102
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
103
+ categoryLabels: { 'cat-update': 'New Label' },
104
+ categoryParents: { 'cat-update': null },
105
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
106
+ });
107
+ const id = await categoryTypeService.getCategoryId(tenantId, 'cat-update');
108
+ const category = await categoryTypeService.loadCategory(tenantId, id);
109
+ expect(category.label).toBe('New Label');
110
+ });
111
+ });
112
+ test('5. Update category parent via initialize', async () => {
113
+ await runInInjectionContext(injector, async () => {
114
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
115
+ categoryLabels: { 'root': 'Root', 'child': 'Child', 'new-root': 'New Root' },
116
+ categoryParents: { 'root': null, 'child': 'root', 'new-root': null },
117
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
118
+ });
119
+ // Move child to new-root
120
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
121
+ categoryLabels: { 'root': 'Root', 'child': 'Child', 'new-root': 'New Root' },
122
+ categoryParents: { 'root': null, 'child': 'new-root', 'new-root': null },
123
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
124
+ });
125
+ const childId = await categoryTypeService.getCategoryId(tenantId, 'child');
126
+ const newRootId = await categoryTypeService.getCategoryId(tenantId, 'new-root');
127
+ const child = await categoryTypeService.loadCategory(tenantId, childId);
128
+ expect(child.parentId).toBe(newRootId);
129
+ });
130
+ });
131
+ // --- 2. Type Management ---
132
+ test('6. Create type with key', async () => {
133
+ await runInInjectionContext(injector, async () => {
134
+ const category = await categoryTypeService.createCategory({ tenantId, label: 'Cat', parentId: null });
135
+ const type = await categoryTypeService.createType({ tenantId, label: 'Type Key', categoryId: category.id, key: 'type-key' });
136
+ expect(type.key).toBe('type-key');
137
+ expect(await categoryTypeService.getTypeId(tenantId, 'type-key')).toBe(type.id);
138
+ });
139
+ });
140
+ test('7. Create type without key', async () => {
141
+ await runInInjectionContext(injector, async () => {
142
+ const category = await categoryTypeService.createCategory({ tenantId, label: 'Cat', parentId: null });
143
+ const type = await categoryTypeService.createType({ tenantId, label: 'Type No Key', categoryId: category.id });
144
+ expect(type.key).toBeNull();
145
+ });
146
+ });
147
+ test('8. Assign type to category via initialize', async () => {
148
+ await runInInjectionContext(injector, async () => {
149
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
150
+ categoryLabels: { cat: 'Category' },
151
+ categoryParents: { cat: null },
152
+ typeLabels: { type: 'Type' },
153
+ typeCategories: { type: 'cat' },
154
+ propertyConfigurations: {}, typeProperties: { type: [] },
155
+ });
156
+ const catId = await categoryTypeService.getCategoryId(tenantId, 'cat');
157
+ const typeId = await categoryTypeService.getTypeId(tenantId, 'type');
158
+ const type = await categoryTypeService.loadType(tenantId, typeId);
159
+ expect(type.categoryId).toBe(catId);
160
+ });
161
+ });
162
+ test('9. Update type label via initialize', async () => {
163
+ await runInInjectionContext(injector, async () => {
164
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
165
+ categoryLabels: { cat: 'Category' },
166
+ categoryParents: { cat: null },
167
+ typeLabels: { type: 'Old Type Label' },
168
+ typeCategories: { type: 'cat' },
169
+ propertyConfigurations: {}, typeProperties: { type: [] },
170
+ });
171
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
172
+ categoryLabels: { cat: 'Category' },
173
+ categoryParents: { cat: null },
174
+ typeLabels: { type: 'New Type Label' },
175
+ typeCategories: { type: 'cat' },
176
+ propertyConfigurations: {}, typeProperties: { type: [] },
177
+ });
178
+ const typeId = await categoryTypeService.getTypeId(tenantId, 'type');
179
+ const type = await categoryTypeService.loadType(tenantId, typeId);
180
+ expect(type.label).toBe('New Type Label');
181
+ });
182
+ });
183
+ test('10. Update type category assignment', async () => {
184
+ await runInInjectionContext(injector, async () => {
185
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
186
+ categoryLabels: { cat1: 'Cat 1', cat2: 'Cat 2' },
187
+ categoryParents: { cat1: null, cat2: null },
188
+ typeLabels: { type: 'Type' },
189
+ typeCategories: { type: 'cat1' },
190
+ propertyConfigurations: {}, typeProperties: { type: [] },
191
+ });
192
+ const cat2Id = await categoryTypeService.getCategoryId(tenantId, 'cat2');
193
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
194
+ categoryLabels: { cat1: 'Cat 1', cat2: 'Cat 2' },
195
+ categoryParents: { cat1: null, cat2: null },
196
+ typeLabels: { type: 'Type' },
197
+ typeCategories: { type: 'cat2' }, // Move to cat2
198
+ propertyConfigurations: {}, typeProperties: { type: [] },
199
+ });
200
+ const typeId = await categoryTypeService.getTypeId(tenantId, 'type');
201
+ const type = await categoryTypeService.loadType(tenantId, typeId);
202
+ expect(type.categoryId).toBe(cat2Id);
203
+ });
204
+ });
205
+ // --- 3. Property Management ---
206
+ test('11. Create property with key (Text)', async () => {
207
+ await runInInjectionContext(injector, async () => {
208
+ const prop = await propertyService.createProperty({ tenantId, label: 'Text Prop', dataType: DocumentPropertyDataType.Text, key: 'prop-text' });
209
+ expect(prop.dataType).toBe(DocumentPropertyDataType.Text);
210
+ expect(await propertyService.getPropertyId(tenantId, 'prop-text')).toBe(prop.id);
211
+ });
212
+ });
213
+ test('12. Create property with key (Number)', async () => {
214
+ await runInInjectionContext(injector, async () => {
215
+ const prop = await propertyService.createProperty({ tenantId, label: 'Num Prop', dataType: DocumentPropertyDataType.Integer, key: 'prop-num' });
216
+ expect(prop.dataType).toBe(DocumentPropertyDataType.Integer);
217
+ });
218
+ });
219
+ test('13. Update property label', async () => {
220
+ await runInInjectionContext(injector, async () => {
221
+ await propertyService.createProperty({ tenantId, label: 'Old', dataType: DocumentPropertyDataType.Text, key: 'prop' });
222
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
223
+ categoryLabels: {}, categoryParents: {}, typeLabels: {}, typeCategories: {},
224
+ propertyConfigurations: { prop: [DocumentPropertyDataType.Text, 'New'] },
225
+ typeProperties: {},
226
+ });
227
+ const id = await propertyService.getPropertyId(tenantId, 'prop');
228
+ const prop = (await propertyService.loadViews(tenantId)).find(p => p.id === id);
229
+ expect(prop?.label).toBe('New');
230
+ });
231
+ });
232
+ test('14. Update property data type', async () => {
233
+ await runInInjectionContext(injector, async () => {
234
+ await propertyService.createProperty({ tenantId, label: 'Prop', dataType: DocumentPropertyDataType.Text, key: 'prop' });
235
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
236
+ categoryLabels: {}, categoryParents: {}, typeLabels: {}, typeCategories: {},
237
+ propertyConfigurations: { prop: [DocumentPropertyDataType.Integer, 'Prop'] },
238
+ typeProperties: {},
239
+ });
240
+ const id = await propertyService.getPropertyId(tenantId, 'prop');
241
+ const prop = (await propertyService.loadViews(tenantId)).find(p => p.id === id);
242
+ expect(prop?.dataType).toBe(DocumentPropertyDataType.Integer);
243
+ });
244
+ });
245
+ test('15. Assign properties to types', async () => {
246
+ await runInInjectionContext(injector, async () => {
247
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
248
+ categoryLabels: { cat: 'Cat' }, categoryParents: { cat: null },
249
+ typeLabels: { type: 'Type' }, typeCategories: { type: 'cat' },
250
+ propertyConfigurations: { prop1: [DocumentPropertyDataType.Text, 'P1'], prop2: [DocumentPropertyDataType.Integer, 'P2'] },
251
+ typeProperties: { type: ['prop1', 'prop2'] },
252
+ });
253
+ const typeId = await categoryTypeService.getTypeId(tenantId, 'type');
254
+ const prop1Id = await propertyService.getPropertyId(tenantId, 'prop1');
255
+ const prop2Id = await propertyService.getPropertyId(tenantId, 'prop2');
256
+ const props = await propertyService.loadViews(tenantId);
257
+ const p1 = props.find(p => p.id === prop1Id);
258
+ const p2 = props.find(p => p.id === prop2Id);
259
+ expect(p1?.typeIds).toContain(typeId);
260
+ expect(p2?.typeIds).toContain(typeId);
261
+ });
262
+ });
263
+ // --- 4. Cache Behavior ---
264
+ test('16. Verify cache hit', async () => {
265
+ await runInInjectionContext(injector, async () => {
266
+ const category = await categoryTypeService.createCategory({ tenantId, label: 'Cache Test', parentId: null, key: 'cache-hit' });
267
+ // Load once to populate cache
268
+ await categoryTypeService.getCategoryId(tenantId, 'cache-hit');
269
+ // Delete from DB manually
270
+ await categoryTypeService.categoryRepository.hardDeleteByQuery({ tenantId, id: category.id });
271
+ // Should still find it in cache
272
+ expect(await categoryTypeService.getCategoryId(tenantId, 'cache-hit')).toBe(category.id);
273
+ });
274
+ });
275
+ test('17. Verify cache miss throws NotFound', async () => {
276
+ await runInInjectionContext(injector, async () => {
277
+ await expect(categoryTypeService.getCategoryId(tenantId, 'missing')).rejects.toThrow(NotFoundError);
278
+ });
279
+ });
280
+ test('18. Manual cache invalidation', async () => {
281
+ await runInInjectionContext(injector, async () => {
282
+ const category = await categoryTypeService.createCategory({ tenantId, label: 'Invalidate', parentId: null, key: 'inv' });
283
+ await categoryTypeService.getCategoryId(tenantId, 'inv'); // cache it
284
+ categoryTypeService.invalidateCache(tenantId);
285
+ await categoryTypeService.categoryRepository.hardDeleteByQuery({ tenantId, id: category.id });
286
+ // Should fail now as cache is cleared and DB is empty
287
+ await expect(categoryTypeService.getCategoryId(tenantId, 'inv')).rejects.toThrow(NotFoundError);
288
+ });
289
+ });
290
+ test('19. Automatic invalidation after initialize', async () => {
291
+ await runInInjectionContext(injector, async () => {
292
+ await categoryTypeService.createCategory({ tenantId, label: 'Auto', parentId: null, key: 'auto' });
293
+ await categoryTypeService.getCategoryId(tenantId, 'auto'); // cache it
294
+ // Run initialize (should invalidate)
295
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
296
+ categoryLabels: { auto: 'Auto Updated' },
297
+ categoryParents: { auto: null },
298
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
299
+ });
300
+ // If we query DB directly, we see the update
301
+ const id = await categoryTypeService.getCategoryId(tenantId, 'auto');
302
+ const cat = await categoryTypeService.loadCategory(tenantId, id);
303
+ expect(cat.label).toBe('Auto Updated');
304
+ });
305
+ });
306
+ test('20. Multi-tenant isolation', async () => {
307
+ await runInInjectionContext(injector, async () => {
308
+ await categoryTypeService.createCategory({ tenantId, label: 'T1', parentId: null, key: 'shared' });
309
+ await categoryTypeService.createCategory({ tenantId: otherTenantId, label: 'T2', parentId: null, key: 'shared' });
310
+ const id1 = await categoryTypeService.getCategoryId(tenantId, 'shared');
311
+ const id2 = await categoryTypeService.getCategoryId(otherTenantId, 'shared');
312
+ expect(id1).not.toBe(id2);
313
+ });
314
+ });
315
+ // --- 5. Initialization Logic ---
316
+ test('21. Initialize from scratch', async () => {
317
+ await runInInjectionContext(injector, async () => {
318
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
319
+ categoryLabels: { c: 'C' }, categoryParents: { c: null },
320
+ typeLabels: { t: 'T' }, typeCategories: { t: 'c' },
321
+ propertyConfigurations: { p: [DocumentPropertyDataType.Text, 'P'] },
322
+ typeProperties: { t: ['p'] },
323
+ });
324
+ expect(await categoryTypeService.getCategoryId(tenantId, 'c')).toBeDefined();
325
+ expect(await categoryTypeService.getTypeId(tenantId, 't')).toBeDefined();
326
+ expect(await propertyService.getPropertyId(tenantId, 'p')).toBeDefined();
327
+ });
328
+ });
329
+ test('22. Idempotent initialization', async () => {
330
+ await runInInjectionContext(injector, async () => {
331
+ const config = {
332
+ categoryLabels: { c: 'C' }, categoryParents: { c: null },
333
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
334
+ };
335
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, config);
336
+ const id1 = await categoryTypeService.getCategoryId(tenantId, 'c');
337
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, config);
338
+ const id2 = await categoryTypeService.getCategoryId(tenantId, 'c');
339
+ expect(id1).toBe(id2);
340
+ });
341
+ });
342
+ test('23. Partial update (add newly defined entity)', async () => {
343
+ await runInInjectionContext(injector, async () => {
344
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
345
+ categoryLabels: { c1: 'C1' }, categoryParents: { c1: null },
346
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
347
+ });
348
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
349
+ categoryLabels: { c1: 'C1', c2: 'C2' },
350
+ categoryParents: { c1: null, c2: null },
351
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
352
+ });
353
+ expect(await categoryTypeService.getCategoryId(tenantId, 'c1')).toBeDefined();
354
+ expect(await categoryTypeService.getCategoryId(tenantId, 'c2')).toBeDefined();
355
+ });
356
+ });
357
+ test('24. Initialize with invalid parent key throws', async () => {
358
+ await runInInjectionContext(injector, async () => {
359
+ await expect(documentManagementService.initializeCategoriesAndTypes(tenantId, {
360
+ categoryLabels: { child: 'Child' },
361
+ // @ts-expect-error - Intentional invalid parent key
362
+ categoryParents: { child: 'missing-parent' },
363
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
364
+ })).rejects.toThrow();
365
+ });
366
+ });
367
+ test('25. Initialize with invalid type category throws', async () => {
368
+ await runInInjectionContext(injector, async () => {
369
+ await expect(documentManagementService.initializeCategoriesAndTypes(tenantId, {
370
+ categoryLabels: {}, categoryParents: {},
371
+ typeLabels: { type: 'Type' },
372
+ // @ts-expect-error - Intentional invalid type category
373
+ typeCategories: { type: 'missing-cat' },
374
+ propertyConfigurations: {}, typeProperties: { type: [] },
375
+ })).rejects.toThrow();
376
+ });
377
+ });
378
+ // --- 6. Edge Cases ---
379
+ test('26. Duplicate keys prevented by DB constraint', async () => {
380
+ // This tests the DB constraint we added
381
+ await runInInjectionContext(injector, async () => {
382
+ await categoryTypeService.createCategory({ tenantId, label: 'C1', parentId: null, key: 'dup' });
383
+ await expect(categoryTypeService.createCategory({ tenantId, label: 'C2', parentId: null, key: 'dup' }))
384
+ .rejects.toThrow();
385
+ });
386
+ });
387
+ test('27. Null key allows duplicates', async () => {
388
+ // This confirms that nulls are not unique
389
+ await runInInjectionContext(injector, async () => {
390
+ await categoryTypeService.createCategory({ tenantId, label: 'C1', parentId: null }); // key is null
391
+ await expect(categoryTypeService.createCategory({ tenantId, label: 'C2', parentId: null }))
392
+ .resolves.toBeDefined();
393
+ });
394
+ });
395
+ test('28. Initialize with multiple properties for a type', async () => {
396
+ await runInInjectionContext(injector, async () => {
397
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
398
+ categoryLabels: { cat: 'Cat' }, categoryParents: { cat: null },
399
+ typeLabels: { type: 'Type' }, typeCategories: { type: 'cat' },
400
+ propertyConfigurations: {
401
+ p1: [DocumentPropertyDataType.Text, 'P1'],
402
+ p2: [DocumentPropertyDataType.Integer, 'P2'],
403
+ p3: [DocumentPropertyDataType.Boolean, 'P3'],
404
+ },
405
+ typeProperties: { type: ['p1', 'p2', 'p3'] },
406
+ });
407
+ const typeId = await categoryTypeService.getTypeId(tenantId, 'type');
408
+ const p1Id = await propertyService.getPropertyId(tenantId, 'p1');
409
+ const p3Id = await propertyService.getPropertyId(tenantId, 'p3');
410
+ const props = await propertyService.loadViews(tenantId);
411
+ const p1 = props.find(p => p.id === p1Id);
412
+ const p3 = props.find(p => p.id === p3Id);
413
+ expect(p1?.typeIds).toContain(typeId);
414
+ expect(p3?.typeIds).toContain(typeId);
415
+ });
416
+ });
417
+ test('29. Large number of entities', async () => {
418
+ await runInInjectionContext(injector, async () => {
419
+ const count = 50;
420
+ const categoryLabels = {};
421
+ const categoryParents = {};
422
+ for (let i = 0; i < count; i++) {
423
+ categoryLabels[`cat-${i}`] = `Category ${i}`;
424
+ categoryParents[`cat-${i}`] = null;
425
+ }
426
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
427
+ categoryLabels, categoryParents,
428
+ typeLabels: {}, typeCategories: {}, propertyConfigurations: {}, typeProperties: {},
429
+ });
430
+ const id = await categoryTypeService.getCategoryId(tenantId, `cat-${count - 1}`);
431
+ expect(id).toBeDefined();
432
+ });
433
+ });
434
+ test('Missing key throws NotFoundError', async () => {
435
+ await runInInjectionContext(injector, async () => {
436
+ await expect(categoryTypeService.getCategoryId(tenantId, 'non-existent')).rejects.toThrow(NotFoundError);
437
+ });
438
+ });
439
+ test('loadCategoriesAndTypes returns properties', async () => {
440
+ await runInInjectionContext(injector, async () => {
441
+ await documentManagementService.initializeCategoriesAndTypes(tenantId, {
442
+ categoryLabels: {}, categoryParents: {}, typeLabels: {}, typeCategories: {},
443
+ propertyConfigurations: { p1: [DocumentPropertyDataType.Text, 'P1'] },
444
+ typeProperties: {},
445
+ });
446
+ const result = await categoryTypeService.loadCategoriesAndTypes(tenantId);
447
+ expect(result.properties).toBeDefined();
448
+ expect(result.properties).toHaveLength(1);
449
+ expect(result.properties[0]?.label).toBe('P1');
450
+ });
451
+ });
452
+ });
@@ -0,0 +1,24 @@
1
+ import { DocumentManagementAuthorizationService } from '../authorization/document-management-authorization.service.js';
2
+ import type { DocumentCollection } from '../models/document-collection.model.js';
3
+ import { DocumentManagementAncillaryService } from '../server/services/document-management-ancillary.service.js';
4
+ import type { DocumentCollectionMetadata } from '../service-models/document-collection-metadata.service-model.js';
5
+ export declare class TestDocumentManagementAncillaryService extends DocumentManagementAncillaryService {
6
+ resolveMetadata(_tenantId: string, collections: DocumentCollection[]): DocumentCollectionMetadata[];
7
+ }
8
+ export declare class TestDocumentManagementAuthorizationService extends DocumentManagementAuthorizationService {
9
+ getTenantId(): string;
10
+ getSubject(): string;
11
+ canReadCollection(): boolean;
12
+ canCreateDocuments(): boolean;
13
+ canDeleteDocuments(): boolean;
14
+ canAssignDocuments(): boolean;
15
+ canUpdateDocument(): boolean;
16
+ canApproveDocument(): boolean;
17
+ canRejectDocument(): boolean;
18
+ canManageRequests(): boolean;
19
+ canManageCategoriesAndTypes(): boolean;
20
+ canReadDocumentRequestsTemplates(): boolean;
21
+ canManageDocumentRequestsTemplates(): boolean;
22
+ canManageValidationDefinitions(): boolean;
23
+ canProgressDocumentWorkflow(): boolean;
24
+ }
@@ -0,0 +1,39 @@
1
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
2
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
3
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
4
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
5
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
6
+ };
7
+ import { Singleton } from '../../injector/index.js';
8
+ import { DocumentManagementAuthorizationService } from '../authorization/document-management-authorization.service.js';
9
+ import { DocumentManagementAncillaryService } from '../server/services/document-management-ancillary.service.js';
10
+ let TestDocumentManagementAncillaryService = class TestDocumentManagementAncillaryService extends DocumentManagementAncillaryService {
11
+ resolveMetadata(_tenantId, collections) {
12
+ return collections.map((c) => ({ id: c.id, name: `Collection ${c.id}`, group: 'Test' }));
13
+ }
14
+ };
15
+ TestDocumentManagementAncillaryService = __decorate([
16
+ Singleton()
17
+ ], TestDocumentManagementAncillaryService);
18
+ export { TestDocumentManagementAncillaryService };
19
+ let TestDocumentManagementAuthorizationService = class TestDocumentManagementAuthorizationService extends DocumentManagementAuthorizationService {
20
+ getTenantId() { return 'test-tenant'; }
21
+ getSubject() { return 'test-user'; }
22
+ canReadCollection() { return true; }
23
+ canCreateDocuments() { return true; }
24
+ canDeleteDocuments() { return true; }
25
+ canAssignDocuments() { return true; }
26
+ canUpdateDocument() { return true; }
27
+ canApproveDocument() { return true; }
28
+ canRejectDocument() { return true; }
29
+ canManageRequests() { return true; }
30
+ canManageCategoriesAndTypes() { return true; }
31
+ canReadDocumentRequestsTemplates() { return true; }
32
+ canManageDocumentRequestsTemplates() { return true; }
33
+ canManageValidationDefinitions() { return true; }
34
+ canProgressDocumentWorkflow() { return true; }
35
+ };
36
+ TestDocumentManagementAuthorizationService = __decorate([
37
+ Singleton()
38
+ ], TestDocumentManagementAuthorizationService);
39
+ export { TestDocumentManagementAuthorizationService };
@@ -1,8 +1,7 @@
1
- import { unwrapError } from '../errors/utils.js';
2
- import { decycle } from './object/decycle.js';
3
- import { objectKeys } from './object/object.js';
4
- import { tryChain } from './try-chain.js';
5
- import { isArray, isDefined, isFunction, isObject, isString, isUndefined } from './type-guards.js';
1
+ import { decycle, objectKeys } from '../utils/object/index.js';
2
+ import { tryChain } from '../utils/try-chain.js';
3
+ import { isArray, isDefined, isFunction, isObject, isString, isUndefined } from '../utils/type-guards.js';
4
+ import { unwrapError } from './utils.js';
6
5
  export function serializeError(error, options = {}) {
7
6
  const { includeName = true, includeRest = 'if-no-extra-info', includeExtraInfo = true, includeStack = true, depth = 5, } = options;
8
7
  if (depth <= 0) {
@@ -97,39 +96,46 @@ function formatSerializedError(serialized, options) {
97
96
  const { includeName = true, includeStack = true, depth = 5 } = options;
98
97
  // 1. Header (Name: Message)
99
98
  const prefix = includeName && name ? `${name}: ` : '';
100
- const header = `${prefix}${message}`;
99
+ const messageString = String(message);
100
+ const header = `${prefix}${messageString}`;
101
+ const alignedHeader = (prefix.length > 0 && messageString.includes('\n'))
102
+ ? `${prefix}${messageString.replace(/\n/g, `\n${' '.repeat(prefix.length)}`)}`
103
+ : header;
104
+ let formatted = alignedHeader;
101
105
  // 2. Body (Rest / Extra Info)
102
- let body = '';
103
- if (rest) {
104
- body += `\n${JSON.stringify(rest, null, 2)}`;
106
+ if (isDefined(rest)) {
107
+ formatted += `\n${indent(JSON.stringify(rest, null, 2), 4)}`;
105
108
  }
106
- if (extraInfo) {
107
- body += `\n${JSON.stringify(extraInfo, null, 2)}`;
109
+ if (isDefined(extraInfo)) {
110
+ formatted += `\n${indent(JSON.stringify(extraInfo, null, 2), 4)}`;
108
111
  }
109
112
  // 3. Stack Trace
110
- let stackString = '';
111
- if (includeStack && stack) {
113
+ if (includeStack && isDefined(stack)) {
112
114
  // Deduplicate header if stack already starts with it (standard Node.js behavior)
113
115
  if (stack.startsWith(header)) {
114
- stackString = stack.slice(header.length);
116
+ formatted += stack.slice(header.length);
115
117
  }
116
118
  else {
117
- stackString = `\n${stack}`;
119
+ formatted += `\n${stack}`;
118
120
  }
119
121
  }
120
122
  // 4. Recursion (Causes and Aggregate Errors)
121
123
  const nextOptions = { ...options, depth: depth - 1 };
122
- const causeString = (includeStack && cause)
123
- ? formatNestedError('Caused by:', cause, nextOptions)
124
- : '';
125
- const aggregateErrorsString = aggregateErrors
126
- ? aggregateErrors
127
- .map((err, i) => formatNestedError(`Sub-error #${i + 1}:`, err, nextOptions))
128
- .join('')
129
- : '';
130
- return `${header}${body}${stackString}${causeString}${aggregateErrorsString}`;
124
+ if (includeStack && isDefined(cause)) {
125
+ formatted += formatNestedError('Caused by:', cause, nextOptions);
126
+ }
127
+ if (isArray(aggregateErrors)) {
128
+ for (const [index, err] of aggregateErrors.entries()) {
129
+ formatted += formatNestedError(`Sub-error #${index + 1}:`, err, nextOptions);
130
+ }
131
+ }
132
+ return formatted;
131
133
  }
132
134
  function formatNestedError(prefix, serializedError, options) {
133
- const formatted = formatSerializedError(serializedError, options).replace(/\n/g, '\n ');
134
- return `\n\n${prefix}\n ${formatted}`;
135
+ const formatted = formatSerializedError(serializedError, options);
136
+ return `\n${prefix}\n${indent(formatted, 2)}`;
137
+ }
138
+ function indent(text, spaces) {
139
+ const indentation = ' '.repeat(spaces);
140
+ return text.split('\n').map((line) => (line.length > 0 ? `${indentation}${line}` : line)).join('\n');
135
141
  }
package/errors/index.d.ts CHANGED
@@ -10,6 +10,7 @@ export * from './custom.error.js';
10
10
  export * from './details.error.js';
11
11
  export * from './errors.localization.js';
12
12
  export * from './forbidden.error.js';
13
+ export * from './format.js';
13
14
  export * from './invalid-credentials.error.js';
14
15
  export * from './invalid-token.error.js';
15
16
  export * from './max-bytes-exceeded.error.js';
package/errors/index.js CHANGED
@@ -10,6 +10,7 @@ export * from './custom.error.js';
10
10
  export * from './details.error.js';
11
11
  export * from './errors.localization.js';
12
12
  export * from './forbidden.error.js';
13
+ export * from './format.js';
13
14
  export * from './invalid-credentials.error.js';
14
15
  export * from './invalid-token.error.js';
15
16
  export * from './max-bytes-exceeded.error.js';
@@ -0,0 +1 @@
1
+ export {};