@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.js CHANGED
@@ -30,6 +30,8 @@ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: tru
30
30
  // src/index.ts
31
31
  var index_exports = {};
32
32
  __export(index_exports, {
33
+ AdminAuth: () => AdminAuth,
34
+ AuthModule: () => AuthModule,
33
35
  TDClient: () => TDClient,
34
36
  ThinkingDifferently: () => ThinkingDifferently
35
37
  });
@@ -38,29 +40,172 @@ module.exports = __toCommonJS(index_exports);
38
40
  // src/client.ts
39
41
  var import_axios = __toESM(require("axios"));
40
42
  var TDClient = class {
41
- constructor(apiKey) {
43
+ constructor(apiKey, securityKey, publicKey) {
44
+ this.adminToken = null;
45
+ this.apikey = apiKey;
46
+ this.securityKey = securityKey;
47
+ this.publicKey = publicKey;
42
48
  this.api = import_axios.default.create({
43
- baseURL: "https://www.thinkingdifferently.dev/api/v1",
49
+ baseURL: "http://localhost:3000/api/v1",
50
+ // baseURL: "https://www.thinkingdifferently.dev/api/v1",
44
51
  headers: {
45
- "x-api-key": apiKey,
52
+ "X-API-Key": apiKey,
46
53
  "Content-Type": "application/json"
47
- }
54
+ },
55
+ withCredentials: true
56
+ // Enable browser cookie sharing automatically
48
57
  });
49
58
  }
50
- async request(method, body) {
59
+ //what is hte use of withCredentials: true in axios config?
60
+ //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,
61
+ // 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
62
+ // 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
63
+ // that is on a different domain than the frontend application.
64
+ setSecurityKey(key) {
65
+ this.securityKey = key;
66
+ }
67
+ setPublicKey(key) {
68
+ this.publicKey = key;
69
+ }
70
+ setAdminToken(token) {
71
+ this.adminToken = token;
72
+ }
73
+ getAdminToken() {
74
+ return this.adminToken;
75
+ }
76
+ getPublicKey() {
77
+ return this.publicKey;
78
+ }
79
+ /**
80
+ * Reusable private error formatting utility
81
+ */
82
+ // private handleError(error: any): Error {
83
+ // if (error.response && error.response.data && error.response.data.error) {
84
+ // const backendError = error.response.data.error;
85
+ //
86
+ // let errorMessage = `[ThinkingDifferently SDK Error] ${backendError.code || error.response.status}: ${backendError.message}`;
87
+ //
88
+ // if (backendError.details) {
89
+ // errorMessage += `\nDetails: ${JSON.stringify(backendError.details, null, 2)}`;
90
+ // }
91
+ //
92
+ // return new Error(errorMessage);
93
+ // }
94
+ //
95
+ // return new Error(
96
+ // `[ThinkingDifferently Network Error] ${error.message || "Could not connect to the API."}`
97
+ // );
98
+ // }
99
+ //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?
100
+ //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.
101
+ // In a well-designed API, errors should be returned in a consistent format (e.g., with an "error" object containing "code" and "message").
102
+ // 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.
103
+ //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?
104
+ // To improve the error handling and make it more robust and informative, we can implement a few strategies:
105
+ // 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.
106
+ // 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.
107
+ // 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."
108
+ // 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.
109
+ // Here's an improved version of the error handling method:
110
+ handleError(error) {
111
+ console.error("[ThinkingDifferently SDK] Raw Error:", error);
112
+ if (error.response) {
113
+ const status = error.response.status;
114
+ const data = error.response.data;
115
+ if (data && data.error) {
116
+ const backendError = data.error;
117
+ let errorMessage = `[ThinkingDifferently SDK Error] ${backendError.code || status}: ${backendError.message}`;
118
+ if (backendError.details) {
119
+ errorMessage += `
120
+ Details: ${JSON.stringify(backendError.details, null, 2)}`;
121
+ }
122
+ return new Error(errorMessage);
123
+ }
124
+ return new Error(`[ThinkingDifferently SDK Error] HTTP ${status}: ${data.message || "An error occurred."}`);
125
+ }
126
+ return new Error(
127
+ `[ThinkingDifferently Network Error] ${error.message || "Could not connect to the API."}`
128
+ );
129
+ }
130
+ /**
131
+ * Handles authentication HTTP requests targeting /auth/admin/login
132
+ */
133
+ async adminLogin(adminEmail, password) {
134
+ try {
135
+ const response = await this.api.post("/auth/admin/login", {
136
+ adminEmail,
137
+ password
138
+ });
139
+ const { token } = response.data;
140
+ if (token) {
141
+ this.setAdminToken(token);
142
+ }
143
+ return response.data;
144
+ } catch (error) {
145
+ throw this.handleError(error);
146
+ }
147
+ }
148
+ /**
149
+ * Determines if an API request is a database write operation.
150
+ */
151
+ isWriteOperation(method, body) {
152
+ if (method === "GET") return false;
153
+ if (method === "PATCH" || method === "DELETE") return true;
154
+ if (body) {
155
+ if (typeof FormData !== "undefined" && body instanceof FormData) {
156
+ return true;
157
+ }
158
+ const query = body.query;
159
+ if (query) {
160
+ const parsed = typeof query === "string" ? JSON.parse(query) : query;
161
+ if (parsed && parsed.operation === "find") {
162
+ return false;
163
+ }
164
+ }
165
+ }
166
+ return true;
167
+ }
168
+ /**
169
+ * Executes queries and mutations against /data endpoint.
170
+ */
171
+ async sendDataRequest(method, body) {
172
+ if (this.isWriteOperation(method, body)) {
173
+ const isBrowser = typeof window !== "undefined";
174
+ if (!isBrowser && !this.securityKey && !this.adminToken) {
175
+ throw new Error(
176
+ "[ThinkingDifferently SDK Error] Write operations require either a valid Security Key or an Admin Session Token."
177
+ );
178
+ }
179
+ }
51
180
  try {
52
- const isFormData = body instanceof FormData;
181
+ const isFormData = typeof FormData !== "undefined" && body instanceof FormData;
182
+ const headers = {};
183
+ if (!isFormData) {
184
+ headers["Content-Type"] = "application/json";
185
+ }
186
+ if (this.securityKey) {
187
+ headers["x-security-key"] = this.securityKey;
188
+ }
189
+ if (this.adminToken) {
190
+ headers["x-admin-token"] = this.adminToken;
191
+ }
192
+ if (isFormData) {
193
+ body.append("key", this.apikey);
194
+ } else {
195
+ body = {
196
+ key: this.apikey,
197
+ query: body
198
+ };
199
+ }
53
200
  const response = await this.api.request({
54
201
  url: "/data",
55
202
  method,
56
- headers: isFormData ? {} : { "Content-Type": "application/json" },
203
+ headers,
57
204
  ...method === "GET" ? { params: body } : { data: body }
58
205
  });
59
206
  return response.data;
60
207
  } catch (error) {
61
- throw new Error(
62
- error?.response?.data?.message || "Something went wrong"
63
- );
208
+ throw this.handleError(error);
64
209
  }
65
210
  }
66
211
  };
@@ -70,6 +215,7 @@ var QueryBuilder = class {
70
215
  constructor(collection, client) {
71
216
  this.client = client;
72
217
  this.query = {
218
+ operation: null,
73
219
  collection,
74
220
  filters: [],
75
221
  limit: null,
@@ -103,11 +249,39 @@ var QueryBuilder = class {
103
249
  // build() {
104
250
  // return this.query;
105
251
  // }
252
+ //to do writing the count method for the query builder
253
+ //the conditions are specified , so now the count method should return the number of documents that match the specified conditions in the query builder
254
+ //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
255
+ async count() {
256
+ this.query.operation = "count";
257
+ console.log("\n================ COUNT REQUEST ================");
258
+ console.log("[SDK] Final Query:", this.query);
259
+ try {
260
+ const response = await this.client.sendDataRequest(
261
+ "POST",
262
+ this.query
263
+ );
264
+ return response.count;
265
+ } catch (error) {
266
+ console.error("[SDK] COUNT ERROR");
267
+ console.error(error);
268
+ throw error;
269
+ }
270
+ }
271
+ toJSON() {
272
+ return structuredClone(this.query);
273
+ }
106
274
  async get() {
275
+ if (arguments.length > 0) {
276
+ throw new Error(
277
+ "[ThinkingDifferently SDK Error] The .get() method takes no arguments. Please use chainable methods like .where() and .limit()."
278
+ );
279
+ }
280
+ this.query.operation = "find";
107
281
  console.log("\n================ GET REQUEST ================");
108
282
  console.log("[SDK] Final Query:", this.query);
109
283
  try {
110
- const response = await this.client.request(
284
+ const response = await this.client.sendDataRequest(
111
285
  "POST",
112
286
  this.query
113
287
  );
@@ -115,45 +289,242 @@ var QueryBuilder = class {
115
289
  if (!Array.isArray(response.data)) {
116
290
  throw new Error("Invalid response format");
117
291
  }
118
- const parsed = response.data.map(
119
- (item) => item.data
120
- );
121
- console.log("[SDK] Parsed Data:", parsed);
122
- return parsed;
292
+ console.log("sdk response ", response);
293
+ console.log("[SDK] Data:", response.data);
294
+ return response.data;
123
295
  } catch (error) {
124
296
  console.error("[SDK] GET ERROR");
125
297
  console.error(error);
126
298
  throw error;
127
299
  }
128
300
  }
129
- //to do writing the count method for the query builder
130
- //the conditions are specified , so now the count method should return the number of documents that match the specified conditions in the query builder
131
- //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
132
- async count() {
133
- console.log("\n================ COUNT REQUEST ================");
134
- console.log("[SDK] Final Query for Count:", this.query);
301
+ async insert(data) {
302
+ this.query.operation = "insert";
303
+ console.log("\n================ INSERT REQUEST ================");
135
304
  try {
136
- const response = await this.client.request(
137
- "POST",
138
- {
139
- ...this.query,
140
- countOnly: true
141
- // Indicate we only want the count
305
+ if (data instanceof FormData) {
306
+ console.log("[SDK] FormData detected");
307
+ const payload = new FormData();
308
+ const extractedData = {};
309
+ for (const [key, value] of data.entries()) {
310
+ if (value instanceof File) {
311
+ payload.append(
312
+ key,
313
+ value
314
+ );
315
+ } else {
316
+ extractedData[key] = value;
317
+ }
142
318
  }
143
- );
144
- console.log("[SDK] Count Response:", response);
145
- if (typeof response.count !== "number") {
146
- throw new Error("Invalid count response from API");
319
+ this.query.data = extractedData;
320
+ payload.append(
321
+ "query",
322
+ JSON.stringify(this.query)
323
+ );
324
+ console.log("[SDK] Query:", this.query);
325
+ return await this.client.sendDataRequest(
326
+ "POST",
327
+ payload
328
+ );
147
329
  }
148
- return response.count;
330
+ this.query.data = data;
331
+ console.log("[SDK] Final Query:", this.query);
332
+ return await this.client.sendDataRequest(
333
+ "POST",
334
+ this.query
335
+ );
149
336
  } catch (error) {
150
- console.error("[SDK] COUNT ERROR");
337
+ console.error("[SDK] INSERT ERROR");
151
338
  console.error(error);
152
339
  throw error;
153
340
  }
154
341
  }
155
- toJSON() {
156
- return structuredClone(this.query);
342
+ async UpdateById(id, data) {
343
+ this.query.operation = "update";
344
+ this.query.id = id;
345
+ this.query.data = data;
346
+ console.log("\n================ UPDATE REQUEST ================");
347
+ console.log("[SDK] Final Query:", this.query);
348
+ try {
349
+ const response = await this.client.sendDataRequest(
350
+ "PATCH",
351
+ this.query
352
+ );
353
+ console.log("[SDK] Update Response:", response);
354
+ return response;
355
+ } catch (error) {
356
+ console.error("[SDK] UPDATE ERROR");
357
+ console.error(error);
358
+ throw error;
359
+ }
360
+ }
361
+ //updateMany
362
+ // its just like the get() it will call hte client
363
+ //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
364
+ async updateMany(data) {
365
+ this.query.operation = "updateMany";
366
+ this.query.data = data;
367
+ console.log("\n================ UPDATE MANY REQUEST ================");
368
+ console.log("[SDK] Final Query:", this.query);
369
+ try {
370
+ const response = await this.client.sendDataRequest(
371
+ "PATCH",
372
+ this.query
373
+ );
374
+ console.log("[SDK] Update Many Response:", response);
375
+ return response;
376
+ } catch (error) {
377
+ console.error("[SDK] UPDATE MANY ERROR");
378
+ console.error(error);
379
+ throw error;
380
+ }
381
+ }
382
+ async DeleteById(id) {
383
+ this.query.operation = "delete";
384
+ this.query.id = id;
385
+ console.log("\n================ DELETE REQUEST ================");
386
+ console.log("[SDK] Final Query:", this.query);
387
+ try {
388
+ const response = await this.client.sendDataRequest(
389
+ "DELETE",
390
+ this.query
391
+ );
392
+ console.log("[SDK] Delete Response:", response);
393
+ return response;
394
+ } catch (error) {
395
+ console.error("[SDK] DELETE ERROR");
396
+ console.error(error);
397
+ throw error;
398
+ }
399
+ }
400
+ //deleteMany
401
+ // its just like the get() it will call hte client
402
+ //example sdk.collection("animals").where("price", ">", 1000).deleteMany() it will delete all the animals whose price is greater than 1000
403
+ async deleteMany() {
404
+ this.query.operation = "deleteMany";
405
+ console.log("\n================ DELETE MANY REQUEST ================");
406
+ console.log("[SDK] Final Query:", this.query);
407
+ try {
408
+ const response = await this.client.sendDataRequest(
409
+ "DELETE",
410
+ this.query
411
+ );
412
+ console.log("[SDK] Delete Many Response:", response);
413
+ return response;
414
+ } catch (error) {
415
+ console.error("[SDK] DELETE MANY ERROR");
416
+ console.error(error);
417
+ throw error;
418
+ }
419
+ }
420
+ };
421
+
422
+ // src/auth.ts
423
+ var crypto = __toESM(require("crypto"));
424
+ var AdminAuth = class {
425
+ constructor(client) {
426
+ this.client = client;
427
+ }
428
+ /**
429
+ * Authenticate an administrator using credentials.
430
+ * The token returned will be automatically stored in the SDK client.
431
+ */
432
+ async credentials(adminEmail, password) {
433
+ return this.client.adminLogin(adminEmail, password);
434
+ }
435
+ /**
436
+ * Get the currently active administrator JWT token.
437
+ */
438
+ getToken() {
439
+ return this.client.getAdminToken();
440
+ }
441
+ /**
442
+ * Manually set the administrator JWT token (e.g. for server-side requests).
443
+ */
444
+ setToken(token) {
445
+ this.client.setAdminToken(token);
446
+ }
447
+ /**
448
+ * Offline verification of Ed25519 (EdDSA) JWT admin token using public key.
449
+ * Supported in Node.js / Server-side environments.
450
+ */
451
+ //detailed explaination of the verifyJwt function:
452
+ // The verifyJwt function is designed to validate a JWT (JSON Web Token) using
453
+ // the Ed25519 signature algorithm (EdDSA). It takes a JWT token as input and an optional public key in PEM format.
454
+ // The function performs several steps to ensure the token's integrity and authenticity:
455
+ // 1. It first determines which public key to use for verification, either from the argument or from the client's configuration.
456
+ // 2. It checks if the runtime environment supports the necessary crypto capabilities for JWT verification.
457
+ // 3. It splits the JWT into its three components: header, payload, and signature.
458
+ // 4. It decodes the signature from base64url format into a Buffer.
459
+ // 5. It imports the public key using Node.js's crypto module.
460
+ // 6. It verifies the signature against the message (header + payload) using the Ed25519 algorithm.
461
+ // 7. If verification succeeds, it decodes and parses the payload JSON.
462
+ // 8. It checks for token expiration based on the "exp" claim in the payload.
463
+ // 9. Finally, it returns the decoded payload if all checks pass,
464
+ // or throws descriptive errors if any step fails.
465
+ verifyJwt(token, publicKeyPem) {
466
+ const keyToUse = publicKeyPem || this.client.getPublicKey();
467
+ if (!keyToUse) {
468
+ throw new Error(
469
+ "[ThinkingDifferently SDK Error] Public key is required for JWT verification. Please configure it in SDKConfig or pass it as an argument."
470
+ );
471
+ }
472
+ if (typeof crypto === "undefined" || !crypto.createPublicKey || !crypto.verify) {
473
+ throw new Error(
474
+ "[ThinkingDifferently SDK Error] JWT verification is only supported in Node.js / Server-side environments."
475
+ );
476
+ }
477
+ const parts = token.split(".");
478
+ if (parts.length !== 3) {
479
+ throw new Error("Invalid JWT format");
480
+ }
481
+ const [headerB64, payloadB64, signatureB64] = parts;
482
+ const message = `${headerB64}.${payloadB64}`;
483
+ let signature;
484
+ try {
485
+ signature = Buffer.from(signatureB64, "base64url");
486
+ } catch (err) {
487
+ throw new Error("Invalid JWT signature encoding");
488
+ }
489
+ let publicKey;
490
+ try {
491
+ publicKey = crypto.createPublicKey({
492
+ key: keyToUse,
493
+ format: "pem",
494
+ type: "spki"
495
+ });
496
+ } catch (err) {
497
+ throw new Error(`Failed to import public key: ${err.message}`);
498
+ }
499
+ const verified = crypto.verify(
500
+ void 0,
501
+ // Algorithm must be undefined for Ed25519 (EdDSA) in Node
502
+ Buffer.from(message),
503
+ publicKey,
504
+ signature
505
+ );
506
+ if (!verified) {
507
+ throw new Error("Invalid JWT signature");
508
+ }
509
+ let payload;
510
+ try {
511
+ const payloadJson = Buffer.from(payloadB64, "base64url").toString("utf8");
512
+ payload = JSON.parse(payloadJson);
513
+ } catch (err) {
514
+ throw new Error("Invalid JWT payload JSON");
515
+ }
516
+ if (payload.exp && typeof payload.exp === "number") {
517
+ const currentTime = Math.floor(Date.now() / 1e3);
518
+ if (payload.exp < currentTime) {
519
+ throw new Error("JWT has expired");
520
+ }
521
+ }
522
+ return payload;
523
+ }
524
+ };
525
+ var AuthModule = class {
526
+ constructor(client) {
527
+ this.admin = new AdminAuth(client);
157
528
  }
158
529
  };
159
530
 
@@ -161,7 +532,26 @@ var QueryBuilder = class {
161
532
  var ThinkingDifferently = class {
162
533
  constructor(config) {
163
534
  console.log("[ThinkingDifferently SDK] Initializing SDK");
164
- this.client = new TDClient(config.apiKey);
535
+ this.client = new TDClient(config.apiKey, config.securityKey, config.publicKey);
536
+ this.auth = new AuthModule(this.client);
537
+ }
538
+ /**
539
+ * Dynamically update the security key (useful for backend projects).
540
+ */
541
+ setSecurityKey(key) {
542
+ this.client.setSecurityKey(key);
543
+ }
544
+ /**
545
+ * Dynamically update the public key used for offline verification.
546
+ */
547
+ setPublicKey(key) {
548
+ this.client.setPublicKey(key);
549
+ }
550
+ /**
551
+ * Helper to verify Ed25519 admin JWT token offline.
552
+ */
553
+ verifyJwt(token, publicKey) {
554
+ return this.auth.admin.verifyJwt(token, publicKey);
165
555
  }
166
556
  collection(name) {
167
557
  return new QueryBuilder(
@@ -195,7 +585,7 @@ var ThinkingDifferently = class {
195
585
  for (const [k, v] of payload.entries()) {
196
586
  console.log(k, v);
197
587
  }
198
- return this.client.request(
588
+ return this.client.sendDataRequest(
199
589
  "POST",
200
590
  payload
201
591
  );
@@ -205,7 +595,7 @@ var ThinkingDifferently = class {
205
595
  key,
206
596
  data
207
597
  });
208
- const response = await this.client.request(
598
+ const response = await this.client.sendDataRequest(
209
599
  "POST",
210
600
  {
211
601
  key,
@@ -226,7 +616,7 @@ var ThinkingDifferently = class {
226
616
  console.log("[SDK] Document ID:", id);
227
617
  console.log("[SDK] Update Data:", data);
228
618
  try {
229
- const response = await this.client.request(
619
+ const response = await this.client.sendDataRequest(
230
620
  "PATCH",
231
621
  {
232
622
  key,
@@ -247,7 +637,7 @@ var ThinkingDifferently = class {
247
637
  console.log("[SDK] Collection Key:", key);
248
638
  console.log("[SDK] Document ID:", id);
249
639
  try {
250
- const response = await this.client.request(
640
+ const response = await this.client.sendDataRequest(
251
641
  "DELETE",
252
642
  {
253
643
  key,
@@ -265,6 +655,8 @@ var ThinkingDifferently = class {
265
655
  };
266
656
  // Annotate the CommonJS export names for ESM import in node:
267
657
  0 && (module.exports = {
658
+ AdminAuth,
659
+ AuthModule,
268
660
  TDClient,
269
661
  ThinkingDifferently
270
662
  });