Skip to main content
Version: 0.3.1

Metadata Endpoint

The framework automatically provides an OData v4 compliant $metadata endpoint that describes all your entities, their properties, data types, relationships, and query functions in CSDL+JSON format.

Accessing Metadata

With Express Router

The $metadata endpoint is automatically registered when you use ExpressRouter:

import express from 'express';
import { DataSource, ExpressRouter, ODataControler } from '@phrasecode/odata';

const app = express();
const dataSource = new DataSource({
dialect: 'postgres',
database: 'mydb',
username: 'user',
password: 'password',
host: 'localhost',
port: 5432,
models: [User, Department, Order],
});

new ExpressRouter(app, {
controllers: [userController, departmentController],
dataSource,
});

app.listen(3000);

// Metadata is automatically available at:
// GET http://localhost:3000/$metadata

With OpenRouter (Next.js, Serverless)

For OpenRouter, you need to manually create a metadata endpoint:

Next.js Example:

// pages/api/$metadata.ts
import { NextApiRequest, NextApiResponse } from 'next';
import { initializeODataRouter } from '../../lib/db-setup';

export default async function handler(req: NextApiRequest, res: NextApiResponse) {
try {
const odataRouter = initializeODataRouter();
const metadata = odataRouter.getMetaData('http://localhost:3000');
res.status(200).json(metadata);
} catch (error) {
console.error('Error:', error);
res.status(500).json({ error: 'Internal Server Error' });
}
}

// Now accessible at: GET /api/$metadata

Serverless Example (AWS Lambda):

// lambda/metadata.ts
import { OpenRouter } from '@phrasecode/odata';
import { dataSource } from './db-setup';

const router = new OpenRouter({ dataSource, pathMapping: { '/user': User } });

export const handler = async (event: any) => {
try {
const metadata = router.getMetaData();
return {
statusCode: 200,
body: JSON.stringify(metadata),
};
} catch (error) {
return {
statusCode: 500,
body: JSON.stringify({ error: 'Internal Server Error' }),
};
}
};

Metadata Response Format

The metadata endpoint returns an OData v4 CSDL+JSON format response:

{
"$Version": "4.0",
"$EntityContainer": "OData.Container",
"entities": {
"User": {
"$Kind": "EntityType",
"$Key": ["id"],
"$Endpoint": "/user",
"id": {
"$Kind": "Property",
"$Type": "Edm.Int32",
"$Nullable": false,
"$AutoIncrement": true
},
"name": {
"$Kind": "Property",
"$Type": "Edm.String",
"$Nullable": false
},
"email": {
"$Kind": "Property",
"$Type": "Edm.String",
"$Nullable": true
},
"departmentId": {
"$Kind": "Property",
"$Type": "Edm.Int32",
"$Nullable": true
},
"orders": {
"$Kind": "NavigationProperty",
"$Type": "Collection(Order)",
"$ReferentialConstraint": {
"id": "Order/userId"
}
},
"department": {
"$Kind": "NavigationProperty",
"$Type": "Department",
"$ReferentialConstraint": {
"departmentId": "Department/id"
}
}
},
"Order": {
"$Kind": "EntityType",
"$Key": ["id"],
"$Endpoint": "/order",
"id": {
"$Kind": "Property",
"$Type": "Edm.Int32",
"$Nullable": false
},
"userId": {
"$Kind": "Property",
"$Type": "Edm.Int32",
"$Nullable": false
},
"total": {
"$Kind": "Property",
"$Type": "Edm.Decimal",
"$Nullable": false
},
"user": {
"$Kind": "NavigationProperty",
"$Type": "User",
"$ReferentialConstraint": {
"userId": "User/id"
}
}
}
},
"functions": {
"User_getActiveUsers": {
"$Kind": "QueryModel",
"resultModel": "User",
"$Endpoint": "/user/active",
"properties": {
"id": { "$Type": "Edm.Int32", "$Nullable": false },
"name": { "$Type": "Edm.String", "$Nullable": false },
"email": { "$Type": "Edm.String", "$Nullable": true }
}
},
"getUserStats": {
"$Kind": "QueryModel",
"resultModel": "UserStats",
"$Endpoint": "/user-stats/summary",
"properties": {
"userId": { "$Type": "Edm.Int32", "$Nullable": false },
"orderCount": { "$Type": "Edm.Int32", "$Nullable": false },
"totalSpent": { "$Type": "Edm.Decimal", "$Nullable": false }
}
}
},
"metadata": {
"title": "OData API",
"baseUrl": "http://localhost:3000",
"generatedAt": "2024-12-07T10:30:00Z",
"format": "CSDL+JSON",
"$Endpoint": "/$metadata"
}
}

