@smartive/datocms-utils 2.3.3 → 3.0.0-next.10
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +134 -56
- package/dist/cache-tags/index.d.ts +2 -0
- package/dist/cache-tags/index.js +3 -0
- package/dist/cache-tags/index.js.map +1 -0
- package/dist/cache-tags/provider/neon.d.ts +33 -0
- package/dist/cache-tags/provider/neon.js +44 -0
- package/dist/cache-tags/provider/neon.js.map +1 -0
- package/dist/cache-tags/provider/noop.d.ts +12 -0
- package/dist/cache-tags/provider/noop.js +24 -0
- package/dist/cache-tags/provider/noop.js.map +1 -0
- package/dist/cache-tags/provider/redis.d.ts +33 -0
- package/dist/cache-tags/provider/redis.js +71 -0
- package/dist/cache-tags/provider/redis.js.map +1 -0
- package/dist/cache-tags/types.d.ts +63 -0
- package/dist/cache-tags/types.js.map +1 -0
- package/dist/{utils.d.ts → cache-tags/utils.d.ts} +1 -1
- package/dist/cache-tags/utils.js.map +1 -0
- package/dist/classnames.d.ts +1 -1
- package/dist/classnames.js +3 -1
- package/dist/classnames.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.js +2 -4
- package/dist/index.js.map +1 -1
- package/package.json +39 -10
- package/src/cache-tags/index.ts +2 -0
- package/src/cache-tags/provider/neon.ts +80 -0
- package/src/cache-tags/provider/noop.ts +32 -0
- package/src/cache-tags/provider/redis.ts +104 -0
- package/src/cache-tags/types.ts +66 -0
- package/src/{utils.ts → cache-tags/utils.ts} +1 -1
- package/src/classnames.ts +7 -1
- package/src/index.ts +2 -4
- package/dist/cache-tags-redis.d.ts +0 -39
- package/dist/cache-tags-redis.js +0 -80
- package/dist/cache-tags-redis.js.map +0 -1
- package/dist/cache-tags.d.ts +0 -43
- package/dist/cache-tags.js +0 -72
- package/dist/cache-tags.js.map +0 -1
- package/dist/types.d.ts +0 -25
- package/dist/types.js.map +0 -1
- package/dist/utils.js.map +0 -1
- package/src/cache-tags-redis.ts +0 -96
- package/src/cache-tags.ts +0 -88
- package/src/types.ts +0 -24
- /package/dist/{types.js → cache-tags/types.js} +0 -0
- /package/dist/{utils.js → cache-tags/utils.js} +0 -0
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# smartive DatoCMS Utilities
|
|
2
2
|
|
|
3
|
-
A
|
|
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
|
-
##
|
|
11
|
+
## Utilities
|
|
12
12
|
|
|
13
|
-
|
|
13
|
+
### General Utilities
|
|
14
14
|
|
|
15
|
-
|
|
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
|
-
|
|
46
|
+
// Generate a unique ID for a GraphQL query
|
|
47
|
+
const queryId = generateQueryId(document, variables);
|
|
18
48
|
|
|
19
|
-
|
|
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
|
-
|
|
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
|
-
|
|
60
|
+
Use Neon serverless Postgres to store cache tag mappings.
|
|
26
61
|
|
|
27
|
-
|
|
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
|
-
|
|
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
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
- `redis.truncateCacheTags`: Wipes out all cache tags from Redis.
|
|
76
|
+
```bash
|
|
77
|
+
npm install @neondatabase/serverless
|
|
78
|
+
```
|
|
45
79
|
|
|
46
|
-
|
|
80
|
+
3. Create and use the store:
|
|
47
81
|
|
|
48
|
-
|
|
82
|
+
```typescript
|
|
83
|
+
import { NeonCacheTagsProvider } from '@smartive/datocms-utils/cache-tags/neon';
|
|
49
84
|
|
|
50
|
-
|
|
85
|
+
const provider = new NeonCacheTagsProvider({
|
|
86
|
+
connectionUrl: process.env.DATABASE_URL!,
|
|
87
|
+
table: 'query_cache_tags',
|
|
88
|
+
});
|
|
51
89
|
|
|
52
|
-
|
|
53
|
-
|
|
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
|
-
|
|
58
|
-
|
|
93
|
+
// Find queries that reference specific tags
|
|
94
|
+
const queries = await provider.queriesReferencingCacheTags(['item:42']);
|
|
59
95
|
|
|
60
|
-
|
|
61
|
-
|
|
96
|
+
// Delete specific cache tags
|
|
97
|
+
await provider.deleteCacheTags(['item:42']);
|
|
62
98
|
|
|
63
|
-
|
|
64
|
-
|
|
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
|
-
|
|
103
|
+
##### Redis Provider
|
|
104
|
+
|
|
105
|
+
Use Redis to store cache tag mappings with better performance for high-traffic applications.
|
|
71
106
|
|
|
72
|
-
|
|
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
|
-
|
|
76
|
-
|
|
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
|
-
|
|
132
|
+
**Redis connection string examples:**
|
|
79
133
|
|
|
80
|
-
|
|
81
|
-
|
|
134
|
+
```bash
|
|
135
|
+
# Upstash Redis
|
|
136
|
+
REDIS_URL=rediss://default:token@endpoint.upstash.io:6379
|
|
82
137
|
|
|
83
|
-
|
|
84
|
-
|
|
138
|
+
# Redis Cloud
|
|
139
|
+
REDIS_URL=redis://username:password@redis-host:6379
|
|
85
140
|
|
|
86
|
-
|
|
87
|
-
|
|
141
|
+
# Local development
|
|
142
|
+
REDIS_URL=redis://localhost:6379
|
|
88
143
|
```
|
|
89
144
|
|
|
90
|
-
####
|
|
145
|
+
#### `CacheTagsProvider` Interface
|
|
146
|
+
|
|
147
|
+
Both providers implement:
|
|
91
148
|
|
|
92
|
-
|
|
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
|
-
|
|
154
|
+
### Complete Example
|
|
95
155
|
|
|
96
|
-
|
|
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
|
-
|
|
176
|
+
## TypeScript Types
|
|
99
177
|
|
|
100
|
-
|
|
178
|
+
The package includes TypeScript types for DatoCMS webhooks and cache tags:
|
|
101
179
|
|
|
102
|
-
- `
|
|
103
|
-
- `
|
|
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
|
-
|
|
184
|
+
## License
|
|
106
185
|
|
|
107
|
-
|
|
108
|
-
- `CacheTagsInvalidateWebhook`: The payload of the DatoCMS cache tags invalidate webhook.
|
|
186
|
+
MIT © [smartive AG](https://github.com/smartive)
|
|
@@ -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"}
|
package/dist/classnames.d.ts
CHANGED
package/dist/classnames.js
CHANGED
|
@@ -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
|
|
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
|
package/dist/classnames.js.map
CHANGED
|
@@ -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,
|
|
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
package/dist/index.js
CHANGED
package/dist/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,cAAc,iBAAiB,CAAC;AAChC,cAAc,YAAY,CAAC"}
|