Skip to content

chore(release): version packages - #71

Merged
dawidurbanski merged 1 commit into
mainfrom
changeset-release/main
Dec 23, 2025
Merged

chore(release): version packages#71
dawidurbanski merged 1 commit into
mainfrom
changeset-release/main

Conversation

@dawidurbanski

Copy link
Copy Markdown
Owner

This PR was opened by the Changesets release GitHub action. When you're ready to do a release, you can merge this and the packages will be published to npm automatically. If you're not ready to do a release yet, that's fine, whenever you add more changesets to main, this PR will be updated.

Releases

@universal-data-layer/adapter-nextjs@2.0.0

Minor Changes

  • #70 5ff7110 Thanks @dawidurbanski! - Add config file support and UDL_ENDPOINT injection for Next.js adapter

    The adapter commands now read the UDL port from udl.config.ts and automatically inject the UDL_ENDPOINT environment variable when spawning Next.js processes.

    Features:

    • dev, build, and start commands read port from config file
    • Port priority: CLI option > config file > default (4000)
    • Next.js processes receive UDL_ENDPOINT env var automatically
    • Supports udl.config.ts, udl.config.js, and udl.config.mjs

    Benefits:

    • No need to manually set UDL_ENDPOINT in environment
    • udl.query() client automatically uses the correct endpoint
    • Consistent port configuration between UDL server and Next.js

    Example:

    // udl.config.ts
    export const { config } = defineConfig({
      port: 5000, // Adapter commands will use this port
    });
    // In Next.js code, udl.query() automatically uses the right endpoint
    const result = await udl.query(GetProducts);

Patch Changes

universal-data-layer@2.0.0

