npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@bunnio/rest-full

v0.0.2-rc.9

Published

Class oriented wrapper for @bunnio/type-guardian, that enables creating and managing openapi endpoints as a single class object

Downloads

53

Readme

rest-full

A Class oriented wrapper for the @bunnio/type-guardian!, that enables the user to customize, and independently manage openapi endpoint (structures).

CONSIDER AS whatever comes before pre alpha VERSION

USAGE

After using type-guardian, with full generation, navigate to a generated folder

Import the three shapes that were generated previously

// You may rename these objects it however you want it
import { paths as InterfacePaths } from "./YourOpenapiSource.interface";
import { paths as ZodPath } from "./YourOpenapiSource.zod";
import { lookupJson } from "./YourOpenapiSource.lookup";

Create the RequestPool by using these shapes

// You must supply the "InterfacePaths" as a template type, to enable proper typing.
const QP = new RequestPool<typeof ZodPath, InterfacePaths, typeof lookupJson>(
  ZodPath,
  lookupJson
);

Once initialized, you can call every path, that is defined in the original openapi, by calling the request function.

const response = qp.request(
  "/image/{image_id}", // path in openapi
  "put", // method defined in path
  "application/json", // body type if applicable or undefined <alias BodyKey>
  {}, // the actual body content or undefined <alias Content>
  {
    path: { image_id: "RandomStringId" },
  }, // every allowed <parameter> specified under <parameters> in openapi
  { validate: { requestBody: true } } // optional settings see below
);

Alternatively you can retrieve the zod types, for each OperationObject by calling getPathZod.

// Path conforms to the OperationObject type defined inside Descriptor<T>,
// You can see it in details in file dist/types.d.ts
interface OperationObject: {
        requestBody?: Descriptor<ZodSchema>["requestBody"];
        responses: Descriptor<ZodSchema>["responses"];
        parameters?: Descriptor<ZodSchema>["parameters"];
    };
const operationObjectZod = qp.getPathZod("/image/with_files", "post");

You can then use these zod schemas to further build your forms or other data submissions.

Example

import { useForm } from "react-hook-form";
type BodyZod = z.infer<(typeof fullZod)["requestBody"]["application/json"]>;
const register = useForm<BodyZod>();

Initialization (in details)

This package provides a class that expects 3 shaped values

(Openapi.interface,Openapi.zod,Openapi.lookup provided by @bunnio/type-guardian!)

Alt Text

and provides a pre typed interface to properly use said Openapi specification.

The default RequestPool object is written for Axios, but you can easily change it to your preferred HTTP client.

An example initialization (same as above)

import { RequestPool } from "@bunnio/rest-full/dist/RequestPool";
import { paths as ZodPath } from "./YourOpenapiSource.zod";
import { lookupJson } from "./YourOpenapiSource.lookup";
import { paths as InterfacePaths } from "./YourOpenapiSource.interface";
const qp = new RequestPool<typeof ZodPath, InterfacePaths, typeof lookupJson>(
  ZodPath,
  lookupJson
);

the RequestPool instance will be a pre-typed interface according to your specification.

You can use it as as:

const response = qp.request(
  "/image/{image_id}", // path in openapi
  "put", // method defined in path
  "application/json", // body type if applicable or undefined <alias BodyKey>
  {}, // the actual body content or undefined <alias Content>
  {
    path: { image_id: "RandomStringId" },
  }, // every allowed <parameter> specified under <parameters> in openapi
  { validate: { requestBody: true } } // optional settings see below
);

Watch the magic happen: Alt Text

Path and method

The path and method will be key combinations available in your specification.

BodyKey and Content

BodyKey and Content are defined by what is allowed in the openapi specification. The default implementation of RequestPool can only parse application/json and multipart/form-data, but let's you submit any shape.

If any other body shape is supplied, RequestPool will throw an error during execution.

To facilitate other parsers you may use the settings object during initialization:

const qp = new RequestPool<typeof ZodPath, InterfacePaths, typeof lookupJson>(
  ZodPath,
  lookupJson,
  {additionalBodyParser:{"multipart/mixed":(
        path, //: P,
        method, //: M,
        bodyKey, //: BodyKey,
        requestContent, //: Content,
        context, //: Context
        )=>{...your parser here}}}
);

You must adhere to the format, every additional type must be matched with a parser function that has the standard inputs (provided by RequestPool)

  • path,method, bodyKey, and requestContent are directly passed from original "request" call
  • context is defined in two segments
    • The mandatory context supplied by the RequestPool
    • The optional context that can be initialized by contextMaker in the settings

The bodyParser is expected to fill out the appropriate fields in the context, see more at the RequestPool.request and Context

Parameters

The parameters shape is matching to what is provided by the type-guardian! interface parser.

interface Example {
  parameters: { path: { image_id: string } };
}

