Skip to content

Commit

Permalink
feat: it works!
Browse files Browse the repository at this point in the history
  • Loading branch information
tefkah committed May 23, 2024
1 parent 0e7ad15 commit e9f4f83
Show file tree
Hide file tree
Showing 16 changed files with 623 additions and 145 deletions.
5 changes: 3 additions & 2 deletions core/actions/_lib/runActionInstance.ts
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ const _runActionInstance = async ({
}

if (actionInstanceResult.status === "rejected") {
logger.debug({ msg: actionInstanceResult.reason });
logger.error({ msg: actionInstanceResult.reason });
return {
error: "Action instance not found",
cause: actionInstanceResult.reason,
Expand Down Expand Up @@ -105,6 +105,7 @@ const _runActionInstance = async ({
});

if (values.error) {
logger.error(values.error);
return {
error: values.error,
};
Expand All @@ -121,7 +122,7 @@ const _runActionInstance = async ({
stageId: actionInstance.stageId,
});

revalidateTag(`community-stages_${pub.communityId}`);
// revalidateTag(`community-stages_${pub.communityId}`);

return result;
} catch (error) {
Expand Down
96 changes: 96 additions & 0 deletions core/actions/_lib/scheduleActionInstance.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
// "use server";

import { jsonArrayFrom } from "kysely/helpers/postgres";

import { logger } from "logger";

import type { PubsId } from "~/kysely/types/public/Pubs";
import type { Rules } from "~/kysely/types/public/Rules";
import type { StagesId } from "~/kysely/types/public/Stages";
import { db } from "~/kysely/database";
import Event from "~/kysely/types/public/Event";
import { addDuration } from "~/lib/dates";
import { getJobsClient } from "~/lib/server/jobs";

export const scheduleActionInstances = async ({
pubId,
stageId,
}: {
pubId: PubsId;
stageId: StagesId;
}) => {
if (!pubId || !stageId) {
throw new Error("pubId and stageId are required");
}

const instances = await db
.selectFrom("action_instances")
.where("action_instances.stage_id", "=", stageId)
// .innerJoin("rules", "rules.action_instance_id", "action_instances.id")
// .where("rules.event", "=", Event.pubInStageForDuration)
.select((eb) => [
"action_instances.id as id",
"action_instances.name as name",
jsonArrayFrom(
eb
.selectFrom("rules")
.select([
"rules.id as id",
"rules.event as event",
"rules.config as config",
"rules.action_instance_id as action_instance_id",
])
.where("rules.action_instance_id", "=", eb.ref("action_instances.id"))
.where("rules.event", "=", Event.pubInStageForDuration)
).as("rules"),
])
.execute();

if (!instances.length) {
logger.warn({
msg: `No action instances found for stage ${stageId}`,
pubId,
stageId,
instances,
});
return;
}

const validRules = instances.flatMap((instance) =>
instance.rules
.filter((rule): rule is Rules & { config: { duration: number } } =>
Boolean(rule.config?.duration && rule.config.interval)
)
.map((rule) => ({ ...rule, actionName: instance.name }))
);

if (!validRules.length) {
logger.warn({ msg: "No action instances found for pub", pubId, stageId, instances });
return;
}

const jobsClient = await getJobsClient();

const results = await Promise.all(
validRules.flatMap(async (rule) => {
const job = await jobsClient.scheduleAction({
actionInstanceId: rule.action_instance_id,
duration: rule.config.duration,
interval: rule.config.interval,
stageId: stageId,
pubId,
});
return {
result: job,
actionInstanceId: rule.action_instance_id,
actionInstanceName: rule.actionName,
runAt: addDuration({
duration: rule.config.duration,
interval: rule.config.interval,
}).toISOString(),
};
})
);

return results;
};
11 changes: 11 additions & 0 deletions core/actions/api/index.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// shared actions between server and client

import type Event from "~/kysely/types/public/Event";
import { pubEnteredStage, pubInStageForDuration, pubLeftStage } from "../_lib/rules";
import * as email from "../email/action";
import * as log from "../log/action";
import * as move from "../move/action";
Expand All @@ -23,6 +24,16 @@ export const getActionNames = () => {
return Object.keys(actions) as (keyof typeof actions)[];
};

export const rules = {
[pubInStageForDuration.event]: pubInStageForDuration,
[pubEnteredStage.event]: pubEnteredStage,
[pubLeftStage.event]: pubLeftStage,
} as const;

export const getRuleByName = (name: keyof typeof rules) => {
return rules[name];
};

const humanReadableEvents: Record<Event, string> = {
pubEnteredStage: "a pub enters this stage",
pubLeftStage: "a pub leaves this stage",
Expand Down
2 changes: 1 addition & 1 deletion core/actions/api/server.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { runActionInstance, runInstancesForEvent } from "../_lib/runActionInstance";
export { scheduleActionInstances } from "../_lib/scheduleActionInstance";
6 changes: 6 additions & 0 deletions core/actions/api/serverActions.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
// everything exported from here should use the "use server" directive
// in order to allow importing these function in client components
//
// BUT, this file should NOT have the "use server" directive

export { runActionInstance, runInstancesForEvent } from "../_lib/runActionInstance";
25 changes: 17 additions & 8 deletions core/app/c/[communitySlug]/stages/manage/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,10 @@ import type { RulesId } from "~/kysely/types/public/Rules";
import { humanReadableEvent } from "~/actions/api";
import { db } from "~/kysely/database";
import { type ActionInstancesId } from "~/kysely/types/public/ActionInstances";
import { CommunitiesId } from "~/kysely/types/public/Communities";
import { defineServerAction } from "~/lib/server/defineServerAction";
import prisma from "~/prisma/db";
import { CreateRuleSchema } from "./components/panel/StagePanelRuleCreator";

async function deleteStages(stageIds: string[]) {
await prisma.stage.deleteMany({
Expand Down Expand Up @@ -236,24 +238,31 @@ export const deleteAction = defineServerAction(async function deleteAction(
}
});

export const addRule = defineServerAction(async function addRule(
event: Event,
actionInstanceId: ActionInstancesId,
communityId: string
) {
export const addRule = defineServerAction(async function addRule({
data,
communityId,
}: {
data: CreateRuleSchema;
communityId: CommunitiesId;
}) {
try {
await db
.insertInto("rules")
.values({ action_instance_id: actionInstanceId, event })
.values({
action_instance_id: data.actionInstanceId as ActionInstancesId,
event: data.event,
config: "additionalConfiguration" in data ? data.additionalConfiguration : null,
})

.executeTakeFirstOrThrow();
} catch (error) {
logger.error(error);
if (error.message?.includes("unique constraint")) {
return {
title: "Rule already exists",
error: `A rule for '${humanReadableEvent(event)}' and this action already exists. Please add another action
error: `A rule for '${humanReadableEvent(data.event)}' and this action already exists. Please add another action
of the same type to this stage in order to have the same action trigger
multiple times for '${humanReadableEvent(event)}'.`,
multiple times for '${humanReadableEvent(data.event)}'.`,
cause: error,
};
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ import { toast } from "ui/use-toast";
import type { ActionInstances, ActionInstancesId } from "~/kysely/types/public/ActionInstances";
import type { PubsId } from "~/kysely/types/public/Pubs";
import { getActionByName } from "~/actions/api";
import { runActionInstance } from "~/actions/api/server";
import { runActionInstance } from "~/actions/api/serverActions";
import { useServerAction } from "~/lib/serverActions";

export const StagePanelPubsRunActionButton = ({
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,20 +16,22 @@ import {
DialogTrigger,
} from "ui/dialog";
import { Form, FormField, FormItem, FormLabel, FormMessage } from "ui/form";
import { Input } from "ui/input";
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "ui/select";

import type { Rules } from "~/actions/_lib/rules";
import type Action from "~/kysely/types/public/Action";
import type { ActionInstances, ActionInstancesId } from "~/kysely/types/public/ActionInstances";
import { actions, humanReadableEvent } from "~/actions/api";
import type { CommunitiesId } from "~/kysely/types/public/Communities";
import { actions, getRuleByName, humanReadableEvent, rules } from "~/actions/api";
import Event from "~/kysely/types/public/Event";
import { useServerAction } from "~/lib/serverActions";
import AutoFormObject from "../../../../../../../../packages/ui/src/auto-form/fields/object";
import { addRule } from "../../actions";

type Props = {
// onAdd: (event: Event, actionInstanceId: ActionInstancesId) => Promise<unknown>;
actionInstances: ActionInstances[];
communityId: string;
communityId: CommunitiesId;
rules: {
id: string;
event: Event;
Expand All @@ -39,25 +41,41 @@ type Props = {
}[];
};

const schema = z.object({
event: z.nativeEnum(Event),
actionInstanceId: z.string(),
additionalConfiguration: z
.object({
duration: z.number(),
interval: z.enum(["hour", "day", "week", "month"]),
})
.optional(),
});
const schema = z.discriminatedUnion("event", [
z.object({
event: z.literal(Event.pubEnteredStage),
actionInstanceId: z.string().uuid(),
}),
z.object({
event: z.literal(Event.pubLeftStage),
actionInstanceId: z.string().uuid(),
}),
...Object.values(rules)
.filter(
(rule): rule is Exclude<Rules, { event: Event.pubEnteredStage | Event.pubLeftStage }> =>
rule.event !== Event.pubEnteredStage && rule.event !== Event.pubLeftStage
)
.map((rule) =>
z.object({
event: z.literal(rule.event),
actionInstanceId: z.string().uuid(),
additionalConfiguration: rule.additionalConfig
? rule.additionalConfig
: z.null().optional(),
})
),
]);

export type CreateRuleSchema = z.infer<typeof schema>;

export const StagePanelRuleCreator = (props: Props) => {
// const runOnAdd = useServerAction(props.onAdd);
const runAddRule = useServerAction(addRule);
const [isOpen, setIsOpen] = useState(false);
const onSubmit = useCallback(
async ({ event, actionInstanceId }: z.infer<typeof schema>) => {
async (data: CreateRuleSchema) => {
setIsOpen(false);
runAddRule(event, actionInstanceId as ActionInstancesId, props.communityId);
runAddRule({ data, communityId: props.communityId });
// runOnAdd(event, actionInstanceId as ActionInstancesId);
},
[props.communityId] // [props.onAdd, runOnAdd]
Expand All @@ -80,6 +98,8 @@ export const StagePanelRuleCreator = (props: Props) => {
.map((rule) => rule.event);
const allowedEvents = Object.values(Event).filter((event) => !disallowedEvents.includes(event));

const rule = getRuleByName(event);

return (
<div className="space-y-2 py-2">
<Dialog open={isOpen} onOpenChange={onOpenChange}>
Expand Down Expand Up @@ -174,55 +194,17 @@ export const StagePanelRuleCreator = (props: Props) => {
</FormItem>
)}
/>
{event === Event.pubInStageForDuration && (
<FormField
control={form.control}
{rule?.additionalConfig && (
<AutoFormObject
// @ts-expect-error FIXME: this fails because AutoFormObject
// expects the schema for `form` to be the same as the one for
// `schema`.
// Could be fixed by changing AutoFormObject to look at the schema of `form` at `path` for
// the schema at `schema`.
form={form}
path={["additionalConfiguration"]}
name="additionalConfiguration"
render={({ field }) => (
<FormItem>
<FormLabel>Additional Config</FormLabel>
<div className="flex flex-row items-center gap-x-2">
<Input
type="number"
onChange={(val) =>
field.onChange({
...field.value,
duration: val.target.valueAsNumber,
})
}
defaultValue={5}
/>
<Select
onValueChange={(val) =>
field.onChange({
...field.value,
interval: val,
})
}
defaultValue={"day"}
>
<SelectTrigger>
<SelectValue placeholder="days" />
</SelectTrigger>
<SelectContent>
{["hour", "day", "week", "month"].map(
(interval) => (
<SelectItem
key={interval}
value={interval}
className="hover:bg-gray-100"
>
{interval}
{"s "}
</SelectItem>
)
)}
</SelectContent>
</Select>
</div>
<FormMessage />
</FormItem>
)}
schema={rule.additionalConfig}
/>
)}
</div>
Expand Down
Loading

0 comments on commit e9f4f83

Please sign in to comment.