Refactoring

This commit is contained in:
Darren Clarke 2024-04-29 17:27:25 +02:00
parent 39cfada3e8
commit dd14dfe72e
41 changed files with 866 additions and 742 deletions

View file

@ -3,10 +3,10 @@
import { FC } from "react"; import { FC } from "react";
import { useFormState } from "react-dom"; import { useFormState } from "react-dom";
import { Grid } from "@mui/material"; import { Grid } from "@mui/material";
import { TextField } from "ui"; import { TextField, Select, MultiValueField } from "ui";
import { Create as InternalCreate } from "@/app/_components/Create"; import { Create as InternalCreate } from "@/app/_components/Create";
import { generateCreateAction } from "@/app/_lib/actions"; import { generateCreateAction } from "@/app/_lib/actions";
import { serviceConfig } from "@/app/_lib/config"; import { serviceConfig } from "@/app/_config/config";
import { FieldDescription } from "@/app/_lib/service"; import { FieldDescription } from "@/app/_lib/service";
type CreateProps = { type CreateProps = {
@ -15,8 +15,18 @@ type CreateProps = {
export const Create: FC<CreateProps> = ({ service }) => { export const Create: FC<CreateProps> = ({ service }) => {
const { const {
[service]: { entity, table, displayName, createFields: fields }, [service]: { entity, table, displayName, createFields },
} = serviceConfig; } = serviceConfig;
const fields = createFields.map((field: any) => {
const copy = { ...field };
Object.keys(copy).forEach((key: any) => {
if (typeof copy[key] === "function") {
delete copy[key];
}
});
return copy;
});
const createAction = generateCreateAction({ entity, table, fields }); const createAction = generateCreateAction({ entity, table, fields });
const initialState = { const initialState = {
message: null, message: null,
@ -39,10 +49,28 @@ export const Create: FC<CreateProps> = ({ service }) => {
formState={formState} formState={formState}
> >
<Grid container direction="row" rowSpacing={3} columnSpacing={2}> <Grid container direction="row" rowSpacing={3} columnSpacing={2}>
{fields.map( {createFields.map(
(field) => (field) =>
!field.hidden && ( !field.hidden && (
<Grid key={field.name} item xs={field.size ?? 6}> <Grid key={field.name} item xs={field.size ?? 6}>
{field.kind === "select" && (
<Select
name={field.name}
label={field.label}
required={field.required ?? false}
formState={formState}
getOptions={field.getOptions}
/>
)}
{field.kind === "multi" && (
<MultiValueField
name={field.name}
label={field.label}
formState={formState}
helperText={field.helperText}
/>
)}
{(!field.kind || field.kind === "text") && (
<TextField <TextField
name={field.name} name={field.name}
label={field.label} label={field.label}
@ -51,6 +79,7 @@ export const Create: FC<CreateProps> = ({ service }) => {
formState={formState} formState={formState}
helperText={field.helperText} helperText={field.helperText}
/> />
)}
</Grid> </Grid>
), ),
)} )}

View file

@ -7,7 +7,7 @@ import { Selectable } from "kysely";
import { Database } from "@/app/_lib/database"; import { Database } from "@/app/_lib/database";
import { Detail as InternalDetail } from "@/app/_components/Detail"; import { Detail as InternalDetail } from "@/app/_components/Detail";
import { generateDeleteAction } from "@/app/_lib/actions"; import { generateDeleteAction } from "@/app/_lib/actions";
import { serviceConfig } from "@/app/_lib/config"; import { serviceConfig } from "@/app/_config/config";
type DetailProps = { type DetailProps = {
service: string; service: string;

View file

@ -1,5 +1,5 @@
import { db } from "@/app/_lib/database"; import { db } from "@/app/_lib/database";
import { serviceConfig } from "@/app/_lib/config"; import { serviceConfig } from "@/app/_config/config";
import { Detail } from "./_components/Detail"; import { Detail } from "./_components/Detail";
type Props = { type Props = {

View file

@ -8,7 +8,7 @@ import { Selectable } from "kysely";
import { Database } from "@/app/_lib/database"; import { Database } from "@/app/_lib/database";
import { Edit as InternalEdit } from "@/app/_components/Edit"; import { Edit as InternalEdit } from "@/app/_components/Edit";
import { generateUpdateAction } from "@/app/_lib/actions"; import { generateUpdateAction } from "@/app/_lib/actions";
import { serviceConfig } from "@/app/_lib/config"; import { serviceConfig } from "@/app/_config/config";
type EditProps = { type EditProps = {
service: string; service: string;

View file

@ -1,5 +1,5 @@
import { db } from "@/app/_lib/database"; import { db } from "@/app/_lib/database";
import { serviceConfig } from "@/app/_lib/config"; import { serviceConfig } from "@/app/_config/config";
import { Edit } from "./_components/Edit"; import { Edit } from "./_components/Edit";
type PageProps = { type PageProps = {

View file

@ -4,7 +4,7 @@ import { FC } from "react";
import { List as InternalList } from "@/app/_components/List"; import { List as InternalList } from "@/app/_components/List";
import type { Selectable } from "kysely"; import type { Selectable } from "kysely";
import { Database } from "@/app/_lib/database"; import { Database } from "@/app/_lib/database";
import { serviceConfig } from "@/app/_lib/config"; import { serviceConfig } from "@/app/_config/config";
type ListProps = { type ListProps = {
service: string; service: string;

View file

@ -1,6 +1,6 @@
import { List } from "./_components/List"; import { List } from "./_components/List";
import { db } from "@/app/_lib/database"; import { db } from "@/app/_lib/database";
import { serviceConfig } from "@/app/_lib/config"; import { serviceConfig } from "@/app/_config/config";
type PageProps = { type PageProps = {
params: { params: {
@ -11,7 +11,12 @@ type PageProps = {
export default async function Page({ params: { segment } }: PageProps) { export default async function Page({ params: { segment } }: PageProps) {
const service = segment[0]; const service = segment[0];
if (!service) return null;
const config = serviceConfig[service]; const config = serviceConfig[service];
if (!config) return null;
const rows = await db.selectFrom(config.table).selectAll().execute(); const rows = await db.selectFrom(config.table).selectAll().execute();
return <List service={service} rows={rows} />; return <List service={service} rows={rows} />;

View file

@ -28,6 +28,7 @@ export const createAction = async ({
currentState, currentState,
formData, formData,
}: CreateActionArgs) => { }: CreateActionArgs) => {
console.log(formData);
const newRecord = fields.reduce( const newRecord = fields.reduce(
(acc: Record<string, any>, field: FieldDescription) => { (acc: Record<string, any>, field: FieldDescription) => {
if (field.autogenerated === "token") { if (field.autogenerated === "token") {
@ -41,13 +42,19 @@ export const createAction = async ({
{}, {},
); );
await db.insertInto(table).values(newRecord).execute(); console.log({ newRecord });
const record = await db
.insertInto(table)
.values(newRecord)
.returning(["id"])
.executeTakeFirstOrThrow();
console.log({ record });
revalidatePath(`/${entity}`); revalidatePath(`/${entity}`);
return { return {
...currentState, ...currentState,
values: newRecord, values: { ...newRecord, id: record.id },
success: true, success: true,
}; };
}; };
@ -104,3 +111,7 @@ export const deleteAction = async ({ entity, table, id }: DeleteActionArgs) => {
return true; return true;
}; };
export const selectAllAction = async (table: keyof Database) => {
return db.selectFrom(table).selectAll().execute();
};

View file

@ -24,7 +24,7 @@ export const Create: FC<CreateProps> = ({
useEffect(() => { useEffect(() => {
if (formState.success) { if (formState.success) {
router.back(); router.push(`/${entity}/${formState.values.id}`);
} }
}, [formState.success, router]); }, [formState.success, router]);

View file

@ -1,4 +1,4 @@
import type { ServiceConfig } from "./service"; import type { ServiceConfig } from "@/app/_lib/service";
import { facebookConfig as facebook } from "./facebook"; import { facebookConfig as facebook } from "./facebook";
import { signalConfig as signal } from "./signal"; import { signalConfig as signal } from "./signal";
import { whatsappConfig as whatsapp } from "./whatsapp"; import { whatsappConfig as whatsapp } from "./whatsapp";

View file

@ -0,0 +1,123 @@
import { Service, ServiceConfig } from "@/app/_lib/service";
export const facebookConfig: ServiceConfig = {
entity: "facebook",
table: "FacebookBot",
displayName: "Facebook Connection",
createFields: [
{
name: "name",
label: "Name",
required: true,
size: 12,
},
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{ name: "appId", label: "App ID", required: true },
{ name: "appSecret", label: "App Secret", required: true },
{ name: "pageId", label: "Page ID", required: true },
{
name: "pageAccessToken",
label: "Page Access Token",
required: true,
},
{
name: "token",
label: "Token",
hidden: true,
required: true,
autogenerated: "token",
},
{
name: "verifyToken",
label: "Verify Token",
hidden: true,
required: true,
autogenerated: "token",
},
],
updateFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{
name: "token",
label: "Token",
disabled: true,
refreshable: true,
},
{
name: "verifyToken",
label: "Verify Token",
disabled: true,
refreshable: true,
},
{ name: "appId", label: "App ID", required: true },
{ name: "appSecret", label: "App Secret", required: true },
{ name: "pageId", label: "Page ID", required: true },
{
name: "pageAccessToken",
label: "Page Access Token",
required: true,
},
],
displayFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
required: true,
size: 12,
},
{
name: "token",
label: "Token",
copyable: true,
},
{
name: "verifyToken",
label: "Verify Token",
copyable: true,
},
{ name: "appId", label: "App ID", required: true },
{ name: "appSecret", label: "App Secret", required: true },
{
name: "pageId",
label: "Page ID",
required: true,
copyable: true,
},
{
name: "pageAccessToken",
label: "Page Access Token",
required: true,
},
],
listColumns: [
{
field: "name",
headerName: "Name",
flex: 1,
},
{
field: "description",
headerName: "Description",
flex: 2,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1,
},
],
};

View file

@ -0,0 +1,86 @@
import { ServiceConfig } from "@/app/_lib/service";
export const signalConfig: ServiceConfig = {
entity: "signal",
table: "SignalBot",
displayName: "Signal Connection",
createFields: [
{
name: "name",
label: "Name",
required: true,
size: 12,
},
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{
name: "phoneNumber",
label: "phoneNumber",
required: true,
},
{
name: "token",
label: "Token",
hidden: true,
required: true,
autogenerated: "token",
},
],
updateFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
},
{
name: "phoneNumber",
label: "phoneNumber",
required: true,
},
],
displayFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
},
{
name: "phoneNumber",
label: "phoneNumber",
},
{
name: "token",
label: "Token",
copyable: true,
},
],
listColumns: [
{
field: "name",
headerName: "Name",
flex: 1,
},
{
field: "phoneNumber",
headerName: "Phone Number",
flex: 1,
},
{
field: "description",
headerName: "Description",
flex: 2,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1,
},
],
};

View file

@ -1,4 +1,4 @@
import { ServiceConfig } from "./service"; import { ServiceConfig } from "@/app/_lib/service";
export const usersConfig: ServiceConfig = { export const usersConfig: ServiceConfig = {
entity: "users", entity: "users",

View file

@ -1,9 +1,9 @@
import { ServiceConfig } from "./service"; import { ServiceConfig } from "@/app/_lib/service";
export const webhooksConfig: ServiceConfig = { export const voiceConfig: ServiceConfig = {
entity: "webhooks", entity: "voice",
table: "Webhook", table: "VoiceLine",
displayName: "Webhook", displayName: "Voice Line",
createFields: [ createFields: [
{ {
name: "name", name: "name",
@ -17,6 +17,11 @@ export const webhooksConfig: ServiceConfig = {
size: 12, size: 12,
lines: 3, lines: 3,
}, },
{
name: "phoneNumber",
label: "phoneNumber",
required: true,
},
], ],
updateFields: [ updateFields: [
{ name: "name", label: "Name", required: true, size: 12 }, { name: "name", label: "Name", required: true, size: 12 },
@ -26,6 +31,11 @@ export const webhooksConfig: ServiceConfig = {
required: true, required: true,
size: 12, size: 12,
}, },
{
name: "phoneNumber",
label: "Phone Number",
required: true,
},
], ],
displayFields: [ displayFields: [
{ name: "name", label: "Name", required: true, size: 12 }, { name: "name", label: "Name", required: true, size: 12 },
@ -35,6 +45,11 @@ export const webhooksConfig: ServiceConfig = {
required: true, required: true,
size: 12, size: 12,
}, },
{
name: "phoneNumber",
label: "Phone Number",
required: true,
},
], ],
listColumns: [ listColumns: [
{ {
@ -42,6 +57,11 @@ export const webhooksConfig: ServiceConfig = {
headerName: "Name", headerName: "Name",
flex: 1, flex: 1,
}, },
{
field: "phoneNumber",
headerName: "Phone Number",
flex: 1,
},
{ {
field: "description", field: "description",
headerName: "Description", headerName: "Description",

View file

@ -0,0 +1,125 @@
import { selectAllAction } from "@/app/_actions/service";
import { ServiceConfig } from "@/app/_lib/service";
const tableLookup = {
whatsapp: "WhatsappBot",
facebook: "FacebookBot",
signal: "SignalBot",
};
export const webhooksConfig: ServiceConfig = {
entity: "webhooks",
table: "Webhook",
displayName: "Webhook",
createFields: [
{
name: "name",
label: "Name",
required: true,
size: 12,
},
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{
name: "httpMethod",
label: "HTTP Method",
kind: "select",
getOptions: async () => [
{ value: "post", label: "POST" },
{ value: "put", label: "PUT" },
],
defaultValue: "post",
required: true,
size: 2,
},
{
name: "endpointUrl",
label: "Endpoint",
required: true,
size: 10,
},
{
name: "backendType",
label: "Backend Type",
kind: "select",
getOptions: async (_formState: any) => [
{ value: "whatsapp", label: "WhatsApp" },
{ value: "facebook", label: "Facebook" },
{ value: "signal", label: "Signal" },
],
defaultValue: "facebook",
required: true,
},
{
name: "backendId",
label: "Backend ID",
kind: "select",
getOptions: async (formState: any) => {
console.log({ formState });
if (!formState || !formState.values.backendType) {
return [];
}
// @ts-expect-error
const table = tableLookup[formState.values.backendType];
console.log({ table });
const result = await selectAllAction(table);
console.log({ result });
return result.map((item: any) => ({
value: item.id,
label: item.name,
}));
},
required: true,
},
{
name: "headers",
label: "HTTP Headers",
kind: "multi",
size: 12,
helperText: "Useful for including authentication headers",
},
],
updateFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
required: true,
size: 12,
},
],
displayFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
required: true,
size: 12,
},
],
listColumns: [
{
field: "name",
headerName: "Name",
flex: 1,
},
{
field: "description",
headerName: "Description",
flex: 2,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1,
},
],
};

View file

@ -0,0 +1,86 @@
import { ServiceConfig } from "@/app/_lib/service";
export const whatsappConfig: ServiceConfig = {
entity: "whatsapp",
table: "WhatsappBot",
displayName: "WhatsApp Connection",
createFields: [
{
name: "name",
label: "Name",
required: true,
size: 12,
},
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{
name: "phoneNumber",
label: "Phone Number",
required: true,
},
{
name: "token",
label: "Token",
hidden: true,
required: true,
autogenerated: "token",
},
],
updateFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
},
{
name: "phoneNumber",
label: "Phone Number",
required: true,
},
],
displayFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
},
{
name: "phoneNumber",
label: "Phone Number",
},
{
name: "token",
label: "Token",
copyable: true,
},
],
listColumns: [
{
field: "name",
headerName: "Name",
flex: 1,
},
{
field: "phoneNumber",
headerName: "Phone Number",
flex: 1,
},
{
field: "description",
headerName: "Description",
flex: 2,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1,
},
],
};

View file

@ -4,6 +4,7 @@ import {
createAction, createAction,
updateAction, updateAction,
deleteAction, deleteAction,
selectAllAction,
} from "@/app/_actions/service"; } from "@/app/_actions/service";
type GenerateCreateActionArgs = { type GenerateCreateActionArgs = {
@ -18,6 +19,8 @@ export function generateCreateAction({
fields, fields,
}: GenerateCreateActionArgs) { }: GenerateCreateActionArgs) {
return async (currentState: any, formData: FormData) => { return async (currentState: any, formData: FormData) => {
console.log({ entity, table, fields });
console.log({ currentState, formData });
return createAction({ return createAction({
entity, entity,
table, table,
@ -63,3 +66,9 @@ export function generateDeleteAction({
return deleteAction({ entity, table, id }); return deleteAction({ entity, table, id });
}; };
} }
export function generateSelectAllAction(table: keyof Database) {
return async () => {
return selectAllAction(table);
};
}

View file

@ -115,6 +115,11 @@ export interface Database {
id: GeneratedAlways<string>; id: GeneratedAlways<string>;
name: string; name: string;
description: string; description: string;
backendType: string;
backendId: string;
endpointUrl: string;
httpMethod: "post" | "put";
headers: Record<string, any>;
createdBy: string; createdBy: string;
createdAt: Date; createdAt: Date;
updatedAt: Date; updatedAt: Date;

View file

@ -1,138 +1,18 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { Service, ServiceConfig } from "./service"; import { makeWorkerUtils, WorkerUtils } from "graphile-worker";
import { Service } from "./service";
import { db } from "./database";
export const facebookConfig: ServiceConfig = { let workerUtils: WorkerUtils;
entity: "facebook",
table: "FacebookBot",
displayName: "Facebook Connection",
createFields: [
{
name: "name",
label: "Name",
required: true,
size: 12,
},
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{ name: "appId", label: "App ID", required: true },
{ name: "appSecret", label: "App Secret", required: true },
{ name: "pageId", label: "Page ID", required: true },
{
name: "pageAccessToken",
label: "Page Access Token",
required: true,
},
{
name: "token",
label: "Token",
hidden: true,
required: true,
autogenerated: "token",
},
{
name: "verifyToken",
label: "Verify Token",
hidden: true,
required: true,
autogenerated: "token",
},
],
updateFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{
name: "token",
label: "Token",
disabled: true,
refreshable: true,
},
{
name: "verifyToken",
label: "Verify Token",
disabled: true,
refreshable: true,
},
{ name: "appId", label: "App ID", required: true },
{ name: "appSecret", label: "App Secret", required: true },
{ name: "pageId", label: "Page ID", required: true },
{
name: "pageAccessToken",
label: "Page Access Token",
required: true,
},
],
displayFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
required: true,
size: 12,
},
{
name: "token",
label: "Token",
copyable: true,
},
{
name: "verifyToken",
label: "Verify Token",
copyable: true,
},
{ name: "appId", label: "App ID", required: true }, const getWorkerUtils = async () => {
{ name: "appSecret", label: "App Secret", required: true }, if (!workerUtils) {
{ workerUtils = await makeWorkerUtils({
name: "pageId", connectionString: process.env.DATABASE_URL,
label: "Page ID", });
required: true, }
copyable: true,
},
{
name: "pageAccessToken",
label: "Page Access Token",
required: true,
},
],
listColumns: [
{
field: "name",
headerName: "Name",
flex: 1,
},
{
field: "description",
headerName: "Description",
flex: 2,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1,
},
],
};
const getAllBots = async (req: NextRequest) => { return workerUtils;
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const getOneBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
}; };
const sendMessage = async (req: NextRequest) => { const sendMessage = async (req: NextRequest) => {
@ -147,80 +27,75 @@ const receiveMessages = async (req: NextRequest) => {
return NextResponse.json({ response: "ok" }); return NextResponse.json({ response: "ok" });
}; };
const registerBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const resetBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const requestCode = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const unverifyBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const refreshBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const createBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const deleteBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const handleWebhook = async (req: NextRequest) => { const handleWebhook = async (req: NextRequest) => {
console.log({ req });
const { searchParams } = req.nextUrl; const { searchParams } = req.nextUrl;
const token = searchParams.get("hub.verify_token"); const submittedToken = searchParams.get("hub.verify_token");
if (token !== process.env.FB_VERIFY_TOKEN) { if (submittedToken) {
// return NextResponse.error("Invalid token", { status: 403 }); console.log({ submittedToken });
const row = await db
.selectFrom("FacebookBot")
.selectAll()
.where("verifyToken", "=", submittedToken)
.executeTakeFirst();
console.log({ row });
if (!row) {
return NextResponse.error();
} }
if (searchParams.get("hub.mode") === "subscribe") { if (searchParams.get("hub.mode") === "subscribe") {
const challenge = searchParams.get("hub.challenge"); const challenge = searchParams.get("hub.challenge");
console.log(token); console.log(submittedToken);
console.log(challenge); console.log(challenge);
return new Response(challenge, { status: 200 }) as NextResponse; return NextResponse.json(challenge) as any;
} }
}
const message = await req.json();
console.log({ message });
const entry = message.entry[0];
console.log({ entry });
const messaging = entry?.messaging[0];
const pageId = messaging?.recipient?.id;
console.log({ pageId });
const row = await db
.selectFrom("FacebookBot")
.selectAll()
.where("pageId", "=", pageId)
.executeTakeFirst();
console.log({ row });
const endpoint = `https://graph.facebook.com/v19.0/${pageId}/messages`;
const inMessage = messaging?.message?.text;
const outgoingMessage = {
recipient: { id: messaging?.sender?.id },
message: { text: `"${inMessage}", right back at you!` },
messaging_type: "RESPONSE",
access_token: row?.pageAccessToken,
};
console.log({ outgoingMessage });
const response = await fetch(endpoint, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(outgoingMessage),
});
console.log({ response });
console.log(message);
const wu = await getWorkerUtils();
await wu.addJob("receive_facebook_message", message);
return NextResponse.json({ response: "ok" }); return NextResponse.json({ response: "ok" });
}; };
export const Facebook: Service = { export const Facebook: Service = {
getAllBots,
getOneBot,
sendMessage, sendMessage,
receiveMessages, receiveMessages,
registerBot,
resetBot,
requestCode,
unverifyBot,
refreshBot,
createBot,
deleteBot,
handleWebhook, handleWebhook,
}; };

View file

@ -1,50 +1,17 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { Service } from "./service"; import { getService } from "./utils";
import { Facebook } from "./facebook";
const services: Record<string, Service> = { const notFound = () => new NextResponse(null, { status: 404 });
facebook: Facebook,
none: NextResponse.error() as any,
};
const getService = (req: NextRequest): Service => {
const service = req.nextUrl.searchParams.get("service") ?? "none";
return services[service];
};
export const getAllBots = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.getAllBots(req);
export const getOneBot = async (req: NextRequest): Promise<NextResponse> => export const getOneBot = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.getOneBot(req); notFound();
export const sendMessage = async (req: NextRequest): Promise<NextResponse> => export const sendMessage = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.sendMessage(req); getService(req)?.sendMessage(req) ?? notFound();
export const receiveMessages = async ( export const receiveMessages = async (
req: NextRequest, req: NextRequest,
): Promise<NextResponse> => getService(req)?.receiveMessages(req); ): Promise<NextResponse> => getService(req)?.receiveMessages(req) ?? notFound();
export const registerBot = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.registerBot(req);
export const resetBot = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.resetBot(req);
export const requestCode = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.requestCode(req);
export const unverifyBot = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.unverifyBot(req);
export const refreshBot = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.refreshBot(req);
export const createBot = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.createBot(req);
export const deleteBot = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.deleteBot(req);
export const handleWebhook = async (req: NextRequest): Promise<NextResponse> => export const handleWebhook = async (req: NextRequest): Promise<NextResponse> =>
getService(req)?.handleWebhook(req); getService(req)?.handleWebhook(req) ?? notFound();

View file

@ -13,9 +13,16 @@ const entities = [
export type Entity = (typeof entities)[number]; export type Entity = (typeof entities)[number];
export type SelectOption = {
value: string;
label: string;
};
export type FieldDescription = { export type FieldDescription = {
name: string; name: string;
label: string; label: string;
kind?: "text" | "phone" | "select" | "multi";
getOptions?: (formState: any) => Promise<SelectOption[]>;
autogenerated?: "token"; autogenerated?: "token";
hidden?: boolean; hidden?: boolean;
type?: string; type?: string;
@ -39,17 +46,18 @@ export type ServiceConfig = {
listColumns: GridColDef[]; listColumns: GridColDef[];
}; };
export type Service = { export class Service {
getAllBots: (req: NextRequest) => Promise<NextResponse>; sendMessage: (req: NextRequest) => Promise<NextResponse> = async (req) => {
getOneBot: (req: NextRequest) => Promise<NextResponse>; return NextResponse.json({ ok: "nice" });
sendMessage: (req: NextRequest) => Promise<NextResponse>; };
receiveMessages: (req: NextRequest) => Promise<NextResponse>;
registerBot: (req: NextRequest) => Promise<NextResponse>; receiveMessages: (req: NextRequest) => Promise<NextResponse> = async (
resetBot: (req: NextRequest) => Promise<NextResponse>; req,
requestCode: (req: NextRequest) => Promise<NextResponse>; ) => {
unverifyBot: (req: NextRequest) => Promise<NextResponse>; return NextResponse.json({ ok: "nice" });
refreshBot: (req: NextRequest) => Promise<NextResponse>; };
createBot: (req: NextRequest) => Promise<NextResponse>;
deleteBot: (req: NextRequest) => Promise<NextResponse>; handleWebhook: (req: NextRequest) => Promise<NextResponse> = async (req) => {
handleWebhook: (req: NextRequest) => Promise<NextResponse>; return NextResponse.json({ ok: "nice" });
}; };
}

View file

@ -1,102 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { Service, ServiceConfig } from "./service"; import { Service } from "./service";
export const signalConfig: ServiceConfig = {
entity: "signal",
table: "SignalBot",
displayName: "Signal Connection",
createFields: [
{
name: "name",
label: "Name",
required: true,
size: 12,
},
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{
name: "phoneNumber",
label: "phoneNumber",
required: true,
},
{
name: "token",
label: "Token",
hidden: true,
required: true,
autogenerated: "token",
},
],
updateFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
},
{
name: "phoneNumber",
label: "phoneNumber",
required: true,
},
],
displayFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
},
{
name: "phoneNumber",
label: "phoneNumber",
},
{
name: "token",
label: "Token",
copyable: true,
},
],
listColumns: [
{
field: "name",
headerName: "Name",
flex: 1,
},
{
field: "phoneNumber",
headerName: "Phone Number",
flex: 1,
},
{
field: "description",
headerName: "Description",
flex: 2,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1,
},
],
};
const getAllBots = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const getOneBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const sendMessage = async (req: NextRequest) => { const sendMessage = async (req: NextRequest) => {
console.log({ req }); console.log({ req });
@ -110,48 +13,6 @@ const receiveMessages = async (req: NextRequest) => {
return NextResponse.json({ response: "ok" }); return NextResponse.json({ response: "ok" });
}; };
const registerBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const resetBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const requestCode = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const unverifyBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const refreshBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const createBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const deleteBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const handleWebhook = async (req: NextRequest) => { const handleWebhook = async (req: NextRequest) => {
console.log({ req }); console.log({ req });
@ -159,16 +20,7 @@ const handleWebhook = async (req: NextRequest) => {
}; };
export const Signal: Service = { export const Signal: Service = {
getAllBots,
getOneBot,
sendMessage, sendMessage,
receiveMessages, receiveMessages,
registerBot,
resetBot,
requestCode,
unverifyBot,
refreshBot,
createBot,
deleteBot,
handleWebhook, handleWebhook,
}; };

View file

@ -0,0 +1,13 @@
import { NextRequest } from "next/server";
import { Service } from "./service";
import { Facebook } from "./facebook";
const services: Record<string, Service> = {
facebook: Facebook,
};
export const getService = (req: NextRequest): Service => {
const service = req.nextUrl.pathname.split("/")?.[2] ?? "none";
return services[service];
};

View file

@ -1,93 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { Service, ServiceConfig } from "./service"; import { Service } from "./service";
export const voiceConfig: ServiceConfig = {
entity: "voice",
table: "VoiceLine",
displayName: "Voice Line",
createFields: [
{
name: "name",
label: "Name",
required: true,
size: 12,
},
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{
name: "phoneNumber",
label: "phoneNumber",
required: true,
},
],
updateFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
required: true,
size: 12,
},
{
name: "phoneNumber",
label: "Phone Number",
required: true,
},
],
displayFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
required: true,
size: 12,
},
{
name: "phoneNumber",
label: "Phone Number",
required: true,
},
],
listColumns: [
{
field: "name",
headerName: "Name",
flex: 1,
},
{
field: "phoneNumber",
headerName: "Phone Number",
flex: 1,
},
{
field: "description",
headerName: "Description",
flex: 2,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1,
},
],
};
const getAllBots = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const getOneBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const sendMessage = async (req: NextRequest) => { const sendMessage = async (req: NextRequest) => {
console.log({ req }); console.log({ req });
@ -101,48 +13,6 @@ const receiveMessages = async (req: NextRequest) => {
return NextResponse.json({ response: "ok" }); return NextResponse.json({ response: "ok" });
}; };
const registerBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const resetBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const requestCode = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const unverifyBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const refreshBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const createBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const deleteBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const handleWebhook = async (req: NextRequest) => { const handleWebhook = async (req: NextRequest) => {
console.log({ req }); console.log({ req });
@ -150,16 +20,7 @@ const handleWebhook = async (req: NextRequest) => {
}; };
export const Voice: Service = { export const Voice: Service = {
getAllBots,
getOneBot,
sendMessage, sendMessage,
receiveMessages, receiveMessages,
registerBot,
resetBot,
requestCode,
unverifyBot,
refreshBot,
createBot,
deleteBot,
handleWebhook, handleWebhook,
}; };

View file

@ -1,102 +1,5 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest, NextResponse } from "next/server";
import { Service, ServiceConfig } from "./service"; import { Service } from "./service";
export const whatsappConfig: ServiceConfig = {
entity: "whatsapp",
table: "WhatsappBot",
displayName: "WhatsApp Connection",
createFields: [
{
name: "name",
label: "Name",
required: true,
size: 12,
},
{
name: "description",
label: "Description",
size: 12,
lines: 3,
},
{
name: "phoneNumber",
label: "Phone Number",
required: true,
},
{
name: "token",
label: "Token",
hidden: true,
required: true,
autogenerated: "token",
},
],
updateFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
},
{
name: "phoneNumber",
label: "Phone Number",
required: true,
},
],
displayFields: [
{ name: "name", label: "Name", required: true, size: 12 },
{
name: "description",
label: "Description",
size: 12,
},
{
name: "phoneNumber",
label: "Phone Number",
},
{
name: "token",
label: "Token",
copyable: true,
},
],
listColumns: [
{
field: "name",
headerName: "Name",
flex: 1,
},
{
field: "phoneNumber",
headerName: "Phone Number",
flex: 1,
},
{
field: "description",
headerName: "Description",
flex: 2,
},
{
field: "updatedAt",
headerName: "Updated At",
valueGetter: (value: any) => new Date(value).toLocaleString(),
flex: 1,
},
],
};
const getAllBots = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const getOneBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const sendMessage = async (req: NextRequest) => { const sendMessage = async (req: NextRequest) => {
console.log({ req }); console.log({ req });
@ -110,48 +13,6 @@ const receiveMessages = async (req: NextRequest) => {
return NextResponse.json({ response: "ok" }); return NextResponse.json({ response: "ok" });
}; };
const registerBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const resetBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const requestCode = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const unverifyBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const refreshBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const createBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const deleteBot = async (req: NextRequest) => {
console.log({ req });
return NextResponse.json({ response: "ok" });
};
const handleWebhook = async (req: NextRequest) => { const handleWebhook = async (req: NextRequest) => {
console.log({ req }); console.log({ req });
@ -159,16 +20,7 @@ const handleWebhook = async (req: NextRequest) => {
}; };
export const Whatsapp: Service = { export const Whatsapp: Service = {
getAllBots,
getOneBot,
sendMessage, sendMessage,
receiveMessages, receiveMessages,
registerBot,
resetBot,
requestCode,
unverifyBot,
refreshBot,
createBot,
deleteBot,
handleWebhook, handleWebhook,
}; };

View file

@ -1 +0,0 @@
export { registerBot as POST } from "@/app/_lib/routing";

View file

@ -1 +0,0 @@
export { requestCode as POST } from "@/app/_lib/routing";

View file

@ -1 +0,0 @@
export { resetBot as POST } from "@/app/_lib/routing";

View file

@ -1 +0,0 @@
export { getAllBots as GET } from "@/app/_lib/routing";

View file

@ -1,8 +1,6 @@
import { NextRequest, NextResponse } from "next/server"; import { NextRequest } from "next/server";
import { handleWebhook } from "@/app/_lib/routing";
const handleRequest = async (req: NextRequest, res: NextResponse) => { const handleRequest = async (req: NextRequest) => handleWebhook(req);
return NextResponse.json({ message: "ok" });
};
export { handleRequest as GET, handleRequest as POST }; export { handleRequest as GET, handleRequest as POST };

View file

@ -26,6 +26,7 @@
"@mui/x-license": "^7.2.0", "@mui/x-license": "^7.2.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"graphile-worker": "^0.16.6",
"kysely": "^0.26.1", "kysely": "^0.26.1",
"material-ui-popup-state": "^5.1.0", "material-ui-popup-state": "^5.1.0",
"mui-chips-input": "^2.1.4", "mui-chips-input": "^2.1.4",

48
package-lock.json generated
View file

@ -38,6 +38,7 @@
"@mui/x-license": "^7.2.0", "@mui/x-license": "^7.2.0",
"date-fns": "^3.6.0", "date-fns": "^3.6.0",
"dotenv": "^16.4.5", "dotenv": "^16.4.5",
"graphile-worker": "^0.16.6",
"kysely": "^0.26.1", "kysely": "^0.26.1",
"material-ui-popup-state": "^5.1.0", "material-ui-popup-state": "^5.1.0",
"mui-chips-input": "^2.1.4", "mui-chips-input": "^2.1.4",
@ -76,6 +77,53 @@
"kysely": "^0.26.1" "kysely": "^0.26.1"
} }
}, },
"apps/bridge-frontend/node_modules/cosmiconfig": {
"version": "8.3.6",
"resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz",
"integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==",
"dependencies": {
"import-fresh": "^3.3.0",
"js-yaml": "^4.1.0",
"parse-json": "^5.2.0",
"path-type": "^4.0.0"
},
"engines": {
"node": ">=14"
},
"funding": {
"url": "https://github.com/sponsors/d-fischer"
},
"peerDependencies": {
"typescript": ">=4.9.5"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"apps/bridge-frontend/node_modules/graphile-worker": {
"version": "0.16.6",
"resolved": "https://registry.npmjs.org/graphile-worker/-/graphile-worker-0.16.6.tgz",
"integrity": "sha512-e7gGYDmGqzju2l83MpzX8vNG/lOtVJiSzI3eZpAFubSxh/cxs7sRrRGBGjzBP1kNG0H+c95etPpNRNlH65PYhw==",
"dependencies": {
"@graphile/logger": "^0.2.0",
"@types/debug": "^4.1.10",
"@types/pg": "^8.10.5",
"cosmiconfig": "^8.3.6",
"graphile-config": "^0.0.1-beta.4",
"json5": "^2.2.3",
"pg": "^8.11.3",
"tslib": "^2.6.2",
"yargs": "^17.7.2"
},
"bin": {
"graphile-worker": "dist/cli.js"
},
"engines": {
"node": ">=14.0.0"
}
},
"apps/bridge-frontend/node_modules/kysely": { "apps/bridge-frontend/node_modules/kysely": {
"version": "0.26.3", "version": "0.26.3",
"resolved": "https://registry.npmjs.org/kysely/-/kysely-0.26.3.tgz", "resolved": "https://registry.npmjs.org/kysely/-/kysely-0.26.3.tgz",

View file

@ -0,0 +1,127 @@
import { FC, useState } from "react";
import {
TextField as InternalTextField,
Grid,
Box,
IconButton,
} from "@mui/material";
import {
AddCircle as AddCircleIcon,
RemoveCircle as RemoveCircleIcon,
} from "@mui/icons-material";
import { colors, typography } from "../styles/theme";
const TextField: FC<any> = (props) => {
return (
<InternalTextField
fullWidth
size="small"
InputProps={{
sx: {
backgroundColor: "#fff",
},
}}
{...props}
/>
);
};
type MultiValueFieldProps = {
name: string;
label: string;
formState: Record<string, any>;
helperText?: string;
};
export const MultiValueField: FC<MultiValueFieldProps> = ({
name,
label,
formState,
helperText,
}) => {
const { darkMediumGray, mediumBlue, brightRed } = colors;
const { body } = typography;
const value = formState.values[name] || {};
const [fields, setFields] = useState<any[]>(Object.entries(value));
const addField = () => {
setFields([...fields, ["", ""]]);
};
const removeField = (index: number) => {
const newFields = [...fields];
newFields.splice(index, 1);
setFields(newFields);
};
const updateField = (index: number, position: number, value: any) => {
const newFields = [...fields];
newFields[index][position] = value;
setFields(newFields);
// formState.values[name] = Object.fromEntries(newFields);
// console.log(formState.values);
};
return (
<Box component="fieldset" sx={{ border: `1px solid ${darkMediumGray}` }}>
<Box
component="input"
name={name}
value={JSON.stringify(Object.fromEntries(fields))}
readOnly
hidden
/>
<Box
component="legend"
sx={{ ...body, color: darkMediumGray, fontSize: 14 }}
>
{label}
</Box>
<Grid container direction="column" spacing={2}>
<Grid
item
container
xs={12}
direction="row"
justifyContent="space-between"
alignItems="center"
>
<Grid item xs={10}>
<Box sx={{ ...body, color: darkMediumGray }}> {helperText}</Box>
</Grid>
<Grid item>
<IconButton sx={{ color: mediumBlue }} onClick={addField}>
<AddCircleIcon />
</IconButton>
</Grid>
</Grid>
{fields.map(([key, value], index) => (
<Grid key={index} item container direction="row" xs={12} spacing={2}>
<Grid item xs={5}>
<TextField
label="Key"
defaultValue={key}
onChange={(e: any) => updateField(index, 0, e.target.value)}
/>
</Grid>
<Grid item xs={6}>
<TextField
label="Value"
defaultValue={value}
onChange={(e: any) => updateField(index, 1, e.target.value)}
/>
</Grid>
<Grid item xs={1} sx={{ textAlign: "right" }}>
<IconButton
sx={{ color: brightRed }}
onClick={() => removeField(index)}
>
<RemoveCircleIcon />
</IconButton>
</Grid>
</Grid>
))}
</Grid>
</Box>
);
};

View file

@ -1,24 +1,45 @@
import { FC } from "react"; import { FC, useState, useEffect } from "react";
import { Select as InternalSelect } from "@mui/material"; import { Select as InternalSelect, MenuItem } from "@mui/material";
export type SelectOption = {
value: string;
label: string;
};
type SelectProps = { type SelectProps = {
name: string; name: string;
label: string; label: string;
formState: Record<string, any>; formState: Record<string, any>;
children: React.ReactNode; required?: boolean;
getOptions?: (formState: any) => Promise<SelectOption[]>;
}; };
export const Select: FC<SelectProps> = ({ export const Select: FC<SelectProps> = ({
name, name,
label, label,
formState, formState,
children, required = false,
}) => ( getOptions,
}) => {
const [options, setOptions] = useState([] as SelectOption[]);
useEffect(() => {
const fetchData = async () => {
if (getOptions) {
const opts = await getOptions(formState);
setOptions(opts);
}
};
fetchData();
}, [getOptions, formState]);
return (
<InternalSelect <InternalSelect
fullWidth fullWidth
name={name} name={name}
label={label} label={label}
variant="outlined" variant="outlined"
required={required}
size="small" size="small"
inputProps={{ id: name }} inputProps={{ id: name }}
defaultValue={formState.values[name] || ""} defaultValue={formState.values[name] || ""}
@ -26,6 +47,11 @@ export const Select: FC<SelectProps> = ({
backgroundColor: "#fff", backgroundColor: "#fff",
}} }}
> >
{children} {options.map((option: SelectOption) => (
<MenuItem key={option.value} value={option.value}>
{option.label}
</MenuItem>
))}
</InternalSelect> </InternalSelect>
); );
};

View file

@ -9,5 +9,6 @@ export { Button } from "./components/Button";
export { TextField } from "./components/TextField"; export { TextField } from "./components/TextField";
export { DisplayTextField } from "./components/DisplayTextField"; export { DisplayTextField } from "./components/DisplayTextField";
export { Select } from "./components/Select"; export { Select } from "./components/Select";
export { MultiValueField } from "./components/MultiValueField";
export { Dialog } from "./components/Dialog"; export { Dialog } from "./components/Dialog";
export { fonts, typography, colors } from "./styles/theme"; export { fonts, typography, colors } from "./styles/theme";

File diff suppressed because one or more lines are too long