Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
94901b1
chore: add support for pre-releases
mfeltscher Feb 12, 2026
098a08b
chore: run pipeline on branch
mfeltscher Feb 12, 2026
adbabf8
chore: also install conventional-changelog-conventionalcommits
mfeltscher Feb 12, 2026
719861b
feat!: refactor cache implementation to support multiple backends and…
mfeltscher Feb 13, 2026
1297d85
fix: use separate exports per provider
mfeltscher Feb 13, 2026
e1e9736
feat: replace @vercel/postgres by @neondatabase/serverless
mfeltscher Feb 13, 2026
a734685
fix: add missing neon database implementation
mfeltscher Feb 13, 2026
fae7ee7
docs: add more type information
mfeltscher Feb 13, 2026
39b9d7b
docs: adjust README
mfeltscher Feb 13, 2026
ba83ca6
chore: rename connection strings to `connectionUrl`
mfeltscher Feb 13, 2026
d9c67a1
feat: enhance classNames function to support more use cases
mfeltscher Feb 13, 2026
5f5e895
feat: add Noop cache provider for testing purposes
mfeltscher Feb 13, 2026
b84d169
feat: switch to classes
mfeltscher Feb 15, 2026
ceb90de
feat: Rename everything and simplify naming
mfeltscher Feb 15, 2026
60f3b97
fix: remove Dato prefix from CacheTags objects
mfeltscher Feb 15, 2026
b1db286
Update README.md
mfeltscher Feb 15, 2026
c4e03fd
Merge branch 'main' into next
mfeltscher Feb 15, 2026
071d9f1
chore: adjust README
mfeltscher Feb 15, 2026
06dca13
fix: switch to SCAN instead of KEYS for Redis
mfeltscher Feb 15, 2026
b752d54
Apply suggestions from code review
mfeltscher Feb 15, 2026
7cfb647
fix: SQL injection vulnerability in Neon cache provider table names (…
Copilot Feb 16, 2026
c362c39
cleanup: Docs and exports
mfeltscher Feb 16, 2026
524270e
Apply suggestions from code review
mfeltscher Feb 16, 2026
edaf308
cleanup: adjust docs
mfeltscher Feb 16, 2026
da599ea
fix: error handling in Redis
mfeltscher Feb 16, 2026
3e1c210
feat: make error handling of cache tags providers configurable (#217)
mfeltscher Feb 16, 2026
5dc9692
fix: error level and documentation
mfeltscher Feb 16, 2026
c8b2f66
Merge branch 'main' into next
mfeltscher Feb 16, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ on:
push:
branches:
- main
- next

permissions:
contents: write # to be able to publish a GitHub release
Expand All @@ -29,5 +30,6 @@ jobs:
semantic_version: 25
extra_plugins: |
@semantic-release/changelog@6
conventional-changelog-conventionalcommits@9
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
22 changes: 19 additions & 3 deletions .releaserc.json
Original file line number Diff line number Diff line change
@@ -1,8 +1,24 @@
{
"branches": ["main"],
"branches": [
"main",
{
"name": "next",
"prerelease": true
}
],
"plugins": [
"@semantic-release/commit-analyzer",
"@semantic-release/release-notes-generator",
[
"@semantic-release/commit-analyzer",
{
"preset": "conventionalcommits"
}
],
[
"@semantic-release/release-notes-generator",
{
"preset": "conventionalcommits"
}
],
"@semantic-release/changelog",
"@semantic-release/npm",
"@semantic-release/github"
Expand Down
195 changes: 139 additions & 56 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,30 +1,67 @@
# smartive DatoCMS Utilities

A set of utilities and helpers to work with DatoCMS in a Next.js project.
A collection of utilities and helpers for working with DatoCMS in Next.js projects.

## Installation

```bash
npm install @smartive/datocms-utils
```

## Usage
## Utilities

Import and use the utilities you need in your project. The following utilities are available.
### General Utilities

## Utilities
#### `classNames`

Cleans and joins an array of class names (strings and numbers), filtering out undefined and boolean values.

```typescript
import { classNames } from '@smartive/datocms-utils';

const className = classNames('btn', isActive && 'btn-active', 42, undefined, 'btn-primary');
// Result: "btn btn-active 42 btn-primary"
```

#### `getTelLink`

Converts a phone number into a `tel:` link by removing non-digit characters (except `+` for international numbers).

```typescript
import { getTelLink } from '@smartive/datocms-utils';

const link = getTelLink('+1 (555) 123-4567');
// Result: "tel:+15551234567"
```

### DatoCMS Cache Tags

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.

#### Core Utilities

```typescript
import { generateQueryId, parseXCacheTagsResponseHeader } from '@smartive/datocms-utils/cache-tags';

// Generate a unique ID for a GraphQL query
const queryId = generateQueryId(document, variables);

// Parse DatoCMS's X-Cache-Tags header
const tags = parseXCacheTagsResponseHeader('tag-a tag-2 other-tag');
// Result: ['tag-a', 'tag-2', 'other-tag']
```

### Utilities for DatoCMS Cache Tags
#### Storage Providers

The following utilities are used to work with [DatoCMS cache tags](https://www.datocms.com/docs/content-delivery-api/cache-tags) and a [Vercel Postgres database](https://vercel.com/docs/storage/vercel-postgres).
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.

- `storeQueryCacheTags`: Stores the cache tags of a query in the database.
- `queriesReferencingCacheTags`: Retrieves the queries that reference cache tags.
- `deleteQueries`: Deletes the cache tags of a query from the database.
##### Neon (Postgres) Provider

#### Setup Postgres database
Use Neon serverless Postgres to store cache tag mappings.

In order for the above utilites to work, you need to setup a the following database. You can use the following SQL script to do that:
**Setup:**

1. Create the cache tags table:

```sql
CREATE TABLE IF NOT EXISTS query_cache_tags (
Expand All @@ -34,75 +71,121 @@ CREATE TABLE IF NOT EXISTS query_cache_tags (
);
```

### Utilities for DatoCMS Cache Tags (Redis)

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.
2. Install [@neondatabase/serverless](https://github.com/neondatabase/serverless)

- `redis.storeQueryCacheTags`: Stores the cache tags of a query in Redis.
- `redis.queriesReferencingCacheTags`: Retrieves the queries that reference cache tags.
- `redis.deleteCacheTags`: Deletes cache tags from Redis.
- `redis.truncateCacheTags`: Wipes out all cache tags from Redis.
```bash
npm install @neondatabase/serverless
```

The Redis connection is automatically initialized on first use using the `REDIS_URL` environment variable.
3. Create and use the store:

#### Environment Variables
```typescript
import { NeonCacheTagsProvider } from '@smartive/datocms-utils/cache-tags/neon';

Add your Redis connection URL to your `.env.local` file:
const provider = new NeonCacheTagsProvider({
connectionUrl: process.env.DATABASE_URL!,
table: 'query_cache_tags',
throwOnError: false, // Optional: Disable error throwing, defaults to `true`
onError(error, ctx) { // Optional: Custom error callback
console.error('CacheTagsProvider error', { error, context: ctx });
},
});

```bash
# Required: Redis connection URL
# For Upstash Redis
REDIS_URL=rediss://default:your-token@your-endpoint.upstash.io:6379
// Store cache tags for a query
await provider.storeQueryCacheTags(queryId, ['item:42', 'product']);

# For Redis Cloud or other providers
REDIS_URL=redis://username:password@your-redis-host:6379
// Find queries that reference specific tags
const queries = await provider.queriesReferencingCacheTags(['item:42']);

# For local development
REDIS_URL=redis://localhost:6379
// Delete specific cache tags
await provider.deleteCacheTags(['item:42']);

# Optional: Key prefix for separating production/preview environments
# Useful when using the same Redis instance for multiple environments
REDIS_KEY_PREFIX=prod # For production
REDIS_KEY_PREFIX=preview # For preview/staging
# Leave empty for development (no prefix)
// Clear all cache tags
await provider.truncateCacheTags();
```

**Note**: Similar to how the Postgres version uses different table names, use `REDIS_KEY_PREFIX` to separate data between environments when using the same Redis instance.
##### Redis Provider

Use Redis to store cache tag mappings with better performance for high-traffic applications.

#### Usage Example
**Setup:**

1. Install [ioredis](https://github.com/redis/ioredis)

```bash
npm install ioredis
```

2. Create and use the provider:

```typescript
// Recommended: Use namespaces for clarity
import { generateQueryId, redis } from '@smartive/datocms-utils';
import { RedisCacheTagsProvider } from '@smartive/datocms-utils/cache-tags/redis';

const provider = new RedisCacheTagsProvider({
connectionUrl: process.env.REDIS_URL!,
keyPrefix: 'prod:', // Optional: namespace for multi-environment setups
throwOnError: process.env.NODE_ENV === 'development', // Optional: Disable error throwing in production - defaults to `true`
});

// Same API as Neon provider
await provider.storeQueryCacheTags(queryId, ['item:42', 'product']);
const queries = await provider.queriesReferencingCacheTags(['item:42']);
await provider.deleteCacheTags(['item:42']);
await provider.truncateCacheTags();
```

const queryId = generateQueryId(query, variables);
**Redis connection string examples:**

// Store cache tags for a query
await redis.storeQueryCacheTags(queryId, ['item:42', 'product', 'category:5']);
```bash
# Upstash Redis
REDIS_URL=rediss://default:token@endpoint.upstash.io:6379

// Find all queries that reference specific tags
const affectedQueries = await redis.queriesReferencingCacheTags(['item:42']);
# Redis Cloud
REDIS_URL=redis://username:password@redis-host:6379

// Delete cache tags (keys will be recreated on next query)
await redis.deleteCacheTags(['item:42']);
# Local development
REDIS_URL=redis://localhost:6379
```

#### Redis Data Structure
#### `CacheTagsProvider` Interface

Both providers implement:

The Redis implementation uses Sets to track query-to-tag relationships:
- `storeQueryCacheTags(queryId: string, cacheTags: CacheTag[])`: Store cache tags for a query
- `queriesReferencingCacheTags(cacheTags: CacheTag[])`: Get query IDs that reference any of the specified tags
- `deleteCacheTags(cacheTags: CacheTag[])`: Delete specific cache tags
- `truncateCacheTags()`: Wipe all cache tags (use with caution)

- **Cache tag keys**: `{prefix}{tag}` → Set of query IDs
### Complete Example

Where `{prefix}` is the optional `REDIS_KEY_PREFIX` environment variable (e.g., `prod:`, `preview:`).
```typescript
import { generateQueryId, parseXCacheTagsResponseHeader } from '@smartive/datocms-utils/cache-tags';
import { RedisCacheTagsProvider } from '@smartive/datocms-utils/cache-tags/redis';

const provider = new RedisCacheTagsProvider({
connectionUrl: process.env.REDIS_URL!,
keyPrefix: 'myapp:',
});

// After making a DatoCMS query
const queryId = generateQueryId(document, variables);
const cacheTags = parseXCacheTagsResponseHeader(response.headers['x-cache-tags']);
await provider.storeQueryCacheTags(queryId, cacheTags);

// When handling DatoCMS webhook for cache invalidation
const affectedQueries = await provider.queriesReferencingCacheTags(webhook.entity.attributes.tags);
// Revalidate affected queries...
await provider.deleteCacheTags(webhook.entity.attributes.tags);
```

When cache tags are invalidated, their keys are deleted entirely. Fresh mappings are created when queries run again.
## TypeScript Types

### Other Utilities
The package includes TypeScript types for DatoCMS webhooks and cache tags:

- `classNames`: Cleans and joins an array of inputs with possible undefined or boolean values. Useful for tailwind classnames.
- `getTelLink`: Formats a phone number to a tel link.
- `CacheTag`: A branded type for cache tags, ensuring type safety
- `CacheTagsInvalidateWebhook`: Type definition for DatoCMS cache tag invalidation webhook payloads
- `CacheTagsProvider`: Interface for cache tag storage implementations

### Types
## License

- `CacheTag`: A branded type for cache tags.
- `CacheTagsInvalidateWebhook`: The payload of the DatoCMS cache tags invalidate webhook.
MIT © [smartive AG](https://github.com/smartive)
Loading