@tstdl/base 0.93.179 → 0.93.181

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.
@@ -12,8 +12,9 @@ import { buildJsonb } from '../orm/index.js';
12
12
  import { DatabaseConfig, injectRepository } from '../orm/server/index.js';
13
13
  import { TaskProcessResult, TaskQueue } from '../task-queue/task-queue.js';
14
14
  import { TemplateService } from '../templates/template.service.js';
15
+ import { toArray } from '../utils/array/array.js';
15
16
  import { currentTimestamp } from '../utils/date-time.js';
16
- import { assertDefined } from '../utils/type-guards.js';
17
+ import { assertDefined, isDefined } from '../utils/type-guards.js';
17
18
  import { MailClient, MailClientConfig } from './mail.client.js';
18
19
  import { MailLog } from './models/index.js';
19
20
  import { mailLog as mailLogTable } from './models/schemas.js';
@@ -24,6 +25,7 @@ let MailService = class MailService {
24
25
  #templateService = inject(TemplateService);
25
26
  #mailLogRepository = injectRepository(MailLog);
26
27
  #taskQueue = inject((TaskQueue), 'mail');
28
+ #moduleConfig = inject(MailModuleConfig, undefined, { optional: true });
27
29
  #defaultClientConfig = inject(MailClientConfig, undefined, { optional: true });
28
30
  #defaultData = inject(MAIL_DEFAULT_DATA, undefined, { optional: true });
29
31
  #logger = inject(Logger, 'MailService');
@@ -42,6 +44,7 @@ let MailService = class MailService {
42
44
  const config = options?.clientConfig ?? this.#defaultClientConfig;
43
45
  assertDefined(config, 'No mail client config provided.');
44
46
  const data = { ...this.#defaultData, ...mailData };
47
+ const rewriteTo = this.#moduleConfig?.rewriteTo;
45
48
  let mailLog;
46
49
  mailLog = await this.#mailLogRepository.insert({
47
50
  timestamp: currentTimestamp(),
@@ -50,8 +53,24 @@ let MailService = class MailService {
50
53
  sendResult: null,
51
54
  errors: [],
52
55
  });
56
+ const finalMailData = isDefined(rewriteTo)
57
+ ? {
58
+ ...data,
59
+ to: rewriteTo,
60
+ cc: undefined,
61
+ bcc: undefined,
62
+ headers: {
63
+ ...data.headers,
64
+ 'X-Original-To': [
65
+ ...toArray(data.to ?? []),
66
+ ...toArray(data.cc ?? []),
67
+ ...toArray(data.bcc ?? []),
68
+ ].map((address) => (typeof address == 'string' ? address : `${address.name} <${address.address}>`)).join(', '),
69
+ },
70
+ }
71
+ : data;
53
72
  try {
54
- const result = await this.#mailClient.send(data, config);
73
+ const result = await this.#mailClient.send(finalMailData, config);
55
74
  await this.#mailLogRepository.update(mailLog.id, { sendResult: result });
56
75
  return result;
57
76
  }
package/mail/module.d.ts CHANGED
@@ -9,6 +9,7 @@ export declare class MailModuleConfig {
9
9
  client?: Type<MailClient>;
10
10
  defaultData?: DefaultMailData;
11
11
  autoMigrate?: boolean;
12
+ rewriteTo?: string;
12
13
  }
13
14
  /**
14
15
  * configure mail module
package/mail/module.js CHANGED
@@ -10,6 +10,7 @@ export class MailModuleConfig {
10
10
  client;
11
11
  defaultData;
12
12
  autoMigrate;
13
+ rewriteTo;
13
14
  }
14
15
  /**
15
16
  * configure mail module
@@ -93,10 +93,10 @@ export declare const notificationApiDefinition: {
93
93
  }>;
94
94
  credentials: true;
95
95
  };
96
- updatePreference: {
96
+ updatePreferences: {
97
97
  resource: string;
98
98
  method: "POST";
99
- parameters: import("../../schema/index.js").ObjectSchema<{
99
+ parameters: import("../../schema/index.js").ArraySchema<{
100
100
  type: string;
101
101
  enabled: boolean;
102
102
  channel: "email" | "in-app" | "web-push";
@@ -210,10 +210,10 @@ declare const _NotificationApiClient: import("../../api/client/index.js").ApiCli
210
210
  }>;
211
211
  credentials: true;
212
212
  };
213
- updatePreference: {
213
+ updatePreferences: {
214
214
  resource: string;
215
215
  method: "POST";
216
- parameters: import("../../schema/index.js").ObjectSchema<{
216
+ parameters: import("../../schema/index.js").ArraySchema<{
217
217
  type: string;
218
218
  enabled: boolean;
219
219
  channel: "email" | "in-app" | "web-push";
@@ -96,14 +96,14 @@ export const notificationApiDefinition = defineApi({
96
96
  })),
97
97
  credentials: true,
98
98
  },
99
- updatePreference: {
99
+ updatePreferences: {
100
100
  resource: 'preferences',
101
101
  method: 'POST',
102
- parameters: object({
102
+ parameters: array(object({
103
103
  type: string(),
104
104
  channel: enumeration(NotificationChannel),
105
105
  enabled: boolean(),
106
- }),
106
+ })),
107
107
  result: literal('ok'),
108
108
  credentials: true,
109
109
  },
@@ -17,6 +17,6 @@ export declare class NotificationApiController implements ApiController<Notifica
17
17
  archiveAll({ getToken }: ApiRequestContext<NotificationApiDefinition, 'archiveAll'>): Promise<'ok'>;
18
18
  unreadCount({ getToken }: ApiRequestContext<NotificationApiDefinition, 'unreadCount'>): Promise<number>;
19
19
  getPreferences({ getToken }: ApiRequestContext<NotificationApiDefinition, 'getPreferences'>): Promise<any>;
20
- updatePreference({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'updatePreference'>): Promise<'ok'>;
20
+ updatePreferences({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'updatePreferences'>): Promise<'ok'>;
21
21
  registerWebPush({ parameters, getToken }: ApiRequestContext<NotificationApiDefinition, 'registerWebPush'>): Promise<'ok'>;
22
22
  }
@@ -85,9 +85,9 @@ let NotificationApiController = class NotificationApiController {
85
85
  const token = await getToken();
86
86
  return await this.notificationService.getPreferences(token.payload.tenant, token.payload.subject);
87
87
  }
88
- async updatePreference({ parameters, getToken }) {
88
+ async updatePreferences({ parameters, getToken }) {
89
89
  const token = await getToken();
90
- await this.notificationService.updatePreference(token.payload.tenant, token.payload.subject, parameters.type, parameters.channel, parameters.enabled);
90
+ await this.notificationService.updatePreferences(token.payload.tenant, token.payload.subject, parameters);
91
91
  return 'ok';
92
92
  }
93
93
  async registerWebPush({ parameters, getToken }) {
@@ -34,5 +34,10 @@ export declare class NotificationService<Definitions extends NotificationDefinit
34
34
  }>;
35
35
  getPreferences(tenantId: string, userId: string): Promise<NotificationPreference[]>;
36
36
  updatePreference(tenantId: string, userId: string, type: string, channel: NotificationChannel, enabled: boolean): Promise<void>;
37
+ updatePreferences(tenantId: string, userId: string, preferences: {
38
+ type: string;
39
+ channel: NotificationChannel;
40
+ enabled: boolean;
41
+ }[]): Promise<void>;
37
42
  registerWebPush(tenantId: string, userId: string, endpoint: string, p256dh: Uint8Array<ArrayBuffer>, auth: Uint8Array<ArrayBuffer>): Promise<void>;
38
43
  }
@@ -235,13 +235,11 @@ let NotificationService = NotificationService_1 = class NotificationService exte
235
235
  return await this.#preferenceRepository.loadManyByQuery({ tenantId, userId });
236
236
  }
237
237
  async updatePreference(tenantId, userId, type, channel, enabled) {
238
- await this.#preferenceRepository.upsert(['tenantId', 'userId', 'type', 'channel'], {
239
- tenantId,
240
- userId,
241
- type,
242
- channel,
243
- enabled,
244
- });
238
+ await this.#preferenceRepository.upsert(['tenantId', 'userId', 'type', 'channel'], { tenantId, userId, type, channel, enabled });
239
+ }
240
+ async updatePreferences(tenantId, userId, preferences) {
241
+ const entities = preferences.map((preference) => ({ tenantId, userId, ...preference }));
242
+ await this.#preferenceRepository.upsertMany(['tenantId', 'userId', 'type', 'channel'], entities);
245
243
  }
246
244
  async registerWebPush(tenantId, userId, endpoint, p256dh, auth) {
247
245
  try {
@@ -27,11 +27,14 @@ describe('Notification API (Integration)', () => {
27
27
  rateLimiter: true,
28
28
  },
29
29
  }));
30
- await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
31
30
  controller = injector.resolve(NotificationApiController);
32
31
  notificationService = injector.resolve(NotificationService);
33
32
  sseService = injector.resolve(NotificationSseService);
34
33
  subjectService = injector.resolve(SubjectService);
34
+ });
35
+ beforeEach(async () => {
36
+ vi.clearAllMocks();
37
+ await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
35
38
  // Create a dummy user
36
39
  const user = await subjectService.createUser({
37
40
  tenantId,
@@ -41,9 +44,6 @@ describe('Notification API (Integration)', () => {
41
44
  });
42
45
  userId = user.id;
43
46
  });
44
- beforeEach(() => {
45
- vi.clearAllMocks();
46
- });
47
47
  afterEach(async () => {
48
48
  await clearTenantData(database, schema, ['in_app', 'in_app_archive', 'log', 'preference', 'web_push_subscription'], tenantId);
49
49
  await clearTenantData(database, 'authentication', ['user', 'subject'], tenantId);
@@ -109,11 +109,11 @@ describe('Notification API (Integration)', () => {
109
109
  await controller.getPreferences(createMockContext());
110
110
  expect(getPreferencesSpy).toHaveBeenCalledWith(tenantId, userId);
111
111
  });
112
- test('updatePreference should call service', async () => {
113
- const updatePreferenceSpy = vi.spyOn(notificationService, 'updatePreference').mockResolvedValue();
114
- const params = { type: 'test', channel: NotificationChannel.Email, enabled: true };
115
- await controller.updatePreference(createMockContext(params));
116
- expect(updatePreferenceSpy).toHaveBeenCalledWith(tenantId, userId, 'test', NotificationChannel.Email, true);
112
+ test('updatePreferences should call service', async () => {
113
+ const updatePreferencesSpy = vi.spyOn(notificationService, 'updatePreferences').mockResolvedValue();
114
+ const params = [{ type: 'test', channel: NotificationChannel.Email, enabled: true }];
115
+ await controller.updatePreferences(createMockContext(params));
116
+ expect(updatePreferencesSpy).toHaveBeenCalledWith(tenantId, userId, params);
117
117
  });
118
118
  test('registerWebPush should call service', async () => {
119
119
  const registerWebPushSpy = vi.spyOn(notificationService, 'registerWebPush').mockResolvedValue();
@@ -25,6 +25,5 @@ export type OrmModuleOptions = {
25
25
  * @param options - Configuration options including connection details, repository settings, and the encryption secret.
26
26
  */
27
27
  export declare function configureOrm({ injector, ...options }?: OrmModuleOptions & {
28
- encryptionSecret?: Uint8Array;
29
28
  injector?: Injector;
30
29
  }): void;
@@ -1,7 +1,6 @@
1
1
  import { Injector } from '../../injector/injector.js';
2
2
  import { isDefined } from '../../utils/type-guards.js';
3
3
  import { EntityRepositoryConfig } from './repository-config.js';
4
- import { ENCRYPTION_SECRET } from './tokens.js';
5
4
  /**
6
5
  * Configuration class for the database connection.
7
6
  */
@@ -20,7 +19,4 @@ export function configureOrm({ injector, ...options } = {}) {
20
19
  if (isDefined(options.repositoryConfig)) {
21
20
  targetInjector.register(EntityRepositoryConfig, { useValue: options.repositoryConfig });
22
21
  }
23
- if (isDefined(options.encryptionSecret)) {
24
- targetInjector.register(ENCRYPTION_SECRET, { useValue: options.encryptionSecret });
25
- }
26
22
  }
@@ -1,6 +1,7 @@
1
1
  /** biome-ignore-all lint/nursery/noExcessiveClassesPerFile: <explanation> */
2
2
  import { SQL, type SQLWrapper } from 'drizzle-orm';
3
3
  import type { AnyPgTable, PgColumn, PgInsertValue, PgSelectBuilder, PgUpdateSetSource, SelectedFields } from 'drizzle-orm/pg-core';
4
+ import { type DerivedKey } from '../../cryptography/index.js';
4
5
  import { afterResolve, resolveArgumentType, type Resolvable } from '../../injector/interfaces.js';
5
6
  import type { DeepPartial, Function, OneOrMany, Record, SimplifyObject, Type, TypedOmit } from '../../types/index.js';
6
7
  import { Entity, type BaseEntity, type EntityMetadataAttributes, type EntityType } from '../entity.js';
@@ -16,7 +17,7 @@ type EntityRepositoryContext = {
16
17
  table: PgTableFromType;
17
18
  columnDefinitions: ColumnDefinition[];
18
19
  columnDefinitionsMap: Map<string, ColumnDefinition>;
19
- encryptionSecret: Uint8Array<ArrayBuffer> | undefined;
20
+ encryptionKey: DerivedKey | undefined;
20
21
  transformContext: TransformContext | Promise<TransformContext> | undefined;
21
22
  };
22
23
  export type InferSelect<T extends BaseEntity = BaseEntity> = PgTableFromType<EntityType<T>>['$inferSelect'];
@@ -8,7 +8,7 @@ var __decorate = (this && this.__decorate) || function (decorators, target, key,
8
8
  import { and, asc, count, desc, eq, getTableName, inArray, isNotNull as isSqlNotNull, isNull as isSqlNull, isSQLWrapper, lte, or, SQL, sql } from 'drizzle-orm';
9
9
  import { match, P } from 'ts-pattern';
10
10
  import { CancellationSignal } from '../../cancellation/token.js';
11
- import { importSymmetricKey } from '../../cryptography/index.js';
11
+ import { injectDerivedCryptoKey } from '../../cryptography/index.js';
12
12
  import { NotFoundError } from '../../errors/not-found.error.js';
13
13
  import { Singleton } from '../../injector/decorators.js';
14
14
  import { inject, injectArgument } from '../../injector/inject.js';
@@ -30,7 +30,6 @@ import { getInheritanceMetadata, isChildEntity } from '../utils.js';
30
30
  import { getColumnDefinitions, getColumnDefinitionsMap, getDrizzleTableFromType, getPrimaryKeyColumnDefinitions, getPrimaryKeyColumns, getTableColumnDefinitions, isTableOwning } from './drizzle/schema-converter.js';
31
31
  import { convertQuery, getTsQuery, getTsVector, resolveTargetColumn } from './query-converter.js';
32
32
  import { EntityRepositoryConfig } from './repository-config.js';
33
- import { ENCRYPTION_SECRET } from './tokens.js';
34
33
  import { injectTransactional, injectTransactionalAsync, isInTransactionalContext, Transactional, tryGetTransactionalContextData } from './transactional.js';
35
34
  const searchScoreColumn = '__tsl_score';
36
35
  const searchDistanceColumn = '__tsl_distance';
@@ -40,7 +39,7 @@ export const repositoryType = Symbol('repositoryType');
40
39
  const entityTypeToken = Symbol('EntityType');
41
40
  let EntityRepository = class EntityRepository extends Transactional {
42
41
  #context = (isInTransactionalContext() ? tryGetTransactionalContextData(this) : undefined) ?? {};
43
- #encryptionSecret = isInTransactionalContext() ? this.#context.encryptionSecret : inject(ENCRYPTION_SECRET, undefined, { optional: true });
42
+ #encryptionKey = isInTransactionalContext() ? this.#context.encryptionKey : injectDerivedCryptoKey('orm:repository-encryption', { name: 'AES-GCM', length: 256 }, ['encrypt', 'decrypt']);
44
43
  #cancellationSignal = isInTransactionalContext() ? undefined : inject(CancellationSignal);
45
44
  #transformContext = this.#context.transformContext;
46
45
  type = assertDefinedPass(this.#context.type ?? this.constructor[entityTypeToken] ?? injectArgument(this, { optional: true }), 'Missing entity type.');
@@ -137,7 +136,7 @@ let EntityRepository = class EntityRepository extends Transactional {
137
136
  table: this.#table,
138
137
  columnDefinitions: this.#columnDefinitions,
139
138
  columnDefinitionsMap: this.#columnDefinitionsMap,
140
- encryptionSecret: this.#encryptionSecret,
139
+ encryptionKey: this.#encryptionKey,
141
140
  transformContext: this.#transformContext,
142
141
  };
143
142
  return context;
@@ -1745,11 +1744,11 @@ let EntityRepository = class EntityRepository extends Transactional {
1745
1744
  }
1746
1745
  async getTransformContext() {
1747
1746
  if (isUndefined(this.#transformContext)) {
1748
- if (isUndefined(this.#encryptionSecret)) {
1747
+ if (isUndefined(this.#encryptionKey)) {
1749
1748
  this.#transformContext = {};
1750
1749
  return this.#transformContext;
1751
1750
  }
1752
- this.#transformContext = importSymmetricKey('raw', { name: 'AES-GCM', length: 256 }, this.#encryptionSecret, false).then((encryptionKey) => ({ encryptionKey }));
1751
+ this.#transformContext = this.#encryptionKey.getKey().then((encryptionKey) => ({ encryptionKey }));
1753
1752
  this.#transformContext = await this.#transformContext;
1754
1753
  }
1755
1754
  return this.#transformContext; // eslint-disable-line @typescript-eslint/return-await
@@ -17,7 +17,6 @@ import { toArrayAsync } from '../../utils/async-iterable-helpers/to-array.js';
17
17
  import { ChildEntity, Column, Inheritance, Table } from '../decorators.js';
18
18
  import { BaseEntity, Entity } from '../entity.js';
19
19
  import { getRepository } from '../server/repository.js';
20
- import { ENCRYPTION_SECRET } from '../server/tokens.js';
21
20
  describe('ORM Repository Extra Coverage', () => {
22
21
  let injector;
23
22
  let database;
@@ -80,7 +79,6 @@ describe('ORM Repository Extra Coverage', () => {
80
79
  ], PlainItem);
81
80
  beforeAll(async () => {
82
81
  ({ injector, database } = await setupIntegrationTest({ orm: { schema } }));
83
- injector.register(ENCRYPTION_SECRET, { useValue: new Uint8Array(32).fill(1) });
84
82
  baseItemRepo = injector.resolve(getRepository(BaseItem));
85
83
  premiumItemRepo = injector.resolve(getRepository(PremiumItem));
86
84
  simpleItemRepo = injector.resolve(getRepository(SimpleItem));
@@ -17,7 +17,6 @@ import { Column, EmbeddedProperty, EncryptedProperty, Reference, Table } from '.
17
17
  import { Entity } from '../entity.js';
18
18
  import { JsonProperty, NumericDateProperty } from '../schemas/index.js';
19
19
  import { getRepository } from '../server/index.js';
20
- import { ENCRYPTION_SECRET } from '../server/tokens.js';
21
20
  describe('ORM Repository Regression (Integration)', () => {
22
21
  let injector;
23
22
  let db;
@@ -101,11 +100,9 @@ describe('ORM Repository Regression (Integration)', () => {
101
100
  Table('posts', { schema })
102
101
  ], Post);
103
102
  beforeAll(async () => {
104
- const encryptionSecret = new Uint8Array(32).fill(1);
105
103
  ({ injector, database: db } = await setupIntegrationTest({
106
104
  orm: { schema },
107
105
  }));
108
- injector.register(ENCRYPTION_SECRET, { useValue: encryptionSecret });
109
106
  standardRepository = injector.resolve(getRepository(StandardEntity));
110
107
  postRepository = injector.resolve(getRepository(Post));
111
108
  await db.execute(sql `CREATE SCHEMA IF NOT EXISTS ${sql.identifier(schema)}`);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tstdl/base",
3
- "version": "0.93.179",
3
+ "version": "0.93.181",
4
4
  "author": "Patrick Hein",
5
5
  "publishConfig": {
6
6
  "access": "public"
@@ -130,13 +130,13 @@ export async function setupIsolatedIntegrationTest(options = {}) {
130
130
  ...options.dbConfig,
131
131
  };
132
132
  const schema = options.orm?.schema ?? 'test';
133
+ configureSecrets({ key: 'tstdl-unit-tests' });
133
134
  // 4. Configure ORM
134
135
  // We disable autoMigrate here because APPLICATION_INITIALIZER is not used in integration tests
135
136
  // We manually run migrations via bootstrapOrm below
136
137
  configureOrm({
137
138
  repositoryConfig: { schema },
138
139
  connection: dbConfig,
139
- encryptionSecret: options.orm?.encryptionSecret ?? new Uint8Array(32),
140
140
  injector,
141
141
  });
142
142
  // 5. Database Resolution
@@ -1 +0,0 @@
1
- export declare const ENCRYPTION_SECRET: import("../../injector/token.js").InjectionToken<Uint8Array<ArrayBuffer>, never>;
@@ -1,2 +0,0 @@
1
- import { injectionToken } from '../../injector/token.js';
2
- export const ENCRYPTION_SECRET = injectionToken('EncryptionSecret');