From a29686e3134fce2233dad9a1f29f1d8b409a7873 Mon Sep 17 00:00:00 2001 From: Sebastian Pietschner Date: Thu, 16 Jan 2025 10:44:09 +1100 Subject: [PATCH] more parsing rules stuff --- packages/frontend/app/router.tsx | 1 + .../frontend/app/server/drizzle/schema.ts | 44 ++ packages/frontend/app/server/queryKeys.ts | 6 + .../server/schema/EventParsingRuleSchema.ts | 4 + .../schema/NewEventParsingRuleSchema.ts | 15 + .../schema/UpdateEventParsingRuleSchema.ts | 16 + packages/frontend/app/server/schema/index.ts | 3 + .../app/server/services/adminService.ts | 110 ++++ .../frontend/drizzle/0016_legal_blade.sql | 18 + .../frontend/drizzle/meta/0016_snapshot.json | 542 ++++++++++++++++++ packages/frontend/drizzle/meta/_journal.json | 7 + 11 files changed, 766 insertions(+) create mode 100644 packages/frontend/app/server/schema/EventParsingRuleSchema.ts create mode 100644 packages/frontend/app/server/schema/NewEventParsingRuleSchema.ts create mode 100644 packages/frontend/app/server/schema/UpdateEventParsingRuleSchema.ts create mode 100644 packages/frontend/app/server/services/adminService.ts create mode 100644 packages/frontend/drizzle/0016_legal_blade.sql create mode 100644 packages/frontend/drizzle/meta/0016_snapshot.json diff --git a/packages/frontend/app/router.tsx b/packages/frontend/app/router.tsx index 3bf43a60a..5960ac097 100644 --- a/packages/frontend/app/router.tsx +++ b/packages/frontend/app/router.tsx @@ -3,6 +3,7 @@ import { createRouter as createTanStackRouter } from "@tanstack/react-router"; import { routeTree } from "./routeTree.gen"; import { QueryClient } from "@tanstack/react-query"; import { routerWithQueryClient } from "@tanstack/react-router-with-query"; +import SuperJSON from "superjson"; export function createRouter() { const queryClient = new QueryClient(); diff --git a/packages/frontend/app/server/drizzle/schema.ts b/packages/frontend/app/server/drizzle/schema.ts index 5a8828bc7..2e62821ee 100644 --- a/packages/frontend/app/server/drizzle/schema.ts +++ b/packages/frontend/app/server/drizzle/schema.ts @@ -9,6 +9,7 @@ import { uniqueIndex, } from "drizzle-orm/pg-core"; import { v4 } from "uuid"; +import { ulid } from "ulidx" export const eventTypes = pgEnum("EventTypes", [ "Social", @@ -69,6 +70,7 @@ export const sessionTable = pgTable("session", { export const userTableRelations = relations(userTable, ({ many }) => ({ rsvps: many(rsvpTable), scancodes: many(scancodeTable), + apiKeys: many(apiKeyTable), })); export const rsvpTable = pgTable( @@ -210,3 +212,45 @@ export const scancodeTableRelations = relations(scancodeTable, ({ one }) => ({ references: [userTable.id], }), })); + +// API Key +export const apiKeyTable = pgTable("ApiKey", { + id: text("id").primaryKey().notNull().$default(() => ulid()), + createdBy: text("createdBy").references(() => userTable.id), + createdAt: timestamp("createdAt", { + precision: 3, + mode: "date", + withTimezone: true, + }).notNull().defaultNow(), + updatedAt: timestamp("updatedAt", { + precision: 3, + mode: "date", + withTimezone: true, + }).notNull().defaultNow(), +}); + +export const apiKeyTableRelations = relations(apiKeyTable, ({ one }) => ({ + createdBy: one(userTable, { + fields: [apiKeyTable.createdBy], + references: [userTable.id], + }), +})) + +/** Parsing rules for event names */ +export const eventParsingRuleTable = pgTable("EventParsingRule", { + id: text("id").primaryKey().notNull().$default(() => ulid()), + createdAt: timestamp("createdAt", { + precision: 3, + mode: "date", + withTimezone: true, + }).notNull().defaultNow(), + updatedAt: timestamp("updatedAt", { + precision: 3, + mode: "date", + withTimezone: true, + }).notNull().defaultNow(), + name: text("name").notNull(), + regex: text("regex").notNull().default(""), + rolesIds: text("roles").array().notNull().default([]), + channelId: text("channelId").notNull(), +}); \ No newline at end of file diff --git a/packages/frontend/app/server/queryKeys.ts b/packages/frontend/app/server/queryKeys.ts index 8a8ab2b91..722fe40e5 100644 --- a/packages/frontend/app/server/queryKeys.ts +++ b/packages/frontend/app/server/queryKeys.ts @@ -71,3 +71,9 @@ export const authQueryKeys = { auth: ["auth"] as const, status: () => [...authQueryKeys.auth, "status"] as const, }; + +export const adminQueryKeys = { + admin: ["admin"] as const, + apiKeys: ["admin", "apiKeys"] as const, + parsingRules: ["admin", "parsingRules"] as const, +}; \ No newline at end of file diff --git a/packages/frontend/app/server/schema/EventParsingRuleSchema.ts b/packages/frontend/app/server/schema/EventParsingRuleSchema.ts new file mode 100644 index 000000000..a2dd3addd --- /dev/null +++ b/packages/frontend/app/server/schema/EventParsingRuleSchema.ts @@ -0,0 +1,4 @@ +import { createSelectSchema } from "drizzle-zod"; +import { eventParsingRuleTable } from "../drizzle/schema"; + +export const EventParsingRule = createSelectSchema(eventParsingRuleTable) \ No newline at end of file diff --git a/packages/frontend/app/server/schema/NewEventParsingRuleSchema.ts b/packages/frontend/app/server/schema/NewEventParsingRuleSchema.ts new file mode 100644 index 000000000..679e22d23 --- /dev/null +++ b/packages/frontend/app/server/schema/NewEventParsingRuleSchema.ts @@ -0,0 +1,15 @@ +import { z } from "zod"; + +export const NewEventParsingRuleSchema = z.object({ + channelId: z.string(), + name: z.string(), + regex: z.string().refine((v) => { + try { + new RegExp(v); + return true; + } catch { + return false; + } + }), + rolesIds: z.array(z.string()), +}) \ No newline at end of file diff --git a/packages/frontend/app/server/schema/UpdateEventParsingRuleSchema.ts b/packages/frontend/app/server/schema/UpdateEventParsingRuleSchema.ts new file mode 100644 index 000000000..6c2fba3c0 --- /dev/null +++ b/packages/frontend/app/server/schema/UpdateEventParsingRuleSchema.ts @@ -0,0 +1,16 @@ +import { z } from "zod"; +import { NewEventParsingRuleSchema } from "./NewEventParsingRuleSchema"; + +export const UpdateEventParsingRuleSchema = z.object({ + channelId: z.string().optional(), + name: z.string().optional(), + regex: z.string().refine((v) => { + try { + new RegExp(v); + return true; + } catch { + return false; + } + }).optional(), + rolesIds: z.array(z.string()).optional(), +}) \ No newline at end of file diff --git a/packages/frontend/app/server/schema/index.ts b/packages/frontend/app/server/schema/index.ts index 00655c352..364053c76 100644 --- a/packages/frontend/app/server/schema/index.ts +++ b/packages/frontend/app/server/schema/index.ts @@ -29,3 +29,6 @@ export * from "./PagedEventsSchema"; export * from "./PagedSchema"; export * from "./UserListParamsSchema"; export * from "./OutreachTimeSchema"; +export * from "./NewEventParsingRuleSchema" +export * from "./EventParsingRuleSchema" +export * from "./UpdateEventParsingRuleSchema" \ No newline at end of file diff --git a/packages/frontend/app/server/services/adminService.ts b/packages/frontend/app/server/services/adminService.ts new file mode 100644 index 000000000..a771a8a73 --- /dev/null +++ b/packages/frontend/app/server/services/adminService.ts @@ -0,0 +1,110 @@ +import { eq } from "drizzle-orm"; +import db from "../drizzle/db"; +import { apiKeyTable, eventParsingRuleTable } from "../drizzle/schema"; +import { z } from "zod"; +import { NewEventParsingRuleSchema, UpdateEventParsingRuleSchema } from "../schema"; +import { TRPCError } from "@trpc/server"; + +/** + * Get all API keys + * @returns A list of all API keys + */ +export async function getApiKeys() { + const apiKeys = await db.query.apiKeyTable.findMany(); + + return apiKeys; +} + +/** + * Delete an API key + * @param id The ID of the API key to delete + * @returns The deleted API key + */ +export async function deleteApiKey(id: string) { + const [deleted] = await db.delete(apiKeyTable).where(eq(apiKeyTable.id, id)).returning() + + return deleted; +} + +/** + * Create a new API key + * @param userId The ID of the user creating the API key + * @returns The created API key + */ +export async function createApiKey(userId: string) { + const [apiKey] = await db.insert(apiKeyTable).values({ + createdBy: userId, + }).returning(); + + return apiKey; +} + +/** + * Create a new parsing rule + * @param data The data to create a new parsing rule + * @returns The created parsing rule + */ +export async function createParsingRule (data: z.infer) { + const { channelId, name, regex, rolesIds } = data; + // Create a new parsing rule + const [parsingRule] = await db.insert(eventParsingRuleTable).values({ + channelId, + name, + regex, + rolesIds, + }).returning(); + + if (!parsingRule) { + throw new TRPCError({ + code: "INTERNAL_SERVER_ERROR" + }) + } + + return parsingRule; +} + +/** + * Get all parsing rules + * @returns A list of all parsing rules + */ +export async function getParsingRules () { + const parsingRules = await db.query.eventParsingRuleTable.findMany(); + + return parsingRules; +} + +/** + * Delete a parsing rule + * @param id The ID of the parsing rule to delete + * @returns The deleted parsing rule + */ +export async function deleteParsingRule (id: string) { + const [deleted] = await db.delete(eventParsingRuleTable).where(eq(eventParsingRuleTable.id, id)).returning() + + return deleted; +} + +/** + * Update a parsing rule + * @param id The ID of the parsing rule to update + * @param data The data to update the parsing rule with + * @returns The updated parsing rule + */ +export async function updateParsingRule (id: string, data: z.infer) { + const { channelId, name, regex, rolesIds } = data; + const [updated] = await db.update(eventParsingRuleTable).set({ + channelId, + name, + regex, + rolesIds, + }).where(eq(eventParsingRuleTable.id, id)).returning(); + + if (!updated) { + throw new TRPCError({ + code: "NOT_FOUND", + message: "Parsing rule not found" + }) + } + + return updated; +} diff --git a/packages/frontend/drizzle/0016_legal_blade.sql b/packages/frontend/drizzle/0016_legal_blade.sql new file mode 100644 index 000000000..659c73e71 --- /dev/null +++ b/packages/frontend/drizzle/0016_legal_blade.sql @@ -0,0 +1,18 @@ +CREATE TABLE "ApiKey" ( + "id" text PRIMARY KEY NOT NULL, + "createdBy" text, + "createdAt" timestamp (3) with time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp (3) with time zone DEFAULT now() NOT NULL +); +--> statement-breakpoint +CREATE TABLE "EventParsingRule" ( + "id" text PRIMARY KEY NOT NULL, + "createdAt" timestamp (3) with time zone DEFAULT now() NOT NULL, + "updatedAt" timestamp (3) with time zone DEFAULT now() NOT NULL, + "name" text NOT NULL, + "regex" text DEFAULT '' NOT NULL, + "roles" text[] DEFAULT '{}' NOT NULL, + "channelId" text NOT NULL +); +--> statement-breakpoint +ALTER TABLE "ApiKey" ADD CONSTRAINT "ApiKey_createdBy_User_id_fk" FOREIGN KEY ("createdBy") REFERENCES "public"."User"("id") ON DELETE no action ON UPDATE no action; \ No newline at end of file diff --git a/packages/frontend/drizzle/meta/0016_snapshot.json b/packages/frontend/drizzle/meta/0016_snapshot.json new file mode 100644 index 000000000..72716d21d --- /dev/null +++ b/packages/frontend/drizzle/meta/0016_snapshot.json @@ -0,0 +1,542 @@ +{ + "id": "b34e59e2-9b9f-409c-9f36-f8fc83c67b11", + "prevId": "0303810a-04d5-4576-862e-aeb2ad4a6901", + "version": "7", + "dialect": "postgresql", + "tables": { + "public.ApiKey": { + "name": "ApiKey", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdBy": { + "name": "createdBy", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ApiKey_createdBy_User_id_fk": { + "name": "ApiKey_createdBy_User_id_fk", + "tableFrom": "ApiKey", + "tableTo": "User", + "columnsFrom": [ + "createdBy" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.EventParsingRule": { + "name": "EventParsingRule", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "regex": { + "name": "regex", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "roles": { + "name": "roles", + "type": "text[]", + "primaryKey": false, + "notNull": true, + "default": "'{}'" + }, + "channelId": { + "name": "channelId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Event": { + "name": "Event", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "startDate": { + "name": "startDate", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "endDate": { + "name": "endDate", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true + }, + "allDay": { + "name": "allDay", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "type": { + "name": "type", + "type": "EventTypes", + "typeSchema": "public", + "primaryKey": false, + "notNull": true, + "default": "'Regular'" + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "isSyncedEvent": { + "name": "isSyncedEvent", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "isPosted": { + "name": "isPosted", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Event_secret_key": { + "name": "Event_secret_key", + "columns": [ + { + "expression": "secret", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.RSVP": { + "name": "RSVP", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "eventId": { + "name": "eventId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "delay": { + "name": "delay", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "status": { + "name": "status", + "type": "RSVPStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "checkinTime": { + "name": "checkinTime", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + }, + "checkoutTime": { + "name": "checkoutTime", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": false + } + }, + "indexes": { + "RSVP_eventId_userId_key": { + "name": "RSVP_eventId_userId_key", + "columns": [ + { + "expression": "eventId", + "isExpression": false, + "asc": true, + "nulls": "last" + }, + { + "expression": "userId", + "isExpression": false, + "asc": true, + "nulls": "last" + } + ], + "isUnique": true, + "concurrently": false, + "method": "btree", + "with": {} + } + }, + "foreignKeys": { + "RSVP_eventId_Event_id_fk": { + "name": "RSVP_eventId_Event_id_fk", + "tableFrom": "RSVP", + "tableTo": "Event", + "columnsFrom": [ + "eventId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "RSVP_userId_User_id_fk": { + "name": "RSVP_userId_User_id_fk", + "tableFrom": "RSVP", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.Scancode": { + "name": "Scancode", + "schema": "", + "columns": { + "code": { + "name": "code", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "Scancode_userId_User_id_fk": { + "name": "Scancode_userId_User_id_fk", + "tableFrom": "Scancode", + "tableTo": "User", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "restrict", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.session": { + "name": "session", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "user_id": { + "name": "user_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp with time zone", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "session_user_id_User_id_fk": { + "name": "session_user_id_User_id_fk", + "tableFrom": "session", + "tableTo": "User", + "columnsFrom": [ + "user_id" + ], + "columnsTo": [ + "id" + ], + "onDelete": "no action", + "onUpdate": "no action" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + }, + "public.User": { + "name": "User", + "schema": "", + "columns": { + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3) with time zone", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "id": { + "name": "id", + "type": "text", + "primaryKey": true, + "notNull": true + }, + "roles": { + "name": "roles", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "accessToken": { + "name": "accessToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "refreshToken": { + "name": "refreshToken", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "accessTokenExpiresAt": { + "name": "accessTokenExpiresAt", + "type": "timestamp (3) with time zone", + "primaryKey": false, + "notNull": false + }, + "defaultStatus": { + "name": "defaultStatus", + "type": "RSVPStatus", + "typeSchema": "public", + "primaryKey": false, + "notNull": false + }, + "additionalOutreachHours": { + "name": "additionalOutreachHours", + "type": "integer", + "primaryKey": false, + "notNull": true, + "default": 0 + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {}, + "policies": {}, + "checkConstraints": {}, + "isRLSEnabled": false + } + }, + "enums": { + "public.EventTypes": { + "name": "EventTypes", + "schema": "public", + "values": [ + "Social", + "Regular", + "Outreach", + "Mentor" + ] + }, + "public.RSVPStatus": { + "name": "RSVPStatus", + "schema": "public", + "values": [ + "LATE", + "MAYBE", + "NO", + "YES", + "ATTENDED" + ] + } + }, + "schemas": {}, + "sequences": {}, + "roles": {}, + "policies": {}, + "views": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/packages/frontend/drizzle/meta/_journal.json b/packages/frontend/drizzle/meta/_journal.json index 17db88398..246d365cc 100644 --- a/packages/frontend/drizzle/meta/_journal.json +++ b/packages/frontend/drizzle/meta/_journal.json @@ -113,6 +113,13 @@ "when": 1736559100263, "tag": "0015_chilly_silver_samurai", "breakpoints": true + }, + { + "idx": 16, + "version": "7", + "when": 1736983757923, + "tag": "0016_legal_blade", + "breakpoints": true } ] } \ No newline at end of file