Skip to main content

Database Actions

Create reusable query templates that can be executed with different input parameters. Database actions allow you to define queries once and reuse them across your application with variable interpolation.

Quick Example

// Create an action template
await ductape.database.action.create({
name: 'Get Users Paginated',
tag: 'postgresdb:get-users-paginated',
tableName: 'users',
operation: DatabaseActionTypes.QUERY,
description: 'Fetch paginated list of active users',
template: {
where: {
is_active: true,
},
limit: '{{limit}}',
offset: '{{offset}}',
orderBy: [
{
column: '{{orderColumn}}',
order: '{{orderDirection}}',
},
],
},
});

// Execute the action with different inputs
const users = await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'get-users-paginated',
input: {
limit: 25,
offset: 0,
orderColumn: 'created_at',
orderDirection: 'DESC',
},
});

Why Use Actions?

Benefits:

  • Reusability - Define query logic once, use everywhere
  • Type Safety - Template validation at creation time
  • Variable Interpolation - Dynamic queries with {{placeholder}} syntax
  • Maintainability - Update query logic in one place
  • Security - Centralized query management
  • Testing - Easy to test query templates

Action Types

QUERY - Read Data

Fetch records from a table:

await ductape.database.action.create({
name: 'Get Active Users',
tag: 'postgresdb:get-active-users',
tableName: 'users',
operation: DatabaseActionTypes.QUERY,
template: {
where: {
status: '{{status}}',
created_at: { $GTE: '{{startDate}}' },
},
select: ['id', 'name', 'email', 'created_at'],
limit: '{{limit}}',
orderBy: [{ column: 'created_at', order: 'DESC' }],
},
});

// Execute
const users = await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'get-active-users',
input: {
status: 'active',
startDate: '2024-01-01',
limit: 50,
},
});

INSERT - Create Records

Insert new records:

await ductape.database.action.create({
name: 'Create User',
tag: 'postgresdb:create-user',
tableName: 'users',
operation: DatabaseActionTypes.INSERT,
template: {
records: [
{
name: '{{name}}',
email: '{{email}}',
status: 'active',
created_at: '{{createdAt}}',
},
],
},
});

// Execute
await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'create-user',
input: {
name: 'Alice Johnson',
email: 'alice@example.com',
createdAt: new Date().toISOString(),
},
});

UPDATE - Modify Records

Update existing records:

await ductape.database.action.create({
name: 'Update User Status',
tag: 'postgresdb:update-user-status',
tableName: 'users',
operation: DatabaseActionTypes.UPDATE,
template: {
status: '{{status}}',
updated_at: '{{updatedAt}}',
},
filterTemplate: {
id: '{{userId}}',
},
});

// Execute
await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'update-user-status',
input: {
userId: 123,
status: 'suspended',
updatedAt: new Date().toISOString(),
},
});

DELETE - Remove Records

Delete records:

await ductape.database.action.create({
name: 'Delete Old Sessions',
tag: 'postgresdb:delete-old-sessions',
tableName: 'sessions',
operation: DatabaseActionTypes.DELETE,
filterTemplate: {
expires_at: { $LT: '{{expirationDate}}' },
},
});

// Execute
await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'delete-old-sessions',
input: {
expirationDate: new Date().toISOString(),
},
});

UPSERT - Insert or Update

Insert new record or update if exists:

await ductape.database.action.create({
name: 'Upsert User Preferences',
tag: 'postgresdb:upsert-user-prefs',
tableName: 'user_preferences',
operation: DatabaseActionTypes.UPSERT,
template: {
records: [
{
user_id: '{{userId}}',
theme: '{{theme}}',
notifications: '{{notifications}}',
updated_at: '{{updatedAt}}',
},
],
conflictColumns: ['user_id'],
},
});

// Execute
await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'upsert-user-prefs',
input: {
userId: 123,
theme: 'dark',
notifications: true,
updatedAt: new Date().toISOString(),
},
});

AGGREGATE - Aggregations

Perform aggregations using the operations object with $COUNT, $SUM, $AVG, $MIN, $MAX:

await ductape.database.action.create({
name: 'User Statistics',
tag: 'postgresdb:user-statistics',
tableName: 'users',
operation: DatabaseActionTypes.QUERY,
template: {
where: {
status: '{{status}}',
created_at: { $GTE: '{{startDate}}' },
},
operations: {
total_count: { $COUNT: '*' },
avg_age: { $AVG: 'age' },
min_age: { $MIN: 'age' },
max_age: { $MAX: 'age' },
},
},
});

// Execute
const result = await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'user-statistics',
input: {
status: 'active',
startDate: '2024-01-01',
},
});

console.log('Statistics:', result);
// { total_count: 150, avg_age: 32.5, min_age: 18, max_age: 65 }

Single Aggregation:

