Skip to content

chore: add initial snippet for @inject directive #75

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@ The snippets are generally broken up into functional areas, with each folder cov
### Custom Directives

- [@dbquery](dbquery/README.md) - Use `@dbquery` for connecting to databases, including pagination and filtering.
- [@inject](injection/README.md) - Use `@inject` for dependency injection, extracting context variables and making them available to multiple fields.
- [@materializer](materializer) - Use of `@materializer` to extend types by linking disparate backends into a single unified view.
- @rest - Connects to REST APIs
- [rest](rest/README.md) - Use of `@rest` for connecting to REST endpoints, including pagination.
Expand Down
27 changes: 27 additions & 0 deletions injection/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
# @inject

The `@inject` directive enables dependency injection in GraphQL schemas by allowing one field to inject expansion variables into other fields within the same request.

## How @inject Works

1. A field annotated with `@inject` is resolved first when any target field is accessed
2. The injecting field must return an object with key-value pairs
3. These pairs become expansion variables available to target fields
4. Target fields can access these variables using the standard expansion variable syntax
5. The `on` argument specifies which fields have access to the injected variables

## Schema Structure

```graphql
# Inject electronic product (JSON) data as expansion variables available to any Query field
_inject_electronic_products: JSON
@inject(on: [{ expose: true, types: "Query", fields: ".*" }])
@materializer(
query: "products"
arguments: { name: "category", const: "electronics" }
)
```

## Snippets

- [user-context](user-context) - Demonstrates simple user context injection for regional e-commerce operations with role-based filtering and currency conversion.
89 changes: 89 additions & 0 deletions injection/user-context/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
# Inject user context

This example demonstrates how to use the `@inject` directive.It provides a powerful way to extract context information (like user preferences, regional settings, and role) and make it available as expansion variables to multiple fields in a single request. This enables clean, reusable context patterns without repetitive parameter passing.

- **Injection field**: `_inject_user_context` - automatically resolved without parameters
- **Target fields**: `orders`, `products`, `recommendations` - can access injected variables
- **Context variables**: `preferred_region`, `currency`, `role`, `language`, `default_user_id`

## How It Works

1. **Context Extraction**: The `_inject_user_context` field provides default user context
2. **Automatic Injection**: Target fields automatically receive context as expansion variables
3. **Flexible Usage**: Target fields can use optional `userId` parameters to override defaults
4. **Shared Context**: Multiple operations in one request share the same injected context

**Note**: The injection field cannot have required parameters - it must be resolvable without arguments.

## Schema Structure

```graphql
_inject_user_context: JSON
@inject(on: [{ expose: true, types: "Query", fields: "orders|products|recommendations" }])
@value(script: { ... }) # Returns default context
```

## Example operations

### Using Default Context

```graphql
query UserDashboardDefault {
orders(limit: 3) { # Uses injected context
id
customerName
total
}
products(category: "electronics") { # Uses injected context
id
name
price
}
}
```

### Overriding with Explicit Parameters

```graphql
query UserDashboardExplicit($userId: ID!) {
orders(userId: $userId, limit: 3) { # Overrides default userId
id
customerName
total
}
}
```

## Try it out

Deploy the schema from `injection/user-context`:

```bash
stepzen deploy
```

### Sample Operations

1. **Get Orders by UserID:**

```bash
stepzen request -f operations.graphql --operation-name=UserOrdersExplicit --var userId=1
```

2. **Get Products by UserID:**

```bash
stepzen request -f operations.graphql --operation-name=UserProductsExplicit --var userId=2 --var category="electronics"
```

3. **Get Recommendations by userId:**

```bash
stepzen request -f operations.graphql --operation-name=UserRecommendationsExplicit --var userId=2 --var count=2
```

4. **multiple injected operations:**

```bash
stepzen request -f operations.graphql --operation-name=UserDashboardDefault
```
219 changes: 219 additions & 0 deletions injection/user-context/api.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,219 @@
# A simple user context injection example to demonstrate the @inject directive.
# This shows how user context can be (extracted and) injected into multiple operations.

type Order {
id: ID!
customerName: String!
total: Float!
status: String!
region: String!
}

type Product {
id: ID!
name: String!
category: String!
price: Float!
region: String!
inStock: Boolean!
}

type Recommendation {
productId: ID!
productName: String!
score: Float!
reason: String!
}

