rosinterface 1.3.0 → 1.3.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/dist/cli/Generate.js +20 -1
- package/dist/cli/Generate.js.map +1 -1
- package/dist/cli/SchemaInferrer.d.ts +9 -0
- package/dist/cli/SchemaInferrer.js +22 -3
- package/dist/cli/SchemaInferrer.js.map +1 -1
- package/dist/client/CommandBuilder.d.ts +337 -2
- package/dist/client/CommandBuilder.js +483 -15
- package/dist/client/CommandBuilder.js.map +1 -1
- package/dist/client/MikrotikClient.d.ts +349 -1
- package/dist/client/MikrotikClient.js +364 -1
- package/dist/client/MikrotikClient.js.map +1 -1
- package/dist/client/MikrotikPool.d.ts +30 -0
- package/dist/client/MikrotikPool.js +31 -1
- package/dist/client/MikrotikPool.js.map +1 -1
- package/dist/client/MikrotikSwarm.d.ts +167 -0
- package/dist/client/MikrotikSwarm.js +178 -1
- package/dist/client/MikrotikSwarm.js.map +1 -1
- package/dist/client/MikrotikTransaction.d.ts +27 -0
- package/dist/client/MikrotikTransaction.js +28 -0
- package/dist/client/MikrotikTransaction.js.map +1 -1
- package/dist/client/ResultParser.d.ts +19 -0
- package/dist/client/ResultParser.js +31 -0
- package/dist/client/ResultParser.js.map +1 -1
- package/dist/client/SnapshotSubscription.d.ts +139 -0
- package/dist/client/SnapshotSubscription.js +169 -0
- package/dist/client/SnapshotSubscription.js.map +1 -1
- package/dist/core/Auth.d.ts +31 -0
- package/dist/core/Auth.js +46 -1
- package/dist/core/Auth.js.map +1 -1
- package/dist/core/CircuitBreaker.d.ts +26 -0
- package/dist/core/CircuitBreaker.js +26 -0
- package/dist/core/CircuitBreaker.js.map +1 -1
- package/dist/core/HttpConstants.d.ts +29 -16
- package/dist/core/HttpConstants.js +23 -7
- package/dist/core/HttpConstants.js.map +1 -1
- package/dist/core/OfflineQueue.d.ts +16 -0
- package/dist/core/OfflineQueue.js +10 -0
- package/dist/core/OfflineQueue.js.map +1 -1
- package/dist/core/RateLimiter.d.ts +24 -0
- package/dist/core/RateLimiter.js +43 -7
- package/dist/core/RateLimiter.js.map +1 -1
- package/dist/core/RestProtocol.d.ts +9 -0
- package/dist/core/RestProtocol.js +16 -1
- package/dist/core/RestProtocol.js.map +1 -1
- package/dist/core/RosError.d.ts +15 -0
- package/dist/core/RosError.js +36 -1
- package/dist/core/RosError.js.map +1 -1
- package/dist/core/RosProtocol.d.ts +21 -0
- package/dist/core/RosProtocol.js +40 -1
- package/dist/core/RosProtocol.js.map +1 -1
- package/dist/core/SchemaMapper.d.ts +41 -0
- package/dist/core/SchemaMapper.js +57 -2
- package/dist/core/SchemaMapper.js.map +1 -1
- package/dist/core/SocketClient.d.ts +34 -0
- package/dist/core/SocketClient.js +51 -3
- package/dist/core/SocketClient.js.map +1 -1
- package/dist/features/FileManager.d.ts +50 -0
- package/dist/features/FileManager.js +72 -6
- package/dist/features/FileManager.js.map +1 -1
- package/dist/features/LiveCollection.d.ts +51 -0
- package/dist/features/LiveCollection.js +69 -0
- package/dist/features/LiveCollection.js.map +1 -1
- package/dist/features/PrometheusExporter.d.ts +18 -1
- package/dist/features/PrometheusExporter.js +21 -1
- package/dist/features/PrometheusExporter.js.map +1 -1
- package/dist/index.d.ts +66 -0
- package/dist/index.js +78 -0
- package/dist/index.js.map +1 -1
- package/dist/types/index.d.ts +4 -0
- package/dist/types/index.js +24 -0
- package/dist/types/index.js.map +1 -1
- package/dist/utils/Helpers.d.ts +16 -0
- package/dist/utils/Helpers.js +17 -1
- package/dist/utils/Helpers.js.map +1 -1
- package/dist/utils/MikrotikCollection.d.ts +85 -0
- package/dist/utils/MikrotikCollection.js +97 -1
- package/dist/utils/MikrotikCollection.js.map +1 -1
- package/package.json +2 -2
|
@@ -4,71 +4,292 @@ exports.CommandBuilder = void 0;
|
|
|
4
4
|
const Helpers_1 = require("../utils/Helpers");
|
|
5
5
|
const MikrotikCollection_1 = require("../utils/MikrotikCollection");
|
|
6
6
|
const OfflineQueue_1 = require("../core/OfflineQueue");
|
|
7
|
+
/**
|
|
8
|
+
* CommandBuilder.ts
|
|
9
|
+
* * The Fluent Interface Engine with Offline-First Capabilities.
|
|
10
|
+
* * Provides syntax sugar for constructing MikroTik commands.
|
|
11
|
+
* * Supports Real-Time Streaming, Persistent Queueing, Smart Caching,
|
|
12
|
+
* * and Enterprise REST Features (Idempotency, Projections).
|
|
13
|
+
*/
|
|
7
14
|
class CommandBuilder {
|
|
8
15
|
constructor(client, menuPath) {
|
|
16
|
+
// Internal storage for query parts
|
|
9
17
|
this.queryParams = {};
|
|
10
18
|
this.propList = [];
|
|
19
|
+
// Internal state for execution options
|
|
11
20
|
this._idempotent = false;
|
|
21
|
+
// Persistence Flag
|
|
12
22
|
this.isPersistentRequest = false;
|
|
13
23
|
this.client = client;
|
|
24
|
+
// Normalize path: Ensure it starts with '/' and doesn't end with '/'
|
|
14
25
|
this.menuPath = menuPath.startsWith('/') ? menuPath : '/' + menuPath;
|
|
15
26
|
if (this.menuPath.endsWith('/') && this.menuPath.length > 1) {
|
|
16
27
|
this.menuPath = this.menuPath.slice(0, -1);
|
|
17
28
|
}
|
|
18
29
|
}
|
|
30
|
+
// ========================================================
|
|
31
|
+
// FLUENT FILTERS
|
|
32
|
+
// ========================================================
|
|
33
|
+
/**
|
|
34
|
+
* Adds a **Query Filter** (Equal Match) to the command.
|
|
35
|
+
*
|
|
36
|
+
* This method appends a filter parameter (`?key=value`) to the API request.
|
|
37
|
+
* Only items matching this condition will be returned or affected.
|
|
38
|
+
*
|
|
39
|
+
* **Feature: Auto-Kebab Case**
|
|
40
|
+
* You can use standard JavaScript camelCase keys. They are automatically converted
|
|
41
|
+
* to MikroTik's kebab-case format.
|
|
42
|
+
* - Input: `macAddress` -> Output: `?mac-address=...`
|
|
43
|
+
* - Input: `rxByte` -> Output: `?rx-byte=...`
|
|
44
|
+
*
|
|
45
|
+
* @param key The field name to filter by (e.g., 'name', 'disabled', 'macAddress').
|
|
46
|
+
* @param value The value to match. Booleans are converted to 'true'/'false' strings.
|
|
47
|
+
* @returns The current builder instance for chaining.
|
|
48
|
+
*
|
|
49
|
+
* @example
|
|
50
|
+
* // Find a specific user by name
|
|
51
|
+
* client.command('/ppp/secret')
|
|
52
|
+
* .where('name', 'john_doe')
|
|
53
|
+
* .print();
|
|
54
|
+
*
|
|
55
|
+
* @example
|
|
56
|
+
* // Find all disabled interfaces (camelCase supported)
|
|
57
|
+
* client.command('/interface')
|
|
58
|
+
* .where('disabled', true) // Sends ?disabled=true
|
|
59
|
+
* .print();
|
|
60
|
+
*/
|
|
19
61
|
where(key, value) {
|
|
20
62
|
const kebabKey = (0, Helpers_1.camelToKebab)(key);
|
|
21
63
|
this.queryParams[`?${kebabKey}`] = this.formatValue(value);
|
|
22
64
|
return this;
|
|
23
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* Adds an **Existence Filter** to the command.
|
|
68
|
+
*
|
|
69
|
+
* Matches items where the specified key exists (is defined), regardless of its value.
|
|
70
|
+
* This is useful for finding items that have optional properties set.
|
|
71
|
+
* Corresponds to the MikroTik API syntax `?key=`.
|
|
72
|
+
*
|
|
73
|
+
* @param key The field name to check for existence.
|
|
74
|
+
* @returns The current builder instance for chaining.
|
|
75
|
+
*
|
|
76
|
+
* @example
|
|
77
|
+
* // Find all firewall rules that have a comment
|
|
78
|
+
* client.command('/ip/firewall/filter')
|
|
79
|
+
* .whereExists('comment')
|
|
80
|
+
* .print();
|
|
81
|
+
*/
|
|
24
82
|
whereExists(key) {
|
|
25
83
|
const kebabKey = (0, Helpers_1.camelToKebab)(key);
|
|
84
|
+
// In MikroTik API, existence is queried by sending the key without value (Socket)
|
|
85
|
+
// or properly mapped in REST via your translateToRest logic.
|
|
26
86
|
this.queryParams[`?${kebabKey}`] = '';
|
|
27
87
|
return this;
|
|
28
88
|
}
|
|
89
|
+
/**
|
|
90
|
+
* **Field Projection (.select)**
|
|
91
|
+
*
|
|
92
|
+
* Restricts the fields returned by the router to a specific list.
|
|
93
|
+
* Using this method significantly reduces CPU load on the router and network bandwidth,
|
|
94
|
+
* especially when querying large tables like the routing table or logs.
|
|
95
|
+
* Corresponds to the MikroTik API argument `.proplist`.
|
|
96
|
+
*
|
|
97
|
+
* **Feature: Auto-Kebab Case**
|
|
98
|
+
* Inputs like `rxByte` are automatically converted to `rx-byte`.
|
|
99
|
+
*
|
|
100
|
+
* **Usage Levels:**
|
|
101
|
+
* * 1. **Novice:** Simple array of strings. `['name', 'address']`
|
|
102
|
+
* * 2. **Advanced (Surgical):** Type-safe list of keys ensuring they exist in interface T.
|
|
103
|
+
*
|
|
104
|
+
* @param fields An array of field names to retrieve (can be type-safe keys).
|
|
105
|
+
* @returns The current builder instance for chaining.
|
|
106
|
+
*
|
|
107
|
+
* @example
|
|
108
|
+
* // Get only the name and uptime of active users (ignoring traffic stats)
|
|
109
|
+
* const users = await client.command<PPPActive>('/ppp/active')
|
|
110
|
+
* .select(['name', 'uptime']) // optimized request
|
|
111
|
+
* .print();
|
|
112
|
+
*/
|
|
29
113
|
select(fields) {
|
|
30
114
|
const fieldStrings = fields.map(String);
|
|
115
|
+
// Convert camelCase (JS) to kebab-case (MikroTik)
|
|
116
|
+
// e.g., 'callerId' -> 'caller-id'
|
|
31
117
|
const kebabFields = fieldStrings.map(f => (0, Helpers_1.camelToKebab)(f));
|
|
32
|
-
|
|
118
|
+
// Use a Set to avoid duplicate fields in the request
|
|
119
|
+
const uniqueFields = new Set([...this.propList, ...kebabFields]);
|
|
120
|
+
this.propList = Array.from(uniqueFields);
|
|
33
121
|
return this;
|
|
34
122
|
}
|
|
123
|
+
/**
|
|
124
|
+
* **Enable Idempotency (.idempotent)**
|
|
125
|
+
*
|
|
126
|
+
* Flags the next operation to be **"Safe from Duplicates"**.
|
|
127
|
+
*
|
|
128
|
+
* * **Effect:** If you call `.add()` and the item already exists (based on a unique key like 'name'),
|
|
129
|
+
* the library will NOT throw an error. Instead, it will gracefully fetch and return the existing item.
|
|
130
|
+
* * **Requirement:** This is primarily supported in REST mode (v7+). In Socket mode, behavior depends on driver support.
|
|
131
|
+
*
|
|
132
|
+
* @returns The current builder instance for chaining.
|
|
133
|
+
* @example
|
|
134
|
+
* // Safe Create: Won't fail if 'vlan10' exists
|
|
135
|
+
* client.command('/interface/vlan')
|
|
136
|
+
* .idempotent()
|
|
137
|
+
* .add({ name: 'vlan10', 'vlan-id': 10 });
|
|
138
|
+
*/
|
|
35
139
|
idempotent(keyField) {
|
|
36
140
|
this._idempotent = true;
|
|
37
141
|
this._idempotencyKey = keyField;
|
|
38
142
|
return this;
|
|
39
143
|
}
|
|
144
|
+
// ========================================================
|
|
145
|
+
// PERSISTENCE MODIFIER
|
|
146
|
+
// ========================================================
|
|
147
|
+
/**
|
|
148
|
+
* **Offline Tolerance Strategy**
|
|
149
|
+
*
|
|
150
|
+
* Marks the current command as **Persistent**.
|
|
151
|
+
* Normally, if the router is disconnected, a command fails immediately.
|
|
152
|
+
* With `.persistent()`, the command is added to an internal retry queue
|
|
153
|
+
* and will be executed automatically as soon as the connection is restored.
|
|
154
|
+
*
|
|
155
|
+
* **Use Case:** Critical background tasks (e.g., scheduled billing cuts) that
|
|
156
|
+
* must eventually run, even if the network is currently unstable.
|
|
157
|
+
*
|
|
158
|
+
* @returns The current builder instance.
|
|
159
|
+
* @example
|
|
160
|
+
* // Even if the router is down, this will run when it comes back up.
|
|
161
|
+
* client.command('/system/reboot').persistent().send();
|
|
162
|
+
*/
|
|
40
163
|
persistent() {
|
|
41
164
|
this.isPersistentRequest = true;
|
|
42
165
|
return this;
|
|
43
166
|
}
|
|
167
|
+
// ========================================================
|
|
168
|
+
// READ TERMINATORS (With Caching & Source Filtering)
|
|
169
|
+
// ========================================================
|
|
170
|
+
/**
|
|
171
|
+
* **Server-Side Search (Multi-Item)**
|
|
172
|
+
*
|
|
173
|
+
* Executes a search directly on the RouterOS CPU.
|
|
174
|
+
* Unlike client-side filtering, this method instructs the router to filter
|
|
175
|
+
* the data *before* sending it over the network.
|
|
176
|
+
*
|
|
177
|
+
* **Performance Note:**
|
|
178
|
+
* extremely efficient for large tables (like thousands of PPPoE secrets),
|
|
179
|
+
* as only the matching rows travel over the wire.
|
|
180
|
+
*
|
|
181
|
+
* @param criteria An object with key-value pairs to match (e.g., `{ disabled: 'true', profile: 'default' }`).
|
|
182
|
+
* @returns A `MikrotikCollection` containing only the matching items.
|
|
183
|
+
*
|
|
184
|
+
* @example
|
|
185
|
+
* // Fetch all users in the 'default' profile
|
|
186
|
+
* const users = await client.command('/ppp/secret').findBy({ profile: 'default' });
|
|
187
|
+
*/
|
|
44
188
|
async findBy(criteria) {
|
|
189
|
+
// Iterate over criteria and use the main .where() method.
|
|
190
|
+
// This ensures formatValue() and camelToKebab() are applied consistently.
|
|
45
191
|
for (const [key, value] of Object.entries(criteria)) {
|
|
192
|
+
// Force casting to handle string|number|boolean properly
|
|
46
193
|
this.where(key, value);
|
|
47
194
|
}
|
|
195
|
+
// Execute the print command (which handles the REST/Socket translation)
|
|
48
196
|
return this.print();
|
|
49
197
|
}
|
|
198
|
+
/**
|
|
199
|
+
* **Server-Side Search (Single-Item)**
|
|
200
|
+
*
|
|
201
|
+
* The most efficient way to retrieve a single specific resource.
|
|
202
|
+
* It applies the filters, executes the query, and returns the first result.
|
|
203
|
+
*
|
|
204
|
+
* @param criteria Unique identifiers (e.g., `{ name: 'admin' }` or `{ macAddress: '00:...' }`).
|
|
205
|
+
* @returns The found item object, or `null` if no match was found.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* // Find a specific interface by name
|
|
209
|
+
* const ether1 = await client.command('/interface').findOne({ name: 'ether1' });
|
|
210
|
+
* if (ether1) console.log(ether1.mac_address);
|
|
211
|
+
*/
|
|
50
212
|
async findOne(criteria) {
|
|
213
|
+
// We reuse the central 'findBy' logic to ensure consistent query parsing
|
|
51
214
|
const collection = await this.findBy(criteria);
|
|
52
|
-
return collection.first();
|
|
215
|
+
return collection.count() > 0 ? collection.first() : null;
|
|
53
216
|
}
|
|
217
|
+
/**
|
|
218
|
+
* **Execution Terminator: Print with Caching**
|
|
219
|
+
*
|
|
220
|
+
* Finalizes the builder chain and sends the `print` command to the router.
|
|
221
|
+
*
|
|
222
|
+
* **Feature: Smart Caching (TTL 5s)**
|
|
223
|
+
* To prevent flooding the router with redundant read requests (e.g., multiple UI components
|
|
224
|
+
* asking for the same data), this method implements a **Read-Through Cache**.
|
|
225
|
+
* 1. **Cache Hit:** If the exact same query was made < 5 seconds ago, returns local data immediately.
|
|
226
|
+
* 2. **Cache Miss:** Fetches from the router, stores the result, and returns it.
|
|
227
|
+
*
|
|
228
|
+
* **Feature: Garbage Collection**
|
|
229
|
+
* Includes a probabilistic strategy (5% chance) to prune expired cache entries
|
|
230
|
+
* on every call to keep memory footprint low.
|
|
231
|
+
*
|
|
232
|
+
* @param extraParams Optional explicit parameters (e.g., `{ 'count-only': 'true' }`).
|
|
233
|
+
* @returns A `MikrotikCollection` (v1.2.0) equipped with pagination and transformation tools.
|
|
234
|
+
*
|
|
235
|
+
* @example
|
|
236
|
+
* // EXAMPLE 1: Standard Fetch (Basic Array)
|
|
237
|
+
* // Get all active PPPoE users as a simple array
|
|
238
|
+
* const users = await client.command('/ppp/active').print().then(c => c.toArray());
|
|
239
|
+
*
|
|
240
|
+
* @example
|
|
241
|
+
* // EXAMPLE 2: Pagination (Frontend Tables)
|
|
242
|
+
* // Get Page 2 of secrets, 25 items per page
|
|
243
|
+
* const page2 = await client.command('/ppp/secret')
|
|
244
|
+
* .where('disabled', 'false')
|
|
245
|
+
* .print()
|
|
246
|
+
* .then(c => c.toPages(2, 25));
|
|
247
|
+
*
|
|
248
|
+
* @example
|
|
249
|
+
* // EXAMPLE 3: High-Performance Lookup (O(1) Map)
|
|
250
|
+
* // Index users by name for instant access without looping
|
|
251
|
+
* const userMap = await client.command('/ppp/secret')
|
|
252
|
+
* .print()
|
|
253
|
+
* .then(c => c.toMap('name'));
|
|
254
|
+
*
|
|
255
|
+
* console.log(userMap['juan_perez']?.password); // Instant access!
|
|
256
|
+
*
|
|
257
|
+
* @example
|
|
258
|
+
* // EXAMPLE 4: Reporting (Grouping)
|
|
259
|
+
* // Group active connections by Service Type (pppoe vs ovpn)
|
|
260
|
+
* const report = await client.command('/ppp/active')
|
|
261
|
+
* .print()
|
|
262
|
+
* .then(c => c.toGrouped('service'));
|
|
263
|
+
*
|
|
264
|
+
* console.log(`PPPoE Users: ${report['pppoe']?.length || 0}`);
|
|
265
|
+
*/
|
|
54
266
|
async print(extraParams) {
|
|
55
267
|
const fluentParams = this.getParams();
|
|
56
268
|
const finalParams = { ...fluentParams, ...extraParams };
|
|
269
|
+
// GENERATE CACHE KEY
|
|
270
|
+
// Unique key based on: Router Host + Menu Path + Query Parameters
|
|
57
271
|
const host = this.client.options?.host || 'default';
|
|
58
272
|
const cacheKey = `${host}:${this.menuPath}:${JSON.stringify(finalParams)}`;
|
|
273
|
+
// CHECK CACHE
|
|
59
274
|
const cached = CommandBuilder.queryCache.get(cacheKey);
|
|
60
275
|
if (cached && Date.now() < cached.expires) {
|
|
61
276
|
return new MikrotikCollection_1.MikrotikCollection(cached.data);
|
|
62
277
|
}
|
|
278
|
+
// NETWORK REQUEST (Cache Miss)
|
|
63
279
|
const rawData = await this.client.write(`${this.menuPath}/print`, finalParams);
|
|
280
|
+
// SAVE TO CACHE
|
|
64
281
|
CommandBuilder.queryCache.set(cacheKey, {
|
|
65
282
|
data: rawData,
|
|
66
283
|
expires: Date.now() + CommandBuilder.CACHE_TTL_MS
|
|
67
284
|
});
|
|
285
|
+
// Garbage Collection: Clean up old keys randomly (simple strategy)
|
|
68
286
|
if (Math.random() > 0.95)
|
|
69
287
|
this.pruneCache();
|
|
70
288
|
return new MikrotikCollection_1.MikrotikCollection(rawData);
|
|
71
289
|
}
|
|
290
|
+
/**
|
|
291
|
+
* Helper to clean expired cache entries to prevent memory leaks.
|
|
292
|
+
*/
|
|
72
293
|
pruneCache() {
|
|
73
294
|
const now = Date.now();
|
|
74
295
|
for (const [key, val] of CommandBuilder.queryCache.entries()) {
|
|
@@ -76,79 +297,306 @@ class CommandBuilder {
|
|
|
76
297
|
CommandBuilder.queryCache.delete(key);
|
|
77
298
|
}
|
|
78
299
|
}
|
|
300
|
+
/**
|
|
301
|
+
* **Execution Terminator: First Result**
|
|
302
|
+
*
|
|
303
|
+
* Syntactic sugar for fetching a list and retrieving only the first item.
|
|
304
|
+
* Useful when you know the query will return a single result or you only care about the top record.
|
|
305
|
+
*
|
|
306
|
+
* @returns The first item of type `T`, or `null` if the collection is empty.
|
|
307
|
+
* @example
|
|
308
|
+
* // Get the first active admin user
|
|
309
|
+
* const admin = await client.command('/user').where('group', 'full').first();
|
|
310
|
+
*/
|
|
79
311
|
async first() {
|
|
312
|
+
// We execute print(). Note: MikroTik API does not natively support 'LIMIT 1'
|
|
313
|
+
// effectively without scripting, so this fetches the filtered list.
|
|
80
314
|
const collection = await this.print();
|
|
81
|
-
return collection.
|
|
315
|
+
// Ensure we return null if the collection is empty, not undefined.
|
|
316
|
+
return collection.count() > 0 ? collection.first() : null;
|
|
82
317
|
}
|
|
318
|
+
// ========================================================
|
|
319
|
+
// WRITE TERMINATORS (Add/Set/Remove)
|
|
320
|
+
// ========================================================
|
|
321
|
+
/**
|
|
322
|
+
* **Execution Terminator: Create Resource (ADD)**
|
|
323
|
+
*
|
|
324
|
+
* Sends an `/add` command to the router to create a new item.
|
|
325
|
+
*
|
|
326
|
+
* **Feature: Automatic Cache Invalidation**
|
|
327
|
+
* Upon success, this method automatically invalidates the local cache for this menu path.
|
|
328
|
+
* This guarantees that the next `.print()` call will fetch fresh data from the router,
|
|
329
|
+
* including the item you just created.
|
|
330
|
+
*
|
|
331
|
+
* **Feature: Offline Queueing**
|
|
332
|
+
* If the router is unreachable and `.persistent()` was used (or global offline mode is on),
|
|
333
|
+
* the command is saved to the `OfflineQueue` for later execution.
|
|
334
|
+
*
|
|
335
|
+
* **Feature: Idempotency**
|
|
336
|
+
* If `.idempotent()` was called, passes the flag to the client to safely handle duplicates.
|
|
337
|
+
*
|
|
338
|
+
* @param data The object containing the properties for the new item.
|
|
339
|
+
* @returns The MikroTik internal ID (e.g., `*1A`) of the created item, or `'QUEUED_OFFLINE'`.
|
|
340
|
+
*
|
|
341
|
+
* @example
|
|
342
|
+
* // Add a new firewall address list entry
|
|
343
|
+
* const id = await client.command('/ip/firewall/address-list').add({
|
|
344
|
+
* list: 'allowed_users',
|
|
345
|
+
* address: '192.168.88.50',
|
|
346
|
+
* comment: 'Added via API'
|
|
347
|
+
* });
|
|
348
|
+
*/
|
|
83
349
|
async add(data) {
|
|
84
350
|
const params = this.prepareParams(data);
|
|
351
|
+
// OFFLINE CHECK
|
|
85
352
|
if (this.shouldDefer()) {
|
|
86
353
|
OfflineQueue_1.OfflineQueue.enqueue({ action: 'add', path: this.menuPath, params: params });
|
|
87
354
|
return 'QUEUED_OFFLINE';
|
|
88
355
|
}
|
|
356
|
+
// REAL EXECUTION
|
|
89
357
|
const response = await this.client.write(`${this.menuPath}/add`, params, {
|
|
90
358
|
idempotent: this._idempotent,
|
|
91
359
|
idempotencyKey: this._idempotencyKey
|
|
92
360
|
});
|
|
93
361
|
this.invalidatePathCache();
|
|
362
|
+
let responseObj = null;
|
|
94
363
|
if (Array.isArray(response) && response.length > 0) {
|
|
95
|
-
|
|
96
|
-
|
|
364
|
+
responseObj = response[0]; // (Socket)
|
|
365
|
+
}
|
|
366
|
+
else if (typeof response === 'object' && response !== null) {
|
|
367
|
+
responseObj = response; // (REST)
|
|
368
|
+
}
|
|
369
|
+
// Idempotency Recovery
|
|
370
|
+
if (this._idempotent && responseObj && responseObj['_idempotent_recovery']) {
|
|
371
|
+
return responseObj;
|
|
372
|
+
}
|
|
373
|
+
let newId = responseObj ? (responseObj['ret'] || responseObj['.id']) : null;
|
|
374
|
+
if (!newId && params['name']) {
|
|
375
|
+
try {
|
|
376
|
+
const search = await this.client.write(`${this.menuPath}/print`, {
|
|
377
|
+
'?name': params['name']
|
|
378
|
+
});
|
|
379
|
+
if (Array.isArray(search) && search.length > 0) {
|
|
380
|
+
return search[0];
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
catch (e) {
|
|
384
|
+
console.warn("Fallback search failed", e);
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
if (newId) {
|
|
388
|
+
try {
|
|
389
|
+
const fetchResponse = await this.client.write(`${this.menuPath}/print`, { '.id': newId });
|
|
390
|
+
if (Array.isArray(fetchResponse) && fetchResponse.length > 0) {
|
|
391
|
+
return fetchResponse[0];
|
|
392
|
+
}
|
|
393
|
+
// (REST direct ID fetch)
|
|
394
|
+
else if (typeof fetchResponse === 'object' && fetchResponse !== null && !Array.isArray(fetchResponse)) {
|
|
395
|
+
return fetchResponse;
|
|
396
|
+
}
|
|
97
397
|
}
|
|
98
|
-
|
|
99
|
-
|
|
398
|
+
catch (error) {
|
|
399
|
+
console.warn(`Auto-fetch failed for ${newId}`, error);
|
|
100
400
|
}
|
|
401
|
+
return newId;
|
|
101
402
|
}
|
|
102
403
|
return '';
|
|
103
404
|
}
|
|
405
|
+
/**
|
|
406
|
+
* **Get or Create (Syntactic Sugar)**
|
|
407
|
+
* * A shorthand method that enables idempotency and executes the add.
|
|
408
|
+
* * Semantically clearer for business logic "Ensure this exists".
|
|
409
|
+
* * @param data The data to ensure exists.
|
|
410
|
+
* @returns The Single item created or recovered.
|
|
411
|
+
*/
|
|
104
412
|
async getOrCreate(data) {
|
|
105
|
-
this.idempotent();
|
|
413
|
+
this.idempotent(); // Enable flag
|
|
106
414
|
const result = await this.add(data);
|
|
415
|
+
// If result is string (Socket ID), we might need to fetch it (not implemented here for speed)
|
|
416
|
+
// If result is object (REST recovery), return it.
|
|
107
417
|
if (typeof result === 'object')
|
|
108
418
|
return result;
|
|
419
|
+
// Fallback for ID return
|
|
109
420
|
return { '.id': result };
|
|
110
421
|
}
|
|
422
|
+
/**
|
|
423
|
+
* **Execution Terminator: Update Resource (SET)**
|
|
424
|
+
*
|
|
425
|
+
* Sends a `/set` command to modify an existing item.
|
|
426
|
+
*
|
|
427
|
+
* **Feature: Idempotency & Cache**
|
|
428
|
+
* Like `.add()`, this triggers cache invalidation. It also handles the tricky
|
|
429
|
+
* `.id` parameter requirement of MikroTik automatically.
|
|
430
|
+
*
|
|
431
|
+
* @param id The internal ID of the item (e.g., `*14`) or a unique name if supported by the menu.
|
|
432
|
+
* @param data An object containing ONLY the fields you want to change (Partial update).
|
|
433
|
+
*
|
|
434
|
+
* @example
|
|
435
|
+
* // Update a PPPoE secret's password
|
|
436
|
+
* await client.command('/ppp/secret').set('*1F', {
|
|
437
|
+
* password: 'new_secure_password',
|
|
438
|
+
* comment: 'Password changed on ' + new Date().toISOString()
|
|
439
|
+
* });
|
|
440
|
+
*/
|
|
111
441
|
async set(id, data) {
|
|
112
442
|
const params = this.prepareParams(data);
|
|
113
443
|
params['.id'] = id;
|
|
444
|
+
// OFFLINE CHECK
|
|
114
445
|
if (this.shouldDefer()) {
|
|
115
446
|
OfflineQueue_1.OfflineQueue.enqueue({ action: 'set', path: this.menuPath, params: params });
|
|
116
|
-
|
|
447
|
+
throw new Error("OFFLINE_QUEUED");
|
|
117
448
|
}
|
|
118
449
|
await this.client.write(`${this.menuPath}/set`, params);
|
|
119
450
|
this.invalidatePathCache();
|
|
451
|
+
// AUTO-FETCH
|
|
452
|
+
try {
|
|
453
|
+
const fetchResponse = await this.client.write(`${this.menuPath}/print`, { '.id': id });
|
|
454
|
+
if (Array.isArray(fetchResponse) && fetchResponse.length > 0) {
|
|
455
|
+
return fetchResponse[0];
|
|
456
|
+
}
|
|
457
|
+
else if (typeof fetchResponse === 'object' && fetchResponse !== null) {
|
|
458
|
+
if (!Array.isArray(fetchResponse)) {
|
|
459
|
+
return fetchResponse;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
catch (error) {
|
|
464
|
+
console.warn(`Set successful, but auto-fetch failed for ${id}`, error);
|
|
465
|
+
}
|
|
466
|
+
return { '.id': id, ...data };
|
|
120
467
|
}
|
|
468
|
+
/**
|
|
469
|
+
* **Execution Terminator: Delete Resource (REMOVE)**
|
|
470
|
+
*
|
|
471
|
+
* Sends a `/remove` command to delete one or more items.
|
|
472
|
+
*
|
|
473
|
+
* **Feature: Batch Deletion**
|
|
474
|
+
* You can pass an array of IDs to delete multiple items in a single API call,
|
|
475
|
+
* which is significantly faster than a loop of delete calls.
|
|
476
|
+
*
|
|
477
|
+
* @param id A single ID string (e.g., `*1A`) or an array of IDs.
|
|
478
|
+
*
|
|
479
|
+
* @example
|
|
480
|
+
* // Remove a single item
|
|
481
|
+
* await client.command('/queue/simple').remove('*A1');
|
|
482
|
+
*
|
|
483
|
+
* @example
|
|
484
|
+
* // Batch remove (Kick multiple active connections)
|
|
485
|
+
* const idsToKick = ['*8001', '*8002', '*8003'];
|
|
486
|
+
* await client.command('/ppp/active').remove(idsToKick);
|
|
487
|
+
*/
|
|
121
488
|
async remove(id) {
|
|
122
|
-
|
|
123
|
-
const
|
|
489
|
+
// Always normalize input to an Array
|
|
490
|
+
const ids = Array.isArray(id) ? id : [id];
|
|
491
|
+
// OFFLINE CHECK
|
|
124
492
|
if (this.shouldDefer()) {
|
|
125
|
-
|
|
126
|
-
|
|
493
|
+
// For offline queue, store the "joined" version (comma-separated) for compactness
|
|
494
|
+
OfflineQueue_1.OfflineQueue.enqueue({
|
|
495
|
+
action: 'remove',
|
|
496
|
+
path: this.menuPath,
|
|
497
|
+
params: { '.id': ids.join(',') }
|
|
498
|
+
});
|
|
499
|
+
// Return empty array indicating online deletion was not confirmed
|
|
500
|
+
return [];
|
|
501
|
+
}
|
|
502
|
+
try {
|
|
503
|
+
// Execute parallel requests.
|
|
504
|
+
// This ensures compatibility with REST API, which typically requires individual
|
|
505
|
+
// DELETE requests per ID rather than a comma-separated list in the URL path.
|
|
506
|
+
await Promise.all(ids.map(singleId => {
|
|
507
|
+
return this.client.write(`${this.menuPath}/remove`, { '.id': singleId });
|
|
508
|
+
}));
|
|
509
|
+
}
|
|
510
|
+
catch (error) {
|
|
511
|
+
console.error("Error during bulk removal:", error);
|
|
512
|
+
throw error;
|
|
127
513
|
}
|
|
128
|
-
|
|
514
|
+
// Invalidate Cache for this path to ensure next read is fresh
|
|
129
515
|
this.invalidatePathCache();
|
|
516
|
+
return ids;
|
|
130
517
|
}
|
|
518
|
+
// ========================================================
|
|
519
|
+
// STREAMING TERMINATORS (UPDATED)
|
|
520
|
+
// ========================================================
|
|
521
|
+
/**
|
|
522
|
+
* **Streaming Terminator: Data Watcher**
|
|
523
|
+
*
|
|
524
|
+
* Initiates a standard real-time stream using the RouterOS `=follow=` protocol.
|
|
525
|
+
* Used for monitoring **changes in configuration or state** (e.g., "Tell me when a new log appears"
|
|
526
|
+
* or "Notify me when a user connects").
|
|
527
|
+
*
|
|
528
|
+
* **Protocol Internals:**
|
|
529
|
+
* This method automatically constructs the complex packet structure required by RouterOS:
|
|
530
|
+
* - Filters are sent as Queries (`?name=...`).
|
|
531
|
+
* - Properties are sent as Attributes (`=.proplist=...`).
|
|
532
|
+
* - The Streaming Flag (`=follow=`) is appended at the end.
|
|
533
|
+
*
|
|
534
|
+
* @param callback Function to execute whenever a new data packet arrives.
|
|
535
|
+
* @returns A `Subscription` object with a `.stop()` method to cancel the stream.
|
|
536
|
+
*
|
|
537
|
+
* @example
|
|
538
|
+
* // Monitor the System Log in real-time
|
|
539
|
+
* const logStream = client.command('/log')
|
|
540
|
+
* .where('topics', 'error') // Only listen for errors
|
|
541
|
+
* .listen((entry) => {
|
|
542
|
+
* console.log(`NEW ERROR: ${entry.message}`);
|
|
543
|
+
* });
|
|
544
|
+
*
|
|
545
|
+
* // Stop after 1 minute
|
|
546
|
+
* setTimeout(() => logStream.stop(), 60000);
|
|
547
|
+
*/
|
|
131
548
|
listen(callback) {
|
|
549
|
+
// Build Base Command
|
|
132
550
|
const lines = [`${this.menuPath}/print`];
|
|
551
|
+
// Get All Parameters (Filters + Selects)
|
|
133
552
|
const params = this.getParams();
|
|
553
|
+
// Smart Parsing: Distinguish between Queries (?) and Attributes (=)
|
|
134
554
|
for (const [key, value] of Object.entries(params)) {
|
|
135
555
|
let cleanKey = key;
|
|
556
|
+
// Remove existing '?' prefix if present (from .where())
|
|
136
557
|
if (cleanKey.startsWith('?')) {
|
|
137
558
|
cleanKey = cleanKey.substring(1);
|
|
138
559
|
}
|
|
139
560
|
if (cleanKey === '.proplist') {
|
|
561
|
+
// Attributes MUST start with '='
|
|
140
562
|
lines.push(`=${cleanKey}=${value}`);
|
|
141
563
|
}
|
|
142
564
|
else {
|
|
565
|
+
// Filters MUST start with '?'
|
|
143
566
|
lines.push(`?${cleanKey}=${value}`);
|
|
144
567
|
}
|
|
145
568
|
}
|
|
569
|
+
// Streaming Argument
|
|
146
570
|
lines.push('=follow=');
|
|
571
|
+
// Send Raw Array to Client (Polymorphic Stream)
|
|
147
572
|
return this.client.stream(lines, undefined, callback);
|
|
148
573
|
}
|
|
574
|
+
/**
|
|
575
|
+
* **Streaming Terminator: Traffic & Torch**
|
|
576
|
+
*
|
|
577
|
+
* A specialized listener designed for commands that stream data but **do not** support
|
|
578
|
+
* the standard `/print` syntax, specifically `/interface/monitor-traffic` and `/tool/torch`.
|
|
579
|
+
*
|
|
580
|
+
* **Difference from .listen():**
|
|
581
|
+
* Standard listeners use Query parameters (`?key=value`). Monitor commands require
|
|
582
|
+
* Action parameters (`=key=value`). This method automatically converts your `.where()`
|
|
583
|
+
* filters into the correct format for these tools.
|
|
584
|
+
*
|
|
585
|
+
* @param callback Function to execute with the live metric data.
|
|
586
|
+
* @returns A `Subscription` object.
|
|
587
|
+
*
|
|
588
|
+
* @example
|
|
589
|
+
* // Monitor Bandwidth on Ether1
|
|
590
|
+
* client.command('/interface') // or '/interface/monitor-traffic'
|
|
591
|
+
* .where('interface', 'ether1')
|
|
592
|
+
* .listenMonitor((stats) => {
|
|
593
|
+
* console.log(`RX: ${stats['rx-bits-per-second']} bps`);
|
|
594
|
+
* });
|
|
595
|
+
*/
|
|
149
596
|
listenMonitor(callback) {
|
|
150
597
|
const rawParams = this.getParams();
|
|
151
598
|
const actionParams = {};
|
|
599
|
+
// Convert standard query params to action params (remove '?')
|
|
152
600
|
for (const [key, value] of Object.entries(rawParams)) {
|
|
153
601
|
const cleanKey = key.startsWith('?') ? key.substring(1) : key;
|
|
154
602
|
actionParams[cleanKey] = value;
|
|
@@ -159,6 +607,12 @@ class CommandBuilder {
|
|
|
159
607
|
}
|
|
160
608
|
return this.client.stream(cmd, actionParams, callback);
|
|
161
609
|
}
|
|
610
|
+
// ========================================================
|
|
611
|
+
// INTERNAL HELPERS
|
|
612
|
+
// ========================================================
|
|
613
|
+
/**
|
|
614
|
+
* Clears cache entries related to the current menu path.
|
|
615
|
+
*/
|
|
162
616
|
invalidatePathCache() {
|
|
163
617
|
const host = this.client.options?.host || 'default';
|
|
164
618
|
const prefix = `${host}:${this.menuPath}`;
|
|
@@ -174,14 +628,21 @@ class CommandBuilder {
|
|
|
174
628
|
return !this.isClientConnected();
|
|
175
629
|
}
|
|
176
630
|
isClientConnected() {
|
|
631
|
+
// Accessing private socket property via 'any' casting (safe within library context)
|
|
177
632
|
const socket = this.client['socket'];
|
|
633
|
+
// If socket is null (pure REST mode), assume connected via HTTP or let fetch fail naturally
|
|
178
634
|
if (!socket)
|
|
179
635
|
return true;
|
|
180
636
|
return socket['connected'] === true;
|
|
181
637
|
}
|
|
638
|
+
/**
|
|
639
|
+
* Merges query params (filters) and property list (selects).
|
|
640
|
+
*/
|
|
182
641
|
getParams() {
|
|
183
642
|
const params = { ...this.queryParams };
|
|
643
|
+
// Add .proplist if select() was used
|
|
184
644
|
if (this.propList.length > 0) {
|
|
645
|
+
// Note: We add it as a plain key here, listen() handles the '=' prefix
|
|
185
646
|
params['.proplist'] = this.propList.join(',');
|
|
186
647
|
}
|
|
187
648
|
return params;
|
|
@@ -202,6 +663,13 @@ class CommandBuilder {
|
|
|
202
663
|
}
|
|
203
664
|
}
|
|
204
665
|
exports.CommandBuilder = CommandBuilder;
|
|
666
|
+
// ========================================================
|
|
667
|
+
// STATIC CACHE (Shared across builders)
|
|
668
|
+
// ========================================================
|
|
669
|
+
/**
|
|
670
|
+
* Short-lived cache to prevent read-spamming the router.
|
|
671
|
+
* TTL: 5 Seconds.
|
|
672
|
+
*/
|
|
205
673
|
CommandBuilder.queryCache = new Map();
|
|
206
|
-
CommandBuilder.CACHE_TTL_MS = 5000;
|
|
674
|
+
CommandBuilder.CACHE_TTL_MS = 5000; // 5s
|
|
207
675
|
//# sourceMappingURL=CommandBuilder.js.map
|