Skip to content

Commit

Permalink
feature/mq (#155)
Browse files Browse the repository at this point in the history
* feature: send MQ message when device is created/updated/deleted

* feature: send MQ message when device is created/updated/deleted

* chore: bump npm packages
  • Loading branch information
moreorover authored Jan 28, 2025
1 parent bcbbd5f commit b794609
Show file tree
Hide file tree
Showing 10 changed files with 448 additions and 240 deletions.
5 changes: 5 additions & 0 deletions .changeset/red-humans-retire.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sensepro-admin": patch
---

feature: send MQ message when device is created/updated/deleted
5 changes: 5 additions & 0 deletions .changeset/stale-buttons-drum.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"sensepro-admin": patch
---

chore: bump npm packages
458 changes: 273 additions & 185 deletions package-lock.json

Large diffs are not rendered by default.

20 changes: 11 additions & 9 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,17 @@
"dependencies": {
"@faker-js/faker": "^9.4.0",
"@mantine/core": "^7.13.4",
"@mantine/dates": "^7.16.0",
"@mantine/dates": "^7.16.2",
"@mantine/hooks": "^7.13.4",
"@mantine/notifications": "^7.16.0",
"@mantine/notifications": "^7.16.2",
"@prisma/client": "^6.2.1",
"@tabler/icons-react": "^3.28.1",
"better-auth": "^1.1.13",
"@tabler/icons-react": "^3.29.0",
"amqplib": "^0.10.5",
"better-auth": "^1.1.14",
"dayjs": "^1.11.13",
"jotai": "^2.11.0",
"jotai": "^2.11.1",
"mantine-form-zod-resolver": "^1.1.0",
"next": "^15.1.4",
"next": "^15.1.6",
"prisma": "^6.2.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
Expand All @@ -42,13 +43,14 @@
},
"devDependencies": {
"@changesets/changelog-github": "^0.5.0",
"@changesets/cli": "^2.27.11",
"@types/node": "^22.10.6",
"@changesets/cli": "^2.27.12",
"@types/amqplib": "^0.10.6",
"@types/node": "^22.12.0",
"@types/react": "^18.3.18",
"@types/react-dom": "^18.3.5",
"autoprefixer": "^10.4.20",
"eslint": "^8.57.1",
"eslint-config-next": "^15.1.4",
"eslint-config-next": "^15.1.6",
"eslint-config-prettier": "^8.10.0",
"postcss": "^8.5.1",
"postcss-preset-mantine": "^1.17.0",
Expand Down
45 changes: 4 additions & 41 deletions src/app/(dashboard)/dashboard/locations/[locationId]/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,7 @@ import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { getLocation } from "@/data-access/location";
import LocationPage from "@/components/dashboard/locations/LocationPage";
import { getCustomer } from "@/data-access/customer";
import { getDevicesByLocationId } from "@/data-access/device";
import { getDeviceTypes } from "@/data-access/deviceType";
import { getRulesByLocationId } from "@/data-access/rule";
import { getLocationPageData } from "@/data-access/locationPage";

type Props = {
params: Promise<{ locationId: string }>;
Expand All @@ -29,43 +26,9 @@ export default async function Page({ params }: Props) {
return redirect("/dashboard/locations");
}

const deviceTypes = await getDeviceTypes();
const customer = await getCustomer(location.customerId);
const devices = await getDevicesByLocationId(locationId);
const rules = await getRulesByLocationId(locationId);
const deviceGroups = devices
.filter((device) => device.deviceTypeId === "controller")
.map((controller) => ({
controllerId: controller.id,
controller,
devices: devices.filter(
(device) => device.controllerId === controller.id,
),
rules: rules
.filter((rule) => rule.controllerId === controller.id)
.map((rule) => ({
rule: {
id: rule.id,
name: rule.name,
type: rule.type,
controllerId: rule.controllerId,
locationId: rule.locationId,
},
devices: rule.devices
.map((ruleDevice) =>
devices.find((device) => device.id === ruleDevice.deviceId),
)
.filter((device) => device !== undefined),
selectedDevices: rule.devices.map((device) => device.deviceId),
})),
devicesAllowedInRules: devices.filter(
(device) =>
device.controllerId === controller.id &&
deviceTypes.find(
(deviceType) => deviceType.id === device.deviceTypeId,
)?.allowInRules,
),
}));
const { customer, deviceGroups, deviceTypes } =
await getLocationPageData(locationId);

return (
<LocationPage
location={location}
Expand Down
2 changes: 1 addition & 1 deletion src/components/dashboard/devices/DeviceForm.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ export default function DeviceForm({
{device.deviceTypeId !== "controller" && (
<NumberInput
label="Pin Number that it is connected to a controller."
placeholder="1"
placeholder=""
key={form.key("pin")}
{...form.getInputProps("pin")}
/>
Expand Down
12 changes: 8 additions & 4 deletions src/components/dashboard/devices/device.schema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,14 @@ export const deviceSchema = z.object({
ip: z.string().ip({ version: "v4" }).nullable(),
tailscaleIp: z.string().ip({ version: "v4" }).nullable(),
pin: z
.union([z.string(), z.number()])
.transform((val) =>
typeof val === "string" && val.trim() === "" ? null : Number(val),
)
.union([z.string(), z.number(), z.null()])
.transform((val) => {
if (val === null) return null;
if (typeof val === "string" && val.trim() === "") {
return null;
}
return typeof val === "number" ? val : Number(val);
})
.refine((pin) => pin === null || pin >= 0, {
message: "Pin number must be at least 0",
})
Expand Down
45 changes: 45 additions & 0 deletions src/data-access/device.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@ import { revalidatePath } from "next/cache";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { ActionResponse } from "@/data-access/serverAction.schema";
import { getLocationPageData } from "@/data-access/locationPage";
import { publishMessage } from "@/data-access/publishMessage";

export async function getDevices() {
const session = await auth.api.getSession({
Expand Down Expand Up @@ -84,6 +86,21 @@ export async function createDevice(device: Device): Promise<ActionResponse> {
data: { ...parse.data },
});
revalidatePath("/devices");

if (c.locationId && c.controllerId) {
const { deviceGroups } = await getLocationPageData(c.locationId);

const filtered = deviceGroups.find(
(deviceGroup) => deviceGroup.controllerId === c.controllerId,
);

// TODO maybe publish a simple message to the queue and let more complex backend to deal with this logic
await publishMessage(
`controller-${filtered!.controller.serialNumber}`,
JSON.stringify(filtered),
);
}

return {
message: `Created Device: ${c.name}`,
type: "SUCCESS",
Expand Down Expand Up @@ -120,6 +137,20 @@ export async function updateDevice(device: Device): Promise<ActionResponse> {
where: { id: parse.data.id },
});
revalidatePath("/devices");

if (c.locationId && c.controllerId) {
const { deviceGroups } = await getLocationPageData(c.locationId);

const filtered = deviceGroups.find(
(deviceGroup) => deviceGroup.controllerId === c.controllerId,
);

// TODO maybe publish a simple message to the queue and let more complex backend to deal with this logic
await publishMessage(
`controller-${filtered!.controller.serialNumber}`,
JSON.stringify(filtered),
);
}
return {
message: `Updated Device: ${c.name}`,
type: "SUCCESS",
Expand Down Expand Up @@ -155,6 +186,20 @@ export async function deleteDevice(device: Device): Promise<ActionResponse> {
where: { id: parse.data.id },
});
revalidatePath("/devices");

if (device.locationId && device.controllerId) {
const { deviceGroups } = await getLocationPageData(device.locationId);

const filtered = deviceGroups.find(
(deviceGroup) => deviceGroup.controllerId === c.controllerId,
);

// TODO maybe publish a simple message to the queue and let more complex backend to deal with this logic
await publishMessage(
`controller-${filtered!.controller.serialNumber}`,
JSON.stringify(filtered),
);
}
return {
message: `Deleted Device: ${c.name}`,
type: "SUCCESS",
Expand Down
64 changes: 64 additions & 0 deletions src/data-access/locationPage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
"use server";

import "server-only";

import { auth } from "@/lib/auth";
import { headers } from "next/headers";
import { redirect } from "next/navigation";
import { getDeviceTypes } from "@/data-access/deviceType";
import { getCustomer } from "@/data-access/customer";
import { getDevicesByLocationId } from "@/data-access/device";
import { getRulesByLocationId } from "@/data-access/rule";
import { getLocation } from "@/data-access/location";

export async function getLocationPageData(locationId: string) {
const session = await auth.api.getSession({
headers: await headers(),
});

if (!session) {
return redirect("/");
}

const location = await getLocation(locationId);

// TODO improve this
const deviceTypes = await getDeviceTypes();
const customer = await getCustomer(location!.customerId!);
const devices = await getDevicesByLocationId(locationId);
const rules = await getRulesByLocationId(locationId);
const deviceGroups = devices
.filter((device) => device.deviceTypeId === "controller")
.map((controller) => ({
controllerId: controller.id,
controller,
devices: devices.filter(
(device) => device.controllerId === controller.id,
),
rules: rules
.filter((rule) => rule.controllerId === controller.id)
.map((rule) => ({
rule: {
id: rule.id,
name: rule.name,
type: rule.type,
controllerId: rule.controllerId,
locationId: rule.locationId,
},
devices: rule.devices
.map((ruleDevice) =>
devices.find((device) => device.id === ruleDevice.deviceId),
)
.filter((device) => device !== undefined),
selectedDevices: rule.devices.map((device) => device.deviceId),
})),
devicesAllowedInRules: devices.filter(
(device) =>
device.controllerId === controller.id &&
deviceTypes.find(
(deviceType) => deviceType.id === device.deviceTypeId,
)?.allowInRules,
),
}));
return { customer, deviceGroups, deviceTypes };
}
32 changes: 32 additions & 0 deletions src/data-access/publishMessage.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
"use server";

import "server-only";

import amqp from "amqplib";

export async function publishMessage(queue: string, message: string) {
let connection;
let channel;

try {
// Step 1: Create a connection
connection = await amqp.connect(process.env.RABBITMQ_URL as string);

// Step 2: Create a channel
channel = await connection.createChannel();

// Step 3: Assert the queue (ensures the queue exists)
await channel.assertQueue(queue, { durable: true, maxLength: 1 });

// Step 4: Send the message to the queue
channel.sendToQueue(queue, Buffer.from(message));
console.log(`Message sent to queue "${queue}"`);

// Step 5: Close the channel and connection
} catch (error) {
console.error("Failed to send message:", error);
} finally {
if (channel) await channel.close();
if (connection) await connection.close();
}
}

0 comments on commit b794609

Please sign in to comment.