@caruuto/caruuto-js 0.4.2 → 0.6.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/index.js CHANGED
@@ -10,7 +10,8 @@ const CaruutoClient = function (caruutoUrl, caruutoApiKey, options = {}) {
10
10
  'created_at',
11
11
  'created_by',
12
12
  'updated_at',
13
- 'updated_by'
13
+ 'updated_by',
14
+ 'v'
14
15
  ]
15
16
 
16
17
  if (this.options.caruutoUrl === undefined) {
@@ -0,0 +1,3 @@
1
+ const API_KEY_PREFIX = 'Api-Key-v1'
2
+
3
+ module.exports = { API_KEY_PREFIX }
@@ -0,0 +1,397 @@
1
+ const undici = require('undici')
2
+ const { API_KEY_PREFIX } = require('../constants')
3
+
4
+ module.exports = function (CaruutoClient, clientInstance) {
5
+ CaruutoClient.prototype.ai = {
6
+ /**
7
+ * Send a message and receive a complete blocking response.
8
+ *
9
+ * @param {Object} opts
10
+ * @param {string} opts.projectId - Caruuto project ID (UUID)
11
+ * @param {string} opts.message - User's message
12
+ * @param {string} [opts.conversationId] - Existing conversation ID (omit to start new)
13
+ * @param {string} [opts.clientSessionId] - Client's anonymous user token
14
+ * @param {string} [opts.source] - 'widget' | 'api' | 'embed'
15
+ * @param {string} [opts.referrerUrl] - URL where the chat is embedded
16
+ * @param {string} [opts.leadEmail] - Capture a lead email on this turn
17
+ * @param {string} [opts.leadName] - Lead name (paired with leadEmail)
18
+ * @param {string} [opts.type] - Restrict retrieval to a single knowledge type
19
+ * @param {number} [opts.maxItems] - Max knowledge items to retrieve (1–10)
20
+ * @returns {Promise<{ answer, conversation_id, context, entities, _meta, from_cache? }>}
21
+ */
22
+ query: async function (opts = {}) {
23
+ const body = buildRequestBody(opts, false)
24
+ const response = await doFetch(
25
+ this,
26
+ `${this.options.caruutoUrl}/api/ai/external`,
27
+ body
28
+ )
29
+ return response.status === 204 ? undefined : response.json()
30
+ }.bind(clientInstance),
31
+
32
+ /**
33
+ * Send a message and receive a streaming response.
34
+ * Returns an async generator. Iterate with `for await`.
35
+ *
36
+ * Yields:
37
+ * { type: 'text', text: string }
38
+ * { type: 'done', conversation_id: string, context: [], entities: [], _meta: {}, from_cache?: boolean }
39
+ *
40
+ * @param {Object} opts - Same options as query()
41
+ * @returns {AsyncGenerator}
42
+ */
43
+ stream: async function* (opts = {}) {
44
+ const body = buildRequestBody(opts, true)
45
+ const response = await doFetch(
46
+ this,
47
+ `${this.options.caruutoUrl}/api/ai/external`,
48
+ body
49
+ )
50
+
51
+ const reader = response.body.getReader()
52
+ const decoder = new TextDecoder()
53
+ let buffer = ''
54
+
55
+ try {
56
+ while (true) {
57
+ const { done, value } = await reader.read()
58
+ if (done) {
59
+ break
60
+ }
61
+
62
+ buffer += decoder.decode(value, { stream: true })
63
+ const lines = buffer.split('\n')
64
+ buffer = lines.pop()
65
+
66
+ for (const line of lines) {
67
+ if (!line.startsWith('data: ')) {
68
+ continue
69
+ }
70
+
71
+ const raw = line.slice(6).trim()
72
+ if (!raw) {
73
+ continue
74
+ }
75
+
76
+ let parsed
77
+ try {
78
+ parsed = JSON.parse(raw)
79
+ } catch (_) {
80
+ continue
81
+ }
82
+
83
+ if (parsed.error) {
84
+ throw new Error(parsed.error)
85
+ } else if (parsed.done) {
86
+ yield { type: 'done', ...parsed }
87
+ } else if (parsed.text !== undefined) {
88
+ yield { type: 'text', text: parsed.text }
89
+ }
90
+ }
91
+ }
92
+ } finally {
93
+ reader.releaseLock()
94
+ }
95
+ }.bind(clientInstance),
96
+
97
+ /**
98
+ * Explicitly end a conversation, marking it as 'ended'.
99
+ * Call this when the user closes the chat widget or navigates away.
100
+ *
101
+ * @param {Object} opts
102
+ * @param {string} opts.conversationId - The conversation to end
103
+ * @param {string} opts.projectId - The project the conversation belongs to
104
+ * @returns {Promise<{ id, status, turn_count, total_input_tokens, total_output_tokens, total_tokens, started_at, ended_at }>}
105
+ */
106
+ endConversation: async function ({ conversationId, projectId } = {}) {
107
+ if (!conversationId) {
108
+ throw new Error('conversationId is required')
109
+ }
110
+
111
+ if (!projectId) {
112
+ throw new Error('projectId is required')
113
+ }
114
+
115
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}/end`
116
+ const response = await doFetch(this, url, { project_id: projectId })
117
+ return response.json()
118
+ }.bind(clientInstance),
119
+
120
+ /**
121
+ * Retrieve assembled context for a message without calling the LLM.
122
+ * Use this when you want to call your own LLM (e.g. with AI SDK + tools)
123
+ * and need Caruuto to handle retrieval, entity detection, and prompt assembly.
124
+ *
125
+ * On a cache hit, returns { from_cache: true, cached_answer, conversation_id, _meta }.
126
+ * On a miss, returns { from_cache: false, system_prompt, history, context, entities,
127
+ * conversation_id, _meta }.
128
+ *
129
+ * @param {Object} opts - Same options as query(), minus stream/lead fields
130
+ * @returns {Promise<Object>}
131
+ */
132
+ context: async function (opts = {}) {
133
+ const {
134
+ projectId,
135
+ message,
136
+ conversationId,
137
+ clientSessionId,
138
+ source,
139
+ referrerUrl,
140
+ type,
141
+ maxItems
142
+ } = opts
143
+
144
+ const body = { project_id: projectId, message }
145
+
146
+ if (conversationId) body.conversation_id = conversationId
147
+ if (clientSessionId) body.client_session_id = clientSessionId
148
+ if (source) body.source = source
149
+ if (referrerUrl) body.referrer_url = referrerUrl
150
+ if (type) body.type = type
151
+ if (maxItems !== undefined) body.max_items = maxItems
152
+
153
+ const response = await doFetch(
154
+ this,
155
+ `${this.options.caruutoUrl}/api/ai/context`,
156
+ body
157
+ )
158
+ return response.json()
159
+ }.bind(clientInstance),
160
+
161
+ /**
162
+ * Persist a completed turn (user message + assistant response) to Caruuto.
163
+ * Call this after your LLM stream completes with the full response text and
164
+ * token usage. Caruuto writes to ai_conversation_messages, updates token
165
+ * totals on the conversation, and caches the answer if it's the first turn.
166
+ *
167
+ * @param {Object} opts
168
+ * @param {string} opts.conversationId - From context() response
169
+ * @param {string} opts.projectId
170
+ * @param {string} opts.userContent - The user's message text
171
+ * @param {string} opts.assistantContent - The full assistant response
172
+ * @param {string} [opts.modelId] - Model used (e.g. 'gpt-4o-mini')
173
+ * @param {number} [opts.inputTokens]
174
+ * @param {number} [opts.outputTokens]
175
+ * @param {Object[]} [opts.contextItems] - From context() response
176
+ * @param {string[]} [opts.entitiesReferenced] - Entity IDs from context()
177
+ * @param {string} [opts.leadEmail]
178
+ * @param {string} [opts.leadName]
179
+ * @returns {Promise<{ ok: boolean, turn_count: number }>}
180
+ */
181
+ /**
182
+ * Load a conversation and its messages, formatted as AI SDK UIMessage objects.
183
+ * Use this to restore a prior conversation into the chat UI.
184
+ *
185
+ * @param {Object} opts
186
+ * @param {string} opts.conversationId - The Caruuto conversation ID
187
+ * @param {string} opts.projectId
188
+ * @returns {Promise<{ id, status, turn_count, started_at, ended_at, messages }>}
189
+ */
190
+ /**
191
+ * Set a conversation's visibility to 'public' or 'private'.
192
+ *
193
+ * @param {Object} opts
194
+ * @param {string} opts.conversationId
195
+ * @param {string} opts.projectId
196
+ * @param {'public'|'private'} opts.visibility
197
+ * @returns {Promise<{ id, visibility, share_url }>}
198
+ */
199
+ /**
200
+ * Fork a conversation or question cache entry into a new conversation.
201
+ *
202
+ * @param {Object} opts
203
+ * @param {string} opts.sourceId - Conversation ID or question cache entry ID
204
+ * @param {string} opts.projectId
205
+ * @param {'conversation'|'question_cache'} opts.source
206
+ * @returns {Promise<{ id: string }>} New conversation ID
207
+ */
208
+ forkConversation: async function ({ sourceId, projectId, source = 'conversation' } = {}) {
209
+ if (!sourceId) throw new Error('sourceId is required')
210
+ if (!projectId) throw new Error('projectId is required')
211
+
212
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${sourceId}/fork`
213
+ const response = await doFetch(this, url, { project_id: projectId, source })
214
+ return response.json()
215
+ }.bind(clientInstance),
216
+
217
+ shareConversation: async function ({ conversationId, projectId, visibility = 'public' } = {}) {
218
+ if (!conversationId) throw new Error('conversationId is required')
219
+ if (!projectId) throw new Error('projectId is required')
220
+
221
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}/share`
222
+ const response = await doFetch(this, url, { project_id: projectId, visibility })
223
+ return response.json()
224
+ }.bind(clientInstance),
225
+
226
+ /**
227
+ * Fetch a publicly shared conversation without authentication.
228
+ * Resolves with null if the conversation is private or not found.
229
+ *
230
+ * @param {Object} opts
231
+ * @param {string} opts.conversationId
232
+ * @returns {Promise<{ id, turn_count, started_at, ended_at, messages } | null>}
233
+ */
234
+ getPublicConversation: async function ({ conversationId } = {}) {
235
+ if (!conversationId) throw new Error('conversationId is required')
236
+
237
+ const fetch = this.options.fetch || undici.fetch
238
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}/public`
239
+
240
+ const response = await fetch(url, { method: 'GET' })
241
+
242
+ if (response.status === 403 || response.status === 404) {
243
+ return null
244
+ }
245
+
246
+ if (!response.ok) {
247
+ const err = new Error('Failed to fetch public conversation')
248
+ err.status = response.status
249
+ throw err
250
+ }
251
+
252
+ return response.json()
253
+ }.bind(clientInstance),
254
+
255
+ loadConversation: async function ({ conversationId, projectId } = {}) {
256
+ if (!conversationId) {
257
+ throw new Error('conversationId is required')
258
+ }
259
+
260
+ if (!projectId) {
261
+ throw new Error('projectId is required')
262
+ }
263
+
264
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}?project_id=${encodeURIComponent(projectId)}`
265
+ const response = await doGet(this, url)
266
+ return response.json()
267
+ }.bind(clientInstance),
268
+
269
+ saveTurn: async function ({
270
+ conversationId,
271
+ projectId,
272
+ userContent,
273
+ assistantContent,
274
+ modelId,
275
+ inputTokens,
276
+ outputTokens,
277
+ contextItems,
278
+ entitiesReferenced,
279
+ leadEmail,
280
+ leadName
281
+ } = {}) {
282
+ if (!conversationId) {
283
+ throw new Error('conversationId is required')
284
+ }
285
+
286
+ if (!projectId) {
287
+ throw new Error('projectId is required')
288
+ }
289
+
290
+ const body = {
291
+ project_id: projectId,
292
+ user_content: userContent,
293
+ assistant_content: assistantContent
294
+ }
295
+
296
+ if (modelId) body.model_id = modelId
297
+ if (inputTokens !== undefined) body.input_tokens = inputTokens
298
+ if (outputTokens !== undefined) body.output_tokens = outputTokens
299
+ if (contextItems) body.context_items = contextItems
300
+ if (entitiesReferenced) body.entities_referenced = entitiesReferenced
301
+ if (leadEmail) body.lead_email = leadEmail
302
+ if (leadName) body.lead_name = leadName
303
+
304
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}/turn`
305
+ const response = await doFetch(this, url, body)
306
+ return response.json()
307
+ }.bind(clientInstance)
308
+ }
309
+ }
310
+
311
+ // ---------------------------------------------------------------------------
312
+ // Helpers (module-private)
313
+ // ---------------------------------------------------------------------------
314
+
315
+ function buildRequestBody(opts, stream) {
316
+ const {
317
+ projectId,
318
+ message,
319
+ conversationId,
320
+ clientSessionId,
321
+ source,
322
+ referrerUrl,
323
+ leadEmail,
324
+ leadName,
325
+ type,
326
+ maxItems
327
+ } = opts
328
+
329
+ const body = { project_id: projectId, message, stream }
330
+
331
+ if (conversationId) body.conversation_id = conversationId
332
+ if (clientSessionId) body.client_session_id = clientSessionId
333
+ if (source) body.source = source
334
+ if (referrerUrl) body.referrer_url = referrerUrl
335
+ if (leadEmail) body.lead_email = leadEmail
336
+ if (leadName) body.lead_name = leadName
337
+ if (type) body.type = type
338
+ if (maxItems !== undefined) body.max_items = maxItems
339
+
340
+ return body
341
+ }
342
+
343
+ async function doGet(client, url) {
344
+ const fetch = client.options.fetch || undici.fetch
345
+
346
+ const response = await fetch(url, {
347
+ method: 'GET',
348
+ headers: {
349
+ authorization: `${API_KEY_PREFIX} ${client.options.caruutoApiKey}`
350
+ }
351
+ })
352
+
353
+ if (!response.ok) {
354
+ const err = new Error('Caruuto AI request failed')
355
+ err.status = response.status
356
+
357
+ try {
358
+ const data = await response.clone().json()
359
+ if (data.error) {
360
+ err.message = data.error
361
+ }
362
+ } catch (_) {}
363
+
364
+ throw err
365
+ }
366
+
367
+ return response
368
+ }
369
+
370
+ async function doFetch(client, url, body) {
371
+ const fetch = client.options.fetch || undici.fetch
372
+
373
+ const response = await fetch(url, {
374
+ method: 'POST',
375
+ headers: {
376
+ 'content-type': 'application/json',
377
+ authorization: `${API_KEY_PREFIX} ${client.options.caruutoApiKey}`
378
+ },
379
+ body: JSON.stringify(body)
380
+ })
381
+
382
+ if (!response.ok) {
383
+ const err = new Error('Caruuto AI request failed')
384
+ err.status = response.status
385
+
386
+ try {
387
+ const data = await response.clone().json()
388
+ if (data.error) {
389
+ err.message = data.error
390
+ }
391
+ } catch (_) {}
392
+
393
+ throw err
394
+ }
395
+
396
+ return response
397
+ }
@@ -1,57 +1,12 @@
1
+ const { makeEndpointMethod } = require('./utils')
2
+
1
3
  module.exports = function (CaruutoClient, clientInstance) {
2
4
  CaruutoClient.prototype.email = {
3
- find: function (query) {
4
- this.endpoint = 'email'
5
-
6
- this.query = query
7
-
8
- const requestPayload = {
9
- method: 'GET',
10
- uri: this._buildURL({ useParams: true })
11
- }
12
-
13
- this._setHeader('content-type', 'application/json')
14
-
15
- return this._createRequestObject(requestPayload)
16
- }.bind(clientInstance),
17
- queue: function (data) {
18
- this.endpoint = 'email'
19
-
20
- const requestPayload = {
21
- body: data,
22
- method: 'POST',
23
- uri: this._buildURL()
24
- }
25
-
26
- this._setHeader('content-type', 'application/json')
27
-
28
- return this._createRequestObject(requestPayload)
29
- }.bind(clientInstance),
30
- update: function (data) {
31
- this.endpoint = 'email'
32
-
33
- const requestPayload = {
34
- body: data,
35
- method: 'PUT',
36
- uri: this._buildURL()
37
- }
38
-
39
- this._setHeader('content-type', 'application/json')
40
-
41
- return this._createRequestObject(requestPayload)
42
- }.bind(clientInstance),
43
- delete: function (data) {
44
- this.endpoint = 'email'
45
-
46
- const requestPayload = {
47
- body: data,
48
- method: 'DELETE',
49
- uri: this._buildURL()
50
- }
51
-
52
- this._setHeader('content-type', 'application/json')
53
-
54
- return this._createRequestObject(requestPayload)
55
- }.bind(clientInstance)
5
+ find: makeEndpointMethod('email', 'GET', { query: true }).bind(
6
+ clientInstance
7
+ ),
8
+ queue: makeEndpointMethod('email', 'POST').bind(clientInstance),
9
+ update: makeEndpointMethod('email', 'PUT').bind(clientInstance),
10
+ delete: makeEndpointMethod('email', 'DELETE').bind(clientInstance)
56
11
  }
57
12
  }
