@elysiumjs/core 0.1.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.
@@ -0,0 +1,261 @@
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
+ import { pgTable } from 'drizzle-orm/pg-core';
24
+
25
+ import { Application } from './app';
26
+ import { Database } from './database';
27
+ import { Tenancy } from './model';
28
+
29
+ /**
30
+ * Database's primary column type.
31
+ * @author Axel Nana <axel.nana@workbud.com>
32
+ */
33
+ export type IdType = number | string;
34
+
35
+ /**
36
+ * Interface of a repository.
37
+ * @author Axel Nana <axel.nana@workbud.com>
38
+ * @template TModel The type of the model class wrapped by the repository.
39
+ * @template TId The primary column type.
40
+ * @template TColumnsMap The table columns config.
41
+ */
42
+ export interface RepositoryInterface<
43
+ TModel extends ModelClass<TColumnsMap>,
44
+ TId extends IdType = string,
45
+ TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
46
+ infer TColumnsMap
47
+ >
48
+ ? TColumnsMap
49
+ : Record<string, PgColumnBuilderBase>
50
+ > {
51
+ /**
52
+ * Retrieves all the records in the database.
53
+ * @returns All the records in the database.
54
+ */
55
+ all(): Promise<TModel['$inferSelect'][]>;
56
+
57
+ /**
58
+ * Retrieves a record by its id.
59
+ * @param id The id of the record to retrieve.
60
+ * @returns The record with the given id.
61
+ */
62
+ find(id: TId): Promise<TModel['$inferSelect'] | null>;
63
+
64
+ /**
65
+ * Inserts a new record in the database.
66
+ * @param data The data to insert.
67
+ * @returns The inserted record.
68
+ */
69
+ insert(data: TModel['$inferInsert']): Promise<TModel['$inferSelect']>;
70
+
71
+ /**
72
+ * Updates a record in the database.
73
+ * @param id The id of the record to update.
74
+ * @param data The data to update.
75
+ * @returns The updated record.
76
+ */
77
+ update(id: TId, data: TModel['$inferUpdate']): Promise<TModel['$inferSelect']>;
78
+
79
+ /**
80
+ * Updates all the records in the database.
81
+ * @param data The data to update.
82
+ * @returns The updated records.
83
+ */
84
+ updateAll(data: TModel['$inferUpdate']): Promise<TModel['$inferSelect'][]>;
85
+
86
+ /**
87
+ * Deletes a record from the database.
88
+ * @param id The ID of the record to delete.
89
+ * @returns The deleted record.
90
+ */
91
+ delete(id: TId): Promise<TModel['$inferSelect']>;
92
+
93
+ /**
94
+ * Deletes all the records from the database.
95
+ * @returns All the records in the database.
96
+ */
97
+ deleteAll(): Promise<TModel['$inferSelect'][]>;
98
+ }
99
+
100
+ /**
101
+ * Type of a repository class.
102
+ * @author Axel Nana <axel.nana@workbud.com>
103
+ * @template TModel The type of the model class wrapped by the repository.
104
+ * @template TId The primary column type.
105
+ * @template TColumnsMap The table columns config.
106
+ */
107
+ export type RepositoryClass<
108
+ TModel extends ModelClass<TColumnsMap>,
109
+ TId extends IdType = string,
110
+ TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
111
+ infer TColumnsMap
112
+ >
113
+ ? TColumnsMap
114
+ : Record<string, PgColumnBuilderBase>
115
+ > = Class<RepositoryInterface<TModel, TId, TColumnsMap>> & {
116
+ /**
117
+ * The drizzle's table schema wrapped by the repository.
118
+ */
119
+ readonly Model: TModel;
120
+
121
+ /**
122
+ * The database connection name to use in the repository.
123
+ *
124
+ * Update this value to change the database connection used by this repository.
125
+ */
126
+ readonly connection: string;
127
+ };
128
+
129
+ /**
130
+ * Mixin used to create a new repository class over a model.
131
+ * @author Axel Nana <axel.nana@workbud.com>
132
+ * @template TModel The type of the model class wrapped by the repository.
133
+ * @template TId The primary column type.
134
+ * @template TColumnsMap The table columns config.
135
+ * @template model The model class wrapped by the repository.
136
+ */
137
+ export const Repository = <
138
+ TModel extends ModelClass<TColumnsMap>,
139
+ TId extends IdType = string,
140
+ TColumnsMap extends Record<string, PgColumnBuilderBase> = TModel extends ModelClass<
141
+ infer TColumnsMap
142
+ >
143
+ ? TColumnsMap
144
+ : Record<string, PgColumnBuilderBase>
145
+ >(
146
+ model: TModel
147
+ ) => {
148
+ type TSelect = TModel['$inferSelect'];
149
+ type TInsert = TModel['$inferInsert'];
150
+ type TUpdate = TModel['$inferUpdate'];
151
+
152
+ const table = pgTable(model.tableName, model.columns);
153
+
154
+ class R implements RepositoryInterface<TModel, TId, TColumnsMap> {
155
+ /**
156
+ * The drizzle's table schema wrapped by this repository.
157
+ */
158
+ public static readonly Model: TModel = model;
159
+
160
+ /**
161
+ * The database connection name to use.
162
+ *
163
+ * Update this value to change the database connection used by this repository.
164
+ */
165
+ public static readonly connection: string = 'default';
166
+
167
+ /**
168
+ * The database connection used by this repository.
169
+ */
170
+ public get db(): DatabaseConnection {
171
+ const connection = (this.constructor as RepositoryClass<TModel, TId, TColumnsMap>).connection;
172
+ let db: DatabaseConnection | null = null;
173
+
174
+ if (connection === 'default') {
175
+ db = (Application.context.getStore()?.get('db:tx') as DatabaseConnection) ?? null;
176
+ }
177
+
178
+ return db ?? Database.getConnection(connection);
179
+ }
180
+
181
+ /**
182
+ * The drizzle's table schema wrapped by this repository.
183
+ */
184
+ public static get table() {
185
+ if (model.supportTenancy) {
186
+ const tenant = Tenancy.getCurrentTenant() ?? 'public';
187
+ return Tenancy.withTenant(tenant, model);
188
+ }
189
+
190
+ return table;
191
+ }
192
+
193
+ /**
194
+ * Retrieves all the records in the database.
195
+ * @returns All the records in the database.
196
+ */
197
+ public async all(): Promise<TSelect[]> {
198
+ return await this.db.select().from(R.table);
199
+ }
200
+
201
+ /**
202
+ * Retrieves a record by its id.
203
+ * @param id The id of the record to retrieve.
204
+ * @returns The record with the given id.
205
+ */
206
+ public async find(id: TId): Promise<TSelect | null> {
207
+ const [row] = await this.db.select().from(R.table).where(eq(R.table.id, id));
208
+ return (row ?? null) as TSelect | null;
209
+ }
210
+
211
+ /**
212
+ * Inserts a new record in the database.
213
+ * @param data The data to insert.
214
+ * @returns The inserted record.
215
+ */
216
+ public async insert(data: TInsert): Promise<TSelect> {
217
+ const [row] = await this.db.insert(R.table).values(data).returning();
218
+ return row as TSelect;
219
+ }
220
+
221
+ /**
222
+ * Updates a record in the database.
223
+ * @param id The id of the record to update.
224
+ * @param data The data to update.
225
+ * @returns The updated record.
226
+ */
227
+ public async update(id: TId, data: TUpdate): Promise<TSelect> {
228
+ const [row] = await this.db.update(R.table).set(data).where(eq(R.table.id, id)).returning();
229
+ return row as TSelect;
230
+ }
231
+
232
+ /**
233
+ * Updates all the records in the database.
234
+ * @param data The data to update.
235
+ * @returns The updated records.
236
+ */
237
+ public async updateAll(data: TUpdate): Promise<TSelect[]> {
238
+ return await this.db.update(R.table).set(data).returning();
239
+ }
240
+
241
+ /**
242
+ * Deletes a record from the database.
243
+ * @param id The ID of the record to delete.
244
+ * @returns The deleted record.
245
+ */
246
+ public async delete(id: TId): Promise<TSelect> {
247
+ const [row] = await this.db.delete(R.table).where(eq(R.table.id, id)).returning();
248
+ return row as TSelect;
249
+ }
250
+
251
+ /**
252
+ * Deletes all the records from the database.
253
+ * @returns All the records in the database.
254
+ */
255
+ public async deleteAll(): Promise<TSelect[]> {
256
+ return await this.db.delete(R.table).returning();
257
+ }
258
+ }
259
+
260
+ return R;
261
+ };
package/src/service.ts ADDED
@@ -0,0 +1,349 @@
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 { Class } from 'type-fest';
16
+
17
+ import { isString } from 'radash';
18
+
19
+ import { Symbols } from './utils';
20
+
21
+ /**
22
+ * The scope of a service.
23
+ * @author Axel Nana <axel.nana@workbud.com>
24
+ */
25
+ export enum ServiceScope {
26
+ /**
27
+ * A single instance of the service is created and shared everywhere it is injected or retrieved.
28
+ */
29
+ SINGLETON,
30
+
31
+ /**
32
+ * A new instance of the service is created every time it is injected or retrieved.
33
+ */
34
+ FACTORY
35
+ }
36
+
37
+ /**
38
+ * A generic service class.
39
+ * @author Axel Nana <axel.nana@workbud.com>
40
+ */
41
+ type ServiceClass = Class<any>;
42
+
43
+ /**
44
+ * A typed service class.
45
+ * @author Axel Nana <axel.nana@workbud.com>
46
+ */
47
+ type TypedServiceClass<T> = Class<T>;
48
+
49
+ /**
50
+ * Describes metadata for each registered service.
51
+ * @author Axel Nana <axel.nana@workbud.com>
52
+ */
53
+ type ServiceRegistration = {
54
+ /**
55
+ * The service's scope.
56
+ */
57
+ scope: ServiceScope;
58
+
59
+ /**
60
+ * A factory function that create the service.
61
+ *
62
+ * When `scope` is set to `Scope.SINGLETON`, this function always return the
63
+ * same instance.
64
+ *
65
+ * @returns An instance to the service.
66
+ */
67
+ factory: () => any;
68
+ };
69
+
70
+ /**
71
+ * Describes the metadata stored for each injected services.
72
+ * @author Axel Nana <axel.nana@workbud.com>
73
+ */
74
+ type InjectedService = {
75
+ /**
76
+ * The name of the injected service. It should match the one used when
77
+ * registering the service.
78
+ */
79
+ name: string;
80
+
81
+ /**
82
+ * The index in the constructor where the service is injected.
83
+ */
84
+ index: number;
85
+ };
86
+
87
+ /**
88
+ * Type for a factory function.
89
+ * @author Axel Nana <axel.nana@workbud.com>
90
+ */
91
+ type FactoryFn<T> = () => T extends void ? never : T;
92
+
93
+ /**
94
+ * Utility function to check if a value is a class.
95
+ * @author Axel Nana <axel.nana@workbud.com>
96
+ * @param value The value to check.
97
+ * @returns `true` if the value is a class, `false` otherwise.
98
+ */
99
+ const isClass = <T, V>(value: Class<T> | V): value is Class<T> => {
100
+ return typeof value === 'function' && value.prototype && value.prototype.constructor === value;
101
+ };
102
+
103
+ /**
104
+ * Storage for registered services.
105
+ * @author Axel Nana <axel.nana@workbud.com>
106
+ */
107
+ const servicesRegistry = new Map<string, ServiceRegistration>();
108
+
109
+ /**
110
+ * Properties required when declaring a service using the `@service()` decorator.
111
+ * @author Axel Nana <axel.nana@workbud.com>
112
+ */
113
+ export type ServiceProps = Partial<{
114
+ /**
115
+ * The name of the service. If not set, it will default to the service class name.
116
+ */
117
+ name: string;
118
+
119
+ /**
120
+ * The service's scope.
121
+ *
122
+ * Set it to `Scope.SINGLETON` to ensure that only one instance of this service is
123
+ * used everywhere it is injected or retrieved.
124
+ *
125
+ * Set it to `Scope.FACTORY` to ensure that every time you inject or retrieve this
126
+ * service, you have a new instance created.
127
+ *
128
+ * @see ServiceScope
129
+ */
130
+ scope: ServiceScope;
131
+ }>;
132
+
133
+ export namespace Service {
134
+ /**
135
+ * Marks a class as a service
136
+ * @author Axel Nana <axel.nana@workbud.com>
137
+ * @param options The decorator options.
138
+ */
139
+ export const register = (options?: ServiceProps) => {
140
+ return function (target: ServiceClass) {
141
+ const name = options?.name ?? target.name;
142
+ const scope = options?.scope ?? ServiceScope.SINGLETON;
143
+
144
+ if (Service.exists(name)) {
145
+ // TODO: Use the logger service here
146
+ console.error(`A service with the name ${name} has already been registered.`);
147
+ process.exit(1);
148
+ }
149
+
150
+ const factory = () => Service.make(target);
151
+
152
+ if (scope === ServiceScope.SINGLETON) {
153
+ const service = factory();
154
+ servicesRegistry.set(name, {
155
+ scope,
156
+ factory() {
157
+ return service;
158
+ }
159
+ });
160
+ } else {
161
+ servicesRegistry.set(name, {
162
+ scope,
163
+ factory
164
+ });
165
+ }
166
+ };
167
+ };
168
+
169
+ /**
170
+ * Resolves a registered service and set it as a parameter value.
171
+ * @author Axel Nana <axel.nana@workbud.com>
172
+ * @param name An optional name for the service. If not set, the name of the parameter's type is used instead.
173
+ */
174
+ export const inject = (name?: string): ParameterDecorator => {
175
+ return function (target, propertyKey, parameterIndex) {
176
+ const services = Reflect.getMetadata(Symbols.services, target, propertyKey!) ?? [];
177
+ const types = Reflect.getMetadata('design:paramtypes', target, propertyKey!) ?? [];
178
+
179
+ services.push({
180
+ name: name ?? types[parameterIndex].name,
181
+ index: parameterIndex
182
+ });
183
+
184
+ Reflect.defineMetadata(Symbols.services, services, target, propertyKey!);
185
+ };
186
+ };
187
+
188
+ /**
189
+ * Retrieves a registered service's instance from the container.
190
+ * @author Axel Nana <axel.nana@workbud.com>
191
+ * @param service The name of the service, or its class.
192
+ * @returns An instance of the registered service, or `null` if no service with that name/type was registered.
193
+ */
194
+ export const get = <T>(service: string | TypedServiceClass<T>): T | null => {
195
+ const name = isString(service) ? service : service.name;
196
+
197
+ if (exists(name)) {
198
+ const service = servicesRegistry.get(name)!;
199
+ return service.factory() as T;
200
+ }
201
+
202
+ return null;
203
+ };
204
+
205
+ /**
206
+ * Instantiates a service with its dependencies.
207
+ *
208
+ * This function always create a new instance of the service, even if the service's scope is `ServiceScope.SINGLETON`
209
+ * and it has already been registered in the container.
210
+ *
211
+ * @author Axel Nana <axel.nana@workbud.com>
212
+ *
213
+ * @param service The service class to instantiate.
214
+ */
215
+ export const make = <T>(service: TypedServiceClass<T>): T => {
216
+ const dependencies: Array<InjectedService> =
217
+ Reflect.getMetadata(Symbols.services, service) ?? [];
218
+
219
+ const params: any[] = [];
220
+ dependencies.forEach((dependency) => {
221
+ const s = get(dependency.name);
222
+
223
+ if (s === null) {
224
+ // TODO: Use the logger service here
225
+ console.error(
226
+ `Cannot inject a service. No service was registered with the name: ${dependency.name}`
227
+ );
228
+ process.exit(1);
229
+ }
230
+
231
+ params[dependency.index] = s;
232
+ });
233
+
234
+ return new service(...params);
235
+ };
236
+
237
+ /**
238
+ * Binds a service to the container and sets it as a singleton.
239
+ * @author Axel Nana <axel.nana@workbud.com>
240
+ * @param service The service class to bind.
241
+ * @param name An optional name for the service. If not set, the name of the service class is used instead.
242
+ */
243
+ export const bind = <T>(service: TypedServiceClass<T>, name?: string): T => {
244
+ const s = make(service);
245
+ return instance(name ?? service.name, s);
246
+ };
247
+
248
+ /**
249
+ * Binds a service to the container and sets it as a factory.
250
+ * @author Axel Nana <axel.nana@workbud.com>
251
+ * @param name A name for the service.
252
+ * @param factory The factory function used to instantiate the service.
253
+ */
254
+ export function factory<T>(name: string, factory: FactoryFn<T>): T;
255
+
256
+ /**
257
+ * Binds a service to the container and sets it as a factory.
258
+ * @author Axel Nana <axel.nana@workbud.com>
259
+ * @param service The service class to bind.
260
+ * @param name An optional name for the service. If not set, the name of the service class is used instead.
261
+ */
262
+ export function factory<T>(service: TypedServiceClass<T>, name?: string): T;
263
+
264
+ /**
265
+ * Binds a service to the container and sets it as a factory.
266
+ * @author Axel Nana <axel.nana@workbud.com>
267
+ * @param serviceOrName The service class to bind or its name.
268
+ * @param nameOrFactory The service name or a factory function.
269
+ */
270
+ export function factory<T>(
271
+ serviceOrName: TypedServiceClass<T> | string,
272
+ nameOrFactory?: FactoryFn<T> | string
273
+ ): T {
274
+ const serviceName = isString(serviceOrName)
275
+ ? serviceOrName
276
+ : ((nameOrFactory as string | undefined) ?? serviceOrName.name);
277
+
278
+ if (exists(serviceName)) {
279
+ // TODO: Use the logger service here
280
+ console.error(`A service with the name ${serviceName} has already been registered.`);
281
+ process.exit(1);
282
+ }
283
+
284
+ const factory = () => {
285
+ return isClass(serviceOrName) ? make(serviceOrName) : (nameOrFactory as FactoryFn<T>)();
286
+ };
287
+
288
+ servicesRegistry.set(serviceName, {
289
+ scope: ServiceScope.FACTORY,
290
+ factory
291
+ });
292
+
293
+ return factory();
294
+ }
295
+
296
+ /**
297
+ * Binds a service's instance to the container.
298
+ * @author Axel Nana <axel.nana@workbud.com>
299
+ * @param service The service class or name to bind.
300
+ * @param instance The instance to bind.
301
+ */
302
+ export const instance = <T>(service: string | TypedServiceClass<T>, instance: T): T => {
303
+ const serviceName = isString(service) ? service : service.name;
304
+
305
+ if (exists(serviceName)) {
306
+ // TODO: Use the logger service here
307
+ console.error(`A service with the name ${serviceName} has already been registered.`);
308
+ process.exit(1);
309
+ }
310
+
311
+ servicesRegistry.set(serviceName, {
312
+ scope: ServiceScope.SINGLETON,
313
+ factory() {
314
+ return instance;
315
+ }
316
+ });
317
+
318
+ return instance;
319
+ };
320
+
321
+ /**
322
+ * Removes a service from the container.
323
+ * @author Axel Nana <axel.nana@workbud.com>
324
+ * @param service The service class or name to remove.
325
+ */
326
+ export const remove = (service: string | TypedServiceClass<any>): void => {
327
+ const serviceName = isString(service) ? service : service.name;
328
+ servicesRegistry.delete(serviceName);
329
+ };
330
+
331
+ /**
332
+ * Checks if a service exists in the container.
333
+ * @author Axel Nana <axel.nana@workbud.com>
334
+ * @param service The service class or name to check.
335
+ * @returns `true` if the service exists, `false` otherwise.
336
+ */
337
+ export const exists = (service: string | TypedServiceClass<any>): boolean => {
338
+ const serviceName = isString(service) ? service : service.name;
339
+ return servicesRegistry.has(serviceName);
340
+ };
341
+
342
+ /**
343
+ * Clears the container.
344
+ * @author Axel Nana <axel.nana@workbud.com>
345
+ */
346
+ export const clear = () => {
347
+ servicesRegistry.clear();
348
+ };
349
+ }
package/src/utils.ts ADDED
@@ -0,0 +1,41 @@
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 { Elysia } from 'elysia';
16
+
17
+ export namespace Symbols {
18
+ export const app = Symbol('app');
19
+
20
+ export const controller = Symbol('controller');
21
+
22
+ export const middlewares = Symbol('middlewares');
23
+
24
+ export const services = Symbol('services');
25
+
26
+ export const websocket = Symbol('websocket');
27
+
28
+ export const wamp = Symbol('wamp');
29
+
30
+ export const http = Symbol('http');
31
+
32
+ export const job = Symbol('job');
33
+
34
+ export const arg = Symbol('command:arguments');
35
+
36
+ export const elysiaPlugin = Symbol('elysia:plugin');
37
+ }
38
+
39
+ export const nextTick = () => new Promise((resolve) => process.nextTick(resolve));
40
+
41
+ export type ElysiaPlugin = () => Promise<Elysia>;