@alt-javascript/camel-lite-component-sql 1.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +64 -0
- package/package.json +42 -0
- package/src/ParameterBinder.js +63 -0
- package/src/SqlComponent.js +118 -0
- package/src/SqlConsumer.js +39 -0
- package/src/SqlEndpoint.js +53 -0
- package/src/SqlProducer.js +86 -0
- package/src/SqliteClientFactory.js +20 -0
- package/src/index.js +6 -0
- package/test/sql.test.js +397 -0
package/README.md
ADDED
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
[](https://opensource.org/licenses/MIT)
|
|
2
|
+
|
|
3
|
+
## What
|
|
4
|
+
|
|
5
|
+
SQL query and update producer using Node.js built-in [`node:sqlite`](https://nodejs.org/api/sqlite.html) — no native compilation required. The SQL statement is placed in the URI path (URL-encoded). Query results are set as the exchange body; for updates the row-change count is set.
|
|
6
|
+
|
|
7
|
+
> **Requires Node.js 22.5.0 or later.** Uses `node:sqlite` (DatabaseSync / StatementSync). No `better-sqlite3` or other native dependency.
|
|
8
|
+
|
|
9
|
+
## Install
|
|
10
|
+
|
|
11
|
+
```sh
|
|
12
|
+
npm install camel-lite-component-sql
|
|
13
|
+
```
|
|
14
|
+
|
|
15
|
+
## URI Syntax
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
sql:<URL-encoded SQL>[?url=jdbc:sqlite::memory:]
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
URL-encode spaces as `+` in the SQL statement.
|
|
22
|
+
|
|
23
|
+
| Parameter | Default | Description |
|
|
24
|
+
|-----------|---------|-------------|
|
|
25
|
+
| `url` | *(required)* | SQLite connection URL. Format: `jdbc:sqlite:<file-path>` or `jdbc:sqlite::memory:` for in-memory. |
|
|
26
|
+
|
|
27
|
+
## Usage
|
|
28
|
+
|
|
29
|
+
```js
|
|
30
|
+
import { CamelContext } from 'camel-lite-core';
|
|
31
|
+
import { SqlComponent } from 'camel-lite-component-sql';
|
|
32
|
+
|
|
33
|
+
const context = new CamelContext();
|
|
34
|
+
context.addComponent('sql', new SqlComponent());
|
|
35
|
+
|
|
36
|
+
context.addRoutes({
|
|
37
|
+
configure(ctx) {
|
|
38
|
+
// Insert: exchange body provides the bound parameter value
|
|
39
|
+
ctx.from('direct:logEvent')
|
|
40
|
+
.to('sql:INSERT+INTO+events+(message)+VALUES+(?)?url=jdbc:sqlite:/tmp/events.db');
|
|
41
|
+
|
|
42
|
+
// Query: results returned as array of row objects
|
|
43
|
+
ctx.from('direct:fetchEvents')
|
|
44
|
+
.to('sql:SELECT+*+FROM+events?url=jdbc:sqlite:/tmp/events.db');
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
await context.start();
|
|
49
|
+
|
|
50
|
+
const template = context.createProducerTemplate();
|
|
51
|
+
|
|
52
|
+
// Insert
|
|
53
|
+
await template.sendBody('direct:logEvent', 'something happened');
|
|
54
|
+
|
|
55
|
+
// Query
|
|
56
|
+
const exchange = await template.send('direct:fetchEvents', ex => { ex.in.body = []; });
|
|
57
|
+
console.log('Rows:', exchange.in.body);
|
|
58
|
+
|
|
59
|
+
await context.stop();
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
## See Also
|
|
63
|
+
|
|
64
|
+
[camel-lite — root README](../../README.md)
|
package/package.json
ADDED
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@alt-javascript/camel-lite-component-sql",
|
|
3
|
+
"version": "1.0.2",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"exports": {
|
|
6
|
+
".": "./src/index.js"
|
|
7
|
+
},
|
|
8
|
+
"dependencies": {
|
|
9
|
+
"@alt-javascript/logger": "^3.0.7",
|
|
10
|
+
"@alt-javascript/config": "^3.0.7",
|
|
11
|
+
"@alt-javascript/common": "^3.0.7",
|
|
12
|
+
"@alt-javascript/camel-lite-core": "1.0.2"
|
|
13
|
+
},
|
|
14
|
+
"scripts": {
|
|
15
|
+
"test": "node --test"
|
|
16
|
+
},
|
|
17
|
+
"repository": {
|
|
18
|
+
"type": "git",
|
|
19
|
+
"url": "https://github.com/alt-javascript/camel-lite"
|
|
20
|
+
},
|
|
21
|
+
"author": "Craig Parravicini",
|
|
22
|
+
"contributors": [
|
|
23
|
+
"Claude (Anthropic)",
|
|
24
|
+
"Apache Camel — design inspiration and pattern source"
|
|
25
|
+
],
|
|
26
|
+
"keywords": [
|
|
27
|
+
"alt-javascript",
|
|
28
|
+
"camel",
|
|
29
|
+
"camel-lite",
|
|
30
|
+
"eai",
|
|
31
|
+
"eip",
|
|
32
|
+
"integration",
|
|
33
|
+
"sql",
|
|
34
|
+
"sqlite",
|
|
35
|
+
"database",
|
|
36
|
+
"component"
|
|
37
|
+
],
|
|
38
|
+
"publishConfig": {
|
|
39
|
+
"registry": "https://registry.npmjs.org/",
|
|
40
|
+
"access": "public"
|
|
41
|
+
}
|
|
42
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ParameterBinder — normalises named `:param` placeholders to driver-specific syntax.
|
|
3
|
+
*
|
|
4
|
+
* Supported dialects:
|
|
5
|
+
* 'pg' → $1, $2, ... (positional, params returned as array)
|
|
6
|
+
* 'mysql2' → ?, ?, ... (positional, params returned as array)
|
|
7
|
+
* 'sqlite' → ?, ?, ... (positional, params returned as array)
|
|
8
|
+
* 'named' → :param (pass-through, params returned as object)
|
|
9
|
+
*
|
|
10
|
+
* Input template uses :paramName placeholders:
|
|
11
|
+
* SELECT * FROM users WHERE id = :id AND status = :status
|
|
12
|
+
*
|
|
13
|
+
* Params are sourced from (in order):
|
|
14
|
+
* 1. exchange.in.body if it is a plain object
|
|
15
|
+
* 2. exchange.in.headers (as key→value Map)
|
|
16
|
+
* 3. Empty object if neither is a plain object
|
|
17
|
+
*/
|
|
18
|
+
export const ParameterBinder = {
|
|
19
|
+
/**
|
|
20
|
+
* Extract bind params from the exchange.
|
|
21
|
+
* @param {import('@alt-javascript/camel-lite-core').Exchange} exchange
|
|
22
|
+
* @returns {object} key→value map
|
|
23
|
+
*/
|
|
24
|
+
extractParams(exchange) {
|
|
25
|
+
const body = exchange.in.body;
|
|
26
|
+
if (body !== null && typeof body === 'object' && !Array.isArray(body)) {
|
|
27
|
+
return body;
|
|
28
|
+
}
|
|
29
|
+
// Fallback: collect all headers as params
|
|
30
|
+
const params = {};
|
|
31
|
+
if (exchange.in.headers instanceof Map) {
|
|
32
|
+
for (const [k, v] of exchange.in.headers) {
|
|
33
|
+
params[k] = v;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
return params;
|
|
37
|
+
},
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Replace :paramName placeholders according to the dialect.
|
|
41
|
+
* @param {string} template - SQL with :paramName placeholders
|
|
42
|
+
* @param {object} params - key→value map of bind values
|
|
43
|
+
* @param {'pg'|'mysql2'|'sqlite'|'named'} dialect
|
|
44
|
+
* @returns {{ sql: string, values: Array|object }}
|
|
45
|
+
*/
|
|
46
|
+
replaceParams(template, params, dialect = 'sqlite') {
|
|
47
|
+
if (dialect === 'named') {
|
|
48
|
+
return { sql: template, values: params };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const values = [];
|
|
52
|
+
let counter = 0;
|
|
53
|
+
const sql = template.replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_, name) => {
|
|
54
|
+
values.push(params[name] ?? null);
|
|
55
|
+
counter++;
|
|
56
|
+
return dialect === 'pg' ? `$${counter}` : '?';
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
return { sql, values };
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
export default ParameterBinder;
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { Component } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
3
|
+
import { SqlEndpoint } from './SqlEndpoint.js';
|
|
4
|
+
|
|
5
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/SqlComponent');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SqlComponent — executes SQL queries as a pipeline step.
|
|
9
|
+
*
|
|
10
|
+
* Datasource resolution (three-step chain, evaluated at send() time):
|
|
11
|
+
*
|
|
12
|
+
* 1. Component-internal map — setDatasource(name, factory) takes priority.
|
|
13
|
+
* Name comes from: ?datasource= URI param, then the sql: path segment.
|
|
14
|
+
*
|
|
15
|
+
* 2. Context bean by name — context.getBean(name) is checked when the
|
|
16
|
+
* component map has no match. The bean is the datasource object directly
|
|
17
|
+
* (not a factory wrapper).
|
|
18
|
+
*
|
|
19
|
+
* 3. Auto-select — when no explicit name is given (or the named
|
|
20
|
+
* lookup found nothing) and context.getBeans() has exactly one entry,
|
|
21
|
+
* that bean is used automatically.
|
|
22
|
+
*
|
|
23
|
+
* setDatasource() registers a factory (zero-arg fn → db connection/pool).
|
|
24
|
+
* Context beans are registered directly: context.registerBean('myDb', db).
|
|
25
|
+
*
|
|
26
|
+
* URI formats (all equivalent):
|
|
27
|
+
* sql:myDsName?query=SELECT+1&dialect=sqlite ← path segment as ds name
|
|
28
|
+
* sql:?datasource=myDsBean&query=SELECT+1 ← explicit param (overrides path)
|
|
29
|
+
* sql:?query=SELECT+1&dialect=sqlite ← auto-select when 1 bean in ctx
|
|
30
|
+
*
|
|
31
|
+
* Supported driver shapes (communicated via 'dialect' URI param, default 'sqlite'):
|
|
32
|
+
* 'sqlite' → node:sqlite (DatabaseSync) — synchronous
|
|
33
|
+
* 'pg' → pg Pool — async: pool.query(sql, values)
|
|
34
|
+
* 'mysql2' → mysql2 Pool — async: pool.query(sql, values)
|
|
35
|
+
* 'named' → node:sqlite with pass-through named params
|
|
36
|
+
*/
|
|
37
|
+
class SqlComponent extends Component {
|
|
38
|
+
#datasources = new Map(); // name → factory function (explicit component-level registration)
|
|
39
|
+
#endpoints = new Map();
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Register a named datasource factory on the component.
|
|
43
|
+
* This takes priority over context bean lookup.
|
|
44
|
+
* @param {string} name
|
|
45
|
+
* @param {function} factory - () => db/pool connection
|
|
46
|
+
*/
|
|
47
|
+
setDatasource(name, factory) {
|
|
48
|
+
this.#datasources.set(name, factory);
|
|
49
|
+
log.info(`SqlComponent: datasource '${name}' registered on component`);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Resolve a datasource connection using the three-step chain.
|
|
55
|
+
* @param {string|null} name - bean/datasource name, or null for auto-select
|
|
56
|
+
* @param {CamelContext|null} context
|
|
57
|
+
* @returns {*} the database connection/pool
|
|
58
|
+
*/
|
|
59
|
+
getDatasource(name, context = null) {
|
|
60
|
+
// Step 1: component-internal map
|
|
61
|
+
if (name && this.#datasources.has(name)) {
|
|
62
|
+
log.debug(`SqlComponent: datasource '${name}' resolved from component map`);
|
|
63
|
+
return this.#datasources.get(name)();
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (context) {
|
|
67
|
+
// Step 2: context bean by name
|
|
68
|
+
if (name) {
|
|
69
|
+
const bean = context.getBean(name);
|
|
70
|
+
if (bean != null) {
|
|
71
|
+
log.debug(`SqlComponent: datasource '${name}' resolved from context bean`);
|
|
72
|
+
return bean;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Step 3: single-bean auto-select
|
|
77
|
+
const allBeans = context.getBeans();
|
|
78
|
+
if (allBeans.length === 1) {
|
|
79
|
+
log.debug(`SqlComponent: datasource auto-selected (single bean in context: '${allBeans[0][0]}')`);
|
|
80
|
+
return allBeans[0][1];
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
throw new Error(
|
|
85
|
+
`SqlComponent: cannot resolve datasource '${name ?? '(none)'}'. ` +
|
|
86
|
+
`Register via component.setDatasource(), context.registerBean(), ` +
|
|
87
|
+
`or ensure exactly one bean is registered in context.`
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
createEndpoint(uri, remaining, parameters, context) {
|
|
92
|
+
if (this.#endpoints.has(uri)) {
|
|
93
|
+
return this.#endpoints.get(uri);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
// ?datasource= param takes priority over the path segment as the ds name.
|
|
97
|
+
// Either may be absent — null triggers auto-select at send() time.
|
|
98
|
+
const pathName = remaining.replace(/^\/+/, '') || null;
|
|
99
|
+
const datasourceName = parameters.get('datasource') ?? pathName;
|
|
100
|
+
|
|
101
|
+
const query = parameters.get('query') ?? '';
|
|
102
|
+
const outputType = parameters.get('outputType') ?? 'rows';
|
|
103
|
+
const dialect = parameters.get('dialect') ?? 'sqlite';
|
|
104
|
+
|
|
105
|
+
if (!query) {
|
|
106
|
+
throw new Error(`SqlEndpoint: 'query' parameter is required on URI: ${uri}`);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
log.info(`SqlComponent creating endpoint: ds=${datasourceName ?? '(auto)'}, dialect=${dialect}, outputType=${outputType}`);
|
|
110
|
+
|
|
111
|
+
const endpoint = new SqlEndpoint(uri, datasourceName, query, outputType, dialect, context, this);
|
|
112
|
+
this.#endpoints.set(uri, endpoint);
|
|
113
|
+
return endpoint;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export { SqlComponent };
|
|
118
|
+
export default SqlComponent;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { Consumer } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
3
|
+
|
|
4
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/SqlConsumer');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* SqlConsumer — stub implementation.
|
|
8
|
+
* sql: is primarily producer-oriented (execute SQL on-demand as a pipeline step).
|
|
9
|
+
* A scheduled poll consumer (timer-driven SELECT) is deferred to a future milestone.
|
|
10
|
+
*/
|
|
11
|
+
class SqlConsumer extends Consumer {
|
|
12
|
+
#endpoint;
|
|
13
|
+
#pipeline;
|
|
14
|
+
|
|
15
|
+
constructor(endpoint, pipeline) {
|
|
16
|
+
super();
|
|
17
|
+
this.#endpoint = endpoint;
|
|
18
|
+
this.#pipeline = pipeline;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async start() {
|
|
22
|
+
const { uri, context } = this.#endpoint;
|
|
23
|
+
context.registerConsumer(uri, this);
|
|
24
|
+
log.info(`SqlConsumer started: ${uri}`);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async stop() {
|
|
28
|
+
const { uri, context } = this.#endpoint;
|
|
29
|
+
context.registerConsumer(uri, null);
|
|
30
|
+
log.info(`SqlConsumer stopped: ${uri}`);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async process(exchange) {
|
|
34
|
+
return this.#pipeline.run(exchange);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export { SqlConsumer };
|
|
39
|
+
export default SqlConsumer;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { Endpoint } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { SqlProducer } from './SqlProducer.js';
|
|
3
|
+
import { SqlConsumer } from './SqlConsumer.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* SqlEndpoint holds the parsed URI state for a sql: endpoint.
|
|
7
|
+
*
|
|
8
|
+
* URI format:
|
|
9
|
+
* sql:datasourceName?query=SELECT+*+FROM+users&outputType=rows&dialect=sqlite
|
|
10
|
+
*
|
|
11
|
+
* outputType: 'rows' (default) → sets exchange.in.body = array of row objects
|
|
12
|
+
* 'rowCount' → sets exchange.in.body = { rowCount: N }
|
|
13
|
+
* dialect: 'sqlite' (default) | 'pg' | 'mysql2' | 'named'
|
|
14
|
+
*/
|
|
15
|
+
class SqlEndpoint extends Endpoint {
|
|
16
|
+
#uri;
|
|
17
|
+
#datasourceName;
|
|
18
|
+
#query;
|
|
19
|
+
#outputType;
|
|
20
|
+
#dialect;
|
|
21
|
+
#context;
|
|
22
|
+
#component;
|
|
23
|
+
|
|
24
|
+
constructor(uri, datasourceName, query, outputType, dialect, context, component) {
|
|
25
|
+
super();
|
|
26
|
+
this.#uri = uri;
|
|
27
|
+
this.#datasourceName = datasourceName;
|
|
28
|
+
this.#query = query;
|
|
29
|
+
this.#outputType = outputType;
|
|
30
|
+
this.#dialect = dialect;
|
|
31
|
+
this.#context = context;
|
|
32
|
+
this.#component = component;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
get uri() { return this.#uri; }
|
|
36
|
+
get datasourceName() { return this.#datasourceName; }
|
|
37
|
+
get query() { return this.#query; }
|
|
38
|
+
get outputType() { return this.#outputType; }
|
|
39
|
+
get dialect() { return this.#dialect; }
|
|
40
|
+
get context() { return this.#context; }
|
|
41
|
+
get component() { return this.#component; }
|
|
42
|
+
|
|
43
|
+
createProducer() {
|
|
44
|
+
return new SqlProducer(this);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
createConsumer(pipeline) {
|
|
48
|
+
return new SqlConsumer(this, pipeline);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export { SqlEndpoint };
|
|
53
|
+
export default SqlEndpoint;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { Producer } from '@alt-javascript/camel-lite-core';
|
|
2
|
+
import { LoggerFactory } from '@alt-javascript/logger';
|
|
3
|
+
import { ParameterBinder } from './ParameterBinder.js';
|
|
4
|
+
|
|
5
|
+
const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/SqlProducer');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* SqlProducer — executes the endpoint's SQL query against the registered datasource.
|
|
9
|
+
*
|
|
10
|
+
* Result mapping:
|
|
11
|
+
* SELECT (outputType='rows') → exchange.in.body = array of row objects
|
|
12
|
+
* INSERT/UPDATE/DELETE → exchange.in.body = { rowCount: N }
|
|
13
|
+
* outputType='rowCount' forced → exchange.in.body = { rowCount: N }
|
|
14
|
+
*
|
|
15
|
+
* Driver detection (via endpoint.dialect):
|
|
16
|
+
* 'sqlite' → better-sqlite3 sync API: db.prepare(sql).all(values) / .run(values)
|
|
17
|
+
* 'pg' → pg async API: pool.query(sql, values) → { rows, rowCount }
|
|
18
|
+
* 'mysql2' → mysql2 async API: pool.query(sql, values) → [rows, fields]
|
|
19
|
+
* 'named' → better-sqlite3 named API: db.prepare(sql).all(namedObj)
|
|
20
|
+
*/
|
|
21
|
+
class SqlProducer extends Producer {
|
|
22
|
+
#endpoint;
|
|
23
|
+
|
|
24
|
+
constructor(endpoint) {
|
|
25
|
+
super();
|
|
26
|
+
this.#endpoint = endpoint;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
async send(exchange) {
|
|
30
|
+
const { datasourceName, query, outputType, dialect, component, context } = this.#endpoint;
|
|
31
|
+
|
|
32
|
+
const db = component.getDatasource(datasourceName, context);
|
|
33
|
+
const params = ParameterBinder.extractParams(exchange);
|
|
34
|
+
const { sql, values } = ParameterBinder.replaceParams(query, params, dialect);
|
|
35
|
+
|
|
36
|
+
log.debug(`SqlProducer [${dialect}] executing: ${sql}`);
|
|
37
|
+
|
|
38
|
+
let result;
|
|
39
|
+
if (dialect === 'sqlite' || dialect === 'named') {
|
|
40
|
+
result = await SqlProducer.#execSqlite(db, sql, values, outputType);
|
|
41
|
+
} else if (dialect === 'pg') {
|
|
42
|
+
result = await SqlProducer.#execPg(db, sql, values, outputType);
|
|
43
|
+
} else if (dialect === 'mysql2') {
|
|
44
|
+
result = await SqlProducer.#execMysql2(db, sql, values, outputType);
|
|
45
|
+
} else {
|
|
46
|
+
throw new Error(`SqlProducer: unknown dialect '${dialect}'`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
exchange.in.body = result;
|
|
50
|
+
log.debug(`SqlProducer done: outputType=${outputType}`);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
static async #execSqlite(db, sql, values, outputType) {
|
|
54
|
+
const stmt = db.prepare(sql);
|
|
55
|
+
const sqlTrimmed = sql.trimStart().toUpperCase();
|
|
56
|
+
const isSelect = sqlTrimmed.startsWith('SELECT') || sqlTrimmed.startsWith('WITH');
|
|
57
|
+
|
|
58
|
+
if (outputType === 'rows' && isSelect) {
|
|
59
|
+
// node:sqlite StatementSync.all() takes spread args, not an array.
|
|
60
|
+
// We spread values so both [] and [...items] forms work.
|
|
61
|
+
return stmt.all(...(Array.isArray(values) ? values : Object.values(values)));
|
|
62
|
+
} else {
|
|
63
|
+
const info = stmt.run(...(Array.isArray(values) ? values : Object.values(values)));
|
|
64
|
+
return { rowCount: info.changes };
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
static async #execPg(pool, sql, values, outputType) {
|
|
69
|
+
const result = await pool.query(sql, values);
|
|
70
|
+
if (outputType === 'rows') {
|
|
71
|
+
return result.rows;
|
|
72
|
+
}
|
|
73
|
+
return { rowCount: result.rowCount };
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
static async #execMysql2(pool, sql, values, outputType) {
|
|
77
|
+
const [rows] = await pool.query(sql, values);
|
|
78
|
+
if (outputType === 'rows' && Array.isArray(rows)) {
|
|
79
|
+
return rows;
|
|
80
|
+
}
|
|
81
|
+
return { rowCount: rows.affectedRows ?? 0 };
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { SqlProducer };
|
|
86
|
+
export default SqlProducer;
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SQLite factory using Node's built-in node:sqlite module (Node 22.5+).
|
|
5
|
+
* No native compile required — uses V8's built-in SQLite support.
|
|
6
|
+
*
|
|
7
|
+
* Note: node:sqlite is marked experimental in Node 22/24 but is stable enough
|
|
8
|
+
* for our use case. No external dependency needed.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Open an in-memory or file-based SQLite database.
|
|
13
|
+
* @param {string} path - ':memory:' or a file path
|
|
14
|
+
* @returns {import('node:sqlite').DatabaseSync}
|
|
15
|
+
*/
|
|
16
|
+
export function openDatabase(path = ':memory:') {
|
|
17
|
+
return new DatabaseSync(path);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { DatabaseSync };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
export { SqlComponent } from './SqlComponent.js';
|
|
2
|
+
export { SqlEndpoint } from './SqlEndpoint.js';
|
|
3
|
+
export { SqlProducer } from './SqlProducer.js';
|
|
4
|
+
export { SqlConsumer } from './SqlConsumer.js';
|
|
5
|
+
export { ParameterBinder } from './ParameterBinder.js';
|
|
6
|
+
export { openDatabase } from './SqliteClientFactory.js';
|
package/test/sql.test.js
ADDED
|
@@ -0,0 +1,397 @@
|
|
|
1
|
+
import { describe, it } from 'node:test';
|
|
2
|
+
import assert from 'node:assert/strict';
|
|
3
|
+
import { DatabaseSync } from 'node:sqlite';
|
|
4
|
+
import { Exchange, CamelContext } from '@alt-javascript/camel-lite-core';
|
|
5
|
+
import { SqlComponent, ParameterBinder, openDatabase } from '@alt-javascript/camel-lite-component-sql';
|
|
6
|
+
|
|
7
|
+
// ---------------------------------------------------------------------------
|
|
8
|
+
// Helper: open an in-memory SQLite DB with a test table
|
|
9
|
+
// ---------------------------------------------------------------------------
|
|
10
|
+
function makeDb() {
|
|
11
|
+
const db = new DatabaseSync(':memory:');
|
|
12
|
+
db.exec(`
|
|
13
|
+
CREATE TABLE items (
|
|
14
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
15
|
+
name TEXT NOT NULL,
|
|
16
|
+
qty INTEGER NOT NULL DEFAULT 0
|
|
17
|
+
)
|
|
18
|
+
`);
|
|
19
|
+
db.prepare('INSERT INTO items (name, qty) VALUES (?, ?)').run('apple', 10);
|
|
20
|
+
db.prepare('INSERT INTO items (name, qty) VALUES (?, ?)').run('banana', 5);
|
|
21
|
+
db.prepare('INSERT INTO items (name, qty) VALUES (?, ?)').run('cherry', 20);
|
|
22
|
+
return db;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function makeExchange(body) {
|
|
26
|
+
const ex = new Exchange();
|
|
27
|
+
ex.in.body = body;
|
|
28
|
+
return ex;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ---------------------------------------------------------------------------
|
|
32
|
+
// ParameterBinder unit tests
|
|
33
|
+
// ---------------------------------------------------------------------------
|
|
34
|
+
|
|
35
|
+
describe('ParameterBinder', () => {
|
|
36
|
+
it('replaceParams: sqlite dialect replaces :name with ?', () => {
|
|
37
|
+
const { sql, values } = ParameterBinder.replaceParams(
|
|
38
|
+
'SELECT * FROM items WHERE name = :name AND qty > :qty',
|
|
39
|
+
{ name: 'apple', qty: 5 },
|
|
40
|
+
'sqlite'
|
|
41
|
+
);
|
|
42
|
+
assert.equal(sql, 'SELECT * FROM items WHERE name = ? AND qty > ?');
|
|
43
|
+
assert.deepEqual(values, ['apple', 5]);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it('replaceParams: pg dialect replaces :name with $1, $2', () => {
|
|
47
|
+
const { sql, values } = ParameterBinder.replaceParams(
|
|
48
|
+
'SELECT * FROM users WHERE id = :id AND role = :role',
|
|
49
|
+
{ id: 42, role: 'admin' },
|
|
50
|
+
'pg'
|
|
51
|
+
);
|
|
52
|
+
assert.equal(sql, 'SELECT * FROM users WHERE id = $1 AND role = $2');
|
|
53
|
+
assert.deepEqual(values, [42, 'admin']);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
it('replaceParams: mysql2 dialect replaces :name with ?', () => {
|
|
57
|
+
const { sql, values } = ParameterBinder.replaceParams(
|
|
58
|
+
'UPDATE t SET status = :status WHERE id = :id',
|
|
59
|
+
{ status: 'active', id: 7 },
|
|
60
|
+
'mysql2'
|
|
61
|
+
);
|
|
62
|
+
assert.equal(sql, 'UPDATE t SET status = ? WHERE id = ?');
|
|
63
|
+
assert.deepEqual(values, ['active', 7]);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('replaceParams: named dialect passes through unchanged', () => {
|
|
67
|
+
const template = 'SELECT * FROM t WHERE x = :x';
|
|
68
|
+
const params = { x: 99 };
|
|
69
|
+
const { sql, values } = ParameterBinder.replaceParams(template, params, 'named');
|
|
70
|
+
assert.equal(sql, template);
|
|
71
|
+
assert.equal(values, params);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
it('replaceParams: missing param defaults to null', () => {
|
|
75
|
+
const { values } = ParameterBinder.replaceParams(
|
|
76
|
+
'SELECT * FROM t WHERE a = :a AND b = :b',
|
|
77
|
+
{ a: 1 }, // b is missing
|
|
78
|
+
'sqlite'
|
|
79
|
+
);
|
|
80
|
+
assert.deepEqual(values, [1, null]);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('extractParams: returns body if it is a plain object', () => {
|
|
84
|
+
const ex = makeExchange({ id: 5, name: 'pear' });
|
|
85
|
+
assert.deepEqual(ParameterBinder.extractParams(ex), { id: 5, name: 'pear' });
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it('extractParams: falls back to headers if body is not an object', () => {
|
|
89
|
+
const ex = makeExchange('string body');
|
|
90
|
+
ex.in.setHeader('id', 42);
|
|
91
|
+
const params = ParameterBinder.extractParams(ex);
|
|
92
|
+
assert.equal(params.id, 42);
|
|
93
|
+
});
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// ---------------------------------------------------------------------------
|
|
97
|
+
// SqlComponent + SqlProducer — SELECT
|
|
98
|
+
// ---------------------------------------------------------------------------
|
|
99
|
+
|
|
100
|
+
describe('SqlProducer: SELECT', () => {
|
|
101
|
+
it('executes SELECT and sets rows array on exchange.in.body', async () => {
|
|
102
|
+
const db = makeDb();
|
|
103
|
+
const comp = new SqlComponent();
|
|
104
|
+
comp.setDatasource('default', () => db);
|
|
105
|
+
|
|
106
|
+
const ctx = new CamelContext();
|
|
107
|
+
const params = new URLSearchParams('query=SELECT+*+FROM+items&dialect=sqlite');
|
|
108
|
+
const ep = comp.createEndpoint('sql:default?query=SELECT+*+FROM+items&dialect=sqlite', 'default', params, ctx);
|
|
109
|
+
const producer = ep.createProducer();
|
|
110
|
+
|
|
111
|
+
const ex = makeExchange(null);
|
|
112
|
+
await producer.send(ex);
|
|
113
|
+
|
|
114
|
+
assert.ok(Array.isArray(ex.in.body), 'body should be an array');
|
|
115
|
+
assert.equal(ex.in.body.length, 3);
|
|
116
|
+
assert.equal(ex.in.body[0].name, 'apple');
|
|
117
|
+
assert.equal(ex.in.body[1].name, 'banana');
|
|
118
|
+
});
|
|
119
|
+
|
|
120
|
+
it('executes parameterized SELECT with :name placeholders', async () => {
|
|
121
|
+
const db = makeDb();
|
|
122
|
+
const comp = new SqlComponent();
|
|
123
|
+
comp.setDatasource('myds', () => db);
|
|
124
|
+
|
|
125
|
+
const ctx = new CamelContext();
|
|
126
|
+
const q = 'SELECT+*+FROM+items+WHERE+name+=+:name';
|
|
127
|
+
const params = new URLSearchParams(`query=${q}&dialect=sqlite`);
|
|
128
|
+
const ep = comp.createEndpoint(`sql:myds?query=${q}&dialect=sqlite`, 'myds', params, ctx);
|
|
129
|
+
const producer = ep.createProducer();
|
|
130
|
+
|
|
131
|
+
const ex = makeExchange({ name: 'cherry' });
|
|
132
|
+
await producer.send(ex);
|
|
133
|
+
|
|
134
|
+
assert.equal(ex.in.body.length, 1);
|
|
135
|
+
assert.equal(ex.in.body[0].qty, 20);
|
|
136
|
+
});
|
|
137
|
+
});
|
|
138
|
+
|
|
139
|
+
// ---------------------------------------------------------------------------
|
|
140
|
+
// SqlProducer — INSERT / UPDATE / DELETE
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
|
|
143
|
+
describe('SqlProducer: mutations', () => {
|
|
144
|
+
it('INSERT returns { rowCount: 1 }', async () => {
|
|
145
|
+
const db = makeDb();
|
|
146
|
+
const comp = new SqlComponent();
|
|
147
|
+
comp.setDatasource('default', () => db);
|
|
148
|
+
|
|
149
|
+
const ctx = new CamelContext();
|
|
150
|
+
const q = encodeURIComponent('INSERT INTO items (name, qty) VALUES (:name, :qty)');
|
|
151
|
+
const params = new URLSearchParams(`query=${q}&dialect=sqlite&outputType=rowCount`);
|
|
152
|
+
const ep = comp.createEndpoint(`sql:default?query=${q}&dialect=sqlite&outputType=rowCount`, 'default', params, ctx);
|
|
153
|
+
const producer = ep.createProducer();
|
|
154
|
+
|
|
155
|
+
const ex = makeExchange({ name: 'grape', qty: 7 });
|
|
156
|
+
await producer.send(ex);
|
|
157
|
+
|
|
158
|
+
assert.deepEqual(ex.in.body, { rowCount: 1 });
|
|
159
|
+
|
|
160
|
+
// Confirm row was inserted
|
|
161
|
+
const rows = db.prepare('SELECT * FROM items WHERE name = ?').all('grape');
|
|
162
|
+
assert.equal(rows.length, 1);
|
|
163
|
+
assert.equal(rows[0].qty, 7);
|
|
164
|
+
});
|
|
165
|
+
|
|
166
|
+
it('UPDATE returns { rowCount: N }', async () => {
|
|
167
|
+
const db = makeDb();
|
|
168
|
+
const comp = new SqlComponent();
|
|
169
|
+
comp.setDatasource('default', () => db);
|
|
170
|
+
|
|
171
|
+
const ctx = new CamelContext();
|
|
172
|
+
const q = encodeURIComponent('UPDATE items SET qty = :qty WHERE name = :name');
|
|
173
|
+
const params = new URLSearchParams(`query=${q}&dialect=sqlite&outputType=rowCount`);
|
|
174
|
+
const ep = comp.createEndpoint(`sql:default?query=${q}&dialect=sqlite&outputType=rowCount`, 'default', params, ctx);
|
|
175
|
+
const producer = ep.createProducer();
|
|
176
|
+
|
|
177
|
+
const ex = makeExchange({ name: 'apple', qty: 99 });
|
|
178
|
+
await producer.send(ex);
|
|
179
|
+
|
|
180
|
+
assert.deepEqual(ex.in.body, { rowCount: 1 });
|
|
181
|
+
const rows = db.prepare('SELECT qty FROM items WHERE name = ?').all('apple');
|
|
182
|
+
assert.equal(rows[0].qty, 99);
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
it('DELETE returns { rowCount: N }', async () => {
|
|
186
|
+
const db = makeDb();
|
|
187
|
+
const comp = new SqlComponent();
|
|
188
|
+
comp.setDatasource('default', () => db);
|
|
189
|
+
|
|
190
|
+
const ctx = new CamelContext();
|
|
191
|
+
const q = encodeURIComponent('DELETE FROM items WHERE name = :name');
|
|
192
|
+
const params = new URLSearchParams(`query=${q}&dialect=sqlite&outputType=rowCount`);
|
|
193
|
+
const ep = comp.createEndpoint(`sql:default?query=${q}&dialect=sqlite&outputType=rowCount`, 'default', params, ctx);
|
|
194
|
+
const producer = ep.createProducer();
|
|
195
|
+
|
|
196
|
+
const ex = makeExchange({ name: 'banana' });
|
|
197
|
+
await producer.send(ex);
|
|
198
|
+
|
|
199
|
+
assert.deepEqual(ex.in.body, { rowCount: 1 });
|
|
200
|
+
const remaining = db.prepare('SELECT COUNT(*) as cnt FROM items').all();
|
|
201
|
+
assert.equal(remaining[0].cnt, 2);
|
|
202
|
+
});
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
// ---------------------------------------------------------------------------
|
|
206
|
+
// Multiple named datasources
|
|
207
|
+
// ---------------------------------------------------------------------------
|
|
208
|
+
|
|
209
|
+
describe('SqlComponent: multiple named datasources', () => {
|
|
210
|
+
it('routes queries to the correct named datasource', async () => {
|
|
211
|
+
const db1 = new DatabaseSync(':memory:');
|
|
212
|
+
db1.exec('CREATE TABLE t (val TEXT)');
|
|
213
|
+
db1.prepare('INSERT INTO t (val) VALUES (?)').run('from-db1');
|
|
214
|
+
|
|
215
|
+
const db2 = new DatabaseSync(':memory:');
|
|
216
|
+
db2.exec('CREATE TABLE t (val TEXT)');
|
|
217
|
+
db2.prepare('INSERT INTO t (val) VALUES (?)').run('from-db2');
|
|
218
|
+
|
|
219
|
+
const comp = new SqlComponent();
|
|
220
|
+
comp.setDatasource('ds1', () => db1);
|
|
221
|
+
comp.setDatasource('ds2', () => db2);
|
|
222
|
+
|
|
223
|
+
const ctx = new CamelContext();
|
|
224
|
+
|
|
225
|
+
const params1 = new URLSearchParams('query=SELECT+val+FROM+t&dialect=sqlite');
|
|
226
|
+
const ep1 = comp.createEndpoint('sql:ds1?query=SELECT+val+FROM+t&dialect=sqlite', 'ds1', params1, ctx);
|
|
227
|
+
|
|
228
|
+
const params2 = new URLSearchParams('query=SELECT+val+FROM+t&dialect=sqlite');
|
|
229
|
+
const ep2 = comp.createEndpoint('sql:ds2?query=SELECT+val+FROM+t&dialect=sqlite', 'ds2', params2, ctx);
|
|
230
|
+
|
|
231
|
+
const ex1 = makeExchange(null);
|
|
232
|
+
await ep1.createProducer().send(ex1);
|
|
233
|
+
assert.equal(ex1.in.body[0].val, 'from-db1');
|
|
234
|
+
|
|
235
|
+
const ex2 = makeExchange(null);
|
|
236
|
+
await ep2.createProducer().send(ex2);
|
|
237
|
+
assert.equal(ex2.in.body[0].val, 'from-db2');
|
|
238
|
+
});
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
// ---------------------------------------------------------------------------
|
|
242
|
+
// SqlComponent: missing datasource throws
|
|
243
|
+
// ---------------------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
describe('SqlComponent: error cases', () => {
|
|
246
|
+
it('throws when query param is missing', () => {
|
|
247
|
+
const comp = new SqlComponent();
|
|
248
|
+
comp.setDatasource('default', () => null);
|
|
249
|
+
const ctx = new CamelContext();
|
|
250
|
+
const params = new URLSearchParams('dialect=sqlite'); // no query
|
|
251
|
+
assert.throws(
|
|
252
|
+
() => comp.createEndpoint('sql:default?dialect=sqlite', 'default', params, ctx),
|
|
253
|
+
/query.*required/i
|
|
254
|
+
);
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('throws when datasource is not registered', async () => {
|
|
258
|
+
const comp = new SqlComponent();
|
|
259
|
+
const ctx = new CamelContext();
|
|
260
|
+
const params = new URLSearchParams('query=SELECT+1&dialect=sqlite');
|
|
261
|
+
const ep = comp.createEndpoint('sql:unknown?query=SELECT+1&dialect=sqlite', 'unknown', params, ctx);
|
|
262
|
+
const producer = ep.createProducer();
|
|
263
|
+
await assert.rejects(
|
|
264
|
+
() => producer.send(makeExchange(null)),
|
|
265
|
+
/cannot resolve datasource 'unknown'/i
|
|
266
|
+
);
|
|
267
|
+
});
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
// ---------------------------------------------------------------------------
|
|
271
|
+
// openDatabase helper
|
|
272
|
+
// ---------------------------------------------------------------------------
|
|
273
|
+
|
|
274
|
+
describe('openDatabase helper', () => {
|
|
275
|
+
it('opens an in-memory SQLite database', () => {
|
|
276
|
+
const db = openDatabase(':memory:');
|
|
277
|
+
assert.ok(db, 'should return a DatabaseSync instance');
|
|
278
|
+
db.exec('CREATE TABLE t (x INTEGER)');
|
|
279
|
+
db.prepare('INSERT INTO t (x) VALUES (?)').run(42);
|
|
280
|
+
const rows = db.prepare('SELECT x FROM t').all();
|
|
281
|
+
assert.equal(rows[0].x, 42);
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Context-aware datasource resolution (three-step chain)
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
describe('SqlComponent: context bean resolution', () => {
|
|
290
|
+
function makeDb() {
|
|
291
|
+
const db = new DatabaseSync(':memory:');
|
|
292
|
+
db.exec('CREATE TABLE t (id INTEGER PRIMARY KEY AUTOINCREMENT, val TEXT)');
|
|
293
|
+
db.prepare('INSERT INTO t (val) VALUES (?)').run('hello');
|
|
294
|
+
return db;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
it('step 2: resolves datasource from context.getBean(name) when not in component map', async () => {
|
|
298
|
+
const db = makeDb();
|
|
299
|
+
const comp = new SqlComponent(); // no setDatasource()
|
|
300
|
+
const ctx = new CamelContext();
|
|
301
|
+
ctx.registerBean('myDb', db);
|
|
302
|
+
|
|
303
|
+
const params = new URLSearchParams('datasource=myDb&query=SELECT+val+FROM+t&dialect=sqlite');
|
|
304
|
+
const ep = comp.createEndpoint('sql:?datasource=myDb&query=SELECT+val+FROM+t&dialect=sqlite', '', params, ctx);
|
|
305
|
+
|
|
306
|
+
const ex = new Exchange();
|
|
307
|
+
ex.in.body = null;
|
|
308
|
+
await ep.createProducer().send(ex);
|
|
309
|
+
|
|
310
|
+
assert.ok(Array.isArray(ex.in.body));
|
|
311
|
+
assert.equal(ex.in.body[0].val, 'hello');
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it('step 3: auto-selects single context bean when no name given', async () => {
|
|
315
|
+
const db = makeDb();
|
|
316
|
+
const comp = new SqlComponent();
|
|
317
|
+
const ctx = new CamelContext();
|
|
318
|
+
ctx.registerBean('theOnlyDb', db); // only one bean
|
|
319
|
+
|
|
320
|
+
const params = new URLSearchParams('query=SELECT+val+FROM+t&dialect=sqlite');
|
|
321
|
+
const ep = comp.createEndpoint('sql:?query=SELECT+val+FROM+t&dialect=sqlite', '', params, ctx);
|
|
322
|
+
|
|
323
|
+
const ex = new Exchange();
|
|
324
|
+
ex.in.body = null;
|
|
325
|
+
await ep.createProducer().send(ex);
|
|
326
|
+
|
|
327
|
+
assert.ok(Array.isArray(ex.in.body));
|
|
328
|
+
assert.equal(ex.in.body[0].val, 'hello');
|
|
329
|
+
});
|
|
330
|
+
|
|
331
|
+
it('step 1 takes priority over context bean when component map has same name', async () => {
|
|
332
|
+
const dbInMap = makeDb();
|
|
333
|
+
dbInMap.exec('INSERT INTO t (val) VALUES (?)', 'from-map');
|
|
334
|
+
|
|
335
|
+
const dbInContext = makeDb();
|
|
336
|
+
|
|
337
|
+
const comp = new SqlComponent();
|
|
338
|
+
comp.setDatasource('myDb', () => dbInMap); // component map — takes priority
|
|
339
|
+
|
|
340
|
+
const ctx = new CamelContext();
|
|
341
|
+
ctx.registerBean('myDb', dbInContext); // context — should be ignored
|
|
342
|
+
|
|
343
|
+
const params = new URLSearchParams('datasource=myDb&query=SELECT+COUNT(*)+AS+cnt+FROM+t&dialect=sqlite');
|
|
344
|
+
const ep = comp.createEndpoint('sql:?datasource=myDb&query=SELECT+COUNT(*)+AS+cnt+FROM+t&dialect=sqlite', '', params, ctx);
|
|
345
|
+
|
|
346
|
+
const ex = new Exchange();
|
|
347
|
+
ex.in.body = null;
|
|
348
|
+
await ep.createProducer().send(ex);
|
|
349
|
+
|
|
350
|
+
// dbInMap has 2 rows (original + 'from-map'), dbInContext has 1
|
|
351
|
+
assert.equal(ex.in.body[0].cnt, 2);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it('?datasource= param overrides the sql: path segment as the name', async () => {
|
|
355
|
+
const db = makeDb();
|
|
356
|
+
const comp = new SqlComponent();
|
|
357
|
+
const ctx = new CamelContext();
|
|
358
|
+
ctx.registerBean('paramBean', db);
|
|
359
|
+
|
|
360
|
+
// Path says 'pathBean' but ?datasource= says 'paramBean'
|
|
361
|
+
const params = new URLSearchParams('datasource=paramBean&query=SELECT+val+FROM+t&dialect=sqlite');
|
|
362
|
+
const ep = comp.createEndpoint('sql:pathBean?datasource=paramBean&query=SELECT+val+FROM+t&dialect=sqlite', 'pathBean', params, ctx);
|
|
363
|
+
|
|
364
|
+
const ex = new Exchange();
|
|
365
|
+
ex.in.body = null;
|
|
366
|
+
await ep.createProducer().send(ex);
|
|
367
|
+
|
|
368
|
+
assert.equal(ex.in.body[0].val, 'hello');
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('throws a descriptive error when no resolution path succeeds', async () => {
|
|
372
|
+
const comp = new SqlComponent();
|
|
373
|
+
const ctx = new CamelContext(); // no beans
|
|
374
|
+
const params = new URLSearchParams('datasource=ghost&query=SELECT+1&dialect=sqlite');
|
|
375
|
+
const ep = comp.createEndpoint('sql:?datasource=ghost&query=SELECT+1&dialect=sqlite', '', params, ctx);
|
|
376
|
+
|
|
377
|
+
await assert.rejects(
|
|
378
|
+
() => ep.createProducer().send(new Exchange()),
|
|
379
|
+
/cannot resolve datasource 'ghost'/i
|
|
380
|
+
);
|
|
381
|
+
});
|
|
382
|
+
|
|
383
|
+
it('throws when no name given and context has zero or multiple beans', async () => {
|
|
384
|
+
const comp = new SqlComponent();
|
|
385
|
+
const ctx = new CamelContext();
|
|
386
|
+
ctx.registerBean('db1', makeDb());
|
|
387
|
+
ctx.registerBean('db2', makeDb()); // two beans — no auto-select
|
|
388
|
+
|
|
389
|
+
const params = new URLSearchParams('query=SELECT+1&dialect=sqlite');
|
|
390
|
+
const ep = comp.createEndpoint('sql:?query=SELECT+1&dialect=sqlite', '', params, ctx);
|
|
391
|
+
|
|
392
|
+
await assert.rejects(
|
|
393
|
+
() => ep.createProducer().send(new Exchange()),
|
|
394
|
+
/cannot resolve datasource/i
|
|
395
|
+
);
|
|
396
|
+
});
|
|
397
|
+
});
|