perf: Hold 10s cache on user.collectionIds (#11579)

This commit is contained in:
Tom Moor
2026-02-26 08:15:35 -05:00
committed by GitHub
parent 7999cf0b98
commit 6aa7f34b01
3 changed files with 102 additions and 53 deletions
+72 -53
View File
@@ -53,6 +53,8 @@ import DeleteAttachmentTask from "@server/queues/tasks/DeleteAttachmentTask";
import type { APIContext } from "@server/types";
import { VerificationCode } from "@server/utils/VerificationCode";
import parseAttachmentIds from "@server/utils/parseAttachmentIds";
import { CacheHelper } from "@server/utils/CacheHelper";
import { RedisPrefixHelper } from "@server/utils/RedisPrefixHelper";
import { ValidationError } from "../errors";
import Attachment from "./Attachment";
import AuthenticationProvider from "./AuthenticationProvider";
@@ -471,65 +473,82 @@ class User extends ParanoidModel<
* @returns An array of collection ids
*/
public collectionIds = async (options: FindOptions<Collection> = {}) => {
const collectionStubs = await Collection.findAll({
attributes: ["id"],
where: {
teamId: this.teamId,
[Op.or]: [
...(this.isGuest
? []
: [
{
permission: {
[Op.in]: Object.values(CollectionPermission),
const hasOptions =
options.transaction || options.paranoid === false || options.lock;
const fetchCollectionIds = async () => {
const collectionStubs = await Collection.findAll({
attributes: ["id"],
where: {
teamId: this.teamId,
[Op.or]: [
...(this.isGuest
? []
: [
{
permission: {
[Op.in]: Object.values(CollectionPermission),
},
},
},
]),
{
"$memberships.id$": { [Op.ne]: null },
},
{
"$groupMemberships.id$": { [Op.ne]: null },
},
],
},
include: [
{
association: "memberships",
attributes: [],
required: false,
where: {
userId: this.id,
},
},
{
association: "groupMemberships",
attributes: [],
required: false,
include: [
]),
{
association: "group",
attributes: [],
required: true,
include: [
{
association: "groupUsers",
attributes: [],
required: true,
where: {
userId: this.id,
},
},
],
"$memberships.id$": { [Op.ne]: null },
},
{
"$groupMemberships.id$": { [Op.ne]: null },
},
],
},
],
paranoid: true,
...options,
});
include: [
{
association: "memberships",
attributes: [],
required: false,
where: {
userId: this.id,
},
},
{
association: "groupMemberships",
attributes: [],
required: false,
include: [
{
association: "group",
attributes: [],
required: true,
include: [
{
association: "groupUsers",
attributes: [],
required: true,
where: {
userId: this.id,
},
},
],
},
],
},
],
paranoid: true,
...options,
});
return Array.from(new Set(collectionStubs.map((c) => c.id)));
return Array.from(new Set(collectionStubs.map((c) => c.id)));
};
if (hasOptions) {
return fetchCollectionIds();
}
return (
(await CacheHelper.getDataOrSet<string[]>(
RedisPrefixHelper.getUserCollectionIdsKey(this.id),
fetchCollectionIds,
10
)) ?? []
);
};
updateActiveAt = async (ctx: Context, force = false) => {
+20
View File
@@ -21,6 +21,8 @@ import type { DocumentPermission } from "@shared/types";
import { CollectionPermission } from "@shared/types";
import { ValidationError } from "@server/errors";
import type { APIContext } from "@server/types";
import { CacheHelper } from "@server/utils/CacheHelper";
import { RedisPrefixHelper } from "@server/utils/RedisPrefixHelper";
import Collection from "./Collection";
import Document from "./Document";
import GroupMembership from "./GroupMembership";
@@ -222,6 +224,15 @@ class UserMembership extends IdModel<
});
}
@AfterCreate
static async invalidateCollectionIdsAfterCreate(model: UserMembership) {
if (model.collectionId) {
await CacheHelper.clearData(
RedisPrefixHelper.getUserCollectionIdsKey(model.userId)
);
}
}
@AfterUpdate
static async updateSourcedMemberships(
model: UserMembership,
@@ -297,6 +308,15 @@ class UserMembership extends IdModel<
await model.insertEvent(context, "remove_user");
}
@AfterDestroy
static async invalidateCollectionIdsAfterDestroy(model: UserMembership) {
if (model.collectionId) {
await CacheHelper.clearData(
RedisPrefixHelper.getUserCollectionIdsKey(model.userId)
);
}
}
/**
* Recreate all sourced permissions for a given permission.
*/
+10
View File
@@ -32,4 +32,14 @@ export class RedisPrefixHelper {
public static getEmbedCheckKey(url: string) {
return `embed:${url}`;
}
/**
* Gets key for caching a user's accessible collection IDs.
*
* @param userId The user ID to generate a key for.
* @returns the cache key string.
*/
public static getUserCollectionIdsKey(userId: string) {
return `uc:${userId}`;
}
}