Catalog
Discovering Data Offers in the sovity EDC Connector
Catalog
The Catalog enables data consumers to discover available data offers from other connectors in the data space. Each connector exposes its published assets through the catalog, allowing participants to browse, evaluate, and negotiate for data access.
How the Catalog Works
┌─────────────────────────────────────────────────────────────────┐
│ Data Space │
│ │
│ ┌───────────────┐ ┌───────────────┐ ┌───────────────┐ │
│ │ Provider A │ │ Provider B │ │ Provider C │ │
│ │ ┌───────────┐ │ │ ┌───────────┐ │ │ ┌───────────┐ │ │
│ │ │ Assets │ │ │ │ Assets │ │ │ │ Assets │ │ │
│ │ │ + Offers │ │ │ │ + Offers │ │ │ │ + Offers │ │ │
│ │ └───────────┘ │ │ └───────────┘ │ │ └───────────┘ │ │
│ └───────┬───────┘ └───────┬───────┘ └───────┬───────┘ │
│ │ │ │ │
│ └────────────────────┼────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────┐ │
│ │ Your Connector │ │
│ │ (Consumer) │ │
│ └───────────────────┘ │
└─────────────────────────────────────────────────────────────────┘When you query another connector's catalog, you receive their published data offers filtered by access policies that you satisfy.
Fetching Data Offers
Endpoint: GET /wrapper/ui/pages/catalog-page/data-offers
Query Parameters
| Parameter | Required | Description |
|---|---|---|
connectorEndpoint | Yes | The DSP endpoint URL of the target connector |
participantId | No | The participant ID of the target connector |
If the connector endpoint URL includes a participantId query parameter, you don't need to provide it separately.
cURL Example
curl -X GET "https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/data-offers?connectorEndpoint=https://provider.example.com/api/dsp&participantId=provider-id" \
-H "X-Api-Key: your-api-key"TypeScript Example
interface UiDataOffer {
endpoint: string;
participantId: string;
asset: UiAsset;
contractOffers: UiContractOffer[];
}
async function fetchDataOffers(
providerEndpoint: string,
participantId?: string
): Promise<UiDataOffer[]> {
const params = new URLSearchParams({
connectorEndpoint: providerEndpoint,
});
if (participantId) {
params.set('participantId', participantId);
}
const response = await fetch(
`https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/data-offers?${params}`,
{
headers: { 'X-Api-Key': 'your-api-key' },
}
);
if (!response.ok) {
throw new Error(`Failed to fetch catalog: ${response.statusText}`);
}
return response.json();
}
// Usage
const offers = await fetchDataOffers(
'https://museum-connector.example.com/api/dsp',
'museum-participant-id'
);
console.log(`Found ${offers.length} data offers`);Understanding Data Offers
Each UiDataOffer contains complete information about an available asset and its contract terms.
UiDataOffer Structure
interface UiDataOffer {
// Connection details
endpoint: string; // Provider's DSP endpoint
participantId: string; // Provider's participant ID
// Asset information
asset: UiAsset;
// Available contract terms
contractOffers: UiContractOffer[];
}Asset Information (UiAsset)
interface UiAsset {
assetId: string;
title: string;
description?: string;
descriptionShortText?: string;
// Availability
dataSourceAvailability: 'LIVE' | 'ON_REQUEST';
// For ON_REQUEST assets
onRequestContactEmail?: string;
onRequestContactEmailSubject?: string;
// Parameterization hints (for LIVE assets)
httpDatasourceHintsProxyMethod?: boolean;
httpDatasourceHintsProxyPath?: boolean;
httpDatasourceHintsProxyQueryParams?: boolean;
httpDatasourceHintsProxyBody?: boolean;
// Metadata
creatorOrganizationName: string;
language?: string;
version?: string;
keywords?: string[];
mediaType?: string;
dataCategory?: string;
dataSubcategory?: string;
// Additional fields...
}Data Source Availability
| Value | Description |
|---|---|
LIVE | Asset has an automated data source (HTTP API, etc.) |
ON_REQUEST | Asset requires manual contact to obtain data |
For ON_REQUEST assets, use the contact email to request access:
if (offer.asset.dataSourceAvailability === 'ON_REQUEST') {
console.log('Contact:', offer.asset.onRequestContactEmail);
console.log('Subject:', offer.asset.onRequestContactEmailSubject);
}Contract Offers (UiContractOffer)
Each asset can have one or more contract offers with different terms:
interface UiContractOffer {
contractOfferId: string;
policy: UiPolicy;
}
interface UiPolicy {
policyJsonLd: string; // Full policy in JSON-LD (needed for negotiation)
expression: UiPolicyExpression; // Parsed policy expression
errors: string[]; // Any parsing errors
}The expression field provides a structured view of the policy:
interface UiPolicyExpression {
type: 'EMPTY' | 'CONSTRAINT' | 'AND' | 'OR' | 'XONE';
constraint?: UiPolicyConstraint; // For type CONSTRAINT
expressions?: UiPolicyExpression[]; // For AND, OR, XONE
}
interface UiPolicyConstraint {
left: string;
operator: 'EQ' | 'NEQ' | 'GT' | 'GEQ' | 'LT' | 'LEQ' | 'IN' | 'HAS_PART' | 'IS_A' | 'IS_ALL_OF' | 'IS_ANY_OF' | 'IS_NONE_OF';
right: UiPolicyLiteral;
}Processing Catalog Results
Display Available Offers
function displayOffers(offers: UiDataOffer[]): void {
for (const offer of offers) {
console.log(`\n📦 ${offer.asset.title}`);
console.log(` ID: ${offer.asset.assetId}`);
console.log(` Provider: ${offer.asset.creatorOrganizationName}`);
console.log(` Type: ${offer.asset.dataSourceAvailability}`);
if (offer.asset.description) {
console.log(` ${offer.asset.descriptionShortText}`);
}
console.log(` Contract Offers: ${offer.contractOffers.length}`);
for (const contractOffer of offer.contractOffers) {
const policyType = contractOffer.policy.expression?.type || 'UNKNOWN';
console.log(` - ${contractOffer.contractOfferId} (${policyType})`);
}
}
}Filter Offers
// Filter by category
const culturalOffers = offers.filter(
o => o.asset.dataCategory === 'Cultural Heritage'
);
// Filter by availability
const liveOffers = offers.filter(
o => o.asset.dataSourceAvailability === 'LIVE'
);
// Filter by keywords
const museumOffers = offers.filter(
o => o.asset.keywords?.includes('museum')
);
// Filter by unrestricted policy
const unrestrictedOffers = offers.filter(
o => o.contractOffers.some(
co => co.policy.expression?.type === 'EMPTY'
)
);Evaluate Policy Constraints
function canSatisfyPolicy(expression: UiPolicyExpression): boolean {
switch (expression.type) {
case 'EMPTY':
return true; // No constraints
case 'CONSTRAINT':
// Check if you can satisfy this constraint
return evaluateConstraint(expression.constraint!);
case 'AND':
// Must satisfy all sub-expressions
return expression.expressions!.every(canSatisfyPolicy);
case 'OR':
// Must satisfy at least one
return expression.expressions!.some(canSatisfyPolicy);
case 'XONE':
// Must satisfy exactly one
const satisfied = expression.expressions!.filter(canSatisfyPolicy);
return satisfied.length === 1;
default:
return false;
}
}
function evaluateConstraint(constraint: UiPolicyConstraint): boolean {
// Implement your constraint evaluation logic
// This depends on your participant's credentials
console.log(`Checking: ${constraint.left} ${constraint.operator} ${constraint.right}`);
return true; // Simplified
}From Catalog to Negotiation
Once you've selected an offer, initiate a contract negotiation:
Complete Workflow
async function discoverAndNegotiate(
providerEndpoint: string,
participantId: string,
assetId: string
): Promise<string> {
// 1. Fetch data offers
const offers = await fetchDataOffers(providerEndpoint, participantId);
// 2. Find the desired asset
const offer = offers.find(o => o.asset.assetId === assetId);
if (!offer) {
throw new Error(`Asset ${assetId} not found in catalog`);
}
// 3. Select a contract offer (usually the first one)
const contractOffer = offer.contractOffers[0];
if (!contractOffer) {
throw new Error('No contract offers available');
}
// 4. Initiate negotiation
const negotiationRequest = {
counterPartyId: offer.participantId,
counterPartyAddress: offer.endpoint,
contractOfferId: contractOffer.contractOfferId,
assetId: offer.asset.assetId,
policyJsonLd: contractOffer.policy.policyJsonLd,
};
const response = await fetch(
'https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/contract-negotiations',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'your-api-key',
},
body: JSON.stringify(negotiationRequest),
}
);
const negotiation = await response.json();
console.log('Negotiation started:', negotiation.contractNegotiationId);
// 5. Wait for agreement
return await waitForAgreement(negotiation.contractNegotiationId);
}
async function waitForAgreement(negotiationId: string): Promise<string> {
const maxAttempts = 60;
const delayMs = 1000;
for (let attempt = 0; attempt < maxAttempts; attempt++) {
const response = await fetch(
`https://api.your-connector-instance.prod.truzztbox.eu/api/management/wrapper/ui/pages/catalog-page/contract-negotiations/${negotiationId}`,
{ headers: { 'X-Api-Key': 'your-api-key' } }
);
const negotiation = await response.json();
switch (negotiation.state.simplifiedState) {
case 'AGREED':
console.log('Agreement established!');
return negotiation.contractAgreementId;
case 'TERMINATED':
throw new Error(`Negotiation terminated: ${negotiation.state.name}`);
case 'IN_PROGRESS':
await new Promise(resolve => setTimeout(resolve, delayMs));
break;
}
}
throw new Error('Negotiation timed out');
}Querying Multiple Providers
To discover offers across the data space, query multiple connectors:
interface ProviderConfig {
endpoint: string;
participantId: string;
name: string;
}
async function discoverAcrossDataSpace(
providers: ProviderConfig[]
): Promise<Map<string, UiDataOffer[]>> {
const results = new Map<string, UiDataOffer[]>();
await Promise.all(
providers.map(async provider => {
try {
const offers = await fetchDataOffers(
provider.endpoint,
provider.participantId
);
results.set(provider.name, offers);
console.log(`${provider.name}: ${offers.length} offers`);
} catch (error) {
console.error(`${provider.name}: Failed to fetch`, error);
results.set(provider.name, []);
}
})
);
return results;
}
// Usage with preconfigured counterparties
const providers: ProviderConfig[] = [
{
endpoint: 'https://museum-a.example.com/api/dsp',
participantId: 'museum-a',
name: 'Museum A',
},
{
endpoint: 'https://archive-b.example.com/api/dsp',
participantId: 'archive-b',
name: 'Archive B',
},
];
const allOffers = await discoverAcrossDataSpace(providers);Error Handling
Common Errors
| Error | Cause | Solution |
|---|---|---|
| Connection timeout | Provider offline | Retry later or check endpoint |
| 401 Unauthorized | Invalid credentials | Verify API key and participant ID |
| Empty response | No published offers or access denied | Check access policies |
| Policy errors | Cannot parse provider's policies | Check errors field in UiPolicy |
Handling Errors
async function safeFetchOffers(
endpoint: string,
participantId: string
): Promise<UiDataOffer[]> {
try {
const offers = await fetchDataOffers(endpoint, participantId);
// Check for policy parsing errors
for (const offer of offers) {
for (const co of offer.contractOffers) {
if (co.policy.errors.length > 0) {
console.warn(
`Policy parsing warnings for ${offer.asset.assetId}:`,
co.policy.errors
);
}
}
}
return offers;
} catch (error) {
if (error instanceof TypeError) {
console.error('Network error - provider may be offline');
} else {
console.error('Failed to fetch catalog:', error);
}
return [];
}
}Caching Strategies
For frequently accessed catalogs, implement caching:
interface CacheEntry {
offers: UiDataOffer[];
fetchedAt: number;
}
const cache = new Map<string, CacheEntry>();
const CACHE_TTL_MS = 5 * 60 * 1000; // 5 minutes
async function getCachedOffers(
endpoint: string,
participantId: string
): Promise<UiDataOffer[]> {
const cacheKey = `${endpoint}:${participantId}`;
const cached = cache.get(cacheKey);
if (cached && Date.now() - cached.fetchedAt < CACHE_TTL_MS) {
console.log('Returning cached offers');
return cached.offers;
}
const offers = await fetchDataOffers(endpoint, participantId);
cache.set(cacheKey, { offers, fetchedAt: Date.now() });
return offers;
}Best Practices
- Handle offline providers: Not all connectors are always available
- Cache catalog results: Catalogs don't change frequently
- Check policy errors: The
errorsfield indicates unsupported policy features - Validate before negotiating: Ensure you can satisfy policy constraints
- Use participant IDs: Always include participant ID for reliable addressing
- Log catalog queries: Track what offers are available for auditing