CLI create-integration templates and shared configs (#511)
* Add tsup config package * Add integration tsconfig to extend from * Return correct version via cli -v * Make create-integration use templates * Add changeset * Switch to liquidjs templates --------- Co-authored-by: Matt Aitken <matt@mattaitken.com>
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"@trigger.dev/cli": patch
|
||||
---
|
||||
|
||||
Improve create-integration output. Use templates and shared configs.
|
||||
@@ -3,6 +3,8 @@
|
||||
"compilerOptions": {
|
||||
"lib": ["DOM", "DOM.Iterable", "ES2019"],
|
||||
"paths": {
|
||||
"@trigger.dev/tsup/*": ["../../config-packages/tsup/src/*"],
|
||||
"@trigger.dev/tsup": ["../../config-packages/tsup/src/index"],
|
||||
"@trigger.dev/sdk/*": ["../../packages/trigger-sdk/src/*"],
|
||||
"@trigger.dev/sdk": ["../../packages/trigger-sdk/src/index"],
|
||||
"@trigger.dev/integration-kit/*": ["../../packages/integration-kit/src/*"],
|
||||
|
||||
@@ -0,0 +1,9 @@
|
||||
{
|
||||
"name": "@trigger.dev/tsup",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"license": "MIT",
|
||||
"devDependencies": {
|
||||
"tsup": "7.1.x"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
export { defineConfig } from "tsup";
|
||||
export { deepMergeOptions } from "./utils";
|
||||
export { options as integrationOptions } from "./integration";
|
||||
@@ -0,0 +1,22 @@
|
||||
import { Options, defineConfig } from "tsup";
|
||||
|
||||
export const options: Options = {
|
||||
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"],
|
||||
};
|
||||
|
||||
export default defineConfig(options);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { Options } from "tsup";
|
||||
|
||||
export const deepMergeOptions = deepMergeRecords<Options>;
|
||||
|
||||
function deepMergeRecords<TRecord extends Record<any, any>>(...options: TRecord[]): TRecord {
|
||||
const result = {} as TRecord;
|
||||
|
||||
for (const option of options) {
|
||||
for (const key in option) {
|
||||
if (option.hasOwnProperty(key)) {
|
||||
const optionValue = option[key];
|
||||
const existingValue = result[key];
|
||||
|
||||
if (
|
||||
existingValue &&
|
||||
typeof existingValue === "object" &&
|
||||
typeof optionValue === "object" &&
|
||||
!Array.isArray(existingValue) &&
|
||||
!Array.isArray(optionValue) &&
|
||||
existingValue !== null &&
|
||||
optionValue !== null
|
||||
) {
|
||||
result[key] = deepMergeRecords(existingValue, optionValue);
|
||||
} else {
|
||||
result[key] = optionValue;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -68,6 +68,7 @@
|
||||
"execa": "^7.0.0",
|
||||
"gradient-string": "^2.0.2",
|
||||
"inquirer": "^9.1.4",
|
||||
"liquidjs": "^10.9.2",
|
||||
"localtunnel": "^2.0.2",
|
||||
"mock-fs": "^5.2.0",
|
||||
"nanoid": "^4.0.2",
|
||||
|
||||
@@ -14,7 +14,10 @@ import { checkApiKeyIsDevServer } from "../utils/getApiKeyType";
|
||||
|
||||
export const program = new Command();
|
||||
|
||||
program.name(COMMAND_NAME).description("The Trigger.dev CLI").version("0.0.1");
|
||||
program
|
||||
.name(COMMAND_NAME)
|
||||
.description("The Trigger.dev CLI")
|
||||
.version(getVersion(), "-v, --version", "Display the version number");
|
||||
|
||||
program
|
||||
.command("init")
|
||||
|
||||
@@ -9,7 +9,8 @@ import { generateIntegrationFiles } from "../utils/generateIntegrationFiles";
|
||||
import { getPackageName } from "../utils/getPackagName";
|
||||
import { installDependencies } from "../utils/installDependencies";
|
||||
import { logger } from "../utils/logger";
|
||||
import { resolvePath } from "../utils/parseNameAndPath";
|
||||
import { relativePath, resolvePath } from "../utils/parseNameAndPath";
|
||||
import { createIntegrationFileFromTemplate } from "../utils/createIntegrationFileFromTemplate";
|
||||
|
||||
const CLIOptionsSchema = z.object({
|
||||
packageName: z.string().optional(),
|
||||
@@ -91,108 +92,107 @@ export async function createIntegrationCommand(path: string, cliOptions: any) {
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
// Create the package.json
|
||||
const packageJson = {
|
||||
name: resolvedOptions.packageName,
|
||||
version: "0.0.1",
|
||||
description: `Trigger.dev integration for ${resolvedOptions.sdkPackage}`,
|
||||
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: {
|
||||
[latestVersion.name]: `^${latestVersion.version}`,
|
||||
[sdkVersion.name]: sdkVersion.version,
|
||||
[integrationKitVersion.name]: integrationKitVersion.version,
|
||||
},
|
||||
engines: {
|
||||
node: ">=16.8.0",
|
||||
},
|
||||
const integrationVersion = await getInternalOrExternalPackageVersion({
|
||||
path: "integrations/github",
|
||||
packageName: "@trigger.dev/github",
|
||||
tag: "latest",
|
||||
monorepoPath: triggerMonorepoPath,
|
||||
prependWorkspace: false,
|
||||
});
|
||||
|
||||
if (!integrationVersion) {
|
||||
logger.error(
|
||||
`Could not find the latest version of @trigger.dev/github. Please try again later.`
|
||||
);
|
||||
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
const baseVariables = {
|
||||
packageName: resolvedOptions.packageName,
|
||||
sdkPackage: resolvedOptions.sdkPackage,
|
||||
integrationVersion: integrationVersion,
|
||||
latestVersion: latestVersion,
|
||||
sdkVersion: sdkVersion,
|
||||
integrationKitVersion: integrationKitVersion,
|
||||
triggerMonorepoPath,
|
||||
};
|
||||
|
||||
await createFileInPath(resolvedPath, "package.json", JSON.stringify(packageJson, null, 2));
|
||||
const getOutputPath = (relativePath: string) => pathModule.join(resolvedPath, relativePath);
|
||||
|
||||
// Create the tsconfig.json
|
||||
const tsconfigJson = {
|
||||
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",
|
||||
const miscIntegrationFiles = [
|
||||
{
|
||||
relativeTemplatePath: "package.json.j2",
|
||||
outputPath: getOutputPath("package.json"),
|
||||
},
|
||||
include: ["./src/**/*.ts", "tsup.config.ts"],
|
||||
exclude: ["node_modules"],
|
||||
};
|
||||
|
||||
await createFileInPath(resolvedPath, "tsconfig.json", JSON.stringify(tsconfigJson, null, 2));
|
||||
|
||||
const readme = `
|
||||
# ${resolvedOptions.packageName}
|
||||
`;
|
||||
|
||||
await createFileInPath(resolvedPath, "README.md", readme);
|
||||
|
||||
// Create the tsup.config.ts
|
||||
const tsupConfig = `
|
||||
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",
|
||||
{
|
||||
// use `tsc --showConfig` to update external tsconfig
|
||||
relativeTemplatePath: `tsconfig-${triggerMonorepoPath ? "internal" : "external"}.json.j2`,
|
||||
outputPath: getOutputPath("tsconfig.json"),
|
||||
},
|
||||
esbuildPlugins: [],
|
||||
external: ["http", "https", "util", "events", "tty", "os", "timers"],
|
||||
},
|
||||
]);
|
||||
{
|
||||
relativeTemplatePath: `tsup.config-${triggerMonorepoPath ? "internal" : "external"}.js.j2`,
|
||||
outputPath: getOutputPath("tsup.config.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "README.md.j2",
|
||||
outputPath: getOutputPath("README.md"),
|
||||
},
|
||||
];
|
||||
|
||||
`;
|
||||
|
||||
await createFileInPath(resolvedPath, "tsup.config.ts", tsupConfig);
|
||||
await createIntegrationFiles(miscIntegrationFiles, baseVariables);
|
||||
|
||||
// create src/*
|
||||
if (resolvedOptions.skipGeneratingCode) {
|
||||
await createFileInPath(resolvedPath, "src/index.ts", "export {}");
|
||||
const getSrcOutputPath = (relativePath: string) =>
|
||||
getOutputPath(pathModule.join("src", relativePath));
|
||||
|
||||
const srcIntegrationFiles = [
|
||||
{
|
||||
relativeTemplatePath: pathModule.join("payload-examples", "index.js.j2"),
|
||||
outputPath: getSrcOutputPath(pathModule.join("payload-examples", "index.ts")),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "events.js.j2",
|
||||
outputPath: getSrcOutputPath("events.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "index.js.j2",
|
||||
outputPath: getSrcOutputPath("index.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "models.js.j2",
|
||||
outputPath: getSrcOutputPath("models.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "schemas.js.j2",
|
||||
outputPath: getSrcOutputPath("schemas.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "types.js.j2",
|
||||
outputPath: getSrcOutputPath("types.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "utils.js.j2",
|
||||
outputPath: getSrcOutputPath("utils.ts"),
|
||||
},
|
||||
{
|
||||
relativeTemplatePath: "webhooks.js.j2",
|
||||
outputPath: getSrcOutputPath("webhooks.ts"),
|
||||
},
|
||||
];
|
||||
|
||||
const validIdentifier = pathModule
|
||||
.basename(path)
|
||||
.replace(/[^a-zA-Z0-9]+/g, "")
|
||||
.replace(/^[0-9]+/g, "");
|
||||
|
||||
await createIntegrationFiles(srcIntegrationFiles, {
|
||||
...baseVariables,
|
||||
apiKeyPropertyName: "apiKey", // TODO: prompt for this
|
||||
authMethod: resolvedOptions.authMethod,
|
||||
identifier: validIdentifier.length ? validIdentifier : "packageName",
|
||||
});
|
||||
} else {
|
||||
await attemptToGenerateIntegrationFiles(pathModule.join(resolvedPath, "src"), resolvedOptions);
|
||||
}
|
||||
@@ -295,8 +295,9 @@ const resolveOptionsWithPrompts = async (
|
||||
resolvedOptions.skipGeneratingCode = true;
|
||||
}
|
||||
|
||||
resolvedOptions.authMethod = await promptAuthMethod();
|
||||
|
||||
if (!resolvedOptions.skipGeneratingCode) {
|
||||
resolvedOptions.authMethod = await promptAuthMethod();
|
||||
resolvedOptions.extraInfo = await promptExtraInfo();
|
||||
}
|
||||
} catch (err) {
|
||||
@@ -448,11 +449,13 @@ async function getInternalOrExternalPackageVersion({
|
||||
tag,
|
||||
path,
|
||||
monorepoPath,
|
||||
prependWorkspace = true,
|
||||
}: {
|
||||
packageName: string;
|
||||
tag: string;
|
||||
path: string;
|
||||
monorepoPath?: string;
|
||||
prependWorkspace?: boolean;
|
||||
}): Promise<{ name: string; version: string } | undefined> {
|
||||
if (!monorepoPath) {
|
||||
return await getLatestPackageVersion(packageName, tag);
|
||||
@@ -470,7 +473,7 @@ async function getInternalOrExternalPackageVersion({
|
||||
|
||||
return {
|
||||
name: packageJson.name,
|
||||
version: `workspace:^${packageJson.version}`,
|
||||
version: `${prependWorkspace ? "workspace:^" : ""}${packageJson.version}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -550,3 +553,26 @@ async function updateJobCatalogWithNewIntegration(
|
||||
};
|
||||
await writeJSONFile(tsConfigPath, newTsConfig);
|
||||
}
|
||||
|
||||
const createIntegrationFiles = async (
|
||||
files: {
|
||||
relativeTemplatePath: string;
|
||||
outputPath: string;
|
||||
}[],
|
||||
variables?: Record<string, any>
|
||||
) => {
|
||||
for (const file of files) {
|
||||
const result = await createIntegrationFileFromTemplate({ ...file, variables });
|
||||
handleCreateResult(file.outputPath, result);
|
||||
}
|
||||
};
|
||||
|
||||
const handleCreateResult = (
|
||||
outputPath: string,
|
||||
result: Awaited<ReturnType<typeof createIntegrationFileFromTemplate>>
|
||||
) => {
|
||||
if (!result.success) {
|
||||
throw new Error(`Failed to create ${pathModule.basename(outputPath)}: ${result.error}`);
|
||||
}
|
||||
logger.success(`✔ Created ${pathModule.basename(outputPath)} at ${relativePath(outputPath)}`);
|
||||
};
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
# {{ packageName }}
|
||||
@@ -0,0 +1,118 @@
|
||||
import { EventSpecification } from "@trigger.dev/sdk";
|
||||
import { CommentEvent, IssueEvent } from "./schemas";
|
||||
import { Get{{ identifier | capitalize }}Payload } from "./types";
|
||||
import {
|
||||
commentCreated,
|
||||
commentRemoved,
|
||||
commentUpdated,
|
||||
issueCreated,
|
||||
issueRemoved,
|
||||
issueUpdated,
|
||||
} from "./payload-examples";
|
||||
import { onCommentProperties, onIssueProperties, updatedFromProperties } from "./utils";
|
||||
|
||||
export const onComment: EventSpecification<Get{{ identifier | capitalize }}Payload<CommentEvent>> = {
|
||||
name: "Comment",
|
||||
title: "On Comment",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [commentCreated, commentRemoved, commentUpdated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<CommentEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
...onCommentProperties(payload),
|
||||
...updatedFromProperties(payload),
|
||||
],
|
||||
};
|
||||
|
||||
export const onCommentCreated: EventSpecification<Get{{ identifier | capitalize }}Payload<CommentEvent, "create">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [commentCreated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<CommentEvent, "create">,
|
||||
runProperties: (payload) => onCommentProperties(payload),
|
||||
};
|
||||
|
||||
export const onCommentRemoved: EventSpecification<Get{{ identifier | capitalize }}Payload<CommentEvent, "remove">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [commentRemoved],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<CommentEvent, "remove">,
|
||||
runProperties: (payload) => onCommentProperties(payload),
|
||||
};
|
||||
|
||||
export const onCommentUpdated: EventSpecification<Get{{ identifier | capitalize }}Payload<CommentEvent, "update">> = {
|
||||
name: "Comment",
|
||||
title: "On Comment Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [commentUpdated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<CommentEvent, "update">,
|
||||
runProperties: (payload) => [...onCommentProperties(payload), ...updatedFromProperties(payload)],
|
||||
};
|
||||
|
||||
export const onIssue: EventSpecification<Get{{ identifier | capitalize }}Payload<IssueEvent>> = {
|
||||
name: "Issue",
|
||||
title: "On Issue",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
examples: [issueCreated, issueRemoved, issueUpdated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<IssueEvent>,
|
||||
runProperties: (payload) => [
|
||||
{ label: "Event action", text: payload.action },
|
||||
...onIssueProperties(payload),
|
||||
...updatedFromProperties(payload),
|
||||
],
|
||||
};
|
||||
|
||||
export const onIssueCreated: EventSpecification<Get{{ identifier | capitalize }}Payload<IssueEvent, "create">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Created",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["create"],
|
||||
},
|
||||
examples: [issueCreated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<IssueEvent, "create">,
|
||||
runProperties: (payload) => onIssueProperties(payload),
|
||||
};
|
||||
|
||||
export const onIssueRemoved: EventSpecification<Get{{ identifier | capitalize }}Payload<IssueEvent, "remove">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Removed",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["remove"],
|
||||
},
|
||||
examples: [issueRemoved],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<IssueEvent, "remove">,
|
||||
runProperties: (payload) => onIssueProperties(payload),
|
||||
};
|
||||
|
||||
export const onIssueUpdated: EventSpecification<Get{{ identifier | capitalize }}Payload<IssueEvent, "update">> = {
|
||||
name: "Issue",
|
||||
title: "On Issue Updated",
|
||||
source: "linear.app",
|
||||
icon: "linear",
|
||||
filter: {
|
||||
action: ["update"],
|
||||
},
|
||||
examples: [issueUpdated],
|
||||
parsePayload: (payload) => payload as Get{{ identifier | capitalize }}Payload<IssueEvent, "update">,
|
||||
runProperties: (payload) => [...onIssueProperties(payload), ...updatedFromProperties(payload)],
|
||||
};
|
||||
@@ -0,0 +1,244 @@
|
||||
import {
|
||||
TriggerIntegration,
|
||||
RunTaskOptions,
|
||||
IO,
|
||||
IOTask,
|
||||
IntegrationTaskKey,
|
||||
RunTaskErrorCallback,
|
||||
Json,
|
||||
retry,
|
||||
ConnectionAuth,
|
||||
Prettify,
|
||||
} from "@trigger.dev/sdk";
|
||||
import {{ identifier | capitalize }}Client from "{{ sdkPackage }}";
|
||||
|
||||
import * as events from "./events";
|
||||
import { {{ identifier | capitalize }}ReturnType, Serialized{{ identifier | capitalize }}Output } from "./types";
|
||||
import { TriggerParams, Webhooks, createTrigger, createWebhookEventSource } from "./webhooks";
|
||||
import { Models } from "./models";
|
||||
|
||||
export type {{ identifier | capitalize }}IntegrationOptions = {
|
||||
id: string;
|
||||
{{ apiKeyPropertyName }}: string;
|
||||
};
|
||||
|
||||
export type {{ identifier | capitalize }}RunTask = InstanceType<typeof {{ identifier | capitalize }}>["runTask"];
|
||||
|
||||
export class {{ identifier | capitalize }}{{ " " }} implements TriggerIntegration {
|
||||
private _options: {{ identifier | capitalize }}IntegrationOptions;
|
||||
private _client?: any;
|
||||
private _io?: IO;
|
||||
private _connectionKey?: string;
|
||||
|
||||
constructor(private options: {{ identifier | capitalize }}IntegrationOptions) {
|
||||
if (Object.keys(options).includes("{{ apiKeyPropertyName }}") && !options.{{ apiKeyPropertyName }}) {
|
||||
throw `Can't create {{ identifier | capitalize }} integration (${options.id}) as {{ apiKeyPropertyName }} was undefined`;
|
||||
}
|
||||
|
||||
this._options = options;
|
||||
}
|
||||
|
||||
get authSource() {
|
||||
{% case authMethod %}
|
||||
{% when "api-key" %}
|
||||
return "LOCAL" as const;
|
||||
{% when "oauth" %}
|
||||
return "HOSTED" as const;
|
||||
{% when "both-methods" %}
|
||||
return this._options.{{ apiKeyPropertyName }} ? "LOCAL" : "HOSTED";
|
||||
{% endcase %}
|
||||
}
|
||||
|
||||
get id() {
|
||||
return this.options.id;
|
||||
}
|
||||
|
||||
get metadata() {
|
||||
return { id: "{{ identifier }}", name: "{{ identifier | capitalize }}" };
|
||||
}
|
||||
|
||||
get source() {
|
||||
return createWebhookEventSource(this);
|
||||
}
|
||||
|
||||
cloneForRun(io: IO, connectionKey: string, auth?: ConnectionAuth) {
|
||||
const {{ identifier }} = new {{ identifier | capitalize }}(this._options);
|
||||
{{ identifier }}._io = io;
|
||||
{{ identifier }}._connectionKey = connectionKey;
|
||||
{{ identifier }}._client = this.createClient(auth);
|
||||
return {{ identifier }};
|
||||
}
|
||||
|
||||
createClient(auth?: ConnectionAuth) {
|
||||
// oauth
|
||||
if (auth) {
|
||||
return new {{ identifier | capitalize }}Client({
|
||||
auth: auth.accessToken,
|
||||
});
|
||||
}
|
||||
|
||||
// apiKey auth
|
||||
if (this._options.{{ apiKeyPropertyName }}) {
|
||||
return new {{ identifier | capitalize }}Client({
|
||||
apiKey: this._options.{{ apiKeyPropertyName }},
|
||||
});
|
||||
}
|
||||
|
||||
throw new Error("No auth");
|
||||
}
|
||||
|
||||
runTask<T, TResult extends Json<T> | void>(
|
||||
key: IntegrationTaskKey,
|
||||
callback: (client: {{ identifier | capitalize }}Client, task: IOTask, io: IO) => Promise<TResult>,
|
||||
options?: RunTaskOptions,
|
||||
errorCallback?: RunTaskErrorCallback
|
||||
): Promise<TResult> {
|
||||
if (!this._io) throw new Error("No IO");
|
||||
if (!this._connectionKey) throw new Error("No connection key");
|
||||
|
||||
return this._io.runTask<TResult>(
|
||||
key,
|
||||
(task, io) => {
|
||||
if (!this._client) throw new Error("No client");
|
||||
return callback(this._client, task, io);
|
||||
},
|
||||
{
|
||||
icon: "{{ identifier }}",
|
||||
retry: retry.standardBackoff,
|
||||
...(options ?? {}),
|
||||
connectionKey: this._connectionKey,
|
||||
},
|
||||
errorCallback ?? onError
|
||||
);
|
||||
}
|
||||
|
||||
// top-level task
|
||||
|
||||
request<T = any>(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
route: string | URL;
|
||||
options: Parameters<{{ identifier | capitalize }}Client["request"]>[1];
|
||||
}
|
||||
): {{ identifier | capitalize }}ReturnType<T> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client) => {
|
||||
const response = await client.request(params.route, params.options);
|
||||
|
||||
return response.json();
|
||||
},
|
||||
{
|
||||
name: "Send Request",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Route", text: params.route.toString() },
|
||||
...(params.options.method ? [{ label: "Method", text: params.options.method }] : []),
|
||||
],
|
||||
callback: { enabled: true },
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
// nested tasks
|
||||
|
||||
get models() {
|
||||
return new Models(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
// events
|
||||
|
||||
onComment(params: TriggerParams = {}) {
|
||||
return createTrigger(this.source, events.onComment, params);
|
||||
}
|
||||
|
||||
onCommentCreated(params: TriggerParams = {}) {
|
||||
return createTrigger(this.source, events.onCommentCreated, params);
|
||||
}
|
||||
|
||||
onCommentRemoved(params: TriggerParams = {}) {
|
||||
return createTrigger(this.source, events.onCommentRemoved, params);
|
||||
}
|
||||
|
||||
onCommentUpdated(params: TriggerParams = {}) {
|
||||
return createTrigger(this.source, events.onCommentUpdated, params);
|
||||
}
|
||||
|
||||
// triggers (webhooks)
|
||||
|
||||
// private, just here to keep webhook logic in a separate file
|
||||
get #webhooks() {
|
||||
return new Webhooks(this.runTask.bind(this));
|
||||
}
|
||||
|
||||
webhook = this.#webhooks.webhook;
|
||||
webhooks = this.#webhooks.webhooks;
|
||||
|
||||
createWebhook = this.#webhooks.createWebhook;
|
||||
deleteWebhook = this.#webhooks.deleteWebhook;
|
||||
updateWebhook = this.#webhooks.updateWebhook;
|
||||
}
|
||||
|
||||
class {{ identifier | capitalize }}ApiError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
readonly request: Request,
|
||||
readonly response: Response
|
||||
) {
|
||||
super(message);
|
||||
this.name = "{{ identifier | capitalize }}ApiError";
|
||||
}
|
||||
}
|
||||
|
||||
function is{{ identifier | capitalize }}ApiError(error: unknown): error is {{ identifier | capitalize }}ApiError {
|
||||
if (typeof error !== "object" || error === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
const apiError = error as {{ identifier | capitalize }}ApiError;
|
||||
|
||||
return (
|
||||
apiError.name === "{{ identifier | capitalize }}ApiError" &&
|
||||
apiError.request instanceof Request &&
|
||||
apiError.response instanceof Response
|
||||
);
|
||||
}
|
||||
|
||||
function shouldRetry(method: string, status: number) {
|
||||
return status === 429 || (method === "GET" && status >= 500);
|
||||
}
|
||||
|
||||
export function onError(error: unknown): ReturnType<RunTaskErrorCallback> {
|
||||
if (!is{{ identifier | capitalize }}ApiError(error)) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (!shouldRetry(error.request.method, error.response.status)) {
|
||||
return {
|
||||
skipRetrying: true,
|
||||
};
|
||||
}
|
||||
|
||||
const rateLimitRemaining = error.response.headers.get("ratelimit-remaining");
|
||||
const rateLimitReset = error.response.headers.get("ratelimit-reset");
|
||||
|
||||
if (rateLimitRemaining === "0" && rateLimitReset) {
|
||||
const resetDate = new Date(Number(rateLimitReset) * 1000);
|
||||
|
||||
if (!Number.isNaN(resetDate.getTime())) {
|
||||
return {
|
||||
retryAt: resetDate,
|
||||
error,
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const serialize{{ identifier | capitalize }}Output = <T>(obj: T): Prettify<Serialized{{ identifier | capitalize }}Output<T>> => {
|
||||
return JSON.parse(JSON.stringify(obj), (key, value) => {
|
||||
if (typeof value === "function" || key.startsWith("_")) {
|
||||
return undefined;
|
||||
}
|
||||
return value;
|
||||
});
|
||||
};
|
||||
@@ -0,0 +1,79 @@
|
||||
import { IntegrationTaskKey } from "@trigger.dev/sdk";
|
||||
import { Model, ModelVersion } from "{{ sdkPackage }}";
|
||||
|
||||
import { {{ capitalizedIdentifier }}RunTask } from "./index";
|
||||
import { modelProperties } from "./utils";
|
||||
import { {{ capitalizedIdentifier }}ReturnType } from "./types";
|
||||
|
||||
export class Models {
|
||||
constructor(private runTask: {{ capitalizedIdentifier }}RunTask) {}
|
||||
|
||||
get(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
}
|
||||
): {{ capitalizedIdentifier }}ReturnType<Model> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.get(params.model_owner, params.model_name);
|
||||
},
|
||||
{
|
||||
name: "Get Model",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
get versions() {
|
||||
return new Versions(this.runTask);
|
||||
}
|
||||
}
|
||||
|
||||
class Versions {
|
||||
constructor(private runTask: {{ capitalizedIdentifier }}RunTask) {}
|
||||
|
||||
get(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
version_id: string;
|
||||
}
|
||||
): {{ capitalizedIdentifier }}ReturnType<ModelVersion> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.versions.get(params.model_owner, params.model_name, params.version_id);
|
||||
},
|
||||
{
|
||||
name: "Get Model Version",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
list(
|
||||
key: IntegrationTaskKey,
|
||||
params: {
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
}
|
||||
): {{ capitalizedIdentifier }}ReturnType<ModelVersion[]> {
|
||||
return this.runTask(
|
||||
key,
|
||||
(client) => {
|
||||
return client.models.versions.list(params.model_owner, params.model_name);
|
||||
},
|
||||
{
|
||||
name: "List Models",
|
||||
params,
|
||||
properties: modelProperties(params),
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"name": "{{ packageName }}",
|
||||
"version": "{{ integrationVersion.version }}",
|
||||
"description": "Trigger.dev integration for {{ sdkPackage }}",
|
||||
"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": {
|
||||
{% if triggerMonorepoPath %}
|
||||
"@trigger.dev/tsconfig": "workspace:*",
|
||||
"@trigger.dev/tsup": "workspace:*",
|
||||
{% endif %}
|
||||
"@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": {
|
||||
"{{ latestVersion.name }}": "^{{ latestVersion.version }}",
|
||||
"{{ sdkVersion.name }}": "{{ sdkVersion.version }}",
|
||||
"{{ integrationKitVersion.name }}": "{{ integrationKitVersion.version }}",
|
||||
"zod": "3.21.4"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.8.0"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import { EventSpecificationExample } from "@trigger.dev/sdk";
|
||||
|
||||
import CommentCreated from "./CommentCreated.json"
|
||||
import CommentRemoved from "./CommentRemoved.json"
|
||||
import CommentUpdated from "./CommentUpdated.json"
|
||||
import IssueCreated from "./IssueCreated.json"
|
||||
import IssueRemoved from "./IssueRemoved.json"
|
||||
import IssueUpdated from "./IssueUpdated.json"
|
||||
|
||||
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 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,
|
||||
};
|
||||
@@ -0,0 +1,120 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export const WebhookResourceTypeSchema = z.union([
|
||||
z.literal("Comment"),
|
||||
z.literal("Issue"),
|
||||
]);
|
||||
export type WebhookResourceType = z.infer<typeof WebhookResourceTypeSchema>;
|
||||
|
||||
export const WebhookActionTypeSchema = z.union([
|
||||
z.literal("create"),
|
||||
z.literal("remove"),
|
||||
z.literal("update"),
|
||||
]);
|
||||
export type WebhookActionType = z.infer<typeof WebhookActionTypeSchema>;
|
||||
|
||||
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(),
|
||||
name: z.string(),
|
||||
organizationId: z.string(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
teamId: z.string().optional().nullable(),
|
||||
updatedAt: z.coerce.date(),
|
||||
});
|
||||
|
||||
const IssueDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
assignee: z.object({ id: z.string(), name: z.string() }).optional().nullable(),
|
||||
assigneeId: 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(),
|
||||
parentId: z.string().optional().nullable(),
|
||||
previousIdentifiers: z.array(z.string()),
|
||||
priority: z.number(),
|
||||
priorityLabel: z.string(),
|
||||
projectId: z.string().optional().nullable(),
|
||||
sortOrder: z.number(),
|
||||
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(),
|
||||
});
|
||||
|
||||
const CommentDataSchema = z.object({
|
||||
archivedAt: z.coerce.date().optional().nullable(),
|
||||
body: 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()),
|
||||
updatedAt: z.coerce.date(),
|
||||
userId: z.string().optional().nullable(),
|
||||
});
|
||||
|
||||
export const WebhookPayloadBaseSchema = z.object({
|
||||
createdAt: z.coerce.date(),
|
||||
organizationId: z.string().optional().nullable(),
|
||||
url: z.string().url().optional().nullable(),
|
||||
webhookId: z.string(),
|
||||
webhookTimestamp: z.coerce.date(),
|
||||
});
|
||||
|
||||
const CREATE = z.literal("create");
|
||||
const REMOVE = z.literal("remove");
|
||||
const UPDATE = z.literal("update");
|
||||
|
||||
export const CommentEventBaseSchema = WebhookPayloadBaseSchema.extend({
|
||||
type: z.literal("Comment"),
|
||||
data: CommentDataSchema,
|
||||
});
|
||||
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<typeof CommentEventSchema>;
|
||||
|
||||
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<typeof IssueEventSchema>;
|
||||
|
||||
export const WebhookPayloadSchema = z.union([
|
||||
CommentEventSchema,
|
||||
IssueEventSchema,
|
||||
]);
|
||||
|
||||
export type WebhookPayload = z.infer<typeof WebhookPayloadSchema>;
|
||||
@@ -0,0 +1,27 @@
|
||||
{
|
||||
"compilerOptions": {
|
||||
"composite": false,
|
||||
"declaration": false,
|
||||
"declarationMap": false,
|
||||
"esModuleInterop": true,
|
||||
"forceConsistentCasingInFileNames": true,
|
||||
"inlineSources": false,
|
||||
"isolatedModules": true,
|
||||
"moduleResolution": "node",
|
||||
"noUnusedLocals": false,
|
||||
"noUnusedParameters": false,
|
||||
"preserveWatchOutput": true,
|
||||
"skipLibCheck": true,
|
||||
"strict": true,
|
||||
"experimentalDecorators": true,
|
||||
"emitDecoratorMetadata": true,
|
||||
"sourceMap": true,
|
||||
"resolveJsonModule": true,
|
||||
"lib": ["dom", "dom.iterable", "es2019"],
|
||||
"module": "commonjs",
|
||||
"target": "es2021",
|
||||
"stripInternal": true
|
||||
},
|
||||
"include": ["./src/**/*.ts", "tsup.config.ts"],
|
||||
"exclude": ["node_modules"]
|
||||
}
|
||||
@@ -0,0 +1,4 @@
|
||||
{
|
||||
"extends": "@trigger.dev/tsconfig/integration.json",
|
||||
"include": ["./src/**/*.ts", "tsup.config.ts"]
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
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"],
|
||||
});
|
||||
@@ -0,0 +1,7 @@
|
||||
import { defineConfig, deepMergeOptions, integrationOptions } from "@trigger.dev/tsup";
|
||||
|
||||
const options = deepMergeOptions(integrationOptions, {
|
||||
// extend base config here
|
||||
});
|
||||
|
||||
export default defineConfig(options);
|
||||
@@ -0,0 +1,28 @@
|
||||
import { Request } from "{{ sdkPackage }}";
|
||||
import { WebhookActionType, WebhookPayload } from "./schemas";
|
||||
|
||||
export type Get{{ identifier | capitalize }}Payload<
|
||||
TPayload extends WebhookPayload,
|
||||
TAction extends any = any,
|
||||
> = TAction extends WebhookActionType ? Extract<TPayload, { action: TAction }> : TPayload;
|
||||
|
||||
type FunctionKeys<T> = {
|
||||
[K in keyof T]: T[K] extends Function ? K : never;
|
||||
}[keyof T];
|
||||
|
||||
export type Serialized{{ identifier | capitalize }}Output<T> = T extends object
|
||||
? T extends Array<infer U>
|
||||
? Array<Serialized{{ identifier | capitalize }}Output<U>>
|
||||
: { [K in keyof T as Exclude<K, FunctionKeys<T> | `_${string}`>]: Serialized{{ identifier | capitalize }}Output<T[K]> }
|
||||
: T;
|
||||
|
||||
export type {{ identifier | capitalize }}ReturnType<
|
||||
TPayload extends Omit<Request, "_request">,
|
||||
K extends unknown = unknown,
|
||||
> = Promise<
|
||||
Awaited<Serialized{{ identifier | capitalize }}Output<Awaited<K extends keyof TPayload ? TPayload[K] : TPayload>>>
|
||||
>;
|
||||
|
||||
export type AwaitNested<T extends object, K extends keyof T> = Omit<T, K> & {
|
||||
[key in K]: Awaited<T[K]>;
|
||||
};
|
||||
@@ -0,0 +1,77 @@
|
||||
import { CommentEvent, IssueEvent, WebhookPayload } from "./schemas";
|
||||
import { Get{{ identifier | capitalize }}Payload } from "./types";
|
||||
|
||||
export type QueryVariables = {
|
||||
after: string;
|
||||
before: string;
|
||||
first: number;
|
||||
includeArchived: boolean;
|
||||
last: number;
|
||||
};
|
||||
|
||||
export type Nullable<T> = Partial<{
|
||||
[K in keyof T]: T[K] | null;
|
||||
}>;
|
||||
|
||||
export const onCommentProperties = (payload: Get{{ identifier | capitalize }}Payload<CommentEvent>) => {
|
||||
return [
|
||||
{ label: "Comment ID", text: payload.data.id },
|
||||
{ label: "Issue ID", text: payload.data.issueId },
|
||||
{ label: "Issue Title", text: payload.data.issue.title, url: payload.url ?? undefined },
|
||||
];
|
||||
};
|
||||
|
||||
export const onIssueProperties = (payload: Get{{ identifier | capitalize }}Payload<IssueEvent>) => {
|
||||
return [
|
||||
{ label: "Issue ID", text: payload.data.id },
|
||||
{
|
||||
label: "Issue",
|
||||
text: `[${payload.data.team.key}-${payload.data.number}] ${payload.data.title}`,
|
||||
url: payload.url ?? undefined,
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const queryProperties = (query: Nullable<QueryVariables>) => {
|
||||
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.includeArchived
|
||||
? [{ label: "Include archived", text: String(query.includeArchived) }]
|
||||
: []),
|
||||
];
|
||||
};
|
||||
|
||||
export const updatedFromProperties = (payload: WebhookPayload) => {
|
||||
if (payload.action !== "update") return [];
|
||||
return [
|
||||
{
|
||||
label: "Updated Keys",
|
||||
text: Object.keys(payload.updatedFrom)
|
||||
.filter((key) => !["editedAt", "updatedAt"].includes(key))
|
||||
.join(", "),
|
||||
},
|
||||
];
|
||||
};
|
||||
|
||||
export const modelProperties = (
|
||||
params: Partial<{
|
||||
model_owner: string;
|
||||
model_name: string;
|
||||
version_id: string;
|
||||
destination: string;
|
||||
}>
|
||||
) => {
|
||||
return [
|
||||
...(params.model_owner ? [{ label: "Model Owner", text: params.model_owner }] : []),
|
||||
...(params.model_name ? [{ label: "Model Name", text: params.model_name }] : []),
|
||||
...(params.version_id ? [{ label: "Model Version", text: params.version_id }] : []),
|
||||
...(params.destination ? [{ label: "Destination Model", text: params.destination }] : []),
|
||||
];
|
||||
};
|
||||
|
||||
export const streamingProperty = (params: { stream?: boolean }) => {
|
||||
return [{ label: "Streaming Enabled", text: String(!!params.stream) }];
|
||||
};
|
||||
@@ -0,0 +1,297 @@
|
||||
import {
|
||||
EventFilter,
|
||||
ExternalSource,
|
||||
ExternalSourceTrigger,
|
||||
HandlerEvent,
|
||||
IntegrationTaskKey,
|
||||
Logger,
|
||||
} from "@trigger.dev/sdk";
|
||||
import {
|
||||
Document,
|
||||
{{ identifier | capitalize }}Webhooks,
|
||||
WebhookPayload,
|
||||
DeletePayload,
|
||||
Webhook,
|
||||
} from "{{ sdkPackage }}";
|
||||
import { z } from "zod";
|
||||
import * as events from "./events";
|
||||
import { {{ identifier | capitalize }}, {{ identifier | capitalize }}RunTask, serialize{{ identifier | capitalize }}Output } from "./index";
|
||||
import { WebhookPayloadSchema } from "./schemas";
|
||||
import { {{ identifier | capitalize }}ReturnType } from "./types";
|
||||
import { queryProperties } from "./utils";
|
||||
|
||||
export class Webhooks {
|
||||
runTask: {{ identifier | capitalize }}RunTask;
|
||||
|
||||
constructor(runTask: {{ identifier | capitalize }}RunTask) {
|
||||
this.runTask = runTask;
|
||||
}
|
||||
|
||||
webhook(key: IntegrationTaskKey, params: { id: string }): {{ identifier | capitalize }}ReturnType<Webhook> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
return serialize{{ identifier | capitalize }}Output(await client.webhook(params.id));
|
||||
},
|
||||
{
|
||||
name: "Get Webhook",
|
||||
params,
|
||||
properties: [{ label: "Webhook ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
webhooks(key: IntegrationTaskKey, params?: Document.WebhooksQueryVariables): {{ identifier | capitalize }}ReturnType<Webhook[]> {
|
||||
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 serialize{{ identifier | capitalize }}Output(hooks);
|
||||
},
|
||||
{
|
||||
name: "List Webhooks",
|
||||
params,
|
||||
properties: queryProperties(params ?? {}),
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
createWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: Document.WebhookCreateInput
|
||||
): {{ identifier | capitalize }}ReturnType<Omit<WebhookPayload, "webhook"> & { webhook: Webhook | undefined }> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
const payload = await client.createWebhook({ ...params, allPublicTeams: !params.teamId });
|
||||
return serialize{{ identifier | capitalize }}Output({
|
||||
...payload,
|
||||
webhook: await payload.webhook,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Create Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Webhook URL", text: params.url },
|
||||
{ label: "Resource Types", text: params.resourceTypes.join(", ") },
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
deleteWebhook(key: IntegrationTaskKey, params: { id: string }): {{ identifier | capitalize }}ReturnType<DeletePayload> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task, io) => {
|
||||
return serialize{{ identifier | capitalize }}Output(await client.deleteWebhook(params.id));
|
||||
},
|
||||
{
|
||||
name: "Delete Webhook",
|
||||
params,
|
||||
properties: [{ label: "Webhook ID", text: params.id }],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
updateWebhook(
|
||||
key: IntegrationTaskKey,
|
||||
params: { id: string; input: Document.WebhookUpdateInput }
|
||||
): {{ identifier | capitalize }}ReturnType<Omit<WebhookPayload, "webhook"> & { webhook: Webhook | undefined }> {
|
||||
return this.runTask(
|
||||
key,
|
||||
async (client, task) => {
|
||||
const payload = await client.updateWebhook(params.id, params.input);
|
||||
return serialize{{ identifier | capitalize }}Output({
|
||||
...payload,
|
||||
webhook: await payload.webhook,
|
||||
});
|
||||
},
|
||||
{
|
||||
name: "Update Webhook",
|
||||
params,
|
||||
properties: [
|
||||
{ label: "Webhook ID", text: params.id },
|
||||
...(params.input.url ? [{ label: "Webhook URL", text: params.input.url }] : []),
|
||||
...(params.input.resourceTypes
|
||||
? [{ label: "Resource Types", text: params.input.resourceTypes.join(", ") }]
|
||||
: []),
|
||||
],
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
type {{ identifier | capitalize }}Events = (typeof events)[keyof typeof events];
|
||||
|
||||
export type TriggerParams = {
|
||||
teamId?: string;
|
||||
filter?: EventFilter;
|
||||
};
|
||||
|
||||
type CreateTriggersResult<TEventSpecification extends {{ identifier | capitalize }}Events> = ExternalSourceTrigger<
|
||||
TEventSpecification,
|
||||
ReturnType<typeof createWebhookEventSource>
|
||||
>;
|
||||
|
||||
export function createTrigger<TEventSpecification extends {{ identifier | capitalize }}Events>(
|
||||
source: ReturnType<typeof createWebhookEventSource>,
|
||||
event: TEventSpecification,
|
||||
params: TriggerParams
|
||||
): CreateTriggersResult<TEventSpecification> {
|
||||
return new ExternalSourceTrigger({
|
||||
event,
|
||||
params,
|
||||
source,
|
||||
options: {},
|
||||
});
|
||||
}
|
||||
|
||||
const WebhookRegistrationDataSchema = z.object({
|
||||
success: z.literal(true),
|
||||
webhook: z.object({
|
||||
id: z.string(),
|
||||
enabled: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
export function createWebhookEventSource(
|
||||
integration: {{ identifier | capitalize }}
|
||||
): ExternalSource<{{ identifier | capitalize }}, TriggerParams, "HTTP", {}> {
|
||||
return new ExternalSource("HTTP", {
|
||||
id: "{{ identifier }}.webhook",
|
||||
schema: z.object({
|
||||
teamId: z.string().optional(),
|
||||
}),
|
||||
version: "0.1.0",
|
||||
integration,
|
||||
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);
|
||||
|
||||
// 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 {{ identifier }}
|
||||
const label = `trigger.${params.teamId ? params.teamId : "all"}`;
|
||||
|
||||
if (httpSource.active && webhookData.success) {
|
||||
const hasMissingOptions = Object.values(options).some(
|
||||
(option) => option.missing.length > 0
|
||||
);
|
||||
if (!hasMissingOptions) return;
|
||||
|
||||
const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
|
||||
id: webhookData.data.webhook.id,
|
||||
input: {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
url: httpSource.url,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(updatedWebhook),
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
// check for existing hooks that match url
|
||||
const listResponse = await io.integration.webhooks("list-webhooks");
|
||||
const existingWebhook = listResponse.find((w) => w.url === httpSource.url);
|
||||
|
||||
if (existingWebhook) {
|
||||
const updatedWebhook = await io.integration.updateWebhook("update-webhook", {
|
||||
id: existingWebhook.id,
|
||||
input: {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
url: httpSource.url,
|
||||
},
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(updatedWebhook),
|
||||
options: registeredOptions,
|
||||
};
|
||||
}
|
||||
|
||||
const createPayload = await io.integration.createWebhook("create-webhook", {
|
||||
label,
|
||||
resourceTypes: allEvents,
|
||||
secret: httpSource.secret,
|
||||
teamId: params.teamId,
|
||||
url: httpSource.url,
|
||||
});
|
||||
|
||||
return {
|
||||
data: WebhookRegistrationDataSchema.parse(createPayload),
|
||||
secret: (await createPayload.webhook)?.secret,
|
||||
options: registeredOptions,
|
||||
};
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
async function webhookHandler(event: HandlerEvent<"HTTP">, logger: Logger, integration: {{ identifier | capitalize }}) {
|
||||
logger.debug("[@trigger.dev/{{ identifier }}] Handling webhook payload");
|
||||
|
||||
const { rawEvent: request, source } = event;
|
||||
|
||||
const payloadUuid = request.headers.get("{{ identifier | capitalize }}-Delivery");
|
||||
const payloadEvent = request.headers.get("{{ identifier | capitalize }}-Event");
|
||||
|
||||
if (!payloadUuid || !payloadEvent) {
|
||||
logger.debug("[@trigger.dev/{{ identifier }}] Missing required {{ identifier | capitalize }} headers");
|
||||
return { events: [] };
|
||||
}
|
||||
|
||||
if (!request.body) {
|
||||
logger.debug("[@trigger.dev/{{ identifier }}] No body found");
|
||||
return { events: [] };
|
||||
}
|
||||
|
||||
const signature = request.headers.get("WEBHOOK_SIGNATURE_HEADER");
|
||||
|
||||
if (!signature) {
|
||||
logger.error("[@trigger.dev/{{ identifier }}] Error validating webhook signature, no signature found");
|
||||
throw Error("[@trigger.dev/{{ identifier }}] No signature found");
|
||||
}
|
||||
|
||||
const rawBody = await request.text();
|
||||
const body = JSON.parse(rawBody);
|
||||
const webhookHelper = new {{ identifier | capitalize }}Webhooks(source.secret);
|
||||
|
||||
if (!webhookHelper.verify(Buffer.from(rawBody), signature)) {
|
||||
logger.error("[@trigger.dev/{{ identifier }}] Error validating webhook signature, they don't match");
|
||||
throw Error("[@trigger.dev/{{ identifier }}] Invalid signature");
|
||||
}
|
||||
|
||||
const webhookPayload = WebhookPayloadSchema.parse(body);
|
||||
|
||||
return {
|
||||
events: [
|
||||
{
|
||||
id: payloadUuid,
|
||||
name: payloadEvent,
|
||||
source: "{{ identifier }}.app",
|
||||
payload: webhookPayload,
|
||||
context: {},
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,61 @@
|
||||
import fs from "fs/promises";
|
||||
import { Liquid } from "liquidjs";
|
||||
import path from "path";
|
||||
|
||||
import { pathExists } from "./fileSystem";
|
||||
import { templatesPath } from "../paths";
|
||||
|
||||
type Result =
|
||||
| {
|
||||
success: true;
|
||||
alreadyExisted: boolean;
|
||||
}
|
||||
| {
|
||||
success: false;
|
||||
error: string;
|
||||
};
|
||||
|
||||
const templatesDir = path.join(templatesPath(), "integration");
|
||||
|
||||
const liquid = new Liquid({
|
||||
root: templatesDir,
|
||||
trimTagRight: true,
|
||||
trimOutputRight: true,
|
||||
});
|
||||
|
||||
export async function createIntegrationFileFromTemplate(params: {
|
||||
relativeTemplatePath: string;
|
||||
variables?: Record<string, any>;
|
||||
outputPath: string;
|
||||
}): Promise<Result> {
|
||||
if (await pathExists(params.outputPath)) {
|
||||
return {
|
||||
success: true,
|
||||
alreadyExisted: true,
|
||||
};
|
||||
}
|
||||
|
||||
try {
|
||||
const output = await liquid.renderFile(params.relativeTemplatePath, params.variables);
|
||||
|
||||
const directoryName = path.dirname(params.outputPath);
|
||||
await fs.mkdir(directoryName, { recursive: true });
|
||||
await fs.writeFile(params.outputPath, output);
|
||||
|
||||
return {
|
||||
success: true,
|
||||
alreadyExisted: false,
|
||||
};
|
||||
} catch (e) {
|
||||
if (e instanceof Error) {
|
||||
return {
|
||||
success: false,
|
||||
error: e.message,
|
||||
};
|
||||
}
|
||||
return {
|
||||
success: false,
|
||||
error: JSON.stringify(e),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -4,3 +4,8 @@ import pathModule from "path";
|
||||
export const resolvePath = (input: string) => {
|
||||
return pathModule.resolve(process.cwd(), input);
|
||||
};
|
||||
|
||||
// Takes an absolute path and derives the relative path from the current working directory
|
||||
export const relativePath = (input: string) => {
|
||||
return pathModule.relative(process.cwd(), input);
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user