@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.
- package/.prettierignore +5 -0
- package/.prettierrc +25 -0
- package/README.md +112 -0
- package/bun.lock +262 -0
- package/index.ts +50 -0
- package/package.json +49 -0
- package/src/app.ts +503 -0
- package/src/cache.ts +218 -0
- package/src/command.ts +908 -0
- package/src/console.ts +487 -0
- package/src/database.ts +130 -0
- package/src/event.ts +225 -0
- package/src/http.ts +669 -0
- package/src/job.ts +196 -0
- package/src/middleware.ts +122 -0
- package/src/model.ts +312 -0
- package/src/module.ts +98 -0
- package/src/redis.ts +407 -0
- package/src/repository.ts +261 -0
- package/src/service.ts +349 -0
- package/src/utils.ts +41 -0
- package/src/wamp.ts +388 -0
- package/src/websocket.ts +186 -0
- package/src/worker.ts +842 -0
package/src/job.ts
ADDED
|
@@ -0,0 +1,196 @@
|
|
|
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 'reflect-metadata';
|
|
16
|
+
|
|
17
|
+
import type { Class } from 'type-fest';
|
|
18
|
+
|
|
19
|
+
import { assign, uid } from 'radash';
|
|
20
|
+
|
|
21
|
+
import { InteractsWithConsole } from './console';
|
|
22
|
+
import { Service } from './service';
|
|
23
|
+
import { Symbols } from './utils';
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Properties required when declaring a job using the `@job()` decorator.
|
|
27
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
28
|
+
*/
|
|
29
|
+
export type JobProps = {
|
|
30
|
+
/**
|
|
31
|
+
* The name of the job.
|
|
32
|
+
*/
|
|
33
|
+
name?: string;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* The queue on which the job should be processed.
|
|
37
|
+
*/
|
|
38
|
+
queue?: string;
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Base class for background jobs.
|
|
43
|
+
*
|
|
44
|
+
* Jobs are classes that can be executed in the background. They provide
|
|
45
|
+
* an abstract execute method that serves as the entry point for the job.
|
|
46
|
+
*
|
|
47
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
48
|
+
*/
|
|
49
|
+
export abstract class Job extends InteractsWithConsole {
|
|
50
|
+
#startedAt?: Date;
|
|
51
|
+
#completedAt?: Date;
|
|
52
|
+
#status: JobStatus = JobStatus.PENDING;
|
|
53
|
+
#err?: Error;
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Marks a class as a job.
|
|
57
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
58
|
+
* @param options The decorator options.
|
|
59
|
+
*/
|
|
60
|
+
public static register(options: JobProps = {}) {
|
|
61
|
+
return function (target: Class<Job>) {
|
|
62
|
+
options = assign({ name: target.name, queue: 'default' }, options);
|
|
63
|
+
|
|
64
|
+
const name = `job.${options.name}`;
|
|
65
|
+
|
|
66
|
+
Service.instance(name, target);
|
|
67
|
+
|
|
68
|
+
Reflect.defineMetadata(Symbols.job, { name, queue: options.queue }, target);
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Unique identifier for the job.
|
|
74
|
+
*/
|
|
75
|
+
public readonly id: string;
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* The timestamp when the job was created.
|
|
79
|
+
*/
|
|
80
|
+
public readonly createdAt: Date;
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* The timestamp when the job started execution.
|
|
84
|
+
*/
|
|
85
|
+
public get startedAt(): Date | undefined {
|
|
86
|
+
return this.#startedAt;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* The timestamp when the job completed execution.
|
|
91
|
+
*/
|
|
92
|
+
public get completedAt(): Date | undefined {
|
|
93
|
+
return this.#completedAt;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* The current status of the job.
|
|
98
|
+
*/
|
|
99
|
+
public get status(): JobStatus {
|
|
100
|
+
return this.#status;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Creates a new job instance.
|
|
105
|
+
*
|
|
106
|
+
* @param id Optional job ID. If not provided, a random ID will be generated.
|
|
107
|
+
*/
|
|
108
|
+
constructor(id?: string) {
|
|
109
|
+
super();
|
|
110
|
+
this.id = id || this.generateJobId();
|
|
111
|
+
this.createdAt = new Date();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Run the job.
|
|
116
|
+
* This method handles the job lifecycle and calls the execute method.
|
|
117
|
+
*/
|
|
118
|
+
public async run(): Promise<void> {
|
|
119
|
+
try {
|
|
120
|
+
this.#status = JobStatus.RUNNING;
|
|
121
|
+
this.#startedAt = new Date();
|
|
122
|
+
|
|
123
|
+
this.debug(`Job ${this.id} started at ${this.#startedAt.toISOString()}`);
|
|
124
|
+
|
|
125
|
+
// Execute the job implementation
|
|
126
|
+
await this.execute();
|
|
127
|
+
|
|
128
|
+
this.#status = JobStatus.COMPLETED;
|
|
129
|
+
this.#completedAt = new Date();
|
|
130
|
+
|
|
131
|
+
this.debug(`Job ${this.id} completed at ${this.#completedAt.toISOString()}`);
|
|
132
|
+
} catch (err) {
|
|
133
|
+
this.#err = err instanceof Error ? err : new Error(String(err));
|
|
134
|
+
|
|
135
|
+
if (this.#status !== JobStatus.CANCELLED) {
|
|
136
|
+
this.#status = JobStatus.FAILED;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
this.#completedAt = new Date();
|
|
140
|
+
|
|
141
|
+
this.trace(this.#err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Cancels the job.
|
|
147
|
+
*/
|
|
148
|
+
public cancel(): void {
|
|
149
|
+
this.#status = JobStatus.CANCELLED;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
/**
|
|
153
|
+
* The main execution method for the job.
|
|
154
|
+
* This must be implemented by all job classes.
|
|
155
|
+
*/
|
|
156
|
+
protected abstract execute(): Promise<void>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Generate a random job ID.
|
|
160
|
+
* @returns A random job ID.
|
|
161
|
+
*/
|
|
162
|
+
private generateJobId(): string {
|
|
163
|
+
return `job_${Date.now()}_${uid(8)}`;
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Possible statuses for a job.
|
|
169
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
170
|
+
*/
|
|
171
|
+
export enum JobStatus {
|
|
172
|
+
/**
|
|
173
|
+
* The job is in pending state, meaning it has been added to the queue but has not yet been processed.
|
|
174
|
+
*/
|
|
175
|
+
PENDING = 'pending',
|
|
176
|
+
|
|
177
|
+
/**
|
|
178
|
+
* The job is in running state, meaning it is currently being processed.
|
|
179
|
+
*/
|
|
180
|
+
RUNNING = 'running',
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The job has completed successfully.
|
|
184
|
+
*/
|
|
185
|
+
COMPLETED = 'completed',
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The job has failed to complete successfully. If there are enough retries left, the job will be restarted.
|
|
189
|
+
*/
|
|
190
|
+
FAILED = 'failed',
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The job has been cancelled by the user or another job.
|
|
194
|
+
*/
|
|
195
|
+
CANCELLED = 'cancelled'
|
|
196
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
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
|
+
import type { Class } from 'type-fest';
|
|
17
|
+
import type { Context, Route } from './http';
|
|
18
|
+
|
|
19
|
+
import { Application } from './app';
|
|
20
|
+
import { Service } from './service';
|
|
21
|
+
import { Symbols } from './utils';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Execute a chain of middlewares, stopping on the first return or throw.
|
|
25
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
26
|
+
* @param middlewares The list of middleware instances to execute.
|
|
27
|
+
* @param context The context to pass to each middleware.
|
|
28
|
+
* @param method The method to call on each middleware.
|
|
29
|
+
*/
|
|
30
|
+
export const executeMiddlewareChain = <T extends Context>(
|
|
31
|
+
middlewares: Middleware[],
|
|
32
|
+
context: T,
|
|
33
|
+
method: keyof Middleware
|
|
34
|
+
): Promise<any> => {
|
|
35
|
+
return Application.context.run(
|
|
36
|
+
new Map([
|
|
37
|
+
['tenant', context.tenant],
|
|
38
|
+
['http:context', context]
|
|
39
|
+
]),
|
|
40
|
+
async () => {
|
|
41
|
+
for (const middleware of middlewares) {
|
|
42
|
+
try {
|
|
43
|
+
const result = await middleware[method](context);
|
|
44
|
+
if (result !== undefined) {
|
|
45
|
+
return result; // Stop the chain if middleware returns a value
|
|
46
|
+
}
|
|
47
|
+
} catch (error) {
|
|
48
|
+
throw error; // Stop the chain and propagate the error
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
);
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Apply a list of middlewares to an Elysia plugin.
|
|
57
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
58
|
+
* @param middlewares The list of middlewares to apply.
|
|
59
|
+
* @param plugin The Elysia plugin instance.
|
|
60
|
+
*/
|
|
61
|
+
export const applyMiddlewares = (middlewares: Class<Middleware>[], plugin: Elysia<Route>) => {
|
|
62
|
+
const mi = middlewares.map((middleware) => Service.make<Middleware>(middleware));
|
|
63
|
+
|
|
64
|
+
plugin.onBeforeHandle((c) => {
|
|
65
|
+
return executeMiddlewareChain(mi, c, 'onBeforeHandle');
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
plugin.onAfterHandle((c) => {
|
|
69
|
+
return executeMiddlewareChain(mi, c, 'onAfterHandle');
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
plugin.onAfterResponse((c) => {
|
|
73
|
+
return executeMiddlewareChain(mi, c, 'onAfterResponse');
|
|
74
|
+
});
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Base class for middleware.
|
|
79
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
80
|
+
*/
|
|
81
|
+
export abstract class Middleware {
|
|
82
|
+
/**
|
|
83
|
+
* Registers a list of middlewares to be applied on a controller or endpoint.
|
|
84
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
85
|
+
* @param middlewares The list of middlewares to apply.
|
|
86
|
+
*/
|
|
87
|
+
public static register(...middlewares: Class<Middleware>[]): ClassDecorator & MethodDecorator {
|
|
88
|
+
return function (
|
|
89
|
+
target: Object,
|
|
90
|
+
propertyKey?: string | symbol,
|
|
91
|
+
descriptor?: PropertyDescriptor
|
|
92
|
+
) {
|
|
93
|
+
if (propertyKey === undefined && descriptor === undefined) {
|
|
94
|
+
const m = Reflect.getMetadata(Symbols.middlewares, target) ?? [];
|
|
95
|
+
m.push(...middlewares);
|
|
96
|
+
Reflect.defineMetadata(Symbols.middlewares, m, target);
|
|
97
|
+
} else {
|
|
98
|
+
const m = Reflect.getMetadata(Symbols.middlewares, target, propertyKey!) ?? [];
|
|
99
|
+
m.push(...middlewares);
|
|
100
|
+
Reflect.defineMetadata(Symbols.middlewares, m, target, propertyKey!);
|
|
101
|
+
}
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
/**
|
|
106
|
+
* Called after the request has been handled, but before the response is sent.
|
|
107
|
+
* @param ctx The request context.
|
|
108
|
+
*/
|
|
109
|
+
public onAfterHandle(ctx: Context): any {}
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Called before the request is handled.
|
|
113
|
+
* @param ctx The request context.
|
|
114
|
+
*/
|
|
115
|
+
public onBeforeHandle(ctx: Context): any {}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Called after the response has been sent.
|
|
119
|
+
* @param ctx The request context.
|
|
120
|
+
*/
|
|
121
|
+
public onAfterResponse(ctx: Context): any {}
|
|
122
|
+
}
|
package/src/model.ts
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
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 { PgColumn, PgColumnBuilderBase, PgSchema, PgTable } from 'drizzle-orm/pg-core';
|
|
16
|
+
import type { TSchema } from 'elysia';
|
|
17
|
+
|
|
18
|
+
import { getTableConfig, pgSchema, pgTable } from 'drizzle-orm/pg-core';
|
|
19
|
+
import { t } from 'elysia';
|
|
20
|
+
|
|
21
|
+
import { Application } from './app';
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Creates a validation schema from a Drizzle table.
|
|
25
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
26
|
+
* @param table The table to create the schema from.
|
|
27
|
+
* @param options The schema generation options.
|
|
28
|
+
* @returns A bew validation schema based on the given table.
|
|
29
|
+
*/
|
|
30
|
+
export const createSchemaFromDrizzle = (
|
|
31
|
+
table: PgTable,
|
|
32
|
+
{ mode = 'select' }: { mode?: 'create' | 'update' | 'select' } = {}
|
|
33
|
+
) => {
|
|
34
|
+
const { columns } = getTableConfig(table);
|
|
35
|
+
if (columns === undefined) return t.Object({});
|
|
36
|
+
|
|
37
|
+
const properties: { [key: string]: TSchema } = {};
|
|
38
|
+
|
|
39
|
+
for (const element of columns) {
|
|
40
|
+
if (['create', 'update'].includes(mode) && element.primary) {
|
|
41
|
+
continue;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
properties[element.name] = parseTypes(element);
|
|
45
|
+
|
|
46
|
+
if (!element.notNull) {
|
|
47
|
+
properties[element.name] = t.Nullable(properties[element.name]);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
if (mode === 'update' || element.hasDefault) {
|
|
51
|
+
properties[element.name] = t.Optional(properties[element.name]);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return t.Object(properties);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Creates a validation schema from a Drizzle column.
|
|
60
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
61
|
+
* @param element The Drizzle column.
|
|
62
|
+
* @returns A validation schema based on the given column.
|
|
63
|
+
*/
|
|
64
|
+
const parseTypes = (element: PgColumn) => {
|
|
65
|
+
let type = (() => {
|
|
66
|
+
switch (element.dataType) {
|
|
67
|
+
case 'string':
|
|
68
|
+
return t.String();
|
|
69
|
+
case 'number':
|
|
70
|
+
return t.Number();
|
|
71
|
+
case 'boolean':
|
|
72
|
+
return t.Boolean();
|
|
73
|
+
case 'array':
|
|
74
|
+
return t.Array(t.Any());
|
|
75
|
+
case 'json':
|
|
76
|
+
return t.Object({});
|
|
77
|
+
case 'date':
|
|
78
|
+
case 'localDate':
|
|
79
|
+
case 'localDateTime':
|
|
80
|
+
return t.Date();
|
|
81
|
+
case 'bigint':
|
|
82
|
+
return t.BigInt();
|
|
83
|
+
case 'buffer':
|
|
84
|
+
return t.File();
|
|
85
|
+
default:
|
|
86
|
+
return t.Never();
|
|
87
|
+
}
|
|
88
|
+
})();
|
|
89
|
+
|
|
90
|
+
if (!element.notNull) {
|
|
91
|
+
type = t.Optional(type);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return type;
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Type of a repository class.
|
|
99
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
100
|
+
* @template TColumnsMap The table columns config.
|
|
101
|
+
* @template TTable The drizzle's table schema wrapped by the repository.
|
|
102
|
+
*/
|
|
103
|
+
export type ModelClass<
|
|
104
|
+
TColumnsMap extends Record<string, PgColumnBuilderBase>,
|
|
105
|
+
TTable extends PgTable = ReturnType<typeof pgTable<string, TColumnsMap>>
|
|
106
|
+
> = {
|
|
107
|
+
/**
|
|
108
|
+
* The data type returned by the select queries.
|
|
109
|
+
*/
|
|
110
|
+
readonly $inferSelect: TTable['$inferSelect'];
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The data type needed by the insert queries.
|
|
114
|
+
*/
|
|
115
|
+
readonly $inferInsert: TTable['$inferInsert'];
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* The data type needed by the update queries.
|
|
119
|
+
*/
|
|
120
|
+
readonly $inferUpdate: Partial<TTable['$inferSelect']>;
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The name of the table wrapped by the model.
|
|
124
|
+
*/
|
|
125
|
+
readonly tableName: string;
|
|
126
|
+
|
|
127
|
+
/**
|
|
128
|
+
* The table columns configuration.
|
|
129
|
+
*/
|
|
130
|
+
readonly columns: TColumnsMap;
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The validation schema for creating records.
|
|
134
|
+
*/
|
|
135
|
+
readonly createSchema: TSchema;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* The validation schema for updating records.
|
|
139
|
+
*/
|
|
140
|
+
readonly updateSchema: TSchema;
|
|
141
|
+
|
|
142
|
+
/**
|
|
143
|
+
* The validation schema for selecting records.
|
|
144
|
+
*/
|
|
145
|
+
readonly selectSchema: TSchema;
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Whether the model supports tenancy.
|
|
149
|
+
*
|
|
150
|
+
* Set it to `true` if the model supports tenancy. This means that the model will be
|
|
151
|
+
* wrapped by the `Tenancy.withTenant()` function when the current tenant is not `public`.
|
|
152
|
+
*/
|
|
153
|
+
readonly supportTenancy: boolean;
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Creates a new instance of the model.
|
|
157
|
+
* @param args The arguments to pass to the constructor.
|
|
158
|
+
* @returns A new instance of the model.
|
|
159
|
+
*/
|
|
160
|
+
new (...args: unknown[]): unknown;
|
|
161
|
+
};
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Mixin used to create a new model class.
|
|
165
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
166
|
+
* @template TColumnsMap The table columns config.
|
|
167
|
+
* @param tableName The name of the table.
|
|
168
|
+
* @param columns The table columns configuration.
|
|
169
|
+
*/
|
|
170
|
+
export const Model = <TColumnsMap extends Record<string, PgColumnBuilderBase>>(
|
|
171
|
+
tableName: string,
|
|
172
|
+
columns: TColumnsMap
|
|
173
|
+
) => {
|
|
174
|
+
const table = pgTable(tableName, columns);
|
|
175
|
+
|
|
176
|
+
class M {
|
|
177
|
+
/**
|
|
178
|
+
* The data type returned by the select queries.
|
|
179
|
+
*/
|
|
180
|
+
public static readonly $inferSelect = table.$inferSelect;
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* The data type needed by the insert queries.
|
|
184
|
+
*/
|
|
185
|
+
public static readonly $inferInsert = table.$inferInsert;
|
|
186
|
+
|
|
187
|
+
/**
|
|
188
|
+
* The data type needed by the update queries.
|
|
189
|
+
*/
|
|
190
|
+
public static readonly $inferUpdate: Partial<typeof table.$inferInsert> = table.$inferInsert;
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* The name of the table wrapped by the model.
|
|
194
|
+
*/
|
|
195
|
+
public static readonly tableName: string = tableName;
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* The table columns configuration.
|
|
199
|
+
*/
|
|
200
|
+
public static readonly columns: TColumnsMap = columns;
|
|
201
|
+
|
|
202
|
+
/**
|
|
203
|
+
* The validation schema for creating records.
|
|
204
|
+
*/
|
|
205
|
+
public static readonly createSchema = createSchemaFromDrizzle(table, { mode: 'create' });
|
|
206
|
+
|
|
207
|
+
/**
|
|
208
|
+
* The validation schema for updating records.
|
|
209
|
+
*/
|
|
210
|
+
public static readonly updateSchema = createSchemaFromDrizzle(table, { mode: 'update' });
|
|
211
|
+
|
|
212
|
+
/**
|
|
213
|
+
* The validation schema for selecting records.
|
|
214
|
+
*/
|
|
215
|
+
public static readonly selectSchema = createSchemaFromDrizzle(table, { mode: 'select' });
|
|
216
|
+
|
|
217
|
+
/**
|
|
218
|
+
* Whether the model supports tenancy.
|
|
219
|
+
*
|
|
220
|
+
* Set it to `true` if the model supports tenancy. This means that the model will be
|
|
221
|
+
* wrapped by the `Tenancy.withTenant()` function when the current tenant is not `public`.
|
|
222
|
+
*/
|
|
223
|
+
public static readonly supportTenancy: boolean = false;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return M;
|
|
227
|
+
};
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* A map of tables registered for each tenant.
|
|
231
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
232
|
+
*/
|
|
233
|
+
const tenantSchemas: Map<string, PgTable> = new Map();
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* A map of known tenant schemas.
|
|
237
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
238
|
+
*/
|
|
239
|
+
const schemaRegistry: Map<string, PgSchema> = new Map();
|
|
240
|
+
|
|
241
|
+
export namespace Tenancy {
|
|
242
|
+
/**
|
|
243
|
+
* Gets the schema for the given tenant.
|
|
244
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
245
|
+
* @param tenant The name of the tenant.
|
|
246
|
+
* @returns The tenant schema for the given tenant.
|
|
247
|
+
*/
|
|
248
|
+
const getTenantSchema = (tenant: string) => {
|
|
249
|
+
return schemaRegistry.get(tenant) ?? registerTenant(tenant);
|
|
250
|
+
};
|
|
251
|
+
|
|
252
|
+
/**
|
|
253
|
+
* Gets the name of the current tenant.
|
|
254
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
255
|
+
* @returns The name of the current tenant, or `null` if no tenant is set.
|
|
256
|
+
*/
|
|
257
|
+
export const getCurrentTenant = (): string | null => {
|
|
258
|
+
const store = Application.context.getStore();
|
|
259
|
+
return (store?.get('tenant') ?? null) as string | null;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
/**
|
|
263
|
+
* Sets the current tenant.
|
|
264
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
265
|
+
* @template T The type of the model class to wrap with the tenant.
|
|
266
|
+
* @template TColumnsMap The table columns config.
|
|
267
|
+
* @param tenant The name of the tenant.
|
|
268
|
+
* @param model The model class that wraps the `PgTable`.
|
|
269
|
+
* @returns The drizzle's table schema wrapped by the tenant.
|
|
270
|
+
*/
|
|
271
|
+
export const withTenant = <
|
|
272
|
+
T extends ModelClass<TColumnsMap>,
|
|
273
|
+
TColumnsMap extends Record<string, PgColumnBuilderBase> = T extends ModelClass<
|
|
274
|
+
infer TColumnsMap
|
|
275
|
+
>
|
|
276
|
+
? TColumnsMap
|
|
277
|
+
: Record<string, PgColumnBuilderBase>
|
|
278
|
+
>(
|
|
279
|
+
tenant: string,
|
|
280
|
+
model: T
|
|
281
|
+
) => {
|
|
282
|
+
if (tenant === 'public') {
|
|
283
|
+
return pgTable(model.tableName, model.columns);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
const tableName = `${tenant}.${model.tableName}`;
|
|
287
|
+
if (tenantSchemas.has(tableName)) {
|
|
288
|
+
return tenantSchemas.get(tableName)!;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
const tenantSchema = getTenantSchema(tenant);
|
|
292
|
+
const schemaTable = tenantSchema.table(model.tableName, model.columns);
|
|
293
|
+
tenantSchemas.set(tableName, schemaTable);
|
|
294
|
+
|
|
295
|
+
return schemaTable;
|
|
296
|
+
};
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Registers a new tenant schema.
|
|
300
|
+
* @author Axel Nana <axel.nana@workbud.com>
|
|
301
|
+
* @param tenant The name of the tenant.
|
|
302
|
+
* @returns A new schema for the given tenant.
|
|
303
|
+
*/
|
|
304
|
+
export const registerTenant = (tenant: string) => {
|
|
305
|
+
if (schemaRegistry.has(tenant)) return schemaRegistry.get(tenant)!;
|
|
306
|
+
|
|
307
|
+
const tenantSchema = pgSchema(tenant);
|
|
308
|
+
schemaRegistry.set(tenant, tenantSchema);
|
|
309
|
+
|
|
310
|
+
return tenantSchema;
|
|
311
|
+
};
|
|
312
|
+
}
|