@es-labs/jslib 0.0.1

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.
Files changed (72) hide show
  1. package/CHANGELOG.md +4 -0
  2. package/README.md +42 -0
  3. package/__test__/services.test.js +32 -0
  4. package/auth/index.js +226 -0
  5. package/auth/keyv.js +23 -0
  6. package/auth/knex.js +29 -0
  7. package/auth/redis.js +23 -0
  8. package/comms/email.js +123 -0
  9. package/comms/nexmo.js +44 -0
  10. package/comms/telegram.js +43 -0
  11. package/comms/telegram2/inbound.js +314 -0
  12. package/comms/telegram2/outbound.js +574 -0
  13. package/comms/webpush.js +60 -0
  14. package/config.js +37 -0
  15. package/express/controller/auth/oauth.js +39 -0
  16. package/express/controller/auth/oidc.js +87 -0
  17. package/express/controller/auth/own.js +100 -0
  18. package/express/controller/auth/saml.js +74 -0
  19. package/express/upload.js +48 -0
  20. package/index.js +1 -0
  21. package/iso/README.md +4 -0
  22. package/iso/__tests__/csv-utils.spec.js +128 -0
  23. package/iso/__tests__/datetime.spec.js +101 -0
  24. package/iso/__tests__/fetch.spec.js +270 -0
  25. package/iso/csv-utils.js +206 -0
  26. package/iso/datetime.js +103 -0
  27. package/iso/fetch.js +129 -0
  28. package/iso/fetch2.js +180 -0
  29. package/iso/log-filter.js +17 -0
  30. package/iso/sleep.js +6 -0
  31. package/iso/ws.js +63 -0
  32. package/node/oss-files/oss-uploader-client-fetch.js +258 -0
  33. package/node/oss-files/oss-uploader-client-fetch.md +31 -0
  34. package/node/oss-files/oss-uploader-client.js +219 -0
  35. package/node/oss-files/oss-uploader-server.js +199 -0
  36. package/node/oss-files/oss-uploader-usage.js +121 -0
  37. package/node/oss-files/oss-uploader-usage.md +34 -0
  38. package/node/oss-files/s3-uploader-client.js +217 -0
  39. package/node/oss-files/s3-uploader-server.js +123 -0
  40. package/node/oss-files/s3-uploader-usage.js +77 -0
  41. package/node/oss-files/s3-uploader-usage.md +34 -0
  42. package/package.json +53 -0
  43. package/packageInfo.js +9 -0
  44. package/services/ali.js +279 -0
  45. package/services/aws.js +194 -0
  46. package/services/db/__tests__/keyv.spec.js +31 -0
  47. package/services/db/keyv.js +14 -0
  48. package/services/db/knex.js +67 -0
  49. package/services/db/redis.js +51 -0
  50. package/services/index.js +57 -0
  51. package/services/mq/README.md +8 -0
  52. package/services/websocket.js +139 -0
  53. package/t4t/README.md +1 -0
  54. package/traps.js +20 -0
  55. package/utils/__tests__/aes.spec.js +52 -0
  56. package/utils/aes.js +23 -0
  57. package/web/UI.md +71 -0
  58. package/web/bwc-autocomplete.js +211 -0
  59. package/web/bwc-combobox.js +343 -0
  60. package/web/bwc-fileupload.js +87 -0
  61. package/web/bwc-loading-overlay.js +54 -0
  62. package/web/bwc-t4t-form.js +511 -0
  63. package/web/bwc-table.js +756 -0
  64. package/web/fetch.js +129 -0
  65. package/web/i18n.js +24 -0
  66. package/web/idle.js +49 -0
  67. package/web/parse-jwt.js +15 -0
  68. package/web/pwa.js +84 -0
  69. package/web/sign-pad.js +164 -0
  70. package/web/t4t-fe.js +164 -0
  71. package/web/util.js +126 -0
  72. package/web/web-cam.js +182 -0
