@tstdl/base 0.93.127 → 0.93.129
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/api/client/client.js +45 -9
- package/api/client/tests/api-client.test.d.ts +1 -0
- package/api/client/tests/api-client.test.js +194 -0
- package/api/types.d.ts +35 -3
- package/authentication/client/authentication.service.js +30 -11
- package/authentication/client/http-client.middleware.js +10 -3
- package/authentication/server/authentication.service.d.ts +12 -0
- package/authentication/server/authentication.service.js +14 -2
- package/authentication/tests/authentication.client-error-handling.test.js +23 -66
- package/authentication/tests/authentication.client-service-refresh.test.js +14 -14
- package/cancellation/token.d.ts +6 -0
- package/cancellation/token.js +8 -0
- package/document-management/server/services/document-file.service.js +10 -9
- package/document-management/server/services/document-management-ancillary.service.d.ts +12 -1
- package/document-management/server/services/document-management-ancillary.service.js +9 -0
- package/file/server/temporary-file.d.ts +2 -1
- package/file/server/temporary-file.js +5 -1
- package/http/client/adapters/undici.adapter.js +0 -2
- package/http/client/http-client-request.d.ts +2 -0
- package/http/client/http-client-request.js +4 -0
- package/http/client/http-client-response.d.ts +1 -1
- package/http/client/http-client-response.js +3 -2
- package/http/utils.d.ts +6 -0
- package/http/utils.js +71 -0
- package/injector/injector.js +2 -0
- package/mail/drizzle/0000_numerous_the_watchers.sql +8 -0
- package/mail/drizzle/meta/0000_snapshot.json +1 -32
- package/mail/drizzle/meta/_journal.json +2 -9
- package/object-storage/s3/s3.object-storage.js +6 -6
- package/object-storage/s3/tests/s3.object-storage.integration.test.js +22 -53
- package/orm/tests/repository-expiration.test.js +3 -3
- package/package.json +1 -1
- package/pdf/utils.d.ts +24 -3
- package/pdf/utils.js +89 -30
- package/process/spawn.d.ts +1 -1
- package/rate-limit/tests/postgres-rate-limiter.test.js +9 -7
- package/renderer/typst.d.ts +5 -0
- package/renderer/typst.js +9 -5
- package/task-queue/tests/complex.test.js +22 -22
- package/task-queue/tests/dependencies.test.js +15 -13
- package/task-queue/tests/queue.test.js +13 -13
- package/task-queue/tests/worker.test.js +12 -12
- package/testing/integration-setup.d.ts +2 -0
- package/testing/integration-setup.js +13 -7
- package/utils/backoff.d.ts +27 -3
- package/utils/backoff.js +31 -9
- package/utils/index.d.ts +1 -0
- package/utils/index.js +1 -0
- package/utils/retry-with-backoff.d.ts +22 -0
- package/utils/retry-with-backoff.js +64 -0
- package/utils/tests/backoff.test.d.ts +1 -0
- package/utils/tests/backoff.test.js +41 -0
- package/utils/tests/retry-with-backoff.test.d.ts +1 -0
- package/utils/tests/retry-with-backoff.test.js +49 -0
- package/mail/drizzle/0000_previous_malcolm_colcord.sql +0 -13
- package/mail/drizzle/0001_flimsy_bloodscream.sql +0 -5
- package/mail/drizzle/meta/0001_snapshot.json +0 -69
|
@@ -70,6 +70,12 @@ describe('AuthenticationClientService Error Handling & Stuck States', () => {
|
|
|
70
70
|
afterEach(async () => {
|
|
71
71
|
await service.dispose();
|
|
72
72
|
});
|
|
73
|
+
function setupServiceWithToken(iatOffset = -10, expOffset = 5) {
|
|
74
|
+
const now = Math.floor(Date.now() / 1000);
|
|
75
|
+
const initialToken = { iat: now + iatOffset, exp: now + expOffset, jti: 'initial' };
|
|
76
|
+
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
77
|
+
service = injector.resolve(AuthenticationClientService);
|
|
78
|
+
}
|
|
73
79
|
test('Corrupt Storage: should handle invalid JSON in storage gracefully', async () => {
|
|
74
80
|
// initialize with corrupt token
|
|
75
81
|
globalThis.localStorage.setItem('AuthenticationService:token', '{ invalid-json');
|
|
@@ -77,84 +83,35 @@ describe('AuthenticationClientService Error Handling & Stuck States', () => {
|
|
|
77
83
|
expect(service.token()).toBeUndefined();
|
|
78
84
|
expect(mockLogger.warn).toHaveBeenCalled(); // Should warn about parse error
|
|
79
85
|
});
|
|
80
|
-
test(
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
+
test.each([
|
|
87
|
+
['InvalidTokenError', new InvalidTokenError()],
|
|
88
|
+
['NotFoundError', new NotFoundError('Session not found')],
|
|
89
|
+
['ForbiddenError', new ForbiddenError()],
|
|
90
|
+
['UnauthorizedError', new UnauthorizedError()],
|
|
91
|
+
['NotSupportedError', new NotSupportedError()],
|
|
92
|
+
['BadRequestError', new BadRequestError()],
|
|
93
|
+
])('Unrecoverable Errors: should logout on %s during refresh', async (_, error) => {
|
|
94
|
+
setupServiceWithToken();
|
|
95
|
+
mockApiClient.refresh.mockRejectedValue(error);
|
|
86
96
|
// Wait for loop to pick up refresh
|
|
87
|
-
await timeout(
|
|
88
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
89
|
-
expect(service.token()).toBeUndefined();
|
|
90
|
-
});
|
|
91
|
-
test('Unrecoverable Errors: should logout on NotFoundError during refresh', async () => {
|
|
92
|
-
const now = Math.floor(Date.now() / 1000);
|
|
93
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
94
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
95
|
-
service = injector.resolve(AuthenticationClientService);
|
|
96
|
-
mockApiClient.refresh.mockRejectedValue(new NotFoundError('Session not found'));
|
|
97
|
-
await timeout(100);
|
|
98
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
99
|
-
expect(service.token()).toBeUndefined();
|
|
100
|
-
});
|
|
101
|
-
test('Unrecoverable Errors: should logout on ForbiddenError during refresh', async () => {
|
|
102
|
-
const now = Math.floor(Date.now() / 1000);
|
|
103
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
104
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
105
|
-
service = injector.resolve(AuthenticationClientService);
|
|
106
|
-
mockApiClient.refresh.mockRejectedValue(new ForbiddenError());
|
|
107
|
-
await timeout(100);
|
|
108
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
109
|
-
expect(service.token()).toBeUndefined();
|
|
110
|
-
});
|
|
111
|
-
test('Unrecoverable Errors: should logout on UnauthorizedError during refresh', async () => {
|
|
112
|
-
const now = Math.floor(Date.now() / 1000);
|
|
113
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
114
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
115
|
-
service = injector.resolve(AuthenticationClientService);
|
|
116
|
-
mockApiClient.refresh.mockRejectedValue(new UnauthorizedError());
|
|
117
|
-
await timeout(100);
|
|
118
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
119
|
-
expect(service.token()).toBeUndefined();
|
|
120
|
-
});
|
|
121
|
-
test('Unrecoverable Errors: should logout on NotSupportedError during refresh', async () => {
|
|
122
|
-
const now = Math.floor(Date.now() / 1000);
|
|
123
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
124
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
125
|
-
service = injector.resolve(AuthenticationClientService);
|
|
126
|
-
mockApiClient.refresh.mockRejectedValue(new NotSupportedError());
|
|
127
|
-
await timeout(100);
|
|
128
|
-
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
129
|
-
expect(service.token()).toBeUndefined();
|
|
130
|
-
});
|
|
131
|
-
test('Unrecoverable Errors: should logout on BadRequestError during refresh', async () => {
|
|
132
|
-
const now = Math.floor(Date.now() / 1000);
|
|
133
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
134
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
135
|
-
service = injector.resolve(AuthenticationClientService);
|
|
136
|
-
mockApiClient.refresh.mockRejectedValue(new BadRequestError());
|
|
137
|
-
await timeout(100);
|
|
97
|
+
await timeout(20);
|
|
138
98
|
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
139
99
|
expect(service.token()).toBeUndefined();
|
|
140
100
|
});
|
|
141
101
|
test('Recoverable Errors: should NOT logout on generic Error during refresh', async () => {
|
|
142
|
-
|
|
143
|
-
const initialToken = { exp: now + 5, jti: 'initial' };
|
|
144
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
145
|
-
service = injector.resolve(AuthenticationClientService);
|
|
102
|
+
setupServiceWithToken();
|
|
146
103
|
mockApiClient.refresh.mockRejectedValue(new Error('Network failure'));
|
|
147
|
-
await timeout(
|
|
104
|
+
await timeout(20);
|
|
148
105
|
// Should NOT have called endSession (logout)
|
|
149
106
|
expect(mockApiClient.endSession).not.toHaveBeenCalled();
|
|
150
107
|
// Token should still be present (retry logic handles it)
|
|
151
108
|
expect(service.token()).toBeDefined();
|
|
109
|
+
// Trigger immediate retry to finish test quickly
|
|
110
|
+
service.requestRefresh();
|
|
111
|
+
await timeout(20);
|
|
152
112
|
});
|
|
153
113
|
test('Logout Failure: should clear local token even if server logout fails', async () => {
|
|
154
|
-
|
|
155
|
-
const initialToken = { exp: now + 1000, jti: 'initial' };
|
|
156
|
-
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
157
|
-
service = injector.resolve(AuthenticationClientService);
|
|
114
|
+
setupServiceWithToken(-1000, 1000);
|
|
158
115
|
mockApiClient.endSession.mockRejectedValue(new Error('Network error during logout'));
|
|
159
116
|
await service.logout();
|
|
160
117
|
expect(mockApiClient.endSession).toHaveBeenCalled();
|
|
@@ -65,59 +65,59 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
|
|
|
65
65
|
test('Zombie Timer: loop should wake up immediately when token changes', async () => {
|
|
66
66
|
// 1. Mock a long expiration
|
|
67
67
|
const now = Math.floor(Date.now() / 1000);
|
|
68
|
-
const initialToken = { exp: now + 3600, jti: 'initial' };
|
|
68
|
+
const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
|
|
69
69
|
// Set in storage so initialize() (called by resolve) loads it
|
|
70
70
|
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
71
71
|
service = injector.resolve(AuthenticationClientService);
|
|
72
72
|
// Wait for loop to enter the race condition (wait phase)
|
|
73
|
-
await timeout(
|
|
73
|
+
await timeout(20);
|
|
74
74
|
// 2. Change token
|
|
75
|
-
const newToken = { exp: now + 3600, jti: 'new' };
|
|
75
|
+
const newToken = { iat: now - 1800, exp: now + 3600, jti: 'new' };
|
|
76
76
|
mockApiClient.refresh.mockResolvedValue(newToken);
|
|
77
77
|
service.requestRefresh(); // This should trigger immediate wake up
|
|
78
78
|
// Wait for loop to process
|
|
79
|
-
await timeout(
|
|
79
|
+
await timeout(20);
|
|
80
80
|
expect(mockApiClient.refresh).toHaveBeenCalled();
|
|
81
81
|
});
|
|
82
82
|
test('Forced Refresh Loss: forceRefreshToken should not be cleared on failure', async () => {
|
|
83
83
|
const now = Math.floor(Date.now() / 1000);
|
|
84
|
-
const initialToken = { exp: now + 3600, jti: 'initial' };
|
|
84
|
+
const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
|
|
85
85
|
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
86
86
|
service = injector.resolve(AuthenticationClientService);
|
|
87
|
-
await timeout(
|
|
87
|
+
await timeout(20);
|
|
88
88
|
// 1. Mock refresh failure
|
|
89
89
|
mockApiClient.refresh.mockRejectedValue(new Error('Network Error'));
|
|
90
90
|
service.requestRefresh();
|
|
91
91
|
// Wait for loop to attempt refresh and fail
|
|
92
|
-
await timeout(
|
|
92
|
+
await timeout(50);
|
|
93
93
|
expect(mockApiClient.refresh).toHaveBeenCalled();
|
|
94
94
|
expect(service.forceRefreshToken.isSet).toBe(true); // Should STILL be set
|
|
95
95
|
});
|
|
96
96
|
test('Lock Contention Backoff: should wait 5 seconds and not busy-loop', async () => {
|
|
97
97
|
const now = Math.floor(Date.now() / 1000);
|
|
98
|
-
const initialToken = { exp: now + 5, jti: 'initial' }; // Expiring soon
|
|
98
|
+
const initialToken = { iat: now - 3600, exp: now + 5, jti: 'initial' }; // Expiring soon
|
|
99
99
|
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
100
100
|
// 1. Mock lock already held
|
|
101
101
|
mockLock.tryUse.mockResolvedValue(undefined); // lockAcquired = false
|
|
102
102
|
const startTime = Date.now();
|
|
103
103
|
service = injector.resolve(AuthenticationClientService);
|
|
104
|
-
// We expect it to try once, fail to get lock, and then wait
|
|
105
|
-
await timeout(
|
|
104
|
+
// We expect it to try once, fail to get lock, and then wait some time.
|
|
105
|
+
await timeout(50);
|
|
106
106
|
expect(mockLock.tryUse).toHaveBeenCalledTimes(1);
|
|
107
107
|
// Check if it's still waiting (not finished loop)
|
|
108
108
|
const duration = Date.now() - startTime;
|
|
109
|
-
expect(duration).toBeLessThan(
|
|
109
|
+
expect(duration).toBeLessThan(500);
|
|
110
110
|
});
|
|
111
111
|
test('Busy Loop: should not busy loop when forceRefreshToken is set and lock is held', async () => {
|
|
112
112
|
const now = Math.floor(Date.now() / 1000);
|
|
113
|
-
const initialToken = { exp: now + 3600, jti: 'initial' };
|
|
113
|
+
const initialToken = { iat: now - 3600, exp: now + 3600, jti: 'initial' };
|
|
114
114
|
globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
|
|
115
115
|
// Mock lock already held
|
|
116
116
|
mockLock.tryUse.mockResolvedValue(undefined);
|
|
117
117
|
service = injector.resolve(AuthenticationClientService);
|
|
118
|
-
await timeout(
|
|
118
|
+
await timeout(20);
|
|
119
119
|
service.requestRefresh(); // Set the flag
|
|
120
|
-
await timeout(
|
|
120
|
+
await timeout(50);
|
|
121
121
|
// If it busy loops, this will be much higher than 1.
|
|
122
122
|
expect(mockLock.tryUse.mock.calls.length).toBeLessThan(5);
|
|
123
123
|
});
|
package/cancellation/token.d.ts
CHANGED
|
@@ -46,6 +46,7 @@ export type ConnectConfig = {
|
|
|
46
46
|
* (emits when set).
|
|
47
47
|
*/
|
|
48
48
|
export declare class CancellationSignal implements PromiseLike<void>, Subscribable<void> {
|
|
49
|
+
#private;
|
|
49
50
|
protected readonly _stateSubject: BehaviorSubject<boolean>;
|
|
50
51
|
/**
|
|
51
52
|
* Observable which emits the current state (true for set, false for unset)
|
|
@@ -91,6 +92,11 @@ export declare class CancellationSignal implements PromiseLike<void>, Subscribab
|
|
|
91
92
|
* @internal
|
|
92
93
|
*/
|
|
93
94
|
constructor(stateSubject: BehaviorSubject<boolean>);
|
|
95
|
+
/**
|
|
96
|
+
* Returns a standard `AbortSignal` that is aborted when this token is set.
|
|
97
|
+
* Useful for interoperability with APIs like `fetch`.
|
|
98
|
+
*/
|
|
99
|
+
get abortSignal(): AbortSignal;
|
|
94
100
|
/**
|
|
95
101
|
* Returns a standard `AbortSignal` that is aborted when this token is set.
|
|
96
102
|
* Useful for interoperability with APIs like `fetch`.
|
package/cancellation/token.js
CHANGED
|
@@ -15,6 +15,7 @@ import { isBoolean } from '../utils/type-guards.js';
|
|
|
15
15
|
*/
|
|
16
16
|
export class CancellationSignal {
|
|
17
17
|
_stateSubject;
|
|
18
|
+
#abortSignal;
|
|
18
19
|
/**
|
|
19
20
|
* Observable which emits the current state (true for set, false for unset)
|
|
20
21
|
* and any subsequent state changes.
|
|
@@ -74,6 +75,13 @@ export class CancellationSignal {
|
|
|
74
75
|
constructor(stateSubject) {
|
|
75
76
|
this._stateSubject = stateSubject;
|
|
76
77
|
}
|
|
78
|
+
/**
|
|
79
|
+
* Returns a standard `AbortSignal` that is aborted when this token is set.
|
|
80
|
+
* Useful for interoperability with APIs like `fetch`.
|
|
81
|
+
*/
|
|
82
|
+
get abortSignal() {
|
|
83
|
+
return (this.#abortSignal ??= this.asAbortSignal());
|
|
84
|
+
}
|
|
77
85
|
/**
|
|
78
86
|
* Returns a standard `AbortSignal` that is aborted when this token is set.
|
|
79
87
|
* Useful for interoperability with APIs like `fetch`.
|
|
@@ -6,7 +6,6 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
|
|
|
6
6
|
};
|
|
7
7
|
import sharp, {} from 'sharp';
|
|
8
8
|
import { match } from 'ts-pattern';
|
|
9
|
-
import { injectGenkit } from '../../../ai/genkit/index.js';
|
|
10
9
|
import { ForbiddenError } from '../../../errors/forbidden.error.js';
|
|
11
10
|
import { NotImplementedError } from '../../../errors/not-implemented.error.js';
|
|
12
11
|
import { getMimeType, getMimeTypeExtensions, mimeTypes } from '../../../file/index.js';
|
|
@@ -20,14 +19,15 @@ import { digest } from '../../../utils/cryptography.js';
|
|
|
20
19
|
import { currentTimestamp } from '../../../utils/date-time.js';
|
|
21
20
|
import { getRandomString } from '../../../utils/random.js';
|
|
22
21
|
import { readableStreamFromPromise, readBinaryStream } from '../../../utils/stream/index.js';
|
|
23
|
-
import { isNotReadableStream, isNotUint8Array, isUint8Array } from '../../../utils/type-guards.js';
|
|
22
|
+
import { isNotReadableStream, isNotUint8Array, isString, isUint8Array } from '../../../utils/type-guards.js';
|
|
24
23
|
import { millisecondsPerMinute } from '../../../utils/units.js';
|
|
25
24
|
import { Document } from '../../models/index.js';
|
|
26
25
|
import { DocumentManagementConfiguration } from '../module.js';
|
|
26
|
+
import { DocumentManagementAncillaryService } from './document-management-ancillary.service.js';
|
|
27
27
|
import { DocumentManagementSingleton } from './singleton.js';
|
|
28
28
|
let DocumentFileService = class DocumentFileService extends Transactional {
|
|
29
29
|
#configuration = inject(DocumentManagementConfiguration);
|
|
30
|
-
#
|
|
30
|
+
#documentManagementAncillaryService = inject(DocumentManagementAncillaryService);
|
|
31
31
|
#fileObjectStorage = inject(ObjectStorage, this.#configuration.fileObjectStorageModule);
|
|
32
32
|
#filePreviewObjectStorage = inject(ObjectStorage, this.#configuration.filePreviewObjectStorageModule);
|
|
33
33
|
#fileUploadObjectStorage = inject(ObjectStorage, { module: this.#configuration.fileUploadObjectStorageModule /* , configuration: { lifecycle: { expiration: { after: 5 * secondsPerMinute } } } */ });
|
|
@@ -86,7 +86,10 @@ let DocumentFileService = class DocumentFileService extends Transactional {
|
|
|
86
86
|
return this.#fileObjectStorage.getContentStream(objectKey);
|
|
87
87
|
}
|
|
88
88
|
async getContentUrl(document, download = false) {
|
|
89
|
-
|
|
89
|
+
const resolvedFilename = await this.#documentManagementAncillaryService.getDocumentFilename(document);
|
|
90
|
+
const fileExtension = getMimeTypeExtensions(document.mimeType)[0] ?? 'bin';
|
|
91
|
+
const filename = isString(resolvedFilename) ? `${resolvedFilename}.${fileExtension}` : resolvedFilename.fullname;
|
|
92
|
+
return await this.getDocumentFileContentObjectUrl(document, filename, download);
|
|
90
93
|
}
|
|
91
94
|
/** Gets the underlying object storage object for the document file */
|
|
92
95
|
async getObject(document) {
|
|
@@ -132,14 +135,12 @@ let DocumentFileService = class DocumentFileService extends Transactional {
|
|
|
132
135
|
await this.#filePreviewObjectStorage.uploadObject(key, image, { contentLength: image.length, contentType: 'image/webp' });
|
|
133
136
|
}
|
|
134
137
|
}
|
|
135
|
-
async getDocumentFileContentObjectUrl(document,
|
|
138
|
+
async getDocumentFileContentObjectUrl(document, filename, download) {
|
|
136
139
|
const key = getObjectKey(document.id);
|
|
137
|
-
const fileExtension = getMimeTypeExtensions(document.mimeType)[0] ?? 'bin';
|
|
138
140
|
const disposition = download ? 'attachment' : 'inline';
|
|
139
|
-
const filename = `${title}.${fileExtension}`;
|
|
140
141
|
return await this.#fileObjectStorage.getDownloadUrl(key, currentTimestamp() + (5 * millisecondsPerMinute), {
|
|
141
|
-
'
|
|
142
|
-
'
|
|
142
|
+
'Content-Type': document.mimeType,
|
|
143
|
+
'Content-Disposition': `${disposition}; filename = "${encodeURIComponent(filename)}"`,
|
|
143
144
|
});
|
|
144
145
|
}
|
|
145
146
|
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
import type { DocumentCollection } from '../../models/index.js';
|
|
1
|
+
import type { Document, DocumentCollection } from '../../models/index.js';
|
|
2
2
|
import type { DocumentCollectionMetadata } from '../../service-models/index.js';
|
|
3
|
+
export type DocumentFileNameResult = string | {
|
|
4
|
+
/** The full name of the document, including extension. */
|
|
5
|
+
fullname: string;
|
|
6
|
+
};
|
|
3
7
|
export declare abstract class DocumentManagementAncillaryService {
|
|
4
8
|
/**
|
|
5
9
|
* Resolves application-specific metadata for a list of document collections.
|
|
@@ -7,4 +11,11 @@ export declare abstract class DocumentManagementAncillaryService {
|
|
|
7
11
|
* @returns A promise that resolves to an array of DocumentCollectionMetadata, corresponding to the input collections.
|
|
8
12
|
*/
|
|
9
13
|
abstract resolveMetadata(tenantId: string, collections: DocumentCollection[]): DocumentCollectionMetadata[] | Promise<DocumentCollectionMetadata[]>;
|
|
14
|
+
/**
|
|
15
|
+
* Resolve the file name for a document, which will be used when downloading the document file. The file extension is automatically determined based on the document's MIME type, so the returned file name should not include the extension.
|
|
16
|
+
* @param document The Document entity for which to resolve the file name.
|
|
17
|
+
* @param title An optional title provided by the api call.
|
|
18
|
+
* @returns The resolved file name for the document, without the file extension.
|
|
19
|
+
*/
|
|
20
|
+
getDocumentFilename(document: Document): DocumentFileNameResult | Promise<DocumentFileNameResult>;
|
|
10
21
|
}
|
|
@@ -1,2 +1,11 @@
|
|
|
1
1
|
export class DocumentManagementAncillaryService {
|
|
2
|
+
/**
|
|
3
|
+
* Resolve the file name for a document, which will be used when downloading the document file. The file extension is automatically determined based on the document's MIME type, so the returned file name should not include the extension.
|
|
4
|
+
* @param document The Document entity for which to resolve the file name.
|
|
5
|
+
* @param title An optional title provided by the api call.
|
|
6
|
+
* @returns The resolved file name for the document, without the file extension.
|
|
7
|
+
*/
|
|
8
|
+
getDocumentFilename(document) {
|
|
9
|
+
return document.title ?? document.originalFileName ?? document.id;
|
|
10
|
+
}
|
|
2
11
|
}
|
|
@@ -12,7 +12,7 @@ export declare class TemporaryFile implements AsyncDisposable {
|
|
|
12
12
|
/**
|
|
13
13
|
* Prevents the temporary file from being deleted on disposal.
|
|
14
14
|
*/
|
|
15
|
-
keep():
|
|
15
|
+
keep(): this;
|
|
16
16
|
read(): Promise<Uint8Array>;
|
|
17
17
|
readText(): Promise<string>;
|
|
18
18
|
readStream(): ReadableStream<Uint8Array>;
|
|
@@ -25,5 +25,6 @@ export declare class TemporaryFile implements AsyncDisposable {
|
|
|
25
25
|
moveTo(path: string, keep?: boolean): Promise<void>;
|
|
26
26
|
delete(): Promise<void>;
|
|
27
27
|
size(): Promise<number>;
|
|
28
|
+
dispose(): Promise<void>;
|
|
28
29
|
[Symbol.asyncDispose](): Promise<void>;
|
|
29
30
|
}
|
|
@@ -36,6 +36,7 @@ export class TemporaryFile {
|
|
|
36
36
|
*/
|
|
37
37
|
keep() {
|
|
38
38
|
this.#keep = true;
|
|
39
|
+
return this;
|
|
39
40
|
}
|
|
40
41
|
async read() {
|
|
41
42
|
return await readFile(this.#path);
|
|
@@ -69,7 +70,7 @@ export class TemporaryFile {
|
|
|
69
70
|
const result = await stat(this.#path);
|
|
70
71
|
return result.size;
|
|
71
72
|
}
|
|
72
|
-
async
|
|
73
|
+
async dispose() {
|
|
73
74
|
if (this.#keep) {
|
|
74
75
|
return;
|
|
75
76
|
}
|
|
@@ -78,4 +79,7 @@ export class TemporaryFile {
|
|
|
78
79
|
}
|
|
79
80
|
catch { }
|
|
80
81
|
}
|
|
82
|
+
async [Symbol.asyncDispose]() {
|
|
83
|
+
await this.dispose();
|
|
84
|
+
}
|
|
81
85
|
}
|
|
@@ -20,7 +20,6 @@ export class UndiciHttpClientAdapterOptions {
|
|
|
20
20
|
;
|
|
21
21
|
let UndiciHttpClientAdapter = class UndiciHttpClientAdapter extends HttpClientAdapter {
|
|
22
22
|
options = inject(UndiciHttpClientAdapterOptions, undefined, { optional: true }) ?? {};
|
|
23
|
-
// eslint-disable-next-line max-lines-per-function, max-statements
|
|
24
23
|
async call(httpClientRequest) {
|
|
25
24
|
let body;
|
|
26
25
|
if (isDefined(httpClientRequest.body?.json)) {
|
|
@@ -64,7 +63,6 @@ let UndiciHttpClientAdapter = class UndiciHttpClientAdapter extends HttpClientAd
|
|
|
64
63
|
const httpClientResponse = new HttpClientResponse({
|
|
65
64
|
request: httpClientRequest,
|
|
66
65
|
statusCode: response.statusCode,
|
|
67
|
-
statusMessage: '?',
|
|
68
66
|
headers: new HttpHeaders(response.headers),
|
|
69
67
|
body: response.body,
|
|
70
68
|
closeHandler: () => response.body.destroy(),
|
|
@@ -34,6 +34,7 @@ export type HttpClientRequestOptions = Partial<TypedOmit<HttpClientRequest, 'url
|
|
|
34
34
|
formData?: HttpFormDataObject | FormData;
|
|
35
35
|
};
|
|
36
36
|
abortSignal?: CancellationSignal;
|
|
37
|
+
priority?: RequestPriority;
|
|
37
38
|
};
|
|
38
39
|
export type HttpRequestCredentials = 'omit' | 'same-origin' | 'include';
|
|
39
40
|
export type HttpClientRequestObject = HttpClientRequestOptions & {
|
|
@@ -95,6 +96,7 @@ export declare class HttpClientRequest implements Disposable {
|
|
|
95
96
|
credentials: HttpRequestCredentials | undefined;
|
|
96
97
|
authorization: HttpRequestAuthorization | undefined;
|
|
97
98
|
body: HttpRequestBody | undefined;
|
|
99
|
+
priority: RequestPriority | undefined;
|
|
98
100
|
/**
|
|
99
101
|
* Request timeout in milliseconds
|
|
100
102
|
* @default 30000
|
|
@@ -61,6 +61,7 @@ export class HttpClientRequest {
|
|
|
61
61
|
credentials;
|
|
62
62
|
authorization;
|
|
63
63
|
body;
|
|
64
|
+
priority;
|
|
64
65
|
/**
|
|
65
66
|
* Request timeout in milliseconds
|
|
66
67
|
* @default 30000
|
|
@@ -101,6 +102,7 @@ export class HttpClientRequest {
|
|
|
101
102
|
this.authorization = requestOptions.authorization;
|
|
102
103
|
this.body = normalizeBody(requestOptions.body);
|
|
103
104
|
this.credentials = requestOptions.credentials;
|
|
105
|
+
this.priority = requestOptions.priority;
|
|
104
106
|
this.timeout = requestOptions.timeout ?? 30000;
|
|
105
107
|
this.throwOnNon200 = requestOptions.throwOnNon200 ?? true;
|
|
106
108
|
this.context = requestOptions.context ?? {};
|
|
@@ -122,6 +124,7 @@ export class HttpClientRequest {
|
|
|
122
124
|
request.authorization = clone(request.authorization, true);
|
|
123
125
|
request.urlParameters = new HttpUrlParameters(request.urlParameters);
|
|
124
126
|
request.body = normalizeBody(request.body);
|
|
127
|
+
request.priority = this.priority;
|
|
125
128
|
return request;
|
|
126
129
|
}
|
|
127
130
|
asObject() {
|
|
@@ -135,6 +138,7 @@ export class HttpClientRequest {
|
|
|
135
138
|
query: this.query.asObject(),
|
|
136
139
|
authorization: this.authorization,
|
|
137
140
|
body,
|
|
141
|
+
priority: this.priority,
|
|
138
142
|
timeout: this.timeout,
|
|
139
143
|
throwOnNon200: this.throwOnNon200,
|
|
140
144
|
context: this.context,
|
|
@@ -9,7 +9,7 @@ export type HttpClientResponseObject = TypedOmit<HttpClientResponse, 'hasBody' |
|
|
|
9
9
|
export type HttpClientResponseOptions = {
|
|
10
10
|
request: HttpClientRequest;
|
|
11
11
|
statusCode: number;
|
|
12
|
-
statusMessage
|
|
12
|
+
statusMessage?: string | null;
|
|
13
13
|
headers: HttpHeadersObject | HttpHeaders;
|
|
14
14
|
body: HttpBody | HttpBodySource;
|
|
15
15
|
closeHandler: () => void;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { isString } from '../../utils/type-guards.js';
|
|
2
2
|
import { HttpBody } from '../http-body.js';
|
|
3
3
|
import { HttpHeaders } from '../http-headers.js';
|
|
4
|
+
import { getHttpStatusMessage } from '../utils.js';
|
|
4
5
|
export class HttpClientResponse {
|
|
5
6
|
closeHandler;
|
|
6
7
|
request;
|
|
@@ -14,7 +15,7 @@ export class HttpClientResponse {
|
|
|
14
15
|
constructor(options) {
|
|
15
16
|
this.request = options.request;
|
|
16
17
|
this.statusCode = options.statusCode;
|
|
17
|
-
this.statusMessage = (isString(options.statusMessage) && (options.statusMessage.length > 0)) ? options.statusMessage :
|
|
18
|
+
this.statusMessage = (isString(options.statusMessage) && (options.statusMessage.length > 0)) ? options.statusMessage : getHttpStatusMessage(this.statusCode);
|
|
18
19
|
this.headers = new HttpHeaders(options.headers);
|
|
19
20
|
this.body = (options.body instanceof HttpBody) ? options.body : new HttpBody(options.body, this.headers);
|
|
20
21
|
this.closeHandler = options.closeHandler;
|
|
@@ -28,7 +29,7 @@ export class HttpClientResponse {
|
|
|
28
29
|
statusCode: this.statusCode,
|
|
29
30
|
statusMessage: this.statusMessage,
|
|
30
31
|
headers: this.headers.asObject(),
|
|
31
|
-
body: this.body
|
|
32
|
+
body: this.body,
|
|
32
33
|
};
|
|
33
34
|
return obj;
|
|
34
35
|
}
|
package/http/utils.d.ts
CHANGED
|
@@ -15,4 +15,10 @@ export declare function readBodyAsTextStream(body: Body, headers: HttpHeaders, o
|
|
|
15
15
|
export declare function readBodyAsJson(body: Body, headers: HttpHeaders, options?: ReadBodyAsJsonOptions): Promise<UndefinableJson>;
|
|
16
16
|
export declare function readBody(body: Body, headers: HttpHeaders, options?: ReadBodyOptions): Promise<string | UndefinableJson | Uint8Array>;
|
|
17
17
|
export declare function readBodyAsStream(body: Body, headers: HttpHeaders, options?: ReadBodyOptions): ReadableStream<string> | ReadableStream<Uint8Array>;
|
|
18
|
+
/**
|
|
19
|
+
* Returns the standard HTTP status message for a given status code.
|
|
20
|
+
* @param statusCode The HTTP status code.
|
|
21
|
+
* @returns The standard status message, or 'Unknown' if the code is not recognized.
|
|
22
|
+
*/
|
|
23
|
+
export declare function getHttpStatusMessage(statusCode: number): string;
|
|
18
24
|
export {};
|
package/http/utils.js
CHANGED
|
@@ -104,3 +104,74 @@ function ensureSize(length, options) {
|
|
|
104
104
|
throw MaxBytesExceededError.fromBytes(options.maxBytes);
|
|
105
105
|
}
|
|
106
106
|
}
|
|
107
|
+
const httpStatusMessages = {
|
|
108
|
+
100: 'Continue',
|
|
109
|
+
101: 'Switching Protocols',
|
|
110
|
+
102: 'Processing',
|
|
111
|
+
103: 'Early Hints',
|
|
112
|
+
200: 'OK',
|
|
113
|
+
201: 'Created',
|
|
114
|
+
202: 'Accepted',
|
|
115
|
+
203: 'Non-Authoritative Information',
|
|
116
|
+
204: 'No Content',
|
|
117
|
+
205: 'Reset Content',
|
|
118
|
+
206: 'Partial Content',
|
|
119
|
+
207: 'Multi-Status',
|
|
120
|
+
208: 'Already Reported',
|
|
121
|
+
226: 'IM Used',
|
|
122
|
+
300: 'Multiple Choices',
|
|
123
|
+
301: 'Moved Permanently',
|
|
124
|
+
302: 'Found',
|
|
125
|
+
303: 'See Other',
|
|
126
|
+
304: 'Not Modified',
|
|
127
|
+
307: 'Temporary Redirect',
|
|
128
|
+
308: 'Permanent Redirect',
|
|
129
|
+
400: 'Bad Request',
|
|
130
|
+
401: 'Unauthorized',
|
|
131
|
+
402: 'Payment Required',
|
|
132
|
+
403: 'Forbidden',
|
|
133
|
+
404: 'Not Found',
|
|
134
|
+
405: 'Method Not Allowed',
|
|
135
|
+
406: 'Not Acceptable',
|
|
136
|
+
407: 'Proxy Authentication Required',
|
|
137
|
+
408: 'Request Timeout',
|
|
138
|
+
409: 'Conflict',
|
|
139
|
+
410: 'Gone',
|
|
140
|
+
411: 'Length Required',
|
|
141
|
+
412: 'Precondition Failed',
|
|
142
|
+
413: 'Content Too Large',
|
|
143
|
+
414: 'URI Too Long',
|
|
144
|
+
415: 'Unsupported Media Type',
|
|
145
|
+
416: 'Range Not Satisfiable',
|
|
146
|
+
417: 'Expectation Failed',
|
|
147
|
+
418: 'I\'m a teapot',
|
|
148
|
+
421: 'Misdirected Request',
|
|
149
|
+
422: 'Unprocessable Content',
|
|
150
|
+
423: 'Locked',
|
|
151
|
+
424: 'Failed Dependency',
|
|
152
|
+
425: 'Too Early',
|
|
153
|
+
426: 'Upgrade Required',
|
|
154
|
+
428: 'Precondition Required',
|
|
155
|
+
429: 'Too Many Requests',
|
|
156
|
+
431: 'Request Header Fields Too Large',
|
|
157
|
+
451: 'Unavailable For Legal Reasons',
|
|
158
|
+
500: 'Internal Server Error',
|
|
159
|
+
501: 'Not Implemented',
|
|
160
|
+
502: 'Bad Gateway',
|
|
161
|
+
503: 'Service Unavailable',
|
|
162
|
+
504: 'Gateway Timeout',
|
|
163
|
+
505: 'HTTP Version Not Supported',
|
|
164
|
+
506: 'Variant Also Negotiates',
|
|
165
|
+
507: 'Insufficient Storage',
|
|
166
|
+
508: 'Loop Detected',
|
|
167
|
+
510: 'Not Extended',
|
|
168
|
+
511: 'Network Authentication Required',
|
|
169
|
+
};
|
|
170
|
+
/**
|
|
171
|
+
* Returns the standard HTTP status message for a given status code.
|
|
172
|
+
* @param statusCode The HTTP status code.
|
|
173
|
+
* @returns The standard status message, or 'Unknown' if the code is not recognized.
|
|
174
|
+
*/
|
|
175
|
+
export function getHttpStatusMessage(statusCode) {
|
|
176
|
+
return httpStatusMessages[statusCode] ?? `Unknown (${statusCode})`;
|
|
177
|
+
}
|
package/injector/injector.js
CHANGED
|
@@ -7,6 +7,7 @@ import { DeferredPromise } from '../promise/deferred-promise.js';
|
|
|
7
7
|
import { reflectionRegistry } from '../reflection/registry.js';
|
|
8
8
|
import { toArray } from '../utils/array/array.js';
|
|
9
9
|
import { FactoryMap } from '../utils/factory-map.js';
|
|
10
|
+
import { noop } from '../utils/noop.js';
|
|
10
11
|
import { ForwardRef } from '../utils/object/forward-ref.js';
|
|
11
12
|
import { objectEntries } from '../utils/object/object.js';
|
|
12
13
|
import { assert, isArray, isDefined, isFunction, isNotNull, isNotObject, isNull, isPromise, isUndefined } from '../utils/type-guards.js';
|
|
@@ -597,6 +598,7 @@ function newInternalResolveContext(injector) {
|
|
|
597
598
|
forwardRefs: new Set(),
|
|
598
599
|
$done: new DeferredPromise(),
|
|
599
600
|
};
|
|
601
|
+
void context.$done.catch(noop); // prevent unhandled rejection if no one is awaiting it
|
|
600
602
|
return context;
|
|
601
603
|
}
|
|
602
604
|
function postProcess(context) {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
{
|
|
2
|
-
"id": "
|
|
2
|
+
"id": "0c48afa4-9ab0-4965-a93e-05a6c1b88e58",
|
|
3
3
|
"prevId": "00000000-0000-0000-0000-000000000000",
|
|
4
4
|
"version": "7",
|
|
5
5
|
"dialect": "postgresql",
|
|
@@ -44,37 +44,6 @@
|
|
|
44
44
|
"type": "text[]",
|
|
45
45
|
"primaryKey": false,
|
|
46
46
|
"notNull": false
|
|
47
|
-
},
|
|
48
|
-
"revision": {
|
|
49
|
-
"name": "revision",
|
|
50
|
-
"type": "integer",
|
|
51
|
-
"primaryKey": false,
|
|
52
|
-
"notNull": true
|
|
53
|
-
},
|
|
54
|
-
"revision_timestamp": {
|
|
55
|
-
"name": "revision_timestamp",
|
|
56
|
-
"type": "timestamp with time zone",
|
|
57
|
-
"primaryKey": false,
|
|
58
|
-
"notNull": true
|
|
59
|
-
},
|
|
60
|
-
"create_timestamp": {
|
|
61
|
-
"name": "create_timestamp",
|
|
62
|
-
"type": "timestamp with time zone",
|
|
63
|
-
"primaryKey": false,
|
|
64
|
-
"notNull": true
|
|
65
|
-
},
|
|
66
|
-
"delete_timestamp": {
|
|
67
|
-
"name": "delete_timestamp",
|
|
68
|
-
"type": "timestamp with time zone",
|
|
69
|
-
"primaryKey": false,
|
|
70
|
-
"notNull": false
|
|
71
|
-
},
|
|
72
|
-
"attributes": {
|
|
73
|
-
"name": "attributes",
|
|
74
|
-
"type": "jsonb",
|
|
75
|
-
"primaryKey": false,
|
|
76
|
-
"notNull": true,
|
|
77
|
-
"default": "'{}'::jsonb"
|
|
78
47
|
}
|
|
79
48
|
},
|
|
80
49
|
"indexes": {},
|
|
@@ -5,15 +5,8 @@
|
|
|
5
5
|
{
|
|
6
6
|
"idx": 0,
|
|
7
7
|
"version": "7",
|
|
8
|
-
"when":
|
|
9
|
-
"tag": "
|
|
10
|
-
"breakpoints": true
|
|
11
|
-
},
|
|
12
|
-
{
|
|
13
|
-
"idx": 1,
|
|
14
|
-
"version": "7",
|
|
15
|
-
"when": 1761124592125,
|
|
16
|
-
"tag": "0001_flimsy_bloodscream",
|
|
8
|
+
"when": 1771240070681,
|
|
9
|
+
"tag": "0000_numerous_the_watchers",
|
|
17
10
|
"breakpoints": true
|
|
18
11
|
}
|
|
19
12
|
]
|