Upgrade sequelize and remove unique email constraints

This commit is contained in:
Jori Lallo
2017-07-12 00:28:18 -07:00
parent 3b146d9b47
commit cd584da5cf
10 changed files with 452 additions and 400 deletions

View File

@@ -55,131 +55,133 @@ const Collection = sequelize.define(
await collection.save();
},
},
classMethods: {
associate: models => {
Collection.hasMany(models.Document, {
as: 'documents',
foreignKey: 'atlasId',
});
Collection.addScope('withRecentDocuments', {
include: [
{
as: 'documents',
limit: 10,
model: models.Document,
order: [['updatedAt', 'DESC']],
},
],
});
},
},
instanceMethods: {
getUrl() {
// const slugifiedName = slug(this.name);
// return `/${slugifiedName}-c${this.urlId}`;
return `/collections/${this.id}`;
},
async getDocumentsStructure() {
// Lazy fill this.documentStructure
if (!this.documentStructure) {
this.documentStructure = this.navigationTree.children;
// Remove parent references from all root documents
await this.navigationTree.children.forEach(async ({ id }) => {
const document = await Document.findById(id);
document.parentDocumentId = null;
await document.save();
});
// Remove root document
const rootDocument = await Document.findById(this.navigationTree.id);
await rootDocument.destroy();
await this.save();
}
return this.documentStructure;
},
async addDocumentToStructure(document, index) {
if (!this.documentStructure) return;
if (!document.parentDocumentId) {
this.documentStructure.splice(
index || this.documentStructure.length,
0,
document.toJSON()
);
// Sequelize doesn't seem to set the value with splice on JSONB field
this.documentStructure = this.documentStructure;
} else {
this.documentStructure = this.documentStructure.map(childDocument => {
if (document.parentDocumentId === childDocument.id) {
childDocument.children.splice(
index || childDocument.children.length,
0,
document.toJSON()
);
}
return childDocument;
});
}
await this.save();
return this;
},
async updateDocument(updatedDocument) {
if (!this.documentStructure) return;
const { id } = updatedDocument;
const updateChildren = documents => {
return documents.map(document => {
if (document.id === id) {
document = {
...updatedDocument.toJSON(),
children: document.children,
};
} else {
document.children = updateChildren(document.children);
}
return document;
});
};
this.documentStructure = updateChildren(this.documentStructure);
await this.save();
return this;
},
async deleteDocument(document) {
if (!this.documentStructure) return;
const deleteFromChildren = (children, id) => {
if (_.find(children, { id })) {
_.remove(children, { id });
} else {
children = children.map(childDocument => {
return {
...childDocument,
children: deleteFromChildren(childDocument.children, id),
};
});
}
return children;
};
this.documentStructure = deleteFromChildren(
this.documentStructure,
document.id
);
await this.save();
return this;
},
},
}
);
// Class methods
Collection.associate = models => {
Collection.hasMany(models.Document, {
as: 'documents',
foreignKey: 'atlasId',
});
Collection.addScope('withRecentDocuments', {
include: [
{
as: 'documents',
limit: 10,
model: models.Document,
order: [['updatedAt', 'DESC']],
},
],
});
};
// Instance methods
Collection.prototype.getUrl = function() {
// const slugifiedName = slug(this.name);
// return `/${slugifiedName}-c${this.urlId}`;
return `/collections/${this.id}`;
};
Collection.prototype.getDocumentsStructure = async function() {
// Lazy fill this.documentStructure
if (!this.documentStructure) {
this.documentStructure = this.navigationTree.children;
// Remove parent references from all root documents
await this.navigationTree.children.forEach(async ({ id }) => {
const document = await Document.findById(id);
document.parentDocumentId = null;
await document.save();
});
// Remove root document
const rootDocument = await Document.findById(this.navigationTree.id);
await rootDocument.destroy();
await this.save();
}
return this.documentStructure;
};
Collection.prototype.addDocumentToStructure = async function(document, index) {
if (!this.documentStructure) return;
if (!document.parentDocumentId) {
this.documentStructure.splice(
index || this.documentStructure.length,
0,
document.toJSON()
);
// Sequelize doesn't seem to set the value with splice on JSONB field
this.documentStructure = this.documentStructure;
} else {
this.documentStructure = this.documentStructure.map(childDocument => {
if (document.parentDocumentId === childDocument.id) {
childDocument.children.splice(
index || childDocument.children.length,
0,
document.toJSON()
);
}
return childDocument;
});
}
await this.save();
return this;
};
Collection.prototype.updateDocument = async function(updatedDocument) {
if (!this.documentStructure) return;
const { id } = updatedDocument;
const updateChildren = documents => {
return documents.map(document => {
if (document.id === id) {
document = {
...updatedDocument.toJSON(),
children: document.children,
};
} else {
document.children = updateChildren(document.children);
}
return document;
});
};
this.documentStructure = updateChildren(this.documentStructure);
await this.save();
return this;
};
Collection.prototype.deleteDocument = async function(document) {
if (!this.documentStructure) return;
const deleteFromChildren = (children, id) => {
if (_.find(children, { id })) {
_.remove(children, { id });
} else {
children = children.map(childDocument => {
return {
...childDocument,
children: deleteFromChildren(childDocument.children, id),
};
});
}
return children;
};
this.documentStructure = deleteFromChildren(
this.documentStructure,
document.id
);
await this.save();
return this;
};
export default Collection;

