diff --git a/packages/backend/1727659258948-add_latest_note.js b/packages/backend/1727659258948-add_latest_note.js new file mode 100644 index 0000000000..b63133cb38 --- /dev/null +++ b/packages/backend/1727659258948-add_latest_note.js @@ -0,0 +1,15 @@ +export class AddLatestNote1727659258948 { + name = 'AddLatestNote1727659258948'; + + async up(queryRunner) { + await queryRunner.query('CREATE TABLE "latest_note" ("user_id" character varying(32) NOT NULL, "note_id" character varying(32) NOT NULL, "userId" character varying(32), "noteId" character varying(32), CONSTRAINT "PK_f619b62bfaafabe68f52fb50c9a" PRIMARY KEY ("user_id"))'); + await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_20e346fffe4a2174585005d6d80" FOREIGN KEY ("userId") REFERENCES "user"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); + await queryRunner.query('ALTER TABLE "latest_note" ADD CONSTRAINT "FK_47a38b1c13de6ce4e5090fb1acd" FOREIGN KEY ("noteId") REFERENCES "note"("id") ON DELETE CASCADE ON UPDATE NO ACTION'); + } + + async down(queryRunner) { + await queryRunner.query('ALTER TABLE "latest_note" DROP CONSTRAINT "FK_47a38b1c13de6ce4e5090fb1acd"'); + await queryRunner.query('ALTER TABLE "latest_note" DROP CONSTRAINT "FK_20e346fffe4a2174585005d6d80"'); + await queryRunner.query('DROP TABLE "latest_note"'); + } +} diff --git a/packages/backend/src/di-symbols.ts b/packages/backend/src/di-symbols.ts index d4a21ab625..72a9aed4f3 100644 --- a/packages/backend/src/di-symbols.ts +++ b/packages/backend/src/di-symbols.ts @@ -19,6 +19,7 @@ export const DI = { announcementReadsRepository: Symbol('announcementReadsRepository'), appsRepository: Symbol('appsRepository'), avatarDecorationsRepository: Symbol('avatarDecorationsRepository'), + latestNotesRepository: Symbol('latestNotesRepository'), noteFavoritesRepository: Symbol('noteFavoritesRepository'), noteThreadMutingsRepository: Symbol('noteThreadMutingsRepository'), noteReactionsRepository: Symbol('noteReactionsRepository'), diff --git a/packages/backend/src/models/LatestNote.ts b/packages/backend/src/models/LatestNote.ts new file mode 100644 index 0000000000..9d56b82620 --- /dev/null +++ b/packages/backend/src/models/LatestNote.ts @@ -0,0 +1,37 @@ +import { PrimaryColumn, Entity, JoinColumn, Column, ManyToOne } from 'typeorm'; +import { MiUser } from '@/models/User.js'; +import { MiNote } from '@/models/Note.js'; + +/** + * Maps a user to the most recent post by that user. + * Public, home-only, and followers-only posts are included. + * DMs are not counted. + */ +@Entity('latest_note') +export class LatestNote { + @PrimaryColumn({ + name: 'user_id', + type: 'varchar' as const, + length: 32, + }) + public userId: string; + + @ManyToOne(() => MiUser, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public user: MiUser | null; + + @Column({ + name: 'note_id', + type: 'varchar' as const, + length: 32, + }) + public noteId: string; + + @ManyToOne(() => MiNote, { + onDelete: 'CASCADE', + }) + @JoinColumn() + public note: MiNote | null; +} diff --git a/packages/backend/src/models/RepositoryModule.ts b/packages/backend/src/models/RepositoryModule.ts index 1eaeb86df6..f44334d84e 100644 --- a/packages/backend/src/models/RepositoryModule.ts +++ b/packages/backend/src/models/RepositoryModule.ts @@ -7,6 +7,7 @@ import type { Provider } from '@nestjs/common'; import { Module } from '@nestjs/common'; import { DI } from '@/di-symbols.js'; import { + LatestNote, MiAbuseReportNotificationRecipient, MiAbuseUserReport, MiAccessToken, @@ -118,6 +119,12 @@ const $avatarDecorationsRepository: Provider = { inject: [DI.db], }; +const $latestNotesRepository: Provider = { + provide: DI.latestNotesRepository, + useFactory: (db: DataSource) => db.getRepository(LatestNote).extend(miRepository as MiRepository), + inject: [DI.db], +}; + const $noteFavoritesRepository: Provider = { provide: DI.noteFavoritesRepository, useFactory: (db: DataSource) => db.getRepository(MiNoteFavorite).extend(miRepository as MiRepository), @@ -511,6 +518,7 @@ const $reversiGamesRepository: Provider = { $announcementReadsRepository, $appsRepository, $avatarDecorationsRepository, + $latestNotesRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, @@ -583,6 +591,7 @@ const $reversiGamesRepository: Provider = { $announcementReadsRepository, $appsRepository, $avatarDecorationsRepository, + $latestNotesRepository, $noteFavoritesRepository, $noteThreadMutingsRepository, $noteReactionsRepository, diff --git a/packages/backend/src/models/_.ts b/packages/backend/src/models/_.ts index f7646dce2a..1a79aeb80d 100644 --- a/packages/backend/src/models/_.ts +++ b/packages/backend/src/models/_.ts @@ -10,6 +10,7 @@ import { RelationIdLoader } from 'typeorm/query-builder/relation-id/RelationIdLo import { RawSqlResultsToEntityTransformer } from 'typeorm/query-builder/transformer/RawSqlResultsToEntityTransformer.js'; import { ObjectUtils } from 'typeorm/util/ObjectUtils.js'; import { OrmUtils } from 'typeorm/util/OrmUtils.js'; +import { LatestNote } from '@/models/LatestNote.js'; import { MiAbuseUserReport } from '@/models/AbuseUserReport.js'; import { MiAbuseReportNotificationRecipient } from '@/models/AbuseReportNotificationRecipient.js'; import { MiAccessToken } from '@/models/AccessToken.js'; @@ -126,6 +127,7 @@ export const miRepository = { } satisfies MiRepository; export { + LatestNote, MiAbuseUserReport, MiAbuseReportNotificationRecipient, MiAccessToken, @@ -224,6 +226,7 @@ export type GalleryPostsRepository = Repository & MiRepository & MiRepository; export type InstancesRepository = Repository & MiRepository; export type MetasRepository = Repository & MiRepository; +export type LatestNoteRepository = Repository & MiRepository; export type ModerationLogsRepository = Repository & MiRepository; export type MutingsRepository = Repository & MiRepository; export type RenoteMutingsRepository = Repository & MiRepository; diff --git a/packages/backend/src/postgres.ts b/packages/backend/src/postgres.ts index 047b7f8ae9..de7353dc8c 100644 --- a/packages/backend/src/postgres.ts +++ b/packages/backend/src/postgres.ts @@ -83,6 +83,7 @@ import { MiReversiGame } from '@/models/ReversiGame.js'; import { Config } from '@/config.js'; import MisskeyLogger from '@/logger.js'; import { bindThis } from '@/decorators.js'; +import {LatestNote} from "@/models/LatestNote.js"; pg.types.setTypeParser(20, Number); @@ -130,6 +131,7 @@ class MyCustomLogger implements Logger { } export const entities = [ + LatestNote, MiAnnouncement, MiAnnouncementRead, MiMeta,