@@ -1,33 +1,10 @@
1
+ const { makeEndpointMethod } = require('./utils')
2
+
1
3
  module.exports = function (CaruutoClient, clientInstance) {
2
4
  CaruutoClient.prototype.tokens = {
3
- create: function (data) {
4
- this.endpoint = 'tokens'
5
-
6
- const requestPayload = {
7
- body: data,
8
- method: 'POST',
9
- uri: this._buildURL()
10
- }
11
-
12
- this._setHeader('content-type', 'application/json')
13
-
14
- return this._createRequestObject(requestPayload)
15
- }.bind(clientInstance),
16
- verify: function (token) {
17
- this.endpoint = 'tokens'
18
-
19
- const requestPayload = {
20
- body: {
21
- token,
22
- verify: true
23
- },
24
- method: 'POST',
25
- uri: this._buildURL()
26
- }
27
-
28
- this._setHeader('content-type', 'application/json')
29
-
30
- return this._createRequestObject(requestPayload)
31
- }.bind(clientInstance)
5
+ create: makeEndpointMethod('tokens', 'POST').bind(clientInstance),
6
+ verify: makeEndpointMethod('tokens', 'POST', {
7
+ transform: token => ({ token, verify: true })
8
+ }).bind(clientInstance)
32
9
  }
33
10
  }
