@alt-javascript/camel-lite-component-nosql 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 ADDED
@@ -0,0 +1,63 @@
1
+ [![License: MIT](https://img.shields.io/badge/License-MIT-yellow.svg)](https://opensource.org/licenses/MIT)
2
+
3
+ ## What
4
+
5
+ NoSQL collection operations via [`@alt-javascript/jsnosqlc`](https://www.npmjs.com/package/@alt-javascript/jsnosqlc). Supports insert, find, update, delete, and count against named collections.
6
+
7
+ ## Install
8
+
9
+ ```sh
10
+ npm install camel-lite-component-nosql @alt-javascript/jsnosqlc
11
+ ```
12
+
13
+ ## URI Syntax
14
+
15
+ ```
16
+ nosql:collectionName[?url=jsnosqlc:memory:&operation=insert]
17
+ ```
18
+
19
+ | Parameter | Default | Description |
20
+ |--------------|------------------|-------------|
21
+ | `url` | *(required)* | jsnosqlc connection URL (e.g. `jsnosqlc:memory:` or a file path). |
22
+ | `operation` | `insert` | Collection operation: `insert`, `find`, `update`, `delete`, or `count`. |
23
+
24
+ ## Usage
25
+
26
+ ```js
27
+ import { CamelContext } from 'camel-lite-core';
28
+ import { NosqlComponent } from 'camel-lite-component-nosql';
29
+
30
+ const context = new CamelContext();
31
+ context.addComponent('nosql', new NosqlComponent());
32
+
33
+ context.addRoutes({
34
+ configure(ctx) {
35
+ // Insert exchange body as a document
36
+ ctx.from('direct:storeEvent')
37
+ .to('nosql:events?url=jsnosqlc:memory:&operation=insert');
38
+
39
+ // Find documents — exchange body used as the query filter
40
+ ctx.from('direct:queryEvents')
41
+ .to('nosql:events?url=jsnosqlc:memory:&operation=find');
42
+ }
43
+ });
44
+
45
+ await context.start();
46
+
47
+ const template = context.createProducerTemplate();
48
+
49
+ // Insert
50
+ await template.sendBody('direct:storeEvent', { type: 'login', userId: 7 });
51
+
52
+ // Find — pass a filter object as body
53
+ const exchange = await template.send('direct:queryEvents', ex => {
54
+ ex.in.body = { type: 'login' };
55
+ });
56
+ console.log('Found:', exchange.in.body);
57
+
58
+ await context.stop();
59
+ ```
60
+
61
+ ## See Also
62
+
63
+ [camel-lite — root README](../../README.md)
package/package.json ADDED
@@ -0,0 +1,43 @@
1
+ {
2
+ "name": "@alt-javascript/camel-lite-component-nosql",
3
+ "version": "1.0.2",
4
+ "type": "module",
5
+ "exports": {
6
+ ".": "./src/index.js"
7
+ },
8
+ "dependencies": {
9
+ "@alt-javascript/jsnosqlc-core": "^1.1.1",
10
+ "@alt-javascript/jsnosqlc-memory": "^1.1.1",
11
+ "@alt-javascript/logger": "^3.0.7",
12
+ "@alt-javascript/config": "^3.0.7",
13
+ "@alt-javascript/common": "^3.0.7",
14
+ "@alt-javascript/camel-lite-core": "1.0.2"
15
+ },
16
+ "scripts": {
17
+ "test": "node --test"
18
+ },
19
+ "repository": {
20
+ "type": "git",
21
+ "url": "https://github.com/alt-javascript/camel-lite"
22
+ },
23
+ "author": "Craig Parravicini",
24
+ "contributors": [
25
+ "Claude (Anthropic)",
26
+ "Apache Camel — design inspiration and pattern source"
27
+ ],
28
+ "keywords": [
29
+ "alt-javascript",
30
+ "camel",
31
+ "camel-lite",
32
+ "eai",
33
+ "eip",
34
+ "integration",
35
+ "nosql",
36
+ "mongodb",
37
+ "component"
38
+ ],
39
+ "publishConfig": {
40
+ "registry": "https://registry.npmjs.org/",
41
+ "access": "public"
42
+ }
43
+ }
@@ -0,0 +1,142 @@
1
+ import { Component } from '@alt-javascript/camel-lite-core';
2
+ import { LoggerFactory } from '@alt-javascript/logger';
3
+ import { NosqlEndpoint } from './NosqlEndpoint.js';
4
+
5
+ const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/NosqlComponent');
6
+
7
+ /**
8
+ * NosqlComponent — executes NoSQL operations as a pipeline step via
9
+ * the @alt-javascript/jsnosqlc abstraction layer.
10
+ *
11
+ * Datasource resolution (three-step chain, evaluated at send() time):
12
+ *
13
+ * 1. Component-internal map — setDatasource(name, factory) takes priority.
14
+ *
15
+ * 2. Context bean by name — context.getBean(name) is checked when the
16
+ * component map has no match. The bean may be a ClientDataSource
17
+ * (has .getClient()) or a jsnosqlc Client directly.
18
+ *
19
+ * 3. Auto-select — when no explicit name is given (or named lookup
20
+ * found nothing) and context.getBeans() has exactly one entry, that bean
21
+ * is used automatically.
22
+ *
23
+ * URI formats:
24
+ * nosql:collection?datasource=myDsBean&operation=insert
25
+ * nosql:collection?operation=insert ← auto-select when 1 bean in ctx
26
+ *
27
+ * Operations (jsnosqlc Collection API):
28
+ * get — exchange.in.body = key (string) → exchange.in.body = doc | null
29
+ * store — exchange.in.body = { key, doc } → exchange.in.body = undefined
30
+ * delete — exchange.in.body = key (string) → exchange.in.body = undefined
31
+ * insert — exchange.in.body = doc (object) → exchange.in.body = assigned key
32
+ * update — exchange.in.body = { key, patch } → exchange.in.body = undefined
33
+ * find — exchange.in.body = Filter (built AST) → exchange.in.body = doc array
34
+ */
35
+ class NosqlComponent extends Component {
36
+ #datasources = new Map(); // name → factory function
37
+ #clients = new Map(); // name → cached Client (from component-map factories)
38
+ #endpoints = new Map();
39
+
40
+ /**
41
+ * Register a named datasource factory on the component.
42
+ * Factory is () => ClientDataSource | Client (sync or async result).
43
+ * @param {string} name
44
+ * @param {function} factory
45
+ */
46
+ setDatasource(name, factory) {
47
+ this.#datasources.set(name, factory);
48
+ log.info(`NosqlComponent: datasource '${name}' registered on component`);
49
+ return this;
50
+ }
51
+
52
+ /**
53
+ * Resolve and return a jsnosqlc Client using the three-step chain.
54
+ * Clients from the component-internal map are cached.
55
+ * @param {string|null} name
56
+ * @param {CamelContext|null} context
57
+ * @returns {Promise<import('@alt-javascript/jsnosqlc-core').Client>}
58
+ */
59
+ async getClient(name, context = null) {
60
+ // Step 1: component-internal map
61
+ if (name && this.#datasources.has(name)) {
62
+ if (this.#clients.has(name)) {
63
+ return this.#clients.get(name);
64
+ }
65
+ log.debug(`NosqlComponent: datasource '${name}' resolved from component map`);
66
+ const factoryResult = this.#datasources.get(name)();
67
+ const client = typeof factoryResult.getClient === 'function'
68
+ ? await factoryResult.getClient()
69
+ : await factoryResult;
70
+ this.#clients.set(name, client);
71
+ log.info(`NosqlComponent: client acquired for datasource '${name}'`);
72
+ return client;
73
+ }
74
+
75
+ if (context) {
76
+ // Step 2: context bean by name
77
+ if (name) {
78
+ const bean = context.getBean(name);
79
+ if (bean != null) {
80
+ const cacheKey = `__ctx__:${name}`;
81
+ if (this.#clients.has(cacheKey)) {
82
+ return this.#clients.get(cacheKey);
83
+ }
84
+ log.debug(`NosqlComponent: datasource '${name}' resolved from context bean`);
85
+ const client = typeof bean.getClient === 'function' ? await bean.getClient() : bean;
86
+ this.#clients.set(cacheKey, client);
87
+ return client;
88
+ }
89
+ }
90
+
91
+ // Step 3: single-bean auto-select
92
+ const allBeans = context.getBeans();
93
+ if (allBeans.length === 1) {
94
+ const [beanName, bean] = allBeans[0];
95
+ const cacheKey = `__ctx__:${beanName}`;
96
+ if (this.#clients.has(cacheKey)) {
97
+ return this.#clients.get(cacheKey);
98
+ }
99
+ log.debug(`NosqlComponent: datasource auto-selected (single bean in context: '${beanName}')`);
100
+ const client = typeof bean.getClient === 'function' ? await bean.getClient() : bean;
101
+ this.#clients.set(cacheKey, client);
102
+ return client;
103
+ }
104
+ }
105
+
106
+ throw new Error(
107
+ `NosqlComponent: cannot resolve datasource '${name ?? '(none)'}'. ` +
108
+ `Register via component.setDatasource(), context.registerBean(), ` +
109
+ `or ensure exactly one bean is registered in context.`
110
+ );
111
+ }
112
+
113
+ /**
114
+ * Close all cached clients from the component-internal map.
115
+ */
116
+ async close() {
117
+ for (const [name, client] of this.#clients) {
118
+ await client.close().catch(() => {});
119
+ log.info(`NosqlComponent: client closed for datasource '${name}'`);
120
+ }
121
+ this.#clients.clear();
122
+ }
123
+
124
+ createEndpoint(uri, remaining, parameters, context) {
125
+ if (this.#endpoints.has(uri)) {
126
+ return this.#endpoints.get(uri);
127
+ }
128
+
129
+ const collection = remaining.replace(/^\/+/, '') || 'default';
130
+ const datasource = parameters.get('datasource') ?? null; // null = auto-select
131
+ const operation = parameters.get('operation') ?? 'get';
132
+
133
+ log.info(`NosqlComponent creating endpoint: collection=${collection}, datasource=${datasource ?? '(auto)'}, op=${operation}`);
134
+
135
+ const endpoint = new NosqlEndpoint(uri, collection, datasource, operation, context, this);
136
+ this.#endpoints.set(uri, endpoint);
137
+ return endpoint;
138
+ }
139
+ }
140
+
141
+ export { NosqlComponent };
142
+ export default NosqlComponent;
@@ -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/NosqlConsumer');
5
+
6
+ /**
7
+ * NosqlConsumer — stub implementation.
8
+ * nosql: is primarily producer-oriented.
9
+ * Change-stream / poll consumer is deferred to a future milestone.
10
+ */
11
+ class NosqlConsumer 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(`NosqlConsumer started: ${uri}`);
25
+ }
26
+
27
+ async stop() {
28
+ const { uri, context } = this.#endpoint;
29
+ context.registerConsumer(uri, null);
30
+ log.info(`NosqlConsumer stopped: ${uri}`);
31
+ }
32
+
33
+ async process(exchange) {
34
+ return this.#pipeline.run(exchange);
35
+ }
36
+ }
37
+
38
+ export { NosqlConsumer };
39
+ export default NosqlConsumer;
@@ -0,0 +1,43 @@
1
+ import { Endpoint } from '@alt-javascript/camel-lite-core';
2
+ import { NosqlProducer } from './NosqlProducer.js';
3
+ import { NosqlConsumer } from './NosqlConsumer.js';
4
+
5
+ /**
6
+ * NosqlEndpoint holds the parsed URI state for a nosql: endpoint.
7
+ */
8
+ class NosqlEndpoint extends Endpoint {
9
+ #uri;
10
+ #collection;
11
+ #datasource;
12
+ #operation;
13
+ #context;
14
+ #component;
15
+
16
+ constructor(uri, collection, datasource, operation, context, component) {
17
+ super();
18
+ this.#uri = uri;
19
+ this.#collection = collection;
20
+ this.#datasource = datasource;
21
+ this.#operation = operation;
22
+ this.#context = context;
23
+ this.#component = component;
24
+ }
25
+
26
+ get uri() { return this.#uri; }
27
+ get collection() { return this.#collection; }
28
+ get datasource() { return this.#datasource; }
29
+ get operation() { return this.#operation; }
30
+ get context() { return this.#context; }
31
+ get component() { return this.#component; }
32
+
33
+ createProducer() {
34
+ return new NosqlProducer(this);
35
+ }
36
+
37
+ createConsumer(pipeline) {
38
+ return new NosqlConsumer(this, pipeline);
39
+ }
40
+ }
41
+
42
+ export { NosqlEndpoint };
43
+ export default NosqlEndpoint;
@@ -0,0 +1,88 @@
1
+ import { Producer } from '@alt-javascript/camel-lite-core';
2
+ import { LoggerFactory } from '@alt-javascript/logger';
3
+
4
+ const log = LoggerFactory.getLogger('@alt-javascript/camel-lite/NosqlProducer');
5
+
6
+ /**
7
+ * NosqlProducer — executes a jsnosqlc Collection operation as a pipeline step.
8
+ *
9
+ * Operation dispatch (endpoint.operation):
10
+ *
11
+ * get — exchange.in.body = key (string)
12
+ * → exchange.in.body = document | null
13
+ *
14
+ * store — exchange.in.body = { key: string, doc: object }
15
+ * → exchange.in.body = undefined (fire-and-forget upsert)
16
+ *
17
+ * delete — exchange.in.body = key (string)
18
+ * → exchange.in.body = undefined
19
+ *
20
+ * insert — exchange.in.body = document (object)
21
+ * → exchange.in.body = assigned key (string)
22
+ *
23
+ * update — exchange.in.body = { key: string, patch: object }
24
+ * → exchange.in.body = undefined (patch merge)
25
+ *
26
+ * find — exchange.in.body = Filter (built AST from Filter.where()...build())
27
+ * → exchange.in.body = document[] (from cursor.getDocuments())
28
+ */
29
+ class NosqlProducer extends Producer {
30
+ #endpoint;
31
+
32
+ constructor(endpoint) {
33
+ super();
34
+ this.#endpoint = endpoint;
35
+ }
36
+
37
+ async send(exchange) {
38
+ const { collection: collectionName, datasource, operation, component, context } = this.#endpoint;
39
+
40
+ log.debug(`NosqlProducer: ${operation} on ${datasource ?? '(auto)'}/${collectionName}`);
41
+
42
+ const client = await component.getClient(datasource, context);
43
+ const col = client.getCollection(collectionName);
44
+ const body = exchange.in.body;
45
+
46
+ switch (operation) {
47
+ case 'get': {
48
+ exchange.in.body = await col.get(body);
49
+ break;
50
+ }
51
+ case 'store': {
52
+ const { key, doc } = body ?? {};
53
+ await col.store(key, doc);
54
+ exchange.in.body = undefined;
55
+ break;
56
+ }
57
+ case 'delete': {
58
+ await col.delete(body);
59
+ exchange.in.body = undefined;
60
+ break;
61
+ }
62
+ case 'insert': {
63
+ exchange.in.body = await col.insert(body);
64
+ break;
65
+ }
66
+ case 'update': {
67
+ const { key, patch } = body ?? {};
68
+ await col.update(key, patch);
69
+ exchange.in.body = undefined;
70
+ break;
71
+ }
72
+ case 'find': {
73
+ // body should be a built Filter AST from Filter.where()...build()
74
+ // null/undefined body means find all — pass null to driver
75
+ const cursor = await col.find(body ?? null);
76
+ exchange.in.body = cursor.getDocuments();
77
+ break;
78
+ }
79
+ default:
80
+ throw new Error(`NosqlProducer: unknown operation '${operation}'`);
81
+ }
82
+
83
+ log.debug(`NosqlProducer: ${operation} complete on ${datasource}/${collectionName}`);
84
+ }
85
+ }
86
+
87
+ export { NosqlProducer };
88
+ export default NosqlProducer;
package/src/index.js ADDED
@@ -0,0 +1,8 @@
1
+ export { NosqlComponent } from './NosqlComponent.js';
2
+ export { NosqlEndpoint } from './NosqlEndpoint.js';
3
+ export { NosqlProducer } from './NosqlProducer.js';
4
+ export { NosqlConsumer } from './NosqlConsumer.js';
5
+
6
+ // Re-export Filter and ClientDataSource from jsnosqlc-core for caller convenience.
7
+ // Callers need Filter to build expressions for find operations.
8
+ export { Filter, ClientDataSource } from '@alt-javascript/jsnosqlc-core';
@@ -0,0 +1,446 @@
1
+ import { describe, it, before } from 'node:test';
2
+ import assert from 'node:assert/strict';
3
+ import { Exchange, CamelContext } from '@alt-javascript/camel-lite-core';
4
+ import { ClientDataSource } from '@alt-javascript/jsnosqlc-core';
5
+ // Self-registers the in-memory driver with DriverManager on import
6
+ import '@alt-javascript/jsnosqlc-memory';
7
+ import { NosqlComponent, Filter } from '@alt-javascript/camel-lite-component-nosql';
8
+
9
+ // ---------------------------------------------------------------------------
10
+ // Helpers
11
+ // ---------------------------------------------------------------------------
12
+
13
+ function makeExchange(body) {
14
+ const ex = new Exchange();
15
+ ex.in.body = body;
16
+ return ex;
17
+ }
18
+
19
+ /** Build a NosqlComponent backed by a fresh in-memory store. */
20
+ function makeComponent(dsName = 'store') {
21
+ const comp = new NosqlComponent();
22
+ comp.setDatasource(dsName, () => new ClientDataSource({ url: 'jsnosqlc:memory:' }));
23
+ return comp;
24
+ }
25
+
26
+ function makeEndpoint(comp, ctx, collection, operation, dsName = 'store') {
27
+ const paramStr = `datasource=${dsName}&operation=${operation}`;
28
+ const uri = `nosql:${collection}?${paramStr}`;
29
+ const params = new URLSearchParams(paramStr);
30
+ return comp.createEndpoint(uri, collection, params, ctx);
31
+ }
32
+
33
+ // ---------------------------------------------------------------------------
34
+ // URI parsing
35
+ // ---------------------------------------------------------------------------
36
+
37
+ describe('NosqlComponent: URI parsing', () => {
38
+ it('parses collection, datasource, and operation from URI', () => {
39
+ const comp = makeComponent();
40
+ const ctx = new CamelContext();
41
+ const ep = makeEndpoint(comp, ctx, 'users', 'insert');
42
+ assert.equal(ep.collection, 'users');
43
+ assert.equal(ep.datasource, 'store');
44
+ assert.equal(ep.operation, 'insert');
45
+ });
46
+
47
+ it('defaults operation to get when not specified', () => {
48
+ const comp = makeComponent();
49
+ const ctx = new CamelContext();
50
+ const uri = 'nosql:items?datasource=store';
51
+ const ep = comp.createEndpoint(uri, 'items', new URLSearchParams('datasource=store'), ctx);
52
+ assert.equal(ep.operation, 'get');
53
+ });
54
+
55
+ it('returns cached endpoint on duplicate URI', () => {
56
+ const comp = makeComponent();
57
+ const ctx = new CamelContext();
58
+ const ep1 = makeEndpoint(comp, ctx, 'col', 'insert');
59
+ const ep2 = makeEndpoint(comp, ctx, 'col', 'insert');
60
+ assert.equal(ep1, ep2);
61
+ });
62
+ });
63
+
64
+ // ---------------------------------------------------------------------------
65
+ // insert → get round-trip
66
+ // ---------------------------------------------------------------------------
67
+
68
+ describe('NosqlProducer: insert and get', () => {
69
+ it('insert returns assigned key; get retrieves the document', async () => {
70
+ const comp = makeComponent();
71
+ const ctx = new CamelContext();
72
+
73
+ // insert
74
+ const insertEp = makeEndpoint(comp, ctx, 'products', 'insert');
75
+ const insertEx = makeExchange({ name: 'Widget', price: 9.99 });
76
+ await insertEp.createProducer().send(insertEx);
77
+ const key = insertEx.in.body;
78
+ assert.equal(typeof key, 'string', 'insert should return a string key');
79
+
80
+ // get
81
+ const getEp = makeEndpoint(comp, ctx, 'products', 'get');
82
+ const getEx = makeExchange(key);
83
+ await getEp.createProducer().send(getEx);
84
+ assert.equal(getEx.in.body.name, 'Widget');
85
+ assert.equal(getEx.in.body.price, 9.99);
86
+ });
87
+
88
+ it('get returns null for a missing key', async () => {
89
+ const comp = makeComponent();
90
+ const ctx = new CamelContext();
91
+ const ep = makeEndpoint(comp, ctx, 'empty', 'get');
92
+ const ex = makeExchange('nonexistent-key');
93
+ await ep.createProducer().send(ex);
94
+ assert.equal(ex.in.body, null);
95
+ });
96
+ });
97
+
98
+ // ---------------------------------------------------------------------------
99
+ // store and get
100
+ // ---------------------------------------------------------------------------
101
+
102
+ describe('NosqlProducer: store and get', () => {
103
+ it('store upserts under caller-supplied key; get retrieves it', async () => {
104
+ const comp = makeComponent();
105
+ const ctx = new CamelContext();
106
+
107
+ const storeEp = makeEndpoint(comp, ctx, 'sessions', 'store');
108
+ const storeEx = makeExchange({ key: 'sess-abc', doc: { userId: 42, role: 'admin' } });
109
+ await storeEp.createProducer().send(storeEx);
110
+ assert.equal(storeEx.in.body, undefined);
111
+
112
+ const getEp = makeEndpoint(comp, ctx, 'sessions', 'get');
113
+ const getEx = makeExchange('sess-abc');
114
+ await getEp.createProducer().send(getEx);
115
+ assert.equal(getEx.in.body.userId, 42);
116
+ assert.equal(getEx.in.body.role, 'admin');
117
+ });
118
+ });
119
+
120
+ // ---------------------------------------------------------------------------
121
+ // delete
122
+ // ---------------------------------------------------------------------------
123
+
124
+ describe('NosqlProducer: delete', () => {
125
+ it('delete removes document; subsequent get returns null', async () => {
126
+ const comp = makeComponent();
127
+ const ctx = new CamelContext();
128
+
129
+ // store first
130
+ await makeEndpoint(comp, ctx, 'cache', 'store').createProducer()
131
+ .send(makeExchange({ key: 'temp', doc: { x: 1 } }));
132
+
133
+ // delete
134
+ const delEp = makeEndpoint(comp, ctx, 'cache', 'delete');
135
+ const delEx = makeExchange('temp');
136
+ await delEp.createProducer().send(delEx);
137
+ assert.equal(delEx.in.body, undefined);
138
+
139
+ // get → null
140
+ const getEx = makeExchange('temp');
141
+ await makeEndpoint(comp, ctx, 'cache', 'get').createProducer().send(getEx);
142
+ assert.equal(getEx.in.body, null);
143
+ });
144
+ });
145
+
146
+ // ---------------------------------------------------------------------------
147
+ // update
148
+ // ---------------------------------------------------------------------------
149
+
150
+ describe('NosqlProducer: update', () => {
151
+ it('update patches document fields; others preserved', async () => {
152
+ const comp = makeComponent();
153
+ const ctx = new CamelContext();
154
+
155
+ // insert a document
156
+ const insertEx = makeExchange({ name: 'Alice', age: 30, role: 'user' });
157
+ await makeEndpoint(comp, ctx, 'users', 'insert').createProducer().send(insertEx);
158
+ const key = insertEx.in.body;
159
+
160
+ // update — patch age and role, name should be preserved
161
+ const updateEx = makeExchange({ key, patch: { age: 31, role: 'admin' } });
162
+ await makeEndpoint(comp, ctx, 'users', 'update').createProducer().send(updateEx);
163
+ assert.equal(updateEx.in.body, undefined);
164
+
165
+ // verify patch applied and name preserved
166
+ const getEx = makeExchange(key);
167
+ await makeEndpoint(comp, ctx, 'users', 'get').createProducer().send(getEx);
168
+ assert.equal(getEx.in.body.name, 'Alice');
169
+ assert.equal(getEx.in.body.age, 31);
170
+ assert.equal(getEx.in.body.role, 'admin');
171
+ });
172
+ });
173
+
174
+ // ---------------------------------------------------------------------------
175
+ // find
176
+ // ---------------------------------------------------------------------------
177
+
178
+ describe('NosqlProducer: find', () => {
179
+ it('find with Filter returns matching documents as array', async () => {
180
+ const comp = makeComponent();
181
+ const ctx = new CamelContext();
182
+
183
+ // insert 3 documents
184
+ const insertEp = makeEndpoint(comp, ctx, 'items', 'insert');
185
+ await insertEp.createProducer().send(makeExchange({ name: 'Widget', price: 9.99 }));
186
+ await insertEp.createProducer().send(makeExchange({ name: 'Gadget', price: 24.99 }));
187
+ await insertEp.createProducer().send(makeExchange({ name: 'Donut', price: 1.99 }));
188
+
189
+ // find where price < 10
190
+ const filter = Filter.where('price').lt(10).build();
191
+ const findEp = makeEndpoint(comp, ctx, 'items', 'find');
192
+ const findEx = makeExchange(filter);
193
+ await findEp.createProducer().send(findEx);
194
+
195
+ assert.ok(Array.isArray(findEx.in.body), 'result should be an array');
196
+ assert.equal(findEx.in.body.length, 2);
197
+ const names = findEx.in.body.map(d => d.name).sort();
198
+ assert.deepEqual(names, ['Donut', 'Widget']);
199
+ });
200
+
201
+ it('find with eq filter returns exact matches', async () => {
202
+ const comp = makeComponent();
203
+ const ctx = new CamelContext();
204
+
205
+ const insertEp = makeEndpoint(comp, ctx, 'users', 'insert');
206
+ await insertEp.createProducer().send(makeExchange({ name: 'Alice', role: 'admin' }));
207
+ await insertEp.createProducer().send(makeExchange({ name: 'Bob', role: 'user' }));
208
+ await insertEp.createProducer().send(makeExchange({ name: 'Carol', role: 'admin' }));
209
+
210
+ const filter = Filter.where('role').eq('admin').build();
211
+ const findEx = makeExchange(filter);
212
+ await makeEndpoint(comp, ctx, 'users', 'find').createProducer().send(findEx);
213
+
214
+ assert.equal(findEx.in.body.length, 2);
215
+ assert.ok(findEx.in.body.every(d => d.role === 'admin'));
216
+ });
217
+ });
218
+
219
+ // ---------------------------------------------------------------------------
220
+ // Multiple named datasources
221
+ // ---------------------------------------------------------------------------
222
+
223
+ describe('NosqlComponent: multiple datasources', () => {
224
+ it('routes operations to the correct named datasource', async () => {
225
+ const comp = new NosqlComponent();
226
+ comp.setDatasource('ds1', () => new ClientDataSource({ url: 'jsnosqlc:memory:' }));
227
+ comp.setDatasource('ds2', () => new ClientDataSource({ url: 'jsnosqlc:memory:' }));
228
+
229
+ const ctx = new CamelContext();
230
+
231
+ const ep1 = comp.createEndpoint('nosql:col?datasource=ds1&operation=insert', 'col', new URLSearchParams('datasource=ds1&operation=insert'), ctx);
232
+ const ep2 = comp.createEndpoint('nosql:col?datasource=ds2&operation=insert', 'col', new URLSearchParams('datasource=ds2&operation=insert'), ctx);
233
+
234
+ const ex1 = makeExchange({ from: 'ds1' });
235
+ const ex2 = makeExchange({ from: 'ds2' });
236
+
237
+ await ep1.createProducer().send(ex1);
238
+ await ep2.createProducer().send(ex2);
239
+
240
+ // Keys are different strings — just check both were created
241
+ assert.equal(typeof ex1.in.body, 'string');
242
+ assert.equal(typeof ex2.in.body, 'string');
243
+ assert.notEqual(ex1.in.body, ex2.in.body);
244
+ });
245
+ });
246
+
247
+ // ---------------------------------------------------------------------------
248
+ // Error cases
249
+ // ---------------------------------------------------------------------------
250
+
251
+ describe('NosqlComponent: error cases', () => {
252
+ it('throws on unknown operation', async () => {
253
+ const comp = makeComponent();
254
+ const ctx = new CamelContext();
255
+ const ep = comp.createEndpoint('nosql:col?datasource=store&operation=truncate', 'col', new URLSearchParams('datasource=store&operation=truncate'), ctx);
256
+
257
+ await assert.rejects(
258
+ () => ep.createProducer().send(makeExchange(null)),
259
+ /unknown operation 'truncate'/i
260
+ );
261
+ });
262
+
263
+ it('throws when datasource is not registered', async () => {
264
+ const comp = new NosqlComponent(); // no datasources registered
265
+ const ctx = new CamelContext();
266
+ const ep = makeEndpoint(comp, ctx, 'col', 'insert', 'ghost');
267
+
268
+ await assert.rejects(
269
+ () => ep.createProducer().send(makeExchange({ x: 1 })),
270
+ /cannot resolve datasource 'ghost'/i
271
+ );
272
+ });
273
+ });
274
+
275
+ // ---------------------------------------------------------------------------
276
+ // NosqlComponent.close() releases clients
277
+ // ---------------------------------------------------------------------------
278
+
279
+ describe('NosqlComponent: close', () => {
280
+ it('close() closes all cached clients', async () => {
281
+ const comp = makeComponent();
282
+ const ctx = new CamelContext();
283
+
284
+ // Trigger client creation
285
+ const ep = makeEndpoint(comp, ctx, 'col', 'insert');
286
+ await ep.createProducer().send(makeExchange({ x: 1 }));
287
+
288
+ // close
289
+ await comp.close();
290
+
291
+ // Subsequent calls re-create client from factory (not throw)
292
+ // — just verify close() does not throw
293
+ // No assertion beyond no-throw is needed here
294
+ });
295
+ });
296
+
297
+ // ---------------------------------------------------------------------------
298
+ // Integration tests (conditional on NOSQL_URL)
299
+ // ---------------------------------------------------------------------------
300
+
301
+ const NOSQL_URL = process.env.NOSQL_URL;
302
+
303
+ if (NOSQL_URL) {
304
+ describe('NoSQL integration (live backend via NOSQL_URL)', () => {
305
+ it('round-trips insert → get → delete via live driver', async () => {
306
+ const comp = new NosqlComponent();
307
+ comp.setDatasource('live', () => new ClientDataSource({ url: NOSQL_URL }));
308
+ const ctx = new CamelContext();
309
+
310
+ const collName = `camel_lite_test`;
311
+ const insertEp = makeEndpoint(comp, ctx, collName, 'insert', 'live');
312
+ const getEp = makeEndpoint(comp, ctx, collName, 'get', 'live');
313
+ const deleteEp = makeEndpoint(comp, ctx, collName, 'delete', 'live');
314
+
315
+ const insertEx = makeExchange({ testField: 'integration', ts: Date.now() });
316
+ await insertEp.createProducer().send(insertEx);
317
+ const key = insertEx.in.body;
318
+ assert.ok(key, 'should have a key after insert');
319
+
320
+ const getEx = makeExchange(key);
321
+ await getEp.createProducer().send(getEx);
322
+ assert.equal(getEx.in.body.testField, 'integration');
323
+
324
+ const delEx = makeExchange(key);
325
+ await deleteEp.createProducer().send(delEx);
326
+
327
+ await comp.close();
328
+ });
329
+ });
330
+ } else {
331
+ describe('NoSQL integration (skipped — set NOSQL_URL=jsnosqlc:mongodb://... to enable)', () => {
332
+ it('skipped', () => { /* no-op */ });
333
+ });
334
+ }
335
+
336
+ // ---------------------------------------------------------------------------
337
+ // Context-aware datasource resolution (three-step chain)
338
+ // ---------------------------------------------------------------------------
339
+
340
+ describe('NosqlComponent: context bean resolution', () => {
341
+ function makeDs() {
342
+ return new ClientDataSource({ url: 'jsnosqlc:memory:' });
343
+ }
344
+
345
+ async function seedCollection(ds, collName, docs) {
346
+ const client = await ds.getClient();
347
+ const col = client.getCollection(collName);
348
+ for (const doc of docs) await col.insert(doc);
349
+ // Note: we intentionally don't close the client here — the component will cache it.
350
+ }
351
+
352
+ it('step 2: resolves datasource from context.getBean(name) when not in component map', async () => {
353
+ const ds = makeDs();
354
+ const comp = new NosqlComponent(); // no setDatasource()
355
+ const ctx = new CamelContext();
356
+ ctx.registerBean('myStore', ds);
357
+
358
+ // Seed data through the component so it uses the cached client
359
+ const insertParams = new URLSearchParams('datasource=myStore&operation=insert');
360
+ const insertEp = comp.createEndpoint('nosql:things?datasource=myStore&operation=insert', 'things', insertParams, ctx);
361
+ await insertEp.createProducer().send(makeExchange({ x: 42 }));
362
+
363
+ const params = new URLSearchParams('datasource=myStore&operation=find');
364
+ const ep = comp.createEndpoint('nosql:things?datasource=myStore&operation=find', 'things', params, ctx);
365
+
366
+ const ex = makeExchange(null);
367
+ await ep.createProducer().send(ex);
368
+
369
+ assert.ok(Array.isArray(ex.in.body));
370
+ assert.equal(ex.in.body.length, 1);
371
+ assert.equal(ex.in.body[0].x, 42);
372
+ });
373
+
374
+ it('step 3: auto-selects single context bean when no datasource given', async () => {
375
+ const ds = makeDs();
376
+ const comp = new NosqlComponent();
377
+ const ctx = new CamelContext();
378
+ ctx.registerBean('onlyStore', ds); // exactly one bean
379
+
380
+ // Seed via component (auto-select)
381
+ const insertParams = new URLSearchParams('operation=insert');
382
+ const insertEp = comp.createEndpoint('nosql:items?operation=insert', 'items', insertParams, ctx);
383
+ await insertEp.createProducer().send(makeExchange({ name: 'auto' }));
384
+
385
+ const params = new URLSearchParams('operation=find');
386
+ const ep = comp.createEndpoint('nosql:items?operation=find', 'items', params, ctx);
387
+
388
+ const ex = makeExchange(null);
389
+ await ep.createProducer().send(ex);
390
+
391
+ assert.equal(ex.in.body.length, 1);
392
+ assert.equal(ex.in.body[0].name, 'auto');
393
+ });
394
+
395
+ it('step 1 takes priority over context bean when component map has same name', async () => {
396
+ const dsInMap = makeDs();
397
+ const dsInContext = makeDs();
398
+
399
+ const comp = new NosqlComponent();
400
+ comp.setDatasource('store', () => dsInMap);
401
+
402
+ const ctx = new CamelContext();
403
+ ctx.registerBean('store', dsInContext);
404
+
405
+ // Insert via component map path
406
+ const insertParams = new URLSearchParams('datasource=store&operation=insert');
407
+ const insertEp = comp.createEndpoint('nosql:col?datasource=store&operation=insert', 'col', insertParams, ctx);
408
+ await insertEp.createProducer().send(makeExchange({ from: 'map' }));
409
+
410
+ const params = new URLSearchParams('datasource=store&operation=find');
411
+ const ep = comp.createEndpoint('nosql:col?datasource=store&operation=find', 'col', params, ctx);
412
+ const ex = makeExchange(null);
413
+ await ep.createProducer().send(ex);
414
+
415
+ // Should read from dsInMap (1 doc), not dsInContext (0 docs)
416
+ assert.equal(ex.in.body.length, 1);
417
+ assert.equal(ex.in.body[0].from, 'map');
418
+ });
419
+
420
+ it('throws descriptive error when no resolution path succeeds', async () => {
421
+ const comp = new NosqlComponent();
422
+ const ctx = new CamelContext(); // no beans
423
+ const params = new URLSearchParams('datasource=ghost&operation=insert');
424
+ const ep = comp.createEndpoint('nosql:col?datasource=ghost&operation=insert', 'col', params, ctx);
425
+
426
+ await assert.rejects(
427
+ () => ep.createProducer().send(makeExchange({ x: 1 })),
428
+ /cannot resolve datasource 'ghost'/i
429
+ );
430
+ });
431
+
432
+ it('throws when no name given and context has multiple beans', async () => {
433
+ const comp = new NosqlComponent();
434
+ const ctx = new CamelContext();
435
+ ctx.registerBean('ds1', makeDs());
436
+ ctx.registerBean('ds2', makeDs()); // two beans — no auto-select
437
+
438
+ const params = new URLSearchParams('operation=insert');
439
+ const ep = comp.createEndpoint('nosql:col?operation=insert', 'col', params, ctx);
440
+
441
+ await assert.rejects(
442
+ () => ep.createProducer().send(makeExchange({ x: 1 })),
443
+ /cannot resolve datasource/i
444
+ );
445
+ });
446
+ });