@elysiumjs/mnemosyne 0.2.0 → 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 +56 -0
- package/dist/cache.d.ts +8 -4
- package/dist/database.d.ts +64 -3
- package/dist/index.js +1 -1
- package/dist/interfaces.d.ts +16 -6
- package/dist/keyv-redis.d.ts +1 -1
- package/dist/repository.d.ts +4 -4
- package/dist/tenancy.d.ts +25 -3
- package/package.json +16 -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/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
|
-
};
|
package/src/tenancy.ts
DELETED
|
@@ -1,274 +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 { Context } from '@elysiumjs/core';
|
|
16
|
-
import type { TenancyStrategy } from './interfaces';
|
|
17
|
-
|
|
18
|
-
import { Application, Middleware, Service } from '@elysiumjs/core';
|
|
19
|
-
|
|
20
|
-
// ============================================================================
|
|
21
|
-
// Types
|
|
22
|
-
// ============================================================================
|
|
23
|
-
|
|
24
|
-
/**
|
|
25
|
-
* The tenancy mode for the application.
|
|
26
|
-
* - `schema`: Each tenant has their own Postgres schema.
|
|
27
|
-
* - `rls`: Row-Level Security with a shared schema and a tenant column.
|
|
28
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
29
|
-
*/
|
|
30
|
-
export type TenancyMode = 'schema' | 'rls';
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
* Global tenancy configuration for the application.
|
|
34
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
35
|
-
*/
|
|
36
|
-
export interface TenancyConfig {
|
|
37
|
-
/**
|
|
38
|
-
* The tenancy mode to use.
|
|
39
|
-
*/
|
|
40
|
-
mode: TenancyMode;
|
|
41
|
-
|
|
42
|
-
/**
|
|
43
|
-
* Configuration specific to RLS mode.
|
|
44
|
-
*/
|
|
45
|
-
rls?: {
|
|
46
|
-
/**
|
|
47
|
-
* The PostgreSQL session variable used to store the current tenant.
|
|
48
|
-
* @default 'app.current_tenant'
|
|
49
|
-
*/
|
|
50
|
-
sessionVariable?: string;
|
|
51
|
-
|
|
52
|
-
/**
|
|
53
|
-
* The default column name used for tenant isolation in RLS policies.
|
|
54
|
-
* @default 'tenant_id'
|
|
55
|
-
*/
|
|
56
|
-
defaultColumn?: string;
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
/**
|
|
60
|
-
* Configuration specific to schema-based tenancy.
|
|
61
|
-
*/
|
|
62
|
-
schema?: {
|
|
63
|
-
/**
|
|
64
|
-
* Whether to automatically create tenant schemas on first use.
|
|
65
|
-
*/
|
|
66
|
-
autoCreate?: boolean;
|
|
67
|
-
};
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
/**
|
|
71
|
-
* Per-model tenancy configuration, used to control how a specific model
|
|
72
|
-
* interacts with the tenancy system.
|
|
73
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
74
|
-
*/
|
|
75
|
-
export interface ModelTenancyConfig {
|
|
76
|
-
/**
|
|
77
|
-
* Whether tenancy is enabled for this model.
|
|
78
|
-
*/
|
|
79
|
-
enabled: boolean;
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Override the global tenancy mode for this model.
|
|
83
|
-
*/
|
|
84
|
-
mode?: TenancyMode;
|
|
85
|
-
|
|
86
|
-
/**
|
|
87
|
-
* The column name used for RLS tenant isolation in this model.
|
|
88
|
-
*/
|
|
89
|
-
column?: string;
|
|
90
|
-
|
|
91
|
-
/**
|
|
92
|
-
* Custom RLS policy name for this model.
|
|
93
|
-
*/
|
|
94
|
-
policyName?: string;
|
|
95
|
-
}
|
|
96
|
-
|
|
97
|
-
// ============================================================================
|
|
98
|
-
// Configuration
|
|
99
|
-
// ============================================================================
|
|
100
|
-
|
|
101
|
-
/** Name of the global service holding the {@link TenancyConfig}, registered by driver modules. */
|
|
102
|
-
export const TENANCY_CONFIG_SERVICE = 'mnemosyne.tenancy.config';
|
|
103
|
-
|
|
104
|
-
/** Name of the global service holding the {@link TenancyStrategy}, registered by driver modules. */
|
|
105
|
-
export const TENANCY_STRATEGY_SERVICE = 'mnemosyne.tenancy.strategy';
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Gets the current global tenancy configuration.
|
|
109
|
-
*
|
|
110
|
-
* Reads the global service `mnemosyne.tenancy.config`. Returns a default
|
|
111
|
-
* configuration if none is registered.
|
|
112
|
-
*
|
|
113
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
114
|
-
* @returns The current tenancy configuration.
|
|
115
|
-
*/
|
|
116
|
-
export const getTenancyConfig = (): TenancyConfig => {
|
|
117
|
-
return Service.get<TenancyConfig>(TENANCY_CONFIG_SERVICE) ?? { mode: 'schema' };
|
|
118
|
-
};
|
|
119
|
-
|
|
120
|
-
/**
|
|
121
|
-
* Gets the tenancy strategy registered by the driver module as the global service
|
|
122
|
-
* `mnemosyne.tenancy.strategy`.
|
|
123
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
124
|
-
* @returns The current tenancy strategy, or `null` if none is registered.
|
|
125
|
-
*/
|
|
126
|
-
export const getTenancyStrategy = (): TenancyStrategy<unknown, unknown> | null => {
|
|
127
|
-
return Service.get<TenancyStrategy<unknown, unknown>>(TENANCY_STRATEGY_SERVICE);
|
|
128
|
-
};
|
|
129
|
-
|
|
130
|
-
// ============================================================================
|
|
131
|
-
// Context Management
|
|
132
|
-
// ============================================================================
|
|
133
|
-
|
|
134
|
-
/**
|
|
135
|
-
* Gets the current tenant from the async context.
|
|
136
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
137
|
-
* @returns The current tenant identifier, or `null` if not set.
|
|
138
|
-
*/
|
|
139
|
-
export const getCurrentTenant = (): string | null => {
|
|
140
|
-
if (!Application.instance || !Application.context.getStore) {
|
|
141
|
-
return null;
|
|
142
|
-
}
|
|
143
|
-
const store = Application.context.getStore();
|
|
144
|
-
return (store?.get('tenant') ?? null) as string | null;
|
|
145
|
-
};
|
|
146
|
-
|
|
147
|
-
/**
|
|
148
|
-
* Runs a callback within the context of a specific tenant.
|
|
149
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
150
|
-
* @param tenant The tenant identifier to set.
|
|
151
|
-
* @param callback The function to execute within the tenant context.
|
|
152
|
-
* @returns The return value of the callback.
|
|
153
|
-
*/
|
|
154
|
-
export const withTenant = <T>(tenant: string, callback: () => T): T => {
|
|
155
|
-
const currentStore = Application.context.getStore();
|
|
156
|
-
const newStore = new Map(currentStore);
|
|
157
|
-
newStore.set('tenant', tenant);
|
|
158
|
-
return Application.context.run(newStore, callback);
|
|
159
|
-
};
|
|
160
|
-
|
|
161
|
-
// ============================================================================
|
|
162
|
-
// TenantMiddleware Base Class
|
|
163
|
-
// ============================================================================
|
|
164
|
-
|
|
165
|
-
/**
|
|
166
|
-
* Base class for tenant-aware middleware.
|
|
167
|
-
*
|
|
168
|
-
* The middleware extracts the tenant identifier from an HTTP header and
|
|
169
|
-
* sets `ctx.tenant`. For RLS mode, route handlers should use the driver's
|
|
170
|
-
* isolation mechanism to wrap their queries in a tenant-scoped transaction.
|
|
171
|
-
*
|
|
172
|
-
* @example
|
|
173
|
-
* ```typescript
|
|
174
|
-
* class OrganizationMiddleware extends TenantMiddleware {
|
|
175
|
-
* protected header = 'x-organization-id';
|
|
176
|
-
* protected allowPublic = false;
|
|
177
|
-
*
|
|
178
|
-
* protected async validateTenant(orgId: string): Promise<boolean> {
|
|
179
|
-
* return Organization.exists(orgId);
|
|
180
|
-
* }
|
|
181
|
-
* }
|
|
182
|
-
* ```
|
|
183
|
-
*
|
|
184
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
185
|
-
*/
|
|
186
|
-
export abstract class TenantMiddleware extends Middleware {
|
|
187
|
-
/**
|
|
188
|
-
* The HTTP header name from which the tenant ID is read.
|
|
189
|
-
*/
|
|
190
|
-
protected header: string = 'x-tenant-id';
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
* Whether requests without a tenant ID are allowed (defaulting to 'public').
|
|
194
|
-
*/
|
|
195
|
-
protected allowPublic: boolean = true;
|
|
196
|
-
|
|
197
|
-
/**
|
|
198
|
-
* The database connection name used for tenant operations.
|
|
199
|
-
*/
|
|
200
|
-
protected connectionName: string = 'default';
|
|
201
|
-
|
|
202
|
-
/**
|
|
203
|
-
* Validates that a tenant identifier is legitimate.
|
|
204
|
-
* Override this to check against your data store.
|
|
205
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
206
|
-
* @param _tenant The tenant identifier to validate.
|
|
207
|
-
* @returns `true` if the tenant is valid, `false` otherwise.
|
|
208
|
-
*/
|
|
209
|
-
protected async validateTenant(_tenant: string): Promise<boolean> {
|
|
210
|
-
return true;
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Hook called after a tenant has been successfully resolved.
|
|
215
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
216
|
-
* @param _tenant The resolved tenant identifier.
|
|
217
|
-
* @param _ctx The request context.
|
|
218
|
-
*/
|
|
219
|
-
protected async onTenantResolved(_tenant: string, _ctx: Context): Promise<void> {}
|
|
220
|
-
|
|
221
|
-
/**
|
|
222
|
-
* Called when the tenant cannot be determined or is invalid.
|
|
223
|
-
* Throws an appropriate error by default.
|
|
224
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
225
|
-
* @param reason Whether the tenant is 'missing' or 'invalid'.
|
|
226
|
-
* @param _ctx The request context.
|
|
227
|
-
*/
|
|
228
|
-
protected onTenantError(reason: 'missing' | 'invalid', _ctx: Context): never {
|
|
229
|
-
if (reason === 'missing') {
|
|
230
|
-
throw new Error('Tenant ID required');
|
|
231
|
-
} else {
|
|
232
|
-
throw new Error('Invalid tenant');
|
|
233
|
-
}
|
|
234
|
-
}
|
|
235
|
-
|
|
236
|
-
/**
|
|
237
|
-
* Extracts and validates the tenant from the incoming request.
|
|
238
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
239
|
-
* @param ctx The request context.
|
|
240
|
-
*/
|
|
241
|
-
public async onRequest(ctx: Context): Promise<any> {
|
|
242
|
-
const tenantId = ctx.request.headers.get(this.header);
|
|
243
|
-
|
|
244
|
-
if (!tenantId || tenantId === 'public') {
|
|
245
|
-
if (this.allowPublic) {
|
|
246
|
-
(ctx as any).tenant = 'public';
|
|
247
|
-
return;
|
|
248
|
-
}
|
|
249
|
-
return this.onTenantError('missing', ctx);
|
|
250
|
-
}
|
|
251
|
-
|
|
252
|
-
const isValid = await this.validateTenant(tenantId);
|
|
253
|
-
if (!isValid) {
|
|
254
|
-
return this.onTenantError('invalid', ctx);
|
|
255
|
-
}
|
|
256
|
-
|
|
257
|
-
(ctx as any).tenant = tenantId;
|
|
258
|
-
await this.onTenantResolved(tenantId, ctx);
|
|
259
|
-
}
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
/**
|
|
263
|
-
* A simple tenant middleware that allows public access by default.
|
|
264
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
265
|
-
*/
|
|
266
|
-
export class SimpleTenantMiddleware extends TenantMiddleware {}
|
|
267
|
-
|
|
268
|
-
/**
|
|
269
|
-
* A strict tenant middleware that rejects requests without a tenant ID.
|
|
270
|
-
* @author Axel Nana <axel.nana@workbud.com>
|
|
271
|
-
*/
|
|
272
|
-
export class StrictTenantMiddleware extends TenantMiddleware {
|
|
273
|
-
protected allowPublic = false;
|
|
274
|
-
}
|