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