@daiso-tech/core 0.45.0 → 0.46.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/README.md +50 -44
- package/dist/cache/contracts/database-cache-adapter.contract.d.ts +10 -48
- package/dist/cache/contracts/database-cache-adapter.contract.js +1 -1
- package/dist/cache/contracts/database-cache-adapter.contract.js.map +1 -1
- package/dist/cache/implementations/adapters/kysely-cache-adapter/kysely-cache-adapter.d.ts +24 -19
- package/dist/cache/implementations/adapters/kysely-cache-adapter/kysely-cache-adapter.js +138 -134
- package/dist/cache/implementations/adapters/kysely-cache-adapter/kysely-cache-adapter.js.map +1 -1
- package/dist/cache/implementations/derivables/cache/database-cache-adapter.d.ts +3 -1
- package/dist/cache/implementations/derivables/cache/database-cache-adapter.js +49 -64
- package/dist/cache/implementations/derivables/cache/database-cache-adapter.js.map +1 -1
- package/dist/cache/implementations/derivables/cache/is-database-cache-adapter.js +5 -10
- package/dist/cache/implementations/derivables/cache/is-database-cache-adapter.js.map +1 -1
- package/dist/cache/implementations/test-utilities/cache-adapter.test-suite.js +240 -238
- package/dist/cache/implementations/test-utilities/cache-adapter.test-suite.js.map +1 -1
- package/dist/cache/implementations/test-utilities/database-cache-dapter.test-suite.d.ts +0 -1
- package/dist/cache/implementations/test-utilities/database-cache-dapter.test-suite.js +215 -630
- package/dist/cache/implementations/test-utilities/database-cache-dapter.test-suite.js.map +1 -1
- package/dist/utilities/functions/error-policy.d.ts +1 -1
- package/dist/utilities/functions/error-policy.js +1 -2
- package/dist/utilities/functions/error-policy.js.map +1 -1
- package/package.json +38 -56
package/README.md
CHANGED
|
@@ -1,64 +1,70 @@
|
|
|
1
|
+
# @daiso-tech/core
|
|
2
|
+
|
|
1
3
|
[](https://www.npmjs.com/package/@daiso-tech/core)
|
|
2
4
|

|
|
3
|
-

|
|
4
6
|
[](https://nodejs.org/api/esm.html)
|
|
5
7
|
[](LICENSE)
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
`@daiso-tech/core` is a TypeScript-first backend library for building web apps and API servers. It includes an ecosystem of official packages designed to work seamlessly together.
|
|
10
|
-
|
|
11
|
-
**@daiso-tech/core** is a library of backend server components designed for maximum flexibility. While each component is fully functional on its own, they are written to work seamlessly together to build robust server-side logic.
|
|
12
|
-
|
|
13
|
-
Key Features:
|
|
14
|
-
- **Framework Agnostic:** Built without a dependency injection (DI) container, the library integrates effortlessly into any framework / library. Whether you use Express.js, backend frameworks like AdonisJS or NestJS, or full-stack frameworks like Next.js, Nuxt, or TanStack Start, these components "just work."
|
|
15
|
-
|
|
16
|
-
- **Runtime Portability:** By leveraging the Adapter Pattern, the library remains decoupled from specific runtimes. Easily implement custom adapters, such as Cloudflare Durable Objects or Amazon DynamoDB, for better serverless integration with cloudflare workers or aws lambda.
|
|
17
|
-
|
|
18
|
-
- **Test Friendly:** Testing is simple because every component comes with an "in-memory" adapter. This means you can run your tests instantly without needing to connect to a real database or server.
|
|
19
|
-
|
|
20
|
-
- **Developer Friendly:** Save time with high-quality documentation and a readable, well-documented codebase. Clear comments and a logical structure make it easy to understand and extend system functionality.
|
|
21
|
-
|
|
22
|
-
- **Type safe:**
|
|
23
|
-
We pay a closer look at type-safety, seamless intellisense, and support for auto imports when designing library APIs.
|
|
9
|
+
`@daiso-tech/core` is a **TypeScript-first backend toolkit** designed for building resilient web applications and API servers. It provides a suite of decoupled, high-performance components that work seamlessly across any JavaScript runtime.
|
|
24
10
|
|
|
25
|
-
- **
|
|
26
|
-
@daiso-tech/core leverages modern JavaScript primitives, including ES modules
|
|
11
|
+
[**Explore the Docs**](https://daiso-core.vercel.app/docs/Installation) | [**NPM Package**](https://www.npmjs.com/package/@daiso-tech/core)
|
|
27
12
|
|
|
28
|
-
|
|
29
|
-
Integrated seamlessly with [standard schema](https://standardschema.dev/) allowing you to use libraries like zod to ensure both compile time and runtime typesafety.
|
|
13
|
+
---
|
|
30
14
|
|
|
31
|
-
|
|
15
|
+
## 🚀 Key Features
|
|
32
16
|
|
|
33
|
-
|
|
17
|
+
* **Framework Agnostic** No Dependency Injection (DI) containers required. Effortlessly integrate with Express, NestJS, AdonisJS, or full-stack frameworks like Next.js, Nuxt, and TanStack Start.
|
|
18
|
+
* **Runtime Portability** Leverages the **Adapter Pattern** to decouple your logic from the runtime. Switch between Node.js, Cloudflare Workers (Durable Objects), or AWS Lambda without rewriting core logic.
|
|
19
|
+
* **Test-Driven Excellence** Every component includes a built-in **"in-memory" adapter**. Run unit tests instantly without spinning up databases or external infrastructure.
|
|
20
|
+
* **Type Safety & DX** Deep IntelliSense support and strict type-safety. Designed for auto-imports and modern developer workflows.
|
|
21
|
+
* **Standard Schema Support** Native integration with [Standard Schema](https://standardschema.dev/), allowing you to use **Zod**, **Valibot**, or **ArkType** for unified runtime validation.
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
Speed up your applications by storing slowly changing data in a cache store.
|
|
23
|
+
---
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
Easily send events accross different applications or in-memory.
|
|
25
|
+
## 🛠 Quick Start
|
|
40
26
|
|
|
41
|
-
|
|
42
|
-
|
|
27
|
+
```bash
|
|
28
|
+
npm install @daiso-tech/core
|
|
29
|
+
```
|
|
43
30
|
|
|
44
|
-
|
|
45
|
-
A Rate-limiter is a resilience primitive used to control the rate of traffic sent or received by a network interface or a service.
|
|
31
|
+
## 📦 Core Components
|
|
46
32
|
|
|
47
|
-
-
|
|
48
|
-
Synchronize the access to a shared resource to prevents several processes, or concurrent code, from executing a section of code at the same time.
|
|
33
|
+
The `@daiso-tech/core` ecosystem provides a growing collection of officially maintained primitives for building robust systems:
|
|
49
34
|
|
|
50
|
-
|
|
51
|
-
|
|
35
|
+
### Resilience
|
|
36
|
+
| | |
|
|
37
|
+
| :--- | :--- |
|
|
38
|
+
| **Circuit Breaker** | Prevents cascading failures by stopping calls to failing external services. |
|
|
39
|
+
| **Rate Limiter** | Controls traffic flow to protect your network interfaces and services. |
|
|
40
|
+
| **Retry** | Retry middleware with support for different backoffs with jitter. |
|
|
41
|
+
| **Timeout** | Timeout middleware that prevents resource exhaustion by killing long-running tasks. |
|
|
42
|
+
| **Fallback** | Fallback middleware that ensures graceful degradation by returning default values. |
|
|
52
43
|
|
|
53
|
-
|
|
54
|
-
|
|
44
|
+
### Concurrency
|
|
45
|
+
| | |
|
|
46
|
+
| :--- | :--- |
|
|
47
|
+
| **Lock** | Ensures mutual exclusion for shared resources across servers or procceses. |
|
|
48
|
+
| **Semaphore** | Limits the number of concurrent servers or procceses accessing a specific resource. |
|
|
49
|
+
| **Shared Lock** | Reader-writer lock coordinating concurrent reads and exclusive writes. |
|
|
55
50
|
|
|
56
|
-
|
|
57
|
-
|
|
51
|
+
### Misc
|
|
52
|
+
| | |
|
|
53
|
+
| :--- | :--- |
|
|
54
|
+
| **Cache** | High-performance caching with support for multiple store adapters. |
|
|
55
|
+
| **EventBus** | Decoupled event-driven communication (In-memory or Distributed via redis). |
|
|
58
56
|
|
|
59
|
-
|
|
60
|
-
|
|
57
|
+
### Utilities
|
|
58
|
+
| | |
|
|
59
|
+
| :--- | :--- |
|
|
60
|
+
| **Hooks** | Agnostic sync/async middleware that integrates with all components. |
|
|
61
|
+
| **Serde** | Custom serialization/deserialization logic that integrates with all components. |
|
|
62
|
+
| **Collection** | Precision filtering and transformation for Arrays, Iterables, AsyncIterables and ArrayLike objects. |
|
|
63
|
+
| **TimeSpan** | A duration class offering seamless time manipulation while integrating with all components. |
|
|
64
|
+
---
|
|
61
65
|
|
|
62
|
-
|
|
63
|
-
Extend any sync and async function with agnostic hooks.@daiso-tech/core includes predefined retry, fallback, timeout and hedging hooks to easily allow handling transient failures.
|
|
66
|
+
## 🛠 Quick Start
|
|
64
67
|
|
|
68
|
+
```bash
|
|
69
|
+
npm install @daiso-tech/core
|
|
70
|
+
```
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module Cache
|
|
3
3
|
*/
|
|
4
|
+
import { type InvokableFn } from "../../utilities/_module.js";
|
|
4
5
|
/**
|
|
5
6
|
*
|
|
6
7
|
* IMPORT_PATH: `"@daiso-tech/core/cache/contracts"`
|
|
@@ -11,32 +12,20 @@ export type ICacheData<TType = unknown> = {
|
|
|
11
12
|
expiration: Date | null;
|
|
12
13
|
};
|
|
13
14
|
/**
|
|
14
|
-
*
|
|
15
15
|
* IMPORT_PATH: `"@daiso-tech/core/cache/contracts"`
|
|
16
16
|
* @group Contracts
|
|
17
17
|
*/
|
|
18
|
-
export type
|
|
19
|
-
|
|
18
|
+
export type IDatabaseCacheTransaction<TType = unknown> = {
|
|
19
|
+
find(key: string): Promise<ICacheData<TType> | null>;
|
|
20
|
+
upsert(key: string, value: TType, expiration?: Date | null): Promise<void>;
|
|
20
21
|
};
|
|
21
22
|
/**
|
|
22
|
-
*
|
|
23
23
|
* IMPORT_PATH: `"@daiso-tech/core/cache/contracts"`
|
|
24
24
|
* @group Contracts
|
|
25
25
|
*/
|
|
26
|
-
export type
|
|
27
|
-
key: string;
|
|
28
|
-
value: TType;
|
|
26
|
+
export type ICacheDataExpiration = {
|
|
29
27
|
expiration: Date | null;
|
|
30
28
|
};
|
|
31
|
-
/**
|
|
32
|
-
*
|
|
33
|
-
* IMPORT_PATH: `"@daiso-tech/core/cache/contracts"`
|
|
34
|
-
* @group Contracts
|
|
35
|
-
*/
|
|
36
|
-
export type ICacheUpdate<TType = unknown> = {
|
|
37
|
-
key: string;
|
|
38
|
-
value: TType;
|
|
39
|
-
};
|
|
40
29
|
/**
|
|
41
30
|
* The `IDatabaseCacheAdapter` contract defines a way for as key-value pairs independent of data storage.
|
|
42
31
|
* This contract simplifies the implementation of cache adapters with CRUD-based databases, such as SQL databases and ORMs like TypeOrm and MikroOrm.
|
|
@@ -45,39 +34,12 @@ export type ICacheUpdate<TType = unknown> = {
|
|
|
45
34
|
* @group Contracts
|
|
46
35
|
*/
|
|
47
36
|
export type IDatabaseCacheAdapter<TType = unknown> = {
|
|
48
|
-
/**
|
|
49
|
-
* The `find` method returns the the `key` data which includs {@link ICacheData | `ICacheData.value`} and {@link ICacheData | `ICacheData.expiration`}.
|
|
50
|
-
*/
|
|
51
37
|
find(key: string): Promise<ICacheData<TType> | null>;
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
* The `upsert` method inserts a key and if the key already exists then key will be updated with new `data.value` and `data.expiration`.
|
|
58
|
-
* The method always returns the old cache data if it exists otherwise null will be returned.
|
|
59
|
-
*/
|
|
60
|
-
upsert(data: ICacheInsert<TType>): Promise<ICacheDataExpiration | null>;
|
|
61
|
-
/**
|
|
62
|
-
* The `removeExpiredMany` method updates a expired `key`.
|
|
63
|
-
*/
|
|
64
|
-
updateExpired(data: ICacheInsert<TType>): Promise<number>;
|
|
65
|
-
/**
|
|
66
|
-
* The `removeExpiredMany` method updates a unexpired `key`.
|
|
67
|
-
*/
|
|
68
|
-
updateUnexpired(data: ICacheUpdate<TType>): Promise<number>;
|
|
69
|
-
/**
|
|
70
|
-
* The `incrementUnexpired` should always throw an error if the existing item is not a number type.
|
|
71
|
-
*/
|
|
72
|
-
incrementUnexpired(data: ICacheUpdate<number>): Promise<number>;
|
|
73
|
-
/**
|
|
74
|
-
* The `removeExpiredMany` method removes multiple expired `keys`.
|
|
75
|
-
*/
|
|
76
|
-
removeExpiredMany(keys: string[]): Promise<number>;
|
|
77
|
-
/**
|
|
78
|
-
* The `removeExpiredMany` method removes multiple unexpired `keys`.
|
|
79
|
-
*/
|
|
80
|
-
removeUnexpiredMany(keys: string[]): Promise<number>;
|
|
38
|
+
transaction<TValue>(trxFn: InvokableFn<[
|
|
39
|
+
trx: IDatabaseCacheTransaction<TType>
|
|
40
|
+
], Promise<TValue>>): Promise<TValue>;
|
|
41
|
+
update(key: string, value: TType): Promise<ICacheDataExpiration | null>;
|
|
42
|
+
removeMany(keys: string[]): Promise<ICacheDataExpiration[]>;
|
|
81
43
|
/**
|
|
82
44
|
* The `removeAll` method removes all keys from the cache.
|
|
83
45
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"database-cache-adapter.contract.js","sourceRoot":"","sources":["../../../src/cache/contracts/database-cache-adapter.contract.ts"],"names":[],"mappings":"AAAA;;GAEG"}
|
|
1
|
+
{"version":3,"file":"database-cache-adapter.contract.js","sourceRoot":"","sources":["../../../src/cache/contracts/database-cache-adapter.contract.ts"],"names":[],"mappings":"AAAA;;GAEG;AAEH,OAAO,EAAoB,MAAM,wBAAwB,CAAC"}
|
|
@@ -2,35 +2,32 @@
|
|
|
2
2
|
* @module Cache
|
|
3
3
|
*/
|
|
4
4
|
import { type Kysely } from "kysely";
|
|
5
|
-
import { type ICacheData, type
|
|
5
|
+
import { type ICacheData, type ICacheDataExpiration, type IDatabaseCacheAdapter, type IDatabaseCacheTransaction } from "../../../../cache/contracts/_module.js";
|
|
6
6
|
import { type ISerde } from "../../../../serde/contracts/_module.js";
|
|
7
7
|
import { type ITimeSpan } from "../../../../time-span/contracts/_module.js";
|
|
8
|
-
import { type IDeinitizable, type IInitizable, type IPrunable } from "../../../../utilities/_module.js";
|
|
8
|
+
import { type IDeinitizable, type IInitizable, type InvokableFn, type IPrunable } from "../../../../utilities/_module.js";
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
11
10
|
* IMPORT_PATH: `"@daiso-tech/core/cache/kysely-cache-adapter"`
|
|
12
11
|
* @group Adapters
|
|
13
12
|
*/
|
|
14
|
-
export type
|
|
13
|
+
export type KyselyCacheTable = {
|
|
15
14
|
key: string;
|
|
16
15
|
value: string;
|
|
17
16
|
expiration: number | string | null;
|
|
18
17
|
};
|
|
19
18
|
/**
|
|
20
|
-
*
|
|
21
19
|
* IMPORT_PATH: `"@daiso-tech/core/cache/kysely-cache-adapter"`
|
|
22
20
|
* @group Adapters
|
|
23
21
|
*/
|
|
24
|
-
export type
|
|
25
|
-
cache:
|
|
22
|
+
export type KyselyCacheTables = {
|
|
23
|
+
cache: KyselyCacheTable;
|
|
26
24
|
};
|
|
27
25
|
/**
|
|
28
|
-
*
|
|
29
26
|
* IMPORT_PATH: `"@daiso-tech/core/cache/kysely-cache-adapter"`
|
|
30
27
|
* @group Adapters
|
|
31
28
|
*/
|
|
32
29
|
export type KyselyCacheAdapterSettings = {
|
|
33
|
-
kysely: Kysely<
|
|
30
|
+
kysely: Kysely<KyselyCacheTables>;
|
|
34
31
|
serde: ISerde<string>;
|
|
35
32
|
/**
|
|
36
33
|
* @default
|
|
@@ -46,6 +43,17 @@ export type KyselyCacheAdapterSettings = {
|
|
|
46
43
|
*/
|
|
47
44
|
shouldRemoveExpiredKeys?: boolean;
|
|
48
45
|
};
|
|
46
|
+
/**
|
|
47
|
+
* @internal
|
|
48
|
+
*/
|
|
49
|
+
export declare class DatabaseCacheTransaction<TType> implements IDatabaseCacheTransaction<TType> {
|
|
50
|
+
private readonly kysely;
|
|
51
|
+
private readonly serde;
|
|
52
|
+
private readonly isMysql;
|
|
53
|
+
constructor(kysely: Kysely<KyselyCacheTables>, serde: ISerde<string>);
|
|
54
|
+
find(key: string): Promise<ICacheData<TType> | null>;
|
|
55
|
+
upsert(key: string, value: TType, expiration?: Date | null): Promise<void>;
|
|
56
|
+
}
|
|
49
57
|
/**
|
|
50
58
|
* To utilize the `KyselyCacheAdapter`, you must install the [`"kysely"`](https://www.npmjs.com/package/kysely) package and configure a `Kysely` class instance.
|
|
51
59
|
* The adapter have been tested with `sqlite`, `postgres` and `mysql` databases.
|
|
@@ -54,8 +62,7 @@ export type KyselyCacheAdapterSettings = {
|
|
|
54
62
|
* @group Adapters
|
|
55
63
|
*/
|
|
56
64
|
export declare class KyselyCacheAdapter<TType = unknown> implements IDatabaseCacheAdapter<TType>, IInitizable, IDeinitizable, IPrunable {
|
|
57
|
-
private
|
|
58
|
-
private static filterExpiredKeys;
|
|
65
|
+
private readonly isMysql;
|
|
59
66
|
private readonly serde;
|
|
60
67
|
private readonly kysely;
|
|
61
68
|
private readonly shouldRemoveExpiredKeys;
|
|
@@ -93,14 +100,12 @@ export declare class KyselyCacheAdapter<TType = unknown> implements IDatabaseCac
|
|
|
93
100
|
*/
|
|
94
101
|
deInit(): Promise<void>;
|
|
95
102
|
find(key: string): Promise<ICacheData<TType> | null>;
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
removeExpiredMany(keys: string[]): Promise<number>;
|
|
103
|
-
removeUnexpiredMany(keys: string[]): Promise<number>;
|
|
103
|
+
private _transaction;
|
|
104
|
+
transaction<TValue>(trxFn: InvokableFn<[
|
|
105
|
+
trx: IDatabaseCacheTransaction<TType>
|
|
106
|
+
], Promise<TValue>>): Promise<TValue>;
|
|
107
|
+
update(key: string, value: TType): Promise<ICacheDataExpiration | null>;
|
|
108
|
+
removeMany(keys: string[]): Promise<ICacheDataExpiration[]>;
|
|
104
109
|
removeAll(): Promise<void>;
|
|
105
110
|
removeByKeyPrefix(prefix: string): Promise<void>;
|
|
106
111
|
}
|
|
@@ -1,12 +1,70 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @module Cache
|
|
3
3
|
*/
|
|
4
|
-
import { MysqlAdapter, Transaction
|
|
4
|
+
import { MysqlAdapter, Transaction } from "kysely";
|
|
5
5
|
import {} from "../../../../cache/contracts/_module.js";
|
|
6
6
|
import {} from "../../../../serde/contracts/_module.js";
|
|
7
7
|
import {} from "../../../../time-span/contracts/_module.js";
|
|
8
8
|
import { TimeSpan } from "../../../../time-span/implementations/_module.js";
|
|
9
9
|
import {} from "../../../../utilities/_module.js";
|
|
10
|
+
/**
|
|
11
|
+
* @internal
|
|
12
|
+
*/
|
|
13
|
+
export class DatabaseCacheTransaction {
|
|
14
|
+
kysely;
|
|
15
|
+
serde;
|
|
16
|
+
isMysql;
|
|
17
|
+
constructor(kysely, serde) {
|
|
18
|
+
this.kysely = kysely;
|
|
19
|
+
this.serde = serde;
|
|
20
|
+
this.isMysql =
|
|
21
|
+
this.kysely.getExecutor().adapter instanceof MysqlAdapter;
|
|
22
|
+
}
|
|
23
|
+
async find(key) {
|
|
24
|
+
const cacheData = await this.kysely
|
|
25
|
+
.selectFrom("cache")
|
|
26
|
+
.where("cache.key", "=", key)
|
|
27
|
+
.select(["cache.expiration", "cache.value"])
|
|
28
|
+
.executeTakeFirst();
|
|
29
|
+
if (cacheData === undefined) {
|
|
30
|
+
return null;
|
|
31
|
+
}
|
|
32
|
+
return {
|
|
33
|
+
value: this.serde.deserialize(cacheData.value),
|
|
34
|
+
expiration: cacheData.expiration === null
|
|
35
|
+
? null
|
|
36
|
+
: new Date(Number(cacheData.expiration)),
|
|
37
|
+
};
|
|
38
|
+
}
|
|
39
|
+
async upsert(key, value, expiration) {
|
|
40
|
+
let expirationAsMs;
|
|
41
|
+
if (expiration instanceof Date) {
|
|
42
|
+
expirationAsMs = expiration.getTime();
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
expirationAsMs = expiration;
|
|
46
|
+
}
|
|
47
|
+
const serializedValue = this.serde.serialize(value);
|
|
48
|
+
await this.kysely
|
|
49
|
+
.insertInto("cache")
|
|
50
|
+
.values({
|
|
51
|
+
key,
|
|
52
|
+
value: serializedValue,
|
|
53
|
+
expiration: expirationAsMs,
|
|
54
|
+
})
|
|
55
|
+
.$if(!this.isMysql, (eb) => eb.onConflict((eb) => eb.column("key").doUpdateSet({
|
|
56
|
+
key,
|
|
57
|
+
value: serializedValue,
|
|
58
|
+
expiration: expirationAsMs,
|
|
59
|
+
})))
|
|
60
|
+
.$if(this.isMysql, (eb) => eb.onDuplicateKeyUpdate({
|
|
61
|
+
key,
|
|
62
|
+
value: serializedValue,
|
|
63
|
+
expiration: expirationAsMs,
|
|
64
|
+
}))
|
|
65
|
+
.execute();
|
|
66
|
+
}
|
|
67
|
+
}
|
|
10
68
|
/**
|
|
11
69
|
* To utilize the `KyselyCacheAdapter`, you must install the [`"kysely"`](https://www.npmjs.com/package/kysely) package and configure a `Kysely` class instance.
|
|
12
70
|
* The adapter have been tested with `sqlite`, `postgres` and `mysql` databases.
|
|
@@ -15,29 +73,7 @@ import {} from "../../../../utilities/_module.js";
|
|
|
15
73
|
* @group Adapters
|
|
16
74
|
*/
|
|
17
75
|
export class KyselyCacheAdapter {
|
|
18
|
-
|
|
19
|
-
return (eb) => {
|
|
20
|
-
const hasNoExpiration = eb("cache.expiration", "is", null);
|
|
21
|
-
const hasExpiration = eb("cache.expiration", "is not", null);
|
|
22
|
-
const hasNotExpired = eb("cache.expiration", ">", Date.now());
|
|
23
|
-
const keysMatch = eb("cache.key", "in", keys);
|
|
24
|
-
return eb.and([
|
|
25
|
-
keysMatch,
|
|
26
|
-
eb.or([
|
|
27
|
-
hasNoExpiration,
|
|
28
|
-
eb.and([hasExpiration, hasNotExpired]),
|
|
29
|
-
]),
|
|
30
|
-
]);
|
|
31
|
-
};
|
|
32
|
-
}
|
|
33
|
-
static filterExpiredKeys(keys) {
|
|
34
|
-
return (eb) => {
|
|
35
|
-
const keysMatch = eb("cache.key", "in", keys);
|
|
36
|
-
const hasExpiration = eb("cache.expiration", "is not", null);
|
|
37
|
-
const hasExpired = eb("cache.expiration", "<=", Date.now());
|
|
38
|
-
return eb.and([keysMatch, hasExpiration, hasExpired]);
|
|
39
|
-
};
|
|
40
|
-
}
|
|
76
|
+
isMysql;
|
|
41
77
|
serde;
|
|
42
78
|
kysely;
|
|
43
79
|
shouldRemoveExpiredKeys;
|
|
@@ -73,6 +109,8 @@ export class KyselyCacheAdapter {
|
|
|
73
109
|
this.serde = serde;
|
|
74
110
|
this.expiredKeysRemovalInterval = TimeSpan.fromTimeSpan(expiredKeysRemovalInterval);
|
|
75
111
|
this.shouldRemoveExpiredKeys = shouldRemoveExpiredKeys;
|
|
112
|
+
this.isMysql =
|
|
113
|
+
this.kysely.getExecutor().adapter instanceof MysqlAdapter;
|
|
76
114
|
}
|
|
77
115
|
async removeAllExpired() {
|
|
78
116
|
await this.kysely
|
|
@@ -138,135 +176,101 @@ export class KyselyCacheAdapter {
|
|
|
138
176
|
}
|
|
139
177
|
}
|
|
140
178
|
async find(key) {
|
|
141
|
-
const
|
|
179
|
+
const cacheData = await this.kysely
|
|
142
180
|
.selectFrom("cache")
|
|
143
181
|
.where("cache.key", "=", key)
|
|
144
182
|
.select(["cache.expiration", "cache.value"])
|
|
145
183
|
.executeTakeFirst();
|
|
146
|
-
if (
|
|
184
|
+
if (cacheData === undefined) {
|
|
147
185
|
return null;
|
|
148
186
|
}
|
|
149
187
|
return {
|
|
150
|
-
value: this.serde.deserialize(
|
|
151
|
-
expiration:
|
|
152
|
-
?
|
|
153
|
-
:
|
|
188
|
+
value: this.serde.deserialize(cacheData.value),
|
|
189
|
+
expiration: cacheData.expiration === null
|
|
190
|
+
? null
|
|
191
|
+
: new Date(Number(cacheData.expiration)),
|
|
154
192
|
};
|
|
155
193
|
}
|
|
156
|
-
|
|
157
|
-
await this.kysely
|
|
158
|
-
.insertInto("cache")
|
|
159
|
-
.values({
|
|
160
|
-
key: data.key,
|
|
161
|
-
value: this.serde.serialize(data.value),
|
|
162
|
-
expiration: data.expiration?.getTime() ?? null,
|
|
163
|
-
})
|
|
164
|
-
.executeTakeFirst();
|
|
165
|
-
}
|
|
166
|
-
async transaction(fn) {
|
|
194
|
+
_transaction(trxFn) {
|
|
167
195
|
if (this.disableTransaction) {
|
|
168
|
-
return
|
|
196
|
+
return trxFn(this.kysely);
|
|
169
197
|
}
|
|
170
|
-
return
|
|
171
|
-
|
|
172
|
-
.setIsolationLevel("serializable")
|
|
173
|
-
.execute(fn);
|
|
174
|
-
}
|
|
175
|
-
async upsert(data) {
|
|
176
|
-
const expiration = data.expiration?.getTime() ?? null;
|
|
177
|
-
return await this.transaction(async (trx) => {
|
|
178
|
-
const prevRow = await trx
|
|
179
|
-
.selectFrom("cache")
|
|
180
|
-
.select(["cache.key", "cache.value", "cache.expiration"])
|
|
181
|
-
.where(KyselyCacheAdapter.filterUnexpiredKeys([data.key]))
|
|
182
|
-
.executeTakeFirst();
|
|
183
|
-
const isMysqlRunning = this.kysely.getExecutor().adapter instanceof MysqlAdapter;
|
|
184
|
-
await trx
|
|
185
|
-
.insertInto("cache")
|
|
186
|
-
.values({
|
|
187
|
-
key: data.key,
|
|
188
|
-
value: this.serde.serialize(data.value),
|
|
189
|
-
expiration,
|
|
190
|
-
})
|
|
191
|
-
.$if(isMysqlRunning, (eb) => eb.onDuplicateKeyUpdate({
|
|
192
|
-
value: this.serde.serialize(data.value),
|
|
193
|
-
expiration,
|
|
194
|
-
}))
|
|
195
|
-
.$if(!isMysqlRunning, (eb) => eb.onConflict((eb) => eb.columns(["key"]).doUpdateSet({
|
|
196
|
-
value: this.serde.serialize(data.value),
|
|
197
|
-
expiration,
|
|
198
|
-
})))
|
|
199
|
-
.executeTakeFirst();
|
|
200
|
-
if (prevRow === undefined) {
|
|
201
|
-
return null;
|
|
202
|
-
}
|
|
203
|
-
return {
|
|
204
|
-
value: this.serde.deserialize(prevRow.value),
|
|
205
|
-
expiration: prevRow.expiration !== null
|
|
206
|
-
? new Date(Number(prevRow.expiration))
|
|
207
|
-
: null,
|
|
208
|
-
};
|
|
198
|
+
return this.kysely.transaction().execute(async (trx) => {
|
|
199
|
+
return await trxFn(trx);
|
|
209
200
|
});
|
|
210
201
|
}
|
|
211
|
-
async
|
|
212
|
-
|
|
213
|
-
.updateTable("cache")
|
|
214
|
-
.where(KyselyCacheAdapter.filterExpiredKeys([data.key]))
|
|
215
|
-
.set({
|
|
216
|
-
value: this.serde.serialize(data.value),
|
|
217
|
-
expiration: data.expiration?.getTime() ?? null,
|
|
218
|
-
})
|
|
219
|
-
.executeTakeFirst();
|
|
220
|
-
return Number(updateResult.numUpdatedRows);
|
|
221
|
-
}
|
|
222
|
-
async updateUnexpired(data) {
|
|
223
|
-
const updateResult = await this.kysely
|
|
224
|
-
.updateTable("cache")
|
|
225
|
-
.where(KyselyCacheAdapter.filterUnexpiredKeys([data.key]))
|
|
226
|
-
.set({
|
|
227
|
-
value: this.serde.serialize(data.value),
|
|
228
|
-
})
|
|
229
|
-
.executeTakeFirst();
|
|
230
|
-
return Number(updateResult.numUpdatedRows);
|
|
202
|
+
async transaction(trxFn) {
|
|
203
|
+
return await this._transaction((trx) => trxFn(new DatabaseCacheTransaction(trx, this.serde)));
|
|
231
204
|
}
|
|
232
|
-
async
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
205
|
+
async update(key, value) {
|
|
206
|
+
let row;
|
|
207
|
+
const serializedValue = this.serde.serialize(value);
|
|
208
|
+
if (this.isMysql) {
|
|
209
|
+
row = await this._transaction(async (trx) => {
|
|
210
|
+
const rows = await trx
|
|
211
|
+
.selectFrom("cache")
|
|
212
|
+
.where("cache.key", "=", key)
|
|
213
|
+
.select("cache.expiration")
|
|
214
|
+
.executeTakeFirst();
|
|
215
|
+
await trx
|
|
216
|
+
.updateTable("cache")
|
|
217
|
+
.where("cache.key", "=", key)
|
|
218
|
+
.set({
|
|
219
|
+
value: serializedValue,
|
|
220
|
+
})
|
|
221
|
+
.execute();
|
|
222
|
+
return rows;
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
else {
|
|
226
|
+
row = await this.kysely
|
|
248
227
|
.updateTable("cache")
|
|
249
|
-
.where(
|
|
228
|
+
.where("cache.key", "=", key)
|
|
250
229
|
.set({
|
|
251
|
-
value:
|
|
230
|
+
value: serializedValue,
|
|
252
231
|
})
|
|
232
|
+
.returning("cache.expiration")
|
|
253
233
|
.executeTakeFirst();
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
.
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
234
|
+
}
|
|
235
|
+
if (row === undefined) {
|
|
236
|
+
return null;
|
|
237
|
+
}
|
|
238
|
+
return {
|
|
239
|
+
expiration: row.expiration === null
|
|
240
|
+
? null
|
|
241
|
+
: new Date(Number(row.expiration)),
|
|
242
|
+
};
|
|
263
243
|
}
|
|
264
|
-
async
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
.
|
|
268
|
-
|
|
269
|
-
|
|
244
|
+
async removeMany(keys) {
|
|
245
|
+
let rows;
|
|
246
|
+
if (this.isMysql) {
|
|
247
|
+
rows = await this._transaction(async (trx) => {
|
|
248
|
+
const rows = await trx
|
|
249
|
+
.selectFrom("cache")
|
|
250
|
+
.where("cache.key", "in", keys)
|
|
251
|
+
.select("cache.expiration")
|
|
252
|
+
.execute();
|
|
253
|
+
await trx
|
|
254
|
+
.deleteFrom("cache")
|
|
255
|
+
.where("cache.key", "in", keys)
|
|
256
|
+
.execute();
|
|
257
|
+
return rows;
|
|
258
|
+
});
|
|
259
|
+
}
|
|
260
|
+
else {
|
|
261
|
+
rows = await this.kysely
|
|
262
|
+
.deleteFrom("cache")
|
|
263
|
+
.where("cache.key", "in", keys)
|
|
264
|
+
.returning("cache.expiration")
|
|
265
|
+
.execute();
|
|
266
|
+
}
|
|
267
|
+
return rows.map((row) => {
|
|
268
|
+
return {
|
|
269
|
+
expiration: row.expiration === null
|
|
270
|
+
? null
|
|
271
|
+
: new Date(Number(row.expiration)),
|
|
272
|
+
};
|
|
273
|
+
});
|
|
270
274
|
}
|
|
271
275
|
async removeAll() {
|
|
272
276
|
await this.kysely.deleteFrom("cache").execute();
|