@elysiumjs/mnemosyne 0.1.2 → 0.3.1
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/CHANGELOG.md +90 -0
- package/README.md +19 -16
- package/dist/cache.d.ts +134 -0
- package/dist/database.d.ts +127 -0
- package/dist/index.d.ts +9 -0
- package/dist/index.js +2 -0
- package/dist/interfaces.d.ts +360 -0
- package/dist/keyv-redis.d.ts +88 -0
- package/dist/mnemosyne.module.d.ts +44 -0
- package/dist/model.d.ts +68 -0
- package/dist/repository.d.ts +96 -0
- package/dist/tenancy.d.ts +201 -0
- package/package.json +21 -12
- package/bunfig.toml +0 -19
- package/src/cache.ts +0 -218
- package/src/database.ts +0 -151
- package/src/index.ts +0 -47
- package/src/interfaces.ts +0 -449
- package/src/keyv-redis.ts +0 -313
- package/src/model.ts +0 -189
- package/src/repository.ts +0 -137
- package/src/tenancy.ts +0 -291
- package/tests/cache.test.ts +0 -464
- package/tests/database.test.ts +0 -249
- package/tests/model.test.ts +0 -234
- package/tests/repository.test.ts +0 -177
- package/tests/setup.ts +0 -26
- package/tsconfig.json +0 -12
package/src/keyv-redis.ts
DELETED
|
@@ -1,313 +0,0 @@
|
|
|
1
|
-
// Copyright (c) 2026-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 { Event, Redis } from '@elysiumjs/core';
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Keyv adapter for redis. This adapter is based on the Bun's Redis client.
|
|
24
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
25
|
-
*/
|
|
26
|
-
export class KeyvRedis extends EventEmitter implements KeyvStoreAdapter {
|
|
27
|
-
#client?: Bun.RedisClient;
|
|
28
|
-
#namespace: string | undefined;
|
|
29
|
-
#keyPrefixSeparator = '::';
|
|
30
|
-
#clearBatchSize = 1000;
|
|
31
|
-
#useUnlink = true;
|
|
32
|
-
#noNamespaceAffectsAll = false;
|
|
33
|
-
|
|
34
|
-
#connection: string;
|
|
35
|
-
|
|
36
|
-
constructor(props: {
|
|
37
|
-
/**
|
|
38
|
-
* The Redis connection to use.
|
|
39
|
-
*/
|
|
40
|
-
connection: string;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Namespace for the connection.
|
|
44
|
-
*/
|
|
45
|
-
namespace?: string;
|
|
46
|
-
|
|
47
|
-
/**
|
|
48
|
-
* Separator to use between namespace and key.
|
|
49
|
-
*/
|
|
50
|
-
keyPrefixSeparator?: string;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* Number of keys to delete in a single batch.
|
|
54
|
-
*/
|
|
55
|
-
clearBatchSize?: number;
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Enable Unlink instead of using Del for clearing keys. This is more performant but may not be supported by all Redis versions.
|
|
59
|
-
*/
|
|
60
|
-
useUnlink?: boolean;
|
|
61
|
-
|
|
62
|
-
/**
|
|
63
|
-
* Whether to allow clearing all keys when no namespace is set.
|
|
64
|
-
* If set to true and no namespace is set, iterate() will return all keys.
|
|
65
|
-
* Defaults to `false`.
|
|
66
|
-
*/
|
|
67
|
-
noNamespaceAffectsAll?: boolean;
|
|
68
|
-
}) {
|
|
69
|
-
super();
|
|
70
|
-
|
|
71
|
-
Event.once('elysium:app:launched', () => {
|
|
72
|
-
this.#client = Redis.getConnection(props.connection);
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
this.#connection = props.connection;
|
|
76
|
-
this.#namespace = props.namespace;
|
|
77
|
-
this.#keyPrefixSeparator = props.keyPrefixSeparator ?? '::';
|
|
78
|
-
this.#clearBatchSize = props.clearBatchSize ?? 1000;
|
|
79
|
-
this.#useUnlink = props.useUnlink ?? true;
|
|
80
|
-
this.#noNamespaceAffectsAll = props.noNamespaceAffectsAll ?? false;
|
|
81
|
-
|
|
82
|
-
this.on('error', (error) => {
|
|
83
|
-
Event.emit('elysium:error', error);
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
public get client() {
|
|
88
|
-
return this.#client!;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Get the options for the adapter.
|
|
93
|
-
*/
|
|
94
|
-
public get opts() {
|
|
95
|
-
return {
|
|
96
|
-
namespace: this.#namespace,
|
|
97
|
-
keyPrefixSeparator: this.#keyPrefixSeparator,
|
|
98
|
-
clearBatchSize: this.#clearBatchSize,
|
|
99
|
-
noNamespaceAffectsAll: this.#noNamespaceAffectsAll,
|
|
100
|
-
dialect: 'redis',
|
|
101
|
-
url: this.#connection
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
|
|
105
|
-
/**
|
|
106
|
-
* Gets the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
|
|
107
|
-
* @default undefined
|
|
108
|
-
*/
|
|
109
|
-
public get namespace(): string | undefined {
|
|
110
|
-
return this.#namespace;
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
/**
|
|
114
|
-
* Sets the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
|
|
115
|
-
*/
|
|
116
|
-
public set namespace(value: string | undefined) {
|
|
117
|
-
this.#namespace = value;
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
public async get<Value>(key: string): Promise<StoredData<Value> | undefined> {
|
|
121
|
-
key = this.createKeyPrefix(key, this.#namespace);
|
|
122
|
-
const value = await this.client.get(key);
|
|
123
|
-
|
|
124
|
-
return value === null ? undefined : (value as Value);
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
public async set(key: string, value: any, ttl?: number) {
|
|
128
|
-
key = this.createKeyPrefix(key, this.#namespace);
|
|
129
|
-
|
|
130
|
-
if (ttl) {
|
|
131
|
-
await this.client.set(key, value, 'PX', ttl);
|
|
132
|
-
} else {
|
|
133
|
-
await this.client.set(key, value);
|
|
134
|
-
}
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
public async setMany(values: Array<KeyvEntry>): Promise<void> {
|
|
138
|
-
// TODO: Use MULTI when available in Bun
|
|
139
|
-
const multi = [];
|
|
140
|
-
|
|
141
|
-
for (const { key, value, ttl } of values) {
|
|
142
|
-
const prefixedKey = this.createKeyPrefix(key, this.#namespace);
|
|
143
|
-
if (ttl) {
|
|
144
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unsafe-argument
|
|
145
|
-
multi.push(this.client.set(prefixedKey, value, 'PX', ttl));
|
|
146
|
-
} else {
|
|
147
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
148
|
-
multi.push(this.client.set(prefixedKey, value));
|
|
149
|
-
}
|
|
150
|
-
}
|
|
151
|
-
|
|
152
|
-
await Promise.all(multi);
|
|
153
|
-
}
|
|
154
|
-
|
|
155
|
-
public async delete(key: string): Promise<boolean> {
|
|
156
|
-
key = this.createKeyPrefix(key, this.#namespace);
|
|
157
|
-
// TODO: Use UNLINK when available in Bun
|
|
158
|
-
const deleted = await (this.#useUnlink
|
|
159
|
-
? this.client.send('UNLINK', [key])
|
|
160
|
-
: this.client.del(key));
|
|
161
|
-
|
|
162
|
-
return deleted > 0;
|
|
163
|
-
}
|
|
164
|
-
|
|
165
|
-
public async clear(): Promise<void> {
|
|
166
|
-
try {
|
|
167
|
-
if (!this.#namespace && this.#noNamespaceAffectsAll) {
|
|
168
|
-
await this.client.send('FLUSHDB', []);
|
|
169
|
-
return;
|
|
170
|
-
}
|
|
171
|
-
|
|
172
|
-
let cursor = '0';
|
|
173
|
-
const batchSize = this.#clearBatchSize;
|
|
174
|
-
const match = this.#namespace ? `${this.#namespace}${this.#keyPrefixSeparator}*` : '*';
|
|
175
|
-
const deletePromises = [];
|
|
176
|
-
|
|
177
|
-
do {
|
|
178
|
-
const result = await this.client.send('SCAN', [
|
|
179
|
-
cursor,
|
|
180
|
-
'MATCH',
|
|
181
|
-
match,
|
|
182
|
-
'COUNT',
|
|
183
|
-
batchSize.toString(),
|
|
184
|
-
'TYPE',
|
|
185
|
-
'string'
|
|
186
|
-
]);
|
|
187
|
-
|
|
188
|
-
// Parse the result
|
|
189
|
-
let [newCursor, keys] = result;
|
|
190
|
-
cursor = newCursor.toString();
|
|
191
|
-
|
|
192
|
-
if (keys.length === 0) {
|
|
193
|
-
continue;
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
if (!this.#namespace) {
|
|
197
|
-
keys = keys.filter((key: string) => !key.includes(this.#keyPrefixSeparator));
|
|
198
|
-
}
|
|
199
|
-
|
|
200
|
-
deletePromises.push(keys.map((key: string) => this.client.del(key)));
|
|
201
|
-
} while (cursor !== '0');
|
|
202
|
-
|
|
203
|
-
await Promise.all(deletePromises);
|
|
204
|
-
} catch (error) {
|
|
205
|
-
this.emit('error', error);
|
|
206
|
-
}
|
|
207
|
-
}
|
|
208
|
-
|
|
209
|
-
public async has(key: string): Promise<boolean> {
|
|
210
|
-
key = this.createKeyPrefix(key, this.#namespace);
|
|
211
|
-
return this.client.exists(key);
|
|
212
|
-
}
|
|
213
|
-
|
|
214
|
-
public async hasMany(keys: string[]): Promise<boolean[]> {
|
|
215
|
-
const multi = [];
|
|
216
|
-
|
|
217
|
-
for (const key of keys) {
|
|
218
|
-
const prefixedKey = this.createKeyPrefix(key, this.#namespace);
|
|
219
|
-
multi.push(this.client.exists(prefixedKey));
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
return Promise.all(multi);
|
|
223
|
-
}
|
|
224
|
-
|
|
225
|
-
public async getMany<Value>(keys: string[]): Promise<Array<StoredData<Value | undefined>>> {
|
|
226
|
-
if (keys.length === 0) {
|
|
227
|
-
return [];
|
|
228
|
-
}
|
|
229
|
-
|
|
230
|
-
keys = keys.map((key) => this.createKeyPrefix(key, this.#namespace));
|
|
231
|
-
const values = await this.client.mget(...keys);
|
|
232
|
-
|
|
233
|
-
return values as Value[];
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
public async disconnect(): Promise<void> {
|
|
237
|
-
this.client.close();
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
public async deleteMany(keys: string[]): Promise<boolean> {
|
|
241
|
-
let result = false;
|
|
242
|
-
|
|
243
|
-
// TODO: Use MULTI when available in Bun
|
|
244
|
-
const multi = [];
|
|
245
|
-
|
|
246
|
-
for (const key of keys) {
|
|
247
|
-
const prefixedKey = this.createKeyPrefix(key, this.#namespace);
|
|
248
|
-
if (this.#useUnlink) {
|
|
249
|
-
multi.push(this.client.send('UNLINK', [prefixedKey]));
|
|
250
|
-
} else {
|
|
251
|
-
multi.push(this.client.del(prefixedKey));
|
|
252
|
-
}
|
|
253
|
-
}
|
|
254
|
-
|
|
255
|
-
const results = await Promise.all(multi);
|
|
256
|
-
|
|
257
|
-
for (const deleted of results) {
|
|
258
|
-
if (typeof deleted === 'number' && deleted > 0) {
|
|
259
|
-
result = true;
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
|
|
263
|
-
return result;
|
|
264
|
-
}
|
|
265
|
-
|
|
266
|
-
public async *iterator<Value>(
|
|
267
|
-
namespace?: string
|
|
268
|
-
): AsyncGenerator<Array<string | Awaited<Value> | undefined>, void> {
|
|
269
|
-
const match = namespace ? `${namespace}${this.#keyPrefixSeparator}*` : '*';
|
|
270
|
-
let cursor = '0';
|
|
271
|
-
|
|
272
|
-
do {
|
|
273
|
-
const result = await this.client.send('SCAN', [cursor, 'MATCH', match, 'TYPE', 'string']);
|
|
274
|
-
|
|
275
|
-
// Parse the result
|
|
276
|
-
let [newCursor, keys] = result;
|
|
277
|
-
cursor = newCursor.toString();
|
|
278
|
-
|
|
279
|
-
if (!namespace && !this.#noNamespaceAffectsAll) {
|
|
280
|
-
keys = keys.filter((key: string) => !key.includes(this.#keyPrefixSeparator));
|
|
281
|
-
}
|
|
282
|
-
|
|
283
|
-
if (keys.length > 0) {
|
|
284
|
-
const values = await this.client.mget(...keys);
|
|
285
|
-
for (const i of keys.keys()) {
|
|
286
|
-
const key = this.getKeyWithoutPrefix(keys[i], namespace);
|
|
287
|
-
const value = values[i];
|
|
288
|
-
yield [key, value!];
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
} while (cursor !== '0');
|
|
292
|
-
}
|
|
293
|
-
|
|
294
|
-
/**
|
|
295
|
-
* Helper function to create a key with a namespace.
|
|
296
|
-
* @param key The key to prefix.
|
|
297
|
-
* @param namespace The namespace to prefix the key with.
|
|
298
|
-
* @returns The key with the namespace such as 'namespace::key'.
|
|
299
|
-
*/
|
|
300
|
-
public createKeyPrefix(key: string, namespace?: string): string {
|
|
301
|
-
return !!namespace ? `${namespace}${this.#keyPrefixSeparator}${key}` : key;
|
|
302
|
-
}
|
|
303
|
-
|
|
304
|
-
/**
|
|
305
|
-
* Helper function to remove the namespace from a key.
|
|
306
|
-
* @param key The key to remove the namespace from.
|
|
307
|
-
* @param namespace The namespace to remove from the key.
|
|
308
|
-
* @returns The key without the namespace.
|
|
309
|
-
*/
|
|
310
|
-
public getKeyWithoutPrefix(key: string, namespace?: string): string {
|
|
311
|
-
return !!namespace ? key.replace(`${namespace}${this.#keyPrefixSeparator}`, '') : key;
|
|
312
|
-
}
|
|
313
|
-
}
|
package/src/model.ts
DELETED
|
@@ -1,189 +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 { TSchema } from 'elysia';
|
|
16
|
-
import type { ColumnMetadata, ModelAdapter } from './interfaces';
|
|
17
|
-
|
|
18
|
-
import { Application } from '@elysiumjs/core';
|
|
19
|
-
import { t } from 'elysia';
|
|
20
|
-
|
|
21
|
-
import { getCurrentTenant, getTenancyStrategy } from './tenancy';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Creates a validation schema from normalized column metadata.
|
|
25
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
26
|
-
* @param columns The column metadata to create the schema from.
|
|
27
|
-
* @param options The schema generation options.
|
|
28
|
-
* @returns A validation schema based on the given columns.
|
|
29
|
-
*/
|
|
30
|
-
export const createSchemaFromModel = (
|
|
31
|
-
columns: ColumnMetadata[],
|
|
32
|
-
{ mode = 'select' }: { mode?: 'create' | 'update' | 'select' } = {}
|
|
33
|
-
) => {
|
|
34
|
-
if (columns === undefined || columns.length === 0) return t.Object({});
|
|
35
|
-
|
|
36
|
-
const properties: { [key: string]: TSchema } = {};
|
|
37
|
-
|
|
38
|
-
for (const column of columns) {
|
|
39
|
-
if (['create', 'update'].includes(mode) && column.isPrimaryKey) {
|
|
40
|
-
continue;
|
|
41
|
-
}
|
|
42
|
-
|
|
43
|
-
properties[column.name] = parseColumnType(column);
|
|
44
|
-
|
|
45
|
-
if (column.nullable) {
|
|
46
|
-
properties[column.name] = t.Nullable(properties[column.name]);
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
if (mode === 'update' || column.hasDefault || column.nullable) {
|
|
50
|
-
properties[column.name] = t.Optional(properties[column.name]);
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
|
|
54
|
-
return t.Object(properties);
|
|
55
|
-
};
|
|
56
|
-
|
|
57
|
-
/**
|
|
58
|
-
* Creates a validation schema from a column's metadata.
|
|
59
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
60
|
-
* @param column The column metadata.
|
|
61
|
-
* @returns A validation schema based on the given column.
|
|
62
|
-
*/
|
|
63
|
-
const parseColumnType = (column: ColumnMetadata) => {
|
|
64
|
-
switch (column.dataType) {
|
|
65
|
-
case 'string':
|
|
66
|
-
return t.String();
|
|
67
|
-
case 'uuid':
|
|
68
|
-
return t.String({ format: 'uuid' });
|
|
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
|
-
return t
|
|
79
|
-
.Transform(t.String({ format: 'date' }))
|
|
80
|
-
.Decode((date) => new Date(date))
|
|
81
|
-
.Encode((date) => date.toISOString());
|
|
82
|
-
case 'datetime':
|
|
83
|
-
return t
|
|
84
|
-
.Transform(t.String({ format: 'date-time' }))
|
|
85
|
-
.Decode((date) => new Date(date))
|
|
86
|
-
.Encode((date) => date.toISOString());
|
|
87
|
-
case 'bigint':
|
|
88
|
-
return t.BigInt();
|
|
89
|
-
case 'buffer':
|
|
90
|
-
return t.File();
|
|
91
|
-
default:
|
|
92
|
-
return t.Never();
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/**
|
|
97
|
-
* Mixin used to create a new abstract model class.
|
|
98
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
99
|
-
* @template TTable The ORM-specific table type.
|
|
100
|
-
* @template TColumns The table columns configuration type.
|
|
101
|
-
* @param tableName The name of the table.
|
|
102
|
-
* @param columns The table columns configuration.
|
|
103
|
-
* @param adapter The model adapter for the ORM.
|
|
104
|
-
* @param baseTable The base ORM table instance.
|
|
105
|
-
*/
|
|
106
|
-
export const AbstractModel = <TTable, TColumns extends Record<string, unknown>>(
|
|
107
|
-
tableName: string,
|
|
108
|
-
columns: TColumns,
|
|
109
|
-
adapter: ModelAdapter<TTable>,
|
|
110
|
-
baseTable: TTable
|
|
111
|
-
) => {
|
|
112
|
-
const columnMetadata = adapter.getColumns(baseTable);
|
|
113
|
-
|
|
114
|
-
class M {
|
|
115
|
-
/**
|
|
116
|
-
* The data type returned by the select queries.
|
|
117
|
-
*/
|
|
118
|
-
public static readonly $inferSelect: unknown = undefined;
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* The data type needed by the insert queries.
|
|
122
|
-
*/
|
|
123
|
-
public static readonly $inferInsert: unknown = undefined;
|
|
124
|
-
|
|
125
|
-
/**
|
|
126
|
-
* The data type needed by the update queries.
|
|
127
|
-
*/
|
|
128
|
-
public static readonly $inferUpdate: unknown = undefined;
|
|
129
|
-
|
|
130
|
-
/**
|
|
131
|
-
* The ORM-specific table schema wrapped by this model.
|
|
132
|
-
* Will be automatically resolved to a tenant-scoped table when tenancy is active.
|
|
133
|
-
*/
|
|
134
|
-
public static get table(): TTable {
|
|
135
|
-
// If we are not inside an Application context, we can't use the tenancy system
|
|
136
|
-
if (!Application.instance || !Application.context.getStore) {
|
|
137
|
-
return baseTable;
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
if (this.supportTenancy) {
|
|
141
|
-
const tenant = getCurrentTenant() ?? 'public';
|
|
142
|
-
if (tenant !== 'public') {
|
|
143
|
-
const strategy = getTenancyStrategy();
|
|
144
|
-
if (strategy) {
|
|
145
|
-
return strategy.resolveTable(baseTable, tenant) as TTable;
|
|
146
|
-
}
|
|
147
|
-
return adapter.createTenantTable(baseTable, tenant);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
return baseTable;
|
|
152
|
-
}
|
|
153
|
-
|
|
154
|
-
/**
|
|
155
|
-
* The name of the table wrapped by the model.
|
|
156
|
-
*/
|
|
157
|
-
public static readonly tableName: string = tableName;
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* The table columns configuration.
|
|
161
|
-
*/
|
|
162
|
-
public static readonly columns: TColumns = columns;
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* The validation schema for creating records.
|
|
166
|
-
*/
|
|
167
|
-
public static readonly insertSchema = createSchemaFromModel(columnMetadata, { mode: 'create' });
|
|
168
|
-
|
|
169
|
-
/**
|
|
170
|
-
* The validation schema for updating records.
|
|
171
|
-
*/
|
|
172
|
-
public static readonly updateSchema = createSchemaFromModel(columnMetadata, { mode: 'update' });
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* The validation schema for selecting records.
|
|
176
|
-
*/
|
|
177
|
-
public static readonly selectSchema = createSchemaFromModel(columnMetadata, { mode: 'select' });
|
|
178
|
-
|
|
179
|
-
/**
|
|
180
|
-
* Whether the model supports tenancy.
|
|
181
|
-
*
|
|
182
|
-
* Set it to `true` if the model supports tenancy. This means that the model will be
|
|
183
|
-
* wrapped by the tenancy strategy when the current tenant is not `public`.
|
|
184
|
-
*/
|
|
185
|
-
public static readonly supportTenancy: boolean = false;
|
|
186
|
-
}
|
|
187
|
-
|
|
188
|
-
return M;
|
|
189
|
-
};
|
package/src/repository.ts
DELETED
|
@@ -1,137 +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 { AbstractDatabase } from './database';
|
|
16
|
-
import type { IdType, ModelClass, RepositoryInterface } from './interfaces';
|
|
17
|
-
|
|
18
|
-
/**
|
|
19
|
-
* Mixin used to create a new abstract repository class over a model.
|
|
20
|
-
*
|
|
21
|
-
* All CRUD methods are abstract and must be implemented by driver-specific
|
|
22
|
-
* subclasses.
|
|
23
|
-
*
|
|
24
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
25
|
-
* @template TModel The type of the model class wrapped by the repository.
|
|
26
|
-
* @template TConnection The database connection type.
|
|
27
|
-
* @param model The model class wrapped by the repository.
|
|
28
|
-
* @param database The database manager instance.
|
|
29
|
-
*/
|
|
30
|
-
export const AbstractRepository = <TModel extends ModelClass, TConnection>(
|
|
31
|
-
model: TModel,
|
|
32
|
-
database: AbstractDatabase<TConnection, unknown>
|
|
33
|
-
) => {
|
|
34
|
-
type TSelect = TModel['$inferSelect'];
|
|
35
|
-
type TInsert = TModel['$inferInsert'];
|
|
36
|
-
type TUpdate = TModel['$inferUpdate'];
|
|
37
|
-
|
|
38
|
-
abstract class R implements RepositoryInterface<TSelect, TInsert, TUpdate> {
|
|
39
|
-
/**
|
|
40
|
-
* The model class wrapped by this repository.
|
|
41
|
-
*/
|
|
42
|
-
public static readonly Model: TModel = model;
|
|
43
|
-
|
|
44
|
-
/**
|
|
45
|
-
* The database connection name to use.
|
|
46
|
-
*
|
|
47
|
-
* Update this value to change the database connection used by this repository.
|
|
48
|
-
*/
|
|
49
|
-
public static readonly connection: string = 'default';
|
|
50
|
-
|
|
51
|
-
/**
|
|
52
|
-
* The database connection used by this repository.
|
|
53
|
-
*/
|
|
54
|
-
public get db(): TConnection {
|
|
55
|
-
return database.getConnection((this.constructor as typeof R).connection);
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Retrieves all the records in the database.
|
|
60
|
-
* @returns All the records in the database.
|
|
61
|
-
*/
|
|
62
|
-
public abstract all(): Promise<TSelect[]>;
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* Get a paginated subset of the records in the database.
|
|
66
|
-
* @param count The number of records to retrieve per page.
|
|
67
|
-
* @param page The page number to retrieve.
|
|
68
|
-
* @returns A paginated response containing the records and the total number of records.
|
|
69
|
-
*/
|
|
70
|
-
public abstract paginate(
|
|
71
|
-
count: number,
|
|
72
|
-
page?: number
|
|
73
|
-
): Promise<{ page: number; data: TSelect[]; total: number }>;
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
* Retrieves a record by its id.
|
|
77
|
-
* @param id The id of the record to retrieve.
|
|
78
|
-
* @returns The record with the given id, or `null` if not found.
|
|
79
|
-
*/
|
|
80
|
-
public abstract find(id: IdType): Promise<TSelect | null>;
|
|
81
|
-
|
|
82
|
-
/**
|
|
83
|
-
* Finds the first record with a column matching the given value.
|
|
84
|
-
* @param column The column to filter by.
|
|
85
|
-
* @param value The value the column should match.
|
|
86
|
-
* @returns The first record with a column matching the given value.
|
|
87
|
-
*/
|
|
88
|
-
public abstract findBy<TKey extends keyof TSelect>(
|
|
89
|
-
column: TKey,
|
|
90
|
-
value: TSelect[TKey]
|
|
91
|
-
): Promise<TSelect | null>;
|
|
92
|
-
|
|
93
|
-
/**
|
|
94
|
-
* Inserts a new record in the database.
|
|
95
|
-
* @param data The data to insert.
|
|
96
|
-
* @returns The inserted record.
|
|
97
|
-
*/
|
|
98
|
-
public abstract insert(data: TInsert): Promise<TSelect>;
|
|
99
|
-
|
|
100
|
-
/**
|
|
101
|
-
* Updates a record in the database.
|
|
102
|
-
* @param id The id of the record to update.
|
|
103
|
-
* @param data The data to update.
|
|
104
|
-
* @returns The updated record.
|
|
105
|
-
*/
|
|
106
|
-
public abstract update(id: IdType, data: TUpdate): Promise<TSelect>;
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Updates all the records in the database.
|
|
110
|
-
* @param data The data to update.
|
|
111
|
-
* @returns The updated records.
|
|
112
|
-
*/
|
|
113
|
-
public abstract updateAll(data: TUpdate): Promise<TSelect[]>;
|
|
114
|
-
|
|
115
|
-
/**
|
|
116
|
-
* Deletes a record from the database.
|
|
117
|
-
* @param id The ID of the record to delete.
|
|
118
|
-
* @returns The deleted record.
|
|
119
|
-
*/
|
|
120
|
-
public abstract delete(id: IdType): Promise<TSelect>;
|
|
121
|
-
|
|
122
|
-
/**
|
|
123
|
-
* Deletes all the records from the database.
|
|
124
|
-
* @returns All the deleted records.
|
|
125
|
-
*/
|
|
126
|
-
public abstract deleteAll(): Promise<TSelect[]>;
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Checks if a record exists in the database.
|
|
130
|
-
* @param id The ID of the record to check.
|
|
131
|
-
* @returns True if the record exists, false otherwise.
|
|
132
|
-
*/
|
|
133
|
-
public abstract exists(id: IdType): Promise<boolean>;
|
|
134
|
-
}
|
|
135
|
-
|
|
136
|
-
return R;
|
|
137
|
-
};
|