Skip to content
Merged
17 changes: 8 additions & 9 deletions core/lib/server/errors.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,11 +2,11 @@ import { type NextApiRequest, type NextApiResponse } from "next/types";
import { ErrorHttpStatusCode } from "@ts-rest/core";
import { NextRequest, NextResponse } from "next/server";

export class HTTPStatusError <Status extends ErrorHttpStatusCode> extends Error {
export class HTTPStatusError<Status extends ErrorHttpStatusCode> extends Error {
readonly status: ErrorHttpStatusCode;

constructor(status: Status, message?: string) {
super(`HTTP Error ${status}${message ? ': ' + message : ''}`);
super(`HTTP Error ${status}${message ? ": " + message : ""}`);
this.status = status;
}
}
Expand Down Expand Up @@ -38,18 +38,17 @@ export class NotFoundError extends HTTPStatusError<404> {
// For use in app router API routes
export const handleErrors = async (routeHandler) => {
try {
return await routeHandler()
} catch(error) {
return await routeHandler();
} catch (error) {
if (error instanceof HTTPStatusError) {
return NextResponse.json({ message: error.message }, { status: error.status })
return NextResponse.json({ message: error.message }, { status: error.status });
}
if (error instanceof Error) {
console.error(error.message);
}
return NextResponse.json({ message: "Internal Server Error"}, { status: 500 })
return NextResponse.json({ message: "Internal Server Error" }, { status: 500 });
}

}
};

export const tsRestHandleErrors = (error: unknown, req: NextApiRequest, res: NextApiResponse) => {
if (error instanceof HTTPStatusError) {
Expand All @@ -59,4 +58,4 @@ export const tsRestHandleErrors = (error: unknown, req: NextApiRequest, res: Nex
console.error(error.message);
}
return res.status(500).json({ message: "Internal Server Error" });
};
};
3 changes: 2 additions & 1 deletion core/lib/server/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from "./pub";
export * from "./members";
export * from "./errors";
export * from "./errors";
export * from "./integrations";
56 changes: 56 additions & 0 deletions core/lib/server/integrations.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import prisma from "~/prisma/db";

export async function setIntegrationInstanceConfig(instanceId: string, config: object) {
return await prisma.integrationInstance.update({
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'd make this an upsert too

Copy link
Copy Markdown
Contributor Author

@qweliant qweliant Nov 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

are integrationInstances always going to be unconfigured before they are used? if not i think this should stay an update. it doesnt make sense to me why we would want to attach an unconfigured instance to a pub and expect users to vibe with that when its in an error state by default.

selfishly, making this an upsert also means we need to query for an integration somehow. i dont believe there exist a relationship between 'integrationInstances' and integrations in our schema unless an array of instances implies a one to many relationship.

this means we'd now have to include the integration and community id's in the instance url redirect to manage a pub. if im wrong about this and querying for a community or integration is trivial via some prisma query im unawre of i care less about what i said in the first and second paragraph though

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

im gonna keep this as update for now

Copy link
Copy Markdown
Contributor

@kalilsn kalilsn Nov 7, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

none of this is true 🙃
integrationInstances do have a relation to integrations and communities, so it isn't hard to query for those. we
do it in the createPub endpoint for example.

i do see your point about how this is different from instance state at the moment. since we don't actually have any way for a user to create an integration instance, we've been seeding them with a config, so they do start configured. but in the future, users are going to be able to add/remove integrations from core and they won't have any config. after they fill out the form to set the config, we'll need an endpoint to create it, or to make this an upsert.

so this will work fine for now, but i think you should add a todo about making this into an upsert and maybe change the name to updateIntegrationInstanceConfig, since set implies upsert to me!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

✨ you were never really here ✨

where: {
id: instanceId,
},
data: {
config,
},
});
}

export const getIntegrationInstanceConfig = async (instanceId: string) => {
return await prisma.integrationInstance.findFirst({
where: {
id: instanceId,
},
select: {
config: true,
},
});
};

export const setIntegrationInstanceState = async (instanceId: string, pubId: string, state) => {
return await prisma.integrationInstanceState.upsert({
where: {
pub_instance: {
instanceId,
pubId,
},
},
update: {
state,
},
create: {
instanceId,
pubId,
state,
},
});
};

export const getIntegrationInstanceState = async (instanceId: string, pubId: string) => {
return await prisma.integrationInstanceState.findUnique({
where: {
pub_instance: {
instanceId,
pubId,
},
},
select: {
state: true,
},
});
};
29 changes: 27 additions & 2 deletions core/pages/api/v0/[...ts-rest].ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,18 +3,23 @@ import { api } from "contracts";
import { compareAPIKeys, getBearerToken } from "~/lib/auth/api";
import {
createPub,
getSuggestedMembers,
deletePub,
getIntegrationInstanceConfig,
getIntegrationInstanceState,
getMembers,
getPub,
getPubType,
getSuggestedMembers,
setIntegrationInstanceConfig,
setIntegrationInstanceState,
tsRestHandleErrors,
updatePub,
deletePub,
} from "~/lib/server";
import { emailUser } from "~/lib/server/email";
import { getJobsClient } from "~/lib/server/jobs";
import { validateToken } from "~/lib/server/token";
import { findOrCreateUser } from "~/lib/server/user";
import prisma from "~/prisma/db";

const checkAuthentication = (authHeader: string) => {
const apiKey = getBearerToken(authHeader);
Expand Down Expand Up @@ -124,6 +129,26 @@ const integrationsRouter = createNextRoute(api.integrations, {
body: members,
};
},
setInstanceConfig: async ({ headers, params, body }) => {
checkAuthentication(headers.authorization);
const config = await setIntegrationInstanceConfig(params.instanceId, { ...body });
return { status: 200, body: config };
},
getInstanceConfig: async ({ headers, params }) => {
checkAuthentication(headers.authorization);
const config = await getIntegrationInstanceConfig(params.instanceId);
return { status: 200, body: config };
},
setInstanceState: async ({ headers, params, body }) => {
checkAuthentication(headers.authorization);
const state = await setIntegrationInstanceState(params.instanceId, params.pubId, body);
return { status: 200, body: state };
},
getInstanceState: async ({ headers, params }) => {
checkAuthentication(headers.authorization);
const state = await getIntegrationInstanceState(params.instanceId, params.pubId);
return { status: 200, body: state };
},
});

