Skip to content

Commit 4de05e7

Browse files
committed
feat: use new types from @octokit/webhooks-definitions
1 parent 1122e5d commit 4de05e7

21 files changed

+1182
-314
lines changed

package-lock.json

Lines changed: 10 additions & 7 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
"devDependencies": {
1717
"@gimenete/type-writer": "^0.1.5",
1818
"@octokit/tsconfig": "^1.0.1",
19-
"@octokit/webhooks-definitions": "^3.22.2",
19+
"@octokit/webhooks-definitions": "^3.42.0",
2020
"@pika/pack": "^0.5.0",
2121
"@pika/plugin-build-node": "^0.9.2",
2222
"@pika/plugin-build-types": "^0.9.2",
@@ -25,6 +25,7 @@
2525
"@types/debug": "^4.1.5",
2626
"@types/express": "^4.17.6",
2727
"@types/jest": "^26.0.9",
28+
"@types/json-schema": "^7.0.6",
2829
"@types/node": "^14.0.14",
2930
"@types/prettier": "^2.0.0",
3031
"@types/simple-mock": "^0.8.1",
@@ -70,7 +71,7 @@
7071
"preset": "ts-jest",
7172
"testEnvironment": "node",
7273
"testMatch": null,
73-
"testRegex": "test/.*/.*.ts",
74+
"testRegex": "test/.*/*-test.ts",
7475
"coverageThreshold": {
7576
"global": {
7677
"statements": 100,

scripts/generate-types2.ts

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
#!/usr/bin/env ts-node-transpile-only
2+
3+
import { strict as assert } from "assert";
4+
import * as fs from "fs";
5+
import { JSONSchema7, JSONSchema7Definition } from "json-schema";
6+
import { format } from "prettier";
7+
8+
type JSONSchemaWithRef = JSONSchema7 & Required<Pick<JSONSchema7, "$ref">>;
9+
10+
interface Schema extends JSONSchema7 {
11+
definitions: Record<string, JSONSchema7>;
12+
oneOf: JSONSchemaWithRef[];
13+
}
14+
15+
const schema = require("@octokit/webhooks-definitions/schema.json") as Schema;
16+
17+
const titleCase = (str: string) => `${str[0].toUpperCase()}${str.substring(1)}`;
18+
19+
const guessAtInterfaceName = (str: string) =>
20+
str.split(/[$_-]/u).map(titleCase).join("");
21+
22+
const guessAtEventName = (name: string) => {
23+
const [, eventName] = /^(.+)[$_-]event/u.exec(name) ?? [];
24+
25+
assert.ok(eventName, `unable to guess event name for "${name}"`);
26+
27+
return eventName;
28+
};
29+
const guessAtActionName = (name: string) => name.replace("$", ".");
30+
31+
const getDefinitionName = (ref: string): string => {
32+
assert.ok(
33+
ref.startsWith("#/definitions/"),
34+
`${ref} does not reference a valid definition`
35+
);
36+
37+
const [, name] = /^#\/definitions\/(.+)$/u.exec(ref) ?? [];
38+
39+
assert.ok(name, `unable to find definition name ${ref}`);
40+
41+
return name;
42+
};
43+
44+
type NameAndActions = [name: string, actions: string[]];
45+
type Property = [key: string, value: string];
46+
type ImportsAndProperties = [imports: string[], properties: Property[]];
47+
48+
const buildEventProperties = ([
49+
eventName,
50+
actions,
51+
]: NameAndActions): ImportsAndProperties => {
52+
const interfaceName = guessAtInterfaceName(eventName);
53+
const importsAndProperties: ImportsAndProperties = [
54+
[interfaceName],
55+
[[guessAtEventName(eventName), interfaceName]],
56+
];
57+
58+
if (actions.length) {
59+
actions.forEach((actionName) => {
60+
const actionInterfaceName = guessAtInterfaceName(`${actionName}_event`);
61+
62+
importsAndProperties[0].push(actionInterfaceName);
63+
importsAndProperties[1].push([
64+
guessAtActionName(actionName),
65+
actionInterfaceName,
66+
]);
67+
});
68+
}
69+
70+
return importsAndProperties;
71+
};
72+
73+
const isJSONSchemaWithRef = (
74+
object: JSONSchema7Definition
75+
): object is JSONSchemaWithRef =>
76+
typeof object === "object" && object.$ref !== undefined;
77+
78+
const listEvents = () => {
79+
return schema.oneOf.map<NameAndActions>(({ $ref }) => {
80+
const name = getDefinitionName($ref);
81+
const definition = schema.definitions[name];
82+
83+
assert.ok(definition, `unable to find definition named ${name}`);
84+
85+
if (definition.oneOf?.every(isJSONSchemaWithRef)) {
86+
return [name, definition.oneOf.map((def) => getDefinitionName(def.$ref))];
87+
}
88+
89+
return [name, []];
90+
});
91+
};
92+
93+
const getImportsAndProperties = (): ImportsAndProperties => {
94+
const importsAndProperties = listEvents().map(buildEventProperties);
95+
96+
return importsAndProperties.reduce<ImportsAndProperties>(
97+
(allImportsAndProperties, [imports, properties]) => {
98+
return [
99+
allImportsAndProperties[0].concat(imports),
100+
allImportsAndProperties[1].concat(properties),
101+
];
102+
},
103+
[[], []]
104+
);
105+
};
106+
107+
const outDir = "src/generated/";
108+
109+
const generateTypeScriptFile = (name: string, contents: string[]) => {
110+
fs.writeFileSync(
111+
`${outDir}/${name}.ts`,
112+
format(contents.join("\n"), { parser: "typescript" })
113+
);
114+
};
115+
116+
const run = () => {
117+
const [imports, properties] = getImportsAndProperties();
118+
119+
const lines: string[] = [
120+
"// THIS FILE IS GENERATED - DO NOT EDIT DIRECTLY",
121+
"// make edits in scripts/generate-types.js",
122+
"",
123+
"import {",
124+
...imports.map((str) => ` ${str},`),
125+
'} from "@octokit/webhooks-definitions/schema";',
126+
"",
127+
"export interface EmitterEventWebhookPayloadMap {",
128+
...properties.map(([key, value]) => `"${key}": ${value}`),
129+
"}",
130+
];
131+
132+
generateTypeScriptFile("get-webhook-payload-type-from-event", lines);
133+
generateTypeScriptFile("webhook-names", [
134+
"// THIS FILE IS GENERATED - DO NOT EDIT DIRECTLY",
135+
"// make edits in scripts/update-known-events.js",
136+
"",
137+
"export const emitterEventNames = [",
138+
'"*",',
139+
'"error",',
140+
...properties.map(([key]) => `"${key}",`),
141+
"];",
142+
]);
143+
};
144+
145+
run();

src/event-handler/on.ts

Lines changed: 11 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,14 @@
1-
import { WebhookEvents } from "../generated/get-webhook-payload-type-from-event";
2-
import { webhookNames } from "../generated/webhook-names";
3-
import { State, WebhookEvent, WebhookEventHandlerError } from "../types";
1+
import { emitterEventNames } from "../generated/webhook-names";
2+
import {
3+
EmitterAnyEvent,
4+
EmitterEventName,
5+
State,
6+
WebhookEventHandlerError,
7+
} from "../types";
48

59
function handleEventHandlers(
610
state: State,
7-
webhookName: WebhookEvents,
11+
webhookName: EmitterEventName,
812
handler: Function
913
) {
1014
if (!state.hooks[webhookName]) {
@@ -15,7 +19,7 @@ function handleEventHandlers(
1519
}
1620
export function receiverOn(
1721
state: State,
18-
webhookNameOrNames: WebhookEvents | WebhookEvents[],
22+
webhookNameOrNames: EmitterEventName | EmitterEventName[],
1923
handler: Function
2024
) {
2125
if (Array.isArray(webhookNameOrNames)) {
@@ -25,7 +29,7 @@ export function receiverOn(
2529
return;
2630
}
2731

28-
if (webhookNames.indexOf(webhookNameOrNames) === -1) {
32+
if (emitterEventNames.indexOf(webhookNameOrNames) === -1) {
2933
console.warn(
3034
`"${webhookNameOrNames}" is not a known webhook name (https://developer.github.com/v3/activity/events/types/)`
3135
);
@@ -45,7 +49,7 @@ export function receiverOn(
4549

4650
export function receiverOnAny(
4751
state: State,
48-
handler: (event: WebhookEvent<any>) => any
52+
handler: (event: EmitterAnyEvent) => any
4953
) {
5054
handleEventHandlers(state, "*", handler);
5155
}

src/event-handler/receive.ts

Lines changed: 21 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,36 @@
11
// @ts-ignore to address #245
22
import AggregateError from "aggregate-error";
3-
4-
import { wrapErrorHandler } from "./wrap-error-handler";
3+
import { EmitterEventWebhookPayloadMap } from "../generated/get-webhook-payload-type-from-event";
54
import {
6-
WebhookEvent,
5+
EmitterEventName,
6+
EmitterWebhookEvent,
77
State,
88
WebhookError,
99
WebhookEventHandlerError,
1010
} from "../types";
11-
import { WebhookEvents } from "../generated/get-webhook-payload-type-from-event";
11+
import { wrapErrorHandler } from "./wrap-error-handler";
12+
13+
type EventAction = Extract<
14+
EmitterEventWebhookPayloadMap[keyof EmitterEventWebhookPayloadMap],
15+
{ action: string }
16+
>["action"];
1217

1318
function getHooks(
1419
state: State,
15-
eventPayloadAction: string,
16-
eventName: WebhookEvents
20+
eventPayloadAction: EventAction | null,
21+
eventName: EmitterEventName
1722
): Function[] {
18-
const hooks = [state.hooks[`${eventName}.${eventPayloadAction}`]];
23+
const hooks = [state.hooks[eventName], state.hooks["*"]];
1924

20-
hooks.push(state.hooks[eventName]);
21-
hooks.push(state.hooks["*"]);
25+
if (eventPayloadAction) {
26+
hooks.unshift(state.hooks[`${eventName}.${eventPayloadAction}`]);
27+
}
2228

2329
return ([] as Function[]).concat(...hooks.filter(Boolean));
2430
}
2531

2632
// main handler function
27-
export function receiverHandle(state: State, event: WebhookEvent) {
33+
export function receiverHandle(state: State, event: EmitterWebhookEvent) {
2834
const errorHandlers = state.hooks.error || [];
2935

3036
if (event instanceof Error) {
@@ -46,7 +52,11 @@ export function receiverHandle(state: State, event: WebhookEvent) {
4652
}
4753

4854
// flatten arrays of event listeners and remove undefined values
49-
const hooks = getHooks(state, event.payload.action, event.name);
55+
const hooks = getHooks(
56+
state,
57+
"action" in event.payload ? event.payload.action : null,
58+
event.name
59+
);
5060

5161
if (hooks.length === 0) {
5262
return Promise.resolve();

src/event-handler/remove-listener.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1-
import { State } from "../types";
1+
import { EmitterEventName, State } from "../types";
22

33
export function removeListener(
44
state: State,
5-
webhookNameOrNames: string | string[],
5+
webhookNameOrNames: EmitterEventName | EmitterEventName[],
66
handler: Function
77
) {
88
if (Array.isArray(webhookNameOrNames)) {

0 commit comments

Comments
 (0)