cap-toolkit
v1.0.1
Published
Utility SDK for SAP CAP projects - eliminates repetitive boilerplate code
Downloads
163
Maintainers
Readme
cap-toolkit
🚀 Eliminate repetitive code in your CAP projects!
I've consolidated the boilerplate code you constantly write in SAP CAP projects into a single package. Ready-to-use utilities for handlers, authentication, authorization, validation, and query operations.
✨ Features
- ✅ Handler Builder - Easily create Before/After handlers
- ✅ Auth & Authorization - Role-based access control
- ✅ Validation - 15+ different validation rules
- ✅ Query Helpers - Helpers for frequently used queries
- ✅ TypeScript - Full type support
📦 Installation
npm install cap-toolkit🚀 Quick Start
Basic Usage
const { HandlerBuilder, Validator, AuthManager } = require('cap-toolkit');
module.exports = (srv) => {
// Set up all handlers at once
new HandlerBuilder(srv, 'Books')
.requireAuth() // Authentication check
.requireRole('Admin', 'Editor') // Role check
.withAudit() // Auto-fill audit fields
.before('CREATE', Validator.required(['title', 'author']))
.before('CREATE', Validator.email('contactEmail'))
.build();
};📚 Detailed Usage
1️⃣ Handler Builder
The most powerful feature! Create handlers in a chaining fashion with Fluent API.
const { HandlerBuilder, Validator } = require('cap-toolkit');
new HandlerBuilder(srv, 'Products')
.requireAuth() // Authentication
.requireRole('User', 'Admin') // Authorization
.withAudit() // Auto-fill createdBy, modifiedBy
.before('CREATE', [
Validator.required(['name', 'price']),
Validator.minLength('name', 3),
Validator.range('price', 0, 1000000)
])
.after('CREATE', async (result, req) => {
console.log('New product added:', result);
})
.build();Other Handler Builder Features:
// Read-only entity
builder.readOnly();
// Soft delete
builder.withSoftDelete('isDeleted');
builder.withSoftDelete('isDeleted', 'bookId'); // Custom key field
// Ownership check
builder.requireOwnership('createdBy');
builder.requireOwnership('createdBy', 'bookId'); // Custom key field
// Rate limiting
builder.withRateLimit(10, 60000); // 10 req/min2️⃣ Authentication & Authorization
const { AuthManager } = require('cap-toolkit');
// In service handler
srv.before('CREATE', 'Orders', AuthManager.requireAuth);
srv.before('DELETE', 'Orders', AuthManager.requireRole('Admin'));
// Ownership check
srv.before('UPDATE', 'Posts', async (req) => {
await AuthManager.checkOwnership(req, 'Posts', 'authorId');
});
// Ownership check with custom key field
srv.before('UPDATE', 'Posts', async (req) => {
await AuthManager.checkOwnership(req, 'Posts', 'authorId', 'postId');
});
// All roles required
srv.before('*', 'SensitiveData',
AuthManager.requireAll('Admin', 'SecurityOfficer')
);
// Custom authorization
srv.before('approve', AuthManager.custom((req, user) => {
return user.department === 'Finance' || user.roles.includes('Manager');
}));3️⃣ Validation (15+ Rules)
const { Validator } = require('cap-toolkit');
new HandlerBuilder(srv, 'Users')
.before('CREATE', [
Validator.required(['email', 'name', 'password']),
Validator.email('email'),
Validator.minLength('password', 8),
Validator.maxLength('name', 100),
Validator.pattern('phone', /^\d{10}$/, 'Phone must be 10 digits'),
Validator.unique('Users', 'email'), // default key: ID
// Validator.unique('Users', 'email', 'userId'), // custom key field
Validator.url('website'),
Validator.positive('age'),
Validator.integer('age'),
Validator.range('age', 18, 100),
Validator.futureDate('startDate'),
Validator.pastDate('birthDate')
])
.build();
// Custom validation
const customValidator = Validator.custom((data) => {
if (data.password !== data.confirmPassword) {
return 'Passwords do not match';
}
});
// Combine validators
const combinedValidator = Validator.combine(
Validator.required('email'),
Validator.email('email'),
Validator.unique('Users', 'email')
);4️⃣ Query Helper
All QueryHelper methods require a request context (req) as the first parameter.
const { QueryHelper } = require('cap-toolkit');
// Find by ID
const book = await QueryHelper.findById(req, 'Books', bookId);
const book2 = await QueryHelper.findById(req, 'Books', bookId, 'bookId'); // custom key field
// Get all records (filtering, sorting, limit)
const books = await QueryHelper.findAll(req, 'Books', {
where: { category: 'Fiction' },
orderBy: ['title', 'author'],
limit: 10,
offset: 20
});
// Check if exists
const exists = await QueryHelper.exists(req, 'Books', { isbn: '123456' });
// Count
const count = await QueryHelper.count(req, 'Books', { category: 'Fiction' });
// Upsert (update or insert)
await QueryHelper.upsert(req, 'Books', bookData, ['isbn']);
await QueryHelper.upsert(req, 'Books', bookData, ['isbn'], 'bookId'); // custom key field
// Bulk operations
await QueryHelper.insertMany(req, 'Books', [book1, book2, book3]);
await QueryHelper.updateMany(req, 'Books', { status: 'archived' }, { year: { '<': 2020 } });
await QueryHelper.deleteMany(req, 'Books', [1, 2, 3, 4, 5]);
await QueryHelper.deleteMany(req, 'Books', ['b1', 'b2'], 'bookId'); // custom key field
// Distinct values
const categories = await QueryHelper.distinct(req, 'Books', 'category');
// Pagination
const result = await QueryHelper.paginate(req, 'Books', 2, 20, {
where: { category: 'Fiction' },
orderBy: 'title'
});
// { data, total, page, pageSize, totalPages, hasNext, hasPrev }
// Aggregate functions
const avgPrice = await QueryHelper.aggregate(req, 'Books', 'avg', 'price');
const totalStock = await QueryHelper.aggregate(req, 'Products', 'sum', 'stock');🎯 Complete Example
const cds = require('@sap/cds');
const {
HandlerBuilder,
Validator,
AuthManager,
QueryHelper
} = require('cap-toolkit');
module.exports = cds.service.impl(async function() {
const { Books, Authors } = this.entities;
// Complete handler setup for Books entity
new HandlerBuilder(this, 'Books')
.requireAuth()
.requireRole('User', 'Admin')
.withAudit()
.before('CREATE', [
Validator.required(['title', 'authorId', 'isbn']),
Validator.minLength('title', 3),
Validator.unique('Books', 'isbn'),
async (req) => {
const authorExists = await QueryHelper.exists(req, 'Authors', {
ID: req.data.authorId
});
if (!authorExists) {
req.reject(403, 'Related record not found');
}
}
])
.after('CREATE', async (result, req) => {
// Increment author's book count
await QueryHelper.updateMany(req, 'Authors',
{ bookCount: { '+=': 1 } },
{ ID: req.data.authorId }
);
})
.before('DELETE', AuthManager.requireRole('Admin'))
.build();
// Custom action - Book search
this.on('searchBooks', async (req) => {
const { search, category, minPrice, maxPrice } = req.data;
let query = SELECT.from('Books');
// Pagination
const result = await QueryHelper.paginate(req, 'Books',
req.data.page || 1,
req.data.pageSize || 20,
{ orderBy: 'title' }
);
return result;
});
});🛠️ API Reference
HandlerBuilder
before(event, ...handlers)- Add before handlerafter(event, ...handlers)- Add after handlerrequireAuth()- Authentication checkrequireRole(...roles)- Role checkwithAudit()- Audit fieldsreadOnly()- Read-only modewithSoftDelete(field, keyField)- Soft deleterequireOwnership(field, keyField)- Ownership checkbuild()- Register handlers
Validator
required(fields)- Required fieldemail(field)- Email formatminLength(field, length)- Minimum lengthmaxLength(field, length)- Maximum lengthpattern(field, regex, message)- Regex checkrange(field, min, max)- Numeric rangeunique(entity, field, keyField)- Uniquenessurl(field)- URL formatdate(field)- Date formatfutureDate(field)- Future datepastDate(field)- Past datepositive(field)- Positive numberinteger(field)- Integercustom(fn)- Custom validation
QueryHelper
All methods require req (transaction context) as first parameter: req, req.tx, or cds.tx.
findById(req, entity, id, keyField)- Find by IDfindAll(req, entity, options)- Get allexists(req, entity, where)- Check existencecount(req, entity, where)- CountfindOne(req, entity, where)- Find one by conditionupsert(req, entity, data, uniqueFields, keyField)- Update or insertinsertMany(req, entity, records)- Bulk insertupdateMany(req, entity, data, where)- Bulk updatedeleteMany(req, entity, ids, keyField)- Bulk deletedeleteWhere(req, entity, where)- Delete by conditiondistinct(req, entity, field)- Distinct valuespaginate(req, entity, page, pageSize, options)- Paginationaggregate(req, entity, operation, field, where)- Aggregate
📄 License
MIT
🤝 Contributing
Pull requests are welcome!
📧 Contact
GitHub: eyupbaycol/cap-toolkit-core
