Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions docs/api/databases/knex.md
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,22 @@ The [common API options](./common.md#options) are:

There are additionally several legacy options in the [common API options](./common.md#options)

### RETURNING clause support

`create` uses the `RETURNING` clause to retrieve the id of a newly created record for PostgreSQL, OracleDB, MSSQL and SQLite. For databases that support it but are not in this list, it can be enabled with the `returning` option of the [Knex configuration](https://knexjs.org/guide/#configuration-options). This is for example required for Firebird via [`knex-firebird-dialect`](https://www.npmjs.com/package/knex-firebird-dialect):

```js
const db = knex({
client: knexFirebirdDialect,
connection: {
/* ... */
},
returning: true
})
```

Setting the option to a falsy value will fall back to the default list of clients.

### getModel([params])

`service.getModel([params])` returns the [Knex](https://knexjs.org/guide/query-builder.html) client for this table.
Expand Down
20 changes: 19 additions & 1 deletion packages/knex/src/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,24 @@ const OPERATORS = {

const RETURNING_CLIENTS = ['postgresql', 'pg', 'oracledb', 'mssql', 'sqlite3']

type ReturningConfig = {
returning?: boolean
}

/**
* Returns whether the database client supports the `RETURNING` clause on inserts.
*
* The `returning` option in the knex configuration can be used to enable it for
* clients that support it but are not in the default list (e.g. Firebird via
* `knex-firebird-dialect`). If it is not set or falsy, the default list of
* clients is used.
*/
export const supportsReturning = (client: Knex.Client) => {
const { returning } = (client.config || {}) as ReturningConfig

return Boolean(returning) || RETURNING_CLIENTS.includes(client.driverName)
}

export class KnexAdapter<
Result,
Data = Partial<Result>,
Expand Down Expand Up @@ -253,7 +271,7 @@ export class KnexAdapter<
}

const { client } = this.db(params)
const returning = RETURNING_CLIENTS.includes(client.driverName) ? [this.id] : []
const returning = supportsReturning(client) ? [this.id] : []
const rows: any = await this.db(params)
.insert(data, returning, { includeTriggerModifications: true })
.catch(errorHandler)
Expand Down
65 changes: 64 additions & 1 deletion packages/knex/test/index.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,14 @@ import { errors } from '@feathersjs/errors'
import { Ajv, getValidator, querySyntax, hooks } from '@feathersjs/schema'

import connection from './connection'
import { ERROR, KnexAdapterParams, KnexService, transaction } from '../src/index'
import {
ERROR,
KnexAdapter,
KnexAdapterParams,
KnexService,
supportsReturning,
transaction
} from '../src/index'
import { AdapterQuery } from '@feathersjs/adapter-commons/lib'

const testSuite = adapterTests([
Expand Down Expand Up @@ -236,6 +243,62 @@ const peopleExtendedOps = new KnexService({
}
})

describe('returning clause support', () => {
const client = (driverName: string, config: any = {}) => ({ driverName, config }) as unknown as Knex.Client

const createStubAdapter = (config: any) => {
const calls: string[][] = []
// Minimal stand-in for a knex instance: callable (like `knex('table')`) with
// the client information and an `insert` that records the returning argument
const Model: any = () => Model

Model.client = { driverName: 'node-firebird-driver-native', config }
Model.insert = (_data: any, returning: string[]) => {
calls.push(returning)

return Promise.resolve(returning.length > 0 ? [{ id: 42 }] : [1])
}

const service = new KnexAdapter<any>({ Model, name: 'people', id: 'id' })

service._get = async (id: any) => ({ id, name: 'Dave' })

return { service, calls }
}

it('uses the default list of clients when the returning option is not set', () => {
assert.strictEqual(supportsReturning(client('postgresql')), true)
assert.strictEqual(supportsReturning(client('oracledb')), true)
assert.strictEqual(supportsReturning(client('sqlite3')), true)
assert.strictEqual(supportsReturning(client('node-firebird-driver-native')), false)
})

it('can be enabled for a client that is not in the default list', () => {
assert.strictEqual(supportsReturning(client('node-firebird-driver-native', { returning: true })), true)
})

it('falls back to the default list when the returning option is falsy', () => {
assert.strictEqual(supportsReturning(client('node-firebird-driver-native', { returning: false })), false)
assert.strictEqual(supportsReturning(client('postgresql', { returning: false })), true)
})

it('passes the id to insert when the returning option is enabled', async () => {
const { service, calls } = createStubAdapter({ returning: true })
const created = await service._create({ name: 'Dave' })

assert.deepStrictEqual(calls, [['id']])
assert.strictEqual(created.id, 42)
})

it('does not pass the id to insert when it is not supported or enabled', async () => {
const { service, calls } = createStubAdapter({})
const created = await service._create({ name: 'Dave' })

assert.deepStrictEqual(calls, [[]])
assert.strictEqual(created.id, 1)
})
})

describe('Feathers Knex Service', () => {
const app = feathers<ServiceTypes>()
.hooks({
Expand Down