coder / backstage-plugins

Official Coder plugins for the Backstage platform
34 stars 2 forks source link

Coder plugin: Add `arbitraryApiCall` method to CoderClient API factory #108

Closed Parkreiner closed 3 months ago

Parkreiner commented 5 months ago

Part of umbrella issue #16. Cannot be started until #107 is done.

This should hopefully be a straightforward (and even quick) update.

Problem

One of the biggest wins we can get for the Coder plugin is making it so that using the plugin doesn't feel so walled-off. We still intend to ship polished UI experiences for our main components, but users will inevitably have use cases that we can't reasonably account for in the UI. So, in that case, why not give them access to the full Coder API, and give them the power to wire things up themselves?

Requirements

Possible solution

Let's say that we have a general-purpose ReadonlyJsonValue type that represents any valid JSON-serializable value:

type ReadonlyJsonValue =
  | string
  | number
  | boolean
  | null
  | readonly ReadonlyJsonValue[]
  | Readonly<{ [key: string]: ReadonlyJsonValue }>;

In that case, we can define a method like this:

type ArbitraryApiInput <
  TBody extends ReadonlyJsonValue = ReadonlyJsonValue,
> = Readonly<{
  // Very niche syntax, but this means that 'method' will have type 'string',
  // but the other predefined methods will still show up in autocomplete
  method: "GET" | "POST" | "PUT" | "DELETE" | (string & {});

  // Ensures that each endpoint at least starts with a '/'
  endpoint: `/${string}`;

  // Genericized body type parameter
  body: TBody;

  // Lets user override default request init (within reason); should not
  // let the user accidentally override things like Coder auth token
  init?: Partial<RequestInit>;
}>;

type CoderClient = {
  // <-- Other existing properties/methods go here -->

  arbitraryApiCall<
    TReturn = any
    TBody extends ReadonlyJsonValue = ReadonlyJsonValue,
  > = (input: ArbitraryApiInput<TBody>) => Promise<TReturn>;
}

Code example

// Unsafe version
function CustomComponent () {
  // Custom hook will be made as part of issue #107
  const client = useCoderClient();

  const onSubmit = async (event) => {
    // Pretend that data is retrieved via the FormData API
    const newWorkspace = await client.arbitraryApiCall({
      method: "POST",
      endpoint: `/organizations/${organizationId}/members/${memberId}/workspaces`,
      body: {
        name: newWorkspaceName,
        // Other properties go here
      },
    });

    // Do something new newly-created workspace here; workspace
    // is of type any
  };

  return (
    <form onSubmit={onSubmit}>
      // Form elements go here
    </form>
  );
}

// "Safer" version is virtually identical, but this time, the user has used type
// parameters
const newWorkspace = await client.arbitraryApiCall<Workspace>({
  // Same runtime arguments as before
});

// At this point, newWorkspace is of type Workspace, which may or may not be
// accurate. It's up to the user to wire things up correctly