@thinkingdifferently/core 1.1.0 → 1.3.0

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/index.mjs CHANGED
@@ -1,29 +1,172 @@
1
1
  // src/client.ts
2
2
  import axios from "axios";
3
3
  var TDClient = class {
4
- constructor(apiKey) {
4
+ constructor(apiKey, securityKey, publicKey) {
5
+ this.adminToken = null;
6
+ this.apikey = apiKey;
7
+ this.securityKey = securityKey;
8
+ this.publicKey = publicKey;
5
9
  this.api = axios.create({
6
- baseURL: "https://www.thinkingdifferently.dev/api/v1",
10
+ baseURL: "http://localhost:3000/api/v1",
11
+ // baseURL: "https://www.thinkingdifferently.dev/api/v1",
7
12
  headers: {
8
- "x-api-key": apiKey,
13
+ "X-API-Key": apiKey,
9
14
  "Content-Type": "application/json"
10
- }
15
+ },
16
+ withCredentials: true
17
+ // Enable browser cookie sharing automatically
11
18
  });
12
19
  }
13
- async request(method, body) {
20
+ //what is hte use of withCredentials: true in axios config?
21
+ //The withCredentials: true option in the Axios configuration is used to indicate that cross-site Access-Control requests should be made using credentials such as cookies,
22
+ // authorization headers, or TLS client certificates. When this option is set to true, it allows the browser to include cookies and other credentials in requests made to
23
+ // a different domain than the one serving the web page. This is particularly important for maintaining sessions and authentication when making API calls to a backend server
24
+ // that is on a different domain than the frontend application.
25
+ setSecurityKey(key) {
26
+ this.securityKey = key;
27
+ }
28
+ setPublicKey(key) {
29
+ this.publicKey = key;
30
+ }
31
+ setAdminToken(token) {
32
+ this.adminToken = token;
33
+ }
34
+ getAdminToken() {
35
+ return this.adminToken;
36
+ }
37
+ getPublicKey() {
38
+ return this.publicKey;
39
+ }
40
+ /**
41
+ * Reusable private error formatting utility
42
+ */
43
+ // private handleError(error: any): Error {
44
+ // if (error.response && error.response.data && error.response.data.error) {
45
+ // const backendError = error.response.data.error;
46
+ //
47
+ // let errorMessage = `[ThinkingDifferently SDK Error] ${backendError.code || error.response.status}: ${backendError.message}`;
48
+ //
49
+ // if (backendError.details) {
50
+ // errorMessage += `\nDetails: ${JSON.stringify(backendError.details, null, 2)}`;
51
+ // }
52
+ //
53
+ // return new Error(errorMessage);
54
+ // }
55
+ //
56
+ // return new Error(
57
+ // `[ThinkingDifferently Network Error] ${error.message || "Could not connect to the API."}`
58
+ // );
59
+ // }
60
+ //why if everything fails then we say that could not connect to the api? is it because we assume that if it's not a structured error from the backend, then it's likely a network or connectivity issue?
61
+ //explain: Yes, that's correct. The rationale behind returning a generic "Could not connect to the API" message when the error doesn't match the expected structured format from the backend is based on the assumption that if the error isn't coming from the API in a recognizable way, it's likely due to a network issue or some other problem preventing communication with the server.
62
+ // In a well-designed API, errors should be returned in a consistent format (e.g., with an "error" object containing "code" and "message").
63
+ // If the error doesn't conform to this structure, it suggests that the request may not have reached the server at all, or there was a failure in the network layer (like DNS issues, server downtime, CORS errors, etc.). Therefore, providing a generic message helps guide developers towards checking their network connection or server status when they encounter such unstructured errors.
64
+ //what if the developer of server forgets to send the error in this structured format? then the sdk will always say could not connect to the api even if the api is working fine but the error is not structured properly. so how can we improve this error handling to be more robust and informative?
65
+ // To improve the error handling and make it more robust and informative, we can implement a few strategies:
66
+ // 1. **Log the Raw Error**: Always log the raw error object to the console or a logging service. This way, developers can see the full context of the error, even if it's not structured.
67
+ // 2. **Check for Common Error Patterns**: Instead of only checking for a specific structured format, we can look for common patterns in error responses (like status codes, message fields, etc.) to provide more context.
68
+ // 3. **Fallback Messages**: If the error doesn't match the expected structure, we can provide a fallback message that includes any available information from the error object (like status code or message) instead of just saying "Could not connect to the API."
69
+ // 4. **Documentation and Guidelines**: Encourage backend developers to follow a consistent error response format through documentation and guidelines. This can help ensure that errors are structured properly, making it easier for the SDK to handle them effectively.
70
+ // Here's an improved version of the error handling method:
71
+ handleError(error) {
72
+ console.error("[ThinkingDifferently SDK] Raw Error:", error);
73
+ if (error.response) {
74
+ const status = error.response.status;
75
+ const data = error.response.data;
76
+ if (data && data.error) {
77
+ const backendError = data.error;
78
+ let errorMessage = `[ThinkingDifferently SDK Error] ${backendError.code || status}: ${backendError.message}`;
79
+ if (backendError.details) {
80
+ errorMessage += `
81
+ Details: ${JSON.stringify(backendError.details, null, 2)}`;
82
+ }
83
+ return new Error(errorMessage);
84
+ }
85
+ return new Error(`[ThinkingDifferently SDK Error] HTTP ${status}: ${data.message || "An error occurred."}`);
86
+ }
87
+ return new Error(
88
+ `[ThinkingDifferently Network Error] ${error.message || "Could not connect to the API."}`
89
+ );
90
+ }
91
+ /**
92
+ * Handles authentication HTTP requests targeting /auth/admin/login
93
+ */
94
+ async adminLogin(adminEmail, password) {
95
+ try {
96
+ const response = await this.api.post("/auth/admin/login", {
97
+ adminEmail,
98
+ password
99
+ });
100
+ const { token } = response.data;
101
+ if (token) {
102
+ this.setAdminToken(token);
103
+ }
104
+ return response.data;
105
+ } catch (error) {
106
+ throw this.handleError(error);
107
+ }
108
+ }
109
+ /**
110
+ * Determines if an API request is a database write operation.
111
+ */
112
+ isWriteOperation(method, body) {
113
+ if (method === "GET") return false;
114
+ if (method === "PATCH" || method === "DELETE") return true;
115
+ if (body) {
116
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
117
+ return true;
118
+ }
119
+ const query = body.query;
120
+ if (query) {
121
+ const parsed = typeof query === "string" ? JSON.parse(query) : query;
122
+ if (parsed && parsed.operation === "find") {
123
+ return false;
124
+ }
125
+ }
126
+ }
127
+ return true;
128
+ }
129
+ /**
130
+ * Executes queries and mutations against /data endpoint.
131
+ */
132
+ async sendDataRequest(method, body) {
133
+ if (this.isWriteOperation(method, body)) {
134
+ const isBrowser = typeof window !== "undefined";
135
+ if (!isBrowser && !this.securityKey && !this.adminToken) {
136
+ throw new Error(
137
+ "[ThinkingDifferently SDK Error] Write operations require either a valid Security Key or an Admin Session Token."
138
+ );
139
+ }
140
+ }
14
141
  try {
15
- const isFormData = body instanceof FormData;
142
+ const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
143
+ const headers = {};
144
+ if (!isFormData) {
145
+ headers["Content-Type"] = "application/json";
146
+ }
147
+ if (this.securityKey) {
148
+ headers["x-security-key"] = this.securityKey;
149
+ }
150
+ if (this.adminToken) {
151
+ headers["x-admin-token"] = this.adminToken;
152
+ }
153
+ if (isFormData) {
154
+ body.append("key", this.apikey);
155
+ } else {
156
+ body = {
157
+ key: this.apikey,
158
+ query: body
159
+ };
160
+ }
16
161
  const response = await this.api.request({
17
162
  url: "/data",
18
163
  method,
19
- headers: isFormData ? {} : { "Content-Type": "application/json" },
164
+ headers,
20
165
  ...method === "GET" ? { params: body } : { data: body }
21
166
  });
22
167
  return response.data;
23
168
  } catch (error) {
24
- throw new Error(
25
- error?.response?.data?.message || "Something went wrong"
26
- );
169
+ throw this.handleError(error);
27
170
  }
28
171
  }
29
172
  };