const router = {
Expand Down
7 changes: 7 additions & 0 deletions core/prisma/exampleCommunitySeeds/unjournal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -779,6 +779,13 @@ export default async function main(prisma: PrismaClient, communityUUID: string)
name: "Unjournal Evaluation Manager",
integrationId: evaluationsIntegration.id,
stageId: stageIds[3],
config: {
pubTypeId: submissionTypeId,
template: {
subject: "You've been invited to review a submission on PubPub",
message: `Please reach out if you have any questions.`,
},
},
},
];

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
-- AlterTable
ALTER TABLE "integration_instances" ADD COLUMN "config" JSONB;

-- AlterTable
ALTER TABLE "users" ALTER COLUMN "firstName" DROP DEFAULT,
ALTER COLUMN "lastName" DROP DEFAULT;
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
-- CreateTable
CREATE TABLE "IntegrationInstanceState" (
"pub_id" TEXT NOT NULL,
"instance_id" TEXT NOT NULL,
"value" JSONB NOT NULL
);

-- CreateIndex
CREATE UNIQUE INDEX "IntegrationInstanceState_pub_id_instance_id_key" ON "IntegrationInstanceState"("pub_id", "instance_id");

-- AddForeignKey
ALTER TABLE "IntegrationInstanceState" ADD CONSTRAINT "IntegrationInstanceState_pub_id_fkey" FOREIGN KEY ("pub_id") REFERENCES "pubs"("id") ON DELETE RESTRICT ON UPDATE CASCADE;

-- AddForeignKey
ALTER TABLE "IntegrationInstanceState" ADD CONSTRAINT "IntegrationInstanceState_instance_id_fkey" FOREIGN KEY ("instance_id") REFERENCES "integration_instances"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
/*
Warnings:

- You are about to drop the column `value` on the `IntegrationInstanceState` table. All the data in the column will be lost.
- Added the required column `state` to the `IntegrationInstanceState` table without a default value. This is not possible if the table is not empty.

*/
-- AlterTable
ALTER TABLE "IntegrationInstanceState" DROP COLUMN "value",
ADD COLUMN "state" JSONB NOT NULL;
27 changes: 20 additions & 7 deletions core/prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -76,12 +76,13 @@ model Pub {
parentId String? @map(name: "parent_id")
children Pub[] @relation("pub_parent")

values PubValue[]
stages Stage[]
claims ActionClaim[]
moves ActionMove[]
integrationInstances IntegrationInstance[]
permissions Permission[]
values PubValue[]
stages Stage[]
claims ActionClaim[]
moves ActionMove[]
integrationInstances IntegrationInstance[]
permissions Permission[]
IntegrationInstanceState IntegrationInstanceState[]

@@map(name: "pubs")
}
Expand Down Expand Up @@ -295,8 +296,20 @@ model IntegrationInstance {
updatedAt DateTime @default(now()) @updatedAt @map(name: "updated_at")
stage Stage? @relation(fields: [stageId], references: [id])
stageId String? @map(name: "stage_id")
config Json?

pubs Pub[]
pubs Pub[]
IntegrationInstanceState IntegrationInstanceState[]

@@map(name: "integration_instances")
}

