diff --git a/docs/downgrade.sql b/docs/downgrade.sql index 64622c3af7..192800257b 100644 --- a/docs/downgrade.sql +++ b/docs/downgrade.sql @@ -1,6 +1,7 @@ BEGIN; DELETE FROM "migrations" WHERE name IN ( + 'NoteFile1710304584214', 'RenameMetaColumns1705944717480', 'SeparateHardMuteWordsAndPatterns1706413792769', 'IndexAltTextAndCw1708872574733', @@ -16,6 +17,9 @@ DELETE FROM "migrations" WHERE name IN ( 'RemoveNativeUtilsMigration1705877093218' ); +-- note-file +DROP TABLE "note_file"; + -- rename-meta-columns ALTER TABLE "meta" RENAME COLUMN "tosUrl" TO "ToSUrl"; ALTER TABLE "meta" RENAME COLUMN "objectStorageUseSsl" TO "objectStorageUseSSL"; diff --git a/packages/backend-rs/src/model/entity/drive_file.rs b/packages/backend-rs/src/model/entity/drive_file.rs index 1a7a0f35b1..a5f7e9bfca 100644 --- a/packages/backend-rs/src/model/entity/drive_file.rs +++ b/packages/backend-rs/src/model/entity/drive_file.rs @@ -64,6 +64,8 @@ pub enum Relation { DriveFolder, #[sea_orm(has_many = "super::messaging_message::Entity")] MessagingMessage, + #[sea_orm(has_many = "super::note_file::Entity")] + NoteFile, #[sea_orm(has_many = "super::page::Entity")] Page, #[sea_orm( @@ -94,6 +96,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::NoteFile.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::Page.def() diff --git a/packages/backend-rs/src/model/entity/mod.rs b/packages/backend-rs/src/model/entity/mod.rs index 38be7561b1..223b07fb4f 100644 --- a/packages/backend-rs/src/model/entity/mod.rs +++ b/packages/backend-rs/src/model/entity/mod.rs @@ -35,6 +35,7 @@ pub mod muting; pub mod note; pub mod note_edit; pub mod note_favorite; +pub mod note_file; pub mod note_reaction; pub mod note_thread_muting; pub mod note_unread; diff --git a/packages/backend-rs/src/model/entity/note.rs b/packages/backend-rs/src/model/entity/note.rs index 4774cae952..c851c776e2 100644 --- a/packages/backend-rs/src/model/entity/note.rs +++ b/packages/backend-rs/src/model/entity/note.rs @@ -100,6 +100,8 @@ pub enum Relation { NoteEdit, #[sea_orm(has_many = "super::note_favorite::Entity")] NoteFavorite, + #[sea_orm(has_many = "super::note_file::Entity")] + NoteFile, #[sea_orm(has_many = "super::note_reaction::Entity")] NoteReaction, #[sea_orm(has_many = "super::note_unread::Entity")] @@ -164,6 +166,12 @@ impl Related for Entity { } } +impl Related for Entity { + fn to() -> RelationDef { + Relation::NoteFile.def() + } +} + impl Related for Entity { fn to() -> RelationDef { Relation::NoteReaction.def() diff --git a/packages/backend-rs/src/model/entity/note_file.rs b/packages/backend-rs/src/model/entity/note_file.rs new file mode 100644 index 0000000000..c75222360b --- /dev/null +++ b/packages/backend-rs/src/model/entity/note_file.rs @@ -0,0 +1,48 @@ +//! `SeaORM` Entity. Generated by sea-orm-codegen 0.12.12 + +use sea_orm::entity::prelude::*; + +#[derive(Clone, Debug, PartialEq, DeriveEntityModel, Eq)] +#[sea_orm(table_name = "note_file")] +pub struct Model { + #[sea_orm(column_name = "serialNo", primary_key)] + pub serial_no: i64, + #[sea_orm(column_name = "noteId")] + pub note_id: String, + #[sea_orm(column_name = "fileId")] + pub file_id: String, +} + +#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)] +pub enum Relation { + #[sea_orm( + belongs_to = "super::drive_file::Entity", + from = "Column::FileId", + to = "super::drive_file::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + DriveFile, + #[sea_orm( + belongs_to = "super::note::Entity", + from = "Column::NoteId", + to = "super::note::Column::Id", + on_update = "NoAction", + on_delete = "Cascade" + )] + Note, +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::DriveFile.def() + } +} + +impl Related for Entity { + fn to() -> RelationDef { + Relation::Note.def() + } +} + +impl ActiveModelBehavior for ActiveModel {} diff --git a/packages/backend-rs/src/model/entity/prelude.rs b/packages/backend-rs/src/model/entity/prelude.rs index 10d3795362..0935ac9ff7 100644 --- a/packages/backend-rs/src/model/entity/prelude.rs +++ b/packages/backend-rs/src/model/entity/prelude.rs @@ -33,6 +33,7 @@ pub use super::muting::Entity as Muting; pub use super::note::Entity as Note; pub use super::note_edit::Entity as NoteEdit; pub use super::note_favorite::Entity as NoteFavorite; +pub use super::note_file::Entity as NoteFile; pub use super::note_reaction::Entity as NoteReaction; pub use super::note_thread_muting::Entity as NoteThreadMuting; pub use super::note_unread::Entity as NoteUnread; diff --git a/packages/backend/src/db/postgre.ts b/packages/backend/src/db/postgre.ts index b14f028e8b..b6c3f0db8f 100644 --- a/packages/backend/src/db/postgre.ts +++ b/packages/backend/src/db/postgre.ts @@ -73,6 +73,7 @@ import { UserPending } from "@/models/entities/user-pending.js"; import { Webhook } from "@/models/entities/webhook.js"; import { UserIp } from "@/models/entities/user-ip.js"; import { NoteEdit } from "@/models/entities/note-edit.js"; +import { NoteFile } from "@/models/entities/note-file.js"; import { entities as charts } from "@/services/chart/entities.js"; import { dbLogger } from "./logger.js"; @@ -143,6 +144,7 @@ export const entities = [ Note, NoteEdit, NoteFavorite, + NoteFile, NoteReaction, NoteWatching, NoteThreadMuting, diff --git a/packages/backend/src/migration/1710304584214-note-file.ts b/packages/backend/src/migration/1710304584214-note-file.ts new file mode 100644 index 0000000000..29c340f2cf --- /dev/null +++ b/packages/backend/src/migration/1710304584214-note-file.ts @@ -0,0 +1,41 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class NoteFile1710304584214 implements MigrationInterface { + name = "NoteFile1710304584214"; + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query( + `CREATE TABLE "note_file" ( + "serialNo" bigserial PRIMARY KEY, + "noteId" varchar(32) NOT NULL, + "fileId" varchar(32) NOT NULL + )`, + ); + await queryRunner.query(` + INSERT INTO "note_file" ("noteId", "fileId") + SELECT "t"."id", "t"."fid" FROM ( + SELECT ROW_NUMBER() OVER () AS "rn", * FROM ( + SELECT "id", UNNEST("fileIds") AS "fid" FROM "note" + ) + ) AS "t" + INNER JOIN "drive_file" ON "drive_file"."id" = "t"."fid" + ORDER BY "rn" + `); + await queryRunner.query( + `ALTER TABLE "note_file" ADD FOREIGN KEY ("noteId") REFERENCES "note" ("id") ON DELETE CASCADE`, + ); + await queryRunner.query( + `ALTER TABLE "note_file" ADD FOREIGN KEY ("fileId") REFERENCES "drive_file" ("id") ON DELETE CASCADE`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_note_file_noteId" ON "note_file" ("noteId")`, + ); + await queryRunner.query( + `CREATE INDEX "IDX_note_file_fileId" ON "note_file" ("fileId")`, + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`DROP TABLE "note_file"`); + } +} diff --git a/packages/backend/src/misc/schema.ts b/packages/backend/src/misc/schema.ts index 9d698527c4..016b7159c9 100644 --- a/packages/backend/src/misc/schema.ts +++ b/packages/backend/src/misc/schema.ts @@ -32,6 +32,7 @@ import { packedQueueCountSchema } from "@/models/schema/queue.js"; import { packedGalleryPostSchema } from "@/models/schema/gallery-post.js"; import { packedEmojiSchema } from "@/models/schema/emoji.js"; import { packedNoteEdit } from "@/models/schema/note-edit.js"; +import { packedNoteFileSchema } from "@/models/schema/note-file.js"; export const refs = { UserLite: packedUserLiteSchema, @@ -47,6 +48,7 @@ export const refs = { App: packedAppSchema, MessagingMessage: packedMessagingMessageSchema, Note: packedNoteSchema, + NoteFile: packedNoteFileSchema, NoteEdit: packedNoteEdit, NoteReaction: packedNoteReactionSchema, NoteFavorite: packedNoteFavoriteSchema, diff --git a/packages/backend/src/models/entities/drive-file.ts b/packages/backend/src/models/entities/drive-file.ts index 5b31a7e336..3c4510b533 100644 --- a/packages/backend/src/models/entities/drive-file.ts +++ b/packages/backend/src/models/entities/drive-file.ts @@ -4,12 +4,17 @@ import { Index, JoinColumn, Column, + ManyToMany, ManyToOne, + OneToMany, + type Relation, } from "typeorm"; import { id } from "../id.js"; +import { Note } from "./note.js"; import { User } from "./user.js"; import { DriveFolder } from "./drive-folder.js"; import { DB_MAX_IMAGE_COMMENT_LENGTH } from "@/misc/hard-limits.js"; +import { NoteFile } from "./note-file.js"; @Entity() @Index(["userId", "folderId", "id"]) @@ -31,12 +36,6 @@ export class DriveFile { }) public userId: User["id"] | null; - @ManyToOne((type) => User, { - onDelete: "SET NULL", - }) - @JoinColumn() - public user: User | null; - @Index() @Column("varchar", { length: 512, @@ -171,12 +170,6 @@ export class DriveFile { }) public folderId: DriveFolder["id"] | null; - @ManyToOne((type) => DriveFolder, { - onDelete: "SET NULL", - }) - @JoinColumn() - public folder: DriveFolder | null; - @Index() @Column("boolean", { default: false, @@ -205,4 +198,30 @@ export class DriveFile { nullable: true, }) public requestIp: string | null; + + //#region Relations + @OneToMany( + () => NoteFile, + (noteFile: NoteFile) => noteFile.file, + ) + public noteFiles: Relation; + + @ManyToMany( + () => Note, + (note: Note) => note.files, + ) + public notes: Relation; + + @ManyToOne(() => User, { + onDelete: "SET NULL", + }) + @JoinColumn() + public user: User | null; + + @ManyToOne(() => DriveFolder, { + onDelete: "SET NULL", + }) + @JoinColumn() + public folder: DriveFolder | null; + //#endregion Relations } diff --git a/packages/backend/src/models/entities/note-file.ts b/packages/backend/src/models/entities/note-file.ts new file mode 100644 index 0000000000..7e7013e03a --- /dev/null +++ b/packages/backend/src/models/entities/note-file.ts @@ -0,0 +1,45 @@ +import { + Entity, + Index, + Column, + ManyToOne, + PrimaryGeneratedColumn, + type Relation, +} from "typeorm"; +import { Note } from "./note.js"; +import { DriveFile } from "./drive-file.js"; +import { id } from "../id.js"; + +@Entity() +export class NoteFile { + @PrimaryGeneratedColumn("increment") + public serialNo: number; + + @Index("IDX_note_file_noteId", { unique: false }) + @Column({ + ...id(), + nullable: false, + }) + public noteId: Note["id"]; + + @Index("IDX_note_file_fileId", { unique: false }) + @Column({ + ...id(), + nullable: false, + }) + public fileId: DriveFile["id"]; + + //#region Relations + @ManyToOne( + () => Note, + (note: Note) => note.noteFiles, + ) + public note: Relation; + + @ManyToOne( + () => DriveFile, + (file: DriveFile) => file.noteFiles, + ) + public file: Relation; + //#endregion Relations +} diff --git a/packages/backend/src/models/entities/note.ts b/packages/backend/src/models/entities/note.ts index 5dca038ac2..a31dd7dd46 100644 --- a/packages/backend/src/models/entities/note.ts +++ b/packages/backend/src/models/entities/note.ts @@ -2,15 +2,20 @@ import { Entity, Index, JoinColumn, + JoinTable, Column, PrimaryColumn, + ManyToMany, ManyToOne, + OneToMany, + type Relation, } from "typeorm"; import { User } from "./user.js"; -import type { DriveFile } from "./drive-file.js"; +import { DriveFile } from "./drive-file.js"; import { id } from "../id.js"; import { noteVisibilities } from "../../types.js"; import { Channel } from "./channel.js"; +import { NoteFile } from "./note-file.js"; @Entity() @Index("IDX_NOTE_TAGS", { synchronize: false }) @@ -34,12 +39,6 @@ export class Note { }) public replyId: Note["id"] | null; - @ManyToOne((type) => Note, { - onDelete: "CASCADE", - }) - @JoinColumn() - public reply: Note | null; - @Index() @Column({ ...id(), @@ -48,12 +47,6 @@ export class Note { }) public renoteId: Note["id"] | null; - @ManyToOne((type) => Note, { - onDelete: "CASCADE", - }) - @JoinColumn() - public renote: Note | null; - @Index() @Column("varchar", { length: 256, @@ -93,12 +86,6 @@ export class Note { }) public userId: User["id"]; - @ManyToOne((type) => User, { - onDelete: "CASCADE", - }) - @JoinColumn() - public user: User | null; - @Column("boolean", { default: false, }) @@ -152,6 +139,7 @@ export class Note { public score: number; // FIXME: file id is not removed from this array even if the file is deleted + // TODO: drop this column and use note_files @Index() @Column({ ...id(), @@ -218,12 +206,55 @@ export class Note { }) public channelId: Channel["id"] | null; - @ManyToOne((type) => Channel, { + //#region Relations + @OneToMany( + () => NoteFile, + (noteFile: NoteFile) => noteFile.note, + ) + public noteFiles: Relation; + + @ManyToMany( + () => DriveFile, + (file: DriveFile) => file.notes, + ) + @JoinTable({ + name: "note_file", + joinColumn: { + name: "noteId", + referencedColumnName: "id", + }, + inverseJoinColumn: { + name: "fileId", + referencedColumnName: "id", + }, + }) + public files: Relation; + + @ManyToOne(() => Note, { + onDelete: "CASCADE", + }) + @JoinColumn() + public reply: Note | null; + + @ManyToOne(() => Note, { + onDelete: "CASCADE", + }) + @JoinColumn() + public renote: Note | null; + + @ManyToOne(() => Channel, { onDelete: "CASCADE", }) @JoinColumn() public channel: Channel | null; + @ManyToOne(() => User, { + onDelete: "CASCADE", + }) + @JoinColumn() + public user: User | null; + //#endregion Relations + //#region Denormalized fields @Index() @Column("varchar", { diff --git a/packages/backend/src/models/index.ts b/packages/backend/src/models/index.ts index 8ae12a63df..5d4ff52198 100644 --- a/packages/backend/src/models/index.ts +++ b/packages/backend/src/models/index.ts @@ -66,12 +66,14 @@ import { InstanceRepository } from "./repositories/instance.js"; import { Webhook } from "./entities/webhook.js"; import { UserIp } from "./entities/user-ip.js"; import { NoteEdit } from "./entities/note-edit.js"; +import { NoteFileRepository } from "./repositories/note-file.js"; export const Announcements = db.getRepository(Announcement); export const AnnouncementReads = db.getRepository(AnnouncementRead); export const Apps = AppRepository; export const Notes = NoteRepository; export const NoteEdits = db.getRepository(NoteEdit); +export const NoteFiles = NoteFileRepository; export const NoteFavorites = NoteFavoriteRepository; export const NoteWatchings = db.getRepository(NoteWatching); export const NoteThreadMutings = db.getRepository(NoteThreadMuting); diff --git a/packages/backend/src/models/repositories/note-file.ts b/packages/backend/src/models/repositories/note-file.ts new file mode 100644 index 0000000000..f755fb5fea --- /dev/null +++ b/packages/backend/src/models/repositories/note-file.ts @@ -0,0 +1,4 @@ +import { db } from "@/db/postgre.js"; +import { NoteFile } from "@/models/entities/note-file.js"; + +export const NoteFileRepository = db.getRepository(NoteFile).extend({}); diff --git a/packages/backend/src/models/schema/note-file.ts b/packages/backend/src/models/schema/note-file.ts new file mode 100644 index 0000000000..c9b8a7e181 --- /dev/null +++ b/packages/backend/src/models/schema/note-file.ts @@ -0,0 +1,24 @@ +export const packedNoteFileSchema = { + type: "object", + properties: { + serialNo: { + type: "number", + optional: false, + nullable: false, + }, + noteId: { + type: "string", + optional: false, + nullable: false, + format: "id", + example: "xxxxxxxxxx", + }, + fileId: { + type: "string", + optional: false, + nullable: false, + format: "id", + example: "xxxxxxxxxx", + }, + }, +} as const; diff --git a/packages/backend/src/services/note/create.ts b/packages/backend/src/services/note/create.ts index fc7f1265b3..fc9913e985 100644 --- a/packages/backend/src/services/note/create.ts +++ b/packages/backend/src/services/note/create.ts @@ -31,6 +31,7 @@ import { Channels, ChannelFollowings, NoteThreadMutings, + NoteFiles, } from "@/models/index.js"; import type { DriveFile } from "@/models/entities/drive-file.js"; import type { App } from "@/models/entities/app.js"; @@ -343,6 +344,12 @@ export default async ( const note = await insertNote(user, data, tags, emojis, mentionedUsers); + await NoteFiles.insert( + note.fileIds.map((fileId) => ({ noteId: note.id, fileId })), + ).catch((e) => { + logger.error(inspect(e)); + }); + res(note); // Register host