API connection is getting created, just need to update with external id now

This commit is contained in:
Matt Aitken
2022-12-12 16:51:17 +00:00
parent 8bec7879c4
commit 7afc596f3f
12 changed files with 288 additions and 32 deletions
+22
View File
@@ -0,0 +1,22 @@
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"command": "pnpm run dev --filter webapp",
"name": "Run webapp",
"request": "launch",
"type": "node-terminal",
"cwd": "${workspaceFolder}"
},
{
"type": "chrome",
"request": "launch",
"name": "Chrome webapp",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/apps/webapp/app"
}
]
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 6.2 KiB

@@ -1,17 +1,15 @@
import { Organization } from ".prisma/client";
import { Popover, Transition } from "@headlessui/react";
import {
BookmarkIcon,
BriefcaseIcon,
CheckIcon,
ChevronDownIcon,
PlusIcon,
} from "@heroicons/react/24/solid";
import { Link } from "@remix-run/react";
import classNames from "classnames";
import React, { Fragment } from "react";
import { Fragment } from "react";
import {
useCurrentOrganizationSlug,
useCurrentOrganization,
useOrganizations,
} from "~/hooks/useOrganizations";
@@ -19,11 +17,7 @@ const actionClassNames = "text-green-500";
export function OrganizationMenu() {
const organizations = useOrganizations();
const currentOrganizationSlug = useCurrentOrganizationSlug();
const currentOrganization = organizations?.find(
(org) => org.slug === currentOrganizationSlug
);
const currentOrganization = useCurrentOrganization();
if (organizations === undefined) {
return null;
@@ -74,7 +68,7 @@ export function OrganizationMenu() {
to={`/orgs/${organization.slug}`}
className={classNames(
"flex items-center justify-between gap-1.5 mx-1 px-3 py-2 text-slate-600 rounded hover:bg-slate-100 transition",
organization.slug === currentOrganizationSlug &&
organization.slug === currentOrganization?.slug &&
"!bg-slate-200"
)}
>
@@ -87,7 +81,7 @@ export function OrganizationMenu() {
{organization.title}
</span>
</div>
{organization.slug === currentOrganizationSlug && (
{organization.slug === currentOrganization?.slug && (
<CheckIcon className="h-5 w-5 text-blue-500" />
)}
</Popover.Button>
+1
View File
@@ -24,6 +24,7 @@ const EnvironmentSchema = z.object({
PRIMARY_REGION: z.string().optional(),
FLY_REGION: z.string().optional(),
SESSION_SECRET: z.string(),
PIZZLY_HOST: z.string(),
});
export type Environment = z.infer<typeof EnvironmentSchema>;
+10
View File
@@ -27,3 +27,13 @@ export function useCurrentOrganizationSlug(): string | undefined {
const routeMatch = useMatchesData("routes/__app/orgs/$organizationSlug");
return routeMatch?.params?.organizationSlug;
}
export function useCurrentOrganization(): Organization | undefined {
const organizations = useOrganizations();
const currentOrganizationSlug = useCurrentOrganizationSlug();
const currentOrganization = organizations?.find(
(org) => org.slug === currentOrganizationSlug
);
return currentOrganization;
}
@@ -0,0 +1,26 @@
import type { APIConnection, Organization } from ".prisma/client";
import { prisma } from "~/db.server";
export async function createAPIConnection({
organizationId,
title,
apiIdentifier,
scopes,
type,
}: Pick<APIConnection, "title" | "apiIdentifier" | "type" | "scopes"> & {
organizationId: Organization["id"];
}) {
return await prisma.aPIConnection.create({
data: {
title,
apiIdentifier,
type,
scopes,
organization: {
connect: {
id: organizationId,
},
},
},
});
}
@@ -1,29 +1,24 @@
import { useCallback } from "react";
const Pizzly = require("@nangohq/pizzly-frontend");
import invariant from "tiny-invariant";
import { useCurrentOrganization } from "~/hooks/useOrganizations";
import { Connect, integrations } from "~/routes/resources/connection";
export default function Integrations() {
const authenticateWithGitHub = useCallback(async () => {
const pizzly = new Pizzly("http://localhost:3004");
pizzly
.auth("github", "test-connection")
.then((result: any) => {
console.log(
`OAuth flow succeeded for provider "${result.providerConfigKey}" and connection-id "${result.connectionId}"!`
);
})
.catch((error: any) => {
console.error(
`There was an error in the OAuth flow for integration "${error.providerConfigKey}" and connection-id "${error.connectionId}": ${error.error.type} - ${error.error.message}`
);
});
}, []);
const organization = useCurrentOrganization();
invariant(organization, "Organization not found");
return (
<div>
<h1>Integrations</h1>
<button onClick={() => authenticateWithGitHub()}>
Connect to GitHub
</button>
<div>
{integrations.map((integration) => (
<Connect
key={integration.key}
integration={integration}
organizationId={organization.id}
/>
))}
</div>
</div>
);
}
@@ -0,0 +1,159 @@
import githubLogo from "../../assets/images/integrations/github.png";
import { useCallback, useEffect } from "react";
import Pizzly from "@nangohq/pizzly-frontend";
import { ActionArgs, json } from "@remix-run/server-runtime";
import { requireUserId } from "~/services/session.server";
import { env } from "~/env.server";
import { z } from "zod";
import { createAPIConnection } from "~/models/apiConnection.server";
import { APIConnectionType } from ".prisma/client";
import { useFetcher } from "@remix-run/react";
type Integration = {
key: string;
name: string;
logo: string;
};
export const integrations: Integration[] = [
{
key: "github",
name: "GitHub",
logo: githubLogo,
},
];
const createSchema = z.object({
type: z.literal("create"),
organizationId: z.string(),
key: z.string(),
});
const updateSchema = z.object({
type: z.literal("update"),
connectionId: z.string(),
externalId: z.number(),
});
const requestSchema = z.discriminatedUnion("type", [
createSchema,
updateSchema,
]);
type CreateResponse = {
host: string;
integrationKey: string;
connectionId: string;
};
export const action = async ({ request, params }: ActionArgs) => {
const userId = await requireUserId(request);
if (userId === null) {
throw new Response("Unauthorized", { status: 401 });
}
if (request.method !== "POST") {
throw new Response("Method Not Allowed", { status: 405 });
}
try {
const formData = await request.formData();
const body = Object.fromEntries(formData.entries());
const parsed = requestSchema.parse(body);
switch (parsed.type) {
case "create": {
const { organizationId, key } = parsed;
const integrationInfo = integrations.find((i) => i.key === key);
if (!integrationInfo) {
throw new Error("Integration not found");
}
const connection = await createAPIConnection({
organizationId,
title: integrationInfo.name,
apiIdentifier: key,
scopes: [],
type: APIConnectionType.HTTP,
});
const response: CreateResponse = {
host: env.PIZZLY_HOST,
integrationKey: key,
connectionId: connection.id,
};
return json(response);
}
case "update": {
const { connectionId, externalId } = parsed;
return {};
}
}
} catch (error: any) {
return json({ message: error.message }, { status: 400 });
}
};
export function Connect({
integration,
organizationId,
}: {
integration: Integration;
organizationId: string;
}) {
const { createFetcher, status } = useCreateConnection();
return (
<createFetcher.Form method="post" action="/resources/connection">
<input type="hidden" name="type" value="create" />
<input type="hidden" name="organizationId" value={organizationId} />
<input type="hidden" name="key" value={integration.key} />
<button
type="submit"
disabled={status === "loading"}
className="border border-gray-500 rounded-md flex h-12 p-1 items-center disabled:opacity-50"
>
<img src={integration.logo} alt={integration.name} className="h-10" />
<h1>Connect to {integration.name}</h1>
</button>
</createFetcher.Form>
);
}
type Status = "loading" | "idle";
export function useCreateConnection() {
const createConnectionFetcher = useFetcher<CreateResponse>();
const status: Status =
createConnectionFetcher.state === "loading" ? "loading" : "idle";
useEffect(() => {
async function authenticationFlow() {
if (createConnectionFetcher.data === undefined) return;
try {
const pizzly = new Pizzly(createConnectionFetcher.data.host);
const result = await pizzly.auth(
createConnectionFetcher.data.integrationKey,
createConnectionFetcher.data.connectionId
);
console.log(
`OAuth flow succeeded for provider "${result.providerConfigKey}" and connection-id "${result.connectionId}"!`
);
} catch (error: any) {
console.error(
`There was an error in the OAuth flow for integration "${error.providerConfigKey}" and connection-id "${error.connectionId}": ${error.error.type} - ${error.error.message}`
);
}
}
authenticationFlow();
}, [createConnectionFetcher.data]);
return {
createFetcher: createConnectionFetcher,
status,
};
}
@@ -0,0 +1,20 @@
-- CreateEnum
CREATE TYPE "APIConnectionType" AS ENUM ('HTTP', 'GRAPHQL');
-- CreateTable
CREATE TABLE "APIConnection" (
"id" TEXT NOT NULL,
"title" TEXT NOT NULL,
"apiIdentifier" TEXT NOT NULL,
"externalId" TEXT NOT NULL,
"scopes" TEXT[],
"type" "APIConnectionType" NOT NULL,
"createdAt" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updatedAt" TIMESTAMP(3) NOT NULL,
"organizationId" TEXT NOT NULL,
CONSTRAINT "APIConnection_pkey" PRIMARY KEY ("id")
);
-- AddForeignKey
ALTER TABLE "APIConnection" ADD CONSTRAINT "APIConnection_organizationId_fkey" FOREIGN KEY ("organizationId") REFERENCES "Organization"("id") ON DELETE CASCADE ON UPDATE CASCADE;
@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "APIConnection" ALTER COLUMN "externalId" DROP NOT NULL;
+26
View File
@@ -46,6 +46,28 @@ model Organization {
users User[]
workflows Workflow[]
environments RuntimeEnvironment[]
apiConnections APIConnection[]
}
model APIConnection {
id String @id @default(cuid())
title String
apiIdentifier String
externalId String?
scopes String[]
type APIConnectionType
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
organization Organization @relation(fields: [organizationId], references: [id], onDelete: Cascade, onUpdate: Cascade)
organizationId String
}
enum APIConnectionType {
HTTP
GRAPHQL
}
model RuntimeEnvironment {
@@ -75,3 +97,7 @@ model Workflow {
@@unique([organizationId, slug])
}
//todo Workflows have connection slots, which are filled with connections (can be empty)
//todo Workflow has one trigger (can also have a slot with connection)
//todo WorkflowRuns belong to a workflow + environment
+2 -1
View File
@@ -92,6 +92,7 @@
"SENTRY_DSN",
"MAILGUN_KEY",
"FROM_EMAIL",
"MERGENT_KEY"
"MERGENT_KEY",
"PIZZLY_HOST"
]
}