Skip to content

Tweak Approach for Resources + Skills #35

Description

@dawsontoth

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:

  • Demonstrate caching from another external system which can give extreme speed-ups and load reduction. Product.sourcedFrom
  • 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.
  • Demonstrate returning response headers, status codes, and bodies.
  • Demonstrate how we do custom routing.
  • Turning off automatic permission checks (target.checkPermission = false).
  • Import from harper not harperdb
  • Look for other target examples we can make clear in the skills.
  • checkPermissionForAddingAProduct

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions