HarperFast / HarperFast/create-harper
Tweak Approach for Resources + Skills
- Dominant language
- JavaScript
- Stars
- 3
- Forks
- 0
- Avg merge
- 15h 44m
- Merged PRs (30d)
- 6
Description
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) {
// 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) ...
// static patch(target: RequestTarget, data: Promise) ...
// 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: '...'});
}
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:
- [ ] Demonstrate caching from another external system which can give extreme speed-ups and load reduction. `Product.sourcedFrom`
- [x] Remove the various example files from the templates like examplePeople.js, and instead place a simple README.md that points to the relevant skills. This should help agents and humans perform better without placing a bunch of junk code in their way.
- [ ] Shift to static, or straight objects, instead of instance methods.
- [x] Demonstrate returning response headers, status codes, and bodies.
- [ ] Demonstrate how we do custom routing.
- [x] Turning off automatic permission checks (`target.checkPermission = false`).
- [x] Import from `harper` not `harperdb`
- [ ] Look for other `target` examples we can make clear in the skills.
- [ ] `checkPermissionForAddingAProduct`
Contributor guide
Assessment
This issue has not been assessed yet.