Advanced
Lifecycle Methods
Understanding plugin lifecycle hooks in Universal Data Layer
Plugin Lifecycle
UDL plugins have access to lifecycle hooks that execute at specific points during initialization and runtime.
Execution Flow
1. Load application config (udl.config.ts)
↓
2. Execute app's onLoad hook (if defined)
↓
3. For each plugin (in order):
a. Resolve plugin path
b. Load plugin's udl.config.ts
c. Execute plugin's onLoad hook
d. Register referenceResolver (if exported)
e. Register entityKeyConfig (if exported)
↓
4. For each source plugin:
- Execute sourceNodes hook
- Process created nodes
↓
5. Build GraphQL schema
↓
6. Start GraphQL server
Available Hooks
onLoad
Called when the plugin is loaded. Use for validation and initialization:
udl.config.ts
export function onLoad({
options,
config,
}: {
options?: MyPluginOptions;
config: UDLConfig;
}) {
// Validate required options
if (!options?.apiKey) {
throw new Error('apiKey is required');
}
// Access application config
console.log(`Server will run on port ${config.port}`);
}
Context Properties
| Property | Type | Description |
|---|---|---|
options | object | undefined | Plugin-specific options from configuration |
config | UDLConfig | The full application configuration |
sourceNodes
Called for source plugins to fetch and create nodes:
udl.config.ts
import type { SourceNodesContext } from 'universal-data-layer';
export async function sourceNodes({
actions,
createNodeId,
createContentDigest,
options,
cacheDir,
}: SourceNodesContext<MyPluginOptions>): Promise<void> {
const items = await fetchData(options);
for (const item of items) {
await actions.createNode({
id: createNodeId(`Item-${item.id}`),
internal: {
type: 'Item',
contentDigest: createContentDigest(item),
},
...item,
});
}
}
Context Properties
| Property | Type | Description |
|---|---|---|
actions | NodeActions | Functions to create/delete nodes |
createNodeId | (id: string) => string | Creates unique node IDs |
createContentDigest | (data: unknown) => string | Creates content hashes |
options | T | undefined | Plugin options (typed via generic) |
cacheDir | string | Path to plugin's cache directory |
Plugin Exports
referenceResolver
Tells UDL how to resolve linked content:
udl.config.ts
import type { ReferenceResolverConfig } from 'universal-data-layer';
export const referenceResolver: ReferenceResolverConfig = {
id: 'my-plugin',
markerField: '_myRef',
lookupField: 'externalId',
isReference: (value): value is MyReference => {
return typeof value === 'object' && '_myRef' in value;
},
getLookupValue: (ref) => ref.refId,
getPossibleTypes: (ref) => ref.possibleTypes ?? [],
priority: 10,
};
| Property | Description |
|---|---|
id | Unique identifier for this resolver |
markerField | Field that marks an object as a reference |
lookupField | Field to use for looking up the referenced node |
isReference | Type guard to identify reference objects |
getLookupValue | Extracts the lookup value from a reference |
getPossibleTypes | Returns possible GraphQL types for the reference |
priority | Resolution priority (higher = checked first) |
entityKeyConfig
Defines how nodes are uniquely identified:
udl.config.ts
import type { EntityKeyConfig } from 'universal-data-layer';
export const entityKeyConfig: EntityKeyConfig = {
idField: 'externalId',
priority: 10,
};
| Property | Description |
|---|---|
idField | Field name used as the unique identifier |
priority | Priority for ID resolution (higher = preferred) |
Async Support
All lifecycle hooks support async operations:
udl.config.ts
export async function onLoad({ options }: { options?: MyPluginOptions }) {
await validateCredentials(options);
await warmupCache(options);
console.log('Plugin ready');
}
export async function sourceNodes(context: SourceNodesContext) {
const items = await fetchAllItems(context.options);
await Promise.all(
items.map((item) => context.actions.createNode(transformItem(item)))
);
}
UDL waits for each promise to resolve before continuing.
Error Handling
Throwing Errors
Throw errors to halt plugin loading:
udl.config.ts
export function onLoad({ options }: { options?: MyPluginOptions }) {
if (!options?.apiKey) {
throw new Error('API key is required');
}
if (!isValidApiKey(options.apiKey)) {
throw new Error('Invalid API key format');
}
}
Graceful Degradation
For non-critical failures, log warnings:
udl.config.ts
export async function sourceNodes(context: SourceNodesContext) {
const items = await fetchItems(context.options);
for (const item of items) {
try {
await context.actions.createNode(transformItem(item));
} catch (error) {
console.warn(`Warning: Failed to process item ${item.id}:`, error);
// Continue with other items
}
}
}
Custom Error Classes
Create specific error types for better debugging:
errors.ts
export class ConfigError extends Error {
constructor(message: string) {
super(message);
this.name = 'ConfigError';
}
}
export class ApiError extends Error {
constructor(message: string, public statusCode?: number) {
super(message);
this.name = 'ApiError';
}
}
export class SyncError extends Error {
constructor(message: string, public isRetryable: boolean) {
super(message);
this.name = 'SyncError';
}
}
Best Practices
1. Validate Early
Check required options in onLoad before sourceNodes runs:
export function onLoad({ options }: { options?: MyPluginOptions }) {
const required = ['apiKey', 'spaceId'] as const;
for (const key of required) {
if (!options?.[key]) {
throw new Error(`Missing required option: ${key}`);
}
}
}
2. Use Parallel Operations
Batch async operations for better performance:
export async function sourceNodes({ actions, options }: SourceNodesContext) {
const [products, categories, tags] = await Promise.all([
fetchProducts(options),
fetchCategories(options),
fetchTags(options),
]);
// Process all items
}
3. Provide Detailed Logging
Use consistent log prefixes:
const LOG_PREFIX = '[my-plugin]';
export function onLoad({ options }: { options?: MyPluginOptions }) {
console.log(`${LOG_PREFIX} Initializing...`);
}
export async function sourceNodes(context: SourceNodesContext) {
console.log(`${LOG_PREFIX} Fetching data...`);
// ...
console.log(`${LOG_PREFIX} Created ${count} nodes`);
}
4. Handle Incremental Updates
Use the cache directory to store sync tokens:
import { readFileSync, writeFileSync, existsSync } from 'fs';
import { join } from 'path';
export async function sourceNodes({ cacheDir, options }: SourceNodesContext) {
const tokenPath = join(cacheDir, 'sync-token.json');
// Load previous sync token
let syncToken: string | undefined;
if (existsSync(tokenPath)) {
syncToken = JSON.parse(readFileSync(tokenPath, 'utf-8')).token;
}
// Perform sync
const result = await syncData(options, syncToken);
// Store new sync token
writeFileSync(tokenPath, JSON.stringify({ token: result.nextSyncToken }));
}
Next Steps
- Creating Plugins - Build your own plugin
- Plugin Overview - How plugins work