mango-cms 0.2.31 → 0.2.40

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.
@@ -3,7 +3,7 @@ let { PlainText, Select, Timestamp, Relationship } = fields
3
3
 
4
4
  export default {
5
5
 
6
- title: PlainText({ required: true }),
6
+ // title: PlainText({ required: true }),
7
7
  author: Relationship({ collection: 'member', single: true, computed: (doc, req) => [req?.member?.id] || [] }),
8
8
  editId: { computed: (doc, req) => req?.member?.id || null },
9
9
  created: { computed: doc => doc.created || new Date, type: 'Float' },
@@ -4,6 +4,7 @@
4
4
  "siteName": "Example",
5
5
  "siteDomain": "example.com",
6
6
  "mangoDomain": "api.example.com",
7
+ "serverIp": null,
7
8
 
8
9
  "mangoThreads": 2,
9
10
  "maxPoolSize": 10,
@@ -28,5 +29,7 @@
28
29
 
29
30
  "algoliaAppId": null,
30
31
  "algoliaSearchKey": null,
31
- "algoliaIndex": null
32
+ "algoliaIndex": null,
33
+
34
+ "corsOrigins": "*"
32
35
  }
@@ -0,0 +1,31 @@
1
+ #!/bin/bash
2
+
3
+ # Get the directory of this script
4
+ SCRIPT_DIR="$( cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd )"
5
+ SETTINGS_FILE="$SCRIPT_DIR/../config/settings.json"
6
+
7
+ # Read serverIp from settings.json
8
+ SERVER_IP=$(grep -o '"serverIp"[[:space:]]*:[[:space:]]*"[^"]*"' "$SETTINGS_FILE" | sed 's/.*"\([^"]*\)".*/\1/')
9
+
10
+ # Check if serverIp is null or not set
11
+ if grep -q '"serverIp"[[:space:]]*:[[:space:]]*null' "$SETTINGS_FILE"; then
12
+ echo "Error: serverIp is set to null in settings.json"
13
+ echo "Please configure the server IP address in $SETTINGS_FILE"
14
+ exit 1
15
+ fi
16
+
17
+ if [ -z "$SERVER_IP" ]; then
18
+ echo "Error: serverIp not found in settings.json"
19
+ echo "Please add serverIp to $SETTINGS_FILE"
20
+ exit 1
21
+ fi
22
+
23
+ echo "Using server IP: $SERVER_IP"
24
+
25
+ cd ~/Downloads;
26
+ ssh root@$SERVER_IP 'rm -rf dump.zip; mongodump; zip -r dump.zip dump'
27
+ rsync root@$SERVER_IP:~/Downloads/dump.zip ~/Downloads/dump.zip;
28
+ unzip -o dump.zip;
29
+ mongorestore --drop dump;
30
+ rm -rf dump;
31
+ rm -rf dump.zip;
@@ -1,6 +1,6 @@
1
1
  const subscribe = ({ io, collection, document, request, individual, originalDocument }) => {
2
2
  let method = request.method
3
- if (collection.subscribe && method != 'read' && individual) {
3
+ if (collection?.subscribe && method != 'read' && individual) {
4
4
 
5
5
  const subscription = io.of(collection.name);
6
6
 
@@ -25,7 +25,7 @@
25
25
  "dayjs": "^1.10.7",
26
26
  "express": "^4.18.1",
27
27
  "google-maps": "^4.3.3",
28
- "mango-cms": "^0.2.31",
28
+ "mango-cms": "^0.2.40",
29
29
  "mapbox-gl": "^2.7.0",
30
30
  "sweetalert2": "^11.4.0",
31
31
  "vite": "^6.2.2",
@@ -1,553 +1,590 @@
1
1
  // import collections from '../../../mango/config/.collections.json'
2
2
  // import { algoliaAppId, algoliaSearchKey, algoliaIndex, port, domain } from '../../../mango/config/settings'
3
3
  import collections from '@collections'
4
+ import endpointsData from '@endpoints'
4
5
  import { algoliaAppId, algoliaSearchKey, algoliaIndex, port, mangoDomain, useDevAPI } from '@settings'
5
- import axios from "axios";
6
+ import axios from 'axios'
6
7
  import { ref } from 'vue'
7
8
  import algoliasearch from 'algoliasearch/dist/algoliasearch-lite.esm.browser'
8
9
  import LocalDB from './localDB'
9
- import { io } from 'socket.io-client';
10
+ import { io } from 'socket.io-client'
10
11
 
11
12
  import { useRoute } from 'vue-router'
12
13
  let route = useRoute()
13
14
 
14
- let endpoints = {
15
- authors: ['get'],
16
- // scripture: { validate: ['post'] }
17
- }
18
-
19
15
  // console.log('collections', collections)
20
16
 
21
- const client = algoliasearch(algoliaAppId, algoliaSearchKey);
22
- const algolia = client.initIndex(algoliaIndex);
17
+ const client = algoliasearch(algoliaAppId, algoliaSearchKey)
18
+ const algolia = client.initIndex(algoliaIndex)
23
19
 
24
20
  let api = `https://${mangoDomain}`
25
21
  let ws = `wss://${mangoDomain}/graphql`
26
22
 
27
23
  if (process.env.NODE_ENV != 'production' && useDevAPI) {
28
- api = `http://localhost:${port}`
29
- ws = `ws://localhost:${port}/graphql`
24
+ api = `http://localhost:${port}`
25
+ ws = `ws://localhost:${port}/graphql`
30
26
  }
31
27
 
32
28
  function getQuery(params) {
29
+ if (params.search != undefined) params.search = JSON.stringify(params.search)
30
+ if (params.fields != undefined) params.fields = JSON.stringify(params.fields)
31
+ if (params.sort != undefined) params.sort = JSON.stringify(params.sort)
33
32
 
34
- if (params.search != undefined)
35
- params.search = JSON.stringify(params.search);
36
- if (params.fields != undefined)
37
- params.fields = JSON.stringify(params.fields);
38
- if (params.sort != undefined) params.sort = JSON.stringify(params.sort);
39
-
40
- const query =
41
- Object.keys(params)
42
- .filter((key) => params[key] != undefined)
43
- ?.map(
44
- (key) =>
45
- `${encodeURIComponent(key)}=${encodeURIComponent(
46
- params[key]
47
- )}`
48
- )
49
- ?.join("&") || "";
50
-
51
- return query
33
+ const query =
34
+ Object.keys(params)
35
+ .filter((key) => params[key] != undefined)
36
+ ?.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
37
+ ?.join('&') || ''
38
+
39
+ return query
52
40
  }
53
41
 
54
42
  const Mango = collections.reduce((a, c) => {
55
-
56
- let localDB = new LocalDB(c.name, api)
57
-
58
- let runQuery = ({ limit, page, search, fields, id, sort, depthLimit, verbose } = {}) => {
59
-
60
- let fullQuery
61
-
62
- const query = getQuery({ limit, page, search, fields, sort, depthLimit, verbose })
63
-
64
- fullQuery = `${api}/${c.name}/${id || ''}?${query}`
65
-
66
- let Authorization = window.localStorage.getItem('token')
67
-
68
- return new Promise((resolve, reject) => {
69
- // If it's a local entry
70
- if (id && !isNaN(id)) {
71
- id = Number(id);
72
- let request = window.indexedDB.open(c.name, 1);
73
- request.onsuccess = (e) => {
74
- let db = e.target.result;
75
- let transaction = db.transaction([c.name], "readwrite");
76
- let store = transaction.objectStore(c.name);
77
- let entryRequest = store.get(id);
78
- entryRequest.onsuccess = (e) => {
79
- let result = entryRequest.result;
80
- if (result) resolve(result);
81
- else reject("No entry found");
82
- };
83
- };
84
- } else {
85
- axios
86
- .get(fullQuery, { headers: { Authorization } })
87
- .then((response) => resolve(verbose ? response?.data : response?.data?.response))
88
- .catch((e) => reject(e));
89
- }
90
- });
91
-
92
- }
93
-
94
- let runGraphql = (query) => {
95
-
96
- let Authorization = window.localStorage.getItem('token')
97
- query = { query }
98
- return new Promise((resolve, reject) => {
99
- axios.post(`${api}/graphql`, query, { headers: { Authorization } })
100
- .then(response => resolve(response?.data?.data))
101
- .catch(e => reject(e))
102
- })
103
-
104
- }
105
-
106
- let runAlgolia = (search, query, algoliaFilters) => {
107
-
108
- search = search || ''
109
-
110
- let filters = `collection:${c.name}`
111
- if (algoliaFilters) filters += ` AND ${algoliaFilters}`
112
-
113
- let algoliaQuery = {
114
- page: query?.page || 0,
115
- filters,
116
- hitsPerPage: query?.limit || 10
117
- }
118
-
119
- if (query?.fields) algoliaQuery.attributesToRetrieve = query.fields
120
-
121
- return new Promise((resolve, reject) => {
122
- algolia.search(search, algoliaQuery)
123
- .then(({ hits, nbHits }) => {
124
- hits.forEach(h => h.id = h.objectID)
125
- resolve({ hits, nbHits })
126
- })
127
- })
128
-
129
- }
130
-
131
- let mangoSave = (data, options = {}) => {
132
-
133
- let { id } = data
134
- let method = id ? 'put' : 'post'
135
-
136
- // // Remove _id and computed fields
137
- delete data.collection
138
- delete data._id
139
- delete data.id
140
-
141
- for (let field of c.fields) {
142
- if (field.computed) delete data[field.name]
143
- if (field.relationship) data[field.name] = Array.isArray(data[field.name]) ? data[field.name].map(r => r?.id || r) : data[field.name]?.id ? data[field.name].id : data[field.name]
144
- }
145
- for (let name in data) {
146
- if (name.includes('__')) delete data[name]
147
- }
148
-
149
- let payload = { ...data }
150
- let Authorization = window.localStorage.getItem('token')
151
- let headers = {
152
- Authorization,
153
- ...options.headers
154
- }
155
-
156
- return new Promise((resolve, reject) => {
157
- axios[method](`${api}/${c.name}/${id || ''}`, payload, { headers })
158
- .then(response => resolve(response?.data?.response))
159
- .catch(e => reject(e))
160
- })
161
- }
162
-
163
- let save = (data, options = {}) => {
164
- if (!options.bypassLocal) return localDB.save(save, data, options)
165
- else return mangoSave(data, options)
166
- }
167
-
168
- let deleteEntry = (data) => {
169
- let id = data.id || data
170
-
171
- let Authorization = window.localStorage.getItem('token')
172
-
173
- return new Promise((resolve, reject) => {
174
- axios.delete(`${api}/${c.name}/${id || ''}`, { headers: { Authorization } })
175
- .then(response => resolve(response?.data))
176
- .catch(e => reject(e))
177
- })
178
- }
179
-
180
-
181
- let sync = () => {
182
-
183
- let remainingEntries = ref([])
184
- let syncedEntries = ref([])
185
- let online = ref(navigator.onLine)
186
- let syncing = ref(false)
187
-
188
- setInterval(async () => {
189
-
190
- // console.log('syncing', syncing.value, route?.params?.id)
191
- online.value = navigator.onLine
192
-
193
- if (syncing.value) return
194
-
195
- syncing.value = true
196
-
197
- let entries = await localDB.getEntries()
198
- remainingEntries.value = entries?.filter(e => (new Date() - new Date(e.updatedLocally)) > 30*1000)
199
-
200
- for (let [index, entry] of remainingEntries.value.entries()) {
201
-
202
- // Don't sync what we're actively working on
203
- if (route?.params?.id == entry.id || (!isNaN(entry.id) && window.location.pathname.includes(`/${entry.id}`))) {
204
- console.log('skipping', entry.id)
205
- continue
206
- }
207
-
208
- try {
209
- let response = await save(entry, {syncing: true})
210
- if (response?.id) remainingEntries.value.splice(index, 1)
211
- } catch(e) {
212
- console.log('Error saving entry', e, entry)
213
- }
214
-
215
- await new Promise(resolve => setTimeout(resolve, 10*1000))
216
-
217
- }
218
-
219
- syncing.value = false
220
- syncedEntries.value = []
221
-
222
- }, 500)
223
-
224
-
225
- return { remainingEntries, syncedEntries, online, syncing }
226
-
227
- }
228
-
229
- let subscribe = ({target, triggers, room} = {}) => {
230
-
231
- let socket = io(`${api}/${c.name}`, { transports: ['websocket'] })
232
- let userId = window.localStorage.getItem('token').split(':')[1]
233
-
234
- room = room || userId
235
- socket.emit('subscribeToThread', room)
236
-
237
- triggers = triggers || {}
238
- let defaultTriggers = {
239
- created: (data) => {
240
- if (Array.isArray(target)) target.push(data)
241
- else Object.assign(target, data)
242
- },
243
- updated: (data) => {
244
- if (Array.isArray(target)) {
245
- const index = target.findIndex(t => t.id == data.id)
246
- if (index !== -1) {
247
- Object.assign(target[index], data)
248
- }
249
- }
250
- else if (target.id == data.id) Object.assign(target, data)
251
- },
252
- deleted: (data) => {
253
- if (Array.isArray(target)) {
254
- const index = target.findIndex(t => t.id == data.id)
255
- if (index !== -1) {
256
- target.splice(index, 1) // Mutates the existing array
257
- }
258
- }
259
- else if (target.id == data.id) {
260
- // Clear the object properties while maintaining reactivity
261
- Object.keys(target).forEach(key => delete target[key])
262
- }
263
- }
264
- }
265
-
266
- let combinedTriggers = { ...defaultTriggers, ...triggers }
267
-
268
- for (let trigger of Object.keys(combinedTriggers)) {
269
- socket.on(`${c.name}:${trigger}`, combinedTriggers[trigger])
270
- }
271
-
272
- }
273
-
274
- a[c.name] = runQuery
275
- a[c.name]['save'] = save
276
- a[c.name]['delete'] = deleteEntry
277
- a[c.name]['subscribe'] = subscribe
278
- a[c.singular] = (id, query) => runQuery({ id, ...query })
279
- a[c.singular]['subscribe'] = (id, callback, message) => {
280
- if (!id) return console.error('No id provided')
281
- return subscribe(id, message, callback)
282
- }
283
-
284
- a[c.name]['local'] = localDB.getEntries
285
- a[c.singular]['local'] = localDB.get
286
- a[c.singular]['local']['delete'] = localDB.delete
287
- a[c.name]['sync'] = sync
288
-
289
- a[c.name]['search'] = runAlgolia
290
- a[c.name]['search']['init'] = (search, query, algoliaFilters) => {
291
- let loading = ref(true)
292
- let data = ref(null)
293
- let error = ref(null)
294
- let totalResults = ref(null)
295
-
296
- let response = runAlgolia(search, query, algoliaFilters)
297
- .then(response => {
298
- data.value = response.hits
299
- totalResults.value = response.nbHits
300
- loading.value = false
301
- })
302
- .catch(e => {
303
- loading.value = false
304
- error.value = e
305
- })
306
-
307
- return { data, loading, error }
308
- }
309
-
310
- a[c.name]['init'] = ({ limit, page, search, fields, id, sort } = {}) => {
311
-
312
- let loading = ref(true)
313
- let data = ref(null)
314
- let error = ref(null)
315
-
316
- let response = runQuery({ limit, page, search, fields, id, sort })
317
- .then(response => {
318
- data.value = response
319
- loading.value = false
320
- })
321
- .catch(e => {
322
- loading.value = false
323
- error.value = e
324
- })
325
-
326
- return { data, loading, error }
327
-
328
- }
329
-
330
- a[c.singular]['init'] = (id) => a[c.name]['init']({ id })
331
-
332
- a.relationRequest = ({ limit, page, search, fields, id, sort, depthLimit, path } = {}) => {
333
-
334
- let fullQuery
335
-
336
-
337
- const params = { limit, page, search, fields, sort, depthLimit }
338
-
339
- if (params.search != undefined) params.search = JSON.stringify(params.search)
340
- if (params.fields != undefined) params.fields = JSON.stringify(params.fields)
341
- if (params.sort != undefined) params.sort = JSON.stringify(params.sort)
342
-
343
- const query = Object.keys(params)
344
- .filter(key => params[key] != undefined)
345
- ?.map(key => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
346
- ?.join('&') || ''
347
- // console.log(query)
348
-
349
- fullQuery = `${api}/${path}?${query}`
350
-
351
- let Authorization = window.localStorage.getItem('token')
352
-
353
- return new Promise((resolve, reject) => {
354
- axios.get(fullQuery, { headers: { Authorization } })
355
- .then(response => resolve(response?.data?.response))
356
- .catch(e => reject(e))
357
- })
358
-
359
- }
360
- a.relationRequest.init = (query) => {
361
-
362
- let loading = ref(true)
363
- let data = ref(null)
364
- let error = ref(null)
365
-
366
- let response = a.relationRequest(query)
367
- .then(response => {
368
- data.value = response
369
- loading.value = false
370
- })
371
- .catch(e => {
372
- loading.value = false
373
- error.value = e
374
- })
375
-
376
- return { data, loading, error }
377
-
378
- }
379
-
380
-
381
- a.graphql = runGraphql
382
- a.graphql.init = (query) => {
383
-
384
- let loading = ref(true)
385
- let data = ref(null)
386
- let error = ref(null)
387
-
388
- let response = runGraphql(query)
389
- .then(response => {
390
- data.value = response
391
- loading.value = false
392
- })
393
- .catch(e => {
394
- loading.value = false
395
- error.value = e
396
- })
397
-
398
- return { data, loading, error }
399
-
400
- }
401
-
402
- return a
403
-
43
+ let localDB = new LocalDB(c.name, api)
44
+
45
+ let runQuery = ({ limit, page, search, fields, id, sort, depthLimit, verbose } = {}) => {
46
+ let fullQuery
47
+
48
+ const query = getQuery({ limit, page, search, fields, sort, depthLimit, verbose })
49
+
50
+ fullQuery = `${api}/${c.name}/${id || ''}?${query}`
51
+
52
+ let Authorization = window.localStorage.getItem('token')
53
+
54
+ return new Promise((resolve, reject) => {
55
+ // If it's a local entry
56
+ if (id && !isNaN(id)) {
57
+ id = Number(id)
58
+ let request = window.indexedDB.open(c.name, 1)
59
+ request.onsuccess = (e) => {
60
+ let db = e.target.result
61
+ let transaction = db.transaction([c.name], 'readwrite')
62
+ let store = transaction.objectStore(c.name)
63
+ let entryRequest = store.get(id)
64
+ entryRequest.onsuccess = (e) => {
65
+ let result = entryRequest.result
66
+ if (result) resolve(result)
67
+ else reject('No entry found')
68
+ }
69
+ }
70
+ } else {
71
+ axios
72
+ .get(fullQuery, { headers: { Authorization } })
73
+ .then((response) => resolve(verbose ? response?.data : response?.data?.response))
74
+ .catch((e) => reject(e))
75
+ }
76
+ })
77
+ }
78
+
79
+ let runGraphql = (query) => {
80
+ let Authorization = window.localStorage.getItem('token')
81
+ query = { query }
82
+ return new Promise((resolve, reject) => {
83
+ axios
84
+ .post(`${api}/graphql`, query, { headers: { Authorization } })
85
+ .then((response) => resolve(response?.data?.data))
86
+ .catch((e) => reject(e))
87
+ })
88
+ }
89
+
90
+ let runAlgolia = (search, query, algoliaFilters) => {
91
+ search = search || ''
92
+
93
+ let filters = `collection:${c.name}`
94
+ if (algoliaFilters) filters += ` AND ${algoliaFilters}`
95
+
96
+ let algoliaQuery = {
97
+ page: query?.page || 0,
98
+ filters,
99
+ hitsPerPage: query?.limit || 10,
100
+ }
101
+
102
+ if (query?.fields) algoliaQuery.attributesToRetrieve = query.fields
103
+
104
+ return new Promise((resolve, reject) => {
105
+ algolia.search(search, algoliaQuery).then(({ hits, nbHits }) => {
106
+ hits.forEach((h) => (h.id = h.objectID))
107
+ resolve({ hits, nbHits })
108
+ })
109
+ })
110
+ }
111
+
112
+ let runNativeSearch = (search, query = {}) => {
113
+ search = search || ''
114
+
115
+ if (search) {
116
+ query.search = {
117
+ ...(query.search || {}),
118
+ $or: [
119
+ { title: { $regex: search, $options: 'i' } },
120
+ { name: { $regex: search, $options: 'i' } },
121
+ { content: { $regex: search, $options: 'i' } },
122
+ { summary: { $regex: search, $options: 'i' } },
123
+ { description: { $regex: search, $options: 'i' } },
124
+ { body: { $regex: search, $options: 'i' } },
125
+ ],
126
+ }
127
+ }
128
+
129
+ query.verbose = true
130
+
131
+ return new Promise((resolve, reject) => {
132
+ runQuery(query).then((response) => {
133
+ let res = { hits: response?.response, nbHits: response?.count }
134
+ console.log('res', res)
135
+ resolve(res)
136
+ })
137
+ })
138
+ }
139
+
140
+ let search = (search, query, algoliaFilters) => {
141
+ if (algoliaAppId) return runAlgolia(search, query, algoliaFilters)
142
+ return runNativeSearch(search, query)
143
+ }
144
+
145
+ let mangoSave = (data, options = {}) => {
146
+ let { id } = data
147
+ let method = id ? 'put' : 'post'
148
+
149
+ // // Remove _id and computed fields
150
+ delete data.collection
151
+ delete data._id
152
+ delete data.id
153
+
154
+ for (let field of c.fields) {
155
+ if (field.computed) delete data[field.name]
156
+ if (field.relationship)
157
+ data[field.name] = Array.isArray(data[field.name]) ? data[field.name].map((r) => r?.id || r) : data[field.name]?.id ? data[field.name].id : data[field.name]
158
+ }
159
+ for (let name in data) {
160
+ if (name.includes('__')) delete data[name]
161
+ }
162
+
163
+ let payload = { ...data }
164
+ let Authorization = window.localStorage.getItem('token')
165
+ let headers = {
166
+ Authorization,
167
+ ...options.headers,
168
+ }
169
+
170
+ return new Promise((resolve, reject) => {
171
+ axios[method](`${api}/${c.name}/${id || ''}`, payload, { headers })
172
+ .then((response) => resolve(response?.data?.response))
173
+ .catch((e) => reject(e))
174
+ })
175
+ }
176
+
177
+ let save = (data, options = { bypassLocal: true }) => {
178
+ if (!options.bypassLocal) return localDB.save(save, data, options)
179
+ else return mangoSave(data, options)
180
+ }
181
+
182
+ let deleteEntry = (data) => {
183
+ let id = data.id || data
184
+
185
+ let Authorization = window.localStorage.getItem('token')
186
+
187
+ return new Promise((resolve, reject) => {
188
+ axios
189
+ .delete(`${api}/${c.name}/${id || ''}`, { headers: { Authorization } })
190
+ .then((response) => resolve(response?.data))
191
+ .catch((e) => reject(e))
192
+ })
193
+ }
194
+
195
+ let sync = () => {
196
+ let remainingEntries = ref([])
197
+ let syncedEntries = ref([])
198
+ let online = ref(navigator.onLine)
199
+ let syncing = ref(false)
200
+
201
+ setInterval(async () => {
202
+ // console.log('syncing', syncing.value, route?.params?.id)
203
+ online.value = navigator.onLine
204
+
205
+ if (syncing.value) return
206
+
207
+ syncing.value = true
208
+
209
+ let entries = await localDB.getEntries()
210
+ remainingEntries.value = entries?.filter((e) => new Date() - new Date(e.updatedLocally) > 30 * 1000)
211
+
212
+ for (let [index, entry] of remainingEntries.value.entries()) {
213
+ // Don't sync what we're actively working on
214
+ if (route?.params?.id == entry.id || (!isNaN(entry.id) && window.location.pathname.includes(`/${entry.id}`))) {
215
+ console.log('skipping', entry.id)
216
+ continue
217
+ }
218
+
219
+ try {
220
+ let response = await save(entry, { syncing: true })
221
+ if (response?.id) remainingEntries.value.splice(index, 1)
222
+ } catch (e) {
223
+ console.log('Error saving entry', e, entry)
224
+ }
225
+
226
+ await new Promise((resolve) => setTimeout(resolve, 10 * 1000))
227
+ }
228
+
229
+ syncing.value = false
230
+ syncedEntries.value = []
231
+ }, 500)
232
+
233
+ return { remainingEntries, syncedEntries, online, syncing }
234
+ }
235
+
236
+ let subscribe = ({ target, triggers, room } = {}) => {
237
+ let socket = io(`${api}/${c.name}`, { transports: ['websocket'] })
238
+ let userId = window.localStorage.getItem('token').split(':')[1]
239
+
240
+ room = room || userId
241
+ socket.emit('subscribeToThread', room)
242
+
243
+ triggers = triggers || {}
244
+ let defaultTriggers = {
245
+ created: (data) => {
246
+ if (Array.isArray(target)) target.push(data)
247
+ else Object.assign(target, data)
248
+ },
249
+ updated: (data) => {
250
+ if (Array.isArray(target)) {
251
+ const index = target.findIndex((t) => t.id == data.id)
252
+ if (index !== -1) {
253
+ Object.assign(target[index], data)
254
+ }
255
+ } else if (target.id == data.id) Object.assign(target, data)
256
+ },
257
+ deleted: (data) => {
258
+ if (Array.isArray(target)) {
259
+ const index = target.findIndex((t) => t.id == data.id)
260
+ if (index !== -1) {
261
+ target.splice(index, 1) // Mutates the existing array
262
+ }
263
+ } else if (target.id == data.id) {
264
+ // Clear the object properties while maintaining reactivity
265
+ Object.keys(target).forEach((key) => delete target[key])
266
+ }
267
+ },
268
+ }
269
+
270
+ let combinedTriggers = { ...defaultTriggers, ...triggers }
271
+
272
+ for (let trigger of Object.keys(combinedTriggers)) {
273
+ socket.on(`${c.name}:${trigger}`, combinedTriggers[trigger])
274
+ }
275
+ }
276
+
277
+ a[c.name] = runQuery
278
+ a[c.name]['save'] = save
279
+ a[c.name]['delete'] = deleteEntry
280
+ a[c.name]['subscribe'] = subscribe
281
+ a[c.singular] = (id, query) => runQuery({ id, ...query })
282
+ a[c.singular]['save'] = save
283
+ a[c.singular]['subscribe'] = (id, callback, message) => {
284
+ if (!id) return console.error('No id provided')
285
+ return subscribe(id, message, callback)
286
+ }
287
+
288
+ a[c.name]['local'] = localDB.getEntries
289
+ a[c.singular]['local'] = localDB.get
290
+ a[c.singular]['local']['delete'] = localDB.delete
291
+ a[c.name]['sync'] = sync
292
+
293
+ a[c.name]['search'] = search
294
+ a[c.name]['search']['init'] = (search, query, algoliaFilters) => {
295
+ let loading = ref(true)
296
+ let data = ref(null)
297
+ let error = ref(null)
298
+ let totalResults = ref(null)
299
+
300
+ let response = search(search, query, algoliaFilters)
301
+ .then((response) => {
302
+ data.value = response.hits
303
+ totalResults.value = response.nbHits
304
+ loading.value = false
305
+ })
306
+ .catch((e) => {
307
+ loading.value = false
308
+ error.value = e
309
+ })
310
+
311
+ return { data, loading, error }
312
+ }
313
+
314
+ a[c.name]['init'] = ({ limit, page, search, fields, id, sort } = {}) => {
315
+ let loading = ref(true)
316
+ let data = ref(null)
317
+ let error = ref(null)
318
+
319
+ let response = runQuery({ limit, page, search, fields, id, sort })
320
+ .then((response) => {
321
+ data.value = response
322
+ loading.value = false
323
+ })
324
+ .catch((e) => {
325
+ loading.value = false
326
+ error.value = e
327
+ })
328
+
329
+ return { data, loading, error }
330
+ }
331
+
332
+ a[c.singular]['init'] = (id) => a[c.name]['init']({ id })
333
+
334
+ a.relationRequest = ({ limit, page, search, fields, id, sort, depthLimit, path } = {}) => {
335
+ let fullQuery
336
+
337
+ const params = { limit, page, search, fields, sort, depthLimit }
338
+
339
+ if (params.search != undefined) params.search = JSON.stringify(params.search)
340
+ if (params.fields != undefined) params.fields = JSON.stringify(params.fields)
341
+ if (params.sort != undefined) params.sort = JSON.stringify(params.sort)
342
+
343
+ const query =
344
+ Object.keys(params)
345
+ .filter((key) => params[key] != undefined)
346
+ ?.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(params[key])}`)
347
+ ?.join('&') || ''
348
+ // console.log(query)
349
+
350
+ fullQuery = `${api}/${path}?${query}`
351
+
352
+ let Authorization = window.localStorage.getItem('token')
353
+
354
+ return new Promise((resolve, reject) => {
355
+ axios
356
+ .get(fullQuery, { headers: { Authorization } })
357
+ .then((response) => resolve(response?.data?.response))
358
+ .catch((e) => reject(e))
359
+ })
360
+ }
361
+ a.relationRequest.init = (query) => {
362
+ let loading = ref(true)
363
+ let data = ref(null)
364
+ let error = ref(null)
365
+
366
+ let response = a
367
+ .relationRequest(query)
368
+ .then((response) => {
369
+ data.value = response
370
+ loading.value = false
371
+ })
372
+ .catch((e) => {
373
+ loading.value = false
374
+ error.value = e
375
+ })
376
+
377
+ return { data, loading, error }
378
+ }
379
+
380
+ a.graphql = runGraphql
381
+ a.graphql.init = (query) => {
382
+ let loading = ref(true)
383
+ let data = ref(null)
384
+ let error = ref(null)
385
+
386
+ let response = runGraphql(query)
387
+ .then((response) => {
388
+ data.value = response
389
+ loading.value = false
390
+ })
391
+ .catch((e) => {
392
+ loading.value = false
393
+ error.value = e
394
+ })
395
+
396
+ return { data, loading, error }
397
+ }
398
+
399
+ return a
404
400
  }, {})
405
401
 
406
402
  Mango.search = (search, query, algoliaFilters) => {
403
+ search = search || ''
407
404
 
408
- search = search || ''
409
-
410
- let filters = ``
411
- if (algoliaFilters) filters += `${algoliaFilters}`
412
-
413
- let algoliaQuery = {
414
- page: query?.page || 0,
415
- filters,
416
- hitsPerPage: query?.limit || 10
417
- }
405
+ let filters = ``
406
+ if (algoliaFilters) filters += `${algoliaFilters}`
418
407
 
419
- if (query?.fields) algoliaQuery.attributesToRetrieve = query.fields
408
+ let algoliaQuery = {
409
+ page: query?.page || 0,
410
+ filters,
411
+ hitsPerPage: query?.limit || 10,
412
+ }
420
413
 
421
- return new Promise((resolve, reject) => {
422
- algolia.search(search, algoliaQuery)
423
- .then(({ hits }) => {
424
- hits.forEach(h => h.id = h.objectID)
425
- resolve(hits)
426
- })
427
- })
414
+ if (query?.fields) algoliaQuery.attributesToRetrieve = query.fields
428
415
 
416
+ return new Promise((resolve, reject) => {
417
+ algolia.search(search, algoliaQuery).then(({ hits }) => {
418
+ hits.forEach((h) => (h.id = h.objectID))
419
+ resolve(hits)
420
+ })
421
+ })
429
422
  }
430
423
 
431
424
  Mango.login = ({ email, password }) => {
432
- return new Promise((resolve, reject) => {
433
- axios.post(`${api}/endpoints/account/login`, { email, password })
434
- .then(response => {
435
- window.localStorage.setItem('token', response.data.token)
436
- window.localStorage.setItem('user', response.data.memberId)
437
- window.localStorage.setItem('email', email)
438
- resolve(response.data)
439
- })
440
- .catch(e => reject(e))
441
- })
425
+ return new Promise((resolve, reject) => {
426
+ axios
427
+ .post(`${api}/endpoints/account/login`, { email, password })
428
+ .then((response) => {
429
+ window.localStorage.setItem('token', response.data.token)
430
+ window.localStorage.setItem('user', response.data.memberId)
431
+ window.localStorage.setItem('email', email)
432
+ resolve(response.data)
433
+ })
434
+ .catch((e) => reject(e))
435
+ })
442
436
  }
443
437
 
444
- Mango.endpoints = Object.keys(endpoints).reduce((a, c) => {
445
-
446
- a[c] = {}
447
-
448
- for (let method of endpoints[c]) {
449
- a[c][method] = () => {
450
-
451
- return new Promise((resolve, reject) => {
452
- console.log('method', method)
453
- console.log('`${api}/endpoints/${c}`', `${api}/endpoints/${c}`)
454
- axios[method](`${api}/endpoints/${c}`)
455
- .then(response => resolve(response?.data))
456
- .catch(e => reject(e))
457
- })
458
-
459
- }
460
- }
461
-
462
- for (let method of endpoints[c]) {
463
- a[c][method]['init'] = () => {
464
-
465
- let loading = ref(true)
466
- let data = ref(null)
467
- let error = ref(null)
468
-
469
- let response = a[c][method]().then(r => {
470
- data.value = r
471
- loading.value = false
472
- })
473
-
474
- return { data, loading, error }
475
- }
476
- }
477
-
478
- return a
479
-
438
+ // Build endpoints from the generated .endpoints.json
439
+ Mango.endpoints = Object.keys(endpointsData).reduce((acc, path) => {
440
+ const methods = endpointsData[path]
441
+
442
+ // Parse the path into nested object structure
443
+ // e.g., "account/login" -> acc.account.login
444
+ const parts = path.split('/')
445
+ let current = acc
446
+
447
+ for (let i = 0; i < parts.length; i++) {
448
+ const part = parts[i]
449
+
450
+ // Skip empty parts (from leading slashes)
451
+ if (!part) continue
452
+
453
+ // If this is the last part, add the methods
454
+ if (i === parts.length - 1) {
455
+ if (!current[part]) current[part] = {}
456
+
457
+ // Add each HTTP method
458
+ for (let method of methods) {
459
+ current[part][method] = (data, options = {}) => {
460
+ let Authorization = window.localStorage.getItem('token')
461
+ let headers = {
462
+ Authorization,
463
+ ...options.headers,
464
+ }
465
+
466
+ return new Promise((resolve, reject) => {
467
+ const config = { headers }
468
+
469
+ // For GET and DELETE, data goes in params
470
+ if (method === 'get' || method === 'delete') {
471
+ if (data) config.params = data
472
+ }
473
+
474
+ // For POST, PUT, PATCH, data is the body
475
+ const requestArgs = (method === 'get' || method === 'delete')
476
+ ? [`${api}/endpoints/${path}`, config]
477
+ : [`${api}/endpoints/${path}`, data, config]
478
+
479
+ axios[method](...requestArgs)
480
+ .then((response) => resolve(response?.data))
481
+ .catch((e) => reject(e))
482
+ })
483
+ }
484
+
485
+ // Add init wrapper for reactive data
486
+ current[part][method]['init'] = (data, options) => {
487
+ let loading = ref(true)
488
+ let response = ref(null)
489
+ let error = ref(null)
490
+
491
+ current[part][method](data, options)
492
+ .then((r) => {
493
+ response.value = r
494
+ loading.value = false
495
+ })
496
+ .catch((e) => {
497
+ error.value = e
498
+ loading.value = false
499
+ })
500
+
501
+ return { data: response, loading, error }
502
+ }
503
+ }
504
+ } else {
505
+ // Create intermediate object if it doesn't exist
506
+ if (!current[part]) current[part] = {}
507
+ current = current[part]
508
+ }
509
+ }
510
+
511
+ return acc
480
512
  }, {})
481
513
 
482
514
  Mango.upload = async (file) => {
483
-
484
- return new Promise((resolve, reject) => {
485
- const formData = new FormData()
486
-
487
- let uploading = true
488
- let filename = file.name
489
- let progress = 0
490
- let url
491
- let error
492
-
493
- // // Compress the image
494
- // if (file.type.includes('image')) {
495
- // let results = await compress.compress([file], {
496
- // quality: .75, // the quality of the image, max is 1,
497
- // maxWidth: 1920, // the max width of the output image, defaults to 1920px
498
- // maxHeight: 1920, // the max height of the output image, defaults to 1920px
499
- // resize: true, // defaults to true, set false if you do not want to resize the image width and height
500
- // rotate: false, // See the rotation section below
501
- // })
502
- // const img1 = results[0]
503
- // const base64str = img1.data
504
- // const imgExt = img1.ext
505
- // const filename = file.name
506
- // file = Compress.convertBase64ToFile(base64str, imgExt)
507
- // file = new File([file], filename, { type: file.type });
508
- // console.log('file', file)
509
- // }
510
-
511
- formData.append('file', file)
512
-
513
- const xhr = new XMLHttpRequest()
514
-
515
- xhr.open('POST', `${api}/upload`, true)
516
-
517
- xhr.upload.onprogress = (event) => {
518
- if (event.lengthComputable) {
519
- progress = (event.loaded / event.total) * 100
520
- }
521
- }
522
-
523
- xhr.onload = () => {
524
- if (xhr.status === 200) {
525
- const json = JSON.parse(xhr.response)
526
- const path = json.paths[0]
527
- const url = api + path
528
- uploading = false
529
- progress = 0
530
- resolve(url)
531
- } else {
532
- error = 'Error while uploading file'
533
- uploading = false
534
- reject(error)
535
- }
536
- }
537
-
538
- xhr.onerror = () => {
539
- error = 'Error while uploading file'
540
- uploading = false
541
- reject(error)
542
- }
543
-
544
- xhr.send(formData)
545
- })
515
+ return new Promise((resolve, reject) => {
516
+ const formData = new FormData()
517
+
518
+ let uploading = true
519
+ let filename = file.name
520
+ let progress = 0
521
+ let url
522
+ let error
523
+
524
+ // // Compress the image
525
+ // if (file.type.includes('image')) {
526
+ // let results = await compress.compress([file], {
527
+ // quality: .75, // the quality of the image, max is 1,
528
+ // maxWidth: 1920, // the max width of the output image, defaults to 1920px
529
+ // maxHeight: 1920, // the max height of the output image, defaults to 1920px
530
+ // resize: true, // defaults to true, set false if you do not want to resize the image width and height
531
+ // rotate: false, // See the rotation section below
532
+ // })
533
+ // const img1 = results[0]
534
+ // const base64str = img1.data
535
+ // const imgExt = img1.ext
536
+ // const filename = file.name
537
+ // file = Compress.convertBase64ToFile(base64str, imgExt)
538
+ // file = new File([file], filename, { type: file.type });
539
+ // console.log('file', file)
540
+ // }
541
+
542
+ formData.append('file', file)
543
+
544
+ const xhr = new XMLHttpRequest()
545
+
546
+ xhr.open('POST', `${api}/upload`, true)
547
+
548
+ xhr.upload.onprogress = (event) => {
549
+ if (event.lengthComputable) {
550
+ progress = (event.loaded / event.total) * 100
551
+ }
552
+ }
553
+
554
+ xhr.onload = () => {
555
+ if (xhr.status === 200) {
556
+ const json = JSON.parse(xhr.response)
557
+ const path = json.paths[0]
558
+ const url = api + path
559
+ uploading = false
560
+ progress = 0
561
+ resolve(url)
562
+ } else {
563
+ error = 'Error while uploading file'
564
+ uploading = false
565
+ reject(error)
566
+ }
567
+ }
568
+
569
+ xhr.onerror = () => {
570
+ error = 'Error while uploading file'
571
+ uploading = false
572
+ reject(error)
573
+ }
574
+
575
+ xhr.send(formData)
576
+ })
546
577
  }
547
578
 
548
579
  Mango.collections = collections
549
580
  Mango.ws = ws
550
581
 
551
- Mango.online = async () => { try { return (await axios.get(`${api}/endpoints/test`))?.data?.includes('🥭') } catch (e) { return false } }
582
+ Mango.online = async () => {
583
+ try {
584
+ return (await axios.get(`${api}/endpoints/test`))?.data?.includes('🥭')
585
+ } catch (e) {
586
+ return false
587
+ }
588
+ }
552
589
 
553
590
  export default Mango
@@ -20,6 +20,7 @@ if (!fs.existsSync(configPath)) {
20
20
  }
21
21
 
22
22
  const collectionsPath = path.resolve(configPath, 'config/.collections.json')
23
+ const endpointsPath = path.resolve(configPath, 'config/.endpoints.json')
23
24
 
24
25
  // https://vitejs.dev/config/
25
26
  export default defineConfig({
@@ -40,11 +41,12 @@ export default defineConfig({
40
41
  // }
41
42
  // }),
42
43
  {
43
- name: 'watch-collections',
44
+ name: 'watch-collections-and-endpoints',
44
45
  configureServer(server) {
45
46
  server.watcher.add(collectionsPath)
47
+ server.watcher.add(endpointsPath)
46
48
  server.watcher.on('change', (file) => {
47
- if (file === collectionsPath) {
49
+ if (file === collectionsPath || file === endpointsPath) {
48
50
  server.ws.send({
49
51
  type: 'full-reload',
50
52
  })
@@ -62,11 +64,12 @@ export default defineConfig({
62
64
  '@mango': configPath,
63
65
  '@settings': path.resolve(configPath, 'config/settings.json'),
64
66
  '@collections': path.resolve(configPath, 'config/.collections.json'),
67
+ '@endpoints': path.resolve(configPath, 'config/.endpoints.json'),
65
68
  '@plugins': path.resolve(configPath, 'plugins'),
66
69
  vue: path.resolve(__dirname, 'node_modules/vue'),
67
70
  },
68
71
  },
69
72
  optimizeDeps: {
70
- exclude: ['vue', '@collections', '@settings'], // Prevent Vite from optimizing Vue separately
73
+ exclude: ['vue', '@collections', '@settings', '@endpoints'], // Prevent Vite from optimizing Vue separately
71
74
  },
72
75
  })
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "mango-cms",
3
- "version": "0.2.31",
3
+ "version": "0.2.40",
4
4
  "main": "./index.js",
5
5
  "exports": {
6
6
  ".": "./index.js",