@tstdl/base 0.93.174 → 0.93.176

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 (50) hide show
  1. package/api/server/middlewares/response-time.middleware.js +2 -5
  2. package/audit/auditor.js +1 -1
  3. package/authentication/client/authentication.service.js +13 -0
  4. package/authentication/server/authentication.service.d.ts +1 -1
  5. package/authentication/server/authentication.service.js +3 -3
  6. package/authentication/tests/authentication.client-service-refresh.test.js +19 -0
  7. package/authentication/tests/authentication.refresh-busy-loop.test.d.ts +1 -0
  8. package/authentication/tests/authentication.refresh-busy-loop.test.js +84 -0
  9. package/circuit-breaker/postgres/circuit-breaker.js +1 -1
  10. package/document-management/server/services/singleton.js +1 -1
  11. package/errors/format.d.ts +6 -1
  12. package/errors/format.js +1 -1
  13. package/http/index.d.ts +1 -0
  14. package/http/index.js +1 -0
  15. package/http/server/http-server-response.d.ts +2 -0
  16. package/http/server/http-server-response.js +2 -0
  17. package/http/server/node/node-http-server.js +3 -0
  18. package/http/server-timing.d.ts +31 -0
  19. package/http/server-timing.js +47 -0
  20. package/http/tests/server-timing.test.d.ts +1 -0
  21. package/http/tests/server-timing.test.js +42 -0
  22. package/key-value-store/postgres/key-value-store.service.js +1 -1
  23. package/lock/postgres/provider.js +1 -1
  24. package/mail/README.md +1 -1
  25. package/mail/drizzle/0001_married_tarantula.sql +10 -0
  26. package/mail/drizzle/meta/0001_snapshot.json +69 -0
  27. package/mail/drizzle/meta/_journal.json +7 -0
  28. package/mail/index.d.ts +1 -0
  29. package/mail/index.js +1 -0
  30. package/mail/mail.service.d.ts +21 -4
  31. package/mail/mail.service.js +44 -13
  32. package/mail/models/mail-log.model.d.ts +2 -1
  33. package/mail/models/mail-log.model.js +1 -1
  34. package/mail/task-definitions.d.ts +20 -0
  35. package/mail/task-definitions.js +1 -0
  36. package/package.json +3 -3
  37. package/rate-limit/postgres/postgres-rate-limiter.js +1 -1
  38. package/task-queue/postgres/drizzle/0001_rapid_infant_terrible.sql +16 -0
  39. package/task-queue/postgres/drizzle/meta/0001_snapshot.json +753 -0
  40. package/task-queue/postgres/drizzle/meta/_journal.json +7 -0
  41. package/task-queue/postgres/task-queue.js +13 -13
  42. package/task-queue/postgres/task.model.d.ts +2 -1
  43. package/task-queue/postgres/task.model.js +3 -3
  44. package/task-queue/task-queue.d.ts +4 -3
  45. package/task-queue/task-queue.js +12 -2
  46. package/task-queue/tests/coverage-branch.test.js +1 -1
  47. package/task-queue/tests/dependencies.test.js +2 -2
  48. package/task-queue/tests/queue.test.js +2 -2
  49. package/task-queue/tests/worker.test.js +39 -5
  50. package/tsconfig.json +0 -2
@@ -1,8 +1,5 @@
1
- import { round } from '../../../utils/math.js';
2
- import { Timer } from '../../../utils/timer.js';
3
1
  export async function responseTimeMiddleware({ response }, next) {
4
- const timer = Timer.startNew();
2
+ const stop = response.serverTiming.start('total');
5
3
  await next();
6
- const time = round(timer.milliseconds, 2);
7
- response.headers.set('X-Response-Time', `${time}ms`);
4
+ stop();
8
5
  }