View File

@@ -98,69 +98,59 @@ const Document = sequelize.define(
afterCreate: createRevision,
afterUpdate: createRevision,
},
instanceMethods: {
getUrl() {
const slugifiedTitle = slugify(this.title);
return `/doc/${slugifiedTitle}-${this.urlId}`;
},
toJSON() {
// 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: [],
};
},
},
classMethods: {
associate: models => {
Document.belongsTo(models.Collection, {
as: 'collection',
foreignKey: 'atlasId',
});
Document.belongsTo(models.User, {
as: 'createdBy',
foreignKey: 'createdById',
});
Document.belongsTo(models.User, {
as: 'updatedBy',
foreignKey: 'lastModifiedById',
});
Document.hasMany(models.Star, {
as: 'starred',
});
Document.addScope(
'defaultScope',
{
include: [
{ model: models.Collection, as: 'collection' },
{ model: models.User, as: 'createdBy' },
{ model: models.User, as: 'updatedBy' },
],
},
{ override: true }
);
},
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: async (user, query, options = {}) => {
const limit = options.limit || 15;
const offset = options.offset || 0;
}
);
const sql = `
// Class methods
Document.associate = models => {
Document.belongsTo(models.Collection, {
as: 'collection',
foreignKey: 'atlasId',
});
Document.belongsTo(models.User, {
as: 'createdBy',
foreignKey: 'createdById',
});
Document.belongsTo(models.User, {
as: 'updatedBy',
foreignKey: 'lastModifiedById',
});
Document.hasMany(models.Star, {
as: 'starred',
});
Document.addScope(
'defaultScope',
{
include: [
{ model: models.Collection, as: 'collection' },
{ model: models.User, as: 'createdBy' },
{ model: models.User, as: 'updatedBy' },
],
},
{ override: true }
);
};
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],
},
});
}
};
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
@@ -169,22 +159,37 @@ const Document = sequelize.define(
LIMIT :limit OFFSET :offset;
`;
const ids = await sequelize
.query(sql, {
replacements: {
query,
limit,
offset,
},
model: Document,
})
.map(document => document.id);
return Document.findAll({
where: { id: ids },
});
const ids = await sequelize
.query(sql, {
replacements: {
query,
limit,
offset,
},
},
}
);
model: Document,
})
.map(document => document.id);
return Document.findAll({
where: { id: ids },
});
};
// Instance methods
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: [],
};
};
export default Document;

View File

