@elysiumjs/mnemosyne 0.2.0 → 0.4.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/CHANGELOG.md +165 -0
- package/README.md +12 -0
- package/dist/cache.d.ts +21 -4
- package/dist/cache.interceptor.d.ts +36 -0
- package/dist/database.d.ts +64 -3
- package/dist/index.d.ts +5 -1
- package/dist/index.js +1 -1
- package/dist/interfaces.d.ts +17 -7
- package/dist/keyv-redis.d.ts +3 -2
- package/dist/mnemosyne.module.d.ts +9 -3
- package/dist/model.d.ts +4 -4
- package/dist/query.d.ts +150 -0
- package/dist/repository.d.ts +4 -4
- package/dist/resource.d.ts +35 -0
- package/dist/tenancy.d.ts +25 -3
- package/dist/unit-of-work.d.ts +68 -0
- package/package.json +18 -6
- package/.turbo/turbo-build.log +0 -8
- package/src/cache.ts +0 -219
- package/src/database.ts +0 -114
- package/src/index.ts +0 -37
- package/src/interfaces.ts +0 -449
- package/src/keyv-redis.ts +0 -326
- package/src/mnemosyne.module.ts +0 -83
- package/src/model.ts +0 -189
- package/src/repository.ts +0 -137
- package/src/tenancy.ts +0 -274
- package/tests/cache.test.ts +0 -505
- package/tests/database.test.ts +0 -113
- package/tests/mnemosyne.module.test.ts +0 -214
- package/tests/model.test.ts +0 -234
- package/tests/repository.test.ts +0 -177
- package/tests/setup.ts +0 -13
- package/tsconfig.json +0 -12
package/src/keyv-redis.ts
DELETED
|
@@ -1,326 +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, Service } 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
|
-
this.#connection = props.connection;
|
|
72
|
-
this.#namespace = props.namespace;
|
|
73
|
-
this.#keyPrefixSeparator = props.keyPrefixSeparator ?? '::';
|
|
74
|
-
this.#clearBatchSize = props.clearBatchSize ?? 1000;
|
|
75
|
-
this.#useUnlink = props.useUnlink ?? true;
|
|
76
|
-
this.#noNamespaceAffectsAll = props.noNamespaceAffectsAll ?? false;
|
|
77
|
-
|
|
78
|
-
this.on('error', (error) => {
|
|
79
|
-
Event.emit('elysium:error', error);
|
|
80
|
-
});
|
|
81
|
-
}
|
|
82
|
-
|
|
83
|
-
/**
|
|
84
|
-
* Resolves the Redis client on first use. The async path is always safe, even when the
|
|
85
|
-
* Redis module has not been initialised yet (lazy boot).
|
|
86
|
-
*/
|
|
87
|
-
protected async getClient(): Promise<Bun.RedisClient> {
|
|
88
|
-
this.#client ??= await Service.resolve<Bun.RedisClient>(
|
|
89
|
-
Redis.getConnectionName(this.#connection)
|
|
90
|
-
);
|
|
91
|
-
return this.#client;
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
/**
|
|
95
|
-
* Get the options for the adapter.
|
|
96
|
-
*/
|
|
97
|
-
public get opts() {
|
|
98
|
-
return {
|
|
99
|
-
namespace: this.#namespace,
|
|
100
|
-
keyPrefixSeparator: this.#keyPrefixSeparator,
|
|
101
|
-
clearBatchSize: this.#clearBatchSize,
|
|
102
|
-
noNamespaceAffectsAll: this.#noNamespaceAffectsAll,
|
|
103
|
-
dialect: 'redis',
|
|
104
|
-
url: this.#connection
|
|
105
|
-
};
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Gets the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
|
|
110
|
-
* @default undefined
|
|
111
|
-
*/
|
|
112
|
-
public get namespace(): string | undefined {
|
|
113
|
-
return this.#namespace;
|
|
114
|
-
}
|
|
115
|
-
|
|
116
|
-
/**
|
|
117
|
-
* Sets the namespace for the adapter. If undefined, it will not use a namespace including keyPrefixing.
|
|
118
|
-
*/
|
|
119
|
-
public set namespace(value: string | undefined) {
|
|
120
|
-
this.#namespace = value;
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
public async get<Value>(key: string): Promise<StoredData<Value> | undefined> {
|
|
124
|
-
const client = await this.getClient();
|
|
125
|
-
key = this.createKeyPrefix(key, this.#namespace);
|
|
126
|
-
const value = await client.get(key);
|
|
127
|
-
|
|
128
|
-
return value === null ? undefined : (value as Value);
|
|
129
|
-
}
|
|
130
|
-
|
|
131
|
-
public async set(key: string, value: any, ttl?: number) {
|
|
132
|
-
const client = await this.getClient();
|
|
133
|
-
key = this.createKeyPrefix(key, this.#namespace);
|
|
134
|
-
|
|
135
|
-
if (ttl) {
|
|
136
|
-
await client.set(key, value, 'PX', ttl);
|
|
137
|
-
} else {
|
|
138
|
-
await client.set(key, value);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
public async setMany(values: Array<KeyvEntry>): Promise<void> {
|
|
143
|
-
const client = await this.getClient();
|
|
144
|
-
// TODO: Use MULTI when available in Bun
|
|
145
|
-
const multi = [];
|
|
146
|
-
|
|
147
|
-
for (const { key, value, ttl } of values) {
|
|
148
|
-
const prefixedKey = this.createKeyPrefix(key, this.#namespace);
|
|
149
|
-
if (ttl) {
|
|
150
|
-
// eslint-disable-next-line @typescript-eslint/naming-convention, @typescript-eslint/no-unsafe-argument
|
|
151
|
-
multi.push(client.set(prefixedKey, value, 'PX', ttl));
|
|
152
|
-
} else {
|
|
153
|
-
// eslint-disable-next-line @typescript-eslint/no-unsafe-argument
|
|
154
|
-
multi.push(client.set(prefixedKey, value));
|
|
155
|
-
}
|
|
156
|
-
}
|
|
157
|
-
|
|
158
|
-
await Promise.all(multi);
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
public async delete(key: string): Promise<boolean> {
|
|
162
|
-
const client = await this.getClient();
|
|
163
|
-
key = this.createKeyPrefix(key, this.#namespace);
|
|
164
|
-
// TODO: Use UNLINK when available in Bun
|
|
165
|
-
const deleted = await (this.#useUnlink ? client.send('UNLINK', [key]) : client.del(key));
|
|
166
|
-
|
|
167
|
-
return deleted > 0;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
public async clear(): Promise<void> {
|
|
171
|
-
try {
|
|
172
|
-
const client = await this.getClient();
|
|
173
|
-
|
|
174
|
-
if (!this.#namespace && this.#noNamespaceAffectsAll) {
|
|
175
|
-
await client.send('FLUSHDB', []);
|
|
176
|
-
return;
|
|
177
|
-
}
|
|
178
|
-
|
|
179
|
-
let cursor = '0';
|
|
180
|
-
const batchSize = this.#clearBatchSize;
|
|
181
|
-
const match = this.#namespace ? `${this.#namespace}${this.#keyPrefixSeparator}*` : '*';
|
|
182
|
-
const deletePromises = [];
|
|
183
|
-
|
|
184
|
-
do {
|
|
185
|
-
const result = await client.send('SCAN', [
|
|
186
|
-
cursor,
|
|
187
|
-
'MATCH',
|
|
188
|
-
match,
|
|
189
|
-
'COUNT',
|
|
190
|
-
batchSize.toString(),
|
|
191
|
-
'TYPE',
|
|
192
|
-
'string'
|
|
193
|
-
]);
|
|
194
|
-
|
|
195
|
-
// Parse the result
|
|
196
|
-
let [newCursor, keys] = result;
|
|
197
|
-
cursor = newCursor.toString();
|
|
198
|
-
|
|
199
|
-
if (keys.length === 0) {
|
|
200
|
-
continue;
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
if (!this.#namespace) {
|
|
204
|
-
keys = keys.filter((key: string) => !key.includes(this.#keyPrefixSeparator));
|
|
205
|
-
}
|
|
206
|
-
|
|
207
|
-
deletePromises.push(keys.map((key: string) => client.del(key)));
|
|
208
|
-
} while (cursor !== '0');
|
|
209
|
-
|
|
210
|
-
await Promise.all(deletePromises);
|
|
211
|
-
} catch (error) {
|
|
212
|
-
this.emit('error', error);
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
public async has(key: string): Promise<boolean> {
|
|
217
|
-
const client = await this.getClient();
|
|
218
|
-
key = this.createKeyPrefix(key, this.#namespace);
|
|
219
|
-
return client.exists(key);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
public async hasMany(keys: string[]): Promise<boolean[]> {
|
|
223
|
-
const client = await this.getClient();
|
|
224
|
-
const multi = [];
|
|
225
|
-
|
|
226
|
-
for (const key of keys) {
|
|
227
|
-
const prefixedKey = this.createKeyPrefix(key, this.#namespace);
|
|
228
|
-
multi.push(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
|
-
const client = await this.getClient();
|
|
236
|
-
if (keys.length === 0) {
|
|
237
|
-
return [];
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
keys = keys.map((key) => this.createKeyPrefix(key, this.#namespace));
|
|
241
|
-
const values = await client.mget(...keys);
|
|
242
|
-
|
|
243
|
-
return values as Value[];
|
|
244
|
-
}
|
|
245
|
-
|
|
246
|
-
public async disconnect(): Promise<void> {
|
|
247
|
-
// The connection is owned by the Redis module; only drop our reference to it.
|
|
248
|
-
this.#client = undefined;
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
public async deleteMany(keys: string[]): Promise<boolean> {
|
|
252
|
-
const client = await this.getClient();
|
|
253
|
-
let result = false;
|
|
254
|
-
|
|
255
|
-
// TODO: Use MULTI when available in Bun
|
|
256
|
-
const multi = [];
|
|
257
|
-
|
|
258
|
-
for (const key of keys) {
|
|
259
|
-
const prefixedKey = this.createKeyPrefix(key, this.#namespace);
|
|
260
|
-
if (this.#useUnlink) {
|
|
261
|
-
multi.push(client.send('UNLINK', [prefixedKey]));
|
|
262
|
-
} else {
|
|
263
|
-
multi.push(client.del(prefixedKey));
|
|
264
|
-
}
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
const results = await Promise.all(multi);
|
|
268
|
-
|
|
269
|
-
for (const deleted of results) {
|
|
270
|
-
if (typeof deleted === 'number' && deleted > 0) {
|
|
271
|
-
result = true;
|
|
272
|
-
}
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
return result;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
public async *iterator<Value>(
|
|
279
|
-
namespace?: string
|
|
280
|
-
): AsyncGenerator<Array<string | Awaited<Value> | undefined>, void> {
|
|
281
|
-
const client = await this.getClient();
|
|
282
|
-
const match = namespace ? `${namespace}${this.#keyPrefixSeparator}*` : '*';
|
|
283
|
-
let cursor = '0';
|
|
284
|
-
|
|
285
|
-
do {
|
|
286
|
-
const result = await client.send('SCAN', [cursor, 'MATCH', match, 'TYPE', 'string']);
|
|
287
|
-
|
|
288
|
-
// Parse the result
|
|
289
|
-
let [newCursor, keys] = result;
|
|
290
|
-
cursor = newCursor.toString();
|
|
291
|
-
|
|
292
|
-
if (!namespace && !this.#noNamespaceAffectsAll) {
|
|
293
|
-
keys = keys.filter((key: string) => !key.includes(this.#keyPrefixSeparator));
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
if (keys.length > 0) {
|
|
297
|
-
const values = await client.mget(...keys);
|
|
298
|
-
for (const i of keys.keys()) {
|
|
299
|
-
const key = this.getKeyWithoutPrefix(keys[i], namespace);
|
|
300
|
-
const value = values[i];
|
|
301
|
-
yield [key, value!];
|
|
302
|
-
}
|
|
303
|
-
}
|
|
304
|
-
} while (cursor !== '0');
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
/**
|
|
308
|
-
* Helper function to create a key with a namespace.
|
|
309
|
-
* @param key The key to prefix.
|
|
310
|
-
* @param namespace The namespace to prefix the key with.
|
|
311
|
-
* @returns The key with the namespace such as 'namespace::key'.
|
|
312
|
-
*/
|
|
313
|
-
public createKeyPrefix(key: string, namespace?: string): string {
|
|
314
|
-
return !!namespace ? `${namespace}${this.#keyPrefixSeparator}${key}` : key;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
/**
|
|
318
|
-
* Helper function to remove the namespace from a key.
|
|
319
|
-
* @param key The key to remove the namespace from.
|
|
320
|
-
* @param namespace The namespace to remove from the key.
|
|
321
|
-
* @returns The key without the namespace.
|
|
322
|
-
*/
|
|
323
|
-
public getKeyWithoutPrefix(key: string, namespace?: string): string {
|
|
324
|
-
return !!namespace ? key.replace(`${namespace}${this.#keyPrefixSeparator}`, '') : key;
|
|
325
|
-
}
|
|
326
|
-
}
|
package/src/mnemosyne.module.ts
DELETED
|
@@ -1,83 +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 { ServiceFactoryDefinition } from '@elysiumjs/core';
|
|
16
|
-
|
|
17
|
-
import { Module } from '@elysiumjs/core';
|
|
18
|
-
import { KeyvCacheableMemory } from 'cacheable';
|
|
19
|
-
|
|
20
|
-
import { makeCacheInterface, MEMORY_CACHE_SERVICE, REDIS_CACHE_SERVICE } from './cache';
|
|
21
|
-
import { KeyvRedis } from './keyv-redis';
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* Configuration of the {@link MnemosyneModule}.
|
|
25
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
26
|
-
*/
|
|
27
|
-
export type MnemosyneModuleConfig = {
|
|
28
|
-
cache: {
|
|
29
|
-
/** The Redis-backed cache. Set to `false` to not provide `Cache.redis`. */
|
|
30
|
-
redis?:
|
|
31
|
-
| {
|
|
32
|
-
/** The name of the Redis connection to use. */
|
|
33
|
-
connection: string;
|
|
34
|
-
}
|
|
35
|
-
| false;
|
|
36
|
-
|
|
37
|
-
/** The memory cache. Set to `false` to not provide `Cache.memory`. */
|
|
38
|
-
memory?:
|
|
39
|
-
| {
|
|
40
|
-
/** The default TTL of entries, in milliseconds. */
|
|
41
|
-
ttl: number;
|
|
42
|
-
|
|
43
|
-
/** The maximum number of entries kept. */
|
|
44
|
-
lruSize: number;
|
|
45
|
-
}
|
|
46
|
-
| false;
|
|
47
|
-
};
|
|
48
|
-
};
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Provides the caches as the global services `mnemosyne.cache.redis` and
|
|
52
|
-
* `mnemosyne.cache.memory`. Tenancy config and strategy, and database connections, are
|
|
53
|
-
* registered by the database driver modules.
|
|
54
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
55
|
-
*/
|
|
56
|
-
@Module.register<MnemosyneModuleConfig>({
|
|
57
|
-
services: (config) => {
|
|
58
|
-
const services: ServiceFactoryDefinition[] = [];
|
|
59
|
-
const { redis, memory } = config.cache;
|
|
60
|
-
|
|
61
|
-
if (redis) {
|
|
62
|
-
services.push({
|
|
63
|
-
name: REDIS_CACHE_SERVICE,
|
|
64
|
-
global: true,
|
|
65
|
-
factory: () => makeCacheInterface(new KeyvRedis({ connection: redis.connection }))
|
|
66
|
-
});
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
if (memory) {
|
|
70
|
-
services.push({
|
|
71
|
-
name: MEMORY_CACHE_SERVICE,
|
|
72
|
-
global: true,
|
|
73
|
-
factory: () =>
|
|
74
|
-
makeCacheInterface(new KeyvCacheableMemory({ ttl: memory.ttl, lruSize: memory.lruSize }))
|
|
75
|
-
});
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
return services;
|
|
79
|
-
}
|
|
80
|
-
})
|
|
81
|
-
export class MnemosyneModule extends Module.configurable<MnemosyneModuleConfig>({
|
|
82
|
-
defaults: { cache: { redis: { connection: 'cache' }, memory: { ttl: 60000, lruSize: 5000 } } }
|
|
83
|
-
}) {}
|
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
|
-
};
|