Skip to content

Commit

Permalink
Record action runs
Browse files Browse the repository at this point in the history
  • Loading branch information
3mcd committed May 9, 2024
1 parent ae535d8 commit 8ca0152
Show file tree
Hide file tree
Showing 10 changed files with 252 additions and 62 deletions.
165 changes: 112 additions & 53 deletions core/actions/_lib/runActionInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,68 +4,67 @@ import { revalidateTag } from "next/cache";
import { captureException } from "@sentry/nextjs";
import { sql } from "kysely";

import { GetPubResponseBody } from "contracts";
import { logger } from "logger";

import type { ActionInstancesId } from "~/kysely/types/public/ActionInstances";
import type Event from "~/kysely/types/public/Event";
import type { PubsId } from "~/kysely/types/public/Pubs";
import type { StagesId } from "~/kysely/types/public/Stages";
import { db } from "~/kysely/database";
import Action from "~/kysely/types/public/Action";
import ActionRunStatus from "~/kysely/types/public/ActionRunStatus";
import { getPub } from "~/lib/server";
import { defineServerAction } from "~/lib/server/defineServerAction";
import { ClientException, ClientExceptionOptions } from "~/lib/serverActions";
import { getActionByName } from "../api";
import { ActionSuccess } from "../types";
import { getActionRunByName } from "./getRuns";
import { validatePubValues } from "./validateFields";

type ActionInstanceRunResult = ClientException | ClientExceptionOptions | ActionSuccess;

type ActionInstanceArgs = {
pubId: PubsId;
actionInstanceId: ActionInstancesId;
runParameters?: Record<string, unknown>;
};

