@machhub-dev/sdk-ts 0.0.6 → 0.0.8
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/cjs/classes/collection.d.ts +6 -0
- package/dist/cjs/classes/collection.js +51 -6
- package/dist/cjs/sdk-ts.js +93 -14
- package/dist/cjs/services/http.service.d.ts +5 -4
- package/dist/cjs/services/http.service.js +16 -23
- package/dist/cjs/services/mqtt.service.js +2 -2
- package/dist/cjs/services/nats.service.d.ts +2 -2
- package/dist/cjs/services/nats.service.js +45 -17
- package/dist/classes/collection.d.ts +6 -0
- package/dist/classes/collection.js +49 -5
- package/dist/sdk-ts.js +93 -14
- package/dist/services/http.service.d.ts +5 -4
- package/dist/services/http.service.js +16 -23
- package/dist/services/mqtt.service.js +2 -2
- package/dist/services/nats.service.d.ts +2 -2
- package/dist/services/nats.service.js +45 -17
- package/package.json +5 -5
- package/src/classes/collection.ts +51 -5
- package/src/sdk-ts.ts +103 -14
- package/src/services/http.service.ts +19 -26
- package/src/services/mqtt.service.ts +2 -2
- package/src/services/nats.service.ts +49 -18
package/dist/sdk-ts.js
CHANGED
|
@@ -15,8 +15,8 @@ class HTTPClient {
|
|
|
15
15
|
* @param applicationID The ID for your application (required)
|
|
16
16
|
* @param httpUrl The base URL for HTTP connection (default = http://localhost:6188)
|
|
17
17
|
*/
|
|
18
|
-
constructor(applicationID, httpUrl, developerKey) {
|
|
19
|
-
this.httpService = new HTTPService(httpUrl, MACHHUB_SDK_PATH, applicationID, developerKey);
|
|
18
|
+
constructor(applicationID, httpUrl, developerKey, runtimeID) {
|
|
19
|
+
this.httpService = new HTTPService(httpUrl, MACHHUB_SDK_PATH, applicationID, developerKey, runtimeID);
|
|
20
20
|
}
|
|
21
21
|
/**
|
|
22
22
|
* Gets server info
|
|
@@ -144,20 +144,20 @@ export class SDK {
|
|
|
144
144
|
if (!config.application_id)
|
|
145
145
|
config = { application_id: "" };
|
|
146
146
|
// console.log("Using application_id:", config.application_id);
|
|
147
|
-
const
|
|
148
|
-
|
|
147
|
+
const { port, runtimeID } = await getEnvConfig();
|
|
148
|
+
console.log("Port:", port);
|
|
149
149
|
if (!config.httpUrl) {
|
|
150
|
-
config.httpUrl = "http://localhost:" +
|
|
150
|
+
config.httpUrl = "http://localhost:" + port;
|
|
151
151
|
}
|
|
152
152
|
if (!config.mqttUrl) {
|
|
153
|
-
config.mqttUrl = "ws://localhost:" +
|
|
153
|
+
config.mqttUrl = "ws://localhost:" + port + "/mqtt";
|
|
154
154
|
}
|
|
155
155
|
if (!config.natsUrl) {
|
|
156
|
-
config.natsUrl = "ws://localhost:" +
|
|
156
|
+
config.natsUrl = "ws://localhost:" + port + "/nats";
|
|
157
157
|
}
|
|
158
158
|
const { application_id, httpUrl, mqttUrl, natsUrl } = config;
|
|
159
|
-
|
|
160
|
-
this.http = new HTTPClient(application_id, httpUrl, config.developer_key);
|
|
159
|
+
console.log("SDK Config:", { application_id, httpUrl, mqttUrl, natsUrl, developer_key: config.developer_key?.split('').map((_, i) => i < config.developer_key.length - 4 ? '*' : config.developer_key[i]).join('') });
|
|
160
|
+
this.http = new HTTPClient(application_id, httpUrl, config.developer_key, runtimeID);
|
|
161
161
|
this.mqtt = await MQTTClient.getInstance(application_id, mqttUrl, config.developer_key);
|
|
162
162
|
this.nats = await NATSClient.getInstance(application_id, natsUrl);
|
|
163
163
|
this._historian = new Historian(this.http["httpService"], this.mqtt["mqttService"]);
|
|
@@ -230,27 +230,106 @@ export class SDK {
|
|
|
230
230
|
return new Collection(this.http["httpService"], this.mqtt ? this.mqtt["mqttService"] : null, collectionName);
|
|
231
231
|
}
|
|
232
232
|
}
|
|
233
|
-
async function
|
|
233
|
+
async function getEnvConfig() {
|
|
234
234
|
try {
|
|
235
|
-
|
|
235
|
+
// Try to find the configuration endpoint by testing different base paths
|
|
236
|
+
const configUrl = await findConfigEndpoint();
|
|
237
|
+
const response = await fetchData(configUrl);
|
|
236
238
|
// console.log('Response:', response);
|
|
237
239
|
// console.log('runtimeID: ', response.runtimeID);
|
|
238
240
|
// console.log('applicationID: ', response.runtimeID.split('XmchX')[0]);
|
|
239
|
-
return response.port;
|
|
241
|
+
return { port: response.port, runtimeID: response.runtimeID };
|
|
240
242
|
}
|
|
241
243
|
catch (error) {
|
|
242
244
|
// console.log('No configured runtime ID:', error);
|
|
243
245
|
// TODO: Use DevPort from SDK Config or default to 61888
|
|
244
|
-
return "61888";
|
|
246
|
+
return { port: "61888", runtimeID: "" };
|
|
245
247
|
}
|
|
246
248
|
}
|
|
247
|
-
|
|
249
|
+
/**
|
|
250
|
+
* Attempts to find the correct configuration endpoint by trying different base paths
|
|
251
|
+
* Handles both port-based hosting (direct) and path-based hosting (reverse proxy)
|
|
252
|
+
*/
|
|
253
|
+
async function findConfigEndpoint() {
|
|
254
|
+
const origin = window.location.origin;
|
|
255
|
+
const pathname = window.location.pathname;
|
|
256
|
+
// List of potential base paths to try, ordered by likelihood
|
|
257
|
+
const basePaths = [
|
|
258
|
+
// 1. Try origin directly (for port-based hosting like localhost:6190)
|
|
259
|
+
origin,
|
|
260
|
+
// 2. Try current path segments for path-based hosting
|
|
261
|
+
...generatePathCandidates(pathname),
|
|
262
|
+
// 3. Try common root paths as fallback
|
|
263
|
+
origin,
|
|
264
|
+
];
|
|
265
|
+
// Remove duplicates while preserving order
|
|
266
|
+
const uniqueBasePaths = [...new Set(basePaths)];
|
|
267
|
+
for (const basePath of uniqueBasePaths) {
|
|
268
|
+
try {
|
|
269
|
+
const configUrl = `${basePath}/_cfg`;
|
|
270
|
+
// Test if this endpoint returns valid JSON config by making a GET request
|
|
271
|
+
const testResponse = await fetch(configUrl, {
|
|
272
|
+
method: 'GET',
|
|
273
|
+
headers: {
|
|
274
|
+
'Accept': 'application/json',
|
|
275
|
+
},
|
|
276
|
+
signal: AbortSignal.timeout(2000) // 2 second timeout
|
|
277
|
+
});
|
|
278
|
+
if (testResponse.ok) {
|
|
279
|
+
// Validate that the response is JSON and contains the expected 'port' field
|
|
280
|
+
const contentType = testResponse.headers.get('content-type');
|
|
281
|
+
if (contentType && contentType.includes('application/json')) {
|
|
282
|
+
try {
|
|
283
|
+
const testData = await testResponse.json();
|
|
284
|
+
// Check if the response has the expected structure with a 'port' field
|
|
285
|
+
if (testData && typeof testData === 'object' && 'port' in testData) {
|
|
286
|
+
// console.log(`Found config endpoint at: ${configUrl}`);
|
|
287
|
+
return configUrl;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
catch (jsonError) {
|
|
291
|
+
// Not valid JSON, continue to next candidate
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
catch (error) {
|
|
298
|
+
// Continue to next candidate
|
|
299
|
+
continue;
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
// If all attempts fail, default to origin + /_cfg
|
|
303
|
+
console.warn('Could not find config endpoint, using default origin');
|
|
304
|
+
return `${origin}/_cfg`;
|
|
305
|
+
}
|
|
306
|
+
/**
|
|
307
|
+
* Generates potential base path candidates from the current pathname
|
|
308
|
+
* For example, /demo2/homepage/settings would generate:
|
|
309
|
+
* - http://localhost/demo2/homepage
|
|
310
|
+
* - http://localhost/demo2
|
|
311
|
+
* - http://localhost
|
|
312
|
+
*/
|
|
313
|
+
function generatePathCandidates(pathname) {
|
|
314
|
+
const origin = window.location.origin;
|
|
315
|
+
const pathSegments = pathname.split('/').filter(segment => segment.length > 0);
|
|
316
|
+
const candidates = [];
|
|
317
|
+
// Generate paths by progressively removing segments from the end
|
|
318
|
+
for (let i = pathSegments.length; i > 0; i--) {
|
|
319
|
+
const path = '/' + pathSegments.slice(0, i).join('/');
|
|
320
|
+
candidates.push(origin + path);
|
|
321
|
+
}
|
|
322
|
+
return candidates;
|
|
323
|
+
}
|
|
324
|
+
async function fetchData(url, options) {
|
|
248
325
|
try {
|
|
249
326
|
const response = await fetch(url, {
|
|
250
327
|
method: 'GET',
|
|
251
328
|
headers: {
|
|
252
329
|
'Accept': 'application/json',
|
|
253
330
|
},
|
|
331
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
332
|
+
...options
|
|
254
333
|
});
|
|
255
334
|
if (!response.ok) {
|
|
256
335
|
throw new Error(`HTTP error! status: ${response.status}`);
|
|
@@ -9,19 +9,19 @@ export declare class HTTPService {
|
|
|
9
9
|
private url;
|
|
10
10
|
private applicationID;
|
|
11
11
|
private developerKey?;
|
|
12
|
-
|
|
12
|
+
private runtimeID?;
|
|
13
|
+
constructor(url: string, prefix: string, applicationID: string, developerKey?: string, runtimeID?: string);
|
|
13
14
|
get request(): RequestParameters;
|
|
14
|
-
private addRuntimeHeaders;
|
|
15
|
-
private getCookie;
|
|
16
15
|
}
|
|
17
16
|
declare class RequestParameters {
|
|
18
17
|
private base;
|
|
19
18
|
private applicationID;
|
|
20
19
|
private developerKey?;
|
|
20
|
+
private runtimeID?;
|
|
21
21
|
query?: Record<string, string>;
|
|
22
22
|
init?: RequestInit;
|
|
23
23
|
headers?: Record<string, string>;
|
|
24
|
-
constructor(base: URL, applicationID: string, developerKey?: string, query?: Record<string, string>);
|
|
24
|
+
constructor(base: URL, applicationID: string, developerKey?: string, runtimeID?: string, query?: Record<string, string>);
|
|
25
25
|
private withQuery;
|
|
26
26
|
private parseInit;
|
|
27
27
|
withBody(body: BodyInit): RequestParameters;
|
|
@@ -33,6 +33,7 @@ declare class RequestParameters {
|
|
|
33
33
|
setHeader(key: string, value: string): RequestParameters;
|
|
34
34
|
setBearerToken(token: string): RequestParameters;
|
|
35
35
|
withAccessToken(): RequestParameters;
|
|
36
|
+
withRuntimeID(): RequestParameters;
|
|
36
37
|
withDomain(): RequestParameters;
|
|
37
38
|
withDeveloperKey(): RequestParameters;
|
|
38
39
|
withContentType(mime: string): RequestParameters;
|
|
@@ -10,46 +10,29 @@ export class HTTPException extends Error {
|
|
|
10
10
|
}
|
|
11
11
|
}
|
|
12
12
|
export class HTTPService {
|
|
13
|
-
constructor(url, prefix, applicationID, developerKey) {
|
|
13
|
+
constructor(url, prefix, applicationID, developerKey, runtimeID) {
|
|
14
14
|
if (prefix == null)
|
|
15
15
|
prefix = "";
|
|
16
16
|
this.url = new URL(prefix, url);
|
|
17
17
|
this.applicationID = "domains:" + applicationID;
|
|
18
18
|
this.developerKey = developerKey;
|
|
19
|
+
this.runtimeID = runtimeID;
|
|
19
20
|
}
|
|
20
21
|
get request() {
|
|
21
|
-
return new RequestParameters(this.url, this.applicationID, this.developerKey);
|
|
22
|
-
}
|
|
23
|
-
addRuntimeHeaders(headers = {}) {
|
|
24
|
-
// Add runtime ID from cookie if available (for hosted applications)
|
|
25
|
-
if (typeof document !== 'undefined') {
|
|
26
|
-
const runtimeID = this.getCookie('machhub_runtime_id');
|
|
27
|
-
if (runtimeID) {
|
|
28
|
-
headers['X-MachHub-Runtime-ID'] = runtimeID;
|
|
29
|
-
}
|
|
30
|
-
}
|
|
31
|
-
return headers;
|
|
32
|
-
}
|
|
33
|
-
getCookie(name) {
|
|
34
|
-
if (typeof document === 'undefined')
|
|
35
|
-
return null;
|
|
36
|
-
const value = `; ${document.cookie}`;
|
|
37
|
-
const parts = value.split(`; ${name}=`);
|
|
38
|
-
if (parts.length === 2) {
|
|
39
|
-
return parts.pop()?.split(';').shift() || null;
|
|
40
|
-
}
|
|
41
|
-
return null;
|
|
22
|
+
return new RequestParameters(this.url, this.applicationID, this.developerKey, this.runtimeID);
|
|
42
23
|
}
|
|
43
24
|
}
|
|
44
25
|
class RequestParameters {
|
|
45
|
-
constructor(base, applicationID, developerKey, query) {
|
|
26
|
+
constructor(base, applicationID, developerKey, runtimeID, query) {
|
|
46
27
|
this.base = base;
|
|
47
28
|
this.applicationID = applicationID;
|
|
48
29
|
this.developerKey = developerKey;
|
|
30
|
+
this.runtimeID = runtimeID;
|
|
49
31
|
this.query = query;
|
|
50
32
|
this.withDomain(); // Ensure withDomain() is called by default
|
|
51
33
|
this.withAccessToken(); // Ensure withAccessToken() is called by default
|
|
52
34
|
this.withDeveloperKey(); // Ensure withDeveloperKey() is called by default
|
|
35
|
+
this.withRuntimeID(); // Ensure withRuntimeID() is called by default
|
|
53
36
|
}
|
|
54
37
|
withQuery(path, query) {
|
|
55
38
|
const newPath = [this.base.pathname, path].map(pathPart => pathPart.replace(/(^\/|\/$)/g, "")).join("/");
|
|
@@ -133,6 +116,10 @@ class RequestParameters {
|
|
|
133
116
|
this.setHeader("Authorization", `Bearer ${tkn}`);
|
|
134
117
|
return this;
|
|
135
118
|
}
|
|
119
|
+
withRuntimeID() {
|
|
120
|
+
this.setHeader("X-Machhub-Runtime-Id", this.runtimeID ?? "");
|
|
121
|
+
return this;
|
|
122
|
+
}
|
|
136
123
|
withDomain() {
|
|
137
124
|
this.setHeader("Domain", this.applicationID);
|
|
138
125
|
return this;
|
|
@@ -163,6 +150,12 @@ class RequestParameters {
|
|
|
163
150
|
if (body) {
|
|
164
151
|
if (body instanceof FormData) {
|
|
165
152
|
init.body = body;
|
|
153
|
+
// Remove Content-Type header if it exists, let browser set it for FormData
|
|
154
|
+
if (init.headers && typeof init.headers === 'object') {
|
|
155
|
+
const headers = init.headers;
|
|
156
|
+
delete headers['Content-Type'];
|
|
157
|
+
delete headers['content-type'];
|
|
158
|
+
}
|
|
166
159
|
}
|
|
167
160
|
else {
|
|
168
161
|
init.body = JSON.stringify(body);
|
|
@@ -36,7 +36,7 @@ export class MQTTService {
|
|
|
36
36
|
this.subscribedTopics.push({ topic, handler });
|
|
37
37
|
if (topic == "")
|
|
38
38
|
return;
|
|
39
|
-
console.log("New Subscription Handler:", topic);
|
|
39
|
+
// console.log("New Subscription Handler:", topic);
|
|
40
40
|
this.client.subscribe(topic, { qos: 2 }, (err) => {
|
|
41
41
|
if (err) {
|
|
42
42
|
console.error(`Failed to subscribe to topic ${topic}:`, err);
|
|
@@ -55,7 +55,7 @@ export class MQTTService {
|
|
|
55
55
|
publish(topic, message) {
|
|
56
56
|
try {
|
|
57
57
|
const payload = JSON.stringify(message);
|
|
58
|
-
console.log("Publishing to", topic, "with payload:", payload);
|
|
58
|
+
// console.log("Publishing to", topic, "with payload:", payload);
|
|
59
59
|
this.client.publish(topic, payload, {
|
|
60
60
|
qos: 2,
|
|
61
61
|
retain: true,
|
|
@@ -66,9 +66,9 @@ export declare class NATSService {
|
|
|
66
66
|
*/
|
|
67
67
|
printFunctions(): void;
|
|
68
68
|
/**
|
|
69
|
-
* Parses a message buffer into a JSON object.
|
|
69
|
+
* Parses a message buffer into a JSON object or returns raw data.
|
|
70
70
|
* @param message {Uint8Array} The message buffer.
|
|
71
|
-
* @returns {unknown} The parsed message.
|
|
71
|
+
* @returns {unknown} The parsed message (JSON object) or the raw string if parsing fails.
|
|
72
72
|
*/
|
|
73
73
|
private parseMessage;
|
|
74
74
|
/**
|
|
@@ -104,20 +104,31 @@ export class NATSService {
|
|
|
104
104
|
console.error("Error handling message:", err);
|
|
105
105
|
return;
|
|
106
106
|
}
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
msg.
|
|
111
|
-
|
|
107
|
+
try {
|
|
108
|
+
const dataStr = Buffer.from(msg.data).toString('utf-8');
|
|
109
|
+
const data = JSON.parse(dataStr);
|
|
110
|
+
const subjectParts = msg.subject.split(".");
|
|
111
|
+
if (subjectParts.length !== 4) {
|
|
112
|
+
msg.respond(JSON.stringify({ error: "Invalid subject format" }));
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
const functionName = subjectParts[3];
|
|
116
|
+
this.executeFunction(functionName, data)
|
|
117
|
+
.then((result) => {
|
|
118
|
+
msg.respond(JSON.stringify(result));
|
|
119
|
+
})
|
|
120
|
+
.catch((e) => {
|
|
121
|
+
console.log("Error executing function '" + functionName + "': ", e);
|
|
122
|
+
msg.respond(JSON.stringify({ status: "failed", error: e.message }));
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
catch (parseError) {
|
|
126
|
+
console.error("Error parsing function execution message:", parseError);
|
|
127
|
+
msg.respond(JSON.stringify({
|
|
128
|
+
status: "failed",
|
|
129
|
+
error: `Failed to parse message: ${parseError.message}`
|
|
130
|
+
}));
|
|
112
131
|
}
|
|
113
|
-
const functionName = subjectParts[3];
|
|
114
|
-
this.executeFunction(functionName, data)
|
|
115
|
-
.then((result) => {
|
|
116
|
-
msg.respond(JSON.stringify(result));
|
|
117
|
-
})
|
|
118
|
-
.catch((e) => {
|
|
119
|
-
msg.respond(JSON.stringify({ status: "failed", error: e.message }));
|
|
120
|
-
});
|
|
121
132
|
},
|
|
122
133
|
});
|
|
123
134
|
if (sub)
|
|
@@ -192,17 +203,34 @@ export class NATSService {
|
|
|
192
203
|
NATSService.log("Available functions: ", Object.keys(this.functions).join(", "));
|
|
193
204
|
}
|
|
194
205
|
/**
|
|
195
|
-
* Parses a message buffer into a JSON object.
|
|
206
|
+
* Parses a message buffer into a JSON object or returns raw data.
|
|
196
207
|
* @param message {Uint8Array} The message buffer.
|
|
197
|
-
* @returns {unknown} The parsed message.
|
|
208
|
+
* @returns {unknown} The parsed message (JSON object) or the raw string if parsing fails.
|
|
198
209
|
*/
|
|
199
210
|
parseMessage(message) {
|
|
200
211
|
try {
|
|
201
|
-
|
|
212
|
+
const messageStr = Buffer.from(message).toString('utf-8');
|
|
213
|
+
// Check if the message is empty
|
|
214
|
+
if (!messageStr || messageStr.trim().length === 0) {
|
|
215
|
+
NATSService.log("Received empty message");
|
|
216
|
+
return null;
|
|
217
|
+
}
|
|
218
|
+
// Try to parse as JSON
|
|
219
|
+
return JSON.parse(messageStr);
|
|
202
220
|
}
|
|
203
221
|
catch (error) {
|
|
222
|
+
// If JSON parsing fails, check if it's binary data or non-JSON content
|
|
223
|
+
const messageStr = Buffer.from(message).toString('utf-8');
|
|
224
|
+
// Check if it looks like binary data (contains non-printable characters)
|
|
225
|
+
const isBinary = message.some(byte => byte < 32 && byte !== 9 && byte !== 10 && byte !== 13);
|
|
226
|
+
if (isBinary) {
|
|
227
|
+
NATSService.log("Received binary data, returning as Uint8Array");
|
|
228
|
+
return message; // Return raw binary data
|
|
229
|
+
}
|
|
230
|
+
// If it's a plain string (not JSON), return as is
|
|
231
|
+
NATSService.log("Failed to parse message as JSON, returning as string:", messageStr.substring(0, 100));
|
|
204
232
|
console.error("Error parsing message:", error);
|
|
205
|
-
return
|
|
233
|
+
return messageStr;
|
|
206
234
|
}
|
|
207
235
|
}
|
|
208
236
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@machhub-dev/sdk-ts",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.8",
|
|
4
4
|
"description": "MACHHUB TYPESCRIPT SDK",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"machhub",
|
|
@@ -20,14 +20,14 @@
|
|
|
20
20
|
"build": "npx tsc && npx tsc --module CommonJS --outDir dist/cjs"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
+
"@nats-io/nats-core": "^3.0.2",
|
|
24
|
+
"@nats-io/transport-node": "^3.0.2",
|
|
23
25
|
"mqtt": "^5.10.4",
|
|
24
26
|
"safe-buffer": "^5.2.1",
|
|
25
27
|
"typescript": "^5.8.3",
|
|
26
|
-
"undici-types": "^7.4.0"
|
|
27
|
-
"@nats-io/nats-core": "^3.0.2",
|
|
28
|
-
"@nats-io/transport-node": "^3.0.2"
|
|
28
|
+
"undici-types": "^7.4.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@types/node": "^22.13.5"
|
|
32
32
|
}
|
|
33
|
-
}
|
|
33
|
+
}
|
|
@@ -1,6 +1,20 @@
|
|
|
1
1
|
import { HTTPService } from "../services/http.service.js";
|
|
2
2
|
import { MQTTService } from "../services/mqtt.service.js";
|
|
3
3
|
|
|
4
|
+
export class CollectionError extends Error {
|
|
5
|
+
public operation: string;
|
|
6
|
+
public collectionName: string;
|
|
7
|
+
public originalError: Error;
|
|
8
|
+
|
|
9
|
+
constructor(operation: string, collectionName: string, originalError: Error) {
|
|
10
|
+
super(`Collection operation '${operation}' failed on '${collectionName}': ${originalError.message}`);
|
|
11
|
+
this.name = 'CollectionError';
|
|
12
|
+
this.operation = operation;
|
|
13
|
+
this.collectionName = collectionName;
|
|
14
|
+
this.originalError = originalError;
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
4
18
|
export class Collection {
|
|
5
19
|
protected httpService: HTTPService;
|
|
6
20
|
protected mqttService: MQTTService | null;
|
|
@@ -13,6 +27,7 @@ export class Collection {
|
|
|
13
27
|
this.collectionName = collectionName;
|
|
14
28
|
}
|
|
15
29
|
|
|
30
|
+
|
|
16
31
|
filter(fieldName: string, operator: "=" | ">" | "<" | "<=" | ">=" | "!=", value: any): Collection {
|
|
17
32
|
this.queryParams[`filter[${fieldName}][${operator}][${typeof value}]`] = value;
|
|
18
33
|
return this;
|
|
@@ -34,31 +49,62 @@ export class Collection {
|
|
|
34
49
|
}
|
|
35
50
|
|
|
36
51
|
async getAll(): Promise<any[]> {
|
|
37
|
-
|
|
52
|
+
try {
|
|
53
|
+
return await this.httpService.request.get(this.collectionName + "/all", this.queryParams);
|
|
54
|
+
} catch (error) {
|
|
55
|
+
throw new CollectionError('getAll', this.collectionName, error as Error);
|
|
56
|
+
}
|
|
38
57
|
}
|
|
39
58
|
|
|
40
59
|
async getOne(id: string): Promise<any> {
|
|
41
60
|
if (!id) {
|
|
42
61
|
throw new Error("ID must be provided");
|
|
43
62
|
}
|
|
44
|
-
|
|
63
|
+
try {
|
|
64
|
+
return await this.httpService.request.get(id);
|
|
65
|
+
} catch (error) {
|
|
66
|
+
throw new CollectionError('getOne', this.collectionName, error as Error);
|
|
67
|
+
}
|
|
45
68
|
}
|
|
46
69
|
|
|
47
70
|
async create(data: Record<string, any>): Promise<any> {
|
|
48
|
-
|
|
71
|
+
try {
|
|
72
|
+
const formData = new FormData();
|
|
73
|
+
|
|
74
|
+
for (const [key, value] of Object.entries(data)) {
|
|
75
|
+
if (value instanceof File) {
|
|
76
|
+
formData.append(key, value, value.name);
|
|
77
|
+
data[key] = value.name
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
formData.append("record", JSON.stringify(data))
|
|
81
|
+
return await this.httpService.request
|
|
82
|
+
.withBody(formData)
|
|
83
|
+
.post(this.collectionName);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new CollectionError("create", this.collectionName, error as Error);
|
|
86
|
+
}
|
|
49
87
|
}
|
|
50
88
|
|
|
51
89
|
async update(id: string, data: Record<string, any>): Promise<any> {
|
|
52
90
|
if (!id) {
|
|
53
91
|
throw new Error("ID must be provided");
|
|
54
92
|
}
|
|
55
|
-
|
|
93
|
+
try {
|
|
94
|
+
return await this.httpService.request.withJSON(data).put(id);
|
|
95
|
+
} catch (error) {
|
|
96
|
+
throw new CollectionError('update', this.collectionName, error as Error);
|
|
97
|
+
}
|
|
56
98
|
}
|
|
57
99
|
|
|
58
100
|
async delete(id: string): Promise<any> {
|
|
59
101
|
if (!id) {
|
|
60
102
|
throw new Error("ID must be provided");
|
|
61
103
|
}
|
|
62
|
-
|
|
104
|
+
try {
|
|
105
|
+
return await this.httpService.request.delete(id);
|
|
106
|
+
} catch (error) {
|
|
107
|
+
throw new CollectionError('delete', this.collectionName, error as Error);
|
|
108
|
+
}
|
|
63
109
|
}
|
|
64
110
|
}
|
package/src/sdk-ts.ts
CHANGED
|
@@ -19,8 +19,8 @@ class HTTPClient {
|
|
|
19
19
|
* @param applicationID The ID for your application (required)
|
|
20
20
|
* @param httpUrl The base URL for HTTP connection (default = http://localhost:6188)
|
|
21
21
|
*/
|
|
22
|
-
constructor(applicationID: string, httpUrl:string, developerKey?: string) {
|
|
23
|
-
this.httpService = new HTTPService(httpUrl, MACHHUB_SDK_PATH, applicationID, developerKey);
|
|
22
|
+
constructor(applicationID: string, httpUrl:string, developerKey?: string, runtimeID?: string) {
|
|
23
|
+
this.httpService = new HTTPService(httpUrl, MACHHUB_SDK_PATH, applicationID, developerKey, runtimeID);
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
/**
|
|
@@ -173,26 +173,26 @@ export class SDK {
|
|
|
173
173
|
// console.log("Using application_id:", config.application_id);
|
|
174
174
|
|
|
175
175
|
|
|
176
|
-
const
|
|
177
|
-
|
|
176
|
+
const {port, runtimeID} = await getEnvConfig();
|
|
177
|
+
console.log("Port:", port);
|
|
178
178
|
|
|
179
179
|
if (!config.httpUrl) {
|
|
180
|
-
config.httpUrl = "http://localhost:" +
|
|
180
|
+
config.httpUrl = "http://localhost:" + port;
|
|
181
181
|
}
|
|
182
182
|
|
|
183
183
|
if (!config.mqttUrl) {
|
|
184
|
-
config.mqttUrl = "ws://localhost:" +
|
|
184
|
+
config.mqttUrl = "ws://localhost:" + port + "/mqtt";
|
|
185
185
|
}
|
|
186
186
|
|
|
187
187
|
if (!config.natsUrl) {
|
|
188
|
-
config.natsUrl = "ws://localhost:" +
|
|
188
|
+
config.natsUrl = "ws://localhost:" + port + "/nats";
|
|
189
189
|
}
|
|
190
190
|
|
|
191
191
|
const { application_id, httpUrl, mqttUrl, natsUrl } = config;
|
|
192
192
|
|
|
193
|
-
|
|
193
|
+
console.log("SDK Config:", { application_id, httpUrl, mqttUrl, natsUrl, developer_key: config.developer_key?.split('').map((_, i) => i < config!.developer_key!.length - 4 ? '*' : config!.developer_key![i]).join('') });
|
|
194
194
|
|
|
195
|
-
this.http = new HTTPClient(application_id, httpUrl, config.developer_key);
|
|
195
|
+
this.http = new HTTPClient(application_id, httpUrl, config.developer_key, runtimeID);
|
|
196
196
|
this.mqtt = await MQTTClient.getInstance(application_id, mqttUrl, config.developer_key);
|
|
197
197
|
this.nats = await NATSClient.getInstance(application_id, natsUrl);
|
|
198
198
|
|
|
@@ -273,28 +273,117 @@ export class SDK {
|
|
|
273
273
|
}
|
|
274
274
|
}
|
|
275
275
|
|
|
276
|
-
async function
|
|
276
|
+
async function getEnvConfig(): Promise<{port:string, runtimeID:string}> {
|
|
277
277
|
try {
|
|
278
|
-
|
|
278
|
+
// Try to find the configuration endpoint by testing different base paths
|
|
279
|
+
const configUrl = await findConfigEndpoint();
|
|
280
|
+
const response = await fetchData<{port:string, runtimeID:string}>(configUrl);
|
|
279
281
|
// console.log('Response:', response);
|
|
280
282
|
// console.log('runtimeID: ', response.runtimeID);
|
|
281
283
|
// console.log('applicationID: ', response.runtimeID.split('XmchX')[0]);
|
|
282
|
-
return response.port;
|
|
284
|
+
return { port: response.port, runtimeID: response.runtimeID};
|
|
283
285
|
} catch (error) {
|
|
284
286
|
// console.log('No configured runtime ID:', error);
|
|
285
287
|
// TODO: Use DevPort from SDK Config or default to 61888
|
|
286
|
-
return "61888";
|
|
288
|
+
return { port: "61888", runtimeID: ""};
|
|
287
289
|
}
|
|
288
290
|
}
|
|
289
291
|
|
|
292
|
+
/**
|
|
293
|
+
* Attempts to find the correct configuration endpoint by trying different base paths
|
|
294
|
+
* Handles both port-based hosting (direct) and path-based hosting (reverse proxy)
|
|
295
|
+
*/
|
|
296
|
+
async function findConfigEndpoint(): Promise<string> {
|
|
297
|
+
const origin = window.location.origin;
|
|
298
|
+
const pathname = window.location.pathname;
|
|
299
|
+
|
|
300
|
+
// List of potential base paths to try, ordered by likelihood
|
|
301
|
+
const basePaths = [
|
|
302
|
+
// 1. Try origin directly (for port-based hosting like localhost:6190)
|
|
303
|
+
origin,
|
|
304
|
+
|
|
305
|
+
// 2. Try current path segments for path-based hosting
|
|
306
|
+
...generatePathCandidates(pathname),
|
|
307
|
+
|
|
308
|
+
// 3. Try common root paths as fallback
|
|
309
|
+
origin,
|
|
310
|
+
];
|
|
311
|
+
|
|
312
|
+
// Remove duplicates while preserving order
|
|
313
|
+
const uniqueBasePaths = [...new Set(basePaths)];
|
|
314
|
+
|
|
315
|
+
for (const basePath of uniqueBasePaths) {
|
|
316
|
+
try {
|
|
317
|
+
const configUrl = `${basePath}/_cfg`;
|
|
318
|
+
|
|
319
|
+
// Test if this endpoint returns valid JSON config by making a GET request
|
|
320
|
+
const testResponse = await fetch(configUrl, {
|
|
321
|
+
method: 'GET',
|
|
322
|
+
headers: {
|
|
323
|
+
'Accept': 'application/json',
|
|
324
|
+
},
|
|
325
|
+
signal: AbortSignal.timeout(2000) // 2 second timeout
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
if (testResponse.ok) {
|
|
329
|
+
// Validate that the response is JSON and contains the expected 'port' field
|
|
330
|
+
const contentType = testResponse.headers.get('content-type');
|
|
331
|
+
if (contentType && contentType.includes('application/json')) {
|
|
332
|
+
try {
|
|
333
|
+
const testData = await testResponse.json();
|
|
334
|
+
// Check if the response has the expected structure with a 'port' field
|
|
335
|
+
if (testData && typeof testData === 'object' && 'port' in testData) {
|
|
336
|
+
// console.log(`Found config endpoint at: ${configUrl}`);
|
|
337
|
+
return configUrl;
|
|
338
|
+
}
|
|
339
|
+
} catch (jsonError) {
|
|
340
|
+
// Not valid JSON, continue to next candidate
|
|
341
|
+
continue;
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
}
|
|
345
|
+
} catch (error) {
|
|
346
|
+
// Continue to next candidate
|
|
347
|
+
continue;
|
|
348
|
+
}
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
// If all attempts fail, default to origin + /_cfg
|
|
352
|
+
console.warn('Could not find config endpoint, using default origin');
|
|
353
|
+
return `${origin}/_cfg`;
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* Generates potential base path candidates from the current pathname
|
|
358
|
+
* For example, /demo2/homepage/settings would generate:
|
|
359
|
+
* - http://localhost/demo2/homepage
|
|
360
|
+
* - http://localhost/demo2
|
|
361
|
+
* - http://localhost
|
|
362
|
+
*/
|
|
363
|
+
function generatePathCandidates(pathname: string): string[] {
|
|
364
|
+
const origin = window.location.origin;
|
|
365
|
+
const pathSegments = pathname.split('/').filter(segment => segment.length > 0);
|
|
366
|
+
const candidates: string[] = [];
|
|
367
|
+
|
|
368
|
+
// Generate paths by progressively removing segments from the end
|
|
369
|
+
for (let i = pathSegments.length; i > 0; i--) {
|
|
370
|
+
const path = '/' + pathSegments.slice(0, i).join('/');
|
|
371
|
+
candidates.push(origin + path);
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
return candidates;
|
|
375
|
+
}
|
|
376
|
+
|
|
290
377
|
|
|
291
|
-
async function fetchData<T>(url: string): Promise<T> {
|
|
378
|
+
async function fetchData<T>(url: string, options?: RequestInit): Promise<T> {
|
|
292
379
|
try {
|
|
293
380
|
const response = await fetch(url, {
|
|
294
381
|
method: 'GET',
|
|
295
382
|
headers: {
|
|
296
383
|
'Accept': 'application/json',
|
|
297
384
|
},
|
|
385
|
+
signal: AbortSignal.timeout(5000), // 5 second timeout
|
|
386
|
+
...options
|
|
298
387
|
});
|
|
299
388
|
|
|
300
389
|
if (!response.ok) {
|