From a37e8e92c5156d8c848f846b1504d0981f449176 Mon Sep 17 00:00:00 2001 From: Jesse Wierzbinski Date: Wed, 17 Apr 2024 13:47:03 -1000 Subject: [PATCH] feat(api): :sparkles: Implement filters API v2 (with some routes missing) --- README.md | 5 +- bun.lockb | Bin 153836 -> 8144 bytes drizzle.config.ts | 8 +- drizzle/0014_wonderful_sandman.sql | 28 + drizzle/meta/0014_snapshot.json | 1965 +++++++++++++++++ drizzle/meta/_journal.json | 213 +- drizzle/schema.ts | 44 + packages/request-parser/index.ts | 106 +- packages/request-parser/package.json | 11 +- .../tests/request-parser.test.ts | 22 + packages/server-handler/index.ts | 6 +- server/api/api/v1/notifications/index.test.ts | 67 + server/api/api/v1/notifications/index.ts | 19 + server/api/api/v1/timelines/home.test.ts | 67 + server/api/api/v1/timelines/home.ts | 4 + server/api/api/v1/timelines/public.test.ts | 65 + server/api/api/v1/timelines/public.ts | 3 + server/api/api/v2/filters/[id]/index.test.ts | 203 ++ server/api/api/v2/filters/[id]/index.ts | 178 ++ server/api/api/v2/filters/index.test.ts | 72 + server/api/api/v2/filters/index.ts | 155 ++ 21 files changed, 3087 insertions(+), 154 deletions(-) create mode 100644 drizzle/0014_wonderful_sandman.sql create mode 100644 drizzle/meta/0014_snapshot.json create mode 100644 server/api/api/v2/filters/[id]/index.test.ts create mode 100644 server/api/api/v2/filters/[id]/index.ts create mode 100644 server/api/api/v2/filters/index.test.ts create mode 100644 server/api/api/v2/filters/index.ts diff --git a/README.md b/README.md index e469088d..ce31763d 100644 --- a/README.md +++ b/README.md @@ -233,10 +233,10 @@ Working endpoints are: - [ ] `/api/v1/trends/tags` - [ ] `/api/v2/filters/:filter_id/keywords` (`GET`, `POST`) - [ ] `/api/v2/filters/:filter_id/statuses` (`GET`, `POST`) -- [ ] `/api/v2/filters/:id` (`GET`, `PUT`, `DELETE`) +- [x] `/api/v2/filters/:id` (`GET`, `PUT`, `DELETE`) - [ ] `/api/v2/filters/keywords/:id` (`GET`, `PUT`, `DELETE`) - [ ] `/api/v2/filters/statuses/:id` (`GET`, `DELETE`) -- [ ] `/api/v2/filters` (`GET`, `POST`) +- [x] `/api/v2/filters` (`GET`, `POST`) - [x] `/api/v2/instance` - [x] `/api/v2/media` - [x] `/api/v2/search` @@ -248,7 +248,6 @@ Working endpoints are: ### Main work to do - [ ] Announcements -- [ ] Filters - [ ] Polls - [ ] Tags - [ ] Lists diff --git a/bun.lockb b/bun.lockb index 7d085a708b5bf7688a5cf1aef110c89033110236..32cfcf0c6ab532b8d5262db47b514994ff69d69a 100755 GIT binary patch literal 8144 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 literal 153836 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 Npcc!S;Fsv%{{tt64a@)l diff --git a/drizzle.config.ts b/drizzle.config.ts index a49f235d..3c43500f 100644 --- a/drizzle.config.ts +++ b/drizzle.config.ts @@ -6,16 +6,16 @@ export default { out: "./drizzle", schema: "./drizzle/schema.ts", dbCredentials: { - /* host: "localhost", + host: "localhost", port: 40000, user: "lysand", password: "lysand", - database: "lysand", */ - host: config.database.host, + database: "lysand", + /* host: config.database.host, port: Number(config.database.port), user: config.database.username, password: config.database.password, - database: config.database.database, + database: config.database.database, */ }, // Print all statements verbose: true, diff --git a/drizzle/0014_wonderful_sandman.sql b/drizzle/0014_wonderful_sandman.sql new file mode 100644 index 00000000..f6aa1f7d --- /dev/null +++ b/drizzle/0014_wonderful_sandman.sql @@ -0,0 +1,28 @@ +CREATE TABLE IF NOT EXISTS "FilterKeywords" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "filterId" uuid NOT NULL, + "keyword" text NOT NULL, + "whole_word" boolean NOT NULL +); +--> statement-breakpoint +CREATE TABLE IF NOT EXISTS "Filters" ( + "id" uuid PRIMARY KEY DEFAULT uuid_generate_v7() NOT NULL, + "userId" uuid NOT NULL, + "context" text[], + "title" text NOT NULL, + "filter_action" text NOT NULL, + "expires_at" timestamp(3), + "created_at" timestamp(3) DEFAULT now() NOT NULL +); +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "FilterKeywords" ADD CONSTRAINT "FilterKeywords_filterId_Filters_id_fk" FOREIGN KEY ("filterId") REFERENCES "Filters"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; +--> statement-breakpoint +DO $$ BEGIN + ALTER TABLE "Filters" ADD CONSTRAINT "Filters_userId_Users_id_fk" FOREIGN KEY ("userId") REFERENCES "Users"("id") ON DELETE cascade ON UPDATE cascade; +EXCEPTION + WHEN duplicate_object THEN null; +END $$; diff --git a/drizzle/meta/0014_snapshot.json b/drizzle/meta/0014_snapshot.json new file mode 100644 index 00000000..c68a8ac8 --- /dev/null +++ b/drizzle/meta/0014_snapshot.json @@ -0,0 +1,1965 @@ +{ + "id": "7e3ced41-62d0-4afd-acd8-f988d138fe98", + "prevId": "3a5d3182-563a-4d3f-b3be-70811fae42b2", + "version": "5", + "dialect": "pg", + "tables": { + "Applications": { + "name": "Applications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "website": { + "name": "website", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "vapid_key": { + "name": "vapid_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "client_id": { + "name": "client_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "secret": { + "name": "secret", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scopes": { + "name": "scopes", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "redirect_uris": { + "name": "redirect_uris", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "Applications_client_id_index": { + "name": "Applications_client_id_index", + "columns": [ + "client_id" + ], + "isUnique": true + } + }, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Attachments": { + "name": "Attachments", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "remote_url": { + "name": "remote_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "thumbnail_url": { + "name": "thumbnail_url", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "mime_type": { + "name": "mime_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "description": { + "name": "description", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "blurhash": { + "name": "blurhash", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "sha256": { + "name": "sha256", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "fps": { + "name": "fps", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "duration": { + "name": "duration", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "width": { + "name": "width", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "height": { + "name": "height", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "size": { + "name": "size", + "type": "integer", + "primaryKey": false, + "notNull": false + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Attachments_noteId_Notes_id_fk": { + "name": "Attachments_noteId_Notes_id_fk", + "tableFrom": "Attachments", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "EmojiToNote": { + "name": "EmojiToNote", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToNote_emojiId_noteId_index": { + "name": "EmojiToNote_emojiId_noteId_index", + "columns": [ + "emojiId", + "noteId" + ], + "isUnique": true + }, + "EmojiToNote_noteId_index": { + "name": "EmojiToNote_noteId_index", + "columns": [ + "noteId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "EmojiToNote_emojiId_Emojis_id_fk": { + "name": "EmojiToNote_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Emojis", + "columnsFrom": [ + "emojiId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToNote_noteId_Notes_id_fk": { + "name": "EmojiToNote_noteId_Notes_id_fk", + "tableFrom": "EmojiToNote", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "EmojiToUser": { + "name": "EmojiToUser", + "schema": "", + "columns": { + "emojiId": { + "name": "emojiId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "EmojiToUser_emojiId_userId_index": { + "name": "EmojiToUser_emojiId_userId_index", + "columns": [ + "emojiId", + "userId" + ], + "isUnique": true + }, + "EmojiToUser_userId_index": { + "name": "EmojiToUser_userId_index", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "EmojiToUser_emojiId_Emojis_id_fk": { + "name": "EmojiToUser_emojiId_Emojis_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Emojis", + "columnsFrom": [ + "emojiId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "EmojiToUser_userId_Users_id_fk": { + "name": "EmojiToUser_userId_Users_id_fk", + "tableFrom": "EmojiToUser", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Emojis": { + "name": "Emojis", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "shortcode": { + "name": "shortcode", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "url": { + "name": "url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "visible_in_picker": { + "name": "visible_in_picker", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "alt": { + "name": "alt", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Emojis_instanceId_Instances_id_fk": { + "name": "Emojis_instanceId_Instances_id_fk", + "tableFrom": "Emojis", + "tableTo": "Instances", + "columnsFrom": [ + "instanceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "FilterKeywords": { + "name": "FilterKeywords", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "filterId": { + "name": "filterId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "keyword": { + "name": "keyword", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "whole_word": { + "name": "whole_word", + "type": "boolean", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "FilterKeywords_filterId_Filters_id_fk": { + "name": "FilterKeywords_filterId_Filters_id_fk", + "tableFrom": "FilterKeywords", + "tableTo": "Filters", + "columnsFrom": [ + "filterId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Filters": { + "name": "Filters", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "context": { + "name": "context", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "title": { + "name": "title", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "filter_action": { + "name": "filter_action", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "expires_at": { + "name": "expires_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": false + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Filters_userId_Users_id_fk": { + "name": "Filters_userId_Users_id_fk", + "tableFrom": "Filters", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Flags": { + "name": "Flags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "flag_type": { + "name": "flag_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'other'" + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Flags_noteId_Notes_id_fk": { + "name": "Flags_noteId_Notes_id_fk", + "tableFrom": "Flags", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Flags_userId_Users_id_fk": { + "name": "Flags_userId_Users_id_fk", + "tableFrom": "Flags", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Instances": { + "name": "Instances", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "base_url": { + "name": "base_url", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "name": { + "name": "name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "version": { + "name": "version", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "logo": { + "name": "logo", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": {}, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Likes": { + "name": "Likes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "likerId": { + "name": "likerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "likedId": { + "name": "likedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Likes_likerId_Users_id_fk": { + "name": "Likes_likerId_Users_id_fk", + "tableFrom": "Likes", + "tableTo": "Users", + "columnsFrom": [ + "likerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Likes_likedId_Notes_id_fk": { + "name": "Likes_likedId_Notes_id_fk", + "tableFrom": "Likes", + "tableTo": "Notes", + "columnsFrom": [ + "likedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "LysandObject": { + "name": "LysandObject", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "remote_id": { + "name": "remote_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "extra_data": { + "name": "extra_data", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "extensions": { + "name": "extensions", + "type": "jsonb", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "LysandObject_remote_id_index": { + "name": "LysandObject_remote_id_index", + "columns": [ + "remote_id" + ], + "isUnique": true + }, + "LysandObject_uri_index": { + "name": "LysandObject_uri_index", + "columns": [ + "uri" + ], + "isUnique": true + } + }, + "foreignKeys": { + "LysandObject_authorId_LysandObject_id_fk": { + "name": "LysandObject_authorId_LysandObject_id_fk", + "tableFrom": "LysandObject", + "tableTo": "LysandObject", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Markers": { + "name": "Markers", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "notificationId": { + "name": "notificationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "timeline": { + "name": "timeline", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Markers_noteId_Notes_id_fk": { + "name": "Markers_noteId_Notes_id_fk", + "tableFrom": "Markers", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_notificationId_Notifications_id_fk": { + "name": "Markers_notificationId_Notifications_id_fk", + "tableFrom": "Markers", + "tableTo": "Notifications", + "columnsFrom": [ + "notificationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Markers_userId_Users_id_fk": { + "name": "Markers_userId_Users_id_fk", + "tableFrom": "Markers", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "ModNotes": { + "name": "ModNotes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModNotes_noteId_Notes_id_fk": { + "name": "ModNotes_noteId_Notes_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_userId_Users_id_fk": { + "name": "ModNotes_userId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModNotes_modId_Users_id_fk": { + "name": "ModNotes_modId_Users_id_fk", + "tableFrom": "ModNotes", + "tableTo": "Users", + "columnsFrom": [ + "modId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "ModTags": { + "name": "ModTags", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "modId": { + "name": "modId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "tag": { + "name": "tag", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "ModTags_noteId_Notes_id_fk": { + "name": "ModTags_noteId_Notes_id_fk", + "tableFrom": "ModTags", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_userId_Users_id_fk": { + "name": "ModTags_userId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "ModTags_modId_Users_id_fk": { + "name": "ModTags_modId_Users_id_fk", + "tableFrom": "ModTags", + "tableTo": "Users", + "columnsFrom": [ + "modId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "NoteToMentions": { + "name": "NoteToMentions", + "schema": "", + "columns": { + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "NoteToMentions_noteId_userId_index": { + "name": "NoteToMentions_noteId_userId_index", + "columns": [ + "noteId", + "userId" + ], + "isUnique": true + }, + "NoteToMentions_userId_index": { + "name": "NoteToMentions_userId_index", + "columns": [ + "userId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "NoteToMentions_noteId_Notes_id_fk": { + "name": "NoteToMentions_noteId_Notes_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "NoteToMentions_userId_Users_id_fk": { + "name": "NoteToMentions_userId_Users_id_fk", + "tableFrom": "NoteToMentions", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Notes": { + "name": "Notes", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "authorId": { + "name": "authorId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updatedAt": { + "name": "updatedAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "reblogId": { + "name": "reblogId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content": { + "name": "content", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "content_type": { + "name": "content_type", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "'text/plain'" + }, + "visibility": { + "name": "visibility", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "replyId": { + "name": "replyId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "quoteId": { + "name": "quoteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "sensitive": { + "name": "sensitive", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "spoiler_text": { + "name": "spoiler_text", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "content_source": { + "name": "content_source", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + } + }, + "indexes": { + "Notes_uri_index": { + "name": "Notes_uri_index", + "columns": [ + "uri" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Notes_authorId_Users_id_fk": { + "name": "Notes_authorId_Users_id_fk", + "tableFrom": "Notes", + "tableTo": "Users", + "columnsFrom": [ + "authorId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_applicationId_Applications_id_fk": { + "name": "Notes_applicationId_Applications_id_fk", + "tableFrom": "Notes", + "tableTo": "Applications", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Notes_reblogId_Notes_id_fk": { + "name": "Notes_reblogId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": [ + "reblogId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notes_replyId_Notes_id_fk": { + "name": "Notes_replyId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": [ + "replyId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + }, + "Notes_quoteId_Notes_id_fk": { + "name": "Notes_quoteId_Notes_id_fk", + "tableFrom": "Notes", + "tableTo": "Notes", + "columnsFrom": [ + "quoteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Notifications": { + "name": "Notifications", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "type": { + "name": "type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "createdAt": { + "name": "createdAt", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "notifiedId": { + "name": "notifiedId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "accountId": { + "name": "accountId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "dismissed": { + "name": "dismissed", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": {}, + "foreignKeys": { + "Notifications_notifiedId_Users_id_fk": { + "name": "Notifications_notifiedId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": [ + "notifiedId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_accountId_Users_id_fk": { + "name": "Notifications_accountId_Users_id_fk", + "tableFrom": "Notifications", + "tableTo": "Users", + "columnsFrom": [ + "accountId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Notifications_noteId_Notes_id_fk": { + "name": "Notifications_noteId_Notes_id_fk", + "tableFrom": "Notifications", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "OpenIdAccounts": { + "name": "OpenIdAccounts", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "server_id": { + "name": "server_id", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdAccounts_userId_Users_id_fk": { + "name": "OpenIdAccounts_userId_Users_id_fk", + "tableFrom": "OpenIdAccounts", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "set null", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "OpenIdLoginFlows": { + "name": "OpenIdLoginFlows", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "code_verifier": { + "name": "code_verifier", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "issuer_id": { + "name": "issuer_id", + "type": "text", + "primaryKey": false, + "notNull": true + } + }, + "indexes": {}, + "foreignKeys": { + "OpenIdLoginFlows_applicationId_Applications_id_fk": { + "name": "OpenIdLoginFlows_applicationId_Applications_id_fk", + "tableFrom": "OpenIdLoginFlows", + "tableTo": "Applications", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Relationships": { + "name": "Relationships", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "ownerId": { + "name": "ownerId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "subjectId": { + "name": "subjectId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "following": { + "name": "following", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "showing_reblogs": { + "name": "showing_reblogs", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "notifying": { + "name": "notifying", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "followed_by": { + "name": "followed_by", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blocking": { + "name": "blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "blocked_by": { + "name": "blocked_by", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting": { + "name": "muting", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "muting_notifications": { + "name": "muting_notifications", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "requested": { + "name": "requested", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "domain_blocking": { + "name": "domain_blocking", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "endorsed": { + "name": "endorsed", + "type": "boolean", + "primaryKey": false, + "notNull": true + }, + "languages": { + "name": "languages", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + } + }, + "indexes": {}, + "foreignKeys": { + "Relationships_ownerId_Users_id_fk": { + "name": "Relationships_ownerId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": [ + "ownerId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Relationships_subjectId_Users_id_fk": { + "name": "Relationships_subjectId_Users_id_fk", + "tableFrom": "Relationships", + "tableTo": "Users", + "columnsFrom": [ + "subjectId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Tokens": { + "name": "Tokens", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "token_type": { + "name": "token_type", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "scope": { + "name": "scope", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "access_token": { + "name": "access_token", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "code": { + "name": "code", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "applicationId": { + "name": "applicationId", + "type": "uuid", + "primaryKey": false, + "notNull": false + } + }, + "indexes": {}, + "foreignKeys": { + "Tokens_userId_Users_id_fk": { + "name": "Tokens_userId_Users_id_fk", + "tableFrom": "Tokens", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "Tokens_applicationId_Applications_id_fk": { + "name": "Tokens_applicationId_Applications_id_fk", + "tableFrom": "Tokens", + "tableTo": "Applications", + "columnsFrom": [ + "applicationId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "UserToPinnedNotes": { + "name": "UserToPinnedNotes", + "schema": "", + "columns": { + "userId": { + "name": "userId", + "type": "uuid", + "primaryKey": false, + "notNull": true + }, + "noteId": { + "name": "noteId", + "type": "uuid", + "primaryKey": false, + "notNull": true + } + }, + "indexes": { + "UserToPinnedNotes_userId_noteId_index": { + "name": "UserToPinnedNotes_userId_noteId_index", + "columns": [ + "userId", + "noteId" + ], + "isUnique": true + }, + "UserToPinnedNotes_noteId_index": { + "name": "UserToPinnedNotes_noteId_index", + "columns": [ + "noteId" + ], + "isUnique": false + } + }, + "foreignKeys": { + "UserToPinnedNotes_userId_Users_id_fk": { + "name": "UserToPinnedNotes_userId_Users_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Users", + "columnsFrom": [ + "userId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + }, + "UserToPinnedNotes_noteId_Notes_id_fk": { + "name": "UserToPinnedNotes_noteId_Notes_id_fk", + "tableFrom": "UserToPinnedNotes", + "tableTo": "Notes", + "columnsFrom": [ + "noteId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + }, + "Users": { + "name": "Users", + "schema": "", + "columns": { + "id": { + "name": "id", + "type": "uuid", + "primaryKey": true, + "notNull": true, + "default": "uuid_generate_v7()" + }, + "uri": { + "name": "uri", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "username": { + "name": "username", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "display_name": { + "name": "display_name", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "password": { + "name": "password", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "email": { + "name": "email", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "note": { + "name": "note", + "type": "text", + "primaryKey": false, + "notNull": true, + "default": "''" + }, + "is_admin": { + "name": "is_admin", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "endpoints": { + "name": "endpoints", + "type": "jsonb", + "primaryKey": false, + "notNull": false + }, + "source": { + "name": "source", + "type": "jsonb", + "primaryKey": false, + "notNull": true + }, + "avatar": { + "name": "avatar", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "header": { + "name": "header", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "created_at": { + "name": "created_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "updated_at": { + "name": "updated_at", + "type": "timestamp(3)", + "primaryKey": false, + "notNull": true, + "default": "now()" + }, + "is_bot": { + "name": "is_bot", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_locked": { + "name": "is_locked", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "is_discoverable": { + "name": "is_discoverable", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + }, + "sanctions": { + "name": "sanctions", + "type": "text[]", + "primaryKey": false, + "notNull": false + }, + "public_key": { + "name": "public_key", + "type": "text", + "primaryKey": false, + "notNull": true + }, + "private_key": { + "name": "private_key", + "type": "text", + "primaryKey": false, + "notNull": false + }, + "instanceId": { + "name": "instanceId", + "type": "uuid", + "primaryKey": false, + "notNull": false + }, + "disable_automoderation": { + "name": "disable_automoderation", + "type": "boolean", + "primaryKey": false, + "notNull": true, + "default": false + } + }, + "indexes": { + "Users_uri_index": { + "name": "Users_uri_index", + "columns": [ + "uri" + ], + "isUnique": true + }, + "Users_username_index": { + "name": "Users_username_index", + "columns": [ + "username" + ], + "isUnique": true + }, + "Users_email_index": { + "name": "Users_email_index", + "columns": [ + "email" + ], + "isUnique": true + } + }, + "foreignKeys": { + "Users_instanceId_Instances_id_fk": { + "name": "Users_instanceId_Instances_id_fk", + "tableFrom": "Users", + "tableTo": "Instances", + "columnsFrom": [ + "instanceId" + ], + "columnsTo": [ + "id" + ], + "onDelete": "cascade", + "onUpdate": "cascade" + } + }, + "compositePrimaryKeys": {}, + "uniqueConstraints": {} + } + }, + "enums": {}, + "schemas": {}, + "_meta": { + "columns": {}, + "schemas": {}, + "tables": {} + } +} \ No newline at end of file diff --git a/drizzle/meta/_journal.json b/drizzle/meta/_journal.json index 44f81811..c76ec305 100644 --- a/drizzle/meta/_journal.json +++ b/drizzle/meta/_journal.json @@ -1,104 +1,111 @@ { - "version": "5", - "dialect": "pg", - "entries": [ - { - "idx": 0, - "version": "5", - "when": 1712805159664, - "tag": "0000_illegal_living_lightning", - "breakpoints": true - }, - { - "idx": 1, - "version": "5", - "when": 1713055774123, - "tag": "0001_salty_night_thrasher", - "breakpoints": true - }, - { - "idx": 2, - "version": "5", - "when": 1713056370431, - "tag": "0002_stiff_ares", - "breakpoints": true - }, - { - "idx": 3, - "version": "5", - "when": 1713056528340, - "tag": "0003_spicy_arachne", - "breakpoints": true - }, - { - "idx": 4, - "version": "5", - "when": 1713056712218, - "tag": "0004_burly_lockjaw", - "breakpoints": true - }, - { - "idx": 5, - "version": "5", - "when": 1713056917973, - "tag": "0005_sleepy_puma", - "breakpoints": true - }, - { - "idx": 6, - "version": "5", - "when": 1713057159867, - "tag": "0006_messy_network", - "breakpoints": true - }, - { - "idx": 7, - "version": "5", - "when": 1713227918208, - "tag": "0007_naive_sleeper", - "breakpoints": true - }, - { - "idx": 8, - "version": "5", - "when": 1713246700119, - "tag": "0008_flawless_brother_voodoo", - "breakpoints": true - }, - { - "idx": 9, - "version": "5", - "when": 1713327832438, - "tag": "0009_easy_slyde", - "breakpoints": true - }, - { - "idx": 10, - "version": "5", - "when": 1713327880929, - "tag": "0010_daffy_frightful_four", - "breakpoints": true - }, - { - "idx": 11, - "version": "5", - "when": 1713333611707, - "tag": "0011_special_the_fury", - "breakpoints": true - }, - { - "idx": 12, - "version": "5", - "when": 1713336108114, - "tag": "0012_certain_thor_girl", - "breakpoints": true - }, - { - "idx": 13, - "version": "5", - "when": 1713336611301, - "tag": "0013_wandering_celestials", - "breakpoints": true - } - ] -} + "version": "5", + "dialect": "pg", + "entries": [ + { + "idx": 0, + "version": "5", + "when": 1712805159664, + "tag": "0000_illegal_living_lightning", + "breakpoints": true + }, + { + "idx": 1, + "version": "5", + "when": 1713055774123, + "tag": "0001_salty_night_thrasher", + "breakpoints": true + }, + { + "idx": 2, + "version": "5", + "when": 1713056370431, + "tag": "0002_stiff_ares", + "breakpoints": true + }, + { + "idx": 3, + "version": "5", + "when": 1713056528340, + "tag": "0003_spicy_arachne", + "breakpoints": true + }, + { + "idx": 4, + "version": "5", + "when": 1713056712218, + "tag": "0004_burly_lockjaw", + "breakpoints": true + }, + { + "idx": 5, + "version": "5", + "when": 1713056917973, + "tag": "0005_sleepy_puma", + "breakpoints": true + }, + { + "idx": 6, + "version": "5", + "when": 1713057159867, + "tag": "0006_messy_network", + "breakpoints": true + }, + { + "idx": 7, + "version": "5", + "when": 1713227918208, + "tag": "0007_naive_sleeper", + "breakpoints": true + }, + { + "idx": 8, + "version": "5", + "when": 1713246700119, + "tag": "0008_flawless_brother_voodoo", + "breakpoints": true + }, + { + "idx": 9, + "version": "5", + "when": 1713327832438, + "tag": "0009_easy_slyde", + "breakpoints": true + }, + { + "idx": 10, + "version": "5", + "when": 1713327880929, + "tag": "0010_daffy_frightful_four", + "breakpoints": true + }, + { + "idx": 11, + "version": "5", + "when": 1713333611707, + "tag": "0011_special_the_fury", + "breakpoints": true + }, + { + "idx": 12, + "version": "5", + "when": 1713336108114, + "tag": "0012_certain_thor_girl", + "breakpoints": true + }, + { + "idx": 13, + "version": "5", + "when": 1713336611301, + "tag": "0013_wandering_celestials", + "breakpoints": true + }, + { + "idx": 14, + "version": "5", + "when": 1713389937821, + "tag": "0014_wonderful_sandman", + "breakpoints": true + } + ] +} \ No newline at end of file diff --git a/drizzle/schema.ts b/drizzle/schema.ts index 5aedb237..c92d3b90 100644 --- a/drizzle/schema.ts +++ b/drizzle/schema.ts @@ -26,6 +26,50 @@ export const Emojis = pgTable("Emojis", { }), }); +export const Filters = pgTable("Filters", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + userId: uuid("userId") + .notNull() + .references(() => Users.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + context: text("context") + .array() + .$type< + ("home" | "notifications" | "public" | "thread" | "account")[] + >(), + title: text("title").notNull(), + filterAction: text("filter_action").notNull().$type<"warn" | "hide">(), + expireAt: timestamp("expires_at", { precision: 3, mode: "string" }), + createdAt: timestamp("created_at", { precision: 3, mode: "string" }) + .defaultNow() + .notNull(), +}); + +export const FilterKeywords = pgTable("FilterKeywords", { + id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), + filterId: uuid("filterId") + .notNull() + .references(() => Filters.id, { + onDelete: "cascade", + onUpdate: "cascade", + }), + keyword: text("keyword").notNull(), + wholeWord: boolean("whole_word").notNull(), +}); + +export const FilterRelations = relations(Filters, ({ many }) => ({ + keywords: many(FilterKeywords), +})); + +export const FilterKeywordsRelations = relations(FilterKeywords, ({ one }) => ({ + filter: one(Filters, { + fields: [FilterKeywords.filterId], + references: [Filters.id], + }), +})); + export const Markers = pgTable("Markers", { id: uuid("id").default(sql`uuid_generate_v7()`).primaryKey().notNull(), noteId: uuid("noteId").references(() => Notes.id, { diff --git a/packages/request-parser/index.ts b/packages/request-parser/index.ts index b0740abb..d45eee98 100644 --- a/packages/request-parser/index.ts +++ b/packages/request-parser/index.ts @@ -1,9 +1,4 @@ -/** - * RequestParser - * @file index.ts - * @module request-parser - * @description Parses Request object into a JavaScript object based on the content type - */ +import { parse } from "qs"; /** * RequestParser @@ -98,19 +93,38 @@ export class RequestParser { const formData = await this.request.formData(); const result: Partial = {}; - for (const [key, value] of formData.entries()) { - if (value instanceof Blob) { - result[key as keyof T] = value as T[keyof T]; - } else if (key.endsWith("[]")) { - const arrayKey = key.slice(0, -2) as keyof T; - if (!result[arrayKey]) { - result[arrayKey] = [] as T[keyof T]; - } + // Check if there are any files in the FormData + if ( + Array.from(formData.values()).some((value) => value instanceof Blob) + ) { + for (const [key, value] of formData.entries()) { + if (value instanceof Blob) { + result[key as keyof T] = value as T[keyof T]; + } else if (key.endsWith("[]")) { + const arrayKey = key.slice(0, -2) as keyof T; + if (!result[arrayKey]) { + result[arrayKey] = [] as T[keyof T]; + } - (result[arrayKey] as FormDataEntryValue[]).push(value); - } else { - result[key as keyof T] = value as T[keyof T]; + (result[arrayKey] as FormDataEntryValue[]).push(value); + } else { + result[key as keyof T] = value as T[keyof T]; + } } + } else { + // Convert to URLSearchParams and parse as query + const searchParams = new URLSearchParams([ + ...formData.entries(), + ] as [string, string][]); + + const parsed = parse(searchParams.toString(), { + parseArrays: true, + interpretNumericEntities: true, + }); + + return castBooleanObject( + parsed as PossiblyRecursiveObject, + ) as Partial; } return result; @@ -159,29 +173,49 @@ export class RequestParser { * @returns JavaScript object of type T */ private parseQuery(): Partial { - const result: Partial = {}; - const url = new URL(this.request.url); + const parsed = parse( + new URL(this.request.url).searchParams.toString(), + { + parseArrays: true, + interpretNumericEntities: true, + }, + ); - for (const [key, value] of url.searchParams.entries()) { - if (decodeURIComponent(key).endsWith("[]")) { - const arrayKey = decodeURIComponent(key).slice( - 0, - -2, - ) as keyof T; - if (!result[arrayKey]) { - result[arrayKey] = [] as T[keyof T]; - } - (result[arrayKey] as string[]).push(decodeURIComponent(value)); - } else { - result[key as keyof T] = castBoolean( - decodeURIComponent(value), - ) as T[keyof T]; - } - } - return result; + return castBooleanObject( + parsed as PossiblyRecursiveObject, + ) as Partial; } } +interface PossiblyRecursiveObject { + [key: string]: + | PossiblyRecursiveObject[] + | PossiblyRecursiveObject + | string + | string[] + | boolean; +} + +// Recursive +const castBooleanObject = (value: PossiblyRecursiveObject | string) => { + if (typeof value === "string") { + return castBoolean(value); + } + + for (const key in value) { + const child = value[key]; + if (Array.isArray(child)) { + value[key] = child.map((v) => castBooleanObject(v)) as string[]; + } else if (typeof child === "object") { + value[key] = castBooleanObject(child); + } else { + value[key] = castBoolean(child as string); + } + } + + return value; +}; + const castBoolean = (value: string) => { if (["true"].includes(value)) { return true; diff --git a/packages/request-parser/package.json b/packages/request-parser/package.json index 2f2d3fdb..22afee91 100644 --- a/packages/request-parser/package.json +++ b/packages/request-parser/package.json @@ -1,6 +1,9 @@ { - "name": "request-parser", - "version": "0.0.0", - "main": "index.ts", - "dependencies": {} + "name": "request-parser", + "version": "0.0.0", + "main": "index.ts", + "dependencies": { "qs": "^6.12.1" }, + "devDependencies": { + "@types/qs": "^6.9.15" + } } diff --git a/packages/request-parser/tests/request-parser.test.ts b/packages/request-parser/tests/request-parser.test.ts index bfa82ea4..e724b9da 100644 --- a/packages/request-parser/tests/request-parser.test.ts +++ b/packages/request-parser/tests/request-parser.test.ts @@ -24,6 +24,28 @@ describe("RequestParser", () => { expect(result.test).toEqual(["value1", "value2"]); }); + test("With Array of objects", async () => { + const request = new Request( + "http://localhost?test[][key]=value1&test[][value]=value2", + ); + const result = await new RequestParser(request).toObject<{ + test: { key: string; value: string }[]; + }>(); + expect(result.test).toEqual([{ key: "value1", value: "value2" }]); + }); + + test("With Array of multiple objects", async () => { + const request = new Request( + "http://localhost?test[][key]=value1&test[][value]=value2&test[][key]=value3&test[][value]=value4", + ); + const result = await new RequestParser(request).toObject<{ + test: { key: string[]; value: string[] }[]; + }>(); + expect(result.test).toEqual([ + { key: ["value1", "value3"], value: ["value2", "value4"] }, + ]); + }); + test("With both at once", async () => { const request = new Request( "http://localhost?param1=value1¶m2=value2&test[]=value1&test[]=value2", diff --git a/packages/server-handler/index.ts b/packages/server-handler/index.ts index b3eedf0e..fab64556 100644 --- a/packages/server-handler/index.ts +++ b/packages/server-handler/index.ts @@ -86,14 +86,12 @@ export const processRoute = async ( return errorResponse("Method not allowed", 405); } - let auth: AuthData | null = null; + const auth: AuthData = await getFromRequest(request); if ( route.meta.auth.required || route.meta.auth.requiredOnMethods?.includes(request.method as HttpVerb) ) { - auth = await getFromRequest(request); - if (!auth.user) { return errorResponse( "Unauthorized: access to this method requires an authenticated user", @@ -112,7 +110,7 @@ export const processRoute = async ( } } - const parsedRequest = await new RequestParser(request) + const parsedRequest = await new RequestParser(request.clone()) .toObject() .catch(async (err) => { await logger.logError( diff --git a/server/api/api/v1/notifications/index.test.ts b/server/api/api/v1/notifications/index.test.ts index 0c0a0ee3..ea0350df 100644 --- a/server/api/api/v1/notifications/index.test.ts +++ b/server/api/api/v1/notifications/index.test.ts @@ -107,4 +107,71 @@ describe(meta.route, () => { ); } }); + + test("should not return notifications with filtered keywords", async () => { + const formData = new FormData(); + + formData.append("title", "Test Filter"); + formData.append("context[]", "notifications"); + formData.append("filter_action", "hide"); + formData.append( + "keywords_attributes[0][keyword]", + timeline[0].content.slice(4, 20), + ); + formData.append("keywords_attributes[0][whole_word]", "false"); + + const filterResponse = await sendTestRequest( + new Request(new URL("/api/v2/filters", config.http.base_url), { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + body: formData, + }), + ); + + expect(filterResponse.status).toBe(200); + + const response = await sendTestRequest( + new Request( + new URL(`${meta.route}?limit=20`, config.http.base_url), + { + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + + const objects = (await response.json()) as APINotification[]; + + expect(objects.length).toBe(3); + // There should be no element with a status with id of timeline[0].id + expect(objects).not.toContainEqual( + expect.objectContaining({ + status: expect.objectContaining({ id: timeline[0].id }), + }), + ); + + // Delete filter + const filterDeleteResponse = await sendTestRequest( + new Request( + new URL( + `/api/v2/filters/${(await filterResponse.json()).id}`, + config.http.base_url, + ), + { + method: "DELETE", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(filterDeleteResponse.status).toBe(200); + }); }); diff --git a/server/api/api/v1/notifications/index.ts b/server/api/api/v1/notifications/index.ts index f26c0f38..b3e44079 100644 --- a/server/api/api/v1/notifications/index.ts +++ b/server/api/api/v1/notifications/index.ts @@ -1,6 +1,7 @@ import { apiRoute, applyConfig, idValidator } from "@api"; import { errorResponse, jsonResponse } from "@response"; import { fetchTimeline } from "@timelines"; +import { sql } from "drizzle-orm"; import { z } from "zod"; import { findManyNotifications, @@ -127,6 +128,24 @@ export default apiRoute( exclude_types ? not(inArray(notification.type, exclude_types)) : undefined, + // Don't show notes that have filtered words in them (via Notification.note.content via Notification.noteId) + // Filters in `Filters` table have keyword in `FilterKeywords` table (use LIKE) + // Filters table has a userId and a context which is an array + sql`NOT EXISTS ( + SELECT 1 + FROM "Filters" + WHERE "Filters"."userId" = ${user.id} + AND "Filters"."filter_action" = 'hide' + AND EXISTS ( + SELECT 1 + FROM "FilterKeywords", "Notifications" as "n_inner", "Notes" + WHERE "FilterKeywords"."filterId" = "Filters"."id" + AND "n_inner"."noteId" = "Notes"."id" + AND "Notes"."content" LIKE '%' || "FilterKeywords"."keyword" || '%' + AND "n_inner"."id" = "Notifications"."id" + ) + AND "Filters"."context" @> ARRAY['notifications'] + )`, ), limit, // @ts-expect-error Yes I KNOW the types are wrong diff --git a/server/api/api/v1/timelines/home.test.ts b/server/api/api/v1/timelines/home.test.ts index f8f5370c..30fa6320 100644 --- a/server/api/api/v1/timelines/home.test.ts +++ b/server/api/api/v1/timelines/home.test.ts @@ -172,5 +172,72 @@ describe(meta.route, () => { expect(status.id).toBe(timeline[index].id); } }); + + test("should not return statuses with filtered keywords", async () => { + const formData = new FormData(); + + formData.append("title", "Test Filter"); + formData.append("context[]", "home"); + formData.append("filter_action", "hide"); + formData.append( + "keywords_attributes[0][keyword]", + timeline[0].content.slice(4, 20), + ); + formData.append("keywords_attributes[0][whole_word]", "false"); + + const filterResponse = await sendTestRequest( + new Request(new URL("/api/v2/filters", config.http.base_url), { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + body: formData, + }), + ); + + expect(filterResponse.status).toBe(200); + + const response = await sendTestRequest( + new Request( + new URL(`${meta.route}?limit=20`, config.http.base_url), + { + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe( + "application/json", + ); + + const objects = (await response.json()) as APIStatus[]; + + expect(objects.length).toBe(20); + // There should be no element with id of timeline[0].id + expect(objects).not.toContainEqual( + expect.objectContaining({ id: timeline[0].id }), + ); + + // Delete filter + const filterDeleteResponse = await sendTestRequest( + new Request( + new URL( + `/api/v2/filters/${(await filterResponse.json()).id}`, + config.http.base_url, + ), + { + method: "DELETE", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(filterDeleteResponse.status).toBe(200); + }); }); }); diff --git a/server/api/api/v1/timelines/home.ts b/server/api/api/v1/timelines/home.ts index 9b4be191..e7f5b623 100644 --- a/server/api/api/v1/timelines/home.ts +++ b/server/api/api/v1/timelines/home.ts @@ -49,6 +49,10 @@ export default apiRoute( // WHERE format (... = ...) sql`EXISTS (SELECT 1 FROM "Relationships" WHERE "Relationships"."subjectId" = ${Notes.authorId} AND "Relationships"."ownerId" = ${user.id} AND "Relationships"."following" = true)`, ), + // Don't show statuses that have filtered words in them + // Filters in `Filters` table have keyword in `FilterKeywords` table (use LIKE) + // Filters table has a userId and a context which is an array + sql`NOT EXISTS (SELECT 1 FROM "Filters" WHERE "Filters"."userId" = ${user.id} AND "Filters"."filter_action" = 'hide' AND EXISTS (SELECT 1 FROM "FilterKeywords" WHERE "FilterKeywords"."filterId" = "Filters"."id" AND "Notes"."content" LIKE '%' || "FilterKeywords"."keyword" || '%') AND "Filters"."context" @> ARRAY['home'])`, ), limit, req.url, diff --git a/server/api/api/v1/timelines/public.test.ts b/server/api/api/v1/timelines/public.test.ts index 902d5213..e8814eba 100644 --- a/server/api/api/v1/timelines/public.test.ts +++ b/server/api/api/v1/timelines/public.test.ts @@ -218,4 +218,69 @@ describe(meta.route, () => { } }); }); + + test("should not return statuses with filtered keywords", async () => { + const formData = new FormData(); + + formData.append("title", "Test Filter"); + formData.append("context[]", "public"); + formData.append("filter_action", "hide"); + formData.append( + "keywords_attributes[0][keyword]", + timeline[0].content.slice(4, 20), + ); + formData.append("keywords_attributes[0][whole_word]", "false"); + + const filterResponse = await sendTestRequest( + new Request(new URL("/api/v2/filters", config.http.base_url), { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + body: formData, + }), + ); + + expect(filterResponse.status).toBe(200); + + const response = await sendTestRequest( + new Request( + new URL(`${meta.route}?limit=20`, config.http.base_url), + { + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(response.status).toBe(200); + expect(response.headers.get("content-type")).toBe("application/json"); + + const objects = (await response.json()) as APIStatus[]; + + expect(objects.length).toBe(20); + // There should be no element with id of timeline[0].id + expect(objects).not.toContainEqual( + expect.objectContaining({ id: timeline[0].id }), + ); + + // Delete filter + const filterDeleteResponse = await sendTestRequest( + new Request( + new URL( + `/api/v2/filters/${(await filterResponse.json()).id}`, + config.http.base_url, + ), + { + method: "DELETE", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(filterDeleteResponse.status).toBe(200); + }); }); diff --git a/server/api/api/v1/timelines/public.ts b/server/api/api/v1/timelines/public.ts index 44a027cb..ed0886ba 100644 --- a/server/api/api/v1/timelines/public.ts +++ b/server/api/api/v1/timelines/public.ts @@ -52,6 +52,9 @@ export default apiRoute( only_media ? sql`EXISTS (SELECT 1 FROM "Attachments" WHERE "Attachments"."noteId" = ${Notes.id})` : undefined, + user + ? sql`NOT EXISTS (SELECT 1 FROM "Filters" WHERE "Filters"."userId" = ${user.id} AND "Filters"."filter_action" = 'hide' AND EXISTS (SELECT 1 FROM "FilterKeywords" WHERE "FilterKeywords"."filterId" = "Filters"."id" AND "Notes"."content" LIKE '%' || "FilterKeywords"."keyword" || '%') AND "Filters"."context" @> ARRAY['public'])` + : undefined, ), limit, req.url, diff --git a/server/api/api/v2/filters/[id]/index.test.ts b/server/api/api/v2/filters/[id]/index.test.ts new file mode 100644 index 00000000..6526f4bb --- /dev/null +++ b/server/api/api/v2/filters/[id]/index.test.ts @@ -0,0 +1,203 @@ +import { afterAll, describe, expect, test } from "bun:test"; +import { config } from "config-manager"; +import { getTestUsers, sendTestRequest } from "~tests/utils"; +import { meta } from "./index"; + +const { users, tokens, deleteUsers } = await getTestUsers(2); + +const formData = new FormData(); + +formData.append("title", "Test Filter"); +formData.append("context[]", "home"); +formData.append("filter_action", "warn"); +formData.append("expires_in", "86400"); +formData.append("keywords_attributes[0][keyword]", "test"); +formData.append("keywords_attributes[0][whole_word]", "true"); + +const response = await sendTestRequest( + new Request(new URL("/api/v2/filters", config.http.base_url), { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + body: formData, + }), +); + +expect(response.status).toBe(200); + +const filter = await response.json(); +expect(filter).toBeObject(); + +afterAll(async () => { + await deleteUsers(); +}); + +// /api/v2/filters/:id +describe(meta.route, () => { + test("should return 401 if not authenticated", async () => { + const response = await sendTestRequest( + new Request(new URL(meta.route, config.http.base_url)), + ); + + expect(response.status).toBe(401); + }); + + test("should get that filter", async () => { + const response = await sendTestRequest( + new Request( + new URL( + meta.route.replace(":id", filter.id), + config.http.base_url, + ), + { + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(response.status).toBe(200); + + const json = await response.json(); + expect(json).toBeObject(); + expect(json).toContainKeys(["id", "title"]); + expect(json.title).toBe("Test Filter"); + expect(json.context).toEqual(["home"]); + expect(json.filter_action).toBe("warn"); + expect(json.expires_at).toBeString(); + expect(json.keywords).toBeArray(); + expect(json.keywords).not.toBeEmpty(); + expect(json.keywords[0]).toContainKeys(["keyword", "whole_word"]); + expect(json.keywords[0].keyword).toEqual("test"); + }); + + test("should edit that filter", async () => { + const formData = new FormData(); + + formData.append("title", "New Filter"); + formData.append("context[]", "notifications"); + formData.append("filter_action", "hide"); + formData.append("expires_in", "86400"); + formData.append("keywords_attributes[0][keyword]", "new"); + formData.append("keywords_attributes[0][id]", filter.keywords[0].id); + formData.append("keywords_attributes[0][whole_word]", "false"); + + const response = await sendTestRequest( + new Request( + new URL( + meta.route.replace(":id", filter.id), + config.http.base_url, + ), + { + method: "PUT", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + body: formData, + }, + ), + ); + + expect(response.status).toBe(200); + + const json = await response.json(); + expect(json).toBeObject(); + expect(json).toContainKeys(["id", "title"]); + expect(json.title).toBe("New Filter"); + expect(json.context).toEqual(["notifications"]); + expect(json.filter_action).toBe("hide"); + expect(json.expires_at).toBeString(); + expect(json.keywords).toBeArray(); + expect(json.keywords).not.toBeEmpty(); + expect(json.keywords[0]).toContainKeys(["keyword", "whole_word"]); + expect(json.keywords[0].keyword).toEqual("new"); + }); + + test("should delete keyword", async () => { + const formData = new FormData(); + + formData.append("keywords_attributes[0][id]", filter.keywords[0].id); + formData.append("keywords_attributes[0][_destroy]", "true"); + + const response = await sendTestRequest( + new Request( + new URL( + meta.route.replace(":id", filter.id), + config.http.base_url, + ), + { + method: "PUT", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + body: formData, + }, + ), + ); + + expect(response.status).toBe(200); + + const json = await response.json(); + expect(json).toBeObject(); + expect(json.keywords).toBeEmpty(); + + // Get the filter again and check + const getResponse = await sendTestRequest( + new Request( + new URL( + meta.route.replace(":id", filter.id), + config.http.base_url, + ), + { + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(getResponse.status).toBe(200); + expect((await getResponse.json()).keywords).toBeEmpty(); + }); + + test("should delete filter", async () => { + const formData = new FormData(); + + const response = await sendTestRequest( + new Request( + new URL( + meta.route.replace(":id", filter.id), + config.http.base_url, + ), + { + method: "DELETE", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + body: formData, + }, + ), + ); + + expect(response.status).toBe(200); + + // Try to GET the filter again + const getResponse = await sendTestRequest( + new Request( + new URL( + meta.route.replace(":id", filter.id), + config.http.base_url, + ), + { + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }, + ), + ); + + expect(getResponse.status).toBe(404); + }); +}); diff --git a/server/api/api/v2/filters/[id]/index.ts b/server/api/api/v2/filters/[id]/index.ts new file mode 100644 index 00000000..0998c359 --- /dev/null +++ b/server/api/api/v2/filters/[id]/index.ts @@ -0,0 +1,178 @@ +import { apiRoute, applyConfig, idValidator } from "@api"; +import { errorResponse, jsonResponse } from "@response"; +import { and, eq, inArray, type InferSelectModel } from "drizzle-orm"; +import { z } from "zod"; +import { db } from "~drizzle/db"; +import { FilterKeywords, Filters } from "~drizzle/schema"; + +export const meta = applyConfig({ + allowedMethods: ["GET", "PUT", "DELETE"], + route: "/api/v2/filters/:id", + ratelimits: { + max: 60, + duration: 60, + }, + auth: { + required: true, + }, +}); + +export const schema = z.object({ + title: z.string().min(1).max(100).optional(), + context: z + .array(z.enum(["home", "notifications", "public", "thread", "account"])) + .optional(), + filter_action: z.enum(["warn", "hide"]).optional().default("warn"), + expires_in: z.coerce + .number() + .int() + .min(60) + .max(60 * 60 * 24 * 365 * 5) + .optional(), + keywords_attributes: z + .array( + z.object({ + keyword: z.string().min(1).max(100).optional(), + id: z.string().regex(idValidator).optional(), + whole_word: z.boolean().optional(), + _destroy: z.boolean().optional(), + }), + ) + .optional(), +}); + +export default apiRoute( + async (req, matchedRoute, extraData) => { + const { user } = extraData.auth; + const id = matchedRoute.params.id; + if (!id.match(idValidator)) return errorResponse("Invalid ID", 400); + + if (!user) return errorResponse("Unauthorized", 401); + + const userFilter = await db.query.Filters.findFirst({ + where: (filter, { eq, and }) => + and(eq(filter.userId, user.id), eq(filter.id, id)), + with: { + keywords: true, + }, + }); + + if (!userFilter) return errorResponse("Filter not found", 404); + + switch (req.method) { + case "GET": { + return jsonResponse({ + id: userFilter.id, + title: userFilter.title, + context: userFilter.context, + expires_at: userFilter.expireAt + ? new Date(userFilter.expireAt).toISOString() + : null, + filter_action: userFilter.filterAction, + keywords: userFilter.keywords.map((keyword) => ({ + id: keyword.id, + keyword: keyword.keyword, + whole_word: keyword.wholeWord, + })), + statuses: [], + }); + } + case "PUT": { + const { + title, + context, + filter_action, + expires_in, + keywords_attributes, + } = extraData.parsedRequest; + + await db + .update(Filters) + .set({ + title, + context, + filterAction: filter_action, + expireAt: new Date( + Date.now() + (expires_in ?? 0), + ).toISOString(), + }) + .where( + and(eq(Filters.userId, user.id), eq(Filters.id, id)), + ); + + const toUpdate = keywords_attributes + ?.filter((keyword) => keyword.id && !keyword._destroy) + .map((keyword) => ({ + keyword: keyword.keyword, + wholeWord: keyword.whole_word ?? false, + id: keyword.id, + })); + + const toDelete = keywords_attributes + ?.filter((keyword) => keyword._destroy && keyword.id) + .map((keyword) => keyword.id ?? ""); + + if (toUpdate && toUpdate.length > 0) { + for (const keyword of toUpdate) { + await db + .update(FilterKeywords) + .set(keyword) + .where( + and( + eq(FilterKeywords.filterId, id), + eq(FilterKeywords.id, keyword.id ?? ""), + ), + ); + } + } + + if (toDelete && toDelete.length > 0) { + await db + .delete(FilterKeywords) + .where( + and( + eq(FilterKeywords.filterId, id), + inArray(FilterKeywords.id, toDelete), + ), + ); + } + + const updatedFilter = await db.query.Filters.findFirst({ + where: (filter, { eq, and }) => + and(eq(filter.userId, user.id), eq(filter.id, id)), + with: { + keywords: true, + }, + }); + + if (!updatedFilter) + return errorResponse("Failed to update filter", 500); + + return jsonResponse({ + id: updatedFilter.id, + title: updatedFilter.title, + context: updatedFilter.context, + expires_at: updatedFilter.expireAt + ? new Date(updatedFilter.expireAt).toISOString() + : null, + filter_action: updatedFilter.filterAction, + keywords: updatedFilter.keywords.map((keyword) => ({ + id: keyword.id, + keyword: keyword.keyword, + whole_word: keyword.wholeWord, + })), + statuses: [], + }); + } + case "DELETE": { + await db + .delete(Filters) + .where( + and(eq(Filters.userId, user.id), eq(Filters.id, id)), + ); + + return jsonResponse({}); + } + } + }, +); diff --git a/server/api/api/v2/filters/index.test.ts b/server/api/api/v2/filters/index.test.ts new file mode 100644 index 00000000..cac5d44c --- /dev/null +++ b/server/api/api/v2/filters/index.test.ts @@ -0,0 +1,72 @@ +import { afterAll, describe, expect, test } from "bun:test"; +import { config } from "config-manager"; +import { getTestUsers, sendTestRequest } from "~tests/utils"; +import { meta } from "./index"; + +const { users, tokens, deleteUsers } = await getTestUsers(2); + +afterAll(async () => { + await deleteUsers(); +}); + +// /api/v2/filters +describe(meta.route, () => { + test("should return 401 if not authenticated", async () => { + const response = await sendTestRequest( + new Request(new URL(meta.route, config.http.base_url)), + ); + + expect(response.status).toBe(401); + }); + + test("should return user filters (none)", async () => { + const response = await sendTestRequest( + new Request(new URL(meta.route, config.http.base_url), { + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + }), + ); + + expect(response.status).toBe(200); + + const json = await response.json(); + expect(json).toBeArray(); + expect(json).toBeEmpty(); + }); + + test("should create a new filter", async () => { + const formData = new FormData(); + + formData.append("title", "Test Filter"); + formData.append("context[]", "home"); + formData.append("filter_action", "warn"); + formData.append("expires_in", "86400"); + formData.append("keywords_attributes[0][keyword]", "test"); + formData.append("keywords_attributes[0][whole_word]", "true"); + + const response = await sendTestRequest( + new Request(new URL(meta.route, config.http.base_url), { + method: "POST", + headers: { + Authorization: `Bearer ${tokens[0].accessToken}`, + }, + body: formData, + }), + ); + + expect(response.status).toBe(200); + + const json = await response.json(); + expect(json).toBeObject(); + expect(json).toContainKeys(["id", "title"]); + expect(json.title).toBe("Test Filter"); + expect(json.context).toEqual(["home"]); + expect(json.filter_action).toBe("warn"); + expect(json.expires_at).toBeString(); + expect(json.keywords).toBeArray(); + expect(json.keywords).not.toBeEmpty(); + expect(json.keywords[0]).toContainKeys(["keyword", "whole_word"]); + expect(json.keywords[0].keyword).toEqual("test"); + }); +}); diff --git a/server/api/api/v2/filters/index.ts b/server/api/api/v2/filters/index.ts new file mode 100644 index 00000000..b5dd0698 --- /dev/null +++ b/server/api/api/v2/filters/index.ts @@ -0,0 +1,155 @@ +import { apiRoute, applyConfig } from "@api"; +import { errorResponse, jsonResponse } from "@response"; +import type { InferSelectModel } from "drizzle-orm"; +import { z } from "zod"; +import { db } from "~drizzle/db"; +import { FilterKeywords, Filters } from "~drizzle/schema"; + +export const meta = applyConfig({ + allowedMethods: ["GET", "POST"], + route: "/api/v2/filters", + ratelimits: { + max: 60, + duration: 60, + }, + auth: { + required: true, + }, +}); + +export const schema = z.object({ + title: z.string().min(1).max(100).optional(), + context: z + .array(z.enum(["home", "notifications", "public", "thread", "account"])) + .optional(), + filter_action: z.enum(["warn", "hide"]).optional().default("warn"), + expires_in: z.coerce + .number() + .int() + .min(60) + .max(60 * 60 * 24 * 365 * 5) + .optional(), + keywords_attributes: z + .array( + z.object({ + keyword: z.string().min(1).max(100), + whole_word: z.boolean().optional(), + }), + ) + .optional(), +}); + +export default apiRoute( + async (req, matchedRoute, extraData) => { + const { user } = extraData.auth; + + if (!user) return errorResponse("Unauthorized", 401); + + switch (req.method) { + case "GET": { + const userFilters = await db.query.Filters.findMany({ + where: (filter, { eq }) => eq(filter.userId, user.id), + with: { + keywords: true, + }, + }); + + return jsonResponse( + userFilters.map((filter) => ({ + id: filter.id, + title: filter.title, + context: filter.context, + expires_at: filter.expireAt + ? new Date( + Date.now() + filter.expireAt, + ).toISOString() + : null, + filter_action: filter.filterAction, + keywords: filter.keywords.map((keyword) => ({ + id: keyword.id, + keyword: keyword.keyword, + whole_word: keyword.wholeWord, + })), + statuses: [], + })), + ); + } + case "POST": { + const { + title, + context, + filter_action, + expires_in, + keywords_attributes, + } = extraData.parsedRequest; + + if (!title || context?.length === 0) { + return errorResponse( + "Missing required fields (title and context)", + 422, + ); + } + + const newFilter = ( + await db + .insert(Filters) + .values({ + title: title ?? "", + context: context ?? [], + filterAction: filter_action, + expireAt: new Date( + Date.now() + (expires_in ?? 0), + ).toISOString(), + userId: user.id, + }) + .returning() + )[0]; + + if (!newFilter) + return errorResponse("Failed to create filter", 500); + + const insertedKeywords = + keywords_attributes && keywords_attributes.length > 0 + ? await db + .insert(FilterKeywords) + .values( + keywords_attributes?.map((keyword) => ({ + filterId: newFilter.id, + keyword: keyword.keyword, + wholeWord: keyword.whole_word ?? false, + })) ?? [], + ) + .returning() + : []; + + return jsonResponse({ + id: newFilter.id, + title: newFilter.title, + context: newFilter.context, + expires_at: expires_in + ? new Date(Date.now() + expires_in).toISOString() + : null, + filter_action: newFilter.filterAction, + keywords: insertedKeywords.map((keyword) => ({ + id: keyword.id, + keyword: keyword.keyword, + whole_word: keyword.wholeWord, + })), + statuses: [], + } as { + id: string; + title: string; + context: string[]; + expires_at: string; + filter_action: "warn" | "hide"; + keywords: { + id: string; + keyword: string; + whole_word: boolean; + }[]; + statuses: []; + }); + } + } + }, +);