Skip to content

Commit b167ca1

Browse files
authored
feat: custom drivers (#11)
* feat(types): define `Driver` class * feat(util): add `isDriver` validator * feat: add `driver` option; note: --driver purposefully overrides `exports.driver` from config file * chore: add `custom drivers` readme docs * break: squash `opts.client` into `opts.driver`~! They were basically the same thing. Now just check if the `driver` string matches a supplied driver name. Customize/override by passing a Driver class or a `path/to/file.js` that contains Driver class. * chore: update readme docs * fix(types): remove `opts.client` key * chore: update `--driver` help text
1 parent 5b6aaf0 commit b167ca1

File tree

6 files changed

+155
-17
lines changed

6 files changed

+155
-17
lines changed

bin.js

+1
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ sade('ley')
2121
.option('-C, --cwd', 'The current directory to resolve from', '.')
2222
.option('-d, --dir', 'The directory of migration files to run', 'migrations')
2323
.option('-c, --config', 'Path to `ley` config file', 'ley.config.js')
24+
.option('-D, --driver', 'The name of a database client driver')
2425
.option('-r, --require', 'Additional module(s) to preload')
2526

2627
.command('up')

index.js

+12-6
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,21 @@ async function parse(opts) {
99

1010
[].concat(opts.require || []).filter(Boolean).forEach(name => {
1111
const tmp = $.exists(name);
12-
if (!tmp) throw new Error(`Cannot find module '${name}'`);
13-
return require(tmp);
12+
if (tmp) return require(tmp);
13+
throw new Error(`Cannot find module '${name}'`);
1414
});
1515

16-
const lib = opts.client || $.detect();
17-
if (!lib) throw new Error('Unable to locate a SQL driver');
16+
// cli(`--driver`) > config(exports.driver) > autodetect
17+
let driver = opts.driver || opts.config.driver || $.detect();
18+
if (!driver) throw new Error('Unable to locate a database driver');
1819

19-
const file = join(__dirname, 'lib', 'clients', lib);
20-
const driver = require(file); // allow throw here
20+
// allow `require` throws
21+
if ($.drivers.includes(driver)) {
22+
driver = require(join(__dirname, 'lib', 'clients', driver));
23+
} else {
24+
if (typeof driver === 'string') driver = require(driver);
25+
$.isDriver(driver); // throws validation error(s)
26+
}
2127

2228
const migrations = await $.glob(dir);
2329

ley.d.ts

+38-1
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ declare namespace Options {
44

55
declare interface Base {
66
cwd?: string;
7-
client?: string;
7+
driver?: string | Driver;
88
require?: string | string[];
99
config?: Config | Resolver;
1010
dir?: string;
@@ -29,6 +29,43 @@ declare class MigrationError extends Error {
2929
readonly migration: Record<'name'|'abs', string>;
3030
}
3131

32+
declare namespace Migration {
33+
type Method = 'up' | 'down';
34+
35+
type Existing = Pick<File, 'name'>;
36+
37+
interface File {
38+
/** The absolute file path */
39+
abs: string;
40+
/** The relative file path from `opts.dir` */
41+
name: string;
42+
}
43+
}
44+
45+
export declare class Driver<Client = unknown> {
46+
/**
47+
* Create a new Client connection using supplied options/config.
48+
* @important Must return the `Client` for further usage.
49+
*/
50+
connect<Client>(config: Options.Config): Promise<Client> | Client;
51+
/**
52+
* Create `migrations` table and query for existing/applied migrations.
53+
* @note You may `require('ley/lib/text')` for premade SQL-like queries.
54+
* @important Must return the `name` of any existing migrations.
55+
*/
56+
setup(client: Client): Promise<Migration.Existing[]>;
57+
/**
58+
* Loop `files` and apply the target `method` action.
59+
* @note Whenever possible, *all* files should partake in the same transaction.
60+
*/
61+
loop(client: Client, files: Migration.File[], method: Migration.Method): Promise<void>;
62+
/**
63+
* Gracefully terminate your client.
64+
* Runs after *all* migrations have been applied, or after an error is thrown.
65+
*/
66+
end(client: Client): Promise<void> | void;
67+
}
68+
3269
export function up(opts?: Options.Up): Promise<string[]>;
3370
export function down(opts?: Options.Down): Promise<string[]>;
3471
export function status(opts?: Options.Base): Promise<string[]>;

lib/util.js

+9-1
Original file line numberDiff line numberDiff line change
@@ -46,15 +46,23 @@ exports.exists = function (dep) {
4646
}
4747

4848
// TODO: sqlite
49+
exports.drivers = ['postgres', 'pg', 'mysql', 'mysql2', 'better-sqlite3'];
50+
4951
exports.detect = function () {
50-
return ['postgres', 'pg', 'mysql', 'mysql2', 'better-sqlite3'].find(exports.exists);
52+
return exports.drivers.find(exports.exists);
5153
}
5254

5355
exports.local = function (str, cwd) {
5456
str = resolve(cwd || '.', str);
5557
return existsSync(str) && require(str);
5658
}
5759

60+
exports.isDriver = function (ctx) {
61+
['connect', 'setup', 'loop', 'end'].forEach(str => {
62+
if (typeof ctx[str] !== 'function') throw new Error(`Driver must have "${str}" function`);
63+
})
64+
}
65+
5866
exports.MigrationError = class MigrationError extends Error {
5967
constructor(err, file) {
6068
super(err.message);

readme.md

+41-9
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@
2323
- [ ] driver support:
2424
- [x] [`pg`](https://www.npmjs.com/package/pg)
2525
- [x] [`postgres`](https://www.npmjs.com/package/postgres)
26-
- [x] [`mysql`]()
27-
- [x] [`mysql2`]()
26+
- [x] [`mysql`](https://www.npmjs.com/package/mysql)
27+
- [x] [`mysql2`](https://www.npmjs.com/package/mysql2)
2828
- [x] [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3)
2929
- [ ] [`sqlite`](https://www.npmjs.com/package/sqlite)
3030
- [ ] complete test coverage
@@ -33,7 +33,7 @@
3333
## Features
3434

3535
* **Agnostic**<br>
36-
_Supports [`postgres`](https://www.npmjs.com/package/postgres), [`pg`](https://www.npmjs.com/package/pg), [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3), [`sqlite`](https://www.npmjs.com/package/sqlite), [`mysql`](https://www.npmjs.com/package/mysql), and [`mysql2`](https://www.npmjs.com/package/mysql2)_
36+
_Supports [`postgres`](https://www.npmjs.com/package/postgres), [`pg`](https://www.npmjs.com/package/pg), [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3), [`sqlite`](https://www.npmjs.com/package/sqlite), [`mysql`](https://www.npmjs.com/package/mysql), [`mysql2`](https://www.npmjs.com/package/mysql2), and [custom drivers!](#drivers)_
3737

3838
* **Lightweight**<br>
3939
_Does **not** include any driver dependencies._
@@ -148,6 +148,35 @@ const ley = require('ley');
148148
const successes = await ley.up({ ... });
149149
```
150150
151+
## Config
152+
153+
> **TL;DR:** The contents of a `ley.config.js` file (default file name) is irrelevant to `ley` itself!
154+
155+
A config file is entirely optional since `ley` assumes that you're providing the correct environment variable(s) for your client driver. However, that may not always be possible. In those instances, a `ley.config.js` file (default file name) can be used to adjust your [driver](#drivers)'s `connect` method.
156+
157+
## Drivers
158+
159+
Out of the box, `ley` includes drivers for the following npm packages:
160+
161+
* [`postgres`](https://www.npmjs.com/package/postgres)
162+
* [`pg`](https://www.npmjs.com/package/pg)
163+
* [`mysql`](https://www.npmjs.com/package/mysql)
164+
* [`mysql2`](https://www.npmjs.com/package/mysql2)
165+
* [`better-sqlite3`](https://www.npmjs.com/package/better-sqlite3)
166+
167+
When no driver is specified, `ley` will attempt to autodetect usage of these libraries in the above order.
168+
169+
However, should you need a driver that's not listed – or should you need to override a supplied driver – you may easily do so via a number of avenues:
170+
171+
1) CLI users can add `--driver <filename>` to any command; or
172+
2) Programmatic users can pass [`opts.driver`](#optsdriver) to any command; or
173+
3) A `ley.config.js` file can export a special `driver` config key.
174+
175+
With any of these, if `driver` is a string then it will be passed through `require()` automatically. Otherwise, with the latter two, the `driver` is assumed to be a [`Driver`](/ley.d.ts#L45-L67) class and is validated as such.
176+
177+
> **Important:** All drivers must adhere to the [`Driver` interface](/ley.d.ts#L45-L67)!
178+
179+
151180
## API
152181

153182
> **Important:** See [Options](#options) for common options shared all commands. <br>In this `API` section, you will only find **command-specific** options listed.
@@ -231,12 +260,13 @@ Default: `migrations`
231260

232261
The directory (relative to `opts.cwd`) to find migration files.
233262

234-
#### opts.client
235-
Type: `string`<br>
263+
#### opts.driver
264+
Type: `string` or `Driver`
236265
Default: `undefined`
237266

238-
The **name** of your desired client driver; for example, `pg`.<br>
239-
When unspecified, `ley` searches for all supported client drivers in this order:
267+
When defined and a `string`, this is the **name** of your [driver](#drivers) implementation. It will pass through `require()` as written. Otherwise it's expected to match a [`Driver` interface](/ley.d.ts#L45-L67) and will be validated immediately.
268+
269+
When undefined, `ley` searches for all supported client drivers in this order:
240270
241271
```js
242272
['postgres', 'pg', 'mysql', 'mysql2', 'better-sqlite3']
@@ -249,8 +279,9 @@ Default: `undefined`
249279
A configuration object for your client driver to establish a connection.<br>
250280
When unspecified, `ley` assumes that your client driver is able to connect through `process.env` variables.
251281
252-
>**Note:** The `ley` CLI will search for a `ley.config.js` config file (configurable).<br>
253-
If found, this file may contain an object or a function that resolves to your config object.
282+
> **Note:** The `ley` CLI will search for a `ley.config.js` config file (configurable). <br>
283+
If found, this file may contain an object or a function that resolves to _anything_ of your chosing. <br>
284+
Please see [Config](#config) for more information.
254285
255286
#### opts.require
256287
Type: `string` or `string[]`<br>
@@ -276,6 +307,7 @@ $ ley -r dotenv/config status
276307
$ ley --require dotenv/config status
277308
```
278309
310+
279311
## License
280312
281313
MIT © [Luke Edwards](https://lukeed.com)

test/util.js

+54
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,58 @@ test('(utils) MigrationError', () => {
231231
assert.equal(error.migration, migration, 'attaches custom "migration" key w/ file data');
232232
});
233233

234+
test('(utils) isDriver :: connect', () => {
235+
assert.throws(
236+
() => $.isDriver({}),
237+
'Driver must have "connect" function'
238+
);
239+
240+
assert.throws(
241+
() => $.isDriver({ connect: 123 }),
242+
'Driver must have "connect" function'
243+
);
244+
});
245+
246+
test('(utils) isDriver :: setup', () => {
247+
let noop = () => {};
248+
249+
assert.throws(
250+
() => $.isDriver({ connect: noop }),
251+
'Driver must have "setup" function'
252+
);
253+
254+
assert.throws(
255+
() => $.isDriver({ connect: noop, setup: 123 }),
256+
'Driver must have "setup" function'
257+
);
258+
});
259+
260+
test('(utils) isDriver :: loop', () => {
261+
let noop = () => {};
262+
263+
assert.throws(
264+
() => $.isDriver({ connect: noop, setup: noop }),
265+
'Driver must have "loop" function'
266+
);
267+
268+
assert.throws(
269+
() => $.isDriver({ connect: noop, setup: noop, loop: 123 }),
270+
'Driver must have "loop" function'
271+
);
272+
});
273+
274+
test('(utils) isDriver :: end', () => {
275+
let noop = () => {};
276+
277+
assert.throws(
278+
() => $.isDriver({ connect: noop, setup: noop, loop: noop }),
279+
'Driver must have "end" function'
280+
);
281+
282+
assert.throws(
283+
() => $.isDriver({ connect: noop, setup: noop, loop: noop, end: 123 }),
284+
'Driver must have "end" function'
285+
);
286+
});
287+
234288
test.run();

0 commit comments

Comments
 (0)