@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/dist/model.d.ts CHANGED
@@ -8,7 +8,7 @@ import type { ColumnMetadata, ModelAdapter } from './interfaces';
8
8
  */
9
9
  export declare const createSchemaFromModel: (columns: ColumnMetadata[], { mode }?: {
10
10
  mode?: 'create' | 'update' | 'select';
11
- }) => import("@sinclair/typebox").TObject<{}>;
11
+ }) => import("typebox").TObject<{}>;
12
12
  /**
13
13
  * Mixin used to create a new abstract model class.
14
14
  * @author Axel Nana <axel.nana@workbud.com>
@@ -49,15 +49,15 @@ export declare const AbstractModel: <TTable, TColumns extends Record<string, unk
49
49
  /**
50
50
  * The validation schema for creating records.
51
51
  */
52
- readonly insertSchema: import("@sinclair/typebox").TObject<{}>;
52
+ readonly insertSchema: import("typebox").TObject<{}>;
53
53
  /**
54
54
  * The validation schema for updating records.
55
55
  */
56
- readonly updateSchema: import("@sinclair/typebox").TObject<{}>;
56
+ readonly updateSchema: import("typebox").TObject<{}>;
57
57
  /**
58
58
  * The validation schema for selecting records.
59
59
  */
60
- readonly selectSchema: import("@sinclair/typebox").TObject<{}>;
60
+ readonly selectSchema: import("typebox").TObject<{}>;
61
61
  /**
62
62
  * Whether the model supports tenancy.
63
63
  *
@@ -0,0 +1,150 @@
1
+ import type { TObject, TSchema } from '@elysiumjs/core';
2
+ import type { ModelClass } from './interfaces';
3
+ /**
4
+ * A filter operator of the query DSL.
5
+ *
6
+ * - `eq`, `ne`, `gt`, `gte`, `lt`, `lte`: comparisons.
7
+ * - `in`: one of a comma-separated list.
8
+ * - `contains`, `startsWith`: case-insensitive text matches.
9
+ * - `isNull`: `true` or `false`.
10
+ * @author Axel Nana <axel.nana@workbud.com>
11
+ */
12
+ export type FilterOperator = 'eq' | 'ne' | 'gt' | 'gte' | 'lt' | 'lte' | 'in' | 'contains' | 'startsWith' | 'isNull';
13
+ /**
14
+ * The type of a filtered or sorted field, which decides how its query values are read.
15
+ * @author Axel Nana <axel.nana@workbud.com>
16
+ */
17
+ export type QueryFieldType = 'string' | 'number' | 'boolean' | 'date' | 'uuid';
18
+ /**
19
+ * A filterable field.
20
+ * @author Axel Nana <axel.nana@workbud.com>
21
+ */
22
+ export type QueryFilterSpec = {
23
+ /** The operators the field accepts. The first one is used by `filter[field]=value`. */
24
+ operators: FilterOperator[];
25
+ /** The type of the values. Read from the model by {@link Query.define}; `string` otherwise. */
26
+ type?: QueryFieldType;
27
+ /** The model property behind the field, when it has another name. */
28
+ column?: string;
29
+ };
30
+ /**
31
+ * What clients may filter and sort by, and how many rows a page holds.
32
+ * @author Axel Nana <axel.nana@workbud.com>
33
+ */
34
+ export type QuerySpec = {
35
+ /** The filterable fields, with their operators. */
36
+ filters?: Record<string, QueryFilterSpec | FilterOperator[]>;
37
+ /** The sortable fields. Sortable columns should not be nullable, for cursor pagination. */
38
+ sort?: string[];
39
+ /** The sort used when the query sets none, such as `-createdAt`. */
40
+ defaultSort?: string;
41
+ /** The page size: the default, and the largest a client may ask for. */
42
+ limit?: {
43
+ default?: number;
44
+ max?: number;
45
+ };
46
+ /** The model property that identifies rows, the tiebreaker of every sort. Defaults to `id`. */
47
+ key?: string;
48
+ /** The types of the model's properties, set by {@link Query.define}. */
49
+ types?: Record<string, QueryFieldType>;
50
+ /** The property names of the model, for sortable fields that map to another column. */
51
+ columns?: Record<string, string>;
52
+ };
53
+ /**
54
+ * One filter of a parsed query.
55
+ * @author Axel Nana <axel.nana@workbud.com>
56
+ */
57
+ export type ParsedFilter = {
58
+ field: string;
59
+ column: string;
60
+ operator: FilterOperator;
61
+ value: unknown;
62
+ };
63
+ /**
64
+ * One sort key of a parsed query.
65
+ * @author Axel Nana <axel.nana@workbud.com>
66
+ */
67
+ export type ParsedSort = {
68
+ field: string;
69
+ column: string;
70
+ direction: 'asc' | 'desc';
71
+ };
72
+ /**
73
+ * A query string, validated against a {@link QuerySpec}, that repositories run.
74
+ * @author Axel Nana <axel.nana@workbud.com>
75
+ */
76
+ export type ParsedQuery = {
77
+ filters: ParsedFilter[];
78
+ /** The sort keys, ending with the row key as the tiebreaker. */
79
+ sort: ParsedSort[];
80
+ /** The values of the sort keys of the last row of the previous page. */
81
+ after?: unknown[];
82
+ limit: number;
83
+ };
84
+ /**
85
+ * A page of rows from cursor pagination.
86
+ * @author Axel Nana <axel.nana@workbud.com>
87
+ */
88
+ export type CursorPage<T> = {
89
+ data: T[];
90
+ /** The cursor of the next page, or `null` on the last page. */
91
+ nextCursor: string | null;
92
+ };
93
+ export declare namespace Query {
94
+ /**
95
+ * Builds a query spec for a model: the types of the filtered and sorted fields are read from
96
+ * the model's select schema.
97
+ * @author Axel Nana <axel.nana@workbud.com>
98
+ * @param model The model class.
99
+ * @param spec The filters, sorts and page size.
100
+ * @example
101
+ * ```ts
102
+ * const PostQuery = Query.define(Post, {
103
+ * filters: { status: ['eq', 'in'], createdAt: ['gte', 'lte'], title: ['contains'] },
104
+ * sort: ['createdAt', 'title'],
105
+ * defaultSort: '-createdAt'
106
+ * });
107
+ * ```
108
+ */
109
+ const define: <TModel extends ModelClass>(model: TModel, spec: Omit<QuerySpec, 'types'>) => QuerySpec;
110
+ /**
111
+ * The query schema of a spec, for `@Http.query()`: it documents the filters, `sort`, `cursor`,
112
+ * `limit` and `fields` parameters in OpenAPI.
113
+ * @author Axel Nana <axel.nana@workbud.com>
114
+ * @param spec The query spec.
115
+ */
116
+ const schema: (spec: QuerySpec) => TObject;
117
+ /**
118
+ * Validates a query string against a spec.
119
+ * @author Axel Nana <axel.nana@workbud.com>
120
+ * @param spec The query spec.
121
+ * @param query The query, as Elysia parses it (`filter[status]` keys stay flat).
122
+ * @returns The parsed query, for a repository's `list()`.
123
+ * @throws {BadRequestError} A 400 problem whose `errors` name every invalid parameter.
124
+ */
125
+ const parse: (spec: QuerySpec, query?: Record<string, unknown>) => ParsedQuery;
126
+ /**
127
+ * Builds the cursor that points after a row.
128
+ * @author Axel Nana <axel.nana@workbud.com>
129
+ * @param query The parsed query, for its sort keys.
130
+ * @param row The last row of a page.
131
+ */
132
+ const cursorAfter: (query: ParsedQuery, row: Record<string, unknown>) => string;
133
+ /**
134
+ * Builds a page from rows fetched with one more row than the limit: the extra row only tells
135
+ * that a next page exists.
136
+ * @author Axel Nana <axel.nana@workbud.com>
137
+ * @param query The parsed query.
138
+ * @param rows Up to `limit + 1` rows.
139
+ */
140
+ const page: <T extends Record<string, unknown>>(query: ParsedQuery, rows: T[]) => CursorPage<T>;
141
+ /**
142
+ * The response schema of a page of resources.
143
+ * @author Axel Nana <axel.nana@workbud.com>
144
+ * @param item The schema of one row.
145
+ */
146
+ const pageSchema: <S extends TSchema>(item: S) => TObject<{
147
+ data: import("typebox").TArray<S>;
148
+ nextCursor: import("typebox").TUnion<[import("typebox").TString, import("typebox").TNull]>;
149
+ }>;
150
+ }
@@ -56,9 +56,9 @@ export declare const AbstractRepository: <TModel extends ModelClass, TConnection
56
56
  * Updates a record in the database.
