@smartive/datocms-utils 2.1.4 → 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.
@@ -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@v5
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@v5
27
+ uses: cycjimmy/semantic-release-action@v6
22
28
  with:
23
- semantic_version: 24
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@v5
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.4](https://github.com/smartive/datocms-utils/compare/v2.1.3...v2.1.4) (2025-11-13)
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
- * **renovate:** pin dev dependencies ([644caba](https://github.com/smartive/datocms-utils/commit/644caba4e818347627f9b6e8d8e6171fa4c26e14))
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"}
@@ -1,21 +1,5 @@
1
- import { DocumentNode } from 'graphql';
2
- import { 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 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
  /**
@@ -1,32 +1,5 @@
1
1
  import { sql } from '@vercel/postgres';
2
- import { createHash } from 'crypto';
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) {
@@ -1 +1 @@
1
- {"version":3,"file":"cache-tags.js","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"cache-tags.js","sourceRoot":"","sources":["../src/cache-tags.ts"],"names":[],"mappings":"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"}
@@ -1 +1 @@
1
- {"version":3,"file":"classnames.js","sourceRoot":"","sources":["../src/classnames.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAG,UAA4C,EAAU,EAAE,CAAC,UAAU,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC"}
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
@@ -1,4 +1,6 @@
1
1
  export * from './cache-tags';
2
+ export * as postgres from './cache-tags';
3
+ export * as redis from './cache-tags-redis';
2
4
  export * from './classnames';
3
5
  export * from './links';
4
6
  export * from './types';
package/dist/index.js CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './cache-tags';
2
+ export * as postgres from './cache-tags';
3
+ export * as redis from './cache-tags-redis';
2
4
  export * from './classnames';
3
5
  export * from './links';
4
6
  export * from './types';
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;AAEH,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"}
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';
@@ -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.4",
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",
@@ -8,8 +8,7 @@
8
8
  "scripts": {
9
9
  "build": "tsc",
10
10
  "lint": "eslint src",
11
- "prettier": "prettier --check src",
12
- "publish": "semantic-release"
11
+ "prettier": "prettier --check src"
13
12
  },
14
13
  "publishConfig": {
15
14
  "access": "public"
@@ -19,18 +18,30 @@
19
18
  ],
20
19
  "author": "smartive AG",
21
20
  "license": "MIT",
21
+ "repository": {
22
+ "url": "https://github.com/smartive/datocms-utils",
23
+ "type": "git"
24
+ },
22
25
  "devDependencies": {
23
- "@smartive/eslint-config": "^7.0.0",
24
- "@smartive/prettier-config": "^3.1.2",
25
- "@types/node": "^24.0.0",
26
- "eslint": "^9.39.1",
27
- "eslint-import-resolver-typescript": "^4.4.4",
28
- "prettier": "^3.3.3",
29
- "semantic-release": "^25.0.0",
30
- "typescript": "^5.6.2"
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"
31
34
  },
32
35
  "dependencies": {
33
36
  "@vercel/postgres": "^0.10.0",
34
37
  "graphql": "^16.9.0"
38
+ },
39
+ "peerDependencies": {
40
+ "ioredis": "^5.4.0"
41
+ },
42
+ "peerDependenciesMeta": {
43
+ "ioredis": {
44
+ "optional": true
45
+ }
35
46
  }
36
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 { createHash } from 'crypto';
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
@@ -4,5 +4,4 @@
4
4
  * @param classNames Array of class names
5
5
  * @returns Clean string to be used for class name
6
6
  */
7
-
8
7
  export const classNames = (...classNames: (string | undefined | boolean)[]): string => classNames.filter(Boolean).join(' ');
package/src/index.ts CHANGED
@@ -1,4 +1,6 @@
1
1
  export * from './cache-tags';
2
+ export * as postgres from './cache-tags';
3
+ export * as redis from './cache-tags-redis';
2
4
  export * from './classnames';
3
5
  export * from './links';
4
6
  export * from './types';
package/src/links.ts CHANGED
@@ -4,7 +4,6 @@
4
4
  * @param phoneNumber Phone number
5
5
  * @returns `tel:` link for the phone number
6
6
  */
7
-
8
7
  export const getTelLink = (phoneNumber: string): string => {
9
8
  if (typeof phoneNumber !== 'string') {
10
9
  throw new Error('Phone number must be a string.');
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
+ };