package/audit/auditor.js CHANGED
@@ -235,7 +235,7 @@ __decorate([
235
235
  Auditor = Auditor_1 = __decorate([
236
236
  Injectable({
237
237
  providers: [
238
- { provide: DatabaseConfig, useFactory: (_, context) => context.resolve(AuditModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: 2 }) },
238
+ { provide: DatabaseConfig, useFactory: (_, context) => context.resolve(AuditModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: true }) },
239
239
  ],
240
240
  })
241
241
  ], Auditor);
@@ -421,6 +421,7 @@ let AuthenticationClientService = class AuthenticationClientService {
421
421
  // 2. Handle token refresh
422
422
  if (needsRefresh) {
423
423
  const lockResult = await this.lock.tryUse(undefined, async () => {
424
+ this.loadToken();
424
425
  const currentToken = this.token();
425
426
  if (isUndefined(currentToken)) {
426
427
  return;
@@ -442,6 +443,18 @@ let AuthenticationClientService = class AuthenticationClientService {
442
443
  this.forceRefreshRequested.set(false);
443
444
  }
444
445
  }
446
+ // Protection against tight loops (e.g. if server clock is ahead and sync failed)
447
+ const newToken = this.token();
448
+ if (isDefined(newToken)) {
449
+ const newNow = this.estimatedServerTimestampSeconds();
450
+ const newBuffer = calculateRefreshBufferSeconds(newToken);
451
+ const stillNeedsRefresh = this.forceRefreshRequested() || (newNow >= newToken.exp - newBuffer);
452
+ if (stillNeedsRefresh) {
453
+ this.logger.warn('Token still needs refresh after attempt. Waiting for minRefreshDelay to avoid tight loop.');
454
+ await waitForNextAction(minRefreshDelay, newToken.exp);
455
+ }
456
+ }
457
+ await waitForNextAction(100, newToken?.exp);
445
458
  continue; // Re-evaluate the loop with the newly refreshed (or synced) token
446
459
  }
447
460
  // 3. Calculate delay and sleep until the next scheduled refresh window
@@ -57,7 +57,7 @@ export declare class AuthenticationServiceOptions {
57
57
  /**
58
58
  * How long a refresh token is valid in milliseconds. Implies session time to live.
59
59
  *
60
- * @default 1 hour
60
+ * @default 6 hours
61
61
  */
62
62
  refreshTokenTimeToLive?: number;
63
63
  /**
@@ -55,7 +55,7 @@ export class AuthenticationServiceOptions {
55
55
  /**
56
56
  * How long a refresh token is valid in milliseconds. Implies session time to live.
57
57
  *
58
- * @default 1 hour
58
+ * @default 6 hours
59
59
  */
60
60
  refreshTokenTimeToLive;
61
61
  /**
@@ -126,7 +126,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
126
126
  };
127
127
  tokenVersion = this.#options.version ?? 1;
128
128
  tokenTimeToLive = this.#options.tokenTimeToLive ?? (5 * millisecondsPerMinute);
129
- refreshTokenTimeToLive = this.#options.refreshTokenTimeToLive ?? (1 * millisecondsPerHour);
129
+ refreshTokenTimeToLive = this.#options.refreshTokenTimeToLive ?? (6 * millisecondsPerHour);
130
130
  rememberRefreshTokenTimeToLive = this.#options.rememberRefreshTokenTimeToLive ?? (30 * millisecondsPerDay);
131
131
  secretResetTokenTimeToLive = this.#options.secretResetTokenTimeToLive ?? (10 * millisecondsPerMinute);
132
132
  derivedTokenSigningSecret;
@@ -787,7 +787,7 @@ let AuthenticationService = AuthenticationService_1 = class AuthenticationServic
787
787
  AuthenticationService = AuthenticationService_1 = __decorate([
788
788
  Singleton({
789
789
  providers: [
790
- provide(DatabaseConfig, { useFactory: (_, context) => context.resolve(AuthenticationModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: 2 }) }),
790
+ provide(DatabaseConfig, { useFactory: (_, context) => context.resolve(AuthenticationModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: true }) }),
791
791
  ],
792
792
  })
793
793
  ], AuthenticationService);
@@ -130,4 +130,23 @@ describe('AuthenticationClientService Refresh Loop Reproduction', () => {
130
130
  await service.refresh();
131
131
  expect(mockApiClient.timestamp).toHaveBeenCalled();
132
132
  });
133
+ test('Cross-tab Sync: should not refresh if another tab already did (simulated via localStorage update)', async () => {
134
+ const now = Math.floor(Date.now() / 1000);
135
+ const initialToken = { iat: now - 3600, exp: now + 5, jti: 'initial' }; // Expiring soon
136
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(initialToken));
137
+ // 1. Mock lock behavior to simulate another tab refreshing while we wait for the lock
138
+ mockLock.tryUse.mockImplementation(async (_timeout, callback) => {
139
+ // Simulate another tab refreshing and updating localStorage
140
+ const newToken = { iat: now, exp: now + 3600, jti: 'refreshed-by-other-tab' };
141
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(newToken));
142
+ const result = await callback({ lost: false });
143
+ return { success: true, result };
144
+ });
145
+ service = injector.resolve(AuthenticationClientService);
146
+ // Wait for loop to run
147
+ await timeout(100);
148
+ // Should NOT have called refresh because loadToken() inside the lock updated the token
149
+ expect(mockApiClient.refresh).not.toHaveBeenCalled();
150
+ expect(service.token()?.jti).toBe('refreshed-by-other-tab');
151
+ });
133
152
  });
@@ -0,0 +1,84 @@
1
+ import { afterEach, beforeEach, describe, expect, test, vi } from 'vitest';
2
+ import { AuthenticationClientService } from '../../authentication/client/authentication.service.js';
3
+ import { AUTHENTICATION_API_CLIENT } from '../../authentication/client/tokens.js';
4
+ import { CancellationSignal, CancellationToken } from '../../cancellation/token.js';
5
+ import { Injector } from '../../injector/index.js';
6
+ import { Lock } from '../../lock/index.js';
7
+ import { Logger } from '../../logger/index.js';
8
+ import { MessageBus } from '../../message-bus/index.js';
9
+ import { configureDefaultSignalsImplementation } from '../../signals/implementation/configure.js';
10
+ import { timeout } from '../../utils/timing.js';
11
+ import { Subject } from 'rxjs';
12
+ describe('AuthenticationClientService Refresh Busy Loop Prevention', () => {
13
+ let injector;
14
+ let service;
15
+ let mockApiClient;
16
+ let mockLock;
17
+ let mockMessageBus;
18
+ let mockLogger;
19
+ let disposeToken;
20
+ beforeEach(() => {
21
+ const storage = new Map();
22
+ globalThis.localStorage = {
23
+ getItem: vi.fn((key) => storage.get(key) ?? null),
24
+ setItem: vi.fn((key, value) => storage.set(key, value)),
25
+ removeItem: vi.fn((key) => storage.delete(key)),
26
+ clear: vi.fn(() => storage.clear()),
27
+ };
28
+ configureDefaultSignalsImplementation();
29
+ injector = new Injector('Test');
30
+ mockApiClient = {
31
+ login: vi.fn(),
32
+ refresh: vi.fn(),
33
+ timestamp: vi.fn().mockResolvedValue(1000), // Fixed timestamp
34
+ endSession: vi.fn().mockResolvedValue(undefined),
35
+ };
36
+ mockLock = {
37
+ tryUse: vi.fn(async (_timeout, callback) => {
38
+ const result = await callback({ lost: false });
39
+ return { success: true, result };
40
+ }),
41
+ use: vi.fn(async (_timeout, callback) => {
42
+ return await callback({ lost: false });
43
+ }),
44
+ };
45
+ mockMessageBus = {
46
+ publishAndForget: vi.fn(),
47
+ messages$: new Subject(),
48
+ dispose: vi.fn(),
49
+ };
50
+ mockLogger = {
51
+ error: vi.fn(),
52
+ warn: vi.fn(),
53
+ info: vi.fn(),
54
+ debug: vi.fn(),
55
+ };
56
+ injector.register(AUTHENTICATION_API_CLIENT, { useValue: mockApiClient });
57
+ injector.register(Lock, { useValue: mockLock });
58
+ injector.register(MessageBus, { useValue: mockMessageBus });
59
+ injector.register(Logger, { useValue: mockLogger });
60
+ disposeToken = new CancellationToken();
61
+ injector.register(CancellationSignal, { useValue: disposeToken.signal });
62
+ });
63
+ afterEach(async () => {
64
+ disposeToken.set();
65
+ await injector.dispose();
66
+ });
67
+ test('should not busy loop when refresh fails to produce a valid token', async () => {
68
+ // 1. Mock a token that is ALWAYS expired
69
+ // Server time is 1000. Token expires at 500.
70
+ const expiredToken = { iat: 0, exp: 500, jti: 'expired', session: 's', tenant: 't', subject: 'sub' };
71
+ globalThis.localStorage.setItem('AuthenticationService:token', JSON.stringify(expiredToken));
72
+ // refresh() will return the SAME expired token
73
+ mockApiClient.refresh.mockResolvedValue(expiredToken);
74
+ // 2. Start service
75
+ service = injector.resolve(AuthenticationClientService);
76
+ // 3. Wait a bit
77
+ // With 100ms mandatory yield + minRefreshDelay (1 min) protection, it should only try once or twice in 500ms
78
+ // WITHOUT protection, it would try thousands of times.
79
+ await timeout(500);
80
+ // It should have tried to refresh
81
+ expect(mockApiClient.refresh.mock.calls.length).toBeLessThan(5);
82
+ expect(mockLogger.warn).toHaveBeenCalledWith(expect.stringContaining('Token still needs refresh after attempt'));
83
+ });
84
+ });
@@ -93,7 +93,7 @@ PostgresCircuitBreakerService = __decorate([
93
93
  Singleton({
94
94
  argumentIdentityProvider: (arg) => isString(arg) ? arg : arg.key,
95
95
  providers: [
96
- provide(DatabaseConfig, { useFactory: (_, context) => context.resolve(PostgresCircuitBreakerModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: 2 }) }),
96
+ provide(DatabaseConfig, { useFactory: (_, context) => context.resolve(PostgresCircuitBreakerModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: true }) }),
97
97
  ],
98
98
  })
99
99
  ], PostgresCircuitBreakerService);
@@ -3,7 +3,7 @@ import { provide } from '../../../injector/injector.js';
3
3
  import { factoryProvider } from '../../../injector/provider.js';
4
4
  import { DatabaseConfig } from '../../../orm/server/index.js';
5
5
  import { DocumentManagementConfiguration } from '../module.js';
6
- export const documentManagementDatabaseConfigFactoryProvider = factoryProvider((_, context) => context.resolve(DocumentManagementConfiguration).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: 2 }));
6
+ export const documentManagementDatabaseConfigFactoryProvider = factoryProvider((_, context) => context.resolve(DocumentManagementConfiguration).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: true }));
7
7
  export const documentManagementDatabaseConfigProvider = provide(DatabaseConfig, documentManagementDatabaseConfigFactoryProvider);
8
8
  export function DocumentManagementSingleton() {
9
9
  return Singleton({ providers: [documentManagementDatabaseConfigProvider] });
@@ -1,4 +1,4 @@
1
- import type { Record, UndefinableJson } from '../types/index.js';
1
+ import type { ObjectLiteral, Record, UndefinableJson } from '../types/index.js';
2
2
  export type FormatErrorOptions = {
3
3
  /** Include error name in message */
4
4
  includeName?: boolean;
@@ -20,6 +20,11 @@ export type SerializedError = {
20
20
  cause?: SerializedError;
21
21
  aggregateErrors?: SerializedError[];
22
22
  };
23
+ export type NonRecursiveSerializedError = Omit<SerializedError, 'extraInfo' | 'cause' | 'aggregateErrors'> & {
24
+ extraInfo?: ObjectLiteral | null | undefined;
25
+ cause?: ObjectLiteral;
26
+ aggregateErrors?: ObjectLiteral[];
27
+ };
23
28
  export interface ErrorExtraInfo {
24
29
  /** Format extra data (without message and stack) as JSON */
25
30
  getExtraInfo(): UndefinableJson | undefined;
package/errors/format.js CHANGED
@@ -29,7 +29,7 @@ export function serializeError(error, options = {}) {
29
29
  aggregateErrors = actualError.errors;
30
30
  }
31
31
  // Extract Extra Info
32
- if (includeExtraInfo && isFunction(actualError.getExtraInfo)) {
32
+ if (includeExtraInfo && isFunction(actualError?.getExtraInfo)) {
33
33
  extraInfo = actualError.getExtraInfo();
34
34
  }
35
35
  // Isolate 'rest' properties via destructuring
package/http/index.d.ts CHANGED
@@ -12,6 +12,7 @@ export * from './http-query.js';
12
12
  export * from './http-url-parameters.js';
13
13
  export * from './http-value-map.js';
14
14
  export * from './http.error.js';
15
+ export * from './server-timing.js';
15
16
  export * from './server/index.js';
16
17
  export * from './tokens.js';
17
18
  export * from './types.js';
package/http/index.js CHANGED
@@ -12,6 +12,7 @@ export * from './http-query.js';
12
12
  export * from './http-url-parameters.js';
13
13
  export * from './http-value-map.js';
14
14
  export * from './http.error.js';
15
+ export * from './server-timing.js';
15
16
  export * from './server/index.js';
16
17
  export * from './tokens.js';
17
18
  export * from './types.js';
@@ -3,6 +3,7 @@ import type { ServerSentEventsSource } from '../../sse/server-sent-events-source
3
3
  import type { Record } from '../../types/index.js';
4
4
  import type { HttpHeadersInput } from '../http-headers.js';
5
5
  import { HttpHeaders } from '../http-headers.js';
6
+ import { ServerTiming } from '../server-timing.js';
6
7
  export type SetCookieObject = SetCookieOptions & {
7
8
  value: string;
8
9
  };
@@ -24,6 +25,7 @@ export type HttpServerResponseOptions = {
24
25
  export declare class HttpServerResponse {
25
26
  #private;
26
27
  readonly headers: HttpHeaders;
28
+ readonly serverTiming: ServerTiming;
27
29
  statusCode: number | undefined;
28
30
  statusMessage: string | undefined;
29
31
  get body(): HttpServerResponseBody | undefined;
@@ -2,10 +2,12 @@ import { formatSetCookie } from '../../cookie/cookie.js';
2
2
  import { objectEntries } from '../../utils/object/object.js';
3
3
  import { isDefined, isUndefined } from '../../utils/type-guards.js';
4
4
  import { HttpHeaders } from '../http-headers.js';
5
+ import { ServerTiming } from '../server-timing.js';
5
6
  export class HttpServerResponse {
6
7
  #body;
7
8
  #bodyType;
8
9
  headers = new HttpHeaders();
10
+ serverTiming = new ServerTiming();
9
11
  statusCode;
10
12
  statusMessage;
11
13
  get body() {
@@ -141,6 +141,9 @@ function getResponder(httpResponse) {
141
141
  return respond;
142
142
  }
143
143
  function writeHeaders(response, httpResponse) {
144
+ if (response.serverTiming.hasMetrics) {
145
+ response.headers.set('Server-Timing', response.serverTiming.toString());
146
+ }
144
147
  for (const [name, value] of response.headers.normalizedEntries()) {
145
148
  httpResponse.setHeader(name, value);
146
149
  }
@@ -0,0 +1,31 @@
1
+ export type ServerTimingMetric = {
2
+ name: string;
3
+ duration?: number;
4
+ description?: string;
5
+ };
6
+ export declare class ServerTiming {
7
+ #private;
8
+ /**
9
+ * Whether there are any metrics.
10
+ */
11
+ get hasMetrics(): boolean;
12
+ /**
13
+ * Add a metric to the server timing.
14
+ * @param name The name of the metric.
15
+ * @param duration The duration of the metric in milliseconds.
16
+ * @param description An optional description for the metric.
17
+ */
18
+ add(name: string, duration?: number, description?: string): void;
19
+ /**
20
+ * Start a timer for a metric.
21
+ * @param name The name of the metric.
22
+ * @param description An optional description for the metric.
23
+ * @returns A function to stop the timer and add the metric.
24
+ */
25
+ start(name: string, description?: string): () => void;
26
+ /**
27
+ * Format the metrics as a `Server-Timing` header value.
28
+ * @returns Comma-separated list of metrics.
29
+ */
30
+ toString(): string;
31
+ }
@@ -0,0 +1,47 @@
1
+ import { round } from '../utils/math.js';
2
+ import { Timer } from '../utils/timer.js';
3
+ import { isDefined } from '../utils/type-guards.js';
4
+ export class ServerTiming {
5
+ #metrics = [];
6
+ /**
7
+ * Whether there are any metrics.
8
+ */
9
+ get hasMetrics() {
10
+ return this.#metrics.length > 0;
11
+ }
12
+ /**
13
+ * Add a metric to the server timing.
14
+ * @param name The name of the metric.
15
+ * @param duration The duration of the metric in milliseconds.
16
+ * @param description An optional description for the metric.
17
+ */
18
+ add(name, duration, description) {
19
+ this.#metrics.push({ name, duration, description });
20
+ }
21
+ /**
22
+ * Start a timer for a metric.
23
+ * @param name The name of the metric.
24
+ * @param description An optional description for the metric.
25
+ * @returns A function to stop the timer and add the metric.
26
+ */
27
+ start(name, description) {
28
+ const timer = Timer.startNew();
29
+ return () => this.add(name, round(timer.milliseconds, 2), description);
30
+ }
31
+ /**
32
+ * Format the metrics as a `Server-Timing` header value.
33
+ * @returns Comma-separated list of metrics.
34
+ */
35
+ toString() {
36
+ return this.#metrics.map((metric) => {
37
+ let result = metric.name;
38
+ if (isDefined(metric.duration)) {
39
+ result += `;dur=${metric.duration}`;
40
+ }
41
+ if (isDefined(metric.description)) {
42
+ result += `;desc="${metric.description}"`;
43
+ }
44
+ return result;
45
+ }).join(', ');
46
+ }
47
+ }
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1,42 @@
1
+ import { describe, expect, test } from 'vitest';
2
+ import { ServerTiming } from '../server-timing.js';
3
+ describe('ServerTiming', () => {
4
+ test('should be empty by default', () => {
5
+ const timing = new ServerTiming();
6
+ expect(timing.hasMetrics).toBe(false);
7
+ expect(timing.toString()).toBe('');
8
+ });
9
+ test('should add a simple metric', () => {
10
+ const timing = new ServerTiming();
11
+ timing.add('db');
12
+ expect(timing.hasMetrics).toBe(true);
13
+ expect(timing.toString()).toBe('db');
14
+ });
15
+ test('should add a metric with duration', () => {
16
+ const timing = new ServerTiming();
17
+ timing.add('db', 53.2);
18
+ expect(timing.toString()).toBe('db;dur=53.2');
19
+ });
20
+ test('should add a metric with duration and description', () => {
21
+ const timing = new ServerTiming();
22
+ timing.add('db', 53.2, 'Database Query');
23
+ expect(timing.toString()).toBe('db;dur=53.2;desc="Database Query"');
24
+ });
25
+ test('should add multiple metrics', () => {
26
+ const timing = new ServerTiming();
27
+ timing.add('cache', undefined, 'Cache Miss');
28
+ timing.add('db', 53.2);
29
+ timing.add('total', 123.45);
30
+ expect(timing.toString()).toBe('cache;desc="Cache Miss", db;dur=53.2, total;dur=123.45');
31
+ });
32
+ test('should use start/stop for metrics', async () => {
33
+ const timing = new ServerTiming();
34
+ const stop = timing.start('total');
35
+ await new Promise((resolve) => setTimeout(resolve, 11));
36
+ stop();
37
+ const result = timing.toString();
38
+ expect(result).toMatch(/total;dur=\d+\.\d+/);
39
+ const duration = Number.parseFloat(result.split('=')[1]);
40
+ expect(duration).toBeGreaterThanOrEqual(10);
41
+ });
42
+ });
@@ -56,7 +56,7 @@ let PostgresKeyValueStore = class PostgresKeyValueStore extends Transactional {
56
56
  PostgresKeyValueStore = __decorate([
57
57
  Singleton({
58
58
  providers: [
59
- { provide: DatabaseConfig, useFactory: (_, context) => context.resolve(PostgresKeyValueStoreModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: 2 }) },
59
+ { provide: DatabaseConfig, useFactory: (_, context) => context.resolve(PostgresKeyValueStoreModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: true }) },
60
60
  ],
61
61
  })
62
62
  ], PostgresKeyValueStore);
@@ -20,7 +20,7 @@ let PostgresLockProvider = class PostgresLockProvider extends LockProvider {
20
20
  PostgresLockProvider = __decorate([
21
21
  Singleton({
22
22
  providers: [
23
- { provide: DatabaseConfig, useFactory: (_, context) => context.resolve(PostgresLockModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: 2 }) },
23
+ { provide: DatabaseConfig, useFactory: (_, context) => context.resolve(PostgresLockModuleConfig).database ?? context.resolve(DatabaseConfig, undefined, { skipSelf: true }) },
24
24
  ],
25
25
  })
26
26
  ], PostgresLockProvider);
package/mail/README.md CHANGED
@@ -203,7 +203,7 @@ export class MarketingService {
203
203
  subject: 'Weekly Newsletter',
204
204
  content: { text: '...' },
205
205
  },
206
- marketingConfig, // Override config
206
+ { clientConfig: marketingConfig },
207
207
  );
208
208
  }
209
209
  }
@@ -0,0 +1,10 @@
1
+ ALTER TABLE "mail"."log" ALTER COLUMN "errors" SET DATA TYPE jsonb USING (
2
+ CASE
3
+ WHEN "errors" IS NULL THEN '[]'::jsonb
4
+ ELSE (
5
+ SELECT COALESCE(jsonb_agg(jsonb_build_object('message', err)), '[]'::jsonb)
6
+ FROM unnest("errors") AS err
7
+ )
8
+ END
9
+ );--> statement-breakpoint
10
+ ALTER TABLE "mail"."log" ALTER COLUMN "errors" SET NOT NULL;
@@ -0,0 +1,69 @@
1
+ {
2
+ "id": "c74f667f-8acf-42e7-ad58-3aea45df3743",
3
+ "prevId": "0c48afa4-9ab0-4965-a93e-05a6c1b88e58",
4
+ "version": "7",
5
+ "dialect": "postgresql",
6
+ "tables": {
7
+ "mail.log": {
8
+ "name": "log",
9
+ "schema": "mail",
10
+ "columns": {
11
+ "id": {
12
+ "name": "id",
13
+ "type": "uuid",
14
+ "primaryKey": true,
15
+ "notNull": true,
16
+ "default": "gen_random_uuid()"
17
+ },
18
+ "timestamp": {
19
+ "name": "timestamp",
20
+ "type": "timestamp with time zone",
21
+ "primaryKey": false,
22
+ "notNull": true
23
+ },
24
+ "template": {
25
+ "name": "template",
26
+ "type": "text",
27
+ "primaryKey": false,
28
+ "notNull": false
29
+ },
30
+ "data": {
31
+ "name": "data",
32
+ "type": "jsonb",
33
+ "primaryKey": false,
34
+ "notNull": true
35
+ },
36
+ "send_result": {
37
+ "name": "send_result",
38
+ "type": "jsonb",
39
+ "primaryKey": false,
40
+ "notNull": false
41
+ },
42
+ "errors": {
43
+ "name": "errors",
44
+ "type": "jsonb",
45
+ "primaryKey": false,
46
+ "notNull": true
47
+ }
48
+ },
49
+ "indexes": {},
50
+ "foreignKeys": {},
51
+ "compositePrimaryKeys": {},
52
+ "uniqueConstraints": {},
53
+ "policies": {},
54
+ "checkConstraints": {},
55
+ "isRLSEnabled": false
56
+ }
57
+ },
58
+ "enums": {},
59
+ "schemas": {},
60
+ "sequences": {},
61
+ "roles": {},
62
+ "policies": {},
63
+ "views": {},
64
+ "_meta": {
65
+ "columns": {},
66
+ "schemas": {},
67
+ "tables": {}
68
+ }
69
+ }
@@ -8,6 +8,13 @@
8
8
  "when": 1771240070681,
9
9
  "tag": "0000_numerous_the_watchers",
10
10
  "breakpoints": true
11
+ },
12
+ {
13
+ "idx": 1,
14
+ "version": "7",
15
+ "when": 1774305325041,
16
+ "tag": "0001_married_tarantula",
17
+ "breakpoints": true
11
18
  }
12
19
  ]
13
20
  }
package/mail/index.d.ts CHANGED
@@ -8,4 +8,5 @@ export * from './mail.client.js';
8
8
  export * from './mail.service.js';
9
9
  export * from './models/index.js';
10
10
  export * from './module.js';
11
+ export * from './task-definitions.js';
11
12
  export * from './tokens.js';
package/mail/index.js CHANGED
@@ -8,4 +8,5 @@ export * from './mail.client.js';
8
8
  export * from './mail.service.js';
9
9
  export * from './models/index.js';
10
10
  export * from './module.js';
11
+ export * from './task-definitions.js';
11
12
  export * from './tokens.js';
@@ -1,10 +1,27 @@
1
+ import type { AfterResolveContext } from '../injector/index.js';
2
+ import { afterResolve } from '../injector/index.js';
3
+ import type { Transaction } from '../orm/server/transaction.js';
1
4
  import type { TypedOmit } from '../types/index.js';
2
5
  import { MailClientConfig } from './mail.client.js';
3
6
  import { type MailData, type MailSendResult, type MailTemplate } from './models/index.js';
7
+ export type MailSendOptions = {
8
+ clientConfig?: MailClientConfig;
9
+ /** @internal */
10
+ templateName?: string;
11
+ };
4
12
  export declare class MailService {
5
13
  #private;
6
- send(mailData: MailData, clientConfig?: MailClientConfig): Promise<MailSendResult>;
7
- /** @deprecated internal */
8
- send(mailData: MailData, clientConfig?: MailClientConfig, templateName?: string): Promise<MailSendResult>;
9
- sendTemplate<Context extends object>(keyOrTemplate: string | MailTemplate<Context>, mailData: TypedOmit<MailData, 'content' | 'subject'>, templateContext?: Context, clientConfig?: MailClientConfig): Promise<MailSendResult>;
14
+ [afterResolve](_: unknown, { cancellationSignal }: AfterResolveContext): void;
15
+ send(mailData: MailData, options?: MailSendOptions): Promise<MailSendResult>;
16
+ sendTemplate<Context extends object>(keyOrTemplate: string | MailTemplate<Context>, mailData: TypedOmit<MailData, 'content' | 'subject'>, templateContext?: Context, options?: MailSendOptions): Promise<MailSendResult>;
17
+ enqueue(mailData: MailData, options?: {
18
+ transaction?: Transaction;
19
+ priority?: number;
20
+ delay?: number;
21
+ }): Promise<void>;
22
+ enqueueTemplate<Context extends object>(keyOrTemplate: string | MailTemplate<Context>, mailData: TypedOmit<MailData, 'content' | 'subject'>, templateContext?: Context, options?: {
23
+ transaction?: Transaction;
24
+ priority?: number;
25
+ delay?: number;
26
+ }): Promise<void>;
10
27
  }