From 7ccd56ef527b566f887f85b02ff4a7eee5198f55 Mon Sep 17 00:00:00 2001 From: Amandeep Singh Date: Sat, 25 Aug 2018 23:36:29 +0530 Subject: [PATCH 1/3] Added support for Graph Insert --- src/index.js | 13 ++++++++++-- test/client.js | 28 ++++++++++++++++++++++++ test/company.js | 8 +++++++ test/index.test.js | 53 ++++++++++++++++++++++++++++++++++++++++++++-- 4 files changed, 98 insertions(+), 4 deletions(-) create mode 100644 test/client.js diff --git a/src/index.js b/src/index.js index 48688d7..da83257 100644 --- a/src/index.js +++ b/src/index.js @@ -62,6 +62,8 @@ class Service { this.allowedEager = options.allowedEager || '[]' this.namedEagerFilters = options.namedEagerFilters this.eagerFilters = options.eagerFilters + this.allowedInsert = options.allowedInsert + this.insertGraphOptions = options.insertGraphOptions } extend (obj) { @@ -338,8 +340,15 @@ class Service { } _create (data, params) { - return this._createQuery(params) - .insert(data, this.id) + let q = this._createQuery(params) + + if (this.allowedInsert) { + q.allowInsert(this.allowedInsert) + q.insertGraph(data, this.insertGraphOptions) + } else { + q.insert(data, this.id) + } + return q .then(row => { let id = null diff --git a/test/client.js b/test/client.js new file mode 100644 index 0000000..cc03e2c --- /dev/null +++ b/test/client.js @@ -0,0 +1,28 @@ +import { Model } from 'objection' + +export default class Client extends Model { + static tableName = 'clients' + static jsonSchema = { + type: 'object', + required: ['name'], + + properties: { + id: { type: 'integer' }, + companyId: { type: 'integer' }, + name: { type: 'string' } + } + } + + static get relationMappings () { + return { + company: { + relation: Model.BelongsToOneRelation, + modelClass: require('./company'), + join: { + from: 'clients.companyId', + to: 'companies.id' + } + } + } + } +} diff --git a/test/company.js b/test/company.js index a87cf2d..18cfa48 100644 --- a/test/company.js +++ b/test/company.js @@ -32,6 +32,14 @@ export default class Company extends Model { from: 'companies.id', to: 'employees.companyId' } + }, + clients: { + relation: Model.HasManyRelation, + modelClass: path.join(__dirname, '/client'), + join: { + from: 'companies.id', + to: 'clients.companyId' + } } } } diff --git a/test/index.test.js b/test/index.test.js index a465c97..afcd265 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -15,6 +15,7 @@ import PeopleRoomsCustomIdSeparator from './people-rooms-custom-id-separator' import Company from './company' import { Model } from 'objection' import Employee from './employee' +import Client from './client' const db = knex({ client: 'sqlite3', @@ -68,7 +69,7 @@ const app = feathers() model: Company, id: 'id', events: ['testing'], - allowedEager: 'ceos', + allowedEager: '[ceos, clients]', namedEagerFilters: { notSnoop (builder) { return builder.whereNot('name', 'Snoop') @@ -81,7 +82,8 @@ const app = feathers() return builder.where('age', '<', '25') } } - ] + ], + allowedInsert: 'clients' }) ) .use( @@ -91,6 +93,13 @@ const app = feathers() allowedEager: 'company' }) ) + .use( + '/clients', + service({ + model: Client, + allowedEager: 'company' + }) + ) let people = app.service('people') let peopleRooms = app.service('people-rooms') @@ -158,6 +167,16 @@ function clean (done) { table.integer('companyId').references('companies.id') table.string('name') }) + }) + }) + .then(() => { + return db.schema.dropTableIfExists('clients').then(() => { + return db.schema + .createTable('clients', table => { + table.increments('id') + table.integer('companyId').references('companies.id') + table.string('name') + }) .then(() => done()) }) }) @@ -457,6 +476,36 @@ describe('Feathers Objection Service', () => { }) }) + describe('Graph Insert Queries', () => { + before(async () => { + await companies.remove(null) + await companies + .create([ + { + name: 'Google', + clients: [ + { + name: 'Dan Davis' + }, + { + name: 'Ken Patrick' + } + ] + }, + { + name: 'Apple' + } + ]) + }) + + it('allows insertGraph queries', () => { + return companies.find({ query: { $eager: 'clients' } }).then(data => { + expect(data[0].clients).to.be.an('array') + expect(data[0].clients).to.have.lengthOf(2) + }) + }) + }) + describe('$like method', () => { beforeEach(async () => { await people From 81ae168734760169bb7552b8187473bbd9035adf Mon Sep 17 00:00:00 2001 From: Amandeep Singh Date: Sun, 26 Aug 2018 03:22:34 +0530 Subject: [PATCH 2/3] Added support for Graph Upsert --- src/index.js | 44 +++++++++++++++++++++++++------------------- test/index.test.js | 42 +++++++++++++++++++++++++++++++++++++++++- 2 files changed, 66 insertions(+), 20 deletions(-) diff --git a/src/index.js b/src/index.js index da83257..e23ded5 100644 --- a/src/index.js +++ b/src/index.js @@ -64,6 +64,8 @@ class Service { this.eagerFilters = options.eagerFilters this.allowedInsert = options.allowedInsert this.insertGraphOptions = options.insertGraphOptions + this.allowedUpsert = options.allowedUpsert + this.upsertGraphOptions = options.upsertGraphOptions } extend (obj) { @@ -408,28 +410,32 @@ class Service { } } - // NOTE (EK): Delete id field so we don't update it - if (Array.isArray(this.id)) { - for (const idKey of this.id) { - delete newObject[idKey] + if (!this.allowedUpsert) { + // NOTE (EK): Delete id field so we don't update it + if (Array.isArray(this.id)) { + for (const idKey of this.id) { + delete newObject[idKey] + } + } else { + delete newObject[this.id] } + return this._createQuery(params) + .where(this.getIdsQuery(id)) + .update(newObject) + .then(() => { + // NOTE (EK): Restore the id field so we can return it to the client + if (Array.isArray(this.id)) { + newObject = Object.assign({}, newObject, this.getIdsQuery(id)) + } else { + newObject[this.id] = id + } + + return newObject + }) } else { - delete newObject[this.id] + return this._createQuery(params) + .upsertGraphAndFetch(newObject, this.upsertGraphOptions) } - - return this._createQuery(params) - .where(this.getIdsQuery(id)) - .update(newObject) - .then(() => { - // NOTE (EK): Restore the id field so we can return it to the client - if (Array.isArray(this.id)) { - newObject = Object.assign({}, newObject, this.getIdsQuery(id)) - } else { - newObject[this.id] = id - } - - return newObject - }) }) .catch(errorHandler) } diff --git a/test/index.test.js b/test/index.test.js index afcd265..8fa71f3 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -83,7 +83,8 @@ const app = feathers() } } ], - allowedInsert: 'clients' + allowedInsert: 'clients', + allowedUpsert: 'clients' }) ) .use( @@ -506,6 +507,45 @@ describe('Feathers Objection Service', () => { }) }) + describe('Graph Upsert Queries', () => { + before(async () => { + await companies.remove(null) + const [google] = await companies + .create([ + { + name: 'Google', + clients: [ + { + name: 'Dan Davis' + } + ] + }, + { + name: 'Apple' + } + ], { query: { $eager: 'clients' } }) + + const newClients = (google.clients) ? google.clients.concat([{ + name: 'Ken Patrick' + }]) : [] + + await companies + .update(google.id, { + id: google.id, + name: 'Alphabet', + clients: newClients + }, { query: { $eager: 'clients' } }) + }) + + it('allows upsertGraph queries on update', () => { + return companies.find({ query: { $eager: 'clients' } }).then(data => { + expect(data[0].name).equal('Alphabet') + expect(data[0].clients).to.be.an('array') + expect(data[0].clients).to.have.lengthOf(2) + }) + }) + }) + describe('$like method', () => { beforeEach(async () => { await people From 3e81d0f9187e7fb848893b93ecd380ec821f97bd Mon Sep 17 00:00:00 2001 From: Amandeep Singh Date: Sun, 26 Aug 2018 03:58:08 +0530 Subject: [PATCH 3/3] Updated docs --- README.md | 47 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/README.md b/README.md index fa4e021..507efd0 100644 --- a/README.md +++ b/README.md @@ -327,6 +327,53 @@ app.service('/user-todos').get([1, 2]); app.service('/user-todos').get({ userId: 1, todoId: 2 }); ``` +### Graph upsert +Arbitrary relation graphs can be upserted (insert + update + delete) using the upsertGraph method. +See [`examples`](https://vincit.github.io/objection.js/#graph-upserts) for a better explanation. +Runs on the `.update(id, data, params)` service method. + +*The relation being upserted must also be present in `allowedEager` option and included in `$eager` query.* + +#### Options + +* **`allowedUpsert`** - relation expression to allow relations to be upserted along with update. +Defaults to `null`, meaning relations will not be automatically upserted unless specified here. +See [`allowUpsert`](https://vincit.github.io/objection.js/#allowupsert) documentation. +* **`upsertGraphOptions`** - See [`upsertGraphOptions`](https://vincit.github.io/objection.js/#upsertgraphoptions) documentation. + +```js +app.use('/companies', service({ + model: Company, + allowedEager: 'clients', + allowedUpsert: 'clients' +}) + +app.service('/companies').update(1, { + name: 'New Name', + clients: [{ + id: 100, + name: 'Existing Client' + }, { + name: 'New Client' + }] +}) +``` + +In the example above, we are updating the name of an existing company, along with adding a new client which is a relationship for companies. The client without the ID would be inserted and related. The client with the ID will just be updated (if there are any changes at all). + +### Graph insert +Arbitrary relation graphs can be inserted using the insertGraph method. +Provides the ability to relate the inserted object with its associations. +Runs on the `.create(data, params)` service method. + +*The relation being created must also be present in `allowedEager` option and included in `$eager` query.* + +#### Options + +* **`allowedInsert`** - relation expression to allow relations to be created along with insert. +Defaults to `null`, meaning relations will not be automatically created unless specified here. +See [`allowInsert`](https://vincit.github.io/objection.js/#allowinsert) documentation. +* **`insertGraphOptions`** - See [`insertGraphOptions`](https://vincit.github.io/objection.js/#insertgraphoptions) documentation. ## Complete Example