@smartive/datocms-utils 3.0.0-next.10 → 3.0.0-next.12
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 +6 -6
- package/dist/cache-tags/provider/neon.d.ts +8 -2
- package/dist/cache-tags/provider/neon.js +20 -2
- package/dist/cache-tags/provider/neon.js.map +1 -1
- package/dist/cache-tags/provider/redis.d.ts +1 -2
- package/dist/cache-tags/provider/redis.js +5 -1
- package/dist/cache-tags/provider/redis.js.map +1 -1
- package/dist/cache-tags/types.d.ts +1 -1
- package/package.json +1 -1
- package/src/cache-tags/provider/neon.ts +25 -3
- package/src/cache-tags/provider/redis.ts +6 -2
- package/src/cache-tags/types.ts +1 -1
package/README.md
CHANGED
|
@@ -14,13 +14,13 @@ npm install @smartive/datocms-utils
|
|
|
14
14
|
|
|
15
15
|
#### `classNames`
|
|
16
16
|
|
|
17
|
-
Cleans and joins an array of class names, filtering out undefined and boolean values.
|
|
17
|
+
Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.
|
|
18
18
|
|
|
19
19
|
```typescript
|
|
20
20
|
import { classNames } from '@smartive/datocms-utils';
|
|
21
21
|
|
|
22
|
-
const className = classNames('btn', isActive && 'btn-active', undefined, 'btn-primary');
|
|
23
|
-
// Result: "btn btn-active btn-primary"
|
|
22
|
+
const className = classNames('btn', isActive && 'btn-active', 42, undefined, 'btn-primary');
|
|
23
|
+
// Result: "btn btn-active 42 btn-primary"
|
|
24
24
|
```
|
|
25
25
|
|
|
26
26
|
#### `getTelLink`
|
|
@@ -53,7 +53,7 @@ const tags = parseXCacheTagsResponseHeader('tag-a tag-2 other-tag');
|
|
|
53
53
|
|
|
54
54
|
#### Storage Providers
|
|
55
55
|
|
|
56
|
-
The package provides
|
|
56
|
+
The package provides multiple storage backends for cache tags: **Neon (Postgres)**, **Redis**, and **Noop**. All implement the same `CacheTagsProvider` interface, with the Noop provider being especially useful for testing and development.
|
|
57
57
|
|
|
58
58
|
##### Neon (Postgres) Provider
|
|
59
59
|
|
|
@@ -118,7 +118,7 @@ npm install ioredis
|
|
|
118
118
|
import { RedisCacheTagsProvider } from '@smartive/datocms-utils/cache-tags/redis';
|
|
119
119
|
|
|
120
120
|
const provider = new RedisCacheTagsProvider({
|
|
121
|
-
|
|
121
|
+
connectionUrl: process.env.REDIS_URL!,
|
|
122
122
|
keyPrefix: 'prod:', // Optional: namespace for multi-environment setups
|
|
123
123
|
});
|
|
124
124
|
|
|
@@ -163,7 +163,7 @@ const provider = new RedisCacheTagsProvider({
|
|
|
163
163
|
});
|
|
164
164
|
|
|
165
165
|
// After making a DatoCMS query
|
|
166
|
-
const queryId = generateQueryId(
|
|
166
|
+
const queryId = generateQueryId(document, variables);
|
|
167
167
|
const cacheTags = parseXCacheTagsResponseHeader(response.headers['x-cache-tags']);
|
|
168
168
|
await provider.storeQueryCacheTags(queryId, cacheTags);
|
|
169
169
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
2
|
-
type NeonCacheTagsProviderConfig = {
|
|
2
|
+
export type NeonCacheTagsProviderConfig = {
|
|
3
3
|
/**
|
|
4
4
|
* Neon connection string. You can find it in the "Connection" tab of your Neon project dashboard.
|
|
5
5
|
* Has the format `postgresql://user:pass@host/db`
|
|
@@ -29,5 +29,11 @@ export declare class NeonCacheTagsProvider implements CacheTagsProvider {
|
|
|
29
29
|
queriesReferencingCacheTags(cacheTags: CacheTag[]): Promise<string[]>;
|
|
30
30
|
deleteCacheTags(cacheTags: CacheTag[]): Promise<number>;
|
|
31
31
|
truncateCacheTags(): Promise<number>;
|
|
32
|
+
/**
|
|
33
|
+
* Validates and quotes a PostgreSQL identifier (table name, column name, etc.) to prevent SQL injection.
|
|
34
|
+
* @param identifier The identifier to validate and quote
|
|
35
|
+
* @returns The properly quoted identifier
|
|
36
|
+
* @throws Error if the identifier is invalid
|
|
37
|
+
*/
|
|
38
|
+
private static quoteIdentifier;
|
|
32
39
|
}
|
|
33
|
-
export {};
|
|
@@ -7,7 +7,7 @@ export class NeonCacheTagsProvider {
|
|
|
7
7
|
table;
|
|
8
8
|
constructor({ connectionUrl, table }) {
|
|
9
9
|
this.sql = neon(connectionUrl, { fullResults: true });
|
|
10
|
-
this.table = table;
|
|
10
|
+
this.table = NeonCacheTagsProvider.quoteIdentifier(table);
|
|
11
11
|
}
|
|
12
12
|
async storeQueryCacheTags(queryId, cacheTags) {
|
|
13
13
|
if (!cacheTags?.length) {
|
|
@@ -31,7 +31,7 @@ export class NeonCacheTagsProvider {
|
|
|
31
31
|
}, []);
|
|
32
32
|
}
|
|
33
33
|
async deleteCacheTags(cacheTags) {
|
|
34
|
-
if (cacheTags
|
|
34
|
+
if (!cacheTags?.length) {
|
|
35
35
|
return 0;
|
|
36
36
|
}
|
|
37
37
|
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
@@ -40,5 +40,23 @@ export class NeonCacheTagsProvider {
|
|
|
40
40
|
async truncateCacheTags() {
|
|
41
41
|
return (await this.sql.query(`DELETE FROM ${this.table}`)).rowCount ?? 0;
|
|
42
42
|
}
|
|
43
|
+
/**
|
|
44
|
+
* Validates and quotes a PostgreSQL identifier (table name, column name, etc.) to prevent SQL injection.
|
|
45
|
+
* @param identifier The identifier to validate and quote
|
|
46
|
+
* @returns The properly quoted identifier
|
|
47
|
+
* @throws Error if the identifier is invalid
|
|
48
|
+
*/
|
|
49
|
+
static quoteIdentifier(identifier) {
|
|
50
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)?$/.test(identifier)) {
|
|
51
|
+
throw new Error(`Invalid table name: ${identifier}. Table names must start with a letter, underscore, or dollar sign and contain only letters, digits, underscores, and dollar signs. Schema-qualified names (e.g., "schema.table") are supported.`);
|
|
52
|
+
}
|
|
53
|
+
// Quote the identifier using double quotes to prevent SQL injection
|
|
54
|
+
// Handle schema-qualified names (e.g., "schema.table")
|
|
55
|
+
// Escape any double quotes within the identifier by doubling them
|
|
56
|
+
return identifier
|
|
57
|
+
.split('.')
|
|
58
|
+
.map((part) => `"${part.replace(/"/g, '""')}"`)
|
|
59
|
+
.join('.');
|
|
60
|
+
}
|
|
43
61
|
}
|
|
44
62
|
//# sourceMappingURL=neon.js.map
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"neon.js","sourceRoot":"","sources":["../../../src/cache-tags/provider/neon.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
2
|
-
type RedisCacheTagsProviderConfig = {
|
|
2
|
+
export type RedisCacheTagsProviderConfig = {
|
|
3
3
|
/**
|
|
4
4
|
* Redis connection string. For example, `redis://user:pass@host:port/db`.
|
|
5
5
|
*/
|
|
@@ -30,4 +30,3 @@ export declare class RedisCacheTagsProvider implements CacheTagsProvider {
|
|
|
30
30
|
*/
|
|
31
31
|
private getKeys;
|
|
32
32
|
}
|
|
33
|
-
export {};
|
|
@@ -20,7 +20,11 @@ export class RedisCacheTagsProvider {
|
|
|
20
20
|
for (const tag of cacheTags) {
|
|
21
21
|
pipeline.sadd(`${this.keyPrefix}${tag}`, queryId);
|
|
22
22
|
}
|
|
23
|
-
await pipeline.exec();
|
|
23
|
+
const results = await pipeline.exec();
|
|
24
|
+
const error = results?.find(([err]) => err)?.[0];
|
|
25
|
+
if (error) {
|
|
26
|
+
throw error;
|
|
27
|
+
}
|
|
24
28
|
}
|
|
25
29
|
async queriesReferencingCacheTags(cacheTags) {
|
|
26
30
|
if (!cacheTags?.length) {
|
|
@@ -1 +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;
|
|
1
|
+
{"version":3,"file":"redis.js","sourceRoot":"","sources":["../../../src/cache-tags/provider/redis.ts"],"names":[],"mappings":"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"}
|
|
@@ -50,7 +50,7 @@ export interface CacheTagsProvider {
|
|
|
50
50
|
* run again, fresh cache tag mappings will be created.
|
|
51
51
|
*
|
|
52
52
|
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
53
|
-
* @returns Number of keys deleted
|
|
53
|
+
* @returns Number of keys deleted
|
|
54
54
|
*
|
|
55
55
|
*/
|
|
56
56
|
deleteCacheTags(cacheTags: CacheTag[]): Promise<number>;
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { neon } from '@neondatabase/serverless';
|
|
2
2
|
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
3
3
|
|
|
4
|
-
type NeonCacheTagsProviderConfig = {
|
|
4
|
+
export type NeonCacheTagsProviderConfig = {
|
|
5
5
|
/**
|
|
6
6
|
* Neon connection string. You can find it in the "Connection" tab of your Neon project dashboard.
|
|
7
7
|
* Has the format `postgresql://user:pass@host/db`
|
|
@@ -30,7 +30,7 @@ export class NeonCacheTagsProvider implements CacheTagsProvider {
|
|
|
30
30
|
|
|
31
31
|
constructor({ connectionUrl, table }: NeonCacheTagsProviderConfig) {
|
|
32
32
|
this.sql = neon(connectionUrl, { fullResults: true });
|
|
33
|
-
this.table = table;
|
|
33
|
+
this.table = NeonCacheTagsProvider.quoteIdentifier(table);
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
public async storeQueryCacheTags(queryId: string, cacheTags: CacheTag[]) {
|
|
@@ -66,7 +66,7 @@ export class NeonCacheTagsProvider implements CacheTagsProvider {
|
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
public async deleteCacheTags(cacheTags: CacheTag[]) {
|
|
69
|
-
if (cacheTags
|
|
69
|
+
if (!cacheTags?.length) {
|
|
70
70
|
return 0;
|
|
71
71
|
}
|
|
72
72
|
const placeholders = cacheTags.map((_, i) => `$${i + 1}`).join(',');
|
|
@@ -77,4 +77,26 @@ export class NeonCacheTagsProvider implements CacheTagsProvider {
|
|
|
77
77
|
public async truncateCacheTags() {
|
|
78
78
|
return (await this.sql.query(`DELETE FROM ${this.table}`)).rowCount ?? 0;
|
|
79
79
|
}
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Validates and quotes a PostgreSQL identifier (table name, column name, etc.) to prevent SQL injection.
|
|
83
|
+
* @param identifier The identifier to validate and quote
|
|
84
|
+
* @returns The properly quoted identifier
|
|
85
|
+
* @throws Error if the identifier is invalid
|
|
86
|
+
*/
|
|
87
|
+
private static quoteIdentifier(identifier: string): string {
|
|
88
|
+
if (!/^[a-zA-Z_$][a-zA-Z0-9_$]*(\.[a-zA-Z_$][a-zA-Z0-9_$]*)?$/.test(identifier)) {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`Invalid table name: ${identifier}. Table names must start with a letter, underscore, or dollar sign and contain only letters, digits, underscores, and dollar signs. Schema-qualified names (e.g., "schema.table") are supported.`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Quote the identifier using double quotes to prevent SQL injection
|
|
95
|
+
// Handle schema-qualified names (e.g., "schema.table")
|
|
96
|
+
// Escape any double quotes within the identifier by doubling them
|
|
97
|
+
return identifier
|
|
98
|
+
.split('.')
|
|
99
|
+
.map((part) => `"${part.replace(/"/g, '""')}"`)
|
|
100
|
+
.join('.');
|
|
101
|
+
}
|
|
80
102
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Redis } from 'ioredis';
|
|
2
2
|
import { type CacheTag, type CacheTagsProvider } from '../types.js';
|
|
3
3
|
|
|
4
|
-
type RedisCacheTagsProviderConfig = {
|
|
4
|
+
export type RedisCacheTagsProviderConfig = {
|
|
5
5
|
/**
|
|
6
6
|
* Redis connection string. For example, `redis://user:pass@host:port/db`.
|
|
7
7
|
*/
|
|
@@ -40,7 +40,11 @@ export class RedisCacheTagsProvider implements CacheTagsProvider {
|
|
|
40
40
|
pipeline.sadd(`${this.keyPrefix}${tag}`, queryId);
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
-
await pipeline.exec();
|
|
43
|
+
const results = await pipeline.exec();
|
|
44
|
+
const error = results?.find(([err]) => err)?.[0];
|
|
45
|
+
if (error) {
|
|
46
|
+
throw error;
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
|
|
46
50
|
public async queriesReferencingCacheTags(cacheTags: CacheTag[]) {
|
package/src/cache-tags/types.ts
CHANGED
|
@@ -52,7 +52,7 @@ export interface CacheTagsProvider {
|
|
|
52
52
|
* run again, fresh cache tag mappings will be created.
|
|
53
53
|
*
|
|
54
54
|
* @param {CacheTag[]} cacheTags Array of cache tags to delete
|
|
55
|
-
* @returns Number of keys deleted
|
|
55
|
+
* @returns Number of keys deleted
|
|
56
56
|
*
|
|
57
57
|
*/
|
|
58
58
|
deleteCacheTags(cacheTags: CacheTag[]): Promise<number>;
|