57
57
  * @param id The id of the record to update.
58
58
  * @param data The data to update.
59
- * @returns The updated record.
59
+ * @returns The updated record, or `null` if no record has that id.
60
60
  */
61
- update(id: IdType, data: TModel["$inferUpdate"]): Promise<TModel["$inferSelect"]>;
61
+ update(id: IdType, data: TModel["$inferUpdate"]): Promise<TModel["$inferSelect"] | null>;
62
62
  /**
63
63
  * Updates all the records in the database.
64
64
  * @param data The data to update.
@@ -68,9 +68,9 @@ export declare const AbstractRepository: <TModel extends ModelClass, TConnection
68
68
  /**
69
69
  * Deletes a record from the database.
70
70
  * @param id The ID of the record to delete.
71
- * @returns The deleted record.
71
+ * @returns The deleted record, or `null` if no record has that id.
72
72
  */
73
- delete(id: IdType): Promise<TModel["$inferSelect"]>;
73
+ delete(id: IdType): Promise<TModel["$inferSelect"] | null>;
74
74
  /**
75
75
  * Deletes all the records from the database.
76
76
  * @returns All the deleted records.
@@ -0,0 +1,35 @@
1
+ import type { TObject, TSchema } from '@elysiumjs/core';
2
+ import type { ModelClass } from './interfaces';
3
+ /**
4
+ * Options of {@link ModelResource}.
5
+ * @author Axel Nana <axel.nana@workbud.com>
6
+ * @template TKey The column names of the model.
7
+ */
8
+ export type ModelResourceOptions<TKey extends string = string> = {
9
+ /** Columns kept out of responses and of the OpenAPI document, such as `passwordHash`. */
10
+ hidden?: TKey[];
11
+ /** Only these columns are exposed. */
12
+ pick?: TKey[];
13
+ /** These columns are left out of the schema entirely. */
14
+ omit?: TKey[];
15
+ /** Extra fields the resource exposes, such as computed or related values. */
16
+ extend?: Record<string, TSchema>;
17
+ };
18
+ /**
19
+ * The response schema of a model: its select schema, with hidden, picked and omitted columns
20
+ * applied. Use it as a route's `response`: the handler can return model rows as they are, and
21
+ * the response holds only the exposed fields, with dates as ISO strings.
22
+ * @author Axel Nana <axel.nana@workbud.com>
23
+ * @param model The model class.
24
+ * @param options The columns to hide, pick or omit, and extra fields.
25
+ * @example
26
+ * ```ts
27
+ * const UserResource = ModelResource(User, { hidden: ['passwordHash'] });
28
+ *
29
+ * @Http.get('/:id', { response: UserResource })
30
+ * public show(@Http.param('id') id: string) {
31
+ * return this.users.find(id);
32
+ * }
33
+ * ```
34
+ */
35
+ export declare const ModelResource: <TModel extends ModelClass>(model: TModel, options?: ModelResourceOptions<Extract<keyof TModel['$inferSelect'], string>>) => TObject;
package/dist/tenancy.d.ts CHANGED
@@ -100,11 +100,30 @@ export declare const getCurrentTenant: () => string | null;
100
100
  * @returns The return value of the callback.
