mirror of
https://github.com/outline/outline.git
synced 2026-06-13 03:14:59 +03:00
0139b91b5d
* chore: Replace lodash with es-toolkit Migrate all direct lodash imports to es-toolkit/compat for a smaller, faster, lodash-compatible utility library. Transitive lodash usage from other packages remains unchanged. * fix: Restore isPlainObject semantics in CanCan policy The lodash migration aliased `isObject` to `lodash/isPlainObject` and the codemod incorrectly mapped the local name to es-toolkit's `isObject`, which also returns true for arrays and functions. This caused condition objects in policy definitions to be skipped, breaking authorization checks across the codebase. * fix: Restore unicode-aware length counting in validators es-toolkit/compat's size() returns string.length, while lodash's _.size() counts unicode code points. Switch to [...value].length to preserve the previous behavior so multi-byte characters like emoji count as one.
173 lines
4.3 KiB
TypeScript
173 lines
4.3 KiB
TypeScript
import invariant from "invariant";
|
|
import { filter, find, isUndefined, orderBy } from "es-toolkit/compat";
|
|
import { action, computed, observable } from "mobx";
|
|
import type { NavigationNode, PublicTeam } from "@shared/types";
|
|
import type Document from "~/models/Document";
|
|
import Share from "~/models/Share";
|
|
import type { PartialExcept } from "~/types";
|
|
import { client } from "~/utils/ApiClient";
|
|
import type RootStore from "./RootStore";
|
|
import Store, { RPCAction } from "./base/Store";
|
|
|
|
export default class SharesStore extends Store<Share> {
|
|
actions = [
|
|
RPCAction.Info,
|
|
RPCAction.List,
|
|
RPCAction.Create,
|
|
RPCAction.Update,
|
|
];
|
|
|
|
@observable
|
|
sharedCache: Map<
|
|
string,
|
|
{ sharedTree: NavigationNode | null; team: PublicTeam } | undefined
|
|
> = new Map();
|
|
|
|
constructor(rootStore: RootStore) {
|
|
super(rootStore, Share);
|
|
}
|
|
|
|
@computed
|
|
get orderedData(): Share[] {
|
|
return orderBy(Array.from(this.data.values()), "createdAt", "asc");
|
|
}
|
|
|
|
@computed
|
|
get published(): Share[] {
|
|
return filter(this.orderedData, (share) => share.published);
|
|
}
|
|
|
|
@action
|
|
revoke = async (share: Share) => {
|
|
await client.post("/shares.revoke", {
|
|
id: share.id,
|
|
});
|
|
this.remove(share.id);
|
|
};
|
|
|
|
@action
|
|
async create(
|
|
params:
|
|
| (PartialExcept<Share, "collectionId"> & { type: "collection" })
|
|
| (PartialExcept<Share, "documentId"> & { type: "document" })
|
|
): Promise<Share> {
|
|
const item =
|
|
params.type === "collection"
|
|
? this.getByCollectionId(params.collectionId)
|
|
: this.getByDocumentId(params.documentId);
|
|
|
|
if (item) {
|
|
return item;
|
|
}
|
|
|
|
return super.create(params);
|
|
}
|
|
|
|
@action
|
|
async fetch(id: string) {
|
|
const share = this.get(id);
|
|
const cache = this.sharedCache.get(id);
|
|
if (share && cache) {
|
|
return share;
|
|
}
|
|
|
|
this.isFetching = true;
|
|
try {
|
|
const res = await client.post(`/${this.apiEndpoint}.info`, {
|
|
id,
|
|
});
|
|
invariant(res?.data, "Data should be available");
|
|
|
|
res.data.shares.map(this.add);
|
|
|
|
if (res.data.collection) {
|
|
this.rootStore.collections.add(res.data.collection);
|
|
}
|
|
|
|
if (res.data.document) {
|
|
this.rootStore.documents.add(res.data.document);
|
|
}
|
|
|
|
this.sharedCache.set(id, {
|
|
sharedTree: res.data.sharedTree,
|
|
team: res.data.team,
|
|
});
|
|
this.addPolicies(res.policies);
|
|
|
|
return this.data.get(id)!;
|
|
} finally {
|
|
this.isFetching = false;
|
|
}
|
|
}
|
|
|
|
@action
|
|
async fetchOne(params: { documentId: string } | { collectionId: string }) {
|
|
const share =
|
|
"collectionId" in params
|
|
? this.getByCollectionId(params.collectionId)
|
|
: this.getByDocumentId(params.documentId);
|
|
if (share) {
|
|
return share;
|
|
}
|
|
this.isFetching = true;
|
|
|
|
try {
|
|
const res = await client.post(`/${this.apiEndpoint}.info`, params);
|
|
|
|
if (isUndefined(res)) {
|
|
return;
|
|
}
|
|
invariant(res?.data, "Data should be available");
|
|
this.addPolicies(res.policies);
|
|
return res.data.shares.map(this.add);
|
|
} finally {
|
|
this.isFetching = false;
|
|
}
|
|
}
|
|
|
|
getByDocumentParents = (document: Document): Share | undefined => {
|
|
const collectionShare = document.collectionId
|
|
? this.getByCollectionId(document.collectionId)
|
|
: undefined;
|
|
|
|
if (collectionShare?.published) {
|
|
return collectionShare;
|
|
}
|
|
|
|
const collection = document.collectionId
|
|
? this.rootStore.collections.get(document.collectionId)
|
|
: undefined;
|
|
if (!collection) {
|
|
return;
|
|
}
|
|
|
|
const parentIds = collection
|
|
.pathToDocument(document.id)
|
|
.slice(0, -1)
|
|
.map((p) => p.id);
|
|
|
|
for (const parentId of parentIds) {
|
|
const share = this.getByDocumentId(parentId);
|
|
|
|
if (share?.includeChildDocuments && share.published) {
|
|
return share;
|
|
}
|
|
}
|
|
|
|
return undefined;
|
|
};
|
|
|
|
getByCollectionId = (collectionId: string): Share | null | undefined =>
|
|
find(this.orderedData, (share) => share.collectionId === collectionId);
|
|
|
|
getByDocumentId = (documentId: string): Share | null | undefined =>
|
|
find(this.orderedData, (share) => share.documentId === documentId);
|
|
|
|
get(id: string): Share | undefined {
|
|
return id
|
|
? (this.data.get(id) ??
|
|
this.orderedData.find((share) => id.endsWith(share.urlId)))
|
|
: undefined;
|
|
}
|
|
}
|