Metadata Structure

Root Object

FieldTypeDescription
$VersionstringOData version (always "4.0")
$EntityContainerstringContainer name (always "OData.Container")
entitiesobjectMap of entity names to entity type definitions
functionsobjectMap of function names to query functions
metadataobjectAPI metadata information

Entity Type Object

FieldTypeDescription
$KindstringAlways "EntityType"
$Keystring[]Array of primary key property names
$EndpointstringAPI endpoint path for this entity
[property]objectProperty or NavigationProperty definitions

Property Object

FieldTypeDescription
$KindstringAlways "Property"
$TypestringOData EDM type (Edm.Int32, Edm.String, etc.)
$NullablebooleanWhether the property can be null
$AutoIncrementbooleanWhether the value auto-increments (optional)
$DefaultValueanyDefault value for the property (optional)
FieldTypeDescription
$KindstringAlways "NavigationProperty"
$TypestringTarget entity type. Collection(Entity) for one-to-many
$ReferentialConstraintobjectKey mappings: { sourceKey: "TargetEntity/targetKey" }

Function Object (Query Endpoints)

FieldTypeDescription
$KindstringAlways "QueryModel"
resultModelstringThe model name for result mapping
$EndpointstringFull endpoint path for this function
propertiesobjectMap of property names to type definitions

Metadata Info Object

FieldTypeDescription
titlestringAPI title
baseUrlstringBase URL of the API (if provided)
generatedAtstringISO timestamp when metadata was generated
formatstringAlways "CSDL+JSON"
$EndpointstringMetadata endpoint path

OData EDM Types

The framework maps database types to OData EDM types:

Database TypeOData EDM Type
INTEGER, INTEdm.Int32
BIGINTEdm.Int64
SMALLINT, TINYINTEdm.Int16
DECIMAL, NUMERICEdm.Decimal
FLOAT, DOUBLE, REALEdm.Double
BOOLEAN, BOOLEdm.Boolean
DATEEdm.Date
DATETIME, TIMESTAMPEdm.DateTimeOffset
TIMEEdm.TimeOfDay
UUID, GUIDEdm.Guid
BLOB, BINARYEdm.Binary
VARCHAR, TEXT, CHAR, STRINGEdm.String

Use Cases

  1. API Documentation: Generate automatic documentation for your API
  2. Client Code Generation: Auto-generate TypeScript/JavaScript client libraries
  3. OData Client Tools: Enable OData-compliant tools to discover your API structure
  4. Validation: Validate queries against the schema before execution
  5. Schema Discovery: Allow developers to explore available entities and their relationships
  6. Query Function Discovery: Discover custom query endpoints and their parameters

Using Metadata Programmatically

// Fetch and use metadata
const response = await fetch('http://localhost:3000/$metadata');
const metadata = await response.json();

// Get OData version
console.log('OData Version:', metadata.$Version);

// Find all entities
console.log('Available entities:', Object.keys(metadata.entities));

// Find all properties of User entity
const userEntity = metadata.entities.User;
const properties = Object.entries(userEntity)
.filter(([_, value]) => value.$Kind === 'Property')
.map(([name]) => name);
console.log('User properties:', properties);

// Find all navigation properties
const navProperties = Object.entries(userEntity)
.filter(([_, value]) => value.$Kind === 'NavigationProperty')
.map(([name]) => name);
console.log('User relationships:', navProperties);

// Find primary keys
console.log('User primary keys:', userEntity.$Key);

// Find all query functions
if (metadata.functions) {
console.log('Available functions:', Object.keys(metadata.functions));

// Get function endpoint
const func = metadata.functions['User_getActiveUsers'];
console.log('Function endpoint:', func.$Endpoint);
}

// Get API info
console.log('API generated at:', metadata.metadata.generatedAt);

Query Functions in Metadata

When you use @Query decorator on controllers, the metadata automatically includes these as functions:

export class UserController extends ODataControler {
constructor() {
super({ model: User, allowedMethod: ['get'] });
}

@Query({
method: 'get',
endpoint: '/active',
parameters: [{ name: 'limit', type: DataTypes.INTEGER, defaultValue: 10 }],
})
async getActiveUsers(event: QueryControllerEvent) {
return this.rawQueryable('SELECT * FROM users WHERE is_active = true LIMIT $limit', {
limit: event.queryParams.limit,
});
}
}

This will appear in metadata as:

{
"functions": {
"User_getActiveUsers": {
"$Kind": "QueryModel",
"resultModel": "User",
"$Endpoint": "/user/active",
"properties": {
"id": { "$Type": "Edm.Int32", "$Nullable": false },
"name": { "$Type": "Edm.String", "$Nullable": false }
}
}
}
}