@azure-utils/storybooks
Version:
Utils to upload and manage Storybooks via Azure Functions and storage.
210 lines (184 loc) • 6.12 kB
text/typescript
/**
* Module exports a single function to register
* the Storybooks management endpoints.
*
* @module
*/
import { app, type HttpFunctionOptions } from "@azure/functions";
import z from "zod";
import { timerPurgeHandler } from "./handlers/timer-purge-handler";
import { registerProjectsRouter } from "./routers/projects-router";
import { registerBuildsRouter } from "./routers/builds-router";
import { registerLabelsRouter } from "./routers/labels-router";
import { registerWebUIRouter } from "./routers/web-ui-router";
import { registerStorybookRouter } from "./routers/storybook-router";
import {
DEFAULT_CHECK_PERMISSIONS_CALLBACK,
DEFAULT_PURGE_SCHEDULE_CRON,
DEFAULT_STORAGE_CONN_STR_ENV_VAR,
DEFAULT_SERVICE_NAME,
} from "./utils/constants";
import type { CheckPermissionsCallback, OpenAPIOptions } from "./utils/types";
import { joinUrl } from "./utils/url-utils";
import { wrapHttpHandlerWithStore } from "./utils/store";
import { EmptyObjectSchema, ProjectIdSchema } from "./models/shared";
export type { CheckPermissionsCallback, OpenAPIOptions };
/**
* Options to register the storybooks router
*/
export type RegisterStorybooksRouterOptions = {
/**
* Name of the service. @default "storybooks"
*/
serviceName?: string;
/**
* Define the route on which all router is placed.
* Can be a sub-path of the main API route.
*
* @default ''
*/
baseRoute?: string;
/**
* Set the Azure Functions authentication level for all routes.
*
* This is a good option to set if the service is used in
* Headless mode and requires single token authentication
* for all the requests.
*
* This setting does not affect health-check route.
*/
authLevel?: "admin";
/**
* Name of the Environment variable which stores
* the connection string to the Azure Storage resource.
* @default 'AzureWebJobsStorage'
*/
storageConnectionStringEnvVar?: string;
/**
* Modify the cron-schedule of timer function
* which purge outdated storybooks.
*
* Pass `null` to disable auto-purge functionality.
*
* @default "0 0 0 * * *" // Every midnight
*/
purgeScheduleCron?: string | null;
/**
* Options to configure OpenAPI schema
*/
openapi?: OpenAPIOptions;
/**
* Directories to serve static files from relative to project root (package.json)
* @default './public'
*/
staticDirs?: string[];
/**
* Callback function to check permissions. The function receives following params
* @param permission - object containing resource and action to permit
* @param context - Invocation context of Azure Function
* @param request - the HTTP request object
*
* @return `true` to allow access, or following to deny:
* - `false` - returns 403 response
* - `HttpResponse` - returns the specified HTTP response
*/
checkPermissions?: CheckPermissionsCallback;
};
/**
* Function to register all routes required to manage the Storybooks including
* GET, POST and DELETE methods.
*
* @returns a function to register additional HTTP handlers for the service.
*/
export function registerStorybooksRouter(
options: RegisterStorybooksRouterOptions = {}
): (name: string, options: HttpFunctionOptions) => void {
const {
serviceName = DEFAULT_SERVICE_NAME,
baseRoute = "",
authLevel,
storageConnectionStringEnvVar = DEFAULT_STORAGE_CONN_STR_ENV_VAR,
purgeScheduleCron,
openapi,
checkPermissions = DEFAULT_CHECK_PERMISSIONS_CALLBACK,
} = options;
const storageConnectionString = process.env[storageConnectionStringEnvVar];
if (!storageConnectionString) {
throw new Error(
"Missing env-var '${storageConnectionStringEnvVar}' value.\n" +
"It is required to connect with Azure Storage resource."
);
}
console.log("Registering Storybooks Router");
const openAPIEnabled = !openapi?.disabled;
app.setup({ enableHttpStream: true });
const handlerWrapper = wrapHttpHandlerWithStore.bind(null, {
serviceName,
baseRoute,
authLevel,
connectionString: storageConnectionString,
openapi,
staticDirs: options.staticDirs || ["./public"],
checkPermissions,
});
const normalisedServiceName = serviceName.toLowerCase().replace(/\s+/g, "_");
registerProjectsRouter({
serviceName: normalisedServiceName,
baseRoute: joinUrl(baseRoute, "projects"),
basePathParamsSchema: EmptyObjectSchema,
openAPIEnabled,
handlerWrapper,
});
registerBuildsRouter({
serviceName: normalisedServiceName,
baseRoute: joinUrl(baseRoute, "projects", "{projectId}", "builds"),
basePathParamsSchema: z.object({ projectId: ProjectIdSchema }),
openAPIEnabled,
handlerWrapper,
});
registerLabelsRouter({
serviceName: normalisedServiceName,
baseRoute: joinUrl(baseRoute, "projects", "{projectId}", "labels"),
basePathParamsSchema: z.object({ projectId: ProjectIdSchema }),
openAPIEnabled,
handlerWrapper,
});
registerStorybookRouter({
serviceName: normalisedServiceName,
baseRoute: joinUrl(baseRoute, "_"),
basePathParamsSchema: EmptyObjectSchema,
openAPIEnabled,
handlerWrapper,
});
registerWebUIRouter({
serviceName: normalisedServiceName,
baseRoute,
basePathParamsSchema: EmptyObjectSchema,
openAPIEnabled,
handlerWrapper,
});
if (purgeScheduleCron !== null) {
app.timer(`${normalisedServiceName}-timer_purge`, {
schedule: purgeScheduleCron || DEFAULT_PURGE_SCHEDULE_CRON,
handler: timerPurgeHandler(storageConnectionString),
});
}
/**
* Register an HTTP function.
*
* The baseRoute and authLevel is inherited.
*
* @param name unique name for the HTTP function
* @param options Options for Azure HTTP function
*/
function registerRoute(name: string, options: HttpFunctionOptions) {
app.http(`${normalisedServiceName}-${name}`, {
authLevel,
...options,
route: joinUrl(baseRoute, options.route || name),
handler: handlerWrapper(options.handler, []),
methods: options.methods || ["GET"],
});
}
return registerRoute;
}