From b9767a9fdc1c766be1bd0a006ca97601c1f1bfa0 Mon Sep 17 00:00:00 2001 From: Tom Moor Date: Fri, 24 Nov 2023 12:59:57 -0500 Subject: [PATCH] fix: Do not rely on class names in production bundle (#6212) --- app/components/PaginatedList.test.tsx | 18 ++-- app/components/PaginatedList.tsx | 10 +-- app/models/ApiKey.ts | 2 + app/models/AuthenticationProvider.ts | 2 + app/models/Collection.ts | 2 + app/models/CollectionGroupMembership.ts | 11 +++ app/models/Comment.ts | 2 + app/models/Document.ts | 2 + app/models/Event.ts | 2 + app/models/FileOperation.ts | 2 + app/models/Group.ts | 2 + app/models/GroupMembership.ts | 10 ++- app/models/Integration.ts | 2 + app/models/Membership.ts | 2 + app/models/Notification.ts | 2 + app/models/Pin.ts | 2 + app/models/Policy.ts | 2 + app/models/Revision.ts | 2 + app/models/SearchQuery.ts | 2 + app/models/Share.ts | 2 + app/models/Star.ts | 2 + app/models/Subscription.ts | 2 + app/models/Team.ts | 2 + app/models/User.ts | 2 + app/models/View.ts | 6 ++ app/models/WebhookSubscription.ts | 2 + app/models/base/Model.ts | 2 + app/models/decorators/Relation.ts | 16 ++-- app/scenes/Search/Search.tsx | 8 +- app/stores/AuthStore.ts | 2 +- app/stores/CommentsStore.ts | 2 - app/stores/RootStore.ts | 96 +++++++++++++++------ app/stores/SearchesStore.ts | 2 - app/stores/base/Store.ts | 38 ++++---- package.json | 2 + server/routes/api/middlewares/pagination.ts | 7 +- shared/constants.ts | 6 ++ yarn.lock | 10 +++ 38 files changed, 202 insertions(+), 86 deletions(-) diff --git a/app/components/PaginatedList.test.tsx b/app/components/PaginatedList.test.tsx index 079757954..6a7dd8c4e 100644 --- a/app/components/PaginatedList.test.tsx +++ b/app/components/PaginatedList.test.tsx @@ -3,8 +3,7 @@ import { shallow } from "enzyme"; import { TFunction } from "i18next"; import * as React from "react"; import { getI18n } from "react-i18next"; -import RootStore from "~/stores/RootStore"; -import { DEFAULT_PAGINATION_LIMIT } from "~/stores/base/Store"; +import { Pagination } from "@shared/constants"; import { runAllPromises } from "~/test/support"; import { Component as PaginatedList } from "./PaginatedList"; @@ -12,17 +11,12 @@ describe("PaginatedList", () => { const render = () => null; const i18n = getI18n(); - const { logout, ...store } = new RootStore(); const props = { i18n, tReady: true, t: ((key: string) => key) as TFunction, - logout: () => { - // - }, - ...store, - }; + } as any; it("with no items renders nothing", () => { const list = shallow( @@ -59,13 +53,13 @@ describe("PaginatedList", () => { ); expect(fetch).toHaveBeenCalledWith({ ...options, - limit: DEFAULT_PAGINATION_LIMIT, + limit: Pagination.defaultLimit, offset: 0, }); }); it("calls fetch when options prop changes", async () => { - const fetchedItems = Array(DEFAULT_PAGINATION_LIMIT).fill(undefined); + const fetchedItems = Array(Pagination.defaultLimit).fill(undefined); const fetch = jest.fn().mockReturnValue(Promise.resolve(fetchedItems)); const list = shallow( { await runAllPromises(); expect(fetch).toHaveBeenCalledWith({ id: "one", - limit: DEFAULT_PAGINATION_LIMIT, + limit: Pagination.defaultLimit, offset: 0, }); fetch.mockReset(); @@ -95,7 +89,7 @@ describe("PaginatedList", () => { await runAllPromises(); expect(fetch).toHaveBeenCalledWith({ id: "two", - limit: DEFAULT_PAGINATION_LIMIT, + limit: Pagination.defaultLimit, offset: 0, }); }); diff --git a/app/components/PaginatedList.tsx b/app/components/PaginatedList.tsx index 407bbb038..3daea4bed 100644 --- a/app/components/PaginatedList.tsx +++ b/app/components/PaginatedList.tsx @@ -5,8 +5,8 @@ import * as React from "react"; import { withTranslation, WithTranslation } from "react-i18next"; import { Waypoint } from "react-waypoint"; import { CompositeStateReturn } from "reakit/Composite"; +import { Pagination } from "@shared/constants"; import RootStore from "~/stores/RootStore"; -import { DEFAULT_PAGINATION_LIMIT } from "~/stores/base/Store"; import ArrowKeyNavigation from "~/components/ArrowKeyNavigation"; import DelayedMount from "~/components/DelayedMount"; import PlaceholderList from "~/components/List/Placeholder"; @@ -86,7 +86,7 @@ class PaginatedList extends React.Component> { reset = () => { this.offset = 0; this.allowLoadMore = true; - this.renderCount = DEFAULT_PAGINATION_LIMIT; + this.renderCount = Pagination.defaultLimit; this.isFetching = false; this.isFetchingInitial = false; this.isFetchingMore = false; @@ -99,7 +99,7 @@ class PaginatedList extends React.Component> { } this.isFetching = true; const counter = ++this.fetchCounter; - const limit = this.props.options?.limit ?? DEFAULT_PAGINATION_LIMIT; + const limit = this.props.options?.limit ?? Pagination.defaultLimit; this.error = undefined; try { @@ -139,12 +139,12 @@ class PaginatedList extends React.Component> { const leftToRender = (this.props.items?.length ?? 0) - this.renderCount; if (leftToRender > 0) { - this.renderCount += DEFAULT_PAGINATION_LIMIT; + this.renderCount += Pagination.defaultLimit; } // If there are less than a pages results in the cache go ahead and fetch // another page from the server - if (leftToRender <= DEFAULT_PAGINATION_LIMIT) { + if (leftToRender <= Pagination.defaultLimit) { this.isFetchingMore = true; await this.fetchResults(); } diff --git a/app/models/ApiKey.ts b/app/models/ApiKey.ts index 5d69535fc..fdf652d86 100644 --- a/app/models/ApiKey.ts +++ b/app/models/ApiKey.ts @@ -3,6 +3,8 @@ import Model from "./base/Model"; import Field from "./decorators/Field"; class ApiKey extends Model { + static modelName = "ApiKey"; + @Field @observable id: string; diff --git a/app/models/AuthenticationProvider.ts b/app/models/AuthenticationProvider.ts index 2e80dd151..1c56852e1 100644 --- a/app/models/AuthenticationProvider.ts +++ b/app/models/AuthenticationProvider.ts @@ -3,6 +3,8 @@ import Model from "./base/Model"; import Field from "./decorators/Field"; class AuthenticationProvider extends Model { + static modelName = "AuthenticationProvider"; + id: string; displayName: string; diff --git a/app/models/Collection.ts b/app/models/Collection.ts index eb6b0c7ec..52d84f442 100644 --- a/app/models/Collection.ts +++ b/app/models/Collection.ts @@ -13,6 +13,8 @@ import { client } from "~/utils/ApiClient"; import Field from "./decorators/Field"; export default class Collection extends ParanoidModel { + static modelName = "Collection"; + store: CollectionsStore; @observable diff --git a/app/models/CollectionGroupMembership.ts b/app/models/CollectionGroupMembership.ts index 1dbab80a0..ece2c8b83 100644 --- a/app/models/CollectionGroupMembership.ts +++ b/app/models/CollectionGroupMembership.ts @@ -1,14 +1,25 @@ import { observable } from "mobx"; import { CollectionPermission } from "@shared/types"; +import Collection from "./Collection"; +import Group from "./Group"; import Model from "./base/Model"; +import Relation from "./decorators/Relation"; class CollectionGroupMembership extends Model { + static modelName = "CollectionGroupMembership"; + id: string; groupId: string; + @Relation(() => Group, { onDelete: "cascade" }) + group: Group; + collectionId: string; + @Relation(() => Collection, { onDelete: "cascade" }) + collection: Collection; + @observable permission: CollectionPermission; } diff --git a/app/models/Comment.ts b/app/models/Comment.ts index bfcda0cfa..8fc2c3660 100644 --- a/app/models/Comment.ts +++ b/app/models/Comment.ts @@ -8,6 +8,8 @@ import Field from "./decorators/Field"; import Relation from "./decorators/Relation"; class Comment extends Model { + static modelName = "Comment"; + /** * Map to keep track of which users are currently typing a reply in this * comments thread. diff --git a/app/models/Document.ts b/app/models/Document.ts index cd079a48c..e13bf02eb 100644 --- a/app/models/Document.ts +++ b/app/models/Document.ts @@ -22,6 +22,8 @@ type SaveOptions = { }; export default class Document extends ParanoidModel { + static modelName = "Document"; + constructor(fields: Record, store: DocumentsStore) { super(fields, store); diff --git a/app/models/Event.ts b/app/models/Event.ts index 49f230afc..22a882d9b 100644 --- a/app/models/Event.ts +++ b/app/models/Event.ts @@ -3,6 +3,8 @@ import Model from "./base/Model"; import Relation from "./decorators/Relation"; class Event extends Model { + static modelName = "Event"; + id: string; name: string; diff --git a/app/models/FileOperation.ts b/app/models/FileOperation.ts index 8673a6f5a..ba0bcdc71 100644 --- a/app/models/FileOperation.ts +++ b/app/models/FileOperation.ts @@ -5,6 +5,8 @@ import User from "./User"; import Model from "./base/Model"; class FileOperation extends Model { + static modelName = "FileOperation"; + id: string; @observable diff --git a/app/models/Group.ts b/app/models/Group.ts index 63772e0c2..0e62f4085 100644 --- a/app/models/Group.ts +++ b/app/models/Group.ts @@ -3,6 +3,8 @@ import Model from "./base/Model"; import Field from "./decorators/Field"; class Group extends Model { + static modelName = "Group"; + @Field @observable id: string; diff --git a/app/models/GroupMembership.ts b/app/models/GroupMembership.ts index 3ba10ddac..e4dcf4c75 100644 --- a/app/models/GroupMembership.ts +++ b/app/models/GroupMembership.ts @@ -1,16 +1,20 @@ +import Group from "./Group"; import User from "./User"; import Model from "./base/Model"; import Relation from "./decorators/Relation"; class GroupMembership extends Model { - id: string; + static modelName = "GroupMembership"; userId: string; - groupId: string; - @Relation(() => User, { onDelete: "cascade" }) user: User; + + groupId: string; + + @Relation(() => Group, { onDelete: "cascade" }) + group: Group; } export default GroupMembership; diff --git a/app/models/Integration.ts b/app/models/Integration.ts index 508647c31..84984ce56 100644 --- a/app/models/Integration.ts +++ b/app/models/Integration.ts @@ -8,6 +8,8 @@ import Model from "~/models/base/Model"; import Field from "./decorators/Field"; class Integration extends Model { + static modelName = "Integration"; + id: string; type: IntegrationType; diff --git a/app/models/Membership.ts b/app/models/Membership.ts index 1c6ef4ebb..5b49edd05 100644 --- a/app/models/Membership.ts +++ b/app/models/Membership.ts @@ -3,6 +3,8 @@ import { CollectionPermission } from "@shared/types"; import Model from "./base/Model"; class Membership extends Model { + static modelName = "Membership"; + id: string; userId: string; diff --git a/app/models/Notification.ts b/app/models/Notification.ts index e4a99b674..4e27e04c5 100644 --- a/app/models/Notification.ts +++ b/app/models/Notification.ts @@ -15,6 +15,8 @@ import Field from "./decorators/Field"; import Relation from "./decorators/Relation"; class Notification extends Model { + static modelName = "Notification"; + @Field @observable id: string; diff --git a/app/models/Pin.ts b/app/models/Pin.ts index 5a4082ace..1c27fdaa1 100644 --- a/app/models/Pin.ts +++ b/app/models/Pin.ts @@ -6,6 +6,8 @@ import Field from "./decorators/Field"; import Relation from "./decorators/Relation"; class Pin extends Model { + static modelName = "Pin"; + /** The collection ID that the document is pinned to. If empty the document is pinned to home. */ collectionId: string; diff --git a/app/models/Policy.ts b/app/models/Policy.ts index ebfea332c..553fd95c2 100644 --- a/app/models/Policy.ts +++ b/app/models/Policy.ts @@ -2,6 +2,8 @@ import { observable } from "mobx"; import Model from "./base/Model"; class Policy extends Model { + static modelName = "Policy"; + id: string; @observable diff --git a/app/models/Revision.ts b/app/models/Revision.ts index da157883c..a2b1954bc 100644 --- a/app/models/Revision.ts +++ b/app/models/Revision.ts @@ -6,6 +6,8 @@ import Model from "./base/Model"; import Relation from "./decorators/Relation"; class Revision extends Model { + static modelName = "Revision"; + /** The document ID that the revision is related to */ documentId: string; diff --git a/app/models/SearchQuery.ts b/app/models/SearchQuery.ts index c164c698f..35d37c9a9 100644 --- a/app/models/SearchQuery.ts +++ b/app/models/SearchQuery.ts @@ -2,6 +2,8 @@ import { client } from "~/utils/ApiClient"; import Model from "./base/Model"; class SearchQuery extends Model { + static modelName = "Search"; + id: string; query: string; diff --git a/app/models/Share.ts b/app/models/Share.ts index 75d5735a3..4b065d032 100644 --- a/app/models/Share.ts +++ b/app/models/Share.ts @@ -6,6 +6,8 @@ import Field from "./decorators/Field"; import Relation from "./decorators/Relation"; class Share extends Model { + static modelName = "Share"; + @Field @observable published: boolean; diff --git a/app/models/Star.ts b/app/models/Star.ts index 4d9f83df5..347f6385b 100644 --- a/app/models/Star.ts +++ b/app/models/Star.ts @@ -7,6 +7,8 @@ import Field from "./decorators/Field"; import Relation from "./decorators/Relation"; class Star extends Model { + static modelName = "Star"; + /** The sort order of the star */ @Field @observable diff --git a/app/models/Subscription.ts b/app/models/Subscription.ts index 0f9627c61..e9d3bd70c 100644 --- a/app/models/Subscription.ts +++ b/app/models/Subscription.ts @@ -9,6 +9,8 @@ import Relation from "./decorators/Relation"; * A subscription represents a request for a user to receive notifications for a document. */ class Subscription extends Model { + static modelName = "Subscription"; + /** The user ID subscribing */ userId: string; diff --git a/app/models/Team.ts b/app/models/Team.ts index 76e658d01..436fcbe2f 100644 --- a/app/models/Team.ts +++ b/app/models/Team.ts @@ -6,6 +6,8 @@ import Model from "./base/Model"; import Field from "./decorators/Field"; class Team extends Model { + static modelName = "Team"; + @Field @observable id: string; diff --git a/app/models/User.ts b/app/models/User.ts index e1814ac6c..2344d9ea6 100644 --- a/app/models/User.ts +++ b/app/models/User.ts @@ -16,6 +16,8 @@ import ParanoidModel from "./base/ParanoidModel"; import Field from "./decorators/Field"; class User extends ParanoidModel { + static modelName = "User"; + @Field @observable id: string; diff --git a/app/models/View.ts b/app/models/View.ts index 42efb0abc..4cc4e934e 100644 --- a/app/models/View.ts +++ b/app/models/View.ts @@ -1,13 +1,19 @@ import { action, observable } from "mobx"; +import Document from "./Document"; import User from "./User"; import Model from "./base/Model"; import Relation from "./decorators/Relation"; class View extends Model { + static modelName = "View"; + id: string; documentId: string; + @Relation(() => Document) + document?: Document; + firstViewedAt: string; @observable diff --git a/app/models/WebhookSubscription.ts b/app/models/WebhookSubscription.ts index 72197eee2..400858fa8 100644 --- a/app/models/WebhookSubscription.ts +++ b/app/models/WebhookSubscription.ts @@ -3,6 +3,8 @@ import Model from "./base/Model"; import Field from "./decorators/Field"; class WebhookSubscription extends Model { + static modelName = "WebhookSubscription"; + @Field @observable id: string; diff --git a/app/models/base/Model.ts b/app/models/base/Model.ts index 147fac3c2..e1bd6d6f3 100644 --- a/app/models/base/Model.ts +++ b/app/models/base/Model.ts @@ -5,6 +5,8 @@ import Logger from "~/utils/Logger"; import { getFieldsForModel } from "../decorators/Field"; export default abstract class Model { + static modelName: string; + @observable id: string; diff --git a/app/models/decorators/Relation.ts b/app/models/decorators/Relation.ts index ec8dec8b7..f61bbd5cf 100644 --- a/app/models/decorators/Relation.ts +++ b/app/models/decorators/Relation.ts @@ -35,7 +35,9 @@ export const getInverseRelationsForModelClass = (targetClass: typeof Model) => { relations.forEach((relation, modelName) => { relation.forEach((properties, propertyName) => { - if (properties.relationClassResolver().name === targetClass.name) { + if ( + properties.relationClassResolver().modelName === targetClass.modelName + ) { inverseRelations.set(propertyName, { ...properties, modelName, @@ -66,13 +68,13 @@ export default function Relation( // this to determine how to update relations when a model is deleted. if (options) { const configForClass = - relations.get(target.constructor.name) || new Map(); + relations.get(target.constructor.modelName) || new Map(); configForClass.set(propertyKey, { options, relationClassResolver: classResolver, idKey, }); - relations.set(target.constructor.name, configForClass); + relations.set(target.constructor.modelName, configForClass); } Object.defineProperty(target, propertyKey, { @@ -83,9 +85,9 @@ export default function Relation( return undefined; } - const relationClassName = classResolver().name; + const relationClassName = classResolver().modelName; const store = - this.store.rootStore[`${relationClassName.toLowerCase()}s`]; + this.store.rootStore.getStoreForModelName(relationClassName); invariant(store, `Store for ${relationClassName} not found`); return store.get(id); @@ -94,9 +96,9 @@ export default function Relation( this[idKey] = newValue ? newValue.id : undefined; if (newValue) { - const relationClassName = classResolver().name; + const relationClassName = classResolver().modelName; const store = - this.store.rootStore[`${relationClassName.toLowerCase()}s`]; + this.store.rootStore.getStoreForModelName(relationClassName); invariant(store, `Store for ${relationClassName} not found`); store.add(newValue); diff --git a/app/scenes/Search/Search.tsx b/app/scenes/Search/Search.tsx index f8a6e95f3..c1e8b11ac 100644 --- a/app/scenes/Search/Search.tsx +++ b/app/scenes/Search/Search.tsx @@ -9,11 +9,11 @@ import { Waypoint } from "react-waypoint"; import styled from "styled-components"; import breakpoint from "styled-components-breakpoint"; import { v4 as uuidv4 } from "uuid"; +import { Pagination } from "@shared/constants"; import { hideScrollbars } from "@shared/styles"; import { DateFilter as TDateFilter } from "@shared/types"; import { SearchParams } from "~/stores/DocumentsStore"; import RootStore from "~/stores/RootStore"; -import { DEFAULT_PAGINATION_LIMIT } from "~/stores/base/Store"; import ArrowKeyNavigation from "~/components/ArrowKeyNavigation"; import DocumentListItem from "~/components/DocumentListItem"; import Empty from "~/components/Empty"; @@ -248,7 +248,7 @@ class Search extends React.Component { if (this.query.trim()) { const params = { offset: this.offset, - limit: DEFAULT_PAGINATION_LIMIT, + limit: Pagination.defaultLimit, dateFilter: this.dateFilter, includeArchived: this.includeArchived, includeDrafts: true, @@ -280,10 +280,10 @@ class Search extends React.Component { createdAt: new Date().toISOString(), }); - if (results.length === 0 || results.length < DEFAULT_PAGINATION_LIMIT) { + if (results.length === 0 || results.length < Pagination.defaultLimit) { this.allowLoadMore = false; } else { - this.offset += DEFAULT_PAGINATION_LIMIT; + this.offset += Pagination.defaultLimit; } } catch (error) { Logger.error("Search query failed", error); diff --git a/app/stores/AuthStore.ts b/app/stores/AuthStore.ts index aaf472cc0..54bd1c890 100644 --- a/app/stores/AuthStore.ts +++ b/app/stores/AuthStore.ts @@ -351,6 +351,6 @@ export default class AuthStore extends Store { // Tell the host application we logged out, if any – allows window cleanup. void Desktop.bridge?.onLogout?.(); - this.rootStore.logout(); + this.rootStore.clear(); }; } diff --git a/app/stores/CommentsStore.ts b/app/stores/CommentsStore.ts index 9f1603375..d1c664c1e 100644 --- a/app/stores/CommentsStore.ts +++ b/app/stores/CommentsStore.ts @@ -10,8 +10,6 @@ import RootStore from "./RootStore"; import Store from "./base/Store"; export default class CommentsStore extends Store { - apiEndpoint = "comments"; - constructor(rootStore: RootStore) { super(rootStore, Comment); } diff --git a/app/stores/RootStore.ts b/app/stores/RootStore.ts index df9fd1331..e14264435 100644 --- a/app/stores/RootStore.ts +++ b/app/stores/RootStore.ts @@ -1,3 +1,5 @@ +import invariant from "invariant"; +import pluralize from "pluralize"; import ApiKeysStore from "./ApiKeysStore"; import AuthStore from "./AuthStore"; import AuthenticationProvidersStore from "./AuthenticationProvidersStore"; @@ -25,6 +27,7 @@ import UiStore from "./UiStore"; import UsersStore from "./UsersStore"; import ViewsStore from "./ViewsStore"; import WebhookSubscriptionsStore from "./WebhookSubscriptionStore"; +import Store from "./base/Store"; export default class RootStore { apiKeys: ApiKeysStore; @@ -56,42 +59,79 @@ export default class RootStore { webhookSubscriptions: WebhookSubscriptionsStore; constructor() { - this.apiKeys = new ApiKeysStore(this); - this.authenticationProviders = new AuthenticationProvidersStore(this); - this.collections = new CollectionsStore(this); - this.collectionGroupMemberships = new CollectionGroupMembershipsStore(this); - this.comments = new CommentsStore(this); - this.dialogs = new DialogsStore(); - this.documents = new DocumentsStore(this); - this.events = new EventsStore(this); - this.groups = new GroupsStore(this); - this.groupMemberships = new GroupMembershipsStore(this); - this.integrations = new IntegrationsStore(this); - this.memberships = new MembershipsStore(this); - this.notifications = new NotificationsStore(this); - this.pins = new PinsStore(this); - this.policies = new PoliciesStore(this); - this.presence = new DocumentPresenceStore(); - this.revisions = new RevisionsStore(this); - this.searches = new SearchesStore(this); - this.shares = new SharesStore(this); - this.stars = new StarsStore(this); - this.subscriptions = new SubscriptionsStore(this); - this.ui = new UiStore(); - this.users = new UsersStore(this); - this.views = new ViewsStore(this); - this.fileOperations = new FileOperationsStore(this); - this.webhookSubscriptions = new WebhookSubscriptionsStore(this); + // Models + this.registerStore(ApiKeysStore); + this.registerStore(AuthenticationProvidersStore); + this.registerStore(CollectionsStore); + this.registerStore(CollectionGroupMembershipsStore); + this.registerStore(CommentsStore); + this.registerStore(DocumentsStore); + this.registerStore(EventsStore); + this.registerStore(GroupsStore); + this.registerStore(GroupMembershipsStore); + this.registerStore(IntegrationsStore); + this.registerStore(MembershipsStore); + this.registerStore(NotificationsStore); + this.registerStore(PinsStore); + this.registerStore(PoliciesStore); + this.registerStore(RevisionsStore); + this.registerStore(SearchesStore); + this.registerStore(SharesStore); + this.registerStore(StarsStore); + this.registerStore(SubscriptionsStore); + this.registerStore(UsersStore); + this.registerStore(ViewsStore); + this.registerStore(FileOperationsStore); + this.registerStore(WebhookSubscriptionsStore); + + // Non-models + this.registerStore(DocumentPresenceStore, "presence"); + this.registerStore(DialogsStore, "dialogs"); + this.registerStore(UiStore, "ui"); // AuthStore must be initialized last as it makes use of the other stores. - this.auth = new AuthStore(this); + this.registerStore(AuthStore, "auth"); } - logout() { + /** + * Get a store by model name. + * + * @param modelName + */ + public getStoreForModelName( + modelName: string + ): RootStore[K] { + const storeName = this.getStoreNameForModelName(modelName); + const store = this[storeName]; + invariant(store, `No store found for model name "${modelName}"`); + + return store; + } + + /** + * Clear all data from the stores except for auth and ui. + */ + public clear() { Object.getOwnPropertyNames(this) .filter((key) => ["auth", "ui"].includes(key) === false) .forEach((key) => { this[key]?.clear?.(); }); } + + /** + * Register a store with the root store. + * + * @param StoreClass + */ + private registerStore(StoreClass: T, name?: string) { + // @ts-expect-error TS thinks we are instantiating an abstract class. + const store = new StoreClass(this); + const storeName = name ?? this.getStoreNameForModelName(store.modelName); + this[storeName] = store; + } + + private getStoreNameForModelName(modelName: string) { + return pluralize(modelName.toLowerCase()); + } } diff --git a/app/stores/SearchesStore.ts b/app/stores/SearchesStore.ts index 6cc6c89f3..e48bf7edf 100644 --- a/app/stores/SearchesStore.ts +++ b/app/stores/SearchesStore.ts @@ -7,8 +7,6 @@ import Store, { RPCAction } from "./base/Store"; export default class SearchesStore extends Store { actions = [RPCAction.List, RPCAction.Delete]; - apiEndpoint = "searches"; - constructor(rootStore: RootStore) { super(rootStore, SearchQuery); } diff --git a/app/stores/base/Store.ts b/app/stores/base/Store.ts index e8ac79808..cdf06ab54 100644 --- a/app/stores/base/Store.ts +++ b/app/stores/base/Store.ts @@ -2,7 +2,7 @@ import invariant from "invariant"; import lowerFirst from "lodash/lowerFirst"; import orderBy from "lodash/orderBy"; import { observable, action, computed, runInAction } from "mobx"; -import { Class } from "utility-types"; +import pluralize from "pluralize"; import RootStore from "~/stores/RootStore"; import Policy from "~/models/Policy"; import Model from "~/models/base/Model"; @@ -22,8 +22,6 @@ export enum RPCAction { type FetchPageParams = PaginationParams & Record; -export const DEFAULT_PAGINATION_LIMIT = 25; - export const PAGINATION_SYMBOL = Symbol.for("pagination"); export default abstract class Store { @@ -39,7 +37,7 @@ export default abstract class Store { @observable isLoaded = false; - model: Class; + model: typeof Model; modelName: string; @@ -56,13 +54,13 @@ export default abstract class Store { RPCAction.Count, ]; - constructor(rootStore: RootStore, model: Class) { + constructor(rootStore: RootStore, model: typeof Model) { this.rootStore = rootStore; this.model = model; - this.modelName = lowerFirst(model.name).replace(/\d$/, ""); + this.modelName = model.modelName; if (!this.apiEndpoint) { - this.apiEndpoint = `${this.modelName}s`; + this.apiEndpoint = pluralize(lowerFirst(model.modelName)); } } @@ -89,6 +87,7 @@ export default abstract class Store { return existingModel; } + // @ts-expect-error TS thinks that we're instantiating an abstract class here const newModel = new ModelClass(item, this); this.data.set(newModel.id, newModel); return newModel; @@ -103,20 +102,21 @@ export default abstract class Store { const inverseRelations = getInverseRelationsForModelClass(this.model); inverseRelations.forEach((relation) => { - // TODO: Need a better way to get the store for a given model name. - const store = this.rootStore[`${relation.modelName.toLowerCase()}s`]; - const items = store.orderedData.filter( - (item: Model) => item[relation.idKey] === id - ); + const store = this.rootStore.getStoreForModelName(relation.modelName); + if ("orderedData" in store) { + const items = (store.orderedData as Model[]).filter( + (item) => item[relation.idKey] === id + ); - if (relation.options.onDelete === "cascade") { - items.forEach((item: Model) => store.remove(item.id)); - } + if (relation.options.onDelete === "cascade") { + items.forEach((item) => store.remove(item.id)); + } - if (relation.options.onDelete === "null") { - items.forEach((item: Model) => { - item[relation.idKey] = null; - }); + if (relation.options.onDelete === "null") { + items.forEach((item) => { + item[relation.idKey] = null; + }); + } } }); diff --git a/package.json b/package.json index a95d1a752..e7d9aeec1 100644 --- a/package.json +++ b/package.json @@ -157,6 +157,7 @@ "patch-package": "^7.0.2", "pg": "^8.11.1", "pg-tsquery": "^8.4.1", + "pluralize": "^8.0.0", "polished": "^4.2.2", "prosemirror-codemark": "^0.4.2", "prosemirror-commands": "^1.5.2", @@ -278,6 +279,7 @@ "@types/node-fetch": "^2.6.9", "@types/nodemailer": "^6.4.14", "@types/passport-oauth2": "^1.4.15", + "@types/pluralize": "^0.0.33", "@types/quoted-printable": "^1.0.0", "@types/randomstring": "^1.1.11", "@types/react": "^17.0.34", diff --git a/server/routes/api/middlewares/pagination.ts b/server/routes/api/middlewares/pagination.ts index b2dba4560..2b132ec0e 100644 --- a/server/routes/api/middlewares/pagination.ts +++ b/server/routes/api/middlewares/pagination.ts @@ -1,14 +1,15 @@ import querystring from "querystring"; import { Next } from "koa"; +import { Pagination } from "@shared/constants"; import { InvalidRequestError } from "@server/errors"; import { AppContext } from "@server/types"; export default function pagination() { return async function paginationMiddleware(ctx: AppContext, next: Next) { const opts = { - defaultLimit: 15, - defaultOffset: 0, - maxLimit: 100, + defaultLimit: Pagination.defaultLimit, + defaultOffset: Pagination.defaultOffset, + maxLimit: Pagination.maxLimit, }; const query = ctx.request.query; const body = ctx.request.body; diff --git a/shared/constants.ts b/shared/constants.ts index 3c4b72909..bb8b4be72 100644 --- a/shared/constants.ts +++ b/shared/constants.ts @@ -7,6 +7,12 @@ import { export const MAX_AVATAR_DISPLAY = 6; +export const Pagination = { + defaultLimit: 25, + defaultOffset: 0, + maxLimit: 100, +}; + export const TeamPreferenceDefaults: TeamPreferences = { [TeamPreference.SeamlessEdit]: true, [TeamPreference.ViewersCanExport]: true, diff --git a/yarn.lock b/yarn.lock index de1f970de..3e963d4b9 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3314,6 +3314,11 @@ dependencies: "@types/express" "*" +"@types/pluralize@^0.0.33": + version "0.0.33" + resolved "https://registry.yarnpkg.com/@types/pluralize/-/pluralize-0.0.33.tgz#8ad9018368c584d268667dd9acd5b3b806e8c82a" + integrity sha512-JOqsl+ZoCpP4e8TDke9W79FDcSgPAR0l6pixx2JHkhnRjvShyYiAYw2LVsnA7K08Y6DeOnaU6ujmENO4os/cYg== + "@types/prismjs@*": version "1.26.0" resolved "https://registry.yarnpkg.com/@types/prismjs/-/prismjs-1.26.0.tgz#a1c3809b0ad61c62cac6d4e0c56d610c910b7654" @@ -10609,6 +10614,11 @@ pkg-up@^3.1.0: dependencies: find-up "^3.0.0" +pluralize@^8.0.0: + version "8.0.0" + resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-8.0.0.tgz#1a6fa16a38d12a1901e0320fa017051c539ce3b1" + integrity sha512-Nc3IT5yHzflTfbjgqWcCPpo7DaKy4FnpB0l/zCAW0Tc7jxAiuqSxHasntB3D7887LSrA93kDJ9IXovxJYxyLCA== + polished@^4.2.2: version "4.2.2" resolved "https://registry.yarnpkg.com/polished/-/polished-4.2.2.tgz#2529bb7c3198945373c52e34618c8fe7b1aa84d1"