From b55a68ed3bcbfde2fa5d5c247ea888b01cc2b7db Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:37:16 +0000 Subject: [PATCH 01/55] Unleash GPT magic --- examples/job-catalog/package.json | 6 +- examples/job-catalog/src/linear.ts | 1 + examples/job-catalog/tsconfig.json | 6 ++ integrations/linear/README.md | 3 + integrations/linear/package.json | 35 ++++++++++++ integrations/linear/src/index.ts | 89 ++++++++++++++++++++++++++++++ integrations/linear/tsconfig.json | 33 +++++++++++ integrations/linear/tsup.config.ts | 24 ++++++++ 8 files changed, 195 insertions(+), 2 deletions(-) create mode 100644 examples/job-catalog/src/linear.ts create mode 100644 integrations/linear/README.md create mode 100644 integrations/linear/package.json create mode 100644 integrations/linear/src/index.ts create mode 100644 integrations/linear/tsconfig.json create mode 100644 integrations/linear/tsup.config.ts diff --git a/examples/job-catalog/package.json b/examples/job-catalog/package.json index b5249bc0412..8104686dbbf 100644 --- a/examples/job-catalog/package.json +++ b/examples/job-catalog/package.json @@ -21,6 +21,7 @@ "dynamic-schedule": "nodemon --watch src/dynamic-schedule.ts -r tsconfig-paths/register -r dotenv/config src/dynamic-schedule.ts", "dynamic-triggers": "nodemon --watch src/dynamic-triggers.ts -r tsconfig-paths/register -r dotenv/config src/dynamic-triggers.ts", "background-fetch": "nodemon --watch src/background-fetch.ts -r tsconfig-paths/register -r dotenv/config src/background-fetch.ts", + "linear": "nodemon --watch src/linear.ts -r tsconfig-paths/register -r dotenv/config src/linear.ts", "dev:trigger": "trigger-cli dev --port 8080" }, "dependencies": { @@ -38,7 +39,8 @@ "@types/node": "20.4.2", "typescript": "5.1.6", "zod": "3.21.4", - "@trigger.dev/airtable": "workspace:*" + "@trigger.dev/airtable": "workspace:*", + "@trigger.dev/linear": "workspace:*" }, "trigger.dev": { "endpointId": "job-catalog" @@ -52,4 +54,4 @@ "ts-node": "^10.9.1", "tsconfig-paths": "^3.14.1" } -} +} \ No newline at end of file diff --git a/examples/job-catalog/src/linear.ts b/examples/job-catalog/src/linear.ts new file mode 100644 index 00000000000..693da49fc40 --- /dev/null +++ b/examples/job-catalog/src/linear.ts @@ -0,0 +1 @@ +export {} \ No newline at end of file diff --git a/examples/job-catalog/tsconfig.json b/examples/job-catalog/tsconfig.json index 563b275458e..e38922dd977 100644 --- a/examples/job-catalog/tsconfig.json +++ b/examples/job-catalog/tsconfig.json @@ -96,6 +96,12 @@ ], "@trigger.dev/airtable/*": [ "../../integrations/airtable/src/*" + ], + "@trigger.dev/linear": [ + "../../integrations/linear/src/index" + ], + "@trigger.dev/linear/*": [ + "../../integrations/linear/src/*" ] } } diff --git a/integrations/linear/README.md b/integrations/linear/README.md new file mode 100644 index 00000000000..13ffab69b4a --- /dev/null +++ b/integrations/linear/README.md @@ -0,0 +1,3 @@ + +# @trigger.dev/linear + \ No newline at end of file diff --git a/integrations/linear/package.json b/integrations/linear/package.json new file mode 100644 index 00000000000..4a9597d1fbe --- /dev/null +++ b/integrations/linear/package.json @@ -0,0 +1,35 @@ +{ + "name": "@trigger.dev/linear", + "version": "0.0.1", + "description": "Trigger.dev integration for @linear/sdk", + "main": "./dist/index.js", + "types": "./dist/index.d.ts", + "publishConfig": { + "access": "public" + }, + "files": [ + "dist/index.js", + "dist/index.d.ts", + "dist/index.js.map" + ], + "devDependencies": { + "@types/node": "16.x", + "rimraf": "^3.0.2", + "tsup": "7.1.x", + "typescript": "4.9.4" + }, + "scripts": { + "clean": "rimraf dist", + "build": "npm run clean && npm run build:tsup", + "build:tsup": "tsup", + "typecheck": "tsc --noEmit" + }, + "dependencies": { + "@linear/sdk": "^8.0.0", + "@trigger.dev/sdk": "workspace:^2.1.0", + "@trigger.dev/integration-kit": "workspace:^2.1.0" + }, + "engines": { + "node": ">=16.8.0" + } +} \ No newline at end of file diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts new file mode 100644 index 00000000000..3fb60867939 --- /dev/null +++ b/integrations/linear/src/index.ts @@ -0,0 +1,89 @@ + +import { safeParseBody } from '@trigger.dev/integration-kit'; +import { + ConnectionAuth, + EventSpecification, + ExternalSource, + ExternalSourceTrigger, + HandlerEvent, + IO, + IOTask, + IntegrationTaskKey, + Json, + Logger, + RunTaskErrorCallback, + RunTaskOptions, + TriggerIntegration, + retry, +} from '@trigger.dev/sdk'; +import { LinearSDK } from '@linear/sdk'; +import { createHmac } from 'crypto'; +import { z } from 'zod'; + +export type LinearIntegrationOptions = { + id: string; + apiKey?: string; + clientId?: string; + secret?: string; +}; + +type LinearRunTask = InstanceType['runTask']; + +class Linear implements TriggerIntegration { + private _options: LinearIntegrationOptions; + private _client?: LinearSDK; + private _io?: IO; + private _connectionKey?: string; + + constructor(private options: LinearIntegrationOptions) { + this._options = options; + } + + get authSource() { + return this._options.apiKey ? 'LOCAL' : 'HOSTED'; + } + + get id() { + return this._options.id; + } + + get metadata() { + return { id: 'linear', name: 'Linear' }; + } + + cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) { + const linear = new Linear(this._options); + linear._io = io; + linear._connectionKey = connectionKey; + linear._client = new LinearSDK({ apiKey: this._options.apiKey || auth.accessToken }); + return linear; + } + + runTask | void>( + key: IntegrationTaskKey, + callback: (client: LinearSDK, task: IOTask, io: IO) => Promise, + options?: RunTaskOptions, + errorCallback?: RunTaskErrorCallback + ): Promise { + if (!this._io) throw new Error('No IO'); + if (!this._connectionKey) throw new Error('No connection key'); + + return this._io.runTask( + key, + (task, io) => { + if (!this._client) throw new Error('No client'); + return callback(this._client, task, io); + }, + { + icon: 'linear', + retry: retry.standardBackoff, + ...(options ?? {}), + connectionKey: this._connectionKey, + }, + errorCallback + ); + } + +} + +export default Linear; \ No newline at end of file diff --git a/integrations/linear/tsconfig.json b/integrations/linear/tsconfig.json new file mode 100644 index 00000000000..ba887725547 --- /dev/null +++ b/integrations/linear/tsconfig.json @@ -0,0 +1,33 @@ +{ + "compilerOptions": { + "composite": false, + "declaration": false, + "declarationMap": false, + "esModuleInterop": true, + "forceConsistentCasingInFileNames": true, + "inlineSources": false, + "isolatedModules": true, + "moduleResolution": "node16", + "noUnusedLocals": false, + "noUnusedParameters": false, + "preserveWatchOutput": true, + "skipLibCheck": true, + "strict": true, + "experimentalDecorators": true, + "emitDecoratorMetadata": true, + "sourceMap": true, + "resolveJsonModule": true, + "lib": [ + "es2019" + ], + "module": "commonjs", + "target": "es2021" + }, + "include": [ + "./src/**/*.ts", + "tsup.config.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/integrations/linear/tsup.config.ts b/integrations/linear/tsup.config.ts new file mode 100644 index 00000000000..0869b214866 --- /dev/null +++ b/integrations/linear/tsup.config.ts @@ -0,0 +1,24 @@ + +import { defineConfig } from "tsup"; + +export default defineConfig([ + { + name: "main", + entry: ["./src/index.ts"], + outDir: "./dist", + platform: "node", + format: ["cjs"], + legacyOutput: true, + sourcemap: true, + clean: true, + bundle: true, + splitting: false, + dts: true, + treeshake: { + preset: "smallest", + }, + esbuildPlugins: [], + external: ["http", "https", "util", "events", "tty", "os", "timers"], + }, +]); + From 664fd304cfb499a351eb24e4d4e6a5bc190613bc Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Wed, 13 Sep 2023 13:56:58 +0000 Subject: [PATCH 02/55] Clean up after GPT --- integrations/linear/src/index.ts | 65 +++++++++++++++++++------------- 1 file changed, 38 insertions(+), 27 deletions(-) diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index 3fb60867939..fe82f8f47b1 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -1,46 +1,39 @@ - -import { safeParseBody } from '@trigger.dev/integration-kit'; import { ConnectionAuth, - EventSpecification, - ExternalSource, - ExternalSourceTrigger, - HandlerEvent, IO, IOTask, IntegrationTaskKey, Json, - Logger, RunTaskErrorCallback, RunTaskOptions, TriggerIntegration, retry, -} from '@trigger.dev/sdk'; -import { LinearSDK } from '@linear/sdk'; -import { createHmac } from 'crypto'; -import { z } from 'zod'; +} from "@trigger.dev/sdk"; +import { LinearClient } from "@linear/sdk"; export type LinearIntegrationOptions = { id: string; - apiKey?: string; - clientId?: string; - secret?: string; + token?: string; }; -type LinearRunTask = InstanceType['runTask']; +export type LinearRunTask = InstanceType["runTask"]; class Linear implements TriggerIntegration { private _options: LinearIntegrationOptions; - private _client?: LinearSDK; + private _client?: LinearClient; private _io?: IO; private _connectionKey?: string; constructor(private options: LinearIntegrationOptions) { + if (Object.keys(options).includes("token") && !options.token) { + throw `Can't create Linear integration (${options.id}) as token was undefined`; + } + this._options = options; } get authSource() { - return this._options.apiKey ? 'LOCAL' : 'HOSTED'; + return this._options.token ? "LOCAL" : "HOSTED"; } get id() { @@ -48,34 +41,50 @@ class Linear implements TriggerIntegration { } get metadata() { - return { id: 'linear', name: 'Linear' }; + return { id: "linear", name: "Linear" }; } cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) { const linear = new Linear(this._options); linear._io = io; linear._connectionKey = connectionKey; - linear._client = new LinearSDK({ apiKey: this._options.apiKey || auth.accessToken }); + linear._client = this.createClient(auth); return linear; } + createClient(auth?: ConnectionAuth) { + if (auth) { + return new LinearClient({ + accessToken: auth.accessToken, + }); + } + + if (this._options.token) { + return new LinearClient({ + apiKey: this._options.token, + }); + } + + throw new Error("No auth"); + } + runTask | void>( key: IntegrationTaskKey, - callback: (client: LinearSDK, task: IOTask, io: IO) => Promise, + callback: (client: LinearClient, task: IOTask, io: IO) => Promise, options?: RunTaskOptions, errorCallback?: RunTaskErrorCallback ): Promise { - if (!this._io) throw new Error('No IO'); - if (!this._connectionKey) throw new Error('No connection key'); + if (!this._io) throw new Error("No IO"); + if (!this._connectionKey) throw new Error("No connection key"); - return this._io.runTask( + return this._io.runTask( key, (task, io) => { - if (!this._client) throw new Error('No client'); + if (!this._client) throw new Error("No client"); return callback(this._client, task, io); }, { - icon: 'linear', + icon: "linear", retry: retry.standardBackoff, ...(options ?? {}), connectionKey: this._connectionKey, @@ -83,7 +92,9 @@ class Linear implements TriggerIntegration { errorCallback ); } - } -export default Linear; \ No newline at end of file +// TODO +export function onError(error: unknown) {} + +export default Linear; From 78a88c6571cab22c604ab4e7183081aad3d20215 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 08:23:37 +0000 Subject: [PATCH 03/55] All the hooks --- integrations/linear/package.json | 3 +- integrations/linear/src/events.ts | 376 ++++++++++++++++++++++++++++ integrations/linear/src/index.ts | 234 ++++++++++++++++- integrations/linear/src/schemas.ts | 263 +++++++++++++++++++ integrations/linear/src/webhooks.ts | 341 +++++++++++++++++++++++++ 5 files changed, 1214 insertions(+), 3 deletions(-) create mode 100644 integrations/linear/src/events.ts create mode 100644 integrations/linear/src/schemas.ts create mode 100644 integrations/linear/src/webhooks.ts diff --git a/integrations/linear/package.json b/integrations/linear/package.json index 4a9597d1fbe..ace272f5535 100644 --- a/integrations/linear/package.json +++ b/integrations/linear/package.json @@ -26,8 +26,9 @@ }, "dependencies": { "@linear/sdk": "^8.0.0", + "@trigger.dev/integration-kit": "workspace:^2.1.0", "@trigger.dev/sdk": "workspace:^2.1.0", - "@trigger.dev/integration-kit": "workspace:^2.1.0" + "zod": "3.21.4" }, "engines": { "node": ">=16.8.0" diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts new file mode 100644 index 00000000000..d8467784c2a --- /dev/null +++ b/integrations/linear/src/events.ts @@ -0,0 +1,376 @@ +import { EventSpecification } from "@trigger.dev/sdk"; +import { + AttachmentEvent, + CommentEvent, + CycleEvent, + IssueEvent, + IssueLabelEvent, + ProjectEvent, + ProjectUpdateEvent, + ReactionEvent, +} from "./schemas"; + +// TODO: payload examples +// TODO: useful properties +// TODO: Issue SLA event + +export const onAttachment: EventSpecification = { + name: "Attachment", + title: "On Attachment", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as AttachmentEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onAttachmentCreate: EventSpecification = { + name: "Attachment", + title: "On Attachment", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + parsePayload: (payload) => payload as AttachmentEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onAttachmentRemove: EventSpecification = { + name: "Attachment", + title: "On Attachment Remove", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + parsePayload: (payload) => payload as AttachmentEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onAttachmentUpdate: EventSpecification = { + name: "Attachment", + title: "On Attachment Update", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + parsePayload: (payload) => payload as AttachmentEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onComment: EventSpecification = { + name: "Comment", + title: "On Comment", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as CommentEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onCommentCreate: EventSpecification = { + name: "Comment", + title: "On Comment", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + parsePayload: (payload) => payload as CommentEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onCommentRemove: EventSpecification = { + name: "Comment", + title: "On Comment Remove", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + parsePayload: (payload) => payload as CommentEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onCommentUpdate: EventSpecification = { + name: "Comment", + title: "On Comment Update", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + parsePayload: (payload) => payload as CommentEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onCycle: EventSpecification = { + name: "Cycle", + title: "On Cycle", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as CycleEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onCycleCreate: EventSpecification = { + name: "Cycle", + title: "On Cycle", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + parsePayload: (payload) => payload as CycleEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onCycleRemove: EventSpecification = { + name: "Cycle", + title: "On Cycle Remove", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + parsePayload: (payload) => payload as CycleEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onCycleUpdate: EventSpecification = { + name: "Cycle", + title: "On Cycle Update", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + parsePayload: (payload) => payload as CycleEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onIssue: EventSpecification = { + name: "Issue", + title: "On Issue", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as IssueEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onIssueCreate: EventSpecification = { + name: "Issue", + title: "On Issue", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + parsePayload: (payload) => payload as IssueEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onIssueRemove: EventSpecification = { + name: "Issue", + title: "On Issue Remove", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + parsePayload: (payload) => payload as IssueEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onIssueUpdate: EventSpecification = { + name: "Issue", + title: "On Issue Update", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + parsePayload: (payload) => payload as IssueEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onIssueLabel: EventSpecification = { + name: "IssueLabel", + title: "On IssueLabel", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as IssueLabelEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onIssueLabelCreate: EventSpecification = { + name: "IssueLabel", + title: "On IssueLabel", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + parsePayload: (payload) => payload as IssueLabelEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onIssueLabelRemove: EventSpecification = { + name: "IssueLabel", + title: "On IssueLabel Remove", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + parsePayload: (payload) => payload as IssueLabelEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onIssueLabelUpdate: EventSpecification = { + name: "IssueLabel", + title: "On IssueLabel Update", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + parsePayload: (payload) => payload as IssueLabelEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onProject: EventSpecification = { + name: "Project", + title: "On Project", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as ProjectEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onProject_Create: EventSpecification = { + name: "Project", + title: "On Project", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + parsePayload: (payload) => payload as ProjectEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onProject_Remove: EventSpecification = { + name: "Project", + title: "On Project Remove", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + parsePayload: (payload) => payload as ProjectEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +// TODO: think of a better naming scheme (clashes with ProjectUpdate entity) +export const onProject_Update: EventSpecification = { + name: "Project", + title: "On Project Update", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + parsePayload: (payload) => payload as ProjectEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onProjectUpdate: EventSpecification = { + name: "ProjectUpdate", + title: "On ProjectUpdate", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as ProjectUpdateEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onProjectUpdateCreate: EventSpecification = { + name: "ProjectUpdate", + title: "On ProjectUpdate", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + parsePayload: (payload) => payload as ProjectUpdateEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onProjectUpdateRemove: EventSpecification = { + name: "ProjectUpdate", + title: "On ProjectUpdate Remove", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + parsePayload: (payload) => payload as ProjectUpdateEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onProjectUpdateUpdate: EventSpecification = { + name: "ProjectUpdate", + title: "On ProjectUpdate Update", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + parsePayload: (payload) => payload as ProjectUpdateEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onReaction: EventSpecification = { + name: "Reaction", + title: "On Reaction", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as ReactionEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onReactionCreate: EventSpecification = { + name: "Reaction", + title: "On Reaction", + source: "linear.app", + icon: "linear", + filter: { + action: ["create"], + }, + parsePayload: (payload) => payload as ReactionEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onReactionRemove: EventSpecification = { + name: "Reaction", + title: "On Reaction Remove", + source: "linear.app", + icon: "linear", + filter: { + action: ["remove"], + }, + parsePayload: (payload) => payload as ReactionEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + +export const onReactionUpdate: EventSpecification = { + name: "Reaction", + title: "On Reaction Update", + source: "linear.app", + icon: "linear", + filter: { + action: ["update"], + }, + parsePayload: (payload) => payload as ReactionEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index fe82f8f47b1..90da0e876c7 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -10,6 +10,8 @@ import { retry, } from "@trigger.dev/sdk"; import { LinearClient } from "@linear/sdk"; +import * as events from "./events"; +import { WebhookActionType, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; export type LinearIntegrationOptions = { id: string; @@ -18,7 +20,7 @@ export type LinearIntegrationOptions = { export type LinearRunTask = InstanceType["runTask"]; -class Linear implements TriggerIntegration { +export class Linear implements TriggerIntegration { private _options: LinearIntegrationOptions; private _client?: LinearClient; private _io?: IO; @@ -44,6 +46,10 @@ class Linear implements TriggerIntegration { return { id: "linear", name: "Linear" }; } + get source() { + return createWebhookEventSource(this); + } + cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) { const linear = new Linear(this._options); linear._io = io; @@ -92,9 +98,233 @@ class Linear implements TriggerIntegration { errorCallback ); } + + // TODO: create separate sources to remove resourceTypes + + onAttachment(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onAttachment, { + ...params, + resourceTypes: ["Attachment"], + }); + } + + onAttachmentCreate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onAttachmentCreate, { + ...params, + resourceTypes: ["Attachment"], + }); + } + + onAttachmentRemove(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onAttachmentRemove, { + ...params, + resourceTypes: ["Attachment"], + }); + } + + onAttachmentUpdate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onAttachmentUpdate, { + ...params, + resourceTypes: ["Attachment"], + }); + } + + onComment(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onComment, { ...params, resourceTypes: ["Comment"] }); + } + + onCommentCreate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCommentCreate, { + ...params, + resourceTypes: ["Comment"], + }); + } + + onCommentRemove(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCommentRemove, { + ...params, + resourceTypes: ["Comment"], + }); + } + + onCommentUpdate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCommentUpdate, { + ...params, + resourceTypes: ["Comment"], + }); + } + + onCycle(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCycle, { ...params, resourceTypes: ["Cycle"] }); + } + + onCycleCreate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCycleCreate, { + ...params, + resourceTypes: ["Cycle"], + }); + } + + onCycleRemove(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCycleRemove, { + ...params, + resourceTypes: ["Cycle"], + }); + } + + onCycleUpdate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCycleUpdate, { + ...params, + resourceTypes: ["Cycle"], + }); + } + + onIssue(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssue, { ...params, resourceTypes: ["Issue"] }); + } + + onIssueCreate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueCreate, { + ...params, + resourceTypes: ["Issue"], + }); + } + + onIssueRemove(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueRemove, { + ...params, + resourceTypes: ["Issue"], + }); + } + + onIssueUpdate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueUpdate, { + ...params, + resourceTypes: ["Issue"], + }); + } + + onIssueLabel(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueLabel, { + ...params, + resourceTypes: ["IssueLabel"], + }); + } + + onIssueLabelCreate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueLabelCreate, { + ...params, + resourceTypes: ["IssueLabel"], + }); + } + + onIssueLabelRemove(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueLabelRemove, { + ...params, + resourceTypes: ["IssueLabel"], + }); + } + + onIssueLabelUpdate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueLabelUpdate, { + ...params, + resourceTypes: ["IssueLabel"], + }); + } + + onProject(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProject, { ...params, resourceTypes: ["Project"] }); + } + + onProject_Create(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProject_Create, { + ...params, + resourceTypes: ["Project"], + }); + } + + onProject_Remove(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProject_Remove, { + ...params, + resourceTypes: ["Project"], + }); + } + + onProject_Update(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProject_Update, { + ...params, + resourceTypes: ["Project"], + }); + } + + onProjectUpdate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectUpdate, { + ...params, + resourceTypes: ["ProjectUpdate"], + }); + } + + onProjectUpdateCreate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectUpdateCreate, { + ...params, + resourceTypes: ["ProjectUpdate"], + }); + } + + onProjectUpdateRemove(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectUpdateRemove, { + ...params, + resourceTypes: ["ProjectUpdate"], + }); + } + + onProjectUpdateUpdate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectUpdateUpdate, { + ...params, + resourceTypes: ["ProjectUpdate"], + }); + } + + onReaction(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onReaction, { + ...params, + resourceTypes: ["Reaction"], + }); + } + + onReactionCreate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onReactionCreate, { + ...params, + resourceTypes: ["Reaction"], + }); + } + + onReactionRemove(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onReactionRemove, { + ...params, + resourceTypes: ["Reaction"], + }); + } + + onReactionUpdate(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onReactionUpdate, { + ...params, + resourceTypes: ["Reaction"], + }); + } + + webhooks() { + return new Webhooks(this.runTask.bind(this)); + } } +type OnChangeParams = { + teamId?: string; + allPublicTeams?: boolean; + actionTypes?: WebhookActionType[]; +}; + // TODO export function onError(error: unknown) {} -export default Linear; +export { events }; diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts new file mode 100644 index 00000000000..8f899c1619a --- /dev/null +++ b/integrations/linear/src/schemas.ts @@ -0,0 +1,263 @@ +import { z } from "zod"; +import { WebhookActionTypeSchema } from "./webhooks"; + +const ReactionShortSchema = z.object({ + emoji: z.string(), + reactions: z.array( + z.object({ + id: z.string(), + userId: z.string(), + reactedAt: z.string(), + }) + ), +}); + +const IssueDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + number: z.number(), + title: z.string(), + priority: z.number(), + boardOrder: z.number(), + sortOrder: z.number(), + teamId: z.string(), + previousIdentifiers: z.array(z.string()), + assigneeId: z.string().optional(), + stateId: z.string(), + priorityLabel: z.string(), + subscriberIds: z.array(z.string()), + labelIds: z.array(z.string()), + assignee: z + .object({ + id: z.string(), + name: z.string(), + }) + .optional(), + state: z.object({ + id: z.string(), + color: z.string(), + name: z.string(), + type: z.string(), + }), + team: z.object({ + id: z.string(), + key: z.string(), + name: z.string(), + }), + labels: z.array( + z.object({ + id: z.string(), + color: z.string(), + name: z.string(), + }) + ), + description: z.string(), +}); + +// FIXME: can't confirm schema as does not seem to trigger +const AttachmentDataSchema = z.object({}); + +const CommentDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + body: z.string(), + issueId: z.string(), + userId: z.string(), + editedAt: z.string(), + reactionData: z.array(ReactionShortSchema), + issue: z.object({ + id: z.string(), + title: z.string(), + }), +}); + +const IssueLabelDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + name: z.string(), + color: z.string(), + organizationId: z.string(), + teamId: z.string(), + creatorId: z.string(), +}); + +const ReactionDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + emoji: z.string(), + userId: z.string(), + comment: z.object({ + id: z.string(), + body: z.string(), + userId: z.string(), + }), + user: z.object({ + id: z.string(), + name: z.string(), + }), +}); + +const MilestoneDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + name: z.string(), + archivedAt: z.coerce.date().optional(), + description: z.string().optional(), + sortOrder: z.number().optional(), + targetDate: z.coerce.date().optional(), +}); + +const RoadmapDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + name: z.string(), + color: z.string().optional(), + archivedAt: z.coerce.date().optional(), + description: z.string().optional(), + sortOrder: z.number().optional(), + targetDate: z.coerce.date().optional(), +}); + +const ProjectDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + name: z.string(), + description: z.string(), + slugId: z.string(), + color: z.string(), + state: z.string(), + creatorId: z.string(), + leadId: z.string(), + sortOrder: z.string(), + issueCountHistory: z.array(z.number()), + completedIssueCountHistory: z.array(z.number()), + scopeHistory: z.array(z.number()), + completedScopeHistory: z.array(z.number()), + inProgressScopeHistory: z.array(z.number()), + slackNewIssue: z.boolean(), + slackIssueComments: z.boolean(), + slackIssueStatuses: z.boolean(), + teamIds: z.array(z.string()), + memberIds: z.array(z.string()), + milestones: z.array(MilestoneDataSchema.partial()), + roadmaps: z.array(RoadmapDataSchema.partial()), +}); + +const ProjectUpdateDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + body: z.string(), + projectId: z.string(), + health: z.string(), + userId: z.string(), + infoSnapshot: z.object({}).passthrough().optional(), + project: ProjectDataSchema.pick({ id: true, name: true }), + user: z.object({ + id: z.string(), + name: z.string(), + }), + roadmaps: z.array(RoadmapDataSchema.partial()), +}); + +const CycleDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + number: z.number(), + startsAt: z.coerce.date(), + endsAt: z.coerce.date(), + issueCountHistory: z.array(z.number()), + completedIssueCountHistory: z.array(z.number()), + scopeHistory: z.array(z.number()), + completedScopeHistory: z.array(z.number()), + inProgressScopeHistory: z.array(z.number()), + teamId: z.string(), + uncompletedIssuesUponCloseIds: z.array(z.string()), +}); + +export const WebhookPayloadBaseSchema = z.object({ + action: WebhookActionTypeSchema, + createdAt: z.coerce.date(), + url: z.string().url(), + // TODO: check if this is always present + organizationId: z.string().optional(), + webhookTimestamp: z.coerce.date(), + webhookId: z.string(), +}); + +export const IssueEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Issue"), + data: IssueDataSchema, + updatedFrom: IssueDataSchema.partial(), +}); +export type IssueEvent = z.infer; + +export const AttachmentEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Attachment"), + data: AttachmentDataSchema, + updatedFrom: AttachmentDataSchema.partial(), +}); +export type AttachmentEvent = z.infer; + +export const CommentEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Comment"), + data: CommentDataSchema, + updatedFrom: CommentDataSchema.partial(), +}); +export type CommentEvent = z.infer; + +export const IssueLabelEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("IssueLabel"), + data: IssueLabelDataSchema, + updatedFrom: IssueLabelDataSchema.partial(), +}); +export type IssueLabelEvent = z.infer; + +export const ReactionEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Reaction"), + data: ReactionDataSchema, + updatedFrom: ReactionDataSchema.partial(), +}); +export type ReactionEvent = z.infer; + +export const ProjectEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Project"), + data: ProjectDataSchema, + updatedFrom: ProjectDataSchema.partial(), +}); +export type ProjectEvent = z.infer; + +export const ProjectUpdateEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("ProjectUpdate"), + data: ProjectUpdateDataSchema, + updatedFrom: ProjectUpdateDataSchema.partial(), +}); +export type ProjectUpdateEvent = z.infer; + +export const CycleEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Cycle"), + data: CycleDataSchema, + updatedFrom: CycleDataSchema.partial(), +}); +export type CycleEvent = z.infer; + +export const WebhookPayloadSchema = z.discriminatedUnion("type", [ + IssueEventSchema, + AttachmentEventSchema, + CommentEventSchema, + IssueLabelEventSchema, + ReactionEventSchema, + ProjectEventSchema, + ProjectUpdateEventSchema, + CycleEventSchema, +]); + +export type WebhookPayload = z.infer; diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts new file mode 100644 index 00000000000..5d2cc93c240 --- /dev/null +++ b/integrations/linear/src/webhooks.ts @@ -0,0 +1,341 @@ +import { + ExternalSource, + ExternalSourceTrigger, + HandlerEvent, + IntegrationTaskKey, + Logger, +} from "@trigger.dev/sdk"; +import { + LinearWebhooks, + LINEAR_WEBHOOK_SIGNATURE_HEADER, + LINEAR_WEBHOOK_TS_FIELD, + WebhookPayload, + DeletePayload, + Webhook, +} from "@linear/sdk"; +import { z } from "zod"; +import * as events from "./events"; +import { Linear, LinearRunTask } from "./index"; +import { + WebhookCreateInput, + WebhookUpdateInput, + WebhooksQueryVariables, +} from "@linear/sdk/dist/_generated_documents"; +import { WebhookPayloadSchema } from "./schemas"; + +export const WebhookResourceTypeSchema = z.union([ + z.literal("Attachment"), + z.literal("Comment"), + z.literal("Cycle"), + z.literal("Issue"), + z.literal("IssueLabel"), + z.literal("Project"), + z.literal("ProjectUpdate"), + z.literal("Reaction"), +]); +export type WebhookResourceType = z.infer; + +export const WebhookActionTypeSchema = z.union([ + z.literal("create"), + z.literal("remove"), + z.literal("update"), +]); +export type WebhookActionType = z.infer; + +type DeleteWebhookParams = { + id: string; +}; + +type UpdateWebhookParams = { + id: string; + input: WebhookUpdateInput; +}; + +// TODO: types +const withoutFunctions = (obj: T): T => { + return JSON.parse(JSON.stringify(obj), (key, value) => { + if (typeof value === "function") { + return undefined; + } + return value; + }); +}; + +export class Webhooks { + runTask: LinearRunTask; + + constructor(runTask: LinearRunTask) { + this.runTask = runTask; + } + + create( + key: IntegrationTaskKey, + params: WebhookCreateInput + // TODO: tidy up return type + ): Promise & { webhook: Webhook | undefined }> { + return this.runTask( + key, + async (client, task, io) => { + const payload = await client.createWebhook(params); + return withoutFunctions({ + ...payload, + webhook: await payload.webhook, + }); + }, + { + name: "Create webhook", + params, + } + ); + } + + list(key: IntegrationTaskKey, params?: WebhooksQueryVariables): Promise { + return this.runTask( + key, + async (client, task, io) => { + let connections = await client.webhooks(params); + const hooks = connections.nodes; + while (connections.pageInfo.hasNextPage) { + connections = await connections.fetchNext(); + hooks.push(...connections.nodes); + } + return withoutFunctions(hooks); + }, + { + name: "List webhooks", + params, + } + ); + } + + delete(key: IntegrationTaskKey, params: DeleteWebhookParams): Promise { + return this.runTask( + key, + async (client, task, io) => { + return withoutFunctions(await client.deleteWebhook(params.id)); + }, + { + name: "Delete webhook", + params, + } + ); + } + + update( + key: IntegrationTaskKey, + params: UpdateWebhookParams + ): Promise & { webhook: Webhook | undefined }> { + return this.runTask( + key, + async (client, task) => { + const payload = await client.updateWebhook(params.id, params.input); + return withoutFunctions({ + ...payload, + webhook: await payload.webhook, + }); + }, + { + name: "Update Webhook", + params, + } + ); + } +} + +type LinearEvents = (typeof events)[keyof typeof events]; + +const TriggerParamsSchema = z.object({ + resourceTypes: z.array(WebhookResourceTypeSchema), + teamId: z.string().optional(), + allPublicTeams: z.boolean().optional(), + actionTypes: z.array(WebhookActionTypeSchema).optional(), +}); + +export type TriggerParams = z.infer; + +type CreateTriggersResult = ExternalSourceTrigger< + TEventSpecification, + ReturnType +>; + +export function createTrigger( + source: ReturnType, + event: TEventSpecification, + params: TriggerParams + // options: { + // actionTypes?: WebhookActionType[]; // via params + // } +): CreateTriggersResult { + return new ExternalSourceTrigger({ + event, + params, + source, + options: {}, + }); +} + +const WebhookRegistrationDataSchema = z.object({ + success: z.literal(true), + webhook: z.object({ + id: z.string(), + enabled: z.boolean(), + }), +}); +type WebhookRegistrationData = z.infer; + +export function createWebhookEventSource( + integration: Linear +): ExternalSource { + return new ExternalSource("HTTP", { + id: "linear.webhook", + schema: TriggerParamsSchema, + // optionSchema: z.object({ }), + version: "0.1.0", + integration, + // TODO: filter by actionTypes + // filter: (params, options) => ({ }), + key: (params) => `${params.teamId ? params.teamId : "all"}`, + handler: webhookHandler, + register: async (event, io, ctx) => { + const { params, source: httpSource, options } = event; + + // (key-specific) stored data, undefined if not registered yet + const webhookData = WebhookRegistrationDataSchema.safeParse(httpSource.data); + + console.log(webhookData); + if (!webhookData.success) { + console.log(webhookData.error); + } + + // set of events to register + const allEvents = Array.from(new Set([...options.event.desired, ...options.event.missing])); + const registeredOptions = { + event: allEvents, + }; + + // easily identify webhooks on linear + const label = `trigger.${params.teamId ? params.teamId : "all"}`; + + // TODO: remove tunnel + const url = process.env["DEV_TUNNEL"] + ? httpSource.url.replace("http://localhost:3030", process.env["DEV_TUNNEL"]) + : httpSource.url; + + if (httpSource.active && webhookData.success) { + const hasMissingOptions = Object.values(options).some( + (option) => option.missing.length > 0 + ); + if (!hasMissingOptions) return; + + const updatedWebhook = await io.integration.webhooks().update("update-webhook", { + id: webhookData.data.webhook.id, + input: { + label, + resourceTypes: allEvents, + secret: httpSource.secret, + url, + }, + }); + + return { + data: WebhookRegistrationDataSchema.parse(updatedWebhook), + options: registeredOptions, + }; + } + + // check for existing hooks that match url + const listResponse = await io.integration.webhooks().list("list-webhooks"); + const existingWebhook = listResponse.find((w) => w.url === url); + + if (existingWebhook) { + const updatedWebhook = await io.integration.webhooks().update("update-webhook", { + id: existingWebhook.id, + input: { + label, + resourceTypes: allEvents, + secret: httpSource.secret, + url, + }, + }); + + return { + data: WebhookRegistrationDataSchema.parse(updatedWebhook), + options: registeredOptions, + }; + } + + const createPayload = await io.integration.webhooks().create("create-webhook", { + allPublicTeams: !params.teamId, + label, + resourceTypes: allEvents, + secret: httpSource.secret, + teamId: params.teamId, + url, + }); + + // TODO + // if(!createPayload.success) + + return { + data: WebhookRegistrationDataSchema.parse(createPayload), + secret: (await createPayload.webhook)?.secret, + options: registeredOptions, + }; + }, + }); +} + +// TODO +const SourceMetadataSchema = z.object({}).optional(); + +async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: Linear) { + logger.debug("[@trigger.dev/linear] Handling webhook payload"); + + const { rawEvent: request, source } = event; + + const LINEAR_IPS = ["35.231.147.226", "35.243.134.228"]; + + // TODO: remove tunnel + if (!process.env["DEV_TUNNEL"] && !LINEAR_IPS.includes(request.headers.get("Host") ?? "")) { + logger.error("[@trigger.dev/linear] Error validating webhook source, IP invalid."); + throw Error("[@trigger.dev/linear] Invalid source IP."); + } + + if (!request.body) { + logger.debug("[@trigger.dev/linear] No body found"); + return { events: [] }; + } + + const signature = request.headers.get(LINEAR_WEBHOOK_SIGNATURE_HEADER); + + if (!signature) { + logger.error("[@trigger.dev/linear] Error validating webhook signature, no signature found"); + throw Error("[@trigger.dev/linear] No signature found"); + } + + const rawBody = await request.text(); + const body = JSON.parse(rawBody); + const webhook = new LinearWebhooks(source.secret); + + // TODO: might want to do two passes, with and without timestamp (delay parsing) + if (!webhook.verify(Buffer.from(rawBody), signature, body[LINEAR_WEBHOOK_TS_FIELD])) { + logger.error("[@trigger.dev/linear] Error validating webhook signature, they don't match"); + throw Error("[@trigger.dev/linear] Invalid signature"); + } + + const webhookPayload = WebhookPayloadSchema.parse(body); + const parsedMetadata = SourceMetadataSchema.parse(source.metadata); + + return { + events: [ + { + id: webhookPayload.webhookId, + name: webhookPayload.webhookTimestamp.toISOString(), + source: "linear.app", + payload: webhookPayload, + context: {}, + }, + ], + metadata: parsedMetadata, + }; +} From bf680990f61a97d0dc86122e51ae1420dd6834b3 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 08:25:05 +0000 Subject: [PATCH 04/55] Provisional integration catalog entry --- .../externalApis/integrationCatalog.server.ts | 2 + .../externalApis/integrations/linear.ts | 335 ++++++++++++++++++ 2 files changed, 337 insertions(+) create mode 100644 apps/webapp/app/services/externalApis/integrations/linear.ts diff --git a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts index 4369ad87472..b86c2e496ce 100644 --- a/apps/webapp/app/services/externalApis/integrationCatalog.server.ts +++ b/apps/webapp/app/services/externalApis/integrationCatalog.server.ts @@ -1,5 +1,6 @@ import { airtable } from "./integrations/airtable"; import { github } from "./integrations/github"; +import { linear } from "./integrations/linear"; import { openai } from "./integrations/openai"; import { plain } from "./integrations/plain"; import { resend } from "./integrations/resend"; @@ -33,6 +34,7 @@ export class IntegrationCatalog { export const integrationCatalog = new IntegrationCatalog({ airtable, github, + linear, openai, plain, resend, diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts new file mode 100644 index 00000000000..c6b21b5bdda --- /dev/null +++ b/apps/webapp/app/services/externalApis/integrations/linear.ts @@ -0,0 +1,335 @@ +import type { HelpSample, Integration, ScopeAnnotation } from "../types"; + +const repoAnnotation: ScopeAnnotation = { + label: "Repo", +}; + +const webhookAnnotation: ScopeAnnotation = { + label: "Webhooks", +}; + +const orgAnnotation: ScopeAnnotation = { + label: "Orgs", +}; + +const keysAnnotation: ScopeAnnotation = { + label: "Keys", +}; + +const userAnnotation: ScopeAnnotation = { + label: "User", +}; + +const usageSample: HelpSample = { + title: "Using the client", + code: ` +import { Github, events } from "@trigger.dev/github"; + +const github = new Github({ + id: "__SLUG__", + token: process.env.GITHUB_TOKEN!, +}); + +client.defineJob({ + id: "github-integration-on-issue-opened", + name: "GitHub Integration - On Issue Opened", + version: "0.1.0", + integrations: { github }, + trigger: github.triggers.repo({ + event: events.onIssueOpened, + owner: "triggerdotdev", + repo: "empty", + }), + run: async (payload, io, ctx) => { + await io.github.addIssueAssignees("add assignee", { + owner: payload.repository.owner.login, + repo: payload.repository.name, + issueNumber: payload.issue.number, + assignees: ["matt-aitken"], + }); + + await io.github.addIssueLabels("add label", { + owner: payload.repository.owner.login, + repo: payload.repository.name, + issueNumber: payload.issue.number, + labels: ["bug"], + }); + + return { payload, ctx }; + }, +}); + + `, +}; + +export const linear: Integration = { + identifier: "linear", + name: "Linear", + packageName: "@trigger.dev/linear@latest", + authenticationMethods: { + oauth2: { + name: "OAuth", + type: "oauth2", + client: { + id: { + envName: "CLOUD_GITHUB_CLIENT_ID", + }, + secret: { + envName: "CLOUD_GITHUB_CLIENT_SECRET", + }, + }, + config: { + authorization: { + url: "https://github.com/login/oauth/authorize", + scopeSeparator: " ", + }, + token: { + url: "https://github.com/login/oauth/access_token", + metadata: { + accountPointer: "/team/name", + }, + }, + refresh: { + url: "https://github.com/login/oauth/authorize", + }, + }, + scopes: [ + { + name: "repo", + description: + "Grants full access to public and private repositories including read and write access to code, commit statuses, repository invitations, collaborators, deployment statuses, and repository webhooks. Note: In addition to repository related resources, the repo scope also grants access to manage organization-owned resources including projects, invitations, team memberships and webhooks. This scope also grants the ability to manage projects owned by users.", + annotations: [repoAnnotation], + }, + + { + name: "repo:status", + description: + "Grants read/write access to commit statuses in public and private repositories. This scope is only necessary to grant other users or services access to private repository commit statuses without granting access to the code.", + annotations: [repoAnnotation], + }, + + { + name: "repo_deployment", + description: + "Grants access to deployment statuses for public and private repositories. This scope is only necessary to grant other users or services access to deployment statuses, without granting access to the code.", + annotations: [repoAnnotation], + }, + + { + name: "public_repo", + description: + "Limits access to public repositories. That includes read/write access to code, commit statuses, repository projects, collaborators, and deployment statuses for public repositories and organizations. Also required for starring public repositories.", + annotations: [repoAnnotation], + }, + + { + name: "repo:invite", + description: + "Grants accept/decline abilities for invitations to collaborate on a repository. This scope is only necessary to grant other users or services access to invites without granting access to the code.", + annotations: [repoAnnotation], + }, + + { + name: "delete_repo", + description: "Grants access to delete adminable repositories.", + annotations: [repoAnnotation], + }, + + { + name: "security_events", + description: + "Grants read and write access to security events in the code scanning API. This scope is only necessary to grant other users or services access to security events without granting access to the code.", + }, + + { + name: "admin:repo_hook", + description: + "Grants read, write, ping, and delete access to repository hooks in public or private repositories. The repo and public_repo scopes grant full access to repositories, including repository hooks. Use the admin:repo_hook scope to limit access to only repository hooks.", + defaultChecked: true, + annotations: [webhookAnnotation], + }, + { + name: "write:repo_hook", + description: + "Grants read, write, and ping access to hooks in public or private repositories.", + annotations: [webhookAnnotation], + }, + { + name: "read:repo_hook", + description: "Grants read and ping access to hooks in public or private repositories.", + annotations: [webhookAnnotation], + }, + + { + name: "admin:org", + description: "Fully manage the organization and its teams, projects, and memberships.", + annotations: [orgAnnotation], + }, + { + name: "write:org", + description: + "Read and write access to organization membership, organization projects, and team membership.", + annotations: [orgAnnotation], + }, + + { + name: "read:org", + description: + "Read-only access to organization membership, organization projects, and team membership.", + annotations: [orgAnnotation], + }, + + { + name: "admin:public_key", + description: "Fully manage public keys.", + annotations: [keysAnnotation], + }, + + { + name: "write:public_key", + description: "Create, list, and view details for public keys.", + annotations: [keysAnnotation], + }, + { + name: "read:public_key", + description: "List and view details for public keys.", + annotations: [keysAnnotation], + }, + + { + name: "admin:org_hook", + description: + "Grants read, write, ping, and delete access to organization hooks. Note: OAuth tokens will only be able to perform these actions on organization hooks which were created by the OAuth App. Personal access tokens will only be able to perform these actions on organization hooks created by a user.", + annotations: [orgAnnotation, webhookAnnotation], + }, + + { + name: "gist", + description: "Grants write access to gists.", + }, + { + name: "notifications", + description: + "Grants read access to a user's notifications, mark as read access to threads, watch and unwatch access to a repository, and read, write, and delete access to thread subscriptions.", + }, + { + name: "user", + description: + " Grants read/write access to profile info only. Note that this scope includes user:email and user:follow.", + annotations: [userAnnotation], + }, + { + name: "read:user", + description: "Grants read access to a user's profile data.", + annotations: [userAnnotation], + }, + + { + name: "user:email", + description: "Grants read access to a user's email addresses.", + annotations: [userAnnotation], + }, + { + name: "user:follow", + description: "Grants access to follow or unfollow other users.", + annotations: [userAnnotation], + }, + { + name: "project", + description: "Grants read/write access to user and organization projects.", + }, + + { + name: "read:project", + description: "Grants read only access to user and organization projects.", + }, + + { + name: "write:discussion", + description: "Allows read and write access for team discussions.", + }, + + { + name: "read:discussion", + description: "Allows read access for team discussions.", + }, + + { + name: "write:packages", + description: "Grants access to upload or publish a package in GitHub Packages.", + }, + + { + name: "read:packages", + description: "Grants access to download or install packages from GitHub Packages.", + }, + + { + name: "delete:packages", + description: "Grants access to delete packages from GitHub Packages.", + }, + + { + name: "admin:gpg_key", + description: "Fully manage GPG keys.", + }, + + { + name: "write:gpg_key", + description: "Create, list, and view details for GPG keys.", + }, + + { + name: "read:gpg_key", + description: "List and view details for GPG keys.", + }, + + { + name: "codespace", + description: + "Grants the ability to create and manage codespaces. Codespaces can expose a GITHUB_TOKEN which may have a different set of scopes", + }, + + { + name: "workflow", + description: + "Grants the ability to add and update GitHub Actions workflow files. Workflow files can be committed without this scope if the same file (with both the same path and contents) exists on another branch in the same repository. Workflow files can expose GITHUB_TOKEN which may have a different set of scopes.", + }, + ], + help: { + samples: [ + { + title: "Creating the client", + code: ` +import { Github } from "@trigger.dev/github"; + +const github = new Github({ + id: "__SLUG__" +}); +`, + }, + usageSample, + ], + }, + }, + apikey: { + type: "apikey", + help: { + samples: [ + { + title: "Creating the client", + code: ` +import { Github } from "@trigger.dev/github"; + +const github = new Github({ + id: "__SLUG__", + token: process.env.GITHUB_TOKEN! +}); +`, + }, + usageSample, + ], + }, + }, + }, +}; From cf32d5e98c957bcd321203cc0f07f21acae022b1 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 08:26:20 +0000 Subject: [PATCH 05/55] Sample webhook jobs --- examples/job-catalog/src/linear.ts | 67 +++++++++++++++++++++++++++++- 1 file changed, 66 insertions(+), 1 deletion(-) diff --git a/examples/job-catalog/src/linear.ts b/examples/job-catalog/src/linear.ts index 693da49fc40..358cb44f51d 100644 --- a/examples/job-catalog/src/linear.ts +++ b/examples/job-catalog/src/linear.ts @@ -1 +1,66 @@ -export {} \ No newline at end of file +import { createExpressServer } from "@trigger.dev/express"; +import { DynamicTrigger, TriggerClient } from "@trigger.dev/sdk"; +import { Linear, events } from "@trigger.dev/linear"; + +export const client = new TriggerClient({ + id: "job-catalog", + apiKey: process.env["TRIGGER_API_KEY"], + apiUrl: process.env["TRIGGER_API_URL"], + verbose: false, + ioLogLocalEnabled: true, +}); + +const linear = new Linear({ + id: "linear-v2", + token: process.env["LINEAR_API_KEY"], +}); + +const dynamicOnAttachmentTrigger = new DynamicTrigger(client, { + id: "linear-attachment", + event: events.onAttachment, + source: linear.source, +}); + +client.defineJob({ + id: "linear-on-issue", + name: "Linear On Issue", + version: "0.1.0", + trigger: linear.onIssue(), + run: async (payload, io, ctx) => { + await io.logger.info("Issue changed!", { + action: payload.action, + id: payload.data.id, + url: payload.url, + }); + }, +}); + +client.defineJob({ + id: "linear-on-Comment", + name: "Linear On Comment", + version: "0.1.0", + trigger: linear.onCommentCreate(), + run: async (payload, io, ctx) => { + await io.logger.info("Comment created!", { + action: payload.action, + id: payload.data.id, + url: payload.url, + }); + }, +}); + +client.defineJob({ + id: "linear-on-Reaction", + name: "Linear On Reaction", + version: "0.1.0", + trigger: linear.onReactionUpdate(), + run: async (payload, io, ctx) => { + await io.logger.info("Reaction updated!", { + action: payload.action, + id: payload.data.id, + url: payload.url, + }); + }, +}); + +createExpressServer(client); From 52cd3f9ff4c6bc16b5bdfa4400f734e76288163e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 09:23:41 +0000 Subject: [PATCH 06/55] Attachments with alpha warnings --- integrations/linear/src/events.ts | 4 ++++ integrations/linear/src/index.ts | 5 ++++- integrations/linear/src/schemas.ts | 20 ++++++++++++++++++-- 3 files changed, 26 insertions(+), 3 deletions(-) diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts index d8467784c2a..2b9133c62db 100644 --- a/integrations/linear/src/events.ts +++ b/integrations/linear/src/events.ts @@ -14,6 +14,7 @@ import { // TODO: useful properties // TODO: Issue SLA event +/** **WARNING:** Still in alpha - use with caution! */ export const onAttachment: EventSpecification = { name: "Attachment", title: "On Attachment", @@ -23,6 +24,7 @@ export const onAttachment: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; +/** **WARNING:** Still in alpha - use with caution! */ export const onAttachmentCreate: EventSpecification = { name: "Attachment", title: "On Attachment", @@ -35,6 +37,7 @@ export const onAttachmentCreate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; +/** **WARNING:** Still in alpha - use with caution! */ export const onAttachmentRemove: EventSpecification = { name: "Attachment", title: "On Attachment Remove", @@ -47,6 +50,7 @@ export const onAttachmentRemove: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; +/** **WARNING:** Still in alpha - use with caution! */ export const onAttachmentUpdate: EventSpecification = { name: "Attachment", title: "On Attachment Update", diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index 90da0e876c7..3e7698d4781 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -101,6 +101,7 @@ export class Linear implements TriggerIntegration { // TODO: create separate sources to remove resourceTypes + /** **WARNING:** Still in alpha - use with caution! */ onAttachment(params: OnChangeParams = {}) { return createTrigger(this.source, events.onAttachment, { ...params, @@ -108,6 +109,7 @@ export class Linear implements TriggerIntegration { }); } + /** **WARNING:** Still in alpha - use with caution! */ onAttachmentCreate(params: OnChangeParams = {}) { return createTrigger(this.source, events.onAttachmentCreate, { ...params, @@ -115,13 +117,14 @@ export class Linear implements TriggerIntegration { }); } + /** **WARNING:** Still in alpha - use with caution! */ onAttachmentRemove(params: OnChangeParams = {}) { return createTrigger(this.source, events.onAttachmentRemove, { ...params, resourceTypes: ["Attachment"], }); } - + /** **WARNING:** Still in alpha - use with caution! */ onAttachmentUpdate(params: OnChangeParams = {}) { return createTrigger(this.source, events.onAttachmentUpdate, { ...params, diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts index 8f899c1619a..73e55292b52 100644 --- a/integrations/linear/src/schemas.ts +++ b/integrations/linear/src/schemas.ts @@ -55,8 +55,23 @@ const IssueDataSchema = z.object({ description: z.string(), }); -// FIXME: can't confirm schema as does not seem to trigger -const AttachmentDataSchema = z.object({}); +/** **WARNING:** Still in alpha - use with caution! */ +const AttachmentDataSchema = z.object({ + id: z.string(), + createdAt: z.coerce.date(), + updatedAt: z.coerce.date(), + title: z.string(), + url: z.string().url(), + creatorId: z.string(), + metadata: z.object({}).passthrough(), + source: z.object({ + type: z.string(), + imageUrl: z.string().url(), + }), + sourceType: z.string(), + groupBySource: z.boolean(), + issueId: z.string(), +}); const CommentDataSchema = z.object({ id: z.string(), @@ -200,6 +215,7 @@ export const IssueEventSchema = WebhookPayloadBaseSchema.extend({ }); export type IssueEvent = z.infer; +/** **WARNING:** Still in alpha - use with caution! */ export const AttachmentEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Attachment"), data: AttachmentDataSchema, From ccf355cf157ee24458ad1dfc5eb838eb5c4b9000 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 09:24:36 +0000 Subject: [PATCH 07/55] Remove some verbose logs --- integrations/linear/src/webhooks.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 5d2cc93c240..8e18e71b0eb 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -202,11 +202,6 @@ export function createWebhookEventSource( // (key-specific) stored data, undefined if not registered yet const webhookData = WebhookRegistrationDataSchema.safeParse(httpSource.data); - console.log(webhookData); - if (!webhookData.success) { - console.log(webhookData.error); - } - // set of events to register const allEvents = Array.from(new Set([...options.event.desired, ...options.event.missing])); const registeredOptions = { From d8d20d99f2ce6633e5183f9d7c5d4b62afa97a00 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 10:22:29 +0000 Subject: [PATCH 08/55] Fix IP restrictions --- integrations/linear/src/webhooks.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 8e18e71b0eb..a18d632f2a7 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -290,8 +290,17 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ const LINEAR_IPS = ["35.231.147.226", "35.243.134.228"]; + const clientIp = + request.headers.get("cf-connecting-ip") ?? + ( + request.headers.get("x-real-ip") ?? + request.headers.get("x-forwarded-for") ?? + // default to allowing request if expected headers missing + LINEAR_IPS[0] + ).split(",")[0]; + // TODO: remove tunnel - if (!process.env["DEV_TUNNEL"] && !LINEAR_IPS.includes(request.headers.get("Host") ?? "")) { + if (!process.env["DEV_TUNNEL"] && !LINEAR_IPS.includes(clientIp)) { logger.error("[@trigger.dev/linear] Error validating webhook source, IP invalid."); throw Error("[@trigger.dev/linear] Invalid source IP."); } From c5b69ce6524e3b40c26b66cdc56e087b8576b8c6 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 10:35:01 +0000 Subject: [PATCH 09/55] Remove tunnel --- integrations/linear/src/webhooks.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index a18d632f2a7..1438ee55a5c 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -211,11 +211,6 @@ export function createWebhookEventSource( // easily identify webhooks on linear const label = `trigger.${params.teamId ? params.teamId : "all"}`; - // TODO: remove tunnel - const url = process.env["DEV_TUNNEL"] - ? httpSource.url.replace("http://localhost:3030", process.env["DEV_TUNNEL"]) - : httpSource.url; - if (httpSource.active && webhookData.success) { const hasMissingOptions = Object.values(options).some( (option) => option.missing.length > 0 @@ -228,7 +223,7 @@ export function createWebhookEventSource( label, resourceTypes: allEvents, secret: httpSource.secret, - url, + url: httpSource.url, }, }); @@ -240,7 +235,7 @@ export function createWebhookEventSource( // check for existing hooks that match url const listResponse = await io.integration.webhooks().list("list-webhooks"); - const existingWebhook = listResponse.find((w) => w.url === url); + const existingWebhook = listResponse.find((w) => w.url === httpSource.url); if (existingWebhook) { const updatedWebhook = await io.integration.webhooks().update("update-webhook", { @@ -249,7 +244,7 @@ export function createWebhookEventSource( label, resourceTypes: allEvents, secret: httpSource.secret, - url, + url: httpSource.url, }, }); @@ -265,7 +260,7 @@ export function createWebhookEventSource( resourceTypes: allEvents, secret: httpSource.secret, teamId: params.teamId, - url, + url: httpSource.url, }); // TODO @@ -299,8 +294,7 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ LINEAR_IPS[0] ).split(",")[0]; - // TODO: remove tunnel - if (!process.env["DEV_TUNNEL"] && !LINEAR_IPS.includes(clientIp)) { + if (!LINEAR_IPS.includes(clientIp)) { logger.error("[@trigger.dev/linear] Error validating webhook source, IP invalid."); throw Error("[@trigger.dev/linear] Invalid source IP."); } From 714adde5f90c5a86a95c6c0183e1298d6246c3f4 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 10:49:19 +0000 Subject: [PATCH 10/55] Revert "Remove tunnel" This reverts commit c5b69ce6524e3b40c26b66cdc56e087b8576b8c6. --- integrations/linear/src/webhooks.ts | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 1438ee55a5c..a18d632f2a7 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -211,6 +211,11 @@ export function createWebhookEventSource( // easily identify webhooks on linear const label = `trigger.${params.teamId ? params.teamId : "all"}`; + // TODO: remove tunnel + const url = process.env["DEV_TUNNEL"] + ? httpSource.url.replace("http://localhost:3030", process.env["DEV_TUNNEL"]) + : httpSource.url; + if (httpSource.active && webhookData.success) { const hasMissingOptions = Object.values(options).some( (option) => option.missing.length > 0 @@ -223,7 +228,7 @@ export function createWebhookEventSource( label, resourceTypes: allEvents, secret: httpSource.secret, - url: httpSource.url, + url, }, }); @@ -235,7 +240,7 @@ export function createWebhookEventSource( // check for existing hooks that match url const listResponse = await io.integration.webhooks().list("list-webhooks"); - const existingWebhook = listResponse.find((w) => w.url === httpSource.url); + const existingWebhook = listResponse.find((w) => w.url === url); if (existingWebhook) { const updatedWebhook = await io.integration.webhooks().update("update-webhook", { @@ -244,7 +249,7 @@ export function createWebhookEventSource( label, resourceTypes: allEvents, secret: httpSource.secret, - url: httpSource.url, + url, }, }); @@ -260,7 +265,7 @@ export function createWebhookEventSource( resourceTypes: allEvents, secret: httpSource.secret, teamId: params.teamId, - url: httpSource.url, + url, }); // TODO @@ -294,7 +299,8 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ LINEAR_IPS[0] ).split(",")[0]; - if (!LINEAR_IPS.includes(clientIp)) { + // TODO: remove tunnel + if (!process.env["DEV_TUNNEL"] && !LINEAR_IPS.includes(clientIp)) { logger.error("[@trigger.dev/linear] Error validating webhook source, IP invalid."); throw Error("[@trigger.dev/linear] Invalid source IP."); } From 3777682a00942b007894833a07d0af73e2659a43 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:18:10 +0000 Subject: [PATCH 11/55] Resolve event name clashes --- examples/job-catalog/src/linear.ts | 6 +- integrations/linear/src/events.ts | 98 +++++++++++++++--------------- integrations/linear/src/index.ts | 97 ++++++++++++++--------------- 3 files changed, 101 insertions(+), 100 deletions(-) diff --git a/examples/job-catalog/src/linear.ts b/examples/job-catalog/src/linear.ts index 358cb44f51d..19bd2e855d5 100644 --- a/examples/job-catalog/src/linear.ts +++ b/examples/job-catalog/src/linear.ts @@ -11,7 +11,7 @@ export const client = new TriggerClient({ }); const linear = new Linear({ - id: "linear-v2", + id: "linear", token: process.env["LINEAR_API_KEY"], }); @@ -39,7 +39,7 @@ client.defineJob({ id: "linear-on-Comment", name: "Linear On Comment", version: "0.1.0", - trigger: linear.onCommentCreate(), + trigger: linear.onCommentCreated(), run: async (payload, io, ctx) => { await io.logger.info("Comment created!", { action: payload.action, @@ -53,7 +53,7 @@ client.defineJob({ id: "linear-on-Reaction", name: "Linear On Reaction", version: "0.1.0", - trigger: linear.onReactionUpdate(), + trigger: linear.onReactionUpdated(), run: async (payload, io, ctx) => { await io.logger.info("Reaction updated!", { action: payload.action, diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts index 2b9133c62db..bc4ea715887 100644 --- a/integrations/linear/src/events.ts +++ b/integrations/linear/src/events.ts @@ -25,9 +25,9 @@ export const onAttachment: EventSpecification = { }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentCreate: EventSpecification = { +export const onAttachmentCreated: EventSpecification = { name: "Attachment", - title: "On Attachment", + title: "On Attachment Created", source: "linear.app", icon: "linear", filter: { @@ -38,9 +38,9 @@ export const onAttachmentCreate: EventSpecification = { }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentRemove: EventSpecification = { +export const onAttachmentRemoved: EventSpecification = { name: "Attachment", - title: "On Attachment Remove", + title: "On Attachment Removed", source: "linear.app", icon: "linear", filter: { @@ -51,9 +51,9 @@ export const onAttachmentRemove: EventSpecification = { }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentUpdate: EventSpecification = { +export const onAttachmentUpdated: EventSpecification = { name: "Attachment", - title: "On Attachment Update", + title: "On Attachment Updated", source: "linear.app", icon: "linear", filter: { @@ -72,9 +72,9 @@ export const onComment: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCommentCreate: EventSpecification = { +export const onCommentCreated: EventSpecification = { name: "Comment", - title: "On Comment", + title: "On Comment Created", source: "linear.app", icon: "linear", filter: { @@ -84,9 +84,9 @@ export const onCommentCreate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCommentRemove: EventSpecification = { +export const onCommentRemoved: EventSpecification = { name: "Comment", - title: "On Comment Remove", + title: "On Comment Removed", source: "linear.app", icon: "linear", filter: { @@ -96,9 +96,9 @@ export const onCommentRemove: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCommentUpdate: EventSpecification = { +export const onCommentUpdated: EventSpecification = { name: "Comment", - title: "On Comment Update", + title: "On Comment Updated", source: "linear.app", icon: "linear", filter: { @@ -117,9 +117,9 @@ export const onCycle: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCycleCreate: EventSpecification = { +export const onCycleCreated: EventSpecification = { name: "Cycle", - title: "On Cycle", + title: "On Cycle Created", source: "linear.app", icon: "linear", filter: { @@ -129,9 +129,9 @@ export const onCycleCreate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCycleRemove: EventSpecification = { +export const onCycleRemoved: EventSpecification = { name: "Cycle", - title: "On Cycle Remove", + title: "On Cycle Removed", source: "linear.app", icon: "linear", filter: { @@ -141,9 +141,9 @@ export const onCycleRemove: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCycleUpdate: EventSpecification = { +export const onCycleUpdated: EventSpecification = { name: "Cycle", - title: "On Cycle Update", + title: "On Cycle Updated", source: "linear.app", icon: "linear", filter: { @@ -162,9 +162,9 @@ export const onIssue: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueCreate: EventSpecification = { +export const onIssueCreated: EventSpecification = { name: "Issue", - title: "On Issue", + title: "On Issue Created", source: "linear.app", icon: "linear", filter: { @@ -174,9 +174,9 @@ export const onIssueCreate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueRemove: EventSpecification = { +export const onIssueRemoved: EventSpecification = { name: "Issue", - title: "On Issue Remove", + title: "On Issue Removed", source: "linear.app", icon: "linear", filter: { @@ -186,9 +186,9 @@ export const onIssueRemove: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueUpdate: EventSpecification = { +export const onIssueUpdated: EventSpecification = { name: "Issue", - title: "On Issue Update", + title: "On Issue Updated", source: "linear.app", icon: "linear", filter: { @@ -207,9 +207,9 @@ export const onIssueLabel: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueLabelCreate: EventSpecification = { +export const onIssueLabelCreated: EventSpecification = { name: "IssueLabel", - title: "On IssueLabel", + title: "On IssueLabel Created", source: "linear.app", icon: "linear", filter: { @@ -219,9 +219,9 @@ export const onIssueLabelCreate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueLabelRemove: EventSpecification = { +export const onIssueLabelRemoved: EventSpecification = { name: "IssueLabel", - title: "On IssueLabel Remove", + title: "On IssueLabel Removed", source: "linear.app", icon: "linear", filter: { @@ -231,9 +231,9 @@ export const onIssueLabelRemove: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueLabelUpdate: EventSpecification = { +export const onIssueLabelUpdated: EventSpecification = { name: "IssueLabel", - title: "On IssueLabel Update", + title: "On IssueLabel Updated", source: "linear.app", icon: "linear", filter: { @@ -252,9 +252,9 @@ export const onProject: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProject_Create: EventSpecification = { +export const onProjectCreated: EventSpecification = { name: "Project", - title: "On Project", + title: "On Project Created", source: "linear.app", icon: "linear", filter: { @@ -264,9 +264,9 @@ export const onProject_Create: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProject_Remove: EventSpecification = { +export const onProjectRemoved: EventSpecification = { name: "Project", - title: "On Project Remove", + title: "On Project Removed", source: "linear.app", icon: "linear", filter: { @@ -276,10 +276,10 @@ export const onProject_Remove: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -// TODO: think of a better naming scheme (clashes with ProjectUpdate entity) -export const onProject_Update: EventSpecification = { +// TODO: think of a better naming scheme (clashes with ProjectUpdated entity) +export const onProjectUpdated: EventSpecification = { name: "Project", - title: "On Project Update", + title: "On Project Updated", source: "linear.app", icon: "linear", filter: { @@ -298,9 +298,9 @@ export const onProjectUpdate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProjectUpdateCreate: EventSpecification = { +export const onProjectUpdateCreated: EventSpecification = { name: "ProjectUpdate", - title: "On ProjectUpdate", + title: "On ProjectUpdate Created", source: "linear.app", icon: "linear", filter: { @@ -310,9 +310,9 @@ export const onProjectUpdateCreate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProjectUpdateRemove: EventSpecification = { +export const onProjectUpdateRemoved: EventSpecification = { name: "ProjectUpdate", - title: "On ProjectUpdate Remove", + title: "On ProjectUpdate Removed", source: "linear.app", icon: "linear", filter: { @@ -322,9 +322,9 @@ export const onProjectUpdateRemove: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProjectUpdateUpdate: EventSpecification = { +export const onProjectUpdateUpdated: EventSpecification = { name: "ProjectUpdate", - title: "On ProjectUpdate Update", + title: "On ProjectUpdate Updated", source: "linear.app", icon: "linear", filter: { @@ -343,9 +343,9 @@ export const onReaction: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onReactionCreate: EventSpecification = { +export const onReactionCreated: EventSpecification = { name: "Reaction", - title: "On Reaction", + title: "On Reaction Created", source: "linear.app", icon: "linear", filter: { @@ -355,9 +355,9 @@ export const onReactionCreate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onReactionRemove: EventSpecification = { +export const onReactionRemoved: EventSpecification = { name: "Reaction", - title: "On Reaction Remove", + title: "On Reaction Removed", source: "linear.app", icon: "linear", filter: { @@ -367,9 +367,9 @@ export const onReactionRemove: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onReactionUpdate: EventSpecification = { +export const onReactionUpdated: EventSpecification = { name: "Reaction", - title: "On Reaction Update", + title: "On Reaction Updated", source: "linear.app", icon: "linear", filter: { diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index 3e7698d4781..45a64e739c2 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -110,23 +110,24 @@ export class Linear implements TriggerIntegration { } /** **WARNING:** Still in alpha - use with caution! */ - onAttachmentCreate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onAttachmentCreate, { + onAttachmentCreated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onAttachmentCreated, { ...params, resourceTypes: ["Attachment"], }); } /** **WARNING:** Still in alpha - use with caution! */ - onAttachmentRemove(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onAttachmentRemove, { + onAttachmentRemoved(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onAttachmentRemoved, { ...params, resourceTypes: ["Attachment"], }); } + /** **WARNING:** Still in alpha - use with caution! */ - onAttachmentUpdate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onAttachmentUpdate, { + onAttachmentUpdated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onAttachmentUpdated, { ...params, resourceTypes: ["Attachment"], }); @@ -136,22 +137,22 @@ export class Linear implements TriggerIntegration { return createTrigger(this.source, events.onComment, { ...params, resourceTypes: ["Comment"] }); } - onCommentCreate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCommentCreate, { + onCommentCreated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCommentCreated, { ...params, resourceTypes: ["Comment"], }); } - onCommentRemove(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCommentRemove, { + onCommentRemoved(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCommentRemoved, { ...params, resourceTypes: ["Comment"], }); } - onCommentUpdate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCommentUpdate, { + onCommentUpdated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCommentUpdated, { ...params, resourceTypes: ["Comment"], }); @@ -161,22 +162,22 @@ export class Linear implements TriggerIntegration { return createTrigger(this.source, events.onCycle, { ...params, resourceTypes: ["Cycle"] }); } - onCycleCreate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCycleCreate, { + onCycleCreated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCycleCreated, { ...params, resourceTypes: ["Cycle"], }); } - onCycleRemove(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCycleRemove, { + onCycleRemoved(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCycleRemoved, { ...params, resourceTypes: ["Cycle"], }); } - onCycleUpdate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCycleUpdate, { + onCycleUpdated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onCycleUpdated, { ...params, resourceTypes: ["Cycle"], }); @@ -186,22 +187,22 @@ export class Linear implements TriggerIntegration { return createTrigger(this.source, events.onIssue, { ...params, resourceTypes: ["Issue"] }); } - onIssueCreate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueCreate, { + onIssueCreated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueCreated, { ...params, resourceTypes: ["Issue"], }); } - onIssueRemove(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueRemove, { + onIssueRemoved(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueRemoved, { ...params, resourceTypes: ["Issue"], }); } - onIssueUpdate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueUpdate, { + onIssueUpdated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueUpdated, { ...params, resourceTypes: ["Issue"], }); @@ -214,22 +215,22 @@ export class Linear implements TriggerIntegration { }); } - onIssueLabelCreate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueLabelCreate, { + onIssueLabelCreated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueLabelCreated, { ...params, resourceTypes: ["IssueLabel"], }); } - onIssueLabelRemove(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueLabelRemove, { + onIssueLabelRemoved(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueLabelRemoved, { ...params, resourceTypes: ["IssueLabel"], }); } - onIssueLabelUpdate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueLabelUpdate, { + onIssueLabelUpdated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueLabelUpdated, { ...params, resourceTypes: ["IssueLabel"], }); @@ -239,22 +240,22 @@ export class Linear implements TriggerIntegration { return createTrigger(this.source, events.onProject, { ...params, resourceTypes: ["Project"] }); } - onProject_Create(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProject_Create, { + onProjectCreated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectCreated, { ...params, resourceTypes: ["Project"], }); } - onProject_Remove(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProject_Remove, { + onProjectRemoved(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectRemoved, { ...params, resourceTypes: ["Project"], }); } - onProject_Update(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProject_Update, { + onProjectUpdated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectUpdated, { ...params, resourceTypes: ["Project"], }); @@ -267,22 +268,22 @@ export class Linear implements TriggerIntegration { }); } - onProjectUpdateCreate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectUpdateCreate, { + onProjectUpdateCreated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectUpdateCreated, { ...params, resourceTypes: ["ProjectUpdate"], }); } - onProjectUpdateRemove(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectUpdateRemove, { + onProjectUpdateRemoved(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectUpdateRemoved, { ...params, resourceTypes: ["ProjectUpdate"], }); } - onProjectUpdateUpdate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectUpdateUpdate, { + onProjectUpdateUpdated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onProjectUpdateUpdated, { ...params, resourceTypes: ["ProjectUpdate"], }); @@ -295,22 +296,22 @@ export class Linear implements TriggerIntegration { }); } - onReactionCreate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onReactionCreate, { + onReactionCreated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onReactionCreated, { ...params, resourceTypes: ["Reaction"], }); } - onReactionRemove(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onReactionRemove, { + onReactionRemoved(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onReactionRemoved, { ...params, resourceTypes: ["Reaction"], }); } - onReactionUpdate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onReactionUpdate, { + onReactionUpdated(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onReactionUpdated, { ...params, resourceTypes: ["Reaction"], }); From a46f237a57792054bfd3eeca697f14ed97bdbbf8 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:32:17 +0000 Subject: [PATCH 12/55] Remove circular dependency --- integrations/linear/src/index.ts | 3 ++- integrations/linear/src/schemas.ts | 20 +++++++++++++++++++- integrations/linear/src/webhooks.ts | 25 +++++-------------------- 3 files changed, 26 insertions(+), 22 deletions(-) diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index 45a64e739c2..11b6b1b4436 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -11,7 +11,8 @@ import { } from "@trigger.dev/sdk"; import { LinearClient } from "@linear/sdk"; import * as events from "./events"; -import { WebhookActionType, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; +import { Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; +import { WebhookActionType } from "./schemas"; export type LinearIntegrationOptions = { id: string; diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts index 73e55292b52..278692ca884 100644 --- a/integrations/linear/src/schemas.ts +++ b/integrations/linear/src/schemas.ts @@ -1,5 +1,23 @@ import { z } from "zod"; -import { WebhookActionTypeSchema } from "./webhooks"; + +export const WebhookResourceTypeSchema = z.union([ + z.literal("Attachment"), + z.literal("Comment"), + z.literal("Cycle"), + z.literal("Issue"), + z.literal("IssueLabel"), + z.literal("Project"), + z.literal("ProjectUpdate"), + z.literal("Reaction"), +]); +export type WebhookResourceType = z.infer; + +export const WebhookActionTypeSchema = z.union([ + z.literal("create"), + z.literal("remove"), + z.literal("update"), +]); +export type WebhookActionType = z.infer; const ReactionShortSchema = z.object({ emoji: z.string(), diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index a18d632f2a7..619fcc0a17c 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -21,26 +21,11 @@ import { WebhookUpdateInput, WebhooksQueryVariables, } from "@linear/sdk/dist/_generated_documents"; -import { WebhookPayloadSchema } from "./schemas"; - -export const WebhookResourceTypeSchema = z.union([ - z.literal("Attachment"), - z.literal("Comment"), - z.literal("Cycle"), - z.literal("Issue"), - z.literal("IssueLabel"), - z.literal("Project"), - z.literal("ProjectUpdate"), - z.literal("Reaction"), -]); -export type WebhookResourceType = z.infer; - -export const WebhookActionTypeSchema = z.union([ - z.literal("create"), - z.literal("remove"), - z.literal("update"), -]); -export type WebhookActionType = z.infer; +import { + WebhookActionTypeSchema, + WebhookPayloadSchema, + WebhookResourceTypeSchema, +} from "./schemas"; type DeleteWebhookParams = { id: string; From a064268a18ffd3df30ee339d2ad841f9568a78d8 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 12:35:11 +0000 Subject: [PATCH 13/55] Use correct payload uuid --- integrations/linear/src/webhooks.ts | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 619fcc0a17c..02fef150248 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -290,6 +290,14 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ throw Error("[@trigger.dev/linear] Invalid source IP."); } + const payloadUuid = request.headers.get("Linear-Delivery"); + const payloadEvent = request.headers.get("Linear-Event"); + + if (!payloadUuid || !payloadEvent) { + logger.debug("[@trigger.dev/linear] Missing expected Linear headers"); + return { events: [] }; + } + if (!request.body) { logger.debug("[@trigger.dev/linear] No body found"); return { events: [] }; @@ -318,7 +326,7 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ return { events: [ { - id: webhookPayload.webhookId, + id: payloadUuid, name: webhookPayload.webhookTimestamp.toISOString(), source: "linear.app", payload: webhookPayload, From ed3d659ac4141d5fea84a69490063b221cc0b9e5 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:25:50 +0000 Subject: [PATCH 14/55] Schema fixes --- integrations/linear/src/schemas.ts | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts index 278692ca884..9223297d696 100644 --- a/integrations/linear/src/schemas.ts +++ b/integrations/linear/src/schemas.ts @@ -98,7 +98,7 @@ const CommentDataSchema = z.object({ body: z.string(), issueId: z.string(), userId: z.string(), - editedAt: z.string(), + editedAt: z.string().optional(), reactionData: z.array(ReactionShortSchema), issue: z.object({ id: z.string(), @@ -219,7 +219,7 @@ const CycleDataSchema = z.object({ export const WebhookPayloadBaseSchema = z.object({ action: WebhookActionTypeSchema, createdAt: z.coerce.date(), - url: z.string().url(), + url: z.string().url().optional(), // TODO: check if this is always present organizationId: z.string().optional(), webhookTimestamp: z.coerce.date(), @@ -229,7 +229,7 @@ export const WebhookPayloadBaseSchema = z.object({ export const IssueEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Issue"), data: IssueDataSchema, - updatedFrom: IssueDataSchema.partial(), + updatedFrom: IssueDataSchema.partial().optional(), }); export type IssueEvent = z.infer; @@ -237,49 +237,49 @@ export type IssueEvent = z.infer; export const AttachmentEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Attachment"), data: AttachmentDataSchema, - updatedFrom: AttachmentDataSchema.partial(), + updatedFrom: AttachmentDataSchema.partial().optional(), }); export type AttachmentEvent = z.infer; export const CommentEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Comment"), data: CommentDataSchema, - updatedFrom: CommentDataSchema.partial(), + updatedFrom: CommentDataSchema.partial().optional(), }); export type CommentEvent = z.infer; export const IssueLabelEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("IssueLabel"), data: IssueLabelDataSchema, - updatedFrom: IssueLabelDataSchema.partial(), + updatedFrom: IssueLabelDataSchema.partial().optional(), }); export type IssueLabelEvent = z.infer; export const ReactionEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Reaction"), data: ReactionDataSchema, - updatedFrom: ReactionDataSchema.partial(), + updatedFrom: ReactionDataSchema.partial().optional(), }); export type ReactionEvent = z.infer; export const ProjectEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Project"), data: ProjectDataSchema, - updatedFrom: ProjectDataSchema.partial(), + updatedFrom: ProjectDataSchema.partial().optional(), }); export type ProjectEvent = z.infer; export const ProjectUpdateEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("ProjectUpdate"), data: ProjectUpdateDataSchema, - updatedFrom: ProjectUpdateDataSchema.partial(), + updatedFrom: ProjectUpdateDataSchema.partial().optional(), }); export type ProjectUpdateEvent = z.infer; export const CycleEventSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Cycle"), data: CycleDataSchema, - updatedFrom: CycleDataSchema.partial(), + updatedFrom: CycleDataSchema.partial().optional(), }); export type CycleEvent = z.infer; From b20eca7185f48e35d76b1e72b15e08fcf3b20ce9 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:26:44 +0000 Subject: [PATCH 15/55] Fix webhook event name --- integrations/linear/src/webhooks.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 02fef150248..6982ce355d4 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -294,7 +294,7 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ const payloadEvent = request.headers.get("Linear-Event"); if (!payloadUuid || !payloadEvent) { - logger.debug("[@trigger.dev/linear] Missing expected Linear headers"); + logger.debug("[@trigger.dev/linear] Missing required Linear headers"); return { events: [] }; } @@ -327,7 +327,7 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ events: [ { id: payloadUuid, - name: webhookPayload.webhookTimestamp.toISOString(), + name: payloadEvent, source: "linear.app", payload: webhookPayload, context: {}, From 7aea120139ccd5281bc36db53aa787b0c3cd294b Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:28:50 +0000 Subject: [PATCH 16/55] Start to Linearify catalog entry --- .../externalApis/integrations/linear.ts | 48 +++++++++---------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts index c6b21b5bdda..c1b40ed3400 100644 --- a/apps/webapp/app/services/externalApis/integrations/linear.ts +++ b/apps/webapp/app/services/externalApis/integrations/linear.ts @@ -23,32 +23,32 @@ const userAnnotation: ScopeAnnotation = { const usageSample: HelpSample = { title: "Using the client", code: ` -import { Github, events } from "@trigger.dev/github"; +import { Linear, events } from "@trigger.dev/linear"; -const github = new Github({ +const linear = new Linear({ id: "__SLUG__", - token: process.env.GITHUB_TOKEN!, + token: process.env.LINEAR_TOKEN!, }); client.defineJob({ - id: "github-integration-on-issue-opened", - name: "GitHub Integration - On Issue Opened", + id: "linear-integration-on-issue-opened", + name: "Linear Integration - On Issue Opened", version: "0.1.0", - integrations: { github }, - trigger: github.triggers.repo({ + integrations: { linear }, + trigger: linear.triggers.repo({ event: events.onIssueOpened, owner: "triggerdotdev", repo: "empty", }), run: async (payload, io, ctx) => { - await io.github.addIssueAssignees("add assignee", { + await io.linear.addIssueAssignees("add assignee", { owner: payload.repository.owner.login, repo: payload.repository.name, issueNumber: payload.issue.number, assignees: ["matt-aitken"], }); - await io.github.addIssueLabels("add label", { + await io.linear.addIssueLabels("add label", { owner: payload.repository.owner.login, repo: payload.repository.name, issueNumber: payload.issue.number, @@ -72,25 +72,25 @@ export const linear: Integration = { type: "oauth2", client: { id: { - envName: "CLOUD_GITHUB_CLIENT_ID", + envName: "CLOUD_LINEAR_CLIENT_ID", }, secret: { - envName: "CLOUD_GITHUB_CLIENT_SECRET", + envName: "CLOUD_LINEAR_CLIENT_SECRET", }, }, config: { authorization: { - url: "https://github.com/login/oauth/authorize", + url: "https://linear.com/login/oauth/authorize", scopeSeparator: " ", }, token: { - url: "https://github.com/login/oauth/access_token", + url: "https://linear.com/login/oauth/access_token", metadata: { accountPointer: "/team/name", }, }, refresh: { - url: "https://github.com/login/oauth/authorize", + url: "https://linear.com/login/oauth/authorize", }, }, scopes: [ @@ -256,17 +256,17 @@ export const linear: Integration = { { name: "write:packages", - description: "Grants access to upload or publish a package in GitHub Packages.", + description: "Grants access to upload or publish a package in Linear Packages.", }, { name: "read:packages", - description: "Grants access to download or install packages from GitHub Packages.", + description: "Grants access to download or install packages from Linear Packages.", }, { name: "delete:packages", - description: "Grants access to delete packages from GitHub Packages.", + description: "Grants access to delete packages from Linear Packages.", }, { @@ -287,13 +287,13 @@ export const linear: Integration = { { name: "codespace", description: - "Grants the ability to create and manage codespaces. Codespaces can expose a GITHUB_TOKEN which may have a different set of scopes", + "Grants the ability to create and manage codespaces. Codespaces can expose a LINEAR_TOKEN which may have a different set of scopes", }, { name: "workflow", description: - "Grants the ability to add and update GitHub Actions workflow files. Workflow files can be committed without this scope if the same file (with both the same path and contents) exists on another branch in the same repository. Workflow files can expose GITHUB_TOKEN which may have a different set of scopes.", + "Grants the ability to add and update Linear Actions workflow files. Workflow files can be committed without this scope if the same file (with both the same path and contents) exists on another branch in the same repository. Workflow files can expose LINEAR_TOKEN which may have a different set of scopes.", }, ], help: { @@ -301,9 +301,9 @@ export const linear: Integration = { { title: "Creating the client", code: ` -import { Github } from "@trigger.dev/github"; +import { Linear } from "@trigger.dev/linear"; -const github = new Github({ +const linear = new Linear({ id: "__SLUG__" }); `, @@ -319,11 +319,11 @@ const github = new Github({ { title: "Creating the client", code: ` -import { Github } from "@trigger.dev/github"; +import { Linear } from "@trigger.dev/linear"; -const github = new Github({ +const linear = new Linear({ id: "__SLUG__", - token: process.env.GITHUB_TOKEN! + token: process.env.LINEAR_TOKEN! }); `, }, From 558cec76e8c3cdd33810d45f7784f30d12de9ff1 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 13:58:43 +0000 Subject: [PATCH 17/55] Remove todo --- integrations/linear/src/webhooks.ts | 3 --- 1 file changed, 3 deletions(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 6982ce355d4..c6eeea2313a 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -253,9 +253,6 @@ export function createWebhookEventSource( url, }); - // TODO - // if(!createPayload.success) - return { data: WebhookRegistrationDataSchema.parse(createPayload), secret: (await createPayload.webhook)?.secret, From 7a4d282a468ea7c5787020aa18293bd62ac6787e Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Fri, 15 Sep 2023 14:06:02 +0000 Subject: [PATCH 18/55] More catalog updates --- .../externalApis/integrations/linear.ts | 255 ++---------------- 1 file changed, 23 insertions(+), 232 deletions(-) diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts index c1b40ed3400..1b1bca035bb 100644 --- a/apps/webapp/app/services/externalApis/integrations/linear.ts +++ b/apps/webapp/app/services/externalApis/integrations/linear.ts @@ -1,24 +1,4 @@ -import type { HelpSample, Integration, ScopeAnnotation } from "../types"; - -const repoAnnotation: ScopeAnnotation = { - label: "Repo", -}; - -const webhookAnnotation: ScopeAnnotation = { - label: "Webhooks", -}; - -const orgAnnotation: ScopeAnnotation = { - label: "Orgs", -}; - -const keysAnnotation: ScopeAnnotation = { - label: "Keys", -}; - -const userAnnotation: ScopeAnnotation = { - label: "User", -}; +import type { HelpSample, Integration } from "../types"; const usageSample: HelpSample = { title: "Using the client", @@ -31,28 +11,18 @@ const linear = new Linear({ }); client.defineJob({ - id: "linear-integration-on-issue-opened", - name: "Linear Integration - On Issue Opened", + id: "linear-integration-on-issue-created", + name: "Linear Integration - On Issue Created", version: "0.1.0", integrations: { linear }, - trigger: linear.triggers.repo({ - event: events.onIssueOpened, - owner: "triggerdotdev", - repo: "empty", - }), + trigger: linear.onIssueCreated(), run: async (payload, io, ctx) => { - await io.linear.addIssueAssignees("add assignee", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - issueNumber: payload.issue.number, - assignees: ["matt-aitken"], + await io.linear.doSomething("some-task", { + foo: "bar" }); - await io.linear.addIssueLabels("add label", { - owner: payload.repository.owner.login, - repo: payload.repository.name, - issueNumber: payload.issue.number, - labels: ["bug"], + await io.linear.doSomethingElse("some-other-task", { + bar: "baz" }); return { payload, ctx }; @@ -80,220 +50,41 @@ export const linear: Integration = { }, config: { authorization: { - url: "https://linear.com/login/oauth/authorize", + url: "https://linear.app/oauth/authorize", scopeSeparator: " ", }, token: { - url: "https://linear.com/login/oauth/access_token", - metadata: { - accountPointer: "/team/name", - }, + url: "https://api.linear.app/oauth/token", + metadata: {}, }, refresh: { - url: "https://linear.com/login/oauth/authorize", + url: "https://linear.app/oauth/authorize", }, }, scopes: [ { - name: "repo", - description: - "Grants full access to public and private repositories including read and write access to code, commit statuses, repository invitations, collaborators, deployment statuses, and repository webhooks. Note: In addition to repository related resources, the repo scope also grants access to manage organization-owned resources including projects, invitations, team memberships and webhooks. This scope also grants the ability to manage projects owned by users.", - annotations: [repoAnnotation], - }, - - { - name: "repo:status", - description: - "Grants read/write access to commit statuses in public and private repositories. This scope is only necessary to grant other users or services access to private repository commit statuses without granting access to the code.", - annotations: [repoAnnotation], - }, - - { - name: "repo_deployment", - description: - "Grants access to deployment statuses for public and private repositories. This scope is only necessary to grant other users or services access to deployment statuses, without granting access to the code.", - annotations: [repoAnnotation], - }, - - { - name: "public_repo", - description: - "Limits access to public repositories. That includes read/write access to code, commit statuses, repository projects, collaborators, and deployment statuses for public repositories and organizations. Also required for starring public repositories.", - annotations: [repoAnnotation], - }, - - { - name: "repo:invite", - description: - "Grants accept/decline abilities for invitations to collaborate on a repository. This scope is only necessary to grant other users or services access to invites without granting access to the code.", - annotations: [repoAnnotation], - }, - - { - name: "delete_repo", - description: "Grants access to delete adminable repositories.", - annotations: [repoAnnotation], - }, - - { - name: "security_events", - description: - "Grants read and write access to security events in the code scanning API. This scope is only necessary to grant other users or services access to security events without granting access to the code.", - }, - - { - name: "admin:repo_hook", - description: - "Grants read, write, ping, and delete access to repository hooks in public or private repositories. The repo and public_repo scopes grant full access to repositories, including repository hooks. Use the admin:repo_hook scope to limit access to only repository hooks.", - defaultChecked: true, - annotations: [webhookAnnotation], - }, - { - name: "write:repo_hook", - description: - "Grants read, write, and ping access to hooks in public or private repositories.", - annotations: [webhookAnnotation], - }, - { - name: "read:repo_hook", - description: "Grants read and ping access to hooks in public or private repositories.", - annotations: [webhookAnnotation], - }, - - { - name: "admin:org", - description: "Fully manage the organization and its teams, projects, and memberships.", - annotations: [orgAnnotation], - }, - { - name: "write:org", - description: - "Read and write access to organization membership, organization projects, and team membership.", - annotations: [orgAnnotation], - }, - - { - name: "read:org", - description: - "Read-only access to organization membership, organization projects, and team membership.", - annotations: [orgAnnotation], - }, - - { - name: "admin:public_key", - description: "Fully manage public keys.", - annotations: [keysAnnotation], - }, - - { - name: "write:public_key", - description: "Create, list, and view details for public keys.", - annotations: [keysAnnotation], - }, - { - name: "read:public_key", - description: "List and view details for public keys.", - annotations: [keysAnnotation], - }, - - { - name: "admin:org_hook", + name: "write", description: - "Grants read, write, ping, and delete access to organization hooks. Note: OAuth tokens will only be able to perform these actions on organization hooks which were created by the OAuth App. Personal access tokens will only be able to perform these actions on organization hooks created by a user.", - annotations: [orgAnnotation, webhookAnnotation], - }, - - { - name: "gist", - description: "Grants write access to gists.", - }, - { - name: "notifications", - description: - "Grants read access to a user's notifications, mark as read access to threads, watch and unwatch access to a repository, and read, write, and delete access to thread subscriptions.", - }, - { - name: "user", - description: - " Grants read/write access to profile info only. Note that this scope includes user:email and user:follow.", - annotations: [userAnnotation], - }, - { - name: "read:user", - description: "Grants read access to a user's profile data.", - annotations: [userAnnotation], - }, - - { - name: "user:email", - description: "Grants read access to a user's email addresses.", - annotations: [userAnnotation], - }, - { - name: "user:follow", - description: "Grants access to follow or unfollow other users.", - annotations: [userAnnotation], - }, - { - name: "project", - description: "Grants read/write access to user and organization projects.", - }, - - { - name: "read:project", - description: "Grants read only access to user and organization projects.", - }, - - { - name: "write:discussion", - description: "Allows read and write access for team discussions.", - }, - - { - name: "read:discussion", - description: "Allows read access for team discussions.", - }, - - { - name: "write:packages", - description: "Grants access to upload or publish a package in Linear Packages.", + "Grants global write access to the user's account. Use a more targeted scope if you don't need full access.", + defaultChecked: true, }, { - name: "read:packages", - description: "Grants access to download or install packages from Linear Packages.", + name: "issue:create", + description: "Grants access to create issues and attachments only.", + annotations: [{ label: "Issues" }], }, { - name: "delete:packages", - description: "Grants access to delete packages from Linear Packages.", - }, - - { - name: "admin:gpg_key", - description: "Fully manage GPG keys.", - }, - - { - name: "write:gpg_key", - description: "Create, list, and view details for GPG keys.", - }, - - { - name: "read:gpg_key", - description: "List and view details for GPG keys.", - }, - - { - name: "codespace", - description: - "Grants the ability to create and manage codespaces. Codespaces can expose a LINEAR_TOKEN which may have a different set of scopes", + name: "comments:create", + description: "Grants access to create new issue comments.", + annotations: [{ label: "Comments" }], }, { - name: "workflow", + name: "admin", description: - "Grants the ability to add and update Linear Actions workflow files. Workflow files can be committed without this scope if the same file (with both the same path and contents) exists on another branch in the same repository. Workflow files can expose LINEAR_TOKEN which may have a different set of scopes.", + "Grants full access to admin-level endpoints. Don't use this unless you really need it.", }, ], help: { From 39625e9c802208f51b157c4f87b51ba0ea489675 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:30:27 +0000 Subject: [PATCH 19/55] Make OAuth work --- .env.example | 2 ++ apps/webapp/app/services/externalApis/integrations/linear.ts | 5 +++-- 2 files changed, 5 insertions(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 295b41745c7..48170e8f365 100644 --- a/.env.example +++ b/.env.example @@ -31,6 +31,8 @@ CLOUD_AIRTABLE_CLIENT_ID= CLOUD_AIRTABLE_CLIENT_SECRET= CLOUD_GITHUB_CLIENT_ID= CLOUD_GITHUB_CLIENT_SECRET= +CLOUD_LINEAR_CLIENT_ID= +CLOUD_LINEAR_CLIENT_SECRET= CLOUD_SLACK_APP_HOST= CLOUD_SLACK_CLIENT_ID= CLOUD_SLACK_CLIENT_SECRET= \ No newline at end of file diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts index 1b1bca035bb..6ffb757f91c 100644 --- a/apps/webapp/app/services/externalApis/integrations/linear.ts +++ b/apps/webapp/app/services/externalApis/integrations/linear.ts @@ -51,7 +51,7 @@ export const linear: Integration = { config: { authorization: { url: "https://linear.app/oauth/authorize", - scopeSeparator: " ", + scopeSeparator: ",", }, token: { url: "https://api.linear.app/oauth/token", @@ -60,13 +60,14 @@ export const linear: Integration = { refresh: { url: "https://linear.app/oauth/authorize", }, + pkce: false, }, scopes: [ { name: "write", description: "Grants global write access to the user's account. Use a more targeted scope if you don't need full access.", - defaultChecked: true, + defaultChecked: true, }, { From 107bf4ac6e3582515cc3e46f539c3c1387438b1f Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:30:35 +0000 Subject: [PATCH 20/55] Rename webhook helper --- integrations/linear/src/webhooks.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index c6eeea2313a..eb2836a4da4 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -309,10 +309,9 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ const rawBody = await request.text(); const body = JSON.parse(rawBody); - const webhook = new LinearWebhooks(source.secret); + const webhookHelper = new LinearWebhooks(source.secret); - // TODO: might want to do two passes, with and without timestamp (delay parsing) - if (!webhook.verify(Buffer.from(rawBody), signature, body[LINEAR_WEBHOOK_TS_FIELD])) { + if (!webhookHelper.verify(Buffer.from(rawBody), signature, body[LINEAR_WEBHOOK_TS_FIELD])) { logger.error("[@trigger.dev/linear] Error validating webhook signature, they don't match"); throw Error("[@trigger.dev/linear] Invalid signature"); } From ec6723daee37ae6604d1c3e8dd8914a53619ca53 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:30:49 +0000 Subject: [PATCH 21/55] Schema juggling --- integrations/linear/src/schemas.ts | 297 ++++++++++++++++------------- 1 file changed, 161 insertions(+), 136 deletions(-) diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts index 9223297d696..bb5970db293 100644 --- a/integrations/linear/src/schemas.ts +++ b/integrations/linear/src/schemas.ts @@ -19,209 +19,234 @@ export const WebhookActionTypeSchema = z.union([ ]); export type WebhookActionType = z.infer; -const ReactionShortSchema = z.object({ - emoji: z.string(), - reactions: z.array( - z.object({ - id: z.string(), - userId: z.string(), - reactedAt: z.string(), - }) - ), +const IssueLabelDataSchema = z.object({ + archivedAt: z.coerce.date().optional().nullable(), + color: z.string(), + createdAt: z.coerce.date(), + creatorId: z.string().optional().nullable(), + description: z.string().optional().nullable(), + id: z.string(), + // isGroup: z.boolean(), // missing + name: z.string(), + organizationId: z.string(), + parentId: z.string().optional().nullable(), + teamId: z.string().optional().nullable(), + updatedAt: z.coerce.date(), }); const IssueDataSchema = z.object({ - id: z.string(), + archivedAt: z.coerce.date().optional().nullable(), + assignee: z.object({ id: z.string(), name: z.string() }).optional().nullable(), + assigneeId: z.string().optional().nullable(), + autoArchivedAt: z.coerce.date().optional().nullable(), + autoClosedAt: z.coerce.date().optional().nullable(), + boardOrder: z.number(), + canceledAt: z.coerce.date().optional().nullable(), + completedAt: z.coerce.date().optional().nullable(), createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), + creatorId: z.string().optional().nullable(), + cycleId: z.string().optional().nullable(), + description: z.string().optional().nullable(), + dueDate: z.coerce.date().optional().nullable(), // timeless + estimate: z.number().optional().nullable(), + favoriteId: z.string().optional().nullable(), + id: z.string(), + labelIds: z.array(z.string()), + labels: z.array(IssueLabelDataSchema.pick({ id: true, color: true, name: true })), number: z.number(), - title: z.string(), + parentId: z.string().optional().nullable(), + previousIdentifiers: z.array(z.string()), priority: z.number(), - boardOrder: z.number(), + priorityLabel: z.string(), + projectId: z.string().optional().nullable(), sortOrder: z.number(), - teamId: z.string(), - previousIdentifiers: z.array(z.string()), - assigneeId: z.string().optional(), + state: z.object({ id: z.string(), color: z.string(), name: z.string(), type: z.string() }), + startedAt: z.coerce.date().optional().nullable(), stateId: z.string(), - priorityLabel: z.string(), + subIssueSortOrder: z.number().optional().nullable(), subscriberIds: z.array(z.string()), - labelIds: z.array(z.string()), - assignee: z - .object({ - id: z.string(), - name: z.string(), - }) - .optional(), - state: z.object({ - id: z.string(), - color: z.string(), - name: z.string(), - type: z.string(), - }), - team: z.object({ - id: z.string(), - key: z.string(), - name: z.string(), - }), - labels: z.array( - z.object({ - id: z.string(), - color: z.string(), - name: z.string(), - }) - ), - description: z.string(), + team: z.object({ id: z.string(), key: z.string(), name: z.string() }), + teamId: z.string(), + title: z.string(), + trashed: z.boolean().optional().nullable(), + triagedAt: z.coerce.date().optional().nullable(), + updatedAt: z.coerce.date(), }); /** **WARNING:** Still in alpha - use with caution! */ const AttachmentDataSchema = z.object({ - id: z.string(), + archivedAt: z.coerce.date().optional().nullable(), createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), - title: z.string(), - url: z.string().url(), - creatorId: z.string(), - metadata: z.object({}).passthrough(), - source: z.object({ - type: z.string(), - imageUrl: z.string().url(), - }), - sourceType: z.string(), + creatorId: z.string().optional().nullable(), groupBySource: z.boolean(), + id: z.string(), issueId: z.string(), + metadata: z.object({}).passthrough(), // JSONObject + source: z + .object({ + type: z.string().nullable(), + imageUrl: z.string().url().nullable(), + }) + .passthrough() + .partial() + .nullable(), // JSONObject + sourceType: z.string().optional().nullable(), + subtitle: z.string().optional().nullable(), + title: z.string(), + updatedAt: z.coerce.date(), + url: z.string().url(), }); const CommentDataSchema = z.object({ - id: z.string(), - createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), + archivedAt: z.coerce.date().optional().nullable(), body: z.string(), - issueId: z.string(), - userId: z.string(), - editedAt: z.string().optional(), - reactionData: z.array(ReactionShortSchema), - issue: z.object({ - id: z.string(), - title: z.string(), - }), -}); - -const IssueLabelDataSchema = z.object({ - id: z.string(), + botActorId: z.string().optional().nullable(), createdAt: z.coerce.date(), + editedAt: z.string().optional().nullable(), + id: z.string(), + issue: IssueDataSchema.pick({ id: true, title: true }), + issueId: z.string(), + parentId: z.string().optional().nullable(), + reactionData: z.array(z.object({}).passthrough()), // JSONObject updatedAt: z.coerce.date(), - name: z.string(), - color: z.string(), - organizationId: z.string(), - teamId: z.string(), - creatorId: z.string(), + userId: z.string().optional().nullable(), }); const ReactionDataSchema = z.object({ - id: z.string(), + archivedAt: z.coerce.date().optional().nullable(), + comment: CommentDataSchema.pick({ + id: true, + body: true, + userId: true, + }) + .optional() + .nullable(), // missing from official schema createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), emoji: z.string(), - userId: z.string(), - comment: z.object({ - id: z.string(), - body: z.string(), - userId: z.string(), - }), - user: z.object({ - id: z.string(), - name: z.string(), - }), + id: z.string(), + updatedAt: z.coerce.date(), + user: z.object({ id: z.string(), name: z.string() }).optional().nullable(), + userId: z.string().optional().nullable(), }); const MilestoneDataSchema = z.object({ - id: z.string(), + archivedAt: z.coerce.date().optional().nullable(), createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), + description: z.string().optional().nullable(), + id: z.string(), name: z.string(), - archivedAt: z.coerce.date().optional(), - description: z.string().optional(), - sortOrder: z.number().optional(), - targetDate: z.coerce.date().optional(), + projectId: z.string().optional().nullable(), + sortOrder: z.number(), + targetDate: z.coerce.date().optional().nullable(), // timeless + updatedAt: z.coerce.date(), }); const RoadmapDataSchema = z.object({ - id: z.string(), + archivedAt: z.coerce.date().optional().nullable(), + color: z.string().optional().nullable(), createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), + creatorId: z.string(), + description: z.string().optional().nullable(), + id: z.string(), name: z.string(), - color: z.string().optional(), - archivedAt: z.coerce.date().optional(), - description: z.string().optional(), - sortOrder: z.number().optional(), - targetDate: z.coerce.date().optional(), + organizationId: z.string(), + ownerId: z.string(), + slugId: z.string(), + sortOrder: z.number(), + updatedAt: z.coerce.date(), }); const ProjectDataSchema = z.object({ - id: z.string(), - createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), - name: z.string(), - description: z.string(), - slugId: z.string(), + archivedAt: z.coerce.date().optional().nullable(), + autoArchivedAt: z.coerce.date().optional().nullable(), + canceledAt: z.coerce.date().optional().nullable(), color: z.string(), - state: z.string(), - creatorId: z.string(), - leadId: z.string(), - sortOrder: z.string(), - issueCountHistory: z.array(z.number()), + completedAt: z.coerce.date().optional().nullable(), completedIssueCountHistory: z.array(z.number()), - scopeHistory: z.array(z.number()), completedScopeHistory: z.array(z.number()), + content: z.string().optional().nullable(), + convertedFromIssueId: z.string().optional().nullable(), + createdAt: z.coerce.date(), + creatorId: z.string(), + description: z.string(), + icon: z.string().optional().nullable(), + id: z.string(), inProgressScopeHistory: z.array(z.number()), - slackNewIssue: z.boolean(), + integrationsSettingsId: z.string().optional().nullable(), + issueCountHistory: z.array(z.number()), + leadId: z.string(), + memberIds: z.array(z.string()), + milestones: z.array(MilestoneDataSchema.pick({ id: true, name: true })), // at projectMilestones key in official schema + name: z.string(), + progress: z.number().optional().nullable(), // missing, should be NonNullable + projectUpdateRemindersPausedUntilAt: z.coerce.date().optional().nullable(), + roadmaps: z + .array(RoadmapDataSchema.pick({ id: true, name: true })) + .optional() + .nullable(), // missing from official schema + scope: z.number().optional().nullable(), // missing, should be NonNullable + scopeHistory: z.array(z.number()), slackIssueComments: z.boolean(), slackIssueStatuses: z.boolean(), + slackNewIssue: z.boolean(), + slugId: z.string(), + sortOrder: z.string(), + startDate: z.coerce.date().optional().nullable(), // timeless + startedAt: z.coerce.date().optional().nullable(), + state: z.string(), + targetDate: z.coerce.date().optional().nullable(), // timeless teamIds: z.array(z.string()), - memberIds: z.array(z.string()), - milestones: z.array(MilestoneDataSchema.partial()), - roadmaps: z.array(RoadmapDataSchema.partial()), + trashed: z.boolean().optional().nullable(), + updatedAt: z.coerce.date(), }); const ProjectUpdateDataSchema = z.object({ - id: z.string(), - createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), + archivedAt: z.coerce.date().optional().nullable(), body: z.string(), - projectId: z.string(), + createdAt: z.coerce.date(), + // diff: z.any().optional().nullable(), // missing, "stringified" JSON but typed as Record + editedAt: z.coerce.date().optional().nullable(), health: z.string(), - userId: z.string(), - infoSnapshot: z.object({}).passthrough().optional(), + id: z.string(), + infoSnapshot: z.object({}).passthrough().optional().nullable(), // JSONObject, marked as "internal" project: ProjectDataSchema.pick({ id: true, name: true }), - user: z.object({ - id: z.string(), - name: z.string(), - }), - roadmaps: z.array(RoadmapDataSchema.partial()), + projectId: z.string(), + roadmaps: z + .array(RoadmapDataSchema.pick({ id: true, name: true })) + .optional() + .nullable(), // missing from official schema + updatedAt: z.coerce.date(), + user: z.object({ id: z.string(), name: z.string() }), + userId: z.string(), }); const CycleDataSchema = z.object({ - id: z.string(), + archivedAt: z.coerce.date().optional().nullable(), + autoArchivedAt: z.coerce.date().optional().nullable(), + completedAt: z.coerce.date().optional().nullable(), + completedIssueCountHistory: z.array(z.number()), + completedScopeHistory: z.array(z.number()), createdAt: z.coerce.date(), - updatedAt: z.coerce.date(), - number: z.number(), - startsAt: z.coerce.date(), + description: z.string().optional().nullable(), endsAt: z.coerce.date(), + id: z.string(), + inProgressScopeHistory: z.array(z.number()), issueCountHistory: z.array(z.number()), - completedIssueCountHistory: z.array(z.number()), + name: z.string().optional().nullable(), + number: z.number(), + progress: z.number().optional().nullable(), // missing, should be NonNullable scopeHistory: z.array(z.number()), - completedScopeHistory: z.array(z.number()), - inProgressScopeHistory: z.array(z.number()), + startsAt: z.coerce.date(), teamId: z.string(), uncompletedIssuesUponCloseIds: z.array(z.string()), + updatedAt: z.coerce.date(), }); export const WebhookPayloadBaseSchema = z.object({ action: WebhookActionTypeSchema, createdAt: z.coerce.date(), - url: z.string().url().optional(), - // TODO: check if this is always present - organizationId: z.string().optional(), + url: z.string().url().optional().nullable(), + organizationId: z.string().optional().nullable(), // missing from official schema - workspace id? webhookTimestamp: z.coerce.date(), webhookId: z.string(), }); From 5cb272dc25c698bb62571f10bb97ade4a24b7ca5 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:30:57 +0000 Subject: [PATCH 22/55] More discrimination --- integrations/linear/src/events.ts | 97 ++++++++--------- integrations/linear/src/schemas.ts | 166 +++++++++++++++++++++++------ integrations/linear/src/types.ts | 5 + 3 files changed, 187 insertions(+), 81 deletions(-) create mode 100644 integrations/linear/src/types.ts diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts index bc4ea715887..a3c94dfbc34 100644 --- a/integrations/linear/src/events.ts +++ b/integrations/linear/src/events.ts @@ -9,6 +9,7 @@ import { ProjectUpdateEvent, ReactionEvent, } from "./schemas"; +import { ExtractCreate, ExtractRemove, ExtractUpdate } from "./types"; // TODO: payload examples // TODO: useful properties @@ -25,7 +26,7 @@ export const onAttachment: EventSpecification = { }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentCreated: EventSpecification = { +export const onAttachmentCreated: EventSpecification> = { name: "Attachment", title: "On Attachment Created", source: "linear.app", @@ -33,12 +34,12 @@ export const onAttachmentCreated: EventSpecification = { filter: { action: ["create"], }, - parsePayload: (payload) => payload as AttachmentEvent, + parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentRemoved: EventSpecification = { +export const onAttachmentRemoved: EventSpecification> = { name: "Attachment", title: "On Attachment Removed", source: "linear.app", @@ -46,12 +47,12 @@ export const onAttachmentRemoved: EventSpecification = { filter: { action: ["remove"], }, - parsePayload: (payload) => payload as AttachmentEvent, + parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentUpdated: EventSpecification = { +export const onAttachmentUpdated: EventSpecification> = { name: "Attachment", title: "On Attachment Updated", source: "linear.app", @@ -59,7 +60,7 @@ export const onAttachmentUpdated: EventSpecification = { filter: { action: ["update"], }, - parsePayload: (payload) => payload as AttachmentEvent, + parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -72,7 +73,7 @@ export const onComment: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCommentCreated: EventSpecification = { +export const onCommentCreated: EventSpecification> = { name: "Comment", title: "On Comment Created", source: "linear.app", @@ -80,11 +81,11 @@ export const onCommentCreated: EventSpecification = { filter: { action: ["create"], }, - parsePayload: (payload) => payload as CommentEvent, + parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCommentRemoved: EventSpecification = { +export const onCommentRemoved: EventSpecification> = { name: "Comment", title: "On Comment Removed", source: "linear.app", @@ -92,11 +93,11 @@ export const onCommentRemoved: EventSpecification = { filter: { action: ["remove"], }, - parsePayload: (payload) => payload as CommentEvent, + parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCommentUpdated: EventSpecification = { +export const onCommentUpdated: EventSpecification> = { name: "Comment", title: "On Comment Updated", source: "linear.app", @@ -104,7 +105,7 @@ export const onCommentUpdated: EventSpecification = { filter: { action: ["update"], }, - parsePayload: (payload) => payload as CommentEvent, + parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -117,7 +118,7 @@ export const onCycle: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCycleCreated: EventSpecification = { +export const onCycleCreated: EventSpecification> = { name: "Cycle", title: "On Cycle Created", source: "linear.app", @@ -125,11 +126,11 @@ export const onCycleCreated: EventSpecification = { filter: { action: ["create"], }, - parsePayload: (payload) => payload as CycleEvent, + parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCycleRemoved: EventSpecification = { +export const onCycleRemoved: EventSpecification> = { name: "Cycle", title: "On Cycle Removed", source: "linear.app", @@ -137,11 +138,11 @@ export const onCycleRemoved: EventSpecification = { filter: { action: ["remove"], }, - parsePayload: (payload) => payload as CycleEvent, + parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onCycleUpdated: EventSpecification = { +export const onCycleUpdated: EventSpecification> = { name: "Cycle", title: "On Cycle Updated", source: "linear.app", @@ -149,7 +150,7 @@ export const onCycleUpdated: EventSpecification = { filter: { action: ["update"], }, - parsePayload: (payload) => payload as CycleEvent, + parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -162,7 +163,7 @@ export const onIssue: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueCreated: EventSpecification = { +export const onIssueCreated: EventSpecification> = { name: "Issue", title: "On Issue Created", source: "linear.app", @@ -170,11 +171,11 @@ export const onIssueCreated: EventSpecification = { filter: { action: ["create"], }, - parsePayload: (payload) => payload as IssueEvent, + parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueRemoved: EventSpecification = { +export const onIssueRemoved: EventSpecification> = { name: "Issue", title: "On Issue Removed", source: "linear.app", @@ -182,11 +183,11 @@ export const onIssueRemoved: EventSpecification = { filter: { action: ["remove"], }, - parsePayload: (payload) => payload as IssueEvent, + parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueUpdated: EventSpecification = { +export const onIssueUpdated: EventSpecification> = { name: "Issue", title: "On Issue Updated", source: "linear.app", @@ -194,7 +195,7 @@ export const onIssueUpdated: EventSpecification = { filter: { action: ["update"], }, - parsePayload: (payload) => payload as IssueEvent, + parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -207,7 +208,7 @@ export const onIssueLabel: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueLabelCreated: EventSpecification = { +export const onIssueLabelCreated: EventSpecification> = { name: "IssueLabel", title: "On IssueLabel Created", source: "linear.app", @@ -215,11 +216,11 @@ export const onIssueLabelCreated: EventSpecification = { filter: { action: ["create"], }, - parsePayload: (payload) => payload as IssueLabelEvent, + parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueLabelRemoved: EventSpecification = { +export const onIssueLabelRemoved: EventSpecification> = { name: "IssueLabel", title: "On IssueLabel Removed", source: "linear.app", @@ -227,11 +228,11 @@ export const onIssueLabelRemoved: EventSpecification = { filter: { action: ["remove"], }, - parsePayload: (payload) => payload as IssueLabelEvent, + parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onIssueLabelUpdated: EventSpecification = { +export const onIssueLabelUpdated: EventSpecification> = { name: "IssueLabel", title: "On IssueLabel Updated", source: "linear.app", @@ -239,7 +240,7 @@ export const onIssueLabelUpdated: EventSpecification = { filter: { action: ["update"], }, - parsePayload: (payload) => payload as IssueLabelEvent, + parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -252,7 +253,7 @@ export const onProject: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProjectCreated: EventSpecification = { +export const onProjectCreated: EventSpecification> = { name: "Project", title: "On Project Created", source: "linear.app", @@ -260,11 +261,11 @@ export const onProjectCreated: EventSpecification = { filter: { action: ["create"], }, - parsePayload: (payload) => payload as ProjectEvent, + parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProjectRemoved: EventSpecification = { +export const onProjectRemoved: EventSpecification> = { name: "Project", title: "On Project Removed", source: "linear.app", @@ -272,12 +273,12 @@ export const onProjectRemoved: EventSpecification = { filter: { action: ["remove"], }, - parsePayload: (payload) => payload as ProjectEvent, + parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; // TODO: think of a better naming scheme (clashes with ProjectUpdated entity) -export const onProjectUpdated: EventSpecification = { +export const onProjectUpdated: EventSpecification> = { name: "Project", title: "On Project Updated", source: "linear.app", @@ -285,7 +286,7 @@ export const onProjectUpdated: EventSpecification = { filter: { action: ["update"], }, - parsePayload: (payload) => payload as ProjectEvent, + parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -298,7 +299,7 @@ export const onProjectUpdate: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProjectUpdateCreated: EventSpecification = { +export const onProjectUpdateCreated: EventSpecification> = { name: "ProjectUpdate", title: "On ProjectUpdate Created", source: "linear.app", @@ -306,11 +307,11 @@ export const onProjectUpdateCreated: EventSpecification = { filter: { action: ["create"], }, - parsePayload: (payload) => payload as ProjectUpdateEvent, + parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProjectUpdateRemoved: EventSpecification = { +export const onProjectUpdateRemoved: EventSpecification> = { name: "ProjectUpdate", title: "On ProjectUpdate Removed", source: "linear.app", @@ -318,11 +319,11 @@ export const onProjectUpdateRemoved: EventSpecification = { filter: { action: ["remove"], }, - parsePayload: (payload) => payload as ProjectUpdateEvent, + parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onProjectUpdateUpdated: EventSpecification = { +export const onProjectUpdateUpdated: EventSpecification> = { name: "ProjectUpdate", title: "On ProjectUpdate Updated", source: "linear.app", @@ -330,7 +331,7 @@ export const onProjectUpdateUpdated: EventSpecification = { filter: { action: ["update"], }, - parsePayload: (payload) => payload as ProjectUpdateEvent, + parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -343,7 +344,7 @@ export const onReaction: EventSpecification = { runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onReactionCreated: EventSpecification = { +export const onReactionCreated: EventSpecification> = { name: "Reaction", title: "On Reaction Created", source: "linear.app", @@ -351,11 +352,11 @@ export const onReactionCreated: EventSpecification = { filter: { action: ["create"], }, - parsePayload: (payload) => payload as ReactionEvent, + parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onReactionRemoved: EventSpecification = { +export const onReactionRemoved: EventSpecification> = { name: "Reaction", title: "On Reaction Removed", source: "linear.app", @@ -363,11 +364,11 @@ export const onReactionRemoved: EventSpecification = { filter: { action: ["remove"], }, - parsePayload: (payload) => payload as ReactionEvent, + parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; -export const onReactionUpdated: EventSpecification = { +export const onReactionUpdated: EventSpecification> = { name: "Reaction", title: "On Reaction Updated", source: "linear.app", @@ -375,6 +376,6 @@ export const onReactionUpdated: EventSpecification = { filter: { action: ["update"], }, - parsePayload: (payload) => payload as ReactionEvent, + parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts index bb5970db293..2c65c7d1935 100644 --- a/integrations/linear/src/schemas.ts +++ b/integrations/linear/src/schemas.ts @@ -243,80 +243,180 @@ const CycleDataSchema = z.object({ }); export const WebhookPayloadBaseSchema = z.object({ - action: WebhookActionTypeSchema, createdAt: z.coerce.date(), - url: z.string().url().optional().nullable(), organizationId: z.string().optional().nullable(), // missing from official schema - workspace id? - webhookTimestamp: z.coerce.date(), + url: z.string().url().optional().nullable(), webhookId: z.string(), + webhookTimestamp: z.coerce.date(), }); -export const IssueEventSchema = WebhookPayloadBaseSchema.extend({ - type: z.literal("Issue"), - data: IssueDataSchema, - updatedFrom: IssueDataSchema.partial().optional(), -}); -export type IssueEvent = z.infer; +const CREATE = z.literal("create"); +const REMOVE = z.literal("remove"); +const UPDATE = z.literal("update"); /** **WARNING:** Still in alpha - use with caution! */ -export const AttachmentEventSchema = WebhookPayloadBaseSchema.extend({ +export const AttachmentEventBaseSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Attachment"), data: AttachmentDataSchema, - updatedFrom: AttachmentDataSchema.partial().optional(), }); +export const AttachmentEventSchema = z.discriminatedUnion("action", [ + AttachmentEventBaseSchema.extend({ + action: CREATE, + }), + AttachmentEventBaseSchema.extend({ + action: REMOVE, + }), + AttachmentEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: AttachmentDataSchema.partial(), + }), +]); export type AttachmentEvent = z.infer; -export const CommentEventSchema = WebhookPayloadBaseSchema.extend({ +export const CommentEventBaseSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Comment"), data: CommentDataSchema, - updatedFrom: CommentDataSchema.partial().optional(), }); +export const CommentEventSchema = z.discriminatedUnion("action", [ + CommentEventBaseSchema.extend({ + action: CREATE, + }), + CommentEventBaseSchema.extend({ + action: REMOVE, + }), + CommentEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: CommentDataSchema.partial(), + }), +]); export type CommentEvent = z.infer; -export const IssueLabelEventSchema = WebhookPayloadBaseSchema.extend({ +export const CycleEventBaseSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Cycle"), + data: CycleDataSchema, +}); +export const CycleEventSchema = z.discriminatedUnion("action", [ + CycleEventBaseSchema.extend({ + action: CREATE, + }), + CycleEventBaseSchema.extend({ + action: REMOVE, + }), + CycleEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: CycleDataSchema.partial(), + }), +]); +export type CycleEvent = z.infer; + +export const IssueEventBaseSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Issue"), + data: IssueDataSchema, +}); +export const IssueEventSchema = z.discriminatedUnion("action", [ + IssueEventBaseSchema.extend({ + action: CREATE, + }), + IssueEventBaseSchema.extend({ + action: REMOVE, + }), + IssueEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: IssueDataSchema.partial(), + }), +]); +export type IssueEvent = z.infer; + +export const IssueLabelEventBaseSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("IssueLabel"), data: IssueLabelDataSchema, - updatedFrom: IssueLabelDataSchema.partial().optional(), }); +export const IssueLabelEventSchema = z.discriminatedUnion("action", [ + IssueLabelEventBaseSchema.extend({ + action: CREATE, + }), + IssueLabelEventBaseSchema.extend({ + action: REMOVE, + }), + IssueLabelEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: IssueLabelDataSchema.partial(), + }), +]); export type IssueLabelEvent = z.infer; -export const ReactionEventSchema = WebhookPayloadBaseSchema.extend({ - type: z.literal("Reaction"), - data: ReactionDataSchema, - updatedFrom: ReactionDataSchema.partial().optional(), +// TODO: confirm this with real-world payload(s) +export const IssueSlaEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("IssueSla"), + action: z.union([z.literal("set"), z.literal("highRisk"), z.literal("breached")]), + issueData: IssueDataSchema, }); -export type ReactionEvent = z.infer; +export type IssueSlaEvent = z.infer; -export const ProjectEventSchema = WebhookPayloadBaseSchema.extend({ +export const ProjectEventBaseSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Project"), data: ProjectDataSchema, - updatedFrom: ProjectDataSchema.partial().optional(), }); +export const ProjectEventSchema = z.discriminatedUnion("action", [ + ProjectEventBaseSchema.extend({ + action: CREATE, + }), + ProjectEventBaseSchema.extend({ + action: REMOVE, + }), + ProjectEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: ProjectDataSchema.partial(), + }), +]); export type ProjectEvent = z.infer; -export const ProjectUpdateEventSchema = WebhookPayloadBaseSchema.extend({ +export const ProjectUpdateEventBaseSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("ProjectUpdate"), data: ProjectUpdateDataSchema, - updatedFrom: ProjectUpdateDataSchema.partial().optional(), }); +export const ProjectUpdateEventSchema = z.discriminatedUnion("action", [ + ProjectUpdateEventBaseSchema.extend({ + action: CREATE, + }), + ProjectUpdateEventBaseSchema.extend({ + action: REMOVE, + }), + ProjectUpdateEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: ProjectUpdateDataSchema.partial(), + }), +]); export type ProjectUpdateEvent = z.infer; -export const CycleEventSchema = WebhookPayloadBaseSchema.extend({ - type: z.literal("Cycle"), - data: CycleDataSchema, - updatedFrom: CycleDataSchema.partial().optional(), +export const ReactionEventBaseSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("Reaction"), + data: ReactionDataSchema, }); -export type CycleEvent = z.infer; +export const ReactionEventSchema = z.discriminatedUnion("action", [ + ReactionEventBaseSchema.extend({ + action: CREATE, + }), + ReactionEventBaseSchema.extend({ + action: REMOVE, + }), + ReactionEventBaseSchema.extend({ + action: UPDATE, + updatedFrom: ReactionDataSchema.partial(), + }), +]); +export type ReactionEvent = z.infer; -export const WebhookPayloadSchema = z.discriminatedUnion("type", [ - IssueEventSchema, +export const WebhookPayloadSchema = z.union([ AttachmentEventSchema, CommentEventSchema, + CycleEventSchema, + IssueEventSchema, IssueLabelEventSchema, - ReactionEventSchema, ProjectEventSchema, ProjectUpdateEventSchema, - CycleEventSchema, + ReactionEventSchema, + IssueSlaEventSchema, ]); export type WebhookPayload = z.infer; diff --git a/integrations/linear/src/types.ts b/integrations/linear/src/types.ts new file mode 100644 index 00000000000..f2d2617d837 --- /dev/null +++ b/integrations/linear/src/types.ts @@ -0,0 +1,5 @@ +import { WebhookPayload } from "./schemas"; + +export type ExtractCreate = Extract; +export type ExtractRemove = Extract; +export type ExtractUpdate = Extract; From 5dc788f2c804e4e98310fd229f75ffcefb6e17b8 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:31:04 +0000 Subject: [PATCH 23/55] Add Issue SLA event --- integrations/linear/src/events.ts | 12 +++++++++++- integrations/linear/src/index.ts | 7 +++++++ integrations/linear/src/schemas.ts | 9 +++++---- 3 files changed, 23 insertions(+), 5 deletions(-) diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts index a3c94dfbc34..f4112a4ccb6 100644 --- a/integrations/linear/src/events.ts +++ b/integrations/linear/src/events.ts @@ -5,6 +5,7 @@ import { CycleEvent, IssueEvent, IssueLabelEvent, + IssueSLAEvent, ProjectEvent, ProjectUpdateEvent, ReactionEvent, @@ -13,7 +14,6 @@ import { ExtractCreate, ExtractRemove, ExtractUpdate } from "./types"; // TODO: payload examples // TODO: useful properties -// TODO: Issue SLA event /** **WARNING:** Still in alpha - use with caution! */ export const onAttachment: EventSpecification = { @@ -244,6 +244,16 @@ export const onIssueLabelUpdated: EventSpecification [{ label: "Change action", text: payload.action }], }; +// TODO: this needs to be tested +export const onIssueSLA: EventSpecification = { + name: "IssueSLA", + title: "On Issue SLA", + source: "linear.app", + icon: "linear", + parsePayload: (payload) => payload as IssueSLAEvent, + runProperties: (payload) => [{ label: "Change action", text: payload.action }], +}; + export const onProject: EventSpecification = { name: "Project", title: "On Project", diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index 11b6b1b4436..a2e02404901 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -237,6 +237,13 @@ export class Linear implements TriggerIntegration { }); } + onIssueSLA(params: OnChangeParams = {}) { + return createTrigger(this.source, events.onIssueSLA, { + ...params, + resourceTypes: ["IssueSla"], + }); + } + onProject(params: OnChangeParams = {}) { return createTrigger(this.source, events.onProject, { ...params, resourceTypes: ["Project"] }); } diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts index 2c65c7d1935..b26c383c990 100644 --- a/integrations/linear/src/schemas.ts +++ b/integrations/linear/src/schemas.ts @@ -6,6 +6,7 @@ export const WebhookResourceTypeSchema = z.union([ z.literal("Cycle"), z.literal("Issue"), z.literal("IssueLabel"), + z.literal("IssueSLA"), z.literal("Project"), z.literal("ProjectUpdate"), z.literal("Reaction"), @@ -346,12 +347,12 @@ export const IssueLabelEventSchema = z.discriminatedUnion("action", [ export type IssueLabelEvent = z.infer; // TODO: confirm this with real-world payload(s) -export const IssueSlaEventSchema = WebhookPayloadBaseSchema.extend({ - type: z.literal("IssueSla"), +export const IssueSLAEventSchema = WebhookPayloadBaseSchema.extend({ + type: z.literal("IssueSLA"), action: z.union([z.literal("set"), z.literal("highRisk"), z.literal("breached")]), issueData: IssueDataSchema, }); -export type IssueSlaEvent = z.infer; +export type IssueSLAEvent = z.infer; export const ProjectEventBaseSchema = WebhookPayloadBaseSchema.extend({ type: z.literal("Project"), @@ -413,10 +414,10 @@ export const WebhookPayloadSchema = z.union([ CycleEventSchema, IssueEventSchema, IssueLabelEventSchema, + IssueSLAEventSchema, ProjectEventSchema, ProjectUpdateEventSchema, ReactionEventSchema, - IssueSlaEventSchema, ]); export type WebhookPayload = z.infer; From 40cc3d9a4129f82756d3f7272e8faf9bfff7cbc0 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:31:13 +0000 Subject: [PATCH 24/55] Simplify triggers --- integrations/linear/src/index.ts | 230 ++++++++-------------------- integrations/linear/src/webhooks.ts | 24 +-- 2 files changed, 75 insertions(+), 179 deletions(-) diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index a2e02404901..2e8619f1893 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -11,8 +11,7 @@ import { } from "@trigger.dev/sdk"; import { LinearClient } from "@linear/sdk"; import * as events from "./events"; -import { Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; -import { WebhookActionType } from "./schemas"; +import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; export type LinearIntegrationOptions = { id: string; @@ -100,229 +99,140 @@ export class Linear implements TriggerIntegration { ); } - // TODO: create separate sources to remove resourceTypes - /** **WARNING:** Still in alpha - use with caution! */ - onAttachment(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onAttachment, { - ...params, - resourceTypes: ["Attachment"], - }); + onAttachment(params: TriggerParams = {}) { + return createTrigger(this.source, events.onAttachment, params); } /** **WARNING:** Still in alpha - use with caution! */ - onAttachmentCreated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onAttachmentCreated, { - ...params, - resourceTypes: ["Attachment"], - }); + onAttachmentCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onAttachmentCreated, params); } /** **WARNING:** Still in alpha - use with caution! */ - onAttachmentRemoved(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onAttachmentRemoved, { - ...params, - resourceTypes: ["Attachment"], - }); + onAttachmentRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onAttachmentRemoved, params); } /** **WARNING:** Still in alpha - use with caution! */ - onAttachmentUpdated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onAttachmentUpdated, { - ...params, - resourceTypes: ["Attachment"], - }); + onAttachmentUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onAttachmentUpdated, params); } - onComment(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onComment, { ...params, resourceTypes: ["Comment"] }); + onComment(params: TriggerParams = {}) { + return createTrigger(this.source, events.onComment, params); } - onCommentCreated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCommentCreated, { - ...params, - resourceTypes: ["Comment"], - }); + onCommentCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCommentCreated, params); } - onCommentRemoved(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCommentRemoved, { - ...params, - resourceTypes: ["Comment"], - }); + onCommentRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCommentRemoved, params); } - onCommentUpdated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCommentUpdated, { - ...params, - resourceTypes: ["Comment"], - }); + onCommentUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCommentUpdated, params); } - onCycle(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCycle, { ...params, resourceTypes: ["Cycle"] }); + onCycle(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCycle, params); } - onCycleCreated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCycleCreated, { - ...params, - resourceTypes: ["Cycle"], - }); + onCycleCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCycleCreated, params); } - onCycleRemoved(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCycleRemoved, { - ...params, - resourceTypes: ["Cycle"], - }); + onCycleRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCycleRemoved, params); } - onCycleUpdated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onCycleUpdated, { - ...params, - resourceTypes: ["Cycle"], - }); + onCycleUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onCycleUpdated, params); } - onIssue(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssue, { ...params, resourceTypes: ["Issue"] }); + onIssue(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssue, params); } - onIssueCreated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueCreated, { - ...params, - resourceTypes: ["Issue"], - }); + onIssueCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssueCreated, params); } - onIssueRemoved(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueRemoved, { - ...params, - resourceTypes: ["Issue"], - }); + onIssueRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssueRemoved, params); } - onIssueUpdated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueUpdated, { - ...params, - resourceTypes: ["Issue"], - }); + onIssueUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssueUpdated, params); } - onIssueLabel(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueLabel, { - ...params, - resourceTypes: ["IssueLabel"], - }); + onIssueLabel(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssueLabel, params); } - onIssueLabelCreated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueLabelCreated, { - ...params, - resourceTypes: ["IssueLabel"], - }); + onIssueLabelCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssueLabelCreated, params); } - onIssueLabelRemoved(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueLabelRemoved, { - ...params, - resourceTypes: ["IssueLabel"], - }); + onIssueLabelRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssueLabelRemoved, params); } - onIssueLabelUpdated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueLabelUpdated, { - ...params, - resourceTypes: ["IssueLabel"], - }); + onIssueLabelUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssueLabelUpdated, params); } - onIssueSLA(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onIssueSLA, { - ...params, - resourceTypes: ["IssueSla"], - }); + onIssueSLA(params: TriggerParams = {}) { + return createTrigger(this.source, events.onIssueSLA, params); } - onProject(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProject, { ...params, resourceTypes: ["Project"] }); + onProject(params: TriggerParams = {}) { + return createTrigger(this.source, events.onProject, params); } - onProjectCreated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectCreated, { - ...params, - resourceTypes: ["Project"], - }); + onProjectCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onProjectCreated, params); } - onProjectRemoved(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectRemoved, { - ...params, - resourceTypes: ["Project"], - }); + onProjectRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onProjectRemoved, params); } - onProjectUpdated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectUpdated, { - ...params, - resourceTypes: ["Project"], - }); + onProjectUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onProjectUpdated, params); } - onProjectUpdate(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectUpdate, { - ...params, - resourceTypes: ["ProjectUpdate"], - }); + onProjectUpdate(params: TriggerParams = {}) { + return createTrigger(this.source, events.onProjectUpdate, params); } - onProjectUpdateCreated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectUpdateCreated, { - ...params, - resourceTypes: ["ProjectUpdate"], - }); + onProjectUpdateCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onProjectUpdateCreated, params); } - onProjectUpdateRemoved(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectUpdateRemoved, { - ...params, - resourceTypes: ["ProjectUpdate"], - }); + onProjectUpdateRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onProjectUpdateRemoved, params); } - onProjectUpdateUpdated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onProjectUpdateUpdated, { - ...params, - resourceTypes: ["ProjectUpdate"], - }); + onProjectUpdateUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onProjectUpdateUpdated, params); } - onReaction(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onReaction, { - ...params, - resourceTypes: ["Reaction"], - }); + onReaction(params: TriggerParams = {}) { + return createTrigger(this.source, events.onReaction, params); } - onReactionCreated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onReactionCreated, { - ...params, - resourceTypes: ["Reaction"], - }); + onReactionCreated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onReactionCreated, params); } - onReactionRemoved(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onReactionRemoved, { - ...params, - resourceTypes: ["Reaction"], - }); + onReactionRemoved(params: TriggerParams = {}) { + return createTrigger(this.source, events.onReactionRemoved, params); } - onReactionUpdated(params: OnChangeParams = {}) { - return createTrigger(this.source, events.onReactionUpdated, { - ...params, - resourceTypes: ["Reaction"], - }); + onReactionUpdated(params: TriggerParams = {}) { + return createTrigger(this.source, events.onReactionUpdated, params); } webhooks() { @@ -330,12 +240,6 @@ export class Linear implements TriggerIntegration { } } -type OnChangeParams = { - teamId?: string; - allPublicTeams?: boolean; - actionTypes?: WebhookActionType[]; -}; - // TODO export function onError(error: unknown) {} diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index eb2836a4da4..0dc6e0c6464 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -1,4 +1,5 @@ import { + EventFilter, ExternalSource, ExternalSourceTrigger, HandlerEvent, @@ -129,14 +130,10 @@ export class Webhooks { type LinearEvents = (typeof events)[keyof typeof events]; -const TriggerParamsSchema = z.object({ - resourceTypes: z.array(WebhookResourceTypeSchema), - teamId: z.string().optional(), - allPublicTeams: z.boolean().optional(), - actionTypes: z.array(WebhookActionTypeSchema).optional(), -}); - -export type TriggerParams = z.infer; +export type TriggerParams = { + teamId?: string; + filter?: EventFilter; +}; type CreateTriggersResult = ExternalSourceTrigger< TEventSpecification, @@ -147,9 +144,6 @@ export function createTrigger( source: ReturnType, event: TEventSpecification, params: TriggerParams - // options: { - // actionTypes?: WebhookActionType[]; // via params - // } ): CreateTriggersResult { return new ExternalSourceTrigger({ event, @@ -166,19 +160,17 @@ const WebhookRegistrationDataSchema = z.object({ enabled: z.boolean(), }), }); -type WebhookRegistrationData = z.infer; export function createWebhookEventSource( integration: Linear ): ExternalSource { return new ExternalSource("HTTP", { id: "linear.webhook", - schema: TriggerParamsSchema, - // optionSchema: z.object({ }), + schema: z.object({ + teamId: z.string().optional(), + }), version: "0.1.0", integration, - // TODO: filter by actionTypes - // filter: (params, options) => ({ }), key: (params) => `${params.teamId ? params.teamId : "all"}`, handler: webhookHandler, register: async (event, io, ctx) => { From 272598a67e7b20c436daa94b000859bf659d7a7d Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:31:19 +0000 Subject: [PATCH 25/55] Handle rate limits --- integrations/linear/src/index.ts | 39 ++++++++++++++++++++++++++++---- 1 file changed, 35 insertions(+), 4 deletions(-) diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index 2e8619f1893..d01c4e49813 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -9,7 +9,7 @@ import { TriggerIntegration, retry, } from "@trigger.dev/sdk"; -import { LinearClient } from "@linear/sdk"; +import { LinearClient, RatelimitedLinearError } from "@linear/sdk"; import * as events from "./events"; import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; @@ -95,7 +95,7 @@ export class Linear implements TriggerIntegration { ...(options ?? {}), connectionKey: this._connectionKey, }, - errorCallback + errorCallback ?? onError ); } @@ -240,7 +240,38 @@ export class Linear implements TriggerIntegration { } } -// TODO -export function onError(error: unknown) {} +export function onError(error: unknown) { + if (!(error instanceof RatelimitedLinearError)) { + return; + } + + const rateLimitRemaining = error.raw?.response?.headers?.get("X-RateLimit-Requests-Remaining"); + const rateLimitReset = error.raw?.response?.headers?.get("X-RateLimit-Requests-Reset"); + + if (rateLimitRemaining === "0" && rateLimitReset) { + const resetDate = new Date(Number(rateLimitReset) * 1000); + + return { + retryAt: resetDate, + error, + }; + } + + const queryComplexity = error.raw?.response?.headers?.get("X-Complexity"); + const complexityRemaining = error.raw?.response?.headers?.get("X-RateLimit-Complexity-Remaining"); + const complexityReset = error.raw?.response?.headers?.get("X-RateLimit-Complexity-Reset"); + + if ( + (complexityRemaining === "0" || Number(complexityRemaining) < Number(queryComplexity)) && + complexityReset + ) { + const resetDate = new Date(Number(complexityReset) * 1000); + + return { + retryAt: resetDate, + error, + }; + } +} export { events }; From 01ba3111c8b848e95014bfef1771ef68d43f9dd1 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:31:27 +0000 Subject: [PATCH 26/55] Fix Project schema --- integrations/linear/src/schemas.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/linear/src/schemas.ts b/integrations/linear/src/schemas.ts index b26c383c990..e8da8b37b15 100644 --- a/integrations/linear/src/schemas.ts +++ b/integrations/linear/src/schemas.ts @@ -191,7 +191,7 @@ const ProjectDataSchema = z.object({ slackIssueStatuses: z.boolean(), slackNewIssue: z.boolean(), slugId: z.string(), - sortOrder: z.string(), + sortOrder: z.number(), startDate: z.coerce.date().optional().nullable(), // timeless startedAt: z.coerce.date().optional().nullable(), state: z.string(), From 08e017658db4a356c0ab45e661855f95f6395a47 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:31:36 +0000 Subject: [PATCH 27/55] Payload examples --- integrations/linear/src/events.ts | 59 ++++++- .../src/payload-examples/Attachment.json | 25 +++ .../payload-examples/AttachmentCreated.json | 25 +++ .../payload-examples/AttachmentRemoved.json | 25 +++ .../payload-examples/AttachmentUpdated.json | 29 ++++ .../linear/src/payload-examples/Comment.json | 22 +++ .../src/payload-examples/CommentCreated.json | 22 +++ .../src/payload-examples/CommentRemoved.json | 22 +++ .../src/payload-examples/CommentUpdated.json | 28 ++++ .../linear/src/payload-examples/Cycle.json | 23 +++ .../src/payload-examples/CycleCreated.json | 23 +++ .../src/payload-examples/CycleRemoved.json | 23 +++ .../src/payload-examples/CycleUpdated.json | 27 +++ .../linear/src/payload-examples/Issue.json | 42 +++++ .../src/payload-examples/IssueCreated.json | 41 +++++ .../src/payload-examples/IssueLabel.json | 17 ++ .../payload-examples/IssueLabelCreated.json | 17 ++ .../payload-examples/IssueLabelRemoved.json | 17 ++ .../payload-examples/IssueLabelUpdated.json | 22 +++ .../src/payload-examples/IssueRemoved.json | 42 +++++ .../src/payload-examples/IssueUpdated.json | 42 +++++ .../linear/src/payload-examples/Project.json | 38 +++++ .../src/payload-examples/ProjectCreated.json | 38 +++++ .../src/payload-examples/ProjectRemoved.json | 40 +++++ .../src/payload-examples/ProjectUpdate.json | 47 ++++++ .../ProjectUpdateCreated.json | 47 ++++++ .../ProjectUpdateRemoved.json | 48 ++++++ .../ProjectUpdateUpdated.json | 53 ++++++ .../src/payload-examples/ProjectUpdated.json | 43 +++++ .../linear/src/payload-examples/Reaction.json | 24 +++ .../src/payload-examples/ReactionCreated.json | 24 +++ .../src/payload-examples/ReactionRemoved.json | 24 +++ .../src/payload-examples/ReactionUpdated.json | 28 ++++ .../linear/src/payload-examples/index.ts | 154 ++++++++++++++++++ 34 files changed, 1200 insertions(+), 1 deletion(-) create mode 100644 integrations/linear/src/payload-examples/Attachment.json create mode 100644 integrations/linear/src/payload-examples/AttachmentCreated.json create mode 100644 integrations/linear/src/payload-examples/AttachmentRemoved.json create mode 100644 integrations/linear/src/payload-examples/AttachmentUpdated.json create mode 100644 integrations/linear/src/payload-examples/Comment.json create mode 100644 integrations/linear/src/payload-examples/CommentCreated.json create mode 100644 integrations/linear/src/payload-examples/CommentRemoved.json create mode 100644 integrations/linear/src/payload-examples/CommentUpdated.json create mode 100644 integrations/linear/src/payload-examples/Cycle.json create mode 100644 integrations/linear/src/payload-examples/CycleCreated.json create mode 100644 integrations/linear/src/payload-examples/CycleRemoved.json create mode 100644 integrations/linear/src/payload-examples/CycleUpdated.json create mode 100644 integrations/linear/src/payload-examples/Issue.json create mode 100644 integrations/linear/src/payload-examples/IssueCreated.json create mode 100644 integrations/linear/src/payload-examples/IssueLabel.json create mode 100644 integrations/linear/src/payload-examples/IssueLabelCreated.json create mode 100644 integrations/linear/src/payload-examples/IssueLabelRemoved.json create mode 100644 integrations/linear/src/payload-examples/IssueLabelUpdated.json create mode 100644 integrations/linear/src/payload-examples/IssueRemoved.json create mode 100644 integrations/linear/src/payload-examples/IssueUpdated.json create mode 100644 integrations/linear/src/payload-examples/Project.json create mode 100644 integrations/linear/src/payload-examples/ProjectCreated.json create mode 100644 integrations/linear/src/payload-examples/ProjectRemoved.json create mode 100644 integrations/linear/src/payload-examples/ProjectUpdate.json create mode 100644 integrations/linear/src/payload-examples/ProjectUpdateCreated.json create mode 100644 integrations/linear/src/payload-examples/ProjectUpdateRemoved.json create mode 100644 integrations/linear/src/payload-examples/ProjectUpdateUpdated.json create mode 100644 integrations/linear/src/payload-examples/ProjectUpdated.json create mode 100644 integrations/linear/src/payload-examples/Reaction.json create mode 100644 integrations/linear/src/payload-examples/ReactionCreated.json create mode 100644 integrations/linear/src/payload-examples/ReactionRemoved.json create mode 100644 integrations/linear/src/payload-examples/ReactionUpdated.json create mode 100644 integrations/linear/src/payload-examples/index.ts diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts index f4112a4ccb6..611eb00d962 100644 --- a/integrations/linear/src/events.ts +++ b/integrations/linear/src/events.ts @@ -11,8 +11,33 @@ import { ReactionEvent, } from "./schemas"; import { ExtractCreate, ExtractRemove, ExtractUpdate } from "./types"; +import { + attachmentCreated, + attachmentRemoved, + attachmentUpdated, + commentCreated, + commentRemoved, + commentUpdated, + cycleCreated, + cycleRemoved, + cycleUpdated, + issueCreated, + issueRemoved, + issueUpdated, + issueLabelCreated, + issueLabelRemoved, + issueLabelUpdated, + projectCreated, + projectRemoved, + projectUpdated, + projectUpdateCreated, + projectUpdateRemoved, + projectUpdateUpdated, + reactionCreated, + reactionRemoved, + reactionUpdated, +} from "./payload-examples"; -// TODO: payload examples // TODO: useful properties /** **WARNING:** Still in alpha - use with caution! */ @@ -21,6 +46,7 @@ export const onAttachment: EventSpecification = { title: "On Attachment", source: "linear.app", icon: "linear", + examples: [attachmentCreated, attachmentRemoved, attachmentUpdated], parsePayload: (payload) => payload as AttachmentEvent, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -34,6 +60,7 @@ export const onAttachmentCreated: EventSpecification payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -47,6 +74,7 @@ export const onAttachmentRemoved: EventSpecification payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -60,6 +88,7 @@ export const onAttachmentUpdated: EventSpecification payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -69,6 +98,7 @@ export const onComment: EventSpecification = { title: "On Comment", source: "linear.app", icon: "linear", + examples: [commentCreated, commentRemoved, commentUpdated], parsePayload: (payload) => payload as CommentEvent, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -81,6 +111,7 @@ export const onCommentCreated: EventSpecification> = filter: { action: ["create"], }, + examples: [commentCreated], parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -93,6 +124,7 @@ export const onCommentRemoved: EventSpecification> = filter: { action: ["remove"], }, + examples: [commentRemoved], parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -105,6 +137,7 @@ export const onCommentUpdated: EventSpecification> = filter: { action: ["update"], }, + examples: [commentUpdated], parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -114,6 +147,7 @@ export const onCycle: EventSpecification = { title: "On Cycle", source: "linear.app", icon: "linear", + examples: [cycleCreated, cycleRemoved, cycleUpdated], parsePayload: (payload) => payload as CycleEvent, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -126,6 +160,7 @@ export const onCycleCreated: EventSpecification> = { filter: { action: ["create"], }, + examples: [cycleCreated], parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -138,6 +173,7 @@ export const onCycleRemoved: EventSpecification> = { filter: { action: ["remove"], }, + examples: [cycleRemoved], parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -150,6 +186,7 @@ export const onCycleUpdated: EventSpecification> = { filter: { action: ["update"], }, + examples: [cycleUpdated], parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -159,6 +196,7 @@ export const onIssue: EventSpecification = { title: "On Issue", source: "linear.app", icon: "linear", + examples: [issueCreated, issueRemoved, issueUpdated], parsePayload: (payload) => payload as IssueEvent, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -171,6 +209,7 @@ export const onIssueCreated: EventSpecification> = { filter: { action: ["create"], }, + examples: [issueCreated], parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -183,6 +222,7 @@ export const onIssueRemoved: EventSpecification> = { filter: { action: ["remove"], }, + examples: [issueRemoved], parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -195,6 +235,7 @@ export const onIssueUpdated: EventSpecification> = { filter: { action: ["update"], }, + examples: [issueUpdated], parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -204,6 +245,7 @@ export const onIssueLabel: EventSpecification = { title: "On IssueLabel", source: "linear.app", icon: "linear", + examples: [issueLabelCreated, issueLabelRemoved, issueLabelUpdated], parsePayload: (payload) => payload as IssueLabelEvent, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -216,6 +258,7 @@ export const onIssueLabelCreated: EventSpecification payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -228,6 +271,7 @@ export const onIssueLabelRemoved: EventSpecification payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -240,6 +284,7 @@ export const onIssueLabelUpdated: EventSpecification payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -259,6 +304,7 @@ export const onProject: EventSpecification = { title: "On Project", source: "linear.app", icon: "linear", + examples: [projectCreated, projectRemoved, projectUpdated], parsePayload: (payload) => payload as ProjectEvent, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -271,6 +317,7 @@ export const onProjectCreated: EventSpecification> = filter: { action: ["create"], }, + examples: [projectCreated], parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -283,6 +330,7 @@ export const onProjectRemoved: EventSpecification> = filter: { action: ["remove"], }, + examples: [projectRemoved], parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -296,6 +344,7 @@ export const onProjectUpdated: EventSpecification> = filter: { action: ["update"], }, + examples: [projectUpdated], parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -305,6 +354,7 @@ export const onProjectUpdate: EventSpecification = { title: "On ProjectUpdate", source: "linear.app", icon: "linear", + examples: [projectUpdateCreated, projectUpdateRemoved, projectUpdateUpdated], parsePayload: (payload) => payload as ProjectUpdateEvent, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -317,6 +367,7 @@ export const onProjectUpdateCreated: EventSpecification payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -329,6 +380,7 @@ export const onProjectUpdateRemoved: EventSpecification payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -341,6 +393,7 @@ export const onProjectUpdateUpdated: EventSpecification payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -350,6 +403,7 @@ export const onReaction: EventSpecification = { title: "On Reaction", source: "linear.app", icon: "linear", + examples: [reactionCreated, reactionRemoved, reactionUpdated], parsePayload: (payload) => payload as ReactionEvent, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -362,6 +416,7 @@ export const onReactionCreated: EventSpecification> filter: { action: ["create"], }, + examples: [reactionCreated], parsePayload: (payload) => payload as ExtractCreate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -374,6 +429,7 @@ export const onReactionRemoved: EventSpecification> filter: { action: ["remove"], }, + examples: [reactionRemoved], parsePayload: (payload) => payload as ExtractRemove, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; @@ -386,6 +442,7 @@ export const onReactionUpdated: EventSpecification> filter: { action: ["update"], }, + examples: [reactionUpdated], parsePayload: (payload) => payload as ExtractUpdate, runProperties: (payload) => [{ label: "Change action", text: payload.action }], }; diff --git a/integrations/linear/src/payload-examples/Attachment.json b/integrations/linear/src/payload-examples/Attachment.json new file mode 100644 index 00000000000..b685ac7ecbc --- /dev/null +++ b/integrations/linear/src/payload-examples/Attachment.json @@ -0,0 +1,25 @@ +{ + "data": { + "id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad", + "url": "https://www.rickastley.co.uk/", + "title": "My Personal Website", + "source": { + "type": "api", + "imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c" + }, + "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "metadata": {}, + "subtitle": "Rick Astley Official Site", + "createdAt": "2023-09-17T10:51:34.932Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "updatedAt": "2023-09-17T10:51:34.932Z", + "sourceType": "api", + "groupBySource": false + }, + "type": "Attachment", + "action": "create", + "createdAt": "2023-09-17T10:51:34.932Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:51:37.795Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/AttachmentCreated.json b/integrations/linear/src/payload-examples/AttachmentCreated.json new file mode 100644 index 00000000000..b685ac7ecbc --- /dev/null +++ b/integrations/linear/src/payload-examples/AttachmentCreated.json @@ -0,0 +1,25 @@ +{ + "data": { + "id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad", + "url": "https://www.rickastley.co.uk/", + "title": "My Personal Website", + "source": { + "type": "api", + "imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c" + }, + "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "metadata": {}, + "subtitle": "Rick Astley Official Site", + "createdAt": "2023-09-17T10:51:34.932Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "updatedAt": "2023-09-17T10:51:34.932Z", + "sourceType": "api", + "groupBySource": false + }, + "type": "Attachment", + "action": "create", + "createdAt": "2023-09-17T10:51:34.932Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:51:37.795Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/AttachmentRemoved.json b/integrations/linear/src/payload-examples/AttachmentRemoved.json new file mode 100644 index 00000000000..d7d0f713b00 --- /dev/null +++ b/integrations/linear/src/payload-examples/AttachmentRemoved.json @@ -0,0 +1,25 @@ +{ + "data": { + "id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad", + "url": "https://www.rickastley.co.uk/", + "title": "Will Never Give You Up", + "source": { + "type": "api", + "imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c" + }, + "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "metadata": {}, + "subtitle": "Rick Astley Official Site", + "createdAt": "2023-09-17T10:51:34.932Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "updatedAt": "2023-09-17T10:51:55.751Z", + "sourceType": "api", + "groupBySource": false + }, + "type": "Attachment", + "action": "remove", + "createdAt": "2023-09-17T10:52:15.457Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:52:15.938Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/AttachmentUpdated.json b/integrations/linear/src/payload-examples/AttachmentUpdated.json new file mode 100644 index 00000000000..95d247b48b6 --- /dev/null +++ b/integrations/linear/src/payload-examples/AttachmentUpdated.json @@ -0,0 +1,29 @@ +{ + "data": { + "id": "8a4c109b-3288-4bef-8a80-8c1618e0f4ad", + "url": "https://www.rickastley.co.uk/", + "title": "Will Never Give You Up", + "source": { + "type": "api", + "imageUrl": "https://uploads.linear.app/attachment-icons/6fca65e5bca9d4ac8eb9d9442c5e3170081f0aa3eb820eebd58766b53fe7236c" + }, + "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "metadata": {}, + "subtitle": "Rick Astley Official Site", + "createdAt": "2023-09-17T10:51:34.932Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "updatedAt": "2023-09-17T10:51:55.751Z", + "sourceType": "api", + "groupBySource": false + }, + "type": "Attachment", + "action": "update", + "createdAt": "2023-09-17T10:51:55.751Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "title": "My Personal Website", + "updatedAt": "2023-09-17T10:51:34.932Z" + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:51:55.809Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/Comment.json b/integrations/linear/src/payload-examples/Comment.json new file mode 100644 index 00000000000..a8f0f128d70 --- /dev/null +++ b/integrations/linear/src/payload-examples/Comment.json @@ -0,0 +1,22 @@ +{ + "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd", + "data": { + "id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95", + "body": "I just wanna tell you how I'm feeling", + "issue": { + "id": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "title": "Welcome to Rick's World ❤️‍🔥" + }, + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "createdAt": "2023-09-17T10:57:11.751Z", + "updatedAt": "2023-09-17T10:57:11.751Z", + "reactionData": [] + }, + "type": "Comment", + "action": "create", + "createdAt": "2023-09-17T10:57:11.751Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:57:11.842Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/CommentCreated.json b/integrations/linear/src/payload-examples/CommentCreated.json new file mode 100644 index 00000000000..a8f0f128d70 --- /dev/null +++ b/integrations/linear/src/payload-examples/CommentCreated.json @@ -0,0 +1,22 @@ +{ + "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd", + "data": { + "id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95", + "body": "I just wanna tell you how I'm feeling", + "issue": { + "id": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "title": "Welcome to Rick's World ❤️‍🔥" + }, + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "createdAt": "2023-09-17T10:57:11.751Z", + "updatedAt": "2023-09-17T10:57:11.751Z", + "reactionData": [] + }, + "type": "Comment", + "action": "create", + "createdAt": "2023-09-17T10:57:11.751Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:57:11.842Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/CommentRemoved.json b/integrations/linear/src/payload-examples/CommentRemoved.json new file mode 100644 index 00000000000..eadef86b5dc --- /dev/null +++ b/integrations/linear/src/payload-examples/CommentRemoved.json @@ -0,0 +1,22 @@ +{ + "data": { + "id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95", + "body": "Gotta make you understand", + "issue": { + "id": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "title": "Welcome to Rick's World ❤️‍🔥" + }, + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "editedAt": "2023-09-17T10:57:24.386Z", + "createdAt": "2023-09-17T10:57:11.751Z", + "updatedAt": "2023-09-17T10:57:24.387Z", + "reactionData": [] + }, + "type": "Comment", + "action": "remove", + "createdAt": "2023-09-17T10:57:37.324Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:57:37.376Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/CommentUpdated.json b/integrations/linear/src/payload-examples/CommentUpdated.json new file mode 100644 index 00000000000..6b930298188 --- /dev/null +++ b/integrations/linear/src/payload-examples/CommentUpdated.json @@ -0,0 +1,28 @@ +{ + "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-linear-👋#comment-a4083fcd", + "data": { + "id": "a4083fcd-2513-4a9b-9a73-bab21cd56a95", + "body": "Gotta make you understand", + "issue": { + "id": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "title": "Welcome to Rick's World ❤️‍🔥" + }, + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "issueId": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "editedAt": "2023-09-17T10:57:24.386Z", + "createdAt": "2023-09-17T10:57:11.751Z", + "updatedAt": "2023-09-17T10:57:24.387Z", + "reactionData": [] + }, + "type": "Comment", + "action": "update", + "createdAt": "2023-09-17T10:57:24.387Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "body": "I just wanna tell you how I'm feeling", + "editedAt": null, + "updatedAt": "2023-09-17T10:57:11.751Z" + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:57:24.446Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/Cycle.json b/integrations/linear/src/payload-examples/Cycle.json new file mode 100644 index 00000000000..800ddd612f7 --- /dev/null +++ b/integrations/linear/src/payload-examples/Cycle.json @@ -0,0 +1,23 @@ +{ + "data": { + "id": "cf1f0ba0-a769-402c-a5a4-308bca3ff53c", + "endsAt": "2023-10-08T23:00:00.000Z", + "number": 1, + "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "startsAt": "2023-09-24T23:00:00.000Z", + "createdAt": "2023-09-17T11:00:19.616Z", + "updatedAt": "2023-09-17T11:00:19.616Z", + "scopeHistory": [], + "issueCountHistory": [], + "completedScopeHistory": [], + "inProgressScopeHistory": [], + "completedIssueCountHistory": [], + "uncompletedIssuesUponCloseIds": [] + }, + "type": "Cycle", + "action": "create", + "createdAt": "2023-09-17T11:00:19.616Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:00:19.696Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/CycleCreated.json b/integrations/linear/src/payload-examples/CycleCreated.json new file mode 100644 index 00000000000..800ddd612f7 --- /dev/null +++ b/integrations/linear/src/payload-examples/CycleCreated.json @@ -0,0 +1,23 @@ +{ + "data": { + "id": "cf1f0ba0-a769-402c-a5a4-308bca3ff53c", + "endsAt": "2023-10-08T23:00:00.000Z", + "number": 1, + "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "startsAt": "2023-09-24T23:00:00.000Z", + "createdAt": "2023-09-17T11:00:19.616Z", + "updatedAt": "2023-09-17T11:00:19.616Z", + "scopeHistory": [], + "issueCountHistory": [], + "completedScopeHistory": [], + "inProgressScopeHistory": [], + "completedIssueCountHistory": [], + "uncompletedIssuesUponCloseIds": [] + }, + "type": "Cycle", + "action": "create", + "createdAt": "2023-09-17T11:00:19.616Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:00:19.696Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/CycleRemoved.json b/integrations/linear/src/payload-examples/CycleRemoved.json new file mode 100644 index 00000000000..c918ca70497 --- /dev/null +++ b/integrations/linear/src/payload-examples/CycleRemoved.json @@ -0,0 +1,23 @@ +{ + "data": { + "id": "fe7092dd-8a68-4a07-9477-b7c0a6f2136d", + "endsAt": "2023-11-13T00:00:00.000Z", + "number": 2, + "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "startsAt": "2023-10-15T23:00:00.000Z", + "createdAt": "2023-09-17T11:00:19.616Z", + "updatedAt": "2023-09-17T11:00:47.427Z", + "scopeHistory": [], + "issueCountHistory": [], + "completedScopeHistory": [], + "inProgressScopeHistory": [], + "completedIssueCountHistory": [], + "uncompletedIssuesUponCloseIds": [] + }, + "type": "Cycle", + "action": "remove", + "createdAt": "2023-09-17T11:01:08.772Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:01:08.807Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/CycleUpdated.json b/integrations/linear/src/payload-examples/CycleUpdated.json new file mode 100644 index 00000000000..9ef3b49b379 --- /dev/null +++ b/integrations/linear/src/payload-examples/CycleUpdated.json @@ -0,0 +1,27 @@ +{ + "data": { + "id": "fe7092dd-8a68-4a07-9477-b7c0a6f2136d", + "endsAt": "2023-11-13T00:00:00.000Z", + "number": 2, + "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "startsAt": "2023-10-15T23:00:00.000Z", + "createdAt": "2023-09-17T11:00:19.616Z", + "updatedAt": "2023-09-17T11:00:47.427Z", + "scopeHistory": [], + "issueCountHistory": [], + "completedScopeHistory": [], + "inProgressScopeHistory": [], + "completedIssueCountHistory": [], + "uncompletedIssuesUponCloseIds": [] + }, + "type": "Cycle", + "action": "update", + "createdAt": "2023-09-17T11:00:47.427Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "endsAt": "2023-11-12T23:00:00.000Z", + "updatedAt": "2023-09-17T11:00:47.414Z" + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:00:47.463Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/Issue.json b/integrations/linear/src/payload-examples/Issue.json new file mode 100644 index 00000000000..bc64472006a --- /dev/null +++ b/integrations/linear/src/payload-examples/Issue.json @@ -0,0 +1,42 @@ +{ + "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-ricks-world-👋", + "data": { + "id": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "team": { + "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "key": "TRI", + "name": "Trigger.dev" + }, + "state": { + "id": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2", + "name": "Todo", + "type": "unstarted", + "color": "#e2e2e2" + }, + "title": "Welcome to Rick's World ❤️‍🔥", + "labels": [], + "number": 1, + "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "stateId": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2", + "labelIds": [], + "priority": 2, + "createdAt": "2023-09-17T10:43:20.862Z", + "sortOrder": -13701.52, + "updatedAt": "2023-09-17T10:52:49.321Z", + "boardOrder": 0, + "description": "Hi there. Complete these issues to learn how to use Linear and discover ✨**ProTips.** When you're done, delete them or move them to another team for others to view.\n\n### **To start, type** `C` to **create your first issue.**\n\nCreate issues from any view using `C` or by clicking the `New issue` button.\n\n \n\n[1189b618-97f2-4e2c-ae25-4f25467679e7](https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\n\nOur issue editor and comments support Markdown. You can also: \n\n* @mention a teammate\n* Drag & drop images or video (Loom & Youtube embed automatically)\n* Use emoji ✅", + "priorityLabel": "High", + "subscriberIds": [], + "previousIdentifiers": [] + }, + "type": "Issue", + "action": "update", + "createdAt": "2023-09-17T10:52:49.321Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "title": "Welcome to Linear 👋", + "updatedAt": "2023-09-17T10:52:16.952Z" + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:52:49.762Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/IssueCreated.json b/integrations/linear/src/payload-examples/IssueCreated.json new file mode 100644 index 00000000000..1884d12c207 --- /dev/null +++ b/integrations/linear/src/payload-examples/IssueCreated.json @@ -0,0 +1,41 @@ +{ + "url": "https://linear.app/triggerdotdev/issue/TRI-10/you-know-the-rules", + "data": { + "id": "4ceeeecb-3442-4972-b27a-4ac7198d4eac", + "team": { + "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "key": "TRI", + "name": "Trigger.dev" + }, + "state": { + "id": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b", + "name": "Backlog", + "type": "backlog", + "color": "#bec2c8" + }, + "title": "You know the rules", + "labels": [], + "number": 10, + "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "stateId": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b", + "labelIds": [], + "priority": 0, + "createdAt": "2023-09-17T11:02:48.688Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "sortOrder": -81, + "updatedAt": "2023-09-17T11:02:48.688Z", + "boardOrder": 0, + "description": "And so do I", + "priorityLabel": "No priority", + "subscriberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "previousIdentifiers": [] + }, + "type": "Issue", + "action": "create", + "createdAt": "2023-09-17T11:02:48.688Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:02:48.800Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/IssueLabel.json b/integrations/linear/src/payload-examples/IssueLabel.json new file mode 100644 index 00000000000..b083ebd7c1b --- /dev/null +++ b/integrations/linear/src/payload-examples/IssueLabel.json @@ -0,0 +1,17 @@ +{ + "data": { + "id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16", + "name": "Give You Up", + "color": "#26b5ce", + "createdAt": "2023-09-17T11:08:22.426Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "updatedAt": "2023-09-17T11:08:22.426Z", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534" + }, + "type": "IssueLabel", + "action": "create", + "createdAt": "2023-09-17T11:08:22.426Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:08:22.463Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/IssueLabelCreated.json b/integrations/linear/src/payload-examples/IssueLabelCreated.json new file mode 100644 index 00000000000..b083ebd7c1b --- /dev/null +++ b/integrations/linear/src/payload-examples/IssueLabelCreated.json @@ -0,0 +1,17 @@ +{ + "data": { + "id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16", + "name": "Give You Up", + "color": "#26b5ce", + "createdAt": "2023-09-17T11:08:22.426Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "updatedAt": "2023-09-17T11:08:22.426Z", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534" + }, + "type": "IssueLabel", + "action": "create", + "createdAt": "2023-09-17T11:08:22.426Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:08:22.463Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/IssueLabelRemoved.json b/integrations/linear/src/payload-examples/IssueLabelRemoved.json new file mode 100644 index 00000000000..b968f24d7c1 --- /dev/null +++ b/integrations/linear/src/payload-examples/IssueLabelRemoved.json @@ -0,0 +1,17 @@ +{ + "data": { + "id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16", + "name": "Let You Down", + "color": "#f2994a", + "createdAt": "2023-09-17T11:08:22.426Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "updatedAt": "2023-09-17T11:08:31.484Z", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534" + }, + "type": "IssueLabel", + "action": "remove", + "createdAt": "2023-09-17T11:08:35.517Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:08:35.556Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/IssueLabelUpdated.json b/integrations/linear/src/payload-examples/IssueLabelUpdated.json new file mode 100644 index 00000000000..234385f2fe7 --- /dev/null +++ b/integrations/linear/src/payload-examples/IssueLabelUpdated.json @@ -0,0 +1,22 @@ +{ + "data": { + "id": "ef8d9e0d-7286-460f-b398-7aaf7d182c16", + "name": "Let You Down", + "color": "#f2994a", + "createdAt": "2023-09-17T11:08:22.426Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "updatedAt": "2023-09-17T11:08:31.484Z", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534" + }, + "type": "IssueLabel", + "action": "update", + "createdAt": "2023-09-17T11:08:31.484Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "name": "Give You Up", + "color": "#26b5ce", + "updatedAt": "2023-09-17T11:08:22.426Z" + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:08:31.527Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/IssueRemoved.json b/integrations/linear/src/payload-examples/IssueRemoved.json new file mode 100644 index 00000000000..1458e07836c --- /dev/null +++ b/integrations/linear/src/payload-examples/IssueRemoved.json @@ -0,0 +1,42 @@ +{ + "data": { + "id": "4ceeeecb-3442-4972-b27a-4ac7198d4eac", + "team": { + "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "key": "TRI", + "name": "Trigger.dev" + }, + "state": { + "id": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b", + "name": "Backlog", + "type": "backlog", + "color": "#bec2c8" + }, + "title": "You know the rules", + "labels": [], + "number": 10, + "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "stateId": "6b6fd762-cc10-4da8-9775-dfc0784d2a3b", + "trashed": true, + "labelIds": [], + "priority": 0, + "createdAt": "2023-09-17T11:02:48.688Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "sortOrder": -81, + "updatedAt": "2023-09-17T11:02:48.689Z", + "archivedAt": "2023-09-17T11:02:58.487Z", + "boardOrder": 0, + "description": "And so do I", + "priorityLabel": "No priority", + "subscriberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "previousIdentifiers": [] + }, + "type": "Issue", + "action": "remove", + "createdAt": "2023-09-17T11:02:58.487Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:02:58.564Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/IssueUpdated.json b/integrations/linear/src/payload-examples/IssueUpdated.json new file mode 100644 index 00000000000..bc64472006a --- /dev/null +++ b/integrations/linear/src/payload-examples/IssueUpdated.json @@ -0,0 +1,42 @@ +{ + "url": "https://linear.app/triggerdotdev/issue/TRI-1/welcome-to-ricks-world-👋", + "data": { + "id": "ee4d5612-0752-441a-95cd-2a0de56689d3", + "team": { + "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "key": "TRI", + "name": "Trigger.dev" + }, + "state": { + "id": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2", + "name": "Todo", + "type": "unstarted", + "color": "#e2e2e2" + }, + "title": "Welcome to Rick's World ❤️‍🔥", + "labels": [], + "number": 1, + "teamId": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "stateId": "8b77cd9c-f0cc-4be0-bd9b-52326d5c62a2", + "labelIds": [], + "priority": 2, + "createdAt": "2023-09-17T10:43:20.862Z", + "sortOrder": -13701.52, + "updatedAt": "2023-09-17T10:52:49.321Z", + "boardOrder": 0, + "description": "Hi there. Complete these issues to learn how to use Linear and discover ✨**ProTips.** When you're done, delete them or move them to another team for others to view.\n\n### **To start, type** `C` to **create your first issue.**\n\nCreate issues from any view using `C` or by clicking the `New issue` button.\n\n \n\n[1189b618-97f2-4e2c-ae25-4f25467679e7](https://uploads.linear.app/fe63b3e2-bf87-46c0-8784-cd7d639287c8/532d146d-bcd6-4602-bf1f-83f674b70fff/1189b618-97f2-4e2c-ae25-4f25467679e7)\n\nOur issue editor and comments support Markdown. You can also: \n\n* @mention a teammate\n* Drag & drop images or video (Loom & Youtube embed automatically)\n* Use emoji ✅", + "priorityLabel": "High", + "subscriberIds": [], + "previousIdentifiers": [] + }, + "type": "Issue", + "action": "update", + "createdAt": "2023-09-17T10:52:49.321Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "title": "Welcome to Linear 👋", + "updatedAt": "2023-09-17T10:52:16.952Z" + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T10:52:49.762Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/Project.json b/integrations/linear/src/payload-examples/Project.json new file mode 100644 index 00000000000..0d80b98a184 --- /dev/null +++ b/integrations/linear/src/payload-examples/Project.json @@ -0,0 +1,38 @@ +{ + "url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512", + "data": { + "id": "708440e6-648c-40f9-be87-9e00bbd8d1ec", + "name": "Rick's World", + "color": "#bec2c8", + "state": "backlog", + "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "slugId": "1e55e5e64512", + "teamIds": [ + "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f" + ], + "roadmaps": [], + "createdAt": "2023-09-17T11:24:43.171Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "memberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "sortOrder": 8161.55, + "updatedAt": "2023-09-17T11:24:43.171Z", + "milestones": [], + "description": "We know the game and we're gonna play it", + "scopeHistory": [], + "slackNewIssue": true, + "issueCountHistory": [], + "slackIssueComments": true, + "slackIssueStatuses": true, + "completedScopeHistory": [], + "inProgressScopeHistory": [], + "completedIssueCountHistory": [] + }, + "type": "Project", + "action": "create", + "createdAt": "2023-09-17T11:24:43.171Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:24:43.228Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ProjectCreated.json b/integrations/linear/src/payload-examples/ProjectCreated.json new file mode 100644 index 00000000000..0d80b98a184 --- /dev/null +++ b/integrations/linear/src/payload-examples/ProjectCreated.json @@ -0,0 +1,38 @@ +{ + "url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512", + "data": { + "id": "708440e6-648c-40f9-be87-9e00bbd8d1ec", + "name": "Rick's World", + "color": "#bec2c8", + "state": "backlog", + "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "slugId": "1e55e5e64512", + "teamIds": [ + "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f" + ], + "roadmaps": [], + "createdAt": "2023-09-17T11:24:43.171Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "memberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "sortOrder": 8161.55, + "updatedAt": "2023-09-17T11:24:43.171Z", + "milestones": [], + "description": "We know the game and we're gonna play it", + "scopeHistory": [], + "slackNewIssue": true, + "issueCountHistory": [], + "slackIssueComments": true, + "slackIssueStatuses": true, + "completedScopeHistory": [], + "inProgressScopeHistory": [], + "completedIssueCountHistory": [] + }, + "type": "Project", + "action": "create", + "createdAt": "2023-09-17T11:24:43.171Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:24:43.228Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ProjectRemoved.json b/integrations/linear/src/payload-examples/ProjectRemoved.json new file mode 100644 index 00000000000..f458fa0e672 --- /dev/null +++ b/integrations/linear/src/payload-examples/ProjectRemoved.json @@ -0,0 +1,40 @@ +{ + "data": { + "id": "708440e6-648c-40f9-be87-9e00bbd8d1ec", + "name": "Rick's World", + "color": "#bec2c8", + "state": "backlog", + "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "slugId": "1e55e5e64512", + "teamIds": [ + "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f" + ], + "trashed": true, + "roadmaps": [], + "createdAt": "2023-09-17T11:24:43.171Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "memberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "sortOrder": 8161.55, + "updatedAt": "2023-09-17T11:25:09.384Z", + "archivedAt": "2023-09-17T11:25:16.188Z", + "milestones": [], + "targetDate": "2024-02-14T00:00:00.000Z", + "description": "We know the game and we're gonna play it", + "scopeHistory": [], + "slackNewIssue": true, + "issueCountHistory": [], + "slackIssueComments": true, + "slackIssueStatuses": true, + "completedScopeHistory": [], + "inProgressScopeHistory": [], + "completedIssueCountHistory": [] + }, + "type": "Project", + "action": "remove", + "createdAt": "2023-09-17T11:25:16.188Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:25:16.358Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ProjectUpdate.json b/integrations/linear/src/payload-examples/ProjectUpdate.json new file mode 100644 index 00000000000..c4ff2e297f3 --- /dev/null +++ b/integrations/linear/src/payload-examples/ProjectUpdate.json @@ -0,0 +1,47 @@ +{ + "data": { + "id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4", + "body": "We're no strangers to love", + "user": { + "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "name": "Rick Astley" + }, + "health": "onTrack", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "project": { + "id": "54dac280-389f-4339-8d5d-de347a56eb79", + "name": "Rick's World" + }, + "roadmaps": [], + "createdAt": "2023-09-17T11:28:23.406Z", + "projectId": "54dac280-389f-4339-8d5d-de347a56eb79", + "updatedAt": "2023-09-17T11:28:23.406Z", + "infoSnapshot": { + "state": "backlog", + "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "memberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "teamsInfo": [ + { + "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "issueInfo": { + "triageCount": 0, + "backlogCount": 0, + "startedCount": 0, + "canceledCount": 0, + "completedCount": 0, + "unstartedCount": 0 + } + } + ], + "milestonesInfo": [] + } + }, + "type": "ProjectUpdate", + "action": "create", + "createdAt": "2023-09-17T11:28:23.406Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:28:23.479Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ProjectUpdateCreated.json b/integrations/linear/src/payload-examples/ProjectUpdateCreated.json new file mode 100644 index 00000000000..c4ff2e297f3 --- /dev/null +++ b/integrations/linear/src/payload-examples/ProjectUpdateCreated.json @@ -0,0 +1,47 @@ +{ + "data": { + "id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4", + "body": "We're no strangers to love", + "user": { + "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "name": "Rick Astley" + }, + "health": "onTrack", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "project": { + "id": "54dac280-389f-4339-8d5d-de347a56eb79", + "name": "Rick's World" + }, + "roadmaps": [], + "createdAt": "2023-09-17T11:28:23.406Z", + "projectId": "54dac280-389f-4339-8d5d-de347a56eb79", + "updatedAt": "2023-09-17T11:28:23.406Z", + "infoSnapshot": { + "state": "backlog", + "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "memberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "teamsInfo": [ + { + "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "issueInfo": { + "triageCount": 0, + "backlogCount": 0, + "startedCount": 0, + "canceledCount": 0, + "completedCount": 0, + "unstartedCount": 0 + } + } + ], + "milestonesInfo": [] + } + }, + "type": "ProjectUpdate", + "action": "create", + "createdAt": "2023-09-17T11:28:23.406Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:28:23.479Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ProjectUpdateRemoved.json b/integrations/linear/src/payload-examples/ProjectUpdateRemoved.json new file mode 100644 index 00000000000..1e74f5f4015 --- /dev/null +++ b/integrations/linear/src/payload-examples/ProjectUpdateRemoved.json @@ -0,0 +1,48 @@ +{ + "data": { + "id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4", + "body": "You know the rules and so do I", + "user": { + "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "name": "Rick Astley" + }, + "health": "onTrack", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "project": { + "id": "54dac280-389f-4339-8d5d-de347a56eb79", + "name": "Rick's World" + }, + "editedAt": "2023-09-17T11:28:39.142Z", + "roadmaps": [], + "createdAt": "2023-09-17T11:28:23.406Z", + "projectId": "54dac280-389f-4339-8d5d-de347a56eb79", + "updatedAt": "2023-09-17T11:28:39.142Z", + "infoSnapshot": { + "state": "backlog", + "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "memberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "teamsInfo": [ + { + "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "issueInfo": { + "triageCount": 0, + "backlogCount": 0, + "startedCount": 0, + "canceledCount": 0, + "completedCount": 0, + "unstartedCount": 0 + } + } + ], + "milestonesInfo": [] + } + }, + "type": "ProjectUpdate", + "action": "remove", + "createdAt": "2023-09-17T11:28:45.146Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:28:45.213Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ProjectUpdateUpdated.json b/integrations/linear/src/payload-examples/ProjectUpdateUpdated.json new file mode 100644 index 00000000000..d862c25d937 --- /dev/null +++ b/integrations/linear/src/payload-examples/ProjectUpdateUpdated.json @@ -0,0 +1,53 @@ +{ + "data": { + "id": "e5b2ae4e-acb5-4c6c-a22a-56c883e353b4", + "body": "You know the rules and so do I", + "user": { + "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "name": "Rick Astley" + }, + "health": "onTrack", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "project": { + "id": "54dac280-389f-4339-8d5d-de347a56eb79", + "name": "Rick's World" + }, + "editedAt": "2023-09-17T11:28:39.142Z", + "roadmaps": [], + "createdAt": "2023-09-17T11:28:23.406Z", + "projectId": "54dac280-389f-4339-8d5d-de347a56eb79", + "updatedAt": "2023-09-17T11:28:39.142Z", + "infoSnapshot": { + "state": "backlog", + "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "memberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "teamsInfo": [ + { + "id": "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f", + "issueInfo": { + "triageCount": 0, + "backlogCount": 0, + "startedCount": 0, + "canceledCount": 0, + "completedCount": 0, + "unstartedCount": 0 + } + } + ], + "milestonesInfo": [] + } + }, + "type": "ProjectUpdate", + "action": "update", + "createdAt": "2023-09-17T11:28:39.142Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "body": "We're no strangers to love", + "editedAt": null, + "updatedAt": "2023-09-17T11:28:23.406Z" + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:28:39.181Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ProjectUpdated.json b/integrations/linear/src/payload-examples/ProjectUpdated.json new file mode 100644 index 00000000000..47d995094d9 --- /dev/null +++ b/integrations/linear/src/payload-examples/ProjectUpdated.json @@ -0,0 +1,43 @@ +{ + "url": "https://linear.app/triggerdotdev/project/ricks-world-1e55e5e64512", + "data": { + "id": "708440e6-648c-40f9-be87-9e00bbd8d1ec", + "name": "Rick's World", + "color": "#bec2c8", + "state": "backlog", + "leadId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "slugId": "1e55e5e64512", + "teamIds": [ + "0d9e46fb-bf4a-4d7b-9bb0-d5928d8b1c9f" + ], + "roadmaps": [], + "createdAt": "2023-09-17T11:24:43.171Z", + "creatorId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "memberIds": [ + "b552c442-62c9-40da-9932-ed8cb6e15a3f" + ], + "sortOrder": 8161.55, + "updatedAt": "2023-09-17T11:25:09.383Z", + "milestones": [], + "targetDate": "2024-02-14T00:00:00.000Z", + "description": "We know the game and we're gonna play it", + "scopeHistory": [], + "slackNewIssue": true, + "issueCountHistory": [], + "slackIssueComments": true, + "slackIssueStatuses": true, + "completedScopeHistory": [], + "inProgressScopeHistory": [], + "completedIssueCountHistory": [] + }, + "type": "Project", + "action": "update", + "createdAt": "2023-09-17T11:25:09.383Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "updatedAt": "2023-09-17T11:24:43.171Z", + "targetDate": null + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:25:09.419Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/Reaction.json b/integrations/linear/src/payload-examples/Reaction.json new file mode 100644 index 00000000000..5cc6aff2be9 --- /dev/null +++ b/integrations/linear/src/payload-examples/Reaction.json @@ -0,0 +1,24 @@ +{ + "data": { + "id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944", + "user": { + "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "name": "Rick Astley" + }, + "emoji": "heart", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "comment": { + "id": "3f44c353-a3a1-45a4-b5da-259818ae21dd", + "body": "Never gonna", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f" + }, + "createdAt": "2023-09-17T11:31:27.394Z", + "updatedAt": "2023-09-17T11:31:27.394Z" + }, + "type": "Reaction", + "action": "create", + "createdAt": "2023-09-17T11:31:27.394Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:31:27.477Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ReactionCreated.json b/integrations/linear/src/payload-examples/ReactionCreated.json new file mode 100644 index 00000000000..856260578ac --- /dev/null +++ b/integrations/linear/src/payload-examples/ReactionCreated.json @@ -0,0 +1,24 @@ +{ + "data": { + "id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944", + "user": { + "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "name": "Rick Astley" + }, + "emoji": "heart", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "comment": { + "id": "3f44c353-a3a1-45a4-b5da-259818ae21dd", + "body": "Never gonna!", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f" + }, + "createdAt": "2023-09-17T11:31:27.394Z", + "updatedAt": "2023-09-17T11:31:27.394Z" + }, + "type": "Reaction", + "action": "create", + "createdAt": "2023-09-17T11:31:27.394Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:31:27.477Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ReactionRemoved.json b/integrations/linear/src/payload-examples/ReactionRemoved.json new file mode 100644 index 00000000000..60faace2506 --- /dev/null +++ b/integrations/linear/src/payload-examples/ReactionRemoved.json @@ -0,0 +1,24 @@ +{ + "data": { + "id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944", + "user": { + "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "name": "Rick Astley" + }, + "emoji": "heart", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "comment": { + "id": "3f44c353-a3a1-45a4-b5da-259818ae21dd", + "body": "Never gonna!", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f" + }, + "createdAt": "2023-09-17T11:31:27.394Z", + "updatedAt": "2023-09-17T11:31:27.394Z" + }, + "type": "Reaction", + "action": "remove", + "createdAt": "2023-09-17T11:32:07.003Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T11:32:07.088Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/ReactionUpdated.json b/integrations/linear/src/payload-examples/ReactionUpdated.json new file mode 100644 index 00000000000..50c86998640 --- /dev/null +++ b/integrations/linear/src/payload-examples/ReactionUpdated.json @@ -0,0 +1,28 @@ +{ + "data": { + "id": "a22b3a1f-109f-427f-aa59-78b4ab1d2944", + "user": { + "id": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "name": "Rick Astley" + }, + "emoji": "heart_on_fire", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f", + "comment": { + "id": "3f44c353-a3a1-45a4-b5da-259818ae21dd", + "body": "Never gonna!", + "userId": "b552c442-62c9-40da-9932-ed8cb6e15a3f" + }, + "createdAt": "2023-09-17T12:31:27.394Z", + "updatedAt": "2023-09-17T12:32:14.394Z" + }, + "type": "Reaction", + "action": "update", + "createdAt": "2023-09-17T12:31:27.394Z", + "webhookId": "d617c08b-47b6-4b09-a611-2e2f6d8fb99f", + "updatedFrom": { + "emoji": "heart", + "updatedAt": "2023-09-17T12:31:27.394Z" + }, + "organizationId": "7c0221e0-76d2-45eb-b803-6163c046c534", + "webhookTimestamp": "2023-09-17T12:31:27.477Z" +} \ No newline at end of file diff --git a/integrations/linear/src/payload-examples/index.ts b/integrations/linear/src/payload-examples/index.ts new file mode 100644 index 00000000000..ffdab1b2b84 --- /dev/null +++ b/integrations/linear/src/payload-examples/index.ts @@ -0,0 +1,154 @@ +import { EventSpecificationExample } from "@trigger.dev/sdk"; + +import AttachmentCreated from "./AttachmentCreated.json" +import AttachmentRemoved from "./AttachmentRemoved.json" +import AttachmentUpdated from "./AttachmentUpdated.json" +import CommentCreated from "./CommentCreated.json" +import CommentRemoved from "./CommentRemoved.json" +import CommentUpdated from "./CommentUpdated.json" +import CycleCreated from "./CycleCreated.json" +import CycleRemoved from "./CycleRemoved.json" +import CycleUpdated from "./CycleUpdated.json" +import IssueCreated from "./IssueCreated.json" +import IssueRemoved from "./IssueRemoved.json" +import IssueUpdated from "./IssueUpdated.json" +import IssueLabelCreated from "./IssueLabelCreated.json" +import IssueLabelRemoved from "./IssueLabelRemoved.json" +import IssueLabelUpdated from "./IssueLabelUpdated.json" +import ProjectCreated from "./ProjectCreated.json" +import ProjectRemoved from "./ProjectRemoved.json" +import ProjectUpdated from "./ProjectUpdated.json" +import ProjectUpdateCreated from "./ProjectUpdateCreated.json" +import ProjectUpdateRemoved from "./ProjectUpdateRemoved.json" +import ProjectUpdateUpdated from "./ProjectUpdateUpdated.json" +import ReactionCreated from "./ReactionCreated.json" +import ReactionRemoved from "./ReactionRemoved.json" +import ReactionUpdated from "./ReactionUpdated.json" + +export const attachmentCreated: EventSpecificationExample = { + id: "AttachmentCreated", + name: "Attachment created", + payload: AttachmentCreated, +}; +export const attachmentRemoved: EventSpecificationExample = { + id: "AttachmentRemoved", + name: "Attachment removed", + payload: AttachmentRemoved, +}; +export const attachmentUpdated: EventSpecificationExample = { + id: "AttachmentUpdated", + name: "Attachment updated", + payload: AttachmentUpdated, +}; + +export const commentCreated: EventSpecificationExample = { + id: "CommentCreated", + name: "Comment created", + payload: CommentCreated, +}; +export const commentRemoved: EventSpecificationExample = { + id: "CommentRemoved", + name: "Comment removed", + payload: CommentRemoved, +}; +export const commentUpdated: EventSpecificationExample = { + id: "CommentUpdated", + name: "Comment updated", + payload: CommentUpdated, +}; + +export const cycleCreated: EventSpecificationExample = { + id: "CycleCreated", + name: "Cycle created", + payload: CycleCreated, +}; +export const cycleRemoved: EventSpecificationExample = { + id: "CycleRemoved", + name: "Cycle removed", + payload: CycleRemoved, +}; +export const cycleUpdated: EventSpecificationExample = { + id: "CycleUpdated", + name: "Cycle updated", + payload: CycleUpdated, +}; + +export const issueCreated: EventSpecificationExample = { + id: "IssueCreated", + name: "Issue created", + payload: IssueCreated, +}; +export const issueRemoved: EventSpecificationExample = { + id: "IssueRemoved", + name: "Issue removed", + payload: IssueRemoved, +}; +export const issueUpdated: EventSpecificationExample = { + id: "IssueUpdated", + name: "Issue updated", + payload: IssueUpdated, +}; + +export const issueLabelCreated: EventSpecificationExample = { + id: "IssueLabelCreated", + name: "IssueLabel created", + payload: IssueLabelCreated, +}; +export const issueLabelRemoved: EventSpecificationExample = { + id: "IssueLabelRemoved", + name: "IssueLabel removed", + payload: IssueLabelRemoved, +}; +export const issueLabelUpdated: EventSpecificationExample = { + id: "IssueLabelUpdated", + name: "IssueLabel updated", + payload: IssueLabelUpdated, +}; + +export const projectCreated: EventSpecificationExample = { + id: "ProjectCreated", + name: "Project created", + payload: ProjectCreated, +}; +export const projectRemoved: EventSpecificationExample = { + id: "ProjectRemoved", + name: "Project removed", + payload: ProjectRemoved, +}; +export const projectUpdated: EventSpecificationExample = { + id: "ProjectUpdated", + name: "Project updated", + payload: ProjectUpdated, +}; + +export const projectUpdateCreated: EventSpecificationExample = { + id: "ProjectUpdateCreated", + name: "ProjectUpdate created", + payload: ProjectUpdateCreated, +}; +export const projectUpdateRemoved: EventSpecificationExample = { + id: "ProjectUpdateRemoved", + name: "ProjectUpdate removed", + payload: ProjectUpdateRemoved, +}; +export const projectUpdateUpdated: EventSpecificationExample = { + id: "ProjectUpdateUpdated", + name: "ProjectUpdate updated", + payload: ProjectUpdateUpdated, +}; + +export const reactionCreated: EventSpecificationExample = { + id: "ReactionCreated", + name: "Reaction created", + payload: ReactionCreated, +}; +export const reactionRemoved: EventSpecificationExample = { + id: "ReactionRemoved", + name: "Reaction removed", + payload: ReactionRemoved, +}; +export const reactionUpdated: EventSpecificationExample = { + id: "ReactionUpdated", + name: "Reaction updated", + payload: ReactionUpdated, +}; \ No newline at end of file From 5b8566095c9acf60ab0c5c08570dd02440006be7 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:31:43 +0000 Subject: [PATCH 28/55] Improve event props --- integrations/linear/src/events.ts | 95 +++++++++++++++++++------------ 1 file changed, 60 insertions(+), 35 deletions(-) diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts index 611eb00d962..49e51b79ac2 100644 --- a/integrations/linear/src/events.ts +++ b/integrations/linear/src/events.ts @@ -38,8 +38,6 @@ import { reactionUpdated, } from "./payload-examples"; -// TODO: useful properties - /** **WARNING:** Still in alpha - use with caution! */ export const onAttachment: EventSpecification = { name: "Attachment", @@ -48,7 +46,10 @@ export const onAttachment: EventSpecification = { icon: "linear", examples: [attachmentCreated, attachmentRemoved, attachmentUpdated], parsePayload: (payload) => payload as AttachmentEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Attachment ID", text: payload.data.id }, + ], }; /** **WARNING:** Still in alpha - use with caution! */ @@ -62,7 +63,7 @@ export const onAttachmentCreated: EventSpecification payload as ExtractCreate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], }; /** **WARNING:** Still in alpha - use with caution! */ @@ -76,7 +77,7 @@ export const onAttachmentRemoved: EventSpecification payload as ExtractRemove, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], }; /** **WARNING:** Still in alpha - use with caution! */ @@ -90,7 +91,7 @@ export const onAttachmentUpdated: EventSpecification payload as ExtractUpdate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], }; export const onComment: EventSpecification = { @@ -100,7 +101,10 @@ export const onComment: EventSpecification = { icon: "linear", examples: [commentCreated, commentRemoved, commentUpdated], parsePayload: (payload) => payload as CommentEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Comment ID", text: payload.data.id }, + ], }; export const onCommentCreated: EventSpecification> = { @@ -113,7 +117,7 @@ export const onCommentCreated: EventSpecification> = }, examples: [commentCreated], parsePayload: (payload) => payload as ExtractCreate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Comment ID", text: payload.data.id }], }; export const onCommentRemoved: EventSpecification> = { @@ -126,7 +130,7 @@ export const onCommentRemoved: EventSpecification> = }, examples: [commentRemoved], parsePayload: (payload) => payload as ExtractRemove, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Comment ID", text: payload.data.id }], }; export const onCommentUpdated: EventSpecification> = { @@ -139,7 +143,7 @@ export const onCommentUpdated: EventSpecification> = }, examples: [commentUpdated], parsePayload: (payload) => payload as ExtractUpdate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Comment ID", text: payload.data.id }], }; export const onCycle: EventSpecification = { @@ -149,7 +153,10 @@ export const onCycle: EventSpecification = { icon: "linear", examples: [cycleCreated, cycleRemoved, cycleUpdated], parsePayload: (payload) => payload as CycleEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Cycle ID", text: payload.data.id }, + ], }; export const onCycleCreated: EventSpecification> = { @@ -162,7 +169,7 @@ export const onCycleCreated: EventSpecification> = { }, examples: [cycleCreated], parsePayload: (payload) => payload as ExtractCreate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], }; export const onCycleRemoved: EventSpecification> = { @@ -175,7 +182,7 @@ export const onCycleRemoved: EventSpecification> = { }, examples: [cycleRemoved], parsePayload: (payload) => payload as ExtractRemove, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], }; export const onCycleUpdated: EventSpecification> = { @@ -188,7 +195,7 @@ export const onCycleUpdated: EventSpecification> = { }, examples: [cycleUpdated], parsePayload: (payload) => payload as ExtractUpdate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], }; export const onIssue: EventSpecification = { @@ -198,7 +205,10 @@ export const onIssue: EventSpecification = { icon: "linear", examples: [issueCreated, issueRemoved, issueUpdated], parsePayload: (payload) => payload as IssueEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Issue ID", text: payload.data.id }, + ], }; export const onIssueCreated: EventSpecification> = { @@ -211,7 +221,7 @@ export const onIssueCreated: EventSpecification> = { }, examples: [issueCreated], parsePayload: (payload) => payload as ExtractCreate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Issue ID", text: payload.data.id }], }; export const onIssueRemoved: EventSpecification> = { @@ -224,7 +234,7 @@ export const onIssueRemoved: EventSpecification> = { }, examples: [issueRemoved], parsePayload: (payload) => payload as ExtractRemove, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Issue ID", text: payload.data.id }], }; export const onIssueUpdated: EventSpecification> = { @@ -237,7 +247,7 @@ export const onIssueUpdated: EventSpecification> = { }, examples: [issueUpdated], parsePayload: (payload) => payload as ExtractUpdate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Issue ID", text: payload.data.id }], }; export const onIssueLabel: EventSpecification = { @@ -247,7 +257,10 @@ export const onIssueLabel: EventSpecification = { icon: "linear", examples: [issueLabelCreated, issueLabelRemoved, issueLabelUpdated], parsePayload: (payload) => payload as IssueLabelEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "IssueLabel ID", text: payload.data.id }, + ], }; export const onIssueLabelCreated: EventSpecification> = { @@ -260,7 +273,7 @@ export const onIssueLabelCreated: EventSpecification payload as ExtractCreate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], }; export const onIssueLabelRemoved: EventSpecification> = { @@ -273,7 +286,7 @@ export const onIssueLabelRemoved: EventSpecification payload as ExtractRemove, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], }; export const onIssueLabelUpdated: EventSpecification> = { @@ -286,7 +299,7 @@ export const onIssueLabelUpdated: EventSpecification payload as ExtractUpdate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], }; // TODO: this needs to be tested @@ -296,7 +309,10 @@ export const onIssueSLA: EventSpecification = { source: "linear.app", icon: "linear", parsePayload: (payload) => payload as IssueSLAEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "SLA action", text: payload.action }, + { label: "Issue ID", text: payload.issueData.id }, + ], }; export const onProject: EventSpecification = { @@ -306,7 +322,10 @@ export const onProject: EventSpecification = { icon: "linear", examples: [projectCreated, projectRemoved, projectUpdated], parsePayload: (payload) => payload as ProjectEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Project ID", text: payload.data.id }, + ], }; export const onProjectCreated: EventSpecification> = { @@ -319,7 +338,7 @@ export const onProjectCreated: EventSpecification> = }, examples: [projectCreated], parsePayload: (payload) => payload as ExtractCreate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Project ID", text: payload.data.id }], }; export const onProjectRemoved: EventSpecification> = { @@ -332,7 +351,7 @@ export const onProjectRemoved: EventSpecification> = }, examples: [projectRemoved], parsePayload: (payload) => payload as ExtractRemove, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Project ID", text: payload.data.id }], }; // TODO: think of a better naming scheme (clashes with ProjectUpdated entity) @@ -346,7 +365,7 @@ export const onProjectUpdated: EventSpecification> = }, examples: [projectUpdated], parsePayload: (payload) => payload as ExtractUpdate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Project ID", text: payload.data.id }], }; export const onProjectUpdate: EventSpecification = { @@ -356,7 +375,10 @@ export const onProjectUpdate: EventSpecification = { icon: "linear", examples: [projectUpdateCreated, projectUpdateRemoved, projectUpdateUpdated], parsePayload: (payload) => payload as ProjectUpdateEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "ProjectUpdate ID", text: payload.data.id }, + ], }; export const onProjectUpdateCreated: EventSpecification> = { @@ -369,7 +391,7 @@ export const onProjectUpdateCreated: EventSpecification payload as ExtractCreate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], }; export const onProjectUpdateRemoved: EventSpecification> = { @@ -382,7 +404,7 @@ export const onProjectUpdateRemoved: EventSpecification payload as ExtractRemove, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], }; export const onProjectUpdateUpdated: EventSpecification> = { @@ -395,7 +417,7 @@ export const onProjectUpdateUpdated: EventSpecification payload as ExtractUpdate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], }; export const onReaction: EventSpecification = { @@ -405,7 +427,10 @@ export const onReaction: EventSpecification = { icon: "linear", examples: [reactionCreated, reactionRemoved, reactionUpdated], parsePayload: (payload) => payload as ReactionEvent, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [ + { label: "Event action", text: payload.action }, + { label: "Reaction ID", text: payload.data.id }, + ], }; export const onReactionCreated: EventSpecification> = { @@ -418,7 +443,7 @@ export const onReactionCreated: EventSpecification> }, examples: [reactionCreated], parsePayload: (payload) => payload as ExtractCreate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], }; export const onReactionRemoved: EventSpecification> = { @@ -431,7 +456,7 @@ export const onReactionRemoved: EventSpecification> }, examples: [reactionRemoved], parsePayload: (payload) => payload as ExtractRemove, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], }; export const onReactionUpdated: EventSpecification> = { @@ -444,5 +469,5 @@ export const onReactionUpdated: EventSpecification> }, examples: [reactionUpdated], parsePayload: (payload) => payload as ExtractUpdate, - runProperties: (payload) => [{ label: "Change action", text: payload.action }], + runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], }; From 8115771ab12de353ba8b842cc05c2e64388669a0 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 06:31:50 +0000 Subject: [PATCH 29/55] Remove redundant source metadata --- integrations/linear/src/webhooks.ts | 5 ----- 1 file changed, 5 deletions(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 0dc6e0c6464..5d665756cb0 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -254,9 +254,6 @@ export function createWebhookEventSource( }); } -// TODO -const SourceMetadataSchema = z.object({}).optional(); - async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: Linear) { logger.debug("[@trigger.dev/linear] Handling webhook payload"); @@ -309,7 +306,6 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ } const webhookPayload = WebhookPayloadSchema.parse(body); - const parsedMetadata = SourceMetadataSchema.parse(source.metadata); return { events: [ @@ -321,6 +317,5 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ context: {}, }, ], - metadata: parsedMetadata, }; } From 3a00e221092f164c466b5600edcbca01c402489b Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 07:00:55 +0000 Subject: [PATCH 30/55] One type to rule them all --- integrations/linear/src/events.ts | 134 ++++++++++++++-------------- integrations/linear/src/types.ts | 9 +- integrations/linear/src/webhooks.ts | 6 +- 3 files changed, 73 insertions(+), 76 deletions(-) diff --git a/integrations/linear/src/events.ts b/integrations/linear/src/events.ts index 49e51b79ac2..492f299313d 100644 --- a/integrations/linear/src/events.ts +++ b/integrations/linear/src/events.ts @@ -10,7 +10,7 @@ import { ProjectUpdateEvent, ReactionEvent, } from "./schemas"; -import { ExtractCreate, ExtractRemove, ExtractUpdate } from "./types"; +import { GetLinearPayload } from "./types"; import { attachmentCreated, attachmentRemoved, @@ -39,13 +39,13 @@ import { } from "./payload-examples"; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachment: EventSpecification = { +export const onAttachment: EventSpecification> = { name: "Attachment", title: "On Attachment", source: "linear.app", icon: "linear", examples: [attachmentCreated, attachmentRemoved, attachmentUpdated], - parsePayload: (payload) => payload as AttachmentEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "Event action", text: payload.action }, { label: "Attachment ID", text: payload.data.id }, @@ -53,7 +53,7 @@ export const onAttachment: EventSpecification = { }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentCreated: EventSpecification> = { +export const onAttachmentCreated: EventSpecification> = { name: "Attachment", title: "On Attachment Created", source: "linear.app", @@ -62,12 +62,12 @@ export const onAttachmentCreated: EventSpecification payload as ExtractCreate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentRemoved: EventSpecification> = { +export const onAttachmentRemoved: EventSpecification> = { name: "Attachment", title: "On Attachment Removed", source: "linear.app", @@ -76,12 +76,12 @@ export const onAttachmentRemoved: EventSpecification payload as ExtractRemove, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], }; /** **WARNING:** Still in alpha - use with caution! */ -export const onAttachmentUpdated: EventSpecification> = { +export const onAttachmentUpdated: EventSpecification> = { name: "Attachment", title: "On Attachment Updated", source: "linear.app", @@ -90,24 +90,24 @@ export const onAttachmentUpdated: EventSpecification payload as ExtractUpdate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Attachment ID", text: payload.data.id }], }; -export const onComment: EventSpecification = { +export const onComment: EventSpecification> = { name: "Comment", title: "On Comment", source: "linear.app", icon: "linear", examples: [commentCreated, commentRemoved, commentUpdated], - parsePayload: (payload) => payload as CommentEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "Event action", text: payload.action }, { label: "Comment ID", text: payload.data.id }, ], }; -export const onCommentCreated: EventSpecification> = { +export const onCommentCreated: EventSpecification> = { name: "Comment", title: "On Comment Created", source: "linear.app", @@ -116,11 +116,11 @@ export const onCommentCreated: EventSpecification> = action: ["create"], }, examples: [commentCreated], - parsePayload: (payload) => payload as ExtractCreate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Comment ID", text: payload.data.id }], }; -export const onCommentRemoved: EventSpecification> = { +export const onCommentRemoved: EventSpecification> = { name: "Comment", title: "On Comment Removed", source: "linear.app", @@ -129,11 +129,11 @@ export const onCommentRemoved: EventSpecification> = action: ["remove"], }, examples: [commentRemoved], - parsePayload: (payload) => payload as ExtractRemove, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Comment ID", text: payload.data.id }], }; -export const onCommentUpdated: EventSpecification> = { +export const onCommentUpdated: EventSpecification> = { name: "Comment", title: "On Comment Updated", source: "linear.app", @@ -142,24 +142,24 @@ export const onCommentUpdated: EventSpecification> = action: ["update"], }, examples: [commentUpdated], - parsePayload: (payload) => payload as ExtractUpdate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Comment ID", text: payload.data.id }], }; -export const onCycle: EventSpecification = { +export const onCycle: EventSpecification> = { name: "Cycle", title: "On Cycle", source: "linear.app", icon: "linear", examples: [cycleCreated, cycleRemoved, cycleUpdated], - parsePayload: (payload) => payload as CycleEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "Event action", text: payload.action }, { label: "Cycle ID", text: payload.data.id }, ], }; -export const onCycleCreated: EventSpecification> = { +export const onCycleCreated: EventSpecification> = { name: "Cycle", title: "On Cycle Created", source: "linear.app", @@ -168,11 +168,11 @@ export const onCycleCreated: EventSpecification> = { action: ["create"], }, examples: [cycleCreated], - parsePayload: (payload) => payload as ExtractCreate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], }; -export const onCycleRemoved: EventSpecification> = { +export const onCycleRemoved: EventSpecification> = { name: "Cycle", title: "On Cycle Removed", source: "linear.app", @@ -181,11 +181,11 @@ export const onCycleRemoved: EventSpecification> = { action: ["remove"], }, examples: [cycleRemoved], - parsePayload: (payload) => payload as ExtractRemove, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], }; -export const onCycleUpdated: EventSpecification> = { +export const onCycleUpdated: EventSpecification> = { name: "Cycle", title: "On Cycle Updated", source: "linear.app", @@ -194,24 +194,24 @@ export const onCycleUpdated: EventSpecification> = { action: ["update"], }, examples: [cycleUpdated], - parsePayload: (payload) => payload as ExtractUpdate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Cycle ID", text: payload.data.id }], }; -export const onIssue: EventSpecification = { +export const onIssue: EventSpecification> = { name: "Issue", title: "On Issue", source: "linear.app", icon: "linear", examples: [issueCreated, issueRemoved, issueUpdated], - parsePayload: (payload) => payload as IssueEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "Event action", text: payload.action }, { label: "Issue ID", text: payload.data.id }, ], }; -export const onIssueCreated: EventSpecification> = { +export const onIssueCreated: EventSpecification> = { name: "Issue", title: "On Issue Created", source: "linear.app", @@ -220,11 +220,11 @@ export const onIssueCreated: EventSpecification> = { action: ["create"], }, examples: [issueCreated], - parsePayload: (payload) => payload as ExtractCreate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Issue ID", text: payload.data.id }], }; -export const onIssueRemoved: EventSpecification> = { +export const onIssueRemoved: EventSpecification> = { name: "Issue", title: "On Issue Removed", source: "linear.app", @@ -233,11 +233,11 @@ export const onIssueRemoved: EventSpecification> = { action: ["remove"], }, examples: [issueRemoved], - parsePayload: (payload) => payload as ExtractRemove, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Issue ID", text: payload.data.id }], }; -export const onIssueUpdated: EventSpecification> = { +export const onIssueUpdated: EventSpecification> = { name: "Issue", title: "On Issue Updated", source: "linear.app", @@ -246,24 +246,24 @@ export const onIssueUpdated: EventSpecification> = { action: ["update"], }, examples: [issueUpdated], - parsePayload: (payload) => payload as ExtractUpdate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Issue ID", text: payload.data.id }], }; -export const onIssueLabel: EventSpecification = { +export const onIssueLabel: EventSpecification> = { name: "IssueLabel", title: "On IssueLabel", source: "linear.app", icon: "linear", examples: [issueLabelCreated, issueLabelRemoved, issueLabelUpdated], - parsePayload: (payload) => payload as IssueLabelEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "Event action", text: payload.action }, { label: "IssueLabel ID", text: payload.data.id }, ], }; -export const onIssueLabelCreated: EventSpecification> = { +export const onIssueLabelCreated: EventSpecification> = { name: "IssueLabel", title: "On IssueLabel Created", source: "linear.app", @@ -272,11 +272,11 @@ export const onIssueLabelCreated: EventSpecification payload as ExtractCreate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], }; -export const onIssueLabelRemoved: EventSpecification> = { +export const onIssueLabelRemoved: EventSpecification> = { name: "IssueLabel", title: "On IssueLabel Removed", source: "linear.app", @@ -285,11 +285,11 @@ export const onIssueLabelRemoved: EventSpecification payload as ExtractRemove, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], }; -export const onIssueLabelUpdated: EventSpecification> = { +export const onIssueLabelUpdated: EventSpecification> = { name: "IssueLabel", title: "On IssueLabel Updated", source: "linear.app", @@ -298,37 +298,37 @@ export const onIssueLabelUpdated: EventSpecification payload as ExtractUpdate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "IssueLabel ID", text: payload.data.id }], }; // TODO: this needs to be tested -export const onIssueSLA: EventSpecification = { +export const onIssueSLA: EventSpecification> = { name: "IssueSLA", title: "On Issue SLA", source: "linear.app", icon: "linear", - parsePayload: (payload) => payload as IssueSLAEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "SLA action", text: payload.action }, { label: "Issue ID", text: payload.issueData.id }, ], }; -export const onProject: EventSpecification = { +export const onProject: EventSpecification> = { name: "Project", title: "On Project", source: "linear.app", icon: "linear", examples: [projectCreated, projectRemoved, projectUpdated], - parsePayload: (payload) => payload as ProjectEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "Event action", text: payload.action }, { label: "Project ID", text: payload.data.id }, ], }; -export const onProjectCreated: EventSpecification> = { +export const onProjectCreated: EventSpecification> = { name: "Project", title: "On Project Created", source: "linear.app", @@ -337,11 +337,11 @@ export const onProjectCreated: EventSpecification> = action: ["create"], }, examples: [projectCreated], - parsePayload: (payload) => payload as ExtractCreate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Project ID", text: payload.data.id }], }; -export const onProjectRemoved: EventSpecification> = { +export const onProjectRemoved: EventSpecification> = { name: "Project", title: "On Project Removed", source: "linear.app", @@ -350,12 +350,12 @@ export const onProjectRemoved: EventSpecification> = action: ["remove"], }, examples: [projectRemoved], - parsePayload: (payload) => payload as ExtractRemove, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Project ID", text: payload.data.id }], }; // TODO: think of a better naming scheme (clashes with ProjectUpdated entity) -export const onProjectUpdated: EventSpecification> = { +export const onProjectUpdated: EventSpecification> = { name: "Project", title: "On Project Updated", source: "linear.app", @@ -364,24 +364,24 @@ export const onProjectUpdated: EventSpecification> = action: ["update"], }, examples: [projectUpdated], - parsePayload: (payload) => payload as ExtractUpdate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Project ID", text: payload.data.id }], }; -export const onProjectUpdate: EventSpecification = { +export const onProjectUpdate: EventSpecification> = { name: "ProjectUpdate", title: "On ProjectUpdate", source: "linear.app", icon: "linear", examples: [projectUpdateCreated, projectUpdateRemoved, projectUpdateUpdated], - parsePayload: (payload) => payload as ProjectUpdateEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "Event action", text: payload.action }, { label: "ProjectUpdate ID", text: payload.data.id }, ], }; -export const onProjectUpdateCreated: EventSpecification> = { +export const onProjectUpdateCreated: EventSpecification> = { name: "ProjectUpdate", title: "On ProjectUpdate Created", source: "linear.app", @@ -390,11 +390,11 @@ export const onProjectUpdateCreated: EventSpecification payload as ExtractCreate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], }; -export const onProjectUpdateRemoved: EventSpecification> = { +export const onProjectUpdateRemoved: EventSpecification> = { name: "ProjectUpdate", title: "On ProjectUpdate Removed", source: "linear.app", @@ -403,11 +403,11 @@ export const onProjectUpdateRemoved: EventSpecification payload as ExtractRemove, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], }; -export const onProjectUpdateUpdated: EventSpecification> = { +export const onProjectUpdateUpdated: EventSpecification> = { name: "ProjectUpdate", title: "On ProjectUpdate Updated", source: "linear.app", @@ -416,24 +416,24 @@ export const onProjectUpdateUpdated: EventSpecification payload as ExtractUpdate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "ProjectUpdate ID", text: payload.data.id }], }; -export const onReaction: EventSpecification = { +export const onReaction: EventSpecification> = { name: "Reaction", title: "On Reaction", source: "linear.app", icon: "linear", examples: [reactionCreated, reactionRemoved, reactionUpdated], - parsePayload: (payload) => payload as ReactionEvent, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [ { label: "Event action", text: payload.action }, { label: "Reaction ID", text: payload.data.id }, ], }; -export const onReactionCreated: EventSpecification> = { +export const onReactionCreated: EventSpecification> = { name: "Reaction", title: "On Reaction Created", source: "linear.app", @@ -442,11 +442,11 @@ export const onReactionCreated: EventSpecification> action: ["create"], }, examples: [reactionCreated], - parsePayload: (payload) => payload as ExtractCreate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], }; -export const onReactionRemoved: EventSpecification> = { +export const onReactionRemoved: EventSpecification> = { name: "Reaction", title: "On Reaction Removed", source: "linear.app", @@ -455,11 +455,11 @@ export const onReactionRemoved: EventSpecification> action: ["remove"], }, examples: [reactionRemoved], - parsePayload: (payload) => payload as ExtractRemove, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], }; -export const onReactionUpdated: EventSpecification> = { +export const onReactionUpdated: EventSpecification> = { name: "Reaction", title: "On Reaction Updated", source: "linear.app", @@ -468,6 +468,6 @@ export const onReactionUpdated: EventSpecification> action: ["update"], }, examples: [reactionUpdated], - parsePayload: (payload) => payload as ExtractUpdate, + parsePayload: (payload) => payload as GetLinearPayload, runProperties: (payload) => [{ label: "Reaction ID", text: payload.data.id }], }; diff --git a/integrations/linear/src/types.ts b/integrations/linear/src/types.ts index f2d2617d837..76331abac05 100644 --- a/integrations/linear/src/types.ts +++ b/integrations/linear/src/types.ts @@ -1,5 +1,6 @@ -import { WebhookPayload } from "./schemas"; +import { WebhookActionType, WebhookPayload } from "./schemas"; -export type ExtractCreate = Extract; -export type ExtractRemove = Extract; -export type ExtractUpdate = Extract; +export type GetLinearPayload< + TPayload extends WebhookPayload, + TAction extends any = any, +> = TAction extends WebhookActionType ? Extract : TPayload; diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 5d665756cb0..0addc7dcc84 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -22,11 +22,7 @@ import { WebhookUpdateInput, WebhooksQueryVariables, } from "@linear/sdk/dist/_generated_documents"; -import { - WebhookActionTypeSchema, - WebhookPayloadSchema, - WebhookResourceTypeSchema, -} from "./schemas"; +import { WebhookPayloadSchema } from "./schemas"; type DeleteWebhookParams = { id: string; From 39dfc4f478caf5f3b1bb1ff7d5bdc12edc07b122 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:05:42 +0000 Subject: [PATCH 31/55] Recursive WithoutFunctions type --- integrations/linear/src/types.ts | 10 ++++++++++ integrations/linear/src/webhooks.ts | 14 +++++++------- 2 files changed, 17 insertions(+), 7 deletions(-) diff --git a/integrations/linear/src/types.ts b/integrations/linear/src/types.ts index 76331abac05..9a73cc44a3b 100644 --- a/integrations/linear/src/types.ts +++ b/integrations/linear/src/types.ts @@ -4,3 +4,13 @@ export type GetLinearPayload< TPayload extends WebhookPayload, TAction extends any = any, > = TAction extends WebhookActionType ? Extract : TPayload; + +type FunctionKeys = { + [K in keyof T]: T[K] extends Function ? K : never +}[keyof T] + +export type WithoutFunctions = T extends object + ? T extends Array + ? Array> + : { [K in keyof T as Exclude>]: WithoutFunctions } + : T \ No newline at end of file diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 0addc7dcc84..a7c4c38d55c 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -5,6 +5,7 @@ import { HandlerEvent, IntegrationTaskKey, Logger, + Prettify, } from "@trigger.dev/sdk"; import { LinearWebhooks, @@ -23,6 +24,7 @@ import { WebhooksQueryVariables, } from "@linear/sdk/dist/_generated_documents"; import { WebhookPayloadSchema } from "./schemas"; +import { WithoutFunctions } from "./types"; type DeleteWebhookParams = { id: string; @@ -33,8 +35,7 @@ type UpdateWebhookParams = { input: WebhookUpdateInput; }; -// TODO: types -const withoutFunctions = (obj: T): T => { +export const withoutFunctions = (obj: T): Prettify> => { return JSON.parse(JSON.stringify(obj), (key, value) => { if (typeof value === "function") { return undefined; @@ -53,8 +54,7 @@ export class Webhooks { create( key: IntegrationTaskKey, params: WebhookCreateInput - // TODO: tidy up return type - ): Promise & { webhook: Webhook | undefined }> { + ): Promise & { webhook: WithoutFunctions }> { return this.runTask( key, async (client, task, io) => { @@ -71,7 +71,7 @@ export class Webhooks { ); } - list(key: IntegrationTaskKey, params?: WebhooksQueryVariables): Promise { + list(key: IntegrationTaskKey, params?: WebhooksQueryVariables): Promise> { return this.runTask( key, async (client, task, io) => { @@ -90,7 +90,7 @@ export class Webhooks { ); } - delete(key: IntegrationTaskKey, params: DeleteWebhookParams): Promise { + delete(key: IntegrationTaskKey, params: DeleteWebhookParams): Promise> { return this.runTask( key, async (client, task, io) => { @@ -106,7 +106,7 @@ export class Webhooks { update( key: IntegrationTaskKey, params: UpdateWebhookParams - ): Promise & { webhook: Webhook | undefined }> { + ): Promise & { webhook: WithoutFunctions }> { return this.runTask( key, async (client, task) => { From cb1251dbd03328fb84d94ac278c45ea54944f7b0 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:11:17 +0000 Subject: [PATCH 32/55] Linear output serializer --- integrations/linear/src/types.ts | 13 +++++++------ integrations/linear/src/webhooks.ts | 22 +++++++++++----------- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/integrations/linear/src/types.ts b/integrations/linear/src/types.ts index 9a73cc44a3b..7de69e3866b 100644 --- a/integrations/linear/src/types.ts +++ b/integrations/linear/src/types.ts @@ -1,3 +1,4 @@ +import { Request } from "@linear/sdk"; import { WebhookActionType, WebhookPayload } from "./schemas"; export type GetLinearPayload< @@ -6,11 +7,11 @@ export type GetLinearPayload< > = TAction extends WebhookActionType ? Extract : TPayload; type FunctionKeys = { - [K in keyof T]: T[K] extends Function ? K : never -}[keyof T] + [K in keyof T]: T[K] extends Function ? K : never; +}[keyof T]; -export type WithoutFunctions = T extends object +export type SerializedLinearOutput = T extends object ? T extends Array - ? Array> - : { [K in keyof T as Exclude>]: WithoutFunctions } - : T \ No newline at end of file + ? Array> + : { [K in keyof T as Exclude | `_${string}`>]: SerializedLinearOutput } + : T; diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index a7c4c38d55c..a23da6793e6 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -24,7 +24,7 @@ import { WebhooksQueryVariables, } from "@linear/sdk/dist/_generated_documents"; import { WebhookPayloadSchema } from "./schemas"; -import { WithoutFunctions } from "./types"; +import { SerializedLinearOutput } from "./types"; type DeleteWebhookParams = { id: string; @@ -35,9 +35,9 @@ type UpdateWebhookParams = { input: WebhookUpdateInput; }; -export const withoutFunctions = (obj: T): Prettify> => { +export const serializeLinearOutput = (obj: T): Prettify> => { return JSON.parse(JSON.stringify(obj), (key, value) => { - if (typeof value === "function") { + if (typeof value === "function" || key.startsWith("_")) { return undefined; } return value; @@ -54,12 +54,12 @@ export class Webhooks { create( key: IntegrationTaskKey, params: WebhookCreateInput - ): Promise & { webhook: WithoutFunctions }> { + ): Promise & { webhook: Webhook | undefined }>> { return this.runTask( key, async (client, task, io) => { const payload = await client.createWebhook(params); - return withoutFunctions({ + return serializeLinearOutput({ ...payload, webhook: await payload.webhook, }); @@ -71,7 +71,7 @@ export class Webhooks { ); } - list(key: IntegrationTaskKey, params?: WebhooksQueryVariables): Promise> { + list(key: IntegrationTaskKey, params?: WebhooksQueryVariables): Promise> { return this.runTask( key, async (client, task, io) => { @@ -81,7 +81,7 @@ export class Webhooks { connections = await connections.fetchNext(); hooks.push(...connections.nodes); } - return withoutFunctions(hooks); + return serializeLinearOutput(hooks); }, { name: "List webhooks", @@ -90,11 +90,11 @@ export class Webhooks { ); } - delete(key: IntegrationTaskKey, params: DeleteWebhookParams): Promise> { + delete(key: IntegrationTaskKey, params: DeleteWebhookParams): Promise> { return this.runTask( key, async (client, task, io) => { - return withoutFunctions(await client.deleteWebhook(params.id)); + return serializeLinearOutput(await client.deleteWebhook(params.id)); }, { name: "Delete webhook", @@ -106,12 +106,12 @@ export class Webhooks { update( key: IntegrationTaskKey, params: UpdateWebhookParams - ): Promise & { webhook: WithoutFunctions }> { + ): Promise & { webhook: Webhook | undefined }>> { return this.runTask( key, async (client, task) => { const payload = await client.updateWebhook(params.id, params.input); - return withoutFunctions({ + return serializeLinearOutput({ ...payload, webhook: await payload.webhook, }); From 944697b652de1e958e7da2eb022564f19aee4f0d Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:21:04 +0000 Subject: [PATCH 33/55] Some tasks --- integrations/linear/src/index.ts | 131 ++++++++++++++++++++++++++- integrations/linear/src/types.ts | 4 + integrations/linear/src/webhooks.ts | 12 +-- references/job-catalog/src/linear.ts | 26 +++++- 4 files changed, 158 insertions(+), 15 deletions(-) diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index d01c4e49813..0037f3f6d28 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -4,14 +4,33 @@ import { IOTask, IntegrationTaskKey, Json, + Prettify, RunTaskErrorCallback, RunTaskOptions, TriggerIntegration, retry, } from "@trigger.dev/sdk"; -import { LinearClient, RatelimitedLinearError } from "@linear/sdk"; +import { + AttachmentPayload, + CommentPayload, + IssuePayload, + LinearClient, + RatelimitedLinearError, +} from "@linear/sdk"; import * as events from "./events"; -import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; +import { + TriggerParams, + Webhooks, + createTrigger, + createWebhookEventSource, +} from "./webhooks"; +import { + AttachmentCreateInput, + CommentCreateInput, + IssueCreateInput, + IssueUpdateInput, +} from "@linear/sdk/dist/_generated_documents"; +import { LinearReturnType, SerializedLinearOutput } from "./types"; export type LinearIntegrationOptions = { id: string; @@ -99,6 +118,105 @@ export class Linear implements TriggerIntegration { ); } + createAttachment( + key: IntegrationTaskKey, + params: AttachmentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createAttachment(params); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Create Attachment", + params, + properties: [ + { label: "Title", text: params.title }, + { label: "URL", text: params.url }, + ], + } + ); + } + + createComment( + key: IntegrationTaskKey, + params: CommentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createComment(params); + return serializeLinearOutput(await payload.comment); + }, + { + name: "Create Comment", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Body", text: params.body ?? "" }, + ], + } + ); + } + + createIssue( + key: IntegrationTaskKey, + params: IssueCreateInput & { title: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssue(params); + return serializeLinearOutput(await payload.issue); + }, + { + name: "Create Issue", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "Title", text: params.title }, + ], + } + ); + } + + updateIssue( + key: IntegrationTaskKey, + params: { id: string; input: IssueUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateIssue(params.id, params.input); + return serializeLinearOutput(await payload.issue); + }, + { + name: "Update Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + deleteIssue( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.deleteIssue(params.id); + return serializeLinearOutput(await payload.entity); + }, + { + name: "Delete Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + /** **WARNING:** Still in alpha - use with caution! */ onAttachment(params: TriggerParams = {}) { return createTrigger(this.source, events.onAttachment, params); @@ -274,4 +392,13 @@ export function onError(error: unknown) { } } +export const serializeLinearOutput = (obj: T): Prettify> => { + return JSON.parse(JSON.stringify(obj), (key, value) => { + if (typeof value === "function" || key.startsWith("_")) { + return undefined; + } + return value; + }); +}; + export { events }; diff --git a/integrations/linear/src/types.ts b/integrations/linear/src/types.ts index 7de69e3866b..695f32b6e33 100644 --- a/integrations/linear/src/types.ts +++ b/integrations/linear/src/types.ts @@ -15,3 +15,7 @@ export type SerializedLinearOutput = T extends object ? Array> : { [K in keyof T as Exclude | `_${string}`>]: SerializedLinearOutput } : T; + +export type LinearReturnType = Promise< + Awaited>> +>; diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index a23da6793e6..8531f315560 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -5,7 +5,6 @@ import { HandlerEvent, IntegrationTaskKey, Logger, - Prettify, } from "@trigger.dev/sdk"; import { LinearWebhooks, @@ -17,7 +16,7 @@ import { } from "@linear/sdk"; import { z } from "zod"; import * as events from "./events"; -import { Linear, LinearRunTask } from "./index"; +import { Linear, LinearRunTask, serializeLinearOutput } from "./index"; import { WebhookCreateInput, WebhookUpdateInput, @@ -35,15 +34,6 @@ type UpdateWebhookParams = { input: WebhookUpdateInput; }; -export const serializeLinearOutput = (obj: T): Prettify> => { - return JSON.parse(JSON.stringify(obj), (key, value) => { - if (typeof value === "function" || key.startsWith("_")) { - return undefined; - } - return value; - }); -}; - export class Webhooks { runTask: LinearRunTask; diff --git a/references/job-catalog/src/linear.ts b/references/job-catalog/src/linear.ts index 19bd2e855d5..8cf34212852 100644 --- a/references/job-catalog/src/linear.ts +++ b/references/job-catalog/src/linear.ts @@ -1,6 +1,6 @@ import { createExpressServer } from "@trigger.dev/express"; -import { DynamicTrigger, TriggerClient } from "@trigger.dev/sdk"; -import { Linear, events } from "@trigger.dev/linear"; +import { DynamicTrigger, TriggerClient, eventTrigger } from "@trigger.dev/sdk"; +import { Linear, events, serializeLinearOutput } from "@trigger.dev/linear"; export const client = new TriggerClient({ id: "job-catalog", @@ -21,6 +21,28 @@ const dynamicOnAttachmentTrigger = new DynamicTrigger(client, { source: linear.source, }); +client.defineJob({ + id: "linear-create-issue", + name: "Linear Create Issue", + version: "0.1.0", + integrations: { linear }, + trigger: eventTrigger({ + name: "linear.create.issue", + }), + run: async (payload, io, ctx) => { + const firstTeam = await io.linear.runTask("get-first-team", async (client) => { + const payload = await client.teams(); + return serializeLinearOutput(payload.nodes[0]); + }); + const issue = await io.linear.createIssue("create-issue", { + teamId: firstTeam.id, + title: "This issue will be deleted shortly", + }); + + return issue && (await io.linear.deleteIssue("delete-issue", { id: issue.id })); + }, +}); + client.defineJob({ id: "linear-on-issue", name: "Linear On Issue", From b3fec827deb6159f00b897788ef4fcba5507ba8f Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Mon, 18 Sep 2023 10:47:17 +0000 Subject: [PATCH 34/55] Update catalog entry --- .../externalApis/integrations/linear.ts | 15 ++++----- integrations/linear/src/index.ts | 31 +++++++++++++++---- 2 files changed, 33 insertions(+), 13 deletions(-) diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts index 6ffb757f91c..037f9862b4e 100644 --- a/apps/webapp/app/services/externalApis/integrations/linear.ts +++ b/apps/webapp/app/services/externalApis/integrations/linear.ts @@ -11,24 +11,25 @@ const linear = new Linear({ }); client.defineJob({ - id: "linear-integration-on-issue-created", - name: "Linear Integration - On Issue Created", + id: "linear-react-to-new-issue", + name: "Linear - React To New Issue", version: "0.1.0", integrations: { linear }, trigger: linear.onIssueCreated(), run: async (payload, io, ctx) => { - await io.linear.doSomething("some-task", { - foo: "bar" + await io.linear.createComment("create-comment", { + issueId: payload.data.id, + body: "Thank's for opening this issue!" }); - await io.linear.doSomethingElse("some-other-task", { - bar: "baz" + await io.linear.createReaction("create-reaction", { + issueId: payload.data.id, + emoji: "+1" }); return { payload, ctx }; }, }); - `, }; diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index 0037f3f6d28..a4f4b4fb8b0 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -16,19 +16,16 @@ import { IssuePayload, LinearClient, RatelimitedLinearError, + ReactionPayload, } from "@linear/sdk"; import * as events from "./events"; -import { - TriggerParams, - Webhooks, - createTrigger, - createWebhookEventSource, -} from "./webhooks"; +import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; import { AttachmentCreateInput, CommentCreateInput, IssueCreateInput, IssueUpdateInput, + ReactionCreateInput, } from "@linear/sdk/dist/_generated_documents"; import { LinearReturnType, SerializedLinearOutput } from "./types"; @@ -217,6 +214,28 @@ export class Linear implements TriggerIntegration { ); } + createReaction( + key: IntegrationTaskKey, + params: ReactionCreateInput & { emoji: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createReaction(params); + return serializeLinearOutput(await payload.reaction); + }, + { + name: "Create Reaction", + params, + properties: [ + { label: "Comment ID", text: params.commentId ?? "N/A" }, + { label: "Issue ID", text: params.issueId ?? "N/A" }, + { label: "Emoji", text: params.emoji }, + ], + } + ); + } + /** **WARNING:** Still in alpha - use with caution! */ onAttachment(params: TriggerParams = {}) { return createTrigger(this.source, events.onAttachment, params); From 4f34d1fe4078d2371a929a214b4ceb2358dd9b0c Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Sep 2023 07:07:46 +0000 Subject: [PATCH 35/55] Dynamic usage sample --- .../externalApis/integrations/linear.ts | 44 +++++-------------- 1 file changed, 10 insertions(+), 34 deletions(-) diff --git a/apps/webapp/app/services/externalApis/integrations/linear.ts b/apps/webapp/app/services/externalApis/integrations/linear.ts index 037f9862b4e..3d2b805debe 100644 --- a/apps/webapp/app/services/externalApis/integrations/linear.ts +++ b/apps/webapp/app/services/externalApis/integrations/linear.ts @@ -1,13 +1,13 @@ import type { HelpSample, Integration } from "../types"; -const usageSample: HelpSample = { - title: "Using the client", - code: ` -import { Linear, events } from "@trigger.dev/linear"; +function usageSample(hasApiKey: boolean): HelpSample { + return { + title: "Using the client", + code: ` +import { Linear } from "@trigger.dev/linear"; const linear = new Linear({ - id: "__SLUG__", - token: process.env.LINEAR_TOKEN!, + id: "__SLUG__",${hasApiKey ? ",\n token: process.env.LINEAR_TOKEN!" : ""} }); client.defineJob({ @@ -31,7 +31,8 @@ client.defineJob({ }, }); `, -}; + }; +} export const linear: Integration = { identifier: "linear", @@ -90,38 +91,13 @@ export const linear: Integration = { }, ], help: { - samples: [ - { - title: "Creating the client", - code: ` -import { Linear } from "@trigger.dev/linear"; - -const linear = new Linear({ - id: "__SLUG__" -}); -`, - }, - usageSample, - ], + samples: [usageSample(false)], }, }, apikey: { type: "apikey", help: { - samples: [ - { - title: "Creating the client", - code: ` -import { Linear } from "@trigger.dev/linear"; - -const linear = new Linear({ - id: "__SLUG__", - token: process.env.LINEAR_TOKEN! -}); -`, - }, - usageSample, - ], + samples: [usageSample(true)], }, }, }, From 1b37324d8122d2996a49601d18b3668225b7bc1b Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Sep 2023 07:08:42 +0000 Subject: [PATCH 36/55] Bump version --- integrations/linear/package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/integrations/linear/package.json b/integrations/linear/package.json index ace272f5535..c65a4a586f6 100644 --- a/integrations/linear/package.json +++ b/integrations/linear/package.json @@ -1,6 +1,6 @@ { "name": "@trigger.dev/linear", - "version": "0.0.1", + "version": "2.1.3", "description": "Trigger.dev integration for @linear/sdk", "main": "./dist/index.js", "types": "./dist/index.d.ts", From eb947505442635c60b5d44f0285db3f0520fd2b4 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Sep 2023 07:11:36 +0000 Subject: [PATCH 37/55] Remove tunnel --- integrations/linear/src/webhooks.ts | 16 +++++----------- 1 file changed, 5 insertions(+), 11 deletions(-) diff --git a/integrations/linear/src/webhooks.ts b/integrations/linear/src/webhooks.ts index 8531f315560..9ed4b1f4caf 100644 --- a/integrations/linear/src/webhooks.ts +++ b/integrations/linear/src/webhooks.ts @@ -174,11 +174,6 @@ export function createWebhookEventSource( // easily identify webhooks on linear const label = `trigger.${params.teamId ? params.teamId : "all"}`; - // TODO: remove tunnel - const url = process.env["DEV_TUNNEL"] - ? httpSource.url.replace("http://localhost:3030", process.env["DEV_TUNNEL"]) - : httpSource.url; - if (httpSource.active && webhookData.success) { const hasMissingOptions = Object.values(options).some( (option) => option.missing.length > 0 @@ -191,7 +186,7 @@ export function createWebhookEventSource( label, resourceTypes: allEvents, secret: httpSource.secret, - url, + url: httpSource.url, }, }); @@ -203,7 +198,7 @@ export function createWebhookEventSource( // check for existing hooks that match url const listResponse = await io.integration.webhooks().list("list-webhooks"); - const existingWebhook = listResponse.find((w) => w.url === url); + const existingWebhook = listResponse.find((w) => w.url === httpSource.url); if (existingWebhook) { const updatedWebhook = await io.integration.webhooks().update("update-webhook", { @@ -212,7 +207,7 @@ export function createWebhookEventSource( label, resourceTypes: allEvents, secret: httpSource.secret, - url, + url: httpSource.url, }, }); @@ -228,7 +223,7 @@ export function createWebhookEventSource( resourceTypes: allEvents, secret: httpSource.secret, teamId: params.teamId, - url, + url: httpSource.url, }); return { @@ -256,8 +251,7 @@ async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integ LINEAR_IPS[0] ).split(",")[0]; - // TODO: remove tunnel - if (!process.env["DEV_TUNNEL"] && !LINEAR_IPS.includes(clientIp)) { + if (!LINEAR_IPS.includes(clientIp)) { logger.error("[@trigger.dev/linear] Error validating webhook source, IP invalid."); throw Error("[@trigger.dev/linear] Invalid source IP."); } From 34c7baad193d4dd0500f61228e97996e705671e2 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Sep 2023 08:02:06 +0000 Subject: [PATCH 38/55] More tasks --- integrations/linear/src/index.ts | 276 +++++++++++++++++++++++++++++++ 1 file changed, 276 insertions(+) diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index a4f4b4fb8b0..d54b390791d 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -13,8 +13,13 @@ import { import { AttachmentPayload, CommentPayload, + CyclePayload, + DeletePayload, + IssueLabelPayload, IssuePayload, LinearClient, + ProjectPayload, + ProjectUpdatePayload, RatelimitedLinearError, ReactionPayload, } from "@linear/sdk"; @@ -22,9 +27,19 @@ import * as events from "./events"; import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; import { AttachmentCreateInput, + AttachmentUpdateInput, CommentCreateInput, + CommentUpdateInput, + CycleCreateInput, + CycleUpdateInput, IssueCreateInput, + IssueLabelCreateInput, + IssueLabelUpdateInput, IssueUpdateInput, + ProjectCreateInput, + ProjectUpdateCreateInput, + ProjectUpdateInput, + ProjectUpdateUpdateInput, ReactionCreateInput, } from "@linear/sdk/dist/_generated_documents"; import { LinearReturnType, SerializedLinearOutput } from "./types"; @@ -136,6 +151,32 @@ export class Linear implements TriggerIntegration { ); } + deleteAttachment(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteAttachment(params.id), { + name: "Delete Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + }); + } + + updateAttachment( + key: IntegrationTaskKey, + params: { id: string; input: AttachmentUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateAttachment(params.id, params.input); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Update Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + } + ); + } + createComment( key: IntegrationTaskKey, params: CommentCreateInput @@ -157,6 +198,74 @@ export class Linear implements TriggerIntegration { ); } + deleteComment(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteComment(params.id), { + name: "Delete Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + }); + } + + updateComment( + key: IntegrationTaskKey, + params: { id: string; input: CommentUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateComment(params.id, params.input); + return serializeLinearOutput(await payload.comment); + }, + { + name: "Update Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + } + ); + } + + createCycle( + key: IntegrationTaskKey, + params: CycleCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createCycle(params); + return serializeLinearOutput(await payload.cycle); + }, + { + name: "Create Cycle", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "Start at", text: params.startsAt.toISOString() }, + { label: "Ends at", text: params.endsAt.toISOString() }, + ], + } + ); + } + + // deleteCycle() does not exist + + updateCycle( + key: IntegrationTaskKey, + params: { id: string; input: CycleUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateCycle(params.id, params.input); + return serializeLinearOutput(await payload.cycle); + }, + { + name: "Update Cycle", + params, + properties: [{ label: "Cycle ID", text: params.id }], + } + ); + } + createIssue( key: IntegrationTaskKey, params: IssueCreateInput & { title: string } @@ -214,6 +323,162 @@ export class Linear implements TriggerIntegration { ); } + createIssueLabel( + key: IntegrationTaskKey, + params: IssueLabelCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssueLabel(params); + return serializeLinearOutput(await payload.issueLabel); + }, + { + name: "Create IssueLabel", + params, + properties: [ + { label: "Label name", text: params.name }, + ], + } + ); + } + + deleteIssueLabel(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteIssueLabel(params.id), { + name: "Delete IssueLabel", + params, + properties: [{ label: "Label ID", text: params.id }], + }); + } + + updateIssueLabel( + key: IntegrationTaskKey, + params: { id: string; input: IssueLabelUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateIssueLabel(params.id, params.input); + return serializeLinearOutput(await payload.issueLabel); + }, + { + name: "Update IssueLabel", + params, + properties: [{ label: "Label ID", text: params.id }], + } + ); + } + + createProject( + key: IntegrationTaskKey, + params: ProjectCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProject(params); + return serializeLinearOutput(await payload.project); + }, + { + name: "Create Project", + params, + properties: [ + { label: "Team IDs", text: params.teamIds.join(", ") }, + { label: "Project name", text: params.name }, + ], + } + ); + } + + deleteProject( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.deleteProject(params.id); + return serializeLinearOutput(await payload.entity); + }, + { + name: "Delete Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + updateProject( + key: IntegrationTaskKey, + params: { id: string; input: ProjectUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateProject(params.id, params.input); + return serializeLinearOutput(await payload.project); + }, + { + name: "Update Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + createProjectUpdate( + key: IntegrationTaskKey, + params: ProjectUpdateCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectUpdate(params); + return serializeLinearOutput(await payload.projectUpdate); + }, + { + name: "Create ProjectUpdate", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + ], + } + ); + } + + deleteProjectUpdate( + key: IntegrationTaskKey, + params: { id: string } + ): Promise { + return this.runTask( + key, + (client) => client.deleteProjectUpdate(params.id), + { + name: "Delete ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + } + ); + } + + updateProjectUpdate( + key: IntegrationTaskKey, + params: { id: string; input: ProjectUpdateUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateProjectUpdate(params.id, params.input); + return serializeLinearOutput(await payload.projectUpdate); + }, + { + name: "Update ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + } + ); + } + createReaction( key: IntegrationTaskKey, params: ReactionCreateInput & { emoji: string } @@ -236,6 +501,16 @@ export class Linear implements TriggerIntegration { ); } + deleteReaction(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteReaction(params.id), { + name: "Delete Reaction", + params, + properties: [{ label: "Reaction ID", text: params.id }], + }); + } + + // updateReaction() does not exist + /** **WARNING:** Still in alpha - use with caution! */ onAttachment(params: TriggerParams = {}) { return createTrigger(this.source, events.onAttachment, params); @@ -368,6 +643,7 @@ export class Linear implements TriggerIntegration { return createTrigger(this.source, events.onReactionRemoved, params); } + /** Good luck ever triggering this! */ onReactionUpdated(params: TriggerParams = {}) { return createTrigger(this.source, events.onReactionUpdated, params); } From cb6e260430fd448a4ebda902c936955f5e09b539 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:45:43 +0000 Subject: [PATCH 39/55] Add optional skipRetrying on runTask errors --- packages/trigger-sdk/src/io.ts | 26 ++++++++++++++++++-------- 1 file changed, 18 insertions(+), 8 deletions(-) diff --git a/packages/trigger-sdk/src/io.ts b/packages/trigger-sdk/src/io.ts index 1e3574e0d15..c01d16fb3d7 100644 --- a/packages/trigger-sdk/src/io.ts +++ b/packages/trigger-sdk/src/io.ts @@ -56,7 +56,11 @@ export type RunTaskErrorCallback = ( error: unknown, task: IOTask, io: IO -) => { retryAt: Date; error?: Error; jitter?: number } | Error | undefined | void; +) => + | { retryAt?: Date; error?: Error; jitter?: number; skipRetrying?: boolean } + | Error + | undefined + | void; export class IO { private _id: string; @@ -574,6 +578,8 @@ export class IO { throw error; } + let skipRetrying = false; + if (onError) { try { const onErrorResult = onError(error, task, this); @@ -582,13 +588,17 @@ export class IO { if (onErrorResult instanceof Error) { error = onErrorResult; } else { - const parsedError = ErrorWithStackSchema.safeParse(onErrorResult.error); + skipRetrying = !!onErrorResult.skipRetrying; + + if (onErrorResult.retryAt && !skipRetrying) { + const parsedError = ErrorWithStackSchema.safeParse(onErrorResult.error); - throw new RetryWithTaskError( - parsedError.success ? parsedError.data : { message: "Unknown error" }, - task, - onErrorResult.retryAt - ); + throw new RetryWithTaskError( + parsedError.success ? parsedError.data : { message: "Unknown error" }, + task, + onErrorResult.retryAt + ); + } } } } catch (innerError) { @@ -602,7 +612,7 @@ export class IO { const parsedError = ErrorWithStackSchema.safeParse(error); - if (options?.retry) { + if (options?.retry && !skipRetrying) { const retryAt = calculateRetryAt(options.retry, task.attempts - 1); if (retryAt) { From 8ea182145b2d5b87f79c4ae232b2d13d342110de Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:46:54 +0000 Subject: [PATCH 40/55] Fail fast on user errors --- integrations/linear/src/index.ts | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index d54b390791d..4004eeda303 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -653,7 +653,16 @@ export class Linear implements TriggerIntegration { } } -export function onError(error: unknown) { +export function onError(error: unknown): ReturnType { + if (error instanceof LinearError) { + // fail fast on user errors + if (error.errors?.some((e) => e.userError)) { + return { + skipRetrying: true, + }; + } + } + if (!(error instanceof RatelimitedLinearError)) { return; } From 52bd96651b1a20fdfb75f5bc693f86b43238c749 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Sep 2023 12:59:09 +0000 Subject: [PATCH 41/55] Entity getter tasks --- integrations/linear/src/index.ts | 270 ++++++++++++++++++++++++++++--- integrations/linear/src/types.ts | 4 +- 2 files changed, 254 insertions(+), 20 deletions(-) diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index 4004eeda303..ae8fad8c5dd 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -11,14 +11,27 @@ import { retry, } from "@trigger.dev/sdk"; import { + Attachment, + AttachmentConnection, AttachmentPayload, + Comment, + CommentConnection, CommentPayload, CyclePayload, DeletePayload, + Issue, + IssueConnection, + IssueLabel, + IssueLabelConnection, IssueLabelPayload, IssuePayload, LinearClient, + LinearError, + Project, + ProjectConnection, ProjectPayload, + ProjectUpdate, + ProjectUpdateConnection, ProjectUpdatePayload, RatelimitedLinearError, ReactionPayload, @@ -28,18 +41,25 @@ import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from import { AttachmentCreateInput, AttachmentUpdateInput, + AttachmentsQueryVariables, CommentCreateInput, CommentUpdateInput, + CommentsQueryVariables, CycleCreateInput, CycleUpdateInput, IssueCreateInput, IssueLabelCreateInput, IssueLabelUpdateInput, + IssueLabelsQueryVariables, IssueUpdateInput, + IssuesQueryVariables, ProjectCreateInput, ProjectUpdateCreateInput, ProjectUpdateInput, + ProjectUpdateQueryVariables, ProjectUpdateUpdateInput, + ProjectUpdatesQueryVariables, + ProjectsQueryVariables, ReactionCreateInput, } from "@linear/sdk/dist/_generated_documents"; import { LinearReturnType, SerializedLinearOutput } from "./types"; @@ -130,6 +150,39 @@ export class Linear implements TriggerIntegration { ); } + attachment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.attachment(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Attachment", + params, + properties: [{ label: "Attachment ID", text: params.id }], + } + ); + } + + attachments( + key: IntegrationTaskKey, + params: AttachmentsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.attachments(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Attachments", + params, + properties: queryProperties(params), + } + ); + } + createAttachment( key: IntegrationTaskKey, params: AttachmentCreateInput @@ -144,6 +197,7 @@ export class Linear implements TriggerIntegration { name: "Create Attachment", params, properties: [ + { label: "Issue ID", text: params.issueId }, { label: "Title", text: params.title }, { label: "URL", text: params.url }, ], @@ -177,6 +231,39 @@ export class Linear implements TriggerIntegration { ); } + comment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.comment(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + } + ); + } + + comments( + key: IntegrationTaskKey, + params: CommentsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.comments(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Comments", + params, + properties: queryProperties(params), + } + ); + } + createComment( key: IntegrationTaskKey, params: CommentCreateInput @@ -266,6 +353,39 @@ export class Linear implements TriggerIntegration { ); } + issue(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issue(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + issues( + key: IntegrationTaskKey, + params: IssuesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issues(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Issues", + params, + properties: queryProperties(params), + } + ); + } + createIssue( key: IntegrationTaskKey, params: IssueCreateInput & { title: string } @@ -323,6 +443,39 @@ export class Linear implements TriggerIntegration { ); } + issueLabel(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issueLabel(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get IssueLabel", + params, + properties: [{ label: "IssueLabel ID", text: params.id }], + } + ); + } + + issueLabels( + key: IntegrationTaskKey, + params: IssueLabelsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issueLabels(params); + return serializeLinearOutput(edges); + }, + { + name: "Get IssueLabels", + params, + properties: queryProperties(params), + } + ); + } + createIssueLabel( key: IntegrationTaskKey, params: IssueLabelCreateInput @@ -336,9 +489,7 @@ export class Linear implements TriggerIntegration { { name: "Create IssueLabel", params, - properties: [ - { label: "Label name", text: params.name }, - ], + properties: [{ label: "Label name", text: params.name }], } ); } @@ -369,6 +520,39 @@ export class Linear implements TriggerIntegration { ); } + project(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.project(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + projects( + key: IntegrationTaskKey, + params: ProjectsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projects(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Projects", + params, + properties: queryProperties(params), + } + ); + } + createProject( key: IntegrationTaskKey, params: ProjectCreateInput @@ -426,41 +610,65 @@ export class Linear implements TriggerIntegration { ); } - createProjectUpdate( + projectUpdate(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.projectUpdate(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + } + ); + } + + projectUpdates( key: IntegrationTaskKey, - params: ProjectUpdateCreateInput - ): LinearReturnType { + params: ProjectUpdatesQueryVariables = {} + ): LinearReturnType { return this.runTask( key, async (client) => { - const payload = await client.createProjectUpdate(params); - return serializeLinearOutput(await payload.projectUpdate); + const edges = await client.projectUpdates(params); + return serializeLinearOutput(edges); }, { - name: "Create ProjectUpdate", + name: "Get ProjectUpdates", params, - properties: [ - { label: "Project ID", text: params.projectId }, - ], + properties: queryProperties(params), } ); } - deleteProjectUpdate( + createProjectUpdate( key: IntegrationTaskKey, - params: { id: string } - ): Promise { + params: ProjectUpdateCreateInput + ): LinearReturnType { return this.runTask( key, - (client) => client.deleteProjectUpdate(params.id), + async (client) => { + const payload = await client.createProjectUpdate(params); + return serializeLinearOutput(await payload.projectUpdate); + }, { - name: "Delete ProjectUpdate", + name: "Create ProjectUpdate", params, - properties: [{ label: "ProjectUpdate ID", text: params.id }], + properties: [{ label: "Project ID", text: params.projectId }], } ); } + deleteProjectUpdate(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteProjectUpdate(params.id), { + name: "Delete ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + }); + } + updateProjectUpdate( key: IntegrationTaskKey, params: { id: string; input: ProjectUpdateUpdateInput } @@ -705,4 +913,30 @@ export const serializeLinearOutput = (obj: T): Prettify = Partial<{ + [K in keyof T]: T[K] | null; +}>; + +const queryProperties = (query: Nullable) => { + return [ + ...(query.after ? [{ label: "After", text: query.after }] : []), + ...(query.before ? [{ label: "Before", text: query.before }] : []), + ...(query.first ? [{ label: "First", text: String(query.first) }] : []), + ...(query.last ? [{ label: "Last", text: String(query.last) }] : []), + ...(query.orderBy ? [{ label: "Order by", text: query.orderBy }] : []), + ...(query.includeArchived + ? [{ label: "Include archived", text: String(query.includeArchived) }] + : []), + ]; +}; + export { events }; diff --git a/integrations/linear/src/types.ts b/integrations/linear/src/types.ts index 695f32b6e33..ac7873d8d1d 100644 --- a/integrations/linear/src/types.ts +++ b/integrations/linear/src/types.ts @@ -16,6 +16,6 @@ export type SerializedLinearOutput = T extends object : { [K in keyof T as Exclude | `_${string}`>]: SerializedLinearOutput } : T; -export type LinearReturnType = Promise< - Awaited>> +export type LinearReturnType = Promise< + Awaited>> >; From 435b5338c26c1145f149487ea89c40683b9c7480 Mon Sep 17 00:00:00 2001 From: nicktrn <55853254+nicktrn@users.noreply.github.com> Date: Tue, 19 Sep 2023 16:05:47 +0000 Subject: [PATCH 42/55] Another couple of tasks --- integrations/linear/src/index.ts | 1562 +++++++++++++++++++++++++----- integrations/linear/src/types.ts | 5 +- integrations/linear/src/utils.ts | 25 + 3 files changed, 1355 insertions(+), 237 deletions(-) create mode 100644 integrations/linear/src/utils.ts diff --git a/integrations/linear/src/index.ts b/integrations/linear/src/index.ts index ae8fad8c5dd..a5db646532a 100644 --- a/integrations/linear/src/index.ts +++ b/integrations/linear/src/index.ts @@ -17,52 +17,127 @@ import { Comment, CommentConnection, CommentPayload, + CreateOrJoinOrganizationResponse, + CycleArchivePayload, CyclePayload, DeletePayload, + DocumentConnection, + DocumentPayload, + DocumentSearchPayload, + Favorite, + FavoriteConnection, + FavoritePayload, Issue, + IssueArchivePayload, IssueConnection, IssueLabel, IssueLabelConnection, IssueLabelPayload, IssuePayload, + IssuePriorityValue, + IssueRelation, + IssueRelationConnection, + IssueRelationPayload, + IssueSearchPayload, LinearClient, LinearError, + NotificationArchivePayload, + NotificationConnection, + NotificationSubscriptionPayload, + Organization, + OrganizationInvitePayload, Project, + ProjectArchivePayload, ProjectConnection, + ProjectLink, + ProjectLinkConnection, + ProjectLinkPayload, + ProjectMilestonePayload, ProjectPayload, + ProjectSearchPayload, ProjectUpdate, ProjectUpdateConnection, ProjectUpdatePayload, RatelimitedLinearError, ReactionPayload, + RoadmapArchivePayload, + RoadmapPayload, + Team, + TeamConnection, + TeamMembership, + TeamMembershipConnection, + TeamMembershipPayload, + TeamPayload, + Template, + User, + UserConnection, + UserPayload, + WorkflowState, + WorkflowStateArchivePayload, + WorkflowStateConnection, + WorkflowStatePayload, } from "@linear/sdk"; import * as events from "./events"; import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks"; import { + ArchiveIssueMutationVariables, + ArchiveProjectMutationVariables, AttachmentCreateInput, + AttachmentLinkDiscordMutationVariables, + AttachmentLinkFrontMutationVariables, + AttachmentLinkIntercomMutationVariables, + AttachmentLinkSlackMutationVariables, + AttachmentLinkUrlMutationVariables, + AttachmentLinkZendeskMutationVariables, AttachmentUpdateInput, AttachmentsQueryVariables, CommentCreateInput, CommentUpdateInput, CommentsQueryVariables, + CreateOrganizationFromOnboardingMutationVariables, + CreateOrganizationInput, CycleCreateInput, CycleUpdateInput, + DocumentCreateInput, + DocumentsQueryVariables, + FavoriteCreateInput, + FavoritesQueryVariables, IssueCreateInput, IssueLabelCreateInput, IssueLabelUpdateInput, IssueLabelsQueryVariables, + IssueRelationCreateInput, + IssueRelationsQueryVariables, IssueUpdateInput, IssuesQueryVariables, + NotificationSubscriptionCreateInput, + NotificationsQueryVariables, + OrganizationInviteCreateInput, ProjectCreateInput, + ProjectLinkCreateInput, + ProjectLinksQueryVariables, + ProjectMilestoneCreateInput, ProjectUpdateCreateInput, ProjectUpdateInput, - ProjectUpdateQueryVariables, ProjectUpdateUpdateInput, ProjectUpdatesQueryVariables, ProjectsQueryVariables, ReactionCreateInput, + RoadmapCreateInput, + SearchDocumentsQueryVariables, + SearchIssuesQueryVariables, + SearchProjectsQueryVariables, + TeamCreateInput, + TeamMembershipCreateInput, + TeamMembershipsQueryVariables, + TeamsQueryVariables, + UpdateUserInput, + UsersQueryVariables, + WorkflowStateCreateInput, + WorkflowStatesQueryVariables, } from "@linear/sdk/dist/_generated_documents"; import { LinearReturnType, SerializedLinearOutput } from "./types"; +import { queryProperties } from "./utils"; export type LinearIntegrationOptions = { id: string; @@ -231,6 +306,222 @@ export class Linear implements TriggerIntegration { ); } + attachmentLinkDiscord( + key: IntegrationTaskKey, + params: { + channelId: string; + issueId: string; + messageId: string; + url: string; + variables?: Omit< + AttachmentLinkDiscordMutationVariables, + "channelId" | "issueId" | "messageId" | "url" + >; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkDiscord( + params.channelId, + params.issueId, + params.messageId, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Discord Message", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Channel ID", text: params.channelId }, + { label: "Message ID", text: params.messageId }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkFront( + key: IntegrationTaskKey, + params: { + conversationId: string; + issueId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkFront( + params.conversationId, + params.issueId, + params.variables + ); + return serializeLinearOutput(payload); + }, + { + name: "Link Front Conversation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Conversation ID", text: params.conversationId }, + ], + } + ); + } + + attachmentLinkIntercom( + key: IntegrationTaskKey, + params: { + conversationId: string; + issueId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkIntercom( + params.conversationId, + params.issueId, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Intercom Conversation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Conversation ID", text: params.conversationId }, + ], + } + ); + } + + attachmentLinkJiraIssue( + key: IntegrationTaskKey, + params: { + issueId: string; + jiraIssueId: string; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkJiraIssue(params.issueId, params.jiraIssueId); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Jira Issue", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Jira Issue ID", text: params.jiraIssueId }, + ], + } + ); + } + + attachmentLinkSlack( + key: IntegrationTaskKey, + params: { + channel: string; + issueId: string; + latest: string; + url: string; + variables?: Omit< + AttachmentLinkSlackMutationVariables, + "channel" | "issueId" | "latest" | "url" + >; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkSlack( + params.channel, + params.issueId, + params.latest, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Slack Message", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Channel", text: params.channel }, + { label: "Latest", text: params.latest }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkURL( + key: IntegrationTaskKey, + params: { + issueId: string; + url: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkURL( + params.issueId, + params.url, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link URL", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "URL", text: params.url }, + ], + } + ); + } + + attachmentLinkZendesk( + key: IntegrationTaskKey, + params: { + issueId: string; + ticketId: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.attachmentLinkZendesk( + params.issueId, + params.ticketId, + params.variables + ); + return serializeLinearOutput(await payload.attachment); + }, + { + name: "Link Zendesk Ticket", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Ticket ID", text: params.ticketId }, + ], + } + ); + } + comment(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { return this.runTask( key, @@ -277,446 +568,1271 @@ export class Linear implements TriggerIntegration { { name: "Create Comment", params, - properties: [ - { label: "Issue ID", text: params.issueId }, - { label: "Body", text: params.body ?? "" }, - ], + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Body", text: params.body ?? "" }, + ], + } + ); + } + + deleteComment(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteComment(params.id), { + name: "Delete Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + }); + } + + updateComment( + key: IntegrationTaskKey, + params: { id: string; input: CommentUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateComment(params.id, params.input); + return serializeLinearOutput(await payload.comment); + }, + { + name: "Update Comment", + params, + properties: [{ label: "Comment ID", text: params.id }], + } + ); + } + + archiveCycle( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveCycle(params.id); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Cycle", + params, + properties: [{ label: "Cycle ID", text: params.id }], + } + ); + } + + createCycle( + key: IntegrationTaskKey, + params: CycleCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createCycle(params); + return serializeLinearOutput(await payload.cycle); + }, + { + name: "Create Cycle", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "Start at", text: params.startsAt.toISOString() }, + { label: "Ends at", text: params.endsAt.toISOString() }, + ], + } + ); + } + + // deleteCycle() does not exist + + updateCycle( + key: IntegrationTaskKey, + params: { id: string; input: CycleUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateCycle(params.id, params.input); + return serializeLinearOutput(await payload.cycle); + }, + { + name: "Update Cycle", + params, + properties: [{ label: "Cycle ID", text: params.id }], + } + ); + } + + document(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.document(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Document", + params, + properties: [{ label: "Document ID", text: params.id }], + } + ); + } + + documents( + key: IntegrationTaskKey, + params: DocumentsQueryVariables + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.documents(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Documents", + params, + properties: queryProperties(params), + } + ); + } + + createDocument( + key: IntegrationTaskKey, + params: DocumentCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createDocument(params); + return serializeLinearOutput(await payload.document); + }, + { + name: "Create Document", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Title", text: params.title }, + ], + } + ); + } + + searchDocuments( + key: IntegrationTaskKey, + params: { + term: string; + variables?: SearchDocumentsQueryVariables; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchDocuments(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Documents", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + favorite(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.favorite(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Favorite", + params, + properties: [{ label: "Favorite ID", text: params.id }], + } + ); + } + + favorites( + key: IntegrationTaskKey, + params: FavoritesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.favorites(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Favorites", + params, + properties: queryProperties(params), + } + ); + } + + createFavorite( + key: IntegrationTaskKey, + params: FavoriteCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createFavorite(params); + return serializeLinearOutput(await payload.favorite); + }, + { + name: "Create Favorite", + params, + } + ); + } + + issue(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issue(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + issues( + key: IntegrationTaskKey, + params: IssuesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issues(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Issues", + params, + properties: queryProperties(params), + } + ); + } + + archiveIssue( + key: IntegrationTaskKey, + params: { + id: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveIssue(params.id, params.variables); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + createIssue( + key: IntegrationTaskKey, + params: IssueCreateInput & { title: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssue(params); + return serializeLinearOutput(await payload.issue); + }, + { + name: "Create Issue", + params, + properties: [ + { label: "Team ID", text: params.teamId }, + { label: "Title", text: params.title }, + ], + } + ); + } + + deleteIssue( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.deleteIssue(params.id); + return serializeLinearOutput(await payload.entity); + }, + { + name: "Delete Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + searchIssues( + key: IntegrationTaskKey, + params: { + term: string; + variables?: SearchIssuesQueryVariables; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchIssues(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Issues", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + updateIssue( + key: IntegrationTaskKey, + params: { id: string; input: IssueUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateIssue(params.id, params.input); + return serializeLinearOutput(await payload.issue); + }, + { + name: "Update Issue", + params, + properties: [{ label: "Issue ID", text: params.id }], + } + ); + } + + issueLabel(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issueLabel(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get IssueLabel", + params, + properties: [{ label: "IssueLabel ID", text: params.id }], + } + ); + } + + issueLabels( + key: IntegrationTaskKey, + params: IssueLabelsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issueLabels(params); + return serializeLinearOutput(edges); + }, + { + name: "Get IssueLabels", + params, + properties: queryProperties(params), + } + ); + } + + createIssueLabel( + key: IntegrationTaskKey, + params: IssueLabelCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssueLabel(params); + return serializeLinearOutput(await payload.issueLabel); + }, + { + name: "Create IssueLabel", + params, + properties: [{ label: "Label name", text: params.name }], + } + ); + } + + deleteIssueLabel(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteIssueLabel(params.id), { + name: "Delete IssueLabel", + params, + properties: [{ label: "Label ID", text: params.id }], + }); + } + + updateIssueLabel( + key: IntegrationTaskKey, + params: { id: string; input: IssueLabelUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateIssueLabel(params.id, params.input); + return serializeLinearOutput(await payload.issueLabel); + }, + { + name: "Update IssueLabel", + params, + properties: [{ label: "Label ID", text: params.id }], + } + ); + } + + issuePriorityValues(key: IntegrationTaskKey): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issuePriorityValues; + return serializeLinearOutput(entity); + }, + { + name: "Get Issue Priority Values", + } + ); + } + + issueRelation(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.issueRelation(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get IssueRelation", + params, + properties: [{ label: "IssueRelation ID", text: params.id }], + } + ); + } + + issueRelations( + key: IntegrationTaskKey, + params: IssueRelationsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.issueRelations(params); + return serializeLinearOutput(edges); + }, + { + name: "Get IssueRelations", + params, + properties: queryProperties(params), + } + ); + } + + createIssueRelation( + key: IntegrationTaskKey, + params: IssueRelationCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createIssueRelation(params); + return serializeLinearOutput(await payload.issueRelation); + }, + { + name: "Create IssueRelation", + params, + properties: [ + { label: "Issue ID", text: params.issueId }, + { label: "Related Issue ID", text: params.relatedIssueId }, + { label: "Relation Type", text: params.type }, + ], + } + ); + } + + notification(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.notification(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Notification", + params, + properties: [{ label: "Notification ID", text: params.id }], + } + ); + } + + notifications( + key: IntegrationTaskKey, + params: NotificationsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.notifications(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Notifications", + params, + properties: queryProperties(params), + } + ); + } + + createNotificationSubscription( + key: IntegrationTaskKey, + params: { + input: NotificationSubscriptionCreateInput; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createNotificationSubscription(params.input); + return serializeLinearOutput(payload); + }, + { + name: "Create Notification Subscription", + params, + } + ); + } + + archiveNotification( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveNotification(params.id); + return serializeLinearOutput(payload); + }, + { + name: "Archive Notification", + params, + properties: [{ label: "Notification ID", text: params.id }], + } + ); + } + + organization(key: IntegrationTaskKey): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.organization; + return serializeLinearOutput(entity); + }, + { + name: "Get Viewer's Organization", + } + ); + } + + createOrganizationFromOnboarding( + key: IntegrationTaskKey, + params: { + input: CreateOrganizationInput; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createOrganizationFromOnboarding( + params.input, + params.variables + ); + return serializeLinearOutput(await payload.organization); + }, + { + name: "Create Organization", + params, + properties: [ + { label: "Name", text: params.input.name }, + { label: "URL Key", text: params.input.urlKey }, + ], + } + ); + } + + createOrganizationInvite( + key: IntegrationTaskKey, + params: { + input: OrganizationInviteCreateInput; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createOrganizationInvite(params.input); + return serializeLinearOutput(await payload.organizationInvite); + }, + { + name: "Create Organization Invite", + params, + properties: [{ label: "Invitee Email", text: params.input.email }], + } + ); + } + + project(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.project(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + projects( + key: IntegrationTaskKey, + params: ProjectsQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projects(params); + return serializeLinearOutput(edges); + }, + { + name: "Get Projects", + params, + properties: queryProperties(params), + } + ); + } + + archiveProject( + key: IntegrationTaskKey, + params: { + id: string; + variables?: Omit; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.archiveProject(params.id, params.variables); + return serializeLinearOutput({ + ...payload, + entity: await payload.entity, + }); + }, + { + name: "Archive Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + createProject( + key: IntegrationTaskKey, + params: ProjectCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProject(params); + return serializeLinearOutput(await payload.project); + }, + { + name: "Create Project", + params, + properties: [ + { label: "Team IDs", text: params.teamIds.join(", ") }, + { label: "Project name", text: params.name }, + ], + } + ); + } + + deleteProject( + key: IntegrationTaskKey, + params: { id: string } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.deleteProject(params.id); + return serializeLinearOutput(await payload.entity); + }, + { + name: "Delete Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + searchProjects( + key: IntegrationTaskKey, + params: { + term: string; + variables?: SearchProjectsQueryVariables; + } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.searchProjects(params.term, params.variables); + return serializeLinearOutput(payload); + }, + { + name: "Search Projects", + params, + properties: [{ label: "Search Term", text: params.term }], + } + ); + } + + updateProject( + key: IntegrationTaskKey, + params: { id: string; input: ProjectUpdateInput } + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.updateProject(params.id, params.input); + return serializeLinearOutput(await payload.project); + }, + { + name: "Update Project", + params, + properties: [{ label: "Project ID", text: params.id }], + } + ); + } + + projectLink(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.projectLink(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get ProjectLink", + params, + properties: [{ label: "ProjectLink ID", text: params.id }], + } + ); + } + + projectLinks( + key: IntegrationTaskKey, + params: ProjectLinksQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projectLinks(params); + return serializeLinearOutput(edges); + }, + { + name: "Get ProjectLinks", + params, + properties: queryProperties(params), + } + ); + } + + createProjectLink( + key: IntegrationTaskKey, + params: ProjectLinkCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectLink(params); + return serializeLinearOutput(await payload.projectLink); + }, + { + name: "Create ProjectLink", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Link Label", text: params.label }, + { label: "Link URL", text: params.url }, + ], + } + ); + } + + createProjectMilestone( + key: IntegrationTaskKey, + params: ProjectMilestoneCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectMilestone(params); + return serializeLinearOutput(await payload.projectMilestone); + }, + { + name: "Create ProjectMilestone", + params, + properties: [ + { label: "Project ID", text: params.projectId }, + { label: "Milestone Name", text: params.name }, + ], + } + ); + } + + projectUpdate(key: IntegrationTaskKey, params: { id: string }): LinearReturnType { + return this.runTask( + key, + async (client) => { + const entity = await client.projectUpdate(params.id); + return serializeLinearOutput(entity); + }, + { + name: "Get ProjectUpdate", + params, + properties: [{ label: "ProjectUpdate ID", text: params.id }], + } + ); + } + + projectUpdates( + key: IntegrationTaskKey, + params: ProjectUpdatesQueryVariables = {} + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const edges = await client.projectUpdates(params); + return serializeLinearOutput(edges); + }, + { + name: "Get ProjectUpdates", + params, + properties: queryProperties(params), + } + ); + } + + createProjectUpdate( + key: IntegrationTaskKey, + params: ProjectUpdateCreateInput + ): LinearReturnType { + return this.runTask( + key, + async (client) => { + const payload = await client.createProjectUpdate(params); + return serializeLinearOutput(await payload.projectUpdate); + }, + { + name: "Create ProjectUpdate", + params, + properties: [{ label: "Project ID", text: params.projectId }], } ); } - deleteComment(key: IntegrationTaskKey, params: { id: string }): Promise { - return this.runTask(key, (client) => client.deleteComment(params.id), { - name: "Delete Comment", + deleteProjectUpdate(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteProjectUpdate(params.id), { + name: "Delete ProjectUpdate", params, - properties: [{ label: "Comment ID", text: params.id }], + properties: [{ label: "ProjectUpdate ID", text: params.id }], }); } - updateComment( + updateProjectUpdate( key: IntegrationTaskKey, - params: { id: string; input: CommentUpdateInput } - ): LinearReturnType { + params: { id: string; input: ProjectUpdateUpdateInput } + ): LinearReturnType { return this.runTask( key, async (client) => { - const payload = await client.updateComment(params.id, params.input); - return serializeLinearOutput(await payload.comment); + const payload = await client.updateProjectUpdate(params.id, params.input); + return serializeLinearOutput(await payload.projectUpdate); }, { - name: "Update Comment", + name: "Update ProjectUpdate", params, - properties: [{ label: "Comment ID", text: params.id }], + properties: [{ label: "ProjectUpdate ID", text: params.id }], } ); } - createCycle( + createReaction( key: IntegrationTaskKey, - params: CycleCreateInput - ): LinearReturnType { + params: ReactionCreateInput & { emoji: string } + ): LinearReturnType { return this.runTask( key, async (client) => { - const payload = await client.createCycle(params); - return serializeLinearOutput(await payload.cycle); + const payload = await client.createReaction(params); + return serializeLinearOutput(await payload.reaction); }, { - name: "Create Cycle", + name: "Create Reaction", params, properties: [ - { label: "Team ID", text: params.teamId }, - { label: "Start at", text: params.startsAt.toISOString() }, - { label: "Ends at", text: params.endsAt.toISOString() }, + { label: "Comment ID", text: params.commentId ?? "N/A" }, + { label: "Issue ID", text: params.issueId ?? "N/A" }, + { label: "Emoji", text: params.emoji }, ], } ); } - // deleteCycle() does not exist + deleteReaction(key: IntegrationTaskKey, params: { id: string }): Promise { + return this.runTask(key, (client) => client.deleteReaction(params.id), { + name: "Delete Reaction", + params, + properties: [{ label: "Reaction ID", text: params.id }], + }); + } - updateCycle( - key: IntegrationTaskKey, - params: { id: string; input: CycleUpdateInput } - ): LinearReturnType { + template(key: IntegrationTaskKey, params: { id: string }): LinearReturnType