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

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions