不要怂,就是干,撸起袖子干!

Commit 2f7ac2d8 by Michael Kaufman Committed by Felix Becker

7425 Association identifiers in ordering and groups (#7454)

1 parent 2909ec1e
......@@ -5,7 +5,7 @@
- [FIXED] Show a reasonable message when using renameColumn with a missing column [#6606](https://github.com/sequelize/sequelize/issues/6606)
- [PERFORMANCE] more efficient array handing for certain large queries [#7175](https://github.com/sequelize/sequelize/pull/7175)
- [FIXED] Add `unique` indexes defined via options to `rawAttributes` [#7196]
- [FIXED] Removed support where `order` value is string and interpreted as `Sequelize.literal()`. [#6935](https://github.com/sequelize/sequelize/issues/6935)
- [REMOVED] Removed support where `order` value is string and interpreted as `Sequelize.literal()`. [#6935](https://github.com/sequelize/sequelize/issues/6935)
- [CHANGED] `DataTypes.DATE` to use `DATETIMEOFFSET` [MSSQL] [#5403](https://github.com/sequelize/sequelize/issues/5403)
- [FIXED] Properly pass options to `sequelize.query` in `removeColumn` [MSSQL] [#7193](https://github.com/sequelize/sequelize/pull/7193)
- [FIXED] Updating `VIRTUAL` field throw `ER_EMPTY_QUERY` [#6356](https://github.com/sequelize/sequelize/issues/6356)
......@@ -63,6 +63,8 @@
- [FIXED] `bulkCreate` now runs in O(N) time instead of O(N^2) time. [#4247](https://github.com/sequelize/sequelize/issues/4247)
- [FIXED] Passing parameters to model getters [#7404](https://github.com/sequelize/sequelize/issues/7404)
- [FIXED] Model.validate runs validation hooks by default [#7182](https://github.com/sequelize/sequelize/pull/7182)
- [ADDED] Added support for associations aliases in orders and groups. [#7425](https://github.com/sequelize/sequelize/issues/7425)
- [REMOVED] Removes support for `{raw: 'injection goes here'}` for order and group. [#7188](https://github.com/sequelize/sequelize/issues/7188)
## BC breaks:
- Model.validate instance method now runs validation hooks by default. Previously you needed to pass { hooks: true }. You can override this behavior by passing { hooks: false }
......
......@@ -305,8 +305,6 @@ something.findOne({
// will return otherfunction(`col1`, 12, 'lalala') DESC
[sequelize.fn('otherfunction', sequelize.fn('awesomefunction', sequelize.col('col'))), 'DESC']
// will return otherfunction(awesomefunction(`col`)) DESC, This nesting is potentially infinite!
[{ raw: 'otherfunction(awesomefunction(`col`))' }, 'DESC']
// This won't be quoted, but direction will be added
]
})
```
......
......@@ -270,10 +270,10 @@ Project.findAll({ offset: 5, limit: 5 })
`order` takes an array of items to order the query by or a sequelize method. Generally you will want to use a tuple/array of either attribute, direction or just direction to ensure proper escaping.
```js
something.findOne({
Subtask.findAll({
order: [
// Will escape username and validate DESC against a list of valid direction parameters
['username', 'DESC'],
['title', 'DESC'],
// Will order by max(age)
sequelize.fn('max', sequelize.col('age')),
......@@ -284,22 +284,38 @@ something.findOne({
// Will order by otherfunction(`col1`, 12, 'lalala') DESC
[sequelize.fn('otherfunction', sequelize.col('col1'), 12, 'lalala'), 'DESC'],
// Will order by name on an associated User
[User, 'name', 'DESC'],
// Will order an associated model's created_at using the model name as the association's name.
[Task, 'createdAt', 'DESC'],
// Will order by name on an associated User aliased as Friend
[{model: User, as: 'Friend'}, 'name', 'DESC'],
// Will order through an associated model's created_at using the model names as the associations' names.
[Task, Project, 'createdAt', 'DESC'],
// Will order by name on a nested associated Company of an associated User
[User, Company, 'name', 'DESC'],
// Will order by an associated model's created_at using the name of the association.
['Task', 'createdAt', 'DESC'],
// Will order by a nested associated model's created_at using the names of the associations.
['Task', 'Project', 'createdAt', 'DESC'],
// Will order by an associated model's created_at using an association object. (preferred method)
[Subtask.associations.Task, 'createdAt', 'DESC'],
// Will order by a nested associated model's created_at using association objects. (preferred method)
[Subtask.associations.Task, Task.associations.Project, 'createdAt', 'DESC'],
// Will order by an associated model's created_at using a simple association object.
[{model: Task, as: 'Task'}, 'createdAt', 'DESC'],
// Will order by a nested associated model's created_at simple association objects.
[{model: Task, as: 'Task'}, {model: Project, as: 'Project'}, 'createdAt', 'DESC']
]
// Will order by max age descending
order: sequelize.literal('max(age) DESC')
// Will order by max age ascencding assuming ascencding is the default order when direction is omitted
// Will order by max age ascending assuming ascending is the default order when direction is omitted
order: sequelize.fn('max', sequelize.col('age'))
// Will order by age ascencding assuming ascencding is the default order when direction is omitted
// Will order by age ascending assuming ascending is the default order when direction is omitted
order: sequelize.col('age')
})
```
......@@ -7,6 +7,7 @@ const DataTypes = require('../../data-types');
const util = require('util');
const _ = require('lodash');
const Dottie = require('dottie');
const Association = require('../../associations/base');
const BelongsTo = require('../../associations/belongs-to');
const BelongsToMany = require('../../associations/belongs-to-many');
const HasMany = require('../../associations/has-many');
......@@ -600,7 +601,8 @@ const QueryGenerator = {
Strings: should proxy to quoteIdentifiers
Arrays:
* Expects array in the form: [<model> (optional), <model> (optional),... String, String (optional)]
Each <model> can be a model or an object {model: Model, as: String}, matching include
Each <model> can be a model, or an object {model: Model, as: String}, matching include, or an
association object, or the name of an association.
* Zero or more models can be included in the array and are used to trace a path through the tree of
included nested associations. This produces the correct table name for the ORDER BY/GROUP BY SQL
and quotes it.
......@@ -617,72 +619,139 @@ const QueryGenerator = {
potentially also be used for other places where we want to be able to call SQL functions (e.g. as default values)
@private
*/
quote(obj, parent, force, connector) {
quote(collection, parent, connector) {
// init
const validOrderOptions = [
'ASC',
'DESC',
'ASC NULLS LAST',
'DESC NULLS LAST',
'ASC NULLS FIRST',
'DESC NULLS FIRST',
'NULLS FIRST',
'NULLS LAST'
];
// default
connector = connector || '.';
if (_.isString(obj)) {
return this.quoteIdentifiers(obj, force);
} else if (Array.isArray(obj)) {
// just quote as identifiers if string
if (typeof collection === 'string'){
return this.quoteIdentifiers(collection);
} else if (Array.isArray(collection)) {
// iterate through the collection and mutate objects into associations
collection.forEach((item, index) => {
const previous = collection[index - 1];
let previousAssociation;
let previousModel;
// set the previous as the parent when previous is undefined or the target of the association
if(!previous && parent !== undefined){
previousModel = parent;
} else if (previous && previous instanceof Association) {
previousAssociation = previous;
previousModel = previous.target;
}
// if the previous item is a model, then attempt getting an association
if (previousModel && previousModel.prototype instanceof Model) {
let model;
let as;
if (typeof item === 'function' && item.prototype instanceof Model) {
// set
model = item;
} else if (_.isPlainObject(item) && item.model && item.model.prototype instanceof Model) {
// set
model = item.model;
as = item.as;
}
if (model) {
// set the as to either the through name or the model name
if (!as && previousAssociation && previousAssociation instanceof Association && previousAssociation.through && previousAssociation.through.model === model) {
// get from previous association
item = new Association(previousModel, model, {
as: model.name
});
} else {
// get association from previous model
item = previousModel.getAssociationForAlias(model, as);
// attempt to use the model name if the item is still null
if (!item) {
item = previousModel.getAssociationForAlias(model, model.name);
}
}
// make sure we have an association
if (!(item instanceof Association)) {
throw new Error(util.format('Unable to find a valid association for model, \'%s\'', model.name));
}
}
}
if (typeof item === 'string') {
// get order index
const orderIndex = validOrderOptions.indexOf(item.toUpperCase());
// see if this is an order
if (index > 0 && orderIndex !== -1) {
item = this.sequelize.literal(' ' + validOrderOptions[orderIndex]);
} else if (previousModel && previousModel.prototype instanceof Model) {
// only go down this path if we have preivous model and check only once
if (previousModel.associations !== undefined && previousModel.associations[item]) {
// convert the item to an association
item = previousModel.associations[item];
} else if (previousModel.rawAttributes !== undefined && previousModel.rawAttributes[item] && item !== previousModel.rawAttributes[item].field) {
// convert the item attribute from it's alias
item = previousModel.rawAttributes[item].field;
}
}
}
collection[index] = item;
}, this);
// loop through array, adding table names of models to quoted
// (checking associations to see if names should be singularised or not)
const len = obj.length;
const collectionLength = collection.length;
const tableNames = [];
let parentAssociation;
let item;
let model;
let as;
let association;
let i = 0;
for (i = 0; i < len - 1; i++) {
item = obj[i];
if (item._modelAttribute || _.isString(item) || item instanceof Utils.SequelizeMethod || 'raw' in item) {
for (i = 0; i < collectionLength - 1; i++) {
item = collection[i];
if (typeof item === 'string' || item._modelAttribute || item instanceof Utils.SequelizeMethod) {
break;
} else if (item instanceof Association) {
tableNames[i] = item.as;
}
}
if (typeof item === 'function' && item.prototype instanceof Model) {
model = item;
as = undefined;
} else {
model = item.model;
as = item.as;
}
// check if model provided is through table
if (!as && parentAssociation && parentAssociation.through && parentAssociation.through.model === model) {
association = {as: model.name};
} else {
// find applicable association for linking parent to this model
association = parent.getAssociationForAlias(model, as);
}
// start building sql
let sql = '';
if (association) {
tableNames[i] = association.as;
parent = model;
parentAssociation = association;
} else {
tableNames[i] = model.tableName;
throw new Error('\'' + tableNames.join(connector) + '\' in order / group clause is not valid association');
}
if (i > 0) {
sql += this.quoteIdentifier(tableNames.join(connector)) + '.';
} else if (typeof collection[0] === 'string' && parent) {
sql += this.quoteIdentifier(parent.name) + '.';
}
// add 1st string as quoted, 2nd as unquoted raw
let sql = (i > 0 ? this.quoteIdentifier(tableNames.join(connector)) + '.' : (_.isString(obj[0]) && parent ? this.quoteIdentifier(parent.name) + '.' : '')) + this.quote(obj[i], parent, force);
if (i < len - 1) {
if (obj[i + 1] instanceof Utils.SequelizeMethod) {
sql += this.handleSequelizeMethod(obj[i + 1]);
} else {
sql += ' ' + obj[i + 1];
}
}
// loop through everything past i and append to the sql
collection.slice(i).forEach((collectionItem) => {
sql += this.quote(collectionItem, parent, connector);
}, this);
return sql;
} else if (obj._modelAttribute) {
return this.quoteTable(obj.Model.name) + '.' + obj.fieldName;
} else if (obj instanceof Utils.SequelizeMethod) {
return this.handleSequelizeMethod(obj);
} else if (_.isObject(obj) && 'raw' in obj) {
return obj.raw;
} else if (collection._modelAttribute) {
return this.quoteTable(collection.Model.name) + '.' + this.quoteIdentifier(collection.fieldName);
} else if (collection instanceof Utils.SequelizeMethod) {
return this.handleSequelizeMethod(collection);
} else if (_.isPlainObject(collection) && collection.raw) {
// simple objects with raw is no longer supported
throw new Error('The `{raw: "..."}` syntax is no longer supported. Use `sequelize.literal` instead.');
} else {
throw new Error('Unknown structure passed to order / group: ' + JSON.stringify(obj));
throw new Error('Unknown structure passed to order / group: ' + util.inspect(collection));
}
},
......@@ -1483,47 +1552,29 @@ const QueryGenerator = {
getQueryOrders(options, model, subQuery) {
const mainQueryOrder = [];
const subQueryOrder = [];
const validOrderOptions = [
'ASC',
'DESC',
'ASC NULLS LAST',
'DESC NULLS LAST',
'ASC NULLS FIRST',
'DESC NULLS FIRST',
'NULLS FIRST',
'NULLS LAST'
];
const validateOrder = order => {
if (order instanceof Utils.SequelizeMethod) {
return;
}
if (!_.includes(validOrderOptions, order.toUpperCase())) {
throw new Error(util.format('Order must be \'ASC\' or \'DESC\', \'%s\' given', order));
}
};
if (Array.isArray(options.order)) {
for (const order of options.order) {
if (Array.isArray(order) && _.size(order) > 1) {
if (typeof order[0] === 'function' && order[0].prototype instanceof Model || typeof order[0].model === 'function' && order[0].model.prototype instanceof Model) {
if (_.isString(order[order.length - 2])) {
validateOrder(_.last(order));
}
} else {
validateOrder(_.last(order));
}
for (let order of options.order) {
// wrap if not array
if (!Array.isArray(order)) {
order = [order];
}
if (subQuery && (Array.isArray(order) && !(typeof order[0] === 'function' && order[0].prototype instanceof Model) && !(order[0] && typeof order[0].model === 'function' && order[0].model.prototype instanceof Model))) {
subQueryOrder.push(this.quote(order, model, false, '->'));
if (
subQuery
&& Array.isArray(order)
&& order[0]
&& !(order[0] instanceof Association)
&& !(typeof order[0] === 'function' && order[0].prototype instanceof Model)
&& !(typeof order[0].model === 'function' && order[0].model.prototype instanceof Model)
&& !(typeof order[0] === 'string' && model && model.associations !== undefined && model.associations[order[0]])
) {
subQueryOrder.push(this.quote(order, model, '->'));
}
mainQueryOrder.push(this.quote(order, model, false, '->'));
mainQueryOrder.push(this.quote(order, model, '->'));
}
} else if (options.order instanceof Utils.SequelizeMethod){
const sql = this.quote(options.order, model, false, '->');
const sql = this.quote(options.order, model, '->');
if (subQuery) {
subQueryOrder.push(sql);
}
......
......@@ -183,39 +183,6 @@ function mapOptionFieldNames(options, Model) {
options.where = mapWhereFieldNames(options.where, Model);
}
if (Array.isArray(options.order)) {
for (const oGroup of options.order) {
let OrderModel;
let attr;
let attrOffset;
if (Array.isArray(oGroup)) {
OrderModel = Model;
// Check if we have ['attr', 'DESC'] or [Model, 'attr', 'DESC']
if (typeof oGroup[oGroup.length - 2] === 'string') {
attrOffset = 2;
// Assume ['attr'], [Model, 'attr'] or [seq.fn('somefn', 1), 'DESC']
} else {
attrOffset = 1;
}
attr = oGroup[oGroup.length - attrOffset];
if (oGroup.length > attrOffset) {
OrderModel = oGroup[oGroup.length - (attrOffset + 1)];
if (OrderModel.model) {
OrderModel = OrderModel.model;
}
}
if (OrderModel.rawAttributes && OrderModel.rawAttributes[attr] && attr !== OrderModel.rawAttributes[attr].field) {
oGroup[oGroup.length - attrOffset] = OrderModel.rawAttributes[attr].field;
}
}
}
}
return options;
}
exports.mapOptionFieldNames = mapOptionFieldNames;
......
......@@ -73,23 +73,6 @@ describe(Support.getTestDialectTeaser('Model'), function() {
return this.sequelize.sync({force: true});
});
it('should throw when 2nd order argument is not ASC or DESC', function () {
return expect(this.User.findAll({
order: [
['id', ';DELETE YOLO INJECTIONS']
]
})).to.eventually.be.rejectedWith(Error, 'Order must be \'ASC\' or \'DESC\', \';DELETE YOLO INJECTIONS\' given');
});
it('should throw with include when last order argument is not ASC or DESC', function () {
return expect(this.User.findAll({
include: [this.Group],
order: [
[this.Group, 'id', ';DELETE YOLO INJECTIONS']
]
})).to.eventually.be.rejectedWith(Error, 'Order must be \'ASC\' or \'DESC\', \';DELETE YOLO INJECTIONS\' given');
});
if (current.dialect.supports['ORDER NULLS']) {
it('should not throw with on NULLS LAST/NULLS FIRST', function () {
return this.User.findAll({
......
......@@ -212,11 +212,6 @@ if (dialect === 'mysql') {
context: QueryGenerator,
needsSequelize: true
}, {
title: 'raw arguments are neither quoted nor escaped',
arguments: ['myTable', {order: [[{raw: 'f1(f2(id))'}, 'DESC']]}],
expectation: 'SELECT * FROM `myTable` ORDER BY f1(f2(id)) DESC;',
context: QueryGenerator
}, {
title: 'functions can take functions as arguments',
arguments: ['myTable', function(sequelize) {
return {
......@@ -577,6 +572,7 @@ if (dialect === 'mysql') {
}
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation);
});
......
......@@ -279,25 +279,20 @@ if (dialect.match(/^postgres/)) {
expectation: 'SELECT * FROM "myTable" AS "myTable" ORDER BY "myTable"."id" DESC;',
context: QueryGenerator,
needsSequelize: true
},{
arguments: ['myTable', {order: [['id', 'DESC'], ['name']]}, function(sequelize) {return sequelize.define('myTable', {});}],
}, {
arguments: ['myTable', {order: [['id', 'DESC'], ['name']]}, function(sequelize) {return sequelize.define('myTable', {});}],
expectation: 'SELECT * FROM "myTable" AS "myTable" ORDER BY "myTable"."id" DESC, "myTable"."name";',
context: QueryGenerator,
needsSequelize: true
},{
}, {
title: 'uses limit 0',
arguments: ['myTable', {limit: 0}],
expectation: 'SELECT * FROM "myTable" LIMIT 0;',
context: QueryGenerator
}, {
title: 'uses offset 0',
arguments: ['myTable', {offset: 0}],
expectation: 'SELECT * FROM "myTable" OFFSET 0;',
context: QueryGenerator
}, {
title: 'raw arguments are neither quoted nor escaped',
arguments: ['myTable', {order: [[{raw: 'f1(f2(id))'},'DESC']]}],
expectation: 'SELECT * FROM "myTable" ORDER BY f1(f2(id)) DESC;',
title: 'uses offset 0',
arguments: ['myTable', {offset: 0}],
expectation: 'SELECT * FROM "myTable" OFFSET 0;',
context: QueryGenerator
}, {
title: 'sequelize.where with .fn as attribute and default comparator',
......@@ -325,7 +320,7 @@ if (dialect.match(/^postgres/)) {
expectation: 'SELECT * FROM "myTable" WHERE (LOWER("user"."name") LIKE \'%t%\' AND "myTable"."type" = 1);',
context: QueryGenerator,
needsSequelize: true
},{
}, {
title: 'functions can take functions as arguments',
arguments: ['myTable', function(sequelize) {
return {
......@@ -923,6 +918,7 @@ if (dialect.match(/^postgres/)) {
}
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation);
});
......
......@@ -198,11 +198,6 @@ if (dialect === 'sqlite') {
context: QueryGenerator,
needsSequelize: true
}, {
title: 'raw arguments are neither quoted nor escaped',
arguments: ['myTable', {order: [[{raw: 'f1(f2(id))'}, 'DESC']]}],
expectation: 'SELECT * FROM `myTable` ORDER BY f1(f2(id)) DESC;',
context: QueryGenerator
}, {
title: 'sequelize.where with .fn as attribute and default comparator',
arguments: ['myTable', function(sequelize) {
return {
......@@ -536,6 +531,7 @@ if (dialect === 'sqlite') {
}
QueryGenerator.options = _.assign(context.options, { timezone: '+00:00' });
QueryGenerator._dialect = this.sequelize.dialect;
QueryGenerator.sequelize = this.sequelize;
var conditions = QueryGenerator[suiteTitle].apply(QueryGenerator, test.arguments);
expect(conditions).to.deep.equal(test.expectation);
});
......
'use strict';
/* jshint -W110 */
const util = require('util');
const chai = require('chai');
const expect = chai.expect;
const Support = require(__dirname + '/../support');
const DataTypes = require(__dirname + '/../../../lib/data-types');
const Model = require(__dirname + '/../../../lib/model');
const expectsql = Support.expectsql;
const current = Support.sequelize;
const sql = current.dialect.QueryGenerator;
// Notice: [] will be replaced by dialect specific tick/quote character when there is not dialect specific expectation but only a default expectation
describe(Support.getTestDialectTeaser('SQL'), () => {
describe('order', () => {
const testsql = (options, expectation) => {
const model = options.model;
it(util.inspect(options, {depth: 2}), () => {
return expectsql(
sql.selectQuery(
options.table || model && model.getTableName(),
options,
options.model
),
expectation
);
});
};
// models
const User = Support.sequelize.define('User', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
name: {
type: DataTypes.STRING,
field: 'name',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'user',
timestamps: true
});
const Project = Support.sequelize.define('Project', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
name: {
type: DataTypes.STRING,
field: 'name',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'project',
timestamps: true
});
const ProjectUser = Support.sequelize.define('ProjectUser', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
userId: {
type: DataTypes.INTEGER,
field: 'user_id',
allowNull: false
},
projectId: {
type: DataTypes.INTEGER,
field: 'project_id',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'project_user',
timestamps: true
});
const Task = Support.sequelize.define('Task', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
name: {
type: DataTypes.STRING,
field: 'name',
allowNull: false
},
projectId: {
type: DataTypes.INTEGER,
field: 'project_id',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'task',
timestamps: true
});
const Subtask = Support.sequelize.define('Subtask', {
id: {
type: DataTypes.INTEGER,
primaryKey: true,
autoIncrement: true,
field: 'id'
},
name: {
type: DataTypes.STRING,
field: 'name',
allowNull: false
},
taskId: {
type: DataTypes.INTEGER,
field: 'task_id',
allowNull: false
},
createdAt: {
type: DataTypes.DATE,
field: 'created_at',
allowNull: false
},
updatedAt: {
type: DataTypes.DATE,
field: 'updated_at',
allowNull: true
}
}, {
tableName: 'subtask',
timestamps: true
});
// Relations
User.belongsToMany(Project, {
as: 'ProjectUserProjects',
through: ProjectUser,
foreignKey: 'user_id',
otherKey: 'project_id'
});
Project.belongsToMany(User, {
as: 'ProjectUserUsers',
through: ProjectUser,
foreignKey: 'project_id',
otherKey: 'user_id'
});
Project.hasMany(Task, {
as: 'Tasks',
foreignKey: 'project_id'
});
ProjectUser.belongsTo(User, {
as: 'User',
foreignKey: 'user_id'
});
ProjectUser.belongsTo(User, {
as: 'Project',
foreignKey: 'project_id'
});
Task.belongsTo(Project, {
as: 'Project',
foreignKey: 'project_id'
});
Task.hasMany(Subtask, {
as: 'Subtasks',
foreignKey: 'task_id'
});
Subtask.belongsTo(Task, {
as: 'Task',
foreignKey: 'task_id'
});
testsql({
model: Subtask,
attributes: [
'id',
'name',
'createdAt'
],
include: Model._validateIncludedElements({
include: [
{
association: Subtask.associations.Task,
required: true,
attributes: [
'id',
'name',
'createdAt'
],
include: [
{
association: Task.associations.Project,
required: true,
attributes: [
'id',
'name',
'createdAt'
]
}
]
}
],
model: Subtask
}).include,
order: [
// order with multiple simple association syntax with direction
[
{
model: Task,
as: 'Task'
},
{
model: Project,
as: 'Project'
},
'createdAt',
'ASC'
],
// order with multiple simple association syntax without direction
[
{
model: Task,
as: 'Task'
},
{
model: Project,
as: 'Project'
},
'createdAt'
],
// order with simple association syntax with direction
[
{
model: Task,
as: 'Task'
},
'createdAt',
'ASC'
],
// order with simple association syntax without direction
[
{
model: Task,
as: 'Task'
},
'createdAt'
],
// through model object as array with direction
[Task, Project, 'createdAt', 'ASC'],
// through model object as array without direction
[Task, Project, 'createdAt'],
// model object as array with direction
[Task, 'createdAt', 'ASC'],
// model object as array without direction
[Task, 'createdAt'],
// through association object as array with direction
[Subtask.associations.Task, Task.associations.Project, 'createdAt', 'ASC'],
// through association object as array without direction
[Subtask.associations.Task, Task.associations.Project, 'createdAt'],
// association object as array with direction
[Subtask.associations.Task, 'createdAt', 'ASC'],
// association object as array without direction
[Subtask.associations.Task, 'createdAt'],
// through association name order as array with direction
['Task', 'Project', 'createdAt', 'ASC'],
// through association name as array without direction
['Task', 'Project', 'createdAt'],
// association name as array with direction
['Task', 'createdAt', 'ASC'],
// association name as array without direction
['Task', 'createdAt'],
// main order as array with direction
['createdAt', 'ASC'],
// main order as array without direction
['createdAt'],
// main order as string
'createdAt'
]
}, {
default: 'SELECT [Subtask].[id], [Subtask].[name], [Subtask].[createdAt], [Task].[id] AS [Task.id], [Task].[name] AS [Task.name], [Task].[created_at] AS [Task.createdAt], [Task->Project].[id] AS [Task.Project.id], [Task->Project].[name] AS [Task.Project.name], [Task->Project].[created_at] AS [Task.Project.createdAt] FROM [subtask] AS [Subtask] INNER JOIN [task] AS [Task] ON [Subtask].[task_id] = [Task].[id] INNER JOIN [project] AS [Task->Project] ON [Task].[project_id] = [Task->Project].[id] ORDER BY [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Task->Project].[created_at] ASC, [Task->Project].[created_at], [Task].[created_at] ASC, [Task].[created_at], [Subtask].[created_at] ASC, [Subtask].[created_at], [Subtask].[created_at];',
postgres: 'SELECT "Subtask"."id", "Subtask"."name", "Subtask"."createdAt", "Task"."id" AS "Task.id", "Task"."name" AS "Task.name", "Task"."created_at" AS "Task.createdAt", "Task->Project"."id" AS "Task.Project.id", "Task->Project"."name" AS "Task.Project.name", "Task->Project"."created_at" AS "Task.Project.createdAt" FROM "subtask" AS "Subtask" INNER JOIN "task" AS "Task" ON "Subtask"."task_id" = "Task"."id" INNER JOIN "project" AS "Task->Project" ON "Task"."project_id" = "Task->Project"."id" ORDER BY "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Task->Project"."created_at" ASC, "Task->Project"."created_at", "Task"."created_at" ASC, "Task"."created_at", "Subtask"."created_at" ASC, "Subtask"."created_at", "Subtask"."created_at";'
});
describe('Invalid', () => {
it('Error on invalid association', () => {
return expect(Subtask.findAll({
order: [
[Project, 'createdAt', 'ASC']
]
})).to.eventually.be.rejectedWith(Error, 'Unable to find a valid association for model, \'Project\'');
});
it('Error on invalid structure', () => {
return expect(Subtask.findAll({
order: [
[Subtask.associations.Task, 'createdAt', Task.associations.Project, 'ASC']
]
})).to.eventually.be.rejectedWith(Error, 'Unknown structure passed to order / group: Project');
});
it('Error when the order is a string', () => {
return expect(Subtask.findAll({
order: 'i am a silly string'
})).to.eventually.be.rejectedWith(Error, 'Order must be type of array or instance of a valid sequelize method.');
});
it('Error when the order contains a `{raw: "..."}` object', () => {
return expect(Subtask.findAll({
order: [
{
raw: 'this should throw an error'
}
]
})).to.eventually.be.rejectedWith(Error, 'The `{raw: "..."}` syntax is no longer supported. Use `sequelize.literal` instead.');
});
it('Error when the order contains a `{raw: "..."}` object wrapped in an array', () => {
return expect(Subtask.findAll({
order: [
[
{
raw: 'this should throw an error'
}
]
]
})).to.eventually.be.rejectedWith(Error, 'The `{raw: "..."}` syntax is no longer supported. Use `sequelize.literal` instead.');
});
});
});
});
......@@ -195,255 +195,6 @@ suite(Support.getTestDialectTeaser('Utils'), () => {
}
});
});
test('string field order', function() {
expect(Utils.mapOptionFieldNames({
order: 'firstName DESC'
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
}
}))).to.eql({
order: 'firstName DESC'
});
});
test('string in array order', function() {
expect(Utils.mapOptionFieldNames({
order: ['firstName DESC']
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
}
}))).to.eql({
order: ['firstName DESC']
});
});
test('single field alias order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName', 'DESC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
}
}))).to.eql({
order: [['first_name', 'DESC']]
});
});
test('multi field alias order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName', 'DESC'], ['lastName', 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
},
lastName: {
type: DataTypes.STRING,
field: 'last_name'
}
}))).to.eql({
order: [['first_name', 'DESC'], ['last_name', 'ASC']]
});
});
test('multi field alias no direction order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName'], ['lastName']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
},
lastName: {
type: DataTypes.STRING,
field: 'last_name'
}
}))).to.eql({
order: [['first_name'], ['last_name']]
});
});
test('field alias to another field order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName', 'DESC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'lastName'
},
lastName: {
type: DataTypes.STRING,
field: 'firstName'
}
}))).to.eql({
order: [['lastName', 'DESC']]
});
});
test('multi field no alias order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName', 'DESC'], ['lastName', 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
},
lastName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [['firstName', 'DESC'], ['lastName', 'ASC']]
});
});
test('multi field alias sub model order', function() {
const Location = Support.sequelize.define('Location', {
latLong: {
type: DataTypes.STRING,
field: 'lat_long'
}
});
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[Item, Location, 'latLong', 'DESC'], ['lastName', 'ASC']]
}, Support.sequelize.define('User', {
lastName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[Item, Location, 'lat_long', 'DESC'], ['lastName', 'ASC']]
});
});
test('multi field alias sub model no direction order', function() {
const Location = Support.sequelize.define('Location', {
latLong: {
type: DataTypes.STRING,
field: 'lat_long'
}
});
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[Item, Location, 'latLong'], ['lastName', 'ASC']]
}, Support.sequelize.define('User', {
lastName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[Item, Location, 'lat_long'], ['lastName', 'ASC']]
});
});
test('function order', function() {
const fn = Support.sequelize.fn('otherfn', 123);
expect(Utils.mapOptionFieldNames({
order: [[fn, 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[fn, 'ASC']]
});
});
test('function no direction order', function() {
const fn = Support.sequelize.fn('otherfn', 123);
expect(Utils.mapOptionFieldNames({
order: [[fn]]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[fn]]
});
});
test('string no direction order', function() {
expect(Utils.mapOptionFieldNames({
order: [['firstName']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING,
field: 'first_name'
}
}))).to.eql({
order: [['first_name']]
});
});
test('model alias order', function() {
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[{ model: Item, as: 'another'}, 'fontColor', 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
},
lastName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[{ model: Item, as: 'another'}, 'font_color', 'ASC']]
});
});
test('model alias no direction order', function() {
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[{ model: Item, as: 'another'}, 'fontColor']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[{ model: Item, as: 'another'}, 'font_color']]
});
});
test('model alias wrong field order', function() {
const Item = Support.sequelize.define('Item', {
fontColor: {
type: DataTypes.STRING,
field: 'font_color'
}
});
expect(Utils.mapOptionFieldNames({
order: [[{ model: Item, as: 'another'}, 'firstName', 'ASC']]
}, Support.sequelize.define('User', {
firstName: {
type: DataTypes.STRING
}
}))).to.eql({
order: [[{ model: Item, as: 'another'}, 'firstName', 'ASC']]
});
});
});
suite('stack', () => {
......
Markdown is supported
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!