Minor Changes

  • #70 dfc7d90 Thanks @dawidurbanski! - Add health check endpoints for production deployments

    Introduces /health and /ready endpoints to support container orchestration (Kubernetes, Docker Swarm), load balancers, and deployment verification.

    Endpoints:

    • GET /health - Liveness probe, returns 200 when server is running
    • GET /ready - Readiness probe, returns 200 when fully initialized, 503 during startup

    Response format:

    // /health
    { "status": "ok", "timestamp": "2025-12-21T10:30:00Z" }
    
    // /ready (when ready)
    { "status": "ready", "timestamp": "2025-12-21T10:30:00Z", "checks": { "graphql": true, "nodeStore": true } }
    
    // /ready (during startup)
    { "status": "initializing", "timestamp": "2025-12-21T10:30:00Z", "checks": { "graphql": false, "nodeStore": false } }
  • #70 6430a55 Thanks @dawidurbanski! - Add webhook HTTP routing with convention-based URL pattern

    Routes incoming webhook requests to the appropriate plugin handler using a fixed URL pattern POST /_webhooks/{pluginName}/sync.

    Features:

    • Convention-based routing: all webhooks use the /sync path
    • Routes webhooks to correct handler based on plugin name
    • Validates HTTP method (only POST allowed)
    • Collects raw request body for handler processing
    • Parses JSON body when content-type is application/json
    • Provides WebhookHandlerContext with store, actions, rawBody, and body
    • Enforces 1MB body size limit to prevent abuse
    • Returns appropriate HTTP status codes (405, 404, 400, 413)
    • Queues webhooks for batch processing with debounce

    URL Format:

    POST /_webhooks/{plugin-name}/sync
    
    Examples:
    POST /_webhooks/contentful/sync
    POST /_webhooks/shopify/sync
    POST /_webhooks/my-plugin/sync
    

    Exports:

    • isWebhookRequest - Check if URL is a webhook request
    • getPluginFromWebhookUrl - Extract plugin name from webhook URL
    • webhookHandler - HTTP handler for webhook requests
    • WEBHOOK_PATH_PREFIX - URL prefix constant (/_webhooks/)
  • #70 b376bed Thanks @dawidurbanski! - Add plugin webhook handler export API

    Plugins can now export a registerWebhookHandler function to handle webhooks with custom logic. When exported, it replaces the default CRUD handler for the plugin's /_webhooks/{plugin-name}/sync endpoint.

    Usage in plugins:

    // Plugin's udl.config.ts
    import { defineConfig } from 'universal-data-layer';
    
    export const config = defineConfig({
      name: 'my-cms-plugin',
      type: 'source',
    });
    
    // Custom webhook handler replaces the default
    export async function registerWebhookHandler({ req, res, actions, body, store, rawBody }) {
      // Verify signature using your CMS's method
      const signature = req.headers['x-webhook-signature'];
      if (!verifySignature(rawBody, signature)) {
        res.writeHead(401);
        res.end('Invalid signature');
        return;
      }
    
      // Handle different event types
      const eventType = req.headers['x-webhook-type'];
    
      if (eventType === 'entry.publish') {
        await actions.createNode(transformEntry(body), { ... });
      } else if (eventType === 'entry.delete') {
        await actions.deleteNode(body.sys.id);
      }
    
      res.writeHead(200);
      res.end();
    }

    Key benefits:

    • Clear separation: sourceNodes for sourcing, registerWebhookHandler for webhooks
    • Convention-based URL: always /_webhooks/{plugin-name}/sync
    • Plugin controls its own routing and signature verification internally
    • Replaces default handler - no confusion about which handler runs

    Handler context:

    The handler receives a flattened context object:

    interface PluginWebhookHandlerContext {
      req: IncomingMessage; // The incoming HTTP request
      res: ServerResponse; // The server response
      actions: NodeActions; // Node CRUD operations
      store: NodeStore; // Access to all nodes
      body: unknown; // Parsed JSON body
      rawBody: Buffer; // Raw body for signature verification
    }

    New exports:

    • PluginWebhookHandler - Type for the handler function
    • PluginWebhookHandlerContext - Type for the handler context
    • registerPluginWebhookHandler - Internal utility for registering custom handlers
  • #70 5ff7110 Thanks @dawidurbanski! - Add centralized cache manager for plugin cache coordination

    Introduces a CacheManager module that provides a central point for coordinating plugin cache updates. This enables webhook handlers and remote sync to persist changes to disk after store modifications.

    Features:

    • registerPluginCache(pluginName, cache): Register a plugin's cache storage
    • initPluginCache(pluginName, cacheLocation, customCache?): Initialize and register a cache
    • savePluginCache(pluginName, store?): Save a specific plugin's nodes to cache
    • saveAffectedPlugins(affectedPlugins, store?): Save caches for multiple plugins
    • replaceAllCaches(store?): Replace all plugin caches (for remote sync)
    • setStore(store): Set the node store reference for cache operations

    Integration:

    • Loader now uses cache manager for plugin cache operations
    • Webhook batch processing automatically saves affected plugin caches
    • Remote sync persists fetched nodes to cache for offline support

    New exports:

    import {
      setStore,
      getStore,
      registerPluginCache,
      initPluginCache,
      savePluginCache,
      saveAffectedPlugins,
      replaceAllCaches,
      clearAllCaches,
      resetCacheManager,
    } from 'universal-data-layer';
  • #70 2e01999 Thanks @dawidurbanski! - Add default webhook handler for standardized CRUD operations

    This release introduces a default webhook handler that provides a standardized way to create, update, and delete nodes via webhooks. Every loaded plugin automatically gets a webhook endpoint registered with zero configuration required.

    Features:

    • Automatic registration of /_webhooks/{plugin-name}/sync endpoint for every plugin
    • Standardized payload format for create, update, delete, and upsert operations
    • Support for custom idField to look up nodes by external identifiers
    • Won't overwrite custom handlers if plugin registers its own

    Zero Configuration:

    // No config needed - default webhooks just work
    // Every plugin gets: /_webhooks/{plugin-name}/sync
    export const { config } = defineConfig({
      plugins: ['@universal-data-layer/plugin-source-contentful'],
    });

    Payload format:

    interface DefaultWebhookPayload {
      operation: 'create' | 'update' | 'delete' | 'upsert';
      nodeId: string; // External ID or internal node ID
      nodeType: string; // Node type (e.g., 'Product', 'Article')
      data?: Record<string, unknown>; // Node data (required for create/update/upsert)
    }

    Example requests:

    # Create a node
    curl -X POST http://localhost:4000/_webhooks/my-plugin/sync \
      -H "Content-Type: application/json" \
      -d '{"operation":"create","nodeId":"123","nodeType":"Product","data":{"name":"Widget"}}'
    
    # Update a node
    curl -X POST http://localhost:4000/_webhooks/my-plugin/sync \
      -d '{"operation":"update","nodeId":"123","nodeType":"Product","data":{"name":"Updated Widget"}}'
    
    # Delete a node
    curl -X POST http://localhost:4000/_webhooks/my-plugin/sync \
      -d '{"operation":"delete","nodeId":"123","nodeType":"Product"}'
    
    # Upsert (create or update)
    curl -X POST http://localhost:4000/_webhooks/my-plugin/sync \
      -d '{"operation":"upsert","nodeId":"123","nodeType":"Product","data":{"name":"Widget"}}'

    idField support:

    When a plugin specifies an idField in its config, the default webhook handler looks up existing nodes by that field:

    // Plugin config
    export const config = defineConfig({
      idField: 'externalId', // Webhook will look up nodes by this field
    });
  • #70 aba060e Thanks @dawidurbanski! - Add deletion log for partial sync support

    This release introduces a DeletionLog class that tracks node deletions with timestamps, enabling clients to perform partial sync without needing a full refetch.

    Features:

    • DeletionLog class for tracking deleted nodes
    • recordDeletion(node): Record a node deletion with timestamp
    • getDeletedSince(timestamp): Query deletions after a given time
    • cleanup(): Remove entries older than TTL (default: 30 days)
    • Serialization support via toJSON() and fromJSON() for persistence
    • Configurable TTL (time-to-live) for deletion entries

    Example usage:

    import { DeletionLog } from 'universal-data-layer';
    
    const log = new DeletionLog(30); // 30 day TTL
    
    // Record a deletion
    log.recordDeletion(deletedNode);
    
    // Query deletions since last sync
    const deletedSince = log.getDeletedSince(lastSyncTimestamp);
    
    // Serialize for persistence
    const data = log.toJSON();
    
    // Restore from persistence
    const restored = DeletionLog.fromJSON(data);
  • #70 8ec4f2b Thanks @dawidurbanski! - feat(core): add graceful shutdown for production deployments

    • Handle SIGTERM and SIGINT signals for graceful shutdown
    • Complete in-flight requests before closing server
    • Return 503 on /ready endpoint during shutdown
    • Configurable grace period (default: 30 seconds)
    • Clean up file watchers and resources on shutdown
    • Log shutdown progress to console
  • #70 5ff7110 Thanks @dawidurbanski! - Add instant webhook relay for remote sync

    Local UDL instances can now receive and process webhooks instantly via WebSocket relay, eliminating the need to wait for batch debounce on the production server.

    How it works:

    1. Production UDL receives a webhook and queues it
    2. Immediately broadcasts webhook:received message to WebSocket subscribers
    3. Local UDL instances receive the message and process the webhook locally
    4. Local caches are updated instantly

    Features:

    • New webhook:queued event on WebhookQueue for instant relay
    • New webhook:received WebSocket message type
    • broadcastWebhookReceived(webhook) method on UDLWebSocketServer
    • onWebhookReceived callback on WebSocketClient and RemoteSyncConfig
    • Local UDL instances can process relayed webhooks using registered handlers
    • Node change events are skipped when handling webhooks locally (avoids double processing)

    Configuration:

    The instant relay is automatically enabled when using remote sync. Local instances register webhook handlers by loading plugins with isLocal: true option.

    Message format:

    interface WebhookReceivedMessage {
      type: 'webhook:received';
      pluginName: string;
      body: unknown;
      headers: Record<string, string | string[] | undefined>;
      timestamp: string;
    }

    Exports:

    • WebhookReceivedEvent: Event data passed to onWebhookReceived callback
  • #70 6ffae50 Thanks @dawidurbanski! - Add outbound webhook triggering with transformPayload support

    Trigger outbound webhooks after a batch of incoming webhooks has been processed. This enables the "30 webhooks → 1 build" optimization by notifying external systems (e.g., Vercel deploy hooks, CI systems) once after processing a batch rather than for each individual webhook.

    Features:

    • OutboundWebhookManager class for managing outbound webhook notifications
    • Configurable outbound webhook endpoints via remote.webhooks.outbound
    • HTTP method selection (POST or GET, default POST)
    • Retry logic with exponential backoff (default: 3 retries, 1000ms base delay)
    • Custom headers support for authentication
    • Parallel triggering to multiple endpoints using Promise.allSettled
    • transformPayload callback for customizing the payload per trigger
    • Default payload includes items array with webhook details

    Example configuration:

    export const { config } = defineConfig({
      remote: {
        webhooks: {
          debounceMs: 5000,
          outbound: [
            {
              // Vercel just needs an empty POST body
              url: 'https://api.vercel.com/v1/integrations/deploy/...',
              transformPayload: () => ({}),
            },
            {
              // Simple GET ping (no body needed)
              url: 'https://my-cdn.example.com/purge',
              method: 'GET',
              transformPayload: () => ({}),
            },
            {
              // Custom payload for CI system
              url: 'https://my-ci.example.com/webhook',
              transformPayload: ({ items, timestamp }) => ({
                event: 'content-updated',
                changes: items.map((i) => i.body),
                timestamp,
              }),
            },
            {
              // No transform = uses default payload with items
              url: 'https://other.example.com/hook',
              headers: { Authorization: 'Bearer token' },
            },
          ],
        },
      },
    });

    transformPayload context:

    type TransformPayloadContext = {
      batch: WebhookBatch; // Raw batch data
      event: 'batch-complete'; // Event type
      timestamp: string; // ISO 8601 timestamp
      source: string; // UDL instance ID
      summary: {
        webhookCount: number;
        plugins: string[];
      };
      items: Array<{
        // Individual webhook items
        pluginName: string;
        body: unknown;
        headers: Record<string, string | string[] | undefined>;
        timestamp: number;
      }>;
    };

    Default outbound webhook payload:

    {
      "event": "batch-complete",
      "timestamp": "2024-01-15T10:30:00.000Z",
      "summary": {
        "webhookCount": 30,
        "plugins": ["@universal-data-layer/plugin-source-contentful"]
      },
      "source": "UDL",
      "items": [
        { "pluginName": "contentful", "body": { "operation": "upsert", ... } },
        ...
      ]
    }

    Exports:

    • OutboundWebhookManager - Class for managing outbound webhooks
    • OutboundWebhookConfig - Configuration type for outbound webhooks
    • OutboundWebhookPayload - Default payload type
    • OutboundWebhookResult - Result type for trigger operations
    • TransformPayloadContext - Context type for transformPayload function
    • TransformPayload - Type for the transform function
    • WebhookItem - Type for individual webhook item info
  • #70 051192e Thanks @dawidurbanski! - feat(core): add remote sync for syncing data from production UDL server

    Added remote.url config option that allows local UDL servers to sync data from a remote production UDL server instead of sourcing from plugins directly.

    When configured:

    • Fetches all nodes from remote /_sync endpoint on startup
    • Automatically connects to remote WebSocket for real-time updates (if enabled on remote)
    • Skips local plugin loading

    New exports:

    • UDLWebSocketClient - WebSocket client for connecting to remote UDL
    • fetchRemoteNodes - Fetch all nodes from remote server
    • tryConnectRemoteWebSocket - Connect to remote WebSocket
    • initRemoteSync - Initialize remote sync (fetch + WebSocket)

    Usage:

    export const config = defineConfig({
      remote: {
        url: 'https://production-udl.example.com',
      },
    });
  • #70 2e01999 Thanks @dawidurbanski! - Add sync query API for partial updates

    This release introduces a GET /_sync endpoint that enables clients to fetch only the nodes that have changed since their last sync. This enables efficient incremental synchronization without requiring a full data refetch.

    Features:

    • GET /_sync?since={timestamp} endpoint for querying changes
    • Returns updated nodes and deleted node IDs since the given timestamp
    • Optional type filtering via types query parameter
    • Server timestamp included for use in subsequent sync calls
    • Integrates with DeletionLog for tracking deleted nodes

    Response format:

    interface SyncResponse {
      updated: Node[]; // Nodes modified after timestamp
      deleted: DeletionLogEntry[]; // Nodes deleted after timestamp
      serverTime: string; // ISO 8601 timestamp for next sync
      hasMore: boolean; // Reserved for future pagination
    }

    Example usage:

    // Initial sync - get all changes since epoch
    const response = await fetch(
      'http://localhost:4000/_sync?since=1970-01-01T00:00:00Z'
    );
    const { updated, deleted, serverTime } = await response.json();
    
    // Store serverTime for next sync
    localStorage.setItem('lastSync', serverTime);
    
    // Subsequent sync - get only recent changes
    const lastSync = localStorage.getItem('lastSync');
    const response = await fetch(`http://localhost:4000/_sync?since=${lastSync}`);

    Type filtering:

    GET /_sync?since=2024-01-01T00:00:00Z&types=Product,Collection
    

    Only returns changes for the specified node types.

  • #70 0ea71da Thanks @dawidurbanski! - # Add updateStrategy config option for sync-based source plugins

    Plugins can now specify how incremental updates from webhooks should be handled:

    • 'webhook' (default): Process webhook payload directly via registerWebhookHandler or the default CRUD handler
    • 'sync': Treat webhooks as notifications only and re-run sourceNodes to fetch changes via the plugin's sync API

    This enables plugins with native sync APIs (like Contentful) to reuse their existing sourceNodes logic for incremental updates, eliminating the need to maintain separate webhook transformation code.

    Usage

    // For sources with sync APIs (like Contentful)
    export const config = defineConfig({
      name: 'my-source-plugin',
      updateStrategy: 'sync',
    });

    When webhooks arrive for a plugin with updateStrategy: 'sync':

    1. Webhooks are batched as usual (debounced)
    2. After the batch, sourceNodes is called once per affected plugin
    3. The plugin's delta sync fetches only changed data
    4. Cache is saved after sync completes

    The Contentful plugin now uses updateStrategy: 'sync' by default, leveraging the Contentful Sync API for efficient incremental updates.

  • #70 5920046 Thanks @dawidurbanski! - Add webhook queue with debouncing and lifecycle hooks

    This release introduces a webhook queue system that batches incoming webhooks and processes them after a configurable debounce period. This prevents N rapid webhook events (e.g., 30 Contentful entry publishes) from triggering N separate processing cycles.

    Features:

    • Webhook queue with configurable debounce period (remote.webhooks.debounceMs, default 5000ms)
    • Maximum queue size before forced processing (remote.webhooks.maxQueueSize, default 100)
    • Lifecycle hooks for custom processing:
      • onWebhookReceived: Transform or filter webhooks before queuing
      • onBeforeWebhookTriggered: Run before batch processing (e.g., invalidate CDN cache)
      • onAfterWebhookTriggered: Run after batch processing (e.g., trigger rebuild)
    • Graceful shutdown flushes pending webhooks
    • HTTP response changed from 200 to 202 Accepted (webhook is queued)

    Example configuration:

    export const { config } = defineConfig({
      remote: {
        webhooks: {
          debounceMs: 5000,
          maxQueueSize: 100,
          hooks: {
            onWebhookReceived: async ({ webhook }) => {
              // Skip drafts
              if (!webhook.body?.sys?.publishedAt) return null;
              return webhook;
            },
            onBeforeWebhookTriggered: async ({ batch }) => {
              await invalidateCDNCache();
            },
            onAfterWebhookTriggered: async ({ batch }) => {
              await triggerRebuild();
            },
          },
        },
      },
    });

    Breaking Changes:

    • Webhook HTTP responses now return 202 Accepted instead of 200 OK
    • Webhooks are processed asynchronously after debounce period instead of immediately
  • #70 2e01999 Thanks @dawidurbanski! - Add WebSocket server for real-time node change notifications

    This release introduces an opt-in WebSocket server that broadcasts node changes to connected clients in real-time. This enables local development machines to receive updates immediately when webhooks modify the data layer, eliminating the need for polling.

    Features:

    • UDLWebSocketServer class for real-time node change broadcasts
    • Broadcasts node:created, node:updated, node:deleted events with full node data
    • Client subscription filtering by node type (or * for all types)
    • Heartbeat mechanism for connection health monitoring
    • Configurable via remote.websockets in UDL config
    • Support for separate WebSocket port or attachment to HTTP server
    • Pass-through options for advanced ws configuration

    Configuration:

    export const { config } = defineConfig({
      remote: {
        websockets: {
          enabled: true,
          path: '/ws', // Default: '/ws'
          port: 4001, // Optional: separate port
          heartbeatIntervalMs: 30000, // Default: 30000
        },
      },
    });

    Client usage:

    const ws = new WebSocket('ws://localhost:4000/ws');
    
    ws.onmessage = (event) => {
      const message = JSON.parse(event.data);
      if (message.type === 'node:created') {
        console.log('New node:', message.data);
      }
    };
    
    // Subscribe to specific types
    ws.send(
      JSON.stringify({ type: 'subscribe', data: ['Product', 'Collection'] })
    );

    Message types:

    • Server → Client: node:created, node:updated, node:deleted, connected, subscribed, pong
    • Client → Server: subscribe, ping

