@ai-growth/nextjs
Version:
Seamlessly integrate Sanity CMS with Next.js applications for automated blog routing and rendering
305 lines (304 loc) • 10.2 kB
JavaScript
import { createClient } from '@sanity/client';
import { getSanityClientConfig, isClient } from './config';
/**
* Cache for the Sanity client instance to avoid recreating it multiple times
*/
let sanityClientCache = null;
/**
* Creates a Sanity client instance using the configuration from environment variables.
*
* This function automatically configures the client based on the current environment:
* - Uses CDN in production when no token is available (read-only access)
* - Disables CDN when authenticated or in development (real-time data)
* - Handles both server-side and client-side environments
*
* @param options - Optional configuration overrides
* @param options.useCache - Whether to cache the client instance (default: true)
* @param options.configOverrides - Manual configuration overrides for testing
* @returns Configured Sanity client instance
*
* @example
* ```typescript
* // Basic usage - uses environment configuration
* const client = getSanityClient();
*
* // Disable caching for testing
* const client = getSanityClient({ useCache: false });
*
* // Manual configuration override
* const client = getSanityClient({
* configOverrides: { projectId: 'test-project' }
* });
* ```
*
* @throws {ConfigurationError} When environment configuration is invalid
*/
export function getSanityClient(options = {}) {
const { useCache = true, configOverrides } = options;
// Return cached client if available and caching is enabled
if (useCache && sanityClientCache) {
return sanityClientCache;
}
// Get configuration from environment (or use overrides for testing)
const config = configOverrides
? { ...getSanityClientConfig(), ...configOverrides }
: getSanityClientConfig();
// Create client configuration
const clientConfig = {
projectId: config.projectId,
dataset: config.dataset,
apiVersion: config.apiVersion,
useCdn: config.useCdn ?? true,
};
// Only include token if it's available
if (config.token) {
clientConfig.token = config.token;
}
// Create the client
const client = createClient(clientConfig);
// Cache the client if caching is enabled
if (useCache) {
sanityClientCache = client;
}
return client;
}
/**
* Clears the cached Sanity client instance.
*
* This is useful for testing scenarios where you need to ensure
* a fresh client is created with different configuration.
*
* @example
* ```typescript
* // Clear cache before testing with different config
* clearSanityClientCache();
* const client = getSanityClient();
* ```
*/
export function clearSanityClientCache() {
sanityClientCache = null;
}
/**
* Gets the current cached Sanity client instance without creating a new one.
*
* @returns The cached client instance or null if no client is cached
*
* @example
* ```typescript
* const cachedClient = getCachedSanityClient();
* if (cachedClient) {
* // Use existing client
* } else {
* // Create new client
* const client = getSanityClient();
* }
* ```
*/
export function getCachedSanityClient() {
return sanityClientCache;
}
/**
* Creates a new Sanity client instance without caching.
*
* This is useful when you need a fresh client instance,
* for example in testing or when working with multiple projects.
*
* @param configOverrides - Manual configuration overrides
* @returns New Sanity client instance
*
* @example
* ```typescript
* // Create fresh client for testing
* const testClient = createSanityClient({
* projectId: 'test-project',
* dataset: 'test'
* });
* ```
*/
export function createSanityClient(configOverrides) {
return getSanityClient({
useCache: false,
...(configOverrides && { configOverrides }),
});
}
/**
* Verifies the connection to Sanity API by performing a lightweight query.
*
* This function tests the API connection by attempting a minimal query
* to the Sanity Content Lake. It measures latency, handles timeouts,
* and categorizes different types of connection failures.
*
* @param options - Configuration options for connection verification
* @param options.timeout - Request timeout in milliseconds (default: 10000)
* @param options.retryAttempts - Number of retry attempts for transient failures (default: 1)
* @param options.client - Custom client instance to use for testing
* @returns Promise resolving to connection status information
*
* @example
* ```typescript
* // Basic connection verification
* const status = await verifyConnection();
* if (status.connected) {
* console.log(`Connected! Latency: ${status.latency}ms`);
* } else {
* console.error(`Connection failed: ${status.error}`);
* }
*
* // Custom timeout and retry
* const status = await verifyConnection({
* timeout: 5000,
* retryAttempts: 2
* });
* ```
*/
export async function verifyConnection(options = {}) {
const { timeout = 10000, retryAttempts = 1, client: providedClient } = options;
const startTime = Date.now();
const timestamp = new Date().toISOString();
const environment = isClient() ? 'client' : 'server';
let attempt = 0;
let lastError = null;
while (attempt <= retryAttempts) {
try {
// Get or use provided client
const client = providedClient || getSanityClient({ useCache: false });
// Create a promise that will timeout
const timeoutPromise = new Promise((_, reject) => {
setTimeout(() => reject(new Error('Connection timeout')), timeout);
});
// Try a lightweight system query first
const queryPromise = client.fetch(`*[_type == "sanity.imageAsset"][0]._id`);
// Race between query and timeout
await Promise.race([queryPromise, timeoutPromise]);
// If we get here, connection succeeded
const latency = Date.now() - startTime;
return {
connected: true,
latency,
timestamp,
environment,
details: {
retryAttempts: attempt,
queryType: 'system',
timedOut: false,
},
};
}
catch (error) {
lastError = error;
attempt++;
// If this was the last attempt, break
if (attempt > retryAttempts) {
break;
}
// Wait before retry (exponential backoff)
if (attempt <= retryAttempts) {
await new Promise(resolve => setTimeout(resolve, Math.pow(2, attempt) * 1000));
}
}
}
// Connection failed after all attempts
const latency = Date.now() - startTime;
const errorMessage = lastError?.message || 'Unknown connection error';
// Categorize error types
let categorizedError = errorMessage;
if (errorMessage.includes('timeout') || errorMessage.includes('TIMEOUT')) {
categorizedError = 'Connection timeout - please check your network connection';
}
else if (errorMessage.includes('Unauthorized') || errorMessage.includes('401')) {
categorizedError = 'Authentication failed - please check your API token';
}
else if (errorMessage.includes('Not Found') || errorMessage.includes('404')) {
categorizedError = 'Project not found - please check your project ID and dataset';
}
else if (errorMessage.includes('ENOTFOUND') || errorMessage.includes('DNS')) {
categorizedError = 'Network error - unable to reach Sanity API';
}
return {
connected: false,
error: categorizedError,
latency,
timestamp,
environment,
details: {
retryAttempts: attempt - 1,
queryType: 'system',
timedOut: errorMessage.includes('timeout'),
},
};
}
/**
* Performs a quick connection health check with default settings.
*
* This is a convenience function that performs connection verification
* with sensible defaults for most use cases.
*
* @returns Promise resolving to boolean indicating connection status
*
* @example
* ```typescript
* if (await isConnected()) {
* console.log('Sanity API is accessible');
* } else {
* console.log('Cannot reach Sanity API');
* }
* ```
*/
export async function isConnected() {
try {
const status = await verifyConnection({ timeout: 5000, retryAttempts: 0 });
return status.connected;
}
catch {
return false;
}
}
/**
* Gets detailed connection information including configuration status.
*
* This function provides comprehensive connection diagnostics including
* configuration validation, API accessibility, and environment details.
*
* @param options - Configuration options
* @param options.includeConfig - Whether to include configuration details in response
* @returns Promise resolving to detailed connection information
*
* @example
* ```typescript
* const info = await getConnectionInfo({ includeConfig: true });
* console.log('Environment:', info.environment);
* console.log('Connected:', info.connected);
* if (info.apiVersion) {
* console.log('API Version:', info.apiVersion);
* }
* ```
*/
export async function getConnectionInfo(options = {}) {
const { includeConfig = false } = options;
let configValid = true;
if (includeConfig) {
try {
// Test configuration validity
getSanityClientConfig();
}
catch {
configValid = false;
}
}
// Get connection status
const connectionStatus = await verifyConnection();
// Try to get API version from config if connection succeeded
if (connectionStatus.connected && includeConfig) {
try {
const config = getSanityClientConfig();
connectionStatus.apiVersion = config.apiVersion;
}
catch {
// Config error already handled above
}
}
return {
...connectionStatus,
...(includeConfig && { configValid }),
};
}