@smartive/datocms-utils 2.1.5 → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.github/workflows/release.yml +10 -5
- package/.github/workflows/test.yml +2 -2
- package/CHANGELOG.md +2 -2
- package/README.md +63 -0
- package/dist/cache-tags-redis.d.ts +39 -0
- package/dist/cache-tags-redis.js +80 -0
- package/dist/cache-tags-redis.js.map +1 -0
- package/dist/cache-tags.d.ts +14 -18
- package/dist/cache-tags.js +19 -28
- package/dist/cache-tags.js.map +1 -1
- package/dist/classnames.js.map +1 -1
- package/dist/index.d.ts +2 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/links.js.map +1 -1
- package/dist/types.d.ts +11 -0
- package/dist/utils.d.ts +18 -0
- package/dist/utils.js +24 -0
- package/dist/utils.js.map +1 -0
- package/package.json +21 -8
- package/renovate.json +1 -7
- package/src/cache-tags-redis.ts +96 -0
- package/src/cache-tags.ts +22 -37
- package/src/classnames.ts +0 -1
- package/src/index.ts +2 -0
- package/src/links.ts +0 -1
- package/src/types.ts +11 -0
- package/src/utils.ts +27 -0
|
@@ -5,24 +5,29 @@ on:
|
|
|
5
5
|
branches:
|
|
6
6
|
- main
|
|
7
7
|
|
|
8
|
+
permissions:
|
|
9
|
+
contents: write # to be able to publish a GitHub release
|
|
10
|
+
issues: write # to be able to comment on released issues
|
|
11
|
+
pull-requests: write # to be able to comment on released pull requests
|
|
12
|
+
id-token: write # to enable use of OIDC for trusted publishing and npm provenance
|
|
13
|
+
|
|
8
14
|
jobs:
|
|
9
15
|
release:
|
|
10
16
|
name: Build & release
|
|
11
17
|
runs-on: ubuntu-latest
|
|
12
18
|
|
|
13
19
|
steps:
|
|
14
|
-
- uses: actions/checkout@
|
|
15
|
-
- uses: actions/setup-node@v6
|
|
20
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
21
|
+
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
|
16
22
|
with:
|
|
17
23
|
node-version: 24
|
|
18
24
|
- run: npm ci
|
|
19
25
|
- run: npm run build
|
|
20
26
|
- name: semantic release
|
|
21
|
-
uses: cycjimmy/semantic-release-action@
|
|
27
|
+
uses: cycjimmy/semantic-release-action@v6
|
|
22
28
|
with:
|
|
23
|
-
semantic_version:
|
|
29
|
+
semantic_version: 25
|
|
24
30
|
extra_plugins: |
|
|
25
31
|
@semantic-release/changelog@6
|
|
26
32
|
env:
|
|
27
33
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
28
|
-
NPM_TOKEN: ${{ secrets.NPM_TOKEN }}
|
|
@@ -15,8 +15,8 @@ jobs:
|
|
|
15
15
|
runs-on: ubuntu-latest
|
|
16
16
|
|
|
17
17
|
steps:
|
|
18
|
-
- uses: actions/checkout@
|
|
19
|
-
- uses: actions/setup-node@v6
|
|
18
|
+
- uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
|
|
19
|
+
- uses: actions/setup-node@6044e13b5dc448c55e2357c09f80417699197238 # v6
|
|
20
20
|
with:
|
|
21
21
|
node-version: 24
|
|
22
22
|
- run: npm ci
|
package/CHANGELOG.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
|
-
## [2.1
|
|
1
|
+
## [2.2.1](https://github.com/smartive/datocms-utils/compare/v2.2.0...v2.2.1) (2026-02-12)
|
|
2
2
|
|
|
3
3
|
|
|
4
4
|
### Bug Fixes
|
|
5
5
|
|
|
6
|
-
*
|
|
6
|
+
* add missing repository.url to package.json ([#206](https://github.com/smartive/datocms-utils/issues/206)) ([d88733e](https://github.com/smartive/datocms-utils/commit/d88733e374bf7ac0161e6a4439032ea6ad2a99b8))
|
package/README.md
CHANGED
|
@@ -34,6 +34,69 @@ CREATE TABLE IF NOT EXISTS query_cache_tags (
|
|
|
34
34
|
);
|
|
35
35
|
```
|
|
36
36
|
|
|
37
|
+
### Utilities for DatoCMS Cache Tags (Redis)
|
|
38
|
+
|
|
39
|
+
The following utilities provide Redis-based alternatives to the Postgres cache tags implementation above. They work with [DatoCMS cache tags](https://www.datocms.com/docs/content-delivery-api/cache-tags) and any Redis instance.
|
|
40
|
+
|
|
41
|
+
- `redis.storeQueryCacheTags`: Stores the cache tags of a query in Redis.
|
|
42
|
+
- `redis.queriesReferencingCacheTags`: Retrieves the queries that reference cache tags.
|
|
43
|
+
- `redis.deleteCacheTags`: Deletes cache tags from Redis.
|
|
44
|
+
- `redis.truncateCacheTags`: Wipes out all cache tags from Redis.
|
|
45
|
+
|
|
46
|
+
The Redis connection is automatically initialized on first use using the `REDIS_URL` environment variable.
|
|
47
|
+
|
|
48
|
+
#### Environment Variables
|
|
49
|
+
|
|
50
|
+
Add your Redis connection URL to your `.env.local` file:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
# Required: Redis connection URL
|
|
54
|
+
# For Upstash Redis
|
|
55
|
+
REDIS_URL=rediss://default:your-token@your-endpoint.upstash.io:6379
|
|
56
|
+
|
|
57
|
+
# For Redis Cloud or other providers
|
|
58
|
+
REDIS_URL=redis://username:password@your-redis-host:6379
|
|
59
|
+
|
|
60
|
+
# For local development
|
|
61
|
+
REDIS_URL=redis://localhost:6379
|
|
62
|
+
|
|
63
|
+
# Optional: Key prefix for separating production/preview environments
|
|
64
|
+
# Useful when using the same Redis instance for multiple environments
|
|
65
|
+
REDIS_KEY_PREFIX=prod # For production
|
|
66
|
+
REDIS_KEY_PREFIX=preview # For preview/staging
|
|
67
|
+
# Leave empty for development (no prefix)
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
**Note**: Similar to how the Postgres version uses different table names, use `REDIS_KEY_PREFIX` to separate data between environments when using the same Redis instance.
|
|
71
|
+
|
|
72
|
+
#### Usage Example
|
|
73
|
+
|
|
74
|
+
```typescript
|
|
75
|
+
// Recommended: Use namespaces for clarity
|
|
76
|
+
import { generateQueryId, redis } from '@smartive/datocms-utils';
|
|
77
|
+
|
|
78
|
+
const queryId = generateQueryId(query, variables);
|
|
79
|
+
|
|
80
|
+
// Store cache tags for a query
|
|
81
|
+
await redis.storeQueryCacheTags(queryId, ['item:42', 'product', 'category:5']);
|
|
82
|
+
|
|
83
|
+
// Find all queries that reference specific tags
|
|
84
|
+
const affectedQueries = await redis.queriesReferencingCacheTags(['item:42']);
|
|
85
|
+
|
|
86
|
+
// Delete cache tags (keys will be recreated on next query)
|
|
87
|
+
await redis.deleteCacheTags(['item:42']);
|
|
88
|
+
```
|
|
89
|
+
|
|
90
|
+
#### Redis Data Structure
|
|
91
|
+
|
|
92
|
+
The Redis implementation uses Sets to track query-to-tag relationships:
|
|
93
|
+
|
|
94
|
+
- **Cache tag keys**: `{prefix}{tag}` → Set of query IDs
|
|
95
|
+
|
|
96
|
+
Where `{prefix}` is the optional `REDIS_KEY_PREFIX` environment variable (e.g., `prod:`, `preview:`).
|
|
97
|
+
|
|
98
|
+
When cache tags are invalidated, their keys are deleted entirely. Fresh mappings are created when queries run again.
|
|
99
|
+
|
|
37
100
|
### Other Utilities
|
|
38
101
|
|
|
39
102
|
- `classNames`: Cleans and joins an array of inputs with possible undefined or boolean values. Useful for tailwind classnames.
|
|
@@ -0,0 +1,39 @@
|
|
|
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>;
|
|
@@ -0,0 +1,80 @@
|
|
|
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
|
|
@@ -0,0 +1 @@
|
|
|
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
CHANGED
|
@@ -1,21 +1,5 @@
|
|
|
1
|
-
import {
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Converts the value of DatoCMS's `X-Cache-Tags` header into an array of strings typed as `CacheTag`.
|
|
5
|
-
* For example, it transforms `'tag-a tag-2 other-tag'` into `['tag-a', 'tag-2', 'other-tag']`.
|
|
6
|
-
*
|
|
7
|
-
* @param string String value of the `X-Cache-Tags` header
|
|
8
|
-
* @returns Array of strings typed as `CacheTag`
|
|
9
|
-
*/
|
|
10
|
-
export declare function parseXCacheTagsResponseHeader(string?: null | string): CacheTag[];
|
|
11
|
-
/**
|
|
12
|
-
* Generates a unique query ID based on the query document and its variables.
|
|
13
|
-
*
|
|
14
|
-
* @param {DocumentNode} document Query document
|
|
15
|
-
* @param {TVariables} variables Query variables
|
|
16
|
-
* @returns Unique query ID
|
|
17
|
-
*/
|
|
18
|
-
export declare const generateQueryId: <TVariables = unknown>(document: DocumentNode, variables?: TVariables) => string;
|
|
1
|
+
import { type CacheTag } from './types';
|
|
2
|
+
export { generateQueryId, parseXCacheTagsResponseHeader } from './utils';
|
|
19
3
|
/**
|
|
20
4
|
* Stores the cache tags of a query in the database.
|
|
21
5
|
*
|
|
@@ -32,11 +16,23 @@ export declare const storeQueryCacheTags: (queryId: string, cacheTags: CacheTag[
|
|
|
32
16
|
* @returns Array of query IDs
|
|
33
17
|
*/
|
|
34
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>;
|
|
35
30
|
/**
|
|
36
31
|
* Deletes the cache tags of a query from the database.
|
|
37
32
|
*
|
|
38
33
|
* @param {string} queryId Unique query ID
|
|
39
34
|
* @param {string} tableId Database table ID
|
|
35
|
+
* @deprecated Use `deleteCacheTags` instead.
|
|
40
36
|
*/
|
|
41
37
|
export declare const deleteQueries: (queryIds: string[], tableId: string) => Promise<void>;
|
|
42
38
|
/**
|
package/dist/cache-tags.js
CHANGED
|
@@ -1,32 +1,5 @@
|
|
|
1
1
|
import { sql } from '@vercel/postgres';
|
|
2
|
-
|
|
3
|
-
import { print } from 'graphql';
|
|
4
|
-
/**
|
|
5
|
-
* Converts the value of DatoCMS's `X-Cache-Tags` header into an array of strings typed as `CacheTag`.
|
|
6
|
-
* For example, it transforms `'tag-a tag-2 other-tag'` into `['tag-a', 'tag-2', 'other-tag']`.
|
|
7
|
-
*
|
|
8
|
-
* @param string String value of the `X-Cache-Tags` header
|
|
9
|
-
* @returns Array of strings typed as `CacheTag`
|
|
10
|
-
*/
|
|
11
|
-
export function parseXCacheTagsResponseHeader(string) {
|
|
12
|
-
if (!string) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
return (string.split(' ') || []).map((tag) => tag);
|
|
16
|
-
}
|
|
17
|
-
/**
|
|
18
|
-
* Generates a unique query ID based on the query document and its variables.
|
|
19
|
-
*
|
|
20
|
-
* @param {DocumentNode} document Query document
|
|
21
|
-
* @param {TVariables} variables Query variables
|
|
22
|
-
* @returns Unique query ID
|
|
23
|
-
*/
|
|
24
|
-
export const generateQueryId = (document, variables) => {
|
|
25
|
-
return createHash('sha1')
|
|
26
|
-
.update(print(document))
|
|
27
|
-
.update(JSON.stringify(variables) || '')
|
|
28
|
-
.digest('hex');
|
|
29
|
-
};
|
|
2
|
+
export { generateQueryId, parseXCacheTagsResponseHeader } from './utils';
|
|
30
3
|
/**
|
|
31
4
|
* Stores the cache tags of a query in the database.
|
|
32
5
|
*
|
|
@@ -57,11 +30,29 @@ export const queriesReferencingCacheTags = async (cacheTags, tableId) => {
|
|
|
57
30
|
const { rows } = await sql.query(`SELECT DISTINCT query_id FROM ${tableId} WHERE cache_tag IN (${placeholders})`, cacheTags);
|
|
58
31
|
return rows.map((row) => row.query_id);
|
|
59
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
|
+
};
|
|
60
50
|
/**
|
|
61
51
|
* Deletes the cache tags of a query from the database.
|
|
62
52
|
*
|
|
63
53
|
* @param {string} queryId Unique query ID
|
|
64
54
|
* @param {string} tableId Database table ID
|
|
55
|
+
* @deprecated Use `deleteCacheTags` instead.
|
|
65
56
|
*/
|
|
66
57
|
export const deleteQueries = async (queryIds, tableId) => {
|
|
67
58
|
if (!queryIds?.length) {
|
package/dist/cache-tags.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cache-tags.js","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;
|
|
1
|
+
{"version":3,"file":"cache-tags.js","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"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"}
|
package/dist/classnames.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"classnames.js","sourceRoot":"","sources":["../src/classnames.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"classnames.js","sourceRoot":"","sources":["../src/classnames.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAG,UAA4C,EAAU,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC"}
|
package/dist/index.d.ts
CHANGED
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,cAAc,CAAC;AAC7B,OAAO,KAAK,QAAQ,MAAM,cAAc,CAAC;AACzC,OAAO,KAAK,KAAK,MAAM,oBAAoB,CAAC;AAC5C,cAAc,cAAc,CAAC;AAC7B,cAAc,SAAS,CAAC;AACxB,cAAc,SAAS,CAAC"}
|
package/dist/links.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"links.js","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"links.js","sourceRoot":"","sources":["../src/links.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,WAAmB,EAAU,EAAE;IACxD,IAAI,OAAO,WAAW,KAAK,QAAQ,EAAE,CAAC;QACpC,MAAM,IAAI,KAAK,CAAC,gCAAgC,CAAC,CAAC;IACpD,CAAC;IAED,qFAAqF;IACrF,MAAM,kBAAkB,GAAG,WAAW,CAAC,OAAO,CAAC,SAAS,EAAE,EAAE,CAAC,CAAC;IAE9D,OAAO,OAAO,kBAAkB,EAAE,CAAC;AACrC,CAAC,CAAC"}
|
package/dist/types.d.ts
CHANGED
|
@@ -1,6 +1,17 @@
|
|
|
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
|
+
*/
|
|
1
7
|
export type CacheTag = string & {
|
|
2
8
|
readonly _: unique symbol;
|
|
3
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
|
+
*/
|
|
4
15
|
export type CacheTagsInvalidateWebhook = {
|
|
5
16
|
entity_type: 'cda_cache_tags';
|
|
6
17
|
event_type: 'invalidate';
|
package/dist/utils.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { type DocumentNode } from 'graphql';
|
|
2
|
+
import { type CacheTag } from './types';
|
|
3
|
+
/**
|
|
4
|
+
* Converts the value of DatoCMS's `X-Cache-Tags` header into an array of strings typed as `CacheTag`.
|
|
5
|
+
* For example, it transforms `'tag-a tag-2 other-tag'` into `['tag-a', 'tag-2', 'other-tag']`.
|
|
6
|
+
*
|
|
7
|
+
* @param string String value of the `X-Cache-Tags` header
|
|
8
|
+
* @returns Array of strings typed as `CacheTag`
|
|
9
|
+
*/
|
|
10
|
+
export declare const parseXCacheTagsResponseHeader: (string?: null | string) => CacheTag[];
|
|
11
|
+
/**
|
|
12
|
+
* Generates a unique query ID based on the query document and its variables.
|
|
13
|
+
*
|
|
14
|
+
* @param {DocumentNode} document Query document
|
|
15
|
+
* @param {TVariables} variables Query variables
|
|
16
|
+
* @returns Unique query ID
|
|
17
|
+
*/
|
|
18
|
+
export declare const generateQueryId: <TVariables = unknown>(document: DocumentNode, variables?: TVariables) => string;
|
package/dist/utils.js
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { print } from 'graphql';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
/**
|
|
4
|
+
* Converts the value of DatoCMS's `X-Cache-Tags` header into an array of strings typed as `CacheTag`.
|
|
5
|
+
* For example, it transforms `'tag-a tag-2 other-tag'` into `['tag-a', 'tag-2', 'other-tag']`.
|
|
6
|
+
*
|
|
7
|
+
* @param string String value of the `X-Cache-Tags` header
|
|
8
|
+
* @returns Array of strings typed as `CacheTag`
|
|
9
|
+
*/
|
|
10
|
+
export const parseXCacheTagsResponseHeader = (string) => (string?.split(' ') ?? []).map((tag) => tag);
|
|
11
|
+
/**
|
|
12
|
+
* Generates a unique query ID based on the query document and its variables.
|
|
13
|
+
*
|
|
14
|
+
* @param {DocumentNode} document Query document
|
|
15
|
+
* @param {TVariables} variables Query variables
|
|
16
|
+
* @returns Unique query ID
|
|
17
|
+
*/
|
|
18
|
+
export const generateQueryId = (document, variables) => {
|
|
19
|
+
return createHash('sha1')
|
|
20
|
+
.update(print(document))
|
|
21
|
+
.update(JSON.stringify(variables) || '')
|
|
22
|
+
.digest('hex');
|
|
23
|
+
};
|
|
24
|
+
//# sourceMappingURL=utils.js.map
|
|
@@ -0,0 +1 @@
|
|
|
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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartive/datocms-utils",
|
|
3
|
-
"version": "2.1
|
|
3
|
+
"version": "2.2.1",
|
|
4
4
|
"description": "A set of utilities and helpers to work with DatoCMS in a Next.js project.",
|
|
5
5
|
"source": "src/index.ts",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -18,17 +18,30 @@
|
|
|
18
18
|
],
|
|
19
19
|
"author": "smartive AG",
|
|
20
20
|
"license": "MIT",
|
|
21
|
+
"repository": {
|
|
22
|
+
"url": "https://github.com/smartive/datocms-utils",
|
|
23
|
+
"type": "git"
|
|
24
|
+
},
|
|
21
25
|
"devDependencies": {
|
|
22
|
-
"@smartive/eslint-config": "
|
|
23
|
-
"@smartive/prettier-config": "
|
|
24
|
-
"@types/node": "
|
|
25
|
-
"eslint": "
|
|
26
|
-
"eslint-import-resolver-typescript": "
|
|
27
|
-
"prettier": "
|
|
28
|
-
"typescript": "
|
|
26
|
+
"@smartive/eslint-config": "7.0.1",
|
|
27
|
+
"@smartive/prettier-config": "3.1.2",
|
|
28
|
+
"@types/node": "24.10.12",
|
|
29
|
+
"eslint": "9.39.2",
|
|
30
|
+
"eslint-import-resolver-typescript": "4.4.4",
|
|
31
|
+
"prettier": "3.8.1",
|
|
32
|
+
"typescript": "5.9.3",
|
|
33
|
+
"ioredis": "5.9.2"
|
|
29
34
|
},
|
|
30
35
|
"dependencies": {
|
|
31
36
|
"@vercel/postgres": "^0.10.0",
|
|
32
37
|
"graphql": "^16.9.0"
|
|
38
|
+
},
|
|
39
|
+
"peerDependencies": {
|
|
40
|
+
"ioredis": "^5.4.0"
|
|
41
|
+
},
|
|
42
|
+
"peerDependenciesMeta": {
|
|
43
|
+
"ioredis": {
|
|
44
|
+
"optional": true
|
|
45
|
+
}
|
|
33
46
|
}
|
|
34
47
|
}
|
package/renovate.json
CHANGED
|
@@ -1,10 +1,4 @@
|
|
|
1
1
|
{
|
|
2
2
|
"$schema": "https://docs.renovatebot.com/renovate-schema.json",
|
|
3
|
-
"extends": [
|
|
4
|
-
"local>smartive/renovate-config",
|
|
5
|
-
":automergeRequireAllStatusChecks",
|
|
6
|
-
":automergeDisabled",
|
|
7
|
-
":automergeStableNonMajor",
|
|
8
|
-
":pinOnlyDevDependencies"
|
|
9
|
-
]
|
|
3
|
+
"extends": ["github>smartive/renovate-config:best-practices-lib"]
|
|
10
4
|
}
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
import { type CacheTag } from './types';
|
|
3
|
+
|
|
4
|
+
let redis: Redis | null = null;
|
|
5
|
+
|
|
6
|
+
const getRedis = (): Redis => {
|
|
7
|
+
redis ??= new Redis(process.env.REDIS_URL!, {
|
|
8
|
+
maxRetriesPerRequest: 3,
|
|
9
|
+
lazyConnect: true,
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
return redis;
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
const keyPrefix = process.env.REDIS_KEY_PREFIX ? `${process.env.REDIS_KEY_PREFIX}:` : '';
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Stores the cache tags of a query in Redis.
|
|
19
|
+
*
|
|
20
|
+
* For each cache tag, adds the query ID to a Redis Set. Sets are unordered
|
|
21
|
+
* collections of unique strings, perfect for tracking which queries use which tags.
|
|
22
|
+
*
|
|
23
|
+
* @param {string} queryId Unique query ID
|
|
24
|
+
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
25
|
+
*
|
|
26
|
+
*/
|
|
27
|
+
export const storeQueryCacheTags = async (queryId: string, cacheTags: CacheTag[]): Promise<void> => {
|
|
28
|
+
if (!cacheTags?.length) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const redis = getRedis();
|
|
33
|
+
const pipeline = redis.pipeline();
|
|
34
|
+
|
|
35
|
+
for (const tag of cacheTags) {
|
|
36
|
+
pipeline.sadd(`${keyPrefix}${tag}`, queryId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
await pipeline.exec();
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Retrieves the query IDs that reference any of the specified cache tags.
|
|
44
|
+
*
|
|
45
|
+
* Uses Redis SUNION to efficiently find all queries associated with the given tags.
|
|
46
|
+
*
|
|
47
|
+
* @param {CacheTag[]} cacheTags Array of cache tags to check
|
|
48
|
+
* @returns Array of unique query IDs
|
|
49
|
+
*
|
|
50
|
+
*/
|
|
51
|
+
export const queriesReferencingCacheTags = async (cacheTags: CacheTag[]): Promise<string[]> => {
|
|
52
|
+
if (!cacheTags?.length) {
|
|
53
|
+
return [];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const redis = getRedis();
|
|
57
|
+
const keys = cacheTags.map((tag) => `${keyPrefix}${tag}`);
|
|
58
|
+
|
|
59
|
+
return redis.sunion(...keys);
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Deletes the specified cache tags from Redis.
|
|
64
|
+
*
|
|
65
|
+
* This removes the cache tag keys entirely. When queries are revalidated and
|
|
66
|
+
* run again, fresh cache tag mappings will be created.
|
|
67
|
+
*
|
|
68
|
+
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
69
|
+
* @returns Number of keys deleted, or null if there was an error
|
|
70
|
+
*
|
|
71
|
+
*/
|
|
72
|
+
export const deleteCacheTags = async (cacheTags: CacheTag[]): Promise<number> => {
|
|
73
|
+
if (!cacheTags?.length) {
|
|
74
|
+
return 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const redis = getRedis();
|
|
78
|
+
const keys = cacheTags.map((tag) => `${keyPrefix}${tag}`);
|
|
79
|
+
|
|
80
|
+
return redis.del(...keys);
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Wipes out all cache tags from Redis.
|
|
85
|
+
*
|
|
86
|
+
* ⚠️ **Warning**: This will delete all cache tag data. Use with caution!
|
|
87
|
+
*/
|
|
88
|
+
export const truncateCacheTags = async (): Promise<void> => {
|
|
89
|
+
const redis = getRedis();
|
|
90
|
+
const pattern = `${keyPrefix}*`;
|
|
91
|
+
const keys = await redis.keys(pattern);
|
|
92
|
+
|
|
93
|
+
if (keys.length > 0) {
|
|
94
|
+
await redis.del(...keys);
|
|
95
|
+
}
|
|
96
|
+
};
|
package/src/cache-tags.ts
CHANGED
|
@@ -1,38 +1,7 @@
|
|
|
1
1
|
import { sql } from '@vercel/postgres';
|
|
2
|
-
import {
|
|
3
|
-
import { DocumentNode, print } from 'graphql';
|
|
4
|
-
import { CacheTag } from './types';
|
|
2
|
+
import { type CacheTag } from './types';
|
|
5
3
|
|
|
6
|
-
|
|
7
|
-
* Converts the value of DatoCMS's `X-Cache-Tags` header into an array of strings typed as `CacheTag`.
|
|
8
|
-
* For example, it transforms `'tag-a tag-2 other-tag'` into `['tag-a', 'tag-2', 'other-tag']`.
|
|
9
|
-
*
|
|
10
|
-
* @param string String value of the `X-Cache-Tags` header
|
|
11
|
-
* @returns Array of strings typed as `CacheTag`
|
|
12
|
-
*/
|
|
13
|
-
|
|
14
|
-
export function parseXCacheTagsResponseHeader(string?: null | string) {
|
|
15
|
-
if (!string) {
|
|
16
|
-
return [];
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
return (string.split(' ') || []).map((tag) => tag as CacheTag);
|
|
20
|
-
}
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Generates a unique query ID based on the query document and its variables.
|
|
24
|
-
*
|
|
25
|
-
* @param {DocumentNode} document Query document
|
|
26
|
-
* @param {TVariables} variables Query variables
|
|
27
|
-
* @returns Unique query ID
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
export const generateQueryId = <TVariables = unknown>(document: DocumentNode, variables?: TVariables): string => {
|
|
31
|
-
return createHash('sha1')
|
|
32
|
-
.update(print(document))
|
|
33
|
-
.update(JSON.stringify(variables) || '')
|
|
34
|
-
.digest('hex');
|
|
35
|
-
};
|
|
4
|
+
export { generateQueryId, parseXCacheTagsResponseHeader } from './utils';
|
|
36
5
|
|
|
37
6
|
/**
|
|
38
7
|
* Stores the cache tags of a query in the database.
|
|
@@ -41,7 +10,6 @@ export const generateQueryId = <TVariables = unknown>(document: DocumentNode, va
|
|
|
41
10
|
* @param {CacheTag[]} cacheTags Array of cache tags
|
|
42
11
|
* @param {string} tableId Database table ID
|
|
43
12
|
*/
|
|
44
|
-
|
|
45
13
|
export const storeQueryCacheTags = async (queryId: string, cacheTags: CacheTag[], tableId: string) => {
|
|
46
14
|
if (!cacheTags?.length) {
|
|
47
15
|
return;
|
|
@@ -60,7 +28,6 @@ export const storeQueryCacheTags = async (queryId: string, cacheTags: CacheTag[]
|
|
|
60
28
|
* @param {string} tableId Database table ID
|
|
61
29
|
* @returns Array of query IDs
|
|
62
30
|
*/
|
|
63
|
-
|
|
64
31
|
export const queriesReferencingCacheTags = async (cacheTags: CacheTag[], tableId: string): Promise<string[]> => {
|
|
65
32
|
if (!cacheTags?.length) {
|
|
66
33
|
return [];
|
|
@@ -76,13 +43,32 @@ export const queriesReferencingCacheTags = async (cacheTags: CacheTag[], tableId
|
|
|
76
43
|
return rows.map((row) => row.query_id);
|
|
77
44
|
};
|
|
78
45
|
|
|
46
|
+
/**
|
|
47
|
+
* Deletes the specified cache tags from the database.
|
|
48
|
+
*
|
|
49
|
+
* This removes the cache tag keys entirely. When queries are revalidated and
|
|
50
|
+
* run again, fresh cache tag mappings will be created.
|
|
51
|
+
*
|
|
52
|
+
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
53
|
+
* @param {string} tableId Database table ID
|
|
54
|
+
*
|
|
55
|
+
*/
|
|
56
|
+
export const deleteCacheTags = async (cacheTags: CacheTag[], tableId: string) => {
|
|
57
|
+
if (cacheTags.length === 0) {
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
61
|
+
|
|
62
|
+
await sql.query(`DELETE FROM ${tableId} WHERE cache_tag IN (${placeholders})`, cacheTags);
|
|
63
|
+
};
|
|
64
|
+
|
|
79
65
|
/**
|
|
80
66
|
* Deletes the cache tags of a query from the database.
|
|
81
67
|
*
|
|
82
68
|
* @param {string} queryId Unique query ID
|
|
83
69
|
* @param {string} tableId Database table ID
|
|
70
|
+
* @deprecated Use `deleteCacheTags` instead.
|
|
84
71
|
*/
|
|
85
|
-
|
|
86
72
|
export const deleteQueries = async (queryIds: string[], tableId: string) => {
|
|
87
73
|
if (!queryIds?.length) {
|
|
88
74
|
return;
|
|
@@ -97,7 +83,6 @@ export const deleteQueries = async (queryIds: string[], tableId: string) => {
|
|
|
97
83
|
*
|
|
98
84
|
* @param {string} tableId Database table ID
|
|
99
85
|
*/
|
|
100
|
-
|
|
101
86
|
export async function truncateCacheTags(tableId: string) {
|
|
102
87
|
await sql.query(`DELETE FROM ${tableId}`);
|
|
103
88
|
}
|
package/src/classnames.ts
CHANGED
package/src/index.ts
CHANGED
package/src/links.ts
CHANGED
package/src/types.ts
CHANGED
|
@@ -1,5 +1,16 @@
|
|
|
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
|
+
*/
|
|
1
7
|
export type CacheTag = string & { readonly _: unique symbol };
|
|
2
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
|
+
*/
|
|
3
14
|
export type CacheTagsInvalidateWebhook = {
|
|
4
15
|
entity_type: 'cda_cache_tags';
|
|
5
16
|
event_type: 'invalidate';
|
package/src/utils.ts
ADDED
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { print, type DocumentNode } from 'graphql';
|
|
2
|
+
import { createHash } from 'node:crypto';
|
|
3
|
+
import { type CacheTag } from './types';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Converts the value of DatoCMS's `X-Cache-Tags` header into an array of strings typed as `CacheTag`.
|
|
7
|
+
* For example, it transforms `'tag-a tag-2 other-tag'` into `['tag-a', 'tag-2', 'other-tag']`.
|
|
8
|
+
*
|
|
9
|
+
* @param string String value of the `X-Cache-Tags` header
|
|
10
|
+
* @returns Array of strings typed as `CacheTag`
|
|
11
|
+
*/
|
|
12
|
+
export const parseXCacheTagsResponseHeader = (string?: null | string) =>
|
|
13
|
+
(string?.split(' ') ?? []).map((tag) => tag as CacheTag);
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Generates a unique query ID based on the query document and its variables.
|
|
17
|
+
*
|
|
18
|
+
* @param {DocumentNode} document Query document
|
|
19
|
+
* @param {TVariables} variables Query variables
|
|
20
|
+
* @returns Unique query ID
|
|
21
|
+
*/
|
|
22
|
+
export const generateQueryId = <TVariables = unknown>(document: DocumentNode, variables?: TVariables): string => {
|
|
23
|
+
return createHash('sha1')
|
|
24
|
+
.update(print(document))
|
|
25
|
+
.update(JSON.stringify(variables) || '')
|
|
26
|
+
.digest('hex');
|
|
27
|
+
};
|