Patch Changes

  • #70 5ff7110 Thanks @dawidurbanski! - Add UDL_ENDPOINT environment variable support to config

    The getConfig() function now checks for the UDL_ENDPOINT environment variable when config hasn't been explicitly initialized. This allows the udl.query() client to automatically use the correct endpoint in child processes.

    Features:

    • UDL_ENDPOINT_ENV constant for the environment variable name
    • DEFAULT_UDL_PORT constant (4000) for consistent default port
    • isConfigInitialized() to check if config was explicitly set
    • resetConfig() for testing isolation

    How it works:

    When getConfig() is called and no config was explicitly set via createConfig(), it checks for the UDL_ENDPOINT environment variable and uses that endpoint if present.

    This enables scenarios like:

    • Next.js adapter sets UDL_ENDPOINT when spawning Next.js
    • udl.query() in Next.js code automatically uses the right endpoint
  • #70 6fe6408 Thanks @dawidurbanski! - refactor: remove hardcoded default port values

    Removed hardcoded default port (4000) from CLI and adapter commands. The port is now only passed when explicitly specified by the user, allowing the config file to determine the default port value instead.

@universal-data-layer/codegen-typed-queries@2.0.0

Patch Changes

@universal-data-layer/plugin-source-contentful@2.0.0

Patch Changes

  • #70 0ea71da Thanks @dawidurbanski! - # Add updateStrategy config option for sync-based source plugins

    Plugins can now specify how incremental updates from webhooks should be handled:

    • 'webhook' (default): Process webhook payload directly via registerWebhookHandler or the default CRUD handler
    • 'sync': Treat webhooks as notifications only and re-run sourceNodes to fetch changes via the plugin's sync API

    This enables plugins with native sync APIs (like Contentful) to reuse their existing sourceNodes logic for incremental updates, eliminating the need to maintain separate webhook transformation code.

    Usage

    // For sources with sync APIs (like Contentful)
    export const config = defineConfig({
      name: 'my-source-plugin',
      updateStrategy: 'sync',
    });

    When webhooks arrive for a plugin with updateStrategy: 'sync':

    1. Webhooks are batched as usual (debounced)
    2. After the batch, sourceNodes is called once per affected plugin
    3. The plugin's delta sync fetches only changed data
    4. Cache is saved after sync completes

    The Contentful plugin now uses updateStrategy: 'sync' by default, leveraging the Contentful Sync API for efficient incremental updates.

  • Updated dependencies [dfc7d90, 6430a55, b376bed, 5ff7110, 5ff7110, 2e01999, aba060e, 8ec4f2b, 5ff7110, 6ffae50, 051192e, 6fe6408, 2e01999, 0ea71da, 5920046, 2e01999]:

    • universal-data-layer@2.0.0

@vercel

vercel Bot commented Dec 23, 2025

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Review Updated (UTC)
universal-data-layer-nextjs Ready Ready Preview, Comment Dec 23, 2025 2:13pm

@dawidurbanski
dawidurbanski merged commit 81b0461 into main Dec 23, 2025
4 of 5 checks passed
@dawidurbanski
dawidurbanski deleted the changeset-release/main branch December 23, 2025 14:13
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant