diff --git a/webhooks/.prettierignore b/webhooks/.prettierignore new file mode 100644 index 0000000..ce98533 --- /dev/null +++ b/webhooks/.prettierignore @@ -0,0 +1,5 @@ +node_modules +dist +*.md +*.js +*.json diff --git a/webhooks/README.md b/webhooks/README.md new file mode 100644 index 0000000..f046035 --- /dev/null +++ b/webhooks/README.md @@ -0,0 +1,9 @@ +# Webhooks + +Based on https://github.com/github/docs/blob/main/src/rest/README.md, this script pulls the updated OpenAPI webhook schemas from https://github.com/github/rest-api-description/blob/main/descriptions/api.github.com/dereferenced/api.github.com.deref.json and generates a `webhooks.json` file for consumption by the language service, preserving any descriptions and summaries as markdown. + +## Usage + +```shell +npm start +``` diff --git a/webhooks/package-lock.json b/webhooks/package-lock.json new file mode 100644 index 0000000..e111cbf --- /dev/null +++ b/webhooks/package-lock.json @@ -0,0 +1,288 @@ +{ + "name": "webhooks", + "version": "1.0.0", + "lockfileVersion": 2, + "requires": true, + "packages": { + "": { + "name": "webhooks", + "version": "1.0.0", + "license": "ISC", + "dependencies": { + "node-fetch": "^3.3.0" + }, + "devDependencies": { + "@types/node-fetch": "^2.6.2" + } + }, + "node_modules/@types/node": { + "version": "18.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", + "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==", + "dev": true + }, + "node_modules/@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "dependencies": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "engines": { + "node": ">= 12" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==", + "engines": { + "node": ">= 8" + } + } + }, + "dependencies": { + "@types/node": { + "version": "18.13.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.13.0.tgz", + "integrity": "sha512-gC3TazRzGoOnoKAhUx+Q0t8S9Tzs74z7m0ipwGpSqQrleP14hKxP4/JUeEQcD3W1/aIpnWl8pHowI7WokuZpXg==", + "dev": true + }, + "@types/node-fetch": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.2.tgz", + "integrity": "sha512-DHqhlq5jeESLy19TYhLakJ07kNumXWjcDdxXsLUMJZ6ue8VZJj4kLPQVE/2mdHh3xZziNF1xppu5lwmS53HR+A==", + "dev": true, + "requires": { + "@types/node": "*", + "form-data": "^3.0.0" + } + }, + "asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "dev": true + }, + "combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "dev": true, + "requires": { + "delayed-stream": "~1.0.0" + } + }, + "data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==" + }, + "delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "dev": true + }, + "fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "requires": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + } + }, + "form-data": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-3.0.1.tgz", + "integrity": "sha512-RHkBKtLWUVwd7SqRIvCZMEvAMoGUp0XU+seQiZejj0COz3RI3hWP4sCv3gZWWLjJTd7rGwcsF5eKZGii0r/hbg==", + "dev": true, + "requires": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "mime-types": "^2.1.12" + } + }, + "formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "requires": { + "fetch-blob": "^3.1.2" + } + }, + "mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true + }, + "mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "requires": { + "mime-db": "1.52.0" + } + }, + "node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==" + }, + "node-fetch": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.0.tgz", + "integrity": "sha512-BKwRP/O0UvoMKp7GNdwPlObhYGB5DQqwhEDQlNKuoqwVYSxkSZCSbHjnFFmUEtwSKRPU4kNK8PbDYYitwaE3QA==", + "requires": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + } + }, + "web-streams-polyfill": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.2.1.tgz", + "integrity": "sha512-e0MO3wdXWKrLbL0DgGnUV7WHVuw9OUvL4hjgnPkIeEvESk74gAITi5G606JtZPp39cd8HA9VQzCIvA49LpPN5Q==" + } + } +} diff --git a/webhooks/package.json b/webhooks/package.json new file mode 100644 index 0000000..970d3e6 --- /dev/null +++ b/webhooks/package.json @@ -0,0 +1,21 @@ +{ + "name": "webhooks", + "version": "1.0.0", + "description": "", + "private": true, + "type": "module", + "main": "dist/index.js", + "scripts": { + "build": "tsc --build tsconfig.json", + "start": "npm run build && node dist/index.js", + "clean": "rimraf dist", + "format": "prettier --write '**/*.ts'", + "format-check": "prettier --check '**/*.ts'" + }, + "dependencies": { + "node-fetch": "^3.3.0" + }, + "devDependencies": { + "@types/node-fetch": "^2.6.2" + } +} \ No newline at end of file diff --git a/webhooks/src/get-body-params.ts b/webhooks/src/get-body-params.ts new file mode 100644 index 0000000..9606d6b --- /dev/null +++ b/webhooks/src/get-body-params.ts @@ -0,0 +1,213 @@ +// Based on https://github.com/github/docs/blob/9be5ebb4855be066ac22a3ae0dafb9d615c7dfb4/src/rest/scripts/utils/get-body-params.js + +// If there is a oneOf at the top level, then we have to present just one +// in the docs. We don't currently have a convention for showing more than one +// set of input parameters in the docs. Having a top-level oneOf is also very +// uncommon. +// Currently there aren't very many operations that require this treatment. +// As an example, the 'Add status check contexts' and 'Set status check contexts' +// operations have a top-level oneOf. + +async function getTopLevelOneOfProperty(schema: any) { + if (!schema.oneOf) { + throw new Error("Schema does not have a requestBody oneOf property defined"); + } + if (!(Array.isArray(schema.oneOf) && schema.oneOf.length > 0)) { + throw new Error("Schema requestBody oneOf property is not an array"); + } + // When a oneOf exists but the `type` differs, the case has historically + // been that the alternate option is an array, where the first option + // is the array as a property of the object. We need to ensure that the + // first option listed is the most comprehensive and preferred option. + const firstOneOfObject = schema.oneOf[0]; + const allOneOfAreObjects = schema.oneOf.every((elem: any) => elem.type === "object"); + let required = firstOneOfObject.required || []; + let properties = firstOneOfObject.properties || {}; + + // When all of the oneOf objects have the `type: object` we + // need to display all of the parameters. + // This merges all of the properties and required values. + if (allOneOfAreObjects) { + for (const each of schema.oneOf.slice(1)) { + Object.assign(firstOneOfObject.properties, each.properties); + required = firstOneOfObject.required.concat(each.required); + } + properties = firstOneOfObject.properties; + } + return {properties, required}; +} + +// Gets the body parameters for a given schema recursively. +export async function getBodyParams(schema: any, topLevel = false): Promise { + const bodyParametersParsed = []; + const schemaObject = schema.oneOf && topLevel ? await getTopLevelOneOfProperty(schema) : schema; + const properties: Record = schemaObject.properties || {}; + const required = schemaObject.required || []; + + // Most operation requestBody schemas are objects. When the type is an array, + // there will not be properties on the `schema` object. + if (topLevel && schema.type === "array") { + const childParamsGroups = []; + const arrayType = schema.items.type; + const paramType = [schema.type]; + if (arrayType === "object") { + childParamsGroups.push(...(await getBodyParams(schema.items, false))); + } else { + paramType.splice(paramType.indexOf("array"), 1, `array of ${arrayType}s`); + } + const paramDecorated = await getTransformedParam(schema, paramType, { + required, + topLevel, + childParamsGroups + }); + return [paramDecorated]; + } + + for (const [paramKey, param] of Object.entries(properties)) { + // OpenAPI 3.0 only had a single value for `type`. OpenAPI 3.1 + // will either be a single value or an array of values. + // This makes type an array regardless of how many values the array + // includes. This allows us to support 3.1 while remaining backwards + // compatible with 3.0. + const paramType = Array.isArray(param.type) ? param.type : [param.type]; + const additionalPropertiesType = param.additionalProperties + ? Array.isArray(param.additionalProperties.type) + ? param.additionalProperties.type + : [param.additionalProperties.type] + : []; + const childParamsGroups = []; + + // If the parameter is an array or object there may be child params + // If the parameter has oneOf or additionalProperties, they need to be + // recursively read too. + + // There are a couple operations with additionalProperties, which allows + // the api to define input parameters with the type dictionary. These are the only + // two operations (at the time of adding this code) that use additionalProperties + // Create a snapshot of dependencies for a repository + // Update a gist + if (param.additionalProperties && additionalPropertiesType.includes("object")) { + const keyParam = { + type: "object", + name: "key", + description: `A user-defined key to represent an item in \`${paramKey}\`.`, + isRequired: param.required, + enum: param.enum, + default: param.default, + childParamsGroups: [] as any[] + }; + keyParam.childParamsGroups.push(...(await getBodyParams(param.additionalProperties, false))); + childParamsGroups.push(keyParam); + } else if (paramType && paramType.includes("array")) { + const arrayType = param.items.type; + if (arrayType) { + paramType.splice(paramType.indexOf("array"), 1, `array of ${arrayType}s`); + } + if (arrayType === "object") { + childParamsGroups.push(...(await getBodyParams(param.items, false))); + } + } else if (paramType && paramType.includes("object")) { + childParamsGroups.push(...(await getBodyParams(param, false))); + } else if (param && param.oneOf) { + // get concatenated description and type + const descriptions = []; + for (const childParam of param.oneOf) { + paramType.push(childParam.type); + // If there is no parent description, create a description from + // each type + if (!param.description) { + if (childParam.type === "array") { + if (childParam.items.description) { + descriptions.push({ + type: childParam.type, + description: childParam.items.description + }); + } + } else { + if (childParam.description) { + descriptions.push({type: childParam.type, description: childParam.description}); + } + } + } + } + // Occasionally, there is no parent description and the description + // is in the first child parameter. + const oneOfDescriptions = descriptions.length ? descriptions[0].description : ""; + if (!param.description) param.description = oneOfDescriptions; + + // This is a workaround for an operation that incorrectly defines allOf for a + // body parameter. As a workaround, we will use the first object in the list of + // the allOf array. Otherwise, fallback to the first item in the array. + // This isn't ideal, and in the case of an actual allOf occurrence, we should + // handle it differently by merging all of the properties. There is currently + // only one occurrence for the operation id repos/update-information-about-pages-site + // See Ecosystem API issue number #3332 for future plans to fix this in the OpenAPI + } else if (param && param.anyOf && Object.keys(param).length === 1) { + const firstObject = Object.values(param.anyOf).find((item: any) => item.type === "object") as { + description: string; + required: boolean; + }; + if (firstObject) { + paramType.push("object"); + param.description = firstObject.description; + param.isRequired = firstObject.required; + childParamsGroups.push(...(await getBodyParams(firstObject, false))); + } else { + paramType.push(param.anyOf[0].type); + param.description = param.anyOf[0].description; + param.isRequired = param.anyOf[0].required; + } + } + + const paramDecorated = await getTransformedParam(param, paramType, { + paramKey, + required, + childParamsGroups, + topLevel + }); + bodyParametersParsed.push(paramDecorated); + } + return bodyParametersParsed; +} + +type Param = { + type: "string" | "string or null" | "number" | "boolean" | "array" | "object" | "object or null" | "integer or null"; + name: string; + in: string; + isRequired: boolean; + description: string; + childParamsGroups?: Param[]; + enum?: string[]; + default: any; +}; + +async function getTransformedParam(param: any, paramType: any, props: any) { + const {paramKey, required, childParamsGroups, topLevel} = props; + const paramDecorated = {} as Param; + + // Supports backwards compatibility for OpenAPI 3.0 + // In 3.1 a nullable type is part of the param.type array and + // the property param.nullable does not exist. + if (param.nullable) paramType.push("null"); + paramDecorated.type = paramType.filter(Boolean).join(" or "); + paramDecorated.name = paramKey; + if (topLevel) { + paramDecorated.in = "body"; + } + paramDecorated.description = param.description; + if (required && required.includes(paramKey)) { + paramDecorated.isRequired = true; + } + if (childParamsGroups && childParamsGroups.length > 0) { + paramDecorated.childParamsGroups = childParamsGroups; + } + if (param.enum) { + paramDecorated.enum = param.enum; + } + + // we also want to catch default values of `false` for booleans + if (param.default !== undefined) { + paramDecorated.default = param.default; + } + return paramDecorated; +} diff --git a/webhooks/src/index.ts b/webhooks/src/index.ts new file mode 100755 index 0000000..2b98bad --- /dev/null +++ b/webhooks/src/index.ts @@ -0,0 +1,36 @@ +import fetch from "node-fetch"; +import {promises as fs} from "fs"; +import Webhook from "./webhook.js"; + +const SCHEMA_URL = + "https://raw.githubusercontent.com/github/rest-api-description/main/descriptions/api.github.com/dereferenced/api.github.com.deref.json"; +const OUTPUT_PATH = "src/webhooks.json"; + +const schema: any = await fetch(SCHEMA_URL).then(res => res.json()); + +const rawWebhooks = Object.values(schema["x-webhooks"]) as any[]; +if (!rawWebhooks) { + throw new Error("No webhooks found in schema"); +} + +const webhooks = []; +for (const webhook of Object.values(rawWebhooks)) { + webhooks.push(new Webhook(webhook.post)); +} + +await Promise.all(webhooks.map(webhook => webhook.process())); + +// The category is the name of the webhook +const categorizedWebhooks: Record> = {}; +for (const webhook of webhooks) { + if (!webhook.action) webhook.action = "default"; + + if (categorizedWebhooks[webhook.category]) { + categorizedWebhooks[webhook.category][webhook.action] = webhook; + } else { + categorizedWebhooks[webhook.category] = {}; + categorizedWebhooks[webhook.category][webhook.action] = webhook; + } +} + +await fs.writeFile(OUTPUT_PATH, JSON.stringify(categorizedWebhooks, null, 2)); diff --git a/webhooks/src/webhook.ts b/webhooks/src/webhook.ts new file mode 100644 index 0000000..5ee0e1a --- /dev/null +++ b/webhooks/src/webhook.ts @@ -0,0 +1,57 @@ +// Based on https://github.com/github/docs/blob/main/src/rest/scripts/utils/webhook.js + +import {getBodyParams} from "./get-body-params.js"; + +const NO_CHILD_PROPERTIES = ["action", "enterprise", "installation", "organization", "repository", "sender"]; + +export default class Webhook { + public description: string; + public summary: string; + public bodyParameters: any[]; + public availability: string[]; + public action: string; + public category: string; + #webhook: any; + + constructor(webhook: any) { + this.#webhook = webhook; + this.description = webhook.description; + this.summary = webhook.summary; + this.bodyParameters = []; + this.availability = webhook["x-github"]["supported-webhook-types"]; + + // for some webhook action types (like some pull-request webhook types) the + // schema properties are under a oneOf so we try and take the action from + // the first one (the action will be the same across oneOf items) + this.action = + webhook.requestBody.content["application/json"]?.schema?.properties?.action?.enum?.[0] || + webhook.requestBody.content["application/json"]?.schema?.oneOf?.[0]?.properties?.action?.enum?.[0] || + null; + + // The OpenAPI uses hyphens for the webhook names, but the webhooks + // are sent using underscores (e.g. `branch_protection_rule` instead + // of `branch-protection-rule`) + this.category = webhook["x-github"].subcategory.replaceAll("-", "_"); + return this; + } + + public async process() { + await this.renderBodyParameterDescriptions(); + } + + private async renderBodyParameterDescriptions() { + if (!this.#webhook.requestBody) return []; + const schema = this.#webhook.requestBody.content["application/json"]?.schema || {}; + + const isObject = schema !== null && typeof schema === "object"; + + this.bodyParameters = isObject ? await getBodyParams(schema, true) : []; + + // Removes the children of the common properties + this.bodyParameters.forEach(param => { + if (NO_CHILD_PROPERTIES.includes(param.name)) { + param.childParamsGroups = []; + } + }); + } +} diff --git a/webhooks/tsconfig.json b/webhooks/tsconfig.json new file mode 100644 index 0000000..a6a8dc8 --- /dev/null +++ b/webhooks/tsconfig.json @@ -0,0 +1,17 @@ +{ + "include": ["src"], + "compilerOptions": { + "module": "esnext", + "target": "ES2020", + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "sourceMap": true, + "strict": true, + "moduleResolution": "node", + "resolveJsonModule": true, + "declaration": true, + "declarationMap": true, + "noEmit": false, + "outDir": "./dist" + }, +}