Skip to content

Actions #588

@GauBen

Description

@GauBen

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 }`
});

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No fields configured for Story.

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions