feat: Pin to home (#2880)

This commit is contained in:
Tom Moor
2021-12-30 16:54:02 -08:00
committed by GitHub
parent 5be2eb75f3
commit eb0c324da8
57 changed files with 1884 additions and 819 deletions

View File

@@ -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",

View File

@@ -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);

View File

@@ -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();

View File

@@ -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]),
});

View File

@@ -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
View 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;