await ductape.database.action.create({
name: 'Total Sales',
tag: 'postgresdb:total-sales',
tableName: 'orders',
operation: DatabaseActionTypes.QUERY,
template: {
where: {
status: 'completed',
created_at: {
$GTE: '{{startDate}}',
$LTE: '{{endDate}}',
},
},
operations: {
total_revenue: { $SUM: 'total' },
},
},
});

RAW_SQL - Custom Queries

Execute raw SQL:

await ductape.database.action.create({
name: 'Complex User Report',
tag: 'postgresdb:complex-user-report',
tableName: 'users', // Required but not used for raw SQL
operation: DatabaseActionTypes.RAW_SQL,
template: {
query: `
SELECT
u.id,
u.name,
COUNT(o.id) as order_count,
SUM(o.total) as total_spent
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE u.status = $1
AND o.created_at >= $2
GROUP BY u.id, u.name
HAVING COUNT(o.id) > $3
ORDER BY total_spent DESC
LIMIT $4
`,
params: ['{{status}}', '{{startDate}}', '{{minOrders}}', '{{limit}}'],
},
});

// Execute
const report = await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'complex-user-report',
input: {
status: 'active',
startDate: '2024-01-01',
minOrders: 5,
limit: 100,
},
});

Variable Interpolation

Basic Placeholders

Use {{variableName}} syntax:

template: {
where: {
status: '{{status}}',
age: { $GT: '{{minAge}}' },
},
limit: '{{limit}}',
}

Nested Placeholders

Placeholders work in nested structures:

template: {
where: {
$OR: {
email: '{{email}}',
username: '{{username}}',
},
},
select: ['{{field1}}', '{{field2}}', '{{field3}}'],
}

Array Placeholders

Use in arrays:

template: {
where: {
status: { $IN: ['{{status1}}', '{{status2}}', '{{status3}}'] },
},
}

Default Values

Provide defaults in input:

await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'get-users',
input: {
limit: 50, // Default limit
offset: 0, // Default offset
},
});

Managing Actions

Create Action

await ductape.database.action.create({
name: 'Action Name',
tag: 'database-tag:action-tag', // Format: database:action
tableName: 'table_name',
operation: DatabaseActionTypes.QUERY,
description: 'Optional description',
template: {
// Your query template
},
});

Required Fields:

FieldTypeDescription
namestringDisplay name for the action
tagstringUnique identifier (format: database:action)
tableNamestringTable to operate on
operationDatabaseActionTypesAction operation (QUERY, INSERT, etc.)
templateobjectQuery template with placeholders

Optional Fields:

FieldTypeDescription
descriptionstringAction description
filterTemplateobjectFilter criteria (for UPDATE/DELETE)

Update Action

await ductape.database.action.update({
tag: 'postgresdb:get-users',
template: {
// Updated template
where: {
status: '{{status}}',
},
limit: '{{limit}}',
},
});

Fetch Action

const action = await ductape.database.action.fetch('postgresdb:get-users');
console.log('Action:', action);

List Actions for Database

const actions = await ductape.database.action.fetchAll('postgresdb');
console.log(`Found ${actions.length} actions`);

actions.forEach(action => {
console.log(`${action.tag}: ${action.name} (${action.type})`);
});

Execute Actions

Basic Execution

const result = await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'get-users',
input: {
status: 'active',
limit: 50,
},
});

With Type Safety

interface User {
id: number;
name: string;
email: string;
status: string;
}

const users = await ductape.database.execute<User[]>({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'get-users',
input: { status: 'active' },
});

// TypeScript knows users is User[]
users.forEach(user => {
console.log(`${user.name} - ${user.email}`);
});

Error Handling

try {
const users = await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'get-users',
input: { status: 'active' },
});
} catch (error) {
if (error.message.includes('not found')) {
console.error('Action not found');
} else if (error.message.includes('validation')) {
console.error('Invalid input parameters');
} else {
console.error('Execution failed:', error.message);
}
}

Common Patterns

Paginated Queries

await ductape.database.action.create({
name: 'Get Products Paginated',
tag: 'postgresdb:get-products-paginated',
tableName: 'products',
operation: DatabaseActionTypes.QUERY,
template: {
where: {
category: '{{category}}',
in_stock: true,
},
select: ['id', 'name', 'price', 'stock'],
limit: '{{limit}}',
offset: '{{offset}}',
orderBy: [
{ column: '{{sortBy}}', order: '{{sortOrder}}' },
],
},
});

// Pagination helper
async function getProductsPage(
category: string,
page: number = 1,
pageSize: number = 20
) {
return ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'get-products-paginated',
input: {
category,
limit: pageSize,
offset: (page - 1) * pageSize,
sortBy: 'name',
sortOrder: 'ASC',
},
});
}

Search Actions

await ductape.database.action.create({
name: 'Search Users',
tag: 'postgresdb:search-users',
tableName: 'users',
operation: DatabaseActionTypes.QUERY,
template: {
where: {
$OR: {
name: { $CONTAINS: '{{query}}' },
email: { $CONTAINS: '{{query}}' },
},
status: 'active',
},
limit: '{{limit}}',
},
});

