@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.
Files changed (46) hide show
  1. package/document-management/api/document-management.api.d.ts +19 -1
  2. package/document-management/api/document-management.api.js +8 -4
  3. package/document-management/models/document-category.model.d.ts +1 -0
  4. package/document-management/models/document-category.model.js +7 -1
  5. package/document-management/models/document-property.model.d.ts +1 -0
  6. package/document-management/models/document-property.model.js +7 -1
  7. package/document-management/models/document-type.model.d.ts +1 -0
  8. package/document-management/models/document-type.model.js +7 -1
  9. package/document-management/models/document-workflow.model.d.ts +1 -0
  10. package/document-management/models/document-workflow.model.js +6 -1
  11. package/document-management/server/api/document-management.api.d.ts +1 -0
  12. package/document-management/server/api/document-management.api.js +8 -7
  13. package/document-management/server/drizzle/{0000_glamorous_lorna_dane.sql → 0000_needy_steel_serpent.sql} +7 -0
  14. package/document-management/server/drizzle/meta/0000_snapshot.json +49 -1
  15. package/document-management/server/drizzle/meta/_journal.json +2 -2
  16. package/document-management/server/module.d.ts +1 -0
  17. package/document-management/server/module.js +1 -0
  18. package/document-management/server/services/document-category-type.service.d.ts +8 -3
  19. package/document-management/server/services/document-category-type.service.js +49 -6
  20. package/document-management/server/services/document-management.service.js +17 -15
  21. package/document-management/server/services/document-property.service.d.ts +3 -1
  22. package/document-management/server/services/document-property.service.js +23 -2
  23. package/document-management/server/services/document-validation.service.js +2 -1
  24. package/document-management/server/services/document-workflow.service.d.ts +3 -3
  25. package/document-management/server/services/document-workflow.service.js +34 -15
  26. package/document-management/server/services/document.service.d.ts +1 -1
  27. package/document-management/server/services/document.service.js +7 -2
  28. package/document-management/service-models/categories-and-types.view-model.d.ts +6 -0
  29. package/document-management/service-models/categories-and-types.view-model.js +18 -0
  30. package/document-management/service-models/document-management.view-model.d.ts +1 -0
  31. package/document-management/service-models/document-management.view-model.js +5 -0
  32. package/document-management/service-models/document.service-model.d.ts +7 -0
  33. package/document-management/service-models/document.service-model.js +7 -1
  34. package/document-management/service-models/enriched/enriched-document-category.view.d.ts +1 -0
  35. package/document-management/service-models/enriched/enriched-document-category.view.js +2 -0
  36. package/document-management/service-models/enriched/enriched-document-type.view.d.ts +1 -0
  37. package/document-management/service-models/enriched/enriched-document-type.view.js +2 -0
  38. package/document-management/tests/document-management-core.test.d.ts +1 -0
  39. package/document-management/tests/document-management-core.test.js +162 -0
  40. package/document-management/tests/document.service.test.d.ts +1 -0
  41. package/document-management/tests/document.service.test.js +139 -0
  42. package/document-management/tests/enum-helpers.test.d.ts +1 -0
  43. package/document-management/tests/enum-helpers.test.js +452 -0
  44. package/document-management/tests/helper.d.ts +24 -0
  45. package/document-management/tests/helper.js +39 -0
  46. package/package.json +5 -5
