@tstdl/base 0.93.95 → 0.93.97
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/document-management/api/document-management.api.d.ts +19 -1
- package/document-management/api/document-management.api.js +8 -4
- package/document-management/models/document-category.model.d.ts +1 -0
- package/document-management/models/document-category.model.js +7 -1
- package/document-management/models/document-property.model.d.ts +1 -0
- package/document-management/models/document-property.model.js +7 -1
- package/document-management/models/document-type.model.d.ts +1 -0
- package/document-management/models/document-type.model.js +7 -1
- package/document-management/models/document-workflow.model.d.ts +1 -0
- package/document-management/models/document-workflow.model.js +6 -1
- package/document-management/server/api/document-management.api.d.ts +1 -0
- package/document-management/server/api/document-management.api.js +8 -7
- package/document-management/server/drizzle/{0000_glamorous_lorna_dane.sql → 0000_needy_steel_serpent.sql} +7 -0
- package/document-management/server/drizzle/meta/0000_snapshot.json +49 -1
- package/document-management/server/drizzle/meta/_journal.json +2 -2
- package/document-management/server/module.d.ts +1 -0
- package/document-management/server/module.js +1 -0
- package/document-management/server/services/document-category-type.service.d.ts +8 -3
- package/document-management/server/services/document-category-type.service.js +49 -6
- package/document-management/server/services/document-management.service.js +17 -15
- package/document-management/server/services/document-property.service.d.ts +3 -1
- package/document-management/server/services/document-property.service.js +23 -2
- package/document-management/server/services/document-validation.service.js +2 -1
- package/document-management/server/services/document-workflow.service.d.ts +3 -3
- package/document-management/server/services/document-workflow.service.js +34 -15
- package/document-management/server/services/document.service.d.ts +1 -1
- package/document-management/server/services/document.service.js +7 -2
- package/document-management/service-models/categories-and-types.view-model.d.ts +6 -0
- package/document-management/service-models/categories-and-types.view-model.js +18 -0
- package/document-management/service-models/document-management.view-model.d.ts +1 -0
- package/document-management/service-models/document-management.view-model.js +5 -0
- package/document-management/service-models/document.service-model.d.ts +7 -0
- package/document-management/service-models/document.service-model.js +7 -1
- package/document-management/service-models/enriched/enriched-document-category.view.d.ts +1 -0
- package/document-management/service-models/enriched/enriched-document-category.view.js +2 -0
- package/document-management/service-models/enriched/enriched-document-type.view.d.ts +1 -0
- package/document-management/service-models/enriched/enriched-document-type.view.js +2 -0
- package/document-management/tests/document-management-core.test.d.ts +1 -0
- package/document-management/tests/document-management-core.test.js +162 -0
- package/document-management/tests/document.service.test.d.ts +1 -0
- package/document-management/tests/document.service.test.js +139 -0
- package/document-management/tests/enum-helpers.test.d.ts +1 -0
- package/document-management/tests/enum-helpers.test.js +452 -0
- package/document-management/tests/helper.d.ts +24 -0
- package/document-management/tests/helper.js +39 -0
- package/package.json +5 -5
|
@@ -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 };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tstdl/base",
|
|
3
|
-
"version": "0.93.
|
|
3
|
+
"version": "0.93.97",
|
|
4
4
|
"author": "Patrick Hein",
|
|
5
5
|
"publishConfig": {
|
|
6
6
|
"access": "public"
|
|
@@ -146,7 +146,7 @@
|
|
|
146
146
|
"type-fest": "^5.4"
|
|
147
147
|
},
|
|
148
148
|
"peerDependencies": {
|
|
149
|
-
"@genkit-ai/google-genai": "^1.
|
|
149
|
+
"@genkit-ai/google-genai": "^1.28",
|
|
150
150
|
"@google-cloud/storage": "^7.18",
|
|
151
151
|
"@google/genai": "^1.38",
|
|
152
152
|
"@toon-format/toon": "^2.1.0",
|
|
@@ -157,13 +157,13 @@
|
|
|
157
157
|
"@zxcvbn-ts/language-en": "^3.0",
|
|
158
158
|
"drizzle-orm": "^0.45",
|
|
159
159
|
"file-type": "^21.3",
|
|
160
|
-
"genkit": "^1.
|
|
160
|
+
"genkit": "^1.28",
|
|
161
161
|
"handlebars": "^4.7",
|
|
162
162
|
"minio": "^8.0",
|
|
163
163
|
"mjml": "^4.18",
|
|
164
164
|
"nodemailer": "^7.0",
|
|
165
165
|
"pg": "^8.17",
|
|
166
|
-
"playwright": "^1.
|
|
166
|
+
"playwright": "^1.58",
|
|
167
167
|
"preact": "^10.28",
|
|
168
168
|
"preact-render-to-string": "^6.6",
|
|
169
169
|
"sharp": "^0.34",
|
|
@@ -189,7 +189,7 @@
|
|
|
189
189
|
"concurrently": "9.2",
|
|
190
190
|
"drizzle-kit": "0.31",
|
|
191
191
|
"eslint": "9.39",
|
|
192
|
-
"globals": "17.
|
|
192
|
+
"globals": "17.1",
|
|
193
193
|
"tsc-alias": "1.8",
|
|
194
194
|
"typedoc-github-wiki-theme": "2.1",
|
|
195
195
|
"typedoc-plugin-markdown": "4.9",
|