Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
# Extract expansion variables from X into any selection of `Query.*`
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

from X , what's X here?

_inject_x: 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
Loading