Skip to content
This repository was archived by the owner on Sep 2, 2025. It is now read-only.

Commit 7fff580

Browse files
authored
Merge pull request #28 from mcchrish/feature-graph-upsert
Graph upsert and insert
2 parents d6f683f + 3e81d0f commit 7fff580

File tree

5 files changed

+210
-23
lines changed

5 files changed

+210
-23
lines changed

README.md

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -327,6 +327,53 @@ app.service('/user-todos').get([1, 2]);
327327
app.service('/user-todos').get({ userId: 1, todoId: 2 });
328328
```
329329
330+
### Graph upsert
331+
Arbitrary relation graphs can be upserted (insert + update + delete) using the upsertGraph method.
332+
See [`examples`](https://vincit.github.io/objection.js/#graph-upserts) for a better explanation.
333+
Runs on the `.update(id, data, params)` service method.
334+
335+
*The relation being upserted must also be present in `allowedEager` option and included in `$eager` query.*
336+
337+
#### Options
338+
339+
* **`allowedUpsert`** - relation expression to allow relations to be upserted along with update.
340+
Defaults to `null`, meaning relations will not be automatically upserted unless specified here.
341+
See [`allowUpsert`](https://vincit.github.io/objection.js/#allowupsert) documentation.
342+
* **`upsertGraphOptions`** - See [`upsertGraphOptions`](https://vincit.github.io/objection.js/#upsertgraphoptions) documentation.
343+
344+
```js
345+
app.use('/companies', service({
346+
model: Company,
347+
allowedEager: 'clients',
348+
allowedUpsert: 'clients'
349+
})
350+
351+
app.service('/companies').update(1, {
352+
name: 'New Name',
353+
clients: [{
354+
id: 100,
355+
name: 'Existing Client'
356+
}, {
357+
name: 'New Client'
358+
}]
359+
})
360+
```
361+
362+
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).
363+
364+
### Graph insert
365+
Arbitrary relation graphs can be inserted using the insertGraph method.
366+
Provides the ability to relate the inserted object with its associations.
367+
Runs on the `.create(data, params)` service method.
368+
369+
*The relation being created must also be present in `allowedEager` option and included in `$eager` query.*
370+
371+
#### Options
372+
373+
* **`allowedInsert`** - relation expression to allow relations to be created along with insert.
374+
Defaults to `null`, meaning relations will not be automatically created unless specified here.
375+
See [`allowInsert`](https://vincit.github.io/objection.js/#allowinsert) documentation.
376+
* **`insertGraphOptions`** - See [`insertGraphOptions`](https://vincit.github.io/objection.js/#insertgraphoptions) documentation.
330377
331378
## Complete Example
332379

src/index.js

Lines changed: 36 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ class Service {
6262
this.allowedEager = options.allowedEager || '[]'
6363
this.namedEagerFilters = options.namedEagerFilters
6464
this.eagerFilters = options.eagerFilters
65+
this.allowedInsert = options.allowedInsert
66+
this.insertGraphOptions = options.insertGraphOptions
67+
this.allowedUpsert = options.allowedUpsert
68+
this.upsertGraphOptions = options.upsertGraphOptions
6569
}
6670

6771
extend (obj) {
@@ -338,8 +342,15 @@ class Service {
338342
}
339343

340344
_create (data, params) {
341-
return this._createQuery(params)
342-
.insert(data, this.id)
345+
let q = this._createQuery(params)
346+
347+
if (this.allowedInsert) {
348+
q.allowInsert(this.allowedInsert)
349+
q.insertGraph(data, this.insertGraphOptions)
350+
} else {
351+
q.insert(data, this.id)
352+
}
353+
return q
343354
.then(row => {
344355
let id = null
345356

@@ -399,28 +410,32 @@ class Service {
399410
}
400411
}
401412

402-
// NOTE (EK): Delete id field so we don't update it
403-
if (Array.isArray(this.id)) {
404-
for (const idKey of this.id) {
405-
delete newObject[idKey]
413+
if (!this.allowedUpsert) {
414+
// NOTE (EK): Delete id field so we don't update it
415+
if (Array.isArray(this.id)) {
416+
for (const idKey of this.id) {
417+
delete newObject[idKey]
418+
}
419+
} else {
420+
delete newObject[this.id]
406421
}
422+
return this._createQuery(params)
423+
.where(this.getIdsQuery(id))
424+
.update(newObject)
425+
.then(() => {
426+
// NOTE (EK): Restore the id field so we can return it to the client
427+
if (Array.isArray(this.id)) {
428+
newObject = Object.assign({}, newObject, this.getIdsQuery(id))
429+
} else {
430+
newObject[this.id] = id
431+
}
432+
433+
return newObject
434+
})
407435
} else {
408-
delete newObject[this.id]
436+
return this._createQuery(params)
437+
.upsertGraphAndFetch(newObject, this.upsertGraphOptions)
409438
}
410-
411-
return this._createQuery(params)
412-
.where(this.getIdsQuery(id))
413-
.update(newObject)
414-
.then(() => {
415-
// NOTE (EK): Restore the id field so we can return it to the client
416-
if (Array.isArray(this.id)) {
417-
newObject = Object.assign({}, newObject, this.getIdsQuery(id))
418-
} else {
419-
newObject[this.id] = id
420-
}
421-
422-
return newObject
423-
})
424439
})
425440
.catch(errorHandler)
426441
}

test/client.js

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { Model } from 'objection'
2+
3+
export default class Client extends Model {
4+
static tableName = 'clients'
5+
static jsonSchema = {
6+
type: 'object',
7+
required: ['name'],
8+
9+
properties: {
10+
id: { type: 'integer' },
11+
companyId: { type: 'integer' },
12+
name: { type: 'string' }
13+
}
14+
}
15+
16+
static get relationMappings () {
17+
return {
18+
company: {
19+
relation: Model.BelongsToOneRelation,
20+
modelClass: require('./company'),
21+
join: {
22+
from: 'clients.companyId',
23+
to: 'companies.id'
24+
}
25+
}
26+
}
27+
}
28+
}

test/company.js

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,14 @@ export default class Company extends Model {
3232
from: 'companies.id',
3333
to: 'employees.companyId'
3434
}
35+
},
36+
clients: {
37+
relation: Model.HasManyRelation,
38+
modelClass: path.join(__dirname, '/client'),
39+
join: {
40+
from: 'companies.id',
41+
to: 'clients.companyId'
42+
}
3543
}
3644
}
3745
}

test/index.test.js

Lines changed: 91 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import PeopleRoomsCustomIdSeparator from './people-rooms-custom-id-separator'
1515
import Company from './company'
1616
import { Model } from 'objection'
1717
import Employee from './employee'
18+
import Client from './client'
1819

1920
const db = knex({
2021
client: 'sqlite3',
@@ -68,7 +69,7 @@ const app = feathers()
6869
model: Company,
6970
id: 'id',
7071
events: ['testing'],
71-
allowedEager: 'ceos',
72+
allowedEager: '[ceos, clients]',
7273
namedEagerFilters: {
7374
notSnoop (builder) {
7475
return builder.whereNot('name', 'Snoop')
@@ -81,7 +82,9 @@ const app = feathers()
8182
return builder.where('age', '<', '25')
8283
}
8384
}
84-
]
85+
],
86+
allowedInsert: 'clients',
87+
allowedUpsert: 'clients'
8588
})
8689
)
8790
.use(
@@ -91,6 +94,13 @@ const app = feathers()
9194
allowedEager: 'company'
9295
})
9396
)
97+
.use(
98+
'/clients',
99+
service({
100+
model: Client,
101+
allowedEager: 'company'
102+
})
103+
)
94104

95105
let people = app.service('people')
96106
let peopleRooms = app.service('people-rooms')
@@ -158,6 +168,16 @@ function clean (done) {
158168
table.integer('companyId').references('companies.id')
159169
table.string('name')
160170
})
171+
})
172+
})
173+
.then(() => {
174+
return db.schema.dropTableIfExists('clients').then(() => {
175+
return db.schema
176+
.createTable('clients', table => {
177+
table.increments('id')
178+
table.integer('companyId').references('companies.id')
179+
table.string('name')
180+
})
161181
.then(() => done())
162182
})
163183
})
@@ -457,6 +477,75 @@ describe('Feathers Objection Service', () => {
457477
})
458478
})
459479

480+
describe('Graph Insert Queries', () => {
481+
before(async () => {
482+
await companies.remove(null)
483+
await companies
484+
.create([
485+
{
486+
name: 'Google',
487+
clients: [
488+
{
489+
name: 'Dan Davis'
490+
},
491+
{
492+
name: 'Ken Patrick'
493+
}
494+
]
495+
},
496+
{
497+
name: 'Apple'
498+
}
499+
])
500+
})
501+
502+
it('allows insertGraph queries', () => {
503+
return companies.find({ query: { $eager: 'clients' } }).then(data => {
504+
expect(data[0].clients).to.be.an('array')
505+
expect(data[0].clients).to.have.lengthOf(2)
506+
})
507+
})
508+
})
509+
510+
describe('Graph Upsert Queries', () => {
511+
before(async () => {
512+
await companies.remove(null)
513+
const [google] = await companies
514+
.create([
515+
{
516+
name: 'Google',
517+
clients: [
518+
{
519+
name: 'Dan Davis'
520+
}
521+
]
522+
},
523+
{
524+
name: 'Apple'
525+
}
526+
], { query: { $eager: 'clients' } })
527+
528+
const newClients = (google.clients) ? google.clients.concat([{
529+
name: 'Ken Patrick'
530+
}]) : []
531+
532+
await companies
533+
.update(google.id, {
534+
id: google.id,
535+
name: 'Alphabet',
536+
clients: newClients
537+
}, { query: { $eager: 'clients' } })
538+
})
539+
540+
it('allows upsertGraph queries on update', () => {
541+
return companies.find({ query: { $eager: 'clients' } }).then(data => {
542+
expect(data[0].name).equal('Alphabet')
543+
expect(data[0].clients).to.be.an('array')
544+
expect(data[0].clients).to.have.lengthOf(2)
545+
})
546+
})
547+
})
548+
460549
describe('$like method', () => {
461550
beforeEach(async () => {
462551
await people

0 commit comments

Comments
 (0)