@elysiumjs/core 0.4.2 → 0.5.0

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/src/redis.ts DELETED
@@ -1,408 +0,0 @@
1
- // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- import type { KeyvStoreAdapter } from 'cacheable';
16
- import type { KeyvEntry, StoredData } from 'keyv';
17
-
18
- import { EventEmitter } from 'node:events';
19
-
20
- import { omit } from 'radash';
21
-
22
- import { Event } from './event';
23
- import { Service } from './service';
24
-
25
- /**
26
- * Properties used to create a new redis connection.
27
- * @author Axel Nana <axel.nana@workbud.com>
28
- */
29
- export type RedisConnectionProps = Bun.RedisOptions & { url: string };
30
-
31
- /**
32
- * Keyv adapter for redis. This adapter is based on the Bun's Redis client.
33
- * @author Axel Nana <axel.nana@workbud.com>
34
- */
35
- export class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
36
- #client?: Bun.RedisClient;
37
- #namespace: string | undefined;
38
- #keyPrefixSeparator = '::';
39
- #clearBatchSize = 1000;
40
- #useUnlink = true;
41
- #noNamespaceAffectsAll = false;
42
-
43
- #connection: string;
44
-
45
- constructor(props: {
46
- /**
47
- * The Redis connection to use.
48
- */
49
- connection: string;
50
-
51
- /**
52
- * Namespace for the connection.
53
- */
54
- namespace?: string;
55
-
56
- /**
57
- * Separator to use between namespace and key.
58
- */
59
- keyPrefixSeparator?: string;
60
-
61
- /**
62
- * Number of keys to delete in a single batch.
63
- */
64
- clearBatchSize?: number;
65
-
66
- /**
67
- * Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions.
68
- */
69
- useUnlink?: boolean;
70
-
71
- /**
72
- * Whether to allow clearing all keys when no namespace is set.
73
- * If set to true and no namespace is set, iterate() will return all keys.
74
- * Defaults to `false`.
75
- */
76
- noNamespaceAffectsAll?: boolean;
77
- }) {
78
- super();
79
-
80
- Event.once('elysium:app:launched', () => {
81
- this.#client = Redis.getConnection(props.connection);
82
- });
83
-
84
- this.#connection = props.connection;
85
- this.#namespace = props.namespace;
86
- this.#keyPrefixSeparator = props.keyPrefixSeparator ?? '::';
87
- this.#clearBatchSize = props.clearBatchSize ?? 1000;
88
- this.#useUnlink = props.useUnlink ?? true;
89
- this.#noNamespaceAffectsAll = props.noNamespaceAffectsAll ?? false;
90
-
91
- this.on('error', (error) => {
92
- Event.emit('elysium:error', error);
93
- });
94
- }
95
-
96
- public get client() {
97
- return this.#client!;
98
- }
99
-
100
- /**
101
- * Get the options for the adapter.
102
- */
103
- public get opts() {
104
- return {
105
- namespace: this.#namespace,
106
- keyPrefixSeparator: this.#keyPrefixSeparator,
107
- clearBatchSize: this.#clearBatchSize,
108
- noNamespaceAffectsAll: this.#noNamespaceAffectsAll,
109
- dialect: 'redis',
110
- url: this.#connection
111
- };
112
- }
113
-
114
- /**
115
- * Gets the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
116
- * @default undefined
117
- */
118
- public get namespace(): string | undefined {
119
- return this.#namespace;
120
- }
121
-
122
- /**
123
- * Sets the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
124
- */
125
- public set namespace(value: string | undefined) {
126
- this.#namespace = value;
127
- }
128
-
129
- public async get<Value>(key: string): Promise<StoredData<Value> | undefined> {
130
- key = this.createKeyPrefix(key, this.#namespace);
131
- const value = await this.client.get(key);
132
-
133
- return value === null ? undefined : (value as Value);
134
- }
135
-
136
- public async set(key: string, value: any, ttl?: number) {
137
- key = this.createKeyPrefix(key, this.#namespace);
138
-
139
- if (ttl) {
140
- await this.client.set(key, value, 'PX', ttl);
141
- } else {
142
- await this.client.set(key, value);
143
- }
144
- }
145
-
146
- public async setMany(values: Array<KeyvEntry>): Promise<void> {
147
- // TODO: Use MULTI when available in Bun
148
- const multi = [];
149
-
150
- for (const { key, value, ttl } of values) {
151
- const prefixedKey = this.createKeyPrefix(key, this.#namespace);
152
- if (ttl) {
153
- // eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unsafe-argument
154
- multi.push(this.client.set(prefixedKey, value, 'PX', ttl));
155
- } else {
156
- // eslint-disable-next-line @typescript-eslint/no-unsafe-argument
157
- multi.push(this.client.set(prefixedKey, value));
158
- }
159
- }
160
-
161
- await Promise.all(multi);
162
- }
163
-
164
- public async delete(key: string): Promise<boolean> {
165
- key = this.createKeyPrefix(key, this.#namespace);
166
- // TODO: Use UNLINK when available in Bun
167
- const deleted = await (this.#useUnlink
168
- ? this.client.send('UNLINK', [key])
169
- : this.client.del(key));
170
-
171
- return deleted > 0;
172
- }
173
-
174
- public async clear(): Promise<void> {
175
- try {
176
- if (!this.#namespace && this.#noNamespaceAffectsAll) {
177
- await this.client.send('FLUSHDB', []);
178
- return;
179
- }
180
-
181
- let cursor = '0';
182
- const batchSize = this.#clearBatchSize;
183
- const match = this.#namespace ? `${this.#namespace}${this.#keyPrefixSeparator}*` : '*';
184
- const deletePromises = [];
185
-
186
- do {
187
- const result = await this.client.send('SCAN', [
188
- cursor,
189
- 'MATCH',
190
- match,
191
- 'COUNT',
192
- batchSize.toString(),
193
- 'TYPE',
194
- 'string'
195
- ]);
196
-
197
- // Parse the result
198
- let [newCursor, keys] = result;
199
- cursor = newCursor.toString();
200
-
201
- if (keys.length === 0) {
202
- continue;
203
- }
204
-
205
- if (!this.#namespace) {
206
- keys = keys.filter((key: string) => !key.includes(this.#keyPrefixSeparator));
207
- }
208
-
209
- deletePromises.push(keys.map((key: string) => this.client.del(key)));
210
- } while (cursor !== '0');
211
-
212
- await Promise.all(deletePromises);
213
- } catch (error) {
214
- this.emit('error', error);
215
- }
216
- }
217
-
218
- public async has(key: string): Promise<boolean> {
219
- key = this.createKeyPrefix(key, this.#namespace);
220
- return this.client.exists(key);
221
- }
222
-
223
- public async hasMany(keys: string[]): Promise<boolean[]> {
224
- const multi = [];
225
-
226
- for (const key of keys) {
227
- const prefixedKey = this.createKeyPrefix(key, this.#namespace);
228
- multi.push(this.client.exists(prefixedKey));
229
- }
230
-
231
- return Promise.all(multi);
232
- }
233
-
234
- public async getMany<Value>(keys: string[]): Promise<Array<StoredData<Value | undefined>>> {
235
- if (keys.length === 0) {
236
- return [];
237
- }
238
-
239
- keys = keys.map((key) => this.createKeyPrefix(key, this.#namespace));
240
- const values = await this.client.mget(...keys);
241
-
242
- return values as Value[];
243
- }
244
-
245
- public async disconnect(): Promise<void> {
246
- this.client.close();
247
- }
248
-
249
- public async deleteMany(keys: string[]): Promise<boolean> {
250
- let result = false;
251
-
252
- // TODO: Use MULTI when available in Bun
253
- const multi = [];
254
-
255
- for (const key of keys) {
256
- const prefixedKey = this.createKeyPrefix(key, this.#namespace);
257
- if (this.#useUnlink) {
258
- multi.push(this.client.send('UNLINK', [prefixedKey]));
259
- } else {
260
- multi.push(this.client.del(prefixedKey));
261
- }
262
- }
263
-
264
- const results = await Promise.all(multi);
265
-
266
- for (const deleted of results) {
267
- if (typeof deleted === 'number' && deleted > 0) {
268
- result = true;
269
- }
270
- }
271
-
272
- return result;
273
- }
274
-
275
- public async *iterator<Value>(
276
- namespace?: string
277
- ): AsyncGenerator<Array<string | Awaited<Value> | undefined>, void> {
278
- const match = namespace ? `${namespace}${this.#keyPrefixSeparator}*` : '*';
279
- let cursor = '0';
280
-
281
- do {
282
- const result = await this.client.send('SCAN', [cursor, 'MATCH', match, 'TYPE', 'string']);
283
-
284
- // Parse the result
285
- let [newCursor, keys] = result;
286
- cursor = newCursor.toString();
287
-
288
- if (!namespace && !this.#noNamespaceAffectsAll) {
289
- keys = keys.filter((key: string) => !key.includes(this.#keyPrefixSeparator));
290
- }
291
-
292
- if (keys.length > 0) {
293
- const values = await this.client.mget(...keys);
294
- for (const i of keys.keys()) {
295
- const key = this.getKeyWithoutPrefix(keys[i], namespace);
296
- const value = values[i];
297
- yield [key, value!];
298
- }
299
- }
300
- } while (cursor !== '0');
301
- }
302
-
303
- /**
304
- * Helper function to create a key with a namespace.
305
- * @param key The key to prefix.
306
- * @param namespace The namespace to prefix the key with.
307
- * @returns The key with the namespace such as 'namespace::key'.
308
- */
309
- public createKeyPrefix(key: string, namespace?: string): string {
310
- return !!namespace ? `${namespace}${this.#keyPrefixSeparator}${key}` : key;
311
- }
312
-
313
- /**
314
- * Helper function to remove the namespace from a key.
315
- * @param key The key to remove the namespace from.
316
- * @param namespace The namespace to remove from the key.
317
- * @returns The key without the namespace.
318
- */
319
- public getKeyWithoutPrefix(key: string, namespace?: string): string {
320
- return !!namespace ? key.replace(`${namespace}${this.#keyPrefixSeparator}`, '') : key;
321
- }
322
- }
323
-
324
- export namespace Redis {
325
- /**
326
- * Creates a service name for a redis connection.
327
- * @author Axel Nana <axel.nana@workbud.com>
328
- * @param name The name of the connection.
329
- * @returns A service name for the connection.
330
- */
331
- const getConnectionName = (name: string) => {
332
- return `redis.connection.${name}`;
333
- };
334
-
335
- /**
336
- * Retrieves the client for a redis connection.
337
- * @author Axel Nana <axel.nana@workbud.com>
338
- * @param name The name of the connection.
339
- * @returns The Bun.RedisClient for the connection with the given name.
340
- */
341
- export const getConnection = (name: string) => {
342
- if (!Service.exists(getConnectionName(name))) {
343
- // TODO: Use logger service here
344
- console.error(
345
- `No Redis connection with name ${name} found. Please make sure to register the connection before using it.`
346
- );
347
- process.exit(1);
348
- }
349
-
350
- return Service.get<Bun.RedisClient>(getConnectionName(name))!;
351
- };
352
-
353
- /**
354
- * Creates and registers a new redis connection.
355
- *
356
- * This will make the registered connection available for dependency injection with
357
- * the key `redis.connection.{name}`, where `{name}` is replaced with the given name.
358
- *
359
- * @author Axel Nana <axel.nana@workbud.com>
360
- *
361
- * @param name The name of the connection.
362
- * @param config The configuration for the redis connection.
363
- * @returns The newly created and registered redis connection.
364
- */
365
- export const registerConnection = (name: string, config: RedisConnectionProps) => {
366
- if (Service.exists(getConnectionName(name))) {
367
- // TODO: Use logger service here
368
- console.error(`A Redis connection with the name ${name} has already been registered.`);
369
- process.exit(1);
370
- }
371
-
372
- return Service.instance(
373
- getConnectionName(name),
374
- new Bun.RedisClient(config.url, omit(config, ['url']))
375
- );
376
- };
377
-
378
- /**
379
- * Checks if a redis connection with the given name exists.
380
- * @author Axel Nana <axel.nana@workbud.com>
381
- * @param name The name of the connection to check.
382
- * @returns `true` if the connection exists, `false` otherwise.
383
- */
384
- export const connectionExists = (name: string) => {
385
- return Service.exists(getConnectionName(name));
386
- };
387
-
388
- /**
389
- * Retrieves the default redis connection.
390
- * @author Axel Nana <axel.nana@workbud.com>
391
- * @returns The default redis connection.
392
- */
393
- export const getDefaultConnection = () => {
394
- return getConnection('default');
395
- };
396
-
397
- /**
398
- * Sets the default redis connection.
399
- * @author Axel Nana <axel.nana@workbud.com>
400
- * @param name The name of the connection to set as default.
401
- * @returns The default redis connection.
402
- */
403
- export const setDefaultConnection = (name: string) => {
404
- const serviceName = getConnectionName('default');
405
- Service.remove(serviceName);
406
- return Service.instance(serviceName, getConnection(name));
407
- };
408
- }
package/src/repository.ts DELETED
@@ -1,295 +0,0 @@
1
- // Copyright (c) 2025-present Workbud Technologies Inc. All rights reserved.
2
- //
3
- // Licensed under the Apache License, Version 2.0 (the "License");
4
- // you may not use this file except in compliance with the License.
5
- // You may obtain a copy of the License at
6
- //
7
- // http://www.apache.org/licenses/LICENSE-2.0
8
- //
9
- // Unless required by applicable law or agreed to in writing, software
10
- // distributed under the License is distributed on an "AS IS" BASIS,
11
- // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12
- // See the License for the specific language governing permissions and
13
- // limitations under the License.
14
-
15
- // @ts-nocheck
16
-
17
- import type { PgColumnBuilderBase } from 'drizzle-orm/pg-core';
18
- import type { Class } from 'type-fest';
19
- import type { DatabaseConnection } from './database';
20
- import type { ModelClass } from './model';
21
-
22
- import { eq } from 'drizzle-orm';
23
-
24
- import { Application } from './app';
25
- import { Database } from './database';
26
-
27
- /**
28
- * Database's primary column type.
29
- * @author Axel Nana <axel.nana@workbud.com>
30
- */
31
- export type IdType = number | string;
32
-
33
- /**
34
- * Interface of a repository.
35
- * @author Axel Nana <axel.nana@workbud.com>
36
- * @template TModel The type of the model class wrapped by the repository.
37
- * @template TId The primary column type.
38
- * @template TColumnsMap The table columns config.
39
- */
40
- export interface RepositoryInterface<
41
- TModel extends ModelClass<TTableName, TColumnsMap>,
42
- TId extends IdType = string,
43
- TTableName extends string = TModel extends ModelClass<infer TTableName, infer _>
44
- ? TTableName
45
- : string,
46
- TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
47
- TTableName,
48
- infer TColumnsMap
49
- >
50
- ? TColumnsMap
51
- : Record<string, PgColumnBuilderBase>
52
- > {
53
- /**
54
- * Retrieves all the records in the database.
55
- * @returns All the records in the database.
56
- */
57
- all(): Promise<TModel['$inferSelect'][]>;
58
-
59
- /**
60
- * Retrieves a record by its id.
61
- * @param id The id of the record to retrieve.
62
- * @returns The record with the given id.
63
- */
64
- find(id: TId): Promise<TModel['$inferSelect'] | null>;
65
-
66
- /**
67
- * Inserts a new record in the database.
68
- * @param data The data to insert.
69
- * @returns The inserted record.
70
- */
71
- insert(data: TModel['$inferInsert']): Promise<TModel['$inferSelect']>;
72
-
73
- /**
74
- * Updates a record in the database.
75
- * @param id The id of the record to update.
76
- * @param data The data to update.
77
- * @returns The updated record.
78
- */
79
- update(id: TId, data: TModel['$inferUpdate']): Promise<TModel['$inferSelect']>;
80
-
81
- /**
82
- * Updates all the records in the database.
83
- * @param data The data to update.
84
- * @returns The updated records.
85
- */
86
- updateAll(data: TModel['$inferUpdate']): Promise<TModel['$inferSelect'][]>;
87
-
88
- /**
89
- * Deletes a record from the database.
90
- * @param id The ID of the record to delete.
91
- * @returns The deleted record.
92
- */
93
- delete(id: TId): Promise<TModel['$inferSelect']>;
94
-
95
- /**
96
- * Deletes all the records from the database.
97
- * @returns All the records in the database.
98
- */
99
- deleteAll(): Promise<TModel['$inferSelect'][]>;
100
- }
101
-
102
- /**
103
- * Type of a repository class.
104
- * @author Axel Nana <axel.nana@workbud.com>
105
- * @template TModel The type of the model class wrapped by the repository.
106
- * @template TId The primary column type.
107
- * @template TColumnsMap The table columns config.
108
- */
109
- export type RepositoryClass<
110
- TModel extends ModelClass<TTableName, TColumnsMap>,
111
- TId extends IdType = string,
112
- TTableName extends string = TModel extends ModelClass<infer TTableName, infer _>
113
- ? TTableName
114
- : string,
115
- TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
116
- TTableName,
117
- infer TColumnsMap
118
- >
119
- ? TColumnsMap
120
- : Record<string, PgColumnBuilderBase>
121
- > = Class<RepositoryInterface<TModel, TId, TTableName, TColumnsMap>> & {
122
- /**
123
- * The drizzle's table schema wrapped by the repository.
124
- */
125
- readonly Model: TModel;
126
-
127
- /**
128
- * The database connection name to use in the repository.
129
- *
130
- * Update this value to change the database connection used by this repository.
131
- */
132
- readonly connection: string;
133
- };
134
-
135
- /**
136
- * Mixin used to create a new repository class over a model.
137
- * @author Axel Nana <axel.nana@workbud.com>
138
- * @template TModel The type of the model class wrapped by the repository.
139
- * @template TId The primary column type.
140
- * @template TColumnsMap The table columns config.
141
- * @template model The model class wrapped by the repository.
142
- */
143
- export const Repository = <
144
- TModel extends ModelClass<TTableName, TColumnsMap>,
145
- TId extends IdType = string,
146
- TTableName extends string = TModel extends ModelClass<infer TTableName, infer _>
147
- ? TTableName
148
- : string,
149
- TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
150
- TTableName,
151
- infer TColumnsMap
152
- >
153
- ? TColumnsMap
154
- : Record<string, PgColumnBuilderBase>
155
- >(
156
- model: TModel
157
- ) => {
158
- type TSelect = TModel['$inferSelect'];
159
- type TInsert = TModel['$inferInsert'];
160
- type TUpdate = TModel['$inferUpdate'];
161
-
162
- class R implements RepositoryInterface<TModel, TId, TTableName, TColumnsMap> {
163
- /**
164
- * The drizzle's table schema wrapped by this repository.
165
- */
166
- public static readonly Model: TModel = model;
167
-
168
- /**
169
- * The database connection name to use.
170
- *
171
- * Update this value to change the database connection used by this repository.
172
- */
173
- public static readonly connection: string = 'default';
174
-
175
- /**
176
- * The database connection used by this repository.
177
- */
178
- public get db(): DatabaseConnection {
179
- const connection = (this.constructor as RepositoryClass<TModel, TId, TTableName, TColumnsMap>)
180
- .connection;
181
- let db: DatabaseConnection | null = null;
182
-
183
- if (connection === 'default') {
184
- db = (Application.context.getStore()?.get('db:tx') as DatabaseConnection) ?? null;
185
- }
186
-
187
- return db ?? Database.getConnection(connection);
188
- }
189
-
190
- /**
191
- * Retrieves all the records in the database.
192
- * @returns All the records in the database.
193
- */
194
- public async all(): Promise<TSelect[]> {
195
- return await this.db.select().from(model.table);
196
- }
197
-
198
- /**
199
- * Get a paginated subset of the records in the database.
200
- * @param page The page number to retrieve.
201
- * @param count The number of records to retrieve per page.
202
- * @returns A paginated response containing the records and the total number of records.
203
- */
204
- public async paginate(
205
- count: number,
206
- page: number = 1
207
- ): Promise<{ page: number; data: TSelect[]; total: number }> {
208
- const total = await this.db.$count(model.table);
209
- const offset = Math.max(page - 1, 0) * count;
210
- return {
211
- page,
212
- total,
213
- data: await this.db.select().from(model.table).offset(offset).limit(count)
214
- };
215
- }
216
-
217
- /**
218
- * Retrieves a record by its id.
219
- * @param id The id of the record to retrieve.
220
- * @returns The record with the given id.
221
- */
222
- public async find(id: TId): Promise<TSelect | null> {
223
- const [row] = await this.db.select().from(model.table).where(eq(model.table.id, id));
224
- return (row ?? null) as TSelect | null;
225
- }
226
-
227
- /**
228
- * Finds the first record with a column matching the given value.
229
- * @param column The column to filter by.
230
- * @param value The value the column should match.
231
- * @returns The first record with a column matching the given value.
232
- */
233
- public async findBy<TKey extends keyof TSelect>(
234
- column: TKey,
235
- value: TSelect[TKey]
236
- ): Promise<TSelect | null> {
237
- const [row] = await this.db.select().from(model.table).where(eq(model.table[column], value));
238
- return (row ?? null) as TSelect | null;
239
- }
240
-
241
- /**
242
- * Inserts a new record in the database.
243
- * @param data The data to insert.
244
- * @returns The inserted record.
245
- */
246
- public async insert(data: TInsert): Promise<TSelect> {
247
- const [row] = await this.db.insert(model.table).values(data).returning();
248
- return row as TSelect;
249
- }
250
-
251
- /**
252
- * Updates a record in the database.
253
- * @param id The id of the record to update.
254
- * @param data The data to update.
255
- * @returns The updated record.
256
- */
257
- public async update(id: TId, data: TUpdate): Promise<TSelect> {
258
- const [row] = await this.db
259
- .update(model.table)
260
- .set(data)
261
- .where(eq(model.table.id, id))
262
- .returning();
263
- return row as TSelect;
264
- }
265
-
266
- /**
267
- * Updates all the records in the database.
268
- * @param data The data to update.
269
- * @returns The updated records.
270
- */
271
- public async updateAll(data: TUpdate): Promise<TSelect[]> {
272
- return await this.db.update(model.table).set(data).returning();
273
- }
274
-
275
- /**
276
- * Deletes a record from the database.
277
- * @param id The ID of the record to delete.
278
- * @returns The deleted record.
279
- */
280
- public async delete(id: TId): Promise<TSelect> {
281
- const [row] = await this.db.delete(model.table).where(eq(model.table.id, id)).returning();
282
- return row as TSelect;
283
- }
284
-
285
- /**
286
- * Deletes all the records from the database.
287
- * @returns All the records in the database.
288
- */
289
- public async deleteAll(): Promise<TSelect[]> {
290
- return await this.db.delete(model.table).returning();
291
- }
292
- }
293
-
294
- return R;
295
- };