const _runActionInstance = async ({
pubId,
actionInstanceId,
runParameters = {},
}: ActionInstanceArgs) => {
const pubPromise = getPub(pubId);

const actionInstancePromise = db
.selectFrom("action_instances")
.where("action_instances.id", "=", actionInstanceId)
.select((eb) => [
"id",
eb.fn.coalesce("config", sql`'{}'`).as("config"),
"created_at as createdAt",
"updated_at as updatedAt",
"stage_id as stageId",
"action",
])
.executeTakeFirstOrThrow();

const [pubResult, actionInstanceResult] = await Promise.allSettled([
pubPromise,
actionInstancePromise,
]);

if (pubResult.status === "rejected") {
return {
error: "Pub not found",
cause: pubResult.reason,
};
}

if (actionInstanceResult.status === "rejected") {
logger.debug({ msg: actionInstanceResult.reason });
return {
error: "Action instance not found",
cause: actionInstanceResult.reason,
};
}
const recordActionRun = async (
actionInstanceId: ActionInstancesId,
pubId: PubsId,
result: ActionInstanceRunResult,
config?: unknown,
params?: unknown,
event?: Event
) => {
const actionRun = await db
.insertInto("action_runs")
.values({
action_instance_id: actionInstanceId,
pub_id: pubId,
status: "error" in result ? ActionRunStatus.failure : ActionRunStatus.success,
config,
params,
event,
})
.execute();

const actionInstance = actionInstanceResult.value;
const pub = pubResult.value;
return actionRun;
};

const _runActionInstance = async (
actionInstance: {
id: ActionInstancesId;
action: Action;
config: unknown;
createdAt: Date;
updatedAt: Date;
stageId: StagesId;
},
pub: GetPubResponseBody,
runParameters = {}
): Promise<ActionInstanceRunResult> => {
if (!actionInstance.action) {
return {
error: "Action not found",
Expand All @@ -74,7 +73,6 @@ const _runActionInstance = async ({

logger.info(actionInstance.action);
const action = getActionByName(actionInstance.action);

const actionRun = await getActionRunByName(actionInstance.action);

if (!actionRun || !action) {
Expand All @@ -95,27 +93,28 @@ const _runActionInstance = async ({
if (!parsedrunParameters.success) {
return {
title: "Invalid pub config",
error: parsedrunParameters.error,
cause: parsedrunParameters.error,
error: "The action was run with invalid parameters",
};
}

const values = validatePubValues({
const pubValuesValidationResult = validatePubValues({
fields: action.pubFields,
values: pub.values,
});

if (values.error) {
if (pubValuesValidationResult?.error) {
return {
error: values.error,
error: pubValuesValidationResult.error,
};
}

try {
const result = await actionRun({
config: parsedConfig.data as any,
pub: {
id: pubId,
values: values as any,
id: pub.id,
values: pub.values as any,
},
runParameters: runParameters,
stageId: actionInstance.stageId,
Expand All @@ -134,12 +133,69 @@ const _runActionInstance = async ({
}
};

const runAndRecordActionInstance = async (
{ pubId, actionInstanceId, runParameters = {} }: ActionInstanceArgs,
event?: Event
) => {
let result: ActionInstanceRunResult;

const pubPromise = getPub(pubId);

const actionInstancePromise = db
.selectFrom("action_instances")
.where("action_instances.id", "=", actionInstanceId)
.select((eb) => [
"id",
eb.fn.coalesce("config", sql`'{}'`).as("config"),
"created_at as createdAt",
"updated_at as updatedAt",
"stage_id as stageId",
"action",
])
.executeTakeFirstOrThrow();

const [pubResult, actionInstanceResult] = await Promise.allSettled([
pubPromise,
actionInstancePromise,
]);

if (pubResult.status === "rejected") {
result = {
error: "Pub not found",
cause: pubResult.reason,
};
} else if (actionInstanceResult.status === "rejected") {
logger.debug({ msg: actionInstanceResult.reason });
result = {
error: "Action instance not found",
cause: actionInstanceResult.reason,
};
} else {
result = await _runActionInstance(
actionInstanceResult.value,
pubResult.value,
runParameters
);
}

await recordActionRun(
actionInstanceId,
pubId,
result,
actionInstanceResult.status === "fulfilled" ? actionInstanceResult.value.config : undefined,
runParameters,
event
);

return result;
};

export const runActionInstance = defineServerAction(async function runActionInstance({
pubId,
actionInstanceId,
runParameters = {},
}: ActionInstanceArgs) {
return _runActionInstance({ pubId, actionInstanceId, runParameters });
return runAndRecordActionInstance({ pubId, actionInstanceId, runParameters });
});

export const runInstancesForEvent = async (pubId: PubsId, stageId: StagesId, event: Event) => {
Expand All @@ -156,10 +212,13 @@ export const runInstancesForEvent = async (pubId: PubsId, stageId: StagesId, eve
return {
actionInstanceId: instance.action_instance_id,
actionInstanceName: instance.name,
result: await _runActionInstance({
pubId,
actionInstanceId: instance.action_instance_id,
}),
result: await runAndRecordActionInstance(
{
pubId,
actionInstanceId: instance.action_instance_id,
},
event
),
};
})
);
Expand Down
2 changes: 0 additions & 2 deletions core/actions/_lib/validateFields.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,4 @@ export const validatePubValues = ({
return { error: `Field ${field.slug} failed schema validation` };
}
}

return values;
};
2 changes: 1 addition & 1 deletion core/actions/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ export const defineAction = <
action: Action<T, AC, RP, N>
) => action;

type ActionSuccess = {
export type ActionSuccess = {
success: true;
/**
* Optionally provide a report to be displayed to the user
Expand Down
10 changes: 10 additions & 0 deletions core/kysely/types/public/ActionRunStatus.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.

/** Represents the enum public.ActionRunStatus */
enum ActionRunStatus {
success = "success",
failure = "failure",
}

export default ActionRunStatus;
39 changes: 39 additions & 0 deletions core/kysely/types/public/ActionRuns.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
// @generated
// This file is automatically generated by Kanel. Do not modify manually.

import { type ColumnType, type Insertable, type Selectable, type Updateable } from "kysely";

import { type ActionInstancesId } from "./ActionInstances";
import { type default as ActionRunStatus } from "./ActionRunStatus";
import { type default as Event } from "./Event";
import { type PubsId } from "./Pubs";

/** Identifier type for public.action_runs */
export type ActionRunsId = string & { __brand: "ActionRunsId" };

/** Represents the table public.action_runs */
export default interface ActionRunsTable {
id: ColumnType<ActionRunsId, ActionRunsId | undefined, ActionRunsId>;

action_instance_id: ColumnType<ActionInstancesId, ActionInstancesId, ActionInstancesId>;

config: ColumnType<unknown | null, unknown | null, unknown | null>;

event: ColumnType<Event | null, Event | null, Event | null>;

params: ColumnType<unknown | null, unknown | null, unknown | null>;

status: ColumnType<ActionRunStatus, ActionRunStatus, ActionRunStatus>;

created_at: ColumnType<Date, Date | string | undefined, Date | string>;

updated_at: ColumnType<Date, Date | string | undefined, Date | string>;

pub_id: ColumnType<PubsId, PubsId, PubsId>;
}

export type ActionRuns = Selectable<ActionRunsTable>;

export type NewActionRuns = Insertable<ActionRunsTable>;

export type ActionRunsUpdate = Updateable<ActionRunsTable>;
3 changes: 3 additions & 0 deletions core/kysely/types/public/PublicSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import { type default as ActionClaimTable } from "./ActionClaim";
import { type default as ActionInstancesTable } from "./ActionInstances";
import { type default as ActionMoveTable } from "./ActionMove";
import { type default as ActionRunsTable } from "./ActionRuns";
import { type default as AuthTokensTable } from "./AuthTokens";
import { type default as CommunitiesTable } from "./Communities";
import { type default as IntegrationInstancesTable } from "./IntegrationInstances";
Expand Down Expand Up @@ -83,4 +84,6 @@ export default interface PublicSchema {
PubsInStages: PubsInStagesTable;

rules: RulesTable;

action_runs: ActionRunsTable;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
-- CreateEnum
CREATE TYPE "ActionRunStatus" AS ENUM ('success', 'failure');

-- CreateTable
CREATE TABLE "ActionRun" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
"action_instance_id" TEXT NOT NULL,
"config" JSONB,
"event" "Event",
"params" JSONB,
"status" "ActionRunStatus" NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "ActionRun_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "ActionRun" ADD CONSTRAINT "ActionRun_action_instance_id_fkey" FOREIGN KEY ("action_instance_id") REFERENCES "action_instances"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
/*
Warnings:
- You are about to drop the `ActionRun` table. If the table is not empty, all the data it contains will be lost.
*/
-- DropForeignKey
ALTER TABLE "ActionRun" DROP CONSTRAINT "ActionRun_action_instance_id_fkey";

-- DropTable
DROP TABLE "ActionRun";

-- CreateTable
CREATE TABLE "action_runs" (
"id" TEXT NOT NULL DEFAULT gen_random_uuid(),
"action_instance_id" TEXT NOT NULL,
"config" JSONB,
"event" "Event",
"params" JSONB,
"status" "ActionRunStatus" NOT NULL,
"created_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,
"updated_at" TIMESTAMP(3) NOT NULL DEFAULT CURRENT_TIMESTAMP,

CONSTRAINT "action_runs_pkey" PRIMARY KEY ("id")
);

-- AddForeignKey
ALTER TABLE "action_runs" ADD CONSTRAINT "action_runs_action_instance_id_fkey" FOREIGN KEY ("action_instance_id") REFERENCES "action_instances"("id") ON DELETE CASCADE ON UPDATE CASCADE;
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
/*
Warnings:
- Added the required column `pub_id` to the `action_runs` table without a default value. This is not possible if the table is not empty.
*/
-- AlterTable
ALTER TABLE "action_runs" ADD COLUMN "pub_id" TEXT NOT NULL;

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

0 comments on commit 8ca0152

Please sign in to comment.