model IntegrationInstanceState {
pub Pub @relation(fields: [pubId], references: [id])
pubId String @map(name: "pub_id")
instance IntegrationInstance @relation(fields: [instanceId], references: [id])
instanceId String @map(name: "instance_id")
state Json

@@unique([pubId, instanceId], name: "pub_instance")
}
2 changes: 1 addition & 1 deletion integrations/evaluations/app/actions/evaluate/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ export const evaluate = async (instanceId: string, pubId: string, values: PubVal
}
try {
const pub = await client.createPub(instanceId, {
pubTypeId: instance.pubTypeId,
pubTypeId: instance.config.pubTypeId,
parentId: pubId,
values: values,
});
Expand Down
2 changes: 1 addition & 1 deletion integrations/evaluations/app/actions/evaluate/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,6 @@ export default async function Page(props: Props) {

const instance = await getInstanceConfig(instanceId);
//dangerously assert instance exists
const pubType = await client.getPubType(instanceId, instance!.pubTypeId);
const pubType = await client.getPubType(instanceId, instance!.config.pubTypeId);
return <Evaluate instanceId={instanceId} pub={pub} pubType={pubType} />;
}
12 changes: 6 additions & 6 deletions integrations/evaluations/app/actions/manage/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ export const save = async (
}
const pub = await client.getPub(instanceId, pubId);
const evaluations = pub.children.filter(
(child) => child.pubTypeId === instanceConfig.pubTypeId
(child) => child.pubTypeId === instanceConfig.config.pubTypeId
);
const evaluationsByEvaluator = evaluations.reduce((acc, evaluation) => {
acc[evaluation.values["unjournal:evaluator"] as string] = evaluation;
Expand All @@ -45,7 +45,7 @@ export const save = async (
// New evaluator added. Make the corresponding evaluation pub.
await client.createPub(instanceId, {
parentId: pubId,
pubTypeId: instanceConfig.pubTypeId,
pubTypeId: instanceConfig.config.pubTypeId,
values: {
"unjournal:title": `Evaluation of ${pubTitle} by ${invite.firstName} ${invite.lastName}`,
"unjournal:evaluator": invite.userId,
Expand Down Expand Up @@ -76,12 +76,12 @@ export const save = async (
}
);
// Save updated email template and job run time
instanceState[invite.userId] = {
instanceState.state[invite.userId] = {
inviteTemplate: invite.template,
inviteTime: instanceState[invite.userId]?.inviteTime ?? runAt.toString(),
inviteTime: instanceState.state[invite.userId]?.inviteTime ?? runAt.toString(),
};
}
await setInstanceState(instanceId, pubId, instanceState);
await setInstanceState(instanceId, pubId, instanceState.state);
revalidatePath("/");
return { success: true };
} catch (error) {
Expand Down Expand Up @@ -109,7 +109,7 @@ export const remove = async (instanceId: string, pubId: string, userId: string)
await client.deletePub(instanceId, evaluation.id);
}
if (instanceState !== undefined) {
const { [userId]: _, ...instanceStateWithoutEvaluator } = instanceState;
const { [userId]: _, ...instanceStateWithoutEvaluator } = instanceState.state;
setInstanceState(instanceId, pubId, instanceStateWithoutEvaluator);
}
await client.unscheduleEmail(instanceId, makeInviteJobKey(instanceId, pubId, userId));
Expand Down
11 changes: 6 additions & 5 deletions integrations/evaluations/app/actions/manage/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ export default async function Page(props: Props) {
const { instanceId, pubId } = props.searchParams;
const instanceConfig = expect(await getInstanceConfig(instanceId));
const instanceState = (await getInstanceState(instanceId, pubId)) ?? {};
console.log(instanceState);
// Fetch the pub and its children
const pub = await client.getPub(instanceId, pubId);
// Load user info for each of the child evaluations
Expand All @@ -23,25 +24,25 @@ export default async function Page(props: Props) {
instanceId,
pub.children
// Only consider the children that are evaluations
.filter((child) => child.pubTypeId === instanceConfig.pubTypeId)
.filter((child) => child.pubTypeId === instanceConfig.config.pubTypeId)
// Extract the evaluator user id
.map((evaluation) => evaluation.values["unjournal:evaluator"] as string)
)
: [];

evaluators.sort(
(a, b) =>
new Date(instanceState[a.id]?.inviteTime).getTime() -
new Date(instanceState[b.id]?.inviteTime).getTime()
new Date(instanceState.state[a.id]?.inviteTime).getTime() -
new Date(instanceState.state[b.id]?.inviteTime).getTime()
);

return (
<EvaluatorInviteForm
instanceId={instanceId}
pub={pub}
evaluators={evaluators}
instanceConfig={instanceConfig}
instanceState={instanceState}
instanceConfig={instanceConfig.config}
instanceState={instanceState.state}
/>
);
}
4 changes: 2 additions & 2 deletions integrations/evaluations/app/configure/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,8 @@ export default async function Page(props: Props) {
return (
<Configure
instanceId={instanceId}
pubTypeId={instance?.pubTypeId}
template={instance?.template}
pubTypeId={instance?.config.pubTypeId}
template={instance?.config.template}
/>
);
}
Loading