@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.
Files changed (218) hide show
  1. package/README.md +166 -0
  2. package/ai/genkit/multi-region.plugin.js +5 -3
  3. package/ai/genkit/tests/multi-region.test.d.ts +1 -0
  4. package/ai/genkit/tests/multi-region.test.js +5 -2
  5. package/ai/parser/parser.js +2 -2
  6. package/ai/prompts/build.js +1 -0
  7. package/ai/prompts/instructions-formatter.d.ts +15 -2
  8. package/ai/prompts/instructions-formatter.js +36 -31
  9. package/ai/prompts/prompt-builder.js +5 -5
  10. package/ai/prompts/steering.d.ts +3 -2
  11. package/ai/prompts/steering.js +3 -1
  12. package/ai/tests/instructions-formatter.test.js +1 -0
  13. package/api/README.md +403 -0
  14. package/api/client/client.js +7 -13
  15. package/api/client/tests/api-client.test.js +10 -10
  16. package/api/default-error-handlers.js +1 -1
  17. package/api/response.d.ts +2 -2
  18. package/api/response.js +22 -33
  19. package/api/server/api-controller.d.ts +1 -1
  20. package/api/server/api-controller.js +3 -3
  21. package/api/server/api-request-token.provider.d.ts +1 -0
  22. package/api/server/api-request-token.provider.js +1 -0
  23. package/api/server/middlewares/allowed-methods.middleware.js +2 -1
  24. package/api/server/middlewares/content-type.middleware.js +2 -1
  25. package/api/types.d.ts +3 -2
  26. package/application/README.md +240 -0
  27. package/application/application.d.ts +1 -1
  28. package/application/application.js +3 -3
  29. package/application/providers.d.ts +20 -2
  30. package/application/providers.js +34 -7
  31. package/audit/README.md +267 -0
  32. package/audit/module.d.ts +5 -0
  33. package/audit/module.js +9 -1
  34. package/authentication/README.md +288 -0
  35. package/authentication/client/authentication.service.d.ts +12 -11
  36. package/authentication/client/authentication.service.js +21 -21
  37. package/authentication/client/http-client.middleware.js +2 -2
  38. package/authentication/server/module.d.ts +5 -0
  39. package/authentication/server/module.js +9 -1
  40. package/authentication/tests/authentication.api-controller.test.js +1 -1
  41. package/authentication/tests/authentication.api-request-token.provider.test.js +1 -1
  42. package/authentication/tests/authentication.client-error-handling.test.js +2 -1
  43. package/authentication/tests/authentication.client-service-refresh.test.js +5 -3
  44. package/authentication/tests/authentication.client-service.test.js +1 -1
  45. package/browser/README.md +401 -0
  46. package/cancellation/README.md +156 -0
  47. package/cancellation/tests/coverage.test.d.ts +1 -0
  48. package/cancellation/tests/coverage.test.js +49 -0
  49. package/cancellation/tests/leak.test.js +24 -29
  50. package/cancellation/tests/token.test.d.ts +1 -0
  51. package/cancellation/tests/token.test.js +136 -0
  52. package/cancellation/token.d.ts +53 -177
  53. package/cancellation/token.js +132 -208
  54. package/circuit-breaker/postgres/module.d.ts +1 -0
  55. package/circuit-breaker/postgres/module.js +5 -1
  56. package/context/README.md +174 -0
  57. package/cookie/README.md +161 -0
  58. package/css/README.md +157 -0
  59. package/data-structures/README.md +320 -0
  60. package/decorators/README.md +140 -0
  61. package/distributed-loop/README.md +231 -0
  62. package/distributed-loop/distributed-loop.js +1 -1
  63. package/document-management/README.md +403 -0
  64. package/document-management/server/configure.js +5 -1
  65. package/document-management/server/module.d.ts +1 -1
  66. package/document-management/server/module.js +1 -1
  67. package/document-management/server/services/document-management-ancillary.service.js +1 -1
  68. package/document-management/server/services/document-management.service.js +9 -7
  69. package/document-management/tests/ai-config-hierarchy.test.js +0 -5
  70. package/document-management/tests/document-management-ai-overrides.test.js +0 -1
  71. package/document-management/tests/document-management-core.test.js +2 -7
  72. package/document-management/tests/document-management.api.test.js +6 -7
  73. package/document-management/tests/document-statistics.service.test.js +11 -12
  74. package/document-management/tests/document-validation-ai-overrides.test.js +0 -1
  75. package/document-management/tests/document.service.test.js +3 -3
  76. package/document-management/tests/enum-helpers.test.js +2 -3
  77. package/dom/README.md +213 -0
  78. package/enumerable/README.md +259 -0
  79. package/enumeration/README.md +121 -0
  80. package/errors/README.md +267 -0
  81. package/examples/document-management/main.d.ts +1 -0
  82. package/examples/document-management/main.js +14 -11
  83. package/file/README.md +191 -0
  84. package/formats/README.md +210 -0
  85. package/function/README.md +144 -0
  86. package/http/README.md +318 -0
  87. package/http/client/adapters/undici.adapter.js +1 -1
  88. package/http/client/http-client-request.d.ts +6 -5
  89. package/http/client/http-client-request.js +8 -9
  90. package/http/server/node/node-http-server.js +1 -2
  91. package/image-service/README.md +137 -0
  92. package/injector/README.md +491 -0
  93. package/intl/README.md +113 -0
  94. package/json-path/README.md +182 -0
  95. package/jsx/README.md +154 -0
  96. package/key-value-store/README.md +191 -0
  97. package/key-value-store/postgres/module.d.ts +1 -0
  98. package/key-value-store/postgres/module.js +5 -1
  99. package/lock/README.md +249 -0
  100. package/lock/postgres/module.d.ts +1 -0
  101. package/lock/postgres/module.js +5 -1
  102. package/lock/web/web-lock.js +119 -47
  103. package/logger/README.md +287 -0
  104. package/mail/README.md +256 -0
  105. package/mail/module.d.ts +5 -1
  106. package/mail/module.js +11 -6
  107. package/memory/README.md +144 -0
  108. package/message-bus/README.md +244 -0
  109. package/message-bus/message-bus-base.js +1 -1
  110. package/module/README.md +182 -0
  111. package/module/module.d.ts +1 -1
  112. package/module/module.js +77 -17
  113. package/module/modules/web-server.module.js +3 -4
  114. package/notification/server/module.d.ts +1 -0
  115. package/notification/server/module.js +5 -1
  116. package/notification/tests/notification-flow.test.js +2 -2
  117. package/notification/tests/notification-type.service.test.js +24 -15
  118. package/object-storage/README.md +300 -0
  119. package/openid-connect/README.md +274 -0
  120. package/orm/README.md +423 -0
  121. package/orm/decorators.d.ts +5 -1
  122. package/orm/decorators.js +1 -1
  123. package/orm/server/drizzle/schema-converter.js +17 -30
  124. package/orm/server/encryption.d.ts +0 -1
  125. package/orm/server/encryption.js +1 -4
  126. package/orm/server/index.d.ts +1 -6
  127. package/orm/server/index.js +1 -6
  128. package/orm/server/migration.d.ts +19 -0
  129. package/orm/server/migration.js +72 -0
  130. package/orm/server/repository.d.ts +1 -1
  131. package/orm/server/transaction.d.ts +5 -10
  132. package/orm/server/transaction.js +22 -26
  133. package/orm/server/transactional.js +3 -3
  134. package/orm/tests/database-migration.test.d.ts +1 -0
  135. package/orm/tests/database-migration.test.js +82 -0
  136. package/orm/tests/encryption.test.js +3 -4
  137. package/orm/utils.d.ts +17 -2
  138. package/orm/utils.js +49 -1
  139. package/package.json +9 -6
  140. package/password/README.md +164 -0
  141. package/pdf/README.md +246 -0
  142. package/polyfills.js +1 -0
  143. package/pool/README.md +198 -0
  144. package/process/README.md +237 -0
  145. package/promise/README.md +252 -0
  146. package/promise/cancelable-promise.js +1 -1
  147. package/random/README.md +193 -0
  148. package/rate-limit/postgres/module.d.ts +1 -0
  149. package/rate-limit/postgres/module.js +5 -1
  150. package/reflection/README.md +305 -0
  151. package/reflection/decorator-data.js +11 -12
  152. package/rpc/README.md +386 -0
  153. package/rxjs-utils/README.md +262 -0
  154. package/schema/README.md +342 -0
  155. package/serializer/README.md +342 -0
  156. package/signals/implementation/README.md +134 -0
  157. package/sse/README.md +278 -0
  158. package/task-queue/README.md +293 -0
  159. package/task-queue/postgres/drizzle/{0000_simple_invisible_woman.sql → 0000_wakeful_sunspot.sql} +22 -14
  160. package/task-queue/postgres/drizzle/meta/0000_snapshot.json +160 -82
  161. package/task-queue/postgres/drizzle/meta/_journal.json +2 -2
  162. package/task-queue/postgres/module.d.ts +1 -0
  163. package/task-queue/postgres/module.js +5 -1
  164. package/task-queue/postgres/schemas.d.ts +9 -6
  165. package/task-queue/postgres/schemas.js +4 -3
  166. package/task-queue/postgres/task-queue.d.ts +4 -13
  167. package/task-queue/postgres/task-queue.js +462 -355
  168. package/task-queue/postgres/task.model.d.ts +12 -5
  169. package/task-queue/postgres/task.model.js +51 -25
  170. package/task-queue/task-context.d.ts +2 -2
  171. package/task-queue/task-context.js +8 -8
  172. package/task-queue/task-queue.d.ts +53 -19
  173. package/task-queue/task-queue.js +121 -55
  174. package/task-queue/tests/cascading-cancellations.test.d.ts +1 -0
  175. package/task-queue/tests/cascading-cancellations.test.js +38 -0
  176. package/task-queue/tests/complex.test.js +45 -229
  177. package/task-queue/tests/coverage-branch.test.d.ts +1 -0
  178. package/task-queue/tests/coverage-branch.test.js +407 -0
  179. package/task-queue/tests/coverage-enhancement.test.d.ts +1 -0
  180. package/task-queue/tests/coverage-enhancement.test.js +144 -0
  181. package/task-queue/tests/dag-dependencies.test.d.ts +1 -0
  182. package/task-queue/tests/dag-dependencies.test.js +41 -0
  183. package/task-queue/tests/dependencies.test.js +28 -26
  184. package/task-queue/tests/extensive-dependencies.test.js +64 -139
  185. package/task-queue/tests/fan-out-spawning.test.d.ts +1 -0
  186. package/task-queue/tests/fan-out-spawning.test.js +53 -0
  187. package/task-queue/tests/idempotent-replacement.test.d.ts +1 -0
  188. package/task-queue/tests/idempotent-replacement.test.js +61 -0
  189. package/task-queue/tests/missing-idempotent-tasks.test.d.ts +1 -0
  190. package/task-queue/tests/missing-idempotent-tasks.test.js +38 -0
  191. package/task-queue/tests/queue.test.js +128 -8
  192. package/task-queue/tests/worker.test.js +39 -16
  193. package/task-queue/tests/zombie-parent.test.d.ts +1 -0
  194. package/task-queue/tests/zombie-parent.test.js +45 -0
  195. package/task-queue/tests/zombie-recovery.test.d.ts +1 -0
  196. package/task-queue/tests/zombie-recovery.test.js +51 -0
  197. package/templates/README.md +287 -0
  198. package/test5.js +5 -5
  199. package/testing/README.md +157 -0
  200. package/testing/integration-setup.d.ts +4 -4
  201. package/testing/integration-setup.js +54 -29
  202. package/text/README.md +346 -0
  203. package/text/localization.service.js +2 -2
  204. package/threading/README.md +238 -0
  205. package/types/README.md +311 -0
  206. package/utils/README.md +322 -0
  207. package/utils/async-iterable-helpers/observable-iterable.d.ts +1 -1
  208. package/utils/async-iterable-helpers/observable-iterable.js +4 -8
  209. package/utils/async-iterable-helpers/take-until.js +4 -4
  210. package/utils/backoff.js +89 -30
  211. package/utils/file-reader.js +1 -2
  212. package/utils/retry-with-backoff.js +1 -1
  213. package/utils/timer.d.ts +1 -1
  214. package/utils/timer.js +5 -7
  215. package/utils/timing.d.ts +1 -1
  216. package/utils/timing.js +2 -4
  217. package/utils/z-base32.d.ts +1 -0
  218. 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
