chore: Move to Typescript (#2783)
This PR moves the entire project to Typescript. Due to the ~1000 ignores this will lead to a messy codebase for a while, but the churn is worth it – all of those ignore comments are places that were never type-safe previously. closes #1282
This commit is contained in:
802
server/models/Document.ts
Normal file
802
server/models/Document.ts
Normal file
@@ -0,0 +1,802 @@
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module '@tom... Remove this comment to see the full error message
|
||||
import removeMarkdown from "@tommoor/remove-markdown";
|
||||
import { compact, find, map, uniq } from "lodash";
|
||||
import randomstring from "randomstring";
|
||||
import Sequelize, { Transaction } from "sequelize";
|
||||
// @ts-expect-error ts-migrate(7016) FIXME: Could not find a declaration file for module 'slat... Remove this comment to see the full error message
|
||||
import MarkdownSerializer from "slate-md-serializer";
|
||||
import isUUID from "validator/lib/isUUID";
|
||||
import { MAX_TITLE_LENGTH } from "@shared/constants";
|
||||
import { DateFilter } from "@shared/types";
|
||||
import getTasks from "@shared/utils/getTasks";
|
||||
import parseTitle from "@shared/utils/parseTitle";
|
||||
import { SLUG_URL_REGEX } from "@shared/utils/routeHelpers";
|
||||
import unescape from "@shared/utils/unescape";
|
||||
import { Collection, User } from "@server/models";
|
||||
import slugify from "@server/utils/slugify";
|
||||
import { DataTypes, sequelize } from "../sequelize";
|
||||
import Revision from "./Revision";
|
||||
|
||||
const Op = Sequelize.Op;
|
||||
const serializer = new MarkdownSerializer();
|
||||
|
||||
export const DOCUMENT_VERSION = 2;
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type.
|
||||
const createUrlId = (doc) => {
|
||||
return (doc.urlId = doc.urlId || randomstring.generate(10));
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type.
|
||||
const beforeCreate = async (doc) => {
|
||||
if (doc.version === undefined) {
|
||||
doc.version = DOCUMENT_VERSION;
|
||||
}
|
||||
|
||||
return beforeSave(doc);
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'doc' implicitly has an 'any' type.
|
||||
const beforeSave = async (doc) => {
|
||||
const { emoji } = parseTitle(doc.text);
|
||||
// emoji in the title is split out for easier display
|
||||
doc.emoji = emoji;
|
||||
// ensure documents have a title
|
||||
doc.title = doc.title || "";
|
||||
|
||||
if (doc.previous("title") && doc.previous("title") !== doc.title) {
|
||||
if (!doc.previousTitles) doc.previousTitles = [];
|
||||
doc.previousTitles = uniq(doc.previousTitles.concat(doc.previous("title")));
|
||||
}
|
||||
|
||||
// add the current user as a collaborator on this doc
|
||||
if (!doc.collaboratorIds) doc.collaboratorIds = [];
|
||||
doc.collaboratorIds = uniq(doc.collaboratorIds.concat(doc.lastModifiedById));
|
||||
// increment revision
|
||||
doc.revisionCount += 1;
|
||||
return doc;
|
||||
};
|
||||
|
||||
const Document = sequelize.define(
|
||||
"document",
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
urlId: {
|
||||
type: DataTypes.STRING,
|
||||
primaryKey: true,
|
||||
},
|
||||
title: {
|
||||
type: DataTypes.STRING,
|
||||
validate: {
|
||||
len: {
|
||||
args: [0, MAX_TITLE_LENGTH],
|
||||
msg: `Document title must be less than ${MAX_TITLE_LENGTH} characters`,
|
||||
},
|
||||
},
|
||||
},
|
||||
previousTitles: DataTypes.ARRAY(DataTypes.STRING),
|
||||
version: DataTypes.SMALLINT,
|
||||
template: DataTypes.BOOLEAN,
|
||||
editorVersion: DataTypes.STRING,
|
||||
text: DataTypes.TEXT,
|
||||
state: DataTypes.BLOB,
|
||||
isWelcome: {
|
||||
type: DataTypes.BOOLEAN,
|
||||
defaultValue: false,
|
||||
},
|
||||
revisionCount: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 0,
|
||||
},
|
||||
archivedAt: DataTypes.DATE,
|
||||
publishedAt: DataTypes.DATE,
|
||||
parentDocumentId: DataTypes.UUID,
|
||||
collaboratorIds: DataTypes.ARRAY(DataTypes.UUID),
|
||||
},
|
||||
{
|
||||
paranoid: true,
|
||||
hooks: {
|
||||
beforeValidate: createUrlId,
|
||||
beforeCreate: beforeCreate,
|
||||
beforeUpdate: beforeSave,
|
||||
},
|
||||
getterMethods: {
|
||||
url: function () {
|
||||
if (!this.title) return `/doc/untitled-${this.urlId}`;
|
||||
const slugifiedTitle = slugify(this.title);
|
||||
return `/doc/${slugifiedTitle}-${this.urlId}`;
|
||||
},
|
||||
tasks: function () {
|
||||
return getTasks(this.text || "");
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
// Class methods
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'models' implicitly has an 'any' type.
|
||||
Document.associate = (models) => {
|
||||
Document.belongsTo(models.Collection, {
|
||||
as: "collection",
|
||||
foreignKey: "collectionId",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Document.belongsTo(models.Team, {
|
||||
as: "team",
|
||||
foreignKey: "teamId",
|
||||
});
|
||||
Document.belongsTo(models.Document, {
|
||||
as: "document",
|
||||
foreignKey: "templateId",
|
||||
});
|
||||
Document.belongsTo(models.User, {
|
||||
as: "createdBy",
|
||||
foreignKey: "createdById",
|
||||
});
|
||||
Document.belongsTo(models.User, {
|
||||
as: "updatedBy",
|
||||
foreignKey: "lastModifiedById",
|
||||
});
|
||||
Document.belongsTo(models.User, {
|
||||
as: "pinnedBy",
|
||||
foreignKey: "pinnedById",
|
||||
});
|
||||
Document.hasMany(models.Revision, {
|
||||
as: "revisions",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Document.hasMany(models.Backlink, {
|
||||
as: "backlinks",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Document.hasMany(models.Star, {
|
||||
as: "starred",
|
||||
onDelete: "cascade",
|
||||
});
|
||||
Document.hasMany(models.View, {
|
||||
as: "views",
|
||||
});
|
||||
Document.addScope("defaultScope", {
|
||||
include: [
|
||||
{
|
||||
model: models.User,
|
||||
as: "createdBy",
|
||||
paranoid: false,
|
||||
},
|
||||
{
|
||||
model: models.User,
|
||||
as: "updatedBy",
|
||||
paranoid: false,
|
||||
},
|
||||
],
|
||||
where: {
|
||||
publishedAt: {
|
||||
[Op.ne]: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
|
||||
Document.addScope("withCollection", (userId, paranoid = true) => {
|
||||
if (userId) {
|
||||
return {
|
||||
include: [
|
||||
{
|
||||
model: models.Collection.scope({
|
||||
method: ["withMembership", userId],
|
||||
}),
|
||||
as: "collection",
|
||||
paranoid,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
include: [
|
||||
{
|
||||
model: models.Collection,
|
||||
as: "collection",
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
Document.addScope("withUnpublished", {
|
||||
include: [
|
||||
{
|
||||
model: models.User,
|
||||
as: "createdBy",
|
||||
paranoid: false,
|
||||
},
|
||||
{
|
||||
model: models.User,
|
||||
as: "updatedBy",
|
||||
paranoid: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
|
||||
Document.addScope("withViews", (userId) => {
|
||||
if (!userId) return {};
|
||||
return {
|
||||
include: [
|
||||
{
|
||||
model: models.View,
|
||||
as: "views",
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
required: false,
|
||||
separate: true,
|
||||
},
|
||||
],
|
||||
};
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
|
||||
Document.addScope("withStarred", (userId) => ({
|
||||
include: [
|
||||
{
|
||||
model: models.Star,
|
||||
as: "starred",
|
||||
where: {
|
||||
userId,
|
||||
},
|
||||
required: false,
|
||||
separate: true,
|
||||
},
|
||||
],
|
||||
}));
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'id' implicitly has an 'any' type.
|
||||
Document.findByPk = async function (id, options = {}) {
|
||||
// allow default preloading of collection membership if `userId` is passed in find options
|
||||
// almost every endpoint needs the collection membership to determine policy permissions.
|
||||
const scope = this.scope(
|
||||
"withUnpublished",
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'userId' does not exist on type '{}'.
|
||||
method: ["withCollection", options.userId, options.paranoid],
|
||||
},
|
||||
{
|
||||
// @ts-expect-error ts-migrate(2339) FIXME: Property 'userId' does not exist on type '{}'.
|
||||
method: ["withViews", options.userId],
|
||||
}
|
||||
);
|
||||
|
||||
if (isUUID(id)) {
|
||||
return scope.findOne({
|
||||
where: {
|
||||
id,
|
||||
},
|
||||
...options,
|
||||
});
|
||||
} else if (id.match(SLUG_URL_REGEX)) {
|
||||
return scope.findOne({
|
||||
where: {
|
||||
urlId: id.match(SLUG_URL_REGEX)[1],
|
||||
},
|
||||
...options,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
type SearchResponse = {
|
||||
results: {
|
||||
ranking: number;
|
||||
context: string;
|
||||
document: Document;
|
||||
}[];
|
||||
totalCount: number;
|
||||
};
|
||||
type SearchOptions = {
|
||||
limit?: number;
|
||||
offset?: number;
|
||||
collectionId?: string;
|
||||
dateFilter?: DateFilter;
|
||||
collaboratorIds?: string[];
|
||||
includeArchived?: boolean;
|
||||
includeDrafts?: boolean;
|
||||
};
|
||||
|
||||
function escape(query: string): string {
|
||||
// replace "\" with escaped "\\" because sequelize.escape doesn't do it
|
||||
// https://github.com/sequelize/sequelize/issues/2950
|
||||
return sequelize.escape(query).replace(/\\/g, "\\\\");
|
||||
}
|
||||
|
||||
Document.searchForTeam = async (
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'team' implicitly has an 'any' type.
|
||||
team,
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type.
|
||||
query,
|
||||
options: SearchOptions = {}
|
||||
): Promise<SearchResponse> => {
|
||||
const limit = options.limit || 15;
|
||||
const offset = options.offset || 0;
|
||||
const wildcardQuery = `${escape(query)}:*`;
|
||||
const collectionIds = await team.collectionIds();
|
||||
|
||||
// If the team has access no public collections then shortcircuit the rest of this
|
||||
if (!collectionIds.length) {
|
||||
return {
|
||||
results: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
// Build the SQL query to get documentIds, ranking, and search term context
|
||||
const whereClause = `
|
||||
"searchVector" @@ to_tsquery('english', :query) AND
|
||||
"teamId" = :teamId AND
|
||||
"collectionId" IN(:collectionIds) AND
|
||||
"deletedAt" IS NULL AND
|
||||
"publishedAt" IS NOT NULL
|
||||
`;
|
||||
const selectSql = `
|
||||
SELECT
|
||||
id,
|
||||
ts_rank(documents."searchVector", to_tsquery('english', :query)) as "searchRanking",
|
||||
ts_headline('english', "text", to_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext"
|
||||
FROM documents
|
||||
WHERE ${whereClause}
|
||||
ORDER BY
|
||||
"searchRanking" DESC,
|
||||
"updatedAt" DESC
|
||||
LIMIT :limit
|
||||
OFFSET :offset;
|
||||
`;
|
||||
const countSql = `
|
||||
SELECT COUNT(id)
|
||||
FROM documents
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
const queryReplacements = {
|
||||
teamId: team.id,
|
||||
query: wildcardQuery,
|
||||
collectionIds,
|
||||
};
|
||||
const resultsQuery = sequelize.query(selectSql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { ...queryReplacements, limit, offset },
|
||||
});
|
||||
const countQuery = sequelize.query(countSql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: queryReplacements,
|
||||
});
|
||||
const [results, [{ count }]] = await Promise.all([resultsQuery, countQuery]);
|
||||
// Final query to get associated document data
|
||||
const documents = await Document.findAll({
|
||||
where: {
|
||||
id: map(results, "id"),
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: Collection,
|
||||
as: "collection",
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: "createdBy",
|
||||
paranoid: false,
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: "updatedBy",
|
||||
paranoid: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
results: map(results, (result) => ({
|
||||
ranking: result.searchRanking,
|
||||
context: removeMarkdown(unescape(result.searchContext), {
|
||||
stripHTML: false,
|
||||
}),
|
||||
document: find(documents, {
|
||||
id: result.id,
|
||||
}),
|
||||
})),
|
||||
totalCount: count,
|
||||
};
|
||||
};
|
||||
|
||||
Document.searchForUser = async (
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'user' implicitly has an 'any' type.
|
||||
user,
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'query' implicitly has an 'any' type.
|
||||
query,
|
||||
options: SearchOptions = {}
|
||||
): Promise<SearchResponse> => {
|
||||
const limit = options.limit || 15;
|
||||
const offset = options.offset || 0;
|
||||
const wildcardQuery = `${escape(query)}:*`;
|
||||
// Ensure we're filtering by the users accessible collections. If
|
||||
// collectionId is passed as an option it is assumed that the authorization
|
||||
// has already been done in the router
|
||||
let collectionIds;
|
||||
|
||||
if (options.collectionId) {
|
||||
collectionIds = [options.collectionId];
|
||||
} else {
|
||||
collectionIds = await user.collectionIds();
|
||||
}
|
||||
|
||||
// If the user has access to no collections then shortcircuit the rest of this
|
||||
if (!collectionIds.length) {
|
||||
return {
|
||||
results: [],
|
||||
totalCount: 0,
|
||||
};
|
||||
}
|
||||
|
||||
let dateFilter;
|
||||
|
||||
if (options.dateFilter) {
|
||||
dateFilter = `1 ${options.dateFilter}`;
|
||||
}
|
||||
|
||||
// Build the SQL query to get documentIds, ranking, and search term context
|
||||
const whereClause = `
|
||||
"searchVector" @@ to_tsquery('english', :query) AND
|
||||
"teamId" = :teamId AND
|
||||
"collectionId" IN(:collectionIds) AND
|
||||
${
|
||||
options.dateFilter ? '"updatedAt" > now() - interval :dateFilter AND' : ""
|
||||
}
|
||||
${
|
||||
options.collaboratorIds
|
||||
? '"collaboratorIds" @> ARRAY[:collaboratorIds]::uuid[] AND'
|
||||
: ""
|
||||
}
|
||||
${options.includeArchived ? "" : '"archivedAt" IS NULL AND'}
|
||||
"deletedAt" IS NULL AND
|
||||
${
|
||||
options.includeDrafts
|
||||
? '("publishedAt" IS NOT NULL OR "createdById" = :userId)'
|
||||
: '"publishedAt" IS NOT NULL'
|
||||
}
|
||||
`;
|
||||
const selectSql = `
|
||||
SELECT
|
||||
id,
|
||||
ts_rank(documents."searchVector", to_tsquery('english', :query)) as "searchRanking",
|
||||
ts_headline('english', "text", to_tsquery('english', :query), 'MaxFragments=1, MinWords=20, MaxWords=30') as "searchContext"
|
||||
FROM documents
|
||||
WHERE ${whereClause}
|
||||
ORDER BY
|
||||
"searchRanking" DESC,
|
||||
"updatedAt" DESC
|
||||
LIMIT :limit
|
||||
OFFSET :offset;
|
||||
`;
|
||||
const countSql = `
|
||||
SELECT COUNT(id)
|
||||
FROM documents
|
||||
WHERE ${whereClause}
|
||||
`;
|
||||
const queryReplacements = {
|
||||
teamId: user.teamId,
|
||||
userId: user.id,
|
||||
collaboratorIds: options.collaboratorIds,
|
||||
query: wildcardQuery,
|
||||
collectionIds,
|
||||
dateFilter,
|
||||
};
|
||||
const resultsQuery = sequelize.query(selectSql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: { ...queryReplacements, limit, offset },
|
||||
});
|
||||
const countQuery = sequelize.query(countSql, {
|
||||
type: sequelize.QueryTypes.SELECT,
|
||||
replacements: queryReplacements,
|
||||
});
|
||||
const [results, [{ count }]] = await Promise.all([resultsQuery, countQuery]);
|
||||
// Final query to get associated document data
|
||||
const documents = await Document.scope(
|
||||
{
|
||||
method: ["withViews", user.id],
|
||||
},
|
||||
{
|
||||
method: ["withCollection", user.id],
|
||||
}
|
||||
).findAll({
|
||||
where: {
|
||||
id: map(results, "id"),
|
||||
},
|
||||
include: [
|
||||
{
|
||||
model: User,
|
||||
as: "createdBy",
|
||||
paranoid: false,
|
||||
},
|
||||
{
|
||||
model: User,
|
||||
as: "updatedBy",
|
||||
paranoid: false,
|
||||
},
|
||||
],
|
||||
});
|
||||
return {
|
||||
results: map(results, (result) => ({
|
||||
ranking: result.searchRanking,
|
||||
context: removeMarkdown(unescape(result.searchContext), {
|
||||
stripHTML: false,
|
||||
}),
|
||||
document: find(documents, {
|
||||
id: result.id,
|
||||
}),
|
||||
})),
|
||||
totalCount: count,
|
||||
};
|
||||
};
|
||||
|
||||
// Hooks
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
|
||||
Document.addHook("beforeSave", async (model) => {
|
||||
if (!model.publishedAt || model.template) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = await Collection.findByPk(model.collectionId);
|
||||
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
await collection.updateDocument(model);
|
||||
model.collection = collection;
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'model' implicitly has an 'any' type.
|
||||
Document.addHook("afterCreate", async (model) => {
|
||||
if (!model.publishedAt || model.template) {
|
||||
return;
|
||||
}
|
||||
|
||||
const collection = await Collection.findByPk(model.collectionId);
|
||||
|
||||
if (!collection) {
|
||||
return;
|
||||
}
|
||||
|
||||
await collection.addDocumentToStructure(model, 0);
|
||||
model.collection = collection;
|
||||
return model;
|
||||
});
|
||||
|
||||
// Instance methods
|
||||
Document.prototype.toMarkdown = function () {
|
||||
const text = unescape(this.text);
|
||||
|
||||
if (this.version) {
|
||||
return `# ${this.title}\n\n${text}`;
|
||||
}
|
||||
|
||||
return text;
|
||||
};
|
||||
|
||||
Document.prototype.migrateVersion = function () {
|
||||
let migrated = false;
|
||||
|
||||
// migrate from document version 0 -> 1
|
||||
if (!this.version) {
|
||||
// removing the title from the document text attribute
|
||||
this.text = this.text.replace(/^#\s(.*)\n/, "");
|
||||
this.version = 1;
|
||||
migrated = true;
|
||||
}
|
||||
|
||||
// migrate from document version 1 -> 2
|
||||
if (this.version === 1) {
|
||||
const nodes = serializer.deserialize(this.text);
|
||||
this.text = serializer.serialize(nodes, {
|
||||
version: 2,
|
||||
});
|
||||
this.version = 2;
|
||||
migrated = true;
|
||||
}
|
||||
|
||||
if (migrated) {
|
||||
return this.save({
|
||||
silent: true,
|
||||
hooks: false,
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
// Note: This method marks the document and it's children as deleted
|
||||
// in the database, it does not permanently delete them OR remove
|
||||
// from the collection structure.
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
|
||||
Document.prototype.deleteWithChildren = async function (options) {
|
||||
// Helper to destroy all child documents for a document
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'documentId' implicitly has an 'any' typ... Remove this comment to see the full error message
|
||||
const loopChildren = async (documentId, opts) => {
|
||||
const childDocuments = await Document.findAll({
|
||||
where: {
|
||||
parentDocumentId: documentId,
|
||||
},
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type.
|
||||
childDocuments.forEach(async (child) => {
|
||||
await loopChildren(child.id, opts);
|
||||
await child.destroy(opts);
|
||||
});
|
||||
};
|
||||
|
||||
await loopChildren(this.id, options);
|
||||
await this.destroy(options);
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
|
||||
Document.prototype.archiveWithChildren = async function (userId, options) {
|
||||
const archivedAt = new Date();
|
||||
|
||||
// Helper to archive all child documents for a document
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'parentDocumentId' implicitly has an 'an... Remove this comment to see the full error message
|
||||
const archiveChildren = async (parentDocumentId) => {
|
||||
const childDocuments = await Document.findAll({
|
||||
where: {
|
||||
parentDocumentId,
|
||||
},
|
||||
});
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'child' implicitly has an 'any' type.
|
||||
childDocuments.forEach(async (child) => {
|
||||
await archiveChildren(child.id);
|
||||
child.archivedAt = archivedAt;
|
||||
child.lastModifiedById = userId;
|
||||
await child.save(options);
|
||||
});
|
||||
};
|
||||
|
||||
await archiveChildren(this.id);
|
||||
this.archivedAt = archivedAt;
|
||||
this.lastModifiedById = userId;
|
||||
return this.save(options);
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
|
||||
Document.prototype.publish = async function (userId: string, options) {
|
||||
if (this.publishedAt) return this.save(options);
|
||||
|
||||
if (!this.template) {
|
||||
const collection = await Collection.findByPk(this.collectionId);
|
||||
await collection.addDocumentToStructure(this, 0);
|
||||
}
|
||||
|
||||
this.lastModifiedById = userId;
|
||||
this.publishedAt = new Date();
|
||||
await this.save(options);
|
||||
return this;
|
||||
};
|
||||
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'options' implicitly has an 'any' type.
|
||||
Document.prototype.unpublish = async function (userId: string, options) {
|
||||
if (!this.publishedAt) return this;
|
||||
const collection = await this.getCollection();
|
||||
await collection.removeDocumentInStructure(this);
|
||||
// unpublishing a document converts the "ownership" to yourself, so that it
|
||||
// can appear in your drafts rather than the original creators
|
||||
this.userId = userId;
|
||||
this.lastModifiedById = userId;
|
||||
this.publishedAt = null;
|
||||
await this.save(options);
|
||||
return this;
|
||||
};
|
||||
|
||||
// Moves a document from being visible to the team within a collection
|
||||
// to the archived area, where it can be subsequently restored.
|
||||
// @ts-expect-error ts-migrate(7006) FIXME: Parameter 'userId' implicitly has an 'any' type.
|
||||
Document.prototype.archive = async function (userId) {
|
||||
// archive any children and remove from the document structure
|
||||
const collection = await this.getCollection();
|
||||
await collection.removeDocumentInStructure(this);
|
||||
this.collection = collection;
|
||||
await this.archiveWithChildren(userId);
|
||||
return this;
|
||||
};
|
||||
|
||||
// Restore an archived document back to being visible to the team
|
||||
Document.prototype.unarchive = async function (userId: string) {
|
||||
const collection = await this.getCollection();
|
||||
|
||||
// check to see if the documents parent hasn't been archived also
|
||||
// If it has then restore the document to the collection root.
|
||||
if (this.parentDocumentId) {
|
||||
const parent = await Document.findOne({
|
||||
where: {
|
||||
id: this.parentDocumentId,
|
||||
archivedAt: {
|
||||
[Op.eq]: null,
|
||||
},
|
||||
},
|
||||
});
|
||||
if (!parent) this.parentDocumentId = null;
|
||||
}
|
||||
|
||||
if (!this.template) {
|
||||
await collection.addDocumentToStructure(this);
|
||||
this.collection = collection;
|
||||
}
|
||||
|
||||
if (this.deletedAt) {
|
||||
await this.restore();
|
||||
}
|
||||
|
||||
this.archivedAt = null;
|
||||
this.lastModifiedById = userId;
|
||||
await this.save();
|
||||
return this;
|
||||
};
|
||||
|
||||
// Delete a document, archived or otherwise.
|
||||
Document.prototype.delete = function (userId: string) {
|
||||
return sequelize.transaction(
|
||||
async (transaction: Transaction): Promise<Document> => {
|
||||
if (!this.archivedAt && !this.template) {
|
||||
// delete any children and remove from the document structure
|
||||
const collection = await this.getCollection({
|
||||
transaction,
|
||||
});
|
||||
if (collection)
|
||||
await collection.deleteDocument(this, {
|
||||
transaction,
|
||||
});
|
||||
} else {
|
||||
await this.destroy({
|
||||
transaction,
|
||||
});
|
||||
}
|
||||
|
||||
await Revision.destroy({
|
||||
where: {
|
||||
documentId: this.id,
|
||||
},
|
||||
transaction,
|
||||
});
|
||||
await this.update(
|
||||
{
|
||||
lastModifiedById: userId,
|
||||
},
|
||||
{
|
||||
transaction,
|
||||
}
|
||||
);
|
||||
return this;
|
||||
}
|
||||
);
|
||||
};
|
||||
|
||||
Document.prototype.getTimestamp = function () {
|
||||
return Math.round(new Date(this.updatedAt).getTime() / 1000);
|
||||
};
|
||||
|
||||
Document.prototype.getSummary = function () {
|
||||
const plain = removeMarkdown(unescape(this.text), {
|
||||
stripHTML: false,
|
||||
});
|
||||
const lines = compact(plain.split("\n"));
|
||||
const notEmpty = lines.length >= 1;
|
||||
|
||||
if (this.version) {
|
||||
return notEmpty ? lines[0] : "";
|
||||
}
|
||||
|
||||
return notEmpty ? lines[1] : "";
|
||||
};
|
||||
|
||||
Document.prototype.toJSON = function () {
|
||||
// Warning: only use for new documents as order of children is
|
||||
// handled in the collection's documentStructure
|
||||
return {
|
||||
id: this.id,
|
||||
title: this.title,
|
||||
url: this.url,
|
||||
children: [],
|
||||
};
|
||||
};
|
||||
|
||||
export default Document;
|
||||
Reference in New Issue
Block a user