Skip to content

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

Closed as not planned
@Parkreiner

Description

@Parkreiner

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

  • A method defined on the CoderClient class that lets users call any Coder API endpoint that they want, but that still has some restrictions and niceties
    • The method will, by necessity, have to return the any type, but beyond that, it should be as type-safe as possible
    • It should only make the user pass in what is absolutely necessary. We should do as much behind the scenes as possible
      • The user shouldn't have to worry about handling auth logic themselves
      • If preflight checks ever become necessary, the user shouldn't need to worry about those either
  • All the Coder types from NPM should be easy to plug into the method as type parameters
  • Method should be defined in a way to ensure that it can't ever lose its this context when passed around as a value in the React UI
  • It should be easy to use this method as a building block for a useCoderApiFunction hook
    • That is, this method should be designed in a way that it provides the core logic for the hook out of the box, just without any types. Then it will be the hook's responsibility to provide those types, and turn the method into something type-safe

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

Metadata

Metadata

Assignees

No one assigned

    Labels

    enhancementNew feature or request

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions