@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.
Files changed (46) hide show
  1. package/README.md +134 -56
  2. package/dist/cache-tags/index.d.ts +2 -0
  3. package/dist/cache-tags/index.js +3 -0
  4. package/dist/cache-tags/index.js.map +1 -0
  5. package/dist/cache-tags/provider/neon.d.ts +33 -0
  6. package/dist/cache-tags/provider/neon.js +44 -0
  7. package/dist/cache-tags/provider/neon.js.map +1 -0
  8. package/dist/cache-tags/provider/noop.d.ts +12 -0
  9. package/dist/cache-tags/provider/noop.js +24 -0
  10. package/dist/cache-tags/provider/noop.js.map +1 -0
  11. package/dist/cache-tags/provider/redis.d.ts +33 -0
  12. package/dist/cache-tags/provider/redis.js +71 -0
  13. package/dist/cache-tags/provider/redis.js.map +1 -0
  14. package/dist/cache-tags/types.d.ts +63 -0
  15. package/dist/cache-tags/types.js.map +1 -0
  16. package/dist/{utils.d.ts → cache-tags/utils.d.ts} +1 -1
  17. package/dist/cache-tags/utils.js.map +1 -0
  18. package/dist/classnames.d.ts +1 -1
  19. package/dist/classnames.js +3 -1
  20. package/dist/classnames.js.map +1 -1
  21. package/dist/index.d.ts +2 -4
  22. package/dist/index.js +2 -4
  23. package/dist/index.js.map +1 -1
  24. package/package.json +39 -10
  25. package/src/cache-tags/index.ts +2 -0
  26. package/src/cache-tags/provider/neon.ts +80 -0
  27. package/src/cache-tags/provider/noop.ts +32 -0
  28. package/src/cache-tags/provider/redis.ts +104 -0
  29. package/src/cache-tags/types.ts +66 -0
  30. package/src/{utils.ts → cache-tags/utils.ts} +1 -1
  31. package/src/classnames.ts +7 -1
  32. package/src/index.ts +2 -4
  33. package/dist/cache-tags-redis.d.ts +0 -39
  34. package/dist/cache-tags-redis.js +0 -80
  35. package/dist/cache-tags-redis.js.map +0 -1
  36. package/dist/cache-tags.d.ts +0 -43
  37. package/dist/cache-tags.js +0 -72
  38. package/dist/cache-tags.js.map +0 -1
  39. package/dist/types.d.ts +0 -25
  40. package/dist/types.js.map +0 -1
  41. package/dist/utils.js.map +0 -1
  42. package/src/cache-tags-redis.ts +0 -96
  43. package/src/cache-tags.ts +0 -88
  44. package/src/types.ts +0 -24
  45. /package/dist/{types.js → cache-tags/types.js} +0 -0
  46. /package/dist/{utils.js → cache-tags/utils.js} +0 -0
package/README.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # smartive DatoCMS Utilities
2
2
 
3
- A set of utilities and helpers to work with DatoCMS in a Next.js project.
3
+ A collection of utilities and helpers for working with DatoCMS in Next.js projects.
4
4
 
5
5
  ## Installation
6
6
 
@@ -8,23 +8,60 @@ A set of utilities and helpers to work with DatoCMS in a Next.js project.
8
8
  npm install @smartive/datocms-utils
9
9
  ```
10
10
 
11
- ## Usage
11
+ ## Utilities
12
12
 
13
- Import and use the utilities you need in your project. The following utilities are available.
13
+ ### General Utilities
14
14
 
15
- ## Utilities
15
+ #### `classNames`
16
+
17
+ Cleans and joins an array of class names, filtering out undefined and boolean values.
18
+
19
+ ```typescript
20
+ import { classNames } from '@smartive/datocms-utils';
21
+
22
+ const className = classNames('btn', isActive && 'btn-active', undefined, 'btn-primary');
23
+ // Result: "btn btn-active btn-primary"
24
+ ```
25
+
26
+ #### `getTelLink`
27
+
28
+ Converts a phone number into a `tel:` link by removing non-digit characters (except `+` for international numbers).
29
+
30
+ ```typescript
31
+ import { getTelLink } from '@smartive/datocms-utils';
32
+
33
+ const link = getTelLink('+1 (555) 123-4567');
34
+ // Result: "tel:+15551234567"
35
+ ```
36
+
37
+ ### DatoCMS Cache Tags
38
+
39
+ Utilities for managing [DatoCMS cache tags](https://www.datocms.com/docs/content-delivery-api/cache-tags) with different storage backends. Cache tags enable efficient cache invalidation by tracking which queries reference which content.
40
+
41
+ #### Core Utilities
42
+
43
+ ```typescript
44
+ import { generateQueryId, parseXCacheTagsResponseHeader } from '@smartive/datocms-utils/cache-tags';
16
45
 
