slonik-interceptor-query-cache 2.4.1 → 3.1.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 +36 -8
- package/dist/{src/factories → factories}/createQueryCacheInterceptor.d.ts +2 -1
- package/dist/{src/factories → factories}/createQueryCacheInterceptor.js +14 -8
- package/dist/factories/createQueryContext.d.ts +3 -0
- package/dist/factories/createQueryContext.js +16 -0
- package/dist/factories/index.d.ts +1 -0
- package/dist/index.d.ts +1 -0
- package/dist/utilities/extractCacheAttributes.d.ts +9 -0
- package/dist/utilities/extractCacheAttributes.js +39 -0
- package/dist/utilities/extractCacheAttributes.test.d.ts +1 -0
- package/dist/utilities/extractCacheAttributes.test.js +76 -0
- package/dist/utilities/index.d.ts +2 -0
- package/dist/utilities/index.js +7 -0
- package/dist/utilities/normalizeCacheAttributes.d.ts +3 -0
- package/dist/utilities/normalizeCacheAttributes.js +13 -0
- package/dist/utilities/normalizeCacheAttributes.test.d.ts +1 -0
- package/dist/utilities/normalizeCacheAttributes.test.js +20 -0
- package/package.json +16 -16
- package/dist/src/factories/index.d.ts +0 -1
- package/dist/src/index.d.ts +0 -1
- package/dist/src/utilities/extractCacheAttributes.d.ts +0 -5
- package/dist/src/utilities/extractCacheAttributes.js +0 -23
- package/dist/src/utilities/index.d.ts +0 -1
- package/dist/src/utilities/index.js +0 -5
- package/src/Logger.ts +0 -7
- package/src/factories/createQueryCacheInterceptor.ts +0 -102
- package/src/factories/index.ts +0 -3
- package/src/index.ts +0 -3
- package/src/utilities/extractCacheAttributes.ts +0 -29
- package/src/utilities/index.ts +0 -3
- package/tsconfig.json +0 -26
- /package/dist/{src/Logger.d.ts → Logger.d.ts} +0 -0
- /package/dist/{src/Logger.js → Logger.js} +0 -0
- /package/dist/{src/factories → factories}/index.js +0 -0
- /package/dist/{src/index.js → index.js} +0 -0
package/README.md
CHANGED
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
# slonik-interceptor-query-cache
|
|
2
2
|
|
|
3
|
-
[](https://travis-ci.com/github/gajus/slonik-interceptor-query-cache)
|
|
4
|
-
[](https://coveralls.io/github/gajus/slonik-interceptor-query-cache)
|
|
5
3
|
[](https://www.npmjs.org/package/slonik-interceptor-query-cache)
|
|
6
4
|
[](https://github.com/gajus/canonical)
|
|
7
5
|
[](https://twitter.com/kuizinas)
|
|
@@ -10,7 +8,7 @@ Caches [Slonik](https://github.com/gajus/slonik) query results.
|
|
|
10
8
|
|
|
11
9
|
## Usage
|
|
12
10
|
|
|
13
|
-
Query cache interceptor is
|
|
11
|
+
Query cache interceptor is initialized using a custom storage service. The [Example Usage](#example-usage) documentation shows how to create a compatible storage service using [`node-cache`](https://www.npmjs.com/package/node-cache).
|
|
14
12
|
|
|
15
13
|
Which queries are cached is controlled using cache attributes. Cache attributes are comments starting with `-- @cache-` prefix. Only queries with cache attributes are cached (see [Cache attributes](#cache-attributes))
|
|
16
14
|
|
|
@@ -20,11 +18,17 @@ Which queries are cached is controlled using cache attributes. Cache attributes
|
|
|
20
18
|
|
|
21
19
|
## Cache attributes
|
|
22
20
|
|
|
23
|
-
|Cache attribute|Description|Required?|Default|
|
|
24
|
-
|
|
25
|
-
|`@cache-ttl`|Number (in seconds) to cache the query for.|Yes
|
|
26
|
-
|`@cache-key`|
|
|
27
|
-
|`@cache-
|
|
21
|
+
|Cache attribute|Description|Required?|Format|Default|
|
|
22
|
+
|---|---|---|---|---|
|
|
23
|
+
|`@cache-ttl`|Number (in seconds) to cache the query for.|Yes|`/^d+$/`|N/A|
|
|
24
|
+
|`@cache-key`|Cache key that uniquelly identifies the query.|No|`/^[$A-Za-z0-9\-_:]+$/`|`$bodyHash:$valueHash`|
|
|
25
|
+
|`@cache-discard-empty`|If set to `true`, then `storage.set` is not invoked when query produces no results.|No|`/^(false|true)$/`|`false`|
|
|
26
|
+
|
|
27
|
+
### `@cache-key`
|
|
28
|
+
|
|
29
|
+
Overrides the default cache key that uniquely identifies the query.
|
|
30
|
+
|
|
31
|
+
If present, `$bodyHash` is substituted with the hash of the query (comments and white-spaces are stripped before hashing the query). `$valueHash` is substituted with the hash of the parameter values.
|
|
28
32
|
|
|
29
33
|
### Example usage
|
|
30
34
|
|
|
@@ -61,6 +65,30 @@ const pool = await createPool('postgres://', {
|
|
|
61
65
|
]
|
|
62
66
|
});
|
|
63
67
|
|
|
68
|
+
// Caches the query results based on a combination of the query hash and the parameter value hash.
|
|
69
|
+
await connection.any(sql`
|
|
70
|
+
-- @cache-ttl 60
|
|
71
|
+
SELECT
|
|
72
|
+
id,
|
|
73
|
+
code_alpha_2
|
|
74
|
+
FROM country
|
|
75
|
+
WHERE
|
|
76
|
+
code_alpha_2 = ${countryCode}
|
|
77
|
+
`);
|
|
78
|
+
|
|
79
|
+
// Caches the query results based only on the parameter value hash.
|
|
80
|
+
await connection.any(sql`
|
|
81
|
+
-- @cache-ttl 60
|
|
82
|
+
-- @cache-key $bodyHash
|
|
83
|
+
SELECT
|
|
84
|
+
id,
|
|
85
|
+
code_alpha_2
|
|
86
|
+
FROM country
|
|
87
|
+
WHERE
|
|
88
|
+
code_alpha_2 = ${countryCode}
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
// Caches the query results using 'foo' key.
|
|
64
92
|
await connection.any(sql`
|
|
65
93
|
-- @cache-ttl 60
|
|
66
94
|
-- @cache-key foo
|
|
@@ -12,17 +12,17 @@ const createQueryCacheInterceptor = (configurationInput) => {
|
|
|
12
12
|
};
|
|
13
13
|
return {
|
|
14
14
|
beforeQueryExecution: async (context, query) => {
|
|
15
|
-
var _a;
|
|
16
15
|
if (context.transactionId) {
|
|
17
16
|
return null;
|
|
18
17
|
}
|
|
19
|
-
const cacheAttributes =
|
|
18
|
+
const cacheAttributes = context.sandbox.cache
|
|
19
|
+
?.cacheAttributes;
|
|
20
20
|
if (!cacheAttributes) {
|
|
21
21
|
return null;
|
|
22
22
|
}
|
|
23
23
|
const maybeResult = await configuration.storage.get(query, cacheAttributes);
|
|
24
24
|
if (maybeResult) {
|
|
25
|
-
log.
|
|
25
|
+
log.debug({
|
|
26
26
|
queryId: context.queryId,
|
|
27
27
|
}, 'query is served from cache');
|
|
28
28
|
return maybeResult;
|
|
@@ -30,13 +30,18 @@ const createQueryCacheInterceptor = (configurationInput) => {
|
|
|
30
30
|
return null;
|
|
31
31
|
},
|
|
32
32
|
beforeQueryResult: async (context, query, result) => {
|
|
33
|
-
var _a;
|
|
34
33
|
if (context.transactionId) {
|
|
35
34
|
return null;
|
|
36
35
|
}
|
|
37
|
-
const cacheAttributes =
|
|
36
|
+
const cacheAttributes = context.sandbox.cache
|
|
37
|
+
?.cacheAttributes;
|
|
38
38
|
if (cacheAttributes) {
|
|
39
|
-
|
|
39
|
+
if (cacheAttributes.discardEmpty && result.rowCount === 0) {
|
|
40
|
+
log.debug('@cache-discard-empty is set and the query result is empty; not caching');
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
await configuration.storage.set(query, cacheAttributes, result);
|
|
44
|
+
}
|
|
40
45
|
}
|
|
41
46
|
return null;
|
|
42
47
|
},
|
|
@@ -44,10 +49,11 @@ const createQueryCacheInterceptor = (configurationInput) => {
|
|
|
44
49
|
if (context.transactionId) {
|
|
45
50
|
return null;
|
|
46
51
|
}
|
|
47
|
-
const
|
|
48
|
-
if (!
|
|
52
|
+
const extractedCacheAttributes = (0, utilities_1.extractCacheAttributes)(query.sql, query.values);
|
|
53
|
+
if (!extractedCacheAttributes) {
|
|
49
54
|
return null;
|
|
50
55
|
}
|
|
56
|
+
const cacheAttributes = (0, utilities_1.normalizeCacheAttributes)(extractedCacheAttributes);
|
|
51
57
|
context.sandbox.cache = {
|
|
52
58
|
cacheAttributes,
|
|
53
59
|
};
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.default = () => {
|
|
4
|
+
return {
|
|
5
|
+
connectionId: '1',
|
|
6
|
+
log: {
|
|
7
|
+
getContext: () => {
|
|
8
|
+
return {
|
|
9
|
+
connectionId: '1',
|
|
10
|
+
poolId: '1',
|
|
11
|
+
};
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
poolId: '1',
|
|
15
|
+
};
|
|
16
|
+
};
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createQueryCacheInterceptor } from './createQueryCacheInterceptor';
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { createQueryCacheInterceptor } from './factories';
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { type PrimitiveValueExpression } from 'slonik';
|
|
2
|
+
export type ExtractedCacheAttributes = {
|
|
3
|
+
bodyHash: string;
|
|
4
|
+
discardEmpty: boolean;
|
|
5
|
+
key: string;
|
|
6
|
+
ttl: number;
|
|
7
|
+
valueHash: string;
|
|
8
|
+
};
|
|
9
|
+
export declare const extractCacheAttributes: (subject: string, values: readonly PrimitiveValueExpression[]) => ExtractedCacheAttributes | null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.extractCacheAttributes = void 0;
|
|
7
|
+
const node_crypto_1 = require("node:crypto");
|
|
8
|
+
const strip_comments_1 = __importDefault(require("strip-comments"));
|
|
9
|
+
const hash = (subject) => {
|
|
10
|
+
return (0, node_crypto_1.createHash)('shake256', {
|
|
11
|
+
outputLength: 12,
|
|
12
|
+
})
|
|
13
|
+
.update(subject)
|
|
14
|
+
.digest('hex');
|
|
15
|
+
};
|
|
16
|
+
// TODO throw an error if an unknown attribute is used
|
|
17
|
+
const extractCacheAttributes = (subject, values) => {
|
|
18
|
+
const ttl = /-- @cache-ttl (\d+)/u.exec(subject)?.[1];
|
|
19
|
+
// https://github.com/jonschlinkert/strip-comments/issues/71
|
|
20
|
+
const bodyHash = hash((0, strip_comments_1.default)(subject)
|
|
21
|
+
.replaceAll(/^\s*--.*$/gmu, '')
|
|
22
|
+
.replaceAll(/\s/gu, ''));
|
|
23
|
+
const discardEmpty = (/-- @cache-discard-empty (true|false)/u.exec(subject)?.[1] ?? 'false') ===
|
|
24
|
+
'true';
|
|
25
|
+
const valueHash = hash(JSON.stringify(values));
|
|
26
|
+
if (ttl) {
|
|
27
|
+
const key = /-- @cache-key ([$\w\-:/]+)/iu.exec(subject)?.[1] ??
|
|
28
|
+
'query:$bodyHash:$valueHash';
|
|
29
|
+
return {
|
|
30
|
+
bodyHash,
|
|
31
|
+
discardEmpty,
|
|
32
|
+
key,
|
|
33
|
+
ttl: Number(ttl),
|
|
34
|
+
valueHash,
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
};
|
|
39
|
+
exports.extractCacheAttributes = extractCacheAttributes;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const extractCacheAttributes_1 = require("./extractCacheAttributes");
|
|
7
|
+
const ava_1 = __importDefault(require("ava"));
|
|
8
|
+
(0, ava_1.default)('returns null when query does not contain cache attributes', (t) => {
|
|
9
|
+
t.is((0, extractCacheAttributes_1.extractCacheAttributes)('', []), null);
|
|
10
|
+
});
|
|
11
|
+
(0, ava_1.default)('extracts @cache-ttl', (t) => {
|
|
12
|
+
t.deepEqual((0, extractCacheAttributes_1.extractCacheAttributes)('-- @cache-ttl 60', []), {
|
|
13
|
+
bodyHash: '46b9dd2b0ba88d13233b3feb',
|
|
14
|
+
discardEmpty: false,
|
|
15
|
+
key: 'query:$bodyHash:$valueHash',
|
|
16
|
+
ttl: 60,
|
|
17
|
+
valueHash: 'ec784925b52067bce01fd820',
|
|
18
|
+
});
|
|
19
|
+
});
|
|
20
|
+
(0, ava_1.default)('extracts @cache-discard-empty', (t) => {
|
|
21
|
+
t.deepEqual((0, extractCacheAttributes_1.extractCacheAttributes)('-- @cache-ttl 60\n-- @cache-discard-empty false', []), {
|
|
22
|
+
bodyHash: '46b9dd2b0ba88d13233b3feb',
|
|
23
|
+
discardEmpty: false,
|
|
24
|
+
key: 'query:$bodyHash:$valueHash',
|
|
25
|
+
ttl: 60,
|
|
26
|
+
valueHash: 'ec784925b52067bce01fd820',
|
|
27
|
+
});
|
|
28
|
+
t.deepEqual((0, extractCacheAttributes_1.extractCacheAttributes)('-- @cache-ttl 60\n-- @cache-discard-empty true', []), {
|
|
29
|
+
bodyHash: '46b9dd2b0ba88d13233b3feb',
|
|
30
|
+
discardEmpty: true,
|
|
31
|
+
key: 'query:$bodyHash:$valueHash',
|
|
32
|
+
ttl: 60,
|
|
33
|
+
valueHash: 'ec784925b52067bce01fd820',
|
|
34
|
+
});
|
|
35
|
+
});
|
|
36
|
+
(0, ava_1.default)('computes the parameter value hash', (t) => {
|
|
37
|
+
t.deepEqual((0, extractCacheAttributes_1.extractCacheAttributes)('-- @cache-ttl 60', [1]), {
|
|
38
|
+
bodyHash: '46b9dd2b0ba88d13233b3feb',
|
|
39
|
+
discardEmpty: false,
|
|
40
|
+
key: 'query:$bodyHash:$valueHash',
|
|
41
|
+
ttl: 60,
|
|
42
|
+
valueHash: '62eb54746ae2932850f8d6ff',
|
|
43
|
+
});
|
|
44
|
+
});
|
|
45
|
+
(0, ava_1.default)('computes the body hash; white spaces do not affect the body hash', (t) => {
|
|
46
|
+
t.deepEqual((0, extractCacheAttributes_1.extractCacheAttributes)('-- @cache-ttl 60\nSELECT 1', []), {
|
|
47
|
+
bodyHash: '553ebdb024592064ca4c2c3a',
|
|
48
|
+
discardEmpty: false,
|
|
49
|
+
key: 'query:$bodyHash:$valueHash',
|
|
50
|
+
ttl: 60,
|
|
51
|
+
valueHash: 'ec784925b52067bce01fd820',
|
|
52
|
+
});
|
|
53
|
+
t.deepEqual((0, extractCacheAttributes_1.extractCacheAttributes)('-- @cache-ttl 60\n\nSELECT 1', []), {
|
|
54
|
+
bodyHash: '553ebdb024592064ca4c2c3a',
|
|
55
|
+
discardEmpty: false,
|
|
56
|
+
key: 'query:$bodyHash:$valueHash',
|
|
57
|
+
ttl: 60,
|
|
58
|
+
valueHash: 'ec784925b52067bce01fd820',
|
|
59
|
+
});
|
|
60
|
+
});
|
|
61
|
+
(0, ava_1.default)('computes the body hash; comments do not affect the body hash', (t) => {
|
|
62
|
+
t.deepEqual((0, extractCacheAttributes_1.extractCacheAttributes)('-- @cache-ttl 60\nSELECT 1', []), {
|
|
63
|
+
bodyHash: '553ebdb024592064ca4c2c3a',
|
|
64
|
+
discardEmpty: false,
|
|
65
|
+
key: 'query:$bodyHash:$valueHash',
|
|
66
|
+
ttl: 60,
|
|
67
|
+
valueHash: 'ec784925b52067bce01fd820',
|
|
68
|
+
});
|
|
69
|
+
t.deepEqual((0, extractCacheAttributes_1.extractCacheAttributes)('-- @cache-ttl 120\nSELECT 1', []), {
|
|
70
|
+
bodyHash: '553ebdb024592064ca4c2c3a',
|
|
71
|
+
discardEmpty: false,
|
|
72
|
+
key: 'query:$bodyHash:$valueHash',
|
|
73
|
+
ttl: 120,
|
|
74
|
+
valueHash: 'ec784925b52067bce01fd820',
|
|
75
|
+
});
|
|
76
|
+
});
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeCacheAttributes = exports.extractCacheAttributes = void 0;
|
|
4
|
+
var extractCacheAttributes_1 = require("./extractCacheAttributes");
|
|
5
|
+
Object.defineProperty(exports, "extractCacheAttributes", { enumerable: true, get: function () { return extractCacheAttributes_1.extractCacheAttributes; } });
|
|
6
|
+
var normalizeCacheAttributes_1 = require("./normalizeCacheAttributes");
|
|
7
|
+
Object.defineProperty(exports, "normalizeCacheAttributes", { enumerable: true, get: function () { return normalizeCacheAttributes_1.normalizeCacheAttributes; } });
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
import { type CacheAttributes } from '../factories/createQueryCacheInterceptor';
|
|
2
|
+
import { type ExtractedCacheAttributes } from './extractCacheAttributes';
|
|
3
|
+
export declare const normalizeCacheAttributes: (extractedCacheAttributes: ExtractedCacheAttributes) => CacheAttributes;
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.normalizeCacheAttributes = void 0;
|
|
4
|
+
const normalizeCacheAttributes = (extractedCacheAttributes) => {
|
|
5
|
+
return {
|
|
6
|
+
discardEmpty: extractedCacheAttributes.discardEmpty,
|
|
7
|
+
key: extractedCacheAttributes.key
|
|
8
|
+
.replaceAll('$bodyHash', extractedCacheAttributes.bodyHash)
|
|
9
|
+
.replaceAll('$valueHash', extractedCacheAttributes.valueHash),
|
|
10
|
+
ttl: extractedCacheAttributes.ttl,
|
|
11
|
+
};
|
|
12
|
+
};
|
|
13
|
+
exports.normalizeCacheAttributes = normalizeCacheAttributes;
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const normalizeCacheAttributes_1 = require("./normalizeCacheAttributes");
|
|
7
|
+
const ava_1 = __importDefault(require("ava"));
|
|
8
|
+
(0, ava_1.default)('replaces $bodyHash and $valueHash', (t) => {
|
|
9
|
+
t.deepEqual((0, normalizeCacheAttributes_1.normalizeCacheAttributes)({
|
|
10
|
+
bodyHash: 'foo',
|
|
11
|
+
discardEmpty: false,
|
|
12
|
+
key: '$bodyHash:$valueHash',
|
|
13
|
+
ttl: 60,
|
|
14
|
+
valueHash: 'bar',
|
|
15
|
+
}), {
|
|
16
|
+
discardEmpty: false,
|
|
17
|
+
key: 'foo:bar',
|
|
18
|
+
ttl: 60,
|
|
19
|
+
});
|
|
20
|
+
});
|
package/package.json
CHANGED
|
@@ -9,12 +9,16 @@
|
|
|
9
9
|
"ts"
|
|
10
10
|
],
|
|
11
11
|
"files": [
|
|
12
|
-
"test
|
|
12
|
+
"src/**/*.test.ts"
|
|
13
13
|
],
|
|
14
14
|
"require": [
|
|
15
15
|
"ts-node/register/transpile-only"
|
|
16
16
|
]
|
|
17
17
|
},
|
|
18
|
+
"dependencies": {
|
|
19
|
+
"roarr": "^7.14.0",
|
|
20
|
+
"strip-comments": "^2.0.1"
|
|
21
|
+
},
|
|
18
22
|
"description": "Cache Slonik query results.",
|
|
19
23
|
"devDependencies": {
|
|
20
24
|
"@istanbuljs/nyc-config-typescript": "^1.0.2",
|
|
@@ -24,9 +28,9 @@
|
|
|
24
28
|
"ava": "^3.15.0",
|
|
25
29
|
"coveralls": "^3.1.1",
|
|
26
30
|
"del-cli": "^4.0.1",
|
|
27
|
-
"eslint-config-canonical": "^41.0.3",
|
|
28
31
|
"eslint": "^8.39.0",
|
|
29
|
-
"
|
|
32
|
+
"eslint-config-canonical": "^41.0.3",
|
|
33
|
+
"husky": "^8.0.0",
|
|
30
34
|
"inline-loops.macro": "^1.2.2",
|
|
31
35
|
"nyc": "^15.1.0",
|
|
32
36
|
"semantic-release": "^20.1.3",
|
|
@@ -37,11 +41,9 @@
|
|
|
37
41
|
"engines": {
|
|
38
42
|
"node": ">=16.0"
|
|
39
43
|
},
|
|
40
|
-
"
|
|
41
|
-
"
|
|
42
|
-
|
|
43
|
-
}
|
|
44
|
-
},
|
|
44
|
+
"files": [
|
|
45
|
+
"dist"
|
|
46
|
+
],
|
|
45
47
|
"keywords": [
|
|
46
48
|
"postgresql",
|
|
47
49
|
"interceptor",
|
|
@@ -50,22 +52,20 @@
|
|
|
50
52
|
"license": "BSD-3-Clause",
|
|
51
53
|
"main": "./dist/src/index.js",
|
|
52
54
|
"name": "slonik-interceptor-query-cache",
|
|
55
|
+
"peerDependencies": {
|
|
56
|
+
"slonik": ">=27.0.0"
|
|
57
|
+
},
|
|
53
58
|
"repository": {
|
|
54
59
|
"type": "git",
|
|
55
60
|
"url": "https://github.com/gajus/slonik-interceptor-query-cache"
|
|
56
61
|
},
|
|
57
62
|
"scripts": {
|
|
58
63
|
"build": "del-cli ./dist && tsc",
|
|
59
|
-
"lint:eslint": "eslint
|
|
64
|
+
"lint:eslint": "eslint .",
|
|
60
65
|
"lint:tsc": "tsc --noEmit",
|
|
66
|
+
"prepare": "husky install",
|
|
61
67
|
"test:ava": "nyc ava --verbose --serial"
|
|
62
68
|
},
|
|
63
69
|
"typings": "./dist/src/index.d.ts",
|
|
64
|
-
"version": "
|
|
65
|
-
"peerDependencies": {
|
|
66
|
-
"slonik": ">=27.0.0"
|
|
67
|
-
},
|
|
68
|
-
"dependencies": {
|
|
69
|
-
"roarr": "^7.14.0"
|
|
70
|
-
}
|
|
70
|
+
"version": "3.1.0"
|
|
71
71
|
}
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createQueryCacheInterceptor, } from './createQueryCacheInterceptor';
|
package/dist/src/index.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { createQueryCacheInterceptor, } from './factories';
|
|
@@ -1,23 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.extractCacheAttributes = void 0;
|
|
4
|
-
const node_crypto_1 = require("node:crypto");
|
|
5
|
-
const extractCacheAttributes = (subject, values) => {
|
|
6
|
-
var _a, _b;
|
|
7
|
-
const ttl = (_a = /-- @cache-ttl (\d+)/u.exec(subject)) === null || _a === void 0 ? void 0 : _a[1];
|
|
8
|
-
if (ttl) {
|
|
9
|
-
let key = (_b = /-- @cache-key ([a-zA-Z0-9\-_:/]+)/ui.exec(subject)) === null || _b === void 0 ? void 0 : _b[1];
|
|
10
|
-
if (!key) {
|
|
11
|
-
throw new Error('@cache-key must be specified when @cache-ttl is specified.');
|
|
12
|
-
}
|
|
13
|
-
if (!/-- @cache-hash-values false/ui.test(subject) && values.length > 0) {
|
|
14
|
-
key += ':' + (0, node_crypto_1.createHash)('sha256').update(JSON.stringify(values)).digest('hex');
|
|
15
|
-
}
|
|
16
|
-
return {
|
|
17
|
-
key,
|
|
18
|
-
ttl: Number(ttl),
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
return null;
|
|
22
|
-
};
|
|
23
|
-
exports.extractCacheAttributes = extractCacheAttributes;
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export { extractCacheAttributes, } from './extractCacheAttributes';
|
|
@@ -1,5 +0,0 @@
|
|
|
1
|
-
"use strict";
|
|
2
|
-
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.extractCacheAttributes = void 0;
|
|
4
|
-
var extractCacheAttributes_1 = require("./extractCacheAttributes");
|
|
5
|
-
Object.defineProperty(exports, "extractCacheAttributes", { enumerable: true, get: function () { return extractCacheAttributes_1.extractCacheAttributes; } });
|
package/src/Logger.ts
DELETED
|
@@ -1,102 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
Logger,
|
|
3
|
-
} from '../Logger';
|
|
4
|
-
import {
|
|
5
|
-
extractCacheAttributes,
|
|
6
|
-
} from '../utilities';
|
|
7
|
-
import {
|
|
8
|
-
type Interceptor,
|
|
9
|
-
type Query,
|
|
10
|
-
type QueryResult,
|
|
11
|
-
type QueryResultRow,
|
|
12
|
-
} from 'slonik';
|
|
13
|
-
|
|
14
|
-
const log = Logger.child({
|
|
15
|
-
namespace: 'createQueryCacheInterceptor',
|
|
16
|
-
});
|
|
17
|
-
|
|
18
|
-
type Sandbox = {
|
|
19
|
-
cache: {
|
|
20
|
-
cacheAttributes: CacheAttributes,
|
|
21
|
-
},
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
type CacheAttributes = {
|
|
25
|
-
key: string,
|
|
26
|
-
ttl: number,
|
|
27
|
-
};
|
|
28
|
-
|
|
29
|
-
type Storage = {
|
|
30
|
-
get: (query: Query, cacheAttributes: CacheAttributes) => Promise<QueryResult<QueryResultRow> | null>,
|
|
31
|
-
set: (query: Query, cacheAttributes: CacheAttributes, queryResult: QueryResult<QueryResultRow>) => Promise<void>,
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
type ConfigurationInput = {
|
|
35
|
-
storage: Storage,
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
type Configuration = {
|
|
39
|
-
storage: Storage,
|
|
40
|
-
};
|
|
41
|
-
|
|
42
|
-
export const createQueryCacheInterceptor = (configurationInput: ConfigurationInput): Interceptor => {
|
|
43
|
-
const configuration: Configuration = {
|
|
44
|
-
...configurationInput,
|
|
45
|
-
};
|
|
46
|
-
|
|
47
|
-
return {
|
|
48
|
-
beforeQueryExecution: async (context, query) => {
|
|
49
|
-
if (context.transactionId) {
|
|
50
|
-
return null;
|
|
51
|
-
}
|
|
52
|
-
|
|
53
|
-
const cacheAttributes = (context.sandbox as Sandbox).cache?.cacheAttributes;
|
|
54
|
-
|
|
55
|
-
if (!cacheAttributes) {
|
|
56
|
-
return null;
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
const maybeResult = await configuration.storage.get(query, cacheAttributes);
|
|
60
|
-
|
|
61
|
-
if (maybeResult) {
|
|
62
|
-
log.info({
|
|
63
|
-
queryId: context.queryId,
|
|
64
|
-
}, 'query is served from cache');
|
|
65
|
-
|
|
66
|
-
return maybeResult;
|
|
67
|
-
}
|
|
68
|
-
|
|
69
|
-
return null;
|
|
70
|
-
},
|
|
71
|
-
beforeQueryResult: async (context, query, result) => {
|
|
72
|
-
if (context.transactionId) {
|
|
73
|
-
return null;
|
|
74
|
-
}
|
|
75
|
-
|
|
76
|
-
const cacheAttributes = (context.sandbox as Sandbox).cache?.cacheAttributes;
|
|
77
|
-
|
|
78
|
-
if (cacheAttributes) {
|
|
79
|
-
await configuration.storage.set(query, cacheAttributes, result);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return null;
|
|
83
|
-
},
|
|
84
|
-
beforeTransformQuery: async (context, query) => {
|
|
85
|
-
if (context.transactionId) {
|
|
86
|
-
return null;
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
const cacheAttributes = extractCacheAttributes(query.sql, query.values);
|
|
90
|
-
|
|
91
|
-
if (!cacheAttributes) {
|
|
92
|
-
return null;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
context.sandbox.cache = {
|
|
96
|
-
cacheAttributes,
|
|
97
|
-
};
|
|
98
|
-
|
|
99
|
-
return null;
|
|
100
|
-
},
|
|
101
|
-
};
|
|
102
|
-
};
|
package/src/factories/index.ts
DELETED
package/src/index.ts
DELETED
|
@@ -1,29 +0,0 @@
|
|
|
1
|
-
import {
|
|
2
|
-
createHash,
|
|
3
|
-
} from 'node:crypto';
|
|
4
|
-
import {
|
|
5
|
-
type PrimitiveValueExpression,
|
|
6
|
-
} from 'slonik';
|
|
7
|
-
|
|
8
|
-
export const extractCacheAttributes = (subject: string, values: readonly PrimitiveValueExpression[]) => {
|
|
9
|
-
const ttl = /-- @cache-ttl (\d+)/u.exec(subject)?.[1];
|
|
10
|
-
|
|
11
|
-
if (ttl) {
|
|
12
|
-
let key = /-- @cache-key ([a-zA-Z0-9\-_:/]+)/ui.exec(subject)?.[1];
|
|
13
|
-
|
|
14
|
-
if (!key) {
|
|
15
|
-
throw new Error('@cache-key must be specified when @cache-ttl is specified.');
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
if (!/-- @cache-hash-values false/ui.test(subject) && values.length > 0) {
|
|
19
|
-
key += ':' + createHash('sha256').update(JSON.stringify(values)).digest('hex');
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
return {
|
|
23
|
-
key,
|
|
24
|
-
ttl: Number(ttl),
|
|
25
|
-
};
|
|
26
|
-
}
|
|
27
|
-
|
|
28
|
-
return null;
|
|
29
|
-
};
|
package/src/utilities/index.ts
DELETED
package/tsconfig.json
DELETED
|
@@ -1,26 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"allowSyntheticDefaultImports": true,
|
|
4
|
-
"declaration": true,
|
|
5
|
-
"esModuleInterop": true,
|
|
6
|
-
"forceConsistentCasingInFileNames": true,
|
|
7
|
-
"module": "commonjs",
|
|
8
|
-
"moduleResolution": "node",
|
|
9
|
-
"noImplicitAny": false,
|
|
10
|
-
"noImplicitReturns": true,
|
|
11
|
-
"noUnusedLocals": true,
|
|
12
|
-
"noUnusedParameters": false,
|
|
13
|
-
"outDir": "dist",
|
|
14
|
-
"skipLibCheck": true,
|
|
15
|
-
"strict": true,
|
|
16
|
-
"target": "es2018"
|
|
17
|
-
},
|
|
18
|
-
"exclude": [
|
|
19
|
-
"dist",
|
|
20
|
-
"node_modules"
|
|
21
|
-
],
|
|
22
|
-
"include": [
|
|
23
|
-
"src",
|
|
24
|
-
"test"
|
|
25
|
-
]
|
|
26
|
-
}
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|