* Allow Document to be fetched without Slug Fixes #3423 This PR refactors the `Document.findByPk` method to not require the `slug` portion of the urlID. Before this function accepted two different 'formats' for the ID. - The `uuid` ID of the Document - The full `urlID` which looked something like `some-document-1234567890` However the `some-document` slug portion of this identifier wasn't actually used when looking for a document. We now allow searching by JUST the postfix of the `urlID`, in the above example that is `1234567890`. We do this via a new Regex pattern to match on that just looks for the right looking id alone, without the prefix. This codepath looks the same as when we find it by the full `urlID` besides the different regex that we match on. The issue #3423 mentions that this should apply to all the API endpoints. I believe that this `findByPk` method is all that should be needed for that change. But if this is incorrect, OR you would like more test coverage on the API endpoints as a more 'end to end test' please let me know! * Change original regex to make the slug optional This has the, I believe to be good, side-effect of making the same logic apply to `Collection` as well. Since `Collection` was always doing the same stripping of the slug before the lookup I believe it should be just as safe to do there. We don't have to touch the code in Collections but we add a test of this behavior there as well. * No reason to rename this now that we aren't doing two matches
408 lines
13 KiB
TypeScript
408 lines
13 KiB
TypeScript
import randomstring from "randomstring";
|
|
import { v4 as uuidv4 } from "uuid";
|
|
import {
|
|
buildUser,
|
|
buildGroup,
|
|
buildCollection,
|
|
buildTeam,
|
|
buildDocument,
|
|
} from "@server/test/factories";
|
|
import { flushdb, seed } from "@server/test/support";
|
|
import slugify from "@server/utils/slugify";
|
|
import Collection from "./Collection";
|
|
import Document from "./Document";
|
|
|
|
beforeEach(() => flushdb());
|
|
beforeEach(jest.resetAllMocks);
|
|
|
|
describe("#url", () => {
|
|
test("should return correct url for the collection", () => {
|
|
const collection = new Collection({
|
|
id: "1234",
|
|
});
|
|
expect(collection.url).toBe(`/collection/untitled-${collection.urlId}`);
|
|
});
|
|
});
|
|
|
|
describe("getDocumentParents", () => {
|
|
test("should return array of parent document ids", async () => {
|
|
const parent = await buildDocument();
|
|
const document = await buildDocument();
|
|
const collection = await buildCollection({
|
|
documentStructure: [
|
|
{ ...parent.toJSON(), children: [document.toJSON()] },
|
|
],
|
|
});
|
|
const result = collection.getDocumentParents(document.id);
|
|
expect(result?.length).toBe(1);
|
|
expect(result[0]).toBe(parent.id);
|
|
});
|
|
|
|
test("should return array of parent document ids", async () => {
|
|
const parent = await buildDocument();
|
|
const document = await buildDocument();
|
|
const collection = await buildCollection({
|
|
documentStructure: [
|
|
{ ...parent.toJSON(), children: [document.toJSON()] },
|
|
],
|
|
});
|
|
const result = collection.getDocumentParents(parent.id);
|
|
expect(result?.length).toBe(0);
|
|
});
|
|
|
|
test("should not error if documentStructure is empty", async () => {
|
|
const parent = await buildDocument();
|
|
await buildDocument();
|
|
const collection = await buildCollection();
|
|
const result = collection.getDocumentParents(parent.id);
|
|
expect(result).toBe(undefined);
|
|
});
|
|
});
|
|
|
|
describe("getDocumentTree", () => {
|
|
test("should return document tree", async () => {
|
|
const document = await buildDocument();
|
|
const collection = await buildCollection({
|
|
documentStructure: [document.toJSON()],
|
|
});
|
|
expect(collection.getDocumentTree(document.id)).toEqual(document.toJSON());
|
|
});
|
|
|
|
test("should return nested documents in tree", async () => {
|
|
const parent = await buildDocument();
|
|
const document = await buildDocument();
|
|
const collection = await buildCollection({
|
|
documentStructure: [
|
|
{ ...parent.toJSON(), children: [document.toJSON()] },
|
|
],
|
|
});
|
|
|
|
expect(collection.getDocumentTree(parent.id)).toEqual({
|
|
...parent.toJSON(),
|
|
children: [document.toJSON()],
|
|
});
|
|
expect(collection.getDocumentTree(document.id)).toEqual(document.toJSON());
|
|
});
|
|
});
|
|
|
|
describe("#addDocumentToStructure", () => {
|
|
test("should add as last element without index", async () => {
|
|
const { collection } = await seed();
|
|
const id = uuidv4();
|
|
const newDocument = new Document({
|
|
id,
|
|
title: "New end node",
|
|
parentDocumentId: null,
|
|
});
|
|
await collection.addDocumentToStructure(newDocument);
|
|
expect(collection.documentStructure!.length).toBe(2);
|
|
expect(collection.documentStructure![1].id).toBe(id);
|
|
});
|
|
|
|
test("should add with an index", async () => {
|
|
const { collection } = await seed();
|
|
const id = uuidv4();
|
|
const newDocument = new Document({
|
|
id,
|
|
title: "New end node",
|
|
parentDocumentId: null,
|
|
});
|
|
await collection.addDocumentToStructure(newDocument, 1);
|
|
expect(collection.documentStructure!.length).toBe(2);
|
|
expect(collection.documentStructure![1].id).toBe(id);
|
|
});
|
|
|
|
test("should add as a child if with parent", async () => {
|
|
const { collection, document } = await seed();
|
|
const id = uuidv4();
|
|
const newDocument = new Document({
|
|
id,
|
|
title: "New end node",
|
|
parentDocumentId: document.id,
|
|
});
|
|
await collection.addDocumentToStructure(newDocument, 1);
|
|
expect(collection.documentStructure!.length).toBe(1);
|
|
expect(collection.documentStructure![0].id).toBe(document.id);
|
|
expect(collection.documentStructure![0].children.length).toBe(1);
|
|
expect(collection.documentStructure![0].children[0].id).toBe(id);
|
|
});
|
|
|
|
test("should add as a child if with parent with index", async () => {
|
|
const { collection, document } = await seed();
|
|
const newDocument = new Document({
|
|
id: uuidv4(),
|
|
title: "node",
|
|
parentDocumentId: document.id,
|
|
});
|
|
const id = uuidv4();
|
|
const secondDocument = new Document({
|
|
id,
|
|
title: "New start node",
|
|
parentDocumentId: document.id,
|
|
});
|
|
await collection.addDocumentToStructure(newDocument);
|
|
await collection.addDocumentToStructure(secondDocument, 0);
|
|
expect(collection.documentStructure!.length).toBe(1);
|
|
expect(collection.documentStructure![0].id).toBe(document.id);
|
|
expect(collection.documentStructure![0].children.length).toBe(2);
|
|
expect(collection.documentStructure![0].children[0].id).toBe(id);
|
|
});
|
|
describe("options: documentJson", () => {
|
|
test("should append supplied json over document's own", async () => {
|
|
const { collection } = await seed();
|
|
const id = uuidv4();
|
|
const newDocument = new Document({
|
|
id: uuidv4(),
|
|
title: "New end node",
|
|
parentDocumentId: null,
|
|
});
|
|
await collection.addDocumentToStructure(newDocument, undefined, {
|
|
documentJson: {
|
|
id,
|
|
title: "Parent",
|
|
url: "parent",
|
|
children: [
|
|
{
|
|
id,
|
|
title: "Totally fake",
|
|
children: [],
|
|
url: "totally-fake",
|
|
},
|
|
],
|
|
},
|
|
});
|
|
expect(collection.documentStructure![1].children.length).toBe(1);
|
|
expect(collection.documentStructure![1].children[0].id).toBe(id);
|
|
});
|
|
});
|
|
});
|
|
|
|
describe("#updateDocument", () => {
|
|
test("should update root document's data", async () => {
|
|
const { collection, document } = await seed();
|
|
document.title = "Updated title";
|
|
await document.save();
|
|
await collection.updateDocument(document);
|
|
expect(collection.documentStructure![0].title).toBe("Updated title");
|
|
});
|
|
|
|
test("should update child document's data", async () => {
|
|
const { collection, document } = await seed();
|
|
const newDocument = await Document.create({
|
|
parentDocumentId: document.id,
|
|
collectionId: collection.id,
|
|
teamId: collection.teamId,
|
|
userId: collection.createdById,
|
|
lastModifiedById: collection.createdById,
|
|
createdById: collection.createdById,
|
|
title: "Child document",
|
|
text: "content",
|
|
});
|
|
await collection.addDocumentToStructure(newDocument);
|
|
newDocument.title = "Updated title";
|
|
await newDocument.save();
|
|
await collection.updateDocument(newDocument);
|
|
const reloaded = await Collection.findByPk(collection.id);
|
|
expect(reloaded!.documentStructure![0].children[0].title).toBe(
|
|
"Updated title"
|
|
);
|
|
});
|
|
});
|
|
|
|
describe("#removeDocument", () => {
|
|
test("should save if removing", async () => {
|
|
const { collection, document } = await seed();
|
|
jest.spyOn(collection, "save");
|
|
await collection.deleteDocument(document);
|
|
expect(collection.save).toBeCalled();
|
|
});
|
|
|
|
test("should remove documents from root", async () => {
|
|
const { collection, document } = await seed();
|
|
await collection.deleteDocument(document);
|
|
expect(collection.documentStructure!.length).toBe(0);
|
|
// Verify that the document was removed
|
|
const collectionDocuments = await Document.findAndCountAll({
|
|
where: {
|
|
collectionId: collection.id,
|
|
},
|
|
});
|
|
expect(collectionDocuments.count).toBe(0);
|
|
});
|
|
|
|
test("should remove a document with child documents", async () => {
|
|
const { collection, document } = await seed();
|
|
// Add a child for testing
|
|
const newDocument = await Document.create({
|
|
parentDocumentId: document.id,
|
|
collectionId: collection.id,
|
|
teamId: collection.teamId,
|
|
userId: collection.createdById,
|
|
lastModifiedById: collection.createdById,
|
|
createdById: collection.createdById,
|
|
title: "Child document",
|
|
text: "content",
|
|
});
|
|
await collection.addDocumentToStructure(newDocument);
|
|
expect(collection.documentStructure![0].children.length).toBe(1);
|
|
// Remove the document
|
|
await collection.deleteDocument(document);
|
|
expect(collection.documentStructure!.length).toBe(0);
|
|
const collectionDocuments = await Document.findAndCountAll({
|
|
where: {
|
|
collectionId: collection.id,
|
|
},
|
|
});
|
|
expect(collectionDocuments.count).toBe(0);
|
|
});
|
|
|
|
test("should remove a child document", async () => {
|
|
const { collection, document } = await seed();
|
|
// Add a child for testing
|
|
const newDocument = await Document.create({
|
|
parentDocumentId: document.id,
|
|
collectionId: collection.id,
|
|
teamId: collection.teamId,
|
|
userId: collection.createdById,
|
|
lastModifiedById: collection.createdById,
|
|
createdById: collection.createdById,
|
|
publishedAt: new Date(),
|
|
title: "Child document",
|
|
text: "content",
|
|
});
|
|
await collection.addDocumentToStructure(newDocument);
|
|
expect(collection.documentStructure!.length).toBe(1);
|
|
expect(collection.documentStructure![0].children.length).toBe(1);
|
|
// Remove the document
|
|
await collection.deleteDocument(newDocument);
|
|
const reloaded = await Collection.findByPk(collection.id);
|
|
expect(reloaded!.documentStructure!.length).toBe(1);
|
|
expect(reloaded!.documentStructure![0].children.length).toBe(0);
|
|
const collectionDocuments = await Document.findAndCountAll({
|
|
where: {
|
|
collectionId: collection.id,
|
|
},
|
|
});
|
|
expect(collectionDocuments.count).toBe(1);
|
|
});
|
|
});
|
|
|
|
describe("#membershipUserIds", () => {
|
|
test("should return collection and group memberships", async () => {
|
|
const team = await buildTeam();
|
|
const teamId = team.id;
|
|
// Make 6 users
|
|
const users = await Promise.all(
|
|
Array(6)
|
|
.fill(undefined)
|
|
.map(() =>
|
|
buildUser({
|
|
teamId,
|
|
})
|
|
)
|
|
);
|
|
const collection = await buildCollection({
|
|
userId: users[0].id,
|
|
permission: null,
|
|
teamId,
|
|
});
|
|
const group1 = await buildGroup({
|
|
teamId,
|
|
});
|
|
const group2 = await buildGroup({
|
|
teamId,
|
|
});
|
|
const createdById = users[0].id;
|
|
await group1.$add("user", users[0], {
|
|
through: {
|
|
createdById,
|
|
},
|
|
});
|
|
await group1.$add("user", users[1], {
|
|
through: {
|
|
createdById,
|
|
},
|
|
});
|
|
await group2.$add("user", users[2], {
|
|
through: {
|
|
createdById,
|
|
},
|
|
});
|
|
await group2.$add("user", users[3], {
|
|
through: {
|
|
createdById,
|
|
},
|
|
});
|
|
await collection.$add("user", users[4], {
|
|
through: {
|
|
createdById,
|
|
},
|
|
});
|
|
await collection.$add("user", users[5], {
|
|
through: {
|
|
createdById,
|
|
},
|
|
});
|
|
await collection.$add("group", group1, {
|
|
through: {
|
|
createdById,
|
|
},
|
|
});
|
|
await collection.$add("group", group2, {
|
|
through: {
|
|
createdById,
|
|
},
|
|
});
|
|
const membershipUserIds = await Collection.membershipUserIds(collection.id);
|
|
expect(membershipUserIds.length).toBe(6);
|
|
});
|
|
});
|
|
|
|
describe("#findByPk", () => {
|
|
test("should return collection with collection Id", async () => {
|
|
const collection = await buildCollection();
|
|
const response = await Collection.findByPk(collection.id);
|
|
expect(response!.id).toBe(collection.id);
|
|
});
|
|
|
|
test("should return collection when urlId is present", async () => {
|
|
const collection = await buildCollection();
|
|
const id = `${slugify(collection.name)}-${collection.urlId}`;
|
|
const response = await Collection.findByPk(id);
|
|
expect(response!.id).toBe(collection.id);
|
|
});
|
|
|
|
test("should return collection when urlId is present, but missing slug", async () => {
|
|
const collection = await buildCollection();
|
|
const id = collection.urlId;
|
|
const response = await Collection.findByPk(id);
|
|
expect(response!.id).toBe(collection.id);
|
|
});
|
|
|
|
test("should return undefined when incorrect uuid type", async () => {
|
|
const collection = await buildCollection();
|
|
const response = await Collection.findByPk(collection.id + "-incorrect");
|
|
expect(response).toBe(undefined);
|
|
});
|
|
|
|
test("should return undefined when incorrect urlId length", async () => {
|
|
const collection = await buildCollection();
|
|
const id = `${slugify(collection.name)}-${collection.urlId}incorrect`;
|
|
const response = await Collection.findByPk(id);
|
|
expect(response).toBe(undefined);
|
|
});
|
|
|
|
test("should return null when no collection is found with uuid", async () => {
|
|
const response = await Collection.findByPk(
|
|
"a9e71a81-7342-4ea3-9889-9b9cc8f667da"
|
|
);
|
|
expect(response).toBe(null);
|
|
});
|
|
|
|
test("should return null when no collection is found with urlId", async () => {
|
|
const id = `${slugify("test collection")}-${randomstring.generate(15)}`;
|
|
const response = await Collection.findByPk(id);
|
|
expect(response).toBe(null);
|
|
});
|
|
});
|