Compare commits

...

7 Commits

Author SHA1 Message Date
Apoorv Mishra 4f4bc2e36a fix: review 2024-09-06 12:09:21 +05:30
Apoorv Mishra c4b2757403 fix: restore deletion 2024-09-04 11:22:25 +05:30
Apoorv Mishra 1f1097250f fix: new PartialExcept type 2024-09-04 11:22:25 +05:30
Apoorv Mishra eb2e38addd fix: PartialWithArchivedAt not needed 2024-09-04 11:22:25 +05:30
Apoorv Mishra 98687c0c64 fix(server): ArchivableModel 2024-09-04 11:22:25 +05:30
Apoorv Mishra e5e69838dc fix(app): ArchivableModel 2024-09-04 11:22:25 +05:30
Apoorv Mishra bf95d4ff6f fix: nested docs should appear in archive 2024-09-04 11:22:25 +05:30
10 changed files with 154 additions and 76 deletions
+58 -49
View File
@@ -25,7 +25,7 @@ import User from "~/models/User";
import UserMembership from "~/models/UserMembership";
import withStores from "~/components/withStores";
import {
PartialWithId,
PartialExcept,
WebsocketCollectionUpdateIndexEvent,
WebsocketEntitiesEvent,
WebsocketEntityDeletedEvent,
@@ -214,23 +214,20 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"documents.update",
action(
(event: PartialWithId<Document> & { title: string; url: string }) => {
documents.add(event);
action((event: PartialExcept<Document, "id" | "title" | "url">) => {
documents.add(event);
if (event.collectionId) {
const collection = collections.get(event.collectionId);
collection?.updateDocument(event);
}
if (event.collectionId) {
const collection = collections.get(event.collectionId);
collection?.updateDocument(event);
}
)
})
);
this.socket.on(
"documents.archive",
action((event: PartialWithId<Document>) => {
documents.add(event);
policies.remove(event.id);
action((event: PartialExcept<Document, "id">) => {
documents.addToArchive(event as Document);
if (event.collectionId) {
const collection = collections.get(event.collectionId);
@@ -241,7 +238,7 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"documents.delete",
action((event: PartialWithId<Document>) => {
action((event: PartialExcept<Document, "id">) => {
documents.add(event);
policies.remove(event.id);
@@ -265,7 +262,7 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"documents.add_user",
async (event: PartialWithId<UserMembership>) => {
async (event: PartialExcept<UserMembership, "id">) => {
userMemberships.add(event);
// Any existing child policies are now invalid
@@ -286,7 +283,7 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"documents.remove_user",
(event: PartialWithId<UserMembership>) => {
(event: PartialExcept<UserMembership, "id">) => {
userMemberships.remove(event.id);
// Any existing child policies are now invalid
@@ -308,7 +305,7 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"documents.add_group",
(event: PartialWithId<GroupMembership>) => {
(event: PartialExcept<GroupMembership, "id">) => {
groupMemberships.add(event);
const group = groups.get(event.groupId!);
@@ -330,16 +327,16 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"documents.remove_group",
(event: PartialWithId<GroupMembership>) => {
(event: PartialExcept<GroupMembership, "id">) => {
groupMemberships.remove(event.id);
}
);
this.socket.on("comments.create", (event: PartialWithId<Comment>) => {
this.socket.on("comments.create", (event: PartialExcept<Comment, "id">) => {
comments.add(event);
});
this.socket.on("comments.update", (event: PartialWithId<Comment>) => {
this.socket.on("comments.update", (event: PartialExcept<Comment, "id">) => {
comments.add(event);
});
@@ -347,11 +344,11 @@ class WebsocketProvider extends React.Component<Props> {
comments.remove(event.modelId);
});
this.socket.on("groups.create", (event: PartialWithId<Group>) => {
this.socket.on("groups.create", (event: PartialExcept<Group, "id">) => {
groups.add(event);
});
this.socket.on("groups.update", (event: PartialWithId<Group>) => {
this.socket.on("groups.update", (event: PartialExcept<Group, "id">) => {
groups.add(event);
});
@@ -359,24 +356,36 @@ class WebsocketProvider extends React.Component<Props> {
groups.remove(event.modelId);
});
this.socket.on("groups.add_user", (event: PartialWithId<GroupUser>) => {
groupUsers.add(event);
});
this.socket.on(
"groups.add_user",
(event: PartialExcept<GroupUser, "id">) => {
groupUsers.add(event);
}
);
this.socket.on("groups.remove_user", (event: PartialWithId<GroupUser>) => {
groupUsers.removeAll({
groupId: event.groupId,
userId: event.userId,
});
});
this.socket.on(
"groups.remove_user",
(event: PartialExcept<GroupUser, "id">) => {
groupUsers.removeAll({
groupId: event.groupId,
userId: event.userId,
});
}
);
this.socket.on("collections.create", (event: PartialWithId<Collection>) => {
collections.add(event);
});
this.socket.on(
"collections.create",
(event: PartialExcept<Collection, "id">) => {
collections.add(event);
}
);
this.socket.on("collections.update", (event: PartialWithId<Collection>) => {
collections.add(event);
});
this.socket.on(
"collections.update",
(event: PartialExcept<Collection, "id">) => {
collections.add(event);
}
);
this.socket.on(
"collections.delete",
@@ -398,7 +407,7 @@ class WebsocketProvider extends React.Component<Props> {
})
);
this.socket.on("teams.update", (event: PartialWithId<Team>) => {
this.socket.on("teams.update", (event: PartialExcept<Team, "id">) => {
if ("sharing" in event && event.sharing !== auth.team?.sharing) {
documents.all.forEach((document) => {
policies.remove(document.id);
@@ -410,23 +419,23 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"notifications.create",
(event: PartialWithId<Notification>) => {
(event: PartialExcept<Notification, "id">) => {
notifications.add(event);
}
);
this.socket.on(
"notifications.update",
(event: PartialWithId<Notification>) => {
(event: PartialExcept<Notification, "id">) => {
notifications.add(event);
}
);
this.socket.on("pins.create", (event: PartialWithId<Pin>) => {
this.socket.on("pins.create", (event: PartialExcept<Pin, "id">) => {
pins.add(event);
});
this.socket.on("pins.update", (event: PartialWithId<Pin>) => {
this.socket.on("pins.update", (event: PartialExcept<Pin, "id">) => {
pins.add(event);
});
@@ -434,11 +443,11 @@ class WebsocketProvider extends React.Component<Props> {
pins.remove(event.modelId);
});
this.socket.on("stars.create", (event: PartialWithId<Star>) => {
this.socket.on("stars.create", (event: PartialExcept<Star, "id">) => {
stars.add(event);
});
this.socket.on("stars.update", (event: PartialWithId<Star>) => {
this.socket.on("stars.update", (event: PartialExcept<Star, "id">) => {
stars.add(event);
});
@@ -496,14 +505,14 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"fileOperations.create",
(event: PartialWithId<FileOperation>) => {
(event: PartialExcept<FileOperation, "id">) => {
fileOperations.add(event);
}
);
this.socket.on(
"fileOperations.update",
(event: PartialWithId<FileOperation>) => {
(event: PartialExcept<FileOperation, "id">) => {
fileOperations.add(event);
if (
@@ -520,7 +529,7 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"subscriptions.create",
(event: PartialWithId<Subscription>) => {
(event: PartialExcept<Subscription, "id">) => {
subscriptions.add(event);
}
);
@@ -532,11 +541,11 @@ class WebsocketProvider extends React.Component<Props> {
}
);
this.socket.on("users.update", (event: PartialWithId<User>) => {
this.socket.on("users.update", (event: PartialExcept<User, "id">) => {
users.add(event);
});
this.socket.on("users.demote", async (event: PartialWithId<User>) => {
this.socket.on("users.demote", async (event: PartialExcept<User, "id">) => {
if (event.id === auth.user?.id) {
documents.all.forEach((document) => policies.remove(document.id));
await collections.fetchAll();
@@ -545,7 +554,7 @@ class WebsocketProvider extends React.Component<Props> {
this.socket.on(
"userMemberships.update",
async (event: PartialWithId<UserMembership>) => {
async (event: PartialExcept<UserMembership, "id">) => {
userMemberships.add(event);
}
);
+6 -6
View File
@@ -27,7 +27,7 @@ import { settingsPath } from "~/utils/routeHelpers";
import Collection from "./Collection";
import Notification from "./Notification";
import View from "./View";
import ParanoidModel from "./base/ParanoidModel";
import ArchivableModel from "./base/ArchivableModel";
import Field from "./decorators/Field";
import Relation from "./decorators/Relation";
@@ -37,7 +37,7 @@ type SaveOptions = JSONObject & {
autosave?: boolean;
};
export default class Document extends ParanoidModel {
export default class Document extends ArchivableModel {
static modelName = "Document";
constructor(fields: Record<string, any>, store: DocumentsStore) {
@@ -175,7 +175,10 @@ export default class Document extends ParanoidModel {
@observable
parentDocumentId: string | undefined;
@Relation(() => Document)
/**
* Parent document that this is a child of, if any.
*/
@Relation(() => Document, { onArchive: "cascade" })
parentDocument?: Document;
@observable
@@ -190,9 +193,6 @@ export default class Document extends ParanoidModel {
@observable
publishedAt: string | undefined;
@observable
archivedAt: string;
/**
* @deprecated Use path instead
*/
+7
View File
@@ -0,0 +1,7 @@
import { observable } from "mobx";
import ParanoidModel from "./ParanoidModel";
export default abstract class ArchivableModel extends ParanoidModel {
@observable
archivedAt: string | undefined;
}
+5 -1
View File
@@ -3,12 +3,16 @@ import type Model from "../base/Model";
/** The behavior of a relationship on deletion */
type DeleteBehavior = "cascade" | "null" | "ignore";
/** The behavior of a relationship on archival */
type ArchiveBehavior = "cascade" | "null" | "ignore";
type RelationOptions<T = Model> = {
/** Whether this relation is required. */
required?: boolean;
/** Behavior of this model when relationship is deleted. */
onDelete: DeleteBehavior | ((item: T) => DeleteBehavior);
onDelete?: DeleteBehavior | ((item: T) => DeleteBehavior);
/** Behavior of this model when relationship is archived. */
onArchive?: ArchiveBehavior | ((item: T) => ArchiveBehavior);
};
type RelationProperties<T = Model> = {
+3 -3
View File
@@ -11,7 +11,7 @@ import Team from "~/models/Team";
import User from "~/models/User";
import env from "~/env";
import { setPostLoginPath } from "~/hooks/useLastVisitedPath";
import { PartialWithId } from "~/types";
import { PartialExcept } from "~/types";
import { client } from "~/utils/ApiClient";
import Desktop from "~/utils/Desktop";
import Logger from "~/utils/Logger";
@@ -19,8 +19,8 @@ import isCloudHosted from "~/utils/isCloudHosted";
import Store from "./base/Store";
type PersistedData = {
user?: PartialWithId<User>;
team?: PartialWithId<Team>;
user?: PartialExcept<User, "id">;
team?: PartialExcept<Team, "id">;
collaborationToken?: string;
availableTeams?: {
id: string;
+2 -2
View File
@@ -21,7 +21,7 @@ import env from "~/env";
import type {
FetchOptions,
PaginationParams,
PartialWithId,
PartialExcept,
SearchResult,
} from "~/types";
import { client } from "~/utils/ApiClient";
@@ -489,7 +489,7 @@ export default class DocumentsStore extends Store<Document> {
super.fetch(
id,
options,
(res: { data: { document: PartialWithId<Document> } }) =>
(res: { data: { document: PartialExcept<Document, "id"> } }) =>
res.data.document
);
+40 -3
View File
@@ -11,10 +11,11 @@ import { Pagination } from "@shared/constants";
import { type JSONObject } from "@shared/types";
import RootStore from "~/stores/RootStore";
import Policy from "~/models/Policy";
import ArchivableModel from "~/models/base/ArchivableModel";
import Model from "~/models/base/Model";
import { LifecycleManager } from "~/models/decorators/Lifecycle";
import { getInverseRelationsForModelClass } from "~/models/decorators/Relation";
import type { PaginationParams, PartialWithId, Properties } from "~/types";
import type { PaginationParams, PartialExcept, Properties } from "~/types";
import { client } from "~/utils/ApiClient";
import Logger from "~/utils/Logger";
import { AuthorizationError, NotFoundError } from "~/utils/errors";
@@ -81,7 +82,7 @@ export default abstract class Store<T extends Model> {
};
@action
add = (item: PartialWithId<T> | T): T => {
add = (item: PartialExcept<T, "id"> | T): T => {
const ModelClass = this.model;
if (!(item instanceof ModelClass)) {
@@ -144,6 +145,42 @@ export default abstract class Store<T extends Model> {
LifecycleManager.executeHooks(model.constructor, "afterRemove", model);
}
@action
addToArchive(item: ArchivableModel): void {
const inverseRelations = getInverseRelationsForModelClass(this.model);
inverseRelations.forEach((relation) => {
const store = this.rootStore.getStoreForModelName(relation.modelName);
if ("orderedData" in store) {
const items = (store.orderedData as ArchivableModel[]).filter(
(data) => data[relation.idKey] === item.id
);
items.forEach((item) => {
let archiveBehavior = relation.options.onArchive;
if (typeof relation.options.onArchive === "function") {
archiveBehavior = relation.options.onArchive(item);
}
if (archiveBehavior === "cascade") {
store.addToArchive(item);
} else if (archiveBehavior === "null") {
item[relation.idKey] = null;
}
});
}
});
// Remove associated policies automatically, not defined through Relation decorator.
if (this.modelName !== "Policy") {
this.rootStore.policies.remove(item.id);
}
item.archivedAt = new Date().toISOString();
(this as unknown as Store<ArchivableModel>).add(item);
}
/**
* Remove all items in the store that match the predicate.
*
@@ -245,7 +282,7 @@ export default abstract class Store<T extends Model> {
async fetch(
id: string,
options: JSONObject = {},
accessor = (res: unknown) => (res as { data: PartialWithId<T> }).data
accessor = (res: unknown) => (res as { data: PartialExcept<T, "id"> }).data
): Promise<T> {
if (!this.actions.includes(RPCAction.Info)) {
throw new Error(`Cannot fetch ${this.modelName}`);
+6 -5
View File
@@ -14,7 +14,8 @@ import Pin from "./models/Pin";
import Star from "./models/Star";
import UserMembership from "./models/UserMembership";
export type PartialWithId<T> = Partial<T> & { id: string };
export type PartialExcept<T, K extends keyof T> = Partial<Omit<T, K>> &
Required<Pick<T, K>>;
export type MenuItemButton = {
type: "button";
@@ -195,10 +196,10 @@ export type WebsocketCollectionUpdateIndexEvent = {
};
export type WebsocketEvent =
| PartialWithId<Pin>
| PartialWithId<Star>
| PartialWithId<FileOperation>
| PartialWithId<UserMembership>
| PartialExcept<Pin, "id">
| PartialExcept<Star, "id">
| PartialExcept<FileOperation, "id">
| PartialExcept<UserMembership, "id">
| WebsocketCollectionUpdateIndexEvent
| WebsocketEntityDeletedEvent
| WebsocketEntitiesEvent;
+2 -7
View File
@@ -64,7 +64,7 @@ import Team from "./Team";
import User from "./User";
import UserMembership from "./UserMembership";
import View from "./View";
import ParanoidModel from "./base/ParanoidModel";
import ArchivableModel from "./base/ArchivableModel";
import Fix from "./decorators/Fix";
import { DocumentHelper } from "./helpers/DocumentHelper";
import IsHexColor from "./validators/IsHexColor";
@@ -259,7 +259,7 @@ type AdditionalFindOptions = {
}))
@Table({ tableName: "documents", modelName: "document" })
@Fix
class Document extends ParanoidModel<
class Document extends ArchivableModel<
InferAttributes<Document>,
Partial<InferCreationAttributes<Document>>
> {
@@ -362,11 +362,6 @@ class Document extends ParanoidModel<
@Column(DataType.INTEGER)
revisionCount: number;
/** Whether the document is archvied, and if so when. */
@IsDate
@Column
archivedAt: Date | null;
/** Whether the document is published, and if so when. */
@IsDate
@Column
+25
View File
@@ -0,0 +1,25 @@
/* eslint-disable @typescript-eslint/ban-types */
import { AllowNull, Column, IsDate } from "sequelize-typescript";
import ParanoidModel from "./ParanoidModel";
class ArchivableModel<
TModelAttributes extends {} = any,
TCreationAttributes extends {} = TModelAttributes
> extends ParanoidModel<TModelAttributes, TCreationAttributes> {
/** Whether the document is archived, and if so when. */
@AllowNull
@IsDate
@Column
archivedAt: Date | null;
/**
* Whether the model has been archived.
*
* @returns True if the model has been archived
*/
get isArchived() {
return !!this.archivedAt;
}
}
export default ArchivableModel;