package/web/fetch.js ADDED
@@ -0,0 +1,129 @@
1
+ // TODO add retry - https://dev.to/ycmjason/javascript-fetch-retry-upon-failure-3p6g
2
+ class Fetch {
3
+ /**
4
+ *
5
+ * @param {*} options
6
+ * @param {*} tokens
7
+ */
8
+ constructor(options = {}, tokens = {}) {
9
+ this.options = {
10
+ baseUrl: '',
11
+ credentials: 'same-origin',
12
+ forceLogoutFn: () => {}, // function to call when forcing a logout
13
+ refreshUrl: '',
14
+ timeoutMs: 0,
15
+ maxRetry: 0
16
+ }
17
+ Object.assign(this.options, options)
18
+ this.tokens = { access: '', refresh: '' }
19
+ Object.assign(this.tokens, tokens)
20
+ }
21
+
22
+ /**
23
+ *
24
+ * @param {string} url
25
+ * @param {string} baseUrl
26
+ * @returns {object} { urlOrigin, urlPath, urlFull, urlSearch }
27
+ * @throws {Error} if URL is invalid
28
+ */
29
+ static parseUrl (url, baseUrl = '') {
30
+ let urlPath = url
31
+ let urlOrigin = baseUrl
32
+ let urlFull = baseUrl + urlPath
33
+ let urlSearch = ''
34
+ try {
35
+ urlSearch = (url.lastIndexOf('?') !== -1) ? url.split('?').pop() : '' // handle /abc/def?aa=1&bb=2
36
+ if (urlSearch) urlSearch = '?' + urlSearch // prepend ?
37
+ const { origin = '', pathname = '', search = '' } = new URL(url) // http://example.com:3001/abc/ees?aa=1&bb=2
38
+ urlOrigin = origin
39
+ urlPath = pathname
40
+ urlFull = origin + pathname
41
+ urlSearch = search
42
+ } catch (e) {
43
+ }
44
+ return { urlOrigin, urlPath, urlFull, urlSearch }
45
+ }
46
+
47
+ setOptions (options) { Object.assign(this.options, options) }
48
+ getOptions () { return this.options }
49
+
50
+ setTokens (tokens) { Object.assign(this.tokens, tokens) }
51
+ getTokens () { return this.tokens }
52
+
53
+ async http (method, url, body = null, query = null, headers = null) {
54
+ const { urlOrigin, urlPath, urlFull, urlSearch } = Fetch.parseUrl(url, this.options.baseUrl)
55
+ try {
56
+ const controller = new AbortController()
57
+ const signal = controller.signal
58
+ if (this.options.timeoutMs > 0) setTimeout(() => controller.abort(), this.options.timeoutMs) // err.name === 'AbortError'
59
+
60
+ let qs = (query && typeof query === 'object') // null is also an object
61
+ ? '?' +
62
+ Object.keys(query).map((key) => encodeURIComponent(key) + '=' + encodeURIComponent(query[key])).join('&')
63
+ : (query || '')
64
+ qs = qs ? qs + urlSearch.substring(1) // remove the question mark
65
+ : urlSearch
66
+
67
+ if (!headers) {
68
+ headers = {
69
+ Accept: 'application/json'
70
+ }
71
+ }
72
+ const options = { method, headers }
73
+ if (this.options.timeoutMs > 0) options.signal = signal
74
+ if (this.options.credentials !== 'include') { // include === HTTPONLY_TOKEN
75
+ if (this.tokens.access) options.headers.Authorization = `Bearer ${this.tokens.access}`
76
+ }
77
+ options.credentials = this.options.credentials
78
+
79
+ if (['POST', 'PATCH', 'PUT'].includes(method)) { // check if HTTP method has req body (DELETE is maybe)
80
+ if (body && body instanceof FormData) {
81
+ options.body = body // options.headers['Content-Type'] = 'multipart/form-data' // NOT NEEDED!!!
82
+ } else if (options.headers['Content-Type'] && options.headers['Content-Type'] === 'application/x-www-form-urlencoded') {
83
+ options.body = new URLSearchParams(body) // body should be JSON
84
+ } else if (options.headers['Content-Type'] && options.headers['Content-Type'] === 'application/octet-stream') {
85
+ options.body = body // handling stream...
86
+ } else {
87
+ options.headers['Content-Type'] = 'application/json' // NEEDED!!!
88
+ options.body = JSON.stringify(body)
89
+ }
90
+ }
91
+
92
+ const rv0 = await fetch(urlFull + qs, options)
93
+ const txt0 = await rv0.text() // handle empty body as xxx.json() cannot
94
+ rv0.data = txt0.length ? JSON.parse(txt0) : {}
95
+ if (rv0.status >= 200 && rv0.status < 400) return rv0
96
+ else if (rv0.status === 401) { // no longer needed urlPath !== '/api/auth/refresh'
97
+ if (rv0.data.message === 'Token Expired Error' && this.options.refreshUrl) {
98
+ try {
99
+ const rv1 = await this.http('POST', urlOrigin + this.options.refreshUrl, { refresh_token: this.tokens.refresh }) // rv1 JSON already processed
100
+ // status code should be < 400 here
101
+ this.tokens.access = rv1.data.access_token
102
+ this.tokens.refresh = rv1.data.refresh_token
103
+ if (options.credentials !== 'include') { // include === HTTPONLY_TOKEN
104
+ if (this.tokens.access) options.headers.Authorization = `Bearer ${this.tokens.access}`
105
+ }
106
+ const rv2 = await fetch(urlFull + qs, options)
107
+ const txt2 = await rv2.text()
108
+ rv2.data = txt2.length ? JSON.parse(txt2) : {}
109
+ return rv2
110
+ } catch (e) {
111
+ throw e
112
+ }
113
+ }
114
+ }
115
+ throw rv0 // error
116
+ } catch (e) {
117
+ if (e?.data?.message !== 'Token Expired Error' && (e.status === 401 || e.status === 403)) this.options.forceLogoutFn()
118
+ throw e // some other error
119
+ }
120
+ }
121
+
122
+ async post (url, body = null, query = null, headers = null) { return this.http('POST', url, body, query, headers) }
123
+ async put (url, body = null, query = null, headers = null) { return this.http('PUT', url, body, query, headers) }
124
+ async patch (url, body = null, query = null, headers = null) { return this.http('PATCH', url, body, query, headers) }
125
+ async del (url, query = null, headers = null) { return this.http('DELETE', url, null, query, headers) }
126
+ async get (url, query = null, headers = null) { return this.http('GET', url, null, query, headers) }
127
+ }
128
+
129
+ export default Fetch
package/web/i18n.js ADDED
@@ -0,0 +1,24 @@
1
+ // https://phrase.com/blog/posts/detecting-a-users-locale/
2
+ // https://gist.github.com/ashour/5f169a6dd9b6293691629ee0d06cae6f
3
+ // get languages / locales from browser
4
+ // sample output: ['en-US', 'en-GB']
5
+
6
+ function getBrowserLocales(options = {}) {
7
+ const defaultOptions = {
8
+ languageCodeOnly: false
9
+ }
10
+ const opt = {
11
+ ...defaultOptions,
12
+ ...options
13
+ }
14
+ const browserLocales = navigator.languages === undefined ? [navigator.language] : navigator.languages
15
+ if (!browserLocales) {
16
+ return undefined
17
+ }
18
+ return browserLocales.map((locale) => {
19
+ const trimmedLocale = locale.trim()
20
+ return opt.languageCodeOnly ? trimmedLocale.split(/-|_/)[0] : trimmedLocale
21
+ })
22
+ }
23
+
24
+ export default getBrowserLocales
package/web/idle.js ADDED
@@ -0,0 +1,49 @@
1
+ const idleTimer = { // logout user if idle
2
+ timeouts: [],
3
+
4
+ _idleSecondsTimer: null,
5
+ _idleSecondsCounter: 0,
6
+ _elapsedTsMs: 0, // elapsed timestamp in milliseconds
7
+ _reset: null,
8
+ _check: null,
9
+
10
+ reset () { this._idleSecondsCounter = 0 },
11
+
12
+ clearElapsedTimeStart () { this._elapsedTsMs = 0 },
13
+ setElapsedTimeStart () { this._elapsedTsMs = Date.now() },
14
+ getElapsedTimeSeconds () { return parseInt((Date.now() - this._elapsedTsMs) / 1000.0) },
15
+
16
+ getIdleTimeSeconds () { return this._idleSecondsCounter },
17
+
18
+ check() {
19
+ this._idleSecondsCounter++
20
+ for (let timeout of this.timeouts) {
21
+ // console.log('check2', this._idleSecondsCounter, timeout.time)
22
+ if (this._idleSecondsCounter >= timeout.time) {
23
+ if (timeout.stop) this.stop()
24
+ timeout.fn()
25
+ }
26
+ }
27
+ },
28
+
29
+ start () {
30
+ if (!this._reset) this._reset = this.reset.bind(this)
31
+ if (!this._check) this._check = this.check.bind(this)
32
+
33
+ this.setElapsedTimeStart()
34
+ document.addEventListener('click', this._reset)
35
+ document.addEventListener('mousemove', this._reset)
36
+ document.addEventListener('keypress', this._reset)
37
+ this._idleSecondsTimer = window.setInterval(this._check, 1000)
38
+ },
39
+
40
+ stop () {
41
+ this.clearElapsedTimeStart()
42
+ document.removeEventListener('click', this._reset)
43
+ document.removeEventListener('mousemove', this._reset)
44
+ document.removeEventListener('keypress', this._reset)
45
+ window.clearInterval(this._idleSecondsTimer)
46
+ }
47
+ }
48
+
49
+ export default idleTimer
@@ -0,0 +1,15 @@
1
+ // parses and returns JWT payload
2
+ // exceptions to be handled by caller
3
+ /**
4
+ *
5
+ * @param {string} token
6
+ * @returns
7
+ */
8
+ export default function (token) {
9
+ const base64Url = token.split('.')[1]
10
+ const base64 = base64Url.replace(/-/g, '+').replace(/_/g, '/')
11
+ const jsonPayload = decodeURIComponent(atob(base64).split('').map(function(c) {
12
+ return '%' + ('00' + c.charCodeAt(0).toString(16)).slice(-2)
13
+ }).join(''))
14
+ return JSON.parse(jsonPayload)
15
+ }
package/web/pwa.js ADDED
@@ -0,0 +1,84 @@
1
+ // // First get a public key from our Express server
2
+ // We use this function to subscribe to our push notifications
3
+ // As soon as you run this code once, it shouldn't run again if the initial subscription went well
4
+ // Except if you clear your storage
5
+ export const webpushSubscribe = async (publicKey) => {
6
+ const registration = await navigator.serviceWorker.ready
7
+ // NOSONAR
8
+ // alternate = navigator.serviceWorker.getRegistration()
9
+ // let subscription = await registration.pushManager.getSubscription()
10
+ // if (subscription) return subscription
11
+
12
+ // this is an annoying part of the process we have to turn our public key into a Uint8Array
13
+ const Uint8ArrayPublicKey = urlBase64ToUint8Array(publicKey)
14
+
15
+ // registering a new subscription to our service worker's Push manager
16
+ let subscription = await registration.pushManager.subscribe({
17
+ userVisibleOnly: true, // chrome only supports user visible for now
18
+ applicationServerKey: Uint8ArrayPublicKey
19
+ })
20
+ subscription = JSON.stringify(subscription)
21
+
22
+ console.log('subscription', subscription)
23
+ return subscription
24
+ }
25
+
26
+ // Let's create an unsubscribe function as well
27
+ export const webpushUnsubscribe = async () => {
28
+ const registration = await navigator.serviceWorker.ready
29
+ const subscription = await registration.pushManager.getSubscription()
30
+ if (subscription) await subscription.unsubscribe() // This tells our browser that we want to unsubscribe
31
+ }
32
+ // This tells our Express server that we want to unsubscribe
33
+ // await fetch(VITE_API_URL + '/api/webpush/unsub?subId=test', {
34
+ // method: 'POST',
35
+ // headers: { 'Content-Type': 'application/json' },
36
+ // body: JSON.stringify(subscription.toJSON())
37
+ // })
38
+
39
+ // I have found this code (or variations of) from; multiple sources
40
+ // but I could not find the original author
41
+ // here's one such source:
42
+ // https://stackoverflow.com/questions/42362235/web-pushnotification-unauthorizedregistration-or-gone-or-unauthorized-sub
43
+ const urlBase64ToUint8Array = (base64String) => {
44
+ // const padding = '='.repeat((4 - base64String.length % 4) % 4)
45
+ // const base64 = (base64String + padding).replace(/\-/g, '+').replace(/_/g, '/')
46
+ // https://stackoverflow.com/questions/52379865/eslint-replace-cant-read-method
47
+ const padding = '='.repeat((4 - (base64String.length % 4)) % 4)
48
+ const base64 = (base64String + padding).replace(/-/g, '+').replace(/_/g, '/')
49
+
50
+ const rawData = window.atob(base64)
51
+ const outputArray = new Uint8Array(rawData.length)
52
+
53
+ for (let i = 0; i < rawData.length; ++i) {
54
+ outputArray[i] = rawData.charCodeAt(i)
55
+ }
56
+ return outputArray
57
+ }
58
+
59
+ const handleSwMessage = async (e) => {
60
+ console.log('handleSwMessage', e)
61
+ //NOSONAR if (e && e.data && e.data.msg === 'pushsubscriptionchange') { }
62
+ }
63
+
64
+ export const addSwMessageEvent = (handler = handleSwMessage) => {
65
+ navigator.serviceWorker.addEventListener('message', handleSwMessage)
66
+ }
67
+
68
+ export const removeSwMessageEvent = (handler = handleSwMessage) => {
69
+ navigator.serviceWorker.removeEventListener('message', handleSwMessage)
70
+ }
71
+
72
+ // https://felixgerschau.com/how-to-communicate-with-service-workers/
73
+ // app to sw
74
+ // // app.js - Somewhere in your web app
75
+ // navigator.serviceWorker.controller.postMessage({
76
+ // type: 'MESSAGE_IDENTIFIER',
77
+ // })
78
+ // service-worker.js
79
+ // On the Service Worker side we have to listen to the message event
80
+ // self.addEventListener('message', (e) => {
81
+ // if (e.data && e.data.type === 'MESSAGE_IDENTIFIER') {
82
+ // // do something
83
+ // }
84
+ // })
@@ -0,0 +1,164 @@
1
+ // FRONTEND ONLY
2
+ const template = document.createElement('template')
3
+ template.innerHTML = `
4
+ <style>
5
+ canvas {
6
+ background-color: var(--vcxwc-sign-pad-background-color, #ddd);
7
+ }
8
+ </style>
9
+ <canvas id="canvas"></canvas>
10
+ `
11
+ class SignPad extends HTMLElement {
12
+ constructor() {
13
+ super()
14
+ const shadowRoot = this.attachShadow({ mode: 'open' })
15
+ shadowRoot.appendChild(template.content.cloneNode(true))
16
+ console.log('SignPad()')
17
+ this.mouse = {
18
+ current: { x: 0, y: 0 },
19
+ previous: { x: 0, y: 0 },
20
+ down: false,
21
+ move: false
22
+ }
23
+ this.handleMouseDown = this.handleMouseDown.bind(this) // bind callback function
24
+ this.handleMouseUp = this.handleMouseUp.bind(this) // bind callback function
25
+ this.handleMouseMove = this.handleMouseMove.bind(this) // bind callback function
26
+ this.currentMouse = this.currentMouse.bind(this) // bind callback function
27
+ this.draw = this.draw.bind(this) // bind callback function
28
+ }
29
+
30
+ static get observedAttributes() {
31
+ // default width = 300 (in px)
32
+ // default height = 150 (in px)
33
+ // default value should be ''
34
+ return ['value', 'width', 'height']
35
+ }
36
+
37
+ // getter and setter for property - show
38
+ get value() {
39
+ return this.getAttribute('value')
40
+ }
41
+
42
+ set value(val) {
43
+ // console.log('setting value', val)
44
+ this.setAttribute('value', val || '')
45
+ }
46
+
47
+ attributeChangedCallback(name, oldVal, newVal) {
48
+ const el = this.shadowRoot.querySelector('#canvas')
49
+ switch (name) {
50
+ case 'width': // set canvas width
51
+ el.width = newVal
52
+ break
53
+ case 'height': // set canvas height
54
+ el.height = newVal
55
+ break
56
+ // value no need to handle
57
+ }
58
+ }
59
+
60
+ connectedCallback() {
61
+ // const styleSheetList = this.shadowRoot.styleSheets // do this here, not in constructor
62
+ // const bg_rule = styleSheetList[0].cssRules[0]
63
+ // const yyy = context_rule.style.getPropertyValue('--vcxwc-sign-pad-background-color')
64
+ // console.log('bg', bg_rule.style.backgroundColor, yyy)
65
+
66
+ // console.log('connect sign')
67
+ this.mouse = {
68
+ current: { x: 0, y: 0 },
69
+ previous: { x: 0, y: 0 },
70
+ down: false,
71
+ move: false
72
+ }
73
+
74
+ // console.log('has value?', this.hasAttribute('value'), this.getAttribute('value'))
75
+ if (!this.hasAttribute('value')) this.setAttribute('value', '')
76
+
77
+ const el = this.shadowRoot.querySelector('#canvas')
78
+ // console.log('canvas w h', el.width, el.height)
79
+ const ctx = el.getContext('2d')
80
+ ctx.translate(0.5, 0.5)
81
+ ctx.imageSmoothingEnabled = false
82
+
83
+ const ctx2d = JSON.parse(this.getAttribute('context2d'))
84
+ // console.log('ctx2d', ctx2d)
85
+ for (const k in ctx2d) {
86
+ // strokeStyle, lineWidth
87
+ ctx[k] = ctx2d[k]
88
+ }
89
+ el.addEventListener('mousedown', this.handleMouseDown)
90
+ el.addEventListener('mouseup', this.handleMouseUp)
91
+ el.addEventListener('mousemove', this.handleMouseMove)
92
+ }
93
+
94
+ adoptedCallback() {}
95
+
96
+ disconnectedCallback() {
97
+ // console.log('disconnect sign')
98
+ const el = this.shadowRoot.querySelector('#canvas')
99
+ el.removeEventListener('mousedown', this.handleMouseDown)
100
+ el.removeEventListener('mouseup', this.handleMouseUp)
101
+ el.removeEventListener('mousemove', this.handleMouseMove)
102
+ }
103
+
104
+ handleMouseDown(event) {
105
+ this.mouse.down = true
106
+ this.mouse.move = false
107
+ this.mouse.current = {
108
+ x: event.offsetX,
109
+ y: event.offsetY
110
+ }
111
+ const el = this.shadowRoot.querySelector('#canvas')
112
+ const ctx = el.getContext('2d')
113
+ ctx.clearRect(0, 0, el.width, el.height) // clear
114
+ ctx.beginPath() // clear lines
115
+ this.value = ''
116
+
117
+ const { x, y } = this.currentMouse()
118
+ ctx.moveTo(x, y)
119
+ }
120
+
121
+ handleMouseUp() {
122
+ const el = this.shadowRoot.querySelector('#canvas')
123
+ if (this.mouse.move) this.value = el.toDataURL('image/png')
124
+ else this.value = ''
125
+
126
+ const event = new CustomEvent('input', { detail: this.value })
127
+ this.dispatchEvent(event)
128
+
129
+ this.mouse.down = false
130
+ this.mouse.move = false
131
+ }
132
+
133
+ handleMouseMove(event) {
134
+ this.mouse.move = true
135
+ this.mouse.current = {
136
+ x: event.offsetX,
137
+ y: event.offsetY
138
+ }
139
+ this.draw(event)
140
+ }
141
+
142
+ draw(event) {
143
+ // requestAnimationFrame(this.draw)
144
+ if (this.mouse.down) {
145
+ const el = this.shadowRoot.querySelector('#canvas')
146
+ // console.log('draw', event, el.width, el.height, c)
147
+ // const c = document.getElementById('canvas')
148
+ const ctx = el.getContext('2d')
149
+ ctx.clearRect(0, 0, el.width, el.height)
150
+ const { x, y } = this.currentMouse()
151
+ ctx.lineTo(x, y)
152
+ ctx.stroke()
153
+ }
154
+ }
155
+
156
+ currentMouse() {
157
+ return {
158
+ x: this.mouse.current.x,
159
+ y: this.mouse.current.y
160
+ }
161
+ }
162
+ }
163
+
164
+ customElements.define('vcxwc-sign-pad', SignPad)
package/web/t4t-fe.js ADDED
@@ -0,0 +1,164 @@
1
+ import Fetch from './fetch.js'
2
+ let tableName = ''
3
+ let parentFilter = null
4
+ let config = null
5
+ let urlPrefix = '/api'
6
+ let http = new Fetch()
7
+ // Do Not Catch Errors - let it be caught by caller
8
+
9
+ function setTableName(name) { // set table name
10
+ tableName = name
11
+ parentFilter = null // TBD: find a more sustainable way using prototype or let caller handle this part
12
+ }
13
+
14
+ // this might change
15
+ const setFetch = (_fetch) => http = _fetch // set the fetch function
16
+ const setParentFilter = (_filter) => parentFilter = _filter
17
+ const setUrlPrefix = (_urlPrefix) => urlPrefix = _urlPrefix
18
+
19
+ async function getConfig() {
20
+ const { data } = await http.get(urlPrefix + '/t4t/config/' + tableName)
21
+ if (data) config = data
22
+ return data
23
+ }
24
+
25
+ async function find(filters, sorter, page, limit) {
26
+ let rv = {
27
+ results: [],
28
+ total: 0
29
+ }
30
+ if (parentFilter) {
31
+ filters.push({col: parentFilter.col, op: "=", val: parentFilter.id, andOr: "and"})
32
+ }
33
+ filters = filters ? JSON.stringify(filters) : '' // [{col, op, val, andOr}, ...]
34
+ sorter = sorter ? JSON.stringify(sorter) : '' // [{ column: '<col_name>', order: 'asc|desc' }, ...]
35
+ const { data } = await http.get(urlPrefix + '/t4t/find/' + tableName, {
36
+ page, limit, filters, sorter
37
+ })
38
+ rv.results = data.results
39
+ rv.total = data.total
40
+ return rv
41
+ }
42
+
43
+ async function download(filters, sorter) {
44
+ const { data } = await http.get(urlPrefix + '/t4t/find/' + tableName, {
45
+ page: 0,
46
+ limit: 0,
47
+ filters: filters ? JSON.stringify(filters) : '',
48
+ sorter: sorter ? JSON.stringify(sorter) : '',
49
+ csv: 1 // it is a csv
50
+ })
51
+ return data
52
+ }
53
+
54
+ async function findOne(__key) {
55
+ let rv = {}
56
+ const { data } = await http.get(urlPrefix + '/t4t/find-one/' + tableName, { __key }) // if multiKey, then seperate values by |, column is implied by order
57
+ if (data) {
58
+ rv = data
59
+ rv.__key = __key
60
+ }
61
+ return rv
62
+ }
63
+
64
+ // process data for use with
65
+ // JSON only, multi-part form, JSON & filelist (signed URL upload)
66
+ function processData(record, { signedUrl = false } = { }) {
67
+ const rv = {
68
+ json: { }
69
+ }
70
+ for (const [k, v] of Object.entries(record)) {
71
+ if (v instanceof FileList) {
72
+ const fileNameArray = []
73
+ for (const file of v) {
74
+ if (signedUrl) {
75
+ if (!rv.files) rv.files = []
76
+ rv.files.push(file)
77
+ } else {
78
+ if (!rv.form) rv.form = new FormData()
79
+ rv.form.append('file-data', file) // add
80
+ }
81
+ fileNameArray.push(file.name)
82
+ }
83
+ rv.json[k] = fileNameArray.join(',') // array
84
+ } else {
85
+ rv.json[k] = v
86
+ }
87
+ }
88
+ if (rv.form) rv.form.append('json-data', JSON.stringify(rv.json)) // set the JSON
89
+ return rv
90
+ }
91
+
92
+ async function create(record) {
93
+ // const { data } = await http.patch(`/authors/${id}`, formData,
94
+ // { onUploadProgress: progressEvent => console.log(Math.round(progressEvent.loaded / progressEvent.total * 100) + '%') } // axios only
95
+ // )
96
+ return await http.post(urlPrefix + `/t4t/create/${tableName}`, record)
97
+ }
98
+
99
+ async function update(__key, record, headers = null) {
100
+ return await http.patch(urlPrefix + `/t4t/update/${tableName}`, record, { __key }, headers)
101
+ }
102
+
103
+ // Handle file removals seperately
104
+ async function remove(items) {
105
+ // console.log(items)
106
+ let ids = []
107
+ // const { pk } = config
108
+ // ids = pk ? items.map((item) => item[pk]) : items.map((item) => item.__key)
109
+ // console.log(ids)
110
+ ids = items
111
+ return await http.post(urlPrefix + '/t4t/remove/' + tableName, { ids })
112
+ }
113
+
114
+ // uploads a single csv for batch processing
115
+ async function upload(file) { // the file object
116
+ // TODO add exception handling
117
+ if (file === null) return false
118
+ const formData = new FormData()
119
+ formData.append('csv-file', file) // call it file
120
+ // console.log('zzz', formData instanceof FormData)
121
+ // for(const pair of formData.entries()) console.log(pair[0], pair[1])
122
+ return await http.post(urlPrefix + '/t4t/upload/' + tableName, formData)
123
+ // formData.append('textdata', JSON.stringify({ name: 'name', age: 25 }))
124
+ // const res = await fetch('/api/upload', { method: 'POST', body: formData })
125
+ // const { id, name, avatar } = record
126
+ // const json = JSON.stringify({ name })
127
+ // // const blob = new Blob([json], { type: 'application/json' })
128
+ // // console.log('json', blob)
129
+ }
130
+
131
+
132
+ // const autoComplete = debounce(async (e, col, _showForm) => {
133
+ // recordObj[_showForm][col] = e.target.value
134
+
135
+ // wrap in debounce
136
+ // parentColVal in use-cases where parent table column changes
137
+ async function autocomplete (search, col, record, parentColVal = '') {
138
+ let res = []
139
+ try {
140
+ const { tableName, limit, key, text, parentTableColName, parentCol } = config.cols[col].options
141
+ const query = { tableName, limit, key, text, search }
142
+ let parentTableColVal = ''
143
+ if (parentTableColName) {
144
+ parentTableColVal = parentColVal || record[parentCol] || ''
145
+ }
146
+ const { data } = await http.post(urlPrefix + '/t4t/autocomplete/' + tableName, {
147
+ key,
148
+ text,
149
+ search,
150
+ limit,
151
+ parentTableColName,
152
+ parentTableColVal
153
+ })
154
+ res = data
155
+ } catch (err) {
156
+ console.log('autocomplete', err.message)
157
+ }
158
+ return res
159
+ }
160
+
161
+ export {
162
+ setFetch, setTableName, setParentFilter, setUrlPrefix, getConfig,
163
+ find, findOne, create, update, remove, upload, download, autocomplete, processData,
164
+ }