@tstdl/base 0.93.97 → 0.93.99
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/audit/auditor.d.ts +1 -1
- package/context/context.d.ts +1 -1
- package/context/context.js +4 -0
- package/cookie/cookie.js +5 -1
- package/css/css-variables.d.ts +38 -0
- package/css/css-variables.js +38 -0
- package/document-management/api/document-management.api.d.ts +17 -3
- package/document-management/api/document-management.api.js +8 -1
- package/document-management/models/document-category.model.js +0 -1
- package/document-management/models/document-property.model.js +0 -1
- package/document-management/server/api/document-management.api.d.ts +1 -0
- package/document-management/server/api/document-management.api.js +13 -1
- package/document-management/server/drizzle/{0000_needy_steel_serpent.sql → 0000_silly_chimera.sql} +0 -2
- package/document-management/server/drizzle/meta/0000_snapshot.json +1 -15
- package/document-management/server/drizzle/meta/_journal.json +2 -2
- package/document-management/server/services/document-statistics.service.d.ts +6 -0
- package/document-management/server/services/document-statistics.service.js +167 -0
- package/document-management/server/services/index.d.ts +1 -0
- package/document-management/server/services/index.js +1 -0
- package/document-management/service-models/document-statistics.view-model.d.ts +38 -0
- package/document-management/service-models/document-statistics.view-model.js +160 -0
- package/document-management/service-models/document.service-model.d.ts +1 -1
- package/document-management/service-models/index.d.ts +1 -0
- package/document-management/service-models/index.js +1 -0
- package/document-management/tests/document-management-core.test.js +2 -2
- package/document-management/tests/document-management.api.test.d.ts +1 -0
- package/document-management/tests/document-management.api.test.js +102 -0
- package/document-management/tests/document-statistics.service.test.d.ts +1 -0
- package/document-management/tests/document-statistics.service.test.js +495 -0
- package/document-management/tests/enum-helpers.test.js +3 -2
- package/enumeration/enumeration.d.ts +24 -0
- package/enumeration/enumeration.js +20 -0
- package/examples/document-management/main.js +1 -1
- package/intl/number-parser.d.ts +16 -9
- package/intl/number-parser.js +31 -19
- package/module/module.js +3 -0
- package/notification/api/notification.api.d.ts +78 -0
- package/notification/api/notification.api.js +81 -0
- package/notification/enums.d.ts +22 -0
- package/notification/enums.js +19 -0
- package/notification/index.d.ts +2 -0
- package/notification/index.js +2 -0
- package/notification/models/in-app-notification.model.d.ts +9 -0
- package/notification/models/in-app-notification.model.js +42 -0
- package/notification/models/index.d.ts +6 -0
- package/notification/models/index.js +6 -0
- package/notification/models/notification-category.model.d.ts +17 -0
- package/notification/models/notification-category.model.js +41 -0
- package/notification/models/notification-log.model.d.ts +13 -0
- package/notification/models/notification-log.model.js +59 -0
- package/notification/models/notification-preference.model.d.ts +9 -0
- package/notification/models/notification-preference.model.js +45 -0
- package/notification/models/notification-table.d.ts +3 -0
- package/notification/models/notification-table.js +4 -0
- package/notification/models/web-push-subscription.model.d.ts +8 -0
- package/notification/models/web-push-subscription.model.js +41 -0
- package/notification/server/api/notification.api-controller.d.ts +16 -0
- package/notification/server/api/notification.api-controller.js +51 -0
- package/notification/server/drizzle/0000_glorious_randall.sql +90 -0
- package/notification/server/drizzle/meta/0000_snapshot.json +652 -0
- package/notification/server/drizzle/meta/_journal.json +13 -0
- package/notification/server/drizzle.config.d.ts +2 -0
- package/notification/server/drizzle.config.js +11 -0
- package/notification/server/index.d.ts +4 -0
- package/notification/server/index.js +4 -0
- package/notification/server/module.d.ts +12 -0
- package/notification/server/module.js +21 -0
- package/notification/server/providers/channel-provider.d.ts +4 -0
- package/notification/server/providers/channel-provider.js +1 -0
- package/notification/server/providers/email-channel-provider.d.ts +6 -0
- package/notification/server/providers/email-channel-provider.js +34 -0
- package/notification/server/providers/in-app-channel-provider.d.ts +7 -0
- package/notification/server/providers/in-app-channel-provider.js +31 -0
- package/notification/server/providers/index.d.ts +4 -0
- package/notification/server/providers/index.js +4 -0
- package/notification/server/providers/web-push-channel-provider.d.ts +6 -0
- package/notification/server/providers/web-push-channel-provider.js +26 -0
- package/notification/server/schemas.d.ts +25 -0
- package/notification/server/schemas.js +12 -0
- package/notification/server/services/index.d.ts +8 -0
- package/notification/server/services/index.js +8 -0
- package/notification/server/services/notification-category.service.d.ts +11 -0
- package/notification/server/services/notification-category.service.js +41 -0
- package/notification/server/services/notification-delivery.task.d.ts +9 -0
- package/notification/server/services/notification-delivery.task.js +1 -0
- package/notification/server/services/notification-delivery.worker.d.ts +12 -0
- package/notification/server/services/notification-delivery.worker.js +108 -0
- package/notification/server/services/notification-sse.service.d.ts +13 -0
- package/notification/server/services/notification-sse.service.js +74 -0
- package/notification/server/services/notification-template.d.ts +12 -0
- package/notification/server/services/notification-template.js +1 -0
- package/notification/server/services/notification-template.service.d.ts +7 -0
- package/notification/server/services/notification-template.service.js +29 -0
- package/notification/server/services/notification.service.d.ts +17 -0
- package/notification/server/services/notification.service.js +80 -0
- package/notification/server/services/singleton.d.ts +3 -0
- package/notification/server/services/singleton.js +10 -0
- package/notification/tests/notification-category.service.test.d.ts +1 -0
- package/notification/tests/notification-category.service.test.js +36 -0
- package/notification/tests/notification-flow.test.d.ts +1 -0
- package/notification/tests/notification-flow.test.js +112 -0
- package/notification/tests/notification-sse.service.test.d.ts +1 -0
- package/notification/tests/notification-sse.service.test.js +20 -0
- package/notification/tests/test-notification.model.d.ts +4 -0
- package/notification/tests/test-notification.model.js +25 -0
- package/object-storage/google/google.object-storage-provider.d.ts +0 -1
- package/object-storage/google/google.object-storage-provider.js +0 -1
- package/object-storage/index.d.ts +0 -1
- package/object-storage/index.js +0 -1
- package/object-storage/s3/s3.object-storage-provider.d.ts +0 -1
- package/object-storage/s3/s3.object-storage-provider.js +0 -1
- package/orm/server/transactional.d.ts +3 -2
- package/orm/server/transactional.js +3 -2
- package/package.json +4 -2
- package/pool/pool.d.ts +1 -1
- package/promise/cancelable-promise.d.ts +1 -0
- package/promise/cancelable-promise.js +1 -0
- package/promise/index.d.ts +1 -0
- package/promise/index.js +1 -0
- package/random/number-generator/index.d.ts +1 -0
- package/random/number-generator/index.js +1 -0
- package/sse/data-stream.js +16 -3
- package/task-queue/task-queue.d.ts +7 -2
- package/task-queue/task-queue.js +4 -1
- package/unit-test/integration-setup.d.ts +7 -6
- package/unit-test/integration-setup.js +7 -2
|
@@ -0,0 +1,495 @@
|
|
|
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 { getRepository } from '../../orm/server/index.js';
|
|
7
|
+
import { clearTenantData, setupIntegrationTest } from '../../unit-test/index.js';
|
|
8
|
+
import { DocumentValidationDefinition } from '../models/document-validation-definition.model.js';
|
|
9
|
+
import { DocumentValidationExecution, DocumentValidationExecutionState, DocumentValidationResultStatus } from '../models/document-validation-execution.model.js';
|
|
10
|
+
import { DocumentWorkflow, DocumentWorkflowState, DocumentWorkflowStep } from '../models/document-workflow.model.js';
|
|
11
|
+
import { DocumentApproval } from '../models/document.model.js';
|
|
12
|
+
import { configureDocumentManagement } from '../server/configure.js';
|
|
13
|
+
import { migrateDocumentManagementSchema } from '../server/module.js';
|
|
14
|
+
import { DocumentCategoryTypeService } from '../server/services/document-category-type.service.js';
|
|
15
|
+
import { DocumentCollectionService } from '../server/services/document-collection.service.js';
|
|
16
|
+
import { DocumentStatisticsService } from '../server/services/document-statistics.service.js';
|
|
17
|
+
import { DocumentTagService } from '../server/services/document-tag.service.js';
|
|
18
|
+
import { DocumentService } from '../server/services/document.service.js';
|
|
19
|
+
import { TestDocumentManagementAncillaryService, TestDocumentManagementAuthorizationService } from './helper.js';
|
|
20
|
+
describe('DocumentStatisticsService', () => {
|
|
21
|
+
let injector;
|
|
22
|
+
let database;
|
|
23
|
+
let documentService;
|
|
24
|
+
let statsService;
|
|
25
|
+
let collectionService;
|
|
26
|
+
let tagService;
|
|
27
|
+
let categoryTypeService;
|
|
28
|
+
const schema = 'document_management';
|
|
29
|
+
const tenantId = crypto.randomUUID();
|
|
30
|
+
beforeAll(async () => {
|
|
31
|
+
({ injector, database } = await setupIntegrationTest({
|
|
32
|
+
modules: { taskQueue: true },
|
|
33
|
+
orm: { schema },
|
|
34
|
+
}));
|
|
35
|
+
const mockObjectStorage = {
|
|
36
|
+
uploadObject: vi.fn(),
|
|
37
|
+
getDownloadUrl: vi.fn(),
|
|
38
|
+
getContent: vi.fn(),
|
|
39
|
+
getContentStream: vi.fn(),
|
|
40
|
+
getObject: vi.fn(),
|
|
41
|
+
exists: vi.fn(),
|
|
42
|
+
};
|
|
43
|
+
const mockAiService = {
|
|
44
|
+
processFile: vi.fn(),
|
|
45
|
+
};
|
|
46
|
+
injector.register(ObjectStorage, { useFactory: () => mockObjectStorage });
|
|
47
|
+
injector.register(AiModuleOptions, { useValue: {} });
|
|
48
|
+
injector.register(AiService, { useValue: mockAiService });
|
|
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
|
+
documentService = await injector.resolveAsync(DocumentService);
|
|
58
|
+
statsService = await injector.resolveAsync(DocumentStatisticsService);
|
|
59
|
+
collectionService = await injector.resolveAsync(DocumentCollectionService);
|
|
60
|
+
tagService = await injector.resolveAsync(DocumentTagService);
|
|
61
|
+
categoryTypeService = await injector.resolveAsync(DocumentCategoryTypeService);
|
|
62
|
+
});
|
|
63
|
+
afterAll(async () => {
|
|
64
|
+
await injector?.dispose();
|
|
65
|
+
});
|
|
66
|
+
beforeEach(async () => {
|
|
67
|
+
await clearTenantData(database, schema, ['validation_execution', 'validation_definition', 'tag_assignment', 'collection_assignment', 'workflow', 'document', 'collection', 'tag', 'type', 'category'], tenantId);
|
|
68
|
+
});
|
|
69
|
+
test('getStatistics should return total count', async () => {
|
|
70
|
+
await runInInjectionContext(injector, async () => {
|
|
71
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
72
|
+
await documentService.create(tenantId, {
|
|
73
|
+
title: 'Doc 1',
|
|
74
|
+
assignment: { collections: [collection.id] },
|
|
75
|
+
skipWorkflow: true,
|
|
76
|
+
}, new Uint8Array([1]));
|
|
77
|
+
await documentService.create(tenantId, {
|
|
78
|
+
title: 'Doc 2',
|
|
79
|
+
assignment: { collections: [collection.id] },
|
|
80
|
+
skipWorkflow: true,
|
|
81
|
+
}, new Uint8Array([1, 2]));
|
|
82
|
+
const stats = await statsService.getStatistics(tenantId, { includeTotalCount: true, collectionIds: [collection.id] });
|
|
83
|
+
expect(stats.totalCount).toBe(2);
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
test('getStatistics should return approval breakdown', async () => {
|
|
87
|
+
await runInInjectionContext(injector, async () => {
|
|
88
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
89
|
+
await documentService.create(tenantId, {
|
|
90
|
+
approval: DocumentApproval.Approved,
|
|
91
|
+
assignment: { collections: [collection.id] },
|
|
92
|
+
skipWorkflow: true,
|
|
93
|
+
}, new Uint8Array([1]));
|
|
94
|
+
await documentService.create(tenantId, {
|
|
95
|
+
approval: DocumentApproval.Pending,
|
|
96
|
+
assignment: { collections: [collection.id] },
|
|
97
|
+
skipWorkflow: true,
|
|
98
|
+
}, new Uint8Array([1]));
|
|
99
|
+
await documentService.create(tenantId, {
|
|
100
|
+
approval: DocumentApproval.Rejected,
|
|
101
|
+
assignment: { collections: [collection.id] },
|
|
102
|
+
skipWorkflow: true,
|
|
103
|
+
}, new Uint8Array([1]));
|
|
104
|
+
const stats = await statsService.getStatistics(tenantId, { includeApprovalBreakdown: true, collectionIds: [collection.id] });
|
|
105
|
+
expect(stats.approvalBreakdown?.approved).toBe(1);
|
|
106
|
+
expect(stats.approvalBreakdown?.pending).toBe(1);
|
|
107
|
+
expect(stats.approvalBreakdown?.rejected).toBe(1);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
test('getStatistics should filter by collection', async () => {
|
|
111
|
+
await runInInjectionContext(injector, async () => {
|
|
112
|
+
const collection1 = await collectionService.createCollection(tenantId, null);
|
|
113
|
+
const collection2 = await collectionService.createCollection(tenantId, null);
|
|
114
|
+
await documentService.create(tenantId, {
|
|
115
|
+
assignment: { collections: [collection1.id] },
|
|
116
|
+
skipWorkflow: true,
|
|
117
|
+
}, new Uint8Array([1]));
|
|
118
|
+
await documentService.create(tenantId, {
|
|
119
|
+
assignment: { collections: [collection2.id] },
|
|
120
|
+
skipWorkflow: true,
|
|
121
|
+
}, new Uint8Array([1]));
|
|
122
|
+
const stats = await statsService.getStatistics(tenantId, { includeTotalCount: true, collectionIds: [collection1.id] });
|
|
123
|
+
expect(stats.totalCount).toBe(1);
|
|
124
|
+
});
|
|
125
|
+
});
|
|
126
|
+
test('getStatistics should return storage usage and total pages', async () => {
|
|
127
|
+
await runInInjectionContext(injector, async () => {
|
|
128
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
129
|
+
const doc = await documentService.create(tenantId, {
|
|
130
|
+
assignment: { collections: [collection.id] },
|
|
131
|
+
skipWorkflow: true,
|
|
132
|
+
}, new Uint8Array([1, 2, 3])); // Size 3
|
|
133
|
+
// Manually set pages since our mock doesn't handle PDF page count
|
|
134
|
+
await documentService.repository.update(doc.id, { pages: 5 });
|
|
135
|
+
const stats = await statsService.getStatistics(tenantId, { includeStorageUsage: true, includeTotalPages: true, collectionIds: [collection.id] });
|
|
136
|
+
expect(stats.storageUsage).toBe(3);
|
|
137
|
+
expect(stats.totalPages).toBe(5);
|
|
138
|
+
});
|
|
139
|
+
});
|
|
140
|
+
test('getStatistics should return data quality metrics', async () => {
|
|
141
|
+
await runInInjectionContext(injector, async () => {
|
|
142
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
143
|
+
// Missing nothing (except type, which is checked separately)
|
|
144
|
+
await documentService.create(tenantId, {
|
|
145
|
+
assignment: { collections: [collection.id] },
|
|
146
|
+
skipWorkflow: true,
|
|
147
|
+
title: 'Title',
|
|
148
|
+
date: 20230101,
|
|
149
|
+
}, new Uint8Array([1]));
|
|
150
|
+
// Missing title and date
|
|
151
|
+
await documentService.create(tenantId, {
|
|
152
|
+
assignment: { collections: [collection.id] },
|
|
153
|
+
skipWorkflow: true,
|
|
154
|
+
}, new Uint8Array([1]));
|
|
155
|
+
const stats = await statsService.getStatistics(tenantId, { includeDataQuality: true, collectionIds: [collection.id] });
|
|
156
|
+
expect(stats.dataQuality?.missingTitle).toBe(1);
|
|
157
|
+
expect(stats.dataQuality?.missingDate).toBe(1);
|
|
158
|
+
expect(stats.dataQuality?.missingType).toBe(2);
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
test('getStatistics should return created last 30 days', async () => {
|
|
162
|
+
await runInInjectionContext(injector, async () => {
|
|
163
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
164
|
+
await documentService.create(tenantId, {
|
|
165
|
+
assignment: { collections: [collection.id] },
|
|
166
|
+
skipWorkflow: true,
|
|
167
|
+
}, new Uint8Array([1]));
|
|
168
|
+
const stats = await statsService.getStatistics(tenantId, { includeCreatedLast30Days: true, collectionIds: [collection.id] });
|
|
169
|
+
expect(stats.createdLast30Days).toBe(1);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
test('getStatistics should return tag usage', async () => {
|
|
173
|
+
await runInInjectionContext(injector, async () => {
|
|
174
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
175
|
+
await documentService.create(tenantId, {
|
|
176
|
+
assignment: { collections: [collection.id] },
|
|
177
|
+
skipWorkflow: true,
|
|
178
|
+
tags: ['Tag1'],
|
|
179
|
+
}, new Uint8Array([1]));
|
|
180
|
+
const [tag] = await tagService.loadOrCreate(tenantId, ['Tag1']);
|
|
181
|
+
const stats = await statsService.getStatistics(tenantId, { includeTagUsage: true, collectionIds: [collection.id] });
|
|
182
|
+
expect(stats.tagUsage?.[tag.id]).toBe(1);
|
|
183
|
+
});
|
|
184
|
+
});
|
|
185
|
+
test('getStatistics should return mime type distribution', async () => {
|
|
186
|
+
await runInInjectionContext(injector, async () => {
|
|
187
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
188
|
+
await documentService.create(tenantId, {
|
|
189
|
+
assignment: { collections: [collection.id] },
|
|
190
|
+
skipWorkflow: true,
|
|
191
|
+
}, new Uint8Array([1]));
|
|
192
|
+
const stats = await statsService.getStatistics(tenantId, { includeMimeTypeDistribution: true, collectionIds: [collection.id] });
|
|
193
|
+
expect(Object.keys(stats.mimeTypeDistribution ?? {}).length).toBeGreaterThan(0);
|
|
194
|
+
});
|
|
195
|
+
});
|
|
196
|
+
test('getStatistics should return type breakdown and filter by category', async () => {
|
|
197
|
+
await runInInjectionContext(injector, async () => {
|
|
198
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
199
|
+
const categoryLabel = `Category ${crypto.randomUUID()}`;
|
|
200
|
+
const category = await categoryTypeService.createCategory({ tenantId, label: categoryLabel, parentId: null });
|
|
201
|
+
const type = await categoryTypeService.createType({ tenantId, label: 'Type 1', categoryId: category.id });
|
|
202
|
+
await documentService.create(tenantId, {
|
|
203
|
+
assignment: { collections: [collection.id] },
|
|
204
|
+
skipWorkflow: true,
|
|
205
|
+
typeId: type.id,
|
|
206
|
+
}, new Uint8Array([1]));
|
|
207
|
+
const stats = await statsService.getStatistics(tenantId, { includeTypeBreakdown: true, collectionIds: [collection.id], categoryIds: [category.id] });
|
|
208
|
+
expect(stats.typeBreakdown?.[type.id]).toBe(1);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
test('getStatistics should return validation failures', async () => {
|
|
212
|
+
await runInInjectionContext(injector, async () => {
|
|
213
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
214
|
+
const doc = await documentService.create(tenantId, {
|
|
215
|
+
assignment: { collections: [collection.id] },
|
|
216
|
+
skipWorkflow: true,
|
|
217
|
+
}, new Uint8Array([1]));
|
|
218
|
+
const workflowRepo = await injector.resolveAsync(getRepository(DocumentWorkflow));
|
|
219
|
+
const validationRepo = await injector.resolveAsync(getRepository(DocumentValidationExecution));
|
|
220
|
+
const definitionRepo = await injector.resolveAsync(getRepository(DocumentValidationDefinition));
|
|
221
|
+
const definition = await definitionRepo.insert({
|
|
222
|
+
tenantId,
|
|
223
|
+
identifier: 'test-val',
|
|
224
|
+
label: 'Test Validation',
|
|
225
|
+
description: null,
|
|
226
|
+
configuration: {},
|
|
227
|
+
});
|
|
228
|
+
const workflow = await workflowRepo.insert({
|
|
229
|
+
tenantId,
|
|
230
|
+
documentId: doc.id,
|
|
231
|
+
step: DocumentWorkflowStep.Validation,
|
|
232
|
+
state: DocumentWorkflowState.Pending,
|
|
233
|
+
skipAi: true,
|
|
234
|
+
completeTimestamp: null,
|
|
235
|
+
failReason: null,
|
|
236
|
+
completeUserId: null,
|
|
237
|
+
});
|
|
238
|
+
await validationRepo.insert({
|
|
239
|
+
tenantId,
|
|
240
|
+
workflowId: workflow.id,
|
|
241
|
+
definitionId: definition.id,
|
|
242
|
+
state: DocumentValidationExecutionState.Completed,
|
|
243
|
+
resultStatus: DocumentValidationResultStatus.Failed,
|
|
244
|
+
resultMessage: null,
|
|
245
|
+
startedAt: null,
|
|
246
|
+
completedAt: null,
|
|
247
|
+
});
|
|
248
|
+
const stats = await statsService.getStatistics(tenantId, { includeValidationFailures: true, collectionIds: [collection.id] });
|
|
249
|
+
expect(stats.validationFailures).toBe(1);
|
|
250
|
+
});
|
|
251
|
+
});
|
|
252
|
+
test('getStatistics should return nothing when no options selected', async () => {
|
|
253
|
+
await runInInjectionContext(injector, async () => {
|
|
254
|
+
const stats = await statsService.getStatistics(tenantId, { collectionIds: [] });
|
|
255
|
+
expect(Object.keys(stats).length).toBe(0);
|
|
256
|
+
});
|
|
257
|
+
});
|
|
258
|
+
test('getStatistics should support combined filters', async () => {
|
|
259
|
+
await runInInjectionContext(injector, async () => {
|
|
260
|
+
const collection1 = await collectionService.createCollection(tenantId, null);
|
|
261
|
+
const collection2 = await collectionService.createCollection(tenantId, null);
|
|
262
|
+
const category1 = await categoryTypeService.createCategory({ tenantId, label: `Cat 1 ${crypto.randomUUID()}`, parentId: null });
|
|
263
|
+
const category2 = await categoryTypeService.createCategory({ tenantId, label: `Cat 2 ${crypto.randomUUID()}`, parentId: null });
|
|
264
|
+
const type1 = await categoryTypeService.createType({ tenantId, label: 'Type 1', categoryId: category1.id });
|
|
265
|
+
const type2 = await categoryTypeService.createType({ tenantId, label: 'Type 2', categoryId: category2.id });
|
|
266
|
+
// Doc 1: Coll 1, Type 1 (Cat 1)
|
|
267
|
+
await documentService.create(tenantId, {
|
|
268
|
+
assignment: { collections: [collection1.id] },
|
|
269
|
+
typeId: type1.id,
|
|
270
|
+
skipWorkflow: true,
|
|
271
|
+
}, new Uint8Array([1]));
|
|
272
|
+
// Doc 2: Coll 1, Type 2 (Cat 2)
|
|
273
|
+
await documentService.create(tenantId, {
|
|
274
|
+
assignment: { collections: [collection1.id] },
|
|
275
|
+
typeId: type2.id,
|
|
276
|
+
skipWorkflow: true,
|
|
277
|
+
}, new Uint8Array([1]));
|
|
278
|
+
// Doc 3: Coll 2, Type 1 (Cat 1)
|
|
279
|
+
await documentService.create(tenantId, {
|
|
280
|
+
assignment: { collections: [collection2.id] },
|
|
281
|
+
typeId: type1.id,
|
|
282
|
+
skipWorkflow: true,
|
|
283
|
+
}, new Uint8Array([1]));
|
|
284
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
285
|
+
includeTotalCount: true,
|
|
286
|
+
collectionIds: [collection1.id],
|
|
287
|
+
categoryIds: [category1.id]
|
|
288
|
+
});
|
|
289
|
+
expect(stats.totalCount).toBe(1); // Only Doc 1 matches both
|
|
290
|
+
});
|
|
291
|
+
});
|
|
292
|
+
test('getStatistics should return multiple stats without category filters', async () => {
|
|
293
|
+
await runInInjectionContext(injector, async () => {
|
|
294
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
295
|
+
const category = await categoryTypeService.createCategory({ tenantId, label: `Cat ${crypto.randomUUID()}`, parentId: null });
|
|
296
|
+
const type = await categoryTypeService.createType({ tenantId, label: 'Type', categoryId: category.id });
|
|
297
|
+
await documentService.create(tenantId, {
|
|
298
|
+
assignment: { collections: [collection.id] },
|
|
299
|
+
typeId: type.id,
|
|
300
|
+
skipWorkflow: true,
|
|
301
|
+
title: 'Title',
|
|
302
|
+
}, new Uint8Array([1, 2, 3]));
|
|
303
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
304
|
+
includeTotalCount: true,
|
|
305
|
+
includeTypeBreakdown: true,
|
|
306
|
+
includeMimeTypeDistribution: true,
|
|
307
|
+
includeStorageUsage: true,
|
|
308
|
+
includeDataQuality: true,
|
|
309
|
+
collectionIds: [collection.id]
|
|
310
|
+
});
|
|
311
|
+
expect(stats.totalCount).toBe(1);
|
|
312
|
+
expect(stats.typeBreakdown?.[type.id]).toBe(1);
|
|
313
|
+
expect(stats.storageUsage).toBe(3);
|
|
314
|
+
expect(stats.dataQuality?.missingTitle).toBe(0);
|
|
315
|
+
});
|
|
316
|
+
});
|
|
317
|
+
test('getStatistics should return stats with only collection filter', async () => {
|
|
318
|
+
await runInInjectionContext(injector, async () => {
|
|
319
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
320
|
+
await documentService.create(tenantId, {
|
|
321
|
+
assignment: { collections: [collection.id] },
|
|
322
|
+
skipWorkflow: true,
|
|
323
|
+
}, new Uint8Array([1]));
|
|
324
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
325
|
+
includeTotalCount: true,
|
|
326
|
+
includeMimeTypeDistribution: true,
|
|
327
|
+
collectionIds: [collection.id]
|
|
328
|
+
});
|
|
329
|
+
expect(stats.totalCount).toBe(1);
|
|
330
|
+
});
|
|
331
|
+
});
|
|
332
|
+
test('getStatistics should return mime type distribution with category filter', async () => {
|
|
333
|
+
await runInInjectionContext(injector, async () => {
|
|
334
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
335
|
+
const category = await categoryTypeService.createCategory({ tenantId, label: `Cat ${crypto.randomUUID()}`, parentId: null });
|
|
336
|
+
const type = await categoryTypeService.createType({ tenantId, label: 'Type', categoryId: category.id });
|
|
337
|
+
await documentService.create(tenantId, {
|
|
338
|
+
assignment: { collections: [collection.id] },
|
|
339
|
+
typeId: type.id,
|
|
340
|
+
skipWorkflow: true,
|
|
341
|
+
}, new Uint8Array([1]));
|
|
342
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
343
|
+
includeMimeTypeDistribution: true,
|
|
344
|
+
collectionIds: [collection.id],
|
|
345
|
+
categoryIds: [category.id]
|
|
346
|
+
});
|
|
347
|
+
expect(Object.keys(stats.mimeTypeDistribution ?? {}).length).toBeGreaterThan(0);
|
|
348
|
+
});
|
|
349
|
+
});
|
|
350
|
+
test('getStatistics should return tag usage with filters', async () => {
|
|
351
|
+
await runInInjectionContext(injector, async () => {
|
|
352
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
353
|
+
const category = await categoryTypeService.createCategory({ tenantId, label: `Cat ${crypto.randomUUID()}`, parentId: null });
|
|
354
|
+
const type = await categoryTypeService.createType({ tenantId, label: 'Type', categoryId: category.id });
|
|
355
|
+
await documentService.create(tenantId, {
|
|
356
|
+
assignment: { collections: [collection.id] },
|
|
357
|
+
typeId: type.id,
|
|
358
|
+
skipWorkflow: true,
|
|
359
|
+
tags: ['Tag1'],
|
|
360
|
+
}, new Uint8Array([1]));
|
|
361
|
+
const [tag] = await tagService.loadOrCreate(tenantId, ['Tag1']);
|
|
362
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
363
|
+
includeTagUsage: true,
|
|
364
|
+
collectionIds: [collection.id],
|
|
365
|
+
categoryIds: [category.id]
|
|
366
|
+
});
|
|
367
|
+
expect(stats.tagUsage?.[tag.id]).toBe(1);
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
test('getStatistics should return validation failures with collection filter', async () => {
|
|
371
|
+
await runInInjectionContext(injector, async () => {
|
|
372
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
373
|
+
const doc = await documentService.create(tenantId, {
|
|
374
|
+
assignment: { collections: [collection.id] },
|
|
375
|
+
skipWorkflow: true,
|
|
376
|
+
}, new Uint8Array([1]));
|
|
377
|
+
const workflowRepo = await injector.resolveAsync(getRepository(DocumentWorkflow));
|
|
378
|
+
const validationRepo = await injector.resolveAsync(getRepository(DocumentValidationExecution));
|
|
379
|
+
const definitionRepo = await injector.resolveAsync(getRepository(DocumentValidationDefinition));
|
|
380
|
+
const definition = await definitionRepo.insert({
|
|
381
|
+
tenantId,
|
|
382
|
+
identifier: 'test-val-2',
|
|
383
|
+
label: 'Test Validation',
|
|
384
|
+
description: null,
|
|
385
|
+
configuration: {},
|
|
386
|
+
});
|
|
387
|
+
const workflow = await workflowRepo.insert({
|
|
388
|
+
tenantId,
|
|
389
|
+
documentId: doc.id,
|
|
390
|
+
step: DocumentWorkflowStep.Validation,
|
|
391
|
+
state: DocumentWorkflowState.Pending,
|
|
392
|
+
skipAi: true,
|
|
393
|
+
completeTimestamp: null,
|
|
394
|
+
failReason: null,
|
|
395
|
+
completeUserId: null,
|
|
396
|
+
});
|
|
397
|
+
await validationRepo.insert({
|
|
398
|
+
tenantId,
|
|
399
|
+
workflowId: workflow.id,
|
|
400
|
+
definitionId: definition.id,
|
|
401
|
+
state: DocumentValidationExecutionState.Completed,
|
|
402
|
+
resultStatus: DocumentValidationResultStatus.Failed,
|
|
403
|
+
resultMessage: null,
|
|
404
|
+
startedAt: null,
|
|
405
|
+
completedAt: null,
|
|
406
|
+
});
|
|
407
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
408
|
+
includeValidationFailures: true,
|
|
409
|
+
collectionIds: [collection.id]
|
|
410
|
+
});
|
|
411
|
+
expect(stats.validationFailures).toBe(1);
|
|
412
|
+
});
|
|
413
|
+
});
|
|
414
|
+
test('getStatistics should return validation failures with category filter', async () => {
|
|
415
|
+
await runInInjectionContext(injector, async () => {
|
|
416
|
+
const categoryLabel = `Cat val ${crypto.randomUUID()}`;
|
|
417
|
+
const category = await categoryTypeService.createCategory({ tenantId, label: categoryLabel, parentId: null });
|
|
418
|
+
const type = await categoryTypeService.createType({ tenantId, label: 'Type val', categoryId: category.id });
|
|
419
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
420
|
+
const doc = await documentService.create(tenantId, {
|
|
421
|
+
assignment: { collections: [collection.id] },
|
|
422
|
+
typeId: type.id,
|
|
423
|
+
skipWorkflow: true,
|
|
424
|
+
}, new Uint8Array([1]));
|
|
425
|
+
const workflowRepo = await injector.resolveAsync(getRepository(DocumentWorkflow));
|
|
426
|
+
const validationRepo = await injector.resolveAsync(getRepository(DocumentValidationExecution));
|
|
427
|
+
const definitionRepo = await injector.resolveAsync(getRepository(DocumentValidationDefinition));
|
|
428
|
+
const definition = await definitionRepo.insert({
|
|
429
|
+
tenantId,
|
|
430
|
+
identifier: 'test-val-3',
|
|
431
|
+
label: 'Test Validation',
|
|
432
|
+
description: null,
|
|
433
|
+
configuration: {},
|
|
434
|
+
});
|
|
435
|
+
const workflow = await workflowRepo.insert({
|
|
436
|
+
tenantId,
|
|
437
|
+
documentId: doc.id,
|
|
438
|
+
step: DocumentWorkflowStep.Validation,
|
|
439
|
+
state: DocumentWorkflowState.Pending,
|
|
440
|
+
skipAi: true,
|
|
441
|
+
completeTimestamp: null,
|
|
442
|
+
failReason: null,
|
|
443
|
+
completeUserId: null,
|
|
444
|
+
});
|
|
445
|
+
await validationRepo.insert({
|
|
446
|
+
tenantId,
|
|
447
|
+
workflowId: workflow.id,
|
|
448
|
+
definitionId: definition.id,
|
|
449
|
+
state: DocumentValidationExecutionState.Completed,
|
|
450
|
+
resultStatus: DocumentValidationResultStatus.Failed,
|
|
451
|
+
resultMessage: null,
|
|
452
|
+
startedAt: null,
|
|
453
|
+
completedAt: null,
|
|
454
|
+
});
|
|
455
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
456
|
+
includeValidationFailures: true,
|
|
457
|
+
collectionIds: [collection.id],
|
|
458
|
+
categoryIds: [category.id]
|
|
459
|
+
});
|
|
460
|
+
expect(stats.validationFailures).toBe(1);
|
|
461
|
+
});
|
|
462
|
+
});
|
|
463
|
+
test('getStatistics should return type breakdown with collection filter', async () => {
|
|
464
|
+
await runInInjectionContext(injector, async () => {
|
|
465
|
+
const collection = await collectionService.createCollection(tenantId, null);
|
|
466
|
+
const category = await categoryTypeService.createCategory({ tenantId, label: `Cat ${crypto.randomUUID()}`, parentId: null });
|
|
467
|
+
const type = await categoryTypeService.createType({ tenantId, label: 'Type', categoryId: category.id });
|
|
468
|
+
await documentService.create(tenantId, {
|
|
469
|
+
assignment: { collections: [collection.id] },
|
|
470
|
+
typeId: type.id,
|
|
471
|
+
skipWorkflow: true,
|
|
472
|
+
}, new Uint8Array([1]));
|
|
473
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
474
|
+
includeTypeBreakdown: true,
|
|
475
|
+
collectionIds: [collection.id]
|
|
476
|
+
});
|
|
477
|
+
expect(stats.typeBreakdown?.[type.id]).toBe(1);
|
|
478
|
+
});
|
|
479
|
+
});
|
|
480
|
+
test('getStatistics should return zeroed stats for empty tenant', async () => {
|
|
481
|
+
await runInInjectionContext(injector, async () => {
|
|
482
|
+
const stats = await statsService.getStatistics(tenantId, {
|
|
483
|
+
includeTotalCount: true,
|
|
484
|
+
includeStorageUsage: true,
|
|
485
|
+
includeTotalPages: true,
|
|
486
|
+
includeValidationFailures: true,
|
|
487
|
+
collectionIds: []
|
|
488
|
+
});
|
|
489
|
+
expect(stats.totalCount).toBe(0);
|
|
490
|
+
expect(stats.storageUsage).toBe(0);
|
|
491
|
+
expect(stats.totalPages).toBe(0);
|
|
492
|
+
expect(stats.validationFailures).toBe(0);
|
|
493
|
+
});
|
|
494
|
+
});
|
|
495
|
+
});
|
|
@@ -5,7 +5,7 @@ import { NotFoundError } from '../../errors/not-found.error.js';
|
|
|
5
5
|
import { runInInjectionContext } from '../../injector/index.js';
|
|
6
6
|
import { ObjectStorage } from '../../object-storage/index.js';
|
|
7
7
|
import { TaskQueue } from '../../task-queue/index.js';
|
|
8
|
-
import {
|
|
8
|
+
import { clearTenantData, setupIntegrationTest } from '../../unit-test/index.js';
|
|
9
9
|
import { DocumentPropertyDataType } from '../models/document-property.model.js';
|
|
10
10
|
import { configureDocumentManagement } from '../server/configure.js';
|
|
11
11
|
import { migrateDocumentManagementSchema } from '../server/module.js';
|
|
@@ -62,7 +62,8 @@ describe('Document Management Extended Suite', () => {
|
|
|
62
62
|
await injector?.dispose();
|
|
63
63
|
});
|
|
64
64
|
beforeEach(async () => {
|
|
65
|
-
await
|
|
65
|
+
await clearTenantData(database, schema, ['type_property', 'property', 'type', 'category'], tenantId);
|
|
66
|
+
await clearTenantData(database, schema, ['type_property', 'property', 'type', 'category'], otherTenantId);
|
|
66
67
|
categoryTypeService.invalidateCache(tenantId);
|
|
67
68
|
propertyService.invalidateCache(tenantId);
|
|
68
69
|
categoryTypeService.invalidateCache(otherTenantId);
|
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
import type { EnumerationObject } from '../types/index.js';
|
|
2
|
+
/**
|
|
3
|
+
* Utility type to extract a union of values from an {@link EnumerationObject}.
|
|
4
|
+
* @template T The type of the enumeration object.
|
|
5
|
+
*/
|
|
2
6
|
export type EnumType<T extends EnumerationObject> = T[keyof T];
|
|
7
|
+
/**
|
|
8
|
+
* Defines a new enumeration by registering a name for an enumeration object.
|
|
9
|
+
* This name can be retrieved at runtime for introspection purposes.
|
|
10
|
+
* @template T The type of the enumeration object.
|
|
11
|
+
* @param name A unique name for the enumeration.
|
|
12
|
+
* @param enumObject The object containing the enumeration values.
|
|
13
|
+
* @returns The original `enumObject`.
|
|
14
|
+
*/
|
|
3
15
|
export declare function defineEnum<const T extends EnumerationObject>(name: string, enumObject: T): T;
|
|
16
|
+
/**
|
|
17
|
+
* Safely attempts to retrieve the registered name of an enumeration object.
|
|
18
|
+
* @param enumeration The enumeration object to look up.
|
|
19
|
+
* @returns The registered name, or `undefined` if not found.
|
|
20
|
+
*/
|
|
4
21
|
export declare function tryGetEnumName(enumeration: EnumerationObject): string | undefined;
|
|
22
|
+
/**
|
|
23
|
+
* Retrieves the registered name of an enumeration object.
|
|
24
|
+
* Throws an error if the enumeration is not registered.
|
|
25
|
+
* @param enumeration The enumeration object to look up.
|
|
26
|
+
* @returns The registered name.
|
|
27
|
+
* @throws Error if the enumeration is not registered.
|
|
28
|
+
*/
|
|
5
29
|
export declare function getEnumName(enumeration: EnumerationObject): string;
|
|
@@ -1,12 +1,32 @@
|
|
|
1
1
|
import { isUndefined } from '../utils/type-guards.js';
|
|
2
2
|
const registry = new WeakMap();
|
|
3
|
+
/**
|
|
4
|
+
* Defines a new enumeration by registering a name for an enumeration object.
|
|
5
|
+
* This name can be retrieved at runtime for introspection purposes.
|
|
6
|
+
* @template T The type of the enumeration object.
|
|
7
|
+
* @param name A unique name for the enumeration.
|
|
8
|
+
* @param enumObject The object containing the enumeration values.
|
|
9
|
+
* @returns The original `enumObject`.
|
|
10
|
+
*/
|
|
3
11
|
export function defineEnum(name, enumObject) {
|
|
4
12
|
registry.set(enumObject, name);
|
|
5
13
|
return enumObject;
|
|
6
14
|
}
|
|
15
|
+
/**
|
|
16
|
+
* Safely attempts to retrieve the registered name of an enumeration object.
|
|
17
|
+
* @param enumeration The enumeration object to look up.
|
|
18
|
+
* @returns The registered name, or `undefined` if not found.
|
|
19
|
+
*/
|
|
7
20
|
export function tryGetEnumName(enumeration) {
|
|
8
21
|
return registry.get(enumeration);
|
|
9
22
|
}
|
|
23
|
+
/**
|
|
24
|
+
* Retrieves the registered name of an enumeration object.
|
|
25
|
+
* Throws an error if the enumeration is not registered.
|
|
26
|
+
* @param enumeration The enumeration object to look up.
|
|
27
|
+
* @returns The registered name.
|
|
28
|
+
* @throws Error if the enumeration is not registered.
|
|
29
|
+
*/
|
|
10
30
|
export function getEnumName(enumeration) {
|
|
11
31
|
const name = tryGetEnumName(enumeration);
|
|
12
32
|
if (isUndefined(name)) {
|
|
@@ -23,7 +23,7 @@ import { inject, injectManyAsync, runInInjectionContext } from '../../injector/i
|
|
|
23
23
|
import { PrettyPrintLogFormatter, provideConsoleLogTransport } from '../../logger/index.js';
|
|
24
24
|
import { configureLocalMessageBus } from '../../message-bus/index.js';
|
|
25
25
|
import { WebServerModule } from '../../module/index.js';
|
|
26
|
-
import { configureS3ObjectStorage } from '../../object-storage/index.js';
|
|
26
|
+
import { configureS3ObjectStorage } from '../../object-storage/s3/index.js';
|
|
27
27
|
import { configureOrm } from '../../orm/server/index.js';
|
|
28
28
|
import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
|
|
29
29
|
import { configurePostgresTaskQueue, migratePostgresTaskQueueSchema } from '../../task-queue/postgres/index.js';
|
package/intl/number-parser.d.ts
CHANGED
|
@@ -1,18 +1,25 @@
|
|
|
1
1
|
export declare class NumberParser {
|
|
2
|
-
private
|
|
3
|
-
private readonly decimal;
|
|
4
|
-
private readonly numeral;
|
|
5
|
-
private readonly loosePattern;
|
|
6
|
-
private readonly numeralIndex;
|
|
7
|
-
private readonly numeralReplacer;
|
|
2
|
+
#private;
|
|
8
3
|
readonly locale: string;
|
|
9
4
|
constructor(locale: string);
|
|
10
5
|
/**
|
|
11
|
-
* Parse a string
|
|
12
|
-
* @param value Value to parse
|
|
13
|
-
* @param loose Try to parse an invalid string by removing unsupported characters. Might produce incorrect results, depending on input
|
|
6
|
+
* Parse a string.
|
|
7
|
+
* @param value Value to parse.
|
|
8
|
+
* @param loose Try to parse an invalid string by removing unsupported characters. Might produce incorrect results, depending on input.
|
|
14
9
|
*/
|
|
15
10
|
parse(value: string, loose?: boolean): number;
|
|
16
11
|
}
|
|
12
|
+
/**
|
|
13
|
+
* Get a memoized {@link NumberParser} instance for a given locale.
|
|
14
|
+
* @param locale The locale to get the parser for.
|
|
15
|
+
* @returns A {@link NumberParser} instance.
|
|
16
|
+
*/
|
|
17
17
|
export declare const getNumberParser: (locale: string) => NumberParser;
|
|
18
|
+
/**
|
|
19
|
+
* Parses a localized number string into a standard JavaScript number.
|
|
20
|
+
* @param locale The locale of the number string.
|
|
21
|
+
* @param value The string to parse.
|
|
22
|
+
* @param loose Whether to use loose parsing (stripping non-numeric characters).
|
|
23
|
+
* @returns The parsed number, or `NaN` if parsing fails.
|
|
24
|
+
*/
|
|
18
25
|
export declare function parseNumber(locale: string, value: string, loose?: boolean): number;
|