Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: restrict instance methods if no primary key #15108

Merged
Merged
Show file tree
Hide file tree
Changes from 12 commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
297a3cb
feat: removed `whereCollection` from model
frostzt Oct 8, 2022
1be7243
tests(unit): added unit tests for models
frostzt Oct 8, 2022
0fad447
feat: `where()` should throw if the model do not have a primary key
frostzt Oct 9, 2022
a27ae99
fix: `where()` should check for primaryKeyAttributes
frostzt Oct 9, 2022
aa04872
tests: update unit tests for all the methods
frostzt Oct 9, 2022
9bd14ad
tests: update integration tests for all the instance methods
frostzt Oct 9, 2022
e536869
fix(tests): fixed typos in integration tests
frostzt Oct 10, 2022
5439ef8
refactor(tests): removed redundent primary key creation
frostzt Oct 10, 2022
08cb8f5
fix: fetch attribute rather than just check it
frostzt Oct 10, 2022
6f4107b
refactor(tests): removed redundent primary key creation
frostzt Oct 10, 2022
6e8e788
refactor: update the Model to use the name of the table
frostzt Oct 10, 2022
648a933
tests: add integration and unit tests
frostzt Oct 11, 2022
46e4ada
tests: updated tests
frostzt Oct 11, 2022
9697e11
feat: check for query type before calling save
frostzt Oct 14, 2022
3491d6e
tests: updated integration and unit tests
frostzt Oct 14, 2022
c5eb5e4
fix: fix upsert implem
ephys Nov 15, 2022
2e8e13f
Merge branch 'main' into feat/restrict-instance-methods-if-no-primary…
ephys Nov 15, 2022
0d5cefa
fix: fix upsert
ephys Nov 16, 2022
dc6870a
Merge branch 'main' into feat/restrict-instance-methods-if-no-primary…
ephys Nov 16, 2022
7926602
Merge branch 'main' into feat/restrict-instance-methods-if-no-primary…
WikiRik Nov 20, 2022
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
23 changes: 15 additions & 8 deletions src/model.js
Expand Up @@ -895,7 +895,6 @@ Specify a different name for either index to resolve this issue.`);
underscored: false,
paranoid: false,
rejectOnEmpty: false,
whereCollection: null,
schema: '',
schemaDelimiter: '',
defaultScope: {},
Expand Down Expand Up @@ -1778,9 +1777,6 @@ Specify a different name for either index to resolve this issue.`);
options.originalAttributes = this._injectDependentVirtualAttributes(options.attributes);
}

// whereCollection is used for non-primary key updates
this.options.whereCollection = options.where || null;

Utils.mapFinderOptions(options, this);

options = this._paranoidClause(this, options);
Expand Down Expand Up @@ -3513,16 +3509,27 @@ Instead of specifying a Model, either:
* @returns {object}
*/
where(checkVersion) {
if (this.constructor.primaryKeyAttributes.length === 0) {
throw new Error(
`This model instance method needs to be able to identify the entity in a stable way, but the model does not have a primary key attribute definition. Either add a primary key to this model, or use one of the following alternatives:

- instance methods "save", "update", "decrement", "increment": Use the static "update" method instead.
- instance method "reload": Use the static "findOne" method instead.
- instance methods "destroy" and "restore": use the static "destroy" and "restore" methods instead.
`.trim(),
);
}

const where = this.constructor.primaryKeyAttributes.reduce((result, attribute) => {
if (_.isNil(this.get(attribute, { raw: true }))) {
throw new TypeError(`This model instance method needs to be able to identify the entity in a stable way, but this model instance is missing the value of its primary key "${attribute}". Make sure that attribute was not excluded when retrieving the model from the database.`);
}

result[attribute] = this.get(attribute, { raw: true });

return result;
}, {});

if (_.size(where) === 0) {
return this.constructor.options.whereCollection;
}

const versionAttr = this.constructor._versionAttribute;
if (checkVersion && versionAttr) {
where[versionAttr] = this.get(versionAttr, { raw: true });
Expand Down
8 changes: 8 additions & 0 deletions test/integration/instance/decrement.test.js
Expand Up @@ -186,5 +186,13 @@ describe(Support.getTestDialectTeaser('Instance'), () => {

await expect(User.findByPk(1)).to.eventually.have.property('updatedAt').equalTime(oldDate);
});

it('is disallowed if no primary key is present', async function () {
const Foo = this.sequelize.define('Foo', {}, { noPrimaryKey: true });
await Foo.sync({ force: true });

const instance = await Foo.create({});
await expect(instance.decrement()).to.be.rejectedWith('but the model does not have a primary key attribute definition.');
});
});
});
8 changes: 8 additions & 0 deletions test/integration/instance/destroy.test.js
Expand Up @@ -248,6 +248,14 @@ describe(Support.getTestDialectTeaser('Instance'), () => {
expect(user.username).to.equal('bar');
});

