UNPKG

@azure-utils/storybooks

Version:

Utils to upload and manage Storybooks via Azure Functions and storage.

296 lines (292 loc) 11.8 kB
import { DEFAULT_GITHUB_BRANCH, DEFAULT_PURGE_AFTER_DAYS, DEFAULT_SERVICE_NAME } from "./constants-CsV1N9r4.mjs"; import { generateProjectAzureTableName, listAzureTableEntities } from "./azure-data-tables-MtZoiS66.mjs"; import { deleteBlobsFromAzureStorageContainerOrThrow, generateAzureStorageContainerName } from "./azure-storage-blob-Dj53y4ng.mjs"; import { BuildSHASchema, LabelSlugSchema, ProjectIdSchema } from "./shared-BAE3ceND.mjs"; import z from "zod"; import { TableClient } from "@azure/data-tables"; import { BlobServiceClient } from "@azure/storage-blob"; //#region src/models/builds.tsx /** @private */ const BuildSchema = z.object({ label: z.string(), sha: BuildSHASchema, authorName: z.string(), authorEmail: z.string().refine((val) => val.includes("@"), "Invalid email format").meta({ description: "Email of the author" }), message: z.optional(z.string()), timestamp: z.string().optional() }); const BuildUploadSchema = BuildSchema.omit({ label: true }).extend({ labels: z.string().array().meta({ description: "Label slugs associated with the build. Must be created beforehand." }) }); const BuildUploadFormSchema = BuildUploadSchema.extend({ zipFile: z.file() }); /** * - partitionKey: label * - rowKey: sha */ var BuildModel = class { #context; #projectId; #tableClient; #blobService; projectModel; constructor(context, connectionString, projectId) { this.#context = context; this.#projectId = projectId; this.#tableClient = TableClient.fromConnectionString(connectionString, generateProjectAzureTableName(projectId, "Builds")); this.#blobService = BlobServiceClient.fromConnectionString(connectionString); this.projectModel = new ProjectModel(context, connectionString); } parse = BuildSchema.parse; async list(options) { this.#context.log("List builds for project '%s'...", this.#projectId); const entities = await listAzureTableEntities(this.#context, this.#tableClient, options); const builds = BuildSchema.array().parse(entities); const groupBySHA = Object.groupBy(builds, (b) => b.sha); const groupedBuilds = Object.values(groupBySHA).map((group) => group && group.length > 0 ? { ...group[0], label: group.map((b) => b.label).join(",") } : void 0).filter((build) => build !== void 0); return groupedBuilds; } async get(sha, labelSlug) { this.#context.log("Get build: '%s' for project '%s'...", sha, this.#projectId); const entity = labelSlug ? await this.#tableClient.getEntity(labelSlug, sha) : (await this.list({ filter: `RowKey eq '${sha}'` })).at(0); return BuildSchema.parse(entity); } async has(sha) { try { await this.get(sha); return true; } catch { return false; } } async create(data) { const { labels, sha,...rest } = data; this.#context.log("Create build '%s' for project '%s'...", sha, this.#projectId); for (const labelSlug of labels.filter(Boolean)) { await this.#tableClient.createEntity({ partitionKey: labelSlug, rowKey: sha, ...rest, label: labelSlug, sha }); const labelModel = this.projectModel.labelModel(this.#projectId); try { await labelModel.update(labelSlug, { buildSHA: sha }); } catch { this.#context.log("A new label '$s' is being created, please update its information", labelSlug); await labelModel.create({ value: labelSlug, buildSHA: sha, type: /\d+/.test(labelSlug) ? "pr" : "branch" }); } } try { const project = await this.projectModel.get(this.#projectId); if (labels.includes(project.gitHubDefaultBranch)) this.projectModel.update(project.id, { buildSHA: sha }); } catch (error) { this.#context.error(error); } } async update() { throw new Error("Update operation is not supported for builds."); } async delete(sha) { this.#context.log("Delete build: '%s' for project '%s'...", sha, this.#projectId); const matchingEntities = await listAzureTableEntities(this.#context, this.#tableClient, { filter: `sha eq '${sha}'` }); for (const entity of matchingEntities) if (entity.partitionKey && entity.rowKey) await this.#tableClient.deleteEntity(entity.partitionKey, entity.rowKey); const containerClient = this.#blobService.getContainerClient(generateAzureStorageContainerName(this.#projectId)); await deleteBlobsFromAzureStorageContainerOrThrow(this.#context, containerClient, sha); } async deleteByLabel(labelSlug) { this.#context.log("Delete build by label: '%s' for project '%s'...", labelSlug, this.#projectId); const matchingEntities = await listAzureTableEntities(this.#context, this.#tableClient, { filter: `label eq '${labelSlug}'` }); const containerClient = this.#blobService.getContainerClient(generateAzureStorageContainerName(this.#projectId)); for (const entity of matchingEntities) if (entity.partitionKey && entity.rowKey) { await this.#tableClient.deleteEntity(entity.partitionKey, entity.rowKey); const remainingLabelsForSHA = await listAzureTableEntities(this.#context, this.#tableClient, { filter: `sha eq '${entity.rowKey}'` }); if (remainingLabelsForSHA.length === 0) await deleteBlobsFromAzureStorageContainerOrThrow(this.#context, containerClient, entity.rowKey); } } }; //#endregion //#region src/models/labels.tsx const labelTypes = ["branch", "pr"]; /** @private */ const LabelSchema = z.object({ slug: LabelSlugSchema, value: z.string().meta({ description: "The value of the label." }), type: z.enum(labelTypes), buildSHA: BuildSHASchema.optional(), timestamp: z.string().optional() }).meta({ id: "storybook-label", description: "A Storybook label." }); const LabelCreateSchema = LabelSchema.omit({ slug: true, timestamp: true }); const LabelUpdateSchema = LabelSchema.omit({ slug: true, timestamp: true }).partial(); var LabelModel = class LabelModel { #context; #projectId; #tableClient; projectModel; constructor(context, connectionString, projectId) { this.#context = context; this.#projectId = projectId; this.#tableClient = TableClient.fromConnectionString(connectionString, generateProjectAzureTableName(projectId, "Labels")); this.projectModel = new ProjectModel(context, connectionString); } async list(options) { this.#context.log("List labels for project '%s'...", this.#projectId); const entities = await listAzureTableEntities(this.#context, this.#tableClient, options); return LabelSchema.array().parse(entities); } async get(slug) { this.#context.log("Get label '%s' for project '%s'...", slug, this.#projectId); const entity = await this.#tableClient.getEntity(this.#projectId, slug); return LabelSchema.parse(entity); } async has(slug) { try { await this.get(slug); return true; } catch { return false; } } async create(data) { this.#context.log("Create label '%s' for project '%s'...", data.value, this.#projectId); const slug = LabelModel.createSlug(data.value); await this.#tableClient.createEntity({ ...data, partitionKey: this.#projectId, rowKey: slug, slug }); } async update(slug, data) { this.#context.log("Update label '%s' for project '%s'...", slug, this.#projectId); await this.#tableClient.updateEntity({ ...data, partitionKey: this.#projectId, rowKey: slug, slug }, "Merge"); } async delete(slug) { this.#context.log("Delete label '%s' for project '%s'...", slug, this.#projectId); const { gitHubDefaultBranch } = await this.projectModel.get(this.#projectId); if (slug === LabelModel.createSlug(gitHubDefaultBranch)) { const message = `Cannot delete the label associated with default branch (${gitHubDefaultBranch}) of the project '${this.#projectId}'.`; this.#context.warn(message); throw new Error(message); } await this.#tableClient.deleteEntity(this.#projectId, slug); await this.projectModel.buildModel(this.#projectId).deleteByLabel(slug); } static createSlug(value) { return value.trim().toLowerCase().replace(/\W+/, "-"); } }; //#endregion //#region src/models/projects.tsx /** @private */ const ProjectSchema = z.object({ id: ProjectIdSchema, name: z.string().meta({ description: "Name of the project." }), purgeBuildsAfterDays: z.coerce.number().min(1).default(DEFAULT_PURGE_AFTER_DAYS).meta({ description: "Days after which the builds in the project should be purged." }), gitHubRepo: z.string().check(z.minLength(1, "Query-param 'gitHubRepo' is required."), z.refine((val) => val.split("/").length === 2, "Query-param 'gitHubRepo' should be in the format 'owner/repo'.")), gitHubPath: z.string().optional().meta({ description: "Path to the storybook project with respect to repository root." }), gitHubDefaultBranch: z.string().default(DEFAULT_GITHUB_BRANCH).meta({ description: "Default branch to use for GitHub repository" }), buildSHA: BuildSHASchema.optional(), timestamp: z.string().optional() }).meta({ id: "storybook-project", description: "Storybook project" }); const ProjectCreateSchema = ProjectSchema.omit({ buildSHA: true, timestamp: true }); const partitionKey = "projects"; var ProjectModel = class { #connectionString; #context; #blobService; #tableClient; constructor(context, connectionString) { this.#connectionString = connectionString; this.#context = context; this.#blobService = BlobServiceClient.fromConnectionString(connectionString); this.#tableClient = TableClient.fromConnectionString(connectionString, DEFAULT_SERVICE_NAME); } parse = ProjectSchema.parse; buildModel(projectId) { return new BuildModel(this.#context, this.#connectionString, projectId); } labelModel(projectId) { return new LabelModel(this.#context, this.#connectionString, projectId); } async list(options) { this.#context.log("List projects..."); const entities = await listAzureTableEntities(this.#context, this.#tableClient, options); return ProjectSchema.array().parse(entities); } async get(id) { this.#context.log("Get project: '%s'...", id); const entity = await this.#tableClient.getEntity(partitionKey, id); return ProjectSchema.parse(entity); } async has(id) { try { await this.get(id); return true; } catch { return false; } } async create(data) { this.#context.log("Create project: '%s'...", data.id); const gitHubDefaultBranch = data.gitHubDefaultBranch || DEFAULT_GITHUB_BRANCH; await this.#tableClient.createTable(); await this.#tableClient.createEntity({ partitionKey, rowKey: data.id, ...data, gitHubDefaultBranch }); await TableClient.fromConnectionString(this.#connectionString, generateProjectAzureTableName(data.id, "Labels")).createTable(); await TableClient.fromConnectionString(this.#connectionString, generateProjectAzureTableName(data.id, "Builds")).createTable(); await this.#blobService.createContainer(generateAzureStorageContainerName(data.id)); await this.labelModel(data.id).create({ type: "branch", value: gitHubDefaultBranch }); } async update(id, data) { this.#context.log("Update project: '%s'...", id); await this.#tableClient.updateEntity({ partitionKey, rowKey: id, ...data, id }, "Merge"); } async delete(id) { this.#context.log("Delete project: '%s'...", id); await this.#tableClient.deleteEntity(partitionKey, id); await TableClient.fromConnectionString(this.#connectionString, generateProjectAzureTableName(id, "Labels")).deleteTable(); await TableClient.fromConnectionString(this.#connectionString, generateProjectAzureTableName(id, "Builds")).deleteTable(); await this.#blobService.deleteContainer(generateAzureStorageContainerName(id)); } }; //#endregion export { BuildModel, BuildSchema, BuildUploadFormSchema, BuildUploadSchema, LabelCreateSchema, LabelModel, LabelSchema, LabelUpdateSchema, ProjectCreateSchema, ProjectModel, ProjectSchema, labelTypes }; //# sourceMappingURL=projects-DDVy3_77.mjs.map