@@ -0,0 +1,162 @@
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 { runInInjectionContext } from '../../injector/index.js';
5
+ import { ObjectStorage } from '../../object-storage/index.js';
6
+ import { TaskQueue } from '../../task-queue/index.js';
7
+ import { setupIntegrationTest, truncateTables } from '../../unit-test/index.js';
8
+ import { configureDocumentManagement } from '../server/configure.js';
9
+ import { migrateDocumentManagementSchema } from '../server/module.js';
10
+ import { DocumentCategoryTypeService } from '../server/services/document-category-type.service.js';
11
+ import { DocumentCollectionService } from '../server/services/document-collection.service.js';
12
+ import { DocumentManagementService } from '../server/services/document-management.service.js';
13
+ import { DocumentPropertyService } from '../server/services/document-property.service.js';
14
+ import { DocumentRequestService } from '../server/services/document-request.service.js';
15
+ import { DocumentWorkflowService } from '../server/services/document-workflow.service.js';
16
+ import { DocumentService } from '../server/services/document.service.js';
17
+ import { TestDocumentManagementAncillaryService, TestDocumentManagementAuthorizationService } from './helper.js';
18
+ describe('Document Management Core', () => {
19
+ let injector;
20
+ let database;
21
+ let documentService;
22
+ let collectionService;
23
+ let requestService;
24
+ let managementService;
25
+ let workflowService;
26
+ let categoryTypeService;
27
+ let propertyService;
28
+ const schema = 'document_management';
29
+ const tenantId = crypto.randomUUID();
30
+ beforeAll(async () => {
31
+ ({ injector, database } = await setupIntegrationTest({
32
+ modules: { taskQueue: false, messageBus: true }, // Disabled TaskQueue to avoid background noise
33
+ orm: { schema },
34
+ }));
35
+ injector.register(AiModuleOptions, { useValue: {} });
36
+ injector.register(AiService, { useValue: { processFile: vi.fn() } });
37
+ injector.register(ObjectStorage, {
38
+ useValue: {
39
+ uploadObject: vi.fn(),
40
+ getDownloadUrl: vi.fn(),
41
+ getContent: vi.fn(),
42
+ getContentStream: vi.fn(),
43
+ getObject: vi.fn(),
44
+ exists: vi.fn(),
45
+ },
46
+ });
47
+ // Mock TaskQueue
48
+ injector.register(TaskQueue, {
49
+ useValue: {
50
+ enqueue: vi.fn(),
51
+ enqueueMany: vi.fn(),
52
+ process: vi.fn(),
53
+ },
54
+ });
55
+ configureDocumentManagement({
56
+ ancillaryService: TestDocumentManagementAncillaryService,
57
+ authorizationService: TestDocumentManagementAuthorizationService,
58
+ fileObjectStorageModule: 'documents',
59
+ fileUploadObjectStorageModule: 'document-uploads',
60
+ filePreviewObjectStorageModule: 'document-previews',
61
+ });
62
+ await runInInjectionContext(injector, migrateDocumentManagementSchema);
63
+ documentService = await injector.resolveAsync(DocumentService);
64
+ collectionService = await injector.resolveAsync(DocumentCollectionService);
65
+ requestService = await injector.resolveAsync(DocumentRequestService);
66
+ managementService = await injector.resolveAsync(DocumentManagementService);
67
+ workflowService = await injector.resolveAsync(DocumentWorkflowService);
68
+ categoryTypeService = await injector.resolveAsync(DocumentCategoryTypeService);
69
+ propertyService = await injector.resolveAsync(DocumentPropertyService);
70
+ });
71
+ afterAll(async () => {
72
+ await injector?.dispose();
73
+ });
74
+ beforeEach(async () => {
75
+ await truncateTables(database, schema, ['request_collection_assignment', 'collection_assignment', 'workflow', 'request', 'document', 'collection', 'type', 'category']);
76
+ });
77
+ // --- Collection Management ---
78
+ test('Create and load collection', async () => {
79
+ await runInInjectionContext(injector, async () => {
80
+ const collection = await collectionService.createCollection(tenantId, null);
81
+ expect(collection.id).toBeDefined();
82
+ const loaded = await collectionService.repository.loadByQuery({ tenantId, id: collection.id });
83
+ expect(loaded.id).toBe(collection.id);
84
+ });
85
+ });
86
+ test('Load collection view resolves metadata', async () => {
87
+ await runInInjectionContext(injector, async () => {
88
+ const collection = await collectionService.createCollection(tenantId, null);
89
+ const data = await managementService.loadData(tenantId, [collection.id]);
90
+ const view = data.collections.find((c) => c.id === collection.id);
91
+ expect(view).toBeDefined();
92
+ // Metadata comes from TestDocumentManagementAncillaryService in helper.ts
93
+ expect(view?.name).toBe(`Collection ${collection.id}`);
94
+ expect(view?.group).toBe('Test');
95
+ });
96
+ });
97
+ // --- Document Assignments ---
98
+ test('Assign document to collection', async () => {
99
+ await runInInjectionContext(injector, async () => {
100
+ const collection = await collectionService.createCollection(tenantId, null);
101
+ const doc = await documentService.create(tenantId, {
102
+ originalFileName: 'test.pdf',
103
+ assignment: { collections: [] },
104
+ skipAi: true,
105
+ }, new Uint8Array([]));
106
+ await documentService.update(tenantId, doc.id, { collections: { assign: [collection.id] } });
107
+ const assignedCollections = await managementService.getRelevantDocumentCollectionIds(tenantId, doc.id);
108
+ expect(assignedCollections).toContain(collection.id);
109
+ });
110
+ });
111
+ test('Unassign document from collection', async () => {
112
+ await runInInjectionContext(injector, async () => {
113
+ const collection = await collectionService.createCollection(tenantId, null);
114
+ const doc = await documentService.create(tenantId, {
115
+ originalFileName: 'test.pdf',
116
+ assignment: { collections: [collection.id] },
117
+ skipAi: true,
118
+ }, new Uint8Array([]));
119
+ // Archive assignment to unassign
120
+ await documentService.update(tenantId, doc.id, { collections: { archive: [collection.id] } });
121
+ // getRelevantDocumentCollectionIds includes all assignments (even archived ones maybe?).
122
+ // Let's check the loadData response for active assignments.
123
+ const data = await managementService.loadData(tenantId, [collection.id]);
124
+ const docView = data.documents.find((d) => d.id === doc.id);
125
+ // The assignment should be filtered out or marked archived in the view?
126
+ // loadData: documentCollectionAssignments.filter((collectionDocument) => collectionDocument.documentId == document.id)
127
+ // The query in loadData loads ALL assignments for the collection.
128
+ // But typically "active" documents are what we care about.
129
+ // If archived, it might still show up. Let's check the 'archiveTimestamp' in the assignment view.
130
+ const assignment = docView?.assignment.collections.find((a) => a.collectionId === collection.id);
131
+ expect(assignment?.archiveTimestamp).not.toBeNull();
132
+ });
133
+ });
134
+ // --- Requests ---
135
+ test('Create and load document request', async () => {
136
+ await runInInjectionContext(injector, async () => {
137
+ // Create a dummy type first
138
+ const category = await categoryTypeService.createCategory({ tenantId, label: 'ReqCat', parentId: null });
139
+ const type = await categoryTypeService.createType({ tenantId, label: 'ReqType', categoryId: category.id });
140
+ const collection = await collectionService.createCollection(tenantId, null);
141
+ const request = await requestService.createRequest(tenantId, type.id, [collection.id], 'New Request');
142
+ expect(request.comment).toBe('New Request');
143
+ // Verify via loadData (indirectly verifies persistence)
144
+ const data = await managementService.loadData(tenantId, [collection.id]);
145
+ const loaded = data.requests.find((r) => r.id === request.id);
146
+ expect(loaded).toBeDefined();
147
+ expect(loaded?.comment).toBe('New Request');
148
+ });
149
+ });
150
+ test('Request with collection assignment', async () => {
151
+ await runInInjectionContext(injector, async () => {
152
+ const category = await categoryTypeService.createCategory({ tenantId, label: 'Cat2', parentId: null });
153
+ const type = await categoryTypeService.createType({ tenantId, label: 'Type2', categoryId: category.id });
154
+ const collection = await collectionService.createCollection(tenantId, null);
155
+ const request = await requestService.createRequest(tenantId, type.id, [collection.id], 'Assignment Request');
156
+ const data = await managementService.loadData(tenantId, [collection.id]);
157
+ const reqView = data.requests.find((r) => r.id === request.id);
158
+ expect(reqView).toBeDefined();
159
+ expect(reqView?.collectionIds).toContain(collection.id);
160
+ });
161
+ });
162
+ });
@@ -0,0 +1,139 @@
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 { runInInjectionContext } from '../../injector/index.js';
5
+ import { ObjectStorage } from '../../object-storage/index.js';
6
+ import { clearTenantData, setupIntegrationTest } from '../../unit-test/index.js';
7
+ import { DocumentWorkflowState } from '../models/document-workflow.model.js';
8
+ import { DocumentApproval } from '../models/document.model.js';
9
+ import { configureDocumentManagement } from '../server/configure.js';
10
+ import { DocumentManagementConfiguration, migrateDocumentManagementSchema } from '../server/module.js';
11
+ import { DocumentCollectionService } from '../server/services/document-collection.service.js';
12
+ import { DocumentWorkflowService } from '../server/services/document-workflow.service.js';
13
+ import { DocumentService } from '../server/services/document.service.js';
14
+ import { TestDocumentManagementAncillaryService, TestDocumentManagementAuthorizationService } from './helper.js';
15
+ describe('DocumentService', () => {
16
+ let injector;
17
+ let database;
18
+ let documentService;
19
+ let workflowService;
20
+ let collectionService;
21
+ const schema = 'document_management';
22
+ const tenantId = crypto.randomUUID();
23
+ beforeAll(async () => {
24
+ ({ injector, database } = await setupIntegrationTest({
25
+ modules: { taskQueue: true },
26
+ orm: { schema },
27
+ }));
28
+ const mockObjectStorage = {
29
+ uploadObject: vi.fn(),
30
+ getDownloadUrl: vi.fn(),
31
+ getContent: vi.fn(),
32
+ getContentStream: vi.fn(),
33
+ getObject: vi.fn(),
34
+ exists: vi.fn(),
35
+ };
36
+ const mockAiService = {
37
+ processFile: vi.fn(),
38
+ };
39
+ injector.register(ObjectStorage, { useFactory: () => mockObjectStorage });
40
+ injector.register(AiModuleOptions, { useValue: {} });
41
+ injector.register(AiService, { useValue: mockAiService });
42
+ configureDocumentManagement({
43
+ ancillaryService: TestDocumentManagementAncillaryService,
44
+ authorizationService: TestDocumentManagementAuthorizationService,
45
+ fileObjectStorageModule: 'documents',
46
+ fileUploadObjectStorageModule: 'document-uploads',
47
+ filePreviewObjectStorageModule: 'document-previews',
48
+ });
49
+ await runInInjectionContext(injector, migrateDocumentManagementSchema);
50
+ documentService = await injector.resolveAsync(DocumentService);
51
+ workflowService = await injector.resolveAsync(DocumentWorkflowService);
52
+ collectionService = await injector.resolveAsync(DocumentCollectionService);
53
+ });
54
+ afterAll(async () => {
55
+ await injector?.dispose();
56
+ });
57
+ beforeEach(async () => {
58
+ await clearTenantData(database, schema, ['collection_assignment', 'workflow', 'document', 'collection'], tenantId);
59
+ });
60
+ test('create with skipWorkflow: true should not initiate workflow and set approval', async () => {
61
+ await runInInjectionContext(injector, async () => {
62
+ const collection = await collectionService.createCollection(tenantId, null);
63
+ const doc = await documentService.create(tenantId, {
64
+ originalFileName: 'test.pdf',
65
+ approval: DocumentApproval.Approved,
66
+ assignment: { collections: [collection.id] },
67
+ skipWorkflow: true,
68
+ }, new Uint8Array([1, 2, 3]));
69
+ expect(doc.approval).toBe(DocumentApproval.Approved);
70
+ const workflows = await workflowService.loadWorkflows(tenantId, doc.id);
71
+ expect(workflows).toHaveLength(0);
72
+ });
73
+ });
74
+ test('create with skipAi: true should initiate workflow with skipAi flag', async () => {
75
+ await runInInjectionContext(injector, async () => {
76
+ const collection = await collectionService.createCollection(tenantId, null);
77
+ const doc = await documentService.create(tenantId, {
78
+ originalFileName: 'test.pdf',
79
+ assignment: { collections: [collection.id] },
80
+ skipAi: true,
81
+ }, new Uint8Array([1, 2, 3]));
82
+ const workflows = await workflowService.loadWorkflows(tenantId, doc.id);
83
+ expect(workflows).toHaveLength(1);
84
+ expect(workflows[0].skipAi).toBe(true);
85
+ });
86
+ });
87
+ test('proceedWorkflow with skipAi: true should allow manual progression from Pending', async () => {
88
+ await runInInjectionContext(injector, async () => {
89
+ const collection = await collectionService.createCollection(tenantId, null);
90
+ const doc = await documentService.create(tenantId, {
91
+ originalFileName: 'test.pdf',
92
+ assignment: { collections: [collection.id] },
93
+ skipAi: true,
94
+ }, new Uint8Array([1, 2, 3]));
95
+ let workflows = await workflowService.loadWorkflows(tenantId, doc.id);
96
+ expect(workflows).toHaveLength(1);
97
+ expect(workflows[0].step).toBe('classification');
98
+ expect(workflows[0].state).toBe('pending');
99
+ await workflowService.proceedWorkflow(tenantId, doc.id, crypto.randomUUID(), DocumentWorkflowState.Completed);
100
+ workflows = await workflowService.loadWorkflows(tenantId, doc.id);
101
+ expect(workflows).toHaveLength(2);
102
+ expect(workflows.find((w) => w.step == 'classification')?.state).toBe('completed');
103
+ expect(workflows.find((w) => w.step == 'extraction')?.state).toBe('pending');
104
+ expect(workflows.find((w) => w.step == 'extraction')?.skipAi).toBe(true);
105
+ });
106
+ });
107
+ test('proceedWorkflow with skipAi: true should allow progression to Review', async () => {
108
+ await runInInjectionContext(injector, async () => {
109
+ const collection = await collectionService.createCollection(tenantId, null);
110
+ const doc = await documentService.create(tenantId, {
111
+ originalFileName: 'test.pdf',
112
+ assignment: { collections: [collection.id] },
113
+ skipAi: true,
114
+ }, new Uint8Array([1, 2, 3]));
115
+ await workflowService.proceedWorkflow(tenantId, doc.id, crypto.randomUUID(), 'review');
116
+ const workflows = await workflowService.loadWorkflows(tenantId, doc.id);
117
+ expect(workflows).toHaveLength(1);
118
+ expect(workflows[0].step).toBe('classification');
119
+ expect(workflows[0].state).toBe('review');
120
+ });
121
+ });
122
+ test('global skipAi: true should override per-document setting', async () => {
123
+ await runInInjectionContext(injector, async () => {
124
+ // Mock configuration override
125
+ const config = injector.resolve(DocumentManagementConfiguration);
126
+ config.skipAi = true;
127
+ const collection = await collectionService.createCollection(tenantId, null);
128
+ const doc = await documentService.create(tenantId, {
129
+ originalFileName: 'test.pdf',
130
+ assignment: { collections: [collection.id] },
131
+ skipAi: false, // Per-document false, but global true
132
+ }, new Uint8Array([1, 2, 3]));
133
+ const workflows = await workflowService.loadWorkflows(tenantId, doc.id);
134
+ expect(workflows).toHaveLength(1);
135
+ expect(workflows[0].skipAi).toBe(true); // Global override
136
+ config.skipAi = false; // Reset for other tests
137
+ });
138
+ });
139
+ });
@@ -0,0 +1 @@
1
+ export {};