@smartive/datocms-utils 3.0.0-next.1 → 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 → cache-tags}/types.d.ts +5 -2
- package/dist/{cache → cache-tags}/types.js.map +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/package.json +20 -8
- 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 → cache-tags}/types.ts +5 -2
- package/src/classnames.ts +7 -1
- package/dist/cache/index.d.ts +0 -4
- package/dist/cache/index.js +0 -5
- package/dist/cache/index.js.map +0 -1
- package/dist/cache/store/postgres.d.ts +0 -4
- package/dist/cache/store/postgres.js +0 -34
- package/dist/cache/store/postgres.js.map +0 -1
- package/dist/cache/store/redis.d.ts +0 -5
- package/dist/cache/store/redis.js +0 -46
- package/dist/cache/store/redis.js.map +0 -1
- package/dist/cache/utils.js.map +0 -1
- package/src/cache/index.ts +0 -4
- package/src/cache/store/postgres.ts +0 -48
- package/src/cache/store/redis.ts +0 -61
- /package/dist/{cache → cache-tags}/types.js +0 -0
- /package/dist/{cache → cache-tags}/utils.d.ts +0 -0
- /package/dist/{cache → cache-tags}/utils.js +0 -0
- /package/src/{cache → cache-tags}/utils.ts +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":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAgBhC;;GAEG;AACH,MAAM,OAAO,sBAAsB;IAChB,KAAK,CAAC;IACN,SAAS,CAAC;IAE3B,YAAY,EAAE,aAAa,EAAE,SAAS,EAAgC;QACpE,IAAI,CAAC,KAAK,GAAG,IAAI,KAAK,CAAC,aAAa,EAAE;YACpC,oBAAoB,EAAE,CAAC;YACvB,WAAW,EAAE,IAAI;SAClB,CAAC,CAAC;QACH,IAAI,CAAC,SAAS,GAAG,SAAS,IAAI,EAAE,CAAC;IACnC,CAAC;IAEM,KAAK,CAAC,mBAAmB,CAAC,OAAe,EAAE,SAAqB;QACrE,IAAI,CAAC,SAAS,EAAE,MAAM,EAAE,CAAC;YACvB,OAAO;QACT,CAAC;QAED,MAAM,QAAQ,GAAG,IAAI,CAAC,KAAK,CAAC,QAAQ,EAAE,CAAC;QAEvC,KAAK,MAAM,GAAG,IAAI,SAAS,EAAE,CAAC;YAC5B,QAAQ,CAAC,IAAI,CAAC,GAAG,IAAI,CAAC,SAAS,GAAG,GAAG,EAAE,EAAE,OAAO,CAAC,CAAC;QACpD,CAAC;QAED,MAAM,QAAQ,CAAC,IAAI,EAAE,CAAC;IACxB,CAAC;IAEM,KAAK,CAAC,2BAA2B,CAAC,SAAqB;QAC5D,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,IAAI,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;QAE/D,OAAO,IAAI,CAAC,KAAK,CAAC,MAAM,CAAC,GAAG,IAAI,CAAC,CAAC;IACpC,CAAC;IAEM,KAAK,CAAC,eAAe,CAAC,SAAqB;QAChD,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,IAAI,CAAC,SAAS,GAAG,GAAG,EAAE,CAAC,CAAC;QAE/D,OAAO,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACjC,CAAC;IAEM,KAAK,CAAC,iBAAiB;QAC5B,MAAM,IAAI,GAAG,MAAM,IAAI,CAAC,OAAO,EAAE,CAAC;QAElC,IAAI,IAAI,CAAC,MAAM,KAAK,CAAC,EAAE,CAAC;YACtB,OAAO,CAAC,CAAC;QACX,CAAC;QAED,OAAO,MAAM,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,GAAG,IAAI,CAAC,CAAC;IACvC,CAAC;IAED;;;;;OAKG;IACK,KAAK,CAAC,OAAO;QACnB,OAAO,IAAI,OAAO,CAAC,CAAC,OAAO,EAAE,MAAM,EAAE,EAAE;YACrC,MAAM,IAAI,GAAa,EAAE,CAAC;YAE1B,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CAAC,UAAU,CAAC;gBACnC,KAAK,EAAE,GAAG,IAAI,CAAC,SAAS,GAAG;gBAC3B,KAAK,EAAE,IAAI;aACZ,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,MAAM,EAAE,CAAC,UAAoB,EAAE,EAAE;gBACzC,IAAI,CAAC,IAAI,CAAC,GAAG,UAAU,CAAC,CAAC;YAC3B,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,KAAK,EAAE,GAAG,EAAE;gBACpB,OAAO,CAAC,IAAI,CAAC,CAAC;YAChB,CAAC,CAAC,CAAC;YAEH,MAAM,CAAC,EAAE,CAAC,OAAO,EAAE,CAAC,GAAG,EAAE,EAAE;gBACzB,MAAM,CAAC,GAAG,CAAC,CAAC;YACd,CAAC,CAAC,CAAC;QACL,CAAC,CAAC,CAAC;IACL,CAAC;CACF"}
|
|
@@ -23,7 +23,10 @@ export type CacheTagsInvalidateWebhook = {
|
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
25
|
};
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Configuration object for creating a `CacheTagsProvider` implementation.
|
|
28
|
+
*/
|
|
29
|
+
export interface CacheTagsProvider {
|
|
27
30
|
/**
|
|
28
31
|
* Stores the cache tags of a query.
|
|
29
32
|
*
|
|
@@ -57,4 +60,4 @@ export type CacheTagsStore = {
|
|
|
57
60
|
* ⚠️ **Warning**: This will delete all cache tag data. Use with caution!
|
|
58
61
|
*/
|
|
59
62
|
truncateCacheTags(): Promise<number>;
|
|
60
|
-
}
|
|
63
|
+
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cache/types.ts"],"names":[],"mappings":""}
|
|
1
|
+
{"version":3,"file":"types.js","sourceRoot":"","sources":["../../src/cache-tags/types.ts"],"names":[],"mappings":""}
|
|
@@ -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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@smartive/datocms-utils",
|
|
3
|
-
"version": "3.0.0-next.
|
|
3
|
+
"version": "3.0.0-next.10",
|
|
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",
|
|
@@ -9,9 +9,21 @@
|
|
|
9
9
|
"types": "./dist/index.d.ts",
|
|
10
10
|
"import": "./dist/index.js"
|
|
11
11
|
},
|
|
12
|
-
"./cache": {
|
|
13
|
-
"types": "./dist/cache/index.d.ts",
|
|
14
|
-
"import": "./dist/cache/index.js"
|
|
12
|
+
"./cache-tags": {
|
|
13
|
+
"types": "./dist/cache-tags/index.d.ts",
|
|
14
|
+
"import": "./dist/cache-tags/index.js"
|
|
15
|
+
},
|
|
16
|
+
"./cache-tags/redis": {
|
|
17
|
+
"types": "./dist/cache-tags/provider/redis.d.ts",
|
|
18
|
+
"import": "./dist/cache-tags/provider/redis.js"
|
|
19
|
+
},
|
|
20
|
+
"./cache-tags/neon": {
|
|
21
|
+
"types": "./dist/cache-tags/provider/neon.d.ts",
|
|
22
|
+
"import": "./dist/cache-tags/provider/neon.js"
|
|
23
|
+
},
|
|
24
|
+
"./cache-tags/noop": {
|
|
25
|
+
"types": "./dist/cache-tags/provider/noop.d.ts",
|
|
26
|
+
"import": "./dist/cache-tags/provider/noop.js"
|
|
15
27
|
}
|
|
16
28
|
},
|
|
17
29
|
"files": [
|
|
@@ -38,10 +50,10 @@
|
|
|
38
50
|
"type": "git"
|
|
39
51
|
},
|
|
40
52
|
"devDependencies": {
|
|
53
|
+
"@neondatabase/serverless": "1.0.2",
|
|
41
54
|
"@smartive/eslint-config": "7.0.1",
|
|
42
55
|
"@smartive/prettier-config": "3.1.2",
|
|
43
|
-
"@types/node": "24.10.
|
|
44
|
-
"@vercel/postgres": "0.10.0",
|
|
56
|
+
"@types/node": "24.10.13",
|
|
45
57
|
"eslint": "9.39.2",
|
|
46
58
|
"eslint-import-resolver-typescript": "4.4.4",
|
|
47
59
|
"graphql": "16.12.0",
|
|
@@ -51,12 +63,12 @@
|
|
|
51
63
|
"typescript": "5.9.3"
|
|
52
64
|
},
|
|
53
65
|
"peerDependencies": {
|
|
54
|
-
"@
|
|
66
|
+
"@neondatabase/serverless": "^1.0.0",
|
|
55
67
|
"graphql": "^15.0.0 || ^16.0.0",
|
|
56
68
|
"ioredis": "^5.4.0"
|
|
57
69
|
},
|
|
58
70
|
"peerDependenciesMeta": {
|
|
59
|
-
"@
|
|
71
|
+
"@neondatabase/serverless": {
|
|
60
72
|
"optional": true
|
|
61
73
|
},
|
|
62
74
|
"graphql": {
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { neon } from '@neondatabase/serverless';
|
|
2
|
+
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
3
|
+
|
|
4
|
+
type NeonCacheTagsProviderConfig = {
|
|
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
|
+
* A `CacheTagsProvider` implementation that uses Neon as the storage backend.
|
|
26
|
+
*/
|
|
27
|
+
export class NeonCacheTagsProvider implements CacheTagsProvider {
|
|
28
|
+
private readonly sql;
|
|
29
|
+
private readonly table;
|
|
30
|
+
|
|
31
|
+
constructor({ connectionUrl, table }: NeonCacheTagsProviderConfig) {
|
|
32
|
+
this.sql = neon(connectionUrl, { fullResults: true });
|
|
33
|
+
this.table = table;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) {
|
|
37
|
+
if (!cacheTags?.length) {
|
|
38
|
+
return;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]);
|
|
42
|
+
const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(',');
|
|
43
|
+
|
|
44
|
+
await this.sql.query(`INSERT INTO ${this.table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public async queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]> {
|
|
48
|
+
if (!cacheTags?.length) {
|
|
49
|
+
return [];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
53
|
+
|
|
54
|
+
const { rows } = await this.sql.query(
|
|
55
|
+
`SELECT DISTINCT query_id FROM ${this.table} WHERE cache_tag IN (${placeholders})`,
|
|
56
|
+
cacheTags,
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
return rows.reduce<string[]>((queryIds, row) => {
|
|
60
|
+
if (typeof row.query_id === 'string') {
|
|
61
|
+
queryIds.push(row.query_id);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
return queryIds;
|
|
65
|
+
}, []);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
public async deleteCacheTags(cacheTags: CacheTag[]) {
|
|
69
|
+
if (cacheTags.length === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
73
|
+
|
|
74
|
+
return (await this.sql.query(`DELETE FROM ${this.table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
public async truncateCacheTags() {
|
|
78
|
+
return (await this.sql.query(`DELETE FROM ${this.table}`)).rowCount ?? 0;
|
|
79
|
+
}
|
|
80
|
+
}
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* A `CacheTagsProvider` implementation that does not perform any actual storage operations.
|
|
5
|
+
*
|
|
6
|
+
* _Note: This implementation is useful for testing purposes or when you want to disable caching without changing the code that interacts with the cache._
|
|
7
|
+
*/
|
|
8
|
+
export class NoopCacheTagsProvider implements CacheTagsProvider {
|
|
9
|
+
public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) {
|
|
10
|
+
console.debug('-- storeQueryCacheTags called', { queryId, cacheTags });
|
|
11
|
+
|
|
12
|
+
return Promise.resolve();
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
public async queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]> {
|
|
16
|
+
console.debug('-- queriesReferencingCacheTags called', { cacheTags });
|
|
17
|
+
|
|
18
|
+
return Promise.resolve([]);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
public async deleteCacheTags(cacheTags: CacheTag[]) {
|
|
22
|
+
console.debug('-- deleteCacheTags called', { cacheTags });
|
|
23
|
+
|
|
24
|
+
return Promise.resolve(0);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
public async truncateCacheTags() {
|
|
28
|
+
console.debug('-- truncateCacheTags called');
|
|
29
|
+
|
|
30
|
+
return Promise.resolve(0);
|
|
31
|
+
}
|
|
32
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { Redis } from 'ioredis';
|
|
2
|
+
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
3
|
+
|
|
4
|
+
type RedisCacheTagsProviderConfig = {
|
|
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
|
+
* A `CacheTagsProvider` implementation that uses Redis as the storage backend.
|
|
19
|
+
*/
|
|
20
|
+
export class RedisCacheTagsProvider implements CacheTagsProvider {
|
|
21
|
+
private readonly redis;
|
|
22
|
+
private readonly keyPrefix;
|
|
23
|
+
|
|
24
|
+
constructor({ connectionUrl, keyPrefix }: RedisCacheTagsProviderConfig) {
|
|
25
|
+
this.redis = new Redis(connectionUrl, {
|
|
26
|
+
maxRetriesPerRequest: 3,
|
|
27
|
+
lazyConnect: true,
|
|
28
|
+
});
|
|
29
|
+
this.keyPrefix = keyPrefix ?? '';
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) {
|
|
33
|
+
if (!cacheTags?.length) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const pipeline = this.redis.pipeline();
|
|
38
|
+
|
|
39
|
+
for (const tag of cacheTags) {
|
|
40
|
+
pipeline.sadd(`${this.keyPrefix}${tag}`, queryId);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
await pipeline.exec();
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public async queriesReferencingCacheTags(cacheTags: CacheTag[]) {
|
|
47
|
+
if (!cacheTags?.length) {
|
|
48
|
+
return [];
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const keys = cacheTags.map((tag) => `${this.keyPrefix}${tag}`);
|
|
52
|
+
|
|
53
|
+
return this.redis.sunion(...keys);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
public async deleteCacheTags(cacheTags: CacheTag[]) {
|
|
57
|
+
if (!cacheTags?.length) {
|
|
58
|
+
return 0;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const keys = cacheTags.map((tag) => `${this.keyPrefix}${tag}`);
|
|
62
|
+
|
|
63
|
+
return this.redis.del(...keys);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
public async truncateCacheTags() {
|
|
67
|
+
const keys = await this.getKeys();
|
|
68
|
+
|
|
69
|
+
if (keys.length === 0) {
|
|
70
|
+
return 0;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return await this.redis.del(...keys);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Retrieves all keys matching the given pattern using the Redis SCAN command.
|
|
78
|
+
* This method is more efficient than using the KEYS command, especially for large datasets.
|
|
79
|
+
*
|
|
80
|
+
* @returns An array of matching keys
|
|
81
|
+
*/
|
|
82
|
+
private async getKeys(): Promise<string[]> {
|
|
83
|
+
return new Promise((resolve, reject) => {
|
|
84
|
+
const keys: string[] = [];
|
|
85
|
+
|
|
86
|
+
const stream = this.redis.scanStream({
|
|
87
|
+
match: `${this.keyPrefix}*`,
|
|
88
|
+
count: 1000,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
stream.on('data', (resultKeys: string[]) => {
|
|
92
|
+
keys.push(...resultKeys);
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
stream.on('end', () => {
|
|
96
|
+
resolve(keys);
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
stream.on('error', (err) => {
|
|
100
|
+
reject(err);
|
|
101
|
+
});
|
|
102
|
+
});
|
|
103
|
+
}
|
|
104
|
+
}
|
|
@@ -23,7 +23,10 @@ export type CacheTagsInvalidateWebhook = {
|
|
|
23
23
|
};
|
|
24
24
|
};
|
|
25
25
|
|
|
26
|
-
|
|
26
|
+
/**
|
|
27
|
+
* Configuration object for creating a `CacheTagsProvider` implementation.
|
|
28
|
+
*/
|
|
29
|
+
export interface CacheTagsProvider {
|
|
27
30
|
/**
|
|
28
31
|
* Stores the cache tags of a query.
|
|
29
32
|
*
|
|
@@ -60,4 +63,4 @@ export type CacheTagsStore = {
|
|
|
60
63
|
* ⚠️ **Warning**: This will delete all cache tag data. Use with caution!
|
|
61
64
|
*/
|
|
62
65
|
truncateCacheTags(): Promise<number>;
|
|
63
|
-
}
|
|
66
|
+
}
|
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:
|
|
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(' ');
|
package/dist/cache/index.d.ts
DELETED
package/dist/cache/index.js
DELETED
package/dist/cache/index.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/cache/index.ts"],"names":[],"mappings":"AAAA,cAAc,qBAAqB,CAAC;AACpC,cAAc,kBAAkB,CAAC;AACjC,cAAc,YAAY,CAAC;AAC3B,cAAc,YAAY,CAAC"}
|
|
@@ -1,34 +0,0 @@
|
|
|
1
|
-
import { sql } from '@vercel/postgres';
|
|
2
|
-
export const createPostgresCacheTagsStore = ({ table }) => {
|
|
3
|
-
const storeQueryCacheTags = async (queryId, cacheTags) => {
|
|
4
|
-
if (!cacheTags?.length) {
|
|
5
|
-
return;
|
|
6
|
-
}
|
|
7
|
-
const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]);
|
|
8
|
-
const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(',');
|
|
9
|
-
await sql.query(`INSERT INTO ${table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags);
|
|
10
|
-
};
|
|
11
|
-
const queriesReferencingCacheTags = async (cacheTags) => {
|
|
12
|
-
if (!cacheTags?.length) {
|
|
13
|
-
return [];
|
|
14
|
-
}
|
|
15
|
-
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
16
|
-
const { rows } = await sql.query(`SELECT DISTINCT query_id FROM ${table} WHERE cache_tag IN (${placeholders})`, cacheTags);
|
|
17
|
-
return rows.map((row) => row.query_id);
|
|
18
|
-
};
|
|
19
|
-
const deleteCacheTags = async (cacheTags) => {
|
|
20
|
-
if (cacheTags.length === 0) {
|
|
21
|
-
return 0;
|
|
22
|
-
}
|
|
23
|
-
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
24
|
-
return (await sql.query(`DELETE FROM ${table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0;
|
|
25
|
-
};
|
|
26
|
-
const truncateCacheTags = async () => (await sql.query(`DELETE FROM ${table}`)).rowCount ?? 0;
|
|
27
|
-
return {
|
|
28
|
-
storeQueryCacheTags,
|
|
29
|
-
queriesReferencingCacheTags,
|
|
30
|
-
deleteCacheTags,
|
|
31
|
-
truncateCacheTags,
|
|
32
|
-
};
|
|
33
|
-
};
|
|
34
|
-
//# sourceMappingURL=postgres.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"postgres.js","sourceRoot":"","sources":["../../../src/cache/store/postgres.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,GAAG,EAAE,MAAM,kBAAkB,CAAC;AAGvC,MAAM,CAAC,MAAM,4BAA4B,GAAG,CAAC,EAAE,KAAK,EAAqB,EAAkB,EAAE;IAC3F,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,GAAqC,MAAM,GAAG,CAAC,KAAK,CAChE,iCAAiC,KAAK,wBAAwB,YAAY,GAAG,EAC7E,SAAS,CACV,CAAC;QAEF,OAAO,IAAI,CAAC,GAAG,CAAC,CAAC,GAAG,EAAE,EAAE,CAAC,GAAG,CAAC,QAAQ,CAAC,CAAC;IACzC,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,46 +0,0 @@
|
|
|
1
|
-
import { Redis } from 'ioredis';
|
|
2
|
-
export const createRedisCacheTagsStore = ({ url, keyPrefix = '' }) => {
|
|
3
|
-
const redis = new Redis(url, {
|
|
4
|
-
maxRetriesPerRequest: 3,
|
|
5
|
-
lazyConnect: true,
|
|
6
|
-
});
|
|
7
|
-
const storeQueryCacheTags = async (queryId, cacheTags) => {
|
|
8
|
-
if (!cacheTags?.length) {
|
|
9
|
-
return;
|
|
10
|
-
}
|
|
11
|
-
const pipeline = redis.pipeline();
|
|
12
|
-
for (const tag of cacheTags) {
|
|
13
|
-
pipeline.sadd(`${keyPrefix}${tag}`, queryId);
|
|
14
|
-
}
|
|
15
|
-
await pipeline.exec();
|
|
16
|
-
};
|
|
17
|
-
const queriesReferencingCacheTags = async (cacheTags) => {
|
|
18
|
-
if (!cacheTags?.length) {
|
|
19
|
-
return [];
|
|
20
|
-
}
|
|
21
|
-
const keys = cacheTags.map((tag) => `${keyPrefix}${tag}`);
|
|
22
|
-
return redis.sunion(...keys);
|
|
23
|
-
};
|
|
24
|
-
const deleteCacheTags = async (cacheTags) => {
|
|
25
|
-
if (!cacheTags?.length) {
|
|
26
|
-
return 0;
|
|
27
|
-
}
|
|
28
|
-
const keys = cacheTags.map((tag) => `${keyPrefix}${tag}`);
|
|
29
|
-
return redis.del(...keys);
|
|
30
|
-
};
|
|
31
|
-
const truncateCacheTags = async () => {
|
|
32
|
-
const pattern = `${keyPrefix}*`;
|
|
33
|
-
const keys = await redis.keys(pattern);
|
|
34
|
-
if (keys.length === 0) {
|
|
35
|
-
return 0;
|
|
36
|
-
}
|
|
37
|
-
return await redis.del(...keys);
|
|
38
|
-
};
|
|
39
|
-
return {
|
|
40
|
-
storeQueryCacheTags,
|
|
41
|
-
queriesReferencingCacheTags,
|
|
42
|
-
deleteCacheTags,
|
|
43
|
-
truncateCacheTags,
|
|
44
|
-
};
|
|
45
|
-
};
|
|
46
|
-
//# sourceMappingURL=redis.js.map
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../../src/cache/store/redis.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,KAAK,EAAE,MAAM,SAAS,CAAC;AAGhC,MAAM,CAAC,MAAM,yBAAyB,GAAG,CAAC,EAAE,GAAG,EAAE,SAAS,GAAG,EAAE,EAAuC,EAAkB,EAAE;IACxH,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"}
|
package/dist/cache/utils.js.map
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
{"version":3,"file":"utils.js","sourceRoot":"","sources":["../../src/cache/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/src/cache/index.ts
DELETED
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
import { sql } from '@vercel/postgres';
|
|
2
|
-
import { type CacheTag, type CacheTagsStore } from '../types.js';
|
|
3
|
-
|
|
4
|
-
export const createPostgresCacheTagsStore = ({ table }: { table: string }): CacheTagsStore => {
|
|
5
|
-
const storeQueryCacheTags = async (queryId: string, cacheTags: CacheTag[]) => {
|
|
6
|
-
if (!cacheTags?.length) {
|
|
7
|
-
return;
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
const tags = cacheTags.flatMap((_, i) => [queryId, cacheTags[i]]);
|
|
11
|
-
const placeholders = cacheTags.map((_, i) => `($${2 * i + 1}, $${2 * i + 2})`).join(',');
|
|
12
|
-
|
|
13
|
-
await sql.query(`INSERT INTO ${table} VALUES ${placeholders} ON CONFLICT DO NOTHING`, tags);
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
const queriesReferencingCacheTags = async (cacheTags: CacheTag[]): Promise<string[]> => {
|
|
17
|
-
if (!cacheTags?.length) {
|
|
18
|
-
return [];
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
22
|
-
|
|
23
|
-
const { rows }: { rows: { query_id: string }[] } = await sql.query(
|
|
24
|
-
`SELECT DISTINCT query_id FROM ${table} WHERE cache_tag IN (${placeholders})`,
|
|
25
|
-
cacheTags,
|
|
26
|
-
);
|
|
27
|
-
|
|
28
|
-
return rows.map((row) => row.query_id);
|
|
29
|
-
};
|
|
30
|
-
|
|
31
|
-
const deleteCacheTags = async (cacheTags: CacheTag[]) => {
|
|
32
|
-
if (cacheTags.length === 0) {
|
|
33
|
-
return 0;
|
|
34
|
-
}
|
|
35
|
-
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
36
|
-
|
|
37
|
-
return (await sql.query(`DELETE FROM ${table} WHERE cache_tag IN (${placeholders})`, cacheTags)).rowCount ?? 0;
|
|
38
|
-
};
|
|
39
|
-
|
|
40
|
-
const truncateCacheTags = async () => (await sql.query(`DELETE FROM ${table}`)).rowCount ?? 0;
|
|
41
|
-
|
|
42
|
-
return {
|
|
43
|
-
storeQueryCacheTags,
|
|
44
|
-
queriesReferencingCacheTags,
|
|
45
|
-
deleteCacheTags,
|
|
46
|
-
truncateCacheTags,
|
|
47
|
-
};
|
|
48
|
-
};
|
package/src/cache/store/redis.ts
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
import { Redis } from 'ioredis';
|
|
2
|
-
import { type CacheTag, type CacheTagsStore } from '../types.js';
|
|
3
|
-
|
|
4
|
-
export const createRedisCacheTagsStore = ({ url, keyPrefix = '' }: { url: string; keyPrefix?: string }): CacheTagsStore => {
|
|
5
|
-
const redis = new Redis(url, {
|
|
6
|
-
maxRetriesPerRequest: 3,
|
|
7
|
-
lazyConnect: true,
|
|
8
|
-
});
|
|
9
|
-
|
|
10
|
-
const storeQueryCacheTags = async (queryId: string, cacheTags: CacheTag[]) => {
|
|
11
|
-
if (!cacheTags?.length) {
|
|
12
|
-
return;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
const pipeline = redis.pipeline();
|
|
16
|
-
|
|
17
|
-
for (const tag of cacheTags) {
|
|
18
|
-
pipeline.sadd(`${keyPrefix}${tag}`, queryId);
|
|
19
|
-
}
|
|
20
|
-
|
|
21
|
-
await pipeline.exec();
|
|
22
|
-
};
|
|
23
|
-
|
|
24
|
-
const queriesReferencingCacheTags = async (cacheTags: CacheTag[]) => {
|
|
25
|
-
if (!cacheTags?.length) {
|
|
26
|
-
return [];
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
const keys = cacheTags.map((tag) => `${keyPrefix}${tag}`);
|
|
30
|
-
|
|
31
|
-
return redis.sunion(...keys);
|
|
32
|
-
};
|
|
33
|
-
|
|
34
|
-
const deleteCacheTags = async (cacheTags: CacheTag[]) => {
|
|
35
|
-
if (!cacheTags?.length) {
|
|
36
|
-
return 0;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
const keys = cacheTags.map((tag) => `${keyPrefix}${tag}`);
|
|
40
|
-
|
|
41
|
-
return redis.del(...keys);
|
|
42
|
-
};
|
|
43
|
-
|
|
44
|
-
const truncateCacheTags = async () => {
|
|
45
|
-
const pattern = `${keyPrefix}*`;
|
|
46
|
-
const keys = await redis.keys(pattern);
|
|
47
|
-
|
|
48
|
-
if (keys.length === 0) {
|
|
49
|
-
return 0;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
|
-
return await redis.del(...keys);
|
|
53
|
-
};
|
|
54
|
-
|
|
55
|
-
return {
|
|
56
|
-
storeQueryCacheTags,
|
|
57
|
-
queriesReferencingCacheTags,
|
|
58
|
-
deleteCacheTags,
|
|
59
|
-
truncateCacheTags,
|
|
60
|
-
};
|
|
61
|
-
};
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|