@@ -1,23 +1,17 @@
// @flow
import { DataTypes, sequelize } from '../sequelize';
const Star = sequelize.define(
'star',
{
id: {
type: DataTypes.UUID,
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
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);
},
},
}
);
});
Star.associate = models => {
Star.belongsTo(models.Document);
Star.belongsTo(models.User);
};
export default Star;

View File

@@ -14,25 +14,6 @@ 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({
name: this.name,
description: 'Your first Collection',
type: 'atlas',
teamId: this.id,
creatorId: userId,
});
return atlas;
},
},
indexes: [
{
unique: true,
@@ -42,4 +23,21 @@ const Team = sequelize.define(
}
);
Team.associate = models => {
Team.hasMany(models.Collection, { as: 'atlases' });
Team.hasMany(models.Document, { as: 'documents' });
Team.hasMany(models.User, { as: 'users' });
};
Team.prototype.createFirstCollection = async function(userId) {
const atlas = await Collection.create({
name: this.name,
description: 'Your first Collection',
type: 'atlas',
teamId: this.id,
creatorId: userId,
});
return atlas;
};
export default Team;

View File

@@ -1,3 +1,4 @@
// @flow
import crypto from 'crypto';
import bcrypt from 'bcrypt';
import { DataTypes, sequelize, encryptedFields } from '../sequelize';
@@ -14,50 +15,18 @@ const User = sequelize.define(
defaultValue: DataTypes.UUIDV4,
primaryKey: true,
},
email: { type: DataTypes.STRING, unique: true },
username: { type: DataTypes.STRING, unique: true },
email: { type: DataTypes.STRING },
username: { type: DataTypes.STRING },
name: DataTypes.STRING,
password: DataTypes.VIRTUAL,
passwordDigest: DataTypes.STRING,
isAdmin: DataTypes.BOOLEAN,
slackAccessToken: encryptedFields.vault('slackAccessToken'),
slackId: { type: DataTypes.STRING, allowNull: true },
slackId: { type: DataTypes.STRING, allowNull: true, unique: true },
slackData: DataTypes.JSONB,
jwtSecret: encryptedFields.vault('jwtSecret'),
},
{
classMethods: {
associate: models => {
User.hasMany(models.ApiKey, { as: 'apiKeys' });
User.hasMany(models.Document, { as: 'documents' });
User.hasMany(models.View, { as: 'views' });
},
},
instanceMethods: {
getJwtToken() {
return JWT.sign({ id: this.id }, this.jwtSecret);
},
async getTeam() {
return this.team;
},
verifyPassword(password) {
return new Promise((resolve, reject) => {
if (!this.passwordDigest) {
resolve(false);
return;
}
bcrypt.compare(password, this.passwordDigest, (err, ok) => {
if (err) {
reject(err);
return;
}
resolve(ok);
});
});
},
},
indexes: [
{
fields: ['email'],
@@ -66,6 +35,38 @@ const User = sequelize.define(
}
);
// Class methods
User.associate = models => {
User.hasMany(models.ApiKey, { as: 'apiKeys' });
User.hasMany(models.Document, { as: 'documents' });
User.hasMany(models.View, { as: 'views' });
};
// Instance methods
User.prototype.getJwtToken = function() {
return JWT.sign({ id: this.id }, this.jwtSecret);
};
User.prototype.getTeam = async function() {
return this.team;
};
User.prototype.verifyPassword = function(password) {
return new Promise((resolve, reject) => {
if (!this.passwordDigest) {
resolve(false);
return;
}
bcrypt.compare(password, this.passwordDigest, (err, ok) => {
if (err) {
reject(err);
return;
}
resolve(ok);
});
});
};
const setRandomJwtSecret = model => {
model.jwtSecret = crypto.randomBytes(64).toString('hex');
};

View File

@@ -15,21 +15,22 @@ const View = sequelize.define(
},
},
{
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;
},
},
classMethods: {},
}
);
View.associate = models => {
View.belongsTo(models.Document);
View.belongsTo(models.User);
};
View.increment = async where => {
const [model, created] = await View.findOrCreate({ where });
if (!created) {
model.count += 1;
model.save();
}
return model;
};
export default View;