refactor (backend): add note_file table to store (noteId, fileId) pairs

Co-authored-by: sup39 <dev@sup39.dev>
This commit is contained in:
naskya 2024-03-17 20:56:54 +09:00
parent 0f880b55e3
commit 2220d5c56e
No known key found for this signature in database
GPG key ID: 712D413B3A9FED5C
16 changed files with 279 additions and 32 deletions

View file

@ -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";

View file

@ -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<super::messaging_message::Entity> for Entity {
}
}
impl Related<super::note_file::Entity> for Entity {
fn to() -> RelationDef {
Relation::NoteFile.def()
}
}
impl Related<super::page::Entity> for Entity {
fn to() -> RelationDef {
Relation::Page.def()

View file

@ -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;

View file

@ -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<super::note_favorite::Entity> for Entity {
}
}
impl Related<super::note_file::Entity> for Entity {
fn to() -> RelationDef {
Relation::NoteFile.def()
}
}
impl Related<super::note_reaction::Entity> for Entity {
fn to() -> RelationDef {
Relation::NoteReaction.def()

View file

@ -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<super::drive_file::Entity> for Entity {
fn to() -> RelationDef {
Relation::DriveFile.def()
}
}
impl Related<super::note::Entity> for Entity {
fn to() -> RelationDef {
Relation::Note.def()
}
}
impl ActiveModelBehavior for ActiveModel {}

View file

@ -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;

View file

@ -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,

View file

@ -0,0 +1,41 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class NoteFile1710304584214 implements MigrationInterface {
name = "NoteFile1710304584214";
public async up(queryRunner: QueryRunner): Promise<void> {
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<void> {
await queryRunner.query(`DROP TABLE "note_file"`);
}
}

View file

@ -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,

View file

@ -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<NoteFile[]>;
@ManyToMany(
() => Note,
(note: Note) => note.files,
)
public notes: Relation<Note[]>;
@ManyToOne(() => User, {
onDelete: "SET NULL",
})
@JoinColumn()
public user: User | null;
@ManyToOne(() => DriveFolder, {
onDelete: "SET NULL",
})
@JoinColumn()
public folder: DriveFolder | null;
//#endregion Relations
}

View file

@ -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<Note>;
@ManyToOne(
() => DriveFile,
(file: DriveFile) => file.noteFiles,
)
public file: Relation<DriveFile>;
//#endregion Relations
}

View file

@ -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<NoteFile[]>;
@ManyToMany(
() => DriveFile,
(file: DriveFile) => file.notes,
)
@JoinTable({
name: "note_file",
joinColumn: {
name: "noteId",
referencedColumnName: "id",
},
inverseJoinColumn: {
name: "fileId",
referencedColumnName: "id",
},
})
public files: Relation<DriveFile[]>;
@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", {

View file

@ -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);

View file

@ -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({});

View file

@ -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;

View file

@ -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