This repository has been archived on 2022-08-14. You can view files and clone it, but cannot push or open issues or pull requests.
outline/server/models/Document.js

248 lines
5.9 KiB
JavaScript
Raw Normal View History

// @flow
2016-05-26 05:38:45 +00:00
import slug from 'slug';
import _ from 'lodash';
2016-05-26 05:38:45 +00:00
import randomstring from 'randomstring';
import MarkdownSerializer from 'slate-md-serializer';
import Plain from 'slate-plain-serializer';
import isUUID from 'validator/lib/isUUID';
2017-04-27 04:47:03 +00:00
import { DataTypes, sequelize } from '../sequelize';
import events from '../events';
2017-11-01 04:55:46 +00:00
import parseTitle from '../../shared/utils/parseTitle';
2016-06-26 18:23:03 +00:00
import Revision from './Revision';
2016-04-29 05:25:37 +00:00
const Markdown = new MarkdownSerializer();
const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/;
2017-07-09 18:26:17 +00:00
// $FlowIssue invalid flow-typed
slug.defaults.mode = 'rfc3986';
const slugify = text =>
slug(text, {
remove: /[.]/g,
});
2016-05-26 05:38:45 +00:00
const createRevision = doc => {
2016-08-12 13:36:48 +00:00
// Create revision of the current (latest)
return Revision.create({
2016-08-12 13:36:48 +00:00
title: doc.title,
text: doc.text,
userId: doc.lastModifiedById,
documentId: doc.id,
});
};
const createUrlId = doc => {
return (doc.urlId = doc.urlId || randomstring.generate(10));
};
const beforeSave = async doc => {
const { emoji } = parseTitle(doc.text);
doc.emoji = emoji;
doc.revisionCount += 1;
// Collaborators
2016-08-18 21:42:53 +00:00
let ids = [];
// Only get previous user IDs if the document already exists
if (doc.id) {
ids = await Revision.findAll({
attributes: [[DataTypes.literal('DISTINCT "userId"'), 'userId']],
where: {
documentId: doc.id,
},
}).map(rev => rev.userId);
}
// We'll add the current user as revision hasn't been generated yet
ids.push(doc.lastModifiedById);
doc.collaboratorIds = _.uniq(ids);
2016-06-26 18:23:03 +00:00
return doc;
};
2017-04-27 04:47:03 +00:00
const Document = sequelize.define(
'document',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
urlId: { type: DataTypes.STRING, primaryKey: true },
private: { type: DataTypes.BOOLEAN, defaultValue: true },
title: DataTypes.STRING,
text: DataTypes.TEXT,
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
parentDocumentId: DataTypes.UUID,
createdById: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
},
},
2017-04-27 04:47:03 +00:00
lastModifiedById: {
type: DataTypes.UUID,
allowNull: false,
references: {
model: 'users',
},
},
2017-04-27 04:47:03 +00:00
collaboratorIds: DataTypes.ARRAY(DataTypes.UUID),
2016-06-26 18:23:03 +00:00
},
2017-04-27 04:47:03 +00:00
{
paranoid: true,
hooks: {
beforeValidate: createUrlId,
beforeCreate: beforeSave,
beforeUpdate: beforeSave,
afterCreate: createRevision,
afterUpdate: createRevision,
2016-05-26 05:38:45 +00:00
},
}
);
// Class methods
Document.associate = models => {
Document.belongsTo(models.Collection, {
as: 'collection',
foreignKey: 'atlasId',
onDelete: 'cascade',
});
Document.belongsTo(models.User, {
as: 'createdBy',
foreignKey: 'createdById',
});
Document.belongsTo(models.User, {
as: 'updatedBy',
foreignKey: 'lastModifiedById',
});
Document.hasMany(models.Revision, {
as: 'revisions',
onDelete: 'cascade',
});
Document.hasMany(models.Star, {
as: 'starred',
});
Document.hasMany(models.View, {
as: 'views',
});
Document.addScope(
'defaultScope',
{
include: [
{ model: models.Collection, as: 'collection' },
{ model: models.User, as: 'createdBy' },
{ model: models.User, as: 'updatedBy' },
],
2016-06-21 06:12:56 +00:00
},
{ override: true }
);
Document.addScope('withViews', userId => ({
include: [
{ model: models.View, as: 'views', where: { userId }, required: false },
],
}));
2017-07-15 23:08:12 +00:00
Document.addScope('withStarred', userId => ({
include: [
{ model: models.Star, as: 'starred', where: { userId }, required: false },
],
}));
};
Document.findById = async id => {
if (isUUID(id)) {
return Document.findOne({
where: { id },
});
} else if (id.match(URL_REGEX)) {
return Document.findOne({
where: {
urlId: id.match(URL_REGEX)[1],
},
});
}
};
2016-08-23 06:37:01 +00:00
2017-12-03 19:04:17 +00:00
Document.searchForUser = async (
user,
query,
options = {}
): Promise<Document[]> => {
const limit = options.limit || 15;
const offset = options.offset || 0;
const sql = `
2017-12-03 19:04:17 +00:00
SELECT *, ts_rank(documents."searchVector", plainto_tsquery('english', :query)) as "searchRanking" FROM documents
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
"teamId" = '${user.teamId}'::uuid AND
"deletedAt" IS NULL
2017-12-03 19:04:17 +00:00
ORDER BY "searchRanking" DESC
LIMIT :limit OFFSET :offset;
`;
2016-08-23 06:37:01 +00:00
const ids = await sequelize
.query(sql, {
replacements: {
query,
limit,
offset,
},
model: Document,
})
.map(document => document.id);
2017-12-03 19:04:17 +00:00
// Second query to get views for the data
const withViewsScope = { method: ['withViews', user.id] };
2017-12-03 19:04:17 +00:00
const documents = await Document.scope(
'defaultScope',
withViewsScope
).findAll({
where: { id: ids },
});
2017-12-03 19:04:17 +00:00
// Order the documents in the same order as the first query
return _.sortBy(documents, doc => ids.indexOf(doc.id));
};
// Hooks
Document.addHook('afterCreate', model =>
events.add({ name: 'documents.create', model })
);
Document.addHook('afterDestroy', model =>
events.add({ name: 'documents.delete', model })
);
Document.addHook('afterUpdate', model =>
events.add({ name: 'documents.update', model })
);
// Instance methods
Document.prototype.getSummary = function() {
const value = Markdown.deserialize(this.text);
const plain = Plain.serialize(value);
const lines = _.compact(plain.split('\n'));
return lines.length >= 1 ? lines[1] : '';
};
Document.prototype.getUrl = function() {
const slugifiedTitle = slugify(this.title);
return `/doc/${slugifiedTitle}-${this.urlId}`;
};
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.getUrl(),
children: [],
};
};
2016-08-23 06:37:01 +00:00
2016-05-20 03:46:34 +00:00
export default Document;