Skip to content

Commit 64c78c7

Browse files
fix: handle foreign key toward non primary key (#585)
1 parent d410f0b commit 64c78c7

File tree

3 files changed

+181
-17
lines changed

3 files changed

+181
-17
lines changed

src/services/belongs-to-updater.js

Lines changed: 21 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,6 @@ const _ = require('lodash');
22
const orm = require('../utils/orm');
33
const associationRecord = require('../utils/association-record');
44

5-
// WORKAROUND: Make the hasOne associations update work while waiting
6-
// for the Sequelize 4 release with the fix of the following
7-
// issue: https://github.com/sequelize/sequelize/issues/6069
8-
const getTargetKey = async (pk, association) => {
9-
let targetKey = pk;
10-
11-
if (association.associationType === 'HasOne') {
12-
targetKey = await associationRecord.get(association.target, pk);
13-
}
14-
15-
return targetKey;
16-
};
17-
185
class BelongsToUpdater {
196
constructor(model, assoc, opts, params, data) {
207
this.model = model;
@@ -24,15 +11,34 @@ class BelongsToUpdater {
2411
this.data = data;
2512
}
2613

14+
// WORKAROUND: Make the hasOne associations update work while waiting
15+
// for the Sequelize 4 release with the fix of the following
16+
// issue: https://github.com/sequelize/sequelize/issues/6069
17+
async _getTargetKey(association) {
18+
const pk = this.data.data.id;
19+
let targetKey = pk;
20+
21+
if (association.associationType === 'HasOne' || association.targetKey !== 'id') {
22+
const record = await associationRecord.get(association.target, pk);
23+
if (association.associationType === 'HasOne') {
24+
targetKey = record;
25+
} else if (association.targetKey !== 'id') {
26+
// NOTICE: special use case with foreign key non pointing to a primary key
27+
targetKey = record[association.targetKey];
28+
}
29+
}
30+
31+
return targetKey;
32+
}
33+
2734
async perform() {
2835
const { associationName, recordId } = this.params;
2936
const record = await orm.findRecord(this.model, recordId);
3037
const association = Object.values(this.model.associations)
3138
.find((a) => a.associationAccessor === associationName);
3239

3340
if (association && this.data.data) {
34-
const pk = this.data.data.id;
35-
const targetKey = await getTargetKey(pk, association);
41+
const targetKey = await this._getTargetKey(association);
3642

3743
// NOTICE: Enable model hooks to change fields values during an association update.
3844
const options = { fields: null };

src/services/resource-creator.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ const Interface = require('forest-express');
44
const { ErrorHTTP422 } = require('./errors');
55
const ResourceGetter = require('./resource-getter');
66
const CompositeKeysManager = require('./composite-keys-manager');
7+
const associationRecord = require('../utils/association-record');
78

89
class ResourceCreator {
910
constructor(model, params) {
@@ -12,11 +13,23 @@ class ResourceCreator {
1213
this.schema = Interface.Schemas.schemas[model.name];
1314
}
1415

16+
async _getTargetKey(name, association) {
17+
const pk = this.params[name];
18+
19+
let targetKey = pk;
20+
if (association.targetKey !== 'id') {
21+
const record = await associationRecord.get(association.target, pk);
22+
targetKey = record[association.targetKey];
23+
}
24+
return targetKey;
25+
}
26+
1527
_makePromisesBeforeSave(record) {
16-
return (promises, [name, association]) => {
28+
return async (promises, [name, association]) => {
1729
if (association.associationType === 'BelongsTo') {
1830
const setterName = `set${_.upperFirst(name)}`;
19-
const promise = record[setterName](this.params[name], { save: false });
31+
const targetKey = await this._getTargetKey(name, association);
32+
const promise = record[setterName](targetKey, { save: false });
2033
promises.push(promise);
2134
}
2235
return promises;

test/databases.test.js

Lines changed: 145 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,16 @@ const HasManyDissociator = require('../src/services/has-many-dissociator');
114114
fieldBad: { type: Sequelize.REAL }, // NOTICE: not supported yet.
115115
});
116116

117+
models.owner = sequelize.define('owner', {
118+
name: { type: Sequelize.STRING },
119+
ownerId: { type: Sequelize.INTEGER, allowNull: false, unique: true },
120+
});
121+
122+
models.project = sequelize.define('project', {
123+
name: { type: Sequelize.STRING },
124+
ownerId: { type: Sequelize.INTEGER },
125+
});
126+
117127
models.address.belongsTo(models.user);
118128
models.addressWithUserAlias.belongsTo(models.user, { as: 'userAlias' });
119129
models.user.hasMany(models.address);
@@ -123,6 +133,20 @@ const HasManyDissociator = require('../src/services/has-many-dissociator');
123133
models.member.hasOne(models.membership);
124134
models.member.hasMany(models.friend);
125135
models.friend.belongsTo(models.member);
136+
models.project.belongsTo(models.owner, {
137+
foreignKey: {
138+
name: 'ownerIdKey',
139+
field: 'owner_id',
140+
},
141+
targetKey: 'ownerId',
142+
});
143+
models.owner.hasMany(models.project, {
144+
foreignKey: {
145+
name: 'ownerIdKey',
146+
field: 'owner_id',
147+
},
148+
sourceKey: 'ownerId',
149+
});
126150

127151
Interface.Schemas = {
128152
schemas: {
@@ -270,6 +294,28 @@ const HasManyDissociator = require('../src/services/has-many-dissociator');
270294
{ field: 'member', type: 'Number', reference: 'member.id' },
271295
],
272296
},
297+
owner: {
298+
name: 'owner',
299+
idField: 'id',
300+
primaryKeys: ['id'],
301+
isCompositePrimary: false,
302+
fields: [
303+
{ field: 'id', type: 'Number' },
304+
{ field: 'name', type: 'STRING' },
305+
{ field: 'ownerId', type: 'Number' },
306+
],
307+
},
308+
project: {
309+
name: 'project',
310+
idField: 'id',
311+
primaryKeys: ['id'],
312+
isCompositePrimary: false,
313+
fields: [
314+
{ field: 'id', type: 'Number' },
315+
{ field: 'name', type: 'STRING' },
316+
{ field: 'ownerId', type: 'Number', reference: 'owner.ownerId' },
317+
],
318+
},
273319
},
274320
};
275321

@@ -533,6 +579,55 @@ const HasManyDissociator = require('../src/services/has-many-dissociator');
533579
});
534580
});
535581

582+
describe('create a record on a collection with a foreign key non pointing to a primary key', () => {
583+
it('should create a record', async () => {
584+
expect.assertions(6);
585+
const { models } = initializeSequelize();
586+
try {
587+
await new ResourceCreator(models.owner, {
588+
id: 1,
589+
name: 'foo',
590+
ownerId: 3,
591+
}).perform();
592+
const result = await new ResourceCreator(models.project, {
593+
id: 1,
594+
name: 'bar',
595+
owner: 1,
596+
}).perform();
597+
598+
expect(result.id).toStrictEqual(1);
599+
expect(result.name).toStrictEqual('bar');
600+
expect(result.ownerIdKey).toStrictEqual(3);
601+
602+
const project = await models.project.findOne({ where: { name: 'bar' }, include: { model: models.owner, as: 'owner' } });
603+
expect(project).not.toBeNull();
604+
expect(project.owner.id).toStrictEqual(1);
605+
expect(project.owner.ownerId).toStrictEqual(3);
606+
} finally {
607+
connectionManager.closeConnection();
608+
}
609+
});
610+
611+
it('should not create a record', async () => {
612+
expect.assertions(1);
613+
const { models } = initializeSequelize();
614+
try {
615+
await new ResourceCreator(models.owner, {
616+
id: 2,
617+
name: 'foo',
618+
ownerId: 4,
619+
}).perform();
620+
await expect(new ResourceCreator(models.project, {
621+
id: 1,
622+
name: 'bar',
623+
owner: 4,
624+
}).perform()).rejects.toThrow(Error('related owner with pk 4 does not exist.'));
625+
} finally {
626+
connectionManager.closeConnection();
627+
}
628+
});
629+
});
630+
536631
describe('create a record on a collection with a composite primary key', () => {
537632
it('should create a record', async () => {
538633
expect.assertions(3);
@@ -658,6 +753,56 @@ const HasManyDissociator = require('../src/services/has-many-dissociator');
658753
}
659754
});
660755
});
756+
describe('update a record on a collection with a foreign key non pointing to a primary key', () => {
757+
it('should update a record', async () => {
758+
expect.assertions(2);
759+
const { models } = initializeSequelize();
760+
try {
761+
await new ResourceCreator(models.owner, {
762+
id: 3,
763+
name: 'foo3',
764+
ownerId: 5,
765+
}).perform();
766+
const result = await new BelongsToUpdater(models.project, null, null, {
767+
recordId: '1',
768+
associationName: 'owner',
769+
}, {
770+
data: {
771+
id: '3',
772+
type: 'owner',
773+
},
774+
}).perform();
775+
776+
expect(result.id).toStrictEqual(1);
777+
expect(result.ownerIdKey).toStrictEqual(5);
778+
} finally {
779+
connectionManager.closeConnection();
780+
}
781+
});
782+
783+
it('should not update a record', async () => {
784+
expect.assertions(1);
785+
const { models } = initializeSequelize();
786+
try {
787+
await new ResourceCreator(models.owner, {
788+
id: 4,
789+
name: 'foo4',
790+
ownerId: 6,
791+
}).perform();
792+
await expect(new BelongsToUpdater(models.project, null, null, {
793+
recordId: '1',
794+
associationName: 'owner',
795+
}, {
796+
data: {
797+
id: '6',
798+
type: 'owner',
799+
},
800+
}).perform()).rejects.toThrow(Error('related owner with pk 6 does not exist.'));
801+
} finally {
802+
connectionManager.closeConnection();
803+
}
804+
});
805+
});
661806
});
662807

663808
describe('resources > resources getter', () => {

0 commit comments

Comments
 (0)