-
Notifications
You must be signed in to change notification settings - Fork 1
Description
Context / Problem to solve / Design
Actions are backend callbacks, exposed for asynchronous usage in the browser. They are a simpler alternative to GQL extensions, they don't live within a data graph.
Design
Here is the first design draft for actions written and used in JS. The design is inspired by SvelteKit remote functions.
// example.action.ts (Only the .action.ts extension matters, the file might be named anything and placed anywhere
export const getExchangeRate = async (...args) => {
// ^^^^^^^^^^^^^^^ The export name is used to create an HTTP endpoint
// (e.g. /modules/<name>/actions/<action>.do or equivalent)
// It must be unique across all .action.ts files
// `args` is what the client sent over the wire, serialized on the client, parsed on the server
doStuff(args);
return 123; // The return value is sent back to the client
}Usage on the client:
// Rate.client.tsx
import { getExchangeRate } from "./example.action.ts";
export default function Rate({ initialValue }: { initialValue: number }) {
const [rate, setRate] = useState(initialValue);
return <button onClick={async () => {
const newValue = await getExchangeRate(...args);
setRate();
}}>
Rate: {rate}. Click to refresh.
</button>
}.action.ts files will be compiled twice: once for the server, once for the client. As far as TypeScript is concerned, we are calling a function on the client. The reality is different: we will be performing a network request. Thanks to React context we can hide the nasty implementation details (e.g. the context):
// example.action.ts gets compiled to something like this on the server:
defineAction("getExchangeRate", async (...args) => {
doStuff(args);
return 123;
});
// ...and to something like this on the client:
const getExchangeRate = async (...args) => {
const ctx = use(IslandContext);
const response = await fetch(ctx.base + "/getExchangeRate.do", {
method: "POST",
body: devalue.stringify(args)
});
const data = await response.text();
return devalue.parse(data);
}We'll call this implementation "raw actions", the base brick to build stuff, but we'll also expose "safe actions", i.e. actions with validation.
This will be done by a single function coming from @jahia/javascript-modules-library: action (how original)
// example.action.ts written as a safe action
import { action } from "@jahia/javascript-modules-library";
const InputSchema = /* any https://standardschema.dev/ compatible schema */ z.object({ foo: z.number() });
export const getExchangeRate = action(InputSchema, (args) => {
// args was validated by InputSchema, this function is not called for invalid args
// the args object type is inferred from the schema: `{ foo: number }`
});