101
101
  */
102
102
  export declare const withTenant: <T>(tenant: string, callback: () => T) => T;
103
+ /**
104
+ * The pattern a tenant identifier must match: 1 to 63 ASCII letters, digits, underscores or dashes,
105
+ * not starting with `pg_` and other than `information_schema` (in any case).
106
+ *
107
+ * Tenant identifiers become PostgreSQL schema names in schema mode, and 63 bytes is the PostgreSQL
108
+ * identifier limit. The pattern leaves out quotes, dots, spaces and other characters that would
109
+ * need escaping or change how an identifier is read, and the names of PostgreSQL system schemas.
110
+ * @author Axel Nana <axel.nana@workbud.com>
111
+ */
112
+ export declare const TENANT_ID_PATTERN: RegExp;
113
+ /**
114
+ * Checks that a tenant identifier is well-formed (see {@link TENANT_ID_PATTERN}). This does not
115
+ * check that the tenant exists.
116
+ * @author Axel Nana <axel.nana@workbud.com>
117
+ * @param tenant The tenant identifier.
118
+ * @returns `true` if the identifier is well-formed, `false` otherwise.
119
+ */
120
+ export declare const isValidTenantId: (tenant: string) => boolean;
103
121
  /**
104
122
  * Base class for tenant-aware middleware.
105
123
  *
106
124
  * The middleware extracts the tenant identifier from an HTTP header and
107
- * sets `ctx.tenant`. For RLS mode, route handlers should use the driver's
125
+ * sets `ctx.tenant`. Identifiers must match {@link TENANT_ID_PATTERN} and be accepted by
126
+ * `validateTenant`, which rejects every tenant unless overridden. For RLS mode, route handlers should use the driver's
108
127
  * isolation mechanism to wrap their queries in a tenant-scoped transaction.
109
128
  *
110
129
  * @example
@@ -136,7 +155,9 @@ export declare abstract class TenantMiddleware extends Middleware {
136
155
  protected connectionName: string;
137
156
  /**
138
157
  * Validates that a tenant identifier is legitimate.
139
- * Override this to check against your data store.
158
+ *
159
+ * Rejects every tenant by default: override it to check the tenant against your data store.
160
+ * Only identifiers matching {@link TENANT_ID_PATTERN} reach this method.
140
161
  * @author Axel Nana <axel.nana@workbud.com>
141
162
  * @param _tenant The tenant identifier to validate.
142
163
  * @returns `true` if the tenant is valid, `false` otherwise.
@@ -165,7 +186,8 @@ export declare abstract class TenantMiddleware extends Middleware {
165
186
  onRequest(ctx: Context): Promise<any>;
166
187
  }
167
188
  /**
168
- * A simple tenant middleware that allows public access by default.
189
+ * A simple tenant middleware that allows public access by default. Requests naming a tenant are
190
+ * rejected until `validateTenant` is overridden.
169
191
  * @author Axel Nana <axel.nana@workbud.com>
170
192
  */
