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

Commit 86f0642

Browse files
authored
feat(datasource): implement connection pooling and sequelize specific options support (#27)
* feat(datasource): add connection pooling support adds ability to prase connection pooling options for postgres, mysql and oracle GH-26 * feat(datasource): add capability to pass options directly to sequelize added ability to connect with url added new property called `sequelizeOptions` in datasource config to allow direct option forwarding to sequelize instance GH-26
1 parent df5bf4d commit 86f0642

File tree

5 files changed

+265
-6
lines changed

5 files changed

+265
-6
lines changed

README.md

Lines changed: 21 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,27 @@ export class PgDataSource
5252
}
5353
```
5454

55+
`SequelizeDataSource` accepts commonly used config in the same way as loopback did. So in most cases you won't need to change your existing configuration. But if you want to use sequelize specific options pass them in `sequelizeOptions` like below:
56+
57+
```ts
58+
let config = {
59+
name: 'db',
60+
connector: 'postgresql',
61+
sequelizeOptions: {
62+
username: 'postgres',
63+
password: 'secret',
64+
dialectOptions: {
65+
ssl: {
66+
rejectUnauthorized: false,
67+
ca: fs.readFileSync('/path/to/root.crt').toString(),
68+
},
69+
},
70+
},
71+
};
72+
```
73+
74+
> Note: Options provided in `sequelizeOptions` will take priority over others, For eg. if you have password specified in both `config.password` and `config.password.sequelizeOptions` the latter one will be used.
75+
5576
### Step 2: Configure Repository
5677

5778
Change the parent class from `DefaultCrudRepository` to `SequelizeCrudRepository` like below.
@@ -219,7 +240,6 @@ There are three built-in debug strings available in this extension to aid in deb
219240
Please note, the current implementation does not support the following:
220241

221242
1. Loopback Migrations (via default `migrate.ts`). Though you're good if using external packages like [`db-migrate`](https://www.npmjs.com/package/db-migrate).
222-
2. Connection Pooling is not implemented yet.
223243

224244
Community contribution is welcome.
225245

src/__tests__/fixtures/datasources/config.ts

Lines changed: 23 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,7 @@ type AvailableConfig = Record<
88
>;
99

1010
export const datasourceTestConfig: Record<
11-
'primary' | 'secondary',
11+
'primary' | 'secondary' | 'url' | 'wrongPassword',
1212
AvailableConfig
1313
> = {
1414
primary: {
@@ -47,4 +47,26 @@ export const datasourceTestConfig: Record<
4747
file: ':memory:',
4848
},
4949
},
50+
url: {
51+
postgresql: {
52+
name: 'using-url',
53+
connector: 'postgresql',
54+
url: 'postgres://postgres:super-secret@localhost:5002/postgres',
55+
},
56+
sqlite3: {
57+
name: 'using-url',
58+
url: 'sqlite::memory:',
59+
},
60+
},
61+
wrongPassword: {
62+
postgresql: {
63+
name: 'wrongPassword',
64+
connector: 'postgresql',
65+
url: 'postgres://postgres:super-secret-wrong@localhost:5002/postgres',
66+
},
67+
sqlite3: {
68+
name: 'wrongPassword',
69+
url: 'sqlite::memory:',
70+
},
71+
},
5072
};

src/__tests__/unit/sequelize.datasource.unit.ts

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
import {expect} from '@loopback/testlab';
22
import {SequelizeDataSource} from '../../sequelize';
33
import {SupportedLoopbackConnectors} from '../../sequelize/connector-mapping';
4+
import {datasourceTestConfig} from '../fixtures/datasources/config';
5+
import {config as primaryDataSourceConfig} from '../fixtures/datasources/primary.datasource';
46

57
describe('Sequelize DataSource', () => {
68
it('throws error when nosql connectors are supplied', () => {
@@ -16,4 +18,98 @@ describe('Sequelize DataSource', () => {
1618
expect(result).which.eql('Specified connector memory is not supported.');
1719
}
1820
});
21+
22+
it('accepts url strings for connection', async () => {
23+
const dataSource = new SequelizeDataSource(
24+
datasourceTestConfig.url[
25+
primaryDataSourceConfig.connector === 'postgresql'
26+
? 'postgresql'
27+
: 'sqlite3'
28+
],
29+
);
30+
expect(await dataSource.init()).to.not.throwError();
31+
await dataSource.stop();
32+
});
33+
34+
it('throws error if url strings has wrong password', async function () {
35+
if (primaryDataSourceConfig.connector !== 'postgresql') {
36+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
37+
this.skip();
38+
}
39+
const dataSource = new SequelizeDataSource(
40+
datasourceTestConfig.wrongPassword.postgresql,
41+
);
42+
try {
43+
await dataSource.init();
44+
} catch (err) {
45+
expect(err.message).to.be.eql(
46+
'password authentication failed for user "postgres"',
47+
);
48+
}
49+
});
50+
51+
it('should be able override sequelize options', async function () {
52+
if (primaryDataSourceConfig.connector !== 'postgresql') {
53+
// eslint-disable-next-line @typescript-eslint/no-invalid-this
54+
this.skip();
55+
}
56+
const dataSource = new SequelizeDataSource({
57+
...datasourceTestConfig.primary.postgresql,
58+
user: 'wrong-username', // expected to be overridden
59+
sequelizeOptions: {
60+
username: datasourceTestConfig.primary.postgresql.user,
61+
},
62+
});
63+
expect(await dataSource.init()).to.not.throwError();
64+
});
65+
66+
it('parses pool options for postgresql', async () => {
67+
const dataSource = new SequelizeDataSource({
68+
name: 'db',
69+
connector: 'postgresql',
70+
min: 10,
71+
max: 20,
72+
idleTimeoutMillis: 18000,
73+
});
74+
75+
const poolOptions = dataSource.getPoolOptions();
76+
77+
expect(poolOptions).to.have.property('min', 10);
78+
expect(poolOptions).to.have.property('max', 20);
79+
expect(poolOptions).to.have.property('idle', 18000);
80+
expect(poolOptions).to.not.have.property('acquire');
81+
});
82+
83+
it('parses pool options for mysql', async () => {
84+
const dataSource = new SequelizeDataSource({
85+
name: 'db',
86+
connector: 'mysql',
87+
connectionLimit: 20,
88+
acquireTimeout: 10000,
89+
});
90+
91+
const poolOptions = dataSource.getPoolOptions();
92+
93+
expect(poolOptions).to.have.property('max', 20);
94+
expect(poolOptions).to.have.property('acquire', 10000);
95+
expect(poolOptions).to.not.have.property('min');
96+
expect(poolOptions).to.not.have.property('idle');
97+
});
98+
99+
it('parses pool options for oracle', async () => {
100+
const dataSource = new SequelizeDataSource({
101+
name: 'db',
102+
connector: 'oracle',
103+
minConn: 10,
104+
maxConn: 20,
105+
timeout: 20000,
106+
});
107+
108+
const poolOptions = dataSource.getPoolOptions();
109+
110+
expect(poolOptions).to.have.property('min', 10);
111+
expect(poolOptions).to.have.property('max', 20);
112+
expect(poolOptions).to.have.property('idle', 20000);
113+
expect(poolOptions).to.not.have.property('acquire');
114+
});
19115
});

src/sequelize/connector-mapping.ts

Lines changed: 53 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import {Dialect as AllSequelizeDialects} from 'sequelize';
1+
import {Dialect as AllSequelizeDialects, PoolOptions} from 'sequelize';
22

33
export type SupportedLoopbackConnectors =
44
| 'mysql'
@@ -19,3 +19,55 @@ export const SupportedConnectorMapping: {
1919
sqlite3: 'sqlite',
2020
db2: 'db2',
2121
};
22+
23+
/**
24+
* Loopback uses different keys for pool options depending on the connector.
25+
*/
26+
export const poolConfigKeys = [
27+
// mysql
28+
'connectionLimit',
29+
'acquireTimeout',
30+
// postgresql
31+
'min',
32+
'max',
33+
'idleTimeoutMillis',
34+
// oracle
35+
'minConn',
36+
'maxConn',
37+
'timeout',
38+
] as const;
39+
export type LoopbackPoolConfigKey = (typeof poolConfigKeys)[number];
40+
41+
export type PoolingEnabledConnector = Exclude<
42+
SupportedLoopbackConnectors,
43+
'db2' | 'sqlite3'
44+
>;
45+
46+
export const poolingEnabledConnectors: PoolingEnabledConnector[] = [
47+
'mysql',
48+
'oracle',
49+
'postgresql',
50+
];
51+
52+
type IConnectionPoolOptions = {
53+
[connectorName in PoolingEnabledConnector]?: {
54+
[sequelizePoolOption in keyof PoolOptions]: LoopbackPoolConfigKey;
55+
};
56+
};
57+
58+
export const ConnectionPoolOptions: IConnectionPoolOptions = {
59+
mysql: {
60+
max: 'connectionLimit',
61+
acquire: 'acquireTimeout',
62+
},
63+
postgresql: {
64+
min: 'min',
65+
max: 'max',
66+
idle: 'idleTimeoutMillis',
67+
},
68+
oracle: {
69+
min: 'minConn',
70+
max: 'maxConn',
71+
idle: 'timeout',
72+
},
73+
};

src/sequelize/sequelize.datasource.base.ts

Lines changed: 72 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,11 +3,17 @@ import {AnyObject} from '@loopback/repository';
33
import debugFactory from 'debug';
44
import {
55
Options as SequelizeOptions,
6+
PoolOptions,
67
Sequelize,
78
Transaction,
89
TransactionOptions,
910
} from 'sequelize';
1011
import {
12+
ConnectionPoolOptions,
13+
LoopbackPoolConfigKey,
14+
poolConfigKeys,
15+
PoolingEnabledConnector,
16+
poolingEnabledConnectors,
1117
SupportedConnectorMapping as supportedConnectorMapping,
1218
SupportedLoopbackConnectors,
1319
} from './connector-mapping';
@@ -35,7 +41,7 @@ export class SequelizeDataSource implements LifeCycleObserver {
3541
}
3642

3743
sequelize?: Sequelize;
38-
sequelizeConfig: SequelizeDataSourceConfig;
44+
sequelizeConfig: SequelizeOptions;
3945
async init(): Promise<void> {
4046
const {config} = this;
4147
const {
@@ -60,9 +66,15 @@ export class SequelizeDataSource implements LifeCycleObserver {
6066
username: user ?? username,
6167
password,
6268
logging: queryLogging,
69+
pool: this.getPoolOptions(),
70+
...config.sequelizeOptions,
6371
};
6472

65-
this.sequelize = new Sequelize(this.sequelizeConfig);
73+
if (config.url) {
74+
this.sequelize = new Sequelize(config.url, this.sequelizeConfig);
75+
} else {
76+
this.sequelize = new Sequelize(this.sequelizeConfig);
77+
}
6678

6779
await this.sequelize.authenticate();
6880
debug('Connection has been established successfully.');
@@ -114,11 +126,68 @@ export class SequelizeDataSource implements LifeCycleObserver {
114126

115127
return this.sequelize!.transaction(options);
116128
}
129+
130+
getPoolOptions(): PoolOptions | undefined {
131+
const config: SequelizeDataSourceConfig = this.config;
132+
const specifiedPoolOptions = Object.keys(config).some(key =>
133+
poolConfigKeys.includes(key as LoopbackPoolConfigKey),
134+
);
135+
const supportsPooling =
136+
config.connector &&
137+
(poolingEnabledConnectors as string[]).includes(config.connector);
138+
139+
if (!(supportsPooling && specifiedPoolOptions)) {
140+
return;
141+
}
142+
const optionMapping =
143+
ConnectionPoolOptions[config.connector as PoolingEnabledConnector];
144+
145+
if (!optionMapping) {
146+
return;
147+
}
148+
149+
const {min, max, acquire, idle} = optionMapping;
150+
const options: PoolOptions = {};
151+
if (max && config[max]) {
152+
options.max = config[max];
153+
}
154+
if (min && config[min]) {
155+
options.min = config[min];
156+
}
157+
if (acquire && config[acquire]) {
158+
options.acquire = config[acquire];
159+
}
160+
if (idle && config[idle]) {
161+
options.idle = config[idle];
162+
}
163+
return options;
164+
}
117165
}
118166

119-
export type SequelizeDataSourceConfig = SequelizeOptions & {
167+
export type SequelizeDataSourceConfig = {
120168
name?: string;
121169
user?: string;
122170
connector?: SupportedLoopbackConnectors;
123171
url?: string;
172+
/**
173+
* Additional sequelize options that are passed directly to
174+
* Sequelize when initializing the connection.
175+
* Any options provided in this way will take priority over
176+
* other configurations that may come from parsing the loopback style configurations.
177+
*
178+
* eg.
179+
* ```ts
180+
* let config = {
181+
* name: 'db',
182+
* connector: 'postgresql',
183+
* sequelizeOptions: {
184+
* dialectOptions: {
185+
* rejectUnauthorized: false,
186+
* ca: fs.readFileSync('/path/to/root.crt').toString(),
187+
* }
188+
* }
189+
* };
190+
* ```
191+
*/
192+
sequelizeOptions?: SequelizeOptions;
124193
} & AnyObject;

0 commit comments

Comments
 (0)