it('is disallowed if no primary key is present', async function () {
const Foo = this.sequelize.define('Foo', {}, { noPrimaryKey: true });
await Foo.sync({ force: true });

const instance = await Foo.create({});
await expect(instance.destroy()).to.be.rejectedWith('but the model does not have a primary key attribute definition.');
});

it('allows sql logging of delete statements', async function () {
const UserDelete = this.sequelize.define('UserDelete', {
name: DataTypes.STRING,
Expand Down
8 changes: 8 additions & 0 deletions test/integration/instance/increment.test.js
Expand Up @@ -118,6 +118,14 @@ describe(Support.getTestDialectTeaser('Instance'), () => {
expect(user2.aNumber).to.be.equal(1);
});

it('is disallowed if no primary key is present', async function () {
const Foo = this.sequelize.define('Foo', {}, { noPrimaryKey: true });
await Foo.sync({ force: true });

const instance = await Foo.create({});
await expect(instance.increment()).to.be.rejectedWith('but the model does not have a primary key attribute definition.');
});

it('should still work right with other concurrent updates', async function () {
const user1 = await this.User.findByPk(1);
// Select the user again (simulating a concurrent query)
Expand Down
8 changes: 8 additions & 0 deletions test/integration/instance/reload.test.js
Expand Up @@ -107,6 +107,14 @@ describe(Support.getTestDialectTeaser('Instance'), () => {
expect(updatedUser.username).to.equal('Doe John');
});

it('is disallowed if no primary key is present', async function () {
const Foo = this.sequelize.define('Foo', {}, { noPrimaryKey: true });
await Foo.sync({ force: true });

const instance = await Foo.create({});
await expect(instance.reload()).to.be.rejectedWith('but the model does not have a primary key attribute definition.');
});

it('should support updating a subset of attributes', async function () {
const user1 = await this.User.create({
aNumber: 1,
Expand Down
36 changes: 36 additions & 0 deletions test/integration/instance/restore.test.js
@@ -0,0 +1,36 @@
'use strict';

const chai = require('chai');

const expect = chai.expect;
const Support = require('../support');
const { DataTypes } = require('@sequelize/core');
const sinon = require('sinon');

const current = Support.sequelize;

describe(Support.getTestDialectTeaser('Instance'), () => {
ephys marked this conversation as resolved.
Show resolved Hide resolved
before(function () {
this.clock = sinon.useFakeTimers();
});

afterEach(function () {
this.clock.reset();
});

after(function () {
this.clock.restore();
ephys marked this conversation as resolved.
Show resolved Hide resolved
});

describe('restore', () => {
ephys marked this conversation as resolved.
Show resolved Hide resolved
it('is disallowed if no primary key is present', async function () {
const User = this.sequelize.define('User', {
name: { type: DataTypes.STRING },
}, { noPrimaryKey: true, paranoid: true });
await User.sync({ force: true });

const instance = await User.create({});
await expect(instance.restore()).to.be.rejectedWith('but the model does not have a primary key attribute definition.');
});
});
});
8 changes: 8 additions & 0 deletions test/integration/instance/save.test.js
Expand Up @@ -129,6 +129,14 @@ describe(Support.getTestDialectTeaser('Instance'), () => {
});
});

it('is disallowed if no primary key is present', async function () {
const Foo = this.sequelize.define('Foo', {}, { noPrimaryKey: true });
await Foo.sync({ force: true });

const instance = await Foo.create({}, { isNewRecord: true });
ephys marked this conversation as resolved.
Show resolved Hide resolved
await expect(instance.save()).to.be.rejectedWith('but the model does not have a primary key attribute definition.');
});

describe('hooks', () => {
it('should update attributes added in hooks when default fields are used', async function () {
const User = this.sequelize.define(`User${Support.rand()}`, {
Expand Down
8 changes: 8 additions & 0 deletions test/integration/instance/update.test.js
Expand Up @@ -182,6 +182,14 @@ describe(Support.getTestDialectTeaser('Instance'), () => {
expect(user.validateSideEffect).not.to.be.ok;
});

it('is disallowed if no primary key is present', async function () {
WikiRik marked this conversation as resolved.
Show resolved Hide resolved
const Foo = this.sequelize.define('Foo', {}, { noPrimaryKey: true });
await Foo.sync({ force: true });

const instance = await Foo.create({});
await expect(instance.update()).to.be.rejectedWith('but the model does not have a primary key attribute definition.');
});

describe('hooks', () => {
it('should update attributes added in hooks when default fields are used', async function () {
const User = this.sequelize.define(`User${Support.rand()}`, {
Expand Down
7 changes: 7 additions & 0 deletions test/unit/instance/decrement.test.js
Expand Up @@ -11,6 +11,13 @@ const sinon = require('sinon');

describe(Support.getTestDialectTeaser('Instance'), () => {
describe('decrement', () => {
it('is not allowed if the instance does not have a primary key defined', async () => {
const User = current.define('User', {});
const instance = User.build({});

await expect(instance.decrement()).to.be.rejectedWith('but this model instance is missing the value of its primary key');
});

describe('options tests', () => {
let stub; let instance;
const Model = current.define('User', {
Expand Down
7 changes: 7 additions & 0 deletions test/unit/instance/destroy.test.js
Expand Up @@ -11,6 +11,13 @@ const sinon = require('sinon');

describe(Support.getTestDialectTeaser('Instance'), () => {
describe('destroy', () => {
it('is not allowed if the instance does not have a primary key defined', async () => {
const User = current.define('User', {});
const instance = User.build({});

await expect(instance.destroy()).to.be.rejectedWith('but this model instance is missing the value of its primary key');
});

describe('options tests', () => {
let stub; let instance;
const Model = current.define('User', {
Expand Down
7 changes: 7 additions & 0 deletions test/unit/instance/increment.test.js
Expand Up @@ -11,6 +11,13 @@ const sinon = require('sinon');

describe(Support.getTestDialectTeaser('Instance'), () => {
describe('increment', () => {
it('is not allowed if the instance does not have a primary key defined', async () => {
const User = current.define('User', {});
const instance = User.build({});

await expect(instance.increment()).to.be.rejectedWith('but this model instance is missing the value of its primary key');
});

describe('options tests', () => {
let stub; let instance;
const Model = current.define('User', {
Expand Down
7 changes: 7 additions & 0 deletions test/unit/instance/reload.test.js
Expand Up @@ -11,6 +11,13 @@ const sinon = require('sinon');

describe(Support.getTestDialectTeaser('Instance'), () => {
describe('reload', () => {
it('is not allowed if the instance does not have a primary key defined', async () => {
const User = current.define('User', {});
const instance = User.build({});

await expect(instance.reload()).to.be.rejectedWith('but this model instance is missing the value of its primary key');
});

describe('options tests', () => {
let stub; let instance;
const Model = current.define('User', {
Expand Down
7 changes: 7 additions & 0 deletions test/unit/instance/restore.test.js
Expand Up @@ -11,6 +11,13 @@ const sinon = require('sinon');

describe(Support.getTestDialectTeaser('Instance'), () => {
describe('restore', () => {
it('is not allowed if the instance does not have a primary key defined', async () => {
WikiRik marked this conversation as resolved.
Show resolved Hide resolved
const User = current.define('User', {});
const instance = User.build({});

await expect(instance.restore()).to.be.rejectedWith('but this model instance is missing the value of its primary key');
});

describe('options tests', () => {
let stub; let instance;
const Model = current.define('User', {
Expand Down
8 changes: 4 additions & 4 deletions test/unit/instance/save.test.js
Expand Up @@ -11,11 +11,11 @@ const sinon = require('sinon');

describe(Support.getTestDialectTeaser('Instance'), () => {
describe('save', () => {
it('should disallow saves if no primary key values is present', async () => {
const Model = current.define('User', {});
const instance = Model.build({}, { isNewRecord: false });
it('is not allowed if the instance does not have a primary key defined', async () => {
const User = current.define('User', {});
const instance = User.build({});
ephys marked this conversation as resolved.
Show resolved Hide resolved

await expect(instance.save()).to.be.rejected;
await expect(instance.save()).to.be.rejectedWith('but this model instance is missing the value of its primary key');
});

describe('options tests', () => {
Expand Down
19 changes: 19 additions & 0 deletions test/unit/instance/update.test.js
@@ -0,0 +1,19 @@
'use strict';

const chai = require('chai');

const expect = chai.expect;
const Support = require('../support');

const current = Support.sequelize;

describe(Support.getTestDialectTeaser('Instance'), () => {
ephys marked this conversation as resolved.
Show resolved Hide resolved
describe('update', () => {
it('is not allowed if the primary key is not defined', async () => {
const User = current.define('User', {});
const instance = User.build({}, { isNewRecord: false });

await expect(instance.update()).to.be.rejectedWith('You attempted to save an instance with no primary key, this is not allowed since');
});
});
});