Batch Inserts

await ductape.database.action.create({
name: 'Bulk Create Orders',
tag: 'postgresdb:bulk-create-orders',
tableName: 'orders',
operation: DatabaseActionTypes.INSERT,
template: {
records: '{{orders}}', // Array of order objects
},
});

// Execute with array
await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'bulk-create-orders',
input: {
orders: [
{ user_id: 1, total: 99.99, status: 'pending' },
{ user_id: 2, total: 149.99, status: 'pending' },
{ user_id: 3, total: 79.99, status: 'pending' },
],
},
});

Conditional Updates

await ductape.database.action.create({
name: 'Deactivate Inactive Users',
tag: 'postgresdb:deactivate-inactive-users',
tableName: 'users',
operation: DatabaseActionTypes.UPDATE,
template: {
status: 'inactive',
deactivated_at: '{{deactivatedAt}}',
},
filterTemplate: {
last_login: { $LT: '{{cutoffDate}}' },
status: 'active',
},
});

Reporting Actions

Use aggregations with operations for analytics:

await ductape.database.action.create({
name: 'Sales Summary',
tag: 'postgresdb:sales-summary',
tableName: 'orders',
operation: DatabaseActionTypes.AGGREGATE,
template: {
where: {
status: 'completed',
created_at: {
$GTE: '{{startDate}}',
$LTE: '{{endDate}}',
},
},
operations: {
total_revenue: { $SUM: 'total' },
order_count: { $COUNT: 'id' },
avg_order_value: { $AVG: 'total' },
highest_order: { $MAX: 'total' },
lowest_order: { $MIN: 'total' },
},
},
});

// Execute
const summary = await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'sales-summary',
input: {
startDate: '2024-01-01',
endDate: '2024-12-31',
},
});

console.log('Summary:', summary);
// {
// total_revenue: 125000.50,
// order_count: 850,
// avg_order_value: 147.06,
// highest_order: 2500.00,
// lowest_order: 9.99
// }

Best Practices

1. Use Descriptive Names

//  Good - clear purpose
await ductape.database.action.create({
name: 'Get Active Users Created This Month',
tag: 'postgresdb:get-active-users-this-month',
// ...
});

// ❌ Avoid - vague
await ductape.database.action.create({
name: 'Get Users',
tag: 'postgresdb:users',
// ...
});

2. Add Descriptions

await ductape.database.action.create({
name: 'Get Overdue Invoices',
tag: 'postgresdb:get-overdue-invoices',
description: 'Fetches all unpaid invoices past their due date for reminder emails',
// ...
});

3. Validate Input

function validateInput(input: any) {
if (!input.limit || input.limit > 1000) {
throw new Error('Limit must be between 1 and 1000');
}
if (input.offset < 0) {
throw new Error('Offset must be non-negative');
}
}

const users = await ductape.database.execute({
product: 'my-product',
env: 'prd',
database: 'postgresdb',
action: 'get-users',
input: validateInput(userInput),
});

4. Use Consistent Naming

//  Good - consistent pattern
'postgresdb:get-users'
'postgresdb:create-user'
'postgresdb:update-user'
'postgresdb:delete-user'

// ❌ Avoid - inconsistent
'postgresdb:getUsers'
'postgresdb:user-create'
'postgresdb:UpdateUser'
'postgresdb:del_user'

5. Keep Actions Focused

//  Good - single purpose
await ductape.database.action.create({
name: 'Get User Orders',
tag: 'postgresdb:get-user-orders',
operation: DatabaseActionTypes.QUERY,
// ... fetch only orders
});

// ❌ Avoid - too complex
await ductape.database.action.create({
name: 'Get User Complete Profile',
tag: 'postgresdb:get-user-profile',
operation: DatabaseActionTypes.RAW_SQL,
template: {
query: `
SELECT users.*, orders.*, addresses.*, payments.*
FROM users
LEFT JOIN orders ...
LEFT JOIN addresses ...
LEFT JOIN payments ...
-- Too many joins, should be separate actions
`,
},
});

6. Limit Result Sets

//  Always include limit
template: {
where: { status: '{{status}}' },
limit: '{{limit}}', // Prevent unbounded queries
}

7. Test Actions

// Test action with various inputs
describe('get-users-paginated', () => {
it('should fetch first page', async () => {
const users = await ductape.database.execute({
product: 'test-product',
env: 'test',
database: 'testdb',
action: 'get-users-paginated',
input: { limit: 10, offset: 0 },
});

expect(users).toHaveLength(10);
});

it('should respect offset', async () => {
const page2 = await ductape.database.execute({
product: 'test-product',
env: 'test',
database: 'testdb',
action: 'get-users-paginated',
input: { limit: 10, offset: 10 },
});

expect(page2[0].id).toBeGreaterThan(10);
});
});

Next Steps

See Also