171
193
  export declare class SimpleTenantMiddleware extends TenantMiddleware {
@@ -0,0 +1,68 @@
1
+ /** A callback run once the unit of work has committed or rolled back. */
2
+ type Hook = () => unknown | Promise<unknown>;
3
+ /** Runs a callback in a database transaction, joining the one open on the connection. */
4
+ export type TransactionRunner = <T>(callback: () => Promise<T>, connection?: string) => Promise<T>;
5
+ /**
6
+ * Wraps a driver's transaction function into the runner of units of work: the outermost run
7
+ * collects the `afterCommit` and `afterRollback` hooks of everything it calls, and runs them once
8
+ * it has committed or rolled back. Inner runs join it. Driver modules register the result as the
9
+ * `db.transaction` service, which `transactional` routes and {@link UnitOfWork.run} use.
10
+ * @author Axel Nana <axel.nana@workbud.com>
11
+ * @param transaction Runs a callback in a transaction, joining an open one.
12
+ */
13
+ export declare const withUnitOfWork: (transaction: TransactionRunner) => TransactionRunner;
14
+ export declare namespace UnitOfWork {
15
+ /**
16
+ * Runs a callback in a unit of work: one transaction on the connection that every repository
17
+ * call made from the callback uses. When a unit of work is already open, the callback joins
18
+ * it, so services compose without knowing which of them opened it.
19
+ *
20
+ * A callback that returns a `DomainError` rolls the work back, like one that throws, and the
21
+ * error is returned. A joined callback hands it to the one that opened the unit of work, which
22
+ * rolls back when it returns it too.
23
+ * @author Axel Nana <axel.nana@workbud.com>
24
+ * @param callback The work to run.
25
+ * @param options The connection of the transaction. Defaults to `default`.
26
+ * @throws {Error} When no database module provides the `db.transaction` service.
27
+ */
28
+ const run: <T>(callback: () => Promise<T>, options?: {
29
+ connection?: string;
30
+ }) => Promise<T>;
31
+ /** Whether a unit of work is open in the current async context. */
32
+ const active: () => boolean;
33
+ /**
34
+ * Runs a callback once the current unit of work has committed, such as publishing the events
35
+ * of an outbox. Outside a unit of work, it runs right away. A failing hook is logged.
36
+ * @author Axel Nana <axel.nana@workbud.com>
37
+ * @param hook The callback.
38
+ */
39
+ const afterCommit: (hook: Hook) => Promise<void>;
40
+ /**
41
+ * Runs a callback if the current unit of work rolls back, such as deleting an uploaded file.
42
+ * Outside a unit of work, it never runs.
43
+ * @author Axel Nana <axel.nana@workbud.com>
44
+ * @param hook The callback.
45
+ */
46
+ const afterRollback: (hook: Hook) => void;
47
+ }
48
+ /**
49
+ * Runs a method in a unit of work (see {@link UnitOfWork.run}): a new transaction, or the one
50
+ * already open. A method that returns a `DomainError` rolls back.
51
+ * @author Axel Nana <axel.nana@workbud.com>
52
+ * @param options The connection of the transaction. Defaults to `default`.
53
+ * @example
54
+ * ```ts
55
+ * @Service.register()
56
+ * class TransferService {
57
+ * @Transactional()
58
+ * public async transfer(from: string, to: string, amount: number) {
59
+ * await this.accounts.debit(from, amount);
60
+ * await this.accounts.credit(to, amount);
61
+ * }
62
+ * }
63
+ * ```
64
+ */
65
+ export declare const Transactional: (options?: {
66
+ connection?: string;
67
+ }) => MethodDecorator;
68
+ export {};
package/package.json CHANGED
@@ -1,15 +1,19 @@
1
1
  {
2
2
  "name": "@elysiumjs/mnemosyne",
3
- "version": "0.2.0",
3
+ "version": "0.4.0",
4
4
  "license": "Apache-2.0",
5
5
  "description": "Database and caching for Elysium.js applications",
6
6
  "private": false,
7
7
  "author": "Workbud Technologies Inc.",
8
8
  "repository": {
9
9
  "type": "git",
10
- "url": "https://github.com/workbud/elysium"
10
+ "url": "git+https://github.com/na2axl/elysiumjs.git",
11
+ "directory": "@elysiumjs/mnemosyne"
12
+ },
13
+ "homepage": "https://github.com/na2axl/elysiumjs/tree/main/@elysiumjs/mnemosyne#readme",
14
+ "bugs": {
15
+ "url": "https://github.com/na2axl/elysiumjs/issues"
11
16
  },
12
- "homepage": "https://github.com/workbud/elysium",
13
17
  "scripts": {
14
18
  "format": "prettier --write \"./src/**/*.ts\"",
15
19
  "clean": "rimraf dist",
@@ -17,7 +21,9 @@
17
21
  "build:ts": "tsc",
18
22
  "build": "bun run clean && bun run build:js && bun run build:ts",
19
23
  "typecheck": "tsc --noEmit",
20
- "prepublishOnly": "bun run build"
24
+ "prepublishOnly": "bun run build",
25
+ "test": "bun test",
26
+ "test:coverage": "bun test --coverage"
21
27
  },
22
28
  "keywords": [
23
29
  "elysium",
@@ -28,12 +34,18 @@
28
34
  "module": "dist/index.js",
29
35
  "type": "module",
30
36
  "types": "dist/index.d.ts",
37
+ "files": [
38
+ "dist",
39
+ "CHANGELOG.md"
40
+ ],
31
41
  "sideEffects": false,
32
42
  "peerDependencies": {
33
- "@elysiumjs/core": "^0.8.0"
43
+ "@elysiumjs/core": "^0.11.0",
44
+ "@elysiumjs/redis": "^0.1.0"
34
45
  },
35
46
  "devDependencies": {
36
- "@elysiumjs/core": "0.8.0"
47
+ "@elysiumjs/core": "0.11.0",
48
+ "@elysiumjs/redis": "0.1.0"
37
49
  },
38
50
  "dependencies": {
39
51
  "cache-manager": "^7.2.9",
@@ -1,8 +0,0 @@
1
- $ bun run clean && bun run build:js && bun run build:ts
2
- $ rimraf dist
3
- $ bun build ./src/index.ts --outdir ./dist --target bun --packages external --root ./src --minify-syntax --minify-whitespace
4
- Bundled 8 modules in 21ms
5
-
6
- index.js 11.16 KB (entry point)
7
-
8
- $ tsc
package/src/cache.ts DELETED
@@ -1,219 +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 { KeyvStoreAdapter } from 'keyv';
16
-
17
- import { Service, ServiceNotFoundError } from '@elysiumjs/core';
18
- import { createCache } from 'cache-manager';
19
- import { Keyv } from 'keyv';
20
- import { alphabetical } from 'radash';
21
-
22
- /** Name of the global service backing `Cache.redis`. */
23
- export const REDIS_CACHE_SERVICE = 'mnemosyne.cache.redis';
24
-
25
- /** Name of the global service backing `Cache.memory`. */
26
- export const MEMORY_CACHE_SERVICE = 'mnemosyne.cache.memory';
27
-
28
- /**
29
- * Interface for a cache storage.
30
- * @author Axel Nana <axel.nana@workbud.com>
31
- */
32
- export interface CacheInterface {
33
- /**
34
- * Retrieves the cached data for a given key.
35
- * @typeParam T The type of the cached data.
36
- *
37
- * @param key The key of the cached data.
38
- */
39
- get<T>(key: string): Promise<T | undefined>;
40
-
41
- /**
42
- * Retrieves the cached data for a given list of keys.
43
- * @typeParam T The type of the cached data.
44
- *
45
- * @param keys The list of cached data keys.
46
- */
47
- mget<T>(keys: string[]): Promise<Array<T | undefined>>;
48
-
49
- /**
50
- * Gets the TTL of the cached data with the given key.
51
- * @param key The key of the cached data.
52
- * @returns The TTL of the cached data, or `undefined` if the data does not exist.
53
- */
54
- ttl(key: string): Promise<number | undefined>;
55
-
56
- /**
57
- * Caches data under the given key and an optional TTL.
58
- * @typeParam T The type of the data to cache.
59
- *
60
- * @param key The key of the cached data.
61
- * @param data The data to cache.
62
- * @param ttl The optional TTL of the cached data. If not defined, the data is cached forever.
63
- * @returns The cached data.
64
- */
65
- set<T>(key: string, data: T, ttl?: number): Promise<T>;
66
-
67
- /**
68
- * Caches data under the given list of keys and an optional TTL.
69
- * @typeParam T The type of the data to cache.
70
- *
71
- * @param data The list of data to cache.
72
- * @returns The list of cached data.
73
- */
74
- mset<T>(
75
- data: Array<{
76
- key: string;
77
- value: T;
78
- ttl?: number;
79
- }>
80
- ): Promise<
81
- Array<{
82
- key: string;
83
- value: T;
84
- ttl?: number;
85
- }>
86
- >;
87
-
88
- /**
89
- * Deletes the cached data with the given key.
90
- * @typeParam T The type of the data to cache.
91
- *
92
- * @param key The key of the cached data.
93
- * @returns `true` if the data was deleted, `false` otherwise.
94
- */
95
- del(key: string): Promise<boolean>;
96
-
97
- /**
98
- * Deletes the cached data with the given list of keys.
99
- * @typeParam T The type of the data to cache.
100
- *
101
- * @param keys The list of cached data keys.
102
- * @returns `true` if the data was deleted, `false` otherwise.
103
- */
104
- mdel(keys: string[]): Promise<boolean>;
105
-
106
- /**
107
- * Deletes all cached data.
108
- * @returns `true` if the data was deleted, `false` otherwise.
109
- */
110
- clear(): Promise<boolean>;
111
-
112
- /**
113
- * Creates a sub-cache with the given tags.
114
- * @param tags The list of tags for the sub-cache.
115
- */
116
- tags(...tags: string[]): Omit<CacheInterface, 'tags'>;
117
- }
118
-
119
- /**
120
- * Makes a cache interface wrapping the given store.
121
- * @author Axel Nana <axel.nana@workburd.com>
122
- * @param store The cache storage to use in the interface.
123
- * @returns A new `CacheInterface`.
124
- */
125
- export const makeCacheInterface = (store: KeyvStoreAdapter): CacheInterface => {
126
- const base = createCache({
127
- stores: [
128
- new Keyv({
129
- store,
130
- namespace: 'cache'
131
- })
132
- ]
133
- }) as Omit<CacheInterface, 'tags'>;
134
-
135
- const tagsCache = new Map<string, Omit<CacheInterface, 'tags'>>();
136
-
137
- return {
138
- ...base,
139
- tags(...tags) {
140
- const namespace = `cache__${alphabetical(tags, (item) => item).join('__')}`;
141
- let cache = tagsCache.get(namespace);
142
-
143
- if (!cache) {
144
- cache = createCache({
145
- stores: [
146
- new Keyv({
147
- store,
148
- namespace
149
- })
150
- ]
151
- }) as Omit<CacheInterface, 'tags'>;
152
-
153
- tagsCache.set(namespace, cache);
154
- }
155
-
156
- return cache;
157
- }
158
- };
159
- };
160
-
161
- const getCache = (name: string): CacheInterface => {
162
- const cache = Service.get<CacheInterface>(name);
163
- if (!cache) throw new ServiceNotFoundError(name);
164
- return cache;
165
- };
166
-
167
- /**
168
- * Static access to the caches provided by the `MnemosyneModule`.
169
- * @author Axel Nana <axel.nana@workbud.com>
170
- */
171
- export abstract class Cache {
172
- /**
173
- * A weak cache storage for simple non-intrusive caching of data.
174
- * @author Axel Nana <axel.nana@workbud.com>
175
- */
176
- public static readonly weak = Object.seal({
177
- /**
178
- * INTERNAL USAGE ONLY.
179
- * @private
180
- */
181
- map: new WeakMap<object, unknown>(),
182
-
183
- /**
184
- * Gets the cached data for a given context.
185
- * @param ctx The cache context.
186
- * @returns The cached data.
187
- */
188
- get<T extends object>(ctx: object): T {
189
- if (!this.map.has(ctx)) this.map.set(ctx, {});
190
- return this.map.get(ctx) as T;
191
- },
192
-
193
- /**
194
- * Deletes the cached data for a given context.
195
- * @param ctx The cache context.
196
- */
197
- del(ctx: object): void {
198
- this.map.delete(ctx);
199
- }
200
- });
201
-
202
- /**
203
- * Redis-based cache storage.
204
- * @author Axel Nana <axel.nana@workbud.com>
205
- * @throws {ServiceNotFoundError} If the `MnemosyneModule` is not loaded or has no redis cache.
206
- */
207
- public static get redis(): CacheInterface {
208
- return getCache(REDIS_CACHE_SERVICE);
209
- }
210
-
211
- /**
212
- * Memory-based cache storage.
213
- * @author Axel Nana <axel.nana@workbud.com>
214
- * @throws {ServiceNotFoundError} If the `MnemosyneModule` is not loaded or has no memory cache.
215
- */
216
- public static get memory(): CacheInterface {
217
- return getCache(MEMORY_CACHE_SERVICE);
218
- }
219
- }