@smartive/datocms-utils 2.3.3 → 3.0.0-next.10
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 +134 -56
- package/dist/cache-tags/index.d.ts +2 -0
- package/dist/cache-tags/index.js +3 -0
- package/dist/cache-tags/index.js.map +1 -0
- package/dist/cache-tags/provider/neon.d.ts +33 -0
- package/dist/cache-tags/provider/neon.js +44 -0
- package/dist/cache-tags/provider/neon.js.map +1 -0
- package/dist/cache-tags/provider/noop.d.ts +12 -0
- package/dist/cache-tags/provider/noop.js +24 -0
- package/dist/cache-tags/provider/noop.js.map +1 -0
- package/dist/cache-tags/provider/redis.d.ts +33 -0
- package/dist/cache-tags/provider/redis.js +71 -0
- package/dist/cache-tags/provider/redis.js.map +1 -0
- package/dist/cache-tags/types.d.ts +63 -0
- package/dist/cache-tags/types.js.map +1 -0
- package/dist/{utils.d.ts → cache-tags/utils.d.ts} +1 -1
- package/dist/cache-tags/utils.js.map +1 -0
- package/dist/classnames.d.ts +1 -1
- package/dist/classnames.js +3 -1
- package/dist/classnames.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/package.json +39 -10
- package/src/cache-tags/index.ts +2 -0
- package/src/cache-tags/provider/neon.ts +80 -0
- package/src/cache-tags/provider/noop.ts +32 -0
- package/src/cache-tags/provider/redis.ts +104 -0
- package/src/cache-tags/types.ts +66 -0
- package/src/{utils.ts → cache-tags/utils.ts} +1 -1
- package/src/classnames.ts +7 -1
- package/src/index.ts +2 -4
- package/dist/cache-tags-redis.d.ts +0 -39
- package/dist/cache-tags-redis.js +0 -80
- package/dist/cache-tags-redis.js.map +0 -1
- package/dist/cache-tags.d.ts +0 -43
- package/dist/cache-tags.js +0 -72
- package/dist/cache-tags.js.map +0 -1
- package/dist/types.d.ts +0 -25
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/cache-tags-redis.ts +0 -96
- package/src/cache-tags.ts +0 -88
- package/src/types.ts +0 -24
- /package/dist/{types.js → cache-tags/types.js} +0 -0
- /package/dist/{utils.js → cache-tags/utils.js} +0 -0
package/package.json
CHANGED
|
@@ -1,16 +1,38 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartive/datocms-utils",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "3.0.0-next.10",
|
|
4
4
|
"description": "A set of utilities and helpers to work with DatoCMS in a Next.js project.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"source": "./src/index.ts",
|
|
7
|
-
"
|
|
8
|
-
|
|
7
|
+
"exports": {
|
|
8
|
+
".": {
|
|
9
|
+
"types": "./dist/index.d.ts",
|
|
10
|
+
"import": "./dist/index.js"
|
|
11
|
+
},
|
|
12
|
+
"./cache-tags": {
|
|
13
|
+
"types": "./dist/cache-tags/index.d.ts",
|
|
14
|
+
"import": "./dist/cache-tags/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./cache-tags/redis": {
|
|
17
|
+
"types": "./dist/cache-tags/provider/redis.d.ts",
|
|
18
|
+
"import": "./dist/cache-tags/provider/redis.js"
|
|
19
|
+
},
|
|
20
|
+
"./cache-tags/neon": {
|
|
21
|
+
"types": "./dist/cache-tags/provider/neon.d.ts",
|
|
22
|
+
"import": "./dist/cache-tags/provider/neon.js"
|
|
23
|
+
},
|
|
24
|
+
"./cache-tags/noop": {
|
|
25
|
+
"types": "./dist/cache-tags/provider/noop.d.ts",
|
|
26
|
+
"import": "./dist/cache-tags/provider/noop.js"
|
|
27
|
+
}
|
|
28
|
+
},
|
|
9
29
|
"files": [
|
|
10
30
|
"dist/**/*",
|
|
11
31
|
"src/**/*"
|
|
12
32
|
],
|
|
13
33
|
"scripts": {
|
|
34
|
+
"clean": "rimraf dist",
|
|
35
|
+
"prebuild": "npm run clean",
|
|
14
36
|
"build": "tsc",
|
|
15
37
|
"lint": "eslint src",
|
|
16
38
|
"prettier": "prettier --check src"
|
|
@@ -28,23 +50,30 @@
|
|
|
28
50
|
"type": "git"
|
|
29
51
|
},
|
|
30
52
|
"devDependencies": {
|
|
53
|
+
"@neondatabase/serverless": "1.0.2",
|
|
31
54
|
"@smartive/eslint-config": "7.0.1",
|
|
32
55
|
"@smartive/prettier-config": "3.1.2",
|
|
33
|
-
"@types/node": "24.10.
|
|
56
|
+
"@types/node": "24.10.13",
|
|
34
57
|
"eslint": "9.39.2",
|
|
35
58
|
"eslint-import-resolver-typescript": "4.4.4",
|
|
59
|
+
"graphql": "16.12.0",
|
|
60
|
+
"ioredis": "5.9.2",
|
|
36
61
|
"prettier": "3.8.1",
|
|
37
|
-
"
|
|
38
|
-
"
|
|
39
|
-
},
|
|
40
|
-
"dependencies": {
|
|
41
|
-
"@vercel/postgres": "^0.10.0",
|
|
42
|
-
"graphql": "^16.9.0"
|
|
62
|
+
"rimraf": "6.1.2",
|
|
63
|
+
"typescript": "5.9.3"
|
|
43
64
|
},
|
|
44
65
|
"peerDependencies": {
|
|
66
|
+
"@neondatabase/serverless": "^1.0.0",
|
|
67
|
+
"graphql": "^15.0.0 || ^16.0.0",
|
|
45
68
|
"ioredis": "^5.4.0"
|
|
46
69
|
},
|
|
47
70
|
"peerDependenciesMeta": {
|
|
71
|
+
"@neondatabase/serverless": {
|
|
72
|
+
"optional": true
|
|
73
|
+
},
|
|
74
|
+
"graphql": {
|
|
75
|
+
"optional": true
|
|
76
|
+
},
|
|
48
77
|
"ioredis": {
|
|
49
78
|
"optional": true
|
|
50
79
|
}
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { neon } from '@neondatabase/serverless';
|
|
2
|
+
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
3
|
+
|
|
4
|
+
type NeonCacheTagsProviderConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* Neon connection string. You can find it in the "Connection" tab of your Neon project dashboard.
|
|
7
|
+
* Has the format `postgresql://user:pass@host/db`
|
|
8
|
+
*/
|
|
9
|
+
readonly connectionUrl: string;
|
|
10
|
+
/**
|
|
11
|
+
* Name of the table where cache tags will be stored. The table must have the following schema:
|
|
12
|
+
*
|
|
13
|
+
* ```sql
|
|
14
|
+
* CREATE TABLE your_table_name (
|
|
15
|
+
* query_id TEXT NOT NULL,
|
|
16
|
+
* cache_tag TEXT NOT NULL,
|
|
17
|
+
* PRIMARY KEY (query_id, cache_tag)
|
|
18
|
+
* );
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
readonly table: string;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* A `CacheTagsProvider` implementation that uses Neon as the storage backend.
|
|
26
|
+
*/
|
|
27
|
+
export class NeonCacheTagsProvider implements CacheTagsProvider {
|
|
28
|
+
private readonly sql;
|
|
29
|
+
private readonly table;
|
|
30
|
+
|
|
31
|
+
constructor({ connectionUrl, table }: NeonCacheTagsProviderConfig) {
|
|
32
|
+
this.sql = neon(connectionUrl, { fullResults: true });
|
|
33
|
+
this.table = table;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) {
|
|
37
|
+
if (!cacheTags?.length) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]);
|
|
42
|
+
const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(',');
|
|
43
|
+
|
|
44
|
+
await this.sql.query(`INSERT INTO ${this.table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]> {
|
|
48
|
+
if (!cacheTags?.length) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
53
|
+
|
|
54
|
+
const { rows } = await this.sql.query(
|
|
55
|
+
`SELECT DISTINCT query_id FROM ${this.table} WHERE cache_tag IN (${placeholders})`,
|
|
56
|
+
cacheTags,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return rows.reduce<string[]>((queryIds, row) => {
|
|
60
|
+
if (typeof row.query_id === 'string') {
|
|
61
|
+
queryIds.push(row.query_id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return queryIds;
|
|
65
|
+
}, []);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public async deleteCacheTags(cacheTags: CacheTag[]) {
|
|
69
|
+
if (cacheTags.length === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
73
|
+
|
|
74
|
+
return (await this.sql.query(`DELETE FROM ${this.table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async truncateCacheTags() {
|
|
78
|
+
return (await this.sql.query(`DELETE FROM ${this.table}`)).rowCount ?? 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A `CacheTagsProvider` implementation that does not perform any actual storage operations.
|
|
5
|
+
*
|
|
6
|
+
* _Note: This implementation is useful for testing purposes or when you want to disable caching without changing the code that interacts with the cache._
|
|
7
|
+
*/
|
|
8
|
+
export class NoopCacheTagsProvider implements CacheTagsProvider {
|
|
9
|
+
public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) {
|
|
10
|
+
console.debug('-- storeQueryCacheTags called', { queryId, cacheTags });
|
|
11
|
+
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]> {
|
|
16
|
+
console.debug('-- queriesReferencingCacheTags called', { cacheTags });
|
|
17
|
+
|
|
18
|
+
return Promise.resolve([]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async deleteCacheTags(cacheTags: CacheTag[]) {
|
|
22
|
+
console.debug('-- deleteCacheTags called', { cacheTags });
|
|
23
|
+
|
|
24
|
+
return Promise.resolve(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async truncateCacheTags() {
|
|
28
|
+
console.debug('-- truncateCacheTags called');
|
|
29
|
+
|
|
30
|
+
return Promise.resolve(0);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
3
|
+
|
|
4
|
+
type RedisCacheTagsProviderConfig = {
|
|
5
|
+
/**
|
|
6
|
+
* Redis connection string. For example, `redis://user:pass@host:port/db`.
|
|
7
|
+
*/
|
|
8
|
+
readonly connectionUrl: string;
|
|
9
|
+
/**
|
|
10
|
+
* Optional prefix for Redis keys. If provided, all keys used to store cache tags will be prefixed with this value.
|
|
11
|
+
* This can be useful to avoid key collisions if the same Redis instance is used for multiple purposes.
|
|
12
|
+
* For example, if you set `keyPrefix` to `'myapp:'`, a cache tag like `'tag1'` will be stored under the key `'myapp:tag1'`.
|
|
13
|
+
*/
|
|
14
|
+
readonly keyPrefix?: string;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* A `CacheTagsProvider` implementation that uses Redis as the storage backend.
|
|
19
|
+
*/
|
|
20
|
+
export class RedisCacheTagsProvider implements CacheTagsProvider {
|
|
21
|
+
private readonly redis;
|
|
22
|
+
private readonly keyPrefix;
|
|
23
|
+
|
|
24
|
+
constructor({ connectionUrl, keyPrefix }: RedisCacheTagsProviderConfig) {
|
|
25
|
+
this.redis = new Redis(connectionUrl, {
|
|
26
|
+
maxRetriesPerRequest: 3,
|
|
27
|
+
lazyConnect: true,
|
|
28
|
+
});
|
|
29
|
+
this.keyPrefix = keyPrefix ?? '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) {
|
|
33
|
+
if (!cacheTags?.length) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pipeline = this.redis.pipeline();
|
|
38
|
+
|
|
39
|
+
for (const tag of cacheTags) {
|
|
40
|
+
pipeline.sadd(`${this.keyPrefix}${tag}`, queryId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await pipeline.exec();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async queriesReferencingCacheTags(cacheTags: CacheTag[]) {
|
|
47
|
+
if (!cacheTags?.length) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const keys = cacheTags.map((tag) => `${this.keyPrefix}${tag}`);
|
|
52
|
+
|
|
53
|
+
return this.redis.sunion(...keys);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async deleteCacheTags(cacheTags: CacheTag[]) {
|
|
57
|
+
if (!cacheTags?.length) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const keys = cacheTags.map((tag) => `${this.keyPrefix}${tag}`);
|
|
62
|
+
|
|
63
|
+
return this.redis.del(...keys);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async truncateCacheTags() {
|
|
67
|
+
const keys = await this.getKeys();
|
|
68
|
+
|
|
69
|
+
if (keys.length === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return await this.redis.del(...keys);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Retrieves all keys matching the given pattern using the Redis SCAN command.
|
|
78
|
+
* This method is more efficient than using the KEYS command, especially for large datasets.
|
|
79
|
+
*
|
|
80
|
+
* @returns An array of matching keys
|
|
81
|
+
*/
|
|
82
|
+
private async getKeys(): Promise<string[]> {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const keys: string[] = [];
|
|
85
|
+
|
|
86
|
+
const stream = this.redis.scanStream({
|
|
87
|
+
match: `${this.keyPrefix}*`,
|
|
88
|
+
count: 1000,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
stream.on('data', (resultKeys: string[]) => {
|
|
92
|
+
keys.push(...resultKeys);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
stream.on('end', () => {
|
|
96
|
+
resolve(keys);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
stream.on('error', (err) => {
|
|
100
|
+
reject(err);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A branded type for cache tags. This is created by intersecting `string`
|
|
3
|
+
* with `{ readonly _: unique symbol }`, making it a unique type.
|
|
4
|
+
* Although it is fundamentally a string, it is treated as a distinct type
|
|
5
|
+
* due to the unique symbol.
|
|
6
|
+
*/
|
|
7
|
+
export type CacheTag = string & { readonly _: unique symbol };
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* A type representing the structure of a webhook payload for cache tag invalidation.
|
|
11
|
+
* It includes the entity type, event type, and the entity details which contain
|
|
12
|
+
* the cache tags to be invalidated.
|
|
13
|
+
*/
|
|
14
|
+
export type CacheTagsInvalidateWebhook = {
|
|
15
|
+
entity_type: 'cda_cache_tags';
|
|
16
|
+
event_type: 'invalidate';
|
|
17
|
+
entity: {
|
|
18
|
+
id: 'cda_cache_tags';
|
|
19
|
+
type: 'cda_cache_tags';
|
|
20
|
+
attributes: {
|
|
21
|
+
tags: CacheTag[];
|
|
22
|
+
};
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Configuration object for creating a `CacheTagsProvider` implementation.
|
|
28
|
+
*/
|
|
29
|
+
export interface CacheTagsProvider {
|
|
30
|
+
/**
|
|
31
|
+
* Stores the cache tags of a query.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} queryId Unique query ID
|
|
34
|
+
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
35
|
+
*
|
|
36
|
+
*/
|
|
37
|
+
storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]): Promise<void>;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Retrieves the query IDs that reference any of the specified cache tags.
|
|
41
|
+
*
|
|
42
|
+
* @param {CacheTag[]} cacheTags Array of cache tags to check
|
|
43
|
+
* @returns Array of unique query IDs
|
|
44
|
+
*
|
|
45
|
+
*/
|
|
46
|
+
queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]>;
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Deletes the specified cache tags.
|
|
50
|
+
*
|
|
51
|
+
* This removes the cache tag keys entirely. When queries are revalidated and
|
|
52
|
+
* run again, fresh cache tag mappings will be created.
|
|
53
|
+
*
|
|
54
|
+
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
55
|
+
* @returns Number of keys deleted, or null if there was an error
|
|
56
|
+
*
|
|
57
|
+
*/
|
|
58
|
+
deleteCacheTags(cacheTags: CacheTag[]): Promise<number>;
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Wipes out all cache tags.
|
|
62
|
+
*
|
|
63
|
+
* ⚠️ **Warning**: This will delete all cache tag data. Use with caution!
|
|
64
|
+
*/
|
|
65
|
+
truncateCacheTags(): Promise<number>;
|
|
66
|
+
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { print, type DocumentNode } from 'graphql';
|
|
2
2
|
import { createHash } from 'node:crypto';
|
|
3
|
-
import { type CacheTag } from './types';
|
|
3
|
+
import { type CacheTag } from './types.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* Converts the value of DatoCMS's `X-Cache-Tags` header into an array of strings typed as `CacheTag`.
|
package/src/classnames.ts
CHANGED
|
@@ -4,4 +4,10 @@
|
|
|
4
4
|
* @param classNames Array of class names
|
|
5
5
|
* @returns Clean string to be used for class name
|
|
6
6
|
*/
|
|
7
|
-
export const classNames = (...classNames:
|
|
7
|
+
export const classNames = (...classNames: unknown[]) =>
|
|
8
|
+
classNames
|
|
9
|
+
.filter(
|
|
10
|
+
(value): value is string | number =>
|
|
11
|
+
(typeof value === 'string' && value.length > 0) || (typeof value === 'number' && Number.isFinite(value)),
|
|
12
|
+
)
|
|
13
|
+
.join(' ');
|
package/src/index.ts
CHANGED
|
@@ -1,39 +0,0 @@
|
|
|
1
|
-
import { type CacheTag } from './types';
|
|
2
|
-
/**
|
|
3
|
-
* Stores the cache tags of a query in Redis.
|
|
4
|
-
*
|
|
5
|
-
* For each cache tag, adds the query ID to a Redis Set. Sets are unordered
|
|
6
|
-
* collections of unique strings, perfect for tracking which queries use which tags.
|
|
7
|
-
*
|
|
8
|
-
* @param {string} queryId Unique query ID
|
|
9
|
-
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
10
|
-
*
|
|
11
|
-
*/
|
|
12
|
-
export declare const storeQueryCacheTags: (queryId: string, cacheTags: CacheTag[]) => Promise<void>;
|
|
13
|
-
/**
|
|
14
|
-
* Retrieves the query IDs that reference any of the specified cache tags.
|
|
15
|
-
*
|
|
16
|
-
* Uses Redis SUNION to efficiently find all queries associated with the given tags.
|
|
17
|
-
*
|
|
18
|
-
* @param {CacheTag[]} cacheTags Array of cache tags to check
|
|
19
|
-
* @returns Array of unique query IDs
|
|
20
|
-
*
|
|
21
|
-
*/
|
|
22
|
-
export declare const queriesReferencingCacheTags: (cacheTags: CacheTag[]) => Promise<string[]>;
|
|
23
|
-
/**
|
|
24
|
-
* Deletes the specified cache tags from Redis.
|
|
25
|
-
*
|
|
26
|
-
* This removes the cache tag keys entirely. When queries are revalidated and
|
|
27
|
-
* run again, fresh cache tag mappings will be created.
|
|
28
|
-
*
|
|
29
|
-
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
30
|
-
* @returns Number of keys deleted, or null if there was an error
|
|
31
|
-
*
|
|
32
|
-
*/
|
|
33
|
-
export declare const deleteCacheTags: (cacheTags: CacheTag[]) => Promise<number>;
|
|
34
|
-
/**
|
|
35
|
-
* Wipes out all cache tags from Redis.
|
|
36
|
-
*
|
|
37
|
-
* ⚠️ **Warning**: This will delete all cache tag data. Use with caution!
|
|
38
|
-
*/
|
|
39
|
-
export declare const truncateCacheTags: () => Promise<void>;
|
package/dist/cache-tags-redis.js
DELETED
|
@@ -1,80 +0,0 @@
|
|
|
1
|
-
import { Redis } from 'ioredis';
|
|
2
|
-
let redis = null;
|
|
3
|
-
const getRedis = () => {
|
|
4
|
-
redis ??= new Redis(process.env.REDIS_URL, {
|
|
5
|
-
maxRetriesPerRequest: 3,
|
|
6
|
-
lazyConnect: true,
|
|
7
|
-
});
|
|
8
|
-
return redis;
|
|
9
|
-
};
|
|
10
|
-
const keyPrefix = process.env.REDIS_KEY_PREFIX ? `${process.env.REDIS_KEY_PREFIX}:` : '';
|
|
11
|
-
/**
|
|
12
|
-
* Stores the cache tags of a query in Redis.
|
|
13
|
-
*
|
|
14
|
-
* For each cache tag, adds the query ID to a Redis Set. Sets are unordered
|
|
15
|
-
* collections of unique strings, perfect for tracking which queries use which tags.
|
|
16
|
-
*
|
|
17
|
-
* @param {string} queryId Unique query ID
|
|
18
|
-
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
19
|
-
*
|
|
20
|
-
*/
|
|
21
|
-
export const storeQueryCacheTags = async (queryId, cacheTags) => {
|
|
22
|
-
if (!cacheTags?.length) {
|
|
23
|
-
return;
|
|
24
|
-
}
|
|
25
|
-
const redis = getRedis();
|
|
26
|
-
const pipeline = redis.pipeline();
|
|
27
|
-
for (const tag of cacheTags) {
|
|
28
|
-
pipeline.sadd(`${keyPrefix}${tag}`, queryId);
|
|
29
|
-
}
|
|
30
|
-
await pipeline.exec();
|
|
31
|
-
};
|
|
32
|
-
/**
|
|
33
|
-
* Retrieves the query IDs that reference any of the specified cache tags.
|
|
34
|
-
*
|
|
35
|
-
* Uses Redis SUNION to efficiently find all queries associated with the given tags.
|
|
36
|
-
*
|
|
37
|
-
* @param {CacheTag[]} cacheTags Array of cache tags to check
|
|
38
|
-
* @returns Array of unique query IDs
|
|
39
|
-
*
|
|
40
|
-
*/
|
|
41
|
-
export const queriesReferencingCacheTags = async (cacheTags) => {
|
|
42
|
-
if (!cacheTags?.length) {
|
|
43
|
-
return [];
|
|
44
|
-
}
|
|
45
|
-
const redis = getRedis();
|
|
46
|
-
const keys = cacheTags.map((tag) => `${keyPrefix}${tag}`);
|
|
47
|
-
return redis.sunion(...keys);
|
|
48
|
-
};
|
|
49
|
-
/**
|
|
50
|
-
* Deletes the specified cache tags from Redis.
|
|
51
|
-
*
|
|
52
|
-
* This removes the cache tag keys entirely. When queries are revalidated and
|
|
53
|
-
* run again, fresh cache tag mappings will be created.
|
|
54
|
-
*
|
|
55
|
-
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
56
|
-
* @returns Number of keys deleted, or null if there was an error
|
|
57
|
-
*
|
|
58
|
-
*/
|
|
59
|
-
export const deleteCacheTags = async (cacheTags) => {
|
|
60
|
-
if (!cacheTags?.length) {
|
|
61
|
-
return 0;
|
|
62
|
-
}
|
|
63
|
-
const redis = getRedis();
|
|
64
|
-
const keys = cacheTags.map((tag) => `${keyPrefix}${tag}`);
|
|
65
|
-
return redis.del(...keys);
|
|
66
|
-
};
|
|
67
|
-
/**
|
|
68
|
-
* Wipes out all cache tags from Redis.
|
|
69
|
-
*
|
|
70
|
-
* ⚠️ **Warning**: This will delete all cache tag data. Use with caution!
|
|
71
|
-
*/
|
|
72
|
-
export const truncateCacheTags = async () => {
|
|
73
|
-
const redis = getRedis();
|
|
74
|
-
const pattern = `${keyPrefix}*`;
|
|
75
|
-
const keys = await redis.keys(pattern);
|
|
76
|
-
if (keys.length > 0) {
|
|
77
|
-
await redis.del(...keys);
|
|
78
|
-
}
|
|
79
|
-
};
|
|
80
|
-
//# sourceMappingURL=cache-tags-redis.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cache-tags-redis.js","sourceRoot":"","sources":["../src/cache-tags-redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,IAAI,KAAK,GAAiB,IAAI,CAAC;AAE/B,MAAM,QAAQ,GAAG,GAAU,EAAE;IAC3B,KAAK,KAAK,IAAI,KAAK,CAAC,OAAO,CAAC,GAAG,CAAC,SAAU,EAAE;QAC1C,oBAAoB,EAAE,CAAC;QACvB,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAEH,OAAO,KAAK,CAAC;AACf,CAAC,CAAC;AAEF,MAAM,SAAS,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,CAAC,CAAC,CAAC,GAAG,OAAO,CAAC,GAAG,CAAC,gBAAgB,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC;AAEzF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,mBAAmB,GAAG,KAAK,EAAE,OAAe,EAAE,SAAqB,EAAiB,EAAE;IACjG,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;QACvB,OAAO;IACT,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;IAElC,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;QAC5B,QAAQ,CAAC,IAAI,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC;IAC/C,CAAC;IAED,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;AACxB,CAAC,CAAC;AAEF;;;;;;;;GAQG;AACH,MAAM,CAAC,MAAM,2BAA2B,GAAG,KAAK,EAAE,SAAqB,EAAqB,EAAE;IAC5F,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;QACvB,OAAO,EAAE,CAAC;IACZ,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;IAE1D,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;AAC/B,CAAC,CAAC;AAEF;;;;;;;;;GASG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,KAAK,EAAE,SAAqB,EAAmB,EAAE;IAC9E,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;QACvB,OAAO,CAAC,CAAC;IACX,CAAC;IAED,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;IAE1D,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;AAC5B,CAAC,CAAC;AAEF;;;;GAIG;AACH,MAAM,CAAC,MAAM,iBAAiB,GAAG,KAAK,IAAmB,EAAE;IACzD,MAAM,KAAK,GAAG,QAAQ,EAAE,CAAC;IACzB,MAAM,OAAO,GAAG,GAAG,SAAS,GAAG,CAAC;IAChC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IAEvC,IAAI,IAAI,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;QACpB,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IAC3B,CAAC;AACH,CAAC,CAAC"}
|
package/dist/cache-tags.d.ts
DELETED
|
@@ -1,43 +0,0 @@
|
|
|
1
|
-
import { type CacheTag } from './types';
|
|
2
|
-
export { generateQueryId, parseXCacheTagsResponseHeader } from './utils';
|
|
3
|
-
/**
|
|
4
|
-
* Stores the cache tags of a query in the database.
|
|
5
|
-
*
|
|
6
|
-
* @param {string} queryId Unique query ID
|
|
7
|
-
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
8
|
-
* @param {string} tableId Database table ID
|
|
9
|
-
*/
|
|
10
|
-
export declare const storeQueryCacheTags: (queryId: string, cacheTags: CacheTag[], tableId: string) => Promise<void>;
|
|
11
|
-
/**
|
|
12
|
-
* Retrieves the queries that reference cache tags.
|
|
13
|
-
*
|
|
14
|
-
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
15
|
-
* @param {string} tableId Database table ID
|
|
16
|
-
* @returns Array of query IDs
|
|
17
|
-
*/
|
|
18
|
-
export declare const queriesReferencingCacheTags: (cacheTags: CacheTag[], tableId: string) => Promise<string[]>;
|
|
19
|
-
/**
|
|
20
|
-
* Deletes the specified cache tags from the database.
|
|
21
|
-
*
|
|
22
|
-
* This removes the cache tag keys entirely. When queries are revalidated and
|
|
23
|
-
* run again, fresh cache tag mappings will be created.
|
|
24
|
-
*
|
|
25
|
-
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
26
|
-
* @param {string} tableId Database table ID
|
|
27
|
-
*
|
|
28
|
-
*/
|
|
29
|
-
export declare const deleteCacheTags: (cacheTags: CacheTag[], tableId: string) => Promise<void>;
|
|
30
|
-
/**
|
|
31
|
-
* Deletes the cache tags of a query from the database.
|
|
32
|
-
*
|
|
33
|
-
* @param {string} queryId Unique query ID
|
|
34
|
-
* @param {string} tableId Database table ID
|
|
35
|
-
* @deprecated Use `deleteCacheTags` instead.
|
|
36
|
-
*/
|
|
37
|
-
export declare const deleteQueries: (queryIds: string[], tableId: string) => Promise<void>;
|
|
38
|
-
/**
|
|
39
|
-
* Wipes out all cache tags from the database.
|
|
40
|
-
*
|
|
41
|
-
* @param {string} tableId Database table ID
|
|
42
|
-
*/
|
|
43
|
-
export declare function truncateCacheTags(tableId: string): Promise<void>;
|
package/dist/cache-tags.js
DELETED
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
import { sql } from '@vercel/postgres';
|
|
2
|
-
export { generateQueryId, parseXCacheTagsResponseHeader } from './utils';
|
|
3
|
-
/**
|
|
4
|
-
* Stores the cache tags of a query in the database.
|
|
5
|
-
*
|
|
6
|
-
* @param {string} queryId Unique query ID
|
|
7
|
-
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
8
|
-
* @param {string} tableId Database table ID
|
|
9
|
-
*/
|
|
10
|
-
export const storeQueryCacheTags = async (queryId, cacheTags, tableId) => {
|
|
11
|
-
if (!cacheTags?.length) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]);
|
|
15
|
-
const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(',');
|
|
16
|
-
await sql.query(`INSERT INTO ${tableId} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags);
|
|
17
|
-
};
|
|
18
|
-
/**
|
|
19
|
-
* Retrieves the queries that reference cache tags.
|
|
20
|
-
*
|
|
21
|
-
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
22
|
-
* @param {string} tableId Database table ID
|
|
23
|
-
* @returns Array of query IDs
|
|
24
|
-
*/
|
|
25
|
-
export const queriesReferencingCacheTags = async (cacheTags, tableId) => {
|
|
26
|
-
if (!cacheTags?.length) {
|
|
27
|
-
return [];
|
|
28
|
-
}
|
|
29
|
-
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
30
|
-
const { rows } = await sql.query(`SELECT DISTINCT query_id FROM ${tableId} WHERE cache_tag IN (${placeholders})`, cacheTags);
|
|
31
|
-
return rows.map((row) => row.query_id);
|
|
32
|
-
};
|
|
33
|
-
/**
|
|
34
|
-
* Deletes the specified cache tags from the database.
|
|
35
|
-
*
|
|
36
|
-
* This removes the cache tag keys entirely. When queries are revalidated and
|
|
37
|
-
* run again, fresh cache tag mappings will be created.
|
|
38
|
-
*
|
|
39
|
-
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
40
|
-
* @param {string} tableId Database table ID
|
|
41
|
-
*
|
|
42
|
-
*/
|
|
43
|
-
export const deleteCacheTags = async (cacheTags, tableId) => {
|
|
44
|
-
if (cacheTags.length === 0) {
|
|
45
|
-
return;
|
|
46
|
-
}
|
|
47
|
-
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
48
|
-
await sql.query(`DELETE FROM ${tableId} WHERE cache_tag IN (${placeholders})`, cacheTags);
|
|
49
|
-
};
|
|
50
|
-
/**
|
|
51
|
-
* Deletes the cache tags of a query from the database.
|
|
52
|
-
*
|
|
53
|
-
* @param {string} queryId Unique query ID
|
|
54
|
-
* @param {string} tableId Database table ID
|
|
55
|
-
* @deprecated Use `deleteCacheTags` instead.
|
|
56
|
-
*/
|
|
57
|
-
export const deleteQueries = async (queryIds, tableId) => {
|
|
58
|
-
if (!queryIds?.length) {
|
|
59
|
-
return;
|
|
60
|
-
}
|
|
61
|
-
const placeholders = queryIds.map((_, i) => `$${i + 1}`).join(',');
|
|
62
|
-
await sql.query(`DELETE FROM ${tableId} WHERE query_id IN (${placeholders})`, queryIds);
|
|
63
|
-
};
|
|
64
|
-
/**
|
|
65
|
-
* Wipes out all cache tags from the database.
|
|
66
|
-
*
|
|
67
|
-
* @param {string} tableId Database table ID
|
|
68
|
-
*/
|
|
69
|
-
export async function truncateCacheTags(tableId) {
|
|
70
|
-
await sql.query(`DELETE FROM ${tableId}`);
|
|
71
|
-
}
|
|
72
|
-
//# sourceMappingURL=cache-tags.js.map
|
package/dist/cache-tags.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"cache-tags.js","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"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"}
|
package/dist/types.d.ts
DELETED
|
@@ -1,25 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* A branded type for cache tags. This is created by intersecting `string`
|
|
3
|
-
* with `{ readonly _: unique symbol }`, making it a unique type.
|
|
4
|
-
* Although it is fundamentally a string, it is treated as a distinct type
|
|
5
|
-
* due to the unique symbol.
|
|
6
|
-
*/
|
|
7
|
-
export type CacheTag = string & {
|
|
8
|
-
readonly _: unique symbol;
|
|
9
|
-
};
|
|
10
|
-
/**
|
|
11
|
-
* A type representing the structure of a webhook payload for cache tag invalidation.
|
|
12
|
-
* It includes the entity type, event type, and the entity details which contain
|
|
13
|
-
* the cache tags to be invalidated.
|
|
14
|
-
*/
|
|
15
|
-
export type CacheTagsInvalidateWebhook = {
|
|
16
|
-
entity_type: 'cda_cache_tags';
|
|
17
|
-
event_type: 'invalidate';
|
|
18
|
-
entity: {
|
|
19
|
-
id: 'cda_cache_tags';
|
|
20
|
-
type: 'cda_cache_tags';
|
|
21
|
-
attributes: {
|
|
22
|
-
tags: CacheTag[];
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
};
|
package/dist/types.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../src/types.ts"],"names":[],"mappings":""}
|
package/dist/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../src/utils.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAqB,MAAM,SAAS,CAAC;AACnD,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAGzC;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,6BAA6B,GAAG,CAAC,MAAsB,EAAE,EAAE,CACtE,CAAC,MAAM,EAAE,KAAK,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAe,CAAC,CAAC;AAE3D;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,eAAe,GAAG,CAAuB,QAAsB,EAAE,SAAsB,EAAU,EAAE;IAC9G,OAAO,UAAU,CAAC,MAAM,CAAC;SACtB,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,CAAC;SACvB,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,SAAS,CAAC,IAAI,EAAE,CAAC;SACvC,MAAM,CAAC,KAAK,CAAC,CAAC;AACnB,CAAC,CAAC"}
|