Refactoring 2
This commit is contained in:
parent
dd14dfe72e
commit
e4b78ceec2
76 changed files with 870 additions and 734 deletions
|
|
@ -1,89 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
import { Grid } from "@mui/material";
|
||||
import { TextField, Select, MultiValueField } from "ui";
|
||||
import { Create as InternalCreate } from "@/app/_components/Create";
|
||||
import { generateCreateAction } from "@/app/_lib/actions";
|
||||
import { serviceConfig } from "@/app/_config/config";
|
||||
import { FieldDescription } from "@/app/_lib/service";
|
||||
|
||||
type CreateProps = {
|
||||
service: string;
|
||||
};
|
||||
|
||||
export const Create: FC<CreateProps> = ({ service }) => {
|
||||
const {
|
||||
[service]: { entity, table, displayName, createFields },
|
||||
} = 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 initialState = {
|
||||
message: null,
|
||||
errors: {},
|
||||
values: fields.reduce(
|
||||
(acc: Record<string, any>, field: FieldDescription) => {
|
||||
acc[field.name] = field.defaultValue;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
),
|
||||
};
|
||||
const [formState, formAction] = useFormState(createAction, initialState);
|
||||
|
||||
return (
|
||||
<InternalCreate
|
||||
title={`Create ${displayName}`}
|
||||
entity={entity}
|
||||
formAction={formAction}
|
||||
formState={formState}
|
||||
>
|
||||
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
||||
{createFields.map(
|
||||
(field) =>
|
||||
!field.hidden && (
|
||||
<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
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
lines={field.lines ?? 1}
|
||||
required={field.required ?? false}
|
||||
formState={formState}
|
||||
helperText={field.helperText}
|
||||
/>
|
||||
)}
|
||||
</Grid>
|
||||
),
|
||||
)}
|
||||
</Grid>
|
||||
</InternalCreate>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,4 +1,4 @@
|
|||
import { Create } from "./_components/Create";
|
||||
import { Create } from "bridge-ui";
|
||||
|
||||
type PageProps = {
|
||||
params: { segment: string[] };
|
||||
|
|
|
|||
|
|
@ -1,45 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
import { DisplayTextField } from "ui";
|
||||
import { Selectable } from "kysely";
|
||||
import { Database } from "@/app/_lib/database";
|
||||
import { Detail as InternalDetail } from "@/app/_components/Detail";
|
||||
import { generateDeleteAction } from "@/app/_lib/actions";
|
||||
import { serviceConfig } from "@/app/_config/config";
|
||||
|
||||
type DetailProps = {
|
||||
service: string;
|
||||
row: Selectable<keyof Database>;
|
||||
};
|
||||
|
||||
export const Detail: FC<DetailProps> = ({ service, row }) => {
|
||||
const {
|
||||
[service]: { entity, table, displayName, displayFields: fields },
|
||||
} = serviceConfig;
|
||||
const deleteAction = generateDeleteAction({ entity, table });
|
||||
|
||||
return (
|
||||
<InternalDetail
|
||||
title={`${displayName}: ${row.name}`}
|
||||
entity={entity}
|
||||
id={row.id as string}
|
||||
deleteAction={deleteAction}
|
||||
>
|
||||
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
||||
{fields.map((field) => (
|
||||
<Grid item xs={field.size ?? 6} key={field.name}>
|
||||
<DisplayTextField
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
lines={field.lines ?? 1}
|
||||
value={row[field.name] as string}
|
||||
copyable={field.copyable ?? false}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</InternalDetail>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { db } from "@/app/_lib/database";
|
||||
import { serviceConfig } from "@/app/_config/config";
|
||||
import { Detail } from "./_components/Detail";
|
||||
import { db } from "bridge-common";
|
||||
import { serviceConfig, Detail } from "bridge-ui";
|
||||
|
||||
type Props = {
|
||||
params: { segment: string[] };
|
||||
|
|
|
|||
|
|
@ -1,60 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { useFormState } from "react-dom";
|
||||
import { Grid } from "@mui/material";
|
||||
import { TextField } from "ui";
|
||||
import { Selectable } from "kysely";
|
||||
import { Database } from "@/app/_lib/database";
|
||||
import { Edit as InternalEdit } from "@/app/_components/Edit";
|
||||
import { generateUpdateAction } from "@/app/_lib/actions";
|
||||
import { serviceConfig } from "@/app/_config/config";
|
||||
|
||||
type EditProps = {
|
||||
service: string;
|
||||
row: Selectable<keyof Database>;
|
||||
};
|
||||
|
||||
export const Edit: FC<EditProps> = ({ service, row }) => {
|
||||
const {
|
||||
[service]: { entity, table, displayName, updateFields: fields },
|
||||
} = serviceConfig;
|
||||
const updateFieldNames = fields.map((val) => val.name);
|
||||
const updateAction = generateUpdateAction({ entity, table, fields });
|
||||
const updateValues = Object.fromEntries(
|
||||
Object.entries(row).filter(([key]) => updateFieldNames.includes(key)),
|
||||
);
|
||||
updateValues.id = row.id;
|
||||
const initialState = {
|
||||
message: null,
|
||||
errors: {},
|
||||
values: updateValues,
|
||||
};
|
||||
const [formState, formAction] = useFormState(updateAction, initialState);
|
||||
|
||||
return (
|
||||
<InternalEdit
|
||||
title={`Edit ${displayName}: ${row.name}`}
|
||||
entity={entity}
|
||||
formAction={formAction}
|
||||
formState={formState}
|
||||
>
|
||||
<Grid container direction="row" rowSpacing={3} columnSpacing={2}>
|
||||
{fields.map((field) => (
|
||||
<Grid key={field.name} item xs={field.size ?? 6}>
|
||||
<TextField
|
||||
name={field.name}
|
||||
label={field.label}
|
||||
lines={field.lines ?? 1}
|
||||
disabled={field.disabled ?? false}
|
||||
refreshable={field.refreshable ?? false}
|
||||
required={field.required ?? false}
|
||||
formState={formState}
|
||||
helperText={field.helperText}
|
||||
/>
|
||||
</Grid>
|
||||
))}
|
||||
</Grid>
|
||||
</InternalEdit>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,6 +1,5 @@
|
|||
import { db } from "@/app/_lib/database";
|
||||
import { serviceConfig } from "@/app/_config/config";
|
||||
import { Edit } from "./_components/Edit";
|
||||
import { db } from "bridge-common";
|
||||
import { serviceConfig, Edit } from "bridge-ui";
|
||||
|
||||
type PageProps = {
|
||||
params: { segment: string[] };
|
||||
|
|
|
|||
|
|
@ -1,25 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { List as InternalList } from "@/app/_components/List";
|
||||
import type { Selectable } from "kysely";
|
||||
import { Database } from "@/app/_lib/database";
|
||||
import { serviceConfig } from "@/app/_config/config";
|
||||
|
||||
type ListProps = {
|
||||
service: string;
|
||||
rows: Selectable<keyof Database>[];
|
||||
};
|
||||
|
||||
export const List: FC<ListProps> = ({ service, rows }) => {
|
||||
const { displayName, entity, listColumns } = serviceConfig[service];
|
||||
|
||||
return (
|
||||
<InternalList
|
||||
title={`${displayName}s`}
|
||||
entity={entity}
|
||||
rows={rows}
|
||||
columns={listColumns}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,32 +1,3 @@
|
|||
type ServiceLayoutProps = {
|
||||
children: any;
|
||||
detail: any;
|
||||
edit: any;
|
||||
create: any;
|
||||
params: {
|
||||
segment: string[];
|
||||
};
|
||||
};
|
||||
import { ServiceLayout } from "bridge-ui";
|
||||
|
||||
export default function ServiceLayout({
|
||||
children,
|
||||
detail,
|
||||
edit,
|
||||
create,
|
||||
params: { segment },
|
||||
}: ServiceLayoutProps) {
|
||||
const length = segment?.length ?? 0;
|
||||
const isCreate = length === 2 && segment[1] === "create";
|
||||
const isEdit = length === 3 && segment[2] === "edit";
|
||||
const id = length > 1 && !isCreate ? segment[1] : null;
|
||||
const isDetail = length === 2 && !!id && !isCreate && !isEdit;
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{isDetail && detail}
|
||||
{isEdit && edit}
|
||||
{isCreate && create}
|
||||
</>
|
||||
);
|
||||
}
|
||||
export default ServiceLayout;
|
||||
|
|
|
|||
|
|
@ -1,6 +1,5 @@
|
|||
import { List } from "./_components/List";
|
||||
import { db } from "@/app/_lib/database";
|
||||
import { serviceConfig } from "@/app/_config/config";
|
||||
import { db } from "bridge-common";
|
||||
import { serviceConfig, List } from "bridge-ui";
|
||||
|
||||
type PageProps = {
|
||||
params: {
|
||||
|
|
|
|||
|
|
@ -1,117 +0,0 @@
|
|||
"use server";
|
||||
|
||||
import { revalidatePath } from "next/cache";
|
||||
import { db, Database } from "@/app/_lib/database";
|
||||
import { FieldDescription, Entity } from "@/app/_lib/service";
|
||||
import crypto from "crypto";
|
||||
|
||||
const generateToken = () => {
|
||||
const length = 20;
|
||||
const randomBytes = crypto.randomBytes(length);
|
||||
const randomString = randomBytes.toString("hex").slice(0, length);
|
||||
|
||||
return randomString;
|
||||
};
|
||||
|
||||
type CreateActionArgs = {
|
||||
entity: Entity;
|
||||
table: keyof Database;
|
||||
fields: FieldDescription[];
|
||||
currentState: any;
|
||||
formData: FormData;
|
||||
};
|
||||
|
||||
export const createAction = async ({
|
||||
entity,
|
||||
table,
|
||||
fields,
|
||||
currentState,
|
||||
formData,
|
||||
}: CreateActionArgs) => {
|
||||
console.log(formData);
|
||||
const newRecord = fields.reduce(
|
||||
(acc: Record<string, any>, field: FieldDescription) => {
|
||||
if (field.autogenerated === "token") {
|
||||
acc[field.name] = generateToken();
|
||||
return acc;
|
||||
}
|
||||
|
||||
acc[field.name] = formData.get(field.name)?.toString() ?? null;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
console.log({ newRecord });
|
||||
const record = await db
|
||||
.insertInto(table)
|
||||
.values(newRecord)
|
||||
.returning(["id"])
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
console.log({ record });
|
||||
revalidatePath(`/${entity}`);
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
values: { ...newRecord, id: record.id },
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
type UpdateActionArgs = {
|
||||
entity: Entity;
|
||||
table: keyof Database;
|
||||
fields: FieldDescription[];
|
||||
currentState: any;
|
||||
formData: FormData;
|
||||
};
|
||||
|
||||
export const updateAction = async ({
|
||||
entity,
|
||||
table,
|
||||
fields,
|
||||
currentState,
|
||||
formData,
|
||||
}: UpdateActionArgs) => {
|
||||
const id = currentState.values.id;
|
||||
const updatedRecord = fields.reduce(
|
||||
(acc: Record<string, any>, field: FieldDescription) => {
|
||||
acc[field.name] = formData.get(field.name)?.toString() ?? null;
|
||||
return acc;
|
||||
},
|
||||
{},
|
||||
);
|
||||
|
||||
await db
|
||||
.updateTable(table)
|
||||
.set(updatedRecord)
|
||||
.where("id", "=", id)
|
||||
.executeTakeFirst();
|
||||
|
||||
revalidatePath(`/${entity}/${id}`);
|
||||
|
||||
return {
|
||||
...currentState,
|
||||
values: updatedRecord,
|
||||
success: true,
|
||||
};
|
||||
};
|
||||
|
||||
type DeleteActionArgs = {
|
||||
entity: Entity;
|
||||
table: keyof Database;
|
||||
id: string;
|
||||
};
|
||||
|
||||
export const deleteAction = async ({ entity, table, id }: DeleteActionArgs) => {
|
||||
await db.deleteFrom(table).where("id", "=", id).execute();
|
||||
|
||||
revalidatePath(`/${entity}`);
|
||||
|
||||
return true;
|
||||
};
|
||||
|
||||
export const selectAllAction = async (table: keyof Database) => {
|
||||
return db.selectFrom(table).selectAll().execute();
|
||||
};
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useEffect } from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Dialog } from "ui";
|
||||
|
||||
interface CreateProps {
|
||||
title: string;
|
||||
entity: string;
|
||||
formAction: any;
|
||||
formState: any;
|
||||
children: any;
|
||||
}
|
||||
|
||||
export const Create: FC<CreateProps> = ({
|
||||
title,
|
||||
entity,
|
||||
formAction,
|
||||
formState,
|
||||
children,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (formState.success) {
|
||||
router.push(`/${entity}/${formState.values.id}`);
|
||||
}
|
||||
}, [formState.success, router]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={title}
|
||||
formAction={formAction}
|
||||
onClose={() => router.push(`/${entity}`)}
|
||||
buttons={
|
||||
<Grid container justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Cancel"
|
||||
kind="secondary"
|
||||
onClick={() => router.push(`/${entity}`)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button text="Save" kind="primary" type="submit" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { Grid, Box } from "@mui/material";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { typography } from "@/app/_styles/theme";
|
||||
|
||||
interface DeleteDialogProps {
|
||||
title: string;
|
||||
entity: string;
|
||||
children: any;
|
||||
}
|
||||
|
||||
export const DeleteDialog: FC<DeleteDialogProps> = ({ title, entity, children }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const { h3 } = typography;
|
||||
|
||||
return (
|
||||
<Box sx={{ height: "100vh", backgroundColor: "#ddd", p: 3 }}>
|
||||
<Grid container direction="column">
|
||||
<Grid item>
|
||||
<Box sx={h3}>{title}</Box>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
{children}
|
||||
</Grid>
|
||||
</Grid>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,97 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useState } from "react";
|
||||
import { Box, Grid } from "@mui/material";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Dialog, Button, colors, typography } from "ui";
|
||||
|
||||
interface DetailProps {
|
||||
title: string;
|
||||
entity: string;
|
||||
id: string;
|
||||
children: any;
|
||||
deleteAction?: Function;
|
||||
}
|
||||
|
||||
export const Detail: FC<DetailProps> = ({
|
||||
title,
|
||||
entity,
|
||||
id,
|
||||
children,
|
||||
deleteAction,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
const { almostBlack } = colors;
|
||||
const { bodyLarge } = typography;
|
||||
const [showDeleteConfirmation, setShowDeleteConfirmation] = useState(false);
|
||||
|
||||
const continueDeleteAction = async () => {
|
||||
await deleteAction?.(id);
|
||||
setShowDeleteConfirmation(false);
|
||||
router.push(`/${entity}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<Dialog
|
||||
open
|
||||
title={title}
|
||||
onClose={() => router.push(`/${entity}`)}
|
||||
buttons={
|
||||
<Grid container justifyContent="space-between">
|
||||
<Grid item container xs="auto" spacing={2}>
|
||||
{deleteAction && (
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Delete"
|
||||
kind="destructive"
|
||||
onClick={() => setShowDeleteConfirmation(true)}
|
||||
/>
|
||||
</Grid>
|
||||
)}
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Edit"
|
||||
kind="secondary"
|
||||
href={`/${entity}/${id}/edit`}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button text="Done" kind="primary" href={`/${entity}`} />
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
<Dialog
|
||||
open={showDeleteConfirmation}
|
||||
size="xs"
|
||||
title="Really delete?"
|
||||
buttons={
|
||||
<Grid container justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Cancel"
|
||||
kind="secondary"
|
||||
onClick={() => setShowDeleteConfirmation(false)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Delete"
|
||||
kind="destructive"
|
||||
onClick={continueDeleteAction}
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
<Box sx={{ ...bodyLarge, color: almostBlack }}>
|
||||
Are you sure you want to delete this record?
|
||||
</Box>
|
||||
</Dialog>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -1,55 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC, useEffect } from "react";
|
||||
import { Grid } from "@mui/material";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { Button, Dialog } from "ui";
|
||||
|
||||
interface EditProps {
|
||||
title: string;
|
||||
entity: string;
|
||||
formAction: any;
|
||||
formState: any;
|
||||
children: any;
|
||||
}
|
||||
|
||||
export const Edit: FC<EditProps> = ({
|
||||
title,
|
||||
entity,
|
||||
formState,
|
||||
formAction,
|
||||
children,
|
||||
}) => {
|
||||
const router = useRouter();
|
||||
|
||||
useEffect(() => {
|
||||
if (formState.success) {
|
||||
router.push(`/${entity}`);
|
||||
}
|
||||
}, [formState.success, router, entity]);
|
||||
|
||||
return (
|
||||
<Dialog
|
||||
open
|
||||
title={title}
|
||||
formAction={formAction}
|
||||
onClose={() => router.push(`/${entity}`)}
|
||||
buttons={
|
||||
<Grid container justifyContent="space-between">
|
||||
<Grid item>
|
||||
<Button
|
||||
text="Cancel"
|
||||
kind="secondary"
|
||||
onClick={() => router.push(`/${entity}`)}
|
||||
/>
|
||||
</Grid>
|
||||
<Grid item>
|
||||
<Button text="Save" kind="primary" type="submit" />
|
||||
</Grid>
|
||||
</Grid>
|
||||
}
|
||||
>
|
||||
{children}
|
||||
</Dialog>
|
||||
);
|
||||
};
|
||||
|
|
@ -5,7 +5,7 @@ import { Grid } from "@mui/material";
|
|||
import { CssBaseline } from "@mui/material";
|
||||
import { SessionProvider } from "next-auth/react";
|
||||
import { css, Global } from "@emotion/react";
|
||||
import { fonts } from "@/app/_styles/theme";
|
||||
import { fonts } from "ui";
|
||||
import { Sidebar } from "./Sidebar";
|
||||
|
||||
export const InternalLayout: FC<PropsWithChildren> = ({ children }) => {
|
||||
|
|
|
|||
|
|
@ -1,35 +0,0 @@
|
|||
"use client";
|
||||
|
||||
import { FC } from "react";
|
||||
import { GridColDef } from "@mui/x-data-grid-pro";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { List as InternalList, Button } from "ui";
|
||||
import type { Selectable } from "kysely";
|
||||
import { Database } from "@/app/_lib/database";
|
||||
|
||||
interface ListProps {
|
||||
title: string;
|
||||
entity: string;
|
||||
rows: Selectable<keyof Database>[];
|
||||
columns: GridColDef<any>[];
|
||||
}
|
||||
|
||||
export const List: FC<ListProps> = ({ title, entity, rows, columns }) => {
|
||||
const router = useRouter();
|
||||
|
||||
const onRowClick = (id: string) => {
|
||||
router.push(`/${entity}/${id}`);
|
||||
};
|
||||
|
||||
return (
|
||||
<InternalList
|
||||
title={title}
|
||||
rows={rows}
|
||||
columns={columns}
|
||||
onRowClick={onRowClick}
|
||||
buttons={
|
||||
<Button text="Create" kind="primary" href={`/${entity}/create`} />
|
||||
}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
|
@ -16,8 +16,8 @@ import {
|
|||
} from "@mui/icons-material";
|
||||
import { signIn } from "next-auth/react";
|
||||
import Image from "next/image";
|
||||
import LinkLogo from "@/app/../public/link-logo-small.png";
|
||||
import { colors } from "@/app/_styles/theme";
|
||||
import LinkLogo from "@/app/_images/link-logo-small.png";
|
||||
import { colors } from "ui";
|
||||
import { useSearchParams } from "next/navigation";
|
||||
|
||||
type LoginProps = {
|
||||
|
|
|
|||
|
|
@ -1,32 +0,0 @@
|
|||
type ServiceLayoutProps = {
|
||||
children: any;
|
||||
detail: any;
|
||||
edit: any;
|
||||
create: any;
|
||||
params: {
|
||||
segment: string[];
|
||||
};
|
||||
};
|
||||
|
||||
export const ServiceLayout = ({
|
||||
children,
|
||||
detail,
|
||||
edit,
|
||||
create,
|
||||
params: { segment },
|
||||
}: ServiceLayoutProps) => {
|
||||
const length = segment?.length ?? 0;
|
||||
const isCreate = length === 1 && segment[0] === "create";
|
||||
const isEdit = length === 2 && segment[1] === "edit";
|
||||
const id = length > 0 && !isCreate ? segment[0] : null;
|
||||
const isDetail = length === 1 && !!id && !isCreate && !isEdit;
|
||||
|
||||
return (
|
||||
<>
|
||||
{children}
|
||||
{isDetail && detail}
|
||||
{isEdit && edit}
|
||||
{isCreate && create}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
|
@ -24,8 +24,8 @@ import {
|
|||
import { usePathname } from "next/navigation";
|
||||
import Link from "next/link";
|
||||
import Image from "next/image";
|
||||
import { typography, fonts } from "@/app/_styles/theme";
|
||||
import LinkLogo from "@/public/link-logo-small.png";
|
||||
import { typography, fonts } from "ui";
|
||||
import LinkLogo from "@/app/_images/link-logo-small.png";
|
||||
import { useSession, signOut } from "next-auth/react";
|
||||
|
||||
const openWidth = 270;
|
||||
|
|
|
|||
|
|
@ -1,16 +0,0 @@
|
|||
import type { ServiceConfig } from "@/app/_lib/service";
|
||||
import { facebookConfig as facebook } from "./facebook";
|
||||
import { signalConfig as signal } from "./signal";
|
||||
import { whatsappConfig as whatsapp } from "./whatsapp";
|
||||
import { voiceConfig as voice } from "./voice";
|
||||
import { webhooksConfig as webhooks } from "./webhooks";
|
||||
import { usersConfig as users } from "./users";
|
||||
|
||||
export const serviceConfig: Record<string, ServiceConfig> = {
|
||||
facebook,
|
||||
signal,
|
||||
whatsapp,
|
||||
voice,
|
||||
webhooks,
|
||||
users,
|
||||
};
|
||||
|
|
@ -1,123 +0,0 @@
|
|||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,56 +0,0 @@
|
|||
import { ServiceConfig } from "@/app/_lib/service";
|
||||
|
||||
export const usersConfig: ServiceConfig = {
|
||||
entity: "users",
|
||||
table: "User",
|
||||
displayName: "User",
|
||||
createFields: [
|
||||
{
|
||||
name: "name",
|
||||
label: "Name",
|
||||
required: true,
|
||||
size: 12,
|
||||
},
|
||||
{
|
||||
name: "email",
|
||||
label: "Email",
|
||||
required: true,
|
||||
size: 12,
|
||||
},
|
||||
],
|
||||
updateFields: [
|
||||
{ name: "name", label: "Name", required: true, size: 12 },
|
||||
{
|
||||
name: "email",
|
||||
label: "Email",
|
||||
required: true,
|
||||
size: 12,
|
||||
},
|
||||
],
|
||||
displayFields: [
|
||||
{ name: "name", label: "Name", required: true, size: 12 },
|
||||
{
|
||||
name: "email",
|
||||
label: "Email",
|
||||
size: 12,
|
||||
},
|
||||
],
|
||||
listColumns: [
|
||||
{
|
||||
field: "name",
|
||||
headerName: "Name",
|
||||
flex: 1,
|
||||
},
|
||||
{
|
||||
field: "email",
|
||||
headerName: "Email",
|
||||
flex: 2,
|
||||
},
|
||||
{
|
||||
field: "updatedAt",
|
||||
headerName: "Updated At",
|
||||
valueGetter: (value: any) => new Date(value).toLocaleString(),
|
||||
flex: 1,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import { ServiceConfig } from "@/app/_lib/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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,125 +0,0 @@
|
|||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
|
@ -1,86 +0,0 @@
|
|||
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,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
Before Width: | Height: | Size: 4.6 KiB After Width: | Height: | Size: 4.6 KiB |
|
|
@ -1,74 +0,0 @@
|
|||
import { Database } from "./database";
|
||||
import { FieldDescription, Entity } from "./service";
|
||||
import {
|
||||
createAction,
|
||||
updateAction,
|
||||
deleteAction,
|
||||
selectAllAction,
|
||||
} from "@/app/_actions/service";
|
||||
|
||||
type GenerateCreateActionArgs = {
|
||||
entity: Entity;
|
||||
table: keyof Database;
|
||||
fields: FieldDescription[];
|
||||
};
|
||||
|
||||
export function generateCreateAction({
|
||||
entity,
|
||||
table,
|
||||
fields,
|
||||
}: GenerateCreateActionArgs) {
|
||||
return async (currentState: any, formData: FormData) => {
|
||||
console.log({ entity, table, fields });
|
||||
console.log({ currentState, formData });
|
||||
return createAction({
|
||||
entity,
|
||||
table,
|
||||
fields,
|
||||
currentState,
|
||||
formData,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
type GenerateUpdateActionArgs = {
|
||||
entity: Entity;
|
||||
table: keyof Database;
|
||||
fields: FieldDescription[];
|
||||
};
|
||||
|
||||
export function generateUpdateAction({
|
||||
entity,
|
||||
table,
|
||||
fields,
|
||||
}: GenerateUpdateActionArgs) {
|
||||
return async (currentState: any, formData: FormData) => {
|
||||
return updateAction({
|
||||
entity,
|
||||
table,
|
||||
fields,
|
||||
currentState,
|
||||
formData,
|
||||
});
|
||||
};
|
||||
}
|
||||
|
||||
type GenerateDeleteActionArgs = {
|
||||
entity: Entity;
|
||||
table: keyof Database;
|
||||
};
|
||||
|
||||
export function generateDeleteAction({
|
||||
entity,
|
||||
table,
|
||||
}: GenerateDeleteActionArgs) {
|
||||
return async (id: string) => {
|
||||
return deleteAction({ entity, table, id });
|
||||
};
|
||||
}
|
||||
|
||||
export function generateSelectAllAction(table: keyof Database) {
|
||||
return async () => {
|
||||
return selectAllAction(table);
|
||||
};
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
import GoogleProvider from "next-auth/providers/google";
|
||||
import { KyselyAdapter } from "@auth/kysely-adapter";
|
||||
import { db } from "./database";
|
||||
import { db } from "bridge-common";
|
||||
|
||||
export const authOptions = {
|
||||
// @ts-ignore
|
||||
|
|
|
|||
|
|
@ -1,147 +0,0 @@
|
|||
import { PostgresDialect, CamelCasePlugin } from "kysely";
|
||||
import type {
|
||||
GeneratedAlways,
|
||||
Generated,
|
||||
ColumnType,
|
||||
Selectable,
|
||||
Insertable,
|
||||
Updateable,
|
||||
} from "kysely";
|
||||
import { Pool, types } from "pg";
|
||||
import { KyselyAuth } from "@auth/kysely-adapter";
|
||||
|
||||
type Timestamp = ColumnType<Date, Date | string>;
|
||||
|
||||
types.setTypeParser(types.builtins.TIMESTAMPTZ, (val) =>
|
||||
new Date(val).toISOString(),
|
||||
);
|
||||
|
||||
type GraphileJob = {
|
||||
taskIdentifier: string;
|
||||
payload: Record<string, any>;
|
||||
priority: number;
|
||||
maxAttempts: number;
|
||||
key: string;
|
||||
queueName: string;
|
||||
};
|
||||
|
||||
export const addGraphileJob = async (jobInfo: GraphileJob) => {
|
||||
// await db.insertInto("graphile_worker.jobs").values(jobInfo).execute();
|
||||
};
|
||||
|
||||
export interface Database {
|
||||
User: {
|
||||
id: string;
|
||||
name: string | null;
|
||||
email: string;
|
||||
emailVerified: Date | null;
|
||||
image: string | null;
|
||||
};
|
||||
|
||||
Account: {
|
||||
id: GeneratedAlways<string>;
|
||||
userId: string;
|
||||
type: "oidc" | "oauth" | "email" | "webauthn";
|
||||
provider: string;
|
||||
providerAccountId: string;
|
||||
refresh_token: string | undefined;
|
||||
access_token: string | undefined;
|
||||
expires_at: number | undefined;
|
||||
token_type: Lowercase<string> | undefined;
|
||||
scope: string | undefined;
|
||||
id_token: string | undefined;
|
||||
session_state: string | undefined;
|
||||
};
|
||||
|
||||
Session: {
|
||||
id: GeneratedAlways<string>;
|
||||
userId: string;
|
||||
sessionToken: string;
|
||||
expires: Date;
|
||||
};
|
||||
|
||||
VerificationToken: {
|
||||
identifier: string;
|
||||
token: string;
|
||||
expires: Date;
|
||||
};
|
||||
|
||||
WhatsappBot: {
|
||||
id: GeneratedAlways<string>;
|
||||
name: string;
|
||||
description: string;
|
||||
phoneNumber: string;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
FacebookBot: {
|
||||
id: GeneratedAlways<string>;
|
||||
name: string | null;
|
||||
description: string | null;
|
||||
token: string | null;
|
||||
pageAccessToken: string | null;
|
||||
appSecret: string | null;
|
||||
verifyToken: string | null;
|
||||
pageId: string | null;
|
||||
appId: string | null;
|
||||
userId: string | null;
|
||||
isVerified: Generated<boolean>;
|
||||
createdAt: GeneratedAlways<Timestamp>;
|
||||
updatedAt: GeneratedAlways<Timestamp>;
|
||||
};
|
||||
|
||||
VoiceLine: {
|
||||
id: GeneratedAlways<string>;
|
||||
name: string;
|
||||
description: string;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
SignalBot: {
|
||||
id: GeneratedAlways<string>;
|
||||
name: string;
|
||||
description: string;
|
||||
phoneNumber: string;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
|
||||
Webhook: {
|
||||
id: GeneratedAlways<string>;
|
||||
name: string;
|
||||
description: string;
|
||||
backendType: string;
|
||||
backendId: string;
|
||||
endpointUrl: string;
|
||||
httpMethod: "post" | "put";
|
||||
headers: Record<string, any>;
|
||||
createdBy: string;
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
};
|
||||
}
|
||||
|
||||
export type FacebookBot = Selectable<Database["FacebookBot"]>;
|
||||
export type SignalBot = Selectable<Database["SignalBot"]>;
|
||||
export type WhatsappBot = Selectable<Database["WhatsappBot"]>;
|
||||
export type VoiceLine = Selectable<Database["VoiceLine"]>;
|
||||
export type Webhook = Selectable<Database["Webhook"]>;
|
||||
export type User = Selectable<Database["User"]>;
|
||||
|
||||
export const db = new KyselyAuth<Database>({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
host: process.env.DATABASE_HOST,
|
||||
database: process.env.DATABASE_NAME,
|
||||
port: parseInt(process.env.DATABASE_PORT!),
|
||||
user: process.env.DATABASE_USER,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
}),
|
||||
}),
|
||||
plugins: [new CamelCasePlugin()],
|
||||
});
|
||||
|
|
@ -1,101 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { makeWorkerUtils, WorkerUtils } from "graphile-worker";
|
||||
import { Service } from "./service";
|
||||
import { db } from "./database";
|
||||
|
||||
let workerUtils: WorkerUtils;
|
||||
|
||||
const getWorkerUtils = async () => {
|
||||
if (!workerUtils) {
|
||||
workerUtils = await makeWorkerUtils({
|
||||
connectionString: process.env.DATABASE_URL,
|
||||
});
|
||||
}
|
||||
|
||||
return workerUtils;
|
||||
};
|
||||
|
||||
const sendMessage = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
const receiveMessages = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
const handleWebhook = async (req: NextRequest) => {
|
||||
const { searchParams } = req.nextUrl;
|
||||
const submittedToken = searchParams.get("hub.verify_token");
|
||||
|
||||
if (submittedToken) {
|
||||
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") {
|
||||
const challenge = searchParams.get("hub.challenge");
|
||||
console.log(submittedToken);
|
||||
console.log(challenge);
|
||||
|
||||
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" });
|
||||
};
|
||||
|
||||
export const Facebook: Service = {
|
||||
sendMessage,
|
||||
receiveMessages,
|
||||
handleWebhook,
|
||||
};
|
||||
|
|
@ -1,17 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { getService } from "./utils";
|
||||
|
||||
const notFound = () => new NextResponse(null, { status: 404 });
|
||||
|
||||
export const getOneBot = async (req: NextRequest): Promise<NextResponse> =>
|
||||
notFound();
|
||||
|
||||
export const sendMessage = async (req: NextRequest): Promise<NextResponse> =>
|
||||
getService(req)?.sendMessage(req) ?? notFound();
|
||||
|
||||
export const receiveMessages = async (
|
||||
req: NextRequest,
|
||||
): Promise<NextResponse> => getService(req)?.receiveMessages(req) ?? notFound();
|
||||
|
||||
export const handleWebhook = async (req: NextRequest): Promise<NextResponse> =>
|
||||
getService(req)?.handleWebhook(req) ?? notFound();
|
||||
|
|
@ -1,63 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { GridColDef } from "@mui/x-data-grid-pro";
|
||||
import { Database } from "./database";
|
||||
|
||||
const entities = [
|
||||
"facebook",
|
||||
"whatsapp",
|
||||
"signal",
|
||||
"voice",
|
||||
"webhooks",
|
||||
"users",
|
||||
] as const;
|
||||
|
||||
export type Entity = (typeof entities)[number];
|
||||
|
||||
export type SelectOption = {
|
||||
value: string;
|
||||
label: string;
|
||||
};
|
||||
|
||||
export type FieldDescription = {
|
||||
name: string;
|
||||
label: string;
|
||||
kind?: "text" | "phone" | "select" | "multi";
|
||||
getOptions?: (formState: any) => Promise<SelectOption[]>;
|
||||
autogenerated?: "token";
|
||||
hidden?: boolean;
|
||||
type?: string;
|
||||
lines?: number;
|
||||
copyable?: boolean;
|
||||
refreshable?: boolean;
|
||||
defaultValue?: string;
|
||||
required?: boolean;
|
||||
disabled?: boolean;
|
||||
size?: number;
|
||||
helperText?: string;
|
||||
};
|
||||
|
||||
export type ServiceConfig = {
|
||||
entity: Entity;
|
||||
table: keyof Database;
|
||||
displayName: string;
|
||||
createFields: FieldDescription[];
|
||||
updateFields: FieldDescription[];
|
||||
displayFields: FieldDescription[];
|
||||
listColumns: GridColDef[];
|
||||
};
|
||||
|
||||
export class Service {
|
||||
sendMessage: (req: NextRequest) => Promise<NextResponse> = async (req) => {
|
||||
return NextResponse.json({ ok: "nice" });
|
||||
};
|
||||
|
||||
receiveMessages: (req: NextRequest) => Promise<NextResponse> = async (
|
||||
req,
|
||||
) => {
|
||||
return NextResponse.json({ ok: "nice" });
|
||||
};
|
||||
|
||||
handleWebhook: (req: NextRequest) => Promise<NextResponse> = async (req) => {
|
||||
return NextResponse.json({ ok: "nice" });
|
||||
};
|
||||
}
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { Service } from "./service";
|
||||
|
||||
const sendMessage = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
const receiveMessages = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
const handleWebhook = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
export const Signal: Service = {
|
||||
sendMessage,
|
||||
receiveMessages,
|
||||
handleWebhook,
|
||||
};
|
||||
|
|
@ -1,13 +0,0 @@
|
|||
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];
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { Service } from "./service";
|
||||
|
||||
const sendMessage = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
const receiveMessages = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
const handleWebhook = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
export const Voice: Service = {
|
||||
sendMessage,
|
||||
receiveMessages,
|
||||
handleWebhook,
|
||||
};
|
||||
|
|
@ -1,26 +0,0 @@
|
|||
import { NextRequest, NextResponse } from "next/server";
|
||||
import { Service } from "./service";
|
||||
|
||||
const sendMessage = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
const receiveMessages = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
const handleWebhook = async (req: NextRequest) => {
|
||||
console.log({ req });
|
||||
|
||||
return NextResponse.json({ response: "ok" });
|
||||
};
|
||||
|
||||
export const Whatsapp: Service = {
|
||||
sendMessage,
|
||||
receiveMessages,
|
||||
handleWebhook,
|
||||
};
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
import { Roboto, Playfair_Display, Poppins } from "next/font/google";
|
||||
|
||||
const roboto = Roboto({
|
||||
weight: ["400"],
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const playfair = Playfair_Display({
|
||||
weight: ["900"],
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
const poppins = Poppins({
|
||||
weight: ["400", "700"],
|
||||
subsets: ["latin"],
|
||||
display: "swap",
|
||||
});
|
||||
|
||||
export const fonts = {
|
||||
roboto,
|
||||
playfair,
|
||||
poppins,
|
||||
};
|
||||
|
||||
export const colors: any = {
|
||||
lightGray: "#ededf0",
|
||||
mediumGray: "#e3e5e5",
|
||||
darkGray: "#33302f",
|
||||
mediumBlue: "#4285f4",
|
||||
green: "#349d7b",
|
||||
lavender: "#a5a6f6",
|
||||
darkLavender: "#5d5fef",
|
||||
pink: "#fcddec",
|
||||
cdrLinkOrange: "#ff7115",
|
||||
coreYellow: "#fac942",
|
||||
helpYellow: "#fff4d5",
|
||||
dwcDarkBlue: "#191847",
|
||||
hazyMint: "#ecf7f8",
|
||||
leafcutterElectricBlue: "#4d6aff",
|
||||
leafcutterLightBlue: "#fafbfd",
|
||||
waterbearElectricPurple: "#332c83",
|
||||
waterbearLightSmokePurple: "#eff3f8",
|
||||
bumpedPurple: "#212058",
|
||||
mutedPurple: "#373669",
|
||||
warningPink: "#ef5da8",
|
||||
lightPink: "#fff0f7",
|
||||
lightGreen: "#f0fff3",
|
||||
lightOrange: "#fff5f0",
|
||||
beige: "#f6f2f1",
|
||||
almostBlack: "#33302f",
|
||||
white: "#ffffff",
|
||||
};
|
||||
|
||||
export const typography: any = {
|
||||
h1: {
|
||||
fontFamily: playfair.style.fontFamily,
|
||||
fontSize: 45,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
margin: 0,
|
||||
},
|
||||
h2: {
|
||||
fontFamily: poppins.style.fontFamily,
|
||||
fontSize: 35,
|
||||
fontWeight: 700,
|
||||
lineHeight: 1.1,
|
||||
margin: 0,
|
||||
},
|
||||
h3: {
|
||||
fontFamily: poppins.style.fontFamily,
|
||||
fontWeight: 400,
|
||||
fontSize: 27,
|
||||
lineHeight: 1.1,
|
||||
margin: 0,
|
||||
},
|
||||
h4: {
|
||||
fontFamily: poppins.style.fontFamily,
|
||||
fontWeight: 700,
|
||||
fontSize: 18,
|
||||
},
|
||||
h5: {
|
||||
fontFamily: roboto.style.fontFamily,
|
||||
fontWeight: 700,
|
||||
fontSize: 16,
|
||||
lineHeight: "24px",
|
||||
textTransform: "uppercase",
|
||||
textAlign: "center",
|
||||
margin: 1,
|
||||
},
|
||||
h6: {
|
||||
fontFamily: roboto.style.fontFamily,
|
||||
fontWeight: 400,
|
||||
fontSize: 14,
|
||||
textAlign: "center",
|
||||
},
|
||||
p: {
|
||||
fontFamily: roboto.style.fontFamily,
|
||||
fontSize: 17,
|
||||
lineHeight: "26.35px",
|
||||
fontWeight: 400,
|
||||
margin: 0,
|
||||
},
|
||||
small: {
|
||||
fontFamily: roboto.style.fontFamily,
|
||||
fontSize: 13,
|
||||
lineHeight: "18px",
|
||||
fontWeight: 400,
|
||||
margin: 0,
|
||||
},
|
||||
};
|
||||
|
|
@ -1 +0,0 @@
|
|||
export { receiveMessages as GET } from "@/app/_lib/routing";
|
||||
|
|
@ -1 +1 @@
|
|||
export { getOneBot as GET } from "@/app/_lib/routing";
|
||||
export { getBot as GET } from "bridge-ui";
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
export { sendMessage as POST } from "@/app/_lib/routing";
|
||||
export { sendMessage as POST } from "bridge-ui";
|
||||
|
|
|
|||
|
|
@ -1,6 +1,3 @@
|
|||
import { NextRequest } from "next/server";
|
||||
import { handleWebhook } from "@/app/_lib/routing";
|
||||
import { handleWebhook } from "bridge-ui";
|
||||
|
||||
const handleRequest = async (req: NextRequest) => handleWebhook(req);
|
||||
|
||||
export { handleRequest as GET, handleRequest as POST };
|
||||
export { handleWebhook as GET, handleWebhook as POST };
|
||||
|
|
|
|||
|
|
@ -1,93 +0,0 @@
|
|||
import * as path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { promises as fs } from "fs";
|
||||
import {
|
||||
Kysely,
|
||||
Migrator,
|
||||
MigrationResult,
|
||||
FileMigrationProvider,
|
||||
PostgresDialect,
|
||||
CamelCasePlugin,
|
||||
} from "kysely";
|
||||
import pkg from "pg";
|
||||
const { Pool } = pkg;
|
||||
import * as dotenv from "dotenv";
|
||||
|
||||
interface Database {}
|
||||
|
||||
export const migrate = async (arg: string) => {
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = path.dirname(__filename);
|
||||
if (process.env.NODE_ENV !== "production") {
|
||||
dotenv.config({ path: path.join(__dirname, "../.env.local") });
|
||||
}
|
||||
const db = new Kysely<Database>({
|
||||
dialect: new PostgresDialect({
|
||||
pool: new Pool({
|
||||
host: process.env.DATABASE_HOST,
|
||||
database: process.env.DATABASE_NAME,
|
||||
port: parseInt(process.env.DATABASE_PORT!),
|
||||
user: process.env.DATABASE_USER,
|
||||
password: process.env.DATABASE_PASSWORD,
|
||||
}),
|
||||
}),
|
||||
plugins: [new CamelCasePlugin()],
|
||||
});
|
||||
const migrator = new Migrator({
|
||||
db,
|
||||
provider: new FileMigrationProvider({
|
||||
fs,
|
||||
path,
|
||||
migrationFolder: path.join(__dirname, "migrations"),
|
||||
}),
|
||||
});
|
||||
|
||||
let error = null;
|
||||
let results: MigrationResult[] = [];
|
||||
|
||||
if (arg === "up:all") {
|
||||
const out = await migrator.migrateToLatest();
|
||||
results = out.results ?? [];
|
||||
error = out.error;
|
||||
} else if (arg === "up:one") {
|
||||
const out = await migrator.migrateUp();
|
||||
results = out.results ?? [];
|
||||
error = out.error;
|
||||
} else if (arg === "down:all") {
|
||||
const migrations = await migrator.getMigrations();
|
||||
for (const _ of migrations) {
|
||||
const out = await migrator.migrateDown();
|
||||
if (out.results) {
|
||||
results = results.concat(out.results);
|
||||
error = out.error;
|
||||
}
|
||||
}
|
||||
} else if (arg === "down:one") {
|
||||
const out = await migrator.migrateDown();
|
||||
if (out.results) {
|
||||
results = out.results ?? [];
|
||||
error = out.error;
|
||||
}
|
||||
}
|
||||
|
||||
results?.forEach((it) => {
|
||||
if (it.status === "Success") {
|
||||
console.log(
|
||||
`Migration "${it.migrationName} ${it.direction.toLowerCase()}" was executed successfully`,
|
||||
);
|
||||
} else if (it.status === "Error") {
|
||||
console.error(`Failed to execute migration "${it.migrationName}"`);
|
||||
}
|
||||
});
|
||||
|
||||
if (error) {
|
||||
console.error("Failed to migrate");
|
||||
console.error(error);
|
||||
process.exit(1);
|
||||
}
|
||||
|
||||
await db.destroy();
|
||||
};
|
||||
|
||||
const arg = process.argv.slice(2).pop();
|
||||
migrate(arg as string);
|
||||
|
|
@ -1,72 +0,0 @@
|
|||
import { Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable("User")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("name", "text")
|
||||
.addColumn("email", "text", (col) => col.unique().notNull())
|
||||
.addColumn("emailVerified", "timestamptz")
|
||||
.addColumn("image", "text")
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable("Account")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("userId", "uuid", (col) =>
|
||||
col.references("User.id").onDelete("cascade").notNull(),
|
||||
)
|
||||
.addColumn("type", "text", (col) => col.notNull())
|
||||
.addColumn("provider", "text", (col) => col.notNull())
|
||||
.addColumn("providerAccountId", "text", (col) => col.notNull())
|
||||
.addColumn("refresh_token", "text")
|
||||
.addColumn("access_token", "text")
|
||||
.addColumn("expires_at", "bigint")
|
||||
.addColumn("token_type", "text")
|
||||
.addColumn("scope", "text")
|
||||
.addColumn("id_token", "text")
|
||||
.addColumn("session_state", "text")
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable("Session")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("userId", "uuid", (col) =>
|
||||
col.references("User.id").onDelete("cascade").notNull(),
|
||||
)
|
||||
.addColumn("sessionToken", "text", (col) => col.notNull().unique())
|
||||
.addColumn("expires", "timestamptz", (col) => col.notNull())
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable("VerificationToken")
|
||||
.addColumn("identifier", "text", (col) => col.notNull())
|
||||
.addColumn("token", "text", (col) => col.notNull().unique())
|
||||
.addColumn("expires", "timestamptz", (col) => col.notNull())
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("Account_userId_index")
|
||||
.on("Account")
|
||||
.column("userId")
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("Session_userId_index")
|
||||
.on("Session")
|
||||
.column("userId")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable("Account").ifExists().execute();
|
||||
await db.schema.dropTable("Session").ifExists().execute();
|
||||
await db.schema.dropTable("User").ifExists().execute();
|
||||
await db.schema.dropTable("VerificationToken").ifExists().execute();
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable("SignalBot")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("phone_number", "text")
|
||||
.addColumn("token", "text", (col) => col.unique().notNull())
|
||||
.addColumn("user_id", "uuid")
|
||||
.addColumn("name", "text")
|
||||
.addColumn("description", "text")
|
||||
.addColumn("auth_info", "text")
|
||||
.addColumn("is_verified", "boolean", (col) =>
|
||||
col.notNull().defaultTo(false),
|
||||
)
|
||||
.addColumn("created_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn("updated_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("SignalBotToken")
|
||||
.on("SignalBot")
|
||||
.column("token")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable("SignalBot").ifExists().execute();
|
||||
}
|
||||
|
|
@ -1,35 +0,0 @@
|
|||
import { Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable("WhatsappBot")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("phone_number", "text")
|
||||
.addColumn("token", "text", (col) => col.unique().notNull())
|
||||
.addColumn("user_id", "uuid")
|
||||
.addColumn("name", "text")
|
||||
.addColumn("description", "text")
|
||||
.addColumn("auth_info", "text")
|
||||
.addColumn("is_verified", "boolean", (col) =>
|
||||
col.notNull().defaultTo(false),
|
||||
)
|
||||
.addColumn("created_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn("updated_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("WhatsappBotToken")
|
||||
.on("WhatsappBot")
|
||||
.column("token")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable("WhatsappBot").ifExists().execute();
|
||||
}
|
||||
|
|
@ -1,77 +0,0 @@
|
|||
import { Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable("VoiceProvider")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("kind", "text", (col) => col.notNull())
|
||||
.addColumn("name", "text", (col) => col.notNull())
|
||||
.addColumn("description", "text")
|
||||
.addColumn("credentials", "jsonb", (col) => col.notNull())
|
||||
.addColumn("created_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn("updated_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("VoiceProviderName")
|
||||
.on("VoiceProvider")
|
||||
.column("name")
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createTable("VoiceLine")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("provider_id", "uuid", (col) =>
|
||||
col.notNull().references("VoiceProvider.id").onDelete("cascade"),
|
||||
)
|
||||
.addColumn("provider_line_sid", "text", (col) => col.notNull())
|
||||
.addColumn("number", "text", (col) => col.notNull())
|
||||
.addColumn("name", "text", (col) => col.notNull())
|
||||
.addColumn("description", "text")
|
||||
.addColumn("language", "text", (col) => col.notNull())
|
||||
.addColumn("voice", "text", (col) => col.notNull())
|
||||
.addColumn("prompt_text", "text")
|
||||
.addColumn("prompt_audio", "jsonb")
|
||||
.addColumn("audio_prompt_enabled", "boolean", (col) =>
|
||||
col.notNull().defaultTo(false),
|
||||
)
|
||||
.addColumn("audio_converted_at", "timestamptz")
|
||||
.addColumn("created_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn("updated_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("VoiceLineProviderId")
|
||||
.on("VoiceLine")
|
||||
.column("provider_id")
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("VoiceLineProviderLineSid")
|
||||
.on("VoiceLine")
|
||||
.column("provider_line_sid")
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("VoiceLineNumber")
|
||||
.on("VoiceLine")
|
||||
.column("number")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable("VoiceLine").ifExists().execute();
|
||||
await db.schema.dropTable("VoiceProvider").ifExists().execute();
|
||||
}
|
||||
|
|
@ -1,38 +0,0 @@
|
|||
import { Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable("FacebookBot")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("name", "text")
|
||||
.addColumn("description", "text")
|
||||
.addColumn("token", "text")
|
||||
.addColumn("page_access_token", "text")
|
||||
.addColumn("app_secret", "text")
|
||||
.addColumn("verify_token", "text")
|
||||
.addColumn("page_id", "text")
|
||||
.addColumn("app_id", "text")
|
||||
.addColumn("user_id", "uuid")
|
||||
.addColumn("is_verified", "boolean", (col) =>
|
||||
col.notNull().defaultTo(false),
|
||||
)
|
||||
.addColumn("created_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn("updated_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("FacebookBotToken")
|
||||
.on("FacebookBot")
|
||||
.column("token")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable("FacebookBot").ifExists().execute();
|
||||
}
|
||||
|
|
@ -1,41 +0,0 @@
|
|||
import { Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable("Webhook")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("name", "text", (col) => col.notNull())
|
||||
.addColumn("description", "text")
|
||||
.addColumn("backend_type", "text", (col) => col.notNull())
|
||||
.addColumn("backend_id", "uuid", (col) => col.notNull())
|
||||
.addColumn("endpoint_url", "text", (col) =>
|
||||
col.notNull().check(sql`endpoint_url ~ '^https?://[^/]+'`),
|
||||
)
|
||||
.addColumn("http_method", "text", (col) =>
|
||||
col
|
||||
.notNull()
|
||||
.defaultTo("post")
|
||||
.check(sql`http_method in ('post', 'put')`),
|
||||
)
|
||||
.addColumn("headers", "jsonb")
|
||||
.addColumn("created_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn("updated_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("WebhookBackendTypeBackendId")
|
||||
.on("Webhook")
|
||||
.column("backend_type")
|
||||
.column("backend_id")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable("Webhook").ifExists().execute();
|
||||
}
|
||||
|
|
@ -1,28 +0,0 @@
|
|||
import { Kysely, sql } from "kysely";
|
||||
|
||||
export async function up(db: Kysely<any>): Promise<void> {
|
||||
await db.schema
|
||||
.createTable("Setting")
|
||||
.addColumn("id", "uuid", (col) =>
|
||||
col.primaryKey().defaultTo(sql`gen_random_uuid()`),
|
||||
)
|
||||
.addColumn("name", "text")
|
||||
.addColumn("value", "jsonb")
|
||||
.addColumn("created_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.addColumn("updated_at", "timestamptz", (col) =>
|
||||
col.notNull().defaultTo(sql`now()`),
|
||||
)
|
||||
.execute();
|
||||
|
||||
await db.schema
|
||||
.createIndex("SettingName")
|
||||
.on("Setting")
|
||||
.column("name")
|
||||
.execute();
|
||||
}
|
||||
|
||||
export async function down(db: Kysely<any>): Promise<void> {
|
||||
await db.schema.dropTable("Setting").ifExists().execute();
|
||||
}
|
||||
|
|
@ -1,6 +1,6 @@
|
|||
/** @type {import('next').NextConfig} */
|
||||
const nextConfig = {
|
||||
transpilePackages: ["ui"],
|
||||
transpilePackages: ["ui", "bridge-common", "bridge-ui"],
|
||||
};
|
||||
|
||||
export default nextConfig;
|
||||
|
|
|
|||
|
|
@ -41,7 +41,9 @@
|
|||
"react-timer-hook": "^3.0.7",
|
||||
"tss-react": "^4.9.7",
|
||||
"tsx": "^4.7.3",
|
||||
"ui": "*"
|
||||
"ui": "*",
|
||||
"bridge-common": "*",
|
||||
"bridge-ui": "*"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/node": "^20",
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@
|
|||
"html-to-text": "^9.0.5",
|
||||
"jest": "^29.7.0",
|
||||
"kysely": "^0.27.3",
|
||||
"bridge-common": "*",
|
||||
"pg": "^8.11.5",
|
||||
"remeda": "^1.61.0",
|
||||
"twilio": "^5.0.4"
|
||||
|
|
|
|||
|
|
@ -1,12 +1,27 @@
|
|||
interface ReceiveFacebookMessageTaskOptions {}
|
||||
import { db, getWorkerUtils } from "bridge-common";
|
||||
|
||||
const receiveFacebookMessageTask = async (
|
||||
options: ReceiveFacebookMessageTaskOptions,
|
||||
): Promise<void> => {
|
||||
console.log(options);
|
||||
// withDb(async (db: AppDatabase) => {
|
||||
// await notifyWebhooks(db, options);
|
||||
// });
|
||||
interface ReceiveFacebookMessageTaskOptions {
|
||||
message: any;
|
||||
}
|
||||
|
||||
const receiveFacebookMessageTask = async ({
|
||||
message,
|
||||
}: ReceiveFacebookMessageTaskOptions): Promise<void> => {
|
||||
const worker = await getWorkerUtils();
|
||||
|
||||
for (const entry of message.entry) {
|
||||
for (const messaging of entry.messaging) {
|
||||
const pageId = messaging.recipient.id;
|
||||
const row = await db
|
||||
.selectFrom("FacebookBot")
|
||||
.selectAll()
|
||||
.where("pageId", "=", pageId)
|
||||
.executeTakeFirstOrThrow();
|
||||
|
||||
console.log({ row });
|
||||
await worker.addJob("notify_webhooks", messaging);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
export default receiveFacebookMessageTask;
|
||||
|
|
|
|||
|
|
@ -4,6 +4,58 @@ const sendFacebookMessageTask = async (
|
|||
options: SendFacebookMessageTaskOptions,
|
||||
): Promise<void> => {
|
||||
console.log(options);
|
||||
|
||||
|
||||
const message = await req.json();
|
||||
|
||||
const message = await req.json();
|
||||
for (const entry of message.entry) {
|
||||
for (const messaging of entry.messaging) {
|
||||
const pageId = messaging.recipient.id;
|
||||
const row = await db
|
||||
.selectFrom("FacebookBot")
|
||||
.selectAll()
|
||||
.where("pageId", "=", pageId)
|
||||
.executeTakeFirst();
|
||||
|
||||
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" });
|
||||
|
||||
// withDb(async (db: AppDatabase) => {
|
||||
// await notifyWebhooks(db, options);
|
||||
// });
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue