Examples
Complete real-world examples demonstrating how to use Sentro in various scenarios.
Basic Blog Application
A simple blog with users, posts, and comments:
import Connector from "@sentrodb/connector-node";
import ConnectorHelperSql from "@sentrodb/connector-helper-sql";
import { ModelCustomizer } from "@sentrodb/connector-node/dist/inc/customizers/modelCustomizer";
const connector = new Connector({
secretKey: process.env.CONNECTOR_SECRET_KEY!,
db: {
host: "localhost",
port: 5432,
user: "postgres",
password: "password",
database: "blog",
type: "postgres"
}
});
await connector.setDatabaseHandler(new ConnectorHelperSql());
// Users customization
connector.customize(() => {
const c = new ModelCustomizer("users");
c.rename("Blog Authors");
c.renameColumn("email", "Email Address");
c.renameColumn("created_at", "Joined Date");
// Add full name display field
c.addDisplayField("fullName", (user) => {
return `${user.firstName} ${user.lastName}`;
});
// Hash password on create
c.onBefore("CREATE", async (payload) => {
if (payload.password) {
const bcrypt = require('bcrypt');
payload.password = await bcrypt.hash(payload.password, 10);
}
return payload;
});
// Remove password from responses
c.onAfter("READ", (result) => {
result.rows.forEach(user => delete user.password);
return result;
});
return c;
});
// Posts customization
connector.customize(() => {
const c = new ModelCustomizer("posts");
c.rename("Blog Posts");
c.renameColumn("created_at", "Published Date");
// Auto-generate slug from title
c.replaceFieldWriting("title", (value) => {
const title = String(value);
const slug = title
.toLowerCase()
.replace(/[^a-z0-9]+/g, '-')
.replace(/^-+|-+$/g, '');
return { title, slug };
});
// Set timestamps
c.onBefore("CREATE", (payload) => {
payload.created_at = new Date().toISOString();
payload.updated_at = new Date().toISOString();
return payload;
});
c.onBefore("UPDATE", (payload) => {
payload.patch.updated_at = new Date().toISOString();
return payload;
});
// Export to markdown action
c.addAction({
type: "table",
id: "export-markdown",
label: "Export to Markdown",
callback: async (request, records) => {
const markdown = records.map(post => {
return `# ${post.title}\n\n${post.content}\n\n---\n`;
}).join('\n');
return {
success: true,
data: markdown,
filename: 'blog-posts.md'
};
}
});
return c;
});
// Comments customization
connector.customize(() => {
const c = new ModelCustomizer("comments");
c.rename("Comments");
// Auto-approve or flag for moderation
c.onBefore("CREATE", async (payload) => {
// Simple spam detection
const hasSpamWords = ['viagra', 'casino', 'lottery']
.some(word => payload.text.toLowerCase().includes(word));
payload.status = hasSpamWords ? 'pending' : 'approved';
payload.created_at = new Date().toISOString();
return payload;
});
// Moderate comment action
c.addAction({
type: "detail",
id: "approve-comment",
label: "Approve Comment",
callback: async (request, record, db) => {
await db.update({
table: 'comments',
data: { status: 'approved' },
where: { id: record.id }
});
return { success: true, message: 'Comment approved' };
}
});
return c;
});
await connector.start();
connector.startStandaloneServer({ port: 4000 });E-commerce Store
Product catalog with orders and payments:
import { S3Integration } from "@sentrodb/integration-s3";
import { StripeIntegration } from "./integrations/stripe";
// Register integrations
connector.use('s3', new S3Integration({
region: 'us-east-1',
bucket: 'products',
credentials: {
accessKeyId: process.env.AWS_ACCESS_KEY_ID!,
secretAccessKey: process.env.AWS_SECRET_ACCESS_KEY!
}
}));
connector.use('stripe', new StripeIntegration(
process.env.STRIPE_SECRET_KEY!
));
// Products customization
connector.customize(() => {
const c = new ModelCustomizer("products");
c.rename("Product Catalog");
// Upload product images to S3
c.replaceFieldWriting("image", async (value) => {
if (typeof value === 'string' && value.startsWith('data:')) {
const s3 = connector.using<S3Integration>('s3');
const buffer = Buffer.from(value.split(',')[1], 'base64');
const result = await s3.prepareUpload({
key: `products/${Date.now()}.jpg`,
fileSize: buffer.length,
contentType: 'image/jpeg'
});
return { image: result.url };
}
return { image: value };
});
// Add formatted price display field
c.addDisplayField("formattedPrice", (product) => {
return `$${product.price.toFixed(2)}`;
});
// Add stock status display field
c.addDisplayField("stockStatus", (product) => {
if (product.stock === 0) return "Out of Stock";
if (product.stock < 10) return "Low Stock";
return "In Stock";
});
// Update inventory action
c.addAction({
type: "detail",
id: "adjust-inventory",
label: "Adjust Inventory",
callback: async (request, record, db) => {
const adjustment = request.body.adjustment;
const newStock = record.stock + adjustment;
await db.update({
table: 'products',
data: { stock: newStock },
where: { id: record.id }
});
return {
success: true,
newStock,
message: `Stock adjusted by ${adjustment}`
};
}
});
return c;
});
// Orders customization
connector.customize(() => {
const c = new ModelCustomizer("orders");
c.rename("Customer Orders");
// Calculate total with tax
c.addDisplayField("totalWithTax", (order) => {
return order.total * 1.1; // 10% tax
});
c.addDisplayField("formattedTotal", (order) => {
return `$${order.total.toFixed(2)}`;
});
// Process payment on create
c.onAfter("CREATE", async (result) => {
const order = result.rows[0];
const stripe = connector.using<StripeIntegration>('stripe');
if (stripe && order.status === 'pending') {
// Create payment intent
const paymentIntent = await stripe.createPaymentIntent(
Math.round(order.total * 100), // Convert to cents
'usd'
);
// Update order with payment intent ID
// (In real app, this would be done before returning)
}
return result;
});
// Generate invoice action
c.addAction({
type: "detail",
id: "generate-invoice",
label: "Generate Invoice",
callback: async (request, record, db) => {
// Fetch order items
const items = await db.get({
table: 'order_items',
where: { orderId: record.id }
});
// Generate PDF (pseudo code)
const pdf = generateInvoicePDF({
order: record,
items: items.rows
});
return {
success: true,
url: pdf.url,
message: "Invoice generated"
};
}
});
return c;
});SaaS Application
Multi-tenant SaaS with organizations and team members:
// Organizations customization
connector.customize(() => {
const c = new ModelCustomizer("organizations");
c.rename("Organizations");
// Create Stripe customer on organization creation
c.onAfter("CREATE", async (result) => {
const org = result.rows[0];
const stripe = connector.using<StripeIntegration>('stripe');
if (stripe) {
const customer = await stripe.createCustomer(
org.email,
org.name
);
await db.update({
table: 'organizations',
data: { stripeCustomerId: customer.id },
where: { id: org.id }
});
}
return result;
});
// Upgrade subscription action
c.addAction({
type: "detail",
id: "upgrade-plan",
label: "Upgrade Plan",
callback: async (request, record, db) => {
const { planId } = request.body;
const stripe = connector.using<StripeIntegration>('stripe');
if (!stripe || !record.stripeCustomerId) {
return { success: false, error: 'Stripe not configured' };
}
const subscription = await stripe.createSubscription(
record.stripeCustomerId,
planId
);
await db.update({
table: 'organizations',
data: {
plan: planId,
subscriptionId: subscription.id
},
where: { id: record.id }
});
return {
success: true,
message: `Upgraded to ${planId} plan`
};
}
});
return c;
});
// Team members customization
connector.customize(() => {
const c = new ModelCustomizer("team_members");
c.rename("Team Members");
// Send invitation email on create
c.onAfter("CREATE", async (result) => {
const member = result.rows[0];
const email = connector.using<EmailService>('email');
if (email) {
await email.sendTemplate(
member.email,
'team-invitation',
{
orgName: member.organizationName,
inviteLink: `https://app.com/accept/${member.inviteToken}`
}
);
}
return result;
});
// Resend invitation action
c.addAction({
type: "detail",
id: "resend-invite",
label: "Resend Invitation",
callback: async (request, record, db) => {
const email = connector.using<EmailService>('email');
if (!email) {
return { success: false, error: 'Email not configured' };
}
await email.sendTemplate(
record.email,
'team-invitation',
{
orgName: record.organizationName,
inviteLink: `https://app.com/accept/${record.inviteToken}`
}
);
return {
success: true,
message: 'Invitation resent'
};
}
});
return c;
});Real Estate Platform
Property listings with geocoding and image uploads:
// Properties customization
connector.customize(() => {
const c = new ModelCustomizer("properties");
c.rename("Property Listings");
// Geocode address on create/update
c.replaceFieldWriting("address", async (value) => {
const address = String(value);
// Geocode the address
const response = await fetch(
`https://maps.googleapis.com/maps/api/geocode/json?address=${encodeURIComponent(address)}&key=${process.env.GOOGLE_MAPS_KEY}`
);
const data = await response.json();
if (data.results.length > 0) {
const location = data.results[0].geometry.location;
return {
address,
latitude: location.lat,
longitude: location.lng
};
}
return { address };
});
// Upload property images to S3
c.replaceFieldWriting("images", async (value) => {
if (!Array.isArray(value)) return { images: '[]' };
const s3 = connector.using<S3Integration>('s3');
const uploadedUrls: string[] = [];
for (const img of value) {
if (typeof img === 'string' && img.startsWith('data:')) {
const buffer = Buffer.from(img.split(',')[1], 'base64');
const result = await s3.prepareUpload({
key: `properties/${Date.now()}-${Math.random()}.jpg`,
fileSize: buffer.length,
contentType: 'image/jpeg'
});
uploadedUrls.push(result.url);
} else {
uploadedUrls.push(img);
}
}
return { images: JSON.stringify(uploadedUrls) };
});
// Add formatted price
c.addDisplayField("formattedPrice", (property) => {
return new Intl.NumberFormat('en-US', {
style: 'currency',
currency: 'USD'
}).format(property.price);
});
// Add price per sqft
c.addDisplayField("pricePerSqft", (property) => {
if (property.sqft > 0) {
return `$${(property.price / property.sqft).toFixed(2)}/sqft`;
}
return 'N/A';
});
// Generate listing PDF
c.addAction({
type: "detail",
id: "generate-pdf",
label: "Generate Listing PDF",
callback: async (request, record, db) => {
const pdf = await generatePropertyPDF(record);
return {
success: true,
url: pdf.url,
message: "PDF generated"
};
}
});
return c;
});Event Management Platform
Events with registrations and email notifications:
// Events customization
connector.customize(() => {
const c = new ModelCustomizer("events");
c.rename("Events");
// Add attendance display fields
c.addDisplayField("spotsRemaining", (event) => {
return event.maxAttendees - event.currentAttendees;
});
c.addDisplayField("isFull", (event) => {
return event.currentAttendees >= event.maxAttendees;
});
// Format dates
c.addDisplayField("formattedDate", (event) => {
return new Date(event.startDate).toLocaleDateString('en-US', {
weekday: 'long',
year: 'numeric',
month: 'long',
day: 'numeric'
});
});
// Send event update emails
c.addAction({
type: "detail",
id: "notify-attendees",
label: "Notify Attendees",
callback: async (request, record, db) => {
const { message } = request.body;
// Get all registrations
const registrations = await db.get({
table: 'registrations',
where: { eventId: record.id }
});
const email = connector.using<EmailService>('email');
for (const reg of registrations.rows) {
await email.send({
to: reg.email,
subject: `Update: ${record.title}`,
html: message
});
}
return {
success: true,
sent: registrations.rows.length,
message: `Notified ${registrations.rows.length} attendees`
};
}
});
return c;
});
// Registrations customization
connector.customize(() => {
const c = new ModelCustomizer("registrations");
c.rename("Event Registrations");
// Check capacity and send confirmation
c.onBefore("CREATE", async (payload, ctx) => {
const event = await db.getSingle({
table: 'events',
where: { id: payload.eventId }
});
if (event.currentAttendees >= event.maxAttendees) {
throw new Error('Event is full');
}
payload.registeredAt = new Date().toISOString();
return payload;
});
c.onAfter("CREATE", async (result) => {
const registration = result.rows[0];
// Increment attendee count
await db.update({
table: 'events',
data: { currentAttendees: db.raw('current_attendees + 1') },
where: { id: registration.eventId }
});
// Send confirmation email
const email = connector.using<EmailService>('email');
const event = await db.getSingle({
table: 'events',
where: { id: registration.eventId }
});
if (email) {
await email.sendTemplate(
registration.email,
'event-confirmation',
{
eventName: event.title,
eventDate: event.startDate,
attendeeName: registration.name
}
);
}
return result;
});
return c;
});Best Practices from Examples
- Use descriptive display names for better UX
- Add computed display fields for frequently needed data
- Hash passwords and remove sensitive data in hooks
- Use field writers for file uploads and external API calls
- Implement validation in before hooks
- Send notifications asynchronously in after hooks
- Use integrations for third-party services
- Add custom actions for business operations
- Keep actions focused and single-purpose
- Handle errors gracefully with meaningful messages
Ready to Build? You now have all the knowledge needed to build powerful database management APIs with Sentro. Start by reviewing the Getting Started guide and build your first application!