Each parameter object may contain any or none of the following groups: path,query,header,~~cookies~~.

interface Example {
  parameters?: {
    query: {
      search_id?: components["schemas"]["StringQueryBody"];
      mode_id?: components["schemas"]["StringQueryMode"];
      search_profile?: components["schemas"]["BoolQueryBody"];
      mode_profile?: components["schemas"]["BoolQueryMode"];
      search_filename?: components["schemas"]["StringQueryBody"];
      mode_filename?: components["schemas"]["StringQueryMode"];
      search_format?: components["schemas"]["StringQueryBody"];
      mode_format?: components["schemas"]["StringQueryMode"];
      search_created_at?: components["schemas"]["DateQueryBody"];
      mode_created_at?: components["schemas"]["DateQueryMode"];
      search_user_id?: components["schemas"]["StringQueryBody"];
      mode_user_id?: components["schemas"]["StringQueryMode"];
      search_group_id?: components["schemas"]["StringQueryBody"];
      mode_group_id?: components["schemas"]["StringQueryMode"];
    };
  };
}

As of now, cookies are not handled regardless if required or provided

The parameters are parsed based on the specification, with respect to their specified style and explode settings.

const mode = parameter.style ?? "simple"; // As default
const explode = parameter.explode ?? false; // As default

Unparsable parameters will throw an error, please refer to either openapi specification, or the src/parameter-tools files

A Path error example:

throw Error(
  `Path style must be one of "simple" | "label" | "matrix"! is ${mode}`
);

Parameters are expected to be string compatbile, or simple objects, content type is not yet supported

Additional settings

Validator

The zod validators may be called via specifying which validators you want to use in the validate object.

Alt Text

The validate object can only contain values that are provided in the previous steps, the type hints should help you call any available validators

Validators are run before the context gets built, and are strictly called only for values/keys that were submitted.

In general it's better practise to use validators BEFORE calling request, see Zod integration

Expected Result Type

The value expectedResultType may contain which content-type you expect,

such as application/json, multipart/form-data,

...if multiple content type is defined in the responses object of the operation.

This is only used to set the proper response content-type.

Default is application/json.

Responses

The response shape is based on the method type, and responses defined in the openapi.

By default

type GetKeys = Pick<PathObject, "get" | "options" | "head">; //-> expects 200 first but checks 201 if available
type PutKeys = Pick<PathObject, "delete" | "patch" | "put">; //-> expects 200 first but checks 201 if available
type PostKeys = Pick<PathObject, "post">; //->expects 201 first but checks 200 if available

the current implementation of response type can not distuingish between supported and not supported response types.

Defintions like:

Alt Text

with content:

Alt Text

will translate to:

Alt Text

Axios decodes json response by default, but any other expected response must be decoded by the programmer

I am currently working on a standardized approach, to enable response parsing, but for now, be aware of this limitation

RequestPool.request and Context

Every time a request is called, the function creates a context, which then gets filled with every information the request may need.

Setting up context

The context shape can be extended via specifing contextMaker in the constructor settings.

Regardless of the supplied shape, the context must contain elements like,

type context<Body> = {
  requestData: Body & { url: string };
  queryParameterChain: string[];
  headers: [string, string][];
  lookup: {
    path: DeepReadonly<PathObject>;
    rootSecurity?: DeepReadonly<YAMLDocumentStructure["security"]>;
    OperationObject: DeepReadonly<OperationObject>;
  };
  method: keyof Pick<
    PathObject,
    "get" | "delete" | "options" | "patch" | "post" | "put" | "trace" | "head"
  >;
};

however, the contextMaker gets all this information, so you only need to append with whatever extra information you might want

{contextMaker?: (starter: DefaultContext<Body>) => Context;}

Context building

A context is created at the beginning of the request.

  • bodyParsers are expected to fill out context.requestData.data (requestData extends AxiosRequestConfig)
  • path parameters are updating the context.requestData.url, /image/{image_id}-> /image/123123
  • query parameters are pushing their [key, value] pairs to context.queryParameterChain
  • headers are being defined as [key, value] pairs in context.headers
  • security functions have no designated context fields, they are expected to fill out their respective headers or query parameters, or data fields

Context Execution

The actual axios request is created in the execute function, which relies only on context.

function execute<
   ...
  >(
    context: Context
  ): AxiosPromise<ResponseFinder<Responses, M>[ExpectedResType]>{...}

Because of this, by the time the execute function gets called the context has to be fully built.

After the execution the context is considered consumed, and will be thrown away.

Security

Global security term means site wide security, defined at root level, local security means security defined at OperationObject

There is no default behaviour for security, but there are two hooks provided for the security operations.