type Query {
"""
default user context becomes available as expansion variables to any field matching the visibility pattern.
"""
_inject_user_context: JSON
@inject(
on: [
{
expose: true
types: "Query"
fields: "orders|products|recommendations"
}
]
)
@value(
script: {
language: ECMASCRIPT
src: """
function getValue() {
// In real applications, this could come from headers or other sources.
return {
"preferred_region": "US_WEST",
"currency": "USD",
"role": "premium",
"language": "en",
"default_user_id": "1"
};
}
getValue()
"""
}
)

"""
Get orders filtered by user's preferred region and role.
Uses injected expansion variables: preferred_region, role, default_user_id etc.,
"""
orders(userId: ID, limit: Int = 10): [Order]
@rest(
endpoint: "stepzen:empty"
ecmascript: """
function transformREST(s) {
// Access injected expansion variables
var region = get('preferred_region');
var role = get('role');
var defaultUserId = get('default_user_id');
var userId = get('userId') || defaultUserId;
var limit = get('limit');

// Mock orders data
var allOrders = [
{id: "1", customerName: "Acme Corp", total: 1500.0, status: "completed", region: "US_WEST", userId: "1"},
{id: "2", customerName: "Tech Solutions", total: 850.0, status: "pending", region: "US_EAST", userId: "2"},
{id: "3", customerName: "Euro Marketing", total: 1200.0, status: "completed", region: "EU_WEST", userId: "2"},
{id: "4", customerName: "Asia Dynamics", total: 2500.0, status: "processing", region: "ASIA", userId: "3"},
{id: "5", customerName: "West Coast Inc", total: 1800.0, status: "completed", region: "US_WEST", userId: "1"},
{id: "6", customerName: "London Ltd", total: 950.0, status: "pending", region: "EU_WEST", userId: "2"}
];

// Filter by user ID first
var userOrders = allOrders.filter(function(order) {
return order.userId === userId;
});

// Filter by user's preferred region
var filteredOrders = userOrders.filter(function(order) {
return order.region === region;
});

// Role-based filtering
if (role === "standard") {
filteredOrders = filteredOrders.filter(function(order) {
return order.status === "completed";
});
}

// Apply limit
if (limit && limit > 0) {
filteredOrders = filteredOrders.slice(0, limit);
}

return JSON.stringify(filteredOrders);
}
"""
)

"""
Get products available in user's region with currency conversion.
Uses injected expansion variables: preferred_region, currency etc.,
"""
products(userId: ID, category: String): [Product]
@rest(
endpoint: "stepzen:empty"
ecmascript: """
function transformREST(s) {
var region = get('preferred_region');
var currency = get('currency');
var defaultUserId = get('default_user_id');
var userId = get('userId') || defaultUserId;
var category = get('category');

// Mock products data
var allProducts = [
{id: "p1", name: "Laptop Pro", category: "electronics", price: 1299.99, region: "US_WEST", inStock: true},
{id: "p2", name: "Office Chair", category: "furniture", price: 299.99, region: "US_WEST", inStock: true},
{id: "p3", name: "EU Laptop", category: "electronics", price: 1199.99, region: "EU_WEST", inStock: true},
{id: "p4", name: "EU Desk", category: "furniture", price: 399.99, region: "EU_WEST", inStock: false},
{id: "p5", name: "Asia Tablet", category: "electronics", price: 599.99, region: "ASIA", inStock: true},
{id: "p6", name: "Monitor 4K", category: "electronics", price: 499.99, region: "US_WEST", inStock: true}
];

// Filter by user's preferred region
var filteredProducts = allProducts.filter(function(product) {
return product.region === region;
});

// Filter by category if provided
if (category) {
filteredProducts = filteredProducts.filter(function(product) {
return product.category === category;
});
}

// Convert currency for EUR users
if (currency === "EUR") {
filteredProducts = filteredProducts.map(function(product) {
return Object.assign({}, product, {
price: Math.round(product.price * 0.85 * 100) / 100
});
});
}

return JSON.stringify(filteredProducts);
}
"""
)

"""
Get personalized product recommendations based on user context.
Uses injected expansion variables: preferred_region, role, language etc.,
"""
recommendations(userId: ID, count: Int = 5): [Recommendation]
@rest(
endpoint: "stepzen:empty"
ecmascript: """
function transformREST(s) {
var region = get('preferred_region');
var role = get('role');
var language = get('language');
var defaultUserId = get('default_user_id');
var userId = get('userId') || defaultUserId;
var count = get('count') || 5;

// Mock recommendations based on region (from injected context)
var recommendations = [];

if (region === "US_WEST") {
recommendations = [
{productId: "p1", productName: "Laptop Pro", score: 0.95, reason: "Popular in your region"},
{productId: "p6", productName: "Monitor 4K", score: 0.88, reason: "Great for productivity"},
{productId: "p2", productName: "Office Chair", score: 0.82, reason: "Highly rated locally"}
];
} else if (region === "EU_WEST") {
recommendations = [
{productId: "p3", productName: "EU Laptop", score: 0.92, reason: "EU optimized model"},
{productId: "p4", productName: "EU Desk", score: 0.79, reason: "Matches local preferences"},
{productId: "p7", productName: "EU Monitor", score: 0.85, reason: "Energy efficient"}
];
} else if (region === "ASIA") {
recommendations = [
{productId: "p5", productName: "Asia Tablet", score: 0.90, reason: "Regional bestseller"},
{productId: "p8", productName: "Wireless Mouse", score: 0.84, reason: "Compact design"},
{productId: "p9", productName: "Keyboard Pro", score: 0.81, reason: "Multi-language support"}
];
}

// Premium users get enhanced recommendations (from injected context)
if (role === "premium") {
recommendations.forEach(function(rec) {
rec.score += 0.05;
rec.reason = "Premium: " + rec.reason;
});
}

// Limit count
recommendations = recommendations.slice(0, count);

return JSON.stringify(recommendations);
}
"""
)
}
19 changes: 19 additions & 0 deletions injection/user-context/index.graphql
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
schema
@sdl(
files: ["api.graphql"]
# Visibility controls how fields are exposed to GraphQL introspection
# and field references through @materializer, @inject, etc.
#
# Only expose the main query fields that users should interact with.
# The _inject_user_context field is hidden from external schema but
# still accessible for injection into the target fields.
visibility: [
{
expose: true
types: "Query"
fields: "orders|products|recommendations"
}
]
) {
query: Query
}
Loading