@caruuto/caruuto-js 0.4.3 → 0.7.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/CHANGELOG.md CHANGED
@@ -5,6 +5,16 @@ All notable changes to this project will be documented in this file.
5
5
  The format is based on [Keep a Changelog](http://keepachangelog.com/)
6
6
  and this project adheres to [Semantic Versioning](http://semver.org/).
7
7
 
8
+ ## 0.7.0 - 2026-06-08
9
+
10
+ ### Added
11
+
12
+ - `ai.query()`, `ai.stream()`, and `ai.context()` now accept `utmSource`,
13
+ `utmMedium`, `utmCampaign`, `utmTerm`, and `utmContent` options, sent through
14
+ as `utm_source` / `utm_medium` / `utm_campaign` / `utm_term` / `utm_content`
15
+ alongside the existing `referrerUrl`. Lets callers forward acquisition data
16
+ from the page URL so Caruuto can attribute AI sessions to a referral channel.
17
+
8
18
  ## 0.3.6 - 2024-09-06
9
19
 
10
20
  ### Added
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,422 @@
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.utmSource] - UTM source param from the page URL
17
+ * @param {string} [opts.utmMedium] - UTM medium param from the page URL
18
+ * @param {string} [opts.utmCampaign] - UTM campaign param from the page URL
19
+ * @param {string} [opts.utmTerm] - UTM term param from the page URL
20
+ * @param {string} [opts.utmContent] - UTM content param from the page URL
21
+ * @param {string} [opts.leadEmail] - Capture a lead email on this turn
22
+ * @param {string} [opts.leadName] - Lead name (paired with leadEmail)
23
+ * @param {string} [opts.type] - Restrict retrieval to a single knowledge type
24
+ * @param {number} [opts.maxItems] - Max knowledge items to retrieve (1–10)
25
+ * @returns {Promise<{ answer, conversation_id, context, entities, _meta, from_cache? }>}
26
+ */
27
+ query: async function (opts = {}) {
28
+ const body = buildRequestBody(opts, false)
29
+ const response = await doFetch(
30
+ this,
31
+ `${this.options.caruutoUrl}/api/ai/external`,
32
+ body
33
+ )
34
+ return response.status === 204 ? undefined : response.json()
35
+ }.bind(clientInstance),
36
+
37
+ /**
38
+ * Send a message and receive a streaming response.
39
+ * Returns an async generator. Iterate with `for await`.
40
+ *
41
+ * Yields:
42
+ * { type: 'text', text: string }
43
+ * { type: 'done', conversation_id: string, context: [], entities: [], _meta: {}, from_cache?: boolean }
44
+ *
45
+ * @param {Object} opts - Same options as query()
46
+ * @returns {AsyncGenerator}
47
+ */
48
+ stream: async function* (opts = {}) {
49
+ const body = buildRequestBody(opts, true)
50
+ const response = await doFetch(
51
+ this,
52
+ `${this.options.caruutoUrl}/api/ai/external`,
53
+ body
54
+ )
55
+
56
+ const reader = response.body.getReader()
57
+ const decoder = new TextDecoder()
58
+ let buffer = ''
59
+
60
+ try {
61
+ while (true) {
62
+ const { done, value } = await reader.read()
63
+ if (done) {
64
+ break
65
+ }
66
+
67
+ buffer += decoder.decode(value, { stream: true })
68
+ const lines = buffer.split('\n')
69
+ buffer = lines.pop()
70
+
71
+ for (const line of lines) {
72
+ if (!line.startsWith('data: ')) {
73
+ continue
74
+ }
75
+
76
+ const raw = line.slice(6).trim()
77
+ if (!raw) {
78
+ continue
79
+ }
80
+
81
+ let parsed
82
+ try {
83
+ parsed = JSON.parse(raw)
84
+ } catch (_) {
85
+ continue
86
+ }
87
+
88
+ if (parsed.error) {
89
+ throw new Error(parsed.error)
90
+ } else if (parsed.done) {
91
+ yield { type: 'done', ...parsed }
92
+ } else if (parsed.text !== undefined) {
93
+ yield { type: 'text', text: parsed.text }
94
+ }
95
+ }
96
+ }
97
+ } finally {
98
+ reader.releaseLock()
99
+ }
100
+ }.bind(clientInstance),
101
+
102
+ /**
103
+ * Explicitly end a conversation, marking it as 'ended'.
104
+ * Call this when the user closes the chat widget or navigates away.
105
+ *
106
+ * @param {Object} opts
107
+ * @param {string} opts.conversationId - The conversation to end
108
+ * @param {string} opts.projectId - The project the conversation belongs to
109
+ * @returns {Promise<{ id, status, turn_count, total_input_tokens, total_output_tokens, total_tokens, started_at, ended_at }>}
110
+ */
111
+ endConversation: async function ({ conversationId, projectId } = {}) {
112
+ if (!conversationId) {
113
+ throw new Error('conversationId is required')
114
+ }
115
+
116
+ if (!projectId) {
117
+ throw new Error('projectId is required')
118
+ }
119
+
120
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}/end`
121
+ const response = await doFetch(this, url, { project_id: projectId })
122
+ return response.json()
123
+ }.bind(clientInstance),
124
+
125
+ /**
126
+ * Retrieve assembled context for a message without calling the LLM.
127
+ * Use this when you want to call your own LLM (e.g. with AI SDK + tools)
128
+ * and need Caruuto to handle retrieval, entity detection, and prompt assembly.
129
+ *
130
+ * On a cache hit, returns { from_cache: true, cached_answer, conversation_id, _meta }.
131
+ * On a miss, returns { from_cache: false, system_prompt, history, context, entities,
132
+ * conversation_id, _meta }.
133
+ *
134
+ * @param {Object} opts - Same options as query(), minus stream/lead fields
135
+ * @returns {Promise<Object>}
136
+ */
137
+ context: async function (opts = {}) {
138
+ const {
139
+ projectId,
140
+ message,
141
+ conversationId,
142
+ clientSessionId,
143
+ source,
144
+ referrerUrl,
145
+ utmSource,
146
+ utmMedium,
147
+ utmCampaign,
148
+ utmTerm,
149
+ utmContent,
150
+ type,
151
+ maxItems
152
+ } = opts
153
+
154
+ const body = { project_id: projectId, message }
155
+
156
+ if (conversationId) body.conversation_id = conversationId
157
+ if (clientSessionId) body.client_session_id = clientSessionId
158
+ if (source) body.source = source
159
+ if (referrerUrl) body.referrer_url = referrerUrl
160
+ if (utmSource) body.utm_source = utmSource
161
+ if (utmMedium) body.utm_medium = utmMedium
162
+ if (utmCampaign) body.utm_campaign = utmCampaign
163
+ if (utmTerm) body.utm_term = utmTerm
164
+ if (utmContent) body.utm_content = utmContent
165
+ if (type) body.type = type
166
+ if (maxItems !== undefined) body.max_items = maxItems
167
+
168
+ const response = await doFetch(
169
+ this,
170
+ `${this.options.caruutoUrl}/api/ai/context`,
171
+ body
172
+ )
173
+ return response.json()
174
+ }.bind(clientInstance),
175
+
176
+ /**
177
+ * Persist a completed turn (user message + assistant response) to Caruuto.
178
+ * Call this after your LLM stream completes with the full response text and
179
+ * token usage. Caruuto writes to ai_conversation_messages, updates token
180
+ * totals on the conversation, and caches the answer if it's the first turn.
181
+ *
182
+ * @param {Object} opts
183
+ * @param {string} opts.conversationId - From context() response
184
+ * @param {string} opts.projectId
185
+ * @param {string} opts.userContent - The user's message text
186
+ * @param {string} opts.assistantContent - The full assistant response
187
+ * @param {string} [opts.modelId] - Model used (e.g. 'gpt-4o-mini')
188
+ * @param {number} [opts.inputTokens]
189
+ * @param {number} [opts.outputTokens]
190
+ * @param {Object[]} [opts.contextItems] - From context() response
191
+ * @param {string[]} [opts.entitiesReferenced] - Entity IDs from context()
192
+ * @param {string} [opts.leadEmail]
193
+ * @param {string} [opts.leadName]
194
+ * @returns {Promise<{ ok: boolean, turn_count: number }>}
195
+ */
196
+ /**
197
+ * Load a conversation and its messages, formatted as AI SDK UIMessage objects.
198
+ * Use this to restore a prior conversation into the chat UI.
199
+ *
200
+ * @param {Object} opts
201
+ * @param {string} opts.conversationId - The Caruuto conversation ID
202
+ * @param {string} opts.projectId
203
+ * @returns {Promise<{ id, status, turn_count, started_at, ended_at, messages }>}
204
+ */
205
+ /**
206
+ * Set a conversation's visibility to 'public' or 'private'.
207
+ *
208
+ * @param {Object} opts
209
+ * @param {string} opts.conversationId
210
+ * @param {string} opts.projectId
211
+ * @param {'public'|'private'} opts.visibility
212
+ * @returns {Promise<{ id, visibility, share_url }>}
213
+ */
214
+ /**
215
+ * Fork a conversation or question cache entry into a new conversation.
216
+ *
217
+ * @param {Object} opts
218
+ * @param {string} opts.sourceId - Conversation ID or question cache entry ID
219
+ * @param {string} opts.projectId
220
+ * @param {'conversation'|'question_cache'} opts.source
221
+ * @returns {Promise<{ id: string }>} New conversation ID
222
+ */
223
+ forkConversation: async function ({ sourceId, projectId, source = 'conversation' } = {}) {
224
+ if (!sourceId) throw new Error('sourceId is required')
225
+ if (!projectId) throw new Error('projectId is required')
226
+
227
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${sourceId}/fork`
228
+ const response = await doFetch(this, url, { project_id: projectId, source })
229
+ return response.json()
230
+ }.bind(clientInstance),
231
+
232
+ shareConversation: async function ({ conversationId, projectId, visibility = 'public' } = {}) {
233
+ if (!conversationId) throw new Error('conversationId is required')
234
+ if (!projectId) throw new Error('projectId is required')
235
+
236
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}/share`
237
+ const response = await doFetch(this, url, { project_id: projectId, visibility })
238
+ return response.json()
239
+ }.bind(clientInstance),
240
+
241
+ /**
242
+ * Fetch a publicly shared conversation without authentication.
243
+ * Resolves with null if the conversation is private or not found.
244
+ *
245
+ * @param {Object} opts
246
+ * @param {string} opts.conversationId
247
+ * @returns {Promise<{ id, turn_count, started_at, ended_at, messages } | null>}
248
+ */
249
+ getPublicConversation: async function ({ conversationId } = {}) {
250
+ if (!conversationId) throw new Error('conversationId is required')
251
+
252
+ const fetch = this.options.fetch || undici.fetch
253
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}/public`
254
+
255
+ const response = await fetch(url, { method: 'GET' })
256
+
257
+ if (response.status === 403 || response.status === 404) {
258
+ return null
259
+ }
260
+
261
+ if (!response.ok) {
262
+ const err = new Error('Failed to fetch public conversation')
263
+ err.status = response.status
264
+ throw err
265
+ }
266
+
267
+ return response.json()
268
+ }.bind(clientInstance),
269
+
270
+ loadConversation: async function ({ conversationId, projectId } = {}) {
271
+ if (!conversationId) {
272
+ throw new Error('conversationId is required')
273
+ }
274
+
275
+ if (!projectId) {
276
+ throw new Error('projectId is required')
277
+ }
278
+
279
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}?project_id=${encodeURIComponent(projectId)}`
280
+ const response = await doGet(this, url)
281
+ return response.json()
282
+ }.bind(clientInstance),
283
+
284
+ saveTurn: async function ({
285
+ conversationId,
286
+ projectId,
287
+ userContent,
288
+ assistantContent,
289
+ modelId,
290
+ inputTokens,
291
+ outputTokens,
292
+ contextItems,
293
+ entitiesReferenced,
294
+ leadEmail,
295
+ leadName
296
+ } = {}) {
297
+ if (!conversationId) {
298
+ throw new Error('conversationId is required')
299
+ }
300
+
301
+ if (!projectId) {
302
+ throw new Error('projectId is required')
303
+ }
304
+
305
+ const body = {
306
+ project_id: projectId,
307
+ user_content: userContent,
308
+ assistant_content: assistantContent
309
+ }
310
+
311
+ if (modelId) body.model_id = modelId
312
+ if (inputTokens !== undefined) body.input_tokens = inputTokens
313
+ if (outputTokens !== undefined) body.output_tokens = outputTokens
314
+ if (contextItems) body.context_items = contextItems
315
+ if (entitiesReferenced) body.entities_referenced = entitiesReferenced
316
+ if (leadEmail) body.lead_email = leadEmail
317
+ if (leadName) body.lead_name = leadName
318
+
319
+ const url = `${this.options.caruutoUrl}/api/ai/conversations/${conversationId}/turn`
320
+ const response = await doFetch(this, url, body)
321
+ return response.json()
322
+ }.bind(clientInstance)
323
+ }
324
+ }
325
+
326
+ // ---------------------------------------------------------------------------
327
+ // Helpers (module-private)
328
+ // ---------------------------------------------------------------------------
329
+
330
+ function buildRequestBody(opts, stream) {
331
+ const {
332
+ projectId,
333
+ message,
334
+ conversationId,
335
+ clientSessionId,
336
+ source,
337
+ referrerUrl,
338
+ utmSource,
339
+ utmMedium,
340
+ utmCampaign,
341
+ utmTerm,
342
+ utmContent,
343
+ leadEmail,
344
+ leadName,
345
+ type,
346
+ maxItems
347
+ } = opts
348
+
349
+ const body = { project_id: projectId, message, stream }
350
+
351
+ if (conversationId) body.conversation_id = conversationId
352
+ if (clientSessionId) body.client_session_id = clientSessionId
353
+ if (source) body.source = source
354
+ if (referrerUrl) body.referrer_url = referrerUrl
355
+ if (utmSource) body.utm_source = utmSource
356
+ if (utmMedium) body.utm_medium = utmMedium
357
+ if (utmCampaign) body.utm_campaign = utmCampaign
358
+ if (utmTerm) body.utm_term = utmTerm
359
+ if (utmContent) body.utm_content = utmContent
360
+ if (leadEmail) body.lead_email = leadEmail
361
+ if (leadName) body.lead_name = leadName
362
+ if (type) body.type = type
363
+ if (maxItems !== undefined) body.max_items = maxItems
364
+
365
+ return body
366
+ }
367
+
368
+ async function doGet(client, url) {
369
+ const fetch = client.options.fetch || undici.fetch
370
+
371
+ const response = await fetch(url, {
372
+ method: 'GET',
373
+ headers: {
374
+ authorization: `${API_KEY_PREFIX} ${client.options.caruutoApiKey}`
375
+ }
376
+ })
377
+
378
+ if (!response.ok) {
379
+ const err = new Error('Caruuto AI request failed')
380
+ err.status = response.status
381
+
382
+ try {
383
+ const data = await response.clone().json()
384
+ if (data.error) {
385
+ err.message = data.error
386
+ }
387
+ } catch (_) {}
388
+
389
+ throw err
390
+ }
391
+
392
+ return response
393
+ }
394
+
395
+ async function doFetch(client, url, body) {
396
+ const fetch = client.options.fetch || undici.fetch
397
+
398
+ const response = await fetch(url, {
399
+ method: 'POST',
400
+ headers: {
401
+ 'content-type': 'application/json',
402
+ authorization: `${API_KEY_PREFIX} ${client.options.caruutoApiKey}`
403
+ },
404
+ body: JSON.stringify(body)
405
+ })
406
+
407
+ if (!response.ok) {
408
+ const err = new Error('Caruuto AI request failed')
409
+ err.status = response.status
410
+
411
+ try {
412
+ const data = await response.clone().json()
413
+ if (data.error) {
414
+ err.message = data.error
415
+ }
416
+ } catch (_) {}
417
+
418
+ throw err
419
+ }
420
+
421
+ return response
422
+ }
@@ -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.3",
3
+ "version": "0.7.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
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
- })()