Document Viewers (#79)
* Recording document views * Add 'views' to document response * Basic displaying of document views, probably want it more sublte than this? But hey, lets get it in there * Bigly improves. RESTful > RPC * Display of who's viewed doc * Add Popover, add Scrollable, move views store * Working server tests 💁 * Document Stars (#81) * Added: Starred documents * UI is dumb but functionality works * Star now displayed inline in title * Optimistic rendering * Documents Endpoints (#85) * More seeds, documents.list endpoint * Upgrade deprecated middleware * document.viewers, specs * Add documents.starred Add request specs for star / unstar endpoints * Basic /starred page * Remove comment * Fixed double layout
This commit is contained in:
@@ -1,8 +1,8 @@
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import randomstring from 'randomstring';
|
||||
|
||||
const Team = sequelize.define(
|
||||
'team',
|
||||
const ApiKey = sequelize.define(
|
||||
'apiKeys',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -30,4 +30,4 @@ const Team = sequelize.define(
|
||||
}
|
||||
);
|
||||
|
||||
export default Team;
|
||||
export default ApiKey;
|
||||
|
||||
@@ -2,8 +2,8 @@
|
||||
import slug from 'slug';
|
||||
import randomstring from 'randomstring';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import _ from 'lodash';
|
||||
import Document from './Document';
|
||||
import _ from 'lodash';
|
||||
|
||||
slug.defaults.mode = 'rfc3986';
|
||||
|
||||
@@ -54,6 +54,14 @@ const Collection = sequelize.define(
|
||||
await collection.save();
|
||||
},
|
||||
},
|
||||
classMethods: {
|
||||
associate: models => {
|
||||
Collection.hasMany(models.Document, {
|
||||
as: 'documents',
|
||||
foreignKey: 'atlasId',
|
||||
});
|
||||
},
|
||||
},
|
||||
instanceMethods: {
|
||||
getUrl() {
|
||||
// const slugifiedName = slug(this.name);
|
||||
@@ -173,6 +181,4 @@ const Collection = sequelize.define(
|
||||
}
|
||||
);
|
||||
|
||||
Collection.hasMany(Document, { as: 'documents', foreignKey: 'atlasId' });
|
||||
|
||||
export default Collection;
|
||||
|
||||
@@ -2,21 +2,24 @@
|
||||
import slug from 'slug';
|
||||
import _ from 'lodash';
|
||||
import randomstring from 'randomstring';
|
||||
|
||||
import isUUID from 'validator/lib/isUUID';
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import { convertToMarkdown } from '../../frontend/utils/markdown';
|
||||
import { truncateMarkdown } from '../utils/truncate';
|
||||
import User from './User';
|
||||
import Revision from './Revision';
|
||||
|
||||
const URL_REGEX = /^[a-zA-Z0-9-]*-([a-zA-Z0-9]{10,15})$/;
|
||||
|
||||
slug.defaults.mode = 'rfc3986';
|
||||
const slugify = text =>
|
||||
slug(text, {
|
||||
remove: /[.]/g,
|
||||
});
|
||||
|
||||
const createRevision = async doc => {
|
||||
const createRevision = doc => {
|
||||
// Create revision of the current (latest)
|
||||
await Revision.create({
|
||||
return Revision.create({
|
||||
title: doc.title,
|
||||
text: doc.text,
|
||||
html: doc.html,
|
||||
@@ -26,10 +29,13 @@ const createRevision = async doc => {
|
||||
});
|
||||
};
|
||||
|
||||
const documentBeforeSave = async doc => {
|
||||
const createUrlId = doc => {
|
||||
return (doc.urlId = doc.urlId || randomstring.generate(10));
|
||||
};
|
||||
|
||||
const beforeSave = async doc => {
|
||||
doc.html = convertToMarkdown(doc.text);
|
||||
doc.preview = truncateMarkdown(doc.text, 160);
|
||||
|
||||
doc.revisionCount += 1;
|
||||
|
||||
// Collaborators
|
||||
@@ -65,7 +71,6 @@ const Document = sequelize.define(
|
||||
html: DataTypes.TEXT,
|
||||
preview: DataTypes.TEXT,
|
||||
revisionCount: { type: DataTypes.INTEGER, defaultValue: 0 },
|
||||
|
||||
parentDocumentId: DataTypes.UUID,
|
||||
createdById: {
|
||||
type: DataTypes.UUID,
|
||||
@@ -86,13 +91,11 @@ const Document = sequelize.define(
|
||||
{
|
||||
paranoid: true,
|
||||
hooks: {
|
||||
beforeValidate: doc => {
|
||||
doc.urlId = doc.urlId || randomstring.generate(10);
|
||||
},
|
||||
beforeCreate: documentBeforeSave,
|
||||
beforeUpdate: documentBeforeSave,
|
||||
afterCreate: async doc => await createRevision(doc),
|
||||
afterUpdate: async doc => await createRevision(doc),
|
||||
beforeValidate: createUrlId,
|
||||
beforeCreate: beforeSave,
|
||||
beforeUpdate: beforeSave,
|
||||
afterCreate: createRevision,
|
||||
afterUpdate: createRevision,
|
||||
},
|
||||
instanceMethods: {
|
||||
getUrl() {
|
||||
@@ -110,34 +113,47 @@ const Document = sequelize.define(
|
||||
};
|
||||
},
|
||||
},
|
||||
classMethods: {
|
||||
associate: models => {
|
||||
Document.belongsTo(models.User);
|
||||
},
|
||||
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],
|
||||
},
|
||||
});
|
||||
}
|
||||
},
|
||||
searchForUser: (user, query, options = {}) => {
|
||||
const limit = options.limit || 15;
|
||||
const offset = options.offset || 0;
|
||||
|
||||
const sql = `
|
||||
SELECT * FROM documents
|
||||
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
|
||||
"teamId" = '${user.teamId}'::uuid AND
|
||||
"deletedAt" IS NULL
|
||||
ORDER BY ts_rank(documents."searchVector", plainto_tsquery('english', :query)) DESC
|
||||
LIMIT :limit OFFSET :offset;
|
||||
`;
|
||||
|
||||
return sequelize.query(sql, {
|
||||
replacements: {
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
model: Document,
|
||||
});
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
Document.belongsTo(User);
|
||||
|
||||
Document.searchForUser = async (user, query, options = {}) => {
|
||||
const limit = options.limit || 15;
|
||||
const offset = options.offset || 0;
|
||||
|
||||
const sql = `
|
||||
SELECT * FROM documents
|
||||
WHERE "searchVector" @@ plainto_tsquery('english', :query) AND
|
||||
"teamId" = '${user.teamId}'::uuid AND
|
||||
"deletedAt" IS NULL
|
||||
ORDER BY ts_rank(documents."searchVector", plainto_tsquery('english', :query)) DESC
|
||||
LIMIT :limit OFFSET :offset;
|
||||
`;
|
||||
|
||||
const documents = await sequelize.query(sql, {
|
||||
replacements: {
|
||||
query,
|
||||
limit,
|
||||
offset,
|
||||
},
|
||||
model: Document,
|
||||
});
|
||||
|
||||
return documents;
|
||||
};
|
||||
|
||||
export default Document;
|
||||
|
||||
23
server/models/Star.js
Normal file
23
server/models/Star.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
|
||||
const Star = sequelize.define(
|
||||
'star',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
},
|
||||
{
|
||||
classMethods: {
|
||||
associate: models => {
|
||||
Star.belongsTo(models.Document);
|
||||
Star.belongsTo(models.User);
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default Star;
|
||||
@@ -1,7 +1,5 @@
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
import Collection from './Collection';
|
||||
import Document from './Document';
|
||||
import User from './User';
|
||||
|
||||
const Team = sequelize.define(
|
||||
'team',
|
||||
@@ -16,6 +14,13 @@ const Team = sequelize.define(
|
||||
slackData: DataTypes.JSONB,
|
||||
},
|
||||
{
|
||||
classMethods: {
|
||||
associate: models => {
|
||||
Team.hasMany(models.Collection, { as: 'atlases' });
|
||||
Team.hasMany(models.Document, { as: 'documents' });
|
||||
Team.hasMany(models.User, { as: 'users' });
|
||||
},
|
||||
},
|
||||
instanceMethods: {
|
||||
async createFirstCollection(userId) {
|
||||
const atlas = await Collection.create({
|
||||
@@ -37,8 +42,4 @@ const Team = sequelize.define(
|
||||
}
|
||||
);
|
||||
|
||||
Team.hasMany(Collection, { as: 'atlases' });
|
||||
Team.hasMany(Document, { as: 'documents' });
|
||||
Team.hasMany(User, { as: 'users' });
|
||||
|
||||
export default Team;
|
||||
|
||||
@@ -26,6 +26,14 @@ const User = sequelize.define(
|
||||
jwtSecret: encryptedFields.vault('jwtSecret'),
|
||||
},
|
||||
{
|
||||
classMethods: {
|
||||
associate: models => {
|
||||
User.hasMany(models.ApiKey, { as: 'apiKeys' });
|
||||
User.hasMany(models.Collection, { as: 'collections' });
|
||||
User.hasMany(models.Document, { as: 'documents' });
|
||||
User.hasMany(models.View, { as: 'views' });
|
||||
},
|
||||
},
|
||||
instanceMethods: {
|
||||
getJwtToken() {
|
||||
return JWT.sign({ id: this.id }, this.jwtSecret);
|
||||
|
||||
@@ -3,7 +3,6 @@ import { User } from '.';
|
||||
import { flushdb, sequelize } from '../test/support';
|
||||
|
||||
beforeEach(flushdb);
|
||||
afterAll(() => sequelize.close());
|
||||
|
||||
it('should set JWT secret and password digest', async () => {
|
||||
const user = User.build({
|
||||
|
||||
35
server/models/View.js
Normal file
35
server/models/View.js
Normal file
@@ -0,0 +1,35 @@
|
||||
// @flow
|
||||
import { DataTypes, sequelize } from '../sequelize';
|
||||
|
||||
const View = sequelize.define(
|
||||
'view',
|
||||
{
|
||||
id: {
|
||||
type: DataTypes.UUID,
|
||||
defaultValue: DataTypes.UUIDV4,
|
||||
primaryKey: true,
|
||||
},
|
||||
count: {
|
||||
type: DataTypes.INTEGER,
|
||||
defaultValue: 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
classMethods: {
|
||||
associate: models => {
|
||||
View.belongsTo(models.Document);
|
||||
View.belongsTo(models.User);
|
||||
},
|
||||
increment: async where => {
|
||||
const [model, created] = await View.findOrCreate({ where });
|
||||
if (!created) {
|
||||
model.count += 1;
|
||||
model.save();
|
||||
}
|
||||
return model;
|
||||
},
|
||||
},
|
||||
}
|
||||
);
|
||||
|
||||
export default View;
|
||||
@@ -1,8 +1,29 @@
|
||||
// @flow
|
||||
import User from './User';
|
||||
import Team from './Team';
|
||||
import Collection from './Collection';
|
||||
import Document from './Document';
|
||||
import Revision from './Revision';
|
||||
import ApiKey from './ApiKey';
|
||||
import View from './View';
|
||||
import Star from './Star';
|
||||
|
||||
export { User, Team, Collection, Document, Revision, ApiKey };
|
||||
const models = {
|
||||
User,
|
||||
Team,
|
||||
Collection,
|
||||
Document,
|
||||
Revision,
|
||||
ApiKey,
|
||||
View,
|
||||
Star,
|
||||
};
|
||||
|
||||
// based on https://github.com/sequelize/express-example/blob/master/models/index.js
|
||||
Object.keys(models).forEach(modelName => {
|
||||
if ('associate' in models[modelName]) {
|
||||
models[modelName].associate(models);
|
||||
}
|
||||
});
|
||||
|
||||
export { User, Team, Collection, Document, Revision, ApiKey, View, Star };
|
||||
|
||||
Reference in New Issue
Block a user