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

Commit 10e56c2

Browse files
Thomas-gitdekelev
andauthored
Atomic graph mutation (#124)
* Fix schema use with graph * Add $startTransaction params * allow for atomic graph upsert/insert * allow for atomic multi create * Fix $startTransaction (atomic) * Is now $atomic * Better testing * README infos Co-authored-by: Dekel Barzilay <[email protected]>
1 parent dd07436 commit 10e56c2

File tree

3 files changed

+211
-14
lines changed

3 files changed

+211
-14
lines changed

README.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,8 @@ Note that all this eager related options are optional.
246246
[`transaction`](https://vincit.github.io/objection.js/api/objection/#transaction)
247247
documentation.
248248

249+
- **`$atomic`** - when `true` ensure that multi create or graph insert/upsert success or fail all at once. Under the hood, automaticaly create a transaction and commit on success or rollback on partial or total failure. __Ignored__ if you added your own `transaction` object in params.
250+
249251
- **`mergeAllowEager`** - Will merge the given expression to the existing expression from the `allowEager` service option.
250252
See [`allowGraph`](https://vincit.github.io/objection.js/api/query-builder/eager-methods.html#allowgraph)
251253
documentation.

src/index.js

Lines changed: 51 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -314,12 +314,46 @@ class Service extends AdapterService {
314314
return null;
315315
}
316316

317+
async _createTransaction (params) {
318+
if (!params.transaction && params.$atomic) {
319+
delete params.$atomic;
320+
params.transaction = params.transaction || {};
321+
params.transaction.trx = await this.Model.startTransaction();
322+
return params.transaction;
323+
}
324+
return null;
325+
}
326+
327+
_commitTransaction (transaction) {
328+
return async (data) => {
329+
if (transaction) {
330+
await transaction.trx.commit();
331+
}
332+
return data;
333+
};
334+
}
335+
336+
_rollbackTransaction (transaction) {
337+
return async (err) => {
338+
if (transaction) {
339+
await transaction.trx.rollback();
340+
}
341+
throw err;
342+
};
343+
}
344+
317345
_createQuery (params = {}) {
318346
const trx = params.transaction ? params.transaction.trx : null;
319347
const schema = params.schema || this.schema;
320348
const query = this.Model.query(trx);
321-
322-
return schema ? query.withSchema(schema) : query;
349+
if (schema) {
350+
query.context({
351+
onBuild (builder) {
352+
builder.withSchema(schema);
353+
}
354+
});
355+
}
356+
return query;
323357
}
324358

325359
_selectQuery (q, $select) {
@@ -587,7 +621,8 @@ class Service extends AdapterService {
587621
* @param {object} data
588622
* @param {object} params
589623
*/
590-
_create (data, params) {
624+
async _create (data, params) {
625+
const transaction = await this._createTransaction(params);
591626
const create = (data, params) => {
592627
const q = this._createQuery(params);
593628
const allowedUpsert = this.mergeRelations(this.allowedUpsert, params.mergeAllowUpsert);
@@ -604,6 +639,7 @@ class Service extends AdapterService {
604639
} else {
605640
q.insert(data, this.id);
606641
}
642+
607643
return q
608644
.then(row => {
609645
if (params.query && params.query.$noSelect) { return data; }
@@ -628,10 +664,10 @@ class Service extends AdapterService {
628664
};
629665

630666
if (Array.isArray(data)) {
631-
return Promise.all(data.map(current => create(current, params)));
667+
return Promise.all(data.map(current => create(current, params))).then(this._commitTransaction(transaction), this._rollbackTransaction(transaction));
632668
}
633669

634-
return create(data, params);
670+
return create(data, params).then(this._commitTransaction(transaction), this._rollbackTransaction(transaction));
635671
}
636672

637673
/**
@@ -648,13 +684,17 @@ class Service extends AdapterService {
648684
// that we can fill any existing keys that the
649685
// client isn't updating with null;
650686
return this.Model.fetchTableMetadata()
651-
.then(meta => {
687+
.then(async meta => {
652688
let newObject = Object.assign({}, data);
689+
let transaction = null;
653690

654691
const allowedUpsert = this.mergeRelations(this.allowedUpsert, params.mergeAllowUpsert);
692+
655693
if (allowedUpsert) {
656694
// Ensure the object we fetched is the one we update
657695
this._checkUpsertId(id, newObject);
696+
// Create transaction if needed
697+
transaction = await this._createTransaction(params);
658698
}
659699

660700
for (const key of meta.columns) {
@@ -666,7 +706,7 @@ class Service extends AdapterService {
666706
if (allowedUpsert) {
667707
return this._createQuery(params)
668708
.allowGraph(allowedUpsert)
669-
.upsertGraphAndFetch(newObject, this.upsertGraphOptions);
709+
.upsertGraphAndFetch(newObject, this.upsertGraphOptions).then(this._commitTransaction(transaction), this._rollbackTransaction(transaction));
670710
}
671711

672712
// NOTE (EK): Delete id field so we don't update it
@@ -710,11 +750,13 @@ class Service extends AdapterService {
710750
this._checkUpsertId(id, dataCopy);
711751

712752
// Get object first to ensure it satisfy user query
713-
return this._get(id, params).then(() => {
753+
return this._get(id, params).then(async () => {
754+
// Create transaction if needed
755+
const transaction = await this._createTransaction(params);
714756
return this._createQuery(params)
715757
.allowGraph(allowedUpsert)
716758
.upsertGraphAndFetch(dataCopy, this.upsertGraphOptions)
717-
.then(this._selectFields(params, data));
759+
.then(this._selectFields(params, data)).then(this._commitTransaction(transaction), this._rollbackTransaction(transaction));
718760
});
719761
}
720762

test/index.test.js

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

2020
const testSuite = adapterTests([
2121
'.options',
@@ -238,6 +238,7 @@ function clean (done) {
238238
table.json('jsonArray');
239239
table.jsonb('jsonbObject');
240240
table.jsonb('jsonbArray');
241+
table.unique('name');
241242
});
242243
});
243244
});
@@ -260,6 +261,7 @@ function clean (done) {
260261
table.increments('id');
261262
table.integer('companyId');
262263
table.string('name');
264+
table.unique('name');
263265
})
264266
.then(() => done());
265267
});
@@ -1063,18 +1065,18 @@ describe('Feathers Objection Service', () => {
10631065
return companies
10641066
.create([
10651067
{
1066-
name: 'Google',
1068+
name: 'Facebook',
10671069
clients: [
10681070
{
1069-
name: 'Dan Davis'
1071+
name: 'Danny Lapierre'
10701072
},
10711073
{
1072-
name: 'Ken Patrick'
1074+
name: 'Kirck Filty'
10731075
}
10741076
]
10751077
},
10761078
{
1077-
name: 'Apple'
1079+
name: 'Yubico'
10781080
}
10791081
]).then(() => {
10801082
companies.createUseUpsertGraph = false;
@@ -1844,6 +1846,157 @@ describe('Feathers Objection Service', () => {
18441846
});
18451847
});
18461848
});
1849+
1850+
it('works with atomic', () => {
1851+
return people.create({ name: 'Rollback' }, { transaction, $atomic: true }).then(() => {
1852+
expect(transaction.trx.isCompleted()).to.equal(false); // Atomic must be ignored and transaction still running
1853+
return transaction.trx.rollback().then(() => {
1854+
return people.find({ query: { name: 'Rollback' } }).then((data) => {
1855+
expect(data.length).to.equal(0);
1856+
});
1857+
});
1858+
});
1859+
});
1860+
});
1861+
1862+
describe('Atomic Transactions', () => {
1863+
before(async () => {
1864+
await companies
1865+
.create([
1866+
{
1867+
name: 'Google',
1868+
clients: [
1869+
{
1870+
name: 'Dan Davis'
1871+
},
1872+
{
1873+
name: 'Ken Patrick'
1874+
}
1875+
]
1876+
},
1877+
{
1878+
name: 'Apple'
1879+
}
1880+
]);
1881+
});
1882+
1883+
after(async () => {
1884+
await clients.remove(null);
1885+
await companies.remove(null);
1886+
});
1887+
1888+
it('Rollback on sub insert failure', () => {
1889+
// Dan Davis already exists
1890+
return companies.create({ name: 'Compaq', clients: [{ name: 'Dan Davis' }] }, { $atomic: true }).catch((error) => {
1891+
expect(error instanceof errors.GeneralError).to.be.ok;
1892+
expect(error.message).to.match(/SQLITE_CONSTRAINT: UNIQUE/);
1893+
return companies.find({ query: { name: 'Compaq', $eager: 'clients' } }).then(
1894+
(data) => {
1895+
expect(data.length).to.equal(0);
1896+
});
1897+
});
1898+
});
1899+
1900+
it('Rollback on multi insert failure', () => {
1901+
// Google already exists
1902+
return companies.create([{ name: 'Google' }, { name: 'Compaq' }], { $atomic: true }).catch((error) => {
1903+
expect(error instanceof errors.GeneralError).to.be.ok;
1904+
expect(error.message).to.match(/SQLITE_CONSTRAINT: UNIQUE/);
1905+
return companies.find({ query: { name: 'Compaq' } }).then(
1906+
(data) => {
1907+
expect(data.length).to.equal(0);
1908+
});
1909+
});
1910+
});
1911+
1912+
it('Rollback on update failure', () => {
1913+
// Dan Davis appears twice, so clients must stay as it is
1914+
return companies.find({ query: { name: 'Google' } }).then(data => {
1915+
return companies.update(data[0].id, {
1916+
name: 'Google',
1917+
clients: [
1918+
{
1919+
name: 'Dan Davis'
1920+
},
1921+
{
1922+
name: 'Dan Davis'
1923+
},
1924+
{
1925+
name: 'Kirk Maelström'
1926+
}
1927+
]
1928+
}, { $atomic: true }).catch((error) => {
1929+
expect(error instanceof errors.GeneralError).to.be.ok;
1930+
expect(error.message).to.match(/SQLITE_CONSTRAINT: UNIQUE/);
1931+
return companies.find({ query: { name: 'Google', $eager: 'clients' } }).then(
1932+
(data) => {
1933+
expect(data.length).to.equal(1);
1934+
expect(data[0].clients.length).to.equal(2);
1935+
expect(data[0].clients[0].name).to.equal('Dan Davis');
1936+
expect(data[0].clients[1].name).to.equal('Ken Patrick');
1937+
});
1938+
});
1939+
});
1940+
});
1941+
1942+
it('Rollback on patch failure', () => {
1943+
// Dan Davis appears twice, so clients must stay as it is
1944+
return companies.find({ query: { name: 'Google' } }).then(data => {
1945+
return companies.patch(data[0].id, {
1946+
clients: [
1947+
{
1948+
name: 'Dan Davis'
1949+
},
1950+
{
1951+
name: 'Dan Davis'
1952+
},
1953+
{
1954+
name: 'Kirk Maelström'
1955+
}
1956+
]
1957+
}, { $atomic: true }).catch((error) => {
1958+
expect(error instanceof UniqueViolationError).to.be.ok;
1959+
expect(error.message).to.match(/SQLITE_CONSTRAINT: UNIQUE/);
1960+
return companies.find({ query: { name: 'Google', $eager: 'clients' } }).then(
1961+
(data) => {
1962+
expect(data.length).to.equal(1);
1963+
expect(data[0].clients.length).to.equal(2);
1964+
expect(data[0].clients[0].name).to.equal('Dan Davis');
1965+
expect(data[0].clients[1].name).to.equal('Ken Patrick');
1966+
});
1967+
});
1968+
});
1969+
});
1970+
1971+
it('Commit on patch success', () => {
1972+
// Dan Davis appears twice, so clients must stay as it is
1973+
return companies.find({ query: { name: 'Google' } }).then(data => {
1974+
return companies.patch(data[0].id, {
1975+
clients: [
1976+
{
1977+
name: 'Dan David'
1978+
},
1979+
{
1980+
name: 'Dan Davis'
1981+
},
1982+
{
1983+
name: 'Kirk Maelström'
1984+
}
1985+
]
1986+
}, { $atomic: true }).catch((error) => {
1987+
expect(error instanceof UniqueViolationError).to.be.ok;
1988+
expect(error.message).to.match(/SQLITE_CONSTRAINT: UNIQUE/);
1989+
return companies.find({ query: { name: 'Google', $eager: 'clients' } }).then(
1990+
(data) => {
1991+
expect(data.length).to.equal(1);
1992+
expect(data[0].clients.length).to.equal(3);
1993+
expect(data[0].clients[0].name).to.equal('Dan David');
1994+
expect(data[0].clients[0].name).to.equal('Dan Davis');
1995+
expect(data[0].clients[1].name).to.equal('Kirk Maelström');
1996+
});
1997+
});
1998+
});
1999+
});
18472000
});
18482001

18492002
describe('$noSelect', () => {

0 commit comments

Comments
 (0)