@tstdl/base 0.93.139 → 0.93.141
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/README.md +166 -0
- package/ai/genkit/multi-region.plugin.js +5 -3
- package/ai/genkit/tests/multi-region.test.d.ts +1 -0
- package/ai/genkit/tests/multi-region.test.js +5 -2
- package/ai/parser/parser.js +2 -2
- package/ai/prompts/build.js +1 -0
- package/ai/prompts/instructions-formatter.d.ts +15 -2
- package/ai/prompts/instructions-formatter.js +36 -31
- package/ai/prompts/prompt-builder.js +5 -5
- package/ai/prompts/steering.d.ts +3 -2
- package/ai/prompts/steering.js +3 -1
- package/ai/tests/instructions-formatter.test.js +1 -0
- package/api/README.md +403 -0
- package/api/client/client.js +7 -13
- package/api/client/tests/api-client.test.js +10 -10
- package/api/default-error-handlers.js +1 -1
- package/api/response.d.ts +2 -2
- package/api/response.js +22 -33
- package/api/server/api-controller.d.ts +1 -1
- package/api/server/api-controller.js +3 -3
- package/api/server/api-request-token.provider.d.ts +1 -0
- package/api/server/api-request-token.provider.js +1 -0
- package/api/server/middlewares/allowed-methods.middleware.js +2 -1
- package/api/server/middlewares/content-type.middleware.js +2 -1
- package/api/types.d.ts +3 -2
- package/application/README.md +240 -0
- package/application/application.d.ts +1 -1
- package/application/application.js +3 -3
- package/application/providers.d.ts +20 -2
- package/application/providers.js +34 -7
- package/audit/README.md +267 -0
- package/audit/module.d.ts +5 -0
- package/audit/module.js +9 -1
- package/authentication/README.md +288 -0
- package/authentication/client/authentication.service.d.ts +12 -11
- package/authentication/client/authentication.service.js +21 -21
- package/authentication/client/http-client.middleware.js +2 -2
- package/authentication/server/module.d.ts +5 -0
- package/authentication/server/module.js +9 -1
- package/authentication/tests/authentication.api-controller.test.js +1 -1
- package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
- package/authentication/tests/authentication.client-error-handling.test.js +2 -1
- package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
- package/authentication/tests/authentication.client-service.test.js +1 -1
- package/browser/README.md +401 -0
- package/cancellation/README.md +156 -0
- package/cancellation/tests/coverage.test.d.ts +1 -0
- package/cancellation/tests/coverage.test.js +49 -0
- package/cancellation/tests/leak.test.js +24 -29
- package/cancellation/tests/token.test.d.ts +1 -0
- package/cancellation/tests/token.test.js +136 -0
- package/cancellation/token.d.ts +53 -177
- package/cancellation/token.js +132 -208
- package/circuit-breaker/postgres/module.d.ts +1 -0
- package/circuit-breaker/postgres/module.js +5 -1
- package/context/README.md +174 -0
- package/cookie/README.md +161 -0
- package/css/README.md +157 -0
- package/data-structures/README.md +320 -0
- package/decorators/README.md +140 -0
- package/distributed-loop/README.md +231 -0
- package/distributed-loop/distributed-loop.js +1 -1
- package/document-management/README.md +403 -0
- package/document-management/server/configure.js +5 -1
- package/document-management/server/module.d.ts +1 -1
- package/document-management/server/module.js +1 -1
- package/document-management/server/services/document-management-ancillary.service.js +1 -1
- package/document-management/server/services/document-management.service.js +9 -7
- package/document-management/tests/ai-config-hierarchy.test.js +0 -5
- package/document-management/tests/document-management-ai-overrides.test.js +0 -1
- package/document-management/tests/document-management-core.test.js +2 -7
- package/document-management/tests/document-management.api.test.js +6 -7
- package/document-management/tests/document-statistics.service.test.js +11 -12
- package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
- package/document-management/tests/document.service.test.js +3 -3
- package/document-management/tests/enum-helpers.test.js +2 -3
- package/dom/README.md +213 -0
- package/enumerable/README.md +259 -0
- package/enumeration/README.md +121 -0
- package/errors/README.md +267 -0
- package/examples/document-management/main.d.ts +1 -0
- package/examples/document-management/main.js +14 -11
- package/file/README.md +191 -0
- package/formats/README.md +210 -0
- package/function/README.md +144 -0
- package/http/README.md +318 -0
- package/http/client/adapters/undici.adapter.js +1 -1
- package/http/client/http-client-request.d.ts +6 -5
- package/http/client/http-client-request.js +8 -9
- package/http/server/node/node-http-server.js +1 -2
- package/image-service/README.md +137 -0
- package/injector/README.md +491 -0
- package/intl/README.md +113 -0
- package/json-path/README.md +182 -0
- package/jsx/README.md +154 -0
- package/key-value-store/README.md +191 -0
- package/key-value-store/postgres/module.d.ts +1 -0
- package/key-value-store/postgres/module.js +5 -1
- package/lock/README.md +249 -0
- package/lock/postgres/module.d.ts +1 -0
- package/lock/postgres/module.js +5 -1
- package/lock/web/web-lock.js +119 -47
- package/logger/README.md +287 -0
- package/mail/README.md +256 -0
- package/mail/module.d.ts +5 -1
- package/mail/module.js +11 -6
- package/memory/README.md +144 -0
- package/message-bus/README.md +244 -0
- package/message-bus/message-bus-base.js +1 -1
- package/module/README.md +182 -0
- package/module/module.d.ts +1 -1
- package/module/module.js +77 -17
- package/module/modules/web-server.module.js +3 -4
- package/notification/server/module.d.ts +1 -0
- package/notification/server/module.js +5 -1
- package/notification/tests/notification-flow.test.js +2 -2
- package/notification/tests/notification-type.service.test.js +24 -15
- package/object-storage/README.md +300 -0
- package/openid-connect/README.md +274 -0
- package/orm/README.md +423 -0
- package/orm/decorators.d.ts +5 -1
- package/orm/decorators.js +1 -1
- package/orm/server/drizzle/schema-converter.js +17 -30
- package/orm/server/encryption.d.ts +0 -1
- package/orm/server/encryption.js +1 -4
- package/orm/server/index.d.ts +1 -6
- package/orm/server/index.js +1 -6
- package/orm/server/migration.d.ts +19 -0
- package/orm/server/migration.js +72 -0
- package/orm/server/repository.d.ts +1 -1
- package/orm/server/transaction.d.ts +5 -10
- package/orm/server/transaction.js +22 -26
- package/orm/server/transactional.js +3 -3
- package/orm/tests/database-migration.test.d.ts +1 -0
- package/orm/tests/database-migration.test.js +82 -0
- package/orm/tests/encryption.test.js +3 -4
- package/orm/utils.d.ts +17 -2
- package/orm/utils.js +49 -1
- package/package.json +9 -6
- package/password/README.md +164 -0
- package/pdf/README.md +246 -0
- package/polyfills.js +1 -0
- package/pool/README.md +198 -0
- package/process/README.md +237 -0
- package/promise/README.md +252 -0
- package/promise/cancelable-promise.js +1 -1
- package/random/README.md +193 -0
- package/rate-limit/postgres/module.d.ts +1 -0
- package/rate-limit/postgres/module.js +5 -1
- package/reflection/README.md +305 -0
- package/reflection/decorator-data.js +11 -12
- package/rpc/README.md +386 -0
- package/rxjs-utils/README.md +262 -0
- package/schema/README.md +342 -0
- package/serializer/README.md +342 -0
- package/signals/implementation/README.md +134 -0
- package/sse/README.md +278 -0
- package/task-queue/README.md +293 -0
- package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
- package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
- package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
- package/task-queue/postgres/module.d.ts +1 -0
- package/task-queue/postgres/module.js +5 -1
- package/task-queue/postgres/schemas.d.ts +9 -6
- package/task-queue/postgres/schemas.js +4 -3
- package/task-queue/postgres/task-queue.d.ts +4 -13
- package/task-queue/postgres/task-queue.js +462 -355
- package/task-queue/postgres/task.model.d.ts +12 -5
- package/task-queue/postgres/task.model.js +51 -25
- package/task-queue/task-context.d.ts +2 -2
- package/task-queue/task-context.js +8 -8
- package/task-queue/task-queue.d.ts +53 -19
- package/task-queue/task-queue.js +121 -55
- package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
- package/task-queue/tests/cascading-cancellations.test.js +38 -0
- package/task-queue/tests/complex.test.js +45 -229
- package/task-queue/tests/coverage-branch.test.d.ts +1 -0
- package/task-queue/tests/coverage-branch.test.js +407 -0
- package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
- package/task-queue/tests/coverage-enhancement.test.js +144 -0
- package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
- package/task-queue/tests/dag-dependencies.test.js +41 -0
- package/task-queue/tests/dependencies.test.js +28 -26
- package/task-queue/tests/extensive-dependencies.test.js +64 -139
- package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
- package/task-queue/tests/fan-out-spawning.test.js +53 -0
- package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
- package/task-queue/tests/idempotent-replacement.test.js +61 -0
- package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
- package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
- package/task-queue/tests/queue.test.js +128 -8
- package/task-queue/tests/worker.test.js +39 -16
- package/task-queue/tests/zombie-parent.test.d.ts +1 -0
- package/task-queue/tests/zombie-parent.test.js +45 -0
- package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
- package/task-queue/tests/zombie-recovery.test.js +51 -0
- package/templates/README.md +287 -0
- package/test5.js +5 -5
- package/testing/README.md +157 -0
- package/testing/integration-setup.d.ts +4 -4
- package/testing/integration-setup.js +54 -29
- package/text/README.md +346 -0
- package/text/localization.service.js +2 -2
- package/threading/README.md +238 -0
- package/types/README.md +311 -0
- package/utils/README.md +322 -0
- package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
- package/utils/async-iterable-helpers/observable-iterable.js +4 -8
- package/utils/async-iterable-helpers/take-until.js +4 -4
- package/utils/backoff.js +89 -30
- package/utils/file-reader.js +1 -2
- package/utils/retry-with-backoff.js +1 -1
- package/utils/timer.d.ts +1 -1
- package/utils/timer.js +5 -7
- package/utils/timing.d.ts +1 -1
- package/utils/timing.js +2 -4
- package/utils/z-base32.d.ts +1 -0
- package/utils/z-base32.js +1 -0
package/module/module.js
CHANGED
|
@@ -1,3 +1,55 @@
|
|
|
1
|
+
var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
|
|
2
|
+
if (value !== null && value !== void 0) {
|
|
3
|
+
if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
|
|
4
|
+
var dispose, inner;
|
|
5
|
+
if (async) {
|
|
6
|
+
if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
|
|
7
|
+
dispose = value[Symbol.asyncDispose];
|
|
8
|
+
}
|
|
9
|
+
if (dispose === void 0) {
|
|
10
|
+
if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
|
|
11
|
+
dispose = value[Symbol.dispose];
|
|
12
|
+
if (async) inner = dispose;
|
|
13
|
+
}
|
|
14
|
+
if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
|
|
15
|
+
if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
|
|
16
|
+
env.stack.push({ value: value, dispose: dispose, async: async });
|
|
17
|
+
}
|
|
18
|
+
else if (async) {
|
|
19
|
+
env.stack.push({ async: true });
|
|
20
|
+
}
|
|
21
|
+
return value;
|
|
22
|
+
};
|
|
23
|
+
var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
|
|
24
|
+
return function (env) {
|
|
25
|
+
function fail(e) {
|
|
26
|
+
env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
|
|
27
|
+
env.hasError = true;
|
|
28
|
+
}
|
|
29
|
+
var r, s = 0;
|
|
30
|
+
function next() {
|
|
31
|
+
while (r = env.stack.pop()) {
|
|
32
|
+
try {
|
|
33
|
+
if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
|
|
34
|
+
if (r.dispose) {
|
|
35
|
+
var result = r.dispose.call(r.value);
|
|
36
|
+
if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
|
|
37
|
+
}
|
|
38
|
+
else s |= 1;
|
|
39
|
+
}
|
|
40
|
+
catch (e) {
|
|
41
|
+
fail(e);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
|
|
45
|
+
if (env.hasError) throw env.error;
|
|
46
|
+
}
|
|
47
|
+
return next();
|
|
48
|
+
};
|
|
49
|
+
})(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
50
|
+
var e = new Error(message);
|
|
51
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
52
|
+
});
|
|
1
53
|
import { CancellationToken } from '../cancellation/index.js';
|
|
2
54
|
import { defineEnum } from '../enumeration/enumeration.js';
|
|
3
55
|
import { enumValueName } from '../utils/enum.js';
|
|
@@ -8,8 +60,8 @@ export const ModuleState = defineEnum('ModuleState', {
|
|
|
8
60
|
Erroneous: 3,
|
|
9
61
|
});
|
|
10
62
|
export class Module {
|
|
11
|
-
runPromise;
|
|
12
|
-
_state;
|
|
63
|
+
runPromise = Promise.resolve();
|
|
64
|
+
_state = ModuleState.Stopped;
|
|
13
65
|
cancellationToken;
|
|
14
66
|
name;
|
|
15
67
|
get state() {
|
|
@@ -20,27 +72,35 @@ export class Module {
|
|
|
20
72
|
}
|
|
21
73
|
constructor(name) {
|
|
22
74
|
this.name = name;
|
|
23
|
-
this.runPromise = Promise.resolve();
|
|
24
|
-
this._state = ModuleState.Stopped;
|
|
25
|
-
this.cancellationToken = new CancellationToken();
|
|
26
75
|
}
|
|
27
76
|
async [Symbol.asyncDispose]() {
|
|
28
77
|
await this.stop();
|
|
29
78
|
}
|
|
30
79
|
async run() {
|
|
31
|
-
|
|
32
|
-
throw new Error(`cannot start module, it is ${this.stateString}`);
|
|
33
|
-
}
|
|
34
|
-
this.cancellationToken.unset();
|
|
80
|
+
const env_1 = { stack: [], error: void 0, hasError: false };
|
|
35
81
|
try {
|
|
36
|
-
this._state
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
82
|
+
if (this._state != ModuleState.Stopped) {
|
|
83
|
+
throw new Error(`cannot start module, it is ${this.stateString}`);
|
|
84
|
+
}
|
|
85
|
+
const cancellationToken = __addDisposableResource(env_1, new CancellationToken(), false);
|
|
86
|
+
this.cancellationToken = cancellationToken;
|
|
87
|
+
try {
|
|
88
|
+
this._state = ModuleState.Running;
|
|
89
|
+
this.runPromise = this._run(cancellationToken);
|
|
90
|
+
await this.runPromise;
|
|
91
|
+
this._state = ModuleState.Stopped;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
this._state = ModuleState.Erroneous;
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (e_1) {
|
|
99
|
+
env_1.error = e_1;
|
|
100
|
+
env_1.hasError = true;
|
|
101
|
+
}
|
|
102
|
+
finally {
|
|
103
|
+
__disposeResources(env_1);
|
|
44
104
|
}
|
|
45
105
|
}
|
|
46
106
|
async stop() {
|
|
@@ -18,7 +18,7 @@ export class WebServerModuleConfiguration {
|
|
|
18
18
|
}
|
|
19
19
|
;
|
|
20
20
|
let WebServerModule = class WebServerModule extends Module {
|
|
21
|
-
config = inject(WebServerModuleConfiguration);
|
|
21
|
+
config = inject(WebServerModuleConfiguration, undefined, { optional: true });
|
|
22
22
|
httpServer = inject(HttpServer);
|
|
23
23
|
apiGateway = inject(ApiGateway);
|
|
24
24
|
apiControllers = inject(API_CONTROLLERS);
|
|
@@ -37,8 +37,8 @@ let WebServerModule = class WebServerModule extends Module {
|
|
|
37
37
|
}
|
|
38
38
|
async _run(cancellationSignal) {
|
|
39
39
|
this.initialize();
|
|
40
|
-
await this.httpServer.listen(this.config
|
|
41
|
-
const closePromise = cancellationSignal
|
|
40
|
+
await this.httpServer.listen(this.config?.port ?? 8000);
|
|
41
|
+
const closePromise = cancellationSignal.wait().then(async () => {
|
|
42
42
|
await this.httpServer[Symbol.asyncDispose]();
|
|
43
43
|
});
|
|
44
44
|
for await (const context of this.httpServer) {
|
|
@@ -58,5 +58,4 @@ export { WebServerModule };
|
|
|
58
58
|
export function configureWebServerModule({ injector, ...config } = {}) {
|
|
59
59
|
const targetInjector = injector ?? Injector;
|
|
60
60
|
targetInjector.register(WebServerModuleConfiguration, { useValue: config });
|
|
61
|
-
targetInjector.registerSingleton(WebServerModule, { useClass: WebServerModule });
|
|
62
61
|
}
|
|
@@ -11,6 +11,7 @@ export declare class NotificationConfiguration {
|
|
|
11
11
|
* Defaults to 30 days.
|
|
12
12
|
*/
|
|
13
13
|
autoArchiveAfter?: number;
|
|
14
|
+
autoMigrate?: boolean;
|
|
14
15
|
}
|
|
15
16
|
export declare function configureNotification({ injector, ...config }: NotificationConfiguration & {
|
|
16
17
|
injector?: Injector;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { inject, Injector } from '../../injector/index.js';
|
|
2
|
-
import { Database, migrate } from '../../orm/server/index.js';
|
|
2
|
+
import { Database, migrate, registerDatabaseMigration } from '../../orm/server/index.js';
|
|
3
3
|
import { isDefined } from '../../utils/type-guards.js';
|
|
4
4
|
import { NotificationAncillaryService } from './services/notification-ancillary.service.js';
|
|
5
5
|
export class NotificationConfiguration {
|
|
@@ -11,6 +11,7 @@ export class NotificationConfiguration {
|
|
|
11
11
|
* Defaults to 30 days.
|
|
12
12
|
*/
|
|
13
13
|
autoArchiveAfter;
|
|
14
|
+
autoMigrate;
|
|
14
15
|
}
|
|
15
16
|
export function configureNotification({ injector, ...config }) {
|
|
16
17
|
const targetInjector = injector ?? Injector;
|
|
@@ -18,6 +19,9 @@ export function configureNotification({ injector, ...config }) {
|
|
|
18
19
|
if (isDefined(config.ancillaryService)) {
|
|
19
20
|
targetInjector.register(NotificationAncillaryService, { useToken: config.ancillaryService });
|
|
20
21
|
}
|
|
22
|
+
if (config.autoMigrate != false) {
|
|
23
|
+
registerDatabaseMigration('Notification', migrateNotificationSchema, { injector });
|
|
24
|
+
}
|
|
21
25
|
}
|
|
22
26
|
/**
|
|
23
27
|
* Migrates the notification schema.
|
|
@@ -10,7 +10,7 @@ import { runInInjectionContext, Singleton } from '../../injector/index.js';
|
|
|
10
10
|
import { MailService } from '../../mail/mail.service.js';
|
|
11
11
|
import { injectRepository } from '../../orm/server/index.js';
|
|
12
12
|
import { clearTenantData, setupIntegrationTest, truncateTables } from '../../testing/index.js';
|
|
13
|
-
import { InAppNotification, NotificationChannel, NotificationLogEntity, NotificationStatus, WebPushSubscription } from '../models/index.js';
|
|
13
|
+
import { InAppNotification, InAppNotificationArchive, NotificationChannel, NotificationLogEntity, NotificationPreference, NotificationStatus, NotificationType, WebPushSubscription } from '../models/index.js';
|
|
14
14
|
import { configureNotification } from '../server/module.js';
|
|
15
15
|
import { EmailChannelProvider } from '../server/providers/email-channel-provider.js';
|
|
16
16
|
import { NotificationAncillaryService } from '../server/services/notification-ancillary.service.js';
|
|
@@ -63,7 +63,7 @@ describe('Notification Flow (Integration)', () => {
|
|
|
63
63
|
worker.registerProvider(NotificationChannel.InApp, injector.resolve(InAppChannelProvider));
|
|
64
64
|
});
|
|
65
65
|
beforeEach(async () => {
|
|
66
|
-
await truncateTables(database, schema, [
|
|
66
|
+
await truncateTables(database, schema, [NotificationLogEntity, InAppNotification, InAppNotificationArchive, NotificationType, NotificationPreference, WebPushSubscription]);
|
|
67
67
|
await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
|
|
68
68
|
vi.clearAllMocks();
|
|
69
69
|
});
|
|
@@ -1,35 +1,44 @@
|
|
|
1
|
-
import { describe, expect, test } from 'vitest';
|
|
1
|
+
import { afterEach, beforeEach, describe, expect, test } from 'vitest';
|
|
2
2
|
import { runInInjectionContext } from '../../injector/index.js';
|
|
3
3
|
import { setupIntegrationTest, truncateTables } from '../../testing/index.js';
|
|
4
4
|
import { NotificationTypeService } from '../server/services/notification-type.service.js';
|
|
5
5
|
describe('NotificationTypeService', () => {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
6
|
+
let injector;
|
|
7
|
+
let database;
|
|
8
|
+
beforeEach(async () => {
|
|
9
|
+
({ injector, database } = await setupIntegrationTest({ modules: { notification: true, authentication: true } }));
|
|
9
10
|
await truncateTables(database, 'notification', ['type']);
|
|
11
|
+
});
|
|
12
|
+
afterEach(async () => {
|
|
13
|
+
await injector.dispose();
|
|
14
|
+
});
|
|
15
|
+
test('should initialize types correctly', async () => {
|
|
10
16
|
const service = injector.resolve(NotificationTypeService);
|
|
17
|
+
const prefix = crypto.randomUUID();
|
|
18
|
+
const type1 = `${prefix}_TYPE1`;
|
|
19
|
+
const type2 = `${prefix}_TYPE2`;
|
|
11
20
|
const typeData = {
|
|
12
|
-
|
|
13
|
-
|
|
21
|
+
[type1]: { label: 'Type 1' },
|
|
22
|
+
[type2]: { label: 'Type 2', throttling: { limit: 1, interval: 1000 } }
|
|
14
23
|
};
|
|
15
24
|
await runInInjectionContext(injector, async () => {
|
|
16
25
|
const result = await service.initializeTypes(typeData);
|
|
17
|
-
expect(result
|
|
18
|
-
expect(result
|
|
19
|
-
expect(result
|
|
26
|
+
expect(result[type1]?.label).toBe('Type 1');
|
|
27
|
+
expect(result[type2]?.key).toBe(type2);
|
|
28
|
+
expect(result[type2]?.throttling?.limit).toBe(1);
|
|
20
29
|
// Verify persistence
|
|
21
|
-
const dbTypes = await service.repository.
|
|
30
|
+
const dbTypes = await service.repository.loadManyByQuery({ key: { $in: [type1, type2] } });
|
|
22
31
|
expect(dbTypes).toHaveLength(2);
|
|
23
32
|
// Update
|
|
24
33
|
const updatedData = {
|
|
25
|
-
|
|
26
|
-
|
|
34
|
+
[type1]: { label: 'Type 1 Updated' },
|
|
35
|
+
[type2]: { label: 'Type 2', throttling: { limit: 1, interval: 1000 } }
|
|
27
36
|
};
|
|
28
37
|
const resultUpdated = await service.initializeTypes(updatedData);
|
|
29
|
-
expect(resultUpdated
|
|
30
|
-
const dbTypesUpdated = await service.repository.
|
|
38
|
+
expect(resultUpdated[type1]?.label).toBe('Type 1 Updated');
|
|
39
|
+
const dbTypesUpdated = await service.repository.loadManyByQuery({ key: { $in: [type1, type2] } });
|
|
31
40
|
expect(dbTypesUpdated).toHaveLength(2);
|
|
32
|
-
expect(dbTypesUpdated.find((c) => c.key ==
|
|
41
|
+
expect(dbTypesUpdated.find((c) => c.key == type1)?.label).toBe('Type 1 Updated');
|
|
33
42
|
});
|
|
34
43
|
});
|
|
35
44
|
});
|
|
@@ -0,0 +1,300 @@
|
|
|
1
|
+
# @tstdl/base/object-storage
|
|
2
|
+
|
|
3
|
+
A flexible and extensible module for handling object storage, providing a strong abstraction layer over concrete implementations like S3. It simplifies file management with module-based isolation, automatic lifecycle management, and seamless dependency injection integration.
|
|
4
|
+
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [✨ Features](#-features)
|
|
8
|
+
- [Core Concepts](#core-concepts)
|
|
9
|
+
- [🚀 Basic Usage](#-basic-usage)
|
|
10
|
+
- [Configuration](#configuration)
|
|
11
|
+
- [Injecting and Using ObjectStorage](#injecting-and-using-objectstorage)
|
|
12
|
+
- [🔧 Advanced Topics](#-advanced-topics)
|
|
13
|
+
- [Lifecycle Management (Expiration)](#lifecycle-management-expiration)
|
|
14
|
+
- [Pre-signed URLs](#pre-signed-urls)
|
|
15
|
+
- [Streaming I/O](#streaming-io)
|
|
16
|
+
- [Listing Objects](#listing-objects)
|
|
17
|
+
- [Moving and Copying](#moving-and-copying)
|
|
18
|
+
- [📚 API](#-api)
|
|
19
|
+
|
|
20
|
+
## ✨ Features
|
|
21
|
+
|
|
22
|
+
- **Abstract `ObjectStorage` Interface**: Decouples your application logic from specific storage vendors.
|
|
23
|
+
- **S3-Compatible Implementation**: Includes a robust `S3ObjectStorage` implementation working with AWS S3, MinIO, Google Cloud Storage (via S3 interoperability), etc.
|
|
24
|
+
- **Google Cloud Storage Implementation**: Native `GoogleObjectStorage` implementation for direct GCS support.
|
|
25
|
+
- **Module-based Isolation**: Organizes objects into logical "modules". These map to either key prefixes in a shared bucket or separate buckets per module.
|
|
26
|
+
- **Automatic Lifecycle Management**: Configure object expiration policies (e.g., delete temp files after 24h) directly via injection tokens.
|
|
27
|
+
- **Stream Support**: Native support for `ReadableStream` and `Uint8Array` for memory-efficient handling of large files.
|
|
28
|
+
- **Pre-signed URLs**: Generate secure, temporary URLs for direct client-side uploads and downloads.
|
|
29
|
+
- **Dependency Injection**: Designed for `@tstdl/base/injector`, allowing context-aware resolution of storage instances.
|
|
30
|
+
|
|
31
|
+
## Core Concepts
|
|
32
|
+
|
|
33
|
+
### ObjectStorage
|
|
34
|
+
|
|
35
|
+
The central abstract class defining the contract for storage operations (upload, download, delete, exists, etc.). You rarely instantiate this directly; instead, you request it via dependency injection.
|
|
36
|
+
|
|
37
|
+
### Modules
|
|
38
|
+
|
|
39
|
+
A **Module** is a logical namespace for a collection of objects (e.g., `user-avatars`, `invoices`, `temp-uploads`).
|
|
40
|
+
|
|
41
|
+
- **Shared Bucket Mode**: Modules are treated as directory prefixes within a single S3 bucket (e.g., `my-bucket/user-avatars/image.png`).
|
|
42
|
+
- **Bucket-Per-Module Mode**: Each module gets its own dedicated S3 bucket (e.g., bucket `user-avatars` contains `image.png`).
|
|
43
|
+
|
|
44
|
+
### ObjectStorageProvider
|
|
45
|
+
|
|
46
|
+
A factory responsible for creating `ObjectStorage` instances for specific modules. The `S3ObjectStorageProvider` handles the creation of S3 clients and ensures buckets exist.
|
|
47
|
+
|
|
48
|
+
### ObjectStorageObject
|
|
49
|
+
|
|
50
|
+
Represents a stored file. It provides methods to access metadata, size, content, and resource URIs without loading the entire file into memory immediately.
|
|
51
|
+
|
|
52
|
+
## 🚀 Basic Usage
|
|
53
|
+
|
|
54
|
+
### Configuration
|
|
55
|
+
|
|
56
|
+
#### S3 (AWS, MinIO, etc.)
|
|
57
|
+
|
|
58
|
+
Configure the S3 provider at your application's entry point (e.g., `bootstrap.ts`).
|
|
59
|
+
|
|
60
|
+
```typescript
|
|
61
|
+
import { configureS3ObjectStorage } from '@tstdl/base/object-storage';
|
|
62
|
+
|
|
63
|
+
// Option A: Single Shared Bucket (Recommended for most use cases)
|
|
64
|
+
configureS3ObjectStorage({
|
|
65
|
+
endpoint: 'http://localhost:9000', // S3 Endpoint
|
|
66
|
+
accessKey: 'minioadmin',
|
|
67
|
+
secretKey: 'minioadmin',
|
|
68
|
+
bucket: 'my-app-storage', // All modules will be subfolders in this bucket
|
|
69
|
+
forcePathStyle: true, // Required for local s3 server
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// Option B: Bucket Per Module
|
|
73
|
+
// configureS3ObjectStorage({
|
|
74
|
+
// endpoint: '...',
|
|
75
|
+
// accessKey: '...',
|
|
76
|
+
// secretKey: '...',
|
|
77
|
+
// bucketPerModule: true // Each module creates a new bucket
|
|
78
|
+
// });
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
#### Google Cloud Storage
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
import { configureGoogleObjectStorage } from '@tstdl/base/object-storage/google';
|
|
85
|
+
|
|
86
|
+
configureGoogleObjectStorage({
|
|
87
|
+
projectId: 'my-project-id',
|
|
88
|
+
keyFilename: '/path/to/keyfile.json',
|
|
89
|
+
bucket: 'my-app-storage',
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
### Injecting and Using ObjectStorage
|
|
94
|
+
|
|
95
|
+
Inject `ObjectStorage` into your services using the module name as the injection argument.
|
|
96
|
+
|
|
97
|
+
```typescript
|
|
98
|
+
import { inject, Singleton } from '@tstdl/base/injector';
|
|
99
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
100
|
+
|
|
101
|
+
@Singleton()
|
|
102
|
+
export class ProfilePictureService {
|
|
103
|
+
// Inject storage specifically for the 'profile-pictures' module
|
|
104
|
+
readonly #storage = inject(ObjectStorage, 'profile-pictures');
|
|
105
|
+
|
|
106
|
+
async savePicture(userId: string, content: Uint8Array): Promise<void> {
|
|
107
|
+
const key = `${userId}.jpg`;
|
|
108
|
+
|
|
109
|
+
// Upload content
|
|
110
|
+
await this.#storage.uploadObject(key, content, {
|
|
111
|
+
contentType: 'image/jpeg',
|
|
112
|
+
metadata: { userId },
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async getPicture(userId: string): Promise<Uint8Array> {
|
|
117
|
+
const key = `${userId}.jpg`;
|
|
118
|
+
|
|
119
|
+
// Check existence
|
|
120
|
+
if (!(await this.#storage.exists(key))) {
|
|
121
|
+
throw new Error('Picture not found');
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
// Download content
|
|
125
|
+
return this.#storage.getContent(key);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async deletePicture(userId: string): Promise<void> {
|
|
129
|
+
await this.#storage.deleteObject(`${userId}.jpg`);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
## 🔧 Advanced Topics
|
|
135
|
+
|
|
136
|
+
### Lifecycle Management (Expiration)
|
|
137
|
+
|
|
138
|
+
You can configure objects to automatically expire (be deleted) after a certain duration. This is configured when injecting the storage instance. The implementation (e.g., S3) will apply lifecycle rules to the bucket.
|
|
139
|
+
|
|
140
|
+
```typescript
|
|
141
|
+
import { inject, Singleton } from '@tstdl/base/injector';
|
|
142
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
143
|
+
import { secondsPerDay } from '@tstdl/base/utils';
|
|
144
|
+
|
|
145
|
+
@Singleton()
|
|
146
|
+
export class TemporaryUploadService {
|
|
147
|
+
// Objects in 'temp-files' will be deleted 1 day after creation
|
|
148
|
+
readonly #tempStorage = inject(ObjectStorage, {
|
|
149
|
+
module: 'temp-files',
|
|
150
|
+
configuration: {
|
|
151
|
+
lifecycle: {
|
|
152
|
+
expiration: {
|
|
153
|
+
after: 1 * secondsPerDay,
|
|
154
|
+
},
|
|
155
|
+
},
|
|
156
|
+
},
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
async saveTempFile(filename: string, data: Uint8Array): Promise<void> {
|
|
160
|
+
await this.#tempStorage.uploadObject(filename, data);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
### Pre-signed URLs
|
|
166
|
+
|
|
167
|
+
Offload bandwidth from your server by generating pre-signed URLs that allow clients to upload or download directly to/from the storage provider.
|
|
168
|
+
|
|
169
|
+
```typescript
|
|
170
|
+
import { inject, Singleton } from '@tstdl/base/injector';
|
|
171
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
172
|
+
import { millisecondsPerMinute, now } from '@tstdl/base/utils';
|
|
173
|
+
|
|
174
|
+
@Singleton()
|
|
175
|
+
export class DownloadService {
|
|
176
|
+
readonly #storage = inject(ObjectStorage, 'reports');
|
|
177
|
+
|
|
178
|
+
async getDownloadLink(reportId: string): Promise<string> {
|
|
179
|
+
const key = `${reportId}.pdf`;
|
|
180
|
+
const expiration = now().getTime() + 15 * millisecondsPerMinute;
|
|
181
|
+
|
|
182
|
+
// Generate a URL valid for 15 minutes
|
|
183
|
+
return this.#storage.getDownloadUrl(key, expiration, {
|
|
184
|
+
'response-content-disposition': 'attachment; filename="report.pdf"',
|
|
185
|
+
});
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
async getUploadLink(reportId: string): Promise<string> {
|
|
189
|
+
const key = `${reportId}.pdf`;
|
|
190
|
+
const expiration = now().getTime() + 15 * millisecondsPerMinute;
|
|
191
|
+
|
|
192
|
+
// Generate a PUT URL for uploading
|
|
193
|
+
return this.#storage.getUploadUrl(key, expiration, {
|
|
194
|
+
contentType: 'application/pdf',
|
|
195
|
+
contentLength: 1024 * 1024 * 5, // 5MB expected size
|
|
196
|
+
});
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
```
|
|
200
|
+
|
|
201
|
+
### Streaming I/O
|
|
202
|
+
|
|
203
|
+
For large files, use streams to avoid loading the entire file into memory.
|
|
204
|
+
|
|
205
|
+
```typescript
|
|
206
|
+
import { inject } from '@tstdl/base/injector';
|
|
207
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
208
|
+
|
|
209
|
+
class LargeFileService {
|
|
210
|
+
readonly #storage = inject(ObjectStorage, 'backups');
|
|
211
|
+
|
|
212
|
+
async uploadStream(filename: string, stream: ReadableStream<Uint8Array>, size: number): Promise<void> {
|
|
213
|
+
await this.#storage.uploadObject(filename, stream, {
|
|
214
|
+
contentLength: size,
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
async streamDownload(filename: string): Promise<ReadableStream<Uint8Array>> {
|
|
219
|
+
return this.#storage.getContentStream(filename);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
```
|
|
223
|
+
|
|
224
|
+
### Listing Objects
|
|
225
|
+
|
|
226
|
+
Use `getObjectsCursor()` to iterate over objects efficiently using an async iterable.
|
|
227
|
+
|
|
228
|
+
```typescript
|
|
229
|
+
async function listAllFiles(storage: ObjectStorage) {
|
|
230
|
+
for await (const obj of storage.getObjectsCursor()) {
|
|
231
|
+
console.log(`Found file: ${obj.key}, Size: ${await obj.getContentLength()}`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
```
|
|
235
|
+
|
|
236
|
+
### Moving and Copying
|
|
237
|
+
|
|
238
|
+
You can move or copy objects within the same module or across different modules (and buckets).
|
|
239
|
+
|
|
240
|
+
```typescript
|
|
241
|
+
import { inject } from '@tstdl/base/injector';
|
|
242
|
+
import { ObjectStorage } from '@tstdl/base/object-storage';
|
|
243
|
+
|
|
244
|
+
class FileOrganizer {
|
|
245
|
+
readonly #inbox = inject(ObjectStorage, 'inbox');
|
|
246
|
+
readonly #archive = inject(ObjectStorage, 'archive');
|
|
247
|
+
|
|
248
|
+
async archiveFile(filename: string): Promise<void> {
|
|
249
|
+
// Move from 'inbox' module to 'archive' module
|
|
250
|
+
await this.#inbox.moveObject(filename, [this.#archive, `2024/${filename}`]);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
async duplicateInInbox(filename: string): Promise<void> {
|
|
254
|
+
// Copy within the same module
|
|
255
|
+
await this.#inbox.copyObject(filename, `copy_of_${filename}`);
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
```
|
|
259
|
+
|
|
260
|
+
## 📚 API
|
|
261
|
+
|
|
262
|
+
### `ObjectStorage` (Abstract Class)
|
|
263
|
+
|
|
264
|
+
| Method | Description |
|
|
265
|
+
| :--------------------------------------------- | :------------------------------------------------------ |
|
|
266
|
+
| `exists(key: string): Promise<boolean>` | Checks if an object exists. |
|
|
267
|
+
| `uploadObject(key, content, options?)` | Uploads data (`Uint8Array` or `ReadableStream`). |
|
|
268
|
+
| `getContent(key): Promise<Uint8Array>` | Downloads the full object content into memory. |
|
|
269
|
+
| `getContentStream(key): ReadableStream` | Gets a stream of the object content. |
|
|
270
|
+
| `getDownloadUrl(key, expiration, headers?)` | Generates a pre-signed download URL. |
|
|
271
|
+
| `getUploadUrl(key, expiration, options?)` | Generates a pre-signed upload URL. |
|
|
272
|
+
| `deleteObject(key): Promise<void>` | Deletes a single object. |
|
|
273
|
+
| `deleteObjects(keys): Promise<void>` | Deletes multiple objects. |
|
|
274
|
+
| `copyObject(source, dest, options?)` | Copies an object (intra- or inter-module). |
|
|
275
|
+
| `moveObject(source, dest, options?)` | Moves an object (copy + delete). |
|
|
276
|
+
| `getObjects(): Promise<ObjectStorageObject[]>` | Lists all objects in the module. |
|
|
277
|
+
| `getObjectsCursor(): AsyncIterable` | Iterates over objects efficiently. |
|
|
278
|
+
| `getObject(key): Promise<ObjectStorageObject>` | Gets a handle to an object without downloading content. |
|
|
279
|
+
|
|
280
|
+
### `S3ObjectStorageProviderConfig`
|
|
281
|
+
|
|
282
|
+
| Property | Type | Description |
|
|
283
|
+
| :---------------- | :-------- | :----------------------------------------------------------------------------- |
|
|
284
|
+
| `endpoint` | `string` | S3 API endpoint (e.g., `https://s3.amazonaws.com` or `http://localhost:9000`). |
|
|
285
|
+
| `region` | `string` | S3 Region (e.g., `us-east-1`). |
|
|
286
|
+
| `accessKey` | `string` | S3 Access Key ID. |
|
|
287
|
+
| `secretKey` | `string` | S3 Secret Access Key. |
|
|
288
|
+
| `bucket` | `string` | Name of the shared bucket. Mutually exclusive with `bucketPerModule`. |
|
|
289
|
+
| `bucketPerModule` | `boolean` | If true, creates a separate bucket for each module. |
|
|
290
|
+
| `forcePathStyle` | `boolean` | Whether to use path-style addressing. Useful for local s3 server. |
|
|
291
|
+
|
|
292
|
+
### `ObjectStorageObject`
|
|
293
|
+
|
|
294
|
+
| Method | Description |
|
|
295
|
+
| :--------------------------------------- | :----------------------------------------- |
|
|
296
|
+
| `getContentLength(): Promise<number>` | Returns the size of the object in bytes. |
|
|
297
|
+
| `getMetadata(): Promise<ObjectMetadata>` | Returns user-defined metadata. |
|
|
298
|
+
| `getContent(): Promise<Uint8Array>` | Downloads content. |
|
|
299
|
+
| `getContentStream(): ReadableStream` | Streams content. |
|
|
300
|
+
| `getResourceUri(): Promise<string>` | Returns the URI (e.g., `s3://bucket/key`). |
|