@smartive/datocms-utils 3.0.0-next.3 → 3.0.0-next.5

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md 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';
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 `CacheTagsStore` 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 { createCacheTagsStore } from '@smartive/datocms-utils/cache/neon';
49
84
 
50
- Add your Redis connection URL to your `.env.local` file:
85
+ const store = createCacheTagsStore({
86
+ connectionString: 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 store.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 store.queriesReferencingCacheTags(['item:42']);
59
95
 
60
- # For local development
61
- REDIS_URL=redis://localhost:6379
96
+ // Delete specific cache tags
97
+ await store.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 store.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 store:
73
116
 
74
117
  ```typescript
75
- // Recommended: Use namespaces for clarity
76
- import { generateQueryId, redis } from '@smartive/datocms-utils';
118
+ import { createCacheTagsStore } from '@smartive/datocms-utils/cache/redis';
119
+
120
+ const store = createCacheTagsStore({
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 store.storeQueryCacheTags(queryId, ['item:42', 'product']);
127
+ const queries = await store.queriesReferencingCacheTags(['item:42']);
128
+ await store.deleteCacheTags(['item:42']);
129
+ await store.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
+ #### `CacheTagsStore` 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';
158
+ import { createCacheTagsStore } from '@smartive/datocms-utils/cache/redis';
159
+
160
+ const store = createCacheTagsStore({
161
+ url: 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 store.storeQueryCacheTags(queryId, cacheTags);
169
+
170
+ // When handling DatoCMS webhook for cache invalidation
171
+ const affectedQueries = await store.queriesReferencingCacheTags(webhook.entity.attributes.tags);
172
+ // Revalidate affected queries...
173
+ await store.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
+ - `CacheTagsStore`: 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,28 @@
1
+ import { type CacheTagsStore } from '../types.js';
2
+ type NeonCacheTagsStoreConfig = {
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
+ * Creates a `CacheTagsStore` implementation using Neon as the storage backend. Neon is a serverless Postgres database service.
23
+ *
24
+ * @param {NeonCacheTagsStoreConfig} config Configuration object containing the Neon connection string and table name.
25
+ * @returns An object implementing the `CacheTagsStore` interface, allowing you to store and manage cache tags in a Neon database.
26
+ */
27
+ export declare const createCacheTagsStore: ({ connectionUrl, table }: NeonCacheTagsStoreConfig) => CacheTagsStore;
28
+ export {};
@@ -0,0 +1,46 @@
1
+ import { neon } from '@neondatabase/serverless';
2
+ /**
3
+ * Creates a `CacheTagsStore` implementation using Neon as the storage backend. Neon is a serverless Postgres database service.
4
+ *
5
+ * @param {NeonCacheTagsStoreConfig} config Configuration object containing the Neon connection string and table name.
6
+ * @returns An object implementing the `CacheTagsStore` interface, allowing you to store and manage cache tags in a Neon database.
7
+ */
8
+ export const createCacheTagsStore = ({ connectionUrl, table }) => {
9
+ const sql = neon(connectionUrl, { fullResults: true });
10
+ const storeQueryCacheTags = async (queryId, cacheTags) => {
11
+ if (!cacheTags?.length) {
12
+ return;
13
+ }
14
+ const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]);
15
+ const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(',');
16
+ await sql.query(`INSERT INTO ${table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags);
17
+ };
18
+ const queriesReferencingCacheTags = async (cacheTags) => {
19
+ if (!cacheTags?.length) {
20
+ return [];
21
+ }
22
+ const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
23
+ const { rows } = await sql.query(`SELECT DISTINCT query_id FROM ${table} WHERE cache_tag IN (${placeholders})`, cacheTags);
24
+ return rows.reduce((queryIds, row) => {
25
+ if (typeof row.query_id === 'string') {
26
+ queryIds.push(row.query_id);
27
+ }
28
+ return queryIds;
29
+ }, []);
30
+ };
31
+ const deleteCacheTags = async (cacheTags) => {
32
+ if (cacheTags.length === 0) {
33
+ return 0;
34
+ }
35
+ const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
36
+ return (await sql.query(`DELETE FROM ${table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0;
37
+ };
38
+ const truncateCacheTags = async () => (await sql.query(`DELETE FROM ${table}`)).rowCount ?? 0;
39
+ return {
40
+ storeQueryCacheTags,
41
+ queriesReferencingCacheTags,
42
+ deleteCacheTags,
43
+ truncateCacheTags,
44
+ };
45
+ };
46
+ //# sourceMappingURL=neon.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"neon.js","sourceRoot":"","sources":["../../../src/cache/provider/neon.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAE,MAAM,0BAA0B,CAAC;AAuBhD;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,EAAE,aAAa,EAAE,KAAK,EAA4B,EAAkB,EAAE;IACzG,MAAM,GAAG,GAAG,IAAI,CAAC,aAAa,EAAE,EAAE,WAAW,EAAE,IAAI,EAAE,CAAC,CAAC;IAEvD,MAAM,mBAAmB,GAAG,KAAK,EAAE,OAAe,EAAE,SAAqB,EAAE,EAAE;QAC3E,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,GAAG,CAAC,KAAK,CAAC,eAAe,KAAK,WAAW,YAAY,yBAAyB,EAAE,IAAI,CAAC,CAAC;IAC9F,CAAC,CAAC;IAEF,MAAM,2BAA2B,GAAG,KAAK,EAAE,SAAqB,EAAqB,EAAE;QACrF,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,GAAG,CAAC,KAAK,CAC9B,iCAAiC,KAAK,wBAAwB,YAAY,GAAG,EAC7E,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,CAAC;IAEF,MAAM,eAAe,GAAG,KAAK,EAAE,SAAqB,EAAE,EAAE;QACtD,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,GAAG,CAAC,KAAK,CAAC,eAAe,KAAK,wBAAwB,YAAY,GAAG,EAAE,SAAS,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;IACjH,CAAC,CAAC;IAEF,MAAM,iBAAiB,GAAG,KAAK,IAAI,EAAE,CAAC,CAAC,MAAM,GAAG,CAAC,KAAK,CAAC,eAAe,KAAK,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,CAAC;IAE9F,OAAO;QACL,mBAAmB;QACnB,2BAA2B;QAC3B,eAAe;QACf,iBAAiB;KAClB,CAAC;AACJ,CAAC,CAAC"}
@@ -1,5 +1,21 @@
1
1
  import { type CacheTagsStore } from '../types.js';
2
- export declare const createCacheTagsStore: ({ url, keyPrefix }: {
3
- url: string;
4
- keyPrefix?: string;
5
- }) => CacheTagsStore;
2
+ type RedisCacheTagsStoreConfig = {
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
+ * Creates a `CacheTagsStore` implementation using Redis as the storage backend.
16
+ *
17
+ * @param {RedisCacheTagsStoreConfig} config Configuration object containing the Redis connection string and optional key prefix.
18
+ * @returns An object implementing the `CacheTagsStore` interface, allowing you to store and manage cache tags in a Redis database.
19
+ */
20
+ export declare const createCacheTagsStore: ({ connectionUrl, keyPrefix }: RedisCacheTagsStoreConfig) => CacheTagsStore;
21
+ export {};
@@ -1,6 +1,12 @@
1
1
  import { Redis } from 'ioredis';
2
- export const createCacheTagsStore = ({ url, keyPrefix = '' }) => {
3
- const redis = new Redis(url, {
2
+ /**
3
+ * Creates a `CacheTagsStore` implementation using Redis as the storage backend.
4
+ *
5
+ * @param {RedisCacheTagsStoreConfig} config Configuration object containing the Redis connection string and optional key prefix.
6
+ * @returns An object implementing the `CacheTagsStore` interface, allowing you to store and manage cache tags in a Redis database.
7
+ */
8
+ export const createCacheTagsStore = ({ connectionUrl, keyPrefix = '' }) => {
9
+ const redis = new Redis(connectionUrl, {
4
10
  maxRetriesPerRequest: 3,
5
11
  lazyConnect: true,
6
12
  });
@@ -1 +1 @@
1
- {"version":3,"file":"redis.js","sourceRoot":"","sources":["../../../src/cache/provider/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,EAAE,GAAG,EAAE,SAAS,GAAG,EAAE,EAAuC,EAAkB,EAAE;IACnH,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,GAAG,EAAE;QAC3B,oBAAoB,EAAE,CAAC;QACvB,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAEH,MAAM,mBAAmB,GAAG,KAAK,EAAE,OAAe,EAAE,SAAqB,EAAE,EAAE;QAC3E,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QAElC,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC;QAC/C,CAAC;QAED,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC,CAAC;IAEF,MAAM,2BAA2B,GAAG,KAAK,EAAE,SAAqB,EAAE,EAAE;QAClE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;QAE1D,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAC;IAEF,MAAM,eAAe,GAAG,KAAK,EAAE,SAAqB,EAAE,EAAE;QACtD,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO,CAAC,CAAC;QACX,CAAC;QAED,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;QAE1D,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,iBAAiB,GAAG,KAAK,IAAI,EAAE;QACnC,MAAM,OAAO,GAAG,GAAG,SAAS,GAAG,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAEvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,CAAC;QACX,CAAC;QAED,OAAO,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,CAAC,CAAC;IAEF,OAAO;QACL,mBAAmB;QACnB,2BAA2B;QAC3B,eAAe;QACf,iBAAiB;KAClB,CAAC;AACJ,CAAC,CAAC"}
1
+ {"version":3,"file":"redis.js","sourceRoot":"","sources":["../../../src/cache/provider/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAgBhC;;;;;GAKG;AACH,MAAM,CAAC,MAAM,oBAAoB,GAAG,CAAC,EAAE,aAAa,EAAE,SAAS,GAAG,EAAE,EAA6B,EAAkB,EAAE;IACnH,MAAM,KAAK,GAAG,IAAI,KAAK,CAAC,aAAa,EAAE;QACrC,oBAAoB,EAAE,CAAC;QACvB,WAAW,EAAE,IAAI;KAClB,CAAC,CAAC;IAEH,MAAM,mBAAmB,GAAG,KAAK,EAAE,OAAe,EAAE,SAAqB,EAAE,EAAE;QAC3E,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,KAAK,CAAC,QAAQ,EAAE,CAAC;QAElC,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC;QAC/C,CAAC;QAED,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC,CAAC;IAEF,MAAM,2BAA2B,GAAG,KAAK,EAAE,SAAqB,EAAE,EAAE;QAClE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO,EAAE,CAAC;QACZ,CAAC;QAED,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;QAE1D,OAAO,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IAC/B,CAAC,CAAC;IAEF,MAAM,eAAe,GAAG,KAAK,EAAE,SAAqB,EAAE,EAAE;QACtD,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO,CAAC,CAAC;QACX,CAAC;QAED,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;QAE1D,OAAO,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IAC5B,CAAC,CAAC;IAEF,MAAM,iBAAiB,GAAG,KAAK,IAAI,EAAE;QACnC,MAAM,OAAO,GAAG,GAAG,SAAS,GAAG,CAAC;QAChC,MAAM,IAAI,GAAG,MAAM,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;QAEvC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,CAAC;QACX,CAAC;QAED,OAAO,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IAClC,CAAC,CAAC;IAEF,OAAO;QACL,mBAAmB;QACnB,2BAA2B;QAC3B,eAAe;QACf,iBAAiB;KAClB,CAAC;AACJ,CAAC,CAAC"}
@@ -23,6 +23,9 @@ export type CacheTagsInvalidateWebhook = {
23
23
  };
24
24
  };
25
25
  };
26
+ /**
27
+ * Configuration object for creating a `CacheTagsStore` implementation.
28
+ */
26
29
  export type CacheTagsStore = {
27
30
  /**
28
31
  * Stores the cache tags of a query.
@@ -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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@smartive/datocms-utils",
3
- "version": "3.0.0-next.3",
3
+ "version": "3.0.0-next.5",
4
4
  "description": "A set of utilities and helpers to work with DatoCMS in a Next.js project.",
5
5
  "type": "module",
6
6
  "source": "./src/index.ts",
@@ -0,0 +1,82 @@
1
+ import { neon } from '@neondatabase/serverless';
2
+ import { type CacheTag, type CacheTagsStore } from '../types.js';
3
+
4
+ type NeonCacheTagsStoreConfig = {
5
+ /**
6
+ * Neon connection string. You can find it in the "Connection" tab of your Neon project dashboard.
7
+ * Has the format `postgresql://user:pass@host/db`
8
+ */
9
+ readonly connectionUrl: string;
10
+ /**
11
+ * Name of the table where cache tags will be stored. The table must have the following schema:
12
+ *
13
+ * ```sql
14
+ * CREATE TABLE your_table_name (
15
+ * query_id TEXT NOT NULL,
16
+ * cache_tag TEXT NOT NULL,
17
+ * PRIMARY KEY (query_id, cache_tag)
18
+ * );
19
+ * ```
20
+ */
21
+ readonly table: string;
22
+ };
23
+
24
+ /**
25
+ * Creates a `CacheTagsStore` implementation using Neon as the storage backend. Neon is a serverless Postgres database service.
26
+ *
27
+ * @param {NeonCacheTagsStoreConfig} config Configuration object containing the Neon connection string and table name.
28
+ * @returns An object implementing the `CacheTagsStore` interface, allowing you to store and manage cache tags in a Neon database.
29
+ */
30
+ export const createCacheTagsStore = ({ connectionUrl, table }: NeonCacheTagsStoreConfig): CacheTagsStore => {
31
+ const sql = neon(connectionUrl, { fullResults: true });
32
+
33
+ const storeQueryCacheTags = async (queryId: string, cacheTags: CacheTag[]) => {
34
+ if (!cacheTags?.length) {
35
+ return;
36
+ }
37
+
38
+ const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]);
39
+ const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(',');
40
+
41
+ await sql.query(`INSERT INTO ${table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags);
42
+ };
43
+
44
+ const queriesReferencingCacheTags = async (cacheTags: CacheTag[]): Promise<string[]> => {
45
+ if (!cacheTags?.length) {
46
+ return [];
47
+ }
48
+
49
+ const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
50
+
51
+ const { rows } = await sql.query(
52
+ `SELECT DISTINCT query_id FROM ${table} WHERE cache_tag IN (${placeholders})`,
53
+ cacheTags,
54
+ );
55
+
56
+ return rows.reduce<string[]>((queryIds, row) => {
57
+ if (typeof row.query_id === 'string') {
58
+ queryIds.push(row.query_id);
59
+ }
60
+
61
+ return queryIds;
62
+ }, []);
63
+ };
64
+
65
+ const deleteCacheTags = async (cacheTags: CacheTag[]) => {
66
+ if (cacheTags.length === 0) {
67
+ return 0;
68
+ }
69
+ const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
70
+
71
+ return (await sql.query(`DELETE FROM ${table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0;
72
+ };
73
+
74
+ const truncateCacheTags = async () => (await sql.query(`DELETE FROM ${table}`)).rowCount ?? 0;
75
+
76
+ return {
77
+ storeQueryCacheTags,
78
+ queriesReferencingCacheTags,
79
+ deleteCacheTags,
80
+ truncateCacheTags,
81
+ };
82
+ };
@@ -1,8 +1,27 @@
1
1
  import { Redis } from 'ioredis';
2
2
  import { type CacheTag, type CacheTagsStore } from '../types.js';
3
3
 
4
- export const createCacheTagsStore = ({ url, keyPrefix = '' }: { url: string; keyPrefix?: string }): CacheTagsStore => {
5
- const redis = new Redis(url, {
4
+ type RedisCacheTagsStoreConfig = {
5
+ /**
6
+ * Redis connection string. For example, `redis://user:pass@host:port/db`.
7
+ */
8
+ readonly connectionUrl: string;
9
+ /**
10
+ * Optional prefix for Redis keys. If provided, all keys used to store cache tags will be prefixed with this value.
11
+ * This can be useful to avoid key collisions if the same Redis instance is used for multiple purposes.
12
+ * For example, if you set `keyPrefix` to `'myapp:'`, a cache tag like `'tag1'` will be stored under the key `'myapp:tag1'`.
13
+ */
14
+ readonly keyPrefix?: string;
15
+ };
16
+
17
+ /**
18
+ * Creates a `CacheTagsStore` implementation using Redis as the storage backend.
19
+ *
20
+ * @param {RedisCacheTagsStoreConfig} config Configuration object containing the Redis connection string and optional key prefix.
21
+ * @returns An object implementing the `CacheTagsStore` interface, allowing you to store and manage cache tags in a Redis database.
22
+ */
23
+ export const createCacheTagsStore = ({ connectionUrl, keyPrefix = '' }: RedisCacheTagsStoreConfig): CacheTagsStore => {
24
+ const redis = new Redis(connectionUrl, {
6
25
  maxRetriesPerRequest: 3,
7
26
  lazyConnect: true,
8
27
  });
@@ -23,6 +23,9 @@ export type CacheTagsInvalidateWebhook = {
23
23
  };
24
24
  };
25
25
 
26
+ /**
27
+ * Configuration object for creating a `CacheTagsStore` implementation.
28
+ */
26
29
  export type CacheTagsStore = {
27
30
  /**
28
31
  * Stores the cache tags of a query.
package/src/classnames.ts CHANGED
@@ -4,4 +4,10 @@
4
4
  * @param classNames Array of class names
5
5
  * @returns Clean string to be used for class name
6
6
  */
7
- export const classNames = (...classNames: (string | undefined | boolean)[]): string => classNames.filter(Boolean).join(' ');
7
+ export const classNames = (...classNames: unknown[]) =>
8
+ classNames
9
+ .filter(
10
+ (value): value is string | number =>
11
+ (typeof value === 'string' && value.length > 0) || (typeof value === 'number' && Number.isFinite(value)),
12
+ )
13
+ .join(' ');