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 }
}
}
}
}