Create a single ApiCatalog (and keep the Api naming consistent)

This commit is contained in:
Eric Allam
2023-04-29 17:27:06 +01:00
parent ea6bb4b467
commit a87fd6d4bd
10 changed files with 61 additions and 69 deletions
@@ -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 */
+2 -2
View File
@@ -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) => {},