wip: pack with scylla
This commit is contained in:
parent
41930bda52
commit
31e4e0b75a
12 changed files with 128 additions and 61 deletions
|
@ -2,7 +2,7 @@ DROP MATERIALIZED VIEW IF EXISTS reaction_by_userid;
|
||||||
DROP INDEX IF EXISTS reaction_by_id;
|
DROP INDEX IF EXISTS reaction_by_id;
|
||||||
DROP TABLE IF EXISTS reaction;
|
DROP TABLE IF EXISTS reaction;
|
||||||
DROP MATERIALIZED VIEW IF EXISTS note_by_userid;
|
DROP MATERIALIZED VIEW IF EXISTS note_by_userid;
|
||||||
DROP INDEX IF EXISTS note_by_id;
|
DROP MATERIALIZED VIEW IF EXISTS note_by_id;
|
||||||
DROP INDEX IF EXISTS note_by_uri;
|
DROP INDEX IF EXISTS note_by_uri;
|
||||||
DROP INDEX IF EXISTS note_by_url;
|
DROP INDEX IF EXISTS note_by_url;
|
||||||
DROP TABLE IF EXISTS note;
|
DROP TABLE IF EXISTS note;
|
||||||
|
|
|
@ -10,7 +10,9 @@ CREATE TYPE IF NOT EXISTS drive_file (
|
||||||
"isSensitive" boolean,
|
"isSensitive" boolean,
|
||||||
"isLink" boolean,
|
"isLink" boolean,
|
||||||
"md5" ascii,
|
"md5" ascii,
|
||||||
"size" int
|
"size" int,
|
||||||
|
"width" int,
|
||||||
|
"height" int,
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE TYPE IF NOT EXISTS note_edit_history (
|
CREATE TYPE IF NOT EXISTS note_edit_history (
|
||||||
|
@ -44,6 +46,7 @@ CREATE TABLE IF NOT EXISTS note ( -- Models timeline
|
||||||
"files" set<frozen<drive_file>>,
|
"files" set<frozen<drive_file>>,
|
||||||
"visibleUserIds" set<ascii>,
|
"visibleUserIds" set<ascii>,
|
||||||
"mentions" set<ascii>,
|
"mentions" set<ascii>,
|
||||||
|
"mentionedRemoteUsers" text,
|
||||||
"emojis" set<text>,
|
"emojis" set<text>,
|
||||||
"tags" set<text>,
|
"tags" set<text>,
|
||||||
"hasPoll" boolean,
|
"hasPoll" boolean,
|
||||||
|
@ -63,9 +66,16 @@ CREATE TABLE IF NOT EXISTS note ( -- Models timeline
|
||||||
PRIMARY KEY ("createdAtDate", "createdAt", "id")
|
PRIMARY KEY ("createdAtDate", "createdAt", "id")
|
||||||
) WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
) WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS note_by_id ON note (id);
|
CREATE INDEX IF NOT EXISTS note_by_uri ON note ("uri");
|
||||||
CREATE INDEX IF NOT EXISTS note_by_uri ON note (uri);
|
CREATE INDEX IF NOT EXISTS note_by_url ON note ("url");
|
||||||
CREATE INDEX IF NOT EXISTS note_by_url ON note (url);
|
|
||||||
|
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_id AS
|
||||||
|
SELECT * FROM note
|
||||||
|
WHERE "id" IS NOT NULL
|
||||||
|
AND "createdAt" IS NOT NULL
|
||||||
|
AND "createdAtDate" IS NOT NULL
|
||||||
|
PRIMARY KEY ("id", "createdAt", "createdAtDate")
|
||||||
|
WITH CLUSTERING ORDER BY ("createdAt" DESC);
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_userid AS
|
CREATE MATERIALIZED VIEW IF NOT EXISTS note_by_userid AS
|
||||||
SELECT * FROM note
|
SELECT * FROM note
|
||||||
|
@ -86,7 +96,7 @@ CREATE TABLE IF NOT EXISTS reaction (
|
||||||
PRIMARY KEY ("noteId", "userId")
|
PRIMARY KEY ("noteId", "userId")
|
||||||
);
|
);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS reaction_by_id ON reaction (id);
|
CREATE INDEX IF NOT EXISTS reaction_by_id ON reaction ("id");
|
||||||
|
|
||||||
CREATE MATERIALIZED VIEW IF NOT EXISTS reaction_by_userid AS
|
CREATE MATERIALIZED VIEW IF NOT EXISTS reaction_by_userid AS
|
||||||
SELECT * FROM reaction
|
SELECT * FROM reaction
|
||||||
|
|
|
@ -39,6 +39,7 @@ export const prepared = {
|
||||||
"files",
|
"files",
|
||||||
"visibleUserIds",
|
"visibleUserIds",
|
||||||
"mentions",
|
"mentions",
|
||||||
|
"mentionedRemoteUsers",
|
||||||
"emojis",
|
"emojis",
|
||||||
"tags",
|
"tags",
|
||||||
"hasPoll",
|
"hasPoll",
|
||||||
|
@ -57,12 +58,12 @@ export const prepared = {
|
||||||
"updatedAt"
|
"updatedAt"
|
||||||
)
|
)
|
||||||
VALUES
|
VALUES
|
||||||
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||||
select: {
|
select: {
|
||||||
byDate: `SELECT * FROM note WHERE "createdAtDate" = ?`,
|
byDate: `SELECT * FROM note WHERE "createdAtDate" = ?`,
|
||||||
byId: `SELECT * FROM note WHERE "id" IN ?`,
|
|
||||||
byUri: `SELECT * FROM note WHERE "uri" IN ?`,
|
byUri: `SELECT * FROM note WHERE "uri" IN ?`,
|
||||||
byUrl: `SELECT * FROM note WHERE "url" IN ?`,
|
byUrl: `SELECT * FROM note WHERE "url" IN ?`,
|
||||||
|
byId: `SELECT * FROM note_by_id WHERE "id" IN ?`,
|
||||||
byUserId: `SELECT * FROM note_by_userid WHERE "userId" IN ?`,
|
byUserId: `SELECT * FROM note_by_userid WHERE "userId" IN ?`,
|
||||||
},
|
},
|
||||||
delete: `DELETE FROM note WHERE "createdAtDate" = ? AND "createdAt" = ? AND "id" = ?`,
|
delete: `DELETE FROM note WHERE "createdAtDate" = ? AND "createdAt" = ? AND "id" = ?`,
|
||||||
|
@ -105,6 +106,8 @@ export interface ScyllaDriveFile {
|
||||||
isLink: boolean;
|
isLink: boolean;
|
||||||
md5: string;
|
md5: string;
|
||||||
size: number;
|
size: number;
|
||||||
|
width: number | null;
|
||||||
|
height: number | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ScyllaNoteEditHistory {
|
export interface ScyllaNoteEditHistory {
|
||||||
|
@ -121,47 +124,48 @@ export type ScyllaNote = Note & {
|
||||||
};
|
};
|
||||||
|
|
||||||
export function parseScyllaNote(row: types.Row): ScyllaNote {
|
export function parseScyllaNote(row: types.Row): ScyllaNote {
|
||||||
const files: ScyllaDriveFile[] = row.get("files");
|
const files: ScyllaDriveFile[] = row.get("files") ?? [];
|
||||||
|
|
||||||
return {
|
return {
|
||||||
createdAtDate: row.get("createdAtDate"),
|
createdAtDate: row.get("createdAtDate"),
|
||||||
createdAt: row.get("createdAt"),
|
createdAt: row.get("createdAt"),
|
||||||
id: row.get("id"),
|
id: row.get("id"),
|
||||||
visibility: row.get("visibility"),
|
visibility: row.get("visibility"),
|
||||||
text: row.get("content"),
|
text: row.get("content") ?? null,
|
||||||
name: row.get("name"),
|
name: row.get("name") ?? null,
|
||||||
cw: row.get("cw"),
|
cw: row.get("cw") ?? null,
|
||||||
localOnly: row.get("localOnly"),
|
localOnly: row.get("localOnly"),
|
||||||
renoteCount: row.get("renoteCount"),
|
renoteCount: row.get("renoteCount"),
|
||||||
repliesCount: row.get("repliesCount"),
|
repliesCount: row.get("repliesCount"),
|
||||||
uri: row.get("uri"),
|
uri: row.get("uri") ?? null,
|
||||||
url: row.get("url"),
|
url: row.get("url") ?? null,
|
||||||
score: row.get("score"),
|
score: row.get("score"),
|
||||||
files,
|
files,
|
||||||
fileIds: files.map((file) => file.id),
|
fileIds: files.map((file) => file.id),
|
||||||
attachedFileTypes: files.map((file) => file.type),
|
attachedFileTypes: files.map((file) => file.type) ?? [],
|
||||||
visibleUserIds: row.get("visibleUserIds"),
|
visibleUserIds: row.get("visibleUserIds") ?? [],
|
||||||
mentions: row.get("mentions"),
|
mentions: row.get("mentions") ?? [],
|
||||||
emojis: row.get("emojis"),
|
emojis: row.get("emojis") ?? [],
|
||||||
tags: row.get("tags"),
|
tags: row.get("tags") ?? [],
|
||||||
hasPoll: row.get("hasPoll"),
|
hasPoll: row.get("hasPoll") ?? false,
|
||||||
threadId: row.get("threadId"),
|
threadId: row.get("threadId") ?? null,
|
||||||
channelId: row.get("channelId"),
|
channelId: row.get("channelId") ?? null,
|
||||||
userId: row.get("userId"),
|
userId: row.get("userId"),
|
||||||
userHost: row.get("userHost"),
|
userHost: row.get("userHost") ?? null,
|
||||||
replyId: row.get("replyId"),
|
replyId: row.get("replyId") ?? null,
|
||||||
replyUserId: row.get("replyUserId"),
|
replyUserId: row.get("replyUserId") ?? null,
|
||||||
replyUserHost: row.get("replyUserHost"),
|
replyUserHost: row.get("replyUserHost") ?? null,
|
||||||
renoteId: row.get("replyId"),
|
renoteId: row.get("renoteId") ?? null,
|
||||||
renoteUserId: row.get("renoteUserId"),
|
renoteUserId: row.get("renoteUserId") ?? null,
|
||||||
renoteUserHost: row.get("renoteUserHost"),
|
renoteUserHost: row.get("renoteUserHost") ?? null,
|
||||||
reactions: row.get("reactions"),
|
reactions: row.get("reactions") ?? {},
|
||||||
noteEdit: row.get("noteEdit"),
|
noteEdit: row.get("noteEdit") ?? [],
|
||||||
updatedAt: row.get("updatedAt"),
|
updatedAt: row.get("updatedAt") ?? null,
|
||||||
|
mentionedRemoteUsers: row.get("mentionedRemoteUsers") ?? "[]",
|
||||||
/* unused postgres denormalization */
|
/* unused postgres denormalization */
|
||||||
channel: null,
|
channel: null,
|
||||||
renote: null,
|
renote: null,
|
||||||
reply: null,
|
reply: null,
|
||||||
mentionedRemoteUsers: "",
|
|
||||||
user: null,
|
user: null,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,6 +2,7 @@ import { redisClient } from "@/db/redis.js";
|
||||||
import { encode, decode } from "msgpackr";
|
import { encode, decode } from "msgpackr";
|
||||||
import { ChainableCommander } from "ioredis";
|
import { ChainableCommander } from "ioredis";
|
||||||
import { Followings } from "@/models/index.js";
|
import { Followings } from "@/models/index.js";
|
||||||
|
import { IsNull } from "typeorm";
|
||||||
|
|
||||||
export class Cache<T> {
|
export class Cache<T> {
|
||||||
private ttl: number;
|
private ttl: number;
|
||||||
|
@ -147,7 +148,7 @@ export class LocalFollowingsCache {
|
||||||
if (!(await cache.hasFollowing())) {
|
if (!(await cache.hasFollowing())) {
|
||||||
const rel = await Followings.find({
|
const rel = await Followings.find({
|
||||||
select: { followeeId: true },
|
select: { followeeId: true },
|
||||||
where: { followerId: cache.myId },
|
where: { followerId: cache.myId, followerHost: IsNull() },
|
||||||
});
|
});
|
||||||
await cache.follow(...rel.map((r) => r.followeeId));
|
await cache.follow(...rel.map((r) => r.followeeId));
|
||||||
}
|
}
|
||||||
|
@ -178,6 +179,6 @@ export class LocalFollowingsCache {
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getAll(): Promise<string[]> {
|
public async getAll(): Promise<string[]> {
|
||||||
return (await redisClient.smembers(this.key))
|
return await redisClient.smembers(this.key);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -105,7 +105,9 @@ export async function toDbReaction(
|
||||||
);
|
);
|
||||||
|
|
||||||
if (emoji) {
|
if (emoji) {
|
||||||
const emojiName = _reacterHost ? `:${name}@${_reacterHost}:` : `:${name}:`;
|
const emojiName = _reacterHost
|
||||||
|
? `:${name}@${_reacterHost}:`
|
||||||
|
: `:${name}:`;
|
||||||
return { name: emojiName, emoji };
|
return { name: emojiName, emoji };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -27,6 +27,14 @@ import {
|
||||||
} from "@/misc/populate-emojis.js";
|
} from "@/misc/populate-emojis.js";
|
||||||
import { db } from "@/db/postgre.js";
|
import { db } from "@/db/postgre.js";
|
||||||
import { IdentifiableError } from "@/misc/identifiable-error.js";
|
import { IdentifiableError } from "@/misc/identifiable-error.js";
|
||||||
|
import {
|
||||||
|
ScyllaNote,
|
||||||
|
parseScyllaNote,
|
||||||
|
prepared,
|
||||||
|
scyllaClient,
|
||||||
|
} from "@/db/scylla.js";
|
||||||
|
import { LocalFollowingsCache } from "@/misc/cache.js";
|
||||||
|
import { userByIdCache } from "@/services/user-cache.js";
|
||||||
|
|
||||||
export async function populatePoll(note: Note, meId: User["id"] | null) {
|
export async function populatePoll(note: Note, meId: User["id"] | null) {
|
||||||
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
const poll = await Polls.findOneByOrFail({ noteId: note.id });
|
||||||
|
@ -124,16 +132,23 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||||
return true;
|
return true;
|
||||||
} else {
|
} else {
|
||||||
// フォロワーかどうか
|
// フォロワーかどうか
|
||||||
const [following, user] = await Promise.all([
|
|
||||||
Followings.count({
|
const user = await userByIdCache.fetch(meId, () =>
|
||||||
|
Users.findOneByOrFail({ id: meId }),
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!user.host) {
|
||||||
|
// user is local
|
||||||
|
const cache = await LocalFollowingsCache.init(meId);
|
||||||
|
return await cache.isFollowing(note.userId);
|
||||||
|
}
|
||||||
|
|
||||||
|
const following = await Followings.exist({
|
||||||
where: {
|
where: {
|
||||||
followeeId: note.userId,
|
followeeId: note.userId,
|
||||||
followerId: meId,
|
followerId: meId,
|
||||||
},
|
},
|
||||||
take: 1,
|
});
|
||||||
}),
|
|
||||||
Users.findOneByOrFail({ id: meId }),
|
|
||||||
]);
|
|
||||||
|
|
||||||
/* If we know the following, everyhting is fine.
|
/* If we know the following, everyhting is fine.
|
||||||
|
|
||||||
|
@ -142,7 +157,7 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||||
in which case we can never know the following. Instead we have
|
in which case we can never know the following. Instead we have
|
||||||
to assume that the users are following each other.
|
to assume that the users are following each other.
|
||||||
*/
|
*/
|
||||||
return following > 0 || (note.userHost != null && user.host != null);
|
return following || !!note.userHost;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -167,8 +182,31 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||||
);
|
);
|
||||||
|
|
||||||
const meId = me ? me.id : null;
|
const meId = me ? me.id : null;
|
||||||
const note =
|
let note: Note | null = null;
|
||||||
typeof src === "object" ? src : await this.findOneByOrFail({ id: src });
|
const noteId = typeof src === "object" ? src.id : src;
|
||||||
|
if (scyllaClient) {
|
||||||
|
const result = await scyllaClient.execute(
|
||||||
|
prepared.note.select.byId,
|
||||||
|
[[noteId]],
|
||||||
|
{ prepare: true },
|
||||||
|
);
|
||||||
|
if (result.rowLength > 0) {
|
||||||
|
note = parseScyllaNote(result.first());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!note) {
|
||||||
|
// Fallback to Postgres
|
||||||
|
note = await this.findOneBy({ id: noteId });
|
||||||
|
}
|
||||||
|
|
||||||
|
if (note === null) {
|
||||||
|
throw new IdentifiableError(
|
||||||
|
"9725d0ce-ba28-4dde-95a7-2cbb2c15de24",
|
||||||
|
"No such note.",
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const host = note.userHost;
|
const host = note.userHost;
|
||||||
|
|
||||||
if (!(await this.isVisibleForMe(note, meId))) {
|
if (!(await this.isVisibleForMe(note, meId))) {
|
||||||
|
@ -222,7 +260,18 @@ export const NoteRepository = db.getRepository(Note).extend({
|
||||||
emojis: noteEmoji,
|
emojis: noteEmoji,
|
||||||
tags: note.tags.length > 0 ? note.tags : undefined,
|
tags: note.tags.length > 0 ? note.tags : undefined,
|
||||||
fileIds: note.fileIds,
|
fileIds: note.fileIds,
|
||||||
files: DriveFiles.packMany(note.fileIds),
|
files: scyllaClient
|
||||||
|
? (note as ScyllaNote).files.map((file) => ({
|
||||||
|
...file,
|
||||||
|
createdAt: file.createdAt.toISOString(),
|
||||||
|
properties: {
|
||||||
|
width: file.width ?? undefined,
|
||||||
|
height: file.height ?? undefined,
|
||||||
|
},
|
||||||
|
userId: null,
|
||||||
|
folderId: null,
|
||||||
|
}))
|
||||||
|
: DriveFiles.packMany(note.fileIds),
|
||||||
replyId: note.replyId,
|
replyId: note.replyId,
|
||||||
renoteId: note.renoteId,
|
renoteId: note.renoteId,
|
||||||
channelId: note.channelId || undefined,
|
channelId: note.channelId || undefined,
|
||||||
|
|
|
@ -309,10 +309,7 @@ export default define(meta, paramDef, async (ps, _user, token) => {
|
||||||
if (Object.keys(updates).length > 0) {
|
if (Object.keys(updates).length > 0) {
|
||||||
await Users.update(user.id, updates);
|
await Users.update(user.id, updates);
|
||||||
const data = await Users.findOneByOrFail({ id: user.id });
|
const data = await Users.findOneByOrFail({ id: user.id });
|
||||||
await userByIdCache.set(
|
await userByIdCache.set(data.id, data);
|
||||||
data.id,
|
|
||||||
data,
|
|
||||||
);
|
|
||||||
if (data.avatarId) {
|
if (data.avatarId) {
|
||||||
data.avatar = await DriveFiles.findOneBy({ id: data.avatarId });
|
data.avatar = await DriveFiles.findOneBy({ id: data.avatarId });
|
||||||
}
|
}
|
||||||
|
|
|
@ -84,7 +84,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
.leftJoinAndSelect("note.reply", "reply")
|
.leftJoinAndSelect("note.reply", "reply")
|
||||||
.leftJoinAndSelect("note.renote", "renote")
|
.leftJoinAndSelect("note.renote", "renote")
|
||||||
.leftJoinAndSelect("reply.user", "replyUser")
|
.leftJoinAndSelect("reply.user", "replyUser")
|
||||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
.leftJoinAndSelect("renote.user", "renoteUser");
|
||||||
|
|
||||||
generateRepliesQuery(query, ps.withReplies, user);
|
generateRepliesQuery(query, ps.withReplies, user);
|
||||||
if (user) {
|
if (user) {
|
||||||
|
|
|
@ -96,7 +96,7 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
.leftJoinAndSelect("note.reply", "reply")
|
.leftJoinAndSelect("note.reply", "reply")
|
||||||
.leftJoinAndSelect("note.renote", "renote")
|
.leftJoinAndSelect("note.renote", "renote")
|
||||||
.leftJoinAndSelect("reply.user", "replyUser")
|
.leftJoinAndSelect("reply.user", "replyUser")
|
||||||
.leftJoinAndSelect("renote.user", "renoteUser")
|
.leftJoinAndSelect("renote.user", "renoteUser");
|
||||||
|
|
||||||
generateChannelQuery(query, user);
|
generateChannelQuery(query, user);
|
||||||
generateRepliesQuery(query, ps.withReplies, user);
|
generateRepliesQuery(query, ps.withReplies, user);
|
||||||
|
|
|
@ -85,7 +85,9 @@ export default define(meta, paramDef, async (ps, user) => {
|
||||||
params.push(ps.sinceId);
|
params.push(ps.sinceId);
|
||||||
}
|
}
|
||||||
|
|
||||||
const result = await scyllaClient.execute(query.join(" "), params, { prepare: true });
|
const result = await scyllaClient.execute(query.join(" "), params, {
|
||||||
|
prepare: true,
|
||||||
|
});
|
||||||
const notes = result.rows.map(parseScyllaNote);
|
const notes = result.rows.map(parseScyllaNote);
|
||||||
return Notes.packMany(notes, user);
|
return Notes.packMany(notes, user);
|
||||||
}
|
}
|
||||||
|
|
|
@ -787,9 +787,14 @@ async function insertNote(
|
||||||
insert.uri,
|
insert.uri,
|
||||||
insert.url,
|
insert.url,
|
||||||
insert.score ?? 0,
|
insert.score ?? 0,
|
||||||
data.files,
|
data.files?.map((file) => ({
|
||||||
|
...file,
|
||||||
|
width: file.properties.width ?? null,
|
||||||
|
height: file.properties.height ?? null,
|
||||||
|
})),
|
||||||
insert.visibleUserIds,
|
insert.visibleUserIds,
|
||||||
insert.mentions,
|
insert.mentions,
|
||||||
|
insert.mentionedRemoteUsers,
|
||||||
insert.emojis,
|
insert.emojis,
|
||||||
insert.tags,
|
insert.tags,
|
||||||
insert.hasPoll,
|
insert.hasPoll,
|
||||||
|
|
|
@ -1,10 +1,7 @@
|
||||||
import { publishMainStream } from "@/services/stream.js";
|
import { publishMainStream } from "@/services/stream.js";
|
||||||
import type { Note } from "@/models/entities/note.js";
|
import type { Note } from "@/models/entities/note.js";
|
||||||
import type { User } from "@/models/entities/user.js";
|
import type { User } from "@/models/entities/user.js";
|
||||||
import {
|
import { NoteUnreads, ChannelFollowings } from "@/models/index.js";
|
||||||
NoteUnreads,
|
|
||||||
ChannelFollowings,
|
|
||||||
} from "@/models/index.js";
|
|
||||||
import { Not, IsNull, In } from "typeorm";
|
import { Not, IsNull, In } from "typeorm";
|
||||||
import type { Channel } from "@/models/entities/channel.js";
|
import type { Channel } from "@/models/entities/channel.js";
|
||||||
import { readNotificationByQuery } from "@/server/api/common/read-notification.js";
|
import { readNotificationByQuery } from "@/server/api/common/read-notification.js";
|
||||||
|
|
Loading…
Reference in a new issue