class RequestPool {
  constructor(
    // {...},
    settings?: {
      //{...},
      globalSecurityHandler?: (
        security: DeepReadonly<SecuritySchema>,
        scopes: DeepReadonly<string[]>,
        name: string,
        fullSecurity: DeepReadonly<SecurityRequirements>,
        context: Context
      ) => void;
      lookupSecurityHandler?: (
        security: DeepReadonly<SecuritySchema>,
        scopes: DeepReadonly<string[]>,
        name: string,
        fullSecurity: DeepReadonly<SecurityRequirements>,
        context: Context
      ) => void;
    }
  ){...};
}

Global security

The RequestPool (constructor parameter) options may contain a globalSecurityHandler, which then will be called for every security defined in the openapi root security object.

With each call, you only get one security schema, but you can see the other keys defined in the fullSecurity object.

function globalSecurityHandler (
        security: DeepReadonly<SecuritySchema>,
        scopes: DeepReadonly<string[]>,
        name: string,
        fullSecurity: DeepReadonly<SecurityRequirements>,
        context: Context
      ) => {};

To understand "&" and "|" behaviors and expectations in openapi security, please refer to the openapi specification.

SecuritySchema, and SecurityRequirements are exact values from the lookUpJson, which is direct translations of the original openapi.

// See more at @bunnio/type-guardian/dist/yaml-tools/securitySchemes/interface.d.ts
export type SecuritySchema =
  | BasicAuth
  | BearerAuth
  | ApiAuth
  | OauthAuth
  | OIDCAuth;
export type SecurityRequirements = {
  [key: string]: string[];
} & ExtraYamlStuff;

It is expected of the programmer to fill out the required context fields when handling security.

Local security

Local security works exactly as global security does.

function lookupSecurityHandler(
  security: DeepReadonly<SecuritySchema>,
  scopes: DeepReadonly<string[]>,
  name: string,
  fullSecurity: DeepReadonly<SecurityRequirements>,
  context: Context
) {}

Priority

If global security is defined, those keys get called first.

Local security keys are called last, so you may overwrite or delete any security set previously via global security.

Best practices

Since RequestPool calls the contextCreator and the security tools on demand, it is best if you use these handlers to connect your security information with any other state manager you may use.

If your security measures do not change often it might be best to use the contextCreator or defaultSettings to always ensure your api key/bearer token is set.

Otherwise its better to expose a function in your security flow,that can always access the latest security tokens and then supply it as a security handler

If your schema does not explicitly describe a security, but you still want to supply, both defaultSettings and contextCreator are/can be callable, so you can just put your security tokens in the context on every context creation.

Zod integration

Zod schemas are pre built via type-guardian!

Alt Text

The RequestPool currently supports 2+1 Zod Schema retriever.

  • getBodyZod
  • getPathZod
  • getParameterZod

it is highly recommended that you use getPathZod, as it retrieves the full zod specification and you can cherry pick which objects you want to use.

type-guardian! keeps a very simple structure for defining zod schemas

requestBody

One of the values retrieved by getPathZod can be requestBodies if applicable. In requestBodies you can expect to find every schema defined in you openapi as [mediaType]:ZodSchema

This also means that you may need to break up the zod definitions, for example if you are sending multipart/form-data

This might get fixed/supported better in a future release

You can use z.infer to make typing easier

const fullZod = qp.getPathZod("/image/with_files", "post");
type ZodStuff = z.infer<(typeof fullZod)["requestBody"]["multipart/form-data"]>;

parameters

Parameters match their interface counterparts, and are slightly different than your openapi definitions.

Parameters are aggregated into four categories

  • query
  • path
  • headers
  • ~~cookies~~
    • cookies are available but the request will disregard it for now

Each category is a key->ZodSchema pair, that you can use to parse the parameter

Alt Text

Additional settings, tools available

YamlNavigator

A yaml navigator is included in the RequestPool.

This class ensures, that every lookUp entry passed through the layers are the actual component, and not just a $ref,

...at least at root level

silentError (default false)

You can silence some errors by setting silentError to true.

This mostly affects zod lookups, when the zod validator is not found.

Silent errors will still log, but not throw an actual error

strictEncoding (default false)

Multipart/* requests may have an additional encoding parameter at the schema level.

This can ensure that all keys are encoded appropriately

The current behaviour is that top level components that are simple string type, or simple string arrays, with format:binary are expected to be files

Any other field is expected to be json

if strictEncoding is set to true, this must be also specified in the encoding section of the openapi

warnOnCookies (default false)

Whether to omit a console log stating that cookies are not supported, when cookies are encountered

throwOnSecurityMissing (default true)

Whether to throw an error if a security is defined in the openapi, but the actual specification (in components) was not found, thus it wasn't executed

defaultSettings

Either a const settings that gets used at every context creation for the value requestData, or a function that returns an AxiosRequestConfig or something that extends AxiosRequestConfig