- if (this._state != ModuleState.Stopped) {
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 = ModuleState.Running;
37
- this.runPromise = this._run(this.cancellationToken);
38
- await this.runPromise;
39
- this._state = ModuleState.Stopped;
40
- }
41
- catch (error) {
42
- this._state = ModuleState.Erroneous;
43
- throw error;
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.port ?? 8000);
41
- const closePromise = cancellationSignal.$set.then(async () => {
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, ['log', 'in_app', 'in_app_archive', 'type', 'preference', 'web_push_subscription']);
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
- test('should initialize types correctly', async () => {
7
- const { injector, database } = await setupIntegrationTest({ modules: { notification: true, authentication: true } });
8
- // Cleanup
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
- TYPE1: { label: 'Type 1' },
13
- TYPE2: { label: 'Type 2', throttling: { limit: 1, interval: 1000 } }
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.TYPE1.label).toBe('Type 1');
18
- expect(result.TYPE2.key).toBe('TYPE2');
19
- expect(result.TYPE2.throttling?.limit).toBe(1);
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.loadAll();
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
- TYPE1: { label: 'Type 1 Updated' },
26
- TYPE2: { label: 'Type 2', throttling: { limit: 1, interval: 1000 } }
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.TYPE1.label).toBe('Type 1 Updated');
30
- const dbTypesUpdated = await service.repository.loadAll();
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 == 'TYPE1')?.label).toBe('Type 1 Updated');
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`). |