Upgrade sequelize and remove unique email constraints
This commit is contained in:
19
server/migrations/20170712055148-non-unique-email.js
Normal file
19
server/migrations/20170712055148-non-unique-email.js
Normal file
@@ -0,0 +1,19 @@
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
queryInterface.removeConstraint('users', 'email_unique_idx');
|
||||
queryInterface.removeConstraint('users', 'username_unique_idx');
|
||||
},
|
||||
|
||||
down: (queryInterface, Sequelize) => {
|
||||
queryInterface.changeColumn('users', 'email', {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
});
|
||||
queryInterface.changeColumn('users', 'username', {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
};
|
||||
13
server/migrations/20170712072234-uniq-slack-id.js
Normal file
13
server/migrations/20170712072234-uniq-slack-id.js
Normal file
@@ -0,0 +1,13 @@
|
||||
module.exports = {
|
||||
up: (queryInterface, Sequelize) => {
|
||||
queryInterface.changeColumn('users', 'slackId', {
|
||||
type: Sequelize.STRING,
|
||||
unique: true,
|
||||
allowNull: false,
|
||||
});
|
||||
},
|
||||
|
||||
down: (queryInterface, Sequelize) => {
|
||||
queryInterface.removeConstraint('users', 'users_slack_id_idx');
|
||||
},
|
||||
};
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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');
|
||||
};
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user