@@ -33,6 +176,7 @@ var QueryBuilder = class {
33
176
  constructor(collection, client) {
34
177
  this.client = client;
35
178
  this.query = {
179
+ operation: null,
36
180
  collection,
37
181
  filters: [],
38
182
  limit: null,
@@ -66,11 +210,39 @@ var QueryBuilder = class {
66
210
  // build() {
67
211
  // return this.query;
68
212
  // }
213
+ //to do writing the count method for the query builder
214
+ //the conditions are specified , so now the count method should return the number of documents that match the specified conditions in the query builder
215
+ //means count will first get the data based on the filters and then return the length of the data array as the count of matching documents
216
+ async count() {
217
+ this.query.operation = "count";
218
+ console.log("\n================ COUNT REQUEST ================");
219
+ console.log("[SDK] Final Query:", this.query);
220
+ try {
221
+ const response = await this.client.sendDataRequest(
222
+ "POST",
223
+ this.query
224
+ );
225
+ return response.count;
226
+ } catch (error) {
227
+ console.error("[SDK] COUNT ERROR");
228
+ console.error(error);
229
+ throw error;
230
+ }
231
+ }
232
+ toJSON() {
233
+ return structuredClone(this.query);
234
+ }
69
235
  async get() {
236
+ if (arguments.length > 0) {
237
+ throw new Error(
238
+ "[ThinkingDifferently SDK Error] The .get() method takes no arguments. Please use chainable methods like .where() and .limit()."
239
+ );
240
+ }
241
+ this.query.operation = "find";
70
242
  console.log("\n================ GET REQUEST ================");
71
243
  console.log("[SDK] Final Query:", this.query);
72
244
  try {
73
- const response = await this.client.request(
245
+ const response = await this.client.sendDataRequest(
74
246
  "POST",
75
247
  this.query
76
248
  );
@@ -78,45 +250,242 @@ var QueryBuilder = class {
78
250
  if (!Array.isArray(response.data)) {
79
251
  throw new Error("Invalid response format");
80
252
  }
81
- const parsed = response.data.map(
82
- (item) => item.data
83
- );
84
- console.log("[SDK] Parsed Data:", parsed);
85
- return parsed;
253
+ console.log("sdk response ", response);
254
+ console.log("[SDK] Data:", response.data);
255
+ return response.data;
86
256
  } catch (error) {
87
257
  console.error("[SDK] GET ERROR");
88
258
  console.error(error);
89
259
  throw error;
90
260
  }
91
261
  }
92
- //to do writing the count method for the query builder
93
- //the conditions are specified , so now the count method should return the number of documents that match the specified conditions in the query builder
94
- //means count will first get the data based on the filters and then return the length of the data array as the count of matching documents
95
- async count() {
96
- console.log("\n================ COUNT REQUEST ================");
97
- console.log("[SDK] Final Query for Count:", this.query);
262
+ async insert(data) {
263
+ this.query.operation = "insert";
264
+ console.log("\n================ INSERT REQUEST ================");
98
265
  try {
99
- const response = await this.client.request(
100
- "POST",
101
- {
102
- ...this.query,
103
- countOnly: true
104
- // Indicate we only want the count
266
+ if (data instanceof FormData) {
267
+ console.log("[SDK] FormData detected");
268
+ const payload = new FormData();
269
+ const extractedData = {};
270
+ for (const [key, value] of data.entries()) {
271
+ if (value instanceof File) {
272
+ payload.append(
273
+ key,
274
+ value
275
+ );
276
+ } else {
277
+ extractedData[key] = value;
278
+ }
105
279
  }
106
- );
107
- console.log("[SDK] Count Response:", response);
108
- if (typeof response.count !== "number") {
109
- throw new Error("Invalid count response from API");
280
+ this.query.data = extractedData;
281
+ payload.append(
282
+ "query",
283
+ JSON.stringify(this.query)
284
+ );
285
+ console.log("[SDK] Query:", this.query);
286
+ return await this.client.sendDataRequest(
287
+ "POST",
288
+ payload
289
+ );
110
290
  }
111
- return response.count;
291
+ this.query.data = data;
292
+ console.log("[SDK] Final Query:", this.query);
293
+ return await this.client.sendDataRequest(
294
+ "POST",
295
+ this.query
296
+ );
112
297
  } catch (error) {
113
- console.error("[SDK] COUNT ERROR");
298
+ console.error("[SDK] INSERT ERROR");
114
299
  console.error(error);
115
300
  throw error;
116
301
  }
117
302
  }
118
- toJSON() {
119
- return structuredClone(this.query);
303
+ async UpdateById(id, data) {
304
+ this.query.operation = "update";
305
+ this.query.id = id;
306
+ this.query.data = data;
307
+ console.log("\n================ UPDATE REQUEST ================");
308
+ console.log("[SDK] Final Query:", this.query);
309
+ try {
310
+ const response = await this.client.sendDataRequest(
311
+ "PATCH",
312
+ this.query
313
+ );
314
+ console.log("[SDK] Update Response:", response);
315
+ return response;
316
+ } catch (error) {
317
+ console.error("[SDK] UPDATE ERROR");
318
+ console.error(error);
319
+ throw error;
320
+ }
321
+ }
322
+ //updateMany
323
+ // its just like the get() it will call hte client
324
+ //example sdk.collection("animals").where("price", ">", 1000).updateMany({status : "expensive"}) it will update all the animals whose price is greater than 1000 and set their status to expensive
325
+ async updateMany(data) {
326
+ this.query.operation = "updateMany";
327
+ this.query.data = data;
328
+ console.log("\n================ UPDATE MANY REQUEST ================");
329
+ console.log("[SDK] Final Query:", this.query);
330
+ try {
331
+ const response = await this.client.sendDataRequest(
332
+ "PATCH",
333
+ this.query
334
+ );
335
+ console.log("[SDK] Update Many Response:", response);
336
+ return response;
337
+ } catch (error) {
338
+ console.error("[SDK] UPDATE MANY ERROR");
339
+ console.error(error);
340
+ throw error;
341
+ }
342
+ }
343
+ async DeleteById(id) {
344
+ this.query.operation = "delete";
345
+ this.query.id = id;
346
+ console.log("\n================ DELETE REQUEST ================");
347
+ console.log("[SDK] Final Query:", this.query);
348
+ try {
349
+ const response = await this.client.sendDataRequest(
350
+ "DELETE",
351
+ this.query
352
+ );
353
+ console.log("[SDK] Delete Response:", response);
354
+ return response;
355
+ } catch (error) {
356
+ console.error("[SDK] DELETE ERROR");
357
+ console.error(error);
358
+ throw error;
359
+ }
360
+ }
361
+ //deleteMany
362
+ // its just like the get() it will call hte client
363
+ //example sdk.collection("animals").where("price", ">", 1000).deleteMany() it will delete all the animals whose price is greater than 1000
364
+ async deleteMany() {
365
+ this.query.operation = "deleteMany";
366
+ console.log("\n================ DELETE MANY REQUEST ================");
367
+ console.log("[SDK] Final Query:", this.query);
368
+ try {
369
+ const response = await this.client.sendDataRequest(
370
+ "DELETE",
371
+ this.query
372
+ );
373
+ console.log("[SDK] Delete Many Response:", response);
374
+ return response;
375
+ } catch (error) {
376
+ console.error("[SDK] DELETE MANY ERROR");
377
+ console.error(error);
378
+ throw error;
379
+ }
380
+ }
381
+ };
382
+
383
+ // src/auth.ts
384
+ import * as crypto from "crypto";
385
+ var AdminAuth = class {
386
+ constructor(client) {
387
+ this.client = client;
388
+ }
389
+ /**
390
+ * Authenticate an administrator using credentials.
391
+ * The token returned will be automatically stored in the SDK client.
392
+ */
393
+ async credentials(adminEmail, password) {
394
+ return this.client.adminLogin(adminEmail, password);
395
+ }
396
+ /**
397
+ * Get the currently active administrator JWT token.
398
+ */
399
+ getToken() {
400
+ return this.client.getAdminToken();
401
+ }
402
+ /**
403
+ * Manually set the administrator JWT token (e.g. for server-side requests).
404
+ */
405
+ setToken(token) {
406
+ this.client.setAdminToken(token);
407
+ }
408
+ /**
409
+ * Offline verification of Ed25519 (EdDSA) JWT admin token using public key.
410
+ * Supported in Node.js / Server-side environments.
411
+ */
412
+ //detailed explaination of the verifyJwt function:
413
+ // The verifyJwt function is designed to validate a JWT (JSON Web Token) using
414
+ // the Ed25519 signature algorithm (EdDSA). It takes a JWT token as input and an optional public key in PEM format.
415
+ // The function performs several steps to ensure the token's integrity and authenticity:
416
+ // 1. It first determines which public key to use for verification, either from the argument or from the client's configuration.
417
+ // 2. It checks if the runtime environment supports the necessary crypto capabilities for JWT verification.
418
+ // 3. It splits the JWT into its three components: header, payload, and signature.
419
+ // 4. It decodes the signature from base64url format into a Buffer.
420
+ // 5. It imports the public key using Node.js's crypto module.
421
+ // 6. It verifies the signature against the message (header + payload) using the Ed25519 algorithm.
422
+ // 7. If verification succeeds, it decodes and parses the payload JSON.
423
+ // 8. It checks for token expiration based on the "exp" claim in the payload.
424
+ // 9. Finally, it returns the decoded payload if all checks pass,
425
+ // or throws descriptive errors if any step fails.
426
+ verifyJwt(token, publicKeyPem) {
427
+ const keyToUse = publicKeyPem || this.client.getPublicKey();
428
+ if (!keyToUse) {
429
+ throw new Error(
430
+ "[ThinkingDifferently SDK Error] Public key is required for JWT verification. Please configure it in SDKConfig or pass it as an argument."
431
+ );
432
+ }
433
+ if (typeof crypto === "undefined" || !crypto.createPublicKey || !crypto.verify) {
434
+ throw new Error(
435
+ "[ThinkingDifferently SDK Error] JWT verification is only supported in Node.js / Server-side environments."
436
+ );
437
+ }
438
+ const parts = token.split(".");
439
+ if (parts.length !== 3) {
440
+ throw new Error("Invalid JWT format");
441
+ }
442
+ const [headerB64, payloadB64, signatureB64] = parts;
443
+ const message = `${headerB64}.${payloadB64}`;
444
+ let signature;
445
+ try {
446
+ signature = Buffer.from(signatureB64, "base64url");
447
+ } catch (err) {
448
+ throw new Error("Invalid JWT signature encoding");
449
+ }
450
+ let publicKey;
451
+ try {
452
+ publicKey = crypto.createPublicKey({
453
+ key: keyToUse,
454
+ format: "pem",
455
+ type: "spki"
456
+ });
457
+ } catch (err) {
458
+ throw new Error(`Failed to import public key: ${err.message}`);
459
+ }
460
+ const verified = crypto.verify(
461
+ void 0,
462
+ // Algorithm must be undefined for Ed25519 (EdDSA) in Node
463
+ Buffer.from(message),
464
+ publicKey,
465
+ signature
466
+ );
467
+ if (!verified) {
468
+ throw new Error("Invalid JWT signature");
469
+ }
470
+ let payload;
471
+ try {
472
+ const payloadJson = Buffer.from(payloadB64, "base64url").toString("utf8");
473
+ payload = JSON.parse(payloadJson);
474
+ } catch (err) {
475
+ throw new Error("Invalid JWT payload JSON");
476
+ }
477
+ if (payload.exp && typeof payload.exp === "number") {
478
+ const currentTime = Math.floor(Date.now() / 1e3);
479
+ if (payload.exp < currentTime) {
480
+ throw new Error("JWT has expired");
481
+ }
482
+ }
483
+ return payload;
484
+ }
485
+ };
486
+ var AuthModule = class {
487
+ constructor(client) {
488
+ this.admin = new AdminAuth(client);
120
489
  }
121
490
  };
122
491
 
@@ -124,7 +493,26 @@ var QueryBuilder = class {
124
493
  var ThinkingDifferently = class {
125
494
  constructor(config) {
126
495
  console.log("[ThinkingDifferently SDK] Initializing SDK");
127
- this.client = new TDClient(config.apiKey);
496
+ this.client = new TDClient(config.apiKey, config.securityKey, config.publicKey);
497
+ this.auth = new AuthModule(this.client);
498
+ }
499
+ /**
500
+ * Dynamically update the security key (useful for backend projects).
501
+ */
502
+ setSecurityKey(key) {
503
+ this.client.setSecurityKey(key);
504
+ }
505
+ /**
506
+ * Dynamically update the public key used for offline verification.
507
+ */
508
+ setPublicKey(key) {
509
+ this.client.setPublicKey(key);
510
+ }
511
+ /**
512
+ * Helper to verify Ed25519 admin JWT token offline.
513
+ */
514
+ verifyJwt(token, publicKey) {
515
+ return this.auth.admin.verifyJwt(token, publicKey);
128
516
  }
129
517
  collection(name) {
130
518
  return new QueryBuilder(
@@ -158,7 +546,7 @@ var ThinkingDifferently = class {
158
546
  for (const [k, v] of payload.entries()) {
159
547
  console.log(k, v);
160
548
  }
161
- return this.client.request(
549
+ return this.client.sendDataRequest(
162
550
  "POST",
163
551
  payload
164
552
  );
@@ -168,7 +556,7 @@ var ThinkingDifferently = class {
168
556
  key,
169
557
  data
170
558
  });
171
- const response = await this.client.request(
559
+ const response = await this.client.sendDataRequest(
172
560
  "POST",
173
561
  {
174
562
  key,
@@ -189,7 +577,7 @@ var ThinkingDifferently = class {
189
577
  console.log("[SDK] Document ID:", id);
190
578
  console.log("[SDK] Update Data:", data);
191
579
  try {
192
- const response = await this.client.request(
580
+ const response = await this.client.sendDataRequest(
193
581
  "PATCH",
194
582
  {
195
583
  key,
@@ -210,7 +598,7 @@ var ThinkingDifferently = class {
210
598
  console.log("[SDK] Collection Key:", key);
211
599
  console.log("[SDK] Document ID:", id);
212
600
  try {
213
- const response = await this.client.request(
601
+ const response = await this.client.sendDataRequest(
214
602
  "DELETE",
215
603
  {
216
604
  key,
@@ -227,6 +615,8 @@ var ThinkingDifferently = class {
227
615
  }
228
616
  };
229
617
  export {
618
+ AdminAuth,
619
+ AuthModule,
230
620
  TDClient,
231
621
  ThinkingDifferently
232
622
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thinkingdifferently/core",
3
- "version": "1.1.0",
3
+ "version": "1.3.0",
4
4
  "description": "Official SDK for Thinking Differently API",
5
5
  "main": "./dist/index.js",
6
6
  "module": "./dist/index.mjs",
@@ -21,7 +21,8 @@
21
21
  "author": "Krrish Savlani",
22
22
  "license": "MIT",
23
23
  "dependencies": {
24
- "axios": "^1.16.0"
24
+ "axios": "^1.16.0",
25
+ "crypto-js": "^4.2.0"
25
26
  },
26
27
  "devDependencies": {
27
28
  "@types/node": "^25.6.2",