17
- ### Utilities for DatoCMS Cache Tags
46
+ // Generate a unique ID for a GraphQL query
47
+ const queryId = generateQueryId(document, variables);
18
48
 
19
- The following utilities are used to work with [DatoCMS cache tags](https://www.datocms.com/docs/content-delivery-api/cache-tags) and a [Vercel Postgres database](https://vercel.com/docs/storage/vercel-postgres).
49
+ // Parse DatoCMS's X-Cache-Tags header
50
+ const tags = parseXCacheTagsResponseHeader('tag-a tag-2 other-tag');
51
+ // Result: ['tag-a', 'tag-2', 'other-tag']
52
+ ```
53
+
54
+ #### Storage Providers
55
+
56
+ The package provides two storage backends for cache tags: **Neon (Postgres)** and **Redis**. Both implement the same `CacheTagsProvider` interface.
20
57
 
21
- - `storeQueryCacheTags`: Stores the cache tags of a query in the database.
22
- - `queriesReferencingCacheTags`: Retrieves the queries that reference cache tags.
23
- - `deleteQueries`: Deletes the cache tags of a query from the database.
58
+ ##### Neon (Postgres) Provider
24
59
 
25
- #### Setup Postgres database
60
+ Use Neon serverless Postgres to store cache tag mappings.
26
61
 
27
- In order for the above utilites to work, you need to setup a the following database. You can use the following SQL script to do that:
62
+ **Setup:**
63
+
64
+ 1. Create the cache tags table:
28
65
 
29
66
  ```sql
30
67
  CREATE TABLE IF NOT EXISTS query_cache_tags (
@@ -34,75 +71,116 @@ CREATE TABLE IF NOT EXISTS query_cache_tags (
34
71
  );
35
72
  ```
36
73
 
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.
74
+ 2. Install [@neondatabase/serverless](https://github.com/neondatabase/serverless)
40
75
 
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.
76
+ ```bash
77
+ npm install @neondatabase/serverless
78
+ ```
45
79
 
46
- The Redis connection is automatically initialized on first use using the `REDIS_URL` environment variable.
80
+ 3. Create and use the store:
47
81
 
48
- #### Environment Variables
82
+ ```typescript
83
+ import { NeonCacheTagsProvider } from '@smartive/datocms-utils/cache-tags/neon';
49
84
 
50
- Add your Redis connection URL to your `.env.local` file:
85
+ const provider = new NeonCacheTagsProvider({
86
+ connectionUrl: process.env.DATABASE_URL!,
87
+ table: 'query_cache_tags',
88
+ });
51
89
 
52
- ```bash
53
- # Required: Redis connection URL
54
- # For Upstash Redis
55
- REDIS_URL=rediss://default:your-token@your-endpoint.upstash.io:6379
90
+ // Store cache tags for a query
91
+ await provider.storeQueryCacheTags(queryId, ['item:42', 'product']);
56
92
 
57
- # For Redis Cloud or other providers
58
- REDIS_URL=redis://username:password@your-redis-host:6379
93
+ // Find queries that reference specific tags
94
+ const queries = await provider.queriesReferencingCacheTags(['item:42']);
59
95
 
60
- # For local development
61
- REDIS_URL=redis://localhost:6379
96
+ // Delete specific cache tags
97
+ await provider.deleteCacheTags(['item:42']);
62
98
 
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)
99
+ // Clear all cache tags
100
+ await provider.truncateCacheTags();
68
101
  ```
69
102
 
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.
103
+ ##### Redis Provider
104
+
105
+ Use Redis to store cache tag mappings with better performance for high-traffic applications.
71
106
 
72
- #### Usage Example
107
+ **Setup:**
108
+
109
+ 1. Install [ioredis](https://github.com/redis/ioredis)
110
+
111
+ ```bash
112
+ npm install ioredis
113
+ ```
114
+
115
+ 2. Create and use the provider:
73
116
 
74
117
  ```typescript
75
- // Recommended: Use namespaces for clarity
76
- import { generateQueryId, redis } from '@smartive/datocms-utils';
118
+ import { RedisCacheTagsProvider } from '@smartive/datocms-utils/cache-tags/redis';
119
+
120
+ const provider = new RedisCacheTagsProvider({
121
+ url: process.env.REDIS_URL!,
122
+ keyPrefix: 'prod:', // Optional: namespace for multi-environment setups
123
+ });
124
+
125
+ // Same API as Neon provider
126
+ await provider.storeQueryCacheTags(queryId, ['item:42', 'product']);
127
+ const queries = await provider.queriesReferencingCacheTags(['item:42']);
128
+ await provider.deleteCacheTags(['item:42']);
129
+ await provider.truncateCacheTags();
130
+ ```
77
131
 
78
- const queryId = generateQueryId(query, variables);
132
+ **Redis connection string examples:**
79
133
 
80
- // Store cache tags for a query
81
- await redis.storeQueryCacheTags(queryId, ['item:42', 'product', 'category:5']);
134
+ ```bash
135
+ # Upstash Redis
136
+ REDIS_URL=rediss://default:token@endpoint.upstash.io:6379
82
137
 
83
- // Find all queries that reference specific tags
84
- const affectedQueries = await redis.queriesReferencingCacheTags(['item:42']);
138
+ # Redis Cloud
139
+ REDIS_URL=redis://username:password@redis-host:6379
85
140
 
86
- // Delete cache tags (keys will be recreated on next query)
87
- await redis.deleteCacheTags(['item:42']);
141
+ # Local development
142
+ REDIS_URL=redis://localhost:6379
88
143
  ```
89
144
 
90
- #### Redis Data Structure
145
+ #### `CacheTagsProvider` Interface
146
+
147
+ Both providers implement:
91
148
 
92
- The Redis implementation uses Sets to track query-to-tag relationships:
149
+ - `storeQueryCacheTags(queryId: string, cacheTags: CacheTag[])`: Store cache tags for a query
150
+ - `queriesReferencingCacheTags(cacheTags: CacheTag[])`: Get query IDs that reference any of the specified tags
151
+ - `deleteCacheTags(cacheTags: CacheTag[])`: Delete specific cache tags
152
+ - `truncateCacheTags()`: Wipe all cache tags (use with caution)
93
153
 
94
- - **Cache tag keys**: `{prefix}{tag}` → Set of query IDs
154
+ ### Complete Example
95
155
 
96
- Where `{prefix}` is the optional `REDIS_KEY_PREFIX` environment variable (e.g., `prod:`, `preview:`).
156
+ ```typescript
157
+ import { generateQueryId, parseXCacheTagsResponseHeader } from '@smartive/datocms-utils/cache-tags';
158
+ import { RedisCacheTagsProvider } from '@smartive/datocms-utils/cache-tags/redis';
159
+
160
+ const provider = new RedisCacheTagsProvider({
161
+ connectionUrl: process.env.REDIS_URL!,
162
+ keyPrefix: 'myapp:',
163
+ });
164
+
165
+ // After making a DatoCMS query
166
+ const queryId = generateQueryId(query, variables);
167
+ const cacheTags = parseXCacheTagsResponseHeader(response.headers['x-cache-tags']);
168
+ await provider.storeQueryCacheTags(queryId, cacheTags);
169
+
170
+ // When handling DatoCMS webhook for cache invalidation
171
+ const affectedQueries = await provider.queriesReferencingCacheTags(webhook.entity.attributes.tags);
172
+ // Revalidate affected queries...
173
+ await provider.deleteCacheTags(webhook.entity.attributes.tags);
174
+ ```
97
175
 
98
- When cache tags are invalidated, their keys are deleted entirely. Fresh mappings are created when queries run again.
176
+ ## TypeScript Types
99
177
 
100
- ### Other Utilities
178
+ The package includes TypeScript types for DatoCMS webhooks and cache tags:
101
179
 
102
- - `classNames`: Cleans and joins an array of inputs with possible undefined or boolean values. Useful for tailwind classnames.
103
- - `getTelLink`: Formats a phone number to a tel link.
180
+ - `CacheTag`: A branded type for cache tags, ensuring type safety
181
+ - `CacheTagsInvalidateWebhook`: Type definition for DatoCMS cache tag invalidation webhook payloads
182
+ - `CacheTagsProvider`: Interface for cache tag storage implementations
104
183
 
105
- ### Types
184
+ ## License
106
185
 
107
- - `CacheTag`: A branded type for cache tags.
108
- - `CacheTagsInvalidateWebhook`: The payload of the DatoCMS cache tags invalidate webhook.
186
+ MIT © [smartive AG](https://github.com/smartive)
@@ -0,0 +1,2 @@
1
+ export * from './types.js';
2
+ export * from './utils.js';
@@ -0,0 +1,3 @@
1
+ export * from './types.js';
2
+ export * from './utils.js';
3
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache-tags/index.ts"],"names":[],"mappings":"AAAA,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
@@ -0,0 +1,33 @@
1
+ import { type CacheTag, type CacheTagsProvider } from '../types.js';
2
+ type NeonCacheTagsProviderConfig = {
3
+ /**
4
+ * Neon connection string. You can find it in the "Connection" tab of your Neon project dashboard.
5
+ * Has the format `postgresql://user:pass@host/db`
6
+ */
7
+ readonly connectionUrl: string;
8
+ /**
9
+ * Name of the table where cache tags will be stored. The table must have the following schema:
10
+ *
11
+ * ```sql
12
+ * CREATE TABLE your_table_name (
13
+ * query_id TEXT NOT NULL,
14
+ * cache_tag TEXT NOT NULL,
15
+ * PRIMARY KEY (query_id, cache_tag)
16
+ * );
17
+ * ```
18
+ */
19
+ readonly table: string;
20
+ };
21
+ /**
22
+ * A `CacheTagsProvider` implementation that uses Neon as the storage backend.
23
+ */
24
+ export declare class NeonCacheTagsProvider implements CacheTagsProvider {
25
+ private readonly sql;
26
+ private readonly table;
27
+ constructor({ connectionUrl, table }: NeonCacheTagsProviderConfig);
28
+ storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]): Promise<void>;
29
+ queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]>;
30
+ deleteCacheTags(cacheTags: CacheTag[]): Promise<number>;
31
+ truncateCacheTags(): Promise<number>;
32
+ }
33
+ export {};
@@ -0,0 +1,44 @@
1
+ import { neon } from '@neondatabase/serverless';
2
+ /**
3
+ * A `CacheTagsProvider` implementation that uses Neon as the storage backend.
4
+ */
5
+ export class NeonCacheTagsProvider {
6
+ sql;
7
+ table;
8
+ constructor({ connectionUrl, table }) {
9
+ this.sql = neon(connectionUrl, { fullResults: true });
10
+ this.table = table;
11
+ }
12
+ async storeQueryCacheTags(queryId, cacheTags) {
13
+ if (!cacheTags?.length) {
14
+ return;
15
+ }
16
+ const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]);
17
+ const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(',');
18
+ await this.sql.query(`INSERT INTO ${this.table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags);
19
+ }
20
+ async queriesReferencingCacheTags(cacheTags) {
21
+ if (!cacheTags?.length) {
22
+ return [];
23
+ }
24
+ const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
25
+ const { rows } = await this.sql.query(`SELECT DISTINCT query_id FROM ${this.table} WHERE cache_tag IN (${placeholders})`, cacheTags);
26
+ return rows.reduce((queryIds, row) => {
27
+ if (typeof row.query_id === 'string') {
28
+ queryIds.push(row.query_id);
29
+ }
30
+ return queryIds;
31
+ }, []);
32
+ }
33
+ async deleteCacheTags(cacheTags) {
34
+ if (cacheTags.length === 0) {
35
+ return 0;
36
+ }
37
+ const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
38
+ return (await this.sql.query(`DELETE FROM ${this.table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0;
39
+ }
40
+ async truncateCacheTags() {
41
+ return (await this.sql.query(`DELETE FROM ${this.table}`)).rowCount ?? 0;
42
+ }
43
+ }
44
+ //# sourceMappingURL=neon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"neon.js","sourceRoot":"","sources":["../../../src/cache-tags/provider/neon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAuBhD;;GAEG;AACH,MAAM,OAAO,qBAAqB;IACf,GAAG,CAAC;IACJ,KAAK,CAAC;IAEvB,YAAY,EAAE,aAAa,EAAE,KAAK,EAA+B;QAC/D,IAAI,CAAC,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;QACtD,IAAI,CAAC,KAAK,GAAG,KAAK,CAAC;IACrB,CAAC;IAEM,KAAK,CAAC,mBAAmB,CAAC,OAAe,EAAE,SAAqB;QACrE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QAED,MAAM,IAAI,GAAG,SAAS,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,OAAO,EAAE,SAAS,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;QAClE,MAAM,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,MAAM,CAAC,GAAG,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEzF,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,KAAK,WAAW,YAAY,yBAAyB,EAAE,IAAI,CAAC,CAAC;IACxG,CAAC;IAEM,KAAK,CAAC,2BAA2B,CAAC,SAAqB;QAC5D,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEpE,MAAM,EAAE,IAAI,EAAE,GAAG,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CACnC,iCAAiC,IAAI,CAAC,KAAK,wBAAwB,YAAY,GAAG,EAClF,SAAS,CACV,CAAC;QAEF,OAAO,IAAI,CAAC,MAAM,CAAW,CAAC,QAAQ,EAAE,GAAG,EAAE,EAAE;YAC7C,IAAI,OAAO,GAAG,CAAC,QAAQ,KAAK,QAAQ,EAAE,CAAC;gBACrC,QAAQ,CAAC,IAAI,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;YAC9B,CAAC;YAED,OAAO,QAAQ,CAAC;QAClB,CAAC,EAAE,EAAE,CAAC,CAAC;IACT,CAAC;IAEM,KAAK,CAAC,eAAe,CAAC,SAAqB;QAChD,IAAI,SAAS,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YAC3B,OAAO,CAAC,CAAC;QACX,CAAC;QACD,MAAM,YAAY,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAEpE,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,KAAK,wBAAwB,YAAY,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;IAC3H,CAAC;IAEM,KAAK,CAAC,iBAAiB;QAC5B,OAAO,CAAC,MAAM,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,eAAe,IAAI,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;IAC3E,CAAC;CACF"}
@@ -0,0 +1,12 @@
1
+ import { type CacheTag, type CacheTagsProvider } from '../types.js';
2
+ /**
3
+ * A `CacheTagsProvider` implementation that does not perform any actual storage operations.
4
+ *
5
+ * _Note: This implementation is useful for testing purposes or when you want to disable caching without changing the code that interacts with the cache._
6
+ */
7
+ export declare class NoopCacheTagsProvider implements CacheTagsProvider {
8
+ storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]): Promise<void>;
9
+ queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]>;
10
+ deleteCacheTags(cacheTags: CacheTag[]): Promise<number>;
11
+ truncateCacheTags(): Promise<number>;
12
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * A `CacheTagsProvider` implementation that does not perform any actual storage operations.
3
+ *
4
+ * _Note: This implementation is useful for testing purposes or when you want to disable caching without changing the code that interacts with the cache._
5
+ */
6
+ export class NoopCacheTagsProvider {
7
+ async storeQueryCacheTags(queryId, cacheTags) {
8
+ console.debug('-- storeQueryCacheTags called', { queryId, cacheTags });
9
+ return Promise.resolve();
10
+ }
11
+ async queriesReferencingCacheTags(cacheTags) {
12
+ console.debug('-- queriesReferencingCacheTags called', { cacheTags });
13
+ return Promise.resolve([]);
14
+ }
15
+ async deleteCacheTags(cacheTags) {
16
+ console.debug('-- deleteCacheTags called', { cacheTags });
17
+ return Promise.resolve(0);
18
+ }
19
+ async truncateCacheTags() {
20
+ console.debug('-- truncateCacheTags called');
21
+ return Promise.resolve(0);
22
+ }
23
+ }
24
+ //# sourceMappingURL=noop.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"noop.js","sourceRoot":"","sources":["../../../src/cache-tags/provider/noop.ts"],"names":[],"mappings":"AAEA;;;;GAIG;AACH,MAAM,OAAO,qBAAqB;IACzB,KAAK,CAAC,mBAAmB,CAAC,OAAe,EAAE,SAAqB;QACrE,OAAO,CAAC,KAAK,CAAC,+BAA+B,EAAE,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC;QAEvE,OAAO,OAAO,CAAC,OAAO,EAAE,CAAC;IAC3B,CAAC;IAEM,KAAK,CAAC,2BAA2B,CAAC,SAAqB;QAC5D,OAAO,CAAC,KAAK,CAAC,uCAAuC,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;QAEtE,OAAO,OAAO,CAAC,OAAO,CAAC,EAAE,CAAC,CAAC;IAC7B,CAAC;IAEM,KAAK,CAAC,eAAe,CAAC,SAAqB;QAChD,OAAO,CAAC,KAAK,CAAC,2BAA2B,EAAE,EAAE,SAAS,EAAE,CAAC,CAAC;QAE1D,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC;IAEM,KAAK,CAAC,iBAAiB;QAC5B,OAAO,CAAC,KAAK,CAAC,6BAA6B,CAAC,CAAC;QAE7C,OAAO,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC;IAC5B,CAAC;CACF"}
@@ -0,0 +1,33 @@
1
+ import { type CacheTag, type CacheTagsProvider } from '../types.js';
2
+ type RedisCacheTagsProviderConfig = {
3
+ /**
4
+ * Redis connection string. For example, `redis://user:pass@host:port/db`.
5
+ */
6
+ readonly connectionUrl: string;
7
+ /**
8
+ * Optional prefix for Redis keys. If provided, all keys used to store cache tags will be prefixed with this value.
9
+ * This can be useful to avoid key collisions if the same Redis instance is used for multiple purposes.
10
+ * For example, if you set `keyPrefix` to `'myapp:'`, a cache tag like `'tag1'` will be stored under the key `'myapp:tag1'`.
11
+ */
12
+ readonly keyPrefix?: string;
13
+ };
14
+ /**
15
+ * A `CacheTagsProvider` implementation that uses Redis as the storage backend.
16
+ */
17
+ export declare class RedisCacheTagsProvider implements CacheTagsProvider {
18
+ private readonly redis;
19
+ private readonly keyPrefix;
20
+ constructor({ connectionUrl, keyPrefix }: RedisCacheTagsProviderConfig);
21
+ storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]): Promise<void>;
22
+ queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]>;
23
+ deleteCacheTags(cacheTags: CacheTag[]): Promise<number>;
24
+ truncateCacheTags(): Promise<number>;
25
+ /**
26
+ * Retrieves all keys matching the given pattern using the Redis SCAN command.
27
+ * This method is more efficient than using the KEYS command, especially for large datasets.
28
+ *
29
+ * @returns An array of matching keys
30
+ */
31
+ private getKeys;
32
+ }
33
+ export {};
@@ -0,0 +1,71 @@
1
+ import { Redis } from 'ioredis';
2
+ /**
3
+ * A `CacheTagsProvider` implementation that uses Redis as the storage backend.
4
+ */
5
+ export class RedisCacheTagsProvider {
6
+ redis;
7
+ keyPrefix;
8
+ constructor({ connectionUrl, keyPrefix }) {
9
+ this.redis = new Redis(connectionUrl, {
10
+ maxRetriesPerRequest: 3,
11
+ lazyConnect: true,
12
+ });
13
+ this.keyPrefix = keyPrefix ?? '';
14
+ }
15
+ async storeQueryCacheTags(queryId, cacheTags) {
16
+ if (!cacheTags?.length) {
17
+ return;
18
+ }
19
+ const pipeline = this.redis.pipeline();
20
+ for (const tag of cacheTags) {
21
+ pipeline.sadd(`${this.keyPrefix}${tag}`, queryId);
22
+ }
23
+ await pipeline.exec();
24
+ }
25
+ async queriesReferencingCacheTags(cacheTags) {
26
+ if (!cacheTags?.length) {
27
+ return [];
28
+ }
29
+ const keys = cacheTags.map((tag) => `${this.keyPrefix}${tag}`);
30
+ return this.redis.sunion(...keys);
31
+ }
32
+ async deleteCacheTags(cacheTags) {
33
+ if (!cacheTags?.length) {
34
+ return 0;
35
+ }
36
+ const keys = cacheTags.map((tag) => `${this.keyPrefix}${tag}`);
37
+ return this.redis.del(...keys);
38
+ }
39
+ async truncateCacheTags() {
40
+ const keys = await this.getKeys();
41
+ if (keys.length === 0) {
42
+ return 0;
43
+ }
44
+ return await this.redis.del(...keys);
45
+ }
46
+ /**
47
+ * Retrieves all keys matching the given pattern using the Redis SCAN command.
48
+ * This method is more efficient than using the KEYS command, especially for large datasets.
49
+ *
50
+ * @returns An array of matching keys
51
+ */
52
+ async getKeys() {
53
+ return new Promise((resolve, reject) => {
54
+ const keys = [];
55
+ const stream = this.redis.scanStream({
56
+ match: `${this.keyPrefix}*`,
57
+ count: 1000,
58
+ });
59
+ stream.on('data', (resultKeys) => {
60
+ keys.push(...resultKeys);
61
+ });
62
+ stream.on('end', () => {
63
+ resolve(keys);
64
+ });
65
+ stream.on('error', (err) => {
66
+ reject(err);
67
+ });
68
+ });
69
+ }
70
+ }
71
+ //# sourceMappingURL=redis.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"redis.js","sourceRoot":"","sources":["../../../src/cache-tags/provider/redis.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,63 @@
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
+ };
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
+ * Retrieves the query IDs that reference any of the specified cache tags.
40
+ *
41
+ * @param {CacheTag[]} cacheTags Array of cache tags to check
42
+ * @returns Array of unique query IDs
43
+ *
44
+ */
45
+ queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]>;
46
+ /**
47
+ * Deletes the specified cache tags.
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
+ * @returns Number of keys deleted, or null if there was an error
54
+ *
55
+ */
56
+ deleteCacheTags(cacheTags: CacheTag[]): Promise<number>;
57
+ /**
58
+ * Wipes out all cache tags.
59
+ *
60
+ * ⚠️ **Warning**: This will delete all cache tag data. Use with caution!
61
+ */
62
+ truncateCacheTags(): Promise<number>;
63
+ }
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cache-tags/types.ts"],"names":[],"mappings":""}
@@ -1,5 +1,5 @@
1
1
  import { type DocumentNode } from 'graphql';
