feat: Pin to home (#2880)
This commit is contained in:
@@ -26,15 +26,6 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#documents.pin should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
"message": "Authentication required",
|
||||
"ok": false,
|
||||
"status": 401,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#documents.restore should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
@@ -71,15 +62,6 @@ Object {
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#documents.unpin should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
"message": "Authentication required",
|
||||
"ok": false,
|
||||
"status": 401,
|
||||
}
|
||||
`;
|
||||
|
||||
exports[`#documents.unstar should require authentication 1`] = `
|
||||
Object {
|
||||
"error": "authentication_required",
|
||||
|
||||
@@ -57,26 +57,24 @@ router.post("collections.create", auth(), async (ctx) => {
|
||||
|
||||
const user = ctx.state.user;
|
||||
authorize(user, "createCollection", user.team);
|
||||
const collections = await Collection.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
deletedAt: null,
|
||||
},
|
||||
attributes: ["id", "index", "updatedAt"],
|
||||
limit: 1,
|
||||
order: [
|
||||
// using LC_COLLATE:"C" because we need byte order to drive the sorting
|
||||
sequelize.literal('"collection"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
});
|
||||
|
||||
if (index) {
|
||||
assertIndexCharacters(
|
||||
index,
|
||||
"Index characters must be between x20 to x7E ASCII"
|
||||
);
|
||||
assertIndexCharacters(index);
|
||||
} else {
|
||||
const collections = await Collection.findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
deletedAt: null,
|
||||
},
|
||||
attributes: ["id", "index", "updatedAt"],
|
||||
limit: 1,
|
||||
order: [
|
||||
// using LC_COLLATE:"C" because we need byte order to drive the sorting
|
||||
sequelize.literal('"collection"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
});
|
||||
|
||||
index = fractionalIndex(
|
||||
null,
|
||||
collections.length ? collections[0].index : null
|
||||
@@ -648,10 +646,7 @@ router.post("collections.move", auth(), async (ctx) => {
|
||||
const id = ctx.body.id;
|
||||
let index = ctx.body.index;
|
||||
assertPresent(index, "index is required");
|
||||
assertIndexCharacters(
|
||||
index,
|
||||
"Index characters must be between x20 to x7E ASCII"
|
||||
);
|
||||
assertIndexCharacters(index);
|
||||
assertUuid(id, "id must be a uuid");
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.findByPk(id);
|
||||
|
||||
@@ -843,68 +843,7 @@ describe("#documents.list", () => {
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe("#documents.pinned", () => {
|
||||
it("should return pinned documents", async () => {
|
||||
const { user, document } = await seed();
|
||||
document.pinnedById = user.id;
|
||||
await document.save();
|
||||
const res = await server.post("/api/documents.pinned", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
collectionId: document.collectionId,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(document.id);
|
||||
});
|
||||
|
||||
it("should return pinned documents in private collections member of", async () => {
|
||||
const { user, collection, document } = await seed();
|
||||
collection.permission = null;
|
||||
await collection.save();
|
||||
document.pinnedById = user.id;
|
||||
await document.save();
|
||||
await CollectionUser.create({
|
||||
collectionId: collection.id,
|
||||
userId: user.id,
|
||||
createdById: user.id,
|
||||
permission: "read_write",
|
||||
});
|
||||
const res = await server.post("/api/documents.pinned", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
collectionId: document.collectionId,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.length).toEqual(1);
|
||||
expect(body.data[0].id).toEqual(document.id);
|
||||
});
|
||||
|
||||
it("should not return pinned documents in private collections not a member of", async () => {
|
||||
const collection = await buildCollection({
|
||||
permission: null,
|
||||
});
|
||||
const user = await buildUser({
|
||||
teamId: collection.teamId,
|
||||
});
|
||||
const res = await server.post("/api/documents.pinned", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
collectionId: collection.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/documents.pinned");
|
||||
expect(res.status).toEqual(401);
|
||||
});
|
||||
});
|
||||
describe("#documents.drafts", () => {
|
||||
it("should return unpublished documents", async () => {
|
||||
const { user, document } = await seed();
|
||||
@@ -1534,39 +1473,7 @@ describe("#documents.starred", () => {
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
});
|
||||
describe("#documents.pin", () => {
|
||||
it("should pin the document", async () => {
|
||||
const { user, document } = await seed();
|
||||
const res = await server.post("/api/documents.pin", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.pinned).toEqual(true);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/documents.pin");
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should require authorization", async () => {
|
||||
const { document } = await seed();
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/documents.pin", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: document.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
describe("#documents.move", () => {
|
||||
it("should move the document", async () => {
|
||||
const { user, document } = await seed();
|
||||
@@ -1807,41 +1714,7 @@ describe("#documents.restore", () => {
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
describe("#documents.unpin", () => {
|
||||
it("should unpin the document", async () => {
|
||||
const { user, document } = await seed();
|
||||
document.pinnedBy = user;
|
||||
await document.save();
|
||||
const res = await server.post("/api/documents.unpin", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: document.id,
|
||||
},
|
||||
});
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(200);
|
||||
expect(body.data.pinned).toEqual(false);
|
||||
});
|
||||
|
||||
it("should require authentication", async () => {
|
||||
const res = await server.post("/api/documents.unpin");
|
||||
const body = await res.json();
|
||||
expect(res.status).toEqual(401);
|
||||
expect(body).toMatchSnapshot();
|
||||
});
|
||||
|
||||
it("should require authorization", async () => {
|
||||
const { document } = await seed();
|
||||
const user = await buildUser();
|
||||
const res = await server.post("/api/documents.unpin", {
|
||||
body: {
|
||||
token: user.getJwtToken(),
|
||||
id: document.id,
|
||||
},
|
||||
});
|
||||
expect(res.status).toEqual(403);
|
||||
});
|
||||
});
|
||||
describe("#documents.star", () => {
|
||||
it("should star the document", async () => {
|
||||
const { user, document } = await seed();
|
||||
|
||||
@@ -142,22 +142,8 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
|
||||
}
|
||||
|
||||
assertSort(sort, Document);
|
||||
// add the users starred state to the response by default
|
||||
const starredScope = {
|
||||
method: ["withStarred", user.id],
|
||||
};
|
||||
const collectionScope = {
|
||||
method: ["withCollection", user.id],
|
||||
};
|
||||
const viewScope = {
|
||||
method: ["withViews", user.id],
|
||||
};
|
||||
const documents = await Document.scope(
|
||||
"defaultScope",
|
||||
starredScope,
|
||||
collectionScope,
|
||||
viewScope
|
||||
).findAll({
|
||||
|
||||
const documents = await Document.defaultScopeWithUser(user.id).findAll({
|
||||
where,
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
@@ -185,57 +171,6 @@ router.post("documents.list", auth(), pagination(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.pinned", auth(), pagination(), async (ctx) => {
|
||||
const { collectionId, sort = "updatedAt" } = ctx.body;
|
||||
let direction = ctx.body.direction;
|
||||
if (direction !== "ASC") direction = "DESC";
|
||||
|
||||
assertUuid(collectionId, "collectionId is required");
|
||||
assertSort(sort, Document);
|
||||
|
||||
const user = ctx.state.user;
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "read", collection);
|
||||
const starredScope = {
|
||||
method: ["withStarred", user.id],
|
||||
};
|
||||
const collectionScope = {
|
||||
method: ["withCollection", user.id],
|
||||
};
|
||||
const viewScope = {
|
||||
method: ["withViews", user.id],
|
||||
};
|
||||
const documents = await Document.scope(
|
||||
"defaultScope",
|
||||
starredScope,
|
||||
collectionScope,
|
||||
viewScope
|
||||
).findAll({
|
||||
where: {
|
||||
teamId: user.teamId,
|
||||
collectionId,
|
||||
pinnedById: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
order: [[sort, direction]],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
});
|
||||
const data = await Promise.all(
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'document' implicitly has an 'any' type.
|
||||
documents.map((document) => presentDocument(document))
|
||||
);
|
||||
const policies = presentPolicies(user, documents);
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data,
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.archived", auth(), pagination(), async (ctx) => {
|
||||
const { sort = "updatedAt" } = ctx.body;
|
||||
|
||||
@@ -807,7 +742,6 @@ router.post("documents.restore", auth(), async (ctx) => {
|
||||
}
|
||||
|
||||
ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
@@ -916,7 +850,6 @@ router.post("documents.search", auth(), pagination(), async (ctx) => {
|
||||
const data = await Promise.all(
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'result' implicitly has an 'any' type.
|
||||
results.map(async (result) => {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
const document = await presentDocument(result.document);
|
||||
return { ...result, document };
|
||||
})
|
||||
@@ -942,62 +875,6 @@ router.post("documents.search", auth(), pagination(), async (ctx) => {
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.pin", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findByPk(id, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "pin", document);
|
||||
document.pinnedById = user.id;
|
||||
await document.save();
|
||||
await Event.create({
|
||||
name: "documents.pin",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
data: {
|
||||
title: document.title,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.unpin", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
const user = ctx.state.user;
|
||||
const document = await Document.findByPk(id, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "unpin", document);
|
||||
document.pinnedById = null;
|
||||
await document.save();
|
||||
await Event.create({
|
||||
name: "documents.unpin",
|
||||
documentId: document.id,
|
||||
collectionId: document.collectionId,
|
||||
teamId: document.teamId,
|
||||
actorId: user.id,
|
||||
data: {
|
||||
title: document.title,
|
||||
},
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("documents.star", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertPresent(id, "id is required");
|
||||
@@ -1095,7 +972,6 @@ router.post("documents.templatize", auth(), async (ctx) => {
|
||||
userId: user.id,
|
||||
});
|
||||
ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
@@ -1218,7 +1094,6 @@ router.post("documents.update", auth(), async (ctx) => {
|
||||
document.updatedBy = user;
|
||||
document.collection = collection;
|
||||
ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
@@ -1271,7 +1146,6 @@ router.post("documents.move", auth(), async (ctx) => {
|
||||
ctx.body = {
|
||||
data: {
|
||||
documents: await Promise.all(
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
documents.map((document) => presentDocument(document))
|
||||
),
|
||||
collections: await Promise.all(
|
||||
@@ -1303,7 +1177,6 @@ router.post("documents.archive", auth(), async (ctx) => {
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
@@ -1388,7 +1261,6 @@ router.post("documents.unpublish", auth(), async (ctx) => {
|
||||
ip: ctx.request.ip,
|
||||
});
|
||||
ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
};
|
||||
@@ -1461,7 +1333,6 @@ router.post("documents.import", auth(), async (ctx) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message
|
||||
document.collection = collection;
|
||||
return (ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
});
|
||||
@@ -1537,7 +1408,6 @@ router.post("documents.create", auth(), async (ctx) => {
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'collection' does not exist on type 'Docu... Remove this comment to see the full error message
|
||||
document.collection = collection;
|
||||
return (ctx.body = {
|
||||
// @ts-expect-error ts-migrate(2554) FIXME: Expected 2 arguments, but got 1.
|
||||
data: await presentDocument(document),
|
||||
policies: presentPolicies(user, [document]),
|
||||
});
|
||||
|
||||
@@ -18,6 +18,7 @@ import integrations from "./integrations";
|
||||
import apiWrapper from "./middlewares/apiWrapper";
|
||||
import editor from "./middlewares/editor";
|
||||
import notificationSettings from "./notificationSettings";
|
||||
import pins from "./pins";
|
||||
import revisions from "./revisions";
|
||||
import searches from "./searches";
|
||||
import shares from "./shares";
|
||||
@@ -50,6 +51,7 @@ router.use("/", events.routes());
|
||||
router.use("/", users.routes());
|
||||
router.use("/", collections.routes());
|
||||
router.use("/", documents.routes());
|
||||
router.use("/", pins.routes());
|
||||
router.use("/", revisions.routes());
|
||||
router.use("/", views.routes());
|
||||
router.use("/", hooks.routes());
|
||||
|
||||
156
server/routes/api/pins.ts
Normal file
156
server/routes/api/pins.ts
Normal file
@@ -0,0 +1,156 @@
|
||||
import Router from "koa-router";
|
||||
import pinCreator from "@server/commands/pinCreator";
|
||||
import pinDestroyer from "@server/commands/pinDestroyer";
|
||||
import pinUpdater from "@server/commands/pinUpdater";
|
||||
import auth from "@server/middlewares/authentication";
|
||||
import { Collection, Document, Pin } from "@server/models";
|
||||
import policy from "@server/policies";
|
||||
import {
|
||||
presentPin,
|
||||
presentDocument,
|
||||
presentPolicies,
|
||||
} from "@server/presenters";
|
||||
import { sequelize, Op } from "@server/sequelize";
|
||||
import { assertUuid, assertIndexCharacters } from "@server/validation";
|
||||
import pagination from "./middlewares/pagination";
|
||||
|
||||
const { authorize } = policy;
|
||||
const router = new Router();
|
||||
|
||||
router.post("pins.create", auth(), async (ctx) => {
|
||||
const { documentId, collectionId } = ctx.body;
|
||||
const { index } = ctx.body;
|
||||
assertUuid(documentId, "documentId is required");
|
||||
|
||||
const { user } = ctx.state;
|
||||
const document = await Document.findByPk(documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
authorize(user, "read", document);
|
||||
|
||||
if (collectionId) {
|
||||
const collection = await Collection.scope({
|
||||
method: ["withMembership", user.id],
|
||||
}).findByPk(collectionId);
|
||||
authorize(user, "update", collection);
|
||||
authorize(user, "pin", document);
|
||||
} else {
|
||||
authorize(user, "pinToHome", document);
|
||||
}
|
||||
|
||||
if (index) {
|
||||
assertIndexCharacters(index);
|
||||
}
|
||||
|
||||
const pin = await pinCreator({
|
||||
user,
|
||||
documentId,
|
||||
collectionId,
|
||||
ip: ctx.request.ip,
|
||||
index,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentPin(pin),
|
||||
policies: presentPolicies(user, [pin]),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("pins.list", auth(), pagination(), async (ctx) => {
|
||||
const { collectionId } = ctx.body;
|
||||
const { user } = ctx.state;
|
||||
|
||||
const [pins, collectionIds] = await Promise.all([
|
||||
Pin.findAll({
|
||||
where: {
|
||||
...(collectionId
|
||||
? { collectionId }
|
||||
: { collectionId: { [Op.eq]: null } }),
|
||||
teamId: user.teamId,
|
||||
},
|
||||
order: [
|
||||
sequelize.literal('"pins"."index" collate "C"'),
|
||||
["updatedAt", "DESC"],
|
||||
],
|
||||
offset: ctx.state.pagination.offset,
|
||||
limit: ctx.state.pagination.limit,
|
||||
}),
|
||||
user.collectionIds(),
|
||||
]);
|
||||
|
||||
const documents = await Document.defaultScopeWithUser(user.id).findAll({
|
||||
where: {
|
||||
id: pins.map((pin: any) => pin.documentId),
|
||||
collectionId: collectionIds,
|
||||
},
|
||||
});
|
||||
|
||||
const policies = presentPolicies(user, [...documents, ...pins]);
|
||||
|
||||
ctx.body = {
|
||||
pagination: ctx.state.pagination,
|
||||
data: {
|
||||
pins: pins.map(presentPin),
|
||||
documents: await Promise.all(
|
||||
documents.map((document: any) => presentDocument(document))
|
||||
),
|
||||
},
|
||||
policies,
|
||||
};
|
||||
});
|
||||
|
||||
router.post("pins.update", auth(), async (ctx) => {
|
||||
const { id, index } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
assertIndexCharacters(index);
|
||||
|
||||
const { user } = ctx.state;
|
||||
let pin = await Pin.findByPk(id);
|
||||
const document = await Document.findByPk(pin.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (pin.collectionId) {
|
||||
authorize(user, "pin", document);
|
||||
} else {
|
||||
authorize(user, "update", pin);
|
||||
}
|
||||
|
||||
pin = await pinUpdater({
|
||||
user,
|
||||
pin,
|
||||
ip: ctx.request.ip,
|
||||
index,
|
||||
});
|
||||
|
||||
ctx.body = {
|
||||
data: presentPin(pin),
|
||||
policies: presentPolicies(user, [pin]),
|
||||
};
|
||||
});
|
||||
|
||||
router.post("pins.delete", auth(), async (ctx) => {
|
||||
const { id } = ctx.body;
|
||||
assertUuid(id, "id is required");
|
||||
|
||||
const { user } = ctx.state;
|
||||
const pin = await Pin.findByPk(id);
|
||||
const document = await Document.findByPk(pin.documentId, {
|
||||
userId: user.id,
|
||||
});
|
||||
|
||||
if (pin.collectionId) {
|
||||
authorize(user, "unpin", document);
|
||||
} else {
|
||||
authorize(user, "delete", pin);
|
||||
}
|
||||
|
||||
await pinDestroyer({ user, pin, ip: ctx.request.ip });
|
||||
|
||||
ctx.body = {
|
||||
success: true,
|
||||
};
|
||||
});
|
||||
|
||||
export default router;
|
||||
Reference in New Issue
Block a user