Two examples from @kriszyp that we can integrate into the skills:
import { tables, getContext, RequestTarget } from 'harper'; // used to be 'harperdb'
// If this is functionally the main endpoint accessing/updating products, we can use the same name for the class as the
// table and directly use/extend tables.Product to make it clear we are extending the table as the main table endpoint
export class Product extends tables.Product {
// first define how this is externally accessed; any REST methods we want to override to define permission/access,
// how we handle requests, any routing we want to do, and actions we want to perform:
static async get(target: RequestTarget) {
// checkPermissionForForGet()
// or just open it up to the whole world
target.checkPermission = false;
// if we want to treat queries for a set of products differently than requests for a single product (by id), we can:
if (target.isCollection) {
target.limit ??= 100; // maybe put a reasonable limit here
return super.get(target);
} // else it is a request for a single product
const record = await super.get(target);
return {
...record, // note that spreading the result of get() requires 5.0 (or loadAsInstance=false)
price: new Date().getDay() == 5 ? record.price * 0.8 : record.price, // 20% off on Fridays!
};
}
static async post(target: RequestTarget, data: Promise<any>) {
// we can branch on URL paths, if we want:
if (target.pathname === '/') {
// it is generally good to do any custom authorization logic early in a static method
checkPermissionForAddingAProduct();
return super.create(target, await data);
} else {
checkPermissionForAddingImageToProduct();
const product = await super.update(target); // get an updatable product instance
if (!product.doesExist()) {
return undefined; // 404 if it doesn't exist
}
const { imageToAdd } = await data;
product.addImage(imageToAdd);
return { message: 'Added image to product', product };
}
}
static delete(target: RequestTarget) {
checkPermissionForDelete();
return super.delete(target);
}
// note that we are just inheriting put, patch, etc.; don't override a method if the default behavior works for us!
// (or if the default restrictive permissions sufficiently protect it)
// static put(target: RequestTarget, data: Promise<any>) ...
// static patch(target: RequestTarget, data: Promise<any>) ...
// Now we have defined all our entry points to the table, move on to define the data model for how each product
// is updated (or created) and validated:
addImage(imageToAdd) {
this.image = imageToAdd; // assign a new image
}
// This is called before any transaction is committed, for put, patch, create
validate(record, partial: boolean) {
// add our own data "cleaning". Note that the record here is mutable! (at least for now, will soon be frozen)
record.description = record.description.trim(); // trim leading/trailing spaces
record.price = +record.price.toFixed(2); // only two decimal places
// execute the default validation that ensures the record has the correct types matching the schema
super.validate(record, partial);
// and add our own custom validation
if (record.price < 0) {
let error = new Error('You can not have a negative price!');
error.statusCode = 400;
throw error;
}
if (isUglyAccordingToAI(this.image)) {
throw new Error('We do not want an ugly product image!');
}
}
}
// an additional note here; we rarely sell standalone applications. We sell caching applications. Basically
// everything we sell is caching data from other sources. Good to include in examples when possible
Product.sourcedFrom({
async get(productId) {
return (await fetch(salesforceUrl + productId)).json();
}
});
function checkPermissionForAddingAProduct() {
const user = getContext().user; // ambient access to context and user through async context tracking
if (user.role.name !== 'product-manager') {
let error = new Error('You are not a product manager!');
error.statusCode = 403;
throw error;
}
}
and another example splitting things up a bit more:
import { tables, getContext, RequestTarget } from 'harper'; // used to be 'harperdb'
// define an endpoint to return some HTML and do some stuff, only thing exposed to HTTP
export const product = {
get() {
return new Response({ headers: { Content-Type: 'text/html' }, body: '<html>...'});
}
async post(target, data) {
let product: Product = await Product.update(target);
product.addImage((await data).imageToAdd);
product.save(); // would happen automatically on commit
}
};
// just do a little data modeling here, but do *not* export or expose to HTTP
class Product extends tables.Product {
addImage(imageToAdd) {
this.image = imageToAdd; // assign a new image
}
// This is called before any transaction is committed, for any updates (including the one above)
validate(record, partial: boolean) {
// execute the default validation that ensures the record has the correct types matching the schema
super.validate(record, partial);
if (isUglyAccordingToAI(this.image)) {
throw new Error('We do not want an ugly product image!');
}
}
}
Some things we should consider accomplishing:
Two examples from @kriszyp that we can integrate into the skills:
and another example splitting things up a bit more:
Some things we should consider accomplishing:
Product.sourcedFromtarget.checkPermission = false).harpernotharperdbtargetexamples we can make clear in the skills.checkPermissionForAddingAProduct