2
- import { type CacheTag } from './types';
2
+ import { type CacheTag } from './types.js';
3
3
  /**
4
4
  * Converts the value of DatoCMS's `X-Cache-Tags` header into an array of strings typed as `CacheTag`.
5
5
  * For example, it transforms `'tag-a tag-2 other-tag'` into `['tag-a', 'tag-2', 'other-tag']`.
@@ -0,0 +1 @@
1
+ {"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/cache-tags/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"}
@@ -4,4 +4,4 @@
4
4
  * @param classNames Array of class names
5
5
  * @returns Clean string to be used for class name
6
6
  */
7
- export declare const classNames: (...classNames: (string | undefined | boolean)[]) => string;
7
+ export declare const classNames: (...classNames: unknown[]) => string;
@@ -4,5 +4,7 @@
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) => classNames.filter(Boolean).join(' ');
7
+ export const classNames = (...classNames) => classNames
8
+ .filter((value) => (typeof value === 'string' && value.length > 0) || (typeof value === 'number' && Number.isFinite(value)))
9
+ .join(' ');
8
10
  //# sourceMappingURL=classnames.js.map
@@ -1 +1 @@
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"}
1
+ {"version":3,"file":"classnames.js","sourceRoot":"","sources":["../src/classnames.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,GAAG,UAAqB,EAAE,EAAE,CACrD,UAAU;KACP,MAAM,CACL,CAAC,KAAK,EAA4B,EAAE,CAClC,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,IAAI,CAAC,OAAO,KAAK,KAAK,QAAQ,IAAI,MAAM,CAAC,QAAQ,CAAC,KAAK,CAAC,CAAC,CAC3G;KACA,IAAI,CAAC,GAAG,CAAC,CAAC"}
package/dist/index.d.ts CHANGED
@@ -1,4 +1,2 @@
1
- export * from './cache-tags';
2
- export * from './classnames';
3
- export * from './links';
4
- export * from './types';
1
+ export * from './classnames.js';
2
+ export * from './links.js';
package/dist/index.js CHANGED
@@ -1,5 +1,3 @@
1
- export * from './cache-tags';
2
- export * from './classnames';
3
- export * from './links';
4
- export * from './types';
1
+ export * from './classnames.js';
2
+ export * from './links.js';
5
3
  //# sourceMappingURL=index.js.map
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,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC"}