Projection
Projection and Data Transformation
Projection in DDO allows you to transform data into a new shape or structure without modifying the original data. Unlike mutations that change existing documents, projections create new representations of your data based on templates you provide.
Understanding Projection
Projection is particularly useful for:
- Creating API responses with specific data structures
- Generating reports and summaries
- Transforming data for different views or interfaces
- Computing derived values from existing data
- Reshaping nested data structures
Basic Projection Syntax
Projection QLOs use template expressions rather than operators:
# Source data
user_data = {
"firstName": "John",
"lastName": "Doe",
"email": "john.doe@example.com",
"age": 30,
"orders": [
{"total": 100, "status": "completed"},
{"total": 150, "status": "pending"}
]
}
# Projection QLO
projection = {
"displayName": "{{ firstName }} {{ lastName }}",
"contact": "{{ email }}",
"&age": "age", # Copy as-is with native type
"isAdult": "?{{ age >= 18 }}", # Boolean result
"&orderCount": "@length(orders)" # Number result
}
# Result:
{
"displayName": "John Doe", # String
"contact": "john.doe@example.com", # String
"age": 30, # Number (native type)
"isAdult": True, # Boolean (native type)
"orderCount": 2 # Number (native type)
}
Simple Field Mapping
Map and rename fields from source to target structure:
# Source: database user record
source = {
"user_id": "12345",
"first_name": "Jane",
"last_name": "Smith",
"email_address": "jane@example.com",
"created_timestamp": "2024-01-15T10:30:00Z"
}
# Projection: API response format
projection = {
"id": "{{ user_id }}",
"name": "{{ first_name }} {{ last_name }}",
"email": "{{ email_address }}",
"joined": "{{ @format_date(created_timestamp, 'YYYY-MM-DD') }}"
}
# Result:
{
"id": "12345",
"name": "Jane Smith",
"email": "jane@example.com",
"joined": "2024-01-15"
}
Nested Structure Creation
Create complex nested structures from flat or differently structured data:
# Source: flat user data
source = {
"userId": "u123",
"firstName": "Alice",
"lastName": "Johnson",
"email": "alice@company.com",
"phone": "+1-555-0123",
"street": "123 Main St",
"city": "Portland",
"state": "OR",
"zipCode": "97201",
"department": "Engineering",
"role": "Senior Developer",
"salary": 95000
}
# Projection: structured profile
projection = {
"id": "{{ userId }}",
"personal": {
"fullName": "{{ firstName }} {{ lastName }}",
"contact": {
"email": "{{ email }}",
"phone": "{{ phone }}"
},
"address": {
"street": "{{ street }}",
"city": "{{ city }}",
"state": "{{ state }}",
"zipCode": "{{ zipCode }}"
}
},
"professional": {
"department": "{{ department }}",
"position": "{{ role }}",
"&compensation": "salary",
"level": "{{ @if_else(salary >= 90000, 'Senior', 'Junior') }}"
}
}
Working with Arrays and Collections
Transform arrays and collections within projections:
# Source: order data with items
source = {
"orderId": "ord_456",
"customer": {"name": "Bob Wilson", "email": "bob@example.com"},
"items": [
{"productId": "p1", "name": "Laptop", "price": 999.99, "quantity": 1},
{"productId": "p2", "name": "Mouse", "price": 29.99, "quantity": 2}
],
"shipping": {"method": "standard", "cost": 9.99}
}
# Projection: order summary
projection = {
"orderNumber": "{{ orderId }}",
"customer": {
"name": "{{ customer.name }}",
"email": "{{ customer.email }}"
},
"summary": {
"&itemCount": "@length(items)",
"&subtotal": "@sum(@map(items, 'price * quantity'))",
"shipping": "?{{ shipping.cost }}",
"&total": "@add(@sum(@map(items, 'price * quantity')), shipping.cost)"
},
"&itemDetails": "@map(items, '{\"product\": name, \"qty\": quantity, \"total\": price * quantity}')",
"shippingMethod": "{{ shipping.method }}"
}
Conditional Projections
Create different outputs based on conditions:
# Source: user account data
source = {
"userId": "u789",
"name": "Charlie Brown",
"accountType": "premium",
"credits": 150,
"lastActive": "2024-01-10T14:30:00Z",
"preferences": {"theme": "dark", "notifications": True}
}
# Projection: different views based on account type
projection = {
"userId": "{{ userId }}",
"displayName": "{{ name }}",
"accountInfo": {
"type": "{{ accountType }}",
"status": "{{ @if_else(accountType == 'premium', 'Premium Member', 'Standard User') }}",
"&credits": "credits",
"hasCredits": "?{{ credits > 0 }}"
},
# Conditional fields based on account type
"features": "{{ @if_else(accountType == 'premium', '{\"advancedReports\": true, \"prioritySupport\": true}', '{\"basicReports\": true}') }}",
"limits": {
"&maxProjects": "@if_else(accountType == 'premium', 50, 5)",
"&storageGB": "@if_else(accountType == 'premium', 100, 10)"
},
"activity": {
"lastSeen": "{{ @time_ago(lastActive) }}",
"&isRecentlyActive": "@diff_dates(@now(), lastActive, 'days') <= 7"
}
}
Aggregation and Statistics
Create summary statistics and aggregated data:
# Source: sales data
source = {
"salesPerson": {"name": "Diana Prince", "region": "West"},
"sales": [
{"date": "2024-01-01", "amount": 1200, "product": "Software"},
{"date": "2024-01-03", "amount": 800, "product": "Hardware"},
{"date": "2024-01-05", "amount": 1500, "product": "Software"},
{"date": "2024-01-07", "amount": 950, "product": "Services"}
]
}
# Projection: sales report
projection = {
"salesperson": {
"name": "{{ salesPerson.name }}",
"region": "{{ salesPerson.region }}"
},
"performance": {
"&totalSales": "@sum(@map(sales, 'amount'))",
"&averageSale": "@avg(@map(sales, 'amount'))",
"&salesCount": "@length(sales)",
"&highestSale": "@max(@map(sales, 'amount'))",
"&lowestSale": "@min(@map(sales, 'amount'))"
},
"breakdown": {
"&byProduct": "@group_by(sales, 'product')",
"&softwareSales": "@sum(@map(@filter(sales, 'product == \"Software\"'), 'amount'))",
"&hardwareSales": "@sum(@map(@filter(sales, 'product == \"Hardware\"'), 'amount'))"
},
"targets": {
"&quarterlyTarget": "15000",
"&progressPercent": "@round(@div(@sum(@map(sales, 'amount')), 15000) * 100)",
"&onTrack": "@sum(@map(sales, 'amount')) >= 4500" # 30% of quarterly target
}
}
Dynamic Field Generation
Generate fields dynamically based on data content:
# Source: product catalog
source = {
"productId": "prod_123",
"name": "Smart Watch",
"categories": ["electronics", "wearable", "fitness"],
"attributes": {
"color": ["black", "white", "blue"],
"size": ["small", "medium", "large"],
"features": ["heart-rate", "gps", "waterproof"]
},
"pricing": {"base": 299.99, "currency": "USD"},
"inventory": {"inStock": 45, "reserved": 5}
}
# Projection: product catalog entry
projection = {
"id": "{{ productId }}",
"title": "{{ name }}",
"categorization": {
"&primaryCategory": "@slice(@sort_by(categories, 'length'), 0, 1)[0]",
"&allCategories": "categories",
"&categoryCount": "@length(categories)"
},
"variants": {
"&colorOptions": "attributes.color",
"&sizeOptions": "attributes.size",
"&totalCombinations": "@mul(@length(attributes.color), @length(attributes.size))"
},
"features": {
"&featureList": "attributes.features",
"&hasGPS": "@includes(attributes.features, 'gps')",
"&isWaterproof": "@includes(attributes.features, 'waterproof')"
},
"pricing": {
"displayPrice": "${{ pricing.base }}",
"&numericPrice": "pricing.base",
"currency": "{{ pricing.currency }}"
},
"availability": {
"&available": "inventory.inStock - inventory.reserved",
"&inStock": "inventory.inStock > inventory.reserved",
"stockLevel": "{{ @if_else(inventory.inStock - inventory.reserved > 10, 'High', @if_else(inventory.inStock - inventory.reserved > 0, 'Low', 'Out of Stock')) }}"
}
}
Projection with JMESPath Queries
Use JMESPath for complex data extraction within projections:
# Source: complex organizational data
source = {
"company": "Tech Corp",
"departments": [
{
"name": "Engineering",
"employees": [
{"name": "Alice", "role": "Senior Dev", "salary": 95000, "skills": ["Python", "React"]},
{"name": "Bob", "role": "Junior Dev", "salary": 65000, "skills": ["JavaScript", "Vue"]}
]
},
{
"name": "Marketing",
"employees": [
{"name": "Carol", "role": "Manager", "salary": 85000, "skills": ["Strategy", "Analytics"]},
{"name": "David", "role": "Specialist", "salary": 55000, "skills": ["Content", "SEO"]}
]
}
]
}
# Projection: company analytics
projection = {
"companyName": "{{ company }}",
"workforce": {
"&totalEmployees": "@length(@search(departments, '[].employees[]'))",
"&departmentCount": "@length(departments)",
"&avgSalary": "@avg(@map(@search(departments, '[].employees[]'), 'salary'))"
},
"departments": {
"&engineeringSize": "@length(@search(departments, '[?name==`Engineering`].employees[]'))",
"&marketingSize": "@length(@search(departments, '[?name==`Marketing`].employees[]'))",
"&seniorStaffCount": "@length(@search(departments, '[].employees[?contains(role, `Senior`) || contains(role, `Manager`)]'))"
},
"skills": {
"&allSkills": "@unique(@search(departments, '[].employees[].skills[]'))",
"&techSkills": "@unique(@search(departments, '[?name==`Engineering`].employees[].skills[]'))",
"&marketingSkills": "@unique(@search(departments, '[?name==`Marketing`].employees[].skills[]'))"
},
"compensation": {
"&totalPayroll": "@sum(@map(@search(departments, '[].employees[]'), 'salary'))",
"&highestPaid": "@max(@map(@search(departments, '[].employees[]'), 'salary'))",
"&engineeringPayroll": "@sum(@map(@search(departments, '[?name==`Engineering`].employees[]'), 'salary'))"
}
}
Advanced Projection Patterns
Advanced Projection Patterns
Special Field Selection with _
Key
DDO provides powerful field selection capabilities using the special _
key in projections:
Include All Fields
Use {"_": "**"}
to include all fields from the source data, plus any additional mappings:
# Source data
source = {
"userId": "u123",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"age": 30,
"department": "Engineering"
}
# Projection: include all fields plus computed ones
projection = {
"_": "**", # Include all original fields
"fullName": "{{ firstName }} {{ lastName }}", # Add computed field
"&isAdult": "age >= 18" # Add boolean field
}
# Result: All original fields + computed fields
{
"userId": "u123",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"age": 30,
"department": "Engineering",
"fullName": "John Doe", # Added
"isAdult": True # Added
}
Include Specified Fields Only
Use {"_": "{field1, field2, ...}"}
to include only specific fields plus mappings:
# Source data
source = {
"userId": "u123",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"age": 30,
"department": "Engineering",
"salary": 95000,
"ssn": "123-45-6789"
}
# Projection: include only safe fields for API response
projection = {
"_": "{userId, firstName, lastName, email, department}", # Only these fields
"fullName": "{{ firstName }} {{ lastName }}", # Plus computed field
"&isStaff": "department != null" # Plus boolean
}
# Result: Only specified fields + computed fields
{
"userId": "u123",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"department": "Engineering",
"fullName": "John Doe", # Added
"isStaff": True # Added
# salary and ssn are excluded
}
Exclude Specified Fields
Use {"_": "?{field1, field2, ...}"}
to include all fields except specified ones:
# Source data (same as above)
source = {
"userId": "u123",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"age": 30,
"department": "Engineering",
"salary": 95000,
"ssn": "123-45-6789",
"internalNotes": "High performer"
}
# Projection: exclude sensitive fields
projection = {
"_": "?{salary, ssn, internalNotes}", # Exclude these fields
"displayName": "{{ firstName }} {{ lastName }}", # Add computed field
"&canEdit": "department == 'Engineering'" # Add permission field
}
# Result: All fields except excluded ones + computed fields
{
"userId": "u123",
"firstName": "John",
"lastName": "Doe",
"email": "john@example.com",
"age": 30,
"department": "Engineering",
"displayName": "John Doe", # Added
"canEdit": True # Added
# salary, ssn, internalNotes are excluded
}
Field Selection Patterns
Dynamic Field Selection
Combine field selection with template expressions for dynamic projections:
# Source: user data with varying fields
source = {
"userId": "u456",
"profile": {
"firstName": "Alice",
"lastName": "Smith",
"email": "alice@company.com",
"phone": "+1-555-0123",
"address": {"city": "Portland", "state": "OR"}
},
"preferences": {"theme": "dark", "notifications": True},
"metadata": {"lastLogin": "2024-01-15T10:30:00Z", "ipAddress": "192.168.1.1"}
}
# Projection: public profile (exclude metadata)
projection = {
"_": "?{metadata}", # Include all except metadata
"contactInfo": {
"email": "{{ profile.email }}",
"phone": "{{ profile.phone }}",
"location": "{{ profile.address.city }}, {{ profile.address.state }}"
},
"&hasNotifications": "preferences.notifications"
}
Nested Field Selection
Apply field selection to nested objects:
# Source: complex user object
source = {
"user": {
"id": "u789",
"personal": {
"firstName": "Bob",
"lastName": "Wilson",
"email": "bob@example.com",
"phone": "+1-555-0456",
"ssn": "987-65-4321"
},
"professional": {
"title": "Senior Developer",
"department": "Engineering",
"salary": 110000,
"startDate": "2020-03-15"
},
"internal": {
"performanceRating": "Excellent",
"notes": "Top performer",
"nextReview": "2024-06-01"
}
}
}
# Projection: employee directory entry
projection = {
"id": "{{ user.id }}",
"personal": {
"_": "?{ssn}", # Include personal info except SSN
"fullName": "{{ user.personal.firstName }} {{ user.personal.lastName }}"
},
"work": {
"_": "?{salary}", # Include work info except salary
"yearsOfService": "?{{ @diff_dates(@now(), user.professional.startDate, 'years') }}"
}
# internal section completely excluded
}
Conditional Field Inclusion
Use field selection with conditional logic:
# Source: order data
source = {
"orderId": "ord_123",
"customer": {
"id": "cust_456",
"name": "Charlie Brown",
"email": "charlie@example.com",
"tier": "premium"
},
"items": [
{"productId": "p1", "name": "Laptop", "price": 999.99, "cost": 600.00},
{"productId": "p2", "name": "Mouse", "price": 29.99, "cost": 15.00}
],
"payment": {
"method": "credit_card",
"cardLast4": "1234",
"total": 1029.98,
"processorFee": 30.90
},
"internal": {
"profit": 384.08,
"commission": 51.50
}
}
# Projection: customer receipt (exclude internal data and costs)
projection = {
"_": "?{internal}", # Exclude all internal data
"customer": {
"_": "{name, email}", # Only customer name and email
"&isPremium": "customer.tier == 'premium'"
},
"items": "?{{ @map(items, '{\"name\": name, \"price\": price}') }}", # Items without cost
"payment": {
"_": "?{processorFee}", # Payment info without fees
"last4": "{{ payment.cardLast4 }}"
},
"summary": {
"&itemCount": "@length(items)",
"orderTotal": "{{ payment.total }}"
}
}
Merging Behavior
When using the _
key, the selected fields are merged with your explicit mappings:
# Source data
source = {
"id": "item_123",
"name": "Smart Phone",
"price": 699.99,
"category": "electronics",
"inStock": True,
"description": "Latest smartphone with advanced features",
"internalCode": "INT_789",
"costPrice": 450.00
}
# Projection with field selection and additional mappings
projection = {
"_": "?{internalCode, costPrice}", # All fields except internal ones
"displayPrice": "${{ price }}", # Format price for display
"availability": "{{ @if_else(inStock, 'Available', 'Out of Stock') }}",
"&searchText": "@lowercase(name + ' ' + category)", # For search functionality
# Note: if there's a conflict, explicit mappings override selected fields
"name": "{{ @titlecase(name) }}" # Override the original name field
}
# Result: Merged fields with explicit mappings taking precedence
{
"id": "item_123", # From field selection
"name": "Smart Phone", # From explicit mapping (overrides)
"price": 699.99, # From field selection
"category": "electronics", # From field selection
"inStock": True, # From field selection
"description": "Latest smartphone...", # From field selection
"displayPrice": "$699.99", # From explicit mapping
"availability": "Available", # From explicit mapping
"searchText": "smart phone electronics" # From explicit mapping
# internalCode and costPrice excluded by field selection
}
Complex Field Selection Examples
Multi-level Data Sanitization
# Source: sensitive user data
source = {
"users": [
{
"id": "u1",
"profile": {
"name": "John Doe",
"email": "john@example.com",
"phone": "+1-555-0123",
"ssn": "123-45-6789"
},
"account": {
"balance": 1500.00,
"creditScore": 750,
"accountNumber": "ACC123456"
},
"preferences": {"theme": "dark"}
}
]
}
# Projection: safe user list for frontend
projection = {
"&sanitizedUsers": "@map(users, '{\"id\": id, \"profile\": {\"_\": \"?{ssn}\", \"displayName\": profile.name}, \"account\": {\"_\": \"?{accountNumber, creditScore}\"}, \"preferences\": preferences}')"
}
API Response Versioning
# Different API versions with different field requirements
base_data = {
"productId": "p123",
"name": "Wireless Headphones",
"price": 199.99,
"description": "High-quality wireless headphones",
"specifications": {"battery": "20h", "weight": "250g"},
"vendor": {"name": "AudioTech", "contactEmail": "vendor@audiotech.com"},
"internal": {"cost": 120.00, "margin": 79.99}
}
# V1 API - Basic fields only
v1_projection = {
"_": "{productId, name, price, description}",
"formattedPrice": "${{ price }}"
}
# V2 API - Include specifications
v2_projection = {
"_": "?{internal}", # All except internal
"specs": "?{{ specifications }}",
"vendor": {
"_": "?{contactEmail}", # Vendor without contact
"company": "{{ vendor.name }}"
}
}
# V3 API - Full details for partners
v3_projection = {
"_": "**", # Include everything
"partnerId": "{{ @md5(vendor.contactEmail) }}", # Add partner identifier
"&profitMargin": "internal.margin / price * 100"
}
The _
key provides flexible field selection that works seamlessly with DDO's template system, allowing you to create clean, secure, and appropriately scoped data projections for different use cases and audiences.
Template Composition
Build complex templates by composing simpler ones:
projection = {
# Base user info
"user": {
"name": "{{ firstName }} {{ lastName }}",
"contact": "{{ email }}"
},
# Computed properties using other projected fields
"summary": "User {{ firstName }} {{ lastName }} has {{ @length(orders) }} orders",
# Complex nested calculations
"analytics": {
"&lifetimeValue": "@sum(@map(orders, 'total'))",
"tier": "{{ @switch(@sum(@map(orders, 'total')), {'< 1000': 'Bronze', '< 5000': 'Silver', '>= 5000': 'Gold'}, 'Bronze') }}",
"&loyaltyScore": "@round(@div(@length(orders), @diff_dates(@now(), firstOrderDate, 'months')))"
}
}
Conditional Structure
Create entirely different structures based on data:
projection = {
# Always include basic info
"id": "{{ userId }}",
"type": "{{ userType }}",
# Conditional sections based on user type
"profile": "{{ @if_else(userType == 'business', '{\"company\": \"' + companyName + '\", \"industry\": \"' + industry + '\"}', '{\"firstName\": \"' + firstName + '\", \"lastName\": \"' + lastName + '\"}') }}",
# Different metrics for different user types
"metrics": {
"&primaryMetric": "@if_else(userType == 'business', revenue, orderCount)",
"label": "{{ @if_else(userType == 'business', 'Revenue', 'Orders') }}"
}
}
Projection provides a powerful way to reshape and transform your data without modifying the source, making it ideal for creating views, reports, API responses, and data exports in exactly the format you need.