Create a single ApiCatalog (and keep the Api naming consistent)
This commit is contained in:
@@ -2,7 +2,7 @@ import { useLocation, useTransition } from "@remix-run/react";
|
||||
import classNames from "classnames";
|
||||
import { useTypedFetcher } from "remix-typedjson";
|
||||
import type { action } from "~/routes/resources/connection/oauth2";
|
||||
import type { ExternalAPI } from "~/services/externalApis/types";
|
||||
import type { ExternalApi } from "~/services/externalApis/types";
|
||||
import { NamedIcon } from "../Icon";
|
||||
import { PrimaryButton } from "../primitives/Buttons";
|
||||
import {
|
||||
@@ -26,7 +26,7 @@ export function ConnectButton({
|
||||
children,
|
||||
className,
|
||||
}: {
|
||||
api: ExternalAPI;
|
||||
api: ExternalApi;
|
||||
authMethodKey: string;
|
||||
organizationId: string;
|
||||
children: React.ReactNode;
|
||||
@@ -149,7 +149,7 @@ export function BasicConnectButton({
|
||||
authMethodKey,
|
||||
organizationId,
|
||||
}: {
|
||||
api: ExternalAPI;
|
||||
api: ExternalApi;
|
||||
authMethodKey: string;
|
||||
organizationId: string;
|
||||
}) {
|
||||
|
||||
@@ -1,8 +1,4 @@
|
||||
import {
|
||||
CursorArrowRaysIcon,
|
||||
PlusCircleIcon,
|
||||
PlusIcon,
|
||||
} from "@heroicons/react/24/outline";
|
||||
import { CursorArrowRaysIcon, PlusIcon } from "@heroicons/react/24/outline";
|
||||
import type { LoaderArgs } from "@remix-run/server-runtime";
|
||||
import { SliderButton } from "@typeform/embed-react";
|
||||
import { typedjson, useTypedLoaderData } from "remix-typedjson";
|
||||
@@ -16,7 +12,6 @@ import { List } from "~/components/layout/List";
|
||||
import { OrganizationsSideMenu } from "~/components/navigation/SideMenu";
|
||||
import { Badge } from "~/components/primitives/Badge";
|
||||
import {
|
||||
SecondaryButton,
|
||||
primaryClasses,
|
||||
secondaryClasses,
|
||||
} from "~/components/primitives/Buttons";
|
||||
@@ -26,14 +21,14 @@ import {
|
||||
PopoverTrigger,
|
||||
} from "~/components/primitives/Popover";
|
||||
import { Body } from "~/components/primitives/text/Body";
|
||||
import { Header3, Header4 } from "~/components/primitives/text/Headers";
|
||||
import { Header3 } from "~/components/primitives/text/Headers";
|
||||
import { SubTitle } from "~/components/primitives/text/SubTitle";
|
||||
import { Title } from "~/components/primitives/text/Title";
|
||||
import { useCurrentOrganization } from "~/hooks/useOrganizations";
|
||||
import { getOrganizationFromSlug } from "~/models/organization.server";
|
||||
import { apiConnectionRepository } from "~/services/externalApis/apiAuthenticationRepository.server";
|
||||
import { apiStore } from "~/services/externalApis/apiStore";
|
||||
import type { ExternalAPI } from "~/services/externalApis/types";
|
||||
import { apiCatalog } from "~/services/externalApis/apiCatalog.server";
|
||||
import type { ExternalApi } from "~/services/externalApis/types";
|
||||
import { requireUser } from "~/services/session.server";
|
||||
import { formatDateTime } from "~/utils";
|
||||
|
||||
@@ -51,11 +46,9 @@ export const loader = async ({ request, params }: LoaderArgs) => {
|
||||
organization.id
|
||||
);
|
||||
|
||||
const apis = apiStore.getApis();
|
||||
|
||||
return typedjson({
|
||||
connections,
|
||||
apis,
|
||||
apis: apiCatalog.getApis(),
|
||||
});
|
||||
};
|
||||
|
||||
@@ -187,7 +180,7 @@ export default function Integrations() {
|
||||
);
|
||||
}
|
||||
|
||||
function AddApiConnection({ api }: { api: ExternalAPI }) {
|
||||
function AddApiConnection({ api }: { api: ExternalApi }) {
|
||||
return (
|
||||
<div className="flex h-20 w-full items-center justify-between gap-2 px-10">
|
||||
<NamedIconInBox
|
||||
|
||||
@@ -9,8 +9,8 @@ import { env } from "~/env.server";
|
||||
import { workerQueue } from "~/services/worker.server";
|
||||
import type { SecretStoreProvider } from "../secrets/secretStore.server";
|
||||
import { SecretStore } from "../secrets/secretStore.server";
|
||||
import type { APIStore } from "./apiStore";
|
||||
import { apiStore as apis } from "./apiStore";
|
||||
import type { ApiCatalog } from "./apiCatalog.server";
|
||||
import { apiCatalog } from "./apiCatalog.server";
|
||||
import {
|
||||
createOAuth2Url,
|
||||
getClientConfigFromEnv,
|
||||
@@ -19,9 +19,9 @@ import {
|
||||
} from "./oauth2.server";
|
||||
import type {
|
||||
AccessToken,
|
||||
APIAuthenticationMethodOAuth2,
|
||||
ApiAuthenticationMethodOAuth2,
|
||||
ConnectionMetadata,
|
||||
ExternalAPI,
|
||||
ExternalApi,
|
||||
GrantTokenParams,
|
||||
RefreshTokenParams,
|
||||
} from "./types";
|
||||
@@ -37,11 +37,14 @@ const randomGenerator = customAlphabet("1234567890abcdef", 3);
|
||||
const tokenRefreshThreshold = 5 * 60;
|
||||
|
||||
export class APIAuthenticationRepository {
|
||||
#apiStore: APIStore;
|
||||
apiCatalog: ApiCatalog;
|
||||
#prismaClient: PrismaClient;
|
||||
|
||||
constructor(apiStore: APIStore = apis, prismaClient: PrismaClient = prisma) {
|
||||
this.#apiStore = apiStore;
|
||||
constructor(
|
||||
catalog: ApiCatalog = apiCatalog,
|
||||
prismaClient: PrismaClient = prisma
|
||||
) {
|
||||
this.apiCatalog = catalog;
|
||||
this.#prismaClient = prismaClient;
|
||||
}
|
||||
|
||||
@@ -60,7 +63,7 @@ export class APIAuthenticationRepository {
|
||||
}
|
||||
|
||||
/** Get all API connections for the organization, for a specific API */
|
||||
async getConnectionsForApi(organizationId: string, api: ExternalAPI) {
|
||||
async getConnectionsForApi(organizationId: string, api: ExternalApi) {
|
||||
const connections = await this.#prismaClient.apiConnection.findMany({
|
||||
where: {
|
||||
organizationId: organizationId,
|
||||
@@ -86,7 +89,7 @@ export class APIAuthenticationRepository {
|
||||
title: string;
|
||||
redirectTo: string;
|
||||
}) {
|
||||
const api = this.#apiStore.getApi(apiIdentifier);
|
||||
const api = this.apiCatalog.getApi(apiIdentifier);
|
||||
if (!api) {
|
||||
throw new Error(`API ${apiIdentifier} not found`);
|
||||
}
|
||||
@@ -180,7 +183,7 @@ export class APIAuthenticationRepository {
|
||||
title: string;
|
||||
pkceCode?: string;
|
||||
}) {
|
||||
const api = this.#apiStore.getApi(apiIdentifier);
|
||||
const api = this.apiCatalog.getApi(apiIdentifier);
|
||||
if (!api) {
|
||||
throw new Error(`API ${apiIdentifier} not found`);
|
||||
}
|
||||
@@ -340,7 +343,7 @@ export class APIAuthenticationRepository {
|
||||
throw new Error(`Connection ${connectionId} not found`);
|
||||
}
|
||||
|
||||
const api = this.#apiStore.getApi(connection.apiIdentifier);
|
||||
const api = this.apiCatalog.getApi(connection.apiIdentifier);
|
||||
if (!api) {
|
||||
throw new Error(`API ${connection.apiIdentifier} not found`);
|
||||
}
|
||||
@@ -489,7 +492,7 @@ export class APIAuthenticationRepository {
|
||||
}
|
||||
|
||||
//add details about the API and authentication method
|
||||
const api = this.#apiStore.getApi(connection.apiIdentifier);
|
||||
const api = this.apiCatalog.getApi(connection.apiIdentifier);
|
||||
if (!api) {
|
||||
throw new Error(
|
||||
`API ${connection.apiIdentifier} not found for connection ${connection.id}`
|
||||
@@ -519,7 +522,7 @@ export class APIAuthenticationRepository {
|
||||
};
|
||||
}
|
||||
|
||||
#callbackUrl(authenticationMethod: APIAuthenticationMethodOAuth2) {
|
||||
#callbackUrl(authenticationMethod: ApiAuthenticationMethodOAuth2) {
|
||||
return authenticationMethod.config.appHostEnvName
|
||||
? process.env[authenticationMethod.config.appHostEnvName]
|
||||
: env.APP_ORIGIN;
|
||||
@@ -529,7 +532,7 @@ export class APIAuthenticationRepository {
|
||||
authenticationMethod,
|
||||
token,
|
||||
}: {
|
||||
authenticationMethod: APIAuthenticationMethodOAuth2;
|
||||
authenticationMethod: ApiAuthenticationMethodOAuth2;
|
||||
token: AccessToken;
|
||||
}) {
|
||||
const metadata: ConnectionMetadata = {};
|
||||
|
||||
@@ -0,0 +1,25 @@
|
||||
import { airtable } from "./apis/airtable";
|
||||
import { slack } from "./apis/slack";
|
||||
import type { ExternalApi } from "./types";
|
||||
|
||||
export class ApiCatalog {
|
||||
#apis: Record<string, ExternalApi>;
|
||||
|
||||
constructor(apis: Record<string, ExternalApi>) {
|
||||
this.#apis = apis;
|
||||
}
|
||||
|
||||
public getApis() {
|
||||
return this.#apis;
|
||||
}
|
||||
|
||||
public getApi(identifier: string) {
|
||||
const api = this.#apis[identifier];
|
||||
if (!api) {
|
||||
return undefined;
|
||||
}
|
||||
return api;
|
||||
}
|
||||
}
|
||||
|
||||
export const apiCatalog = new ApiCatalog({ slack, airtable });
|
||||
@@ -1,4 +0,0 @@
|
||||
import { airtable } from "./apis/airtable";
|
||||
import { slack } from "./apis/slack";
|
||||
|
||||
export const apis = { slack, airtable };
|
||||
@@ -1,25 +0,0 @@
|
||||
import { apis } from "./apiCatalog";
|
||||
import type { ExternalAPI } from "./types";
|
||||
|
||||
/** Used to get External APIs */
|
||||
export class APIStore {
|
||||
#apis: Record<string, ExternalAPI>;
|
||||
|
||||
constructor(apis: Record<string, ExternalAPI>) {
|
||||
this.#apis = apis;
|
||||
}
|
||||
|
||||
public getApis() {
|
||||
return this.#apis;
|
||||
}
|
||||
|
||||
public getApi(identifier: string) {
|
||||
const api = this.#apis[identifier];
|
||||
if (!api) {
|
||||
return undefined;
|
||||
}
|
||||
return api;
|
||||
}
|
||||
}
|
||||
|
||||
export const apiStore = new APIStore(apis);
|
||||
@@ -1,6 +1,6 @@
|
||||
import type { ExternalAPI } from "../types";
|
||||
import type { ExternalApi } from "../types";
|
||||
|
||||
export const airtable: ExternalAPI = {
|
||||
export const airtable: ExternalApi = {
|
||||
identifier: "airtable",
|
||||
name: "Airtable",
|
||||
authenticationMethods: {
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { ExternalAPI, ScopeAnnotation } from "../types";
|
||||
import type { ExternalApi, ScopeAnnotation } from "../types";
|
||||
|
||||
const userAnnotation: ScopeAnnotation = {
|
||||
label: "User",
|
||||
@@ -10,7 +10,7 @@ const botAnnotation: ScopeAnnotation = {
|
||||
color: "#FFF067",
|
||||
};
|
||||
|
||||
export const slack: ExternalAPI = {
|
||||
export const slack: ExternalApi = {
|
||||
identifier: "slack",
|
||||
name: "Slack",
|
||||
authenticationMethods: {
|
||||
|
||||
@@ -1,16 +1,16 @@
|
||||
import { z } from "zod";
|
||||
|
||||
export type ExternalAPI = {
|
||||
export type ExternalApi = {
|
||||
/** Used to uniquely identify an API */
|
||||
identifier: string;
|
||||
/** The name of the API */
|
||||
name: string;
|
||||
/** The possible authentication methods we support for this API */
|
||||
authenticationMethods: Record<string, APIAuthenticationMethod>;
|
||||
authenticationMethods: Record<string, ApiAuthenticationMethod>;
|
||||
};
|
||||
|
||||
/** An authentication method that can be used */
|
||||
export type APIAuthenticationMethod = APIAuthenticationMethodOAuth2;
|
||||
export type ApiAuthenticationMethod = ApiAuthenticationMethodOAuth2;
|
||||
|
||||
export type AuthorizationLocation = "header" | "body";
|
||||
|
||||
@@ -58,7 +58,7 @@ export type RefreshTokenParams = {
|
||||
};
|
||||
|
||||
//A useful reference is the Simple OAuth2 npm library: https://github.com/lelylan/simple-oauth2/blob/HEAD/API.md#options
|
||||
export type APIAuthenticationMethodOAuth2 = {
|
||||
export type ApiAuthenticationMethodOAuth2 = {
|
||||
/** The displayable name of the authentication method */
|
||||
name: string;
|
||||
/** The type of authentication method */
|
||||
|
||||
@@ -99,7 +99,7 @@ new Job({
|
||||
gh,
|
||||
},
|
||||
// issueEvent is a helper function that creates a trigger for a GitHub issue event webhook
|
||||
trigger: gh.onIssueOpened({
|
||||
trigger: gh.triggers.onIssueOpened({
|
||||
repo: "ericallam/basic-starter-100k",
|
||||
}),
|
||||
run: async (event, io, ctx) => {
|
||||
@@ -150,7 +150,7 @@ new Job({
|
||||
gh,
|
||||
},
|
||||
// issueEvent is a helper function that creates a trigger for a GitHub issue event webhook
|
||||
trigger: gh.onIssueComment({
|
||||
trigger: gh.triggers.onIssueComment({
|
||||
repo: "ericallam/basic-starter-100k",
|
||||
}),
|
||||
run: async (event, io, ctx) => {},
|
||||
|
||||
Reference in New Issue
Block a user