init source

This commit is contained in:
Le Viet
2022-03-07 22:07:57 +07:00
parent e4376f3777
commit 8aba590a8d
11240 changed files with 1012977 additions and 0 deletions
+21
View File
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2014-present Sequelize contributors
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+69
View File
@@ -0,0 +1,69 @@
# Sequelize
[![npm version](https://badgen.net/npm/v/sequelize)](https://www.npmjs.com/package/sequelize)
[![Travis Build Status](https://badgen.net/travis/sequelize/sequelize?icon=travis)](https://travis-ci.org/sequelize/sequelize)
[![Appveyor Build Status](https://ci.appveyor.com/api/projects/status/9l1ypgwsp5ij46m3/branch/master?svg=true)](https://ci.appveyor.com/project/sushantdhiman/sequelize/branch/master)
[![npm downloads](https://badgen.net/npm/dm/sequelize)](https://www.npmjs.com/package/sequelize)
[![codecov](https://badgen.net/codecov/c/github/sequelize/sequelize?icon=codecov)](https://codecov.io/gh/sequelize/sequelize)
[![Last commit](https://badgen.net/github/last-commit/sequelize/sequelize)](https://github.com/sequelize/sequelize)
[![Merged PRs](https://badgen.net/github/merged-prs/sequelize/sequelize)](https://github.com/sequelize/sequelize)
[![GitHub stars](https://badgen.net/github/stars/sequelize/sequelize)](https://github.com/sequelize/sequelize)
[![Slack Status](https://sequelize-slack.herokuapp.com/badge.svg)](http://sequelize-slack.herokuapp.com/)
[![node](https://badgen.net/npm/node/sequelize)](https://www.npmjs.com/package/sequelize)
[![License](https://badgen.net/github/license/sequelize/sequelize)](https://github.com/sequelize/sequelize/blob/master/LICENSE)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
Sequelize is a promise-based Node.js ORM for Postgres, MySQL, MariaDB, SQLite and Microsoft SQL Server. It features solid transaction support, relations, eager and lazy loading, read replication and more.
Sequelize follows [SEMVER](http://semver.org). Supports Node v6 and above to use ES6 features.
New to Sequelize? Take a look at the [Tutorials and Guides](https://sequelize.org/master). You might also be interested in the [API Reference](https://sequelize.org/master/identifiers).
## Table of Contents
- [Installation](#installation)
- [Documentation](#documentation)
- [Responsible disclosure](#responsible-disclosure)
- [Resources](#resources)
## Installation
```bash
$ npm install --save sequelize # This will install v5
# And one of the following:
$ npm install --save pg pg-hstore # Postgres
$ npm install --save mysql2
$ npm install --save mariadb
$ npm install --save sqlite3
$ npm install --save tedious # Microsoft SQL Server
```
## Documentation
- [v5 Documentation](https://sequelize.org/master)
- [v4 Documentation](https://sequelize.org/v4)
- [v3 Documentation](https://sequelize.org/v3)
- [Contributing](https://github.com/sequelize/sequelize/blob/master/CONTRIBUTING.md)
## Responsible disclosure
If you have security issues to report please refer to our [Responsible Disclosure Policy](./SECURITY.md) for more details.
## Resources
- [Changelog](https://github.com/sequelize/sequelize/releases)
- [Slack](http://sequelize-slack.herokuapp.com/)
- [Stack Overflow](https://stackoverflow.com/questions/tagged/sequelize.js)
### Tools
- [Sequelize CLI](https://github.com/sequelize/cli)
- [Sequelize & TypeScript](https://sequelize.org/master/manual/typescript.html)
- [Enhanced TypeScript with decorators](https://github.com/RobinBuschmann/sequelize-typescript)
- [Sequelize & GraphQL](https://github.com/mickhansen/graphql-sequelize)
- [Add-ons & Plugins](https://sequelize.org/master/manual/resources.html)
- [Sequelize & CockroachDB](https://github.com/cockroachdb/sequelize-cockroachdb)
### Learning
- [Getting Started](https://sequelize.org/master/manual/getting-started)
- [Express Example](https://github.com/sequelize/express-example)
### Translations
- [English v3/v4/v5](https://sequelize.org) (OFFICIAL)
- [中文文档 v4/v5](https://github.com/demopark/sequelize-docs-Zh-CN) (UNOFFICIAL)
+8
View File
@@ -0,0 +1,8 @@
'use strict';
/**
* The entry point.
*
* @module Sequelize
*/
module.exports = require('./lib/sequelize');
+144
View File
@@ -0,0 +1,144 @@
'use strict';
const { AssociationError } = require('./../errors');
/**
* Creating associations in sequelize is done by calling one of the belongsTo / hasOne / hasMany / belongsToMany functions on a model (the source), and providing another model as the first argument to the function (the target).
*
* * hasOne - adds a foreign key to the target and singular association mixins to the source.
* * belongsTo - add a foreign key and singular association mixins to the source.
* * hasMany - adds a foreign key to target and plural association mixins to the source.
* * belongsToMany - creates an N:M association with a join table and adds plural association mixins to the source. The junction table is created with sourceId and targetId.
*
* Creating an association will add a foreign key constraint to the attributes. All associations use `CASCADE` on update and `SET NULL` on delete, except for n:m, which also uses `CASCADE` on delete.
*
* When creating associations, you can provide an alias, via the `as` option. This is useful if the same model is associated twice, or you want your association to be called something other than the name of the target model.
*
* As an example, consider the case where users have many pictures, one of which is their profile picture. All pictures have a `userId`, but in addition the user model also has a `profilePictureId`, to be able to easily load the user's profile picture.
*
* ```js
* User.hasMany(Picture)
* User.belongsTo(Picture, { as: 'ProfilePicture', constraints: false })
*
* user.getPictures() // gets you all pictures
* user.getProfilePicture() // gets you only the profile picture
*
* User.findAll({
* where: ...,
* include: [
* { model: Picture }, // load all pictures
* { model: Picture, as: 'ProfilePicture' }, // load the profile picture.
* // Notice that the spelling must be the exact same as the one in the association
* ]
* })
* ```
* To get full control over the foreign key column added by sequelize, you can use the `foreignKey` option. It can either be a string, that specifies the name, or and object type definition,
* equivalent to those passed to `sequelize.define`.
*
* ```js
* User.hasMany(Picture, { foreignKey: 'uid' })
* ```
*
* The foreign key column in Picture will now be called `uid` instead of the default `userId`.
*
* ```js
* User.hasMany(Picture, {
* foreignKey: {
* name: 'uid',
* allowNull: false
* }
* })
* ```
*
* This specifies that the `uid` column cannot be null. In most cases this will already be covered by the foreign key constraints, which sequelize creates automatically, but can be useful in case where the foreign keys are disabled, e.g. due to circular references (see `constraints: false` below).
*
* When fetching associated models, you can limit your query to only load some models. These queries are written in the same way as queries to `find`/`findAll`. To only get pictures in JPG, you can do:
*
* ```js
* user.getPictures({
* where: {
* format: 'jpg'
* }
* })
* ```
*
* There are several ways to update and add new associations. Continuing with our example of users and pictures:
* ```js
* user.addPicture(p) // Add a single picture
* user.setPictures([p1, p2]) // Associate user with ONLY these two picture, all other associations will be deleted
* user.addPictures([p1, p2]) // Associate user with these two pictures, but don't touch any current associations
* ```
*
* You don't have to pass in a complete object to the association functions, if your associated model has a single primary key:
*
* ```js
* user.addPicture(req.query.pid) // Here pid is just an integer, representing the primary key of the picture
* ```
*
* In the example above we have specified that a user belongs to his profile picture. Conceptually, this might not make sense, but since we want to add the foreign key to the user model this is the way to do it.
*
* Note how we also specified `constraints: false` for profile picture. This is because we add a foreign key from user to picture (profilePictureId), and from picture to user (userId). If we were to add foreign keys to both, it would create a cyclic dependency, and sequelize would not know which table to create first, since user depends on picture, and picture depends on user. These kinds of problems are detected by sequelize before the models are synced to the database, and you will get an error along the lines of `Error: Cyclic dependency found. 'users' is dependent of itself`. If you encounter this, you should either disable some constraints, or rethink your associations completely.
*/
class Association {
constructor(source, target, options = {}) {
/**
* @type {Model}
*/
this.source = source;
/**
* @type {Model}
*/
this.target = target;
this.options = options;
this.scope = options.scope;
this.isSelfAssociation = this.source === this.target;
this.as = options.as;
/**
* The type of the association. One of `HasMany`, `BelongsTo`, `HasOne`, `BelongsToMany`
* @type {string}
*/
this.associationType = '';
if (source.hasAlias(options.as)) {
throw new AssociationError(`You have used the alias ${options.as} in two separate associations. ` +
'Aliased associations must have unique aliases.'
);
}
}
/**
* Normalize input
*
* @param {Array|string} input it may be array or single obj, instance or primary key
*
* @private
* @returns {Array} built objects
*/
toInstanceArray(input) {
if (!Array.isArray(input)) {
input = [input];
}
return input.map(element => {
if (element instanceof this.target) return element;
const tmpInstance = {};
tmpInstance[this.target.primaryKeyAttribute] = element;
return this.target.build(tmpInstance, { isNewRecord: false });
});
}
[Symbol.for('nodejs.util.inspect.custom')]() {
return this.as;
}
inspect() {
return this.as;
}
}
module.exports = Association;
+818
View File
@@ -0,0 +1,818 @@
'use strict';
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const BelongsTo = require('./belongs-to');
const HasMany = require('./has-many');
const HasOne = require('./has-one');
const AssociationError = require('../errors').AssociationError;
const EmptyResultError = require('../errors').EmptyResultError;
const Op = require('../operators');
/**
* Many-to-many association with a join table.
*
* When the join table has additional attributes, these can be passed in the options object:
*
* ```js
* UserProject = sequelize.define('user_project', {
* role: Sequelize.STRING
* });
* User.belongsToMany(Project, { through: UserProject });
* Project.belongsToMany(User, { through: UserProject });
* // through is required!
*
* user.addProject(project, { through: { role: 'manager' }});
* ```
*
* All methods allow you to pass either a persisted instance, its primary key, or a mixture:
*
* ```js
* Project.create({ id: 11 }).then(project => {
* user.addProjects([project, 12]);
* });
* ```
*
* If you want to set several target instances, but with different attributes you have to set the attributes on the instance, using a property with the name of the through model:
*
* ```js
* p1.UserProjects = {
* started: true
* }
* user.setProjects([p1, p2], { through: { started: false }}) // The default value is false, but p1 overrides that.
* ```
*
* Similarly, when fetching through a join table with custom attributes, these attributes will be available as an object with the name of the through model.
* ```js
* user.getProjects().then(projects => {
* let p1 = projects[0]
* p1.UserProjects.started // Is this project started yet?
* })
* ```
*
* In the API reference below, add the name of the association to the method, e.g. for `User.belongsToMany(Project)` the getter will be `user.getProjects()`.
*
* @see {@link Model.belongsToMany}
*/
class BelongsToMany extends Association {
constructor(source, target, options) {
super(source, target, options);
if (this.options.through === undefined || this.options.through === true || this.options.through === null) {
throw new AssociationError(`${source.name}.belongsToMany(${target.name}) requires through option, pass either a string or a model`);
}
if (!this.options.through.model) {
this.options.through = {
model: options.through
};
}
this.associationType = 'BelongsToMany';
this.targetAssociation = null;
this.sequelize = source.sequelize;
this.through = Object.assign({}, this.options.through);
this.isMultiAssociation = true;
this.doubleLinked = false;
if (!this.as && this.isSelfAssociation) {
throw new AssociationError('\'as\' must be defined for many-to-many self-associations');
}
if (this.as) {
this.isAliased = true;
if (_.isPlainObject(this.as)) {
this.options.name = this.as;
this.as = this.as.plural;
} else {
this.options.name = {
plural: this.as,
singular: Utils.singularize(this.as)
};
}
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
this.combinedTableName = Utils.combineTableNames(
this.source.tableName,
this.isSelfAssociation ? this.as || this.target.tableName : this.target.tableName
);
/*
* If self association, this is the target association - Unless we find a pairing association
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
/*
* Find paired association (if exists)
*/
_.each(this.target.associations, association => {
if (association.associationType !== 'BelongsToMany') return;
if (association.target !== this.source) return;
if (this.options.through.model === association.options.through.model) {
this.paired = association;
association.paired = this;
}
});
/*
* Default/generated source/target keys
*/
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
if (this.options.targetKey) {
this.targetKey = this.options.targetKey;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
} else {
this.targetKeyDefault = true;
this.targetKey = this.target.primaryKeyAttribute;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
}
this._createForeignAndOtherKeys();
if (typeof this.through.model === 'string') {
if (!this.sequelize.isDefined(this.through.model)) {
this.through.model = this.sequelize.define(this.through.model, {}, Object.assign(this.options, {
tableName: this.through.model,
indexes: [], //we don't want indexes here (as referenced in #2416)
paranoid: false, // A paranoid join table does not make sense
validate: {} // Don't propagate model-level validations
}));
} else {
this.through.model = this.sequelize.model(this.through.model);
}
}
this.options = Object.assign(this.options, _.pick(this.through.model.options, [
'timestamps', 'createdAt', 'updatedAt', 'deletedAt', 'paranoid'
]));
if (this.paired) {
let needInjectPaired = false;
if (this.targetKeyDefault) {
this.targetKey = this.paired.sourceKey;
this.targetKeyField = this.paired.sourceKeyField;
this._createForeignAndOtherKeys();
}
if (this.paired.targetKeyDefault) {
// in this case paired.otherKey depends on paired.targetKey,
// so cleanup previously wrong generated otherKey
if (this.paired.targetKey !== this.sourceKey) {
delete this.through.model.rawAttributes[this.paired.otherKey];
this.paired.targetKey = this.sourceKey;
this.paired.targetKeyField = this.sourceKeyField;
this.paired._createForeignAndOtherKeys();
needInjectPaired = true;
}
}
if (this.otherKeyDefault) {
this.otherKey = this.paired.foreignKey;
}
if (this.paired.otherKeyDefault) {
// If paired otherKey was inferred we should make sure to clean it up
// before adding a new one that matches the foreignKey
if (this.paired.otherKey !== this.foreignKey) {
delete this.through.model.rawAttributes[this.paired.otherKey];
this.paired.otherKey = this.foreignKey;
needInjectPaired = true;
}
}
if (needInjectPaired) {
this.paired._injectAttributes();
}
}
if (this.through) {
this.throughModel = this.through.model;
}
this.options.tableName = this.combinedName = this.through.model === Object(this.through.model) ? this.through.model.tableName : this.through.model;
this.associationAccessor = this.as;
// Get singular and plural names, trying to uppercase the first letter, unless the model forbids it
const plural = _.upperFirst(this.options.name.plural);
const singular = _.upperFirst(this.options.name.singular);
this.accessors = {
get: `get${plural}`,
set: `set${plural}`,
addMultiple: `add${plural}`,
add: `add${singular}`,
create: `create${singular}`,
remove: `remove${singular}`,
removeMultiple: `remove${plural}`,
hasSingle: `has${singular}`,
hasAll: `has${plural}`,
count: `count${plural}`
};
}
_createForeignAndOtherKeys() {
/*
* Default/generated foreign/other keys
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else {
this.foreignKeyAttribute = {};
this.foreignKey = this.options.foreignKey || Utils.camelize(
[
this.source.options.name.singular,
this.sourceKey
].join('_')
);
}
if (_.isObject(this.options.otherKey)) {
this.otherKeyAttribute = this.options.otherKey;
this.otherKey = this.otherKeyAttribute.name || this.otherKeyAttribute.fieldName;
} else {
if (!this.options.otherKey) {
this.otherKeyDefault = true;
}
this.otherKeyAttribute = {};
this.otherKey = this.options.otherKey || Utils.camelize(
[
this.isSelfAssociation ? Utils.singularize(this.as) : this.target.options.name.singular,
this.targetKey
].join('_')
);
}
}
// the id is in the target table
// or in an extra table which connects two tables
_injectAttributes() {
this.identifier = this.foreignKey;
this.foreignIdentifier = this.otherKey;
// remove any PKs previously defined by sequelize
// but ignore any keys that are part of this association (#5865)
_.each(this.through.model.rawAttributes, (attribute, attributeName) => {
if (attribute.primaryKey === true && attribute._autoGenerated === true) {
if (attributeName === this.foreignKey || attributeName === this.otherKey) {
// this key is still needed as it's part of the association
// so just set primaryKey to false
attribute.primaryKey = false;
}
else {
delete this.through.model.rawAttributes[attributeName];
}
this.primaryKeyDeleted = true;
}
});
const sourceKey = this.source.rawAttributes[this.sourceKey];
const sourceKeyType = sourceKey.type;
const sourceKeyField = this.sourceKeyField;
const targetKey = this.target.rawAttributes[this.targetKey];
const targetKeyType = targetKey.type;
const targetKeyField = this.targetKeyField;
const sourceAttribute = _.defaults({}, this.foreignKeyAttribute, { type: sourceKeyType });
const targetAttribute = _.defaults({}, this.otherKeyAttribute, { type: targetKeyType });
if (this.primaryKeyDeleted === true) {
targetAttribute.primaryKey = sourceAttribute.primaryKey = true;
} else if (this.through.unique !== false) {
let uniqueKey;
if (typeof this.options.uniqueKey === 'string' && this.options.uniqueKey !== '') {
uniqueKey = this.options.uniqueKey;
} else {
uniqueKey = [this.through.model.tableName, this.foreignKey, this.otherKey, 'unique'].join('_');
}
targetAttribute.unique = sourceAttribute.unique = uniqueKey;
}
if (!this.through.model.rawAttributes[this.foreignKey]) {
this.through.model.rawAttributes[this.foreignKey] = {
_autoGenerated: true
};
}
if (!this.through.model.rawAttributes[this.otherKey]) {
this.through.model.rawAttributes[this.otherKey] = {
_autoGenerated: true
};
}
if (this.options.constraints !== false) {
sourceAttribute.references = {
model: this.source.getTableName(),
key: sourceKeyField
};
// For the source attribute the passed option is the priority
sourceAttribute.onDelete = this.options.onDelete || this.through.model.rawAttributes[this.foreignKey].onDelete;
sourceAttribute.onUpdate = this.options.onUpdate || this.through.model.rawAttributes[this.foreignKey].onUpdate;
if (!sourceAttribute.onDelete) sourceAttribute.onDelete = 'CASCADE';
if (!sourceAttribute.onUpdate) sourceAttribute.onUpdate = 'CASCADE';
targetAttribute.references = {
model: this.target.getTableName(),
key: targetKeyField
};
// But the for target attribute the previously defined option is the priority (since it could've been set by another belongsToMany call)
targetAttribute.onDelete = this.through.model.rawAttributes[this.otherKey].onDelete || this.options.onDelete;
targetAttribute.onUpdate = this.through.model.rawAttributes[this.otherKey].onUpdate || this.options.onUpdate;
if (!targetAttribute.onDelete) targetAttribute.onDelete = 'CASCADE';
if (!targetAttribute.onUpdate) targetAttribute.onUpdate = 'CASCADE';
}
this.through.model.rawAttributes[this.foreignKey] = Object.assign(this.through.model.rawAttributes[this.foreignKey], sourceAttribute);
this.through.model.rawAttributes[this.otherKey] = Object.assign(this.through.model.rawAttributes[this.otherKey], targetAttribute);
this.through.model.refreshAttributes();
this.identifierField = this.through.model.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignIdentifierField = this.through.model.rawAttributes[this.otherKey].field || this.otherKey;
if (this.paired && !this.paired.foreignIdentifierField) {
this.paired.foreignIdentifierField = this.through.model.rawAttributes[this.paired.otherKey].field || this.paired.otherKey;
}
this.toSource = new BelongsTo(this.through.model, this.source, {
foreignKey: this.foreignKey
});
this.manyFromSource = new HasMany(this.source, this.through.model, {
foreignKey: this.foreignKey
});
this.oneFromSource = new HasOne(this.source, this.through.model, {
foreignKey: this.foreignKey,
as: this.through.model.name
});
this.toTarget = new BelongsTo(this.through.model, this.target, {
foreignKey: this.otherKey
});
this.manyFromTarget = new HasMany(this.target, this.through.model, {
foreignKey: this.otherKey
});
this.oneFromTarget = new HasOne(this.target, this.through.model, {
foreignKey: this.otherKey,
as: this.through.model.name
});
if (this.paired && this.paired.otherKeyDefault) {
this.paired.toTarget = new BelongsTo(this.paired.through.model, this.paired.target, {
foreignKey: this.paired.otherKey
});
this.paired.oneFromTarget = new HasOne(this.paired.target, this.paired.through.model, {
foreignKey: this.paired.otherKey,
as: this.paired.through.model.name
});
}
Helpers.checkNamingCollision(this);
return this;
}
mixin(obj) {
const methods = ['get', 'count', 'hasSingle', 'hasAll', 'set', 'add', 'addMultiple', 'remove', 'removeMultiple', 'create'];
const aliases = {
hasSingle: 'has',
hasAll: 'has',
addMultiple: 'add',
removeMultiple: 'remove'
};
Helpers.mixinMethods(this, obj, methods, aliases);
}
/**
* Get everything currently associated with this, using an optional where clause.
*
* @see
* {@link Model} for a full explanation of options
*
* @param {Model} instance instance
* @param {Object} [options] find options
* @param {Object} [options.where] An optional where clause to limit the associated models
* @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
* @param {string} [options.schema] Apply a schema on the related model
*
* @returns {Promise<Array<Model>>}
*/
get(instance, options) {
options = Utils.cloneDeep(options) || {};
const through = this.through;
let scopeWhere;
let throughWhere;
if (this.scope) {
scopeWhere = _.clone(this.scope);
}
options.where = {
[Op.and]: [
scopeWhere,
options.where
]
};
if (Object(through.model) === through.model) {
throughWhere = {};
throughWhere[this.foreignKey] = instance.get(this.sourceKey);
if (through.scope) {
Object.assign(throughWhere, through.scope);
}
//If a user pass a where on the options through options, make an "and" with the current throughWhere
if (options.through && options.through.where) {
throughWhere = {
[Op.and]: [throughWhere, options.through.where]
};
}
options.include = options.include || [];
options.include.push({
association: this.oneFromTarget,
attributes: options.joinTableAttributes,
required: true,
where: throughWhere
});
}
let model = this.target;
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
model = model.unscoped();
} else {
model = model.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
model = model.schema(options.schema, options.schemaDelimiter);
}
return model.findAll(options);
}
/**
* Count everything currently associated with this, using an optional where clause.
*
* @param {Model} instance instance
* @param {Object} [options] find options
* @param {Object} [options.where] An optional where clause to limit the associated models
* @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
*
* @returns {Promise<number>}
*/
count(instance, options) {
const sequelize = this.target.sequelize;
options = Utils.cloneDeep(options);
options.attributes = [
[sequelize.fn('COUNT', sequelize.col([this.target.name, this.targetKeyField].join('.'))), 'count']
];
options.joinTableAttributes = [];
options.raw = true;
options.plain = true;
return this.get(instance, options).then(result => parseInt(result.count, 10));
}
/**
* Check if one or more instance(s) are associated with this. If a list of instances is passed, the function returns true if _all_ instances are associated
*
* @param {Model} sourceInstance source instance to check for an association with
* @param {Model|Model[]|string[]|string|number[]|number} [instances] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to getAssociations
*
* @returns {Promise<boolean>}
*/
has(sourceInstance, instances, options) {
if (!Array.isArray(instances)) {
instances = [instances];
}
options = Object.assign({
raw: true
}, options, {
scope: false,
attributes: [this.targetKey],
joinTableAttributes: []
});
const instancePrimaryKeys = instances.map(instance => {
if (instance instanceof this.target) {
return instance.where();
}
return {
[this.targetKey]: instance
};
});
options.where = {
[Op.and]: [
{ [Op.or]: instancePrimaryKeys },
options.where
]
};
return this.get(sourceInstance, options).then(associatedObjects =>
_.differenceBy(instancePrimaryKeys, associatedObjects, this.targetKey).length === 0
);
}
/**
* Set the associated models by passing an array of instances or their primary keys.
* Everything that it not in the passed array will be un-associated.
*
* @param {Model} sourceInstance source instance to associate new instances with
* @param {Model|Model[]|string[]|string|number[]|number} [newAssociatedObjects] A single instance or primary key, or a mixed array of persisted instances or primary keys
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate`, `update` and `destroy`
* @param {Object} [options.validate] Run validation for the join model
* @param {Object} [options.through] Additional attributes for the join table.
*
* @returns {Promise}
*/
set(sourceInstance, newAssociatedObjects, options) {
options = options || {};
const sourceKey = this.sourceKey;
const targetKey = this.targetKey;
const identifier = this.identifier;
const foreignIdentifier = this.foreignIdentifier;
let where = {};
if (newAssociatedObjects === null) {
newAssociatedObjects = [];
} else {
newAssociatedObjects = this.toInstanceArray(newAssociatedObjects);
}
where[identifier] = sourceInstance.get(sourceKey);
where = Object.assign(where, this.through.scope);
const updateAssociations = currentRows => {
const obsoleteAssociations = [];
const promises = [];
const defaultAttributes = options.through || {};
const unassociatedObjects = newAssociatedObjects.filter(obj =>
!currentRows.some(currentRow => currentRow[foreignIdentifier] === obj.get(targetKey))
);
for (const currentRow of currentRows) {
const newObj = newAssociatedObjects.find(obj => currentRow[foreignIdentifier] === obj.get(targetKey));
if (!newObj) {
obsoleteAssociations.push(currentRow);
} else {
let throughAttributes = newObj[this.through.model.name];
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof this.through.model) {
throughAttributes = {};
}
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (Object.keys(attributes).length) {
promises.push(
this.through.model.update(attributes, Object.assign(options, {
where: {
[identifier]: sourceInstance.get(sourceKey),
[foreignIdentifier]: newObj.get(targetKey)
}
}
))
);
}
}
}
if (obsoleteAssociations.length > 0) {
const where = Object.assign({
[identifier]: sourceInstance.get(sourceKey),
[foreignIdentifier]: obsoleteAssociations.map(obsoleteAssociation => obsoleteAssociation[foreignIdentifier])
}, this.through.scope);
promises.push(
this.through.model.destroy(_.defaults({
where
}, options))
);
}
if (unassociatedObjects.length > 0) {
const bulk = unassociatedObjects.map(unassociatedObject => {
let attributes = {};
attributes[identifier] = sourceInstance.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
attributes = _.defaults(attributes, unassociatedObject[this.through.model.name], defaultAttributes);
Object.assign(attributes, this.through.scope);
attributes = Object.assign(attributes, this.through.scope);
return attributes;
});
promises.push(this.through.model.bulkCreate(bulk, Object.assign({ validate: true }, options)));
}
return Utils.Promise.all(promises);
};
return this.through.model.findAll(_.defaults({ where, raw: true }, options))
.then(currentRows => updateAssociations(currentRows))
.catch(error => {
if (error instanceof EmptyResultError) return updateAssociations([]);
throw error;
});
}
/**
* Associate one or several rows with source instance. It will not un-associate any already associated instance
* that may be missing from `newInstances`.
*
* @param {Model} sourceInstance source instance to associate new instances with
* @param {Model|Model[]|string[]|string|number[]|number} [newInstances] A single instance or primary key, or a mixed array of persisted instances or primary keys
* @param {Object} [options] Options passed to `through.findAll`, `bulkCreate` and `update`
* @param {Object} [options.validate] Run validation for the join model.
* @param {Object} [options.through] Additional attributes for the join table.
*
* @returns {Promise}
*/
add(sourceInstance, newInstances, options) {
// If newInstances is null or undefined, no-op
if (!newInstances) return Utils.Promise.resolve();
options = _.clone(options) || {};
const association = this;
const sourceKey = association.sourceKey;
const targetKey = association.targetKey;
const identifier = association.identifier;
const foreignIdentifier = association.foreignIdentifier;
const defaultAttributes = options.through || {};
newInstances = association.toInstanceArray(newInstances);
const where = {
[identifier]: sourceInstance.get(sourceKey),
[foreignIdentifier]: newInstances.map(newInstance => newInstance.get(targetKey))
};
Object.assign(where, association.through.scope);
const updateAssociations = currentRows => {
const promises = [];
const unassociatedObjects = [];
const changedAssociations = [];
for (const obj of newInstances) {
const existingAssociation = currentRows && currentRows.find(current => current[foreignIdentifier] === obj.get(targetKey));
if (!existingAssociation) {
unassociatedObjects.push(obj);
} else {
const throughAttributes = obj[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
if (Object.keys(attributes).some(attribute => attributes[attribute] !== existingAssociation[attribute])) {
changedAssociations.push(obj);
}
}
}
if (unassociatedObjects.length > 0) {
const bulk = unassociatedObjects.map(unassociatedObject => {
const throughAttributes = unassociatedObject[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
attributes[identifier] = sourceInstance.get(sourceKey);
attributes[foreignIdentifier] = unassociatedObject.get(targetKey);
Object.assign(attributes, association.through.scope);
return attributes;
});
promises.push(association.through.model.bulkCreate(bulk, Object.assign({ validate: true }, options)));
}
for (const assoc of changedAssociations) {
let throughAttributes = assoc[association.through.model.name];
const attributes = _.defaults({}, throughAttributes, defaultAttributes);
// Quick-fix for subtle bug when using existing objects that might have the through model attached (not as an attribute object)
if (throughAttributes instanceof association.through.model) {
throughAttributes = {};
}
const where = {
[identifier]: sourceInstance.get(sourceKey),
[foreignIdentifier]: assoc.get(targetKey)
};
promises.push(association.through.model.update(attributes, Object.assign(options, { where })));
}
return Utils.Promise.all(promises);
};
return association.through.model.findAll(_.defaults({ where, raw: true }, options))
.then(currentRows => updateAssociations(currentRows))
.then(([associations]) => associations)
.catch(error => {
if (error instanceof EmptyResultError) return updateAssociations();
throw error;
});
}
/**
* Un-associate one or more instance(s).
*
* @param {Model} sourceInstance instance to un associate instances with
* @param {Model|Model[]|string|string[]|number|number[]} [oldAssociatedObjects] Can be an Instance or its primary key, or a mixed array of instances and primary keys
* @param {Object} [options] Options passed to `through.destroy`
*
* @returns {Promise}
*/
remove(sourceInstance, oldAssociatedObjects, options) {
const association = this;
options = options || {};
oldAssociatedObjects = association.toInstanceArray(oldAssociatedObjects);
const where = {
[association.identifier]: sourceInstance.get(association.sourceKey),
[association.foreignIdentifier]: oldAssociatedObjects.map(newInstance => newInstance.get(association.targetKey))
};
return association.through.model.destroy(_.defaults({ where }, options));
}
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Model} sourceInstance source instance
* @param {Object} [values] values for target model
* @param {Object} [options] Options passed to create and add
* @param {Object} [options.through] Additional attributes for the join table
*
* @returns {Promise}
*/
create(sourceInstance, values, options) {
const association = this;
options = options || {};
values = values || {};
if (Array.isArray(options)) {
options = {
fields: options
};
}
if (association.scope) {
Object.assign(values, association.scope);
if (options.fields) {
options.fields = options.fields.concat(Object.keys(association.scope));
}
}
// Create the related model instance
return association.target.create(values, options).then(newAssociatedObject =>
sourceInstance[association.accessors.add](newAssociatedObject, _.omit(options, ['fields'])).return(newAssociatedObject)
);
}
verifyAssociationAlias(alias) {
if (typeof alias === 'string') {
return this.as === alias;
}
if (alias && alias.plural) {
return this.as === alias.plural;
}
return !this.isAliased;
}
}
module.exports = BelongsToMany;
module.exports.BelongsToMany = BelongsToMany;
module.exports.default = BelongsToMany;
+252
View File
@@ -0,0 +1,252 @@
'use strict';
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-one association
*
* In the API reference below, add the name of the association to the method, e.g. for `User.belongsTo(Project)` the getter will be `user.getProject()`.
*
* @see {@link Model.belongsTo}
*/
class BelongsTo extends Association {
constructor(source, target, options) {
super(source, target, options);
this.associationType = 'BelongsTo';
this.isSingleAssociation = true;
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true;
this.options.name = {
singular: this.as
};
} else {
this.as = this.target.options.name.singular;
this.options.name = this.target.options.name;
}
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelize(
[
this.as,
this.target.primaryKeyAttribute
].join('_')
);
}
this.identifier = this.foreignKey;
if (this.source.rawAttributes[this.identifier]) {
this.identifierField = this.source.rawAttributes[this.identifier].field || this.identifier;
}
if (
this.options.targetKey
&& !this.target.rawAttributes[this.options.targetKey]
) {
throw new Error(`Unknown attribute "${this.options.targetKey}" passed as targetKey, define this attribute on model "${this.target.name}" first`);
}
this.targetKey = this.options.targetKey || this.target.primaryKeyAttribute;
this.targetKeyField = this.target.rawAttributes[this.targetKey].field || this.targetKey;
this.targetKeyIsPrimary = this.targetKey === this.target.primaryKeyAttribute;
this.targetIdentifier = this.targetKey;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
// Get singular name, trying to uppercase the first letter, unless the model forbids it
const singular = _.upperFirst(this.options.name.singular);
this.accessors = {
get: `get${singular}`,
set: `set${singular}`,
create: `create${singular}`
};
}
// the id is in the source table
_injectAttributes() {
const newAttributes = {};
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.target.rawAttributes[this.targetKey].type,
allowNull: true
});
if (this.options.constraints !== false) {
const source = this.source.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
this.options.onDelete = this.options.onDelete || (source.allowNull ? 'SET NULL' : 'NO ACTION');
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.target, this.source, this.options, this.targetKeyField);
Utils.mergeDefaults(this.source.rawAttributes, newAttributes);
this.source.refreshAttributes();
this.identifierField = this.source.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this);
return this;
}
mixin(obj) {
const methods = ['get', 'set', 'create'];
Helpers.mixinMethods(this, obj, methods);
}
/**
* Get the associated instance.
*
* @param {Model|Array<Model>} instances source instances
* @param {Object} [options] find options
* @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false.
* @param {string} [options.schema] Apply a schema on the related model
*
* @see
* {@link Model.findOne} for a full explanation of options
*
* @returns {Promise<Model>}
*/
get(instances, options) {
const where = {};
let Target = this.target;
let instance;
options = Utils.cloneDeep(options);
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
Target = Target.schema(options.schema, options.schemaDelimiter);
}
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
if (instances) {
where[this.targetKey] = {
[Op.in]: instances.map(instance => instance.get(this.foreignKey))
};
} else {
if (this.targetKeyIsPrimary && !options.where) {
return Target.findByPk(instance.get(this.foreignKey), options);
}
where[this.targetKey] = instance.get(this.foreignKey);
options.limit = null;
}
options.where = options.where ?
{ [Op.and]: [where, options.where] } :
where;
if (instances) {
return Target.findAll(options).then(results => {
const result = {};
for (const instance of instances) {
result[instance.get(this.foreignKey, { raw: true })] = null;
}
for (const instance of results) {
result[instance.get(this.targetKey, { raw: true })] = instance;
}
return result;
});
}
return Target.findOne(options);
}
/**
* Set the associated model.
*
* @param {Model} sourceInstance the source instance
* @param {?<Model>|string|number} [associatedInstance] An persisted instance or the primary key of an instance to associate with this. Pass `null` or `undefined` to remove the association.
* @param {Object} [options={}] options passed to `this.save`
* @param {boolean} [options.save=true] Skip saving this after setting the foreign key if false.
*
* @returns {Promise}
*/
set(sourceInstance, associatedInstance, options = {}) {
let value = associatedInstance;
if (associatedInstance instanceof this.target) {
value = associatedInstance[this.targetKey];
}
sourceInstance.set(this.foreignKey, value);
if (options.save === false) return;
options = Object.assign({
fields: [this.foreignKey],
allowNull: [this.foreignKey],
association: true
}, options);
// passes the changed field to save, so only that field get updated.
return sourceInstance.save(options);
}
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Model} sourceInstance the source instance
* @param {Object} [values={}] values to create associated model instance with
* @param {Object} [options={}] Options passed to `target.create` and setAssociation.
*
* @see
* {@link Model#create} for a full explanation of options
*
* @returns {Promise<Model>} The created target model
*/
create(sourceInstance, values, options) {
values = values || {};
options = options || {};
return this.target.create(values, options)
.then(newAssociatedObject => sourceInstance[this.accessors.set](newAssociatedObject, options)
.then(() => newAssociatedObject)
);
}
verifyAssociationAlias(alias) {
if (typeof alias === 'string') {
return this.as === alias;
}
if (alias && alias.singular) {
return this.as === alias.singular;
}
return !this.isAliased;
}
}
module.exports = BelongsTo;
module.exports.BelongsTo = BelongsTo;
module.exports.default = BelongsTo;
+487
View File
@@ -0,0 +1,487 @@
'use strict';
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-many association
*
* In the API reference below, add the name of the association to the method, e.g. for `User.hasMany(Project)` the getter will be `user.getProjects()`.
* If the association is aliased, use the alias instead, e.g. `User.hasMany(Project, { as: 'jobs' })` will be `user.getJobs()`.
*
* @see {@link Model.hasMany}
*/
class HasMany extends Association {
constructor(source, target, options) {
super(source, target, options);
this.associationType = 'HasMany';
this.targetAssociation = null;
this.sequelize = source.sequelize;
this.isMultiAssociation = true;
this.foreignKeyAttribute = {};
if (this.options.through) {
throw new Error('N:M associations are not supported with hasMany. Use belongsToMany instead');
}
/*
* If self association, this is the target association
*/
if (this.isSelfAssociation) {
this.targetAssociation = this;
}
if (this.as) {
this.isAliased = true;
if (_.isPlainObject(this.as)) {
this.options.name = this.as;
this.as = this.as.plural;
} else {
this.options.name = {
plural: this.as,
singular: Utils.singularize(this.as)
};
}
} else {
this.as = this.target.options.name.plural;
this.options.name = this.target.options.name;
}
/*
* Foreign key setup
*/
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelize(
[
this.source.options.name.singular,
this.source.primaryKeyAttribute
].join('_')
);
}
if (this.target.rawAttributes[this.foreignKey]) {
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
/*
* Source key setup
*/
this.sourceKey = this.options.sourceKey || this.source.primaryKeyAttribute;
if (this.source.rawAttributes[this.sourceKey]) {
this.sourceKeyAttribute = this.sourceKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
} else {
this.sourceKeyAttribute = this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.primaryKeyField;
}
// Get singular and plural names
// try to uppercase the first letter, unless the model forbids it
const plural = _.upperFirst(this.options.name.plural);
const singular = _.upperFirst(this.options.name.singular);
this.associationAccessor = this.as;
this.accessors = {
get: `get${plural}`,
set: `set${plural}`,
addMultiple: `add${plural}`,
add: `add${singular}`,
create: `create${singular}`,
remove: `remove${singular}`,
removeMultiple: `remove${plural}`,
hasSingle: `has${singular}`,
hasAll: `has${plural}`,
count: `count${plural}`
};
}
// the id is in the target table
// or in an extra table which connects two tables
_injectAttributes() {
const newAttributes = {};
// Create a new options object for use with addForeignKeyConstraints, to avoid polluting this.options in case it is later used for a n:m
const constraintOptions = _.clone(this.options);
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.sourceKeyAttribute].type,
allowNull: true
});
if (this.options.constraints !== false) {
const target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
constraintOptions.onDelete = constraintOptions.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
constraintOptions.onUpdate = constraintOptions.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, constraintOptions, this.sourceKeyField);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.target.refreshAttributes();
this.source.refreshAttributes();
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.foreignKeyField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
Helpers.checkNamingCollision(this);
return this;
}
mixin(obj) {
const methods = ['get', 'count', 'hasSingle', 'hasAll', 'set', 'add', 'addMultiple', 'remove', 'removeMultiple', 'create'];
const aliases = {
hasSingle: 'has',
hasAll: 'has',
addMultiple: 'add',
removeMultiple: 'remove'
};
Helpers.mixinMethods(this, obj, methods, aliases);
}
/**
* Get everything currently associated with this, using an optional where clause.
*
* @param {Model|Array<Model>} instances source instances
* @param {Object} [options] find options
* @param {Object} [options.where] An optional where clause to limit the associated models
* @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
* @param {string} [options.schema] Apply a schema on the related model
*
* @see
* {@link Model.findAll} for a full explanation of options
*
* @returns {Promise<Array<Model>>}
*/
get(instances, options = {}) {
const where = {};
let Model = this.target;
let instance;
let values;
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
options = Object.assign({}, options);
if (this.scope) {
Object.assign(where, this.scope);
}
if (instances) {
values = instances.map(instance => instance.get(this.sourceKey, { raw: true }));
if (options.limit && instances.length > 1) {
options.groupedLimit = {
limit: options.limit,
on: this, // association
values
};
delete options.limit;
} else {
where[this.foreignKey] = {
[Op.in]: values
};
delete options.groupedLimit;
}
} else {
where[this.foreignKey] = instance.get(this.sourceKey, { raw: true });
}
options.where = options.where ?
{ [Op.and]: [where, options.where] } :
where;
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
Model = Model.unscoped();
} else {
Model = Model.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
Model = Model.schema(options.schema, options.schemaDelimiter);
}
return Model.findAll(options).then(results => {
if (instance) return results;
const result = {};
for (const instance of instances) {
result[instance.get(this.sourceKey, { raw: true })] = [];
}
for (const instance of results) {
result[instance.get(this.foreignKey, { raw: true })].push(instance);
}
return result;
});
}
/**
* Count everything currently associated with this, using an optional where clause.
*
* @param {Model} instance the source instance
* @param {Object} [options] find & count options
* @param {Object} [options.where] An optional where clause to limit the associated models
* @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
*
* @returns {Promise<number>}
*/
count(instance, options) {
options = Utils.cloneDeep(options);
options.attributes = [
[
this.sequelize.fn(
'COUNT',
this.sequelize.col(`${this.target.name}.${this.target.primaryKeyField}`)
),
'count'
]
];
options.raw = true;
options.plain = true;
return this.get(instance, options).then(result => parseInt(result.count, 10));
}
/**
* Check if one or more rows are associated with `this`.
*
* @param {Model} sourceInstance the source instance
* @param {Model|Model[]|string[]|string|number[]|number} [targetInstances] Can be an array of instances or their primary keys
* @param {Object} [options] Options passed to getAssociations
*
* @returns {Promise}
*/
has(sourceInstance, targetInstances, options) {
const where = {};
if (!Array.isArray(targetInstances)) {
targetInstances = [targetInstances];
}
options = Object.assign({}, options, {
scope: false,
attributes: [this.target.primaryKeyAttribute],
raw: true
});
where[Op.or] = targetInstances.map(instance => {
if (instance instanceof this.target) {
return instance.where();
}
return {
[this.target.primaryKeyAttribute]: instance
};
});
options.where = {
[Op.and]: [
where,
options.where
]
};
return this.get(sourceInstance, options).then(associatedObjects => associatedObjects.length === targetInstances.length);
}
/**
* Set the associated models by passing an array of persisted instances or their primary keys. Everything that is not in the passed array will be un-associated
*
* @param {Model} sourceInstance source instance to associate new instances with
* @param {Model|Model[]|string[]|string|number[]|number} [targetInstances] An array of persisted instances or primary key of instances to associate with this. Pass `null` or `undefined` to remove all associations.
* @param {Object} [options] Options passed to `target.findAll` and `update`.
* @param {Object} [options.validate] Run validation for the join model
*
* @returns {Promise}
*/
set(sourceInstance, targetInstances, options) {
if (targetInstances === null) {
targetInstances = [];
} else {
targetInstances = this.toInstanceArray(targetInstances);
}
return this.get(sourceInstance, _.defaults({ scope: false, raw: true }, options)).then(oldAssociations => {
const promises = [];
const obsoleteAssociations = oldAssociations.filter(old =>
!targetInstances.find(obj =>
obj[this.target.primaryKeyAttribute] === old[this.target.primaryKeyAttribute]
)
);
const unassociatedObjects = targetInstances.filter(obj =>
!oldAssociations.find(old =>
obj[this.target.primaryKeyAttribute] === old[this.target.primaryKeyAttribute]
)
);
let updateWhere;
let update;
if (obsoleteAssociations.length > 0) {
update = {};
update[this.foreignKey] = null;
updateWhere = {
[this.target.primaryKeyAttribute]: obsoleteAssociations.map(associatedObject =>
associatedObject[this.target.primaryKeyAttribute]
)
};
promises.push(this.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
if (unassociatedObjects.length > 0) {
updateWhere = {};
update = {};
update[this.foreignKey] = sourceInstance.get(this.sourceKey);
Object.assign(update, this.scope);
updateWhere[this.target.primaryKeyAttribute] = unassociatedObjects.map(unassociatedObject =>
unassociatedObject[this.target.primaryKeyAttribute]
);
promises.push(this.target.unscoped().update(
update,
_.defaults({
where: updateWhere
}, options)
));
}
return Utils.Promise.all(promises).return(sourceInstance);
});
}
/**
* Associate one or more target rows with `this`. This method accepts a Model / string / number to associate a single row,
* or a mixed array of Model / string / numbers to associate multiple rows.
*
* @param {Model} sourceInstance the source instance
* @param {Model|Model[]|string[]|string|number[]|number} [targetInstances] A single instance or primary key, or a mixed array of persisted instances or primary keys
* @param {Object} [options] Options passed to `target.update`.
*
* @returns {Promise}
*/
add(sourceInstance, targetInstances, options = {}) {
if (!targetInstances) return Utils.Promise.resolve();
const update = {};
targetInstances = this.toInstanceArray(targetInstances);
update[this.foreignKey] = sourceInstance.get(this.sourceKey);
Object.assign(update, this.scope);
const where = {
[this.target.primaryKeyAttribute]: targetInstances.map(unassociatedObject =>
unassociatedObject.get(this.target.primaryKeyAttribute)
)
};
return this.target.unscoped().update(update, _.defaults({ where }, options)).return(sourceInstance);
}
/**
* Un-associate one or several target rows.
*
* @param {Model} sourceInstance instance to un associate instances with
* @param {Model|Model[]|string|string[]|number|number[]} [targetInstances] Can be an Instance or its primary key, or a mixed array of instances and primary keys
* @param {Object} [options] Options passed to `target.update`
*
* @returns {Promise}
*/
remove(sourceInstance, targetInstances, options = {}) {
const update = {
[this.foreignKey]: null
};
targetInstances = this.toInstanceArray(targetInstances);
const where = {
[this.foreignKey]: sourceInstance.get(this.sourceKey),
[this.target.primaryKeyAttribute]: targetInstances.map(targetInstance =>
targetInstance.get(this.target.primaryKeyAttribute)
)
};
return this.target.unscoped().update(update, _.defaults({ where }, options)).return(this);
}
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Model} sourceInstance source instance
* @param {Object} [values] values for target model instance
* @param {Object} [options] Options passed to `target.create`
*
* @returns {Promise}
*/
create(sourceInstance, values, options = {}) {
if (Array.isArray(options)) {
options = {
fields: options
};
}
if (values === undefined) {
values = {};
}
if (this.scope) {
for (const attribute of Object.keys(this.scope)) {
values[attribute] = this.scope[attribute];
if (options.fields) options.fields.push(attribute);
}
}
values[this.foreignKey] = sourceInstance.get(this.sourceKey);
if (options.fields) options.fields.push(this.foreignKey);
return this.target.create(values, options);
}
verifyAssociationAlias(alias) {
if (typeof alias === 'string') {
return this.as === alias;
}
if (alias && alias.plural) {
return this.as === alias.plural;
}
return !this.isAliased;
}
}
module.exports = HasMany;
module.exports.HasMany = HasMany;
module.exports.default = HasMany;
+280
View File
@@ -0,0 +1,280 @@
'use strict';
const Utils = require('./../utils');
const Helpers = require('./helpers');
const _ = require('lodash');
const Association = require('./base');
const Op = require('../operators');
/**
* One-to-one association
*
* In the API reference below, add the name of the association to the method, e.g. for `User.hasOne(Project)` the getter will be `user.getProject()`.
* This is almost the same as `belongsTo` with one exception - The foreign key will be defined on the target model.
*
* @see {@link Model.hasOne}
*/
class HasOne extends Association {
constructor(source, target, options) {
super(source, target, options);
this.associationType = 'HasOne';
this.isSingleAssociation = true;
this.foreignKeyAttribute = {};
if (this.as) {
this.isAliased = true;
this.options.name = {
singular: this.as
};
} else {
this.as = this.target.options.name.singular;
this.options.name = this.target.options.name;
}
if (_.isObject(this.options.foreignKey)) {
this.foreignKeyAttribute = this.options.foreignKey;
this.foreignKey = this.foreignKeyAttribute.name || this.foreignKeyAttribute.fieldName;
} else if (this.options.foreignKey) {
this.foreignKey = this.options.foreignKey;
}
if (!this.foreignKey) {
this.foreignKey = Utils.camelize(
[
Utils.singularize(this.options.as || this.source.name),
this.source.primaryKeyAttribute
].join('_')
);
}
if (
this.options.sourceKey
&& !this.source.rawAttributes[this.options.sourceKey]
) {
throw new Error(`Unknown attribute "${this.options.sourceKey}" passed as sourceKey, define this attribute on model "${this.source.name}" first`);
}
this.sourceKey = this.sourceKeyAttribute = this.options.sourceKey || this.source.primaryKeyAttribute;
this.sourceKeyField = this.source.rawAttributes[this.sourceKey].field || this.sourceKey;
this.sourceKeyIsPrimary = this.sourceKey === this.source.primaryKeyAttribute;
this.associationAccessor = this.as;
this.options.useHooks = options.useHooks;
if (this.target.rawAttributes[this.foreignKey]) {
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
}
// Get singular name, trying to uppercase the first letter, unless the model forbids it
const singular = _.upperFirst(this.options.name.singular);
this.accessors = {
get: `get${singular}`,
set: `set${singular}`,
create: `create${singular}`
};
}
// the id is in the target table
_injectAttributes() {
const newAttributes = {};
newAttributes[this.foreignKey] = _.defaults({}, this.foreignKeyAttribute, {
type: this.options.keyType || this.source.rawAttributes[this.sourceKey].type,
allowNull: true
});
if (this.options.constraints !== false) {
const target = this.target.rawAttributes[this.foreignKey] || newAttributes[this.foreignKey];
this.options.onDelete = this.options.onDelete || (target.allowNull ? 'SET NULL' : 'CASCADE');
this.options.onUpdate = this.options.onUpdate || 'CASCADE';
}
Helpers.addForeignKeyConstraints(newAttributes[this.foreignKey], this.source, this.target, this.options, this.sourceKeyField);
Utils.mergeDefaults(this.target.rawAttributes, newAttributes);
this.target.refreshAttributes();
this.identifierField = this.target.rawAttributes[this.foreignKey].field || this.foreignKey;
Helpers.checkNamingCollision(this);
return this;
}
mixin(obj) {
const methods = ['get', 'set', 'create'];
Helpers.mixinMethods(this, obj, methods);
}
/**
* Get the associated instance.
*
* @param {Model|Array<Model>} instances source instances
* @param {Object} [options] find options
* @param {string|boolean} [options.scope] Apply a scope on the related model, or remove its default scope by passing false
* @param {string} [options.schema] Apply a schema on the related model
*
* @see
* {@link Model.findOne} for a full explanation of options
*
* @returns {Promise<Model>}
*/
get(instances, options) {
const where = {};
let Target = this.target;
let instance;
options = Utils.cloneDeep(options);
if (Object.prototype.hasOwnProperty.call(options, 'scope')) {
if (!options.scope) {
Target = Target.unscoped();
} else {
Target = Target.scope(options.scope);
}
}
if (Object.prototype.hasOwnProperty.call(options, 'schema')) {
Target = Target.schema(options.schema, options.schemaDelimiter);
}
if (!Array.isArray(instances)) {
instance = instances;
instances = undefined;
}
if (instances) {
where[this.foreignKey] = {
[Op.in]: instances.map(instance => instance.get(this.sourceKey))
};
} else {
where[this.foreignKey] = instance.get(this.sourceKey);
}
if (this.scope) {
Object.assign(where, this.scope);
}
options.where = options.where ?
{ [Op.and]: [where, options.where] } :
where;
if (instances) {
return Target.findAll(options).then(results => {
const result = {};
for (const instance of instances) {
result[instance.get(this.sourceKey, { raw: true })] = null;
}
for (const instance of results) {
result[instance.get(this.foreignKey, { raw: true })] = instance;
}
return result;
});
}
return Target.findOne(options);
}
/**
* Set the associated model.
*
* @param {Model} sourceInstance the source instance
* @param {?<Model>|string|number} [associatedInstance] An persisted instance or the primary key of an instance to associate with this. Pass `null` or `undefined` to remove the association.
* @param {Object} [options] Options passed to getAssociation and `target.save`
*
* @returns {Promise}
*/
set(sourceInstance, associatedInstance, options) {
let alreadyAssociated;
options = Object.assign({}, options, {
scope: false
});
return sourceInstance[this.accessors.get](options).then(oldInstance => {
// TODO Use equals method once #5605 is resolved
alreadyAssociated = oldInstance && associatedInstance && this.target.primaryKeyAttributes.every(attribute =>
oldInstance.get(attribute, { raw: true }) === (associatedInstance.get ? associatedInstance.get(attribute, { raw: true }) : associatedInstance)
);
if (oldInstance && !alreadyAssociated) {
oldInstance[this.foreignKey] = null;
return oldInstance.save(Object.assign({}, options, {
fields: [this.foreignKey],
allowNull: [this.foreignKey],
association: true
}));
}
}).then(() => {
if (associatedInstance && !alreadyAssociated) {
if (!(associatedInstance instanceof this.target)) {
const tmpInstance = {};
tmpInstance[this.target.primaryKeyAttribute] = associatedInstance;
associatedInstance = this.target.build(tmpInstance, {
isNewRecord: false
});
}
Object.assign(associatedInstance, this.scope);
associatedInstance.set(this.foreignKey, sourceInstance.get(this.sourceKeyAttribute));
return associatedInstance.save(options);
}
return null;
});
}
/**
* Create a new instance of the associated model and associate it with this.
*
* @param {Model} sourceInstance the source instance
* @param {Object} [values={}] values to create associated model instance with
* @param {Object} [options] Options passed to `target.create` and setAssociation.
*
* @see
* {@link Model#create} for a full explanation of options
*
* @returns {Promise<Model>} The created target model
*/
create(sourceInstance, values, options) {
values = values || {};
options = options || {};
if (this.scope) {
for (const attribute of Object.keys(this.scope)) {
values[attribute] = this.scope[attribute];
if (options.fields) {
options.fields.push(attribute);
}
}
}
values[this.foreignKey] = sourceInstance.get(this.sourceKeyAttribute);
if (options.fields) {
options.fields.push(this.foreignKey);
}
return this.target.create(values, options);
}
verifyAssociationAlias(alias) {
if (typeof alias === 'string') {
return this.as === alias;
}
if (alias && alias.singular) {
return this.as === alias.singular;
}
return !this.isAliased;
}
}
module.exports = HasOne;
+69
View File
@@ -0,0 +1,69 @@
'use strict';
function checkNamingCollision(association) {
if (Object.prototype.hasOwnProperty.call(association.source.rawAttributes, association.as)) {
throw new Error(
`Naming collision between attribute '${association.as}'` +
` and association '${association.as}' on model ${association.source.name}` +
'. To remedy this, change either foreignKey or as in your association definition'
);
}
}
exports.checkNamingCollision = checkNamingCollision;
function addForeignKeyConstraints(newAttribute, source, target, options, key) {
// FK constraints are opt-in: users must either set `foreignKeyConstraints`
// on the association, or request an `onDelete` or `onUpdate` behavior
if (options.foreignKeyConstraint || options.onDelete || options.onUpdate) {
// Find primary keys: composite keys not supported with this approach
const primaryKeys = Object.keys(source.primaryKeys)
.map(primaryKeyAttribute => source.rawAttributes[primaryKeyAttribute].field || primaryKeyAttribute);
if (primaryKeys.length === 1 || !primaryKeys.includes(key)) {
if (source._schema) {
newAttribute.references = {
model: source.sequelize.getQueryInterface().QueryGenerator.addSchema({
tableName: source.tableName,
_schema: source._schema,
_schemaDelimiter: source._schemaDelimiter
})
};
} else {
newAttribute.references = { model: source.tableName };
}
newAttribute.references.key = key || primaryKeys[0];
newAttribute.onDelete = options.onDelete;
newAttribute.onUpdate = options.onUpdate;
}
}
}
exports.addForeignKeyConstraints = addForeignKeyConstraints;
/**
* Mixin (inject) association methods to model prototype
*
* @private
*
* @param {Object} association instance
* @param {Object} obj Model prototype
* @param {Array} methods Method names to inject
* @param {Object} aliases Mapping between model and association method names
*
*/
function mixinMethods(association, obj, methods, aliases) {
aliases = aliases || {};
for (const method of methods) {
// don't override custom methods
if (!Object.prototype.hasOwnProperty.call(obj, association.accessors[method])) {
const realMethod = aliases[method] || method;
obj[association.accessors[method]] = function() {
return association[realMethod](this, ...Array.from(arguments));
};
}
}
}
exports.mixinMethods = mixinMethods;
+12
View File
@@ -0,0 +1,12 @@
'use strict';
const Association = require('./base');
Association.BelongsTo = require('./belongs-to');
Association.HasOne = require('./has-one');
Association.HasMany = require('./has-many');
Association.BelongsToMany = require('./belongs-to-many');
module.exports = Association;
module.exports.default = Association;
module.exports.Association = Association;
+124
View File
@@ -0,0 +1,124 @@
'use strict';
const _ = require('lodash');
const HasOne = require('./has-one');
const HasMany = require('./has-many');
const BelongsToMany = require('./belongs-to-many');
const BelongsTo = require('./belongs-to');
function isModel(model, sequelize) {
return model
&& model.prototype
&& model.prototype instanceof sequelize.Sequelize.Model;
}
const Mixin = {
hasMany(target, options = {}) {
if (!isModel(target, this.sequelize)) {
throw new Error(`${this.name}.hasMany called with something that's not a subclass of Sequelize.Model`);
}
const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options = Object.assign(options, _.omit(source.options, ['hooks']));
if (options.useHooks) {
this.runHooks('beforeAssociate', { source, target, type: HasMany }, options);
}
// the id is in the foreign table or in a connecting table
const association = new HasMany(source, target, options);
source.associations[association.associationAccessor] = association;
association._injectAttributes();
association.mixin(source.prototype);
if (options.useHooks) {
this.runHooks('afterAssociate', { source, target, type: HasMany, association }, options);
}
return association;
},
belongsToMany(target, options = {}) {
if (!isModel(target, this.sequelize)) {
throw new Error(`${this.name}.belongsToMany called with something that's not a subclass of Sequelize.Model`);
}
const source = this;
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
options.timestamps = options.timestamps === undefined ? this.sequelize.options.timestamps : options.timestamps;
options = Object.assign(options, _.omit(source.options, ['hooks', 'timestamps', 'scopes', 'defaultScope']));
if (options.useHooks) {
this.runHooks('beforeAssociate', { source, target, type: BelongsToMany }, options);
}
// the id is in the foreign table or in a connecting table
const association = new BelongsToMany(source, target, options);
source.associations[association.associationAccessor] = association;
association._injectAttributes();
association.mixin(source.prototype);
if (options.useHooks) {
this.runHooks('afterAssociate', { source, target, type: BelongsToMany, association }, options);
}
return association;
},
getAssociations(target) {
return _.values(this.associations).filter(association => association.target.name === target.name);
},
getAssociationForAlias(target, alias) {
// Two associations cannot have the same alias, so we can use find instead of filter
return this.getAssociations(target).find(association => association.verifyAssociationAlias(alias)) || null;
}
};
// The logic for hasOne and belongsTo is exactly the same
function singleLinked(Type) {
return function(target, options = {}) {
// eslint-disable-next-line no-invalid-this
const source = this;
if (!isModel(target, source.sequelize)) {
throw new Error(`${source.name}.${_.lowerFirst(Type.name)} called with something that's not a subclass of Sequelize.Model`);
}
// Since this is a mixin, we'll need a unique letiable name for hooks (since Model will override our hooks option)
options.hooks = options.hooks === undefined ? false : Boolean(options.hooks);
options.useHooks = options.hooks;
if (options.useHooks) {
source.runHooks('beforeAssociate', { source, target, type: Type }, options);
}
// the id is in the foreign table
const association = new Type(source, target, Object.assign(options, source.options));
source.associations[association.associationAccessor] = association;
association._injectAttributes();
association.mixin(source.prototype);
if (options.useHooks) {
source.runHooks('afterAssociate', { source, target, type: Type, association }, options);
}
return association;
};
}
Mixin.hasOne = singleLinked(HasOne);
Mixin.belongsTo = singleLinked(BelongsTo);
module.exports = Mixin;
module.exports.Mixin = Mixin;
module.exports.default = Mixin;
+1054
View File
File diff suppressed because it is too large Load Diff
+103
View File
@@ -0,0 +1,103 @@
'use strict';
const { classToInvokable } = require('./utils');
class ABSTRACT {
static toString(...args) {
return new this().toString(...args);
}
toString(...args) {
return this.toSql(...args);
}
toSql() {
throw new Error('toSql implementation missing');
}
}
class INITIALLY_DEFERRED extends ABSTRACT {
toSql() {
return 'DEFERRABLE INITIALLY DEFERRED';
}
}
class INITIALLY_IMMEDIATE extends ABSTRACT {
toSql() {
return 'DEFERRABLE INITIALLY IMMEDIATE';
}
}
class NOT extends ABSTRACT {
toSql() {
return 'NOT DEFERRABLE';
}
}
class SET_DEFERRED extends ABSTRACT {
constructor(constraints) {
super();
this.constraints = constraints;
}
toSql(queryGenerator) {
return queryGenerator.setDeferredQuery(this.constraints);
}
}
class SET_IMMEDIATE extends ABSTRACT {
constructor(constraints) {
super();
this.constraints = constraints;
}
toSql(queryGenerator) {
return queryGenerator.setImmediateQuery(this.constraints);
}
}
/**
* A collection of properties related to deferrable constraints. It can be used to
* make foreign key constraints deferrable and to set the constraints within a
* transaction. This is only supported in PostgreSQL.
*
* The foreign keys can be configured like this. It will create a foreign key
* that will check the constraints immediately when the data was inserted.
*
* ```js
* sequelize.define('Model', {
* foreign_id: {
* type: Sequelize.INTEGER,
* references: {
* model: OtherModel,
* key: 'id',
* deferrable: Sequelize.Deferrable.INITIALLY_IMMEDIATE
* }
* }
* });
* ```
*
* The constraints can be configured in a transaction like this. It will
* trigger a query once the transaction has been started and set the constraints
* to be checked at the very end of the transaction.
*
* ```js
* sequelize.transaction({
* deferrable: Sequelize.Deferrable.SET_DEFERRED
* });
* ```
*
* @property INITIALLY_DEFERRED Defer constraints checks to the end of transactions.
* @property INITIALLY_IMMEDIATE Trigger the constraint checks immediately
* @property NOT Set the constraints to not deferred. This is the default in PostgreSQL and it make it impossible to dynamically defer the constraints within a transaction.
* @property SET_DEFERRED
* @property SET_IMMEDIATE
*/
const Deferrable = module.exports = { // eslint-disable-line
INITIALLY_DEFERRED: classToInvokable(INITIALLY_DEFERRED),
INITIALLY_IMMEDIATE: classToInvokable(INITIALLY_IMMEDIATE),
NOT: classToInvokable(NOT),
SET_DEFERRED: classToInvokable(SET_DEFERRED),
SET_IMMEDIATE: classToInvokable(SET_IMMEDIATE)
};
+346
View File
@@ -0,0 +1,346 @@
'use strict';
const { Pool, TimeoutError } = require('sequelize-pool');
const _ = require('lodash');
const semver = require('semver');
const Promise = require('../../promise');
const errors = require('../../errors');
const { logger } = require('../../utils/logger');
const debug = logger.debugContext('pool');
/**
* Abstract Connection Manager
*
* Connection manager which handles pooling & replication.
* Uses sequelize-pool for pooling
*
* @private
*/
class ConnectionManager {
constructor(dialect, sequelize) {
const config = _.cloneDeep(sequelize.config);
this.sequelize = sequelize;
this.config = config;
this.dialect = dialect;
this.versionPromise = null;
this.dialectName = this.sequelize.options.dialect;
if (config.pool === false) {
throw new Error('Support for pool:false was removed in v4.0');
}
config.pool = _.defaults(config.pool || {}, {
max: 5,
min: 0,
idle: 10000,
acquire: 60000,
evict: 1000,
validate: this._validate.bind(this)
});
this.initPools();
}
refreshTypeParser(dataTypes) {
_.each(dataTypes, dataType => {
if (Object.prototype.hasOwnProperty.call(dataType, 'parse')) {
if (dataType.types[this.dialectName]) {
this._refreshTypeParser(dataType);
} else {
throw new Error(`Parse function not supported for type ${dataType.key} in dialect ${this.dialectName}`);
}
}
});
}
/**
* Try to load dialect module from various configured options.
* Priority goes like dialectModulePath > dialectModule > require(default)
*
* @param {string} moduleName Name of dialect module to lookup
*
* @private
* @returns {Object}
*/
_loadDialectModule(moduleName) {
try {
if (this.sequelize.config.dialectModulePath) {
return require(this.sequelize.config.dialectModulePath);
}
if (this.sequelize.config.dialectModule) {
return this.sequelize.config.dialectModule;
}
return require(moduleName);
} catch (err) {
if (err.code === 'MODULE_NOT_FOUND') {
if (this.sequelize.config.dialectModulePath) {
throw new Error(`Unable to find dialect at ${this.sequelize.config.dialectModulePath}`);
}
throw new Error(`Please install ${moduleName} package manually`);
}
throw err;
}
}
/**
* Handler which executes on process exit or connection manager shutdown
*
* @private
* @returns {Promise}
*/
_onProcessExit() {
if (!this.pool) {
return Promise.resolve();
}
return this.pool.drain().then(() => {
debug('connection drain due to process exit');
return this.pool.destroyAllNow();
});
}
/**
* Drain the pool and close it permanently
*
* @returns {Promise}
*/
close() {
// Mark close of pool
this.getConnection = function getConnection() {
return Promise.reject(new Error('ConnectionManager.getConnection was called after the connection manager was closed!'));
};
return this._onProcessExit();
}
/**
* Initialize connection pool. By default pool autostart is set to false, so no connection will be
* be created unless `pool.acquire` is called.
*/
initPools() {
const config = this.config;
if (!config.replication) {
this.pool = new Pool({
name: 'sequelize',
create: () => this._connect(config),
destroy: connection => {
return this._disconnect(connection)
.tap(() => { debug('connection destroy'); });
},
validate: config.pool.validate,
max: config.pool.max,
min: config.pool.min,
acquireTimeoutMillis: config.pool.acquire,
idleTimeoutMillis: config.pool.idle,
reapIntervalMillis: config.pool.evict
});
debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, no replication`);
return;
}
if (!Array.isArray(config.replication.read)) {
config.replication.read = [config.replication.read];
}
// Map main connection config
config.replication.write = _.defaults(config.replication.write, _.omit(config, 'replication'));
// Apply defaults to each read config
config.replication.read = config.replication.read.map(readConfig =>
_.defaults(readConfig, _.omit(this.config, 'replication'))
);
// custom pooling for replication (original author @janmeier)
let reads = 0;
this.pool = {
release: client => {
if (client.queryType === 'read') {
this.pool.read.release(client);
} else {
this.pool.write.release(client);
}
},
acquire: (queryType, useMaster) => {
useMaster = useMaster === undefined ? false : useMaster;
if (queryType === 'SELECT' && !useMaster) {
return this.pool.read.acquire();
}
return this.pool.write.acquire();
},
destroy: connection => {
this.pool[connection.queryType].destroy(connection);
debug('connection destroy');
},
destroyAllNow: () => {
return Promise.join(
this.pool.read.destroyAllNow(),
this.pool.write.destroyAllNow()
).tap(() => { debug('all connections destroyed'); });
},
drain: () => {
return Promise.join(
this.pool.write.drain(),
this.pool.read.drain()
);
},
read: new Pool({
name: 'sequelize:read',
create: () => {
// round robin config
const nextRead = reads++ % config.replication.read.length;
return this._connect(config.replication.read[nextRead]).tap(connection => {
connection.queryType = 'read';
});
},
destroy: connection => this._disconnect(connection),
validate: config.pool.validate,
max: config.pool.max,
min: config.pool.min,
acquireTimeoutMillis: config.pool.acquire,
idleTimeoutMillis: config.pool.idle,
reapIntervalMillis: config.pool.evict
}),
write: new Pool({
name: 'sequelize:write',
create: () => {
return this._connect(config.replication.write).tap(connection => {
connection.queryType = 'write';
});
},
destroy: connection => this._disconnect(connection),
validate: config.pool.validate,
max: config.pool.max,
min: config.pool.min,
acquireTimeoutMillis: config.pool.acquire,
idleTimeoutMillis: config.pool.idle,
reapIntervalMillis: config.pool.evict
})
};
debug(`pool created with max/min: ${config.pool.max}/${config.pool.min}, with replication`);
}
/**
* Get connection from pool. It sets database version if it's not already set.
* Call pool.acquire to get a connection
*
* @param {Object} [options] Pool options
* @param {string} [options.type] Set which replica to use. Available options are `read` and `write`
* @param {boolean} [options.useMaster=false] Force master or write replica to get connection from
*
* @returns {Promise<Connection>}
*/
getConnection(options) {
options = options || {};
let promise;
if (this.sequelize.options.databaseVersion === 0) {
if (this.versionPromise) {
promise = this.versionPromise;
} else {
promise = this.versionPromise = this._connect(this.config.replication.write || this.config)
.then(connection => {
const _options = {};
_options.transaction = { connection }; // Cheat .query to use our private connection
_options.logging = () => {};
_options.logging.__testLoggingFn = true;
//connection might have set databaseVersion value at initialization,
//avoiding a useless round trip
if (this.sequelize.options.databaseVersion === 0) {
return this.sequelize.databaseVersion(_options).then(version => {
const parsedVersion = _.get(semver.coerce(version), 'version') || version;
this.sequelize.options.databaseVersion = semver.valid(parsedVersion)
? parsedVersion
: this.defaultVersion;
this.versionPromise = null;
return this._disconnect(connection);
});
}
this.versionPromise = null;
return this._disconnect(connection);
}).catch(err => {
this.versionPromise = null;
throw err;
});
}
} else {
promise = Promise.resolve();
}
return promise.then(() => {
return this.pool.acquire(options.type, options.useMaster)
.catch(error => {
if (error instanceof TimeoutError) throw new errors.ConnectionAcquireTimeoutError(error);
throw error;
});
}).tap(() => { debug('connection acquired'); });
}
/**
* Release a pooled connection so it can be utilized by other connection requests
*
* @param {Connection} connection
*
* @returns {Promise}
*/
releaseConnection(connection) {
return Promise.try(() => {
this.pool.release(connection);
debug('connection released');
});
}
/**
* Call dialect library to get connection
*
* @param {*} config Connection config
* @private
* @returns {Promise<Connection>}
*/
_connect(config) {
return this.sequelize.runHooks('beforeConnect', config)
.then(() => this.dialect.connectionManager.connect(config))
.then(connection => this.sequelize.runHooks('afterConnect', connection, config).return(connection));
}
/**
* Call dialect library to disconnect a connection
*
* @param {Connection} connection
* @private
* @returns {Promise}
*/
_disconnect(connection) {
return this.sequelize.runHooks('beforeDisconnect', connection)
.then(() => this.dialect.connectionManager.disconnect(connection))
.then(() => this.sequelize.runHooks('afterDisconnect', connection));
}
/**
* Determine if a connection is still valid or not
*
* @param {Connection} connection
*
* @returns {boolean}
*/
_validate(connection) {
if (!this.dialect.connectionManager.validate) {
return true;
}
return this.dialect.connectionManager.validate(connection);
}
}
module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
+72
View File
@@ -0,0 +1,72 @@
'use strict';
class AbstractDialect {}
AbstractDialect.prototype.supports = {
'DEFAULT': true,
'DEFAULT VALUES': false,
'VALUES ()': false,
'LIMIT ON UPDATE': false,
'ON DUPLICATE KEY': true,
'ORDER NULLS': false,
'UNION': true,
'UNION ALL': true,
/* does the dialect support returning values for inserted/updated fields */
returnValues: false,
/* features specific to autoIncrement values */
autoIncrement: {
/* does the dialect require modification of insert queries when inserting auto increment fields */
identityInsert: false,
/* does the dialect support inserting default/null values for autoincrement fields */
defaultValue: true,
/* does the dialect support updating autoincrement fields */
update: true
},
/* Do we need to say DEFAULT for bulk insert */
bulkDefault: false,
schemas: false,
transactions: true,
settingIsolationLevelDuringTransaction: true,
transactionOptions: {
type: false
},
migrations: true,
upserts: true,
inserts: {
ignoreDuplicates: '', /* dialect specific words for INSERT IGNORE or DO NOTHING */
updateOnDuplicate: false, /* whether dialect supports ON DUPLICATE KEY UPDATE */
onConflictDoNothing: '' /* dialect specific words for ON CONFLICT DO NOTHING */
},
constraints: {
restrict: true,
addConstraint: true,
dropConstraint: true,
unique: true,
default: false,
check: true,
foreignKey: true,
primaryKey: true
},
index: {
collate: true,
length: false,
parser: false,
concurrently: false,
type: false,
using: true,
functionBased: false
},
joinTableDependent: true,
groupedLimit: true,
indexViaAlter: false,
JSON: false,
deferrableConstraints: false
};
module.exports = AbstractDialect;
module.exports.AbstractDialect = AbstractDialect;
module.exports.default = AbstractDialect;
+2653
View File
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,87 @@
/**
* Quote helpers implement quote ability for all dialects.
* These are basic block of query building
*
* Its better to implement all dialect implementation together here. Which will allow
* even abstract generator to use them by just specifying dialect type.
*
* Defining these helpers in each query dialect will leave
* code in dual dependency of abstract <-> specific dialect
*/
'use strict';
const Utils = require('../../../../utils');
/**
* list of reserved words in PostgreSQL 10
* source: https://www.postgresql.org/docs/10/static/sql-keywords-appendix.html
*
* @private
*/
const postgresReservedWords = 'all,analyse,analyze,and,any,array,as,asc,asymmetric,authorization,binary,both,case,cast,check,collate,collation,column,concurrently,constraint,create,cross,current_catalog,current_date,current_role,current_schema,current_time,current_timestamp,current_user,default,deferrable,desc,distinct,do,else,end,except,false,fetch,for,foreign,freeze,from,full,grant,group,having,ilike,in,initially,inner,intersect,into,is,isnull,join,lateral,leading,left,like,limit,localtime,localtimestamp,natural,not,notnull,null,offset,on,only,or,order,outer,overlaps,placing,primary,references,returning,right,select,session_user,similar,some,symmetric,table,tablesample,then,to,trailing,true,union,unique,user,using,variadic,verbose,when,where,window,with'.split(',');
/**
*
* @param {string} dialect Dialect name
* @param {string} identifier Identifier to quote
* @param {Object} [options]
* @param {boolean} [options.force=false]
* @param {boolean} [options.quoteIdentifiers=true]
*
* @returns {string}
* @private
*/
function quoteIdentifier(dialect, identifier, options) {
if (identifier === '*') return identifier;
options = Utils.defaults(options || {}, {
force: false,
quoteIdentifiers: true
});
switch (dialect) {
case 'sqlite':
case 'mariadb':
case 'mysql':
return Utils.addTicks(Utils.removeTicks(identifier, '`'), '`');
case 'postgres':
const rawIdentifier = Utils.removeTicks(identifier, '"');
if (
options.force !== true &&
options.quoteIdentifiers === false &&
!identifier.includes('.') &&
!identifier.includes('->') &&
!postgresReservedWords.includes(rawIdentifier.toLowerCase())
) {
// In Postgres, if tables or attributes are created double-quoted,
// they are also case sensitive. If they contain any uppercase
// characters, they must always be double-quoted. This makes it
// impossible to write queries in portable SQL if tables are created in
// this way. Hence, we strip quotes if we don't want case sensitivity.
return rawIdentifier;
}
return Utils.addTicks(rawIdentifier, '"');
case 'mssql':
return `[${identifier.replace(/[[\]']+/g, '')}]`;
default:
throw new Error(`Dialect "${dialect}" is not supported`);
}
}
module.exports.quoteIdentifier = quoteIdentifier;
/**
* Test if a give string is already quoted
*
* @param {string} identifier
*
* @returns {boolean}
* @private
*/
function isIdentifierQuoted(identifier) {
return /^\s*(?:([`"'])(?:(?!\1).|\1{2})*\1\.?)+\s*$/i.test(identifier);
}
module.exports.isIdentifierQuoted = isIdentifierQuoted;
@@ -0,0 +1,84 @@
'use strict';
const _ = require('lodash');
const Op = require('../../../operators');
const Utils = require('../../../utils');
const OperatorHelpers = {
OperatorMap: {
[Op.eq]: '=',
[Op.ne]: '!=',
[Op.gte]: '>=',
[Op.gt]: '>',
[Op.lte]: '<=',
[Op.lt]: '<',
[Op.not]: 'IS NOT',
[Op.is]: 'IS',
[Op.in]: 'IN',
[Op.notIn]: 'NOT IN',
[Op.like]: 'LIKE',
[Op.notLike]: 'NOT LIKE',
[Op.iLike]: 'ILIKE',
[Op.notILike]: 'NOT ILIKE',
[Op.startsWith]: 'LIKE',
[Op.endsWith]: 'LIKE',
[Op.substring]: 'LIKE',
[Op.regexp]: '~',
[Op.notRegexp]: '!~',
[Op.iRegexp]: '~*',
[Op.notIRegexp]: '!~*',
[Op.between]: 'BETWEEN',
[Op.notBetween]: 'NOT BETWEEN',
[Op.overlap]: '&&',
[Op.contains]: '@>',
[Op.contained]: '<@',
[Op.adjacent]: '-|-',
[Op.strictLeft]: '<<',
[Op.strictRight]: '>>',
[Op.noExtendRight]: '&<',
[Op.noExtendLeft]: '&>',
[Op.any]: 'ANY',
[Op.all]: 'ALL',
[Op.and]: ' AND ',
[Op.or]: ' OR ',
[Op.col]: 'COL',
[Op.placeholder]: '$$PLACEHOLDER$$'
},
OperatorsAliasMap: {},
setOperatorsAliases(aliases) {
if (!aliases || _.isEmpty(aliases)) {
this.OperatorsAliasMap = false;
} else {
this.OperatorsAliasMap = Object.assign({}, aliases);
}
},
_replaceAliases(orig) {
const obj = {};
if (!this.OperatorsAliasMap) {
return orig;
}
Utils.getOperators(orig).forEach(op => {
const item = orig[op];
if (_.isPlainObject(item)) {
obj[op] = this._replaceAliases(item);
} else {
obj[op] = item;
}
});
_.forOwn(orig, (item, prop) => {
prop = this.OperatorsAliasMap[prop] || prop;
if (_.isPlainObject(item)) {
item = this._replaceAliases(item);
}
obj[prop] = item;
});
return obj;
}
};
module.exports = OperatorHelpers;
@@ -0,0 +1,80 @@
'use strict';
const uuidv4 = require('uuid/v4');
const TransactionQueries = {
/**
* Returns a query that sets the transaction isolation level.
*
* @param {string} value The isolation level.
* @param {Object} options An object with options.
* @returns {string} The generated sql query.
* @private
*/
setIsolationLevelQuery(value, options) {
if (options.parent) {
return;
}
return `SET TRANSACTION ISOLATION LEVEL ${value};`;
},
generateTransactionId() {
return uuidv4();
},
/**
* Returns a query that starts a transaction.
*
* @param {Transaction} transaction
* @returns {string} The generated sql query.
* @private
*/
startTransactionQuery(transaction) {
if (transaction.parent) {
// force quoting of savepoint identifiers for postgres
return `SAVEPOINT ${this.quoteIdentifier(transaction.name, true)};`;
}
return 'START TRANSACTION;';
},
deferConstraintsQuery() {},
setConstraintQuery() {},
setDeferredQuery() {},
setImmediateQuery() {},
/**
* Returns a query that commits a transaction.
*
* @param {Transaction} transaction An object with options.
* @returns {string} The generated sql query.
* @private
*/
commitTransactionQuery(transaction) {
if (transaction.parent) {
return;
}
return 'COMMIT;';
},
/**
* Returns a query that rollbacks a transaction.
*
* @param {Transaction} transaction
* @returns {string} The generated sql query.
* @private
*/
rollbackTransactionQuery(transaction) {
if (transaction.parent) {
// force quoting of savepoint identifiers for postgres
return `ROLLBACK TO SAVEPOINT ${this.quoteIdentifier(transaction.name, true)};`;
}
return 'ROLLBACK;';
}
};
module.exports = TransactionQueries;
+741
View File
@@ -0,0 +1,741 @@
'use strict';
const _ = require('lodash');
const SqlString = require('../../sql-string');
const QueryTypes = require('../../query-types');
const Dot = require('dottie');
const deprecations = require('../../utils/deprecations');
const uuid = require('uuid/v4');
class AbstractQuery {
constructor(connection, sequelize, options) {
this.uuid = uuid();
this.connection = connection;
this.instance = options.instance;
this.model = options.model;
this.sequelize = sequelize;
this.options = Object.assign({
plain: false,
raw: false,
// eslint-disable-next-line no-console
logging: console.log
}, options || {});
this.checkLoggingOption();
}
/**
* rewrite query with parameters
*
* Examples:
*
* query.formatBindParameters('select $1 as foo', ['fooval']);
*
* query.formatBindParameters('select $foo as foo', { foo: 'fooval' });
*
* Options
* skipUnescape: bool, skip unescaping $$
* skipValueReplace: bool, do not replace (but do unescape $$). Check correct syntax and if all values are available
*
* @param {string} sql
* @param {Object|Array} values
* @param {string} dialect
* @param {Function} [replacementFunc]
* @param {Object} [options]
* @private
*/
static formatBindParameters(sql, values, dialect, replacementFunc, options) {
if (!values) {
return [sql, []];
}
options = options || {};
if (typeof replacementFunc !== 'function') {
options = replacementFunc || {};
replacementFunc = undefined;
}
if (!replacementFunc) {
if (options.skipValueReplace) {
replacementFunc = (match, key, values) => {
if (values[key] !== undefined) {
return match;
}
return undefined;
};
} else {
replacementFunc = (match, key, values, timeZone, dialect) => {
if (values[key] !== undefined) {
return SqlString.escape(values[key], timeZone, dialect);
}
return undefined;
};
}
} else if (options.skipValueReplace) {
const origReplacementFunc = replacementFunc;
replacementFunc = (match, key, values, timeZone, dialect, options) => {
if (origReplacementFunc(match, key, values, timeZone, dialect, options) !== undefined) {
return match;
}
return undefined;
};
}
const timeZone = null;
const list = Array.isArray(values);
sql = sql.replace(/\$(\$|\w+)/g, (match, key) => {
if ('$' === key) {
return options.skipUnescape ? match : key;
}
let replVal;
if (list) {
if (key.match(/^[1-9]\d*$/)) {
key = key - 1;
replVal = replacementFunc(match, key, values, timeZone, dialect, options);
}
} else if (!key.match(/^\d*$/)) {
replVal = replacementFunc(match, key, values, timeZone, dialect, options);
}
if (replVal === undefined) {
throw new Error(`Named bind parameter "${match}" has no value in the given object.`);
}
return replVal;
});
return [sql, []];
}
/**
* Execute the passed sql query.
*
* Examples:
*
* query.run('SELECT 1')
*
* @private
*/
run() {
throw new Error('The run method wasn\'t overwritten!');
}
/**
* Check the logging option of the instance and print deprecation warnings.
*
* @private
*/
checkLoggingOption() {
if (this.options.logging === true) {
deprecations.noTrueLogging();
// eslint-disable-next-line no-console
this.options.logging = console.log;
}
}
/**
* Get the attributes of an insert query, which contains the just inserted id.
*
* @returns {string} The field name.
* @private
*/
getInsertIdField() {
return 'insertId';
}
getUniqueConstraintErrorMessage(field) {
let message = field ? `${field} must be unique` : 'Must be unique';
if (field && this.model) {
for (const key of Object.keys(this.model.uniqueKeys)) {
if (this.model.uniqueKeys[key].fields.includes(field.replace(/"/g, ''))) {
if (this.model.uniqueKeys[key].msg) {
message = this.model.uniqueKeys[key].msg;
}
}
}
}
return message;
}
isRawQuery() {
return this.options.type === QueryTypes.RAW;
}
isVersionQuery() {
return this.options.type === QueryTypes.VERSION;
}
isUpsertQuery() {
return this.options.type === QueryTypes.UPSERT;
}
isInsertQuery(results, metaData) {
let result = true;
if (this.options.type === QueryTypes.INSERT) {
return true;
}
// is insert query if sql contains insert into
result = result && this.sql.toLowerCase().startsWith('insert into');
// is insert query if no results are passed or if the result has the inserted id
result = result && (!results || Object.prototype.hasOwnProperty.call(results, this.getInsertIdField()));
// is insert query if no metadata are passed or if the metadata has the inserted id
result = result && (!metaData || Object.prototype.hasOwnProperty.call(metaData, this.getInsertIdField()));
return result;
}
handleInsertQuery(results, metaData) {
if (this.instance) {
// add the inserted row id to the instance
const autoIncrementAttribute = this.model.autoIncrementAttribute;
let id = null;
id = id || results && results[this.getInsertIdField()];
id = id || metaData && metaData[this.getInsertIdField()];
this.instance[autoIncrementAttribute] = id;
}
}
isShowTablesQuery() {
return this.options.type === QueryTypes.SHOWTABLES;
}
handleShowTablesQuery(results) {
return _.flatten(results.map(resultSet => _.values(resultSet)));
}
isShowIndexesQuery() {
return this.options.type === QueryTypes.SHOWINDEXES;
}
isShowConstraintsQuery() {
return this.options.type === QueryTypes.SHOWCONSTRAINTS;
}
isDescribeQuery() {
return this.options.type === QueryTypes.DESCRIBE;
}
isSelectQuery() {
return this.options.type === QueryTypes.SELECT;
}
isBulkUpdateQuery() {
return this.options.type === QueryTypes.BULKUPDATE;
}
isBulkDeleteQuery() {
return this.options.type === QueryTypes.BULKDELETE;
}
isForeignKeysQuery() {
return this.options.type === QueryTypes.FOREIGNKEYS;
}
isUpdateQuery() {
return this.options.type === QueryTypes.UPDATE;
}
handleSelectQuery(results) {
let result = null;
// Map raw fields to names if a mapping is provided
if (this.options.fieldMap) {
const fieldMap = this.options.fieldMap;
results = results.map(result => _.reduce(fieldMap, (result, name, field) => {
if (result[field] !== undefined && name !== field) {
result[name] = result[field];
delete result[field];
}
return result;
}, result));
}
// Raw queries
if (this.options.raw) {
result = results.map(result => {
let o = {};
for (const key in result) {
if (Object.prototype.hasOwnProperty.call(result, key)) {
o[key] = result[key];
}
}
if (this.options.nest) {
o = Dot.transform(o);
}
return o;
});
// Queries with include
} else if (this.options.hasJoin === true) {
results = AbstractQuery._groupJoinData(results, {
model: this.model,
includeMap: this.options.includeMap,
includeNames: this.options.includeNames
}, {
checkExisting: this.options.hasMultiAssociation
});
result = this.model.bulkBuild(results, {
isNewRecord: false,
include: this.options.include,
includeNames: this.options.includeNames,
includeMap: this.options.includeMap,
includeValidated: true,
attributes: this.options.originalAttributes || this.options.attributes,
raw: true
});
// Regular queries
} else {
result = this.model.bulkBuild(results, {
isNewRecord: false,
raw: true,
attributes: this.options.originalAttributes || this.options.attributes
});
}
// return the first real model instance if options.plain is set (e.g. Model.find)
if (this.options.plain) {
result = result.length === 0 ? null : result[0];
}
return result;
}
isShowOrDescribeQuery() {
let result = false;
result = result || this.sql.toLowerCase().startsWith('show');
result = result || this.sql.toLowerCase().startsWith('describe');
return result;
}
isCallQuery() {
return this.sql.toLowerCase().startsWith('call');
}
/**
* @param {string} sql
* @param {Function} debugContext
* @param {Array|Object} parameters
* @protected
* @returns {Function} A function to call after the query was completed.
*/
_logQuery(sql, debugContext, parameters) {
const { connection, options } = this;
const benchmark = this.sequelize.options.benchmark || options.benchmark;
const logQueryParameters = this.sequelize.options.logQueryParameters || options.logQueryParameters;
const startTime = Date.now();
let logParameter = '';
if (logQueryParameters && parameters) {
const delimiter = sql.endsWith(';') ? '' : ';';
let paramStr;
if (Array.isArray(parameters)) {
paramStr = parameters.map(p=>JSON.stringify(p)).join(', ');
} else {
paramStr = JSON.stringify(parameters);
}
logParameter = `${delimiter} ${paramStr}`;
}
const fmt = `(${connection.uuid || 'default'}): ${sql}${logParameter}`;
const msg = `Executing ${fmt}`;
debugContext(msg);
if (!benchmark) {
this.sequelize.log(`Executing ${fmt}`, options);
}
return () => {
const afterMsg = `Executed ${fmt}`;
debugContext(afterMsg);
if (benchmark) {
this.sequelize.log(afterMsg, Date.now() - startTime, options);
}
};
}
/**
* The function takes the result of the query execution and groups
* the associated data by the callee.
*
* Example:
* groupJoinData([
* {
* some: 'data',
* id: 1,
* association: { foo: 'bar', id: 1 }
* }, {
* some: 'data',
* id: 1,
* association: { foo: 'bar', id: 2 }
* }, {
* some: 'data',
* id: 1,
* association: { foo: 'bar', id: 3 }
* }
* ])
*
* Result:
* Something like this:
*
* [
* {
* some: 'data',
* id: 1,
* association: [
* { foo: 'bar', id: 1 },
* { foo: 'bar', id: 2 },
* { foo: 'bar', id: 3 }
* ]
* }
* ]
*
* @param {Array} rows
* @param {Object} includeOptions
* @param {Object} options
* @private
*/
static _groupJoinData(rows, includeOptions, options) {
/*
* Assumptions
* ID is not necessarily the first field
* All fields for a level is grouped in the same set (i.e. Panel.id, Task.id, Panel.title is not possible)
* Parent keys will be seen before any include/child keys
* Previous set won't necessarily be parent set (one parent could have two children, one child would then be previous set for the other)
*/
/*
* Author (MH) comment: This code is an unreadable mess, but it's performant.
* groupJoinData is a performance critical function so we prioritize perf over readability.
*/
if (!rows.length) {
return [];
}
// Generic looping
let i;
let length;
let $i;
let $length;
// Row specific looping
let rowsI;
let row;
const rowsLength = rows.length;
// Key specific looping
let keys;
let key;
let keyI;
let keyLength;
let prevKey;
let values;
let topValues;
let topExists;
const checkExisting = options.checkExisting;
// If we don't have to deduplicate we can pre-allocate the resulting array
let itemHash;
let parentHash;
let topHash;
const results = checkExisting ? [] : new Array(rowsLength);
const resultMap = {};
const includeMap = {};
// Result variables for the respective functions
let $keyPrefix;
let $keyPrefixString;
let $prevKeyPrefixString; // eslint-disable-line
let $prevKeyPrefix;
let $lastKeyPrefix;
let $current;
let $parent;
// Map each key to an include option
let previousPiece;
const buildIncludeMap = piece => {
if (Object.prototype.hasOwnProperty.call($current.includeMap, piece)) {
includeMap[key] = $current = $current.includeMap[piece];
if (previousPiece) {
previousPiece = `${previousPiece}.${piece}`;
} else {
previousPiece = piece;
}
includeMap[previousPiece] = $current;
}
};
// Calculate the string prefix of a key ('User.Results' for 'User.Results.id')
const keyPrefixStringMemo = {};
const keyPrefixString = (key, memo) => {
if (!Object.prototype.hasOwnProperty.call(memo, key)) {
memo[key] = key.substr(0, key.lastIndexOf('.'));
}
return memo[key];
};
// Removes the prefix from a key ('id' for 'User.Results.id')
const removeKeyPrefixMemo = {};
const removeKeyPrefix = key => {
if (!Object.prototype.hasOwnProperty.call(removeKeyPrefixMemo, key)) {
const index = key.lastIndexOf('.');
removeKeyPrefixMemo[key] = key.substr(index === -1 ? 0 : index + 1);
}
return removeKeyPrefixMemo[key];
};
// Calculates the array prefix of a key (['User', 'Results'] for 'User.Results.id')
const keyPrefixMemo = {};
const keyPrefix = key => {
// We use a double memo and keyPrefixString so that different keys with the same prefix will receive the same array instead of differnet arrays with equal values
if (!Object.prototype.hasOwnProperty.call(keyPrefixMemo, key)) {
const prefixString = keyPrefixString(key, keyPrefixStringMemo);
if (!Object.prototype.hasOwnProperty.call(keyPrefixMemo, prefixString)) {
keyPrefixMemo[prefixString] = prefixString ? prefixString.split('.') : [];
}
keyPrefixMemo[key] = keyPrefixMemo[prefixString];
}
return keyPrefixMemo[key];
};
// Calcuate the last item in the array prefix ('Results' for 'User.Results.id')
const lastKeyPrefixMemo = {};
const lastKeyPrefix = key => {
if (!Object.prototype.hasOwnProperty.call(lastKeyPrefixMemo, key)) {
const prefix = keyPrefix(key);
const length = prefix.length;
lastKeyPrefixMemo[key] = !length ? '' : prefix[length - 1];
}
return lastKeyPrefixMemo[key];
};
const getUniqueKeyAttributes = model => {
let uniqueKeyAttributes = _.chain(model.uniqueKeys);
uniqueKeyAttributes = uniqueKeyAttributes
.result(`${uniqueKeyAttributes.findKey()}.fields`)
.map(field => _.findKey(model.attributes, chr => chr.field === field))
.value();
return uniqueKeyAttributes;
};
const stringify = obj => obj instanceof Buffer ? obj.toString('hex') : obj;
let primaryKeyAttributes;
let uniqueKeyAttributes;
let prefix;
for (rowsI = 0; rowsI < rowsLength; rowsI++) {
row = rows[rowsI];
// Keys are the same for all rows, so only need to compute them on the first row
if (rowsI === 0) {
keys = Object.keys(row);
keyLength = keys.length;
}
if (checkExisting) {
topExists = false;
// Compute top level hash key (this is usually just the primary key values)
$length = includeOptions.model.primaryKeyAttributes.length;
topHash = '';
if ($length === 1) {
topHash = stringify(row[includeOptions.model.primaryKeyAttributes[0]]);
}
else if ($length > 1) {
for ($i = 0; $i < $length; $i++) {
topHash += stringify(row[includeOptions.model.primaryKeyAttributes[$i]]);
}
}
else if (!_.isEmpty(includeOptions.model.uniqueKeys)) {
uniqueKeyAttributes = getUniqueKeyAttributes(includeOptions.model);
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
topHash += row[uniqueKeyAttributes[$i]];
}
}
}
topValues = values = {};
$prevKeyPrefix = undefined;
for (keyI = 0; keyI < keyLength; keyI++) {
key = keys[keyI];
// The string prefix isn't actualy needed
// We use it so keyPrefix for different keys will resolve to the same array if they have the same prefix
// TODO: Find a better way?
$keyPrefixString = keyPrefixString(key, keyPrefixStringMemo);
$keyPrefix = keyPrefix(key);
// On the first row we compute the includeMap
if (rowsI === 0 && !Object.prototype.hasOwnProperty.call(includeMap, key)) {
if (!$keyPrefix.length) {
includeMap[key] = includeMap[''] = includeOptions;
} else {
$current = includeOptions;
previousPiece = undefined;
$keyPrefix.forEach(buildIncludeMap);
}
}
// End of key set
if ($prevKeyPrefix !== undefined && $prevKeyPrefix !== $keyPrefix) {
if (checkExisting) {
// Compute hash key for this set instance
// TODO: Optimize
length = $prevKeyPrefix.length;
$parent = null;
parentHash = null;
if (length) {
for (i = 0; i < length; i++) {
prefix = $parent ? `${$parent}.${$prevKeyPrefix[i]}` : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
itemHash = prefix;
if ($length === 1) {
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[0]}`]);
}
else if ($length > 1) {
for ($i = 0; $i < $length; $i++) {
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[$i]}`]);
}
}
else if (!_.isEmpty(includeMap[prefix].model.uniqueKeys)) {
uniqueKeyAttributes = getUniqueKeyAttributes(includeMap[prefix].model);
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
itemHash += row[`${prefix}.${uniqueKeyAttributes[$i]}`];
}
}
if (!parentHash) {
parentHash = topHash;
}
itemHash = parentHash + itemHash;
$parent = prefix;
if (i < length - 1) {
parentHash = itemHash;
}
}
} else {
itemHash = topHash;
}
if (itemHash === topHash) {
if (!resultMap[itemHash]) {
resultMap[itemHash] = values;
} else {
topExists = true;
}
} else if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
if (includeMap[prevKey].association.isSingleAssociation) {
if ($parent) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
}
} else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
// Reset values
values = {};
} else {
// If checkExisting is false it's because there's only 1:1 associations in this query
// However we still need to map onto the appropriate parent
// For 1:1 we map forward, initializing the value object on the parent to be filled in the next iterations of the loop
$current = topValues;
length = $keyPrefix.length;
if (length) {
for (i = 0; i < length; i++) {
if (i === length - 1) {
values = $current[$keyPrefix[i]] = {};
}
$current = $current[$keyPrefix[i]] || {};
}
}
}
}
// End of iteration, set value and set prev values (for next iteration)
values[removeKeyPrefix(key)] = row[key];
prevKey = key;
$prevKeyPrefix = $keyPrefix;
$prevKeyPrefixString = $keyPrefixString;
}
if (checkExisting) {
length = $prevKeyPrefix.length;
$parent = null;
parentHash = null;
if (length) {
for (i = 0; i < length; i++) {
prefix = $parent ? `${$parent}.${$prevKeyPrefix[i]}` : $prevKeyPrefix[i];
primaryKeyAttributes = includeMap[prefix].model.primaryKeyAttributes;
$length = primaryKeyAttributes.length;
itemHash = prefix;
if ($length === 1) {
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[0]}`]);
}
else if ($length > 0) {
for ($i = 0; $i < $length; $i++) {
itemHash += stringify(row[`${prefix}.${primaryKeyAttributes[$i]}`]);
}
}
else if (!_.isEmpty(includeMap[prefix].model.uniqueKeys)) {
uniqueKeyAttributes = getUniqueKeyAttributes(includeMap[prefix].model);
for ($i = 0; $i < uniqueKeyAttributes.length; $i++) {
itemHash += row[`${prefix}.${uniqueKeyAttributes[$i]}`];
}
}
if (!parentHash) {
parentHash = topHash;
}
itemHash = parentHash + itemHash;
$parent = prefix;
if (i < length - 1) {
parentHash = itemHash;
}
}
} else {
itemHash = topHash;
}
if (itemHash === topHash) {
if (!resultMap[itemHash]) {
resultMap[itemHash] = values;
} else {
topExists = true;
}
} else if (!resultMap[itemHash]) {
$parent = resultMap[parentHash];
$lastKeyPrefix = lastKeyPrefix(prevKey);
if (includeMap[prevKey].association.isSingleAssociation) {
if ($parent) {
$parent[$lastKeyPrefix] = resultMap[itemHash] = values;
}
} else {
if (!$parent[$lastKeyPrefix]) {
$parent[$lastKeyPrefix] = [];
}
$parent[$lastKeyPrefix].push(resultMap[itemHash] = values);
}
}
if (!topExists) {
results.push(topValues);
}
} else {
results[rowsI] = topValues;
}
}
return results;
}
}
module.exports = AbstractQuery;
module.exports.AbstractQuery = AbstractQuery;
module.exports.default = AbstractQuery;
+145
View File
@@ -0,0 +1,145 @@
'use strict';
const AbstractConnectionManager = require('../abstract/connection-manager');
const SequelizeErrors = require('../../errors');
const Promise = require('../../promise');
const { logger } = require('../../utils/logger');
const DataTypes = require('../../data-types').mariadb;
const momentTz = require('moment-timezone');
const debug = logger.debugContext('connection:mariadb');
const parserStore = require('../parserStore')('mariadb');
/**
* MariaDB Connection Manager
*
* Get connections, validate and disconnect them.
* AbstractConnectionManager pooling use it to handle MariaDB specific connections
* Use https://github.com/MariaDB/mariadb-connector-nodejs to connect with MariaDB server
*
* @extends AbstractConnectionManager
* @returns Class<ConnectionManager>
* @private
*/
class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) {
sequelize.config.port = sequelize.config.port || 3306;
super(dialect, sequelize);
this.lib = this._loadDialectModule('mariadb');
this.refreshTypeParser(DataTypes);
}
static _typecast(field, next) {
if (parserStore.get(field.type)) {
return parserStore.get(field.type)(field, this.sequelize.options, next);
}
return next();
}
_refreshTypeParser(dataType) {
parserStore.refresh(dataType);
}
_clearTypeParser() {
parserStore.clear();
}
/**
* Connect with MariaDB database based on config, Handle any errors in connection
* Set the pool handlers on connection.error
* Also set proper timezone once connection is connected.
*
* @param {Object} config
* @returns {Promise<Connection>}
* @private
*/
connect(config) {
// Named timezone is not supported in mariadb, convert to offset
let tzOffset = this.sequelize.options.timezone;
tzOffset = /\//.test(tzOffset) ? momentTz.tz(tzOffset).format('Z')
: tzOffset;
const connectionConfig = {
host: config.host,
port: config.port,
user: config.username,
password: config.password,
database: config.database,
timezone: tzOffset,
typeCast: ConnectionManager._typecast.bind(this),
bigNumberStrings: false,
supportBigNumbers: true,
foundRows: false
};
if (config.dialectOptions) {
Object.assign(connectionConfig, config.dialectOptions);
}
if (!this.sequelize.config.keepDefaultTimezone) {
// set timezone for this connection
if (connectionConfig.initSql) {
if (!Array.isArray(
connectionConfig.initSql)) {
connectionConfig.initSql = [connectionConfig.initSql];
}
connectionConfig.initSql.push(`SET time_zone = '${tzOffset}'`);
} else {
connectionConfig.initSql = `SET time_zone = '${tzOffset}'`;
}
}
return this.lib.createConnection(connectionConfig)
.then(connection => {
this.sequelize.options.databaseVersion = connection.serverVersion();
debug('connection acquired');
connection.on('error', error => {
switch (error.code) {
case 'ESOCKET':
case 'ECONNRESET':
case 'EPIPE':
case 'PROTOCOL_CONNECTION_LOST':
this.pool.destroy(connection);
}
});
return connection;
})
.catch(err => {
switch (err.code) {
case 'ECONNREFUSED':
throw new SequelizeErrors.ConnectionRefusedError(err);
case 'ER_ACCESS_DENIED_ERROR':
case 'ER_ACCESS_DENIED_NO_PASSWORD_ERROR':
throw new SequelizeErrors.AccessDeniedError(err);
case 'ENOTFOUND':
throw new SequelizeErrors.HostNotFoundError(err);
case 'EHOSTUNREACH':
case 'ENETUNREACH':
case 'EADDRNOTAVAIL':
throw new SequelizeErrors.HostNotReachableError(err);
case 'EINVAL':
throw new SequelizeErrors.InvalidConnectionError(err);
default:
throw new SequelizeErrors.ConnectionError(err);
}
});
}
disconnect(connection) {
// Don't disconnect connections with CLOSED state
if (!connection.isValid()) {
debug('connection tried to disconnect but was already at CLOSED state');
return Promise.resolve();
}
//wrap native Promise into bluebird
return Promise.resolve(connection.end());
}
validate(connection) {
return connection && connection.isValid();
}
}
module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
+122
View File
@@ -0,0 +1,122 @@
'use strict';
const _ = require('lodash');
const moment = require('moment-timezone');
module.exports = BaseTypes => {
BaseTypes.ABSTRACT.prototype.dialectTypes = 'https://mariadb.com/kb/en/library/resultset/#field-types';
/**
* types: [buffer_type, ...]
* @see documentation : https://mariadb.com/kb/en/library/resultset/#field-types
* @see connector implementation : https://github.com/MariaDB/mariadb-connector-nodejs/blob/master/lib/const/field-type.js
*/
BaseTypes.DATE.types.mariadb = ['DATETIME'];
BaseTypes.STRING.types.mariadb = ['VAR_STRING'];
BaseTypes.CHAR.types.mariadb = ['STRING'];
BaseTypes.TEXT.types.mariadb = ['BLOB'];
BaseTypes.TINYINT.types.mariadb = ['TINY'];
BaseTypes.SMALLINT.types.mariadb = ['SHORT'];
BaseTypes.MEDIUMINT.types.mariadb = ['INT24'];
BaseTypes.INTEGER.types.mariadb = ['LONG'];
BaseTypes.BIGINT.types.mariadb = ['LONGLONG'];
BaseTypes.FLOAT.types.mariadb = ['FLOAT'];
BaseTypes.TIME.types.mariadb = ['TIME'];
BaseTypes.DATEONLY.types.mariadb = ['DATE'];
BaseTypes.BOOLEAN.types.mariadb = ['TINY'];
BaseTypes.BLOB.types.mariadb = ['TINYBLOB', 'BLOB', 'LONGBLOB'];
BaseTypes.DECIMAL.types.mariadb = ['NEWDECIMAL'];
BaseTypes.UUID.types.mariadb = false;
BaseTypes.ENUM.types.mariadb = false;
BaseTypes.REAL.types.mariadb = ['DOUBLE'];
BaseTypes.DOUBLE.types.mariadb = ['DOUBLE'];
BaseTypes.GEOMETRY.types.mariadb = ['GEOMETRY'];
BaseTypes.JSON.types.mariadb = ['JSON'];
class DECIMAL extends BaseTypes.DECIMAL {
toSql() {
let definition = super.toSql();
if (this._unsigned) {
definition += ' UNSIGNED';
}
if (this._zerofill) {
definition += ' ZEROFILL';
}
return definition;
}
}
class DATE extends BaseTypes.DATE {
toSql() {
return `DATETIME${this._length ? `(${this._length})` : ''}`;
}
_stringify(date, options) {
date = this._applyTimezone(date, options);
return date.format('YYYY-MM-DD HH:mm:ss.SSS');
}
static parse(value, options) {
value = value.string();
if (value === null) {
return value;
}
if (moment.tz.zone(options.timezone)) {
value = moment.tz(value, options.timezone).toDate();
}
else {
value = new Date(`${value} ${options.timezone}`);
}
return value;
}
}
class DATEONLY extends BaseTypes.DATEONLY {
static parse(value) {
return value.string();
}
}
class UUID extends BaseTypes.UUID {
toSql() {
return 'CHAR(36) BINARY';
}
}
class GEOMETRY extends BaseTypes.GEOMETRY {
constructor(type, srid) {
super(type, srid);
if (_.isEmpty(this.type)) {
this.sqlType = this.key;
}
else {
this.sqlType = this.type;
}
}
toSql() {
return this.sqlType;
}
}
class ENUM extends BaseTypes.ENUM {
toSql(options) {
return `ENUM(${this.values.map(value => options.escape(value)).join(', ')})`;
}
}
class JSONTYPE extends BaseTypes.JSON {
_stringify(value, options) {
return options.operation === 'where' && typeof value === 'string' ? value
: JSON.stringify(value);
}
}
return {
ENUM,
DATE,
DATEONLY,
UUID,
GEOMETRY,
DECIMAL,
JSON: JSONTYPE
};
};
+61
View File
@@ -0,0 +1,61 @@
'use strict';
const _ = require('lodash');
const AbstractDialect = require('../abstract');
const ConnectionManager = require('./connection-manager');
const Query = require('./query');
const QueryGenerator = require('./query-generator');
const DataTypes = require('../../data-types').mariadb;
class MariadbDialect extends AbstractDialect {
constructor(sequelize) {
super();
this.sequelize = sequelize;
this.connectionManager = new ConnectionManager(this, sequelize);
this.QueryGenerator = new QueryGenerator({
_dialect: this,
sequelize
});
}
}
MariadbDialect.prototype.supports = _.merge(
_.cloneDeep(AbstractDialect.prototype.supports), {
'VALUES ()': true,
'LIMIT ON UPDATE': true,
lock: true,
forShare: 'LOCK IN SHARE MODE',
settingIsolationLevelDuringTransaction: false,
schemas: true,
inserts: {
ignoreDuplicates: ' IGNORE',
updateOnDuplicate: ' ON DUPLICATE KEY UPDATE'
},
index: {
collate: false,
length: true,
parser: true,
type: true,
using: 1
},
constraints: {
dropConstraint: false,
check: false
},
indexViaAlter: true,
NUMERIC: true,
GEOMETRY: true,
JSON: true,
REGEXP: true
});
ConnectionManager.prototype.defaultVersion = '5.5.3';
MariadbDialect.prototype.Query = Query;
MariadbDialect.prototype.QueryGenerator = QueryGenerator;
MariadbDialect.prototype.DataTypes = DataTypes;
MariadbDialect.prototype.name = 'mariadb';
MariadbDialect.prototype.TICK_CHAR = '`';
MariadbDialect.prototype.TICK_CHAR_LEFT = MariadbDialect.prototype.TICK_CHAR;
MariadbDialect.prototype.TICK_CHAR_RIGHT = MariadbDialect.prototype.TICK_CHAR;
module.exports = MariadbDialect;
+38
View File
@@ -0,0 +1,38 @@
'use strict';
const MySQLQueryGenerator = require('../mysql/query-generator');
class MariaDBQueryGenerator extends MySQLQueryGenerator {
createSchema(schema, options) {
options = Object.assign({
charset: null,
collate: null
}, options || {});
const charset = options.charset ? ` DEFAULT CHARACTER SET ${this.escape(options.charset)}` : '';
const collate = options.collate ? ` DEFAULT COLLATE ${this.escape(options.collate)}` : '';
return `CREATE SCHEMA IF NOT EXISTS ${this.quoteIdentifier(schema)}${charset}${collate};`;
}
dropSchema(schema) {
return `DROP SCHEMA IF EXISTS ${this.quoteIdentifier(schema)};`;
}
showSchemasQuery(options) {
const skip = options.skip && Array.isArray(options.skip) && options.skip.length > 0 ? options.skip : null;
return `SELECT SCHEMA_NAME as schema_name FROM INFORMATION_SCHEMA.SCHEMATA WHERE SCHEMA_NAME NOT IN ('MYSQL', 'INFORMATION_SCHEMA', 'PERFORMANCE_SCHEMA'${skip ? skip.reduce( (sql, schemaName) => sql += `,${this.escape(schemaName)}`, '') : ''});`;
}
showTablesQuery(database) {
let query = 'SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\'';
if (database) {
query += ` AND TABLE_SCHEMA = ${this.escape(database)}`;
} else {
query += ' AND TABLE_SCHEMA NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\')';
}
return `${query};`;
}
}
module.exports = MariaDBQueryGenerator;
+325
View File
@@ -0,0 +1,325 @@
'use strict';
const AbstractQuery = require('../abstract/query');
const sequelizeErrors = require('../../errors');
const _ = require('lodash');
const DataTypes = require('../../data-types');
const Promise = require('../../promise');
const { logger } = require('../../utils/logger');
const ER_DUP_ENTRY = 1062;
const ER_ROW_IS_REFERENCED = 1451;
const ER_NO_REFERENCED_ROW = 1452;
const debug = logger.debugContext('sql:mariadb');
class Query extends AbstractQuery {
constructor(connection, sequelize, options) {
super(connection, sequelize, Object.assign({ showWarnings: false }, options));
}
static formatBindParameters(sql, values, dialect) {
const bindParam = [];
const replacementFunc = (match, key, val) => {
if (val[key] !== undefined) {
bindParam.push(val[key]);
return '?';
}
return undefined;
};
sql = AbstractQuery.formatBindParameters(sql, values, dialect,
replacementFunc)[0];
return [sql, bindParam.length > 0 ? bindParam : undefined];
}
run(sql, parameters) {
this.sql = sql;
const { connection, options } = this;
const showWarnings = this.sequelize.options.showWarnings
|| options.showWarnings;
const complete = this._logQuery(sql, debug, parameters);
if (parameters) {
debug('parameters(%j)', parameters);
}
return Promise.resolve(
connection.query(this.sql, parameters)
.then(results => {
complete();
// Log warnings if we've got them.
if (showWarnings && results && results.warningStatus > 0) {
return this.logWarnings(results);
}
return results;
})
.catch(err => {
// MariaDB automatically rolls-back transactions in the event of a deadlock
if (options.transaction && err.errno === 1213) {
options.transaction.finished = 'rollback';
}
complete();
err.sql = sql;
err.parameters = parameters;
throw this.formatError(err);
})
)
// Log warnings if we've got them.
.then(results => {
if (showWarnings && results && results.warningStatus > 0) {
return this.logWarnings(results);
}
return results;
})
// Return formatted results...
.then(results => this.formatResults(results));
}
/**
* High level function that handles the results of a query execution.
*
*
* Example:
* query.formatResults([
* {
* id: 1, // this is from the main table
* attr2: 'snafu', // this is from the main table
* Tasks.id: 1, // this is from the associated table
* Tasks.title: 'task' // this is from the associated table
* }
* ])
*
* @param {Array} data - The result of the query execution.
* @private
*/
formatResults(data) {
let result = this.instance;
if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery()
|| this.isUpsertQuery()) {
return data.affectedRows;
}
if (this.isInsertQuery(data)) {
this.handleInsertQuery(data);
if (!this.instance) {
// handle bulkCreate AI primary key
if (this.model
&& this.model.autoIncrementAttribute
&& this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
&& this.model.rawAttributes[this.model.primaryKeyAttribute]
) {
//ONLY TRUE IF @auto_increment_increment is set to 1 !!
//Doesn't work with GALERA => each node will reserve increment (x for first server, x+1 for next node ...
const startId = data[this.getInsertIdField()];
result = new Array(data.affectedRows);
const pkField = this.model.rawAttributes[this.model.primaryKeyAttribute].field;
for (let i = 0; i < data.affectedRows; i++) {
result[i] = { [pkField]: startId + i };
}
return [result, data.affectedRows];
}
return [data[this.getInsertIdField()], data.affectedRows];
}
}
if (this.isSelectQuery()) {
this.handleJsonSelectQuery(data);
return this.handleSelectQuery(data);
}
if (this.isInsertQuery() || this.isUpdateQuery()) {
return [result, data.affectedRows];
}
if (this.isCallQuery()) {
return data[0];
}
if (this.isRawQuery()) {
const meta = data.meta;
delete data.meta;
return [data, meta];
}
if (this.isShowIndexesQuery()) {
return this.handleShowIndexesQuery(data);
}
if (this.isForeignKeysQuery() || this.isShowConstraintsQuery()) {
return data;
}
if (this.isShowTablesQuery()) {
return this.handleShowTablesQuery(data);
}
if (this.isDescribeQuery()) {
result = {};
for (const _result of data) {
result[_result.Field] = {
type: _result.Type.toLowerCase().startsWith('enum') ? _result.Type.replace(/^enum/i,
'ENUM') : _result.Type.toUpperCase(),
allowNull: _result.Null === 'YES',
defaultValue: _result.Default,
primaryKey: _result.Key === 'PRI',
autoIncrement: Object.prototype.hasOwnProperty.call(_result, 'Extra')
&& _result.Extra.toLowerCase() === 'auto_increment',
comment: _result.Comment ? _result.Comment : null
};
}
return result;
}
if (this.isVersionQuery()) {
return data[0].version;
}
return result;
}
handleJsonSelectQuery(rows) {
if (!this.model || !this.model.fieldRawAttributesMap) {
return;
}
for (const _field of Object.keys(this.model.fieldRawAttributesMap)) {
const modelField = this.model.fieldRawAttributesMap[_field];
if (modelField.type instanceof DataTypes.JSON) {
//value is return as String, no JSON
rows = rows.map(row => {
row[modelField.fieldName] = row[modelField.fieldName] ? JSON.parse(
row[modelField.fieldName]) : null;
if (DataTypes.JSON.parse) {
return DataTypes.JSON.parse(modelField, this.sequelize.options,
row[modelField.fieldName]);
}
return row;
});
}
}
}
logWarnings(results) {
return this.run('SHOW WARNINGS').then(warningResults => {
const warningMessage = `MariaDB Warnings (${this.connection.uuid
|| 'default'}): `;
const messages = [];
for (const _warningRow of warningResults) {
if (_warningRow === undefined || typeof _warningRow[Symbol.iterator]
!== 'function') {
continue;
}
for (const _warningResult of _warningRow) {
if (Object.prototype.hasOwnProperty.call(_warningResult, 'Message')) {
messages.push(_warningResult.Message);
} else {
for (const _objectKey of _warningResult.keys()) {
messages.push(
[_objectKey, _warningResult[_objectKey]].join(': '));
}
}
}
}
this.sequelize.log(warningMessage + messages.join('; '), this.options);
return results;
});
}
formatError(err) {
switch (err.errno) {
case ER_DUP_ENTRY: {
const match = err.message.match(
/Duplicate entry '([\s\S]*)' for key '?((.|\s)*?)'?\s.*$/);
let fields = {};
let message = 'Validation error';
const values = match ? match[1].split('-') : undefined;
const fieldKey = match ? match[2] : undefined;
const fieldVal = match ? match[1] : undefined;
const uniqueKey = this.model && this.model.uniqueKeys[fieldKey];
if (uniqueKey) {
if (uniqueKey.msg) {
message = uniqueKey.msg;
}
fields = _.zipObject(uniqueKey.fields, values);
} else {
fields[fieldKey] = fieldVal;
}
const errors = [];
_.forOwn(fields, (value, field) => {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
value,
this.instance,
'not_unique'
));
});
return new sequelizeErrors.UniqueConstraintError(
{ message, errors, parent: err, fields });
}
case ER_ROW_IS_REFERENCED:
case ER_NO_REFERENCED_ROW: {
// e.g. CONSTRAINT `example_constraint_name` FOREIGN KEY (`example_id`) REFERENCES `examples` (`id`)
const match = err.message.match(
/CONSTRAINT ([`"])(.*)\1 FOREIGN KEY \(\1(.*)\1\) REFERENCES \1(.*)\1 \(\1(.*)\1\)/);
const quoteChar = match ? match[1] : '`';
const fields = match ? match[3].split(
new RegExp(`${quoteChar}, *${quoteChar}`)) : undefined;
return new sequelizeErrors.ForeignKeyConstraintError({
reltype: err.errno === 1451 ? 'parent' : 'child',
table: match ? match[4] : undefined,
fields,
value: fields && fields.length && this.instance
&& this.instance[fields[0]] || undefined,
index: match ? match[2] : undefined,
parent: err
});
}
default:
return new sequelizeErrors.DatabaseError(err);
}
}
handleShowTablesQuery(results) {
return results.map(resultSet => ({
tableName: resultSet.TABLE_NAME,
schema: resultSet.TABLE_SCHEMA
}));
}
handleShowIndexesQuery(data) {
let currItem;
const result = [];
data.forEach(item => {
if (!currItem || currItem.name !== item.Key_name) {
currItem = {
primary: item.Key_name === 'PRIMARY',
fields: [],
name: item.Key_name,
tableName: item.Table,
unique: item.Non_unique !== 1,
type: item.Index_type
};
result.push(currItem);
}
currItem.fields[item.Seq_in_index - 1] = {
attribute: item.Column_name,
length: item.Sub_part || undefined,
order: item.Collation === 'A' ? 'ASC' : undefined
};
});
return result;
}
}
module.exports = Query;
+180
View File
@@ -0,0 +1,180 @@
'use strict';
const AbstractConnectionManager = require('../abstract/connection-manager');
const ResourceLock = require('./resource-lock');
const Promise = require('../../promise');
const { logger } = require('../../utils/logger');
const sequelizeErrors = require('../../errors');
const DataTypes = require('../../data-types').mssql;
const parserStore = require('../parserStore')('mssql');
const debug = logger.debugContext('connection:mssql');
const debugTedious = logger.debugContext('connection:mssql:tedious');
class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) {
sequelize.config.port = sequelize.config.port || 1433;
super(dialect, sequelize);
this.lib = this._loadDialectModule('tedious');
this.refreshTypeParser(DataTypes);
}
_refreshTypeParser(dataType) {
parserStore.refresh(dataType);
}
_clearTypeParser() {
parserStore.clear();
}
connect(config) {
const connectionConfig = {
server: config.host,
authentication: {
type: 'default',
options: {
userName: config.username || undefined,
password: config.password || undefined
}
},
options: {
port: parseInt(config.port, 10),
database: config.database,
encrypt: false
}
};
if (config.dialectOptions) {
// only set port if no instance name was provided
if (
config.dialectOptions.options &&
config.dialectOptions.options.instanceName
) {
delete connectionConfig.options.port;
}
if (config.dialectOptions.authentication) {
Object.assign(connectionConfig.authentication, config.dialectOptions.authentication);
}
Object.assign(connectionConfig.options, config.dialectOptions.options);
}
return new Promise((resolve, reject) => {
const connection = new this.lib.Connection(connectionConfig);
connection.lib = this.lib;
const resourceLock = new ResourceLock(connection);
const connectHandler = error => {
connection.removeListener('end', endHandler);
connection.removeListener('error', errorHandler);
if (error) return reject(error);
debug('connection acquired');
resolve(resourceLock);
};
const endHandler = () => {
connection.removeListener('connect', connectHandler);
connection.removeListener('error', errorHandler);
reject(new Error('Connection was closed by remote server'));
};
const errorHandler = error => {
connection.removeListener('connect', connectHandler);
connection.removeListener('end', endHandler);
reject(error);
};
connection.once('error', errorHandler);
connection.once('end', endHandler);
connection.once('connect', connectHandler);
/*
* Permanently attach this event before connection is even acquired
* tedious sometime emits error even after connect(with error).
*
* If we dont attach this even that unexpected error event will crash node process
*
* E.g. connectTimeout is set higher than requestTimeout
*/
connection.on('error', error => {
switch (error.code) {
case 'ESOCKET':
case 'ECONNRESET':
this.pool.destroy(resourceLock);
}
});
if (config.dialectOptions && config.dialectOptions.debug) {
connection.on('debug', debugTedious.log.bind(debugTedious));
}
}).catch(error => {
if (!error.code) {
throw new sequelizeErrors.ConnectionError(error);
}
switch (error.code) {
case 'ESOCKET':
if (error.message.includes('connect EHOSTUNREACH')) {
throw new sequelizeErrors.HostNotReachableError(error);
}
if (error.message.includes('connect ENETUNREACH')) {
throw new sequelizeErrors.HostNotReachableError(error);
}
if (error.message.includes('connect EADDRNOTAVAIL')) {
throw new sequelizeErrors.HostNotReachableError(error);
}
if (error.message.includes('getaddrinfo ENOTFOUND')) {
throw new sequelizeErrors.HostNotFoundError(error);
}
if (error.message.includes('connect ECONNREFUSED')) {
throw new sequelizeErrors.ConnectionRefusedError(error);
}
throw new sequelizeErrors.ConnectionError(error);
case 'ER_ACCESS_DENIED_ERROR':
case 'ELOGIN':
throw new sequelizeErrors.AccessDeniedError(error);
case 'EINVAL':
throw new sequelizeErrors.InvalidConnectionError(error);
default:
throw new sequelizeErrors.ConnectionError(error);
}
});
}
disconnect(connectionLock) {
/**
* Abstract connection may try to disconnect raw connection used for fetching version
*/
const connection = connectionLock.unwrap
? connectionLock.unwrap()
: connectionLock;
// Don't disconnect a connection that is already disconnected
if (connection.closed) {
return Promise.resolve();
}
return new Promise(resolve => {
connection.on('end', resolve);
connection.close();
debug('connection closed');
});
}
validate(connectionLock) {
/**
* Abstract connection may try to validate raw connection used for fetching version
*/
const connection = connectionLock.unwrap
? connectionLock.unwrap()
: connectionLock;
return connection && connection.loggedIn;
}
}
module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
+216
View File
@@ -0,0 +1,216 @@
'use strict';
const moment = require('moment');
module.exports = BaseTypes => {
const warn = BaseTypes.ABSTRACT.warn.bind(undefined, 'https://msdn.microsoft.com/en-us/library/ms187752%28v=sql.110%29.aspx');
/**
* Removes unsupported MSSQL options, i.e., LENGTH, UNSIGNED and ZEROFILL, for the integer data types.
*
* @param {Object} dataType The base integer data type.
* @private
*/
function removeUnsupportedIntegerOptions(dataType) {
if (dataType._length || dataType.options.length || dataType._unsigned || dataType._zerofill) {
warn(`MSSQL does not support '${dataType.key}' with options. Plain '${dataType.key}' will be used instead.`);
dataType._length = undefined;
dataType.options.length = undefined;
dataType._unsigned = undefined;
dataType._zerofill = undefined;
}
}
/**
* types: [hex, ...]
* @see hex here https://github.com/tediousjs/tedious/blob/master/src/data-type.js
*/
BaseTypes.DATE.types.mssql = [43];
BaseTypes.STRING.types.mssql = [231, 173];
BaseTypes.CHAR.types.mssql = [175];
BaseTypes.TEXT.types.mssql = false;
// https://msdn.microsoft.com/en-us/library/ms187745(v=sql.110).aspx
BaseTypes.TINYINT.types.mssql = [30];
BaseTypes.SMALLINT.types.mssql = [34];
BaseTypes.MEDIUMINT.types.mssql = false;
BaseTypes.INTEGER.types.mssql = [38];
BaseTypes.BIGINT.types.mssql = false;
BaseTypes.FLOAT.types.mssql = [109];
BaseTypes.TIME.types.mssql = [41];
BaseTypes.DATEONLY.types.mssql = [40];
BaseTypes.BOOLEAN.types.mssql = [104];
BaseTypes.BLOB.types.mssql = [165];
BaseTypes.DECIMAL.types.mssql = [106];
BaseTypes.UUID.types.mssql = false;
BaseTypes.ENUM.types.mssql = false;
BaseTypes.REAL.types.mssql = [109];
BaseTypes.DOUBLE.types.mssql = [109];
// BaseTypes.GEOMETRY.types.mssql = [240]; // not yet supported
BaseTypes.GEOMETRY.types.mssql = false;
class BLOB extends BaseTypes.BLOB {
toSql() {
if (this._length) {
if (this._length.toLowerCase() === 'tiny') { // tiny = 2^8
warn('MSSQL does not support BLOB with the `length` = `tiny` option. `VARBINARY(256)` will be used instead.');
return 'VARBINARY(256)';
}
warn('MSSQL does not support BLOB with the `length` option. `VARBINARY(MAX)` will be used instead.');
}
return 'VARBINARY(MAX)';
}
_hexify(hex) {
return `0x${hex}`;
}
}
class STRING extends BaseTypes.STRING {
toSql() {
if (!this._binary) {
return `NVARCHAR(${this._length})`;
}
return `BINARY(${this._length})`;
}
_stringify(value, options) {
if (this._binary) {
return BLOB.prototype._stringify(value);
}
return options.escape(value);
}
_bindParam(value, options) {
return options.bindParam(this._binary ? Buffer.from(value) : value);
}
}
STRING.prototype.escape = false;
class TEXT extends BaseTypes.TEXT {
toSql() {
// TEXT is deprecated in mssql and it would normally be saved as a non-unicode string.
// Using unicode is just future proof
if (this._length) {
if (this._length.toLowerCase() === 'tiny') { // tiny = 2^8
warn('MSSQL does not support TEXT with the `length` = `tiny` option. `NVARCHAR(256)` will be used instead.');
return 'NVARCHAR(256)';
}
warn('MSSQL does not support TEXT with the `length` option. `NVARCHAR(MAX)` will be used instead.');
}
return 'NVARCHAR(MAX)';
}
}
class BOOLEAN extends BaseTypes.BOOLEAN {
toSql() {
return 'BIT';
}
}
class UUID extends BaseTypes.UUID {
toSql() {
return 'CHAR(36)';
}
}
class NOW extends BaseTypes.NOW {
toSql() {
return 'GETDATE()';
}
}
class DATE extends BaseTypes.DATE {
toSql() {
return 'DATETIMEOFFSET';
}
}
class DATEONLY extends BaseTypes.DATEONLY {
static parse(value) {
return moment(value).format('YYYY-MM-DD');
}
}
class INTEGER extends BaseTypes.INTEGER {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class TINYINT extends BaseTypes.TINYINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class SMALLINT extends BaseTypes.SMALLINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class BIGINT extends BaseTypes.BIGINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class REAL extends BaseTypes.REAL {
constructor(length, decimals) {
super(length, decimals);
// MSSQL does not support any options for real
if (this._length || this.options.length || this._unsigned || this._zerofill) {
warn('MSSQL does not support REAL with options. Plain `REAL` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._unsigned = undefined;
this._zerofill = undefined;
}
}
}
class FLOAT extends BaseTypes.FLOAT {
constructor(length, decimals) {
super(length, decimals);
// MSSQL does only support lengths as option.
// Values between 1-24 result in 7 digits precision (4 bytes storage size)
// Values between 25-53 result in 15 digits precision (8 bytes storage size)
// If decimals are provided remove these and print a warning
if (this._decimals) {
warn('MSSQL does not support Float with decimals. Plain `FLOAT` will be used instead.');
this._length = undefined;
this.options.length = undefined;
}
if (this._unsigned) {
warn('MSSQL does not support Float unsigned. `UNSIGNED` was removed.');
this._unsigned = undefined;
}
if (this._zerofill) {
warn('MSSQL does not support Float zerofill. `ZEROFILL` was removed.');
this._zerofill = undefined;
}
}
}
class ENUM extends BaseTypes.ENUM {
toSql() {
return 'VARCHAR(255)';
}
}
return {
BLOB,
BOOLEAN,
ENUM,
STRING,
UUID,
DATE,
DATEONLY,
NOW,
TINYINT,
SMALLINT,
INTEGER,
BIGINT,
REAL,
FLOAT,
TEXT
};
};
+63
View File
@@ -0,0 +1,63 @@
'use strict';
const _ = require('lodash');
const AbstractDialect = require('../abstract');
const ConnectionManager = require('./connection-manager');
const Query = require('./query');
const QueryGenerator = require('./query-generator');
const DataTypes = require('../../data-types').mssql;
class MssqlDialect extends AbstractDialect {
constructor(sequelize) {
super();
this.sequelize = sequelize;
this.connectionManager = new ConnectionManager(this, sequelize);
this.QueryGenerator = new QueryGenerator({
_dialect: this,
sequelize
});
}
}
MssqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
'DEFAULT': true,
'DEFAULT VALUES': true,
'LIMIT ON UPDATE': true,
'ORDER NULLS': false,
lock: false,
transactions: true,
migrations: false,
returnValues: {
output: true
},
schemas: true,
autoIncrement: {
identityInsert: true,
defaultValue: false,
update: false
},
constraints: {
restrict: false,
default: true
},
index: {
collate: false,
length: false,
parser: false,
type: true,
using: false,
where: true
},
NUMERIC: true,
tmpTableTrigger: true
});
ConnectionManager.prototype.defaultVersion = '12.0.2000'; // SQL Server 2014 Express
MssqlDialect.prototype.Query = Query;
MssqlDialect.prototype.name = 'mssql';
MssqlDialect.prototype.TICK_CHAR = '"';
MssqlDialect.prototype.TICK_CHAR_LEFT = '[';
MssqlDialect.prototype.TICK_CHAR_RIGHT = ']';
MssqlDialect.prototype.DataTypes = DataTypes;
module.exports = MssqlDialect;
+899
View File
@@ -0,0 +1,899 @@
'use strict';
const _ = require('lodash');
const Utils = require('../../utils');
const DataTypes = require('../../data-types');
const TableHints = require('../../table-hints');
const AbstractQueryGenerator = require('../abstract/query-generator');
const randomBytes = require('crypto').randomBytes;
const semver = require('semver');
const Op = require('../../operators');
/* istanbul ignore next */
const throwMethodUndefined = function(methodName) {
throw new Error(`The method "${methodName}" is not defined! Please add it to your sql dialect.`);
};
class MSSQLQueryGenerator extends AbstractQueryGenerator {
createDatabaseQuery(databaseName, options) {
options = Object.assign({
collate: null
}, options || {});
const collation = options.collate ? `COLLATE ${this.escape(options.collate)}` : '';
return [
'IF NOT EXISTS (SELECT * FROM sys.databases WHERE name =', wrapSingleQuote(databaseName), ')',
'BEGIN',
'CREATE DATABASE', this.quoteIdentifier(databaseName),
`${collation};`,
'END;'
].join(' ');
}
dropDatabaseQuery(databaseName) {
return [
'IF EXISTS (SELECT * FROM sys.databases WHERE name =', wrapSingleQuote(databaseName), ')',
'BEGIN',
'DROP DATABASE', this.quoteIdentifier(databaseName), ';',
'END;'
].join(' ');
}
createSchema(schema) {
return [
'IF NOT EXISTS (SELECT schema_name',
'FROM information_schema.schemata',
'WHERE schema_name =', wrapSingleQuote(schema), ')',
'BEGIN',
"EXEC sp_executesql N'CREATE SCHEMA",
this.quoteIdentifier(schema),
";'",
'END;'
].join(' ');
}
dropSchema(schema) {
// Mimics Postgres CASCADE, will drop objects belonging to the schema
const quotedSchema = wrapSingleQuote(schema);
return [
'IF EXISTS (SELECT schema_name',
'FROM information_schema.schemata',
'WHERE schema_name =', quotedSchema, ')',
'BEGIN',
'DECLARE @id INT, @ms_sql NVARCHAR(2000);',
'DECLARE @cascade TABLE (',
'id INT NOT NULL IDENTITY PRIMARY KEY,',
'ms_sql NVARCHAR(2000) NOT NULL );',
'INSERT INTO @cascade ( ms_sql )',
"SELECT CASE WHEN o.type IN ('F','PK')",
"THEN N'ALTER TABLE ['+ s.name + N'].[' + p.name + N'] DROP CONSTRAINT [' + o.name + N']'",
"ELSE N'DROP TABLE ['+ s.name + N'].[' + o.name + N']' END",
'FROM sys.objects o',
'JOIN sys.schemas s on o.schema_id = s.schema_id',
'LEFT OUTER JOIN sys.objects p on o.parent_object_id = p.object_id',
"WHERE o.type IN ('F', 'PK', 'U') AND s.name = ", quotedSchema,
'ORDER BY o.type ASC;',
'SELECT TOP 1 @id = id, @ms_sql = ms_sql FROM @cascade ORDER BY id;',
'WHILE @id IS NOT NULL',
'BEGIN',
'BEGIN TRY EXEC sp_executesql @ms_sql; END TRY',
'BEGIN CATCH BREAK; THROW; END CATCH;',
'DELETE FROM @cascade WHERE id = @id;',
'SELECT @id = NULL, @ms_sql = NULL;',
'SELECT TOP 1 @id = id, @ms_sql = ms_sql FROM @cascade ORDER BY id;',
'END',
"EXEC sp_executesql N'DROP SCHEMA", this.quoteIdentifier(schema), ";'",
'END;'
].join(' ');
}
showSchemasQuery() {
return [
'SELECT "name" as "schema_name" FROM sys.schemas as s',
'WHERE "s"."name" NOT IN (',
"'INFORMATION_SCHEMA', 'dbo', 'guest', 'sys', 'archive'",
')', 'AND', '"s"."name" NOT LIKE', "'db_%'"
].join(' ');
}
versionQuery() {
// Uses string manipulation to convert the MS Maj.Min.Patch.Build to semver Maj.Min.Patch
return [
'DECLARE @ms_ver NVARCHAR(20);',
"SET @ms_ver = REVERSE(CONVERT(NVARCHAR(20), SERVERPROPERTY('ProductVersion')));",
"SELECT REVERSE(SUBSTRING(@ms_ver, CHARINDEX('.', @ms_ver)+1, 20)) AS 'version'"
].join(' ');
}
createTableQuery(tableName, attributes, options) {
const query = (table, attrs) => `IF OBJECT_ID('${table}', 'U') IS NULL CREATE TABLE ${table} (${attrs})`,
primaryKeys = [],
foreignKeys = {},
attrStr = [];
let commentStr = '';
for (const attr in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
let dataType = attributes[attr];
let match;
if (dataType.includes('COMMENT ')) {
const commentMatch = dataType.match(/^(.+) (COMMENT.*)$/);
const commentText = commentMatch[2].replace('COMMENT', '').trim();
commentStr += this.commentTemplate(commentText, tableName, attr);
// remove comment related substring from dataType
dataType = commentMatch[1];
}
if (dataType.includes('PRIMARY KEY')) {
primaryKeys.push(attr);
if (dataType.includes('REFERENCES')) {
// MSSQL doesn't support inline REFERENCES declarations: move to the end
match = dataType.match(/^(.+) (REFERENCES.*)$/);
attrStr.push(`${this.quoteIdentifier(attr)} ${match[1].replace('PRIMARY KEY', '')}`);
foreignKeys[attr] = match[2];
} else {
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType.replace('PRIMARY KEY', '')}`);
}
} else if (dataType.includes('REFERENCES')) {
// MSSQL doesn't support inline REFERENCES declarations: move to the end
match = dataType.match(/^(.+) (REFERENCES.*)$/);
attrStr.push(`${this.quoteIdentifier(attr)} ${match[1]}`);
foreignKeys[attr] = match[2];
} else {
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType}`);
}
}
}
let attributesClause = attrStr.join(', ');
const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
if (options.uniqueKeys) {
_.each(options.uniqueKeys, (columns, indexName) => {
if (columns.customIndex) {
if (typeof indexName !== 'string') {
indexName = `uniq_${tableName}_${columns.fields.join('_')}`;
}
attributesClause += `, CONSTRAINT ${this.quoteIdentifier(indexName)} UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
}
});
}
if (pkString.length > 0) {
attributesClause += `, PRIMARY KEY (${pkString})`;
}
for (const fkey in foreignKeys) {
if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) {
attributesClause += `, FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`;
}
}
return `${query(this.quoteTable(tableName), attributesClause)};${commentStr}`;
}
describeTableQuery(tableName, schema) {
let sql = [
'SELECT',
"c.COLUMN_NAME AS 'Name',",
"c.DATA_TYPE AS 'Type',",
"c.CHARACTER_MAXIMUM_LENGTH AS 'Length',",
"c.IS_NULLABLE as 'IsNull',",
"COLUMN_DEFAULT AS 'Default',",
"pk.CONSTRAINT_TYPE AS 'Constraint',",
"COLUMNPROPERTY(OBJECT_ID(c.TABLE_SCHEMA+'.'+c.TABLE_NAME), c.COLUMN_NAME, 'IsIdentity') as 'IsIdentity',",
"prop.value AS 'Comment'",
'FROM',
'INFORMATION_SCHEMA.TABLES t',
'INNER JOIN',
'INFORMATION_SCHEMA.COLUMNS c ON t.TABLE_NAME = c.TABLE_NAME AND t.TABLE_SCHEMA = c.TABLE_SCHEMA',
'LEFT JOIN (SELECT tc.table_schema, tc.table_name, ',
'cu.column_name, tc.constraint_type ',
'FROM information_schema.TABLE_CONSTRAINTS tc ',
'JOIN information_schema.KEY_COLUMN_USAGE cu ',
'ON tc.table_schema=cu.table_schema and tc.table_name=cu.table_name ',
'and tc.constraint_name=cu.constraint_name ',
'and tc.constraint_type=\'PRIMARY KEY\') pk ',
'ON pk.table_schema=c.table_schema ',
'AND pk.table_name=c.table_name ',
'AND pk.column_name=c.column_name ',
'INNER JOIN sys.columns AS sc',
"ON sc.object_id = object_id(t.table_schema + '.' + t.table_name) AND sc.name = c.column_name",
'LEFT JOIN sys.extended_properties prop ON prop.major_id = sc.object_id',
'AND prop.minor_id = sc.column_id',
"AND prop.name = 'MS_Description'",
'WHERE t.TABLE_NAME =', wrapSingleQuote(tableName)
].join(' ');
if (schema) {
sql += `AND t.TABLE_SCHEMA =${wrapSingleQuote(schema)}`;
}
return sql;
}
renameTableQuery(before, after) {
return `EXEC sp_rename ${this.quoteTable(before)}, ${this.quoteTable(after)};`;
}
showTablesQuery() {
return "SELECT TABLE_NAME, TABLE_SCHEMA FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = 'BASE TABLE';";
}
dropTableQuery(tableName) {
const qouteTbl = this.quoteTable(tableName);
return `IF OBJECT_ID('${qouteTbl}', 'U') IS NOT NULL DROP TABLE ${qouteTbl};`;
}
addColumnQuery(table, key, dataType) {
// FIXME: attributeToSQL SHOULD be using attributes in addColumnQuery
// but instead we need to pass the key along as the field here
dataType.field = key;
let commentStr = '';
if (dataType.comment && _.isString(dataType.comment)) {
commentStr = this.commentTemplate(dataType.comment, table, key);
// attributeToSQL will try to include `COMMENT 'Comment Text'` when it returns if the comment key
// is present. This is needed for createTable statement where that part is extracted with regex.
// Here we can intercept the object and remove comment property since we have the original object.
delete dataType['comment'];
}
const def = this.attributeToSQL(dataType, {
context: 'addColumn'
});
return `ALTER TABLE ${this.quoteTable(table)} ADD ${this.quoteIdentifier(key)} ${def};${commentStr}`;
}
commentTemplate(comment, table, column) {
return ' EXEC sp_addextendedproperty ' +
`@name = N'MS_Description', @value = ${this.escape(comment)}, ` +
'@level0type = N\'Schema\', @level0name = \'dbo\', ' +
`@level1type = N'Table', @level1name = ${this.quoteIdentifier(table)}, ` +
`@level2type = N'Column', @level2name = ${this.quoteIdentifier(column)};`;
}
removeColumnQuery(tableName, attributeName) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP COLUMN ${this.quoteIdentifier(attributeName)};`;
}
changeColumnQuery(tableName, attributes) {
const attrString = [],
constraintString = [];
let commentString = '';
for (const attributeName in attributes) {
const quotedAttrName = this.quoteIdentifier(attributeName);
let definition = attributes[attributeName];
if (definition.includes('COMMENT ')) {
const commentMatch = definition.match(/^(.+) (COMMENT.*)$/);
const commentText = commentMatch[2].replace('COMMENT', '').trim();
commentString += this.commentTemplate(commentText, tableName, attributeName);
// remove comment related substring from dataType
definition = commentMatch[1];
}
if (definition.includes('REFERENCES')) {
constraintString.push(`FOREIGN KEY (${quotedAttrName}) ${definition.replace(/.+?(?=REFERENCES)/, '')}`);
} else {
attrString.push(`${quotedAttrName} ${definition}`);
}
}
let finalQuery = '';
if (attrString.length) {
finalQuery += `ALTER COLUMN ${attrString.join(', ')}`;
finalQuery += constraintString.length ? ' ' : '';
}
if (constraintString.length) {
finalQuery += `ADD ${constraintString.join(', ')}`;
}
return `ALTER TABLE ${this.quoteTable(tableName)} ${finalQuery};${commentString}`;
}
renameColumnQuery(tableName, attrBefore, attributes) {
const newName = Object.keys(attributes)[0];
return `EXEC sp_rename '${this.quoteTable(tableName)}.${attrBefore}', '${newName}', 'COLUMN';`;
}
bulkInsertQuery(tableName, attrValueHashes, options, attributes) {
const quotedTable = this.quoteTable(tableName);
options = options || {};
attributes = attributes || {};
const tuples = [];
const allAttributes = [];
const allQueries = [];
let needIdentityInsertWrapper = false,
outputFragment = '';
if (options.returning) {
outputFragment = ' OUTPUT INSERTED.*';
}
const emptyQuery = `INSERT INTO ${quotedTable}${outputFragment} DEFAULT VALUES`;
attrValueHashes.forEach(attrValueHash => {
// special case for empty objects with primary keys
const fields = Object.keys(attrValueHash);
const firstAttr = attributes[fields[0]];
if (fields.length === 1 && firstAttr && firstAttr.autoIncrement && attrValueHash[fields[0]] === null) {
allQueries.push(emptyQuery);
return;
}
// normal case
_.forOwn(attrValueHash, (value, key) => {
if (value !== null && attributes[key] && attributes[key].autoIncrement) {
needIdentityInsertWrapper = true;
}
if (!allAttributes.includes(key)) {
if (value === null && attributes[key] && attributes[key].autoIncrement)
return;
allAttributes.push(key);
}
});
});
if (allAttributes.length > 0) {
attrValueHashes.forEach(attrValueHash => {
tuples.push(`(${
allAttributes.map(key =>
this.escape(attrValueHash[key])).join(',')
})`);
});
const quotedAttributes = allAttributes.map(attr => this.quoteIdentifier(attr)).join(',');
allQueries.push(tupleStr => `INSERT INTO ${quotedTable} (${quotedAttributes})${outputFragment} VALUES ${tupleStr};`);
}
const commands = [];
let offset = 0;
const batch = Math.floor(250 / (allAttributes.length + 1)) + 1;
while (offset < Math.max(tuples.length, 1)) {
const tupleStr = tuples.slice(offset, Math.min(tuples.length, offset + batch));
let generatedQuery = allQueries.map(v => typeof v === 'string' ? v : v(tupleStr)).join(';');
if (needIdentityInsertWrapper) {
generatedQuery = `SET IDENTITY_INSERT ${quotedTable} ON; ${generatedQuery}; SET IDENTITY_INSERT ${quotedTable} OFF;`;
}
commands.push(generatedQuery);
offset += batch;
}
return commands.join(';');
}
updateQuery(tableName, attrValueHash, where, options, attributes) {
const sql = super.updateQuery(tableName, attrValueHash, where, options, attributes);
if (options.limit) {
const updateArgs = `UPDATE TOP(${this.escape(options.limit)})`;
sql.query = sql.query.replace('UPDATE', updateArgs);
}
return sql;
}
upsertQuery(tableName, insertValues, updateValues, where, model) {
const targetTableAlias = this.quoteTable(`${tableName}_target`);
const sourceTableAlias = this.quoteTable(`${tableName}_source`);
const primaryKeysAttrs = [];
const identityAttrs = [];
const uniqueAttrs = [];
const tableNameQuoted = this.quoteTable(tableName);
let needIdentityInsertWrapper = false;
//Obtain primaryKeys, uniquekeys and identity attrs from rawAttributes as model is not passed
for (const key in model.rawAttributes) {
if (model.rawAttributes[key].primaryKey) {
primaryKeysAttrs.push(model.rawAttributes[key].field || key);
}
if (model.rawAttributes[key].unique) {
uniqueAttrs.push(model.rawAttributes[key].field || key);
}
if (model.rawAttributes[key].autoIncrement) {
identityAttrs.push(model.rawAttributes[key].field || key);
}
}
//Add unique indexes defined by indexes option to uniqueAttrs
for (const index of model._indexes) {
if (index.unique && index.fields) {
for (const field of index.fields) {
const fieldName = typeof field === 'string' ? field : field.name || field.attribute;
if (!uniqueAttrs.includes(fieldName) && model.rawAttributes[fieldName]) {
uniqueAttrs.push(fieldName);
}
}
}
}
const updateKeys = Object.keys(updateValues);
const insertKeys = Object.keys(insertValues);
const insertKeysQuoted = insertKeys.map(key => this.quoteIdentifier(key)).join(', ');
const insertValuesEscaped = insertKeys.map(key => this.escape(insertValues[key])).join(', ');
const sourceTableQuery = `VALUES(${insertValuesEscaped})`; //Virtual Table
let joinCondition;
//IDENTITY_INSERT Condition
identityAttrs.forEach(key => {
if (updateValues[key] && updateValues[key] !== null) {
needIdentityInsertWrapper = true;
/*
* IDENTITY_INSERT Column Cannot be updated, only inserted
* http://stackoverflow.com/a/30176254/2254360
*/
}
});
//Filter NULL Clauses
const clauses = where[Op.or].filter(clause => {
let valid = true;
/*
* Exclude NULL Composite PK/UK. Partial Composite clauses should also be excluded as it doesn't guarantee a single row
*/
for (const key in clause) {
if (!clause[key]) {
valid = false;
break;
}
}
return valid;
});
/*
* Generate ON condition using PK(s).
* If not, generate using UK(s). Else throw error
*/
const getJoinSnippet = array => {
return array.map(key => {
key = this.quoteIdentifier(key);
return `${targetTableAlias}.${key} = ${sourceTableAlias}.${key}`;
});
};
if (clauses.length === 0) {
throw new Error('Primary Key or Unique key should be passed to upsert query');
} else {
// Search for primary key attribute in clauses -- Model can have two separate unique keys
for (const key in clauses) {
const keys = Object.keys(clauses[key]);
if (primaryKeysAttrs.includes(keys[0])) {
joinCondition = getJoinSnippet(primaryKeysAttrs).join(' AND ');
break;
}
}
if (!joinCondition) {
joinCondition = getJoinSnippet(uniqueAttrs).join(' AND ');
}
}
// Remove the IDENTITY_INSERT Column from update
const updateSnippet = updateKeys.filter(key => !identityAttrs.includes(key))
.map(key => {
const value = this.escape(updateValues[key]);
key = this.quoteIdentifier(key);
return `${targetTableAlias}.${key} = ${value}`;
}).join(', ');
const insertSnippet = `(${insertKeysQuoted}) VALUES(${insertValuesEscaped})`;
let query = `MERGE INTO ${tableNameQuoted} WITH(HOLDLOCK) AS ${targetTableAlias} USING (${sourceTableQuery}) AS ${sourceTableAlias}(${insertKeysQuoted}) ON ${joinCondition}`;
query += ` WHEN MATCHED THEN UPDATE SET ${updateSnippet} WHEN NOT MATCHED THEN INSERT ${insertSnippet} OUTPUT $action, INSERTED.*;`;
if (needIdentityInsertWrapper) {
query = `SET IDENTITY_INSERT ${tableNameQuoted} ON; ${query} SET IDENTITY_INSERT ${tableNameQuoted} OFF;`;
}
return query;
}
truncateTableQuery(tableName) {
return `TRUNCATE TABLE ${this.quoteTable(tableName)}`;
}
deleteQuery(tableName, where, options = {}, model) {
const table = this.quoteTable(tableName);
let whereClause = this.getWhereConditions(where, null, model, options);
let limit = '';
if (options.limit) {
limit = ` TOP(${this.escape(options.limit)})`;
}
if (whereClause) {
whereClause = ` WHERE ${whereClause}`;
}
return `DELETE${limit} FROM ${table}${whereClause}; SELECT @@ROWCOUNT AS AFFECTEDROWS;`;
}
showIndexesQuery(tableName) {
return `EXEC sys.sp_helpindex @objname = N'${this.quoteTable(tableName)}';`;
}
showConstraintsQuery(tableName) {
return `EXEC sp_helpconstraint @objname = ${this.escape(this.quoteTable(tableName))};`;
}
removeIndexQuery(tableName, indexNameOrAttributes) {
let indexName = indexNameOrAttributes;
if (typeof indexName !== 'string') {
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
}
return `DROP INDEX ${this.quoteIdentifiers(indexName)} ON ${this.quoteIdentifiers(tableName)}`;
}
attributeToSQL(attribute) {
if (!_.isPlainObject(attribute)) {
attribute = {
type: attribute
};
}
// handle self referential constraints
if (attribute.references) {
if (attribute.Model && attribute.Model.tableName === attribute.references.model) {
this.sequelize.log('MSSQL does not support self referencial constraints, '
+ 'we will remove it but we recommend restructuring your query');
attribute.onDelete = '';
attribute.onUpdate = '';
}
}
let template;
if (attribute.type instanceof DataTypes.ENUM) {
if (attribute.type.values && !attribute.values) attribute.values = attribute.type.values;
// enums are a special case
template = attribute.type.toSql();
template += ` CHECK (${this.quoteIdentifier(attribute.field)} IN(${attribute.values.map(value => {
return this.escape(value);
}).join(', ') }))`;
return template;
}
template = attribute.type.toString();
if (attribute.allowNull === false) {
template += ' NOT NULL';
} else if (!attribute.primaryKey && !Utils.defaultValueSchemable(attribute.defaultValue)) {
template += ' NULL';
}
if (attribute.autoIncrement) {
template += ' IDENTITY(1,1)';
}
// Blobs/texts cannot have a defaultValue
if (attribute.type !== 'TEXT' && attribute.type._binary !== true &&
Utils.defaultValueSchemable(attribute.defaultValue)) {
template += ` DEFAULT ${this.escape(attribute.defaultValue)}`;
}
if (attribute.unique === true) {
template += ' UNIQUE';
}
if (attribute.primaryKey) {
template += ' PRIMARY KEY';
}
if (attribute.references) {
template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`;
if (attribute.references.key) {
template += ` (${this.quoteIdentifier(attribute.references.key)})`;
} else {
template += ` (${this.quoteIdentifier('id')})`;
}
if (attribute.onDelete) {
template += ` ON DELETE ${attribute.onDelete.toUpperCase()}`;
}
if (attribute.onUpdate) {
template += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`;
}
}
if (attribute.comment && typeof attribute.comment === 'string') {
template += ` COMMENT ${attribute.comment}`;
}
return template;
}
attributesToSQL(attributes, options) {
const result = {},
existingConstraints = [];
let key,
attribute;
for (key in attributes) {
attribute = attributes[key];
if (attribute.references) {
if (existingConstraints.includes(attribute.references.model.toString())) {
// no cascading constraints to a table more than once
attribute.onDelete = '';
attribute.onUpdate = '';
} else {
existingConstraints.push(attribute.references.model.toString());
// NOTE: this really just disables cascading updates for all
// definitions. Can be made more robust to support the
// few cases where MSSQL actually supports them
attribute.onUpdate = '';
}
}
if (key && !attribute.field) attribute.field = key;
result[attribute.field || key] = this.attributeToSQL(attribute, options);
}
return result;
}
createTrigger() {
throwMethodUndefined('createTrigger');
}
dropTrigger() {
throwMethodUndefined('dropTrigger');
}
renameTrigger() {
throwMethodUndefined('renameTrigger');
}
createFunction() {
throwMethodUndefined('createFunction');
}
dropFunction() {
throwMethodUndefined('dropFunction');
}
renameFunction() {
throwMethodUndefined('renameFunction');
}
/**
* Generate common SQL prefix for ForeignKeysQuery.
*
* @param {string} catalogName
* @returns {string}
*/
_getForeignKeysQueryPrefix(catalogName) {
return `${'SELECT ' +
'constraint_name = OBJ.NAME, ' +
'constraintName = OBJ.NAME, '}${
catalogName ? `constraintCatalog = '${catalogName}', ` : ''
}constraintSchema = SCHEMA_NAME(OBJ.SCHEMA_ID), ` +
'tableName = TB.NAME, ' +
`tableSchema = SCHEMA_NAME(TB.SCHEMA_ID), ${
catalogName ? `tableCatalog = '${catalogName}', ` : ''
}columnName = COL.NAME, ` +
`referencedTableSchema = SCHEMA_NAME(RTB.SCHEMA_ID), ${
catalogName ? `referencedCatalog = '${catalogName}', ` : ''
}referencedTableName = RTB.NAME, ` +
'referencedColumnName = RCOL.NAME ' +
'FROM sys.foreign_key_columns FKC ' +
'INNER JOIN sys.objects OBJ ON OBJ.OBJECT_ID = FKC.CONSTRAINT_OBJECT_ID ' +
'INNER JOIN sys.tables TB ON TB.OBJECT_ID = FKC.PARENT_OBJECT_ID ' +
'INNER JOIN sys.columns COL ON COL.COLUMN_ID = PARENT_COLUMN_ID AND COL.OBJECT_ID = TB.OBJECT_ID ' +
'INNER JOIN sys.tables RTB ON RTB.OBJECT_ID = FKC.REFERENCED_OBJECT_ID ' +
'INNER JOIN sys.columns RCOL ON RCOL.COLUMN_ID = REFERENCED_COLUMN_ID AND RCOL.OBJECT_ID = RTB.OBJECT_ID';
}
/**
* Generates an SQL query that returns all foreign keys details of a table.
*
* @param {string|Object} table
* @param {string} catalogName database name
* @returns {string}
*/
getForeignKeysQuery(table, catalogName) {
const tableName = table.tableName || table;
let sql = `${this._getForeignKeysQueryPrefix(catalogName)
} WHERE TB.NAME =${wrapSingleQuote(tableName)}`;
if (table.schema) {
sql += ` AND SCHEMA_NAME(TB.SCHEMA_ID) =${wrapSingleQuote(table.schema)}`;
}
return sql;
}
getForeignKeyQuery(table, attributeName) {
const tableName = table.tableName || table;
let sql = `${this._getForeignKeysQueryPrefix()
} WHERE TB.NAME =${wrapSingleQuote(tableName)
} AND COL.NAME =${wrapSingleQuote(attributeName)}`;
if (table.schema) {
sql += ` AND SCHEMA_NAME(TB.SCHEMA_ID) =${wrapSingleQuote(table.schema)}`;
}
return sql;
}
getPrimaryKeyConstraintQuery(table, attributeName) {
const tableName = wrapSingleQuote(table.tableName || table);
return [
'SELECT K.TABLE_NAME AS tableName,',
'K.COLUMN_NAME AS columnName,',
'K.CONSTRAINT_NAME AS constraintName',
'FROM INFORMATION_SCHEMA.TABLE_CONSTRAINTS AS C',
'JOIN INFORMATION_SCHEMA.KEY_COLUMN_USAGE AS K',
'ON C.TABLE_NAME = K.TABLE_NAME',
'AND C.CONSTRAINT_CATALOG = K.CONSTRAINT_CATALOG',
'AND C.CONSTRAINT_SCHEMA = K.CONSTRAINT_SCHEMA',
'AND C.CONSTRAINT_NAME = K.CONSTRAINT_NAME',
'WHERE C.CONSTRAINT_TYPE = \'PRIMARY KEY\'',
`AND K.COLUMN_NAME = ${wrapSingleQuote(attributeName)}`,
`AND K.TABLE_NAME = ${tableName};`
].join(' ');
}
dropForeignKeyQuery(tableName, foreignKey) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP ${this.quoteIdentifier(foreignKey)}`;
}
getDefaultConstraintQuery(tableName, attributeName) {
const quotedTable = this.quoteTable(tableName);
return 'SELECT name FROM sys.default_constraints ' +
`WHERE PARENT_OBJECT_ID = OBJECT_ID('${quotedTable}', 'U') ` +
`AND PARENT_COLUMN_ID = (SELECT column_id FROM sys.columns WHERE NAME = ('${attributeName}') ` +
`AND object_id = OBJECT_ID('${quotedTable}', 'U'));`;
}
dropConstraintQuery(tableName, constraintName) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(constraintName)};`;
}
setIsolationLevelQuery() {
}
generateTransactionId() {
return randomBytes(10).toString('hex');
}
startTransactionQuery(transaction) {
if (transaction.parent) {
return `SAVE TRANSACTION ${this.quoteIdentifier(transaction.name)};`;
}
return 'BEGIN TRANSACTION;';
}
commitTransactionQuery(transaction) {
if (transaction.parent) {
return;
}
return 'COMMIT TRANSACTION;';
}
rollbackTransactionQuery(transaction) {
if (transaction.parent) {
return `ROLLBACK TRANSACTION ${this.quoteIdentifier(transaction.name)};`;
}
return 'ROLLBACK TRANSACTION;';
}
selectFromTableFragment(options, model, attributes, tables, mainTableAs, where) {
let topFragment = '';
let mainFragment = `SELECT ${attributes.join(', ')} FROM ${tables}`;
// Handle SQL Server 2008 with TOP instead of LIMIT
if (semver.valid(this.sequelize.options.databaseVersion) && semver.lt(this.sequelize.options.databaseVersion, '11.0.0')) {
if (options.limit) {
topFragment = `TOP ${options.limit} `;
}
if (options.offset) {
const offset = options.offset || 0,
isSubQuery = options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation;
let orders = { mainQueryOrder: [] };
if (options.order) {
orders = this.getQueryOrders(options, model, isSubQuery);
}
if (!orders.mainQueryOrder.length) {
orders.mainQueryOrder.push(this.quoteIdentifier(model.primaryKeyField));
}
const tmpTable = mainTableAs ? mainTableAs : 'OffsetTable';
const whereFragment = where ? ` WHERE ${where}` : '';
/*
* For earlier versions of SQL server, we need to nest several queries
* in order to emulate the OFFSET behavior.
*
* 1. The outermost query selects all items from the inner query block.
* This is due to a limitation in SQL server with the use of computed
* columns (e.g. SELECT ROW_NUMBER()...AS x) in WHERE clauses.
* 2. The next query handles the LIMIT and OFFSET behavior by getting
* the TOP N rows of the query where the row number is > OFFSET
* 3. The innermost query is the actual set we want information from
*/
const fragment = `SELECT TOP 100 PERCENT ${attributes.join(', ')} FROM ` +
`(SELECT ${topFragment}*` +
` FROM (SELECT ROW_NUMBER() OVER (ORDER BY ${orders.mainQueryOrder.join(', ')}) as row_num, * ` +
` FROM ${tables} AS ${tmpTable}${whereFragment})` +
` AS ${tmpTable} WHERE row_num > ${offset})` +
` AS ${tmpTable}`;
return fragment;
}
mainFragment = `SELECT ${topFragment}${attributes.join(', ')} FROM ${tables}`;
}
if (mainTableAs) {
mainFragment += ` AS ${mainTableAs}`;
}
if (options.tableHint && TableHints[options.tableHint]) {
mainFragment += ` WITH (${TableHints[options.tableHint]})`;
}
return mainFragment;
}
addLimitAndOffset(options, model) {
// Skip handling of limit and offset as postfixes for older SQL Server versions
if (semver.valid(this.sequelize.options.databaseVersion) && semver.lt(this.sequelize.options.databaseVersion, '11.0.0')) {
return '';
}
const offset = options.offset || 0;
const isSubQuery = options.subQuery === undefined
? options.hasIncludeWhere || options.hasIncludeRequired || options.hasMultiAssociation
: options.subQuery;
let fragment = '';
let orders = {};
if (options.order) {
orders = this.getQueryOrders(options, model, isSubQuery);
}
if (options.limit || options.offset) {
if (!options.order || options.include && !orders.subQueryOrder.length) {
fragment += options.order && !isSubQuery ? ', ' : ' ORDER BY ';
fragment += `${this.quoteTable(options.tableAs || model.name)}.${this.quoteIdentifier(model.primaryKeyField)}`;
}
if (options.offset || options.limit) {
fragment += ` OFFSET ${this.escape(offset)} ROWS`;
}
if (options.limit) {
fragment += ` FETCH NEXT ${this.escape(options.limit)} ROWS ONLY`;
}
}
return fragment;
}
booleanValue(value) {
return value ? 1 : 0;
}
}
// private methods
function wrapSingleQuote(identifier) {
return Utils.addTicks(Utils.removeTicks(identifier, "'"), "'");
}
module.exports = MSSQLQueryGenerator;
+68
View File
@@ -0,0 +1,68 @@
'use strict';
/**
Returns an object that treats MSSQL's inabilities to do certain queries.
@class QueryInterface
@static
@private
*/
/**
A wrapper that fixes MSSQL's inability to cleanly remove columns from existing tables if they have a default constraint.
@param {QueryInterface} qi
@param {string} tableName The name of the table.
@param {string} attributeName The name of the attribute that we want to remove.
@param {Object} options
@param {boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
@private
*/
const removeColumn = function(qi, tableName, attributeName, options) {
options = Object.assign({ raw: true }, options || {});
const findConstraintSql = qi.QueryGenerator.getDefaultConstraintQuery(tableName, attributeName);
return qi.sequelize.query(findConstraintSql, options)
.then(([results]) => {
if (!results.length) {
// No default constraint found -- we can cleanly remove the column
return;
}
const dropConstraintSql = qi.QueryGenerator.dropConstraintQuery(tableName, results[0].name);
return qi.sequelize.query(dropConstraintSql, options);
})
.then(() => {
const findForeignKeySql = qi.QueryGenerator.getForeignKeyQuery(tableName, attributeName);
return qi.sequelize.query(findForeignKeySql, options);
})
.then(([results]) => {
if (!results.length) {
// No foreign key constraints found, so we can remove the column
return;
}
const dropForeignKeySql = qi.QueryGenerator.dropForeignKeyQuery(tableName, results[0].constraint_name);
return qi.sequelize.query(dropForeignKeySql, options);
})
.then(() => {
//Check if the current column is a primaryKey
const primaryKeyConstraintSql = qi.QueryGenerator.getPrimaryKeyConstraintQuery(tableName, attributeName);
return qi.sequelize.query(primaryKeyConstraintSql, options);
})
.then(([result]) => {
if (!result.length) {
return;
}
const dropConstraintSql = qi.QueryGenerator.dropConstraintQuery(tableName, result[0].constraintName);
return qi.sequelize.query(dropConstraintSql, options);
})
.then(() => {
const removeSql = qi.QueryGenerator.removeColumnQuery(tableName, attributeName);
return qi.sequelize.query(removeSql, options);
});
};
module.exports = {
removeColumn
};
+389
View File
@@ -0,0 +1,389 @@
'use strict';
const Promise = require('../../promise');
const AbstractQuery = require('../abstract/query');
const sequelizeErrors = require('../../errors');
const parserStore = require('../parserStore')('mssql');
const _ = require('lodash');
const { logger } = require('../../utils/logger');
const debug = logger.debugContext('sql:mssql');
class Query extends AbstractQuery {
getInsertIdField() {
return 'id';
}
getSQLTypeFromJsType(value, TYPES) {
const paramType = { type: TYPES.VarChar, typeOptions: {} };
paramType.type = TYPES.NVarChar;
if (typeof value === 'number') {
if (Number.isInteger(value)) {
if (value >= -2147483648 && value <= 2147483647) {
paramType.type = TYPES.Int;
} else {
paramType.type = TYPES.BigInt;
}
} else {
paramType.type = TYPES.Numeric;
//Default to a reasonable numeric precision/scale pending more sophisticated logic
paramType.typeOptions = { precision: 30, scale: 15 };
}
}
if (Buffer.isBuffer(value)) {
paramType.type = TYPES.VarBinary;
}
return paramType;
}
_run(connection, sql, parameters) {
this.sql = sql;
const { options } = this;
const complete = this._logQuery(sql, debug, parameters);
return new Promise((resolve, reject) => {
const handleTransaction = err => {
if (err) {
reject(this.formatError(err));
return;
}
resolve(this.formatResults());
};
// TRANSACTION SUPPORT
if (sql.startsWith('BEGIN TRANSACTION')) {
return connection.beginTransaction(handleTransaction, options.transaction.name, connection.lib.ISOLATION_LEVEL[options.isolationLevel]);
}
if (sql.startsWith('COMMIT TRANSACTION')) {
return connection.commitTransaction(handleTransaction);
}
if (sql.startsWith('ROLLBACK TRANSACTION')) {
return connection.rollbackTransaction(handleTransaction, options.transaction.name);
}
if (sql.startsWith('SAVE TRANSACTION')) {
return connection.saveTransaction(handleTransaction, options.transaction.name);
}
const results = [];
const request = new connection.lib.Request(sql, (err, rowCount) => {
complete();
if (err) {
err.sql = sql;
err.parameters = parameters;
reject(this.formatError(err));
} else {
resolve(this.formatResults(results, rowCount));
}
});
if (parameters) {
_.forOwn(parameters, (value, key) => {
const paramType = this.getSQLTypeFromJsType(value, connection.lib.TYPES);
request.addParameter(key, paramType.type, value, paramType.typeOptions);
});
}
request.on('row', columns => {
const row = {};
for (const column of columns) {
const typeid = column.metadata.type.id;
const parse = parserStore.get(typeid);
let value = column.value;
if (value !== null & !!parse) {
value = parse(value);
}
row[column.metadata.colName] = value;
}
results.push(row);
});
connection.execSql(request);
});
}
run(sql, parameters) {
return Promise.using(this.connection.lock(), connection => this._run(connection, sql, parameters));
}
static formatBindParameters(sql, values, dialect) {
const bindParam = {};
const replacementFunc = (match, key, values) => {
if (values[key] !== undefined) {
bindParam[key] = values[key];
return `@${key}`;
}
return undefined;
};
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
return [sql, bindParam];
}
/**
* High level function that handles the results of a query execution.
*
* @param {Array} data - The result of the query execution.
* @param {number} rowCount
* @private
* @example
* Example:
* query.formatResults([
* {
* id: 1, // this is from the main table
* attr2: 'snafu', // this is from the main table
* Tasks.id: 1, // this is from the associated table
* Tasks.title: 'task' // this is from the associated table
* }
* ])
*/
formatResults(data, rowCount) {
let result = this.instance;
if (this.isInsertQuery(data)) {
this.handleInsertQuery(data);
if (!this.instance) {
if (this.options.plain) {
// NOTE: super contrived. This just passes the newly added query-interface
// test returning only the PK. There isn't a way in MSSQL to identify
// that a given return value is the PK, and we have no schema information
// because there was no calling Model.
const record = data[0];
result = record[Object.keys(record)[0]];
} else {
result = data;
}
}
}
if (this.isShowTablesQuery()) {
return this.handleShowTablesQuery(data);
}
if (this.isDescribeQuery()) {
result = {};
for (const _result of data) {
if (_result.Default) {
_result.Default = _result.Default.replace("('", '').replace("')", '').replace(/'/g, '');
}
result[_result.Name] = {
type: _result.Type.toUpperCase(),
allowNull: _result.IsNull === 'YES' ? true : false,
defaultValue: _result.Default,
primaryKey: _result.Constraint === 'PRIMARY KEY',
autoIncrement: _result.IsIdentity === 1,
comment: _result.Comment
};
if (
result[_result.Name].type.includes('CHAR')
&& _result.Length
) {
if (_result.Length === -1) {
result[_result.Name].type += '(MAX)';
} else {
result[_result.Name].type += `(${_result.Length})`;
}
}
}
}
if (this.isSelectQuery()) {
return this.handleSelectQuery(data);
}
if (this.isShowIndexesQuery()) {
return this.handleShowIndexesQuery(data);
}
if (this.isUpsertQuery()) {
return data[0];
}
if (this.isCallQuery()) {
return data[0];
}
if (this.isBulkUpdateQuery()) {
return data.length;
}
if (this.isBulkDeleteQuery()) {
return data[0] && data[0].AFFECTEDROWS;
}
if (this.isVersionQuery()) {
return data[0].version;
}
if (this.isForeignKeysQuery()) {
return data;
}
if (this.isInsertQuery() || this.isUpdateQuery()) {
return [result, rowCount];
}
if (this.isShowConstraintsQuery()) {
return this.handleShowConstraintsQuery(data);
}
if (this.isRawQuery()) {
// MSSQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
return [data, data];
}
return result;
}
handleShowTablesQuery(results) {
return results.map(resultSet => {
return {
tableName: resultSet.TABLE_NAME,
schema: resultSet.TABLE_SCHEMA
};
});
}
handleShowConstraintsQuery(data) {
//Convert snake_case keys to camelCase as it's generated by stored procedure
return data.slice(1).map(result => {
const constraint = {};
for (const key in result) {
constraint[_.camelCase(key)] = result[key];
}
return constraint;
});
}
formatError(err) {
let match;
match = err.message.match(/Violation of (?:UNIQUE|PRIMARY) KEY constraint '((.|\s)*)'. Cannot insert duplicate key in object '.*'.(:? The duplicate key value is \((.*)\).)?/);
match = match || err.message.match(/Cannot insert duplicate key row in object .* with unique index '(.*)'/);
if (match && match.length > 1) {
let fields = {};
const uniqueKey = this.model && this.model.uniqueKeys[match[1]];
let message = 'Validation error';
if (uniqueKey && !!uniqueKey.msg) {
message = uniqueKey.msg;
}
if (match[4]) {
const values = match[4].split(',').map(part => part.trim());
if (uniqueKey) {
fields = _.zipObject(uniqueKey.fields, values);
} else {
fields[match[1]] = match[4];
}
}
const errors = [];
_.forOwn(fields, (value, field) => {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
value,
this.instance,
'not_unique'
));
});
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
}
match = err.message.match(/Failed on step '(.*)'.Could not create constraint. See previous errors./) ||
err.message.match(/The DELETE statement conflicted with the REFERENCE constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./) ||
err.message.match(/The (?:INSERT|MERGE|UPDATE) statement conflicted with the FOREIGN KEY constraint "(.*)". The conflict occurred in database "(.*)", table "(.*)", column '(.*)'./);
if (match && match.length > 0) {
return new sequelizeErrors.ForeignKeyConstraintError({
fields: null,
index: match[1],
parent: err
});
}
match = err.message.match(/Could not drop constraint. See previous errors./);
if (match && match.length > 0) {
let constraint = err.sql.match(/(?:constraint|index) \[(.+?)\]/i);
constraint = constraint ? constraint[1] : undefined;
let table = err.sql.match(/table \[(.+?)\]/i);
table = table ? table[1] : undefined;
return new sequelizeErrors.UnknownConstraintError({
message: match[1],
constraint,
table,
parent: err
});
}
return new sequelizeErrors.DatabaseError(err);
}
isShowOrDescribeQuery() {
let result = false;
result = result || this.sql.toLowerCase().startsWith("select c.column_name as 'name', c.data_type as 'type', c.is_nullable as 'isnull'");
result = result || this.sql.toLowerCase().startsWith('select tablename = t.name, name = ind.name,');
result = result || this.sql.toLowerCase().startsWith('exec sys.sp_helpindex @objname');
return result;
}
isShowIndexesQuery() {
return this.sql.toLowerCase().startsWith('exec sys.sp_helpindex @objname');
}
handleShowIndexesQuery(data) {
// Group by index name, and collect all fields
data = data.reduce((acc, item) => {
if (!(item.index_name in acc)) {
acc[item.index_name] = item;
item.fields = [];
}
item.index_keys.split(',').forEach(column => {
let columnName = column.trim();
if (columnName.includes('(-)')) {
columnName = columnName.replace('(-)', '');
}
acc[item.index_name].fields.push({
attribute: columnName,
length: undefined,
order: column.includes('(-)') ? 'DESC' : 'ASC',
collate: undefined
});
});
delete item.index_keys;
return acc;
}, {});
return _.map(data, item => ({
primary: item.index_name.toLowerCase().startsWith('pk'),
fields: item.fields,
name: item.index_name,
tableName: undefined,
unique: item.index_description.toLowerCase().includes('unique'),
type: undefined
}));
}
handleInsertQuery(results, metaData) {
if (this.instance) {
// add the inserted row id to the instance
const autoIncrementAttribute = this.model.autoIncrementAttribute;
let id = null;
let autoIncrementAttributeAlias = null;
if (Object.prototype.hasOwnProperty.call(this.model.rawAttributes, autoIncrementAttribute) &&
this.model.rawAttributes[autoIncrementAttribute].field !== undefined)
autoIncrementAttributeAlias = this.model.rawAttributes[autoIncrementAttribute].field;
id = id || results && results[0][this.getInsertIdField()];
id = id || metaData && metaData[this.getInsertIdField()];
id = id || results && results[0][autoIncrementAttribute];
id = id || autoIncrementAttributeAlias && results && results[0][autoIncrementAttributeAlias];
this.instance[autoIncrementAttribute] = id;
}
}
}
module.exports = Query;
module.exports.Query = Query;
module.exports.default = Query;
+25
View File
@@ -0,0 +1,25 @@
'use strict';
const Promise = require('../../promise');
class ResourceLock {
constructor(resource) {
this.resource = resource;
this.previous = Promise.resolve(resource);
}
unwrap() {
return this.resource;
}
lock() {
const lock = this.previous;
let resolve;
this.previous = new Promise(r => {
resolve = r;
});
return lock.disposer(resolve);
}
}
module.exports = ResourceLock;
+159
View File
@@ -0,0 +1,159 @@
'use strict';
const AbstractConnectionManager = require('../abstract/connection-manager');
const SequelizeErrors = require('../../errors');
const Promise = require('../../promise');
const { logger } = require('../../utils/logger');
const DataTypes = require('../../data-types').mysql;
const momentTz = require('moment-timezone');
const debug = logger.debugContext('connection:mysql');
const parserStore = require('../parserStore')('mysql');
/**
* MySQL Connection Manager
*
* Get connections, validate and disconnect them.
* AbstractConnectionManager pooling use it to handle MySQL specific connections
* Use https://github.com/sidorares/node-mysql2 to connect with MySQL server
*
* @extends AbstractConnectionManager
* @returns Class<ConnectionManager>
* @private
*/
class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) {
sequelize.config.port = sequelize.config.port || 3306;
super(dialect, sequelize);
this.lib = this._loadDialectModule('mysql2');
this.refreshTypeParser(DataTypes);
}
_refreshTypeParser(dataType) {
parserStore.refresh(dataType);
}
_clearTypeParser() {
parserStore.clear();
}
static _typecast(field, next) {
if (parserStore.get(field.type)) {
return parserStore.get(field.type)(field, this.sequelize.options, next);
}
return next();
}
/**
* Connect with MySQL database based on config, Handle any errors in connection
* Set the pool handlers on connection.error
* Also set proper timezone once connection is connected.
*
* @param {Object} config
* @returns {Promise<Connection>}
* @private
*/
connect(config) {
const connectionConfig = Object.assign({
host: config.host,
port: config.port,
user: config.username,
flags: '-FOUND_ROWS',
password: config.password,
database: config.database,
timezone: this.sequelize.options.timezone,
typeCast: ConnectionManager._typecast.bind(this),
bigNumberStrings: false,
supportBigNumbers: true
}, config.dialectOptions);
return new Promise((resolve, reject) => {
const connection = this.lib.createConnection(connectionConfig);
const errorHandler = e => {
// clean up connect & error event if there is error
connection.removeListener('connect', connectHandler);
connection.removeListener('error', connectHandler);
reject(e);
};
const connectHandler = () => {
// clean up error event if connected
connection.removeListener('error', errorHandler);
resolve(connection);
};
// don't use connection.once for error event handling here
// mysql2 emit error two times in case handshake was failed
// first error is protocol_lost and second is timeout
// if we will use `once.error` node process will crash on 2nd error emit
connection.on('error', errorHandler);
connection.once('connect', connectHandler);
})
.tap(() => { debug('connection acquired'); })
.then(connection => {
connection.on('error', error => {
switch (error.code) {
case 'ESOCKET':
case 'ECONNRESET':
case 'EPIPE':
case 'PROTOCOL_CONNECTION_LOST':
this.pool.destroy(connection);
}
});
return new Promise((resolve, reject) => {
if (!this.sequelize.config.keepDefaultTimezone) {
// set timezone for this connection
// but named timezone are not directly supported in mysql, so get its offset first
let tzOffset = this.sequelize.options.timezone;
tzOffset = /\//.test(tzOffset) ? momentTz.tz(tzOffset).format('Z') : tzOffset;
return connection.query(`SET time_zone = '${tzOffset}'`, err => {
if (err) { reject(err); } else { resolve(connection); }
});
}
// return connection without executing SET time_zone query
resolve(connection);
});
})
.catch(err => {
switch (err.code) {
case 'ECONNREFUSED':
throw new SequelizeErrors.ConnectionRefusedError(err);
case 'ER_ACCESS_DENIED_ERROR':
throw new SequelizeErrors.AccessDeniedError(err);
case 'ENOTFOUND':
throw new SequelizeErrors.HostNotFoundError(err);
case 'EHOSTUNREACH':
throw new SequelizeErrors.HostNotReachableError(err);
case 'EINVAL':
throw new SequelizeErrors.InvalidConnectionError(err);
default:
throw new SequelizeErrors.ConnectionError(err);
}
});
}
disconnect(connection) {
// Don't disconnect connections with CLOSED state
if (connection._closing) {
debug('connection tried to disconnect but was already at CLOSED state');
return Promise.resolve();
}
return Promise.fromCallback(callback => connection.end(callback));
}
validate(connection) {
return connection
&& !connection._fatalError
&& !connection._protocolError
&& !connection._closing
&& !connection.stream.destroyed;
}
}
module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
+140
View File
@@ -0,0 +1,140 @@
'use strict';
const wkx = require('wkx');
const _ = require('lodash');
const moment = require('moment-timezone');
module.exports = BaseTypes => {
BaseTypes.ABSTRACT.prototype.dialectTypes = 'https://dev.mysql.com/doc/refman/5.7/en/data-types.html';
/**
* types: [buffer_type, ...]
* @see buffer_type here https://dev.mysql.com/doc/refman/5.7/en/c-api-prepared-statement-type-codes.html
* @see hex here https://github.com/sidorares/node-mysql2/blob/master/lib/constants/types.js
*/
BaseTypes.DATE.types.mysql = ['DATETIME'];
BaseTypes.STRING.types.mysql = ['VAR_STRING'];
BaseTypes.CHAR.types.mysql = ['STRING'];
BaseTypes.TEXT.types.mysql = ['BLOB'];
BaseTypes.TINYINT.types.mysql = ['TINY'];
BaseTypes.SMALLINT.types.mysql = ['SHORT'];
BaseTypes.MEDIUMINT.types.mysql = ['INT24'];
BaseTypes.INTEGER.types.mysql = ['LONG'];
BaseTypes.BIGINT.types.mysql = ['LONGLONG'];
BaseTypes.FLOAT.types.mysql = ['FLOAT'];
BaseTypes.TIME.types.mysql = ['TIME'];
BaseTypes.DATEONLY.types.mysql = ['DATE'];
BaseTypes.BOOLEAN.types.mysql = ['TINY'];
BaseTypes.BLOB.types.mysql = ['TINYBLOB', 'BLOB', 'LONGBLOB'];
BaseTypes.DECIMAL.types.mysql = ['NEWDECIMAL'];
BaseTypes.UUID.types.mysql = false;
BaseTypes.ENUM.types.mysql = false;
BaseTypes.REAL.types.mysql = ['DOUBLE'];
BaseTypes.DOUBLE.types.mysql = ['DOUBLE'];
BaseTypes.GEOMETRY.types.mysql = ['GEOMETRY'];
BaseTypes.JSON.types.mysql = ['JSON'];
class DECIMAL extends BaseTypes.DECIMAL {
toSql() {
let definition = super.toSql();
if (this._unsigned) {
definition += ' UNSIGNED';
}
if (this._zerofill) {
definition += ' ZEROFILL';
}
return definition;
}
}
class DATE extends BaseTypes.DATE {
toSql() {
return `DATETIME${this._length ? `(${this._length})` : ''}`;
}
_stringify(date, options) {
date = this._applyTimezone(date, options);
// Fractional DATETIMEs only supported on MySQL 5.6.4+
if (this._length) {
return date.format('YYYY-MM-DD HH:mm:ss.SSS');
}
return date.format('YYYY-MM-DD HH:mm:ss');
}
static parse(value, options) {
value = value.string();
if (value === null) {
return value;
}
if (moment.tz.zone(options.timezone)) {
value = moment.tz(value, options.timezone).toDate();
}
else {
value = new Date(`${value} ${options.timezone}`);
}
return value;
}
}
class DATEONLY extends BaseTypes.DATEONLY {
static parse(value) {
return value.string();
}
}
class UUID extends BaseTypes.UUID {
toSql() {
return 'CHAR(36) BINARY';
}
}
const SUPPORTED_GEOMETRY_TYPES = ['POINT', 'LINESTRING', 'POLYGON'];
class GEOMETRY extends BaseTypes.GEOMETRY {
constructor(type, srid) {
super(type, srid);
if (_.isEmpty(this.type)) {
this.sqlType = this.key;
return;
}
if (SUPPORTED_GEOMETRY_TYPES.includes(this.type)) {
this.sqlType = this.type;
return;
}
throw new Error(`Supported geometry types are: ${SUPPORTED_GEOMETRY_TYPES.join(', ')}`);
}
static parse(value) {
value = value.buffer();
// Empty buffer, MySQL doesn't support POINT EMPTY
// check, https://dev.mysql.com/worklog/task/?id=2381
if (!value || value.length === 0) {
return null;
}
// For some reason, discard the first 4 bytes
value = value.slice(4);
return wkx.Geometry.parse(value).toGeoJSON();
}
toSql() {
return this.sqlType;
}
}
class ENUM extends BaseTypes.ENUM {
toSql(options) {
return `ENUM(${this.values.map(value => options.escape(value)).join(', ')})`;
}
}
class JSONTYPE extends BaseTypes.JSON {
_stringify(value, options) {
return options.operation === 'where' && typeof value === 'string' ? value : JSON.stringify(value);
}
}
return {
ENUM,
DATE,
DATEONLY,
UUID,
GEOMETRY,
DECIMAL,
JSON: JSONTYPE
};
};
+60
View File
@@ -0,0 +1,60 @@
'use strict';
const _ = require('lodash');
const AbstractDialect = require('../abstract');
const ConnectionManager = require('./connection-manager');
const Query = require('./query');
const QueryGenerator = require('./query-generator');
const DataTypes = require('../../data-types').mysql;
class MysqlDialect extends AbstractDialect {
constructor(sequelize) {
super();
this.sequelize = sequelize;
this.connectionManager = new ConnectionManager(this, sequelize);
this.QueryGenerator = new QueryGenerator({
_dialect: this,
sequelize
});
}
}
MysqlDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
'VALUES ()': true,
'LIMIT ON UPDATE': true,
lock: true,
forShare: 'LOCK IN SHARE MODE',
settingIsolationLevelDuringTransaction: false,
inserts: {
ignoreDuplicates: ' IGNORE',
updateOnDuplicate: ' ON DUPLICATE KEY UPDATE'
},
index: {
collate: false,
length: true,
parser: true,
type: true,
using: 1
},
constraints: {
dropConstraint: false,
check: false
},
indexViaAlter: true,
indexHints: true,
NUMERIC: true,
GEOMETRY: true,
JSON: true,
REGEXP: true
});
ConnectionManager.prototype.defaultVersion = '5.6.0';
MysqlDialect.prototype.Query = Query;
MysqlDialect.prototype.QueryGenerator = QueryGenerator;
MysqlDialect.prototype.DataTypes = DataTypes;
MysqlDialect.prototype.name = 'mysql';
MysqlDialect.prototype.TICK_CHAR = '`';
MysqlDialect.prototype.TICK_CHAR_LEFT = MysqlDialect.prototype.TICK_CHAR;
MysqlDialect.prototype.TICK_CHAR_RIGHT = MysqlDialect.prototype.TICK_CHAR;
module.exports = MysqlDialect;
+541
View File
@@ -0,0 +1,541 @@
'use strict';
const _ = require('lodash');
const Utils = require('../../utils');
const AbstractQueryGenerator = require('../abstract/query-generator');
const util = require('util');
const Op = require('../../operators');
const jsonFunctionRegex = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i;
const jsonOperatorRegex = /^\s*(->>?|@>|<@|\?[|&]?|\|{2}|#-)/i;
const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
const foreignKeyFields = 'CONSTRAINT_NAME as constraint_name,'
+ 'CONSTRAINT_NAME as constraintName,'
+ 'CONSTRAINT_SCHEMA as constraintSchema,'
+ 'CONSTRAINT_SCHEMA as constraintCatalog,'
+ 'TABLE_NAME as tableName,'
+ 'TABLE_SCHEMA as tableSchema,'
+ 'TABLE_SCHEMA as tableCatalog,'
+ 'COLUMN_NAME as columnName,'
+ 'REFERENCED_TABLE_SCHEMA as referencedTableSchema,'
+ 'REFERENCED_TABLE_SCHEMA as referencedTableCatalog,'
+ 'REFERENCED_TABLE_NAME as referencedTableName,'
+ 'REFERENCED_COLUMN_NAME as referencedColumnName';
const typeWithoutDefault = new Set(['BLOB', 'TEXT', 'GEOMETRY', 'JSON']);
class MySQLQueryGenerator extends AbstractQueryGenerator {
constructor(options) {
super(options);
this.OperatorMap = Object.assign({}, this.OperatorMap, {
[Op.regexp]: 'REGEXP',
[Op.notRegexp]: 'NOT REGEXP'
});
}
createDatabaseQuery(databaseName, options) {
options = Object.assign({
charset: null,
collate: null
}, options || {});
const database = this.quoteIdentifier(databaseName);
const charset = options.charset ? ` DEFAULT CHARACTER SET ${this.escape(options.charset)}` : '';
const collate = options.collate ? ` DEFAULT COLLATE ${this.escape(options.collate)}` : '';
return `${`CREATE DATABASE IF NOT EXISTS ${database}${charset}${collate}`.trim()};`;
}
dropDatabaseQuery(databaseName) {
return `DROP DATABASE IF EXISTS ${this.quoteIdentifier(databaseName).trim()};`;
}
createSchema() {
return 'SHOW TABLES';
}
showSchemasQuery() {
return 'SHOW TABLES';
}
versionQuery() {
return 'SELECT VERSION() as `version`';
}
createTableQuery(tableName, attributes, options) {
options = Object.assign({
engine: 'InnoDB',
charset: null,
rowFormat: null
}, options || {});
const primaryKeys = [];
const foreignKeys = {};
const attrStr = [];
for (const attr in attributes) {
if (!Object.prototype.hasOwnProperty.call(attributes, attr)) continue;
const dataType = attributes[attr];
let match;
if (dataType.includes('PRIMARY KEY')) {
primaryKeys.push(attr);
if (dataType.includes('REFERENCES')) {
// MySQL doesn't support inline REFERENCES declarations: move to the end
match = dataType.match(/^(.+) (REFERENCES.*)$/);
attrStr.push(`${this.quoteIdentifier(attr)} ${match[1].replace('PRIMARY KEY', '')}`);
foreignKeys[attr] = match[2];
} else {
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType.replace('PRIMARY KEY', '')}`);
}
} else if (dataType.includes('REFERENCES')) {
// MySQL doesn't support inline REFERENCES declarations: move to the end
match = dataType.match(/^(.+) (REFERENCES.*)$/);
attrStr.push(`${this.quoteIdentifier(attr)} ${match[1]}`);
foreignKeys[attr] = match[2];
} else {
attrStr.push(`${this.quoteIdentifier(attr)} ${dataType}`);
}
}
const table = this.quoteTable(tableName);
let attributesClause = attrStr.join(', ');
const comment = options.comment && typeof options.comment === 'string' ? ` COMMENT ${this.escape(options.comment)}` : '';
const engine = options.engine;
const charset = options.charset ? ` DEFAULT CHARSET=${options.charset}` : '';
const collation = options.collate ? ` COLLATE ${options.collate}` : '';
const rowFormat = options.rowFormat ? ` ROW_FORMAT=${options.rowFormat}` : '';
const initialAutoIncrement = options.initialAutoIncrement ? ` AUTO_INCREMENT=${options.initialAutoIncrement}` : '';
const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
if (options.uniqueKeys) {
_.each(options.uniqueKeys, (columns, indexName) => {
if (columns.customIndex) {
if (typeof indexName !== 'string') {
indexName = `uniq_${tableName}_${columns.fields.join('_')}`;
}
attributesClause += `, UNIQUE ${this.quoteIdentifier(indexName)} (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
}
});
}
if (pkString.length > 0) {
attributesClause += `, PRIMARY KEY (${pkString})`;
}
for (const fkey in foreignKeys) {
if (Object.prototype.hasOwnProperty.call(foreignKeys, fkey)) {
attributesClause += `, FOREIGN KEY (${this.quoteIdentifier(fkey)}) ${foreignKeys[fkey]}`;
}
}
return `CREATE TABLE IF NOT EXISTS ${table} (${attributesClause}) ENGINE=${engine}${comment}${charset}${collation}${initialAutoIncrement}${rowFormat};`;
}
describeTableQuery(tableName, schema, schemaDelimiter) {
const table = this.quoteTable(
this.addSchema({
tableName,
_schema: schema,
_schemaDelimiter: schemaDelimiter
})
);
return `SHOW FULL COLUMNS FROM ${table};`;
}
showTablesQuery(database) {
let query = 'SELECT TABLE_NAME FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_TYPE = \'BASE TABLE\'';
if (database) {
query += ` AND TABLE_SCHEMA = ${this.escape(database)}`;
} else {
query += ' AND TABLE_SCHEMA NOT IN (\'MYSQL\', \'INFORMATION_SCHEMA\', \'PERFORMANCE_SCHEMA\', \'SYS\')';
}
return `${query};`;
}
addColumnQuery(table, key, dataType) {
const definition = this.attributeToSQL(dataType, {
context: 'addColumn',
tableName: table,
foreignKey: key
});
return `ALTER TABLE ${this.quoteTable(table)} ADD ${this.quoteIdentifier(key)} ${definition};`;
}
removeColumnQuery(tableName, attributeName) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP ${this.quoteIdentifier(attributeName)};`;
}
changeColumnQuery(tableName, attributes) {
const attrString = [];
const constraintString = [];
for (const attributeName in attributes) {
let definition = attributes[attributeName];
if (definition.includes('REFERENCES')) {
const attrName = this.quoteIdentifier(attributeName);
definition = definition.replace(/.+?(?=REFERENCES)/, '');
constraintString.push(`FOREIGN KEY (${attrName}) ${definition}`);
} else {
attrString.push(`\`${attributeName}\` \`${attributeName}\` ${definition}`);
}
}
let finalQuery = '';
if (attrString.length) {
finalQuery += `CHANGE ${attrString.join(', ')}`;
finalQuery += constraintString.length ? ' ' : '';
}
if (constraintString.length) {
finalQuery += `ADD ${constraintString.join(', ')}`;
}
return `ALTER TABLE ${this.quoteTable(tableName)} ${finalQuery};`;
}
renameColumnQuery(tableName, attrBefore, attributes) {
const attrString = [];
for (const attrName in attributes) {
const definition = attributes[attrName];
attrString.push(`\`${attrBefore}\` \`${attrName}\` ${definition}`);
}
return `ALTER TABLE ${this.quoteTable(tableName)} CHANGE ${attrString.join(', ')};`;
}
handleSequelizeMethod(smth, tableName, factory, options, prepend) {
if (smth instanceof Utils.Json) {
// Parse nested object
if (smth.conditions) {
const conditions = this.parseConditionObject(smth.conditions).map(condition =>
`${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
);
return conditions.join(' AND ');
}
if (smth.path) {
let str;
// Allow specifying conditions using the sqlite json functions
if (this._checkValidJsonStatement(smth.path)) {
str = smth.path;
} else {
// Also support json property accessors
const paths = _.toPath(smth.path);
const column = paths.shift();
str = this.jsonPathExtractionQuery(column, paths);
}
if (smth.value) {
str += util.format(' = %s', this.escape(smth.value));
}
return str;
}
} else if (smth instanceof Utils.Cast) {
if (/timestamp/i.test(smth.type)) {
smth.type = 'datetime';
} else if (smth.json && /boolean/i.test(smth.type)) {
// true or false cannot be casted as booleans within a JSON structure
smth.type = 'char';
} else if (/double precision/i.test(smth.type) || /boolean/i.test(smth.type) || /integer/i.test(smth.type)) {
smth.type = 'decimal';
} else if (/text/i.test(smth.type)) {
smth.type = 'char';
}
}
return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
}
_toJSONValue(value) {
// true/false are stored as strings in mysql
if (typeof value === 'boolean') {
return value.toString();
}
// null is stored as a string in mysql
if (value === null) {
return 'null';
}
return value;
}
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
options.onDuplicate = 'UPDATE ';
options.onDuplicate += Object.keys(updateValues).map(key => {
key = this.quoteIdentifier(key);
return `${key}=VALUES(${key})`;
}).join(', ');
return this.insertQuery(tableName, insertValues, model.rawAttributes, options);
}
truncateTableQuery(tableName) {
return `TRUNCATE ${this.quoteTable(tableName)}`;
}
deleteQuery(tableName, where, options = {}, model) {
let limit = '';
let query = `DELETE FROM ${this.quoteTable(tableName)}`;
if (options.limit) {
limit = ` LIMIT ${this.escape(options.limit)}`;
}
where = this.getWhereConditions(where, null, model, options);
if (where) {
query += ` WHERE ${where}`;
}
return query + limit;
}
showIndexesQuery(tableName, options) {
return `SHOW INDEX FROM ${this.quoteTable(tableName)}${(options || {}).database ? ` FROM \`${options.database}\`` : ''}`;
}
showConstraintsQuery(table, constraintName) {
const tableName = table.tableName || table;
const schemaName = table.schema;
let sql = [
'SELECT CONSTRAINT_CATALOG AS constraintCatalog,',
'CONSTRAINT_NAME AS constraintName,',
'CONSTRAINT_SCHEMA AS constraintSchema,',
'CONSTRAINT_TYPE AS constraintType,',
'TABLE_NAME AS tableName,',
'TABLE_SCHEMA AS tableSchema',
'from INFORMATION_SCHEMA.TABLE_CONSTRAINTS',
`WHERE table_name='${tableName}'`
].join(' ');
if (constraintName) {
sql += ` AND constraint_name = '${constraintName}'`;
}
if (schemaName) {
sql += ` AND TABLE_SCHEMA = '${schemaName}'`;
}
return `${sql};`;
}
removeIndexQuery(tableName, indexNameOrAttributes) {
let indexName = indexNameOrAttributes;
if (typeof indexName !== 'string') {
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
}
return `DROP INDEX ${this.quoteIdentifier(indexName)} ON ${this.quoteTable(tableName)}`;
}
attributeToSQL(attribute, options) {
if (!_.isPlainObject(attribute)) {
attribute = {
type: attribute
};
}
const attributeString = attribute.type.toString({ escape: this.escape.bind(this) });
let template = attributeString;
if (attribute.allowNull === false) {
template += ' NOT NULL';
}
if (attribute.autoIncrement) {
template += ' auto_increment';
}
// BLOB/TEXT/GEOMETRY/JSON cannot have a default value
if (!typeWithoutDefault.has(attributeString)
&& attribute.type._binary !== true
&& Utils.defaultValueSchemable(attribute.defaultValue)) {
template += ` DEFAULT ${this.escape(attribute.defaultValue)}`;
}
if (attribute.unique === true) {
template += ' UNIQUE';
}
if (attribute.primaryKey) {
template += ' PRIMARY KEY';
}
if (attribute.comment) {
template += ` COMMENT ${this.escape(attribute.comment)}`;
}
if (attribute.first) {
template += ' FIRST';
}
if (attribute.after) {
template += ` AFTER ${this.quoteIdentifier(attribute.after)}`;
}
if (attribute.references) {
if (options && options.context === 'addColumn' && options.foreignKey) {
const attrName = this.quoteIdentifier(options.foreignKey);
const fkName = this.quoteIdentifier(`${options.tableName}_${attrName}_foreign_idx`);
template += `, ADD CONSTRAINT ${fkName} FOREIGN KEY (${attrName})`;
}
template += ` REFERENCES ${this.quoteTable(attribute.references.model)}`;
if (attribute.references.key) {
template += ` (${this.quoteIdentifier(attribute.references.key)})`;
} else {
template += ` (${this.quoteIdentifier('id')})`;
}
if (attribute.onDelete) {
template += ` ON DELETE ${attribute.onDelete.toUpperCase()}`;
}
if (attribute.onUpdate) {
template += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`;
}
}
return template;
}
attributesToSQL(attributes, options) {
const result = {};
for (const key in attributes) {
const attribute = attributes[key];
result[attribute.field || key] = this.attributeToSQL(attribute, options);
}
return result;
}
/**
* Check whether the statmement is json function or simple path
*
* @param {string} stmt The statement to validate
* @returns {boolean} true if the given statement is json function
* @throws {Error} throw if the statement looks like json function but has invalid token
* @private
*/
_checkValidJsonStatement(stmt) {
if (typeof stmt !== 'string') {
return false;
}
let currentIndex = 0;
let openingBrackets = 0;
let closingBrackets = 0;
let hasJsonFunction = false;
let hasInvalidToken = false;
while (currentIndex < stmt.length) {
const string = stmt.substr(currentIndex);
const functionMatches = jsonFunctionRegex.exec(string);
if (functionMatches) {
currentIndex += functionMatches[0].indexOf('(');
hasJsonFunction = true;
continue;
}
const operatorMatches = jsonOperatorRegex.exec(string);
if (operatorMatches) {
currentIndex += operatorMatches[0].length;
hasJsonFunction = true;
continue;
}
const tokenMatches = tokenCaptureRegex.exec(string);
if (tokenMatches) {
const capturedToken = tokenMatches[1];
if (capturedToken === '(') {
openingBrackets++;
} else if (capturedToken === ')') {
closingBrackets++;
} else if (capturedToken === ';') {
hasInvalidToken = true;
break;
}
currentIndex += tokenMatches[0].length;
continue;
}
break;
}
// Check invalid json statement
if (hasJsonFunction && (hasInvalidToken || openingBrackets !== closingBrackets)) {
throw new Error(`Invalid json statement: ${stmt}`);
}
// return true if the statement has valid json function
return hasJsonFunction;
}
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {Object} table The table.
* @param {string} schemaName The name of the schema.
* @returns {string} The generated sql query.
* @private
*/
getForeignKeysQuery(table, schemaName) {
const tableName = table.tableName || table;
return `SELECT ${foreignKeyFields} FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE where TABLE_NAME = '${tableName}' AND CONSTRAINT_NAME!='PRIMARY' AND CONSTRAINT_SCHEMA='${schemaName}' AND REFERENCED_TABLE_NAME IS NOT NULL;`;
}
/**
* Generates an SQL query that returns the foreign key constraint of a given column.
*
* @param {Object} table The table.
* @param {string} columnName The name of the column.
* @returns {string} The generated sql query.
* @private
*/
getForeignKeyQuery(table, columnName) {
const quotedSchemaName = table.schema ? wrapSingleQuote(table.schema) : '';
const quotedTableName = wrapSingleQuote(table.tableName || table);
const quotedColumnName = wrapSingleQuote(columnName);
return `SELECT ${foreignKeyFields} FROM INFORMATION_SCHEMA.KEY_COLUMN_USAGE`
+ ` WHERE (REFERENCED_TABLE_NAME = ${quotedTableName}${table.schema
? ` AND REFERENCED_TABLE_SCHEMA = ${quotedSchemaName}`
: ''} AND REFERENCED_COLUMN_NAME = ${quotedColumnName})`
+ ` OR (TABLE_NAME = ${quotedTableName}${table.schema ?
` AND TABLE_SCHEMA = ${quotedSchemaName}` : ''} AND COLUMN_NAME = ${quotedColumnName} AND REFERENCED_TABLE_NAME IS NOT NULL)`;
}
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {string} tableName The name of the table.
* @param {string} foreignKey The name of the foreign key constraint.
* @returns {string} The generated sql query.
* @private
*/
dropForeignKeyQuery(tableName, foreignKey) {
return `ALTER TABLE ${this.quoteTable(tableName)}
DROP FOREIGN KEY ${this.quoteIdentifier(foreignKey)};`;
}
}
// private methods
function wrapSingleQuote(identifier) {
return Utils.addTicks(identifier, '\'');
}
module.exports = MySQLQueryGenerator;
+91
View File
@@ -0,0 +1,91 @@
'use strict';
/**
Returns an object that treats MySQL's inabilities to do certain queries.
@class QueryInterface
@static
@private
*/
const Promise = require('../../promise');
const sequelizeErrors = require('../../errors');
/**
A wrapper that fixes MySQL's inability to cleanly remove columns from existing tables if they have a foreign key constraint.
@param {QueryInterface} qi
@param {string} tableName The name of the table.
@param {string} columnName The name of the attribute that we want to remove.
@param {Object} options
@private
*/
function removeColumn(qi, tableName, columnName, options) {
options = options || {};
return qi.sequelize.query(
qi.QueryGenerator.getForeignKeyQuery(tableName.tableName ? tableName : {
tableName,
schema: qi.sequelize.config.database
}, columnName),
Object.assign({ raw: true }, options)
)
.then(([results]) => {
//Exclude primary key constraint
if (!results.length || results[0].constraint_name === 'PRIMARY') {
// No foreign key constraints found, so we can remove the column
return;
}
return Promise.map(results, constraint => qi.sequelize.query(
qi.QueryGenerator.dropForeignKeyQuery(tableName, constraint.constraint_name),
Object.assign({ raw: true }, options)
));
})
.then(() => qi.sequelize.query(
qi.QueryGenerator.removeColumnQuery(tableName, columnName),
Object.assign({ raw: true }, options)
));
}
/**
* @param {QueryInterface} qi
* @param {string} tableName
* @param {string} constraintName
* @param {Object} options
*
* @private
*/
function removeConstraint(qi, tableName, constraintName, options) {
const sql = qi.QueryGenerator.showConstraintsQuery(
tableName.tableName ? tableName : {
tableName,
schema: qi.sequelize.config.database
}, constraintName);
return qi.sequelize.query(sql, Object.assign({}, options,
{ type: qi.sequelize.QueryTypes.SHOWCONSTRAINTS }))
.then(constraints => {
const constraint = constraints[0];
let query;
if (!constraint || !constraint.constraintType) {
throw new sequelizeErrors.UnknownConstraintError(
{
message: `Constraint ${constraintName} on table ${tableName} does not exist`,
constraint: constraintName,
table: tableName
});
}
if (constraint.constraintType === 'FOREIGN KEY') {
query = qi.QueryGenerator.dropForeignKeyQuery(tableName, constraintName);
} else {
query = qi.QueryGenerator.removeIndexQuery(constraint.tableName, constraint.constraintName);
}
return qi.sequelize.query(query, options);
});
}
exports.removeConstraint = removeConstraint;
exports.removeColumn = removeColumn;
+279
View File
@@ -0,0 +1,279 @@
'use strict';
const Utils = require('../../utils');
const AbstractQuery = require('../abstract/query');
const sequelizeErrors = require('../../errors');
const _ = require('lodash');
const { logger } = require('../../utils/logger');
const debug = logger.debugContext('sql:mysql');
class Query extends AbstractQuery {
constructor(connection, sequelize, options) {
super(connection, sequelize, Object.assign({ showWarnings: false }, options));
}
static formatBindParameters(sql, values, dialect) {
const bindParam = [];
const replacementFunc = (match, key, values) => {
if (values[key] !== undefined) {
bindParam.push(values[key]);
return '?';
}
return undefined;
};
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
return [sql, bindParam.length > 0 ? bindParam : undefined];
}
run(sql, parameters) {
this.sql = sql;
const { connection, options } = this;
//do we need benchmark for this query execution
const showWarnings = this.sequelize.options.showWarnings || options.showWarnings;
const complete = this._logQuery(sql, debug, parameters);
return new Utils.Promise((resolve, reject) => {
const handler = (err, results) => {
complete();
if (err) {
// MySQL automatically rolls-back transactions in the event of a deadlock
if (options.transaction && err.errno === 1213) {
options.transaction.finished = 'rollback';
}
err.sql = sql;
err.parameters = parameters;
reject(this.formatError(err));
} else {
resolve(results);
}
};
if (parameters) {
debug('parameters(%j)', parameters);
connection.execute(sql, parameters, handler).setMaxListeners(100);
} else {
connection.query({ sql }, handler).setMaxListeners(100);
}
})
// Log warnings if we've got them.
.then(results => {
if (showWarnings && results && results.warningStatus > 0) {
return this.logWarnings(results);
}
return results;
})
// Return formatted results...
.then(results => this.formatResults(results));
}
/**
* High level function that handles the results of a query execution.
*
*
* Example:
* query.formatResults([
* {
* id: 1, // this is from the main table
* attr2: 'snafu', // this is from the main table
* Tasks.id: 1, // this is from the associated table
* Tasks.title: 'task' // this is from the associated table
* }
* ])
*
* @param {Array} data - The result of the query execution.
* @private
*/
formatResults(data) {
let result = this.instance;
if (this.isInsertQuery(data)) {
this.handleInsertQuery(data);
if (!this.instance) {
// handle bulkCreate AI primiary key
if (
data.constructor.name === 'ResultSetHeader'
&& this.model
&& this.model.autoIncrementAttribute
&& this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
&& this.model.rawAttributes[this.model.primaryKeyAttribute]
) {
const startId = data[this.getInsertIdField()];
result = [];
for (let i = startId; i < startId + data.affectedRows; i++) {
result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: i });
}
} else {
result = data[this.getInsertIdField()];
}
}
}
if (this.isSelectQuery()) {
return this.handleSelectQuery(data);
}
if (this.isShowTablesQuery()) {
return this.handleShowTablesQuery(data);
}
if (this.isDescribeQuery()) {
result = {};
for (const _result of data) {
const enumRegex = /^enum/i;
result[_result.Field] = {
type: enumRegex.test(_result.Type) ? _result.Type.replace(enumRegex, 'ENUM') : _result.Type.toUpperCase(),
allowNull: _result.Null === 'YES',
defaultValue: _result.Default,
primaryKey: _result.Key === 'PRI',
autoIncrement: Object.prototype.hasOwnProperty.call(_result, 'Extra') && _result.Extra.toLowerCase() === 'auto_increment',
comment: _result.Comment ? _result.Comment : null
};
}
return result;
}
if (this.isShowIndexesQuery()) {
return this.handleShowIndexesQuery(data);
}
if (this.isCallQuery()) {
return data[0];
}
if (this.isBulkUpdateQuery() || this.isBulkDeleteQuery() || this.isUpsertQuery()) {
return data.affectedRows;
}
if (this.isVersionQuery()) {
return data[0].version;
}
if (this.isForeignKeysQuery()) {
return data;
}
if (this.isInsertQuery() || this.isUpdateQuery()) {
return [result, data.affectedRows];
}
if (this.isShowConstraintsQuery()) {
return data;
}
if (this.isRawQuery()) {
// MySQL returns row data and metadata (affected rows etc) in a single object - let's standarize it, sorta
return [data, data];
}
return result;
}
logWarnings(results) {
return this.run('SHOW WARNINGS').then(warningResults => {
const warningMessage = `MySQL Warnings (${this.connection.uuid || 'default'}): `;
const messages = [];
for (const _warningRow of warningResults) {
if (_warningRow === undefined || typeof _warningRow[Symbol.iterator] !== 'function') continue;
for (const _warningResult of _warningRow) {
if (Object.prototype.hasOwnProperty.call(_warningResult, 'Message')) {
messages.push(_warningResult.Message);
} else {
for (const _objectKey of _warningResult.keys()) {
messages.push([_objectKey, _warningResult[_objectKey]].join(': '));
}
}
}
}
this.sequelize.log(warningMessage + messages.join('; '), this.options);
return results;
});
}
formatError(err) {
const errCode = err.errno || err.code;
switch (errCode) {
case 1062: {
const match = err.message.match(/Duplicate entry '([\s\S]*)' for key '?((.|\s)*?)'?$/);
let fields = {};
let message = 'Validation error';
const values = match ? match[1].split('-') : undefined;
const fieldKey = match ? match[2] : undefined;
const fieldVal = match ? match[1] : undefined;
const uniqueKey = this.model && this.model.uniqueKeys[fieldKey];
if (uniqueKey) {
if (uniqueKey.msg) message = uniqueKey.msg;
fields = _.zipObject(uniqueKey.fields, values);
} else {
fields[fieldKey] = fieldVal;
}
const errors = [];
_.forOwn(fields, (value, field) => {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
value,
this.instance,
'not_unique'
));
});
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
}
case 1451:
case 1452: {
// e.g. CONSTRAINT `example_constraint_name` FOREIGN KEY (`example_id`) REFERENCES `examples` (`id`)
const match = err.message.match(/CONSTRAINT ([`"])(.*)\1 FOREIGN KEY \(\1(.*)\1\) REFERENCES \1(.*)\1 \(\1(.*)\1\)/);
const quoteChar = match ? match[1] : '`';
const fields = match ? match[3].split(new RegExp(`${quoteChar}, *${quoteChar}`)) : undefined;
return new sequelizeErrors.ForeignKeyConstraintError({
reltype: String(errCode) === '1451' ? 'parent' : 'child',
table: match ? match[4] : undefined,
fields,
value: fields && fields.length && this.instance && this.instance[fields[0]] || undefined,
index: match ? match[2] : undefined,
parent: err
});
}
default:
return new sequelizeErrors.DatabaseError(err);
}
}
handleShowIndexesQuery(data) {
// Group by index name, and collect all fields
data = data.reduce((acc, item) => {
if (!(item.Key_name in acc)) {
acc[item.Key_name] = item;
item.fields = [];
}
acc[item.Key_name].fields[item.Seq_in_index - 1] = {
attribute: item.Column_name,
length: item.Sub_part || undefined,
order: item.Collation === 'A' ? 'ASC' : undefined
};
delete item.column_name;
return acc;
}, {});
return _.map(data, item => ({
primary: item.Key_name === 'PRIMARY',
fields: item.fields,
name: item.Key_name,
tableName: item.Table,
unique: item.Non_unique !== 1,
type: item.Index_type
}));
}
}
module.exports = Query;
module.exports.Query = Query;
module.exports.default = Query;
+23
View File
@@ -0,0 +1,23 @@
'use strict';
const stores = new Map();
module.exports = dialect => {
if (!stores.has(dialect)) {
stores.set(dialect, new Map());
}
return {
clear() {
stores.get(dialect).clear();
},
refresh(dataType) {
for (const type of dataType.types[dialect]) {
stores.get(dialect).set(type, dataType.parse);
}
},
get(type) {
return stores.get(dialect).get(type);
}
};
};
+320
View File
@@ -0,0 +1,320 @@
'use strict';
const _ = require('lodash');
const AbstractConnectionManager = require('../abstract/connection-manager');
const { logger } = require('../../utils/logger');
const debug = logger.debugContext('connection:pg');
const Promise = require('../../promise');
const sequelizeErrors = require('../../errors');
const semver = require('semver');
const dataTypes = require('../../data-types');
const moment = require('moment-timezone');
class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) {
sequelize.config.port = sequelize.config.port || 5432;
super(dialect, sequelize);
const pgLib = this._loadDialectModule('pg');
this.lib = this.sequelize.config.native ? pgLib.native : pgLib;
this._clearDynamicOIDs();
this._clearTypeParser();
this.refreshTypeParser(dataTypes.postgres);
}
// Expose this as a method so that the parsing may be updated when the user has added additional, custom types
_refreshTypeParser(dataType) {
const arrayParserBuilder = parser => {
return value => this.lib.types.arrayParser.create(value, parser).parse();
};
const rangeParserBuilder = parser => {
return value => dataType.parse(value, { parser });
};
// Set range parsers
if (dataType.key.toLowerCase() === 'range') {
for (const name in this.nameOidMap) {
const entry = this.nameOidMap[name];
if (! entry.rangeOid) continue;
const rangeParser = rangeParserBuilder(this.getTypeParser(entry.oid));
const arrayRangeParser = arrayParserBuilder(rangeParser);
this.oidParserMap.set(entry.rangeOid, rangeParser);
if (! entry.arrayRangeOid) continue;
this.oidParserMap.set(entry.arrayRangeOid, arrayRangeParser);
}
return;
}
// Create parsers for normal or enum data types
const parser = value => dataType.parse(value);
const arrayParser = arrayParserBuilder(parser);
// Set enum parsers
if (dataType.key.toLowerCase() === 'enum') {
this.enumOids.oids.forEach(oid => {
this.oidParserMap.set(oid, parser);
});
this.enumOids.arrayOids.forEach(arrayOid => {
this.oidParserMap.set(arrayOid, arrayParser);
});
return;
}
// Set parsers for normal data types
dataType.types.postgres.forEach(name => {
if (! this.nameOidMap[name]) return;
this.oidParserMap.set(this.nameOidMap[name].oid, parser);
if (! this.nameOidMap[name].arrayOid) return;
this.oidParserMap.set(this.nameOidMap[name].arrayOid, arrayParser);
});
}
_clearTypeParser() {
this.oidParserMap = new Map();
}
getTypeParser(oid, ...args) {
if (this.oidParserMap.get(oid)) return this.oidParserMap.get(oid);
return this.lib.types.getTypeParser(oid, ...args);
}
connect(config) {
config.user = config.username;
const connectionConfig = _.pick(config, [
'user', 'password', 'host', 'database', 'port'
]);
connectionConfig.types = {
getTypeParser: ConnectionManager.prototype.getTypeParser.bind(this)
};
if (config.dialectOptions) {
_.merge(connectionConfig,
_.pick(config.dialectOptions, [
// see [http://www.postgresql.org/docs/9.3/static/runtime-config-logging.html#GUC-APPLICATION-NAME]
'application_name',
// choose the SSL mode with the PGSSLMODE environment variable
// object format: [https://github.com/brianc/node-postgres/blob/master/lib/connection.js#L79]
// see also [http://www.postgresql.org/docs/9.3/static/libpq-ssl.html]
'ssl',
// In addition to the values accepted by the corresponding server,
// you can use "auto" to determine the right encoding from the
// current locale in the client (LC_CTYPE environment variable on Unix systems)
'client_encoding',
// !! DO NOT SET THIS TO TRUE !!
// (unless you know what you're doing)
// see [http://www.postgresql.org/message-id/flat/bc9549a50706040852u27633f41ib1e6b09f8339d845@mail.gmail.com#bc9549a50706040852u27633f41ib1e6b09f8339d845@mail.gmail.com]
'binary',
// This should help with backends incorrectly considering idle clients to be dead and prematurely disconnecting them.
// this feature has been added in pg module v6.0.0, check pg/CHANGELOG.md
'keepAlive',
// Times out queries after a set time in milliseconds. Added in pg v7.3
'statement_timeout'
]));
}
return new Promise((resolve, reject) => {
let responded = false;
const connection = new this.lib.Client(connectionConfig);
const parameterHandler = message => {
switch (message.parameterName) {
case 'server_version':
if (this.sequelize.options.databaseVersion === 0) {
const version = semver.coerce(message.parameterValue).version;
this.sequelize.options.databaseVersion = semver.valid(version)
? version
: this.defaultVersion;
}
break;
case 'standard_conforming_strings':
connection['standard_conforming_strings'] = message.parameterValue;
break;
}
};
const endHandler = () => {
debug('connection timeout');
if (!responded) {
reject(new sequelizeErrors.ConnectionTimedOutError(new Error('Connection timed out')));
}
};
// If we didn't ever hear from the client.connect() callback the connection timeout
// node-postgres does not treat this as an error since no active query was ever emitted
connection.once('end', endHandler);
if (!this.sequelize.config.native) {
// Receive various server parameters for further configuration
connection.connection.on('parameterStatus', parameterHandler);
}
connection.connect(err => {
responded = true;
if (!this.sequelize.config.native) {
// remove parameter handler
connection.connection.removeListener('parameterStatus', parameterHandler);
}
if (err) {
if (err.code) {
switch (err.code) {
case 'ECONNREFUSED':
reject(new sequelizeErrors.ConnectionRefusedError(err));
break;
case 'ENOTFOUND':
reject(new sequelizeErrors.HostNotFoundError(err));
break;
case 'EHOSTUNREACH':
reject(new sequelizeErrors.HostNotReachableError(err));
break;
case 'EINVAL':
reject(new sequelizeErrors.InvalidConnectionError(err));
break;
default:
reject(new sequelizeErrors.ConnectionError(err));
break;
}
} else {
reject(new sequelizeErrors.ConnectionError(err));
}
} else {
debug('connection acquired');
connection.removeListener('end', endHandler);
resolve(connection);
}
});
}).tap(connection => {
let query = '';
if (this.sequelize.options.standardConformingStrings !== false && connection['standard_conforming_strings'] !== 'on') {
// Disable escape characters in strings
// see https://github.com/sequelize/sequelize/issues/3545 (security issue)
// see https://www.postgresql.org/docs/current/static/runtime-config-compatible.html#GUC-STANDARD-CONFORMING-STRINGS
query += 'SET standard_conforming_strings=on;';
}
if (this.sequelize.options.clientMinMessages !== false) {
query += `SET client_min_messages TO ${this.sequelize.options.clientMinMessages};`;
}
if (!this.sequelize.config.keepDefaultTimezone) {
const isZone = !!moment.tz.zone(this.sequelize.options.timezone);
if (isZone) {
query += `SET TIME ZONE '${this.sequelize.options.timezone}';`;
} else {
query += `SET TIME ZONE INTERVAL '${this.sequelize.options.timezone}' HOUR TO MINUTE;`;
}
}
if (query) {
return connection.query(query);
}
}).tap(connection => {
if (Object.keys(this.nameOidMap).length === 0 &&
this.enumOids.oids.length === 0 &&
this.enumOids.arrayOids.length === 0) {
return this._refreshDynamicOIDs(connection);
}
}).tap(connection => {
// Don't let a Postgres restart (or error) to take down the whole app
connection.on('error', error => {
connection._invalid = true;
debug(`connection error ${error.code || error.message}`);
this.pool.destroy(connection);
});
});
}
disconnect(connection) {
if (connection._ending) {
debug('connection tried to disconnect but was already at ENDING state');
return Promise.resolve();
}
return Promise.fromCallback(callback => connection.end(callback));
}
validate(connection) {
return !connection._invalid && !connection._ending;
}
_refreshDynamicOIDs(connection) {
const databaseVersion = this.sequelize.options.databaseVersion;
const supportedVersion = '8.3.0';
// Check for supported version
if ( (databaseVersion && semver.gte(databaseVersion, supportedVersion)) === false) {
return Promise.resolve();
}
// Refresh dynamic OIDs for some types
// These include Geometry / Geography / HStore / Enum / Citext / Range
return (connection || this.sequelize).query(
'WITH ranges AS (' +
' SELECT pg_range.rngtypid, pg_type.typname AS rngtypname,' +
' pg_type.typarray AS rngtyparray, pg_range.rngsubtype' +
' FROM pg_range LEFT OUTER JOIN pg_type ON pg_type.oid = pg_range.rngtypid' +
')' +
'SELECT pg_type.typname, pg_type.typtype, pg_type.oid, pg_type.typarray,' +
' ranges.rngtypname, ranges.rngtypid, ranges.rngtyparray' +
' FROM pg_type LEFT OUTER JOIN ranges ON pg_type.oid = ranges.rngsubtype' +
' WHERE (pg_type.typtype IN(\'b\', \'e\'));'
).then(results => {
let result = Array.isArray(results) ? results.pop() : results;
// When searchPath is prepended then two statements are executed and the result is
// an array of those two statements. First one is the SET search_path and second is
// the SELECT query result.
if (Array.isArray(result)) {
if (result[0].command === 'SET') {
result = result.pop();
}
}
const newNameOidMap = {};
const newEnumOids = { oids: [], arrayOids: [] };
for (const row of result.rows) {
// Mapping enums, handled separatedly
if (row.typtype === 'e') {
newEnumOids.oids.push(row.oid);
if (row.typarray) newEnumOids.arrayOids.push(row.typarray);
continue;
}
// Mapping base types and their arrays
newNameOidMap[row.typname] = { oid: row.oid };
if (row.typarray) newNameOidMap[row.typname].arrayOid = row.typarray;
// Mapping ranges(of base types) and their arrays
if (row.rngtypid) {
newNameOidMap[row.typname].rangeOid = row.rngtypid;
if (row.rngtyparray) newNameOidMap[row.typname].arrayRangeOid = row.rngtyparray;
}
}
// Replace all OID mappings. Avoids temporary empty OID mappings.
this.nameOidMap = newNameOidMap;
this.enumOids = newEnumOids;
this.refreshTypeParser(dataTypes.postgres);
});
}
_clearDynamicOIDs() {
this.nameOidMap = {};
this.enumOids = { oids: [], arrayOids: [] };
}
}
module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
+526
View File
@@ -0,0 +1,526 @@
'use strict';
const _ = require('lodash');
const wkx = require('wkx');
module.exports = BaseTypes => {
const warn = BaseTypes.ABSTRACT.warn.bind(undefined, 'http://www.postgresql.org/docs/9.4/static/datatype.html');
/**
* Removes unsupported Postgres options, i.e., LENGTH, UNSIGNED and ZEROFILL, for the integer data types.
*
* @param {Object} dataType The base integer data type.
* @private
*/
function removeUnsupportedIntegerOptions(dataType) {
if (dataType._length || dataType.options.length || dataType._unsigned || dataType._zerofill) {
warn(`PostgresSQL does not support '${dataType.key}' with LENGTH, UNSIGNED or ZEROFILL. Plain '${dataType.key}' will be used instead.`);
dataType._length = undefined;
dataType.options.length = undefined;
dataType._unsigned = undefined;
dataType._zerofill = undefined;
}
}
/**
* types:
* {
* oids: [oid],
* array_oids: [oid]
* }
* @see oid here https://github.com/lib/pq/blob/master/oid/types.go
*/
BaseTypes.UUID.types.postgres = ['uuid'];
BaseTypes.CIDR.types.postgres = ['cidr'];
BaseTypes.INET.types.postgres = ['inet'];
BaseTypes.MACADDR.types.postgres = ['macaddr'];
BaseTypes.JSON.types.postgres = ['json'];
BaseTypes.JSONB.types.postgres = ['jsonb'];
BaseTypes.TIME.types.postgres = ['time'];
class DATEONLY extends BaseTypes.DATEONLY {
_stringify(value, options) {
if (value === Infinity) {
return 'Infinity';
}
if (value === -Infinity) {
return '-Infinity';
}
return super._stringify(value, options);
}
_sanitize(value, options) {
if ((!options || options && !options.raw) && value !== Infinity && value !== -Infinity) {
if (typeof value === 'string') {
const lower = value.toLowerCase();
if (lower === 'infinity') {
return Infinity;
}
if (lower === '-infinity') {
return -Infinity;
}
}
return super._sanitize(value);
}
return value;
}
static parse(value) {
if (value === 'infinity') {
return Infinity;
}
if (value === '-infinity') {
return -Infinity;
}
return value;
}
}
BaseTypes.DATEONLY.types.postgres = ['date'];
class DECIMAL extends BaseTypes.DECIMAL {
static parse(value) {
return value;
}
}
// numeric
BaseTypes.DECIMAL.types.postgres = ['numeric'];
class STRING extends BaseTypes.STRING {
toSql() {
if (this._binary) {
return 'BYTEA';
}
return super.toSql();
}
}
BaseTypes.STRING.types.postgres = ['varchar'];
class TEXT extends BaseTypes.TEXT {
toSql() {
if (this._length) {
warn('PostgreSQL does not support TEXT with options. Plain `TEXT` will be used instead.');
this._length = undefined;
}
return 'TEXT';
}
}
BaseTypes.TEXT.types.postgres = ['text'];
class CITEXT extends BaseTypes.CITEXT {
static parse(value) {
return value;
}
}
BaseTypes.CITEXT.types.postgres = ['citext'];
class CHAR extends BaseTypes.CHAR {
toSql() {
if (this._binary) {
return 'BYTEA';
}
return super.toSql();
}
}
BaseTypes.CHAR.types.postgres = ['char', 'bpchar'];
class BOOLEAN extends BaseTypes.BOOLEAN {
toSql() {
return 'BOOLEAN';
}
_sanitize(value) {
if (value !== null && value !== undefined) {
if (Buffer.isBuffer(value) && value.length === 1) {
// Bit fields are returned as buffers
value = value[0];
}
if (typeof value === 'string') {
// Only take action on valid boolean strings.
return value === 'true' || value === 't' ? true : value === 'false' || value === 'f' ? false : value;
}
if (typeof value === 'number') {
// Only take action on valid boolean integers.
return value === 1 ? true : value === 0 ? false : value;
}
}
return value;
}
}
BOOLEAN.parse = BOOLEAN.prototype._sanitize;
BaseTypes.BOOLEAN.types.postgres = ['bool'];
class DATE extends BaseTypes.DATE {
toSql() {
return 'TIMESTAMP WITH TIME ZONE';
}
validate(value) {
if (value !== Infinity && value !== -Infinity) {
return super.validate(value);
}
return true;
}
_stringify(value, options) {
if (value === Infinity) {
return 'Infinity';
}
if (value === -Infinity) {
return '-Infinity';
}
return super._stringify(value, options);
}
_sanitize(value, options) {
if ((!options || options && !options.raw) && !(value instanceof Date) && !!value && value !== Infinity && value !== -Infinity) {
if (typeof value === 'string') {
const lower = value.toLowerCase();
if (lower === 'infinity') {
return Infinity;
}
if (lower === '-infinity') {
return -Infinity;
}
}
return new Date(value);
}
return value;
}
}
BaseTypes.DATE.types.postgres = ['timestamptz'];
class TINYINT extends BaseTypes.TINYINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
// int2
BaseTypes.TINYINT.types.postgres = ['int2'];
class SMALLINT extends BaseTypes.SMALLINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
// int2
BaseTypes.SMALLINT.types.postgres = ['int2'];
class INTEGER extends BaseTypes.INTEGER {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
INTEGER.parse = function parse(value) {
return parseInt(value, 10);
};
// int4
BaseTypes.INTEGER.types.postgres = ['int4'];
class BIGINT extends BaseTypes.BIGINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
// int8
BaseTypes.BIGINT.types.postgres = ['int8'];
class REAL extends BaseTypes.REAL {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
// float4
BaseTypes.REAL.types.postgres = ['float4'];
class DOUBLE extends BaseTypes.DOUBLE {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
// float8
BaseTypes.DOUBLE.types.postgres = ['float8'];
class FLOAT extends BaseTypes.FLOAT {
constructor(length, decimals) {
super(length, decimals);
// POSTGRES does only support lengths as parameter.
// Values between 1-24 result in REAL
// Values between 25-53 result in DOUBLE PRECISION
// If decimals are provided remove these and print a warning
if (this._decimals) {
warn('PostgreSQL does not support FLOAT with decimals. Plain `FLOAT` will be used instead.');
this._length = undefined;
this.options.length = undefined;
this._decimals = undefined;
}
if (this._unsigned) {
warn('PostgreSQL does not support FLOAT unsigned. `UNSIGNED` was removed.');
this._unsigned = undefined;
}
if (this._zerofill) {
warn('PostgreSQL does not support FLOAT zerofill. `ZEROFILL` was removed.');
this._zerofill = undefined;
}
}
}
delete FLOAT.parse; // Float has no separate type in PG
class BLOB extends BaseTypes.BLOB {
toSql() {
if (this._length) {
warn('PostgreSQL does not support BLOB (BYTEA) with options. Plain `BYTEA` will be used instead.');
this._length = undefined;
}
return 'BYTEA';
}
_hexify(hex) {
// bytea hex format http://www.postgresql.org/docs/current/static/datatype-binary.html
return `E'\\\\x${hex}'`;
}
}
BaseTypes.BLOB.types.postgres = ['bytea'];
class GEOMETRY extends BaseTypes.GEOMETRY {
toSql() {
let result = this.key;
if (this.type) {
result += `(${this.type}`;
if (this.srid) {
result += `,${this.srid}`;
}
result += ')';
}
return result;
}
static parse(value) {
const b = Buffer.from(value, 'hex');
return wkx.Geometry.parse(b).toGeoJSON();
}
_stringify(value, options) {
return `ST_GeomFromGeoJSON(${options.escape(JSON.stringify(value))})`;
}
_bindParam(value, options) {
return `ST_GeomFromGeoJSON(${options.bindParam(value)})`;
}
}
BaseTypes.GEOMETRY.types.postgres = ['geometry'];
class GEOGRAPHY extends BaseTypes.GEOGRAPHY {
toSql() {
let result = 'GEOGRAPHY';
if (this.type) {
result += `(${this.type}`;
if (this.srid) {
result += `,${this.srid}`;
}
result += ')';
}
return result;
}
static parse(value) {
const b = Buffer.from(value, 'hex');
return wkx.Geometry.parse(b).toGeoJSON();
}
_stringify(value, options) {
return `ST_GeomFromGeoJSON(${options.escape(JSON.stringify(value))})`;
}
bindParam(value, options) {
return `ST_GeomFromGeoJSON(${options.bindParam(value)})`;
}
}
BaseTypes.GEOGRAPHY.types.postgres = ['geography'];
let hstore;
class HSTORE extends BaseTypes.HSTORE {
constructor() {
super();
if (!hstore) {
// All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
hstore = require('./hstore');
}
}
_value(value) {
if (!hstore) {
// All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
hstore = require('./hstore');
}
return hstore.stringify(value);
}
_stringify(value) {
return `'${this._value(value)}'`;
}
_bindParam(value, options) {
return options.bindParam(this._value(value));
}
static parse(value) {
if (!hstore) {
// All datatype files are loaded at import - make sure we don't load the hstore parser before a hstore is instantiated
hstore = require('./hstore');
}
return hstore.parse(value);
}
}
HSTORE.prototype.escape = false;
BaseTypes.HSTORE.types.postgres = ['hstore'];
class RANGE extends BaseTypes.RANGE {
_value(values, options) {
if (!Array.isArray(values)) {
return this.options.subtype.stringify(values, options);
}
const valueInclusivity = [true, false];
const valuesStringified = values.map((value, index) => {
if (_.isObject(value) && Object.prototype.hasOwnProperty.call(value, 'value')) {
if (Object.prototype.hasOwnProperty.call(value, 'inclusive')) {
valueInclusivity[index] = value.inclusive;
}
value = value.value;
}
if (value === null || value === -Infinity || value === Infinity) {
// Pass through "unbounded" bounds unchanged
return value;
}
if (this.options.subtype.stringify) {
return this.options.subtype.stringify(value, options);
}
return options.escape(value);
});
// Array.map does not preserve extra array properties
valuesStringified.inclusive = valueInclusivity;
return range.stringify(valuesStringified);
}
_stringify(values, options) {
const value = this._value(values, options);
if (!Array.isArray(values)) {
return `'${value}'::${this.toCastType()}`;
}
return `'${value}'`;
}
_bindParam(values, options) {
const value = this._value(values, options);
if (!Array.isArray(values)) {
return `${options.bindParam(value)}::${this.toCastType()}`;
}
return options.bindParam(value);
}
toSql() {
return BaseTypes.RANGE.types.postgres.subtypes[this._subtype.toLowerCase()];
}
toCastType() {
return BaseTypes.RANGE.types.postgres.castTypes[this._subtype.toLowerCase()];
}
static parse(value, options = { parser: val => val }) {
return range.parse(value, options.parser);
}
}
const range = require('./range');
RANGE.prototype.escape = false;
BaseTypes.RANGE.types.postgres = {
subtypes: {
integer: 'int4range',
decimal: 'numrange',
date: 'tstzrange',
dateonly: 'daterange',
bigint: 'int8range'
},
castTypes: {
integer: 'int4',
decimal: 'numeric',
date: 'timestamptz',
dateonly: 'date',
bigint: 'int8'
}
};
// TODO: Why are base types being manipulated??
BaseTypes.ARRAY.prototype.escape = false;
BaseTypes.ARRAY.prototype._value = function _value(values, options) {
return values.map(value => {
if (options && options.bindParam && this.type && this.type._value) {
return this.type._value(value, options);
}
if (this.type && this.type.stringify) {
value = this.type.stringify(value, options);
if (this.type.escape === false) {
return value;
}
}
return options.escape(value);
}, this);
};
BaseTypes.ARRAY.prototype._stringify = function _stringify(values, options) {
let str = `ARRAY[${this._value(values, options).join(',')}]`;
if (this.type) {
const Utils = require('../../utils');
let castKey = this.toSql();
if (this.type instanceof BaseTypes.ENUM) {
castKey = `${Utils.addTicks(
Utils.generateEnumName(options.field.Model.getTableName(), options.field.fieldName),
'"'
) }[]`;
}
str += `::${castKey}`;
}
return str;
};
BaseTypes.ARRAY.prototype._bindParam = function _bindParam(values, options) {
return options.bindParam(this._value(values, options));
};
class ENUM extends BaseTypes.ENUM {
static parse(value) {
return value;
}
}
BaseTypes.ENUM.types.postgres = [null];
return {
DECIMAL,
BLOB,
STRING,
CHAR,
TEXT,
CITEXT,
TINYINT,
SMALLINT,
INTEGER,
BIGINT,
BOOLEAN,
DATE,
DATEONLY,
REAL,
'DOUBLE PRECISION': DOUBLE,
FLOAT,
GEOMETRY,
GEOGRAPHY,
HSTORE,
RANGE,
ENUM
};
};
+15
View File
@@ -0,0 +1,15 @@
'use strict';
const hstore = require('pg-hstore')({ sanitize: true });
function stringify(data) {
if (data === null) return null;
return hstore.stringify(data);
}
exports.stringify = stringify;
function parse(value) {
if (value === null) return null;
return hstore.parse(value);
}
exports.parse = parse;
+71
View File
@@ -0,0 +1,71 @@
'use strict';
const _ = require('lodash');
const AbstractDialect = require('../abstract');
const ConnectionManager = require('./connection-manager');
const Query = require('./query');
const QueryGenerator = require('./query-generator');
const DataTypes = require('../../data-types').postgres;
class PostgresDialect extends AbstractDialect {
constructor(sequelize) {
super();
this.sequelize = sequelize;
this.connectionManager = new ConnectionManager(this, sequelize);
this.QueryGenerator = new QueryGenerator({
_dialect: this,
sequelize
});
}
}
PostgresDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
'DEFAULT VALUES': true,
'EXCEPTION': true,
'ON DUPLICATE KEY': false,
'ORDER NULLS': true,
returnValues: {
returning: true
},
bulkDefault: true,
schemas: true,
lock: true,
lockOf: true,
lockKey: true,
lockOuterJoinFailure: true,
skipLocked: true,
forShare: 'FOR SHARE',
index: {
concurrently: true,
using: 2,
where: true,
functionBased: true
},
inserts: {
onConflictDoNothing: ' ON CONFLICT DO NOTHING',
updateOnDuplicate: ' ON CONFLICT DO UPDATE SET'
},
NUMERIC: true,
ARRAY: true,
RANGE: true,
GEOMETRY: true,
REGEXP: true,
GEOGRAPHY: true,
JSON: true,
JSONB: true,
HSTORE: true,
deferrableConstraints: true,
searchPath: true
});
ConnectionManager.prototype.defaultVersion = '9.4.0';
PostgresDialect.prototype.Query = Query;
PostgresDialect.prototype.DataTypes = DataTypes;
PostgresDialect.prototype.name = 'postgres';
PostgresDialect.prototype.TICK_CHAR = '"';
PostgresDialect.prototype.TICK_CHAR_LEFT = PostgresDialect.prototype.TICK_CHAR;
PostgresDialect.prototype.TICK_CHAR_RIGHT = PostgresDialect.prototype.TICK_CHAR;
module.exports = PostgresDialect;
module.exports.default = PostgresDialect;
module.exports.PostgresDialect = PostgresDialect;
+945
View File
@@ -0,0 +1,945 @@
'use strict';
const Utils = require('../../utils');
const util = require('util');
const DataTypes = require('../../data-types');
const AbstractQueryGenerator = require('../abstract/query-generator');
const semver = require('semver');
const _ = require('lodash');
class PostgresQueryGenerator extends AbstractQueryGenerator {
setSearchPath(searchPath) {
return `SET search_path to ${searchPath};`;
}
createDatabaseQuery(databaseName, options) {
options = Object.assign({
encoding: null,
collate: null
}, options || {});
const values = {
database: this.quoteTable(databaseName),
encoding: options.encoding ? ` ENCODING = ${this.escape(options.encoding)}` : '',
collation: options.collate ? ` LC_COLLATE = ${this.escape(options.collate)}` : '',
ctype: options.ctype ? ` LC_CTYPE = ${this.escape(options.ctype)}` : '',
template: options.template ? ` TEMPLATE = ${this.escape(options.template)}` : ''
};
return `CREATE DATABASE ${values.database}${values.encoding}${values.collation}${values.ctype}${values.template};`;
}
dropDatabaseQuery(databaseName) {
return `DROP DATABASE IF EXISTS ${this.quoteTable(databaseName)};`;
}
createSchema(schema) {
const databaseVersion = _.get(this, 'sequelize.options.databaseVersion', 0);
if (databaseVersion && semver.gte(databaseVersion, '9.2.0')) {
return `CREATE SCHEMA IF NOT EXISTS ${schema};`;
}
return `CREATE SCHEMA ${schema};`;
}
dropSchema(schema) {
return `DROP SCHEMA IF EXISTS ${schema} CASCADE;`;
}
showSchemasQuery() {
return "SELECT schema_name FROM information_schema.schemata WHERE schema_name <> 'information_schema' AND schema_name != 'public' AND schema_name !~ E'^pg_';";
}
versionQuery() {
return 'SHOW SERVER_VERSION';
}
createTableQuery(tableName, attributes, options) {
options = Object.assign({}, options || {});
//Postgres 9.0 does not support CREATE TABLE IF NOT EXISTS, 9.1 and above do
const databaseVersion = _.get(this, 'sequelize.options.databaseVersion', 0);
const attrStr = [];
let comments = '';
let columnComments = '';
const quotedTable = this.quoteTable(tableName);
if (options.comment && typeof options.comment === 'string') {
comments += `; COMMENT ON TABLE ${quotedTable} IS ${this.escape(options.comment)}`;
}
for (const attr in attributes) {
const quotedAttr = this.quoteIdentifier(attr);
const i = attributes[attr].indexOf('COMMENT ');
if (i !== -1) {
// Move comment to a separate query
const escapedCommentText = this.escape(attributes[attr].substring(i + 8));
columnComments += `; COMMENT ON COLUMN ${quotedTable}.${quotedAttr} IS ${escapedCommentText}`;
attributes[attr] = attributes[attr].substring(0, i);
}
const dataType = this.dataTypeMapping(tableName, attr, attributes[attr]);
attrStr.push(`${quotedAttr} ${dataType}`);
}
let attributesClause = attrStr.join(', ');
if (options.uniqueKeys) {
_.each(options.uniqueKeys, columns => {
if (columns.customIndex) {
attributesClause += `, UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
}
});
}
const pks = _.reduce(attributes, (acc, attribute, key) => {
if (attribute.includes('PRIMARY KEY')) {
acc.push(this.quoteIdentifier(key));
}
return acc;
}, []).join(',');
if (pks.length > 0) {
attributesClause += `, PRIMARY KEY (${pks})`;
}
return `CREATE TABLE ${databaseVersion === 0 || semver.gte(databaseVersion, '9.1.0') ? 'IF NOT EXISTS ' : ''}${quotedTable} (${attributesClause})${comments}${columnComments};`;
}
dropTableQuery(tableName, options) {
options = options || {};
return `DROP TABLE IF EXISTS ${this.quoteTable(tableName)}${options.cascade ? ' CASCADE' : ''};`;
}
showTablesQuery() {
return "SELECT table_name FROM information_schema.tables WHERE table_schema = 'public' AND table_type LIKE '%TABLE' AND table_name != 'spatial_ref_sys';";
}
describeTableQuery(tableName, schema) {
if (!schema) schema = 'public';
return 'SELECT ' +
'pk.constraint_type as "Constraint",' +
'c.column_name as "Field", ' +
'c.column_default as "Default",' +
'c.is_nullable as "Null", ' +
'(CASE WHEN c.udt_name = \'hstore\' THEN c.udt_name ELSE c.data_type END) || (CASE WHEN c.character_maximum_length IS NOT NULL THEN \'(\' || c.character_maximum_length || \')\' ELSE \'\' END) as "Type", ' +
'(SELECT array_agg(e.enumlabel) FROM pg_catalog.pg_type t JOIN pg_catalog.pg_enum e ON t.oid=e.enumtypid WHERE t.typname=c.udt_name) AS "special", ' +
'(SELECT pgd.description FROM pg_catalog.pg_statio_all_tables AS st INNER JOIN pg_catalog.pg_description pgd on (pgd.objoid=st.relid) WHERE c.ordinal_position=pgd.objsubid AND c.table_name=st.relname) AS "Comment" ' +
'FROM information_schema.columns c ' +
'LEFT JOIN (SELECT tc.table_schema, tc.table_name, ' +
'cu.column_name, tc.constraint_type ' +
'FROM information_schema.TABLE_CONSTRAINTS tc ' +
'JOIN information_schema.KEY_COLUMN_USAGE cu ' +
'ON tc.table_schema=cu.table_schema and tc.table_name=cu.table_name ' +
'and tc.constraint_name=cu.constraint_name ' +
'and tc.constraint_type=\'PRIMARY KEY\') pk ' +
'ON pk.table_schema=c.table_schema ' +
'AND pk.table_name=c.table_name ' +
'AND pk.column_name=c.column_name ' +
`WHERE c.table_name = ${this.escape(tableName)} AND c.table_schema = ${this.escape(schema)} `;
}
/**
* Check whether the statmement is json function or simple path
*
* @param {string} stmt The statement to validate
* @returns {boolean} true if the given statement is json function
* @throws {Error} throw if the statement looks like json function but has invalid token
*/
_checkValidJsonStatement(stmt) {
if (typeof stmt !== 'string') {
return false;
}
// https://www.postgresql.org/docs/current/static/functions-json.html
const jsonFunctionRegex = /^\s*((?:[a-z]+_){0,2}jsonb?(?:_[a-z]+){0,2})\([^)]*\)/i;
const jsonOperatorRegex = /^\s*(->>?|#>>?|@>|<@|\?[|&]?|\|{2}|#-)/i;
const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
let currentIndex = 0;
let openingBrackets = 0;
let closingBrackets = 0;
let hasJsonFunction = false;
let hasInvalidToken = false;
while (currentIndex < stmt.length) {
const string = stmt.substr(currentIndex);
const functionMatches = jsonFunctionRegex.exec(string);
if (functionMatches) {
currentIndex += functionMatches[0].indexOf('(');
hasJsonFunction = true;
continue;
}
const operatorMatches = jsonOperatorRegex.exec(string);
if (operatorMatches) {
currentIndex += operatorMatches[0].length;
hasJsonFunction = true;
continue;
}
const tokenMatches = tokenCaptureRegex.exec(string);
if (tokenMatches) {
const capturedToken = tokenMatches[1];
if (capturedToken === '(') {
openingBrackets++;
} else if (capturedToken === ')') {
closingBrackets++;
} else if (capturedToken === ';') {
hasInvalidToken = true;
break;
}
currentIndex += tokenMatches[0].length;
continue;
}
break;
}
// Check invalid json statement
hasInvalidToken |= openingBrackets !== closingBrackets;
if (hasJsonFunction && hasInvalidToken) {
throw new Error(`Invalid json statement: ${stmt}`);
}
// return true if the statement has valid json function
return hasJsonFunction;
}
handleSequelizeMethod(smth, tableName, factory, options, prepend) {
if (smth instanceof Utils.Json) {
// Parse nested object
if (smth.conditions) {
const conditions = this.parseConditionObject(smth.conditions).map(condition =>
`${this.jsonPathExtractionQuery(condition.path[0], _.tail(condition.path))} = '${condition.value}'`
);
return conditions.join(' AND ');
}
if (smth.path) {
let str;
// Allow specifying conditions using the postgres json syntax
if (this._checkValidJsonStatement(smth.path)) {
str = smth.path;
} else {
// Also support json property accessors
const paths = _.toPath(smth.path);
const column = paths.shift();
str = this.jsonPathExtractionQuery(column, paths);
}
if (smth.value) {
str += util.format(' = %s', this.escape(smth.value));
}
return str;
}
}
return super.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
}
addColumnQuery(table, key, dataType) {
const dbDataType = this.attributeToSQL(dataType, { context: 'addColumn', table, key });
const definition = this.dataTypeMapping(table, key, dbDataType);
const quotedKey = this.quoteIdentifier(key);
const quotedTable = this.quoteTable(this.extractTableDetails(table));
let query = `ALTER TABLE ${quotedTable} ADD COLUMN ${quotedKey} ${definition};`;
if (dataType.type && dataType.type instanceof DataTypes.ENUM || dataType instanceof DataTypes.ENUM) {
query = this.pgEnum(table, key, dataType) + query;
}
return query;
}
removeColumnQuery(tableName, attributeName) {
const quotedTableName = this.quoteTable(this.extractTableDetails(tableName));
const quotedAttributeName = this.quoteIdentifier(attributeName);
return `ALTER TABLE ${quotedTableName} DROP COLUMN ${quotedAttributeName};`;
}
changeColumnQuery(tableName, attributes) {
const query = subQuery => `ALTER TABLE ${this.quoteTable(tableName)} ALTER COLUMN ${subQuery};`;
const sql = [];
for (const attributeName in attributes) {
let definition = this.dataTypeMapping(tableName, attributeName, attributes[attributeName]);
let attrSql = '';
if (definition.includes('NOT NULL')) {
attrSql += query(`${this.quoteIdentifier(attributeName)} SET NOT NULL`);
definition = definition.replace('NOT NULL', '').trim();
} else if (!definition.includes('REFERENCES')) {
attrSql += query(`${this.quoteIdentifier(attributeName)} DROP NOT NULL`);
}
if (definition.includes('DEFAULT')) {
attrSql += query(`${this.quoteIdentifier(attributeName)} SET DEFAULT ${definition.match(/DEFAULT ([^;]+)/)[1]}`);
definition = definition.replace(/(DEFAULT[^;]+)/, '').trim();
} else if (!definition.includes('REFERENCES')) {
attrSql += query(`${this.quoteIdentifier(attributeName)} DROP DEFAULT`);
}
if (attributes[attributeName].startsWith('ENUM(')) {
attrSql += this.pgEnum(tableName, attributeName, attributes[attributeName]);
definition = definition.replace(/^ENUM\(.+\)/, this.pgEnumName(tableName, attributeName, { schema: false }));
definition += ` USING (${this.quoteIdentifier(attributeName)}::${this.pgEnumName(tableName, attributeName)})`;
}
if (definition.match(/UNIQUE;*$/)) {
definition = definition.replace(/UNIQUE;*$/, '');
attrSql += query(`ADD UNIQUE (${this.quoteIdentifier(attributeName)})`).replace('ALTER COLUMN', '');
}
if (definition.includes('REFERENCES')) {
definition = definition.replace(/.+?(?=REFERENCES)/, '');
attrSql += query(`ADD FOREIGN KEY (${this.quoteIdentifier(attributeName)}) ${definition}`).replace('ALTER COLUMN', '');
} else {
attrSql += query(`${this.quoteIdentifier(attributeName)} TYPE ${definition}`);
}
sql.push(attrSql);
}
return sql.join('');
}
renameColumnQuery(tableName, attrBefore, attributes) {
const attrString = [];
for (const attributeName in attributes) {
attrString.push(`${this.quoteIdentifier(attrBefore)} TO ${this.quoteIdentifier(attributeName)}`);
}
return `ALTER TABLE ${this.quoteTable(tableName)} RENAME COLUMN ${attrString.join(', ')};`;
}
fn(fnName, tableName, parameters, body, returns, language) {
fnName = fnName || 'testfunc';
language = language || 'plpgsql';
returns = returns ? `RETURNS ${returns}` : '';
parameters = parameters || '';
return `CREATE OR REPLACE FUNCTION pg_temp.${fnName}(${parameters}) ${returns} AS $func$ BEGIN ${body} END; $func$ LANGUAGE ${language}; SELECT * FROM pg_temp.${fnName}();`;
}
exceptionFn(fnName, tableName, parameters, main, then, when, returns, language) {
when = when || 'unique_violation';
const body = `${main} EXCEPTION WHEN ${when} THEN ${then};`;
return this.fn(fnName, tableName, parameters, body, returns, language);
}
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
const primaryField = this.quoteIdentifier(model.primaryKeyField);
const upsertOptions = _.defaults({ bindParam: false }, options);
const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, upsertOptions);
const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes);
insert.query = insert.query.replace('RETURNING *', `RETURNING ${primaryField} INTO primary_key`);
update.query = update.query.replace('RETURNING *', `RETURNING ${primaryField} INTO primary_key`);
return this.exceptionFn(
'sequelize_upsert',
tableName,
'OUT created boolean, OUT primary_key text',
`${insert.query} created := true;`,
`${update.query}; created := false`
);
}
truncateTableQuery(tableName, options = {}) {
return [
`TRUNCATE ${this.quoteTable(tableName)}`,
options.restartIdentity ? ' RESTART IDENTITY' : '',
options.cascade ? ' CASCADE' : ''
].join('');
}
deleteQuery(tableName, where, options = {}, model) {
const table = this.quoteTable(tableName);
let whereClause = this.getWhereConditions(where, null, model, options);
const limit = options.limit ? ` LIMIT ${this.escape(options.limit)}` : '';
let primaryKeys = '';
let primaryKeysSelection = '';
if (whereClause) {
whereClause = ` WHERE ${whereClause}`;
}
if (options.limit) {
if (!model) {
throw new Error('Cannot LIMIT delete without a model.');
}
const pks = _.values(model.primaryKeys).map(pk => this.quoteIdentifier(pk.field)).join(',');
primaryKeys = model.primaryKeyAttributes.length > 1 ? `(${pks})` : pks;
primaryKeysSelection = pks;
return `DELETE FROM ${table} WHERE ${primaryKeys} IN (SELECT ${primaryKeysSelection} FROM ${table}${whereClause}${limit})`;
}
return `DELETE FROM ${table}${whereClause}`;
}
showIndexesQuery(tableName) {
let schemaJoin = '';
let schemaWhere = '';
if (typeof tableName !== 'string') {
schemaJoin = ', pg_namespace s';
schemaWhere = ` AND s.oid = t.relnamespace AND s.nspname = '${tableName.schema}'`;
tableName = tableName.tableName;
}
// This is ARCANE!
return 'SELECT i.relname AS name, ix.indisprimary AS primary, ix.indisunique AS unique, ix.indkey AS indkey, ' +
'array_agg(a.attnum) as column_indexes, array_agg(a.attname) AS column_names, pg_get_indexdef(ix.indexrelid) ' +
`AS definition FROM pg_class t, pg_class i, pg_index ix, pg_attribute a${schemaJoin} ` +
'WHERE t.oid = ix.indrelid AND i.oid = ix.indexrelid AND a.attrelid = t.oid AND ' +
`t.relkind = 'r' and t.relname = '${tableName}'${schemaWhere} ` +
'GROUP BY i.relname, ix.indexrelid, ix.indisprimary, ix.indisunique, ix.indkey ORDER BY i.relname;';
}
showConstraintsQuery(tableName) {
//Postgres converts camelCased alias to lowercase unless quoted
return [
'SELECT constraint_catalog AS "constraintCatalog",',
'constraint_schema AS "constraintSchema",',
'constraint_name AS "constraintName",',
'table_catalog AS "tableCatalog",',
'table_schema AS "tableSchema",',
'table_name AS "tableName",',
'constraint_type AS "constraintType",',
'is_deferrable AS "isDeferrable",',
'initially_deferred AS "initiallyDeferred"',
'from INFORMATION_SCHEMA.table_constraints',
`WHERE table_name='${tableName}';`
].join(' ');
}
removeIndexQuery(tableName, indexNameOrAttributes) {
let indexName = indexNameOrAttributes;
if (typeof indexName !== 'string') {
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
}
return `DROP INDEX IF EXISTS ${this.quoteIdentifiers(indexName)}`;
}
addLimitAndOffset(options) {
let fragment = '';
/* eslint-disable */
if (options.limit != null) {
fragment += ' LIMIT ' + this.escape(options.limit);
}
if (options.offset != null) {
fragment += ' OFFSET ' + this.escape(options.offset);
}
/* eslint-enable */
return fragment;
}
attributeToSQL(attribute, options) {
if (!_.isPlainObject(attribute)) {
attribute = {
type: attribute
};
}
let type;
if (
attribute.type instanceof DataTypes.ENUM ||
attribute.type instanceof DataTypes.ARRAY && attribute.type.type instanceof DataTypes.ENUM
) {
const enumType = attribute.type.type || attribute.type;
let values = attribute.values;
if (enumType.values && !attribute.values) {
values = enumType.values;
}
if (Array.isArray(values) && values.length > 0) {
type = `ENUM(${values.map(value => this.escape(value)).join(', ')})`;
if (attribute.type instanceof DataTypes.ARRAY) {
type += '[]';
}
} else {
throw new Error("Values for ENUM haven't been defined.");
}
}
if (!type) {
type = attribute.type;
}
let sql = type.toString();
if (Object.prototype.hasOwnProperty.call(attribute, 'allowNull') && !attribute.allowNull) {
sql += ' NOT NULL';
}
if (attribute.autoIncrement) {
if (attribute.autoIncrementIdentity) {
sql += ' GENERATED BY DEFAULT AS IDENTITY';
} else {
sql += ' SERIAL';
}
}
if (Utils.defaultValueSchemable(attribute.defaultValue)) {
sql += ` DEFAULT ${this.escape(attribute.defaultValue, attribute)}`;
}
if (attribute.unique === true) {
sql += ' UNIQUE';
}
if (attribute.primaryKey) {
sql += ' PRIMARY KEY';
}
if (attribute.references) {
let referencesTable = this.quoteTable(attribute.references.model);
let schema;
if (options.schema) {
schema = options.schema;
} else if (
(!attribute.references.model || typeof attribute.references.model == 'string')
&& options.table
&& options.table.schema
) {
schema = options.table.schema;
}
if (schema) {
referencesTable = this.quoteTable(this.addSchema({
tableName: referencesTable,
_schema: schema
}));
}
let referencesKey;
if (attribute.references.key) {
referencesKey = this.quoteIdentifiers(attribute.references.key);
} else {
referencesKey = this.quoteIdentifier('id');
}
sql += ` REFERENCES ${referencesTable} (${referencesKey})`;
if (attribute.onDelete) {
sql += ` ON DELETE ${attribute.onDelete.toUpperCase()}`;
}
if (attribute.onUpdate) {
sql += ` ON UPDATE ${attribute.onUpdate.toUpperCase()}`;
}
if (attribute.references.deferrable) {
sql += ` ${attribute.references.deferrable.toString(this)}`;
}
}
if (attribute.comment && typeof attribute.comment === 'string') {
if (options && (options.context === 'addColumn' || options.context === 'changeColumn')) {
const quotedAttr = this.quoteIdentifier(options.key);
const escapedCommentText = this.escape(attribute.comment);
sql += `; COMMENT ON COLUMN ${this.quoteTable(options.table)}.${quotedAttr} IS ${escapedCommentText}`;
} else {
// for createTable event which does it's own parsing
// TODO: centralize creation of comment statements here
sql += ` COMMENT ${attribute.comment}`;
}
}
return sql;
}
deferConstraintsQuery(options) {
return options.deferrable.toString(this);
}
setConstraintQuery(columns, type) {
let columnFragment = 'ALL';
if (columns) {
columnFragment = columns.map(column => this.quoteIdentifier(column)).join(', ');
}
return `SET CONSTRAINTS ${columnFragment} ${type}`;
}
setDeferredQuery(columns) {
return this.setConstraintQuery(columns, 'DEFERRED');
}
setImmediateQuery(columns) {
return this.setConstraintQuery(columns, 'IMMEDIATE');
}
attributesToSQL(attributes, options) {
const result = {};
for (const key in attributes) {
const attribute = attributes[key];
result[attribute.field || key] = this.attributeToSQL(attribute, Object.assign({ key }, options || {}));
}
return result;
}
createTrigger(tableName, triggerName, eventType, fireOnSpec, functionName, functionParams, optionsArray) {
const decodedEventType = this.decodeTriggerEventType(eventType);
const eventSpec = this.expandTriggerEventSpec(fireOnSpec);
const expandedOptions = this.expandOptions(optionsArray);
const paramList = this.expandFunctionParamList(functionParams);
return `CREATE ${this.triggerEventTypeIsConstraint(eventType)}TRIGGER ${this.quoteIdentifier(triggerName)} ${decodedEventType} ${
eventSpec} ON ${this.quoteTable(tableName)}${expandedOptions ? ` ${expandedOptions}` : ''} EXECUTE PROCEDURE ${functionName}(${paramList});`;
}
dropTrigger(tableName, triggerName) {
return `DROP TRIGGER ${this.quoteIdentifier(triggerName)} ON ${this.quoteTable(tableName)} RESTRICT;`;
}
renameTrigger(tableName, oldTriggerName, newTriggerName) {
return `ALTER TRIGGER ${this.quoteIdentifier(oldTriggerName)} ON ${this.quoteTable(tableName)} RENAME TO ${this.quoteIdentifier(newTriggerName)};`;
}
createFunction(functionName, params, returnType, language, body, optionsArray, options) {
if (!functionName || !returnType || !language || !body) throw new Error('createFunction missing some parameters. Did you pass functionName, returnType, language and body?');
const paramList = this.expandFunctionParamList(params);
const variableList = options && options.variables ? this.expandFunctionVariableList(options.variables) : '';
const expandedOptionsArray = this.expandOptions(optionsArray);
const statement = options && options.force ? 'CREATE OR REPLACE FUNCTION' : 'CREATE FUNCTION';
return `${statement} ${functionName}(${paramList}) RETURNS ${returnType} AS $func$ ${variableList} BEGIN ${body} END; $func$ language '${language}'${expandedOptionsArray};`;
}
dropFunction(functionName, params) {
if (!functionName) throw new Error('requires functionName');
// RESTRICT is (currently, as of 9.2) default but we'll be explicit
const paramList = this.expandFunctionParamList(params);
return `DROP FUNCTION ${functionName}(${paramList}) RESTRICT;`;
}
renameFunction(oldFunctionName, params, newFunctionName) {
const paramList = this.expandFunctionParamList(params);
return `ALTER FUNCTION ${oldFunctionName}(${paramList}) RENAME TO ${newFunctionName};`;
}
databaseConnectionUri(config) {
let uri = `${config.protocol}://${config.user}:${config.password}@${config.host}`;
if (config.port) {
uri += `:${config.port}`;
}
uri += `/${config.database}`;
if (config.ssl) {
uri += `?ssl=${config.ssl}`;
}
return uri;
}
pgEscapeAndQuote(val) {
return this.quoteIdentifier(Utils.removeTicks(this.escape(val), "'"));
}
expandFunctionParamList(params) {
if (params === undefined || !Array.isArray(params)) {
throw new Error('expandFunctionParamList: function parameters array required, including an empty one for no arguments');
}
const paramList = [];
params.forEach(curParam => {
const paramDef = [];
if (curParam.type) {
if (curParam.direction) { paramDef.push(curParam.direction); }
if (curParam.name) { paramDef.push(curParam.name); }
paramDef.push(curParam.type);
} else {
throw new Error('function or trigger used with a parameter without any type');
}
const joined = paramDef.join(' ');
if (joined) paramList.push(joined);
});
return paramList.join(', ');
}
expandFunctionVariableList(variables) {
if (!Array.isArray(variables)) {
throw new Error('expandFunctionVariableList: function variables must be an array');
}
const variableDefinitions = [];
variables.forEach(variable => {
if (!variable.name || !variable.type) {
throw new Error('function variable must have a name and type');
}
let variableDefinition = `DECLARE ${variable.name} ${variable.type}`;
if (variable.default) {
variableDefinition += ` := ${variable.default}`;
}
variableDefinition += ';';
variableDefinitions.push(variableDefinition);
});
return variableDefinitions.join(' ');
}
expandOptions(options) {
return options === undefined || _.isEmpty(options) ?
'' : options.join(' ');
}
decodeTriggerEventType(eventSpecifier) {
const EVENT_DECODER = {
'after': 'AFTER',
'before': 'BEFORE',
'instead_of': 'INSTEAD OF',
'after_constraint': 'AFTER'
};
if (!EVENT_DECODER[eventSpecifier]) {
throw new Error(`Invalid trigger event specified: ${eventSpecifier}`);
}
return EVENT_DECODER[eventSpecifier];
}
triggerEventTypeIsConstraint(eventSpecifier) {
return eventSpecifier === 'after_constraint' ? 'CONSTRAINT ' : '';
}
expandTriggerEventSpec(fireOnSpec) {
if (_.isEmpty(fireOnSpec)) {
throw new Error('no table change events specified to trigger on');
}
return _.map(fireOnSpec, (fireValue, fireKey) => {
const EVENT_MAP = {
'insert': 'INSERT',
'update': 'UPDATE',
'delete': 'DELETE',
'truncate': 'TRUNCATE'
};
if (!EVENT_MAP[fireValue]) {
throw new Error(`parseTriggerEventSpec: undefined trigger event ${fireKey}`);
}
let eventSpec = EVENT_MAP[fireValue];
if (eventSpec === 'UPDATE') {
if (Array.isArray(fireValue) && fireValue.length > 0) {
eventSpec += ` OF ${fireValue.join(', ')}`;
}
}
return eventSpec;
}).join(' OR ');
}
pgEnumName(tableName, attr, options) {
options = options || {};
const tableDetails = this.extractTableDetails(tableName, options);
let enumName = Utils.addTicks(Utils.generateEnumName(tableDetails.tableName, attr), '"');
// pgListEnums requires the enum name only, without the schema
if (options.schema !== false && tableDetails.schema) {
enumName = this.quoteIdentifier(tableDetails.schema) + tableDetails.delimiter + enumName;
}
return enumName;
}
pgListEnums(tableName, attrName, options) {
let enumName = '';
const tableDetails = this.extractTableDetails(tableName, options);
if (tableDetails.tableName && attrName) {
enumName = ` AND t.typname=${this.pgEnumName(tableDetails.tableName, attrName, { schema: false }).replace(/"/g, "'")}`;
}
return 'SELECT t.typname enum_name, array_agg(e.enumlabel ORDER BY enumsortorder) enum_value FROM pg_type t ' +
'JOIN pg_enum e ON t.oid = e.enumtypid ' +
'JOIN pg_catalog.pg_namespace n ON n.oid = t.typnamespace ' +
`WHERE n.nspname = '${tableDetails.schema}'${enumName} GROUP BY 1`;
}
pgEnum(tableName, attr, dataType, options) {
const enumName = this.pgEnumName(tableName, attr, options);
let values;
if (dataType.values) {
values = `ENUM(${dataType.values.map(value => this.escape(value)).join(', ')})`;
} else {
values = dataType.toString().match(/^ENUM\(.+\)/)[0];
}
let sql = `CREATE TYPE ${enumName} AS ${values};`;
if (!!options && options.force === true) {
sql = this.pgEnumDrop(tableName, attr) + sql;
}
return sql;
}
pgEnumAdd(tableName, attr, value, options) {
const enumName = this.pgEnumName(tableName, attr);
let sql = `ALTER TYPE ${enumName} ADD VALUE `;
if (semver.gte(this.sequelize.options.databaseVersion, '9.3.0')) {
sql += 'IF NOT EXISTS ';
}
sql += this.escape(value);
if (options.before) {
sql += ` BEFORE ${this.escape(options.before)}`;
} else if (options.after) {
sql += ` AFTER ${this.escape(options.after)}`;
}
return sql;
}
pgEnumDrop(tableName, attr, enumName) {
enumName = enumName || this.pgEnumName(tableName, attr);
return `DROP TYPE IF EXISTS ${enumName}; `;
}
fromArray(text) {
text = text.replace(/^{/, '').replace(/}$/, '');
let matches = text.match(/("(?:\\.|[^"\\\\])*"|[^,]*)(?:\s*,\s*|\s*$)/ig);
if (matches.length < 1) {
return [];
}
matches = matches.map(m => m.replace(/",$/, '').replace(/,$/, '').replace(/(^"|"$)/, ''));
return matches.slice(0, -1);
}
padInt(i) {
return i < 10 ? `0${i.toString()}` : i.toString();
}
dataTypeMapping(tableName, attr, dataType) {
if (dataType.includes('PRIMARY KEY')) {
dataType = dataType.replace('PRIMARY KEY', '');
}
if (dataType.includes('SERIAL')) {
if (dataType.includes('BIGINT')) {
dataType = dataType.replace('SERIAL', 'BIGSERIAL');
dataType = dataType.replace('BIGINT', '');
} else if (dataType.includes('SMALLINT')) {
dataType = dataType.replace('SERIAL', 'SMALLSERIAL');
dataType = dataType.replace('SMALLINT', '');
} else {
dataType = dataType.replace('INTEGER', '');
}
dataType = dataType.replace('NOT NULL', '');
}
if (dataType.startsWith('ENUM(')) {
dataType = dataType.replace(/^ENUM\(.+\)/, this.pgEnumName(tableName, attr));
}
return dataType;
}
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {string} tableName The name of the table.
* @returns {string} The generated sql query.
* @private
*/
getForeignKeysQuery(tableName) {
return 'SELECT conname as constraint_name, pg_catalog.pg_get_constraintdef(r.oid, true) as condef FROM pg_catalog.pg_constraint r ' +
`WHERE r.conrelid = (SELECT oid FROM pg_class WHERE relname = '${tableName}' LIMIT 1) AND r.contype = 'f' ORDER BY 1;`;
}
/**
* Generate common SQL prefix for getForeignKeyReferencesQuery.
*
* @returns {string}
*/
_getForeignKeyReferencesQueryPrefix() {
return 'SELECT ' +
'DISTINCT tc.constraint_name as constraint_name, ' +
'tc.constraint_schema as constraint_schema, ' +
'tc.constraint_catalog as constraint_catalog, ' +
'tc.table_name as table_name,' +
'tc.table_schema as table_schema,' +
'tc.table_catalog as table_catalog,' +
'kcu.column_name as column_name,' +
'ccu.table_schema AS referenced_table_schema,' +
'ccu.table_catalog AS referenced_table_catalog,' +
'ccu.table_name AS referenced_table_name,' +
'ccu.column_name AS referenced_column_name ' +
'FROM information_schema.table_constraints AS tc ' +
'JOIN information_schema.key_column_usage AS kcu ' +
'ON tc.constraint_name = kcu.constraint_name ' +
'JOIN information_schema.constraint_column_usage AS ccu ' +
'ON ccu.constraint_name = tc.constraint_name ';
}
/**
* Generates an SQL query that returns all foreign keys details of a table.
*
* As for getForeignKeysQuery is not compatible with getForeignKeyReferencesQuery, so add a new function.
*
* @param {string} tableName
* @param {string} catalogName
* @param {string} schemaName
*/
getForeignKeyReferencesQuery(tableName, catalogName, schemaName) {
return `${this._getForeignKeyReferencesQueryPrefix()
}WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name = '${tableName}'${
catalogName ? ` AND tc.table_catalog = '${catalogName}'` : ''
}${schemaName ? ` AND tc.table_schema = '${schemaName}'` : ''}`;
}
getForeignKeyReferenceQuery(table, columnName) {
const tableName = table.tableName || table;
const schema = table.schema;
return `${this._getForeignKeyReferencesQueryPrefix()
}WHERE constraint_type = 'FOREIGN KEY' AND tc.table_name='${tableName}' AND kcu.column_name = '${columnName}'${
schema ? ` AND tc.table_schema = '${schema}'` : ''}`;
}
/**
* Generates an SQL query that removes a foreign key from a table.
*
* @param {string} tableName The name of the table.
* @param {string} foreignKey The name of the foreign key constraint.
* @returns {string} The generated sql query.
* @private
*/
dropForeignKeyQuery(tableName, foreignKey) {
return `ALTER TABLE ${this.quoteTable(tableName)} DROP CONSTRAINT ${this.quoteIdentifier(foreignKey)};`;
}
}
module.exports = PostgresQueryGenerator;
+157
View File
@@ -0,0 +1,157 @@
'use strict';
const DataTypes = require('../../data-types');
const Promise = require('../../promise');
const QueryTypes = require('../../query-types');
const _ = require('lodash');
/**
Returns an object that handles Postgres special needs to do certain queries.
@class QueryInterface
@static
@private
*/
/**
* Ensure enum and their values.
*
* @param {QueryInterface} qi
* @param {string} tableName Name of table to create
* @param {Object} attributes Object representing a list of normalized table attributes
* @param {Object} [options]
* @param {Model} [model]
*
* @returns {Promise}
* @private
*/
function ensureEnums(qi, tableName, attributes, options, model) {
const keys = Object.keys(attributes);
const keyLen = keys.length;
let sql = '';
let promises = [];
let i = 0;
for (i = 0; i < keyLen; i++) {
const attribute = attributes[keys[i]];
const type = attribute.type;
if (
type instanceof DataTypes.ENUM ||
type instanceof DataTypes.ARRAY && type.type instanceof DataTypes.ENUM //ARRAY sub type is ENUM
) {
sql = qi.QueryGenerator.pgListEnums(tableName, attribute.field || keys[i], options);
promises.push(qi.sequelize.query(
sql,
Object.assign({}, options, { plain: true, raw: true, type: QueryTypes.SELECT })
));
}
}
return Promise.all(promises).then(results => {
promises = [];
let enumIdx = 0;
// This little function allows us to re-use the same code that prepends or appends new value to enum array
const addEnumValue = (field, value, relativeValue, position = 'before', spliceStart = promises.length) => {
const valueOptions = _.clone(options);
valueOptions.before = null;
valueOptions.after = null;
switch (position) {
case 'after':
valueOptions.after = relativeValue;
break;
case 'before':
default:
valueOptions.before = relativeValue;
break;
}
promises.splice(spliceStart, 0, () => {
return qi.sequelize.query(qi.QueryGenerator.pgEnumAdd(
tableName, field, value, valueOptions
), valueOptions);
});
};
for (i = 0; i < keyLen; i++) {
const attribute = attributes[keys[i]];
const type = attribute.type;
const enumType = type.type || type;
const field = attribute.field || keys[i];
if (
type instanceof DataTypes.ENUM ||
type instanceof DataTypes.ARRAY && enumType instanceof DataTypes.ENUM //ARRAY sub type is ENUM
) {
// If the enum type doesn't exist then create it
if (!results[enumIdx]) {
promises.push(() => {
return qi.sequelize.query(qi.QueryGenerator.pgEnum(tableName, field, enumType, options), Object.assign({}, options, { raw: true }));
});
} else if (!!results[enumIdx] && !!model) {
const enumVals = qi.QueryGenerator.fromArray(results[enumIdx].enum_value);
const vals = enumType.values;
// Going through already existing values allows us to make queries that depend on those values
// We will prepend all new values between the old ones, but keep in mind - we can't change order of already existing values
// Then we append the rest of new values AFTER the latest already existing value
// E.g.: [1,2] -> [0,2,1] ==> [1,0,2]
// E.g.: [1,2,3] -> [2,1,3,4] ==> [1,2,3,4]
// E.g.: [1] -> [0,2,3] ==> [1,0,2,3]
let lastOldEnumValue;
let rightestPosition = -1;
for (let oldIndex = 0; oldIndex < enumVals.length; oldIndex++) {
const enumVal = enumVals[oldIndex];
const newIdx = vals.indexOf(enumVal);
lastOldEnumValue = enumVal;
if (newIdx === -1) {
continue;
}
const newValuesBefore = vals.slice(0, newIdx);
const promisesLength = promises.length;
// we go in reverse order so we could stop when we meet old value
for (let reverseIdx = newValuesBefore.length - 1; reverseIdx >= 0; reverseIdx--) {
if (~enumVals.indexOf(newValuesBefore[reverseIdx])) {
break;
}
addEnumValue(field, newValuesBefore[reverseIdx], lastOldEnumValue, 'before', promisesLength);
}
// we detect the most 'right' position of old value in new enum array so we can append new values to it
if (newIdx > rightestPosition) {
rightestPosition = newIdx;
}
}
if (lastOldEnumValue && rightestPosition < vals.length - 1) {
const remainingEnumValues = vals.slice(rightestPosition + 1);
for (let reverseIdx = remainingEnumValues.length - 1; reverseIdx >= 0; reverseIdx--) {
addEnumValue(field, remainingEnumValues[reverseIdx], lastOldEnumValue, 'after');
}
}
enumIdx++;
}
}
}
return promises
.reduce((promise, asyncFunction) => promise.then(asyncFunction), Promise.resolve())
.tap(() => {
// If ENUM processed, then refresh OIDs
if (promises.length) {
return qi.sequelize.dialect.connectionManager._refreshDynamicOIDs();
}
});
});
}
exports.ensureEnums = ensureEnums;
+382
View File
@@ -0,0 +1,382 @@
'use strict';
const AbstractQuery = require('../abstract/query');
const QueryTypes = require('../../query-types');
const Promise = require('../../promise');
const sequelizeErrors = require('../../errors');
const _ = require('lodash');
const { logger } = require('../../utils/logger');
const debug = logger.debugContext('sql:pg');
class Query extends AbstractQuery {
/**
* Rewrite query with parameters.
*
* @param {string} sql
* @param {Array|Object} values
* @param {string} dialect
* @private
*/
static formatBindParameters(sql, values, dialect) {
const stringReplaceFunc = value => typeof value === 'string' ? value.replace(/\0/g, '\\0') : value;
let bindParam;
if (Array.isArray(values)) {
bindParam = values.map(stringReplaceFunc);
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
} else {
bindParam = [];
let i = 0;
const seen = {};
const replacementFunc = (match, key, values) => {
if (seen[key] !== undefined) {
return seen[key];
}
if (values[key] !== undefined) {
i = i + 1;
bindParam.push(stringReplaceFunc(values[key]));
seen[key] = `$${i}`;
return `$${i}`;
}
return undefined;
};
sql = AbstractQuery.formatBindParameters(sql, values, dialect, replacementFunc)[0];
}
return [sql, bindParam];
}
run(sql, parameters) {
const { connection } = this;
if (!_.isEmpty(this.options.searchPath)) {
sql = this.sequelize.getQueryInterface().QueryGenerator.setSearchPath(this.options.searchPath) + sql;
}
this.sql = sql;
const query = parameters && parameters.length
? new Promise((resolve, reject) => connection.query(sql, parameters, (error, result) => error ? reject(error) : resolve(result)))
: new Promise((resolve, reject) => connection.query(sql, (error, result) => error ? reject(error) : resolve(result)));
const complete = this._logQuery(sql, debug, parameters);
return query.catch(err => {
// set the client so that it will be reaped if the connection resets while executing
if (err.code === 'ECONNRESET') {
connection._invalid = true;
}
err.sql = sql;
err.parameters = parameters;
throw this.formatError(err);
})
.then(queryResult => {
complete();
let rows = Array.isArray(queryResult)
? queryResult.reduce((allRows, r) => allRows.concat(r.rows || []), [])
: queryResult.rows;
const rowCount = Array.isArray(queryResult)
? queryResult.reduce(
(count, r) => Number.isFinite(r.rowCount) ? count + r.rowCount : count,
0
)
: queryResult.rowCount;
if (this.sequelize.options.minifyAliases && this.options.aliasesMapping) {
rows = rows
.map(row => _.toPairs(row)
.reduce((acc, [key, value]) => {
const mapping = this.options.aliasesMapping.get(key);
acc[mapping || key] = value;
return acc;
}, {})
);
}
const isTableNameQuery = sql.startsWith('SELECT table_name FROM information_schema.tables');
const isRelNameQuery = sql.startsWith('SELECT relname FROM pg_class WHERE oid IN');
if (isRelNameQuery) {
return rows.map(row => ({
name: row.relname,
tableName: row.relname.split('_')[0]
}));
}
if (isTableNameQuery) {
return rows.map(row => _.values(row));
}
if (rows[0] && rows[0].sequelize_caught_exception !== undefined) {
if (rows[0].sequelize_caught_exception !== null) {
throw this.formatError({
code: '23505',
detail: rows[0].sequelize_caught_exception
});
}
for (const row of rows) {
delete row.sequelize_caught_exception;
}
}
if (this.isShowIndexesQuery()) {
for (const row of rows) {
const attributes = /ON .*? (?:USING .*?\s)?\(([^]*)\)/gi.exec(row.definition)[1].split(',');
// Map column index in table to column name
const columns = _.zipObject(
row.column_indexes,
this.sequelize.getQueryInterface().QueryGenerator.fromArray(row.column_names)
);
delete row.column_indexes;
delete row.column_names;
let field;
let attribute;
// Indkey is the order of attributes in the index, specified by a string of attribute indexes
row.fields = row.indkey.split(' ').map((indKey, index) => {
field = columns[indKey];
// for functional indices indKey = 0
if (!field) {
return null;
}
attribute = attributes[index];
return {
attribute: field,
collate: attribute.match(/COLLATE "(.*?)"/) ? /COLLATE "(.*?)"/.exec(attribute)[1] : undefined,
order: attribute.includes('DESC') ? 'DESC' : attribute.includes('ASC') ? 'ASC' : undefined,
length: undefined
};
}).filter(n => n !== null);
delete row.columns;
}
return rows;
}
if (this.isForeignKeysQuery()) {
const result = [];
for (const row of rows) {
let defParts;
if (row.condef !== undefined && (defParts = row.condef.match(/FOREIGN KEY \((.+)\) REFERENCES (.+)\((.+)\)( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?( ON (UPDATE|DELETE) (CASCADE|RESTRICT))?/))) {
row.id = row.constraint_name;
row.table = defParts[2];
row.from = defParts[1];
row.to = defParts[3];
let i;
for (i = 5; i <= 8; i += 3) {
if (/(UPDATE|DELETE)/.test(defParts[i])) {
row[`on_${defParts[i].toLowerCase()}`] = defParts[i + 1];
}
}
}
result.push(row);
}
return result;
}
if (this.isSelectQuery()) {
let result = rows;
// Postgres will treat tables as case-insensitive, so fix the case
// of the returned values to match attributes
if (this.options.raw === false && this.sequelize.options.quoteIdentifiers === false) {
const attrsMap = _.reduce(this.model.rawAttributes, (m, v, k) => {
m[k.toLowerCase()] = k;
return m;
}, {});
result = rows.map(row => {
return _.mapKeys(row, (value, key) => {
const targetAttr = attrsMap[key];
if (typeof targetAttr === 'string' && targetAttr !== key) {
return targetAttr;
}
return key;
});
});
}
return this.handleSelectQuery(result);
}
if (QueryTypes.DESCRIBE === this.options.type) {
const result = {};
for (const row of rows) {
result[row.Field] = {
type: row.Type.toUpperCase(),
allowNull: row.Null === 'YES',
defaultValue: row.Default,
comment: row.Comment,
special: row.special ? this.sequelize.getQueryInterface().QueryGenerator.fromArray(row.special) : [],
primaryKey: row.Constraint === 'PRIMARY KEY'
};
if (result[row.Field].type === 'BOOLEAN') {
result[row.Field].defaultValue = { 'false': false, 'true': true }[result[row.Field].defaultValue];
if (result[row.Field].defaultValue === undefined) {
result[row.Field].defaultValue = null;
}
}
if (typeof result[row.Field].defaultValue === 'string') {
result[row.Field].defaultValue = result[row.Field].defaultValue.replace(/'/g, '');
if (result[row.Field].defaultValue.includes('::')) {
const split = result[row.Field].defaultValue.split('::');
if (split[1].toLowerCase() !== 'regclass)') {
result[row.Field].defaultValue = split[0];
}
}
}
}
return result;
}
if (this.isVersionQuery()) {
return rows[0].server_version;
}
if (this.isShowOrDescribeQuery()) {
return rows;
}
if (QueryTypes.BULKUPDATE === this.options.type) {
if (!this.options.returning) {
return parseInt(rowCount, 10);
}
return this.handleSelectQuery(rows);
}
if (QueryTypes.BULKDELETE === this.options.type) {
return parseInt(rowCount, 10);
}
if (this.isUpsertQuery()) {
return rows[0];
}
if (this.isInsertQuery() || this.isUpdateQuery()) {
if (this.instance && this.instance.dataValues) {
for (const key in rows[0]) {
if (Object.prototype.hasOwnProperty.call(rows[0], key)) {
const record = rows[0][key];
const attr = _.find(this.model.rawAttributes, attribute => attribute.fieldName === key || attribute.field === key);
this.instance.dataValues[attr && attr.fieldName || key] = record;
}
}
}
return [
this.instance || rows && (this.options.plain && rows[0] || rows) || undefined,
rowCount
];
}
if (this.isRawQuery()) {
return [rows, queryResult];
}
return rows;
});
}
formatError(err) {
let match;
let table;
let index;
let fields;
let errors;
let message;
const code = err.code || err.sqlState;
const errMessage = err.message || err.messagePrimary;
const errDetail = err.detail || err.messageDetail;
switch (code) {
case '23503':
index = errMessage.match(/violates foreign key constraint "(.+?)"/);
index = index ? index[1] : undefined;
table = errMessage.match(/on table "(.+?)"/);
table = table ? table[1] : undefined;
return new sequelizeErrors.ForeignKeyConstraintError({ message: errMessage, fields: null, index, table, parent: err });
case '23505':
// there are multiple different formats of error messages for this error code
// this regex should check at least two
if (errDetail && (match = errDetail.replace(/"/g, '').match(/Key \((.*?)\)=\((.*?)\)/))) {
fields = _.zipObject(match[1].split(', '), match[2].split(', '));
errors = [];
message = 'Validation error';
_.forOwn(fields, (value, field) => {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
value,
this.instance,
'not_unique'
));
});
if (this.model && this.model.uniqueKeys) {
_.forOwn(this.model.uniqueKeys, constraint => {
if (_.isEqual(constraint.fields, Object.keys(fields)) && !!constraint.msg) {
message = constraint.msg;
return false;
}
});
}
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
}
return new sequelizeErrors.UniqueConstraintError({
message: errMessage,
parent: err
});
case '23P01':
match = errDetail.match(/Key \((.*?)\)=\((.*?)\)/);
if (match) {
fields = _.zipObject(match[1].split(', '), match[2].split(', '));
}
message = 'Exclusion constraint error';
return new sequelizeErrors.ExclusionConstraintError({
message,
constraint: err.constraint,
fields,
table: err.table,
parent: err
});
case '42704':
if (err.sql && /(CONSTRAINT|INDEX)/gi.test(err.sql)) {
message = 'Unknown constraint error';
index = errMessage.match(/(?:constraint|index) "(.+?)"/i);
index = index ? index[1] : undefined;
table = errMessage.match(/relation "(.+?)"/i);
table = table ? table[1] : undefined;
throw new sequelizeErrors.UnknownConstraintError({
message,
constraint: index,
fields,
table,
parent: err
});
}
// falls through
default:
return new sequelizeErrors.DatabaseError(err);
}
}
isForeignKeysQuery() {
return /SELECT conname as constraint_name, pg_catalog\.pg_get_constraintdef\(r\.oid, true\) as condef FROM pg_catalog\.pg_constraint r WHERE r\.conrelid = \(SELECT oid FROM pg_class WHERE relname = '.*' LIMIT 1\) AND r\.contype = 'f' ORDER BY 1;/.test(this.sql);
}
getInsertIdField() {
return 'id';
}
}
module.exports = Query;
module.exports.Query = Query;
module.exports.default = Query;
+79
View File
@@ -0,0 +1,79 @@
'use strict';
const _ = require('lodash');
function stringifyRangeBound(bound) {
if (bound === null) {
return '' ;
}
if (bound === Infinity || bound === -Infinity) {
return bound.toString().toLowerCase();
}
return JSON.stringify(bound);
}
function parseRangeBound(bound, parseType) {
if (!bound) {
return null;
}
if (bound === 'infinity') {
return Infinity;
}
if (bound === '-infinity') {
return -Infinity;
}
return parseType(bound);
}
function stringify(data) {
if (data === null) return null;
if (!Array.isArray(data)) throw new Error('range must be an array');
if (!data.length) return 'empty';
if (data.length !== 2) throw new Error('range array length must be 0 (empty) or 2 (lower and upper bounds)');
if (Object.prototype.hasOwnProperty.call(data, 'inclusive')) {
if (data.inclusive === false) data.inclusive = [false, false];
else if (!data.inclusive) data.inclusive = [true, false];
else if (data.inclusive === true) data.inclusive = [true, true];
} else {
data.inclusive = [true, false];
}
_.each(data, (value, index) => {
if (_.isObject(value)) {
if (Object.prototype.hasOwnProperty.call(value, 'inclusive')) data.inclusive[index] = !!value.inclusive;
if (Object.prototype.hasOwnProperty.call(value, 'value')) data[index] = value.value;
}
});
const lowerBound = stringifyRangeBound(data[0]);
const upperBound = stringifyRangeBound(data[1]);
return `${(data.inclusive[0] ? '[' : '(') + lowerBound},${upperBound}${data.inclusive[1] ? ']' : ')'}`;
}
exports.stringify = stringify;
function parse(value, parser) {
if (value === null) return null;
if (value === 'empty') {
return [];
}
let result = value
.substring(1, value.length - 1)
.split(',', 2);
if (result.length !== 2) return value;
result = result.map((item, index) => {
return {
value: parseRangeBound(item, parser),
inclusive: index === 0 ? value[0] === '[' : value[value.length - 1] === ']'
};
});
return result;
}
exports.parse = parse;
+92
View File
@@ -0,0 +1,92 @@
'use strict';
const AbstractConnectionManager = require('../abstract/connection-manager');
const Promise = require('../../promise');
const { logger } = require('../../utils/logger');
const debug = logger.debugContext('connection:sqlite');
const dataTypes = require('../../data-types').sqlite;
const sequelizeErrors = require('../../errors');
const parserStore = require('../parserStore')('sqlite');
class ConnectionManager extends AbstractConnectionManager {
constructor(dialect, sequelize) {
super(dialect, sequelize);
// We attempt to parse file location from a connection uri
// but we shouldn't match sequelize default host.
if (this.sequelize.options.host === 'localhost') {
delete this.sequelize.options.host;
}
this.connections = {};
this.lib = this._loadDialectModule('sqlite3').verbose();
this.refreshTypeParser(dataTypes);
}
_onProcessExit() {
const promises = Object.getOwnPropertyNames(this.connections)
.map(connection => Promise.fromCallback(callback => this.connections[connection].close(callback)));
return Promise
.all(promises)
.then(() => super._onProcessExit.call(this));
}
// Expose this as a method so that the parsing may be updated when the user has added additional, custom types
_refreshTypeParser(dataType) {
parserStore.refresh(dataType);
}
_clearTypeParser() {
parserStore.clear();
}
getConnection(options) {
options = options || {};
options.uuid = options.uuid || 'default';
options.inMemory = (this.sequelize.options.storage || this.sequelize.options.host || ':memory:') === ':memory:' ? 1 : 0;
const dialectOptions = this.sequelize.options.dialectOptions;
options.readWriteMode = dialectOptions && dialectOptions.mode;
if (this.connections[options.inMemory || options.uuid]) {
return Promise.resolve(this.connections[options.inMemory || options.uuid]);
}
return new Promise((resolve, reject) => {
this.connections[options.inMemory || options.uuid] = new this.lib.Database(
this.sequelize.options.storage || this.sequelize.options.host || ':memory:',
options.readWriteMode || this.lib.OPEN_READWRITE | this.lib.OPEN_CREATE, // default mode
err => {
if (err) return reject(new sequelizeErrors.ConnectionError(err));
debug(`connection acquired ${options.uuid}`);
resolve(this.connections[options.inMemory || options.uuid]);
}
);
}).tap(connection => {
if (this.sequelize.config.password) {
// Make it possible to define and use password for sqlite encryption plugin like sqlcipher
connection.run(`PRAGMA KEY=${this.sequelize.escape(this.sequelize.config.password)}`);
}
if (this.sequelize.options.foreignKeys !== false) {
// Make it possible to define and use foreign key constraints unless
// explicitly disallowed. It's still opt-in per relation
connection.run('PRAGMA FOREIGN_KEYS=ON');
}
});
}
releaseConnection(connection, force) {
if (connection.filename === ':memory:' && force !== true) return;
if (connection.uuid) {
connection.close();
debug(`connection released ${connection.uuid}`);
delete this.connections[connection.uuid];
}
}
}
module.exports = ConnectionManager;
module.exports.ConnectionManager = ConnectionManager;
module.exports.default = ConnectionManager;
+213
View File
@@ -0,0 +1,213 @@
'use strict';
module.exports = BaseTypes => {
const warn = BaseTypes.ABSTRACT.warn.bind(undefined, 'https://www.sqlite.org/datatype3.html');
/**
* Removes unsupported SQLite options, i.e., UNSIGNED and ZEROFILL, for the integer data types.
*
* @param {Object} dataType The base integer data type.
* @private
*/
function removeUnsupportedIntegerOptions(dataType) {
if (dataType._zerofill || dataType._unsigned) {
warn(`SQLite does not support '${dataType.key}' with UNSIGNED or ZEROFILL. Plain '${dataType.key}' will be used instead.`);
dataType._unsigned = undefined;
dataType._zerofill = undefined;
}
}
/**
* @see https://sqlite.org/datatype3.html
*/
BaseTypes.DATE.types.sqlite = ['DATETIME'];
BaseTypes.STRING.types.sqlite = ['VARCHAR', 'VARCHAR BINARY'];
BaseTypes.CHAR.types.sqlite = ['CHAR', 'CHAR BINARY'];
BaseTypes.TEXT.types.sqlite = ['TEXT'];
BaseTypes.TINYINT.types.sqlite = ['TINYINT'];
BaseTypes.SMALLINT.types.sqlite = ['SMALLINT'];
BaseTypes.MEDIUMINT.types.sqlite = ['MEDIUMINT'];
BaseTypes.INTEGER.types.sqlite = ['INTEGER'];
BaseTypes.BIGINT.types.sqlite = ['BIGINT'];
BaseTypes.FLOAT.types.sqlite = ['FLOAT'];
BaseTypes.TIME.types.sqlite = ['TIME'];
BaseTypes.DATEONLY.types.sqlite = ['DATE'];
BaseTypes.BOOLEAN.types.sqlite = ['TINYINT'];
BaseTypes.BLOB.types.sqlite = ['TINYBLOB', 'BLOB', 'LONGBLOB'];
BaseTypes.DECIMAL.types.sqlite = ['DECIMAL'];
BaseTypes.UUID.types.sqlite = ['UUID'];
BaseTypes.ENUM.types.sqlite = false;
BaseTypes.REAL.types.sqlite = ['REAL'];
BaseTypes.DOUBLE.types.sqlite = ['DOUBLE PRECISION'];
BaseTypes.GEOMETRY.types.sqlite = false;
BaseTypes.JSON.types.sqlite = ['JSON', 'JSONB'];
class JSONTYPE extends BaseTypes.JSON {
static parse(data) {
return JSON.parse(data);
}
}
class DATE extends BaseTypes.DATE {
static parse(date, options) {
if (!date.includes('+')) {
// For backwards compat. Dates inserted by sequelize < 2.0dev12 will not have a timestamp set
return new Date(date + options.timezone);
}
return new Date(date); // We already have a timezone stored in the string
}
}
class DATEONLY extends BaseTypes.DATEONLY {
static parse(date) {
return date;
}
}
class STRING extends BaseTypes.STRING {
toSql() {
if (this._binary) {
return `VARCHAR BINARY(${this._length})`;
}
return super.toSql(this);
}
}
class TEXT extends BaseTypes.TEXT {
toSql() {
if (this._length) {
warn('SQLite does not support TEXT with options. Plain `TEXT` will be used instead.');
this._length = undefined;
}
return 'TEXT';
}
}
class CITEXT extends BaseTypes.CITEXT {
toSql() {
return 'TEXT COLLATE NOCASE';
}
}
class CHAR extends BaseTypes.CHAR {
toSql() {
if (this._binary) {
return `CHAR BINARY(${this._length})`;
}
return super.toSql();
}
}
class NUMBER extends BaseTypes.NUMBER {
toSql() {
let result = this.key;
if (this._unsigned) {
result += ' UNSIGNED';
}
if (this._zerofill) {
result += ' ZEROFILL';
}
if (this._length) {
result += `(${this._length}`;
if (typeof this._decimals === 'number') {
result += `,${this._decimals}`;
}
result += ')';
}
return result;
}
}
class TINYINT extends BaseTypes.TINYINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class SMALLINT extends BaseTypes.SMALLINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class MEDIUMINT extends BaseTypes.MEDIUMINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class INTEGER extends BaseTypes.INTEGER {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class BIGINT extends BaseTypes.BIGINT {
constructor(length) {
super(length);
removeUnsupportedIntegerOptions(this);
}
}
class FLOAT extends BaseTypes.FLOAT {
}
class DOUBLE extends BaseTypes.DOUBLE {
}
class REAL extends BaseTypes.REAL { }
function parseFloating(value) {
if (typeof value !== 'string') {
return value;
}
if (value === 'NaN') {
return NaN;
}
if (value === 'Infinity') {
return Infinity;
}
if (value === '-Infinity') {
return -Infinity;
}
}
for (const floating of [FLOAT, DOUBLE, REAL]) {
floating.parse = parseFloating;
}
for (const num of [FLOAT, DOUBLE, REAL, TINYINT, SMALLINT, MEDIUMINT, INTEGER, BIGINT]) {
num.prototype.toSql = NUMBER.prototype.toSql;
}
class ENUM extends BaseTypes.ENUM {
toSql() {
return 'TEXT';
}
}
return {
DATE,
DATEONLY,
STRING,
CHAR,
NUMBER,
FLOAT,
REAL,
'DOUBLE PRECISION': DOUBLE,
TINYINT,
SMALLINT,
MEDIUMINT,
INTEGER,
BIGINT,
TEXT,
ENUM,
JSON: JSONTYPE,
CITEXT
};
};
+57
View File
@@ -0,0 +1,57 @@
'use strict';
const _ = require('lodash');
const AbstractDialect = require('../abstract');
const ConnectionManager = require('./connection-manager');
const Query = require('./query');
const QueryGenerator = require('./query-generator');
const DataTypes = require('../../data-types').sqlite;
class SqliteDialect extends AbstractDialect {
constructor(sequelize) {
super();
this.sequelize = sequelize;
this.connectionManager = new ConnectionManager(this, sequelize);
this.QueryGenerator = new QueryGenerator({
_dialect: this,
sequelize
});
}
}
SqliteDialect.prototype.supports = _.merge(_.cloneDeep(AbstractDialect.prototype.supports), {
'DEFAULT': false,
'DEFAULT VALUES': true,
'UNION ALL': false,
inserts: {
ignoreDuplicates: ' OR IGNORE',
updateOnDuplicate: ' ON CONFLICT DO UPDATE SET'
},
index: {
using: false,
where: true,
functionBased: true
},
transactionOptions: {
type: true
},
constraints: {
addConstraint: false,
dropConstraint: false
},
joinTableDependent: false,
groupedLimit: false,
JSON: true
});
ConnectionManager.prototype.defaultVersion = '3.8.0';
SqliteDialect.prototype.Query = Query;
SqliteDialect.prototype.DataTypes = DataTypes;
SqliteDialect.prototype.name = 'sqlite';
SqliteDialect.prototype.TICK_CHAR = '`';
SqliteDialect.prototype.TICK_CHAR_LEFT = SqliteDialect.prototype.TICK_CHAR;
SqliteDialect.prototype.TICK_CHAR_RIGHT = SqliteDialect.prototype.TICK_CHAR;
module.exports = SqliteDialect;
module.exports.SqliteDialect = SqliteDialect;
module.exports.default = SqliteDialect;
+480
View File
@@ -0,0 +1,480 @@
'use strict';
const Utils = require('../../utils');
const Transaction = require('../../transaction');
const _ = require('lodash');
const MySqlQueryGenerator = require('../mysql/query-generator');
const AbstractQueryGenerator = require('../abstract/query-generator');
class SQLiteQueryGenerator extends MySqlQueryGenerator {
createSchema() {
return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
}
showSchemasQuery() {
return "SELECT name FROM `sqlite_master` WHERE type='table' and name!='sqlite_sequence';";
}
versionQuery() {
return 'SELECT sqlite_version() as `version`';
}
createTableQuery(tableName, attributes, options) {
options = options || {};
const primaryKeys = [];
const needsMultiplePrimaryKeys = _.values(attributes).filter(definition => definition.includes('PRIMARY KEY')).length > 1;
const attrArray = [];
for (const attr in attributes) {
if (Object.prototype.hasOwnProperty.call(attributes, attr)) {
const dataType = attributes[attr];
const containsAutoIncrement = dataType.includes('AUTOINCREMENT');
let dataTypeString = dataType;
if (dataType.includes('PRIMARY KEY')) {
if (dataType.includes('INT')) {
// Only INTEGER is allowed for primary key, see https://github.com/sequelize/sequelize/issues/969 (no lenght, unsigned etc)
dataTypeString = containsAutoIncrement ? 'INTEGER PRIMARY KEY AUTOINCREMENT' : 'INTEGER PRIMARY KEY';
if (dataType.includes(' REFERENCES')) {
dataTypeString += dataType.substr(dataType.indexOf(' REFERENCES'));
}
}
if (needsMultiplePrimaryKeys) {
primaryKeys.push(attr);
dataTypeString = dataType.replace('PRIMARY KEY', 'NOT NULL');
}
}
attrArray.push(`${this.quoteIdentifier(attr)} ${dataTypeString}`);
}
}
const table = this.quoteTable(tableName);
let attrStr = attrArray.join(', ');
const pkString = primaryKeys.map(pk => this.quoteIdentifier(pk)).join(', ');
if (options.uniqueKeys) {
_.each(options.uniqueKeys, columns => {
if (columns.customIndex) {
attrStr += `, UNIQUE (${columns.fields.map(field => this.quoteIdentifier(field)).join(', ')})`;
}
});
}
if (pkString.length > 0) {
attrStr += `, PRIMARY KEY (${pkString})`;
}
const sql = `CREATE TABLE IF NOT EXISTS ${table} (${attrStr});`;
return this.replaceBooleanDefaults(sql);
}
booleanValue(value) {
return value ? 1 : 0;
}
/**
* Check whether the statmement is json function or simple path
*
* @param {string} stmt The statement to validate
* @returns {boolean} true if the given statement is json function
* @throws {Error} throw if the statement looks like json function but has invalid token
*/
_checkValidJsonStatement(stmt) {
if (typeof stmt !== 'string') {
return false;
}
// https://sqlite.org/json1.html
const jsonFunctionRegex = /^\s*(json(?:_[a-z]+){0,2})\([^)]*\)/i;
const tokenCaptureRegex = /^\s*((?:([`"'])(?:(?!\2).|\2{2})*\2)|[\w\d\s]+|[().,;+-])/i;
let currentIndex = 0;
let openingBrackets = 0;
let closingBrackets = 0;
let hasJsonFunction = false;
let hasInvalidToken = false;
while (currentIndex < stmt.length) {
const string = stmt.substr(currentIndex);
const functionMatches = jsonFunctionRegex.exec(string);
if (functionMatches) {
currentIndex += functionMatches[0].indexOf('(');
hasJsonFunction = true;
continue;
}
const tokenMatches = tokenCaptureRegex.exec(string);
if (tokenMatches) {
const capturedToken = tokenMatches[1];
if (capturedToken === '(') {
openingBrackets++;
} else if (capturedToken === ')') {
closingBrackets++;
} else if (capturedToken === ';') {
hasInvalidToken = true;
break;
}
currentIndex += tokenMatches[0].length;
continue;
}
break;
}
// Check invalid json statement
hasInvalidToken |= openingBrackets !== closingBrackets;
if (hasJsonFunction && hasInvalidToken) {
throw new Error(`Invalid json statement: ${stmt}`);
}
// return true if the statement has valid json function
return hasJsonFunction;
}
//sqlite can't cast to datetime so we need to convert date values to their ISO strings
_toJSONValue(value) {
if (value instanceof Date) {
return value.toISOString();
}
if (Array.isArray(value) && value[0] instanceof Date) {
return value.map(val => val.toISOString());
}
return value;
}
handleSequelizeMethod(smth, tableName, factory, options, prepend) {
if (smth instanceof Utils.Json) {
return super.handleSequelizeMethod(smth, tableName, factory, options, prepend);
}
if (smth instanceof Utils.Cast) {
if (/timestamp/i.test(smth.type)) {
smth.type = 'datetime';
}
}
return AbstractQueryGenerator.prototype.handleSequelizeMethod.call(this, smth, tableName, factory, options, prepend);
}
addColumnQuery(table, key, dataType) {
const attributes = {};
attributes[key] = dataType;
const fields = this.attributesToSQL(attributes, { context: 'addColumn' });
const attribute = `${this.quoteIdentifier(key)} ${fields[key]}`;
const sql = `ALTER TABLE ${this.quoteTable(table)} ADD ${attribute};`;
return this.replaceBooleanDefaults(sql);
}
showTablesQuery() {
return 'SELECT name FROM `sqlite_master` WHERE type=\'table\' and name!=\'sqlite_sequence\';';
}
upsertQuery(tableName, insertValues, updateValues, where, model, options) {
options.ignoreDuplicates = true;
const bind = [];
const bindParam = this.bindParam(bind);
const upsertOptions = _.defaults({ bindParam }, options);
const insert = this.insertQuery(tableName, insertValues, model.rawAttributes, upsertOptions);
const update = this.updateQuery(tableName, updateValues, where, upsertOptions, model.rawAttributes);
const query = `${insert.query} ${update.query}`;
return { query, bind };
}
updateQuery(tableName, attrValueHash, where, options, attributes) {
options = options || {};
_.defaults(options, this.options);
attrValueHash = Utils.removeNullValuesFromHash(attrValueHash, options.omitNull, options);
const modelAttributeMap = {};
const values = [];
const bind = [];
const bindParam = options.bindParam || this.bindParam(bind);
if (attributes) {
_.each(attributes, (attribute, key) => {
modelAttributeMap[key] = attribute;
if (attribute.field) {
modelAttributeMap[attribute.field] = attribute;
}
});
}
for (const key in attrValueHash) {
const value = attrValueHash[key];
if (value instanceof Utils.SequelizeMethod || options.bindParam === false) {
values.push(`${this.quoteIdentifier(key)}=${this.escape(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'UPDATE' })}`);
} else {
values.push(`${this.quoteIdentifier(key)}=${this.format(value, modelAttributeMap && modelAttributeMap[key] || undefined, { context: 'UPDATE' }, bindParam)}`);
}
}
let query;
const whereOptions = _.defaults({ bindParam }, options);
if (options.limit) {
query = `UPDATE ${this.quoteTable(tableName)} SET ${values.join(',')} WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(tableName)} ${this.whereQuery(where, whereOptions)} LIMIT ${this.escape(options.limit)})`;
} else {
query = `UPDATE ${this.quoteTable(tableName)} SET ${values.join(',')} ${this.whereQuery(where, whereOptions)}`;
}
return { query, bind };
}
truncateTableQuery(tableName, options = {}) {
return [
`DELETE FROM ${this.quoteTable(tableName)}`,
options.restartIdentity ? `; DELETE FROM ${this.quoteTable('sqlite_sequence')} WHERE ${this.quoteIdentifier('name')} = ${Utils.addTicks(Utils.removeTicks(this.quoteTable(tableName), '`'), "'")};` : ''
].join('');
}
deleteQuery(tableName, where, options = {}, model) {
_.defaults(options, this.options);
let whereClause = this.getWhereConditions(where, null, model, options);
if (whereClause) {
whereClause = `WHERE ${whereClause}`;
}
if (options.limit) {
whereClause = `WHERE rowid IN (SELECT rowid FROM ${this.quoteTable(tableName)} ${whereClause} LIMIT ${this.escape(options.limit)})`;
}
return `DELETE FROM ${this.quoteTable(tableName)} ${whereClause}`;
}
attributesToSQL(attributes) {
const result = {};
for (const name in attributes) {
const dataType = attributes[name];
const fieldName = dataType.field || name;
if (_.isObject(dataType)) {
let sql = dataType.type.toString();
if (Object.prototype.hasOwnProperty.call(dataType, 'allowNull') && !dataType.allowNull) {
sql += ' NOT NULL';
}
if (Utils.defaultValueSchemable(dataType.defaultValue)) {
// TODO thoroughly check that DataTypes.NOW will properly
// get populated on all databases as DEFAULT value
// i.e. mysql requires: DEFAULT CURRENT_TIMESTAMP
sql += ` DEFAULT ${this.escape(dataType.defaultValue, dataType)}`;
}
if (dataType.unique === true) {
sql += ' UNIQUE';
}
if (dataType.primaryKey) {
sql += ' PRIMARY KEY';
if (dataType.autoIncrement) {
sql += ' AUTOINCREMENT';
}
}
if (dataType.references) {
const referencesTable = this.quoteTable(dataType.references.model);
let referencesKey;
if (dataType.references.key) {
referencesKey = this.quoteIdentifier(dataType.references.key);
} else {
referencesKey = this.quoteIdentifier('id');
}
sql += ` REFERENCES ${referencesTable} (${referencesKey})`;
if (dataType.onDelete) {
sql += ` ON DELETE ${dataType.onDelete.toUpperCase()}`;
}
if (dataType.onUpdate) {
sql += ` ON UPDATE ${dataType.onUpdate.toUpperCase()}`;
}
}
result[fieldName] = sql;
} else {
result[fieldName] = dataType;
}
}
return result;
}
showIndexesQuery(tableName) {
return `PRAGMA INDEX_LIST(${this.quoteTable(tableName)})`;
}
showConstraintsQuery(tableName, constraintName) {
let sql = `SELECT sql FROM sqlite_master WHERE tbl_name='${tableName}'`;
if (constraintName) {
sql += ` AND sql LIKE '%${constraintName}%'`;
}
return `${sql};`;
}
removeIndexQuery(tableName, indexNameOrAttributes) {
let indexName = indexNameOrAttributes;
if (typeof indexName !== 'string') {
indexName = Utils.underscore(`${tableName}_${indexNameOrAttributes.join('_')}`);
}
return `DROP INDEX IF EXISTS ${this.quoteIdentifier(indexName)}`;
}
describeTableQuery(tableName, schema, schemaDelimiter) {
const table = {
_schema: schema,
_schemaDelimiter: schemaDelimiter,
tableName
};
return `PRAGMA TABLE_INFO(${this.quoteTable(this.addSchema(table))});`;
}
describeCreateTableQuery(tableName) {
return `SELECT sql FROM sqlite_master WHERE tbl_name='${tableName}';`;
}
removeColumnQuery(tableName, attributes) {
attributes = this.attributesToSQL(attributes);
let backupTableName;
if (typeof tableName === 'object') {
backupTableName = {
tableName: `${tableName.tableName}_backup`,
schema: tableName.schema
};
} else {
backupTableName = `${tableName}_backup`;
}
const quotedTableName = this.quoteTable(tableName);
const quotedBackupTableName = this.quoteTable(backupTableName);
const attributeNames = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
// Temporary table cannot work for foreign keys.
return `${this.createTableQuery(backupTableName, attributes)
}INSERT INTO ${quotedBackupTableName} SELECT ${attributeNames} FROM ${quotedTableName};`
+ `DROP TABLE ${quotedTableName};${
this.createTableQuery(tableName, attributes)
}INSERT INTO ${quotedTableName} SELECT ${attributeNames} FROM ${quotedBackupTableName};`
+ `DROP TABLE ${quotedBackupTableName};`;
}
_alterConstraintQuery(tableName, attributes, createTableSql) {
let backupTableName;
attributes = this.attributesToSQL(attributes);
if (typeof tableName === 'object') {
backupTableName = {
tableName: `${tableName.tableName}_backup`,
schema: tableName.schema
};
} else {
backupTableName = `${tableName}_backup`;
}
const quotedTableName = this.quoteTable(tableName);
const quotedBackupTableName = this.quoteTable(backupTableName);
const attributeNames = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
return `${createTableSql
.replace(`CREATE TABLE ${quotedTableName}`, `CREATE TABLE ${quotedBackupTableName}`)
.replace(`CREATE TABLE ${quotedTableName.replace(/`/g, '"')}`, `CREATE TABLE ${quotedBackupTableName}`)
}INSERT INTO ${quotedBackupTableName} SELECT ${attributeNames} FROM ${quotedTableName};`
+ `DROP TABLE ${quotedTableName};`
+ `ALTER TABLE ${quotedBackupTableName} RENAME TO ${quotedTableName};`;
}
renameColumnQuery(tableName, attrNameBefore, attrNameAfter, attributes) {
let backupTableName;
attributes = this.attributesToSQL(attributes);
if (typeof tableName === 'object') {
backupTableName = {
tableName: `${tableName.tableName}_backup`,
schema: tableName.schema
};
} else {
backupTableName = `${tableName}_backup`;
}
const quotedTableName = this.quoteTable(tableName);
const quotedBackupTableName = this.quoteTable(backupTableName);
const attributeNamesImport = Object.keys(attributes).map(attr =>
attrNameAfter === attr ? `${this.quoteIdentifier(attrNameBefore)} AS ${this.quoteIdentifier(attr)}` : this.quoteIdentifier(attr)
).join(', ');
const attributeNamesExport = Object.keys(attributes).map(attr => this.quoteIdentifier(attr)).join(', ');
return `${this.createTableQuery(backupTableName, attributes).replace('CREATE TABLE', 'CREATE TEMPORARY TABLE')
}INSERT INTO ${quotedBackupTableName} SELECT ${attributeNamesImport} FROM ${quotedTableName};`
+ `DROP TABLE ${quotedTableName};${
this.createTableQuery(tableName, attributes)
}INSERT INTO ${quotedTableName} SELECT ${attributeNamesExport} FROM ${quotedBackupTableName};`
+ `DROP TABLE ${quotedBackupTableName};`;
}
startTransactionQuery(transaction) {
if (transaction.parent) {
return `SAVEPOINT ${this.quoteIdentifier(transaction.name)};`;
}
return `BEGIN ${transaction.options.type} TRANSACTION;`;
}
setIsolationLevelQuery(value) {
switch (value) {
case Transaction.ISOLATION_LEVELS.REPEATABLE_READ:
return '-- SQLite is not able to choose the isolation level REPEATABLE READ.';
case Transaction.ISOLATION_LEVELS.READ_UNCOMMITTED:
return 'PRAGMA read_uncommitted = ON;';
case Transaction.ISOLATION_LEVELS.READ_COMMITTED:
return 'PRAGMA read_uncommitted = OFF;';
case Transaction.ISOLATION_LEVELS.SERIALIZABLE:
return '-- SQLite\'s default isolation level is SERIALIZABLE. Nothing to do.';
default:
throw new Error(`Unknown isolation level: ${value}`);
}
}
replaceBooleanDefaults(sql) {
return sql.replace(/DEFAULT '?false'?/g, 'DEFAULT 0').replace(/DEFAULT '?true'?/g, 'DEFAULT 1');
}
/**
* Generates an SQL query that returns all foreign keys of a table.
*
* @param {string} tableName The name of the table.
* @returns {string} The generated sql query.
* @private
*/
getForeignKeysQuery(tableName) {
return `PRAGMA foreign_key_list(${tableName})`;
}
}
module.exports = SQLiteQueryGenerator;
+208
View File
@@ -0,0 +1,208 @@
'use strict';
const _ = require('lodash');
const Promise = require('../../promise');
const sequelizeErrors = require('../../errors');
const QueryTypes = require('../../query-types');
/**
Returns an object that treats SQLite's inabilities to do certain queries.
@class QueryInterface
@static
@private
*/
/**
A wrapper that fixes SQLite's inability to remove columns from existing tables.
It will create a backup of the table, drop the table afterwards and create a
new table with the same name but without the obsolete column.
@param {QueryInterface} qi
@param {string} tableName The name of the table.
@param {string} attributeName The name of the attribute that we want to remove.
@param {Object} options
@param {boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
@since 1.6.0
@private
*/
function removeColumn(qi, tableName, attributeName, options) {
options = options || {};
return qi.describeTable(tableName, options).then(fields => {
delete fields[attributeName];
const sql = qi.QueryGenerator.removeColumnQuery(tableName, fields);
const subQueries = sql.split(';').filter(q => q !== '');
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
});
}
exports.removeColumn = removeColumn;
/**
A wrapper that fixes SQLite's inability to change columns from existing tables.
It will create a backup of the table, drop the table afterwards and create a
new table with the same name but with a modified version of the respective column.
@param {QueryInterface} qi
@param {string} tableName The name of the table.
@param {Object} attributes An object with the attribute's name as key and its options as value object.
@param {Object} options
@param {boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
@since 1.6.0
@private
*/
function changeColumn(qi, tableName, attributes, options) {
const attributeName = Object.keys(attributes)[0];
options = options || {};
return qi.describeTable(tableName, options).then(fields => {
fields[attributeName] = attributes[attributeName];
const sql = qi.QueryGenerator.removeColumnQuery(tableName, fields);
const subQueries = sql.split(';').filter(q => q !== '');
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
});
}
exports.changeColumn = changeColumn;
/**
A wrapper that fixes SQLite's inability to rename columns from existing tables.
It will create a backup of the table, drop the table afterwards and create a
new table with the same name but with a renamed version of the respective column.
@param {QueryInterface} qi
@param {string} tableName The name of the table.
@param {string} attrNameBefore The name of the attribute before it was renamed.
@param {string} attrNameAfter The name of the attribute after it was renamed.
@param {Object} options
@param {boolean|Function} [options.logging] A function that logs the sql queries, or false for explicitly not logging these queries
@since 1.6.0
@private
*/
function renameColumn(qi, tableName, attrNameBefore, attrNameAfter, options) {
options = options || {};
return qi.describeTable(tableName, options).then(fields => {
fields[attrNameAfter] = _.clone(fields[attrNameBefore]);
delete fields[attrNameBefore];
const sql = qi.QueryGenerator.renameColumnQuery(tableName, attrNameBefore, attrNameAfter, fields);
const subQueries = sql.split(';').filter(q => q !== '');
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
});
}
exports.renameColumn = renameColumn;
/**
* @param {QueryInterface} qi
* @param {string} tableName
* @param {string} constraintName
* @param {Object} options
*
* @private
*/
function removeConstraint(qi, tableName, constraintName, options) {
let createTableSql;
return qi.showConstraint(tableName, constraintName)
.then(constraints => {
// sqlite can't show only one constraint, so we find here the one to remove
const constraint = constraints.find(constaint => constaint.constraintName === constraintName);
if (constraint) {
createTableSql = constraint.sql;
constraint.constraintName = qi.QueryGenerator.quoteIdentifier(constraint.constraintName);
let constraintSnippet = `, CONSTRAINT ${constraint.constraintName} ${constraint.constraintType} ${constraint.constraintCondition}`;
if (constraint.constraintType === 'FOREIGN KEY') {
const referenceTableName = qi.QueryGenerator.quoteTable(constraint.referenceTableName);
constraint.referenceTableKeys = constraint.referenceTableKeys.map(columnName => qi.QueryGenerator.quoteIdentifier(columnName));
const referenceTableKeys = constraint.referenceTableKeys.join(', ');
constraintSnippet += ` REFERENCES ${referenceTableName} (${referenceTableKeys})`;
constraintSnippet += ` ON UPDATE ${constraint.updateAction}`;
constraintSnippet += ` ON DELETE ${constraint.deleteAction}`;
}
createTableSql = createTableSql.replace(constraintSnippet, '');
createTableSql += ';';
return qi.describeTable(tableName, options);
}
throw new sequelizeErrors.UnknownConstraintError({
message: `Constraint ${constraintName} on table ${tableName} does not exist`,
constraint: constraintName,
table: tableName
});
})
.then(fields => {
const sql = qi.QueryGenerator._alterConstraintQuery(tableName, fields, createTableSql);
const subQueries = sql.split(';').filter(q => q !== '');
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
});
}
exports.removeConstraint = removeConstraint;
/**
* @param {QueryInterface} qi
* @param {string} tableName
* @param {Object} options
*
* @private
*/
function addConstraint(qi, tableName, options) {
const constraintSnippet = qi.QueryGenerator.getConstraintSnippet(tableName, options);
const describeCreateTableSql = qi.QueryGenerator.describeCreateTableQuery(tableName);
let createTableSql;
return qi.sequelize.query(describeCreateTableSql, Object.assign({}, options, { type: QueryTypes.SELECT, raw: true }))
.then(constraints => {
const sql = constraints[0].sql;
const index = sql.length - 1;
//Replace ending ')' with constraint snippet - Simulates String.replaceAt
//http://stackoverflow.com/questions/1431094
createTableSql = `${sql.substr(0, index)}, ${constraintSnippet})${sql.substr(index + 1)};`;
return qi.describeTable(tableName, options);
})
.then(fields => {
const sql = qi.QueryGenerator._alterConstraintQuery(tableName, fields, createTableSql);
const subQueries = sql.split(';').filter(q => q !== '');
return Promise.each(subQueries, subQuery => qi.sequelize.query(`${subQuery};`, Object.assign({ raw: true }, options)));
});
}
exports.addConstraint = addConstraint;
/**
* @param {QueryInterface} qi
* @param {string} tableName
* @param {Object} options Query Options
*
* @private
* @returns {Promise}
*/
function getForeignKeyReferencesForTable(qi, tableName, options) {
const database = qi.sequelize.config.database;
const query = qi.QueryGenerator.getForeignKeysQuery(tableName, database);
return qi.sequelize.query(query, options)
.then(result => {
return result.map(row => ({
tableName,
columnName: row.from,
referencedTableName: row.table,
referencedColumnName: row.to,
tableCatalog: database,
referencedTableCatalog: database
}));
});
}
exports.getForeignKeyReferencesForTable = getForeignKeyReferencesForTable;
+460
View File
@@ -0,0 +1,460 @@
'use strict';
const _ = require('lodash');
const Utils = require('../../utils');
const Promise = require('../../promise');
const AbstractQuery = require('../abstract/query');
const QueryTypes = require('../../query-types');
const sequelizeErrors = require('../../errors');
const parserStore = require('../parserStore')('sqlite');
const { logger } = require('../../utils/logger');
const debug = logger.debugContext('sql:sqlite');
class Query extends AbstractQuery {
getInsertIdField() {
return 'lastID';
}
/**
* rewrite query with parameters.
*
* @param {string} sql
* @param {Array|Object} values
* @param {string} dialect
* @private
*/
static formatBindParameters(sql, values, dialect) {
let bindParam;
if (Array.isArray(values)) {
bindParam = {};
values.forEach((v, i) => {
bindParam[`$${i + 1}`] = v;
});
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
} else {
bindParam = {};
if (typeof values === 'object') {
for (const k of Object.keys(values)) {
bindParam[`$${k}`] = values[k];
}
}
sql = AbstractQuery.formatBindParameters(sql, values, dialect, { skipValueReplace: true })[0];
}
return [sql, bindParam];
}
_collectModels(include, prefix) {
const ret = {};
if (include) {
for (const _include of include) {
let key;
if (!prefix) {
key = _include.as;
} else {
key = `${prefix}.${_include.as}`;
}
ret[key] = _include.model;
if (_include.include) {
_.merge(ret, this._collectModels(_include.include, key));
}
}
}
return ret;
}
_handleQueryResponse(metaData, columnTypes, err, results) {
if (err) {
err.sql = this.sql;
throw this.formatError(err);
}
let result = this.instance;
// add the inserted row id to the instance
if (this.isInsertQuery(results, metaData)) {
this.handleInsertQuery(results, metaData);
if (!this.instance) {
// handle bulkCreate AI primary key
if (
metaData.constructor.name === 'Statement'
&& this.model
&& this.model.autoIncrementAttribute
&& this.model.autoIncrementAttribute === this.model.primaryKeyAttribute
&& this.model.rawAttributes[this.model.primaryKeyAttribute]
) {
const startId = metaData[this.getInsertIdField()] - metaData.changes + 1;
result = [];
for (let i = startId; i < startId + metaData.changes; i++) {
result.push({ [this.model.rawAttributes[this.model.primaryKeyAttribute].field]: i });
}
} else {
result = metaData[this.getInsertIdField()];
}
}
}
if (this.isShowTablesQuery()) {
return results.map(row => row.name);
}
if (this.isShowConstraintsQuery()) {
result = results;
if (results && results[0] && results[0].sql) {
result = this.parseConstraintsFromSql(results[0].sql);
}
return result;
}
if (this.isSelectQuery()) {
if (this.options.raw) {
return this.handleSelectQuery(results);
}
// This is a map of prefix strings to models, e.g. user.projects -> Project model
const prefixes = this._collectModels(this.options.include);
results = results.map(result => {
return _.mapValues(result, (value, name) => {
let model;
if (name.includes('.')) {
const lastind = name.lastIndexOf('.');
model = prefixes[name.substr(0, lastind)];
name = name.substr(lastind + 1);
} else {
model = this.options.model;
}
const tableName = model.getTableName().toString().replace(/`/g, '');
const tableTypes = columnTypes[tableName] || {};
if (tableTypes && !(name in tableTypes)) {
// The column is aliased
_.forOwn(model.rawAttributes, (attribute, key) => {
if (name === key && attribute.field) {
name = attribute.field;
return false;
}
});
}
return Object.prototype.hasOwnProperty.call(tableTypes, name)
? this.applyParsers(tableTypes[name], value)
: value;
});
});
return this.handleSelectQuery(results);
}
if (this.isShowOrDescribeQuery()) {
return results;
}
if (this.sql.includes('PRAGMA INDEX_LIST')) {
return this.handleShowIndexesQuery(results);
}
if (this.sql.includes('PRAGMA INDEX_INFO')) {
return results;
}
if (this.sql.includes('PRAGMA TABLE_INFO')) {
// this is the sqlite way of getting the metadata of a table
result = {};
let defaultValue;
for (const _result of results) {
if (_result.dflt_value === null) {
// Column schema omits any "DEFAULT ..."
defaultValue = undefined;
} else if (_result.dflt_value === 'NULL') {
// Column schema is a "DEFAULT NULL"
defaultValue = null;
} else {
defaultValue = _result.dflt_value;
}
result[_result.name] = {
type: _result.type,
allowNull: _result.notnull === 0,
defaultValue,
primaryKey: _result.pk !== 0
};
if (result[_result.name].type === 'TINYINT(1)') {
result[_result.name].defaultValue = { '0': false, '1': true }[result[_result.name].defaultValue];
}
if (typeof result[_result.name].defaultValue === 'string') {
result[_result.name].defaultValue = result[_result.name].defaultValue.replace(/'/g, '');
}
}
return result;
}
if (this.sql.includes('PRAGMA foreign_keys;')) {
return results[0];
}
if (this.sql.includes('PRAGMA foreign_keys')) {
return results;
}
if (this.sql.includes('PRAGMA foreign_key_list')) {
return results;
}
if ([QueryTypes.BULKUPDATE, QueryTypes.BULKDELETE].includes(this.options.type)) {
return metaData.changes;
}
if (this.options.type === QueryTypes.UPSERT) {
return undefined;
}
if (this.options.type === QueryTypes.VERSION) {
return results[0].version;
}
if (this.options.type === QueryTypes.RAW) {
return [results, metaData];
}
if (this.isUpdateQuery() || this.isInsertQuery()) {
return [result, metaData.changes];
}
return result;
}
run(sql, parameters) {
const conn = this.connection;
this.sql = sql;
const method = this.getDatabaseMethod();
let complete;
if (method === 'exec') {
// exec does not support bind parameter
sql = AbstractQuery.formatBindParameters(sql, this.options.bind, this.options.dialect || 'sqlite', { skipUnescape: true })[0];
this.sql = sql;
complete = this._logQuery(sql, debug);
} else {
complete = this._logQuery(sql, debug, parameters);
}
return new Promise(resolve => {
const columnTypes = {};
conn.serialize(() => {
const executeSql = () => {
if (sql.startsWith('-- ')) {
return resolve();
}
resolve(new Promise((resolve, reject) => {
const query = this;
// cannot use arrow function here because the function is bound to the statement
function afterExecute(executionError, results) {
try {
complete();
// `this` is passed from sqlite, we have no control over this.
// eslint-disable-next-line no-invalid-this
resolve(query._handleQueryResponse(this, columnTypes, executionError, results));
return;
} catch (error) {
reject(error);
}
}
if (method === 'exec') {
// exec does not support bind parameter
conn[method](sql, afterExecute);
} else {
if (!parameters) parameters = [];
conn[method](sql, parameters, afterExecute);
}
}));
return null;
};
if (this.getDatabaseMethod() === 'all') {
let tableNames = [];
if (this.options && this.options.tableNames) {
tableNames = this.options.tableNames;
} else if (/FROM `(.*?)`/i.exec(this.sql)) {
tableNames.push(/FROM `(.*?)`/i.exec(this.sql)[1]);
}
// If we already have the metadata for the table, there's no need to ask for it again
tableNames = tableNames.filter(tableName => !(tableName in columnTypes) && tableName !== 'sqlite_master');
if (!tableNames.length) {
return executeSql();
}
return Promise.map(tableNames, tableName =>
new Promise(resolve => {
tableName = tableName.replace(/`/g, '');
columnTypes[tableName] = {};
conn.all(`PRAGMA table_info(\`${tableName}\`)`, (err, results) => {
if (!err) {
for (const result of results) {
columnTypes[tableName][result.name] = result.type;
}
}
resolve();
});
})
).then(executeSql);
}
return executeSql();
});
});
}
parseConstraintsFromSql(sql) {
let constraints = sql.split('CONSTRAINT ');
let referenceTableName, referenceTableKeys, updateAction, deleteAction;
constraints.splice(0, 1);
constraints = constraints.map(constraintSql => {
//Parse foreign key snippets
if (constraintSql.includes('REFERENCES')) {
//Parse out the constraint condition form sql string
updateAction = constraintSql.match(/ON UPDATE (CASCADE|SET NULL|RESTRICT|NO ACTION|SET DEFAULT){1}/);
deleteAction = constraintSql.match(/ON DELETE (CASCADE|SET NULL|RESTRICT|NO ACTION|SET DEFAULT){1}/);
if (updateAction) {
updateAction = updateAction[1];
}
if (deleteAction) {
deleteAction = deleteAction[1];
}
const referencesRegex = /REFERENCES.+\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/;
const referenceConditions = constraintSql.match(referencesRegex)[0].split(' ');
referenceTableName = Utils.removeTicks(referenceConditions[1]);
let columnNames = referenceConditions[2];
columnNames = columnNames.replace(/\(|\)/g, '').split(', ');
referenceTableKeys = columnNames.map(column => Utils.removeTicks(column));
}
const constraintCondition = constraintSql.match(/\((?:[^)(]+|\((?:[^)(]+|\([^)(]*\))*\))*\)/)[0];
constraintSql = constraintSql.replace(/\(.+\)/, '');
const constraint = constraintSql.split(' ');
if (constraint[1] === 'PRIMARY' || constraint[1] === 'FOREIGN') {
constraint[1] += ' KEY';
}
return {
constraintName: Utils.removeTicks(constraint[0]),
constraintType: constraint[1],
updateAction,
deleteAction,
sql: sql.replace(/"/g, '`'), //Sqlite returns double quotes for table name
constraintCondition,
referenceTableName,
referenceTableKeys
};
});
return constraints;
}
applyParsers(type, value) {
if (type.includes('(')) {
// Remove the length part
type = type.substr(0, type.indexOf('('));
}
type = type.replace('UNSIGNED', '').replace('ZEROFILL', '');
type = type.trim().toUpperCase();
const parse = parserStore.get(type);
if (value !== null && parse) {
return parse(value, { timezone: this.sequelize.options.timezone });
}
return value;
}
formatError(err) {
switch (err.code) {
case 'SQLITE_CONSTRAINT': {
if (err.message.includes('FOREIGN KEY constraint failed')) {
return new sequelizeErrors.ForeignKeyConstraintError({
parent: err
});
}
let fields = [];
// Sqlite pre 2.2 behavior - Error: SQLITE_CONSTRAINT: columns x, y are not unique
let match = err.message.match(/columns (.*?) are/);
if (match !== null && match.length >= 2) {
fields = match[1].split(', ');
} else {
// Sqlite post 2.2 behavior - Error: SQLITE_CONSTRAINT: UNIQUE constraint failed: table.x, table.y
match = err.message.match(/UNIQUE constraint failed: (.*)/);
if (match !== null && match.length >= 2) {
fields = match[1].split(', ').map(columnWithTable => columnWithTable.split('.')[1]);
}
}
const errors = [];
let message = 'Validation error';
for (const field of fields) {
errors.push(new sequelizeErrors.ValidationErrorItem(
this.getUniqueConstraintErrorMessage(field),
'unique violation', // sequelizeErrors.ValidationErrorItem.Origins.DB,
field,
this.instance && this.instance[field],
this.instance,
'not_unique'
));
}
if (this.model) {
_.forOwn(this.model.uniqueKeys, constraint => {
if (_.isEqual(constraint.fields, fields) && !!constraint.msg) {
message = constraint.msg;
return false;
}
});
}
return new sequelizeErrors.UniqueConstraintError({ message, errors, parent: err, fields });
}
case 'SQLITE_BUSY':
return new sequelizeErrors.TimeoutError(err);
default:
return new sequelizeErrors.DatabaseError(err);
}
}
handleShowIndexesQuery(data) {
// Sqlite returns indexes so the one that was defined last is returned first. Lets reverse that!
return Promise.map(data.reverse(), item => {
item.fields = [];
item.primary = false;
item.unique = !!item.unique;
item.constraintName = item.name;
return this.run(`PRAGMA INDEX_INFO(\`${item.name}\`)`).then(columns => {
for (const column of columns) {
item.fields[column.seqno] = {
attribute: column.name,
length: undefined,
order: undefined
};
}
return item;
});
});
}
getDatabaseMethod() {
if (this.isUpsertQuery()) {
return 'exec'; // Needed to run multiple queries in one
}
if (this.isInsertQuery() || this.isUpdateQuery() || this.isBulkUpdateQuery() || this.sql.toLowerCase().includes('CREATE TEMPORARY TABLE'.toLowerCase()) || this.options.type === QueryTypes.BULKDELETE) {
return 'run';
}
return 'all';
}
}
module.exports = Query;
module.exports.Query = Query;
module.exports.default = Query;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const BaseError = require('./base-error');
/**
* Thrown when an association is improperly constructed (see message for details)
*/
class AssociationError extends BaseError {
constructor(message) {
super(message);
this.name = 'SequelizeAssociationError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AssociationError;
+18
View File
@@ -0,0 +1,18 @@
'use strict';
/**
* Sequelize provides a host of custom error classes, to allow you to do easier debugging. All of these errors are exposed on the sequelize object and the sequelize constructor.
* All sequelize errors inherit from the base JS error object.
*
* This means that errors can be accessed using `Sequelize.ValidationError`
* The Base Error all Sequelize Errors inherit from.
*/
class BaseError extends Error {
constructor(message) {
super(message);
this.name = 'SequelizeBaseError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = BaseError;
+22
View File
@@ -0,0 +1,22 @@
'use strict';
const BaseError = require('./base-error');
/**
* Thrown when bulk operation fails, it represent per record level error.
* Used with Promise.AggregateError
*
* @param {Error} error Error for a given record/instance
* @param {Object} record DAO instance that error belongs to
*/
class BulkRecordError extends BaseError {
constructor(error, record) {
super(error.message);
this.name = 'SequelizeBulkRecordError';
this.errors = error;
this.record = record;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = BulkRecordError;
+22
View File
@@ -0,0 +1,22 @@
'use strict';
const BaseError = require('./base-error');
/**
* A base class for all connection related errors.
*/
class ConnectionError extends BaseError {
constructor(parent) {
super(parent ? parent.message : '');
this.name = 'SequelizeConnectionError';
/**
* The connection specific error which triggered this one
* @type {Error}
*/
this.parent = parent;
this.original = parent;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ConnectionError;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const ConnectionError = require('./../connection-error');
/**
* Thrown when a connection to a database is refused due to insufficient privileges
*/
class AccessDeniedError extends ConnectionError {
constructor(parent) {
super(parent);
this.name = 'SequelizeAccessDeniedError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = AccessDeniedError;
@@ -0,0 +1,16 @@
'use strict';
const ConnectionError = require('./../connection-error');
/**
* Thrown when connection is not acquired due to timeout
*/
class ConnectionAcquireTimeoutError extends ConnectionError {
constructor(parent) {
super(parent);
this.name = 'SequelizeConnectionAcquireTimeoutError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ConnectionAcquireTimeoutError;
@@ -0,0 +1,16 @@
'use strict';
const ConnectionError = require('./../connection-error');
/**
* Thrown when a connection to a database is refused
*/
class ConnectionRefusedError extends ConnectionError {
constructor(parent) {
super(parent);
this.name = 'SequelizeConnectionRefusedError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ConnectionRefusedError;
@@ -0,0 +1,16 @@
'use strict';
const ConnectionError = require('./../connection-error');
/**
* Thrown when a connection to a database times out
*/
class ConnectionTimedOutError extends ConnectionError {
constructor(parent) {
super(parent);
this.name = 'SequelizeConnectionTimedOutError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ConnectionTimedOutError;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const ConnectionError = require('./../connection-error');
/**
* Thrown when a connection to a database has a hostname that was not found
*/
class HostNotFoundError extends ConnectionError {
constructor(parent) {
super(parent);
this.name = 'SequelizeHostNotFoundError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = HostNotFoundError;
@@ -0,0 +1,16 @@
'use strict';
const ConnectionError = require('./../connection-error');
/**
* Thrown when a connection to a database has a hostname that was not reachable
*/
class HostNotReachableError extends ConnectionError {
constructor(parent) {
super(parent);
this.name = 'SequelizeHostNotReachableError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = HostNotReachableError;
@@ -0,0 +1,16 @@
'use strict';
const ConnectionError = require('./../connection-error');
/**
* Thrown when a connection to a database has invalid values for any of the connection parameters
*/
class InvalidConnectionError extends ConnectionError {
constructor(parent) {
super(parent);
this.name = 'SequelizeInvalidConnectionError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = InvalidConnectionError;
+34
View File
@@ -0,0 +1,34 @@
'use strict';
const BaseError = require('./base-error');
/**
* A base class for all database related errors.
*/
class DatabaseError extends BaseError {
constructor(parent) {
super(parent.message);
this.name = 'SequelizeDatabaseError';
/**
* @type {Error}
*/
this.parent = parent;
/**
* @type {Error}
*/
this.original = parent;
/**
* The SQL that triggered the error
* @type {string}
*/
this.sql = parent.sql;
/**
* The parameters for the sql that triggered the error
* @type {Array<any>}
*/
this.parameters = parent.parameters;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = DatabaseError;
@@ -0,0 +1,24 @@
'use strict';
const DatabaseError = require('./../database-error');
/**
* Thrown when an exclusion constraint is violated in the database
*/
class ExclusionConstraintError extends DatabaseError {
constructor(options) {
options = options || {};
options.parent = options.parent || { sql: '' };
super(options.parent);
this.name = 'SequelizeExclusionConstraintError';
this.message = options.message || options.parent.message || '';
this.constraint = options.constraint;
this.fields = options.fields;
this.table = options.table;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ExclusionConstraintError;
@@ -0,0 +1,26 @@
'use strict';
const DatabaseError = require('./../database-error');
/**
* Thrown when a foreign key constraint is violated in the database
*/
class ForeignKeyConstraintError extends DatabaseError {
constructor(options) {
options = options || {};
options.parent = options.parent || { sql: '' };
super(options.parent);
this.name = 'SequelizeForeignKeyConstraintError';
this.message = options.message || options.parent.message || 'Database Error';
this.fields = options.fields;
this.table = options.table;
this.value = options.value;
this.index = options.index;
this.reltype = options.reltype;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = ForeignKeyConstraintError;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const DatabaseError = require('./../database-error');
/**
* Thrown when a database query times out because of a deadlock
*/
class TimeoutError extends DatabaseError {
constructor(parent) {
super(parent);
this.name = 'SequelizeTimeoutError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = TimeoutError;
+24
View File
@@ -0,0 +1,24 @@
'use strict';
const DatabaseError = require('./../database-error');
/**
* Thrown when constraint name is not found in the database
*/
class UnknownConstraintError extends DatabaseError {
constructor(options) {
options = options || {};
options.parent = options.parent || { sql: '' };
super(options.parent);
this.name = 'SequelizeUnknownConstraintError';
this.message = options.message || 'The specified constraint does not exist';
this.constraint = options.constraint;
this.fields = options.fields;
this.table = options.table;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = UnknownConstraintError;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const BaseError = require('./base-error');
/**
* Thrown when an include statement is improperly constructed (see message for details)
*/
class EagerLoadingError extends BaseError {
constructor(message) {
super(message);
this.name = 'SequelizeEagerLoadingError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = EagerLoadingError;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const BaseError = require('./base-error');
/**
* Thrown when a record was not found, Usually used with rejectOnEmpty mode (see message for details)
*/
class EmptyResultError extends BaseError {
constructor(message) {
super(message);
this.name = 'SequelizeEmptyResultError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = EmptyResultError;
+31
View File
@@ -0,0 +1,31 @@
'use strict';
exports.BaseError = require('./base-error');
exports.AssociationError = require('./association-error');
exports.BulkRecordError = require('./bulk-record-error');
exports.ConnectionError = require('./connection-error');
exports.DatabaseError = require('./database-error');
exports.EagerLoadingError = require('./eager-loading-error');
exports.EmptyResultError = require('./empty-result-error');
exports.InstanceError = require('./instance-error');
exports.OptimisticLockError = require('./optimistic-lock-error');
exports.QueryError = require('./query-error');
exports.SequelizeScopeError = require('./sequelize-scope-error');
exports.ValidationError = require('./validation-error');
exports.ValidationErrorItem = exports.ValidationError.ValidationErrorItem;
exports.AccessDeniedError = require('./connection/access-denied-error');
exports.ConnectionAcquireTimeoutError = require('./connection/connection-acquire-timeout-error');
exports.ConnectionRefusedError = require('./connection/connection-refused-error');
exports.ConnectionTimedOutError = require('./connection/connection-timed-out-error');
exports.HostNotFoundError = require('./connection/host-not-found-error');
exports.HostNotReachableError = require('./connection/host-not-reachable-error');
exports.InvalidConnectionError = require('./connection/invalid-connection-error');
exports.ExclusionConstraintError = require('./database/exclusion-constraint-error');
exports.ForeignKeyConstraintError = require('./database/foreign-key-constraint-error');
exports.TimeoutError = require('./database/timeout-error');
exports.UnknownConstraintError = require('./database/unknown-constraint-error');
exports.UniqueConstraintError = require('./validation/unique-constraint-error');
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const BaseError = require('./base-error');
/**
* Thrown when a some problem occurred with Instance methods (see message for details)
*/
class InstanceError extends BaseError {
constructor(message) {
super(message);
this.name = 'SequelizeInstanceError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = InstanceError;
+33
View File
@@ -0,0 +1,33 @@
'use strict';
const BaseError = require('./base-error');
/**
* Thrown when attempting to update a stale model instance
*/
class OptimisticLockError extends BaseError {
constructor(options) {
options = options || {};
options.message = options.message || `Attempting to update a stale model instance: ${options.modelName}`;
super(options.message);
this.name = 'SequelizeOptimisticLockError';
/**
* The name of the model on which the update was attempted
* @type {string}
*/
this.modelName = options.modelName;
/**
* The values of the attempted update
* @type {object}
*/
this.values = options.values;
/**
*
* @type {object}
*/
this.where = options.where;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = OptimisticLockError;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const BaseError = require('./base-error');
/**
* Thrown when a query is passed invalid options (see message for details)
*/
class QueryError extends BaseError {
constructor(message) {
super(message);
this.name = 'SequelizeQueryError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = QueryError;
+16
View File
@@ -0,0 +1,16 @@
'use strict';
const BaseError = require('./base-error');
/**
* Scope Error. Thrown when the sequelize cannot query the specified scope.
*/
class SequelizeScopeError extends BaseError {
constructor(parent) {
super(parent);
this.name = 'SequelizeScopeError';
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = SequelizeScopeError;
+209
View File
@@ -0,0 +1,209 @@
'use strict';
const BaseError = require('./base-error');
/**
* Validation Error. Thrown when the sequelize validation has failed. The error contains an `errors` property,
* which is an array with 1 or more ValidationErrorItems, one for each validation that failed.
*
* @param {string} message Error message
* @param {Array} [errors] Array of ValidationErrorItem objects describing the validation errors
*
* @property errors {ValidationErrorItems[]}
*/
class ValidationError extends BaseError {
constructor(message, errors) {
super(message);
this.name = 'SequelizeValidationError';
this.message = 'Validation Error';
/**
*
* @type {ValidationErrorItem[]}
*/
this.errors = errors || [];
// Use provided error message if available...
if (message) {
this.message = message;
// ... otherwise create a concatenated message out of existing errors.
} else if (this.errors.length > 0 && this.errors[0].message) {
this.message = this.errors.map(err => `${err.type || err.origin}: ${err.message}`).join(',\n');
}
Error.captureStackTrace(this, this.constructor);
}
/**
* Gets all validation error items for the path / field specified.
*
* @param {string} path The path to be checked for error items
*
* @returns {Array<ValidationErrorItem>} Validation error items for the specified path
*/
get(path) {
return this.errors.reduce((reduced, error) => {
if (error.path === path) {
reduced.push(error);
}
return reduced;
}, []);
}
}
/**
* Validation Error Item
* Instances of this class are included in the `ValidationError.errors` property.
*/
class ValidationErrorItem {
/**
* Creates new validation error item
*
* @param {string} message An error message
* @param {string} type The type/origin of the validation error
* @param {string} path The field that triggered the validation error
* @param {string} value The value that generated the error
* @param {Object} [inst] the DAO instance that caused the validation error
* @param {Object} [validatorKey] a validation "key", used for identification
* @param {string} [fnName] property name of the BUILT-IN validator function that caused the validation error (e.g. "in" or "len"), if applicable
* @param {string} [fnArgs] parameters used with the BUILT-IN validator function, if applicable
*/
constructor(message, type, path, value, inst, validatorKey, fnName, fnArgs) {
/**
* An error message
*
* @type {string} message
*/
this.message = message || '';
/**
* The type/origin of the validation error
*
* @type {string}
*/
this.type = null;
/**
* The field that triggered the validation error
*
* @type {string}
*/
this.path = path || null;
/**
* The value that generated the error
*
* @type {string}
*/
this.value = value !== undefined ? value : null;
this.origin = null;
/**
* The DAO instance that caused the validation error
*
* @type {Model}
*/
this.instance = inst || null;
/**
* A validation "key", used for identification
*
* @type {string}
*/
this.validatorKey = validatorKey || null;
/**
* Property name of the BUILT-IN validator function that caused the validation error (e.g. "in" or "len"), if applicable
*
* @type {string}
*/
this.validatorName = fnName || null;
/**
* Parameters used with the BUILT-IN validator function, if applicable
*
* @type {string}
*/
this.validatorArgs = fnArgs || [];
if (type) {
if (ValidationErrorItem.Origins[ type ]) {
this.origin = type;
} else {
const lowercaseType = `${type}`.toLowerCase().trim();
const realType = ValidationErrorItem.TypeStringMap[ lowercaseType ];
if (realType && ValidationErrorItem.Origins[ realType ]) {
this.origin = realType;
this.type = type;
}
}
}
// This doesn't need captureStackTrace because it's not a subclass of Error
}
/**
* return a lowercase, trimmed string "key" that identifies the validator.
*
* Note: the string will be empty if the instance has neither a valid `validatorKey` property nor a valid `validatorName` property
*
* @param {boolean} [useTypeAsNS=true] controls whether the returned value is "namespace",
* this parameter is ignored if the validator's `type` is not one of ValidationErrorItem.Origins
* @param {string} [NSSeparator='.'] a separator string for concatenating the namespace, must be not be empty,
* defaults to "." (fullstop). only used and validated if useTypeAsNS is TRUE.
* @throws {Error} thrown if NSSeparator is found to be invalid.
* @returns {string}
*
* @private
*/
getValidatorKey(useTypeAsNS, NSSeparator) {
const useTANS = useTypeAsNS === undefined || !!useTypeAsNS;
const NSSep = NSSeparator === undefined ? '.' : NSSeparator;
const type = this.origin;
const key = this.validatorKey || this.validatorName;
const useNS = useTANS && type && ValidationErrorItem.Origins[ type ];
if (useNS && (typeof NSSep !== 'string' || !NSSep.length)) {
throw new Error('Invalid namespace separator given, must be a non-empty string');
}
if (!(typeof key === 'string' && key.length)) {
return '';
}
return (useNS ? [type, key].join(NSSep) : key).toLowerCase().trim();
}
}
/**
* An enum that defines valid ValidationErrorItem `origin` values
*
* @type {Object}
* @property CORE {string} specifies errors that originate from the sequelize "core"
* @property DB {string} specifies validation errors that originate from the storage engine
* @property FUNCTION {string} specifies validation errors that originate from validator functions (both built-in and custom) defined for a given attribute
*/
ValidationErrorItem.Origins = {
CORE: 'CORE',
DB: 'DB',
FUNCTION: 'FUNCTION'
};
/**
* An object that is used internally by the `ValidationErrorItem` class
* that maps current `type` strings (as given to ValidationErrorItem.constructor()) to
* our new `origin` values.
*
* @type {Object}
*/
ValidationErrorItem.TypeStringMap = {
'notnull violation': 'CORE',
'string violation': 'CORE',
'unique violation': 'DB',
'validation error': 'FUNCTION'
};
module.exports = ValidationError;
module.exports.ValidationErrorItem = ValidationErrorItem;
@@ -0,0 +1,26 @@
'use strict';
const ValidationError = require('./../validation-error');
/**
* Thrown when a unique constraint is violated in the database
*/
class UniqueConstraintError extends ValidationError {
constructor(options) {
options = options || {};
options.parent = options.parent || { sql: '' };
options.message = options.message || options.parent.message || 'Validation Error';
options.errors = options.errors || {};
super(options.message, options.errors);
this.name = 'SequelizeUniqueConstraintError';
this.errors = options.errors;
this.fields = options.fields;
this.parent = options.parent;
this.original = options.parent;
this.sql = options.parent.sql;
Error.captureStackTrace(this, this.constructor);
}
}
module.exports = UniqueConstraintError;
+557
View File
@@ -0,0 +1,557 @@
'use strict';
const _ = require('lodash');
const { logger } = require('./utils/logger');
const Promise = require('./promise');
const debug = logger.debugContext('hooks');
const hookTypes = {
beforeValidate: { params: 2 },
afterValidate: { params: 2 },
validationFailed: { params: 3 },
beforeCreate: { params: 2 },
afterCreate: { params: 2 },
beforeDestroy: { params: 2 },
afterDestroy: { params: 2 },
beforeRestore: { params: 2 },
afterRestore: { params: 2 },
beforeUpdate: { params: 2 },
afterUpdate: { params: 2 },
beforeSave: { params: 2, proxies: ['beforeUpdate', 'beforeCreate'] },
afterSave: { params: 2, proxies: ['afterUpdate', 'afterCreate'] },
beforeUpsert: { params: 2 },
afterUpsert: { params: 2 },
beforeBulkCreate: { params: 2 },
afterBulkCreate: { params: 2 },
beforeBulkDestroy: { params: 1 },
afterBulkDestroy: { params: 1 },
beforeBulkRestore: { params: 1 },
afterBulkRestore: { params: 1 },
beforeBulkUpdate: { params: 1 },
afterBulkUpdate: { params: 1 },
beforeFind: { params: 1 },
beforeFindAfterExpandIncludeAll: { params: 1 },
beforeFindAfterOptions: { params: 1 },
afterFind: { params: 2 },
beforeCount: { params: 1 },
beforeDefine: { params: 2, sync: true, noModel: true },
afterDefine: { params: 1, sync: true, noModel: true },
beforeInit: { params: 2, sync: true, noModel: true },
afterInit: { params: 1, sync: true, noModel: true },
beforeAssociate: { params: 2, sync: true },
afterAssociate: { params: 2, sync: true },
beforeConnect: { params: 1, noModel: true },
afterConnect: { params: 2, noModel: true },
beforeDisconnect: { params: 1, noModel: true },
afterDisconnect: { params: 1, noModel: true },
beforeSync: { params: 1 },
afterSync: { params: 1 },
beforeBulkSync: { params: 1 },
afterBulkSync: { params: 1 },
beforeQuery: { params: 2 },
afterQuery: { params: 2 }
};
exports.hooks = hookTypes;
/**
* get array of current hook and its proxies combined
*
* @param {string} hookType any hook type @see {@link hookTypes}
*
* @private
*/
const getProxiedHooks = hookType =>
hookTypes[hookType].proxies
? hookTypes[hookType].proxies.concat(hookType)
: [hookType]
;
function getHooks(hooked, hookType) {
return (hooked.options.hooks || {})[hookType] || [];
}
const Hooks = {
/**
* Process user supplied hooks definition
*
* @param {Object} hooks hooks definition
*
* @private
* @memberof Sequelize
* @memberof Sequelize.Model
*/
_setupHooks(hooks) {
this.options.hooks = {};
_.map(hooks || {}, (hooksArray, hookName) => {
if (!Array.isArray(hooksArray)) hooksArray = [hooksArray];
hooksArray.forEach(hookFn => this.addHook(hookName, hookFn));
});
},
runHooks(hooks, ...hookArgs) {
if (!hooks) throw new Error('runHooks requires at least 1 argument');
let hookType;
if (typeof hooks === 'string') {
hookType = hooks;
hooks = getHooks(this, hookType);
if (this.sequelize) {
hooks = hooks.concat(getHooks(this.sequelize, hookType));
}
}
if (!Array.isArray(hooks)) {
hooks = [hooks];
}
// synchronous hooks
if (hookTypes[hookType] && hookTypes[hookType].sync) {
for (let hook of hooks) {
if (typeof hook === 'object') {
hook = hook.fn;
}
debug(`running hook(sync) ${hookType}`);
hook.apply(this, hookArgs);
}
return;
}
// asynchronous hooks (default)
return Promise.each(hooks, hook => {
if (typeof hook === 'object') {
hook = hook.fn;
}
debug(`running hook ${hookType}`);
return hook.apply(this, hookArgs);
}).return();
},
/**
* Add a hook to the model
*
* @param {string} hookType hook name @see {@link hookTypes}
* @param {string|Function} [name] Provide a name for the hook function. It can be used to remove the hook later or to order hooks based on some sort of priority system in the future.
* @param {Function} fn The hook function
*
* @memberof Sequelize
* @memberof Sequelize.Model
*/
addHook(hookType, name, fn) {
if (typeof name === 'function') {
fn = name;
name = null;
}
debug(`adding hook ${hookType}`);
// check for proxies, add them too
hookType = getProxiedHooks(hookType);
hookType.forEach(type => {
const hooks = getHooks(this, type);
hooks.push(name ? { name, fn } : fn);
this.options.hooks[type] = hooks;
});
return this;
},
/**
* Remove hook from the model
*
* @param {string} hookType @see {@link hookTypes}
* @param {string|Function} name name of hook or function reference which was attached
*
* @memberof Sequelize
* @memberof Sequelize.Model
*/
removeHook(hookType, name) {
const isReference = typeof name === 'function' ? true : false;
if (!this.hasHook(hookType)) {
return this;
}
debug(`removing hook ${hookType}`);
// check for proxies, add them too
hookType = getProxiedHooks(hookType);
for (const type of hookType) {
this.options.hooks[type] = this.options.hooks[type].filter(hook => {
if (isReference && typeof hook === 'function') {
return hook !== name; // check if same method
}
if (!isReference && typeof hook === 'object') {
return hook.name !== name;
}
return true;
});
}
return this;
},
/**
* Check whether the mode has any hooks of this type
*
* @param {string} hookType @see {@link hookTypes}
*
* @alias hasHooks
*
* @memberof Sequelize
* @memberof Sequelize.Model
*/
hasHook(hookType) {
return this.options.hooks[hookType] && !!this.options.hooks[hookType].length;
}
};
Hooks.hasHooks = Hooks.hasHook;
function applyTo(target, isModel = false) {
_.mixin(target, Hooks);
for (const hook of Object.keys(hookTypes)) {
if (isModel && hookTypes[hook].noModel) {
continue;
}
target[hook] = function(name, callback) {
return this.addHook(hook, name, callback);
};
}
}
exports.applyTo = applyTo;
/**
* A hook that is run before validation
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
* @name beforeValidate
* @memberof Sequelize.Model
*/
/**
* A hook that is run after validation
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
* @name afterValidate
* @memberof Sequelize.Model
*/
/**
* A hook that is run when validation fails
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options, error. Error is the
* SequelizeValidationError. If the callback throws an error, it will replace the original validation error.
* @name validationFailed
* @memberof Sequelize.Model
*/
/**
* A hook that is run before creating a single instance
* @param {string} name
* @param {Function} fn A callback function that is called with attributes, options
* @name beforeCreate
* @memberof Sequelize.Model
*/
/**
* A hook that is run after creating a single instance
* @param {string} name
* @param {Function} fn A callback function that is called with attributes, options
* @name afterCreate
* @memberof Sequelize.Model
*/
/**
* A hook that is run before creating or updating a single instance, It proxies `beforeCreate` and `beforeUpdate`
* @param {string} name
* @param {Function} fn A callback function that is called with attributes, options
* @name beforeSave
* @memberof Sequelize.Model
*/
/**
* A hook that is run before upserting
* @param {string} name
* @param {Function} fn A callback function that is called with attributes, options
* @name beforeUpsert
* @memberof Sequelize.Model
*/
/**
* A hook that is run after upserting
* @param {string} name
* @param {Function} fn A callback function that is called with the result of upsert(), options
* @name afterUpsert
* @memberof Sequelize.Model
*/
/**
* A hook that is run after creating or updating a single instance, It proxies `afterCreate` and `afterUpdate`
* @param {string} name
* @param {Function} fn A callback function that is called with attributes, options
* @name afterSave
* @memberof Sequelize.Model
*/
/**
* A hook that is run before destroying a single instance
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
*
* @name beforeDestroy
* @memberof Sequelize.Model
*/
/**
* A hook that is run after destroying a single instance
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
*
* @name afterDestroy
* @memberof Sequelize.Model
*/
/**
* A hook that is run before restoring a single instance
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
*
* @name beforeRestore
* @memberof Sequelize.Model
*/
/**
* A hook that is run after restoring a single instance
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
*
* @name afterRestore
* @memberof Sequelize.Model
*/
/**
* A hook that is run before updating a single instance
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
* @name beforeUpdate
* @memberof Sequelize.Model
*/
/**
* A hook that is run after updating a single instance
* @param {string} name
* @param {Function} fn A callback function that is called with instance, options
* @name afterUpdate
* @memberof Sequelize.Model
*/
/**
* A hook that is run before creating instances in bulk
* @param {string} name
* @param {Function} fn A callback function that is called with instances, options
* @name beforeBulkCreate
* @memberof Sequelize.Model
*/
/**
* A hook that is run after creating instances in bulk
* @param {string} name
* @param {Function} fn A callback function that is called with instances, options
* @name afterBulkCreate
* @memberof Sequelize.Model
*/
/**
* A hook that is run before destroying instances in bulk
* @param {string} name
* @param {Function} fn A callback function that is called with options
*
* @name beforeBulkDestroy
* @memberof Sequelize.Model
*/
/**
* A hook that is run after destroying instances in bulk
* @param {string} name
* @param {Function} fn A callback function that is called with options
*
* @name afterBulkDestroy
* @memberof Sequelize.Model
*/
/**
* A hook that is run before restoring instances in bulk
* @param {string} name
* @param {Function} fn A callback function that is called with options
*
* @name beforeBulkRestore
* @memberof Sequelize.Model
*/
/**
* A hook that is run after restoring instances in bulk
* @param {string} name
* @param {Function} fn A callback function that is called with options
*
* @name afterBulkRestore
* @memberof Sequelize.Model
*/
/**
* A hook that is run before updating instances in bulk
* @param {string} name
* @param {Function} fn A callback function that is called with options
* @name beforeBulkUpdate
* @memberof Sequelize.Model
*/
/**
* A hook that is run after updating instances in bulk
* @param {string} name
* @param {Function} fn A callback function that is called with options
* @name afterBulkUpdate
* @memberof Sequelize.Model
*/
/**
* A hook that is run before a find (select) query
* @param {string} name
* @param {Function} fn A callback function that is called with options
* @name beforeFind
* @memberof Sequelize.Model
*/
/**
* A hook that is run before a find (select) query, after any { include: {all: ...} } options are expanded
* @param {string} name
* @param {Function} fn A callback function that is called with options
* @name beforeFindAfterExpandIncludeAll
* @memberof Sequelize.Model
*/
/**
* A hook that is run before a find (select) query, after all option parsing is complete
* @param {string} name
* @param {Function} fn A callback function that is called with options
* @name beforeFindAfterOptions
* @memberof Sequelize.Model
*/
/**
* A hook that is run after a find (select) query
* @param {string} name
* @param {Function} fn A callback function that is called with instance(s), options
* @name afterFind
* @memberof Sequelize.Model
*/
/**
* A hook that is run before a count query
* @param {string} name
* @param {Function} fn A callback function that is called with options
* @name beforeCount
* @memberof Sequelize.Model
*/
/**
* A hook that is run before a define call
* @param {string} name
* @param {Function} fn A callback function that is called with attributes, options
* @name beforeDefine
* @memberof Sequelize
*/
/**
* A hook that is run after a define call
* @param {string} name
* @param {Function} fn A callback function that is called with factory
* @name afterDefine
* @memberof Sequelize
*/
/**
* A hook that is run before Sequelize() call
* @param {string} name
* @param {Function} fn A callback function that is called with config, options
* @name beforeInit
* @memberof Sequelize
*/
/**
* A hook that is run after Sequelize() call
* @param {string} name
* @param {Function} fn A callback function that is called with sequelize
* @name afterInit
* @memberof Sequelize
*/
/**
* A hook that is run before a connection is created
* @param {string} name
* @param {Function} fn A callback function that is called with config passed to connection
* @name beforeConnect
* @memberof Sequelize
*/
/**
* A hook that is run after a connection is created
* @param {string} name
* @param {Function} fn A callback function that is called with the connection object and the config passed to connection
* @name afterConnect
* @memberof Sequelize
*/
/**
* A hook that is run before a connection is disconnected
* @param {string} name
* @param {Function} fn A callback function that is called with the connection object
* @name beforeDisconnect
* @memberof Sequelize
*/
/**
* A hook that is run after a connection is disconnected
* @param {string} name
* @param {Function} fn A callback function that is called with the connection object
* @name afterDisconnect
* @memberof Sequelize
*/
/**
* A hook that is run before Model.sync call
* @param {string} name
* @param {Function} fn A callback function that is called with options passed to Model.sync
* @name beforeSync
* @memberof Sequelize
*/
/**
* A hook that is run after Model.sync call
* @param {string} name
* @param {Function} fn A callback function that is called with options passed to Model.sync
* @name afterSync
* @memberof Sequelize
*/
/**
* A hook that is run before sequelize.sync call
* @param {string} name
* @param {Function} fn A callback function that is called with options passed to sequelize.sync
* @name beforeBulkSync
* @memberof Sequelize
*/
/**
* A hook that is run after sequelize.sync call
* @param {string} name
* @param {Function} fn A callback function that is called with options passed to sequelize.sync
* @name afterBulkSync
* @memberof Sequelize
*/
+14
View File
@@ -0,0 +1,14 @@
'use strict';
/**
* An enum of index hints to be used in mysql for querying with index hints
*
* @property USE
* @property FORCE
* @property IGNORE
*/
const IndexHints = module.exports = { // eslint-disable-line
USE: 'USE',
FORCE: 'FORCE',
IGNORE: 'IGNORE'
};
+431
View File
@@ -0,0 +1,431 @@
'use strict';
const _ = require('lodash');
const Utils = require('./utils');
const sequelizeError = require('./errors');
const Promise = require('./promise');
const DataTypes = require('./data-types');
const BelongsTo = require('./associations/belongs-to');
const validator = require('./utils/validator-extras').validator;
/**
* Instance Validator.
*
* @param {Instance} modelInstance The model instance.
* @param {Object} options A dictionary with options.
*
* @private
*/
class InstanceValidator {
constructor(modelInstance, options) {
options = _.clone(options) || {};
if (options.fields && !options.skip) {
options.skip = _.difference(Object.keys(modelInstance.constructor.rawAttributes), options.fields);
}
// assign defined and default options
this.options = _.defaults(options, {
skip: [],
hooks: true
});
this.modelInstance = modelInstance;
/**
* Exposes a reference to validator.js. This allows you to add custom validations using `validator.extend`
* @name validator
* @private
*/
this.validator = validator;
/**
* All errors will be stored here from the validations.
*
* @type {Array} Will contain keys that correspond to attributes which will
* be Arrays of Errors.
* @private
*/
this.errors = [];
/**
* @type {boolean} Indicates if validations are in progress
* @private
*/
this.inProgress = false;
}
/**
* The main entry point for the Validation module, invoke to start the dance.
*
* @returns {Promise}
* @private
*/
_validate() {
if (this.inProgress) throw new Error('Validations already in progress.');
this.inProgress = true;
return Promise.all([
this._perAttributeValidators().reflect(),
this._customValidators().reflect()
]).then(() => {
if (this.errors.length) {
throw new sequelizeError.ValidationError(null, this.errors);
}
});
}
/**
* Invoke the Validation sequence and run validation hooks if defined
* - Before Validation Model Hooks
* - Validation
* - On validation success: After Validation Model Hooks
* - On validation failure: Validation Failed Model Hooks
*
* @returns {Promise}
* @private
*/
validate() {
return this.options.hooks ? this._validateAndRunHooks() : this._validate();
}
/**
* Invoke the Validation sequence and run hooks
* - Before Validation Model Hooks
* - Validation
* - On validation success: After Validation Model Hooks
* - On validation failure: Validation Failed Model Hooks
*
* @returns {Promise}
* @private
*/
_validateAndRunHooks() {
const runHooks = this.modelInstance.constructor.runHooks.bind(this.modelInstance.constructor);
return runHooks('beforeValidate', this.modelInstance, this.options)
.then(() =>
this._validate()
.catch(error => runHooks('validationFailed', this.modelInstance, this.options, error)
.then(newError => { throw newError || error; }))
)
.then(() => runHooks('afterValidate', this.modelInstance, this.options))
.return(this.modelInstance);
}
/**
* Will run all the validators defined per attribute (built-in validators and custom validators)
*
* @returns {Promise<Array.<Promise.PromiseInspection>>} A promise from .reflect().
* @private
*/
_perAttributeValidators() {
// promisify all attribute invocations
const validators = [];
_.forIn(this.modelInstance.rawAttributes, (rawAttribute, field) => {
if (this.options.skip.includes(field)) {
return;
}
const value = this.modelInstance.dataValues[field];
if (value instanceof Utils.SequelizeMethod) {
return;
}
if (!rawAttribute._autoGenerated && !rawAttribute.autoIncrement) {
// perform validations based on schema
this._validateSchema(rawAttribute, field, value);
}
if (Object.prototype.hasOwnProperty.call(this.modelInstance.validators, field)) {
validators.push(this._singleAttrValidate(value, field, rawAttribute.allowNull).reflect());
}
});
return Promise.all(validators);
}
/**
* Will run all the custom validators defined in the model's options.
*
* @returns {Promise<Array.<Promise.PromiseInspection>>} A promise from .reflect().
* @private
*/
_customValidators() {
const validators = [];
_.each(this.modelInstance._modelOptions.validate, (validator, validatorType) => {
if (this.options.skip.includes(validatorType)) {
return;
}
const valprom = this._invokeCustomValidator(validator, validatorType)
// errors are handled in settling, stub this
.catch(() => {})
.reflect();
validators.push(valprom);
});
return Promise.all(validators);
}
/**
* Validate a single attribute with all the defined built-in validators and custom validators.
*
* @private
*
* @param {*} value Anything.
* @param {string} field The field name.
* @param {boolean} allowNull Whether or not the schema allows null values
*
* @returns {Promise} A promise, will always resolve, auto populates error on this.error local object.
*/
_singleAttrValidate(value, field, allowNull) {
// If value is null and allowNull is false, no validators should run (see #9143)
if ((value === null || value === undefined) && !allowNull) {
// The schema validator (_validateSchema) has already generated the validation error. Nothing to do here.
return Promise.resolve();
}
// Promisify each validator
const validators = [];
_.forIn(this.modelInstance.validators[field], (test, validatorType) => {
if (validatorType === 'isUrl' || validatorType === 'isURL' || validatorType === 'isEmail') {
// Preserve backwards compat. Validator.js now expects the second param to isURL and isEmail to be an object
if (typeof test === 'object' && test !== null && test.msg) {
test = {
msg: test.msg
};
} else if (test === true) {
test = {};
}
}
// Custom validators should always run, except if value is null and allowNull is false (see #9143)
if (typeof test === 'function') {
validators.push(this._invokeCustomValidator(test, validatorType, true, value, field).reflect());
return;
}
// If value is null, built-in validators should not run (only custom validators have to run) (see #9134).
if (value === null || value === undefined) {
return;
}
const validatorPromise = this._invokeBuiltinValidator(value, test, validatorType, field);
// errors are handled in settling, stub this
validatorPromise.catch(() => {});
validators.push(validatorPromise.reflect());
});
return Promise
.all(validators)
.then(results => this._handleReflectedResult(field, value, results));
}
/**
* Prepare and invoke a custom validator.
*
* @private
*
* @param {Function} validator The custom validator.
* @param {string} validatorType the custom validator type (name).
* @param {boolean} optAttrDefined Set to true if custom validator was defined from the attribute
* @param {*} optValue value for attribute
* @param {string} optField field for attribute
*
* @returns {Promise} A promise.
*/
_invokeCustomValidator(validator, validatorType, optAttrDefined, optValue, optField) {
let validatorFunction = null; // the validation function to call
let isAsync = false;
const validatorArity = validator.length;
// check if validator is async and requires a callback
let asyncArity = 1;
let errorKey = validatorType;
let invokeArgs;
if (optAttrDefined) {
asyncArity = 2;
invokeArgs = optValue;
errorKey = optField;
}
if (validatorArity === asyncArity) {
isAsync = true;
}
if (isAsync) {
if (optAttrDefined) {
validatorFunction = Promise.promisify(validator.bind(this.modelInstance, invokeArgs));
} else {
validatorFunction = Promise.promisify(validator.bind(this.modelInstance));
}
return validatorFunction()
.catch(e => this._pushError(false, errorKey, e, optValue, validatorType));
}
return Promise
.try(() => validator.call(this.modelInstance, invokeArgs))
.catch(e => this._pushError(false, errorKey, e, optValue, validatorType));
}
/**
* Prepare and invoke a build-in validator.
*
* @private
*
* @param {*} value Anything.
* @param {*} test The test case.
* @param {string} validatorType One of known to Sequelize validators.
* @param {string} field The field that is being validated
*
* @returns {Object} An object with specific keys to invoke the validator.
*/
_invokeBuiltinValidator(value, test, validatorType, field) {
return Promise.try(() => {
// Cast value as string to pass new Validator.js string requirement
const valueString = String(value);
// check if Validator knows that kind of validation test
if (typeof validator[validatorType] !== 'function') {
throw new Error(`Invalid validator function: ${validatorType}`);
}
const validatorArgs = this._extractValidatorArgs(test, validatorType, field);
if (!validator[validatorType](valueString, ...validatorArgs)) {
throw Object.assign(new Error(test.msg || `Validation ${validatorType} on ${field} failed`), { validatorName: validatorType, validatorArgs });
}
});
}
/**
* Will extract arguments for the validator.
*
* @param {*} test The test case.
* @param {string} validatorType One of known to Sequelize validators.
* @param {string} field The field that is being validated.
*
* @private
*/
_extractValidatorArgs(test, validatorType, field) {
let validatorArgs = test.args || test;
const isLocalizedValidator = typeof validatorArgs !== 'string' && (validatorType === 'isAlpha' || validatorType === 'isAlphanumeric' || validatorType === 'isMobilePhone');
if (!Array.isArray(validatorArgs)) {
if (validatorType === 'isImmutable') {
validatorArgs = [validatorArgs, field, this.modelInstance];
} else if (isLocalizedValidator || validatorType === 'isIP') {
validatorArgs = [];
} else {
validatorArgs = [validatorArgs];
}
} else {
validatorArgs = validatorArgs.slice(0);
}
return validatorArgs;
}
/**
* Will validate a single field against its schema definition (isnull).
*
* @param {Object} rawAttribute As defined in the Schema.
* @param {string} field The field name.
* @param {*} value anything.
*
* @private
*/
_validateSchema(rawAttribute, field, value) {
if (rawAttribute.allowNull === false && (value === null || value === undefined)) {
const association = _.values(this.modelInstance.constructor.associations).find(association => association instanceof BelongsTo && association.foreignKey === rawAttribute.fieldName);
if (!association || !this.modelInstance.get(association.associationAccessor)) {
const validators = this.modelInstance.validators[field];
const errMsg = _.get(validators, 'notNull.msg', `${this.modelInstance.constructor.name}.${field} cannot be null`);
this.errors.push(new sequelizeError.ValidationErrorItem(
errMsg,
'notNull Violation', // sequelizeError.ValidationErrorItem.Origins.CORE,
field,
value,
this.modelInstance,
'is_null'
));
}
}
if (rawAttribute.type instanceof DataTypes.STRING || rawAttribute.type instanceof DataTypes.TEXT || rawAttribute.type instanceof DataTypes.CITEXT) {
if (Array.isArray(value) || _.isObject(value) && !(value instanceof Utils.SequelizeMethod) && !Buffer.isBuffer(value)) {
this.errors.push(new sequelizeError.ValidationErrorItem(
`${field} cannot be an array or an object`,
'string violation', // sequelizeError.ValidationErrorItem.Origins.CORE,
field,
value,
this.modelInstance,
'not_a_string'
));
}
}
}
/**
* Handles the returned result of a Promise.reflect.
*
* If errors are found it populates this.error.
*
* @param {string} field The attribute name.
* @param {string|number} value The data value.
* @param {Array<Promise.PromiseInspection>} promiseInspections objects.
*
* @private
*/
_handleReflectedResult(field, value, promiseInspections) {
for (const promiseInspection of promiseInspections) {
if (promiseInspection.isRejected()) {
const rejection = promiseInspection.error();
const isBuiltIn = !!rejection.validatorName;
this._pushError(isBuiltIn, field, rejection, value, rejection.validatorName, rejection.validatorArgs);
}
}
}
/**
* Signs all errors retaining the original.
*
* @param {boolean} isBuiltin - Determines if error is from builtin validator.
* @param {string} errorKey - name of invalid attribute.
* @param {Error|string} rawError - The original error.
* @param {string|number} value - The data that triggered the error.
* @param {string} fnName - Name of the validator, if any
* @param {Array} fnArgs - Arguments for the validator [function], if any
*
* @private
*/
_pushError(isBuiltin, errorKey, rawError, value, fnName, fnArgs) {
const message = rawError.message || rawError || 'Validation error';
const error = new sequelizeError.ValidationErrorItem(
message,
'Validation error', // sequelizeError.ValidationErrorItem.Origins.FUNCTION,
errorKey,
value,
this.modelInstance,
fnName,
isBuiltin ? fnName : undefined,
isBuiltin ? fnArgs : undefined
);
error[InstanceValidator.RAW_KEY_NAME] = rawError;
this.errors.push(error);
}
}
/**
* @define {string} The error key for arguments as passed by custom validators
* @private
*/
InstanceValidator.RAW_KEY_NAME = 'original';
module.exports = InstanceValidator;
module.exports.InstanceValidator = InstanceValidator;
module.exports.default = InstanceValidator;
+98
View File
@@ -0,0 +1,98 @@
'use strict';
const Toposort = require('toposort-class');
const _ = require('lodash');
class ModelManager {
constructor(sequelize) {
this.models = [];
this.sequelize = sequelize;
}
addModel(model) {
this.models.push(model);
this.sequelize.models[model.name] = model;
return model;
}
removeModel(modelToRemove) {
this.models = this.models.filter(model => model.name !== modelToRemove.name);
delete this.sequelize.models[modelToRemove.name];
}
getModel(against, options) {
options = _.defaults(options || {}, {
attribute: 'name'
});
return this.models.find(model => model[options.attribute] === against);
}
get all() {
return this.models;
}
/**
* Iterate over Models in an order suitable for e.g. creating tables.
* Will take foreign key constraints into account so that dependencies are visited before dependents.
*
* @param {Function} iterator method to execute on each model
* @param {Object} [options] iterator options
* @private
*/
forEachModel(iterator, options) {
const models = {};
const sorter = new Toposort();
let sorted;
let dep;
options = _.defaults(options || {}, {
reverse: true
});
for (const model of this.models) {
let deps = [];
let tableName = model.getTableName();
if (_.isObject(tableName)) {
tableName = `${tableName.schema}.${tableName.tableName}`;
}
models[tableName] = model;
for (const attrName in model.rawAttributes) {
if (Object.prototype.hasOwnProperty.call(model.rawAttributes, attrName)) {
const attribute = model.rawAttributes[attrName];
if (attribute.references) {
dep = attribute.references.model;
if (_.isObject(dep)) {
dep = `${dep.schema}.${dep.tableName}`;
}
deps.push(dep);
}
}
}
deps = deps.filter(dep => tableName !== dep);
sorter.add(tableName, deps);
}
sorted = sorter.sort();
if (options.reverse) {
sorted = sorted.reverse();
}
for (const name of sorted) {
iterator(models[name], name);
}
}
}
module.exports = ModelManager;
module.exports.ModelManager = ModelManager;
module.exports.default = ModelManager;
+4519
View File
File diff suppressed because it is too large Load Diff
+90
View File
@@ -0,0 +1,90 @@
'use strict';
/**
* Operator symbols to be used when querying data
*
* @see {@link Model#where}
*
* @property eq
* @property ne
* @property gte
* @property gt
* @property lte
* @property lt
* @property not
* @property is
* @property in
* @property notIn
* @property like
* @property notLike
* @property iLike
* @property notILike
* @property startsWith
* @property endsWith
* @property substring
* @property regexp
* @property notRegexp
* @property iRegexp
* @property notIRegexp
* @property between
* @property notBetween
* @property overlap
* @property contains
* @property contained
* @property adjacent
* @property strictLeft
* @property strictRight
* @property noExtendRight
* @property noExtendLeft
* @property and
* @property or
* @property any
* @property all
* @property values
* @property col
* @property placeholder
* @property join
*/
const Op = {
eq: Symbol.for('eq'),
ne: Symbol.for('ne'),
gte: Symbol.for('gte'),
gt: Symbol.for('gt'),
lte: Symbol.for('lte'),
lt: Symbol.for('lt'),
not: Symbol.for('not'),
is: Symbol.for('is'),
in: Symbol.for('in'),
notIn: Symbol.for('notIn'),
like: Symbol.for('like'),
notLike: Symbol.for('notLike'),
iLike: Symbol.for('iLike'),
notILike: Symbol.for('notILike'),
startsWith: Symbol.for('startsWith'),
endsWith: Symbol.for('endsWith'),
substring: Symbol.for('substring'),
regexp: Symbol.for('regexp'),
notRegexp: Symbol.for('notRegexp'),
iRegexp: Symbol.for('iRegexp'),
notIRegexp: Symbol.for('notIRegexp'),
between: Symbol.for('between'),
notBetween: Symbol.for('notBetween'),
overlap: Symbol.for('overlap'),
contains: Symbol.for('contains'),
contained: Symbol.for('contained'),
adjacent: Symbol.for('adjacent'),
strictLeft: Symbol.for('strictLeft'),
strictRight: Symbol.for('strictRight'),
noExtendRight: Symbol.for('noExtendRight'),
noExtendLeft: Symbol.for('noExtendLeft'),
and: Symbol.for('and'),
or: Symbol.for('or'),
any: Symbol.for('any'),
all: Symbol.for('all'),
values: Symbol.for('values'),
col: Symbol.for('col'),
placeholder: Symbol.for('placeholder'),
join: Symbol.for('join')
};
module.exports = Op;
+7
View File
@@ -0,0 +1,7 @@
'use strict';
const Promise = require('bluebird').getNewLibraryCopy();
module.exports = Promise;
module.exports.Promise = Promise;
module.exports.default = Promise;
+1464
View File
File diff suppressed because it is too large Load Diff
+38
View File
@@ -0,0 +1,38 @@
'use strict';
/**
* An enum of query types used by `sequelize.query`
*
* @see {@link Sequelize#query}
*
* @property SELECT
* @property INSERT
* @property UPDATE
* @property BULKUPDATE
* @property BULKDELETE
* @property DELETE
* @property UPSERT
* @property VERSION
* @property SHOWTABLES
* @property SHOWINDEXES
* @property DESCRIBE
* @property RAW
* @property FOREIGNKEYS
* @property SHOWCONSTRAINTS
*/
const QueryTypes = module.exports = { // eslint-disable-line
SELECT: 'SELECT',
INSERT: 'INSERT',
UPDATE: 'UPDATE',
BULKUPDATE: 'BULKUPDATE',
BULKDELETE: 'BULKDELETE',
DELETE: 'DELETE',
UPSERT: 'UPSERT',
VERSION: 'VERSION',
SHOWTABLES: 'SHOWTABLES',
SHOWINDEXES: 'SHOWINDEXES',
DESCRIBE: 'DESCRIBE',
RAW: 'RAW',
FOREIGNKEYS: 'FOREIGNKEYS',
SHOWCONSTRAINTS: 'SHOWCONSTRAINTS'
};
Generated Vendored Executable
+1376
View File
File diff suppressed because it is too large Load Diff
+124
View File
@@ -0,0 +1,124 @@
'use strict';
const dataTypes = require('./data-types');
const { logger } = require('./utils/logger');
function arrayToList(array, timeZone, dialect, format) {
return array.reduce((sql, val, i) => {
if (i !== 0) {
sql += ', ';
}
if (Array.isArray(val)) {
sql += `(${arrayToList(val, timeZone, dialect, format)})`;
} else {
sql += escape(val, timeZone, dialect, format);
}
return sql;
}, '');
}
exports.arrayToList = arrayToList;
function escape(val, timeZone, dialect, format) {
let prependN = false;
if (val === undefined || val === null) {
return 'NULL';
}
switch (typeof val) {
case 'boolean':
// SQLite doesn't have true/false support. MySQL aliases true/false to 1/0
// for us. Postgres actually has a boolean type with true/false literals,
// but sequelize doesn't use it yet.
if (dialect === 'sqlite' || dialect === 'mssql') {
return +!!val;
}
return (!!val).toString();
case 'number':
return val.toString();
case 'string':
// In mssql, prepend N to all quoted vals which are originally a string (for
// unicode compatibility)
prependN = dialect === 'mssql';
break;
}
if (val instanceof Date) {
val = dataTypes[dialect].DATE.prototype.stringify(val, { timezone: timeZone });
}
if (Buffer.isBuffer(val)) {
if (dataTypes[dialect].BLOB) {
return dataTypes[dialect].BLOB.prototype.stringify(val);
}
return dataTypes.BLOB.prototype.stringify(val);
}
if (Array.isArray(val)) {
const partialEscape = escVal => escape(escVal, timeZone, dialect, format);
if (dialect === 'postgres' && !format) {
return dataTypes.ARRAY.prototype.stringify(val, { escape: partialEscape });
}
return arrayToList(val, timeZone, dialect, format);
}
if (!val.replace) {
throw new Error(`Invalid value ${logger.inspect(val)}`);
}
if (dialect === 'postgres' || dialect === 'sqlite' || dialect === 'mssql') {
// http://www.postgresql.org/docs/8.2/static/sql-syntax-lexical.html#SQL-SYNTAX-STRINGS
// http://stackoverflow.com/q/603572/130598
val = val.replace(/'/g, "''");
if (dialect === 'postgres') {
// null character is not allowed in Postgres
val = val.replace(/\0/g, '\\0');
}
} else {
// eslint-disable-next-line no-control-regex
val = val.replace(/[\0\n\r\b\t\\'"\x1a]/g, s => {
switch (s) {
case '\0': return '\\0';
case '\n': return '\\n';
case '\r': return '\\r';
case '\b': return '\\b';
case '\t': return '\\t';
case '\x1a': return '\\Z';
default: return `\\${s}`;
}
});
}
return `${(prependN ? "N'" : "'") + val}'`;
}
exports.escape = escape;
function format(sql, values, timeZone, dialect) {
values = [].concat(values);
if (typeof sql !== 'string') {
throw new Error(`Invalid SQL string provided: ${sql}`);
}
return sql.replace(/\?/g, match => {
if (!values.length) {
return match;
}
return escape(values.shift(), timeZone, dialect, true);
});
}
exports.format = format;
function formatNamedParameters(sql, values, timeZone, dialect) {
return sql.replace(/:+(?!\d)(\w+)/g, (value, key) => {
if ('postgres' === dialect && '::' === value.slice(0, 2)) {
return value;
}
if (values[key] !== undefined) {
return escape(values[key], timeZone, dialect, true);
}
throw new Error(`Named parameter "${value}" has no value in the given object.`);
});
}
exports.formatNamedParameters = formatNamedParameters;
+38
View File
@@ -0,0 +1,38 @@
'use strict';
/**
* An enum of table hints to be used in mssql for querying with table hints
*
* @property NOLOCK
* @property READUNCOMMITTED
* @property UPDLOCK
* @property REPEATABLEREAD
* @property SERIALIZABLE
* @property READCOMMITTED
* @property TABLOCK
* @property TABLOCKX
* @property PAGLOCK
* @property ROWLOCK
* @property NOWAIT
* @property READPAST
* @property XLOCK
* @property SNAPSHOT
* @property NOEXPAND
*/
const TableHints = module.exports = { // eslint-disable-line
NOLOCK: 'NOLOCK',
READUNCOMMITTED: 'READUNCOMMITTED',
UPDLOCK: 'UPDLOCK',
REPEATABLEREAD: 'REPEATABLEREAD',
SERIALIZABLE: 'SERIALIZABLE',
READCOMMITTED: 'READCOMMITTED',
TABLOCK: 'TABLOCK',
TABLOCKX: 'TABLOCKX',
PAGLOCK: 'PAGLOCK',
ROWLOCK: 'ROWLOCK',
NOWAIT: 'NOWAIT',
READPAST: 'READPAST',
XLOCK: 'XLOCK',
SNAPSHOT: 'SNAPSHOT',
NOEXPAND: 'NOEXPAND'
};
+314
View File
@@ -0,0 +1,314 @@
'use strict';
const Promise = require('./promise');
/**
* The transaction object is used to identify a running transaction.
* It is created by calling `Sequelize.transaction()`.
* To run a query under a transaction, you should pass the transaction in the options object.
*
* @class Transaction
* @see {@link Sequelize.transaction}
*/
class Transaction {
/**
* Creates a new transaction instance
*
* @param {Sequelize} sequelize A configured sequelize Instance
* @param {Object} options An object with options
* @param {string} [options.type] Sets the type of the transaction. Sqlite only
* @param {string} [options.isolationLevel] Sets the isolation level of the transaction.
* @param {string} [options.deferrable] Sets the constraints to be deferred or immediately checked. PostgreSQL only
*/
constructor(sequelize, options) {
this.sequelize = sequelize;
this.savepoints = [];
this._afterCommitHooks = [];
// get dialect specific transaction options
const generateTransactionId = this.sequelize.dialect.QueryGenerator.generateTransactionId;
this.options = Object.assign({
type: sequelize.options.transactionType,
isolationLevel: sequelize.options.isolationLevel,
readOnly: false
}, options || {});
this.parent = this.options.transaction;
if (this.parent) {
this.id = this.parent.id;
this.parent.savepoints.push(this);
this.name = `${this.id}-sp-${this.parent.savepoints.length}`;
} else {
this.id = this.name = generateTransactionId();
}
delete this.options.transaction;
}
/**
* Commit the transaction
*
* @returns {Promise}
*/
commit() {
if (this.finished) {
return Promise.reject(new Error(`Transaction cannot be committed because it has been finished with state: ${this.finished}`));
}
this._clearCls();
return this
.sequelize
.getQueryInterface()
.commitTransaction(this, this.options)
.finally(() => {
this.finished = 'commit';
if (!this.parent) {
return this.cleanup();
}
return null;
}).tap(
() => Promise.each(
this._afterCommitHooks,
hook => Promise.resolve(hook.apply(this, [this])))
);
}
/**
* Rollback (abort) the transaction
*
* @returns {Promise}
*/
rollback() {
if (this.finished) {
return Promise.reject(new Error(`Transaction cannot be rolled back because it has been finished with state: ${this.finished}`));
}
if (!this.connection) {
return Promise.reject(new Error('Transaction cannot be rolled back because it never started'));
}
this._clearCls();
return this
.sequelize
.getQueryInterface()
.rollbackTransaction(this, this.options)
.finally(() => {
if (!this.parent) {
return this.cleanup();
}
return this;
});
}
prepareEnvironment(useCLS) {
let connectionPromise;
if (useCLS === undefined) {
useCLS = true;
}
if (this.parent) {
connectionPromise = Promise.resolve(this.parent.connection);
} else {
const acquireOptions = { uuid: this.id };
if (this.options.readOnly) {
acquireOptions.type = 'SELECT';
}
connectionPromise = this.sequelize.connectionManager.getConnection(acquireOptions);
}
return connectionPromise
.then(connection => {
this.connection = connection;
this.connection.uuid = this.id;
})
.then(() => {
return this.begin()
.then(() => this.setDeferrable())
.catch(setupErr => this.rollback().finally(() => {
throw setupErr;
}));
})
.tap(() => {
if (useCLS && this.sequelize.constructor._cls) {
this.sequelize.constructor._cls.set('transaction', this);
}
return null;
});
}
setDeferrable() {
if (this.options.deferrable) {
return this
.sequelize
.getQueryInterface()
.deferConstraints(this, this.options);
}
}
begin() {
const queryInterface = this.sequelize.getQueryInterface();
if ( this.sequelize.dialect.supports.settingIsolationLevelDuringTransaction ) {
return queryInterface.startTransaction(this, this.options).then(() => {
return queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options);
});
}
return queryInterface.setIsolationLevel(this, this.options.isolationLevel, this.options).then(() => {
return queryInterface.startTransaction(this, this.options);
});
}
cleanup() {
const res = this.sequelize.connectionManager.releaseConnection(this.connection);
this.connection.uuid = undefined;
return res;
}
_clearCls() {
const cls = this.sequelize.constructor._cls;
if (cls) {
if (cls.get('transaction') === this) {
cls.set('transaction', null);
}
}
}
/**
* A hook that is run after a transaction is committed
*
* @param {Function} fn A callback function that is called with the committed transaction
* @name afterCommit
* @memberof Sequelize.Transaction
*/
afterCommit(fn) {
if (!fn || typeof fn !== 'function') {
throw new Error('"fn" must be a function');
}
this._afterCommitHooks.push(fn);
}
/**
* Types can be set per-transaction by passing `options.type` to `sequelize.transaction`.
* Default to `DEFERRED` but you can override the default type by passing `options.transactionType` in `new Sequelize`.
* Sqlite only.
*
* Pass in the desired level as the first argument:
*
* @example
* return sequelize.transaction({type: Sequelize.Transaction.TYPES.EXCLUSIVE}, transaction => {
* // your transactions
* }).then(result => {
* // transaction has been committed. Do something after the commit if required.
* }).catch(err => {
* // do something with the err.
* });
*
* @property DEFERRED
* @property IMMEDIATE
* @property EXCLUSIVE
*/
static get TYPES() {
return {
DEFERRED: 'DEFERRED',
IMMEDIATE: 'IMMEDIATE',
EXCLUSIVE: 'EXCLUSIVE'
};
}
/**
* Isolation levels can be set per-transaction by passing `options.isolationLevel` to `sequelize.transaction`.
* Sequelize uses the default isolation level of the database, you can override this by passing `options.isolationLevel` in Sequelize constructor options.
*
* Pass in the desired level as the first argument:
*
* @example
* return sequelize.transaction({isolationLevel: Sequelize.Transaction.ISOLATION_LEVELS.SERIALIZABLE}, transaction => {
* // your transactions
* }).then(result => {
* // transaction has been committed. Do something after the commit if required.
* }).catch(err => {
* // do something with the err.
* });
*
* @property READ_UNCOMMITTED
* @property READ_COMMITTED
* @property REPEATABLE_READ
* @property SERIALIZABLE
*/
static get ISOLATION_LEVELS() {
return {
READ_UNCOMMITTED: 'READ UNCOMMITTED',
READ_COMMITTED: 'READ COMMITTED',
REPEATABLE_READ: 'REPEATABLE READ',
SERIALIZABLE: 'SERIALIZABLE'
};
}
/**
* Possible options for row locking. Used in conjunction with `find` calls:
*
* @example
* // t1 is a transaction
* Model.findAll({
* where: ...,
* transaction: t1,
* lock: t1.LOCK...
* });
*
* @example <caption>Postgres also supports specific locks while eager loading by using OF:</caption>
* UserModel.findAll({
* where: ...,
* include: [TaskModel, ...],
* transaction: t1,
* lock: {
* level: t1.LOCK...,
* of: UserModel
* }
* });
*
* # UserModel will be locked but TaskModel won't!
*
* @example <caption>You can also skip locked rows:</caption>
* // t1 is a transaction
* Model.findAll({
* where: ...,
* transaction: t1,
* lock: true,
* skipLocked: true
* });
* # The query will now return any rows that aren't locked by another transaction
*
* @returns {Object}
* @property UPDATE
* @property SHARE
* @property KEY_SHARE Postgres 9.3+ only
* @property NO_KEY_UPDATE Postgres 9.3+ only
*/
static get LOCK() {
return {
UPDATE: 'UPDATE',
SHARE: 'SHARE',
KEY_SHARE: 'KEY SHARE',
NO_KEY_UPDATE: 'NO KEY UPDATE'
};
}
/**
* Please see {@link Transaction.LOCK}
*/
get LOCK() {
return Transaction.LOCK;
}
}
module.exports = Transaction;
module.exports.Transaction = Transaction;
module.exports.default = Transaction;
+637
View File
@@ -0,0 +1,637 @@
'use strict';
const DataTypes = require('./data-types');
const SqlString = require('./sql-string');
const _ = require('lodash');
const uuidv1 = require('uuid/v1');
const uuidv4 = require('uuid/v4');
const Promise = require('./promise');
const operators = require('./operators');
const operatorsSet = new Set(_.values(operators));
let inflection = require('inflection');
exports.classToInvokable = require('./utils/classToInvokable').classToInvokable;
exports.Promise = Promise;
function useInflection(_inflection) {
inflection = _inflection;
}
exports.useInflection = useInflection;
function camelizeIf(str, condition) {
let result = str;
if (condition) {
result = camelize(str);
}
return result;
}
exports.camelizeIf = camelizeIf;
function underscoredIf(str, condition) {
let result = str;
if (condition) {
result = underscore(str);
}
return result;
}
exports.underscoredIf = underscoredIf;
function isPrimitive(val) {
const type = typeof val;
return type === 'string' || type === 'number' || type === 'boolean';
}
exports.isPrimitive = isPrimitive;
// Same concept as _.merge, but don't overwrite properties that have already been assigned
function mergeDefaults(a, b) {
return _.mergeWith(a, b, objectValue => {
// If it's an object, let _ handle it this time, we will be called again for each property
if (!_.isPlainObject(objectValue) && objectValue !== undefined) {
return objectValue;
}
});
}
exports.mergeDefaults = mergeDefaults;
// An alternative to _.merge, which doesn't clone its arguments
// Cloning is a bad idea because options arguments may contain references to sequelize
// models - which again reference database libs which don't like to be cloned (in particular pg-native)
function merge() {
const result = {};
for (const obj of arguments) {
_.forOwn(obj, (value, key) => {
if (value !== undefined) {
if (!result[key]) {
result[key] = value;
} else if (_.isPlainObject(value) && _.isPlainObject(result[key])) {
result[key] = merge(result[key], value);
} else if (Array.isArray(value) && Array.isArray(result[key])) {
result[key] = value.concat(result[key]);
} else {
result[key] = value;
}
}
});
}
return result;
}
exports.merge = merge;
function spliceStr(str, index, count, add) {
return str.slice(0, index) + add + str.slice(index + count);
}
exports.spliceStr = spliceStr;
function camelize(str) {
return str.trim().replace(/[-_\s]+(.)?/g, (match, c) => c.toUpperCase());
}
exports.camelize = camelize;
function underscore(str) {
return inflection.underscore(str);
}
exports.underscore = underscore;
function singularize(str) {
return inflection.singularize(str);
}
exports.singularize = singularize;
function pluralize(str) {
return inflection.pluralize(str);
}
exports.pluralize = pluralize;
function format(arr, dialect) {
const timeZone = null;
// Make a clone of the array beacuse format modifies the passed args
return SqlString.format(arr[0], arr.slice(1), timeZone, dialect);
}
exports.format = format;
function formatNamedParameters(sql, parameters, dialect) {
const timeZone = null;
return SqlString.formatNamedParameters(sql, parameters, timeZone, dialect);
}
exports.formatNamedParameters = formatNamedParameters;
function cloneDeep(obj, onlyPlain) {
obj = obj || {};
return _.cloneDeepWith(obj, elem => {
// Do not try to customize cloning of arrays or POJOs
if (Array.isArray(elem) || _.isPlainObject(elem)) {
return undefined;
}
// If we specified to clone only plain objects & arrays, we ignore everyhing else
// In any case, don't clone stuff that's an object, but not a plain one - fx example sequelize models and instances
if (onlyPlain || typeof elem === 'object') {
return elem;
}
// Preserve special data-types like `fn` across clones. _.get() is used for checking up the prototype chain
if (elem && typeof elem.clone === 'function') {
return elem.clone();
}
});
}
exports.cloneDeep = cloneDeep;
/* Expand and normalize finder options */
function mapFinderOptions(options, Model) {
if (options.attributes && Array.isArray(options.attributes)) {
options.attributes = Model._injectDependentVirtualAttributes(options.attributes);
options.attributes = options.attributes.filter(v => !Model._virtualAttributes.has(v));
}
mapOptionFieldNames(options, Model);
return options;
}
exports.mapFinderOptions = mapFinderOptions;
/* Used to map field names in attributes and where conditions */
function mapOptionFieldNames(options, Model) {
if (Array.isArray(options.attributes)) {
options.attributes = options.attributes.map(attr => {
// Object lookups will force any variable to strings, we don't want that for special objects etc
if (typeof attr !== 'string') return attr;
// Map attributes to aliased syntax attributes
if (Model.rawAttributes[attr] && attr !== Model.rawAttributes[attr].field) {
return [Model.rawAttributes[attr].field, attr];
}
return attr;
});
}
if (options.where && _.isPlainObject(options.where)) {
options.where = mapWhereFieldNames(options.where, Model);
}
return options;
}
exports.mapOptionFieldNames = mapOptionFieldNames;
function mapWhereFieldNames(attributes, Model) {
if (attributes) {
getComplexKeys(attributes).forEach(attribute => {
const rawAttribute = Model.rawAttributes[attribute];
if (rawAttribute && rawAttribute.field !== rawAttribute.fieldName) {
attributes[rawAttribute.field] = attributes[attribute];
delete attributes[attribute];
}
if (_.isPlainObject(attributes[attribute])
&& !(rawAttribute && (
rawAttribute.type instanceof DataTypes.HSTORE
|| rawAttribute.type instanceof DataTypes.JSON))) { // Prevent renaming of HSTORE & JSON fields
attributes[attribute] = mapOptionFieldNames({
where: attributes[attribute]
}, Model).where;
}
if (Array.isArray(attributes[attribute])) {
attributes[attribute].forEach((where, index) => {
if (_.isPlainObject(where)) {
attributes[attribute][index] = mapWhereFieldNames(where, Model);
}
});
}
});
}
return attributes;
}
exports.mapWhereFieldNames = mapWhereFieldNames;
/* Used to map field names in values */
function mapValueFieldNames(dataValues, fields, Model) {
const values = {};
for (const attr of fields) {
if (dataValues[attr] !== undefined && !Model._virtualAttributes.has(attr)) {
// Field name mapping
if (Model.rawAttributes[attr] && Model.rawAttributes[attr].field && Model.rawAttributes[attr].field !== attr) {
values[Model.rawAttributes[attr].field] = dataValues[attr];
} else {
values[attr] = dataValues[attr];
}
}
}
return values;
}
exports.mapValueFieldNames = mapValueFieldNames;
function isColString(value) {
return typeof value === 'string' && value[0] === '$' && value[value.length - 1] === '$';
}
exports.isColString = isColString;
function canTreatArrayAsAnd(arr) {
return arr.some(arg => _.isPlainObject(arg) || arg instanceof Where);
}
exports.canTreatArrayAsAnd = canTreatArrayAsAnd;
function combineTableNames(tableName1, tableName2) {
return tableName1.toLowerCase() < tableName2.toLowerCase() ? tableName1 + tableName2 : tableName2 + tableName1;
}
exports.combineTableNames = combineTableNames;
function toDefaultValue(value, dialect) {
if (typeof value === 'function') {
const tmp = value();
if (tmp instanceof DataTypes.ABSTRACT) {
return tmp.toSql();
}
return tmp;
}
if (value instanceof DataTypes.UUIDV1) {
return uuidv1();
}
if (value instanceof DataTypes.UUIDV4) {
return uuidv4();
}
if (value instanceof DataTypes.NOW) {
return now(dialect);
}
if (_.isPlainObject(value) || Array.isArray(value)) {
return _.clone(value);
}
return value;
}
exports.toDefaultValue = toDefaultValue;
/**
* Determine if the default value provided exists and can be described
* in a db schema using the DEFAULT directive.
*
* @param {*} value Any default value.
* @returns {boolean} yes / no.
* @private
*/
function defaultValueSchemable(value) {
if (value === undefined) { return false; }
// TODO this will be schemable when all supported db
// have been normalized for this case
if (value instanceof DataTypes.NOW) { return false; }
if (value instanceof DataTypes.UUIDV1 || value instanceof DataTypes.UUIDV4) { return false; }
return typeof value !== 'function';
}
exports.defaultValueSchemable = defaultValueSchemable;
function removeNullValuesFromHash(hash, omitNull, options) {
let result = hash;
options = options || {};
options.allowNull = options.allowNull || [];
if (omitNull) {
const _hash = {};
_.forIn(hash, (val, key) => {
if (options.allowNull.includes(key) || key.endsWith('Id') || val !== null && val !== undefined) {
_hash[key] = val;
}
});
result = _hash;
}
return result;
}
exports.removeNullValuesFromHash = removeNullValuesFromHash;
function stack() {
const orig = Error.prepareStackTrace;
Error.prepareStackTrace = (_, stack) => stack;
const err = new Error();
Error.captureStackTrace(err, stack);
const errStack = err.stack;
Error.prepareStackTrace = orig;
return errStack;
}
exports.stack = stack;
const dialects = new Set(['mariadb', 'mysql', 'postgres', 'sqlite', 'mssql']);
function now(dialect) {
const d = new Date();
if (!dialects.has(dialect)) {
d.setMilliseconds(0);
}
return d;
}
exports.now = now;
// Note: Use the `quoteIdentifier()` and `escape()` methods on the
// `QueryInterface` instead for more portable code.
const TICK_CHAR = '`';
exports.TICK_CHAR = TICK_CHAR;
function addTicks(s, tickChar) {
tickChar = tickChar || TICK_CHAR;
return tickChar + removeTicks(s, tickChar) + tickChar;
}
exports.addTicks = addTicks;
function removeTicks(s, tickChar) {
tickChar = tickChar || TICK_CHAR;
return s.replace(new RegExp(tickChar, 'g'), '');
}
exports.removeTicks = removeTicks;
/**
* Receives a tree-like object and returns a plain object which depth is 1.
*
* - Input:
*
* {
* name: 'John',
* address: {
* street: 'Fake St. 123',
* coordinates: {
* longitude: 55.6779627,
* latitude: 12.5964313
* }
* }
* }
*
* - Output:
*
* {
* name: 'John',
* address.street: 'Fake St. 123',
* address.coordinates.latitude: 55.6779627,
* address.coordinates.longitude: 12.5964313
* }
*
* @param {Object} value an Object
* @returns {Object} a flattened object
* @private
*/
function flattenObjectDeep(value) {
if (!_.isPlainObject(value)) return value;
const flattenedObj = {};
function flattenObject(obj, subPath) {
Object.keys(obj).forEach(key => {
const pathToProperty = subPath ? `${subPath}.${key}` : key;
if (typeof obj[key] === 'object' && obj[key] !== null) {
flattenObject(obj[key], pathToProperty);
} else {
flattenedObj[pathToProperty] = _.get(obj, key);
}
});
return flattenedObj;
}
return flattenObject(value, undefined);
}
exports.flattenObjectDeep = flattenObjectDeep;
/**
* Utility functions for representing SQL functions, and columns that should be escaped.
* Please do not use these functions directly, use Sequelize.fn and Sequelize.col instead.
* @private
*/
class SequelizeMethod {}
exports.SequelizeMethod = SequelizeMethod;
class Fn extends SequelizeMethod {
constructor(fn, args) {
super();
this.fn = fn;
this.args = args;
}
clone() {
return new Fn(this.fn, this.args);
}
}
exports.Fn = Fn;
class Col extends SequelizeMethod {
constructor(col, ...args) {
super();
if (args.length > 0) {
col = args;
}
this.col = col;
}
}
exports.Col = Col;
class Cast extends SequelizeMethod {
constructor(val, type, json) {
super();
this.val = val;
this.type = (type || '').trim();
this.json = json || false;
}
}
exports.Cast = Cast;
class Literal extends SequelizeMethod {
constructor(val) {
super();
this.val = val;
}
}
exports.Literal = Literal;
class Json extends SequelizeMethod {
constructor(conditionsOrPath, value) {
super();
if (_.isObject(conditionsOrPath)) {
this.conditions = conditionsOrPath;
} else {
this.path = conditionsOrPath;
if (value) {
this.value = value;
}
}
}
}
exports.Json = Json;
class Where extends SequelizeMethod {
constructor(attribute, comparator, logic) {
super();
if (logic === undefined) {
logic = comparator;
comparator = '=';
}
this.attribute = attribute;
this.comparator = comparator;
this.logic = logic;
}
}
exports.Where = Where;
//Collection of helper methods to make it easier to work with symbol operators
/**
* getOperators
*
* @param {Object} obj
* @returns {Array<Symbol>} All operators properties of obj
* @private
*/
function getOperators(obj) {
return Object.getOwnPropertySymbols(obj).filter(s => operatorsSet.has(s));
}
exports.getOperators = getOperators;
/**
* getComplexKeys
*
* @param {Object} obj
* @returns {Array<string|Symbol>} All keys including operators
* @private
*/
function getComplexKeys(obj) {
return getOperators(obj).concat(Object.keys(obj));
}
exports.getComplexKeys = getComplexKeys;
/**
* getComplexSize
*
* @param {Object|Array} obj
* @returns {number} Length of object properties including operators if obj is array returns its length
* @private
*/
function getComplexSize(obj) {
return Array.isArray(obj) ? obj.length : getComplexKeys(obj).length;
}
exports.getComplexSize = getComplexSize;
/**
* Returns true if a where clause is empty, even with Symbols
*
* @param {Object} obj
* @returns {boolean}
* @private
*/
function isWhereEmpty(obj) {
return !!obj && _.isEmpty(obj) && getOperators(obj).length === 0;
}
exports.isWhereEmpty = isWhereEmpty;
/**
* Returns ENUM name by joining table and column name
*
* @param {string} tableName
* @param {string} columnName
* @returns {string}
* @private
*/
function generateEnumName(tableName, columnName) {
return `enum_${tableName}_${columnName}`;
}
exports.generateEnumName = generateEnumName;
/**
* Returns an new Object which keys are camelized
*
* @param {Object} obj
* @returns {string}
* @private
*/
function camelizeObjectKeys(obj) {
const newObj = new Object();
Object.keys(obj).forEach(key => {
newObj[camelize(key)] = obj[key];
});
return newObj;
}
exports.camelizeObjectKeys = camelizeObjectKeys;
/**
* Assigns own and inherited enumerable string and symbol keyed properties of source
* objects to the destination object.
*
* https://lodash.com/docs/4.17.4#defaults
*
* **Note:** This method mutates `object`.
*
* @param {Object} object The destination object.
* @param {...Object} [sources] The source objects.
* @returns {Object} Returns `object`.
* @private
*/
function defaults(object, ...sources) {
object = Object(object);
sources.forEach(source => {
if (source) {
source = Object(source);
getComplexKeys(source).forEach(key => {
const value = object[key];
if (
value === undefined ||
_.eq(value, Object.prototype[key]) &&
!Object.prototype.hasOwnProperty.call(object, key)
) {
object[key] = source[key];
}
});
}
});
return object;
}
exports.defaults = defaults;
/**
*
* @param {Object} index
* @param {Array} index.fields
* @param {string} [index.name]
* @param {string|Object} tableName
*
* @returns {Object}
* @private
*/
function nameIndex(index, tableName) {
if (tableName.tableName) tableName = tableName.tableName;
if (!Object.prototype.hasOwnProperty.call(index, 'name')) {
const fields = index.fields.map(
field => typeof field === 'string' ? field : field.name || field.attribute
);
index.name = underscore(`${tableName}_${fields.join('_')}`);
}
return index;
}
exports.nameIndex = nameIndex;
/**
* Checks if 2 arrays intersect.
*
* @param {Array} arr1
* @param {Array} arr2
* @private
*/
function intersects(arr1, arr2) {
return arr1.some(v => arr2.includes(v));
}
exports.intersects = intersects;
+24
View File
@@ -0,0 +1,24 @@
'use strict';
/**
* Wraps a constructor to not need the `new` keyword using a proxy.
* Only used for data types.
*
* @param {Function} Class The class instance to wrap as invocable.
* @returns {Proxy} Wrapped class instance.
* @private
*/
function classToInvokable(Class) {
return new Proxy(Class, {
apply(Target, thisArg, args) {
return new Target(...args);
},
construct(Target, args) {
return new Target(...args);
},
get(target, p) {
return target[p];
}
});
}
exports.classToInvokable = classToInvokable;
+11
View File
@@ -0,0 +1,11 @@
'use strict';
const { deprecate } = require('util');
const noop = () => {};
exports.noRawAttributes = deprecate(noop, 'Use sequelize.fn / sequelize.literal to construct attributes', 'SEQUELIZE0001');
exports.noTrueLogging = deprecate(noop, 'The logging-option should be either a function or false. Default: console.log', 'SEQUELIZE0002');
exports.noStringOperators = deprecate(noop, 'String based operators are deprecated. Please use Symbol based operators for better security, read more at https://sequelize.org/master/manual/querying.html#operators', 'SEQUELIZE0003');
exports.noBoolOperatorAliases = deprecate(noop, 'A boolean value was passed to options.operatorsAliases. This is a no-op with v5 and should be removed.', 'SEQUELIZE0004');
exports.noDoubleNestedGroup = deprecate(noop, 'Passing a double nested nested array to `group` is unsupported and will be removed in v6.', 'SEQUELIZE0005');
+39
View File
@@ -0,0 +1,39 @@
'use strict';
/**
* Sequelize module for debug and deprecation messages.
* It require a `context` for which messages will be printed.
*
* @module logging
* @private
*/
const debug = require('debug');
const util = require('util');
class Logger {
constructor(config) {
this.config = Object.assign({
context: 'sequelize',
debug: true
}, config);
}
warn(message) {
// eslint-disable-next-line no-console
console.warn(`(${this.config.context}) Warning: ${message}`);
}
inspect(value) {
return util.inspect(value, false, 3);
}
debugContext(name) {
return debug(`${this.config.context}:${name}`);
}
}
exports.logger = new Logger();
exports.Logger = Logger;
+102
View File
@@ -0,0 +1,102 @@
'use strict';
const _ = require('lodash');
const validator = _.cloneDeep(require('validator'));
const moment = require('moment');
const extensions = {
extend(name, fn) {
this[name] = fn;
return this;
},
notEmpty(str) {
return !str.match(/^[\s\t\r\n]*$/);
},
len(str, min, max) {
return this.isLength(str, min, max);
},
isUrl(str) {
return this.isURL(str);
},
isIPv6(str) {
return this.isIP(str, 6);
},
isIPv4(str) {
return this.isIP(str, 4);
},
notIn(str, values) {
return !this.isIn(str, values);
},
regex(str, pattern, modifiers) {
str += '';
if (Object.prototype.toString.call(pattern).slice(8, -1) !== 'RegExp') {
pattern = new RegExp(pattern, modifiers);
}
return str.match(pattern);
},
notRegex(str, pattern, modifiers) {
return !this.regex(str, pattern, modifiers);
},
isDecimal(str) {
return str !== '' && !!str.match(/^(?:-?(?:[0-9]+))?(?:\.[0-9]*)?(?:[eE][+-]?(?:[0-9]+))?$/);
},
min(str, val) {
const number = parseFloat(str);
return isNaN(number) || number >= val;
},
max(str, val) {
const number = parseFloat(str);
return isNaN(number) || number <= val;
},
not(str, pattern, modifiers) {
return this.notRegex(str, pattern, modifiers);
},
contains(str, elem) {
return !!elem && str.includes(elem);
},
notContains(str, elem) {
return !this.contains(str, elem);
},
is(str, pattern, modifiers) {
return this.regex(str, pattern, modifiers);
}
};
exports.extensions = extensions;
// instance based validators
validator.isImmutable = function(value, validatorArgs, field, modelInstance) {
return modelInstance.isNewRecord || modelInstance.dataValues[field] === modelInstance._previousDataValues[field];
};
// extra validators
validator.notNull = function(val) {
return val !== null && val !== undefined;
};
// https://github.com/chriso/validator.js/blob/6.2.0/validator.js
_.forEach(extensions, (extend, key) => {
validator[key] = extend;
});
// map isNull to isEmpty
// https://github.com/chriso/validator.js/commit/e33d38a26ee2f9666b319adb67c7fc0d3dea7125
validator.isNull = validator.isEmpty;
// isDate removed in 7.0.0
// https://github.com/chriso/validator.js/commit/095509fc707a4dc0e99f85131df1176ad6389fc9
validator.isDate = function(dateString) {
// avoid http://momentjs.com/guides/#/warnings/js-date/
// by doing a preliminary check on `dateString`
const parsed = Date.parse(dateString);
if (isNaN(parsed)) {
// fail if we can't parse it
return false;
}
// otherwise convert to ISO 8601 as moment prefers
// http://momentjs.com/docs/#/parsing/string/
const date = new Date(parsed);
return moment(date.toISOString()).isValid();
};
exports.validator = validator;
+395
View File
@@ -0,0 +1,395 @@
3.1.0 / 2017-09-26
==================
* Add `DEBUG_HIDE_DATE` env var (#486)
* Remove ReDoS regexp in %o formatter (#504)
* Remove "component" from package.json
* Remove `component.json`
* Ignore package-lock.json
* Examples: fix colors printout
* Fix: browser detection
* Fix: spelling mistake (#496, @EdwardBetts)
3.0.1 / 2017-08-24
==================
* Fix: Disable colors in Edge and Internet Explorer (#489)
3.0.0 / 2017-08-08
==================
* Breaking: Remove DEBUG_FD (#406)
* Breaking: Use `Date#toISOString()` instead to `Date#toUTCString()` when output is not a TTY (#418)
* Breaking: Make millisecond timer namespace specific and allow 'always enabled' output (#408)
* Addition: document `enabled` flag (#465)
* Addition: add 256 colors mode (#481)
* Addition: `enabled()` updates existing debug instances, add `destroy()` function (#440)
* Update: component: update "ms" to v2.0.0
* Update: separate the Node and Browser tests in Travis-CI
* Update: refactor Readme, fixed documentation, added "Namespace Colors" section, redid screenshots
* Update: separate Node.js and web browser examples for organization
* Update: update "browserify" to v14.4.0
* Fix: fix Readme typo (#473)
2.6.9 / 2017-09-22
==================
* remove ReDoS regexp in %o formatter (#504)
2.6.8 / 2017-05-18
==================
* Fix: Check for undefined on browser globals (#462, @marbemac)
2.6.7 / 2017-05-16
==================
* Fix: Update ms to 2.0.0 to fix regular expression denial of service vulnerability (#458, @hubdotcom)
* Fix: Inline extend function in node implementation (#452, @dougwilson)
* Docs: Fix typo (#455, @msasad)
2.6.5 / 2017-04-27
==================
* Fix: null reference check on window.documentElement.style.WebkitAppearance (#447, @thebigredgeek)
* Misc: clean up browser reference checks (#447, @thebigredgeek)
* Misc: add npm-debug.log to .gitignore (@thebigredgeek)
2.6.4 / 2017-04-20
==================
* Fix: bug that would occur if process.env.DEBUG is a non-string value. (#444, @LucianBuzzo)
* Chore: ignore bower.json in npm installations. (#437, @joaovieira)
* Misc: update "ms" to v0.7.3 (@tootallnate)
2.6.3 / 2017-03-13
==================
* Fix: Electron reference to `process.env.DEBUG` (#431, @paulcbetts)
* Docs: Changelog fix (@thebigredgeek)
2.6.2 / 2017-03-10
==================
* Fix: DEBUG_MAX_ARRAY_LENGTH (#420, @slavaGanzin)
* Docs: Add backers and sponsors from Open Collective (#422, @piamancini)
* Docs: Add Slackin invite badge (@tootallnate)
2.6.1 / 2017-02-10
==================
* Fix: Module's `export default` syntax fix for IE8 `Expected identifier` error
* Fix: Whitelist DEBUG_FD for values 1 and 2 only (#415, @pi0)
* Fix: IE8 "Expected identifier" error (#414, @vgoma)
* Fix: Namespaces would not disable once enabled (#409, @musikov)
2.6.0 / 2016-12-28
==================
* Fix: added better null pointer checks for browser useColors (@thebigredgeek)
* Improvement: removed explicit `window.debug` export (#404, @tootallnate)
* Improvement: deprecated `DEBUG_FD` environment variable (#405, @tootallnate)
2.5.2 / 2016-12-25
==================
* Fix: reference error on window within webworkers (#393, @KlausTrainer)
* Docs: fixed README typo (#391, @lurch)
* Docs: added notice about v3 api discussion (@thebigredgeek)
2.5.1 / 2016-12-20
==================
* Fix: babel-core compatibility
2.5.0 / 2016-12-20
==================
* Fix: wrong reference in bower file (@thebigredgeek)
* Fix: webworker compatibility (@thebigredgeek)
* Fix: output formatting issue (#388, @kribblo)
* Fix: babel-loader compatibility (#383, @escwald)
* Misc: removed built asset from repo and publications (@thebigredgeek)
* Misc: moved source files to /src (#378, @yamikuronue)
* Test: added karma integration and replaced babel with browserify for browser tests (#378, @yamikuronue)
* Test: coveralls integration (#378, @yamikuronue)
* Docs: simplified language in the opening paragraph (#373, @yamikuronue)
2.4.5 / 2016-12-17
==================
* Fix: `navigator` undefined in Rhino (#376, @jochenberger)
* Fix: custom log function (#379, @hsiliev)
* Improvement: bit of cleanup + linting fixes (@thebigredgeek)
* Improvement: rm non-maintainted `dist/` dir (#375, @freewil)
* Docs: simplified language in the opening paragraph. (#373, @yamikuronue)
2.4.4 / 2016-12-14
==================
* Fix: work around debug being loaded in preload scripts for electron (#368, @paulcbetts)
2.4.3 / 2016-12-14
==================
* Fix: navigation.userAgent error for react native (#364, @escwald)
2.4.2 / 2016-12-14
==================
* Fix: browser colors (#367, @tootallnate)
* Misc: travis ci integration (@thebigredgeek)
* Misc: added linting and testing boilerplate with sanity check (@thebigredgeek)
2.4.1 / 2016-12-13
==================
* Fix: typo that broke the package (#356)
2.4.0 / 2016-12-13
==================
* Fix: bower.json references unbuilt src entry point (#342, @justmatt)
* Fix: revert "handle regex special characters" (@tootallnate)
* Feature: configurable util.inspect()`options for NodeJS (#327, @tootallnate)
* Feature: %O`(big O) pretty-prints objects (#322, @tootallnate)
* Improvement: allow colors in workers (#335, @botverse)
* Improvement: use same color for same namespace. (#338, @lchenay)
2.3.3 / 2016-11-09
==================
* Fix: Catch `JSON.stringify()` errors (#195, Jovan Alleyne)
* Fix: Returning `localStorage` saved values (#331, Levi Thomason)
* Improvement: Don't create an empty object when no `process` (Nathan Rajlich)
2.3.2 / 2016-11-09
==================
* Fix: be super-safe in index.js as well (@TooTallNate)
* Fix: should check whether process exists (Tom Newby)
2.3.1 / 2016-11-09
==================
* Fix: Added electron compatibility (#324, @paulcbetts)
* Improvement: Added performance optimizations (@tootallnate)
* Readme: Corrected PowerShell environment variable example (#252, @gimre)
* Misc: Removed yarn lock file from source control (#321, @fengmk2)
2.3.0 / 2016-11-07
==================
* Fix: Consistent placement of ms diff at end of output (#215, @gorangajic)
* Fix: Escaping of regex special characters in namespace strings (#250, @zacronos)
* Fix: Fixed bug causing crash on react-native (#282, @vkarpov15)
* Feature: Enabled ES6+ compatible import via default export (#212 @bucaran)
* Feature: Added %O formatter to reflect Chrome's console.log capability (#279, @oncletom)
* Package: Update "ms" to 0.7.2 (#315, @DevSide)
* Package: removed superfluous version property from bower.json (#207 @kkirsche)
* Readme: fix USE_COLORS to DEBUG_COLORS
* Readme: Doc fixes for format string sugar (#269, @mlucool)
* Readme: Updated docs for DEBUG_FD and DEBUG_COLORS environment variables (#232, @mattlyons0)
* Readme: doc fixes for PowerShell (#271 #243, @exoticknight @unreadable)
* Readme: better docs for browser support (#224, @matthewmueller)
* Tooling: Added yarn integration for development (#317, @thebigredgeek)
* Misc: Renamed History.md to CHANGELOG.md (@thebigredgeek)
* Misc: Added license file (#226 #274, @CantemoInternal @sdaitzman)
* Misc: Updated contributors (@thebigredgeek)
2.2.0 / 2015-05-09
==================
* package: update "ms" to v0.7.1 (#202, @dougwilson)
* README: add logging to file example (#193, @DanielOchoa)
* README: fixed a typo (#191, @amir-s)
* browser: expose `storage` (#190, @stephenmathieson)
* Makefile: add a `distclean` target (#189, @stephenmathieson)
2.1.3 / 2015-03-13
==================
* Updated stdout/stderr example (#186)
* Updated example/stdout.js to match debug current behaviour
* Renamed example/stderr.js to stdout.js
* Update Readme.md (#184)
* replace high intensity foreground color for bold (#182, #183)
2.1.2 / 2015-03-01
==================
* dist: recompile
* update "ms" to v0.7.0
* package: update "browserify" to v9.0.3
* component: fix "ms.js" repo location
* changed bower package name
* updated documentation about using debug in a browser
* fix: security error on safari (#167, #168, @yields)
2.1.1 / 2014-12-29
==================
* browser: use `typeof` to check for `console` existence
* browser: check for `console.log` truthiness (fix IE 8/9)
* browser: add support for Chrome apps
* Readme: added Windows usage remarks
* Add `bower.json` to properly support bower install
2.1.0 / 2014-10-15
==================
* node: implement `DEBUG_FD` env variable support
* package: update "browserify" to v6.1.0
* package: add "license" field to package.json (#135, @panuhorsmalahti)
2.0.0 / 2014-09-01
==================
* package: update "browserify" to v5.11.0
* node: use stderr rather than stdout for logging (#29, @stephenmathieson)
1.0.4 / 2014-07-15
==================
* dist: recompile
* example: remove `console.info()` log usage
* example: add "Content-Type" UTF-8 header to browser example
* browser: place %c marker after the space character
* browser: reset the "content" color via `color: inherit`
* browser: add colors support for Firefox >= v31
* debug: prefer an instance `log()` function over the global one (#119)
* Readme: update documentation about styled console logs for FF v31 (#116, @wryk)
1.0.3 / 2014-07-09
==================
* Add support for multiple wildcards in namespaces (#122, @seegno)
* browser: fix lint
1.0.2 / 2014-06-10
==================
* browser: update color palette (#113, @gscottolson)
* common: make console logging function configurable (#108, @timoxley)
* node: fix %o colors on old node <= 0.8.x
* Makefile: find node path using shell/which (#109, @timoxley)
1.0.1 / 2014-06-06
==================
* browser: use `removeItem()` to clear localStorage
* browser, node: don't set DEBUG if namespaces is undefined (#107, @leedm777)
* package: add "contributors" section
* node: fix comment typo
* README: list authors
1.0.0 / 2014-06-04
==================
* make ms diff be global, not be scope
* debug: ignore empty strings in enable()
* node: make DEBUG_COLORS able to disable coloring
* *: export the `colors` array
* npmignore: don't publish the `dist` dir
* Makefile: refactor to use browserify
* package: add "browserify" as a dev dependency
* Readme: add Web Inspector Colors section
* node: reset terminal color for the debug content
* node: map "%o" to `util.inspect()`
* browser: map "%j" to `JSON.stringify()`
* debug: add custom "formatters"
* debug: use "ms" module for humanizing the diff
* Readme: add "bash" syntax highlighting
* browser: add Firebug color support
* browser: add colors for WebKit browsers
* node: apply log to `console`
* rewrite: abstract common logic for Node & browsers
* add .jshintrc file
0.8.1 / 2014-04-14
==================
* package: re-add the "component" section
0.8.0 / 2014-03-30
==================
* add `enable()` method for nodejs. Closes #27
* change from stderr to stdout
* remove unnecessary index.js file
0.7.4 / 2013-11-13
==================
* remove "browserify" key from package.json (fixes something in browserify)
0.7.3 / 2013-10-30
==================
* fix: catch localStorage security error when cookies are blocked (Chrome)
* add debug(err) support. Closes #46
* add .browser prop to package.json. Closes #42
0.7.2 / 2013-02-06
==================
* fix package.json
* fix: Mobile Safari (private mode) is broken with debug
* fix: Use unicode to send escape character to shell instead of octal to work with strict mode javascript
0.7.1 / 2013-02-05
==================
* add repository URL to package.json
* add DEBUG_COLORED to force colored output
* add browserify support
* fix component. Closes #24
0.7.0 / 2012-05-04
==================
* Added .component to package.json
* Added debug.component.js build
0.6.0 / 2012-03-16
==================
* Added support for "-" prefix in DEBUG [Vinay Pulim]
* Added `.enabled` flag to the node version [TooTallNate]
0.5.0 / 2012-02-02
==================
* Added: humanize diffs. Closes #8
* Added `debug.disable()` to the CS variant
* Removed padding. Closes #10
* Fixed: persist client-side variant again. Closes #9
0.4.0 / 2012-02-01
==================
* Added browser variant support for older browsers [TooTallNate]
* Added `debug.enable('project:*')` to browser variant [TooTallNate]
* Added padding to diff (moved it to the right)
0.3.0 / 2012-01-26
==================
* Added millisecond diff when isatty, otherwise UTC string
0.2.0 / 2012-01-22
==================
* Added wildcard support
0.1.0 / 2011-12-02
==================
* Added: remove colors unless stderr isatty [TooTallNate]
0.0.1 / 2010-01-03
==================
* Initial release
+19
View File
@@ -0,0 +1,19 @@
(The MIT License)
Copyright (c) 2014 TJ Holowaychuk <tj@vision-media.ca>
Permission is hereby granted, free of charge, to any person obtaining a copy of this software
and associated documentation files (the 'Software'), to deal in the Software without restriction,
including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so,
subject to the following conditions:
The above copyright notice and this permission notice shall be included in all copies or substantial
portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT
LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+455
View File
@@ -0,0 +1,455 @@
# debug
[![Build Status](https://travis-ci.org/visionmedia/debug.svg?branch=master)](https://travis-ci.org/visionmedia/debug) [![Coverage Status](https://coveralls.io/repos/github/visionmedia/debug/badge.svg?branch=master)](https://coveralls.io/github/visionmedia/debug?branch=master) [![Slack](https://visionmedia-community-slackin.now.sh/badge.svg)](https://visionmedia-community-slackin.now.sh/) [![OpenCollective](https://opencollective.com/debug/backers/badge.svg)](#backers)
[![OpenCollective](https://opencollective.com/debug/sponsors/badge.svg)](#sponsors)
<img width="647" src="https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png">
A tiny JavaScript debugging utility modelled after Node.js core's debugging
technique. Works in Node.js and web browsers.
## Installation
```bash
$ npm install debug
```
## Usage
`debug` exposes a function; simply pass this function the name of your module, and it will return a decorated version of `console.error` for you to pass debug statements to. This will allow you to toggle the debug output for different parts of your module as well as the module as a whole.
Example [_app.js_](./examples/node/app.js):
```js
var debug = require('debug')('http')
, http = require('http')
, name = 'My App';
// fake app
debug('booting %o', name);
http.createServer(function(req, res){
debug(req.method + ' ' + req.url);
res.end('hello\n');
}).listen(3000, function(){
debug('listening');
});
// fake worker of some kind
require('./worker');
```
Example [_worker.js_](./examples/node/worker.js):
```js
var a = require('debug')('worker:a')
, b = require('debug')('worker:b');
function work() {
a('doing lots of uninteresting work');
setTimeout(work, Math.random() * 1000);
}
work();
function workb() {
b('doing some work');
setTimeout(workb, Math.random() * 2000);
}
workb();
```
The `DEBUG` environment variable is then used to enable these based on space or
comma-delimited names.
Here are some examples:
<img width="647" alt="screen shot 2017-08-08 at 12 53 04 pm" src="https://user-images.githubusercontent.com/71256/29091703-a6302cdc-7c38-11e7-8304-7c0b3bc600cd.png">
<img width="647" alt="screen shot 2017-08-08 at 12 53 38 pm" src="https://user-images.githubusercontent.com/71256/29091700-a62a6888-7c38-11e7-800b-db911291ca2b.png">
<img width="647" alt="screen shot 2017-08-08 at 12 53 25 pm" src="https://user-images.githubusercontent.com/71256/29091701-a62ea114-7c38-11e7-826a-2692bedca740.png">
#### Windows command prompt notes
##### CMD
On Windows the environment variable is set using the `set` command.
```cmd
set DEBUG=*,-not_this
```
Example:
```cmd
set DEBUG=* & node app.js
```
##### PowerShell (VS Code default)
PowerShell uses different syntax to set environment variables.
```cmd
$env:DEBUG = "*,-not_this"
```
Example:
```cmd
$env:DEBUG='app';node app.js
```
Then, run the program to be debugged as usual.
npm script example:
```js
"windowsDebug": "@powershell -Command $env:DEBUG='*';node app.js",
```
## Namespace Colors
Every debug instance has a color generated for it based on its namespace name.
This helps when visually parsing the debug output to identify which debug instance
a debug line belongs to.
#### Node.js
In Node.js, colors are enabled when stderr is a TTY. You also _should_ install
the [`supports-color`](https://npmjs.org/supports-color) module alongside debug,
otherwise debug will only use a small handful of basic colors.
<img width="521" src="https://user-images.githubusercontent.com/71256/29092181-47f6a9e6-7c3a-11e7-9a14-1928d8a711cd.png">
#### Web Browser
Colors are also enabled on "Web Inspectors" that understand the `%c` formatting
option. These are WebKit web inspectors, Firefox ([since version
31](https://hacks.mozilla.org/2014/05/editable-box-model-multiple-selection-sublime-text-keys-much-more-firefox-developer-tools-episode-31/))
and the Firebug plugin for Firefox (any version).
<img width="524" src="https://user-images.githubusercontent.com/71256/29092033-b65f9f2e-7c39-11e7-8e32-f6f0d8e865c1.png">
## Millisecond diff
When actively developing an application it can be useful to see when the time spent between one `debug()` call and the next. Suppose for example you invoke `debug()` before requesting a resource, and after as well, the "+NNNms" will show you how much time was spent between calls.
<img width="647" src="https://user-images.githubusercontent.com/71256/29091486-fa38524c-7c37-11e7-895f-e7ec8e1039b6.png">
When stdout is not a TTY, `Date#toISOString()` is used, making it more useful for logging the debug information as shown below:
<img width="647" src="https://user-images.githubusercontent.com/71256/29091956-6bd78372-7c39-11e7-8c55-c948396d6edd.png">
## Conventions
If you're using this in one or more of your libraries, you _should_ use the name of your library so that developers may toggle debugging as desired without guessing names. If you have more than one debuggers you _should_ prefix them with your library name and use ":" to separate features. For example "bodyParser" from Connect would then be "connect:bodyParser". If you append a "*" to the end of your name, it will always be enabled regardless of the setting of the DEBUG environment variable. You can then use it for normal output as well as debug output.
## Wildcards
The `*` character may be used as a wildcard. Suppose for example your library has
debuggers named "connect:bodyParser", "connect:compress", "connect:session",
instead of listing all three with
`DEBUG=connect:bodyParser,connect:compress,connect:session`, you may simply do
`DEBUG=connect:*`, or to run everything using this module simply use `DEBUG=*`.
You can also exclude specific debuggers by prefixing them with a "-" character.
For example, `DEBUG=*,-connect:*` would include all debuggers except those
starting with "connect:".
## Environment Variables
When running through Node.js, you can set a few environment variables that will
change the behavior of the debug logging:
| Name | Purpose |
|-----------|-------------------------------------------------|
| `DEBUG` | Enables/disables specific debugging namespaces. |
| `DEBUG_HIDE_DATE` | Hide date from debug output (non-TTY). |
| `DEBUG_COLORS`| Whether or not to use colors in the debug output. |
| `DEBUG_DEPTH` | Object inspection depth. |
| `DEBUG_SHOW_HIDDEN` | Shows hidden properties on inspected objects. |
__Note:__ The environment variables beginning with `DEBUG_` end up being
converted into an Options object that gets used with `%o`/`%O` formatters.
See the Node.js documentation for
[`util.inspect()`](https://nodejs.org/api/util.html#util_util_inspect_object_options)
for the complete list.
## Formatters
Debug uses [printf-style](https://wikipedia.org/wiki/Printf_format_string) formatting.
Below are the officially supported formatters:
| Formatter | Representation |
|-----------|----------------|
| `%O` | Pretty-print an Object on multiple lines. |
| `%o` | Pretty-print an Object all on a single line. |
| `%s` | String. |
| `%d` | Number (both integer and float). |
| `%j` | JSON. Replaced with the string '[Circular]' if the argument contains circular references. |
| `%%` | Single percent sign ('%'). This does not consume an argument. |
### Custom formatters
You can add custom formatters by extending the `debug.formatters` object.
For example, if you wanted to add support for rendering a Buffer as hex with
`%h`, you could do something like:
```js
const createDebug = require('debug')
createDebug.formatters.h = (v) => {
return v.toString('hex')
}
// …elsewhere
const debug = createDebug('foo')
debug('this is hex: %h', new Buffer('hello world'))
// foo this is hex: 68656c6c6f20776f726c6421 +0ms
```
## Browser Support
You can build a browser-ready script using [browserify](https://github.com/substack/node-browserify),
or just use the [browserify-as-a-service](https://wzrd.in/) [build](https://wzrd.in/standalone/debug@latest),
if you don't want to build it yourself.
Debug's enable state is currently persisted by `localStorage`.
Consider the situation shown below where you have `worker:a` and `worker:b`,
and wish to debug both. You can enable this using `localStorage.debug`:
```js
localStorage.debug = 'worker:*'
```
And then refresh the page.
```js
a = debug('worker:a');
b = debug('worker:b');
setInterval(function(){
a('doing some work');
}, 1000);
setInterval(function(){
b('doing some work');
}, 1200);
```
## Output streams
By default `debug` will log to stderr, however this can be configured per-namespace by overriding the `log` method:
Example [_stdout.js_](./examples/node/stdout.js):
```js
var debug = require('debug');
var error = debug('app:error');
// by default stderr is used
error('goes to stderr!');
var log = debug('app:log');
// set this namespace to log via console.log
log.log = console.log.bind(console); // don't forget to bind to console!
log('goes to stdout');
error('still goes to stderr!');
// set all output to go via console.info
// overrides all per-namespace log settings
debug.log = console.info.bind(console);
error('now goes to stdout via console.info');
log('still goes to stdout, but via console.info now');
```
## Extend
You can simply extend debugger
```js
const log = require('debug')('auth');
//creates new debug instance with extended namespace
const logSign = log.extend('sign');
const logLogin = log.extend('login');
log('hello'); // auth hello
logSign('hello'); //auth:sign hello
logLogin('hello'); //auth:login hello
```
## Set dynamically
You can also enable debug dynamically by calling the `enable()` method :
```js
let debug = require('debug');
console.log(1, debug.enabled('test'));
debug.enable('test');
console.log(2, debug.enabled('test'));
debug.disable();
console.log(3, debug.enabled('test'));
```
print :
```
1 false
2 true
3 false
```
Usage :
`enable(namespaces)`
`namespaces` can include modes separated by a colon and wildcards.
Note that calling `enable()` completely overrides previously set DEBUG variable :
```
$ DEBUG=foo node -e 'var dbg = require("debug"); dbg.enable("bar"); console.log(dbg.enabled("foo"))'
=> false
```
`disable()`
Will disable all namespaces. The functions returns the namespaces currently
enabled (and skipped). This can be useful if you want to disable debugging
temporarily without knowing what was enabled to begin with.
For example:
```js
let debug = require('debug');
debug.enable('foo:*,-foo:bar');
let namespaces = debug.disable();
debug.enable(namespaces);
```
Note: There is no guarantee that the string will be identical to the initial
enable string, but semantically they will be identical.
## Checking whether a debug target is enabled
After you've created a debug instance, you can determine whether or not it is
enabled by checking the `enabled` property:
```javascript
const debug = require('debug')('http');
if (debug.enabled) {
// do stuff...
}
```
You can also manually toggle this property to force the debug instance to be
enabled or disabled.
## Authors
- TJ Holowaychuk
- Nathan Rajlich
- Andrew Rhyne
## Backers
Support us with a monthly donation and help us continue our activities. [[Become a backer](https://opencollective.com/debug#backer)]
<a href="https://opencollective.com/debug/backer/0/website" target="_blank"><img src="https://opencollective.com/debug/backer/0/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/1/website" target="_blank"><img src="https://opencollective.com/debug/backer/1/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/2/website" target="_blank"><img src="https://opencollective.com/debug/backer/2/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/3/website" target="_blank"><img src="https://opencollective.com/debug/backer/3/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/4/website" target="_blank"><img src="https://opencollective.com/debug/backer/4/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/5/website" target="_blank"><img src="https://opencollective.com/debug/backer/5/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/6/website" target="_blank"><img src="https://opencollective.com/debug/backer/6/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/7/website" target="_blank"><img src="https://opencollective.com/debug/backer/7/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/8/website" target="_blank"><img src="https://opencollective.com/debug/backer/8/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/9/website" target="_blank"><img src="https://opencollective.com/debug/backer/9/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/10/website" target="_blank"><img src="https://opencollective.com/debug/backer/10/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/11/website" target="_blank"><img src="https://opencollective.com/debug/backer/11/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/12/website" target="_blank"><img src="https://opencollective.com/debug/backer/12/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/13/website" target="_blank"><img src="https://opencollective.com/debug/backer/13/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/14/website" target="_blank"><img src="https://opencollective.com/debug/backer/14/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/15/website" target="_blank"><img src="https://opencollective.com/debug/backer/15/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/16/website" target="_blank"><img src="https://opencollective.com/debug/backer/16/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/17/website" target="_blank"><img src="https://opencollective.com/debug/backer/17/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/18/website" target="_blank"><img src="https://opencollective.com/debug/backer/18/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/19/website" target="_blank"><img src="https://opencollective.com/debug/backer/19/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/20/website" target="_blank"><img src="https://opencollective.com/debug/backer/20/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/21/website" target="_blank"><img src="https://opencollective.com/debug/backer/21/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/22/website" target="_blank"><img src="https://opencollective.com/debug/backer/22/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/23/website" target="_blank"><img src="https://opencollective.com/debug/backer/23/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/24/website" target="_blank"><img src="https://opencollective.com/debug/backer/24/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/25/website" target="_blank"><img src="https://opencollective.com/debug/backer/25/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/26/website" target="_blank"><img src="https://opencollective.com/debug/backer/26/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/27/website" target="_blank"><img src="https://opencollective.com/debug/backer/27/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/28/website" target="_blank"><img src="https://opencollective.com/debug/backer/28/avatar.svg"></a>
<a href="https://opencollective.com/debug/backer/29/website" target="_blank"><img src="https://opencollective.com/debug/backer/29/avatar.svg"></a>
## Sponsors
Become a sponsor and get your logo on our README on Github with a link to your site. [[Become a sponsor](https://opencollective.com/debug#sponsor)]
<a href="https://opencollective.com/debug/sponsor/0/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/0/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/1/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/1/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/2/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/2/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/3/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/3/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/4/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/4/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/5/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/5/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/6/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/6/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/7/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/7/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/8/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/8/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/9/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/9/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/10/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/10/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/11/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/11/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/12/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/12/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/13/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/13/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/14/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/14/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/15/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/15/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/16/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/16/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/17/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/17/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/18/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/18/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/19/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/19/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/20/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/20/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/21/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/21/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/22/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/22/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/23/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/23/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/24/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/24/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/25/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/25/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/26/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/26/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/27/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/27/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/28/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/28/avatar.svg"></a>
<a href="https://opencollective.com/debug/sponsor/29/website" target="_blank"><img src="https://opencollective.com/debug/sponsor/29/avatar.svg"></a>
## License
(The MIT License)
Copyright (c) 2014-2017 TJ Holowaychuk &lt;tj@vision-media.ca&gt;
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
'Software'), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
The above copyright notice and this permission notice shall be
included in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+912
View File
@@ -0,0 +1,912 @@
"use strict";
function _toConsumableArray(arr) { return _arrayWithoutHoles(arr) || _iterableToArray(arr) || _nonIterableSpread(); }
function _nonIterableSpread() { throw new TypeError("Invalid attempt to spread non-iterable instance"); }
function _iterableToArray(iter) { if (Symbol.iterator in Object(iter) || Object.prototype.toString.call(iter) === "[object Arguments]") return Array.from(iter); }
function _arrayWithoutHoles(arr) { if (Array.isArray(arr)) { for (var i = 0, arr2 = new Array(arr.length); i < arr.length; i++) { arr2[i] = arr[i]; } return arr2; } }
function _typeof(obj) { if (typeof Symbol === "function" && typeof Symbol.iterator === "symbol") { _typeof = function _typeof(obj) { return typeof obj; }; } else { _typeof = function _typeof(obj) { return obj && typeof Symbol === "function" && obj.constructor === Symbol && obj !== Symbol.prototype ? "symbol" : typeof obj; }; } return _typeof(obj); }
(function (f) {
if ((typeof exports === "undefined" ? "undefined" : _typeof(exports)) === "object" && typeof module !== "undefined") {
module.exports = f();
} else if (typeof define === "function" && define.amd) {
define([], f);
} else {
var g;
if (typeof window !== "undefined") {
g = window;
} else if (typeof global !== "undefined") {
g = global;
} else if (typeof self !== "undefined") {
g = self;
} else {
g = this;
}
g.debug = f();
}
})(function () {
var define, module, exports;
return function () {
function r(e, n, t) {
function o(i, f) {
if (!n[i]) {
if (!e[i]) {
var c = "function" == typeof require && require;
if (!f && c) return c(i, !0);
if (u) return u(i, !0);
var a = new Error("Cannot find module '" + i + "'");
throw a.code = "MODULE_NOT_FOUND", a;
}
var p = n[i] = {
exports: {}
};
e[i][0].call(p.exports, function (r) {
var n = e[i][1][r];
return o(n || r);
}, p, p.exports, r, e, n, t);
}
return n[i].exports;
}
for (var u = "function" == typeof require && require, i = 0; i < t.length; i++) {
o(t[i]);
}
return o;
}
return r;
}()({
1: [function (require, module, exports) {
/**
* Helpers.
*/
var s = 1000;
var m = s * 60;
var h = m * 60;
var d = h * 24;
var w = d * 7;
var y = d * 365.25;
/**
* Parse or format the given `val`.
*
* Options:
*
* - `long` verbose formatting [false]
*
* @param {String|Number} val
* @param {Object} [options]
* @throws {Error} throw an error if val is not a non-empty string or a number
* @return {String|Number}
* @api public
*/
module.exports = function (val, options) {
options = options || {};
var type = _typeof(val);
if (type === 'string' && val.length > 0) {
return parse(val);
} else if (type === 'number' && isNaN(val) === false) {
return options.long ? fmtLong(val) : fmtShort(val);
}
throw new Error('val is not a non-empty string or a valid number. val=' + JSON.stringify(val));
};
/**
* Parse the given `str` and return milliseconds.
*
* @param {String} str
* @return {Number}
* @api private
*/
function parse(str) {
str = String(str);
if (str.length > 100) {
return;
}
var match = /^((?:\d+)?\-?\d?\.?\d+) *(milliseconds?|msecs?|ms|seconds?|secs?|s|minutes?|mins?|m|hours?|hrs?|h|days?|d|weeks?|w|years?|yrs?|y)?$/i.exec(str);
if (!match) {
return;
}
var n = parseFloat(match[1]);
var type = (match[2] || 'ms').toLowerCase();
switch (type) {
case 'years':
case 'year':
case 'yrs':
case 'yr':
case 'y':
return n * y;
case 'weeks':
case 'week':
case 'w':
return n * w;
case 'days':
case 'day':
case 'd':
return n * d;
case 'hours':
case 'hour':
case 'hrs':
case 'hr':
case 'h':
return n * h;
case 'minutes':
case 'minute':
case 'mins':
case 'min':
case 'm':
return n * m;
case 'seconds':
case 'second':
case 'secs':
case 'sec':
case 's':
return n * s;
case 'milliseconds':
case 'millisecond':
case 'msecs':
case 'msec':
case 'ms':
return n;
default:
return undefined;
}
}
/**
* Short format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function fmtShort(ms) {
var msAbs = Math.abs(ms);
if (msAbs >= d) {
return Math.round(ms / d) + 'd';
}
if (msAbs >= h) {
return Math.round(ms / h) + 'h';
}
if (msAbs >= m) {
return Math.round(ms / m) + 'm';
}
if (msAbs >= s) {
return Math.round(ms / s) + 's';
}
return ms + 'ms';
}
/**
* Long format for `ms`.
*
* @param {Number} ms
* @return {String}
* @api private
*/
function fmtLong(ms) {
var msAbs = Math.abs(ms);
if (msAbs >= d) {
return plural(ms, msAbs, d, 'day');
}
if (msAbs >= h) {
return plural(ms, msAbs, h, 'hour');
}
if (msAbs >= m) {
return plural(ms, msAbs, m, 'minute');
}
if (msAbs >= s) {
return plural(ms, msAbs, s, 'second');
}
return ms + ' ms';
}
/**
* Pluralization helper.
*/
function plural(ms, msAbs, n, name) {
var isPlural = msAbs >= n * 1.5;
return Math.round(ms / n) + ' ' + name + (isPlural ? 's' : '');
}
}, {}],
2: [function (require, module, exports) {
// shim for using process in browser
var process = module.exports = {}; // cached from whatever global is present so that test runners that stub it
// don't break things. But we need to wrap it in a try catch in case it is
// wrapped in strict mode code which doesn't define any globals. It's inside a
// function because try/catches deoptimize in certain engines.
var cachedSetTimeout;
var cachedClearTimeout;
function defaultSetTimout() {
throw new Error('setTimeout has not been defined');
}
function defaultClearTimeout() {
throw new Error('clearTimeout has not been defined');
}
(function () {
try {
if (typeof setTimeout === 'function') {
cachedSetTimeout = setTimeout;
} else {
cachedSetTimeout = defaultSetTimout;
}
} catch (e) {
cachedSetTimeout = defaultSetTimout;
}
try {
if (typeof clearTimeout === 'function') {
cachedClearTimeout = clearTimeout;
} else {
cachedClearTimeout = defaultClearTimeout;
}
} catch (e) {
cachedClearTimeout = defaultClearTimeout;
}
})();
function runTimeout(fun) {
if (cachedSetTimeout === setTimeout) {
//normal enviroments in sane situations
return setTimeout(fun, 0);
} // if setTimeout wasn't available but was latter defined
if ((cachedSetTimeout === defaultSetTimout || !cachedSetTimeout) && setTimeout) {
cachedSetTimeout = setTimeout;
return setTimeout(fun, 0);
}
try {
// when when somebody has screwed with setTimeout but no I.E. maddness
return cachedSetTimeout(fun, 0);
} catch (e) {
try {
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
return cachedSetTimeout.call(null, fun, 0);
} catch (e) {
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error
return cachedSetTimeout.call(this, fun, 0);
}
}
}
function runClearTimeout(marker) {
if (cachedClearTimeout === clearTimeout) {
//normal enviroments in sane situations
return clearTimeout(marker);
} // if clearTimeout wasn't available but was latter defined
if ((cachedClearTimeout === defaultClearTimeout || !cachedClearTimeout) && clearTimeout) {
cachedClearTimeout = clearTimeout;
return clearTimeout(marker);
}
try {
// when when somebody has screwed with setTimeout but no I.E. maddness
return cachedClearTimeout(marker);
} catch (e) {
try {
// When we are in I.E. but the script has been evaled so I.E. doesn't trust the global object when called normally
return cachedClearTimeout.call(null, marker);
} catch (e) {
// same as above but when it's a version of I.E. that must have the global object for 'this', hopfully our context correct otherwise it will throw a global error.
// Some versions of I.E. have different rules for clearTimeout vs setTimeout
return cachedClearTimeout.call(this, marker);
}
}
}
var queue = [];
var draining = false;
var currentQueue;
var queueIndex = -1;
function cleanUpNextTick() {
if (!draining || !currentQueue) {
return;
}
draining = false;
if (currentQueue.length) {
queue = currentQueue.concat(queue);
} else {
queueIndex = -1;
}
if (queue.length) {
drainQueue();
}
}
function drainQueue() {
if (draining) {
return;
}
var timeout = runTimeout(cleanUpNextTick);
draining = true;
var len = queue.length;
while (len) {
currentQueue = queue;
queue = [];
while (++queueIndex < len) {
if (currentQueue) {
currentQueue[queueIndex].run();
}
}
queueIndex = -1;
len = queue.length;
}
currentQueue = null;
draining = false;
runClearTimeout(timeout);
}
process.nextTick = function (fun) {
var args = new Array(arguments.length - 1);
if (arguments.length > 1) {
for (var i = 1; i < arguments.length; i++) {
args[i - 1] = arguments[i];
}
}
queue.push(new Item(fun, args));
if (queue.length === 1 && !draining) {
runTimeout(drainQueue);
}
}; // v8 likes predictible objects
function Item(fun, array) {
this.fun = fun;
this.array = array;
}
Item.prototype.run = function () {
this.fun.apply(null, this.array);
};
process.title = 'browser';
process.browser = true;
process.env = {};
process.argv = [];
process.version = ''; // empty string to avoid regexp issues
process.versions = {};
function noop() {}
process.on = noop;
process.addListener = noop;
process.once = noop;
process.off = noop;
process.removeListener = noop;
process.removeAllListeners = noop;
process.emit = noop;
process.prependListener = noop;
process.prependOnceListener = noop;
process.listeners = function (name) {
return [];
};
process.binding = function (name) {
throw new Error('process.binding is not supported');
};
process.cwd = function () {
return '/';
};
process.chdir = function (dir) {
throw new Error('process.chdir is not supported');
};
process.umask = function () {
return 0;
};
}, {}],
3: [function (require, module, exports) {
/**
* This is the common logic for both the Node.js and web browser
* implementations of `debug()`.
*/
function setup(env) {
createDebug.debug = createDebug;
createDebug.default = createDebug;
createDebug.coerce = coerce;
createDebug.disable = disable;
createDebug.enable = enable;
createDebug.enabled = enabled;
createDebug.humanize = require('ms');
Object.keys(env).forEach(function (key) {
createDebug[key] = env[key];
});
/**
* Active `debug` instances.
*/
createDebug.instances = [];
/**
* The currently active debug mode names, and names to skip.
*/
createDebug.names = [];
createDebug.skips = [];
/**
* Map of special "%n" handling functions, for the debug "format" argument.
*
* Valid key names are a single, lower or upper-case letter, i.e. "n" and "N".
*/
createDebug.formatters = {};
/**
* Selects a color for a debug namespace
* @param {String} namespace The namespace string for the for the debug instance to be colored
* @return {Number|String} An ANSI color code for the given namespace
* @api private
*/
function selectColor(namespace) {
var hash = 0;
for (var i = 0; i < namespace.length; i++) {
hash = (hash << 5) - hash + namespace.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
return createDebug.colors[Math.abs(hash) % createDebug.colors.length];
}
createDebug.selectColor = selectColor;
/**
* Create a debugger with the given `namespace`.
*
* @param {String} namespace
* @return {Function}
* @api public
*/
function createDebug(namespace) {
var prevTime;
function debug() {
for (var _len = arguments.length, args = new Array(_len), _key = 0; _key < _len; _key++) {
args[_key] = arguments[_key];
}
// Disabled?
if (!debug.enabled) {
return;
}
var self = debug; // Set `diff` timestamp
var curr = Number(new Date());
var ms = curr - (prevTime || curr);
self.diff = ms;
self.prev = prevTime;
self.curr = curr;
prevTime = curr;
args[0] = createDebug.coerce(args[0]);
if (typeof args[0] !== 'string') {
// Anything else let's inspect with %O
args.unshift('%O');
} // Apply any `formatters` transformations
var index = 0;
args[0] = args[0].replace(/%([a-zA-Z%])/g, function (match, format) {
// If we encounter an escaped % then don't increase the array index
if (match === '%%') {
return match;
}
index++;
var formatter = createDebug.formatters[format];
if (typeof formatter === 'function') {
var val = args[index];
match = formatter.call(self, val); // Now we need to remove `args[index]` since it's inlined in the `format`
args.splice(index, 1);
index--;
}
return match;
}); // Apply env-specific formatting (colors, etc.)
createDebug.formatArgs.call(self, args);
var logFn = self.log || createDebug.log;
logFn.apply(self, args);
}
debug.namespace = namespace;
debug.enabled = createDebug.enabled(namespace);
debug.useColors = createDebug.useColors();
debug.color = selectColor(namespace);
debug.destroy = destroy;
debug.extend = extend; // Debug.formatArgs = formatArgs;
// debug.rawLog = rawLog;
// env-specific initialization logic for debug instances
if (typeof createDebug.init === 'function') {
createDebug.init(debug);
}
createDebug.instances.push(debug);
return debug;
}
function destroy() {
var index = createDebug.instances.indexOf(this);
if (index !== -1) {
createDebug.instances.splice(index, 1);
return true;
}
return false;
}
function extend(namespace, delimiter) {
var newDebug = createDebug(this.namespace + (typeof delimiter === 'undefined' ? ':' : delimiter) + namespace);
newDebug.log = this.log;
return newDebug;
}
/**
* Enables a debug mode by namespaces. This can include modes
* separated by a colon and wildcards.
*
* @param {String} namespaces
* @api public
*/
function enable(namespaces) {
createDebug.save(namespaces);
createDebug.names = [];
createDebug.skips = [];
var i;
var split = (typeof namespaces === 'string' ? namespaces : '').split(/[\s,]+/);
var len = split.length;
for (i = 0; i < len; i++) {
if (!split[i]) {
// ignore empty strings
continue;
}
namespaces = split[i].replace(/\*/g, '.*?');
if (namespaces[0] === '-') {
createDebug.skips.push(new RegExp('^' + namespaces.substr(1) + '$'));
} else {
createDebug.names.push(new RegExp('^' + namespaces + '$'));
}
}
for (i = 0; i < createDebug.instances.length; i++) {
var instance = createDebug.instances[i];
instance.enabled = createDebug.enabled(instance.namespace);
}
}
/**
* Disable debug output.
*
* @return {String} namespaces
* @api public
*/
function disable() {
var namespaces = [].concat(_toConsumableArray(createDebug.names.map(toNamespace)), _toConsumableArray(createDebug.skips.map(toNamespace).map(function (namespace) {
return '-' + namespace;
}))).join(',');
createDebug.enable('');
return namespaces;
}
/**
* Returns true if the given mode name is enabled, false otherwise.
*
* @param {String} name
* @return {Boolean}
* @api public
*/
function enabled(name) {
if (name[name.length - 1] === '*') {
return true;
}
var i;
var len;
for (i = 0, len = createDebug.skips.length; i < len; i++) {
if (createDebug.skips[i].test(name)) {
return false;
}
}
for (i = 0, len = createDebug.names.length; i < len; i++) {
if (createDebug.names[i].test(name)) {
return true;
}
}
return false;
}
/**
* Convert regexp to namespace
*
* @param {RegExp} regxep
* @return {String} namespace
* @api private
*/
function toNamespace(regexp) {
return regexp.toString().substring(2, regexp.toString().length - 2).replace(/\.\*\?$/, '*');
}
/**
* Coerce `val`.
*
* @param {Mixed} val
* @return {Mixed}
* @api private
*/
function coerce(val) {
if (val instanceof Error) {
return val.stack || val.message;
}
return val;
}
createDebug.enable(createDebug.load());
return createDebug;
}
module.exports = setup;
}, {
"ms": 1
}],
4: [function (require, module, exports) {
(function (process) {
/* eslint-env browser */
/**
* This is the web browser implementation of `debug()`.
*/
exports.log = log;
exports.formatArgs = formatArgs;
exports.save = save;
exports.load = load;
exports.useColors = useColors;
exports.storage = localstorage();
/**
* Colors.
*/
exports.colors = ['#0000CC', '#0000FF', '#0033CC', '#0033FF', '#0066CC', '#0066FF', '#0099CC', '#0099FF', '#00CC00', '#00CC33', '#00CC66', '#00CC99', '#00CCCC', '#00CCFF', '#3300CC', '#3300FF', '#3333CC', '#3333FF', '#3366CC', '#3366FF', '#3399CC', '#3399FF', '#33CC00', '#33CC33', '#33CC66', '#33CC99', '#33CCCC', '#33CCFF', '#6600CC', '#6600FF', '#6633CC', '#6633FF', '#66CC00', '#66CC33', '#9900CC', '#9900FF', '#9933CC', '#9933FF', '#99CC00', '#99CC33', '#CC0000', '#CC0033', '#CC0066', '#CC0099', '#CC00CC', '#CC00FF', '#CC3300', '#CC3333', '#CC3366', '#CC3399', '#CC33CC', '#CC33FF', '#CC6600', '#CC6633', '#CC9900', '#CC9933', '#CCCC00', '#CCCC33', '#FF0000', '#FF0033', '#FF0066', '#FF0099', '#FF00CC', '#FF00FF', '#FF3300', '#FF3333', '#FF3366', '#FF3399', '#FF33CC', '#FF33FF', '#FF6600', '#FF6633', '#FF9900', '#FF9933', '#FFCC00', '#FFCC33'];
/**
* Currently only WebKit-based Web Inspectors, Firefox >= v31,
* and the Firebug extension (any Firefox version) are known
* to support "%c" CSS customizations.
*
* TODO: add a `localStorage` variable to explicitly enable/disable colors
*/
// eslint-disable-next-line complexity
function useColors() {
// NB: In an Electron preload script, document will be defined but not fully
// initialized. Since we know we're in Chrome, we'll just detect this case
// explicitly
if (typeof window !== 'undefined' && window.process && (window.process.type === 'renderer' || window.process.__nwjs)) {
return true;
} // Internet Explorer and Edge do not support colors.
if (typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/(edge|trident)\/(\d+)/)) {
return false;
} // Is webkit? http://stackoverflow.com/a/16459606/376773
// document is undefined in react-native: https://github.com/facebook/react-native/pull/1632
return typeof document !== 'undefined' && document.documentElement && document.documentElement.style && document.documentElement.style.WebkitAppearance || // Is firebug? http://stackoverflow.com/a/398120/376773
typeof window !== 'undefined' && window.console && (window.console.firebug || window.console.exception && window.console.table) || // Is firefox >= v31?
// https://developer.mozilla.org/en-US/docs/Tools/Web_Console#Styling_messages
typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/firefox\/(\d+)/) && parseInt(RegExp.$1, 10) >= 31 || // Double check webkit in userAgent just in case we are in a worker
typeof navigator !== 'undefined' && navigator.userAgent && navigator.userAgent.toLowerCase().match(/applewebkit\/(\d+)/);
}
/**
* Colorize log arguments if enabled.
*
* @api public
*/
function formatArgs(args) {
args[0] = (this.useColors ? '%c' : '') + this.namespace + (this.useColors ? ' %c' : ' ') + args[0] + (this.useColors ? '%c ' : ' ') + '+' + module.exports.humanize(this.diff);
if (!this.useColors) {
return;
}
var c = 'color: ' + this.color;
args.splice(1, 0, c, 'color: inherit'); // The final "%c" is somewhat tricky, because there could be other
// arguments passed either before or after the %c, so we need to
// figure out the correct index to insert the CSS into
var index = 0;
var lastC = 0;
args[0].replace(/%[a-zA-Z%]/g, function (match) {
if (match === '%%') {
return;
}
index++;
if (match === '%c') {
// We only are interested in the *last* %c
// (the user may have provided their own)
lastC = index;
}
});
args.splice(lastC, 0, c);
}
/**
* Invokes `console.log()` when available.
* No-op when `console.log` is not a "function".
*
* @api public
*/
function log() {
var _console;
// This hackery is required for IE8/9, where
// the `console.log` function doesn't have 'apply'
return (typeof console === "undefined" ? "undefined" : _typeof(console)) === 'object' && console.log && (_console = console).log.apply(_console, arguments);
}
/**
* Save `namespaces`.
*
* @param {String} namespaces
* @api private
*/
function save(namespaces) {
try {
if (namespaces) {
exports.storage.setItem('debug', namespaces);
} else {
exports.storage.removeItem('debug');
}
} catch (error) {// Swallow
// XXX (@Qix-) should we be logging these?
}
}
/**
* Load `namespaces`.
*
* @return {String} returns the previously persisted debug modes
* @api private
*/
function load() {
var r;
try {
r = exports.storage.getItem('debug');
} catch (error) {} // Swallow
// XXX (@Qix-) should we be logging these?
// If debug isn't set in LS, and we're in Electron, try to load $DEBUG
if (!r && typeof process !== 'undefined' && 'env' in process) {
r = process.env.DEBUG;
}
return r;
}
/**
* Localstorage attempts to return the localstorage.
*
* This is necessary because safari throws
* when a user disables cookies/localstorage
* and you attempt to access it.
*
* @return {LocalStorage}
* @api private
*/
function localstorage() {
try {
// TVMLKit (Apple TV JS Runtime) does not have a window object, just localStorage in the global context
// The Browser also has localStorage in the global context.
return localStorage;
} catch (error) {// Swallow
// XXX (@Qix-) should we be logging these?
}
}
module.exports = require('./common')(exports);
var formatters = module.exports.formatters;
/**
* Map %j to `JSON.stringify()`, since no Web Inspectors do that by default.
*/
formatters.j = function (v) {
try {
return JSON.stringify(v);
} catch (error) {
return '[UnexpectedJSONParseError]: ' + error.message;
}
};
}).call(this, require('_process'));
}, {
"./common": 3,
"_process": 2
}]
}, {}, [4])(4);
});

Some files were not shown because too many files have changed in this diff Show More