@@ -0,0 +1,32 @@
1
+ /**
2
+ * Creates an endpoint method bound to a given HTTP method and endpoint name.
3
+ *
4
+ * @param {string} endpoint - The API endpoint path segment (e.g. 'email', 'tokens')
5
+ * @param {string} httpMethod - HTTP verb ('GET', 'POST', 'PUT', 'DELETE')
6
+ * @param {Object} [opts]
7
+ * @param {boolean} [opts.query] - If true, the argument is treated as a query filter (GET style)
8
+ * @param {Function} [opts.transform] - Transform the argument before using it as the request body
9
+ */
10
+ function makeEndpointMethod(endpoint, httpMethod, opts = {}) {
11
+ const { transform, query: isQuery = false } = opts
12
+
13
+ return function (data) {
14
+ this.endpoint = endpoint
15
+
16
+ const requestPayload = { method: httpMethod }
17
+
18
+ if (isQuery) {
19
+ this.query = data
20
+ requestPayload.uri = this._buildURL({ useParams: true })
21
+ } else {
22
+ requestPayload.body = transform ? transform(data) : data
23
+ requestPayload.uri = this._buildURL()
24
+ }
25
+
26
+ this._setHeader('content-type', 'application/json')
27
+
28
+ return this._createRequestObject(requestPayload)
29
+ }
30
+ }
31
+
32
+ module.exports = { makeEndpointMethod }
package/lib/extensions.js CHANGED
@@ -33,11 +33,15 @@ module.exports = function (CaruutoClient, clientInstance) {
33
33
  ```
34
34
  */
35
35
 
36
+ const aiExtension = require('./extensions/ai')
36
37
  const emailExtension = require('./extensions/email')
37
38
  const tokensExtension = require('./extensions/tokens')
38
39
 
39
- // Load all extensions from the lib/extensions directory/
40
40
  module.exports = function (CaruutoClient, clientInstance) {
41
+ if (typeof aiExtension === 'function') {
42
+ aiExtension(CaruutoClient, clientInstance)
43
+ }
44
+
41
45
  if (typeof emailExtension === 'function') {
42
46
  emailExtension(CaruutoClient, clientInstance)
43
47
  }
@@ -45,6 +49,4 @@ module.exports = function (CaruutoClient, clientInstance) {
45
49
  if (typeof tokensExtension === 'function') {
46
50
  tokensExtension(CaruutoClient, clientInstance)
47
51
  }
48
-
49
- // Add any other extensions here.
50
52
  }
package/lib/filters.js CHANGED
@@ -25,21 +25,6 @@ module.exports = function (CaruutoClient) {
25
25
  return this
26
26
  }
27
27
 
28
- /**
29
- * Select a bucket
30
- *
31
- * @param {String} bucket
32
- * @return API
33
- * @api public
34
- */
35
- CaruutoClient.prototype.inMedia = function (bucket) {
36
- bucket = bucket || true
37
-
38
- this.mediaBucket = bucket
39
-
40
- return this
41
- }
42
-
43
28
  /**
44
29
  * Select a document limit
45
30
  *
package/lib/helpers.js CHANGED
@@ -1,5 +1,4 @@
1
1
  const querystring = require('query-string')
2
- const url = require('node:url')
3
2
 
4
3
  module.exports = function (CaruutoClient) {
5
4
  /**
@@ -35,7 +34,6 @@ module.exports = function (CaruutoClient) {
35
34
  * @api private
36
35
  */
37
36
  CaruutoClient.prototype._buildURL = function (options) {
38
- // console.log('options :>> ', options)
39
37
  options = options || {}
40
38
 
41
39
  options.version = this.customVersion || `v${this.options.version || 0}`
@@ -43,28 +41,13 @@ module.exports = function (CaruutoClient) {
43
41
  let url = ''
44
42
 
45
43
  url += this.options.caruutoUrl
46
- // url += ':' + this.options.port
47
44
 
48
- if (this.mediaBucket) {
49
- url +=
50
- '/media' +
51
- (typeof this.mediaBucket === 'string' ? '/' + this.mediaBucket : '')
52
-
53
- if (this.terminator === 'uploadFile') {
54
- url += '/upload'
55
- }
56
- } else if (this.collection) {
57
- url += '/' + options.version + '/' + this.collection
45
+ if (this.collection) {
46
+ url += '/' + options.version + '/' + encodeURIComponent(this.collection)
58
47
  } else if (this.searchPhrase) {
59
48
  url += '/' + options.version
60
49
  } else {
61
- url +=
62
- '/' +
63
- (this.customVersion !== undefined
64
- ? this.customVersion
65
- : options.version) +
66
- '/' +
67
- this.endpoint
50
+ url += '/' + options.version + '/' + encodeURIComponent(this.endpoint)
68
51
  }
69
52
 
70
53
  if (options.signUrl) {
@@ -75,28 +58,16 @@ module.exports = function (CaruutoClient) {
75
58
  url += '/sync'
76
59
  }
77
60
 
78
- // if (options.config) {
79
- // url += '/config'
80
- // }
81
-
82
- if (options.status) {
83
- url += '/status'
84
- }
85
-
86
61
  if (options.collections) {
87
62
  url = this.options.caruutoUrl + '/api/contentTypes'
88
63
  }
89
64
 
90
- // if (options.stats) {
91
- // url += '/stats'
92
- // }
93
-
94
65
  if (this.count) {
95
66
  url += '/count'
96
67
  }
97
68
 
98
69
  if (options.id) {
99
- url += '/' + options.id
70
+ url += '/' + encodeURIComponent(options.id)
100
71
  }
101
72
 
102
73
  if (this.searchPhrase) {
@@ -122,14 +93,6 @@ module.exports = function (CaruutoClient) {
122
93
  params.page = this.page
123
94
  }
124
95
 
125
- if (this.compose !== undefined) {
126
- params.compose = this.compose
127
- }
128
-
129
- if (typeof this.history !== 'undefined') {
130
- params.includeHistory = this.history
131
- }
132
-
133
96
  if (this.sort) {
134
97
  params.sort = JSON.stringify(this.sort)
135
98
  }
@@ -158,7 +121,7 @@ module.exports = function (CaruutoClient) {
158
121
  * @api private
159
122
  */
160
123
  CaruutoClient.prototype._createRequestObject = function (options) {
161
- const parsedUri = url.parse(options.uri)
124
+ const parsedUri = new URL(options.uri)
162
125
  const requestObject = {
163
126
  ...options,
164
127
  uri: {
@@ -175,7 +138,9 @@ module.exports = function (CaruutoClient) {
175
138
  }
176
139
 
177
140
  if (typeof this.options.callback === 'function') {
178
- return this.options.callback(requestObject, this)
141
+ return Promise.resolve(
142
+ this.options.callback(requestObject, this)
143
+ ).finally(() => this._reset())
179
144
  }
180
145
 
181
146
  return requestObject
@@ -186,14 +151,18 @@ module.exports = function (CaruutoClient) {
186
151
  * @param {Object} object The object to encode
187
152
  * @return {Object} The encoded object
188
153
  */
189
- CaruutoClient.prototype._encodeObjectKeys = function (object) {
154
+ CaruutoClient.prototype._encodeObjectKeys = function (object, _depth) {
155
+ if ((_depth || 0) > 10) {
156
+ return {}
157
+ }
158
+
190
159
  return Object.keys(object).reduce((result, key) => {
191
160
  if (
192
161
  object[key] &&
193
162
  typeof object[key] === 'object' &&
194
163
  !Array.isArray(object[key])
195
164
  ) {
196
- result[key] = this._encodeObjectKeys(object[key])
165
+ result[key] = this._encodeObjectKeys(object[key], (_depth || 0) + 1)
197
166
  } else {
198
167
  result[key] =
199
168
  typeof object[key] === 'string'
@@ -205,19 +174,6 @@ module.exports = function (CaruutoClient) {
205
174
  }, {})
206
175
  }
207
176
 
208
- /**
209
- * Logs a message
210
- *
211
- * @param {String} message
212
- * @return undefined
213
- * @api private
214
- */
215
- CaruutoClient.prototype._log = function (message) {
216
- if (console && console.log) {
217
- console.log(`[Caruuto JS] ${message}`)
218
- }
219
- }
220
-
221
177
  /**
222
178
  * Clear any saved options and parameters
223
179
  *
@@ -225,9 +181,20 @@ module.exports = function (CaruutoClient) {
225
181
  * @api private
226
182
  */
227
183
  CaruutoClient.prototype._reset = function () {
228
- this.params = {}
184
+ this.collection = undefined
185
+ this.count = undefined
229
186
  this.customVersion = undefined
187
+ this.endpoint = undefined
188
+ this.fields = undefined
189
+ this.headers = undefined
190
+ this.limit = undefined
191
+ this.page = undefined
192
+ this.params = {}
230
193
  this.property = undefined
194
+ this.query = undefined
195
+ this.searchPhrase = undefined
196
+ this.sort = undefined
197
+ this.terminator = undefined
231
198
  }
232
199
 
233
200
  /**
@@ -1,93 +1,61 @@
1
- // const crossFetch = require('cross-fetch')
2
1
  const debug = require('debug')('caruuto-js')
3
2
  const undici = require('undici')
3
+ const { API_KEY_PREFIX } = require('./constants')
4
4
 
5
- const resolveFetch = customFetch => {
6
- let _fetch
5
+ function buildRequestOptions(client, requestObject) {
6
+ const headers = Object.assign({}, requestObject.headers)
7
7
 
8
- if (customFetch) {
9
- _fetch = customFetch
10
- // } else if (typeof fetch === 'undefined') {
11
- // _fetch = crossFetch
12
- } else {
13
- _fetch = undici.fetch
8
+ if (client.options.caruutoApiKey) {
9
+ headers.authorization = `${API_KEY_PREFIX} ${client.options.caruutoApiKey}`
14
10
  }
15
11
 
16
- return (...args) => _fetch(...args)
12
+ if (
13
+ headers['content-type'] !== 'application/json' &&
14
+ client.terminator !== 'uploadFile'
15
+ ) {
16
+ headers['content-type'] = 'application/json'
17
+ }
18
+
19
+ const options = { headers, method: requestObject.method }
20
+
21
+ if (
22
+ requestObject.body &&
23
+ requestObject.method !== 'GET' &&
24
+ requestObject.method !== 'HEAD'
25
+ ) {
26
+ if (client.terminator === 'uploadFile') {
27
+ options.body = requestObject.body
28
+ } else if (typeof requestObject.body === 'object') {
29
+ options.body = JSON.stringify(requestObject.body)
30
+ } else {
31
+ options.body = requestObject.body
32
+ }
33
+ }
34
+
35
+ return options
17
36
  }
18
37
 
19
38
  module.exports = function (CaruutoClient) {
20
39
  /**
21
- * Makes a request to API and handles possible failures
40
+ * Makes a request to the API and handles possible failures.
22
41
  *
23
- * @param {Object} options - request options
24
- * @return Results
42
+ * @param {Object} requestObject
43
+ * @return Promise
25
44
  * @api private
26
45
  */
27
46
  CaruutoClient.prototype._processRequest = async function (requestObject) {
28
- const fetch = resolveFetch(this.options.fetch)
47
+ const fetch = this.options.fetch || undici.fetch
48
+ const options = buildRequestOptions(this, requestObject)
49
+ const uri = requestObject.uri.href
29
50
 
30
- const options = {
31
- headers: Object.assign({}, requestObject.headers),
32
- method: requestObject.method,
33
- uri: requestObject.uri.href
34
- }
51
+ debug(`Querying URI: ${requestObject.uri.path}`)
35
52
 
36
- // Add authorization header if an API key has been provided.
37
- if (this.options.caruutoApiKey) {
38
- options.headers.authorization = `Api-Key-v1 ${this.options.caruutoApiKey}`
39
- }
40
-
41
- if (requestObject.body) {
42
- // If the body is a FormData object, we need to handle it differently
43
- if (this.terminator === 'uploadFile') {
44
- options.body = requestObject.body
45
- } else if (typeof requestObject.body === 'object') {
46
- // If the body is an object, we need to stringify it
47
- options.body = JSON.stringify(requestObject.body)
48
- } else {
49
- options.body = requestObject.body
50
- }
51
- }
52
-
53
- if (
54
- options.headers['content-type'] !== 'application/json' &&
55
- this.terminator !== 'uploadFile'
56
- ) {
57
- options.headers['content-type'] = 'application/json'
58
- }
59
-
60
- // if (this.terminator === 'uploadFile') {
61
- // If we are uploading a file, we need to set the content type to multipart/form-data
62
- // options.headers['content-type'] = 'multipart/form-data'
63
- // }
64
-
65
- // If the request is a GET request, we don't need to set the body
66
- if (requestObject.method === 'GET' || requestObject.method === 'HEAD') {
67
- delete options.body
68
- }
69
-
70
- // if (
71
- // requestObject.headers &&
72
- // requestObject.headers['content-type'] &&
73
- // requestObject.headers['content-type'] !== 'application/json'
74
- // ) {
75
- // options.json = false
76
- // } else {
77
- // options.json = true
78
- // }
79
-
80
- debug(`Querying URI: ${decodeURIComponent(options.uri)}`)
81
-
82
- // Make the request using fetch
83
- const response = await fetch(options.uri, options).catch(err => {
84
- return Promise.reject(err)
85
- })
53
+ const response = await fetch(uri, options).catch(err => Promise.reject(err))
86
54
 
87
55
  if (response.status >= 400) {
88
56
  const err = new Error()
89
57
  err.status = response.status
90
- err.uri = options.uri
58
+ err.uri = requestObject.uri.path
91
59
 
92
60
  try {
93
61
  const responseData = await response.json()
@@ -99,12 +67,15 @@ module.exports = function (CaruutoClient) {
99
67
  if (responseData.errors) {
100
68
  err.errors = responseData.errors
101
69
  }
102
- } catch (error) {
103
- console.log('error :>> ', error)
104
- }
70
+ } catch (_) {}
71
+
105
72
  return Promise.reject(err)
106
73
  }
107
74
 
108
- return await response.json()
75
+ if (response.status === 204) {
76
+ return undefined
77
+ }
78
+
79
+ return response.json()
109
80
  }
110
81
  }
@@ -1,10 +1,9 @@
1
- // const FormData = require('form-data')
2
1
  const undici = require('undici')
3
2
  const { FormData } = undici
4
3
 
5
4
  module.exports = function (CaruutoClient) {
6
5
  /**
7
- * Create one/multiple documents or hooks
6
+ * Create one/multiple documents
8
7
  *
9
8
  * @param {Object} data
10
9
  * @return Promise
@@ -20,16 +19,10 @@ module.exports = function (CaruutoClient) {
20
19
 
21
20
  this._setHeader('content-type', 'application/json')
22
21
 
23
- // if (this._isValidHook()) {
24
- // requestPayload.body = data
25
-
26
- // this._setHeader('content-type', 'text/plain')
27
- // } else {
28
22
  requestPayload.body =
29
23
  data instanceof Array
30
24
  ? data.map(this._stripReservedProperties.bind(this))
31
25
  : this._stripReservedProperties(data)
32
- // }
33
26
 
34
27
  return this._createRequestObject(requestPayload)
35
28
  }
@@ -48,14 +41,6 @@ module.exports = function (CaruutoClient) {
48
41
  uri: this._buildURL()
49
42
  }
50
43
 
51
- // if (!this._isValidHook()) {
52
- // if (this.isClient) {
53
- // if (!this.isClient.id && !this.isClient.self) {
54
- // throw new Error(
55
- // 'Unable run delete on all clients. Please use the whereClientIs() filter.'
56
- // )
57
- // }
58
- // } else {
59
44
  if (this.query === undefined) {
60
45
  throw new Error('Unable to find query for delete')
61
46
  }
@@ -63,8 +48,6 @@ module.exports = function (CaruutoClient) {
63
48
  requestPayload.body = {
64
49
  query: this.query
65
50
  }
66
- // }
67
- // }
68
51
 
69
52
  return this._createRequestObject(requestPayload)
70
53
  }
@@ -87,8 +70,6 @@ module.exports = function (CaruutoClient) {
87
70
  uri: this._buildURL({
88
71
  useParams: true
89
72
  })
90
- }).then(response => {
91
- return response
92
73
  })
93
74
  }
94
75
 
@@ -107,36 +88,6 @@ module.exports = function (CaruutoClient) {
107
88
  })
108
89
  }
109
90
 
110
- /**
111
- * Get the config for a collection if one is specified, or for main API if not
112
- *
113
- * @return Promise
114
- * @api public
115
- */
116
- CaruutoClient.prototype.getConfig = function () {
117
- this.terminator = 'getConfig'
118
-
119
- return this._createRequestObject({
120
- method: 'GET',
121
- uri: this._buildURL({ config: true })
122
- })
123
- }
124
-
125
- /**
126
- * Get the languages supported by the API
127
- *
128
- * @return Promise
129
- * @api public
130
- */
131
- // CaruutoClient.prototype.getLanguages = function () {
132
- // this.terminator = 'getLanguages'
133
-
134
- // return this._createRequestObject({
135
- // method: 'GET',
136
- // uri: this._buildURL({ languages: true })
137
- // })
138
- // }
139
-
140
91
  /**
141
92
  * Get a signed URL for media upload
142
93
  *
@@ -153,21 +104,6 @@ module.exports = function (CaruutoClient) {
153
104
  })
154
105
  }
155
106
 
156
- /**
157
- * Get the status of the API
158
- *
159
- * @return Promise
160
- * @api public
161
- */
162
- CaruutoClient.prototype.getStatus = function () {
163
- this.terminator = 'getStatus'
164
-
165
- return this._createRequestObject({
166
- method: 'POST',
167
- uri: this._buildURL({ status: true })
168
- })
169
- }
170
-
171
107
  /**
172
108
  * Upload a file to the media bucket.
173
109
  *
@@ -192,13 +128,11 @@ module.exports = function (CaruutoClient) {
192
128
 
193
129
  const requestPayload = {
194
130
  method: 'POST',
195
- uri: this._buildURL()
131
+ uri: this.options.caruutoUrl + '/api/media/upload'
196
132
  }
197
133
 
198
134
  const formData = new FormData()
199
135
 
200
- console.log(directory, fileName, mimeType, buffer, contentLength)
201
-
202
136
  try {
203
137
  formData.append(
204
138
  'file',
@@ -233,25 +167,6 @@ module.exports = function (CaruutoClient) {
233
167
 
234
168
  this._setHeader('content-type', 'application/json')
235
169
 
236
- // if (this._isValidHook()) {
237
- // requestPayload.body = update
238
-
239
- // this._setHeader('content-type', 'text/plain')
240
- // } else {
241
- // if (this.isClient) {
242
- // if (!this.isClient.id && !this.isClient.self) {
243
- // throw new Error(
244
- // 'Unable to run update on all clients. Please use whereClientIs() or whereClientIsSelf() filters.'
245
- // )
246
- // }
247
-
248
- // // Remove `clientId` from the payload.
249
- // if (update.clientId) {
250
- // delete update.clientId
251
- // }
252
-
253
- // requestPayload.body = update
254
- // } else {
255
170
  if (this.query === undefined) {
256
171
  throw new Error('Unable to find query for update')
257
172
  }
@@ -260,8 +175,6 @@ module.exports = function (CaruutoClient) {
260
175
  query: this.query,
261
176
  update: this._stripReservedProperties(update)
262
177
  }
263
- // }
264
- // }
265
178
 
266
179
  return this._createRequestObject(requestPayload)
267
180
  }
@@ -302,44 +215,13 @@ module.exports = function (CaruutoClient) {
302
215
  return this._createRequestObject(requestPayload)
303
216
  }
304
217
 
305
- /*
306
- * Create a new token.
307
- * @param {Object} data
308
- * @api public
309
- */
218
+ // Requires createClient() — delegates to the tokens extension.
310
219
  CaruutoClient.prototype.createToken = function (data) {
311
- this.endpoint = 'tokens'
312
-
313
- const requestPayload = {
314
- body: data,
315
- method: 'POST',
316
- uri: this._buildURL()
317
- }
318
-
319
- this._setHeader('content-type', 'application/json')
320
-
321
- return this._createRequestObject(requestPayload)
220
+ return this.tokens.create(data)
322
221
  }
323
222
 
324
- /*
325
- * Verify a token.
326
- * @param {Object} data
327
- * @api public
328
- */
223
+ // Requires createClient() — delegates to the tokens extension.
329
224
  CaruutoClient.prototype.verifyToken = function (token) {
330
- this.endpoint = 'tokens'
331
-
332
- const requestPayload = {
333
- body: {
334
- token,
335
- verify: true
336
- },
337
- method: 'POST',
338
- uri: this._buildURL()
339
- }
340
-
341
- this._setHeader('content-type', 'application/json')
342
-
343
- return this._createRequestObject(requestPayload)
225
+ return this.tokens.verify(token)
344
226
  }
345
227
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@caruuto/caruuto-js",
3
- "version": "0.4.2",
3
+ "version": "0.6.0",
4
4
  "description": "A high-level library for interacting with Caruuto",
5
5
  "exports": "./index.js",
6
6
  "scripts": {
@@ -10,24 +10,20 @@
10
10
  "author": "Jim Lambie <jim@27.works>",
11
11
  "license": "GPL",
12
12
  "dependencies": {
13
- "cross-fetch": "^4.0.0",
14
13
  "debug": "^2.6.1",
15
14
  "encoding": "^0.1.13",
16
- "got": "^11.0.0",
17
- "human-interval": "^2.0.1",
18
- "mocha": "^10.2.0",
19
15
  "query-string": "5.0.1",
20
- "undici": "^5.29.0"
16
+ "undici": "6.21.3"
21
17
  },
22
18
  "repository": {
23
19
  "type": "git",
24
- "url": "https://github.com/Caruuto/caruuto-js.git"
20
+ "url": "git+https://github.com/Caruuto/caruuto-js.git"
25
21
  },
26
22
  "bugs": {
27
23
  "url": "https://github.com/Caruuto/caruuto-js/issues"
28
24
  },
29
25
  "engines": {
30
- "node": ">=12"
26
+ "node": ">=18"
31
27
  },
32
28
  "husky": {
33
29
  "hooks": {
@@ -42,30 +38,16 @@
42
38
  },
43
39
  "homepage": "https://github.com/Caruuto/caruuto-js",
44
40
  "devDependencies": {
45
- "chai": "^5.2.0",
46
41
  "coveralls": "^3.0.2",
47
42
  "eslint": "8.5.0",
48
- "eslint-config-next": "13.0.5",
49
43
  "eslint-config-prettier": "^8.6.0",
50
44
  "eslint-plugin-prettier": "^4.2.1",
51
- "eslint-plugin-react": "^7.32.2",
52
- "form-data": "^4.0.2",
53
45
  "husky": "^8.0.0",
54
46
  "lint-staged": "^12.5.0",
55
- "mockery": "^1.7.0",
56
- "nock": "^12.0.0",
47
+ "mocha": "^10.2.0",
57
48
  "nyc": "^15.0.1",
58
49
  "prettier": "2.8.4",
59
- "prettier-standard": "^15.0.1",
60
50
  "should": "^9.0.2",
61
- "sinon": "^4.5.0",
62
- "standard": "^16.0.3",
63
- "supertest": "^4.0.0",
64
- "underscore": "^1.8.3"
65
- },
66
- "standard.options": {
67
- "ignore": [],
68
- "plugins": [],
69
- "envs": []
51
+ "sinon": "^4.5.0"
70
52
  }
71
- }
53
+ }
package/.editorconfig DELETED
@@ -1,9 +0,0 @@
1
- root = true
2
-
3
- [*]
4
- charset = utf-8
5
- end_of_line = lf
6
- indent_size = 2
7
- indent_style = space
8
- insert_final_newline = true
9
- trim_trailing_whitespace = true
package/.eslintignore DELETED
@@ -1 +0,0 @@
1
- node_modules
package/.eslintrc.js DELETED
@@ -1,19 +0,0 @@
1
- module.exports = {
2
- env: {
3
- es2021: true,
4
- mocha: true,
5
- node: true
6
- },
7
- extends: ['eslint:recommended', 'plugin:prettier/recommended'],
8
- overrides: [],
9
- parserOptions: {
10
- ecmaVersion: 12,
11
- sourceType: 'module'
12
- },
13
- plugins: [],
14
- rules: {
15
- 'no-redeclare': 0,
16
- 'prettier/prettier': 0,
17
- quotes: ['error', 'single']
18
- }
19
- }
package/.prettierignore DELETED
@@ -1 +0,0 @@
1
- node_modules
@@ -1,13 +0,0 @@
1
- #! /usr/bin/env node
2
-
3
- const exec = require('child_process').exec
4
-
5
- if (process.env.CI) {
6
- exec(
7
- 'cat ./coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js',
8
- (err, out) => {
9
- if (err) console.log(err)
10
- console.log(out)
11
- }
12
- )
13
- }
package/x.js DELETED
@@ -1,53 +0,0 @@
1
- const { createClient } = require('./index')
2
-
3
- // https://caruuto.27.works/api
4
- // EYeRSSEtmcVF9OFhVHjcf1TcPLHbTpPgBbHVHIce1buYKHOCNB1A2NC4UoThmQhk
5
-
6
- ;(async () => {
7
- const x = createClient(
8
- 'https://caruuto.27.works/api',
9
- 'EYeRSSEtmcVF9OFhVHjcf1TcPLHbTpPgBbHVHIce1buYKHOCNB1A2NC4UoThmQhk',
10
- { reservedProperties: ['created_at'] }
11
- )
12
-
13
- // const signedUrl = await x.inMedia('radical').getSignedUrl('test.jpg')
14
-
15
- // console.log(signedUrl)
16
-
17
- // const y = await x.insertDataPoint({
18
- // memberId: '0bcdd373-fbb6-4998-b0ed-57c842709acc',
19
- // optionId: '10',
20
- // questionId: 'f373b0a2-9c87-41f1-af49-f643da8f836c'
21
- // })
22
-
23
- // x.search({ phrase: 'grower' })
24
- // const y = await x.find()
25
- // console.log('y :>> ', y)
26
-
27
- // const z = await x.tokens.create({
28
- // email: 'jameslambie@gmail.com',
29
- // expiry: 7200000,
30
- // type: 'RESET'
31
- // })
32
- // const z = await x.verifyToken('q8ug8epjx5hzcg48ku')
33
- // console.log('z :>> ', z)
34
-
35
- // console.log('p :>> ', await x.create({ x: 1 }))
36
-
37
- // console.log('x :>> ', x.email)
38
- // console.log('x :>> ', x.email.queue)
39
- // const xx = await x.email.queue({
40
- // to: 'x@x.com',
41
- // from: 'z@z.com',
42
- // templateId: 'test'
43
- // })
44
-
45
- // console.log('xx :>> ', xx)
46
-
47
- x.in('retailers')
48
- console.log('x :>> ', x)
49
-
50
- const zz = await x.find({ sent: true })
51
-
52
- console.log('zz :>> ', zz)
53
- })()