spyne 0.23.0 → 0.25.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/lib/spyne.esm.js +3 -3
- package/lib/spyne.esm.js.map +1 -1
- package/lib/spyne.umd.js +2 -2
- package/lib/spyne.umd.js.LICENSE.txt +2 -2
- package/package.json +1 -1
- package/src/spyne/channels/channel-fetch-class.js +14 -5
- package/src/spyne/spyne-app.js +7 -3
- package/src/spyne/spyne.js +5 -2
- package/src/spyne/utils/channel-fetch-util.js +239 -95
- package/src/spyne/utils/sanitize-data.js +320 -35
- package/src/spyne/utils/sanitize-html.js +59 -12
- package/src/spyne/utils/spyne-app-properties.js +35 -6
- package/src/spyne/utils/spyne-warn.js +26 -0
- package/src/spyne/views/dom-element-template.js +5 -0
- package/src/spyne/views/dom-element.js +22 -2
- package/src/spyne/views/view-stream-selector.js +52 -47
- package/src/tests/channels/channel-fetch.test.js +88 -1
- package/src/tests/index.test.js +5 -0
- package/src/tests/utils/channel-fetch-util.test.js +44 -2
- package/src/tests/utils/security.test.js +352 -8
- package/src/tests/views/dom-el-selectors.test.js +7 -7
- package/src/tests/views/view-stream-selector.test.js +178 -0
package/package.json
CHANGED
|
@@ -17,11 +17,13 @@ export class ChannelFetch extends Channel {
|
|
|
17
17
|
* <li>Requires a url property at instantiation.</li>
|
|
18
18
|
* <li>The response data is published as a ChannelPayload.</li>
|
|
19
19
|
* <li>The action for the returned ChannelFetch data is always in the following format, "CHANNEL_NAME_RESPONSE_EVENT"</li>
|
|
20
|
+
* <li>Fetch failures (network errors, non-OK HTTP statuses, unparseable responses) are published as a conformed error ChannelPayload with the action format, "CHANNEL_NAME_ERROR_EVENT"</li>
|
|
21
|
+
* <li>The error payload contains filterable properties such as errorType, status, statusText, url and message.</li>
|
|
20
22
|
* <li>The default request type is a GET request that returns a JSON object.</li>
|
|
21
23
|
* <li>However, any type of request and return type can be configured by using the body property when creating the instance.</li>
|
|
22
|
-
* <li>The mapFn gives access to the returned response and can be use to parse the data before it is published to other Channels and ViewStream instances.</li>
|
|
24
|
+
* <li>The mapFn gives access to the returned response and can be use to parse the data before it is published to other Channels and ViewStream instances. The mapFn is not called for error payloads.</li>
|
|
23
25
|
* <li>Channel Fetch will send the last response to future subscribers, and will not make further http(s) requests unless directed to do so.</li>
|
|
24
|
-
* <li>Data can be Fetched again, by sending a "
|
|
26
|
+
* <li>Data can be Fetched again, by sending a "CHANNEL_NAME_REQUEST_EVENT" action from a ViewStream's sendInfoToChannel method.</li>
|
|
25
27
|
* </ul>
|
|
26
28
|
*
|
|
27
29
|
* @constructor
|
|
@@ -35,7 +37,7 @@ export class ChannelFetch extends Channel {
|
|
|
35
37
|
*
|
|
36
38
|
* @example
|
|
37
39
|
* TITLE["<h4>A ViewStream Instance Sending A Fetch Update Request</h4>"]
|
|
38
|
-
* const action = "
|
|
40
|
+
* const action = "CHANNEL_FETCHCHANNEL_NAME_REQUEST_EVENT";
|
|
39
41
|
* const url = "//site.com/json/";
|
|
40
42
|
* const body = {
|
|
41
43
|
* method: "POST"
|
|
@@ -56,6 +58,7 @@ export class ChannelFetch extends Channel {
|
|
|
56
58
|
}
|
|
57
59
|
props.extendedActionsArr = [
|
|
58
60
|
`${name}_RESPONSE_EVENT`,
|
|
61
|
+
`${name}_ERROR_EVENT`,
|
|
59
62
|
[`${name}_REQUEST_EVENT`, 'onFetchUpdate']
|
|
60
63
|
]
|
|
61
64
|
props.sendCachedPayload = true
|
|
@@ -89,6 +92,7 @@ export class ChannelFetch extends Channel {
|
|
|
89
92
|
addRegisteredActions(name) {
|
|
90
93
|
const arr = [
|
|
91
94
|
'CHANNEL_RESPONSE_EVENT',
|
|
95
|
+
'CHANNEL_ERROR_EVENT',
|
|
92
96
|
['CHANNEL_UPDATE_RESPONSE_EVENT', 'onFetchUpdate'],
|
|
93
97
|
['CHANNEL_FETCH_REQUEST_EVENT', 'onFetchUpdate']
|
|
94
98
|
]
|
|
@@ -108,7 +112,12 @@ export class ChannelFetch extends Channel {
|
|
|
108
112
|
}
|
|
109
113
|
|
|
110
114
|
onFetchReturned(streamItem) {
|
|
111
|
-
|
|
115
|
+
const isFetchError = streamItem?.isChannelFetchError === true
|
|
116
|
+
const action = isFetchError
|
|
117
|
+
? `${this.props.name}_ERROR_EVENT`
|
|
118
|
+
: `${this.props.name}_RESPONSE_EVENT`
|
|
119
|
+
|
|
120
|
+
return this.createChannelPayloadItem(streamItem, action)
|
|
112
121
|
}
|
|
113
122
|
|
|
114
123
|
createChannelPayloadItem(payload, action = `${this.props.name}_RESPONSE_EVENT`) {
|
|
@@ -118,7 +127,7 @@ export class ChannelFetch extends Channel {
|
|
|
118
127
|
}
|
|
119
128
|
|
|
120
129
|
getPropsForFetch(evt) {
|
|
121
|
-
const dataObj =
|
|
130
|
+
const dataObj = evt?.payload ?? evt?.viewStreamInfo?.payload ?? {}
|
|
122
131
|
return pick(['mapFn', 'url', 'header', 'body', 'mode', 'method', 'responseType', 'debug'], dataObj)
|
|
123
132
|
}
|
|
124
133
|
|
package/src/spyne/spyne-app.js
CHANGED
|
@@ -6,7 +6,7 @@ import { sanitizeHTMLConfigure } from './utils/sanitize-html.js'
|
|
|
6
6
|
import { sanitizeDataConfigure } from './utils/sanitize-data.js'
|
|
7
7
|
|
|
8
8
|
const _channels = new ChannelsMap()
|
|
9
|
-
const version = '0.
|
|
9
|
+
const version = '0.25.0'
|
|
10
10
|
|
|
11
11
|
class SpyneApplication {
|
|
12
12
|
/**
|
|
@@ -41,7 +41,7 @@ class SpyneApplication {
|
|
|
41
41
|
init(config = {}, testMode = false) {
|
|
42
42
|
// this.channels = new ChannelsMap();
|
|
43
43
|
/*!
|
|
44
|
-
* Spyne 0.
|
|
44
|
+
* Spyne 0.25.0
|
|
45
45
|
* https://spynejs.org
|
|
46
46
|
*
|
|
47
47
|
* @license
|
|
@@ -74,6 +74,10 @@ class SpyneApplication {
|
|
|
74
74
|
scrollLockY: 0,
|
|
75
75
|
debug: false,
|
|
76
76
|
strict: false,
|
|
77
|
+
mode: 'app',
|
|
78
|
+
disableSanitize: false,
|
|
79
|
+
iframes: {},
|
|
80
|
+
customElements: [],
|
|
77
81
|
baseHref: undefined,
|
|
78
82
|
IMG_PATH: imgPath,
|
|
79
83
|
pluginMethods:{
|
|
@@ -137,7 +141,7 @@ class SpyneApplication {
|
|
|
137
141
|
}
|
|
138
142
|
|
|
139
143
|
sanitizeHTMLConfigure(SpyneAppProperties.config);
|
|
140
|
-
sanitizeDataConfigure(SpyneAppProperties.config)
|
|
144
|
+
sanitizeDataConfigure(SpyneAppProperties.config);
|
|
141
145
|
|
|
142
146
|
}
|
|
143
147
|
|
package/src/spyne/spyne.js
CHANGED
|
@@ -17,7 +17,8 @@ import { deepMerge } from './utils/deep-merge.js'
|
|
|
17
17
|
import { safeClone } from './utils/safe-clone.js'
|
|
18
18
|
import { SpyneAppProperties } from './utils/spyne-app-properties.js'
|
|
19
19
|
import { SpyneApp } from './spyne-app.js'
|
|
20
|
-
import
|
|
20
|
+
import { setSpyneWarningsDisabled } from './utils/spyne-warn.js'
|
|
21
|
+
import sanitizeData, { sanitizeEventTarget, allowCustomElements } from './utils/sanitize-data.js'
|
|
21
22
|
|
|
22
23
|
export {
|
|
23
24
|
ViewStreamElement,
|
|
@@ -40,5 +41,7 @@ export {
|
|
|
40
41
|
deepMerge,
|
|
41
42
|
safeClone,
|
|
42
43
|
sanitizeData,
|
|
43
|
-
sanitizeEventTarget
|
|
44
|
+
sanitizeEventTarget,
|
|
45
|
+
allowCustomElements,
|
|
46
|
+
setSpyneWarningsDisabled
|
|
44
47
|
}
|
|
@@ -1,65 +1,44 @@
|
|
|
1
|
-
import { from } from 'rxjs'
|
|
2
|
-
import {
|
|
3
|
-
import { compose, prop, defaultTo,
|
|
4
|
-
import sanitizeData
|
|
1
|
+
import { from, of } from 'rxjs'
|
|
2
|
+
import { catchError, map, mergeMap, share, tap } from 'rxjs/operators'
|
|
3
|
+
import { compose, prop, defaultTo, pick, mergeDeepRight } from 'ramda'
|
|
4
|
+
import sanitizeData from './sanitize-data.js'
|
|
5
|
+
import { spyneWarn } from './spyne-warn.js'
|
|
5
6
|
|
|
6
7
|
export class ChannelFetchUtil {
|
|
7
8
|
/**
|
|
8
9
|
* @module ChannelFetchUtil
|
|
9
10
|
* @type util
|
|
10
11
|
*
|
|
11
|
-
*
|
|
12
12
|
* @desc
|
|
13
|
-
* This is the core object used for ChannelFetch. This utility wraps the
|
|
13
|
+
* This is the core object used for ChannelFetch. This utility wraps the JavaScript
|
|
14
|
+
* fetch API into an observable.
|
|
14
15
|
*
|
|
15
16
|
* @constructor
|
|
16
17
|
* @param {Object} options Properties used to create the fetch request
|
|
17
|
-
* @param {
|
|
18
|
-
* @param {Boolean} testMode Controls
|
|
19
|
-
*
|
|
20
|
-
* @property {Object} options.url - = undefined; The url used for the request
|
|
21
|
-
* @property {Object} options.serverOptions - = undefined; The properties, header, body, mode, method, for the request
|
|
22
|
-
* @property {Object} options.mapFn - = undefined; A method that can be used to parse the data before it's returned
|
|
23
|
-
* @property {Object} options.responseType - = 'json'; Default is json
|
|
24
|
-
* @property {Object} options.debug - = false; will trace the fetch response to the console befor the observable completes
|
|
25
|
-
* @property {Function} Subscriber - = undefined; the method that will be called when the fetch is complete.
|
|
26
|
-
* @property {Boolean} testMode - = false; Used for unit testing.
|
|
27
|
-
*
|
|
28
|
-
* @returns The fetched response parsed by the set parameters.
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @example
|
|
32
|
-
* TITLE['<h4>Using ChannelFetchUtil to Retrieve an Image</h4>']
|
|
33
|
-
*
|
|
34
|
-
* const url = "/static/images/myimage.jpg";
|
|
35
|
-
* const responseType = "blob";
|
|
36
|
-
* const onImgBlobReturned = (blob)=>{
|
|
37
|
-
* let blobUrl = URL.createObjectURL(blob);
|
|
38
|
-
* this.appendView(
|
|
39
|
-
* new ViewStream({
|
|
40
|
-
* tagName: 'img',
|
|
41
|
-
* src: blobUrl,
|
|
42
|
-
* width:300
|
|
43
|
-
* })
|
|
44
|
-
* )}
|
|
45
|
-
*
|
|
46
|
-
* new ChannelFetchUtil({url, responseType}, onImgBlobReturned);
|
|
18
|
+
* @param {Function} subscriber A method assigned to listen to the result
|
|
19
|
+
* @param {Boolean} testMode Controls initialization for unit tests
|
|
20
|
+
* @param {String} CHANNEL_NAME The name of the channel using this fetch util
|
|
47
21
|
*
|
|
22
|
+
* @property {String} options.url - The URL used for the request
|
|
23
|
+
* @property {Object} options.serverOptions - The properties for the request
|
|
24
|
+
* @property {Function} options.mapFn - A method that can parse the data before it is returned
|
|
25
|
+
* @property {String} options.responseType - Default is json
|
|
26
|
+
* @property {Boolean} options.debug - Logs fetch response details before observable completes
|
|
27
|
+
* @property {Boolean} options.disableSanitize - Allows trusted fetch data to skip sanitization
|
|
48
28
|
*
|
|
29
|
+
* @returns The fetched response parsed by the configured parameters.
|
|
49
30
|
*/
|
|
50
31
|
|
|
51
|
-
constructor(options, subscriber, testMode, CHANNEL_NAME) {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const testSubscriber = (p) => console.log('FETCH RETURNED ', p)
|
|
32
|
+
constructor(options = {}, subscriber, testMode, CHANNEL_NAME) {
|
|
33
|
+
const testSubscriber = p => console.log('FETCH RETURNED ', p)
|
|
55
34
|
|
|
56
35
|
this._mapFn = ChannelFetchUtil.setMapFn(options)
|
|
57
36
|
this._url = ChannelFetchUtil.setUrl(options)
|
|
58
37
|
this._responseType = ChannelFetchUtil.setResponseType(options)
|
|
59
38
|
this._serverOptions = ChannelFetchUtil.setServerOptions(options)
|
|
60
39
|
this._subscriber = subscriber !== undefined ? subscriber : testSubscriber
|
|
61
|
-
this.debug = options
|
|
62
|
-
this.disableSanitize = options?.disableSanitize
|
|
40
|
+
this.debug = options?.debug === true
|
|
41
|
+
this.disableSanitize = options?.disableSanitize === true
|
|
63
42
|
this.channelName = CHANNEL_NAME
|
|
64
43
|
|
|
65
44
|
const fetchProps = {
|
|
@@ -70,6 +49,7 @@ export class ChannelFetchUtil {
|
|
|
70
49
|
disableSanitize: this.disableSanitize,
|
|
71
50
|
debug: this.debug
|
|
72
51
|
}
|
|
52
|
+
|
|
73
53
|
if (testMode !== true) {
|
|
74
54
|
ChannelFetchUtil.startWindowFetch(fetchProps, this._subscriber, this.channelName)
|
|
75
55
|
}
|
|
@@ -77,60 +57,189 @@ export class ChannelFetchUtil {
|
|
|
77
57
|
|
|
78
58
|
static startWindowFetch(props, subscriber, channelName) {
|
|
79
59
|
const { mapFn, url, serverOptions, responseType, debug } = props
|
|
80
|
-
|
|
81
|
-
const
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
return (data) => {
|
|
87
|
-
const disableSanitize = props?.disableSanitize === true
|
|
88
|
-
/*
|
|
89
|
-
if (disableSanitize) {
|
|
90
|
-
const env = (typeof process !== 'undefined' && process.env && process.env.NODE_ENV)
|
|
91
|
-
? process.env.NODE_ENV
|
|
92
|
-
: 'development'
|
|
93
|
-
|
|
94
|
-
const msg = `SPYNE SECURITY WARNING — Sanitization disabled for fetch from ${url}. Data will enter the reactive stream unsanitized. Only use disableSanitize=true for trusted or controlled sources.`
|
|
95
|
-
|
|
96
|
-
if (env === 'production') {
|
|
97
|
-
throw new Error(`SpyneJS security policy violation: disableSanitize=true is not allowed in production (${url}).`)
|
|
98
|
-
} else {
|
|
99
|
-
console.warn(
|
|
100
|
-
`%c${msg}`,
|
|
101
|
-
'color:#f33;font-weight:bold;'
|
|
102
|
-
)
|
|
103
|
-
}
|
|
104
|
-
}
|
|
105
|
-
*/
|
|
106
|
-
|
|
107
|
-
const sanitizedData = disableSanitize ? data : sanitizeData(data)
|
|
108
|
-
return mapMethod(sanitizedData, metadata)
|
|
109
|
-
}
|
|
60
|
+
|
|
61
|
+
const metadata = {
|
|
62
|
+
channelName,
|
|
63
|
+
url,
|
|
64
|
+
responseType,
|
|
65
|
+
serverOptions
|
|
110
66
|
}
|
|
111
67
|
|
|
112
|
-
|
|
68
|
+
const tapLogDebug = p => {
|
|
69
|
+
console.log('DEBUG FETCH :', p, metadata)
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const tapLog = debug === true ? tap(tapLogDebug) : tap(() => {})
|
|
73
|
+
|
|
74
|
+
const mapWrapper = mapMethod => data => {
|
|
75
|
+
const disableSanitize = props?.disableSanitize === true
|
|
76
|
+
|
|
77
|
+
// Fetched data is remote and untrusted: always sanitize in 'app' mode,
|
|
78
|
+
// regardless of the application's authoring/richtext posture.
|
|
79
|
+
const sanitizedData = disableSanitize ? data : sanitizeData(data, { mode: 'app' })
|
|
80
|
+
|
|
81
|
+
return mapMethod(sanitizedData, metadata)
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const response$ = from(window.fetch(url, serverOptions)).pipe(
|
|
85
|
+
mergeMap(response =>
|
|
86
|
+
from(ChannelFetchUtil.parseResponse(response, metadata))
|
|
87
|
+
),
|
|
88
|
+
|
|
89
|
+
tapLog,
|
|
113
90
|
|
|
114
|
-
|
|
115
|
-
.pipe(tap(tapLog), flatMap(r => from(r[responseType]())),
|
|
116
|
-
map(mapWrapper(mapFn)),
|
|
117
|
-
publish())
|
|
91
|
+
map(mapWrapper(mapFn)),
|
|
118
92
|
|
|
119
|
-
|
|
93
|
+
catchError(error =>
|
|
94
|
+
of(ChannelFetchUtil.createFetchErrorPayload(error, metadata))
|
|
95
|
+
),
|
|
96
|
+
|
|
97
|
+
share()
|
|
98
|
+
)
|
|
120
99
|
|
|
121
100
|
response$.subscribe(subscriber)
|
|
122
101
|
}
|
|
123
102
|
|
|
103
|
+
static async parseResponse(response, metadata) {
|
|
104
|
+
const { responseType } = metadata
|
|
105
|
+
|
|
106
|
+
const status = response.status
|
|
107
|
+
const statusText = response.statusText
|
|
108
|
+
const contentType = response.headers?.get?.('content-type') || ''
|
|
109
|
+
|
|
110
|
+
const responseMetadata = {
|
|
111
|
+
...metadata,
|
|
112
|
+
status,
|
|
113
|
+
statusText,
|
|
114
|
+
contentType,
|
|
115
|
+
ok: response.ok
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
if (response.ok !== true) {
|
|
119
|
+
const rawBody = await ChannelFetchUtil.safeReadText(response)
|
|
120
|
+
|
|
121
|
+
throw ChannelFetchUtil.createFetchError({
|
|
122
|
+
errorType: 'FETCH_HTTP_ERROR',
|
|
123
|
+
message: `Fetch request failed with status ${status} ${statusText}`,
|
|
124
|
+
metadata: responseMetadata,
|
|
125
|
+
rawBody
|
|
126
|
+
})
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
if (responseType === 'json') {
|
|
130
|
+
return ChannelFetchUtil.parseJsonResponse(response, responseMetadata)
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
if (responseType === 'text') {
|
|
134
|
+
return response.text()
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (responseType === 'blob') {
|
|
138
|
+
return response.blob()
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
if (responseType === 'arrayBuffer') {
|
|
142
|
+
return response.arrayBuffer()
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (responseType === 'formData') {
|
|
146
|
+
return response.formData()
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
throw ChannelFetchUtil.createFetchError({
|
|
150
|
+
errorType: 'FETCH_UNSUPPORTED_RESPONSE_TYPE',
|
|
151
|
+
message: `Unsupported fetch responseType "${responseType}"`,
|
|
152
|
+
metadata: responseMetadata
|
|
153
|
+
})
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
static async parseJsonResponse(response, metadata) {
|
|
157
|
+
const rawBody = await response.text()
|
|
158
|
+
const trimmedBody = rawBody.trim()
|
|
159
|
+
|
|
160
|
+
if (trimmedBody === '') {
|
|
161
|
+
return null
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
return JSON.parse(trimmedBody)
|
|
166
|
+
} catch (error) {
|
|
167
|
+
throw ChannelFetchUtil.createFetchError({
|
|
168
|
+
errorType: 'FETCH_RESPONSE_PARSE_ERROR',
|
|
169
|
+
message: 'Expected JSON but could not parse response body.',
|
|
170
|
+
metadata,
|
|
171
|
+
rawBody,
|
|
172
|
+
originalError: error
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
static createFetchError({ errorType, message, metadata, rawBody = '', originalError }) {
|
|
178
|
+
const error = new Error(message)
|
|
179
|
+
|
|
180
|
+
error.isChannelFetchError = true
|
|
181
|
+
error.errorType = errorType
|
|
182
|
+
error.metadata = metadata
|
|
183
|
+
error.rawBody = rawBody
|
|
184
|
+
error.originalError = originalError
|
|
185
|
+
|
|
186
|
+
return error
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
static createFetchErrorPayload(error, metadata) {
|
|
190
|
+
const fetchMetadata = error?.metadata || metadata || {}
|
|
191
|
+
|
|
192
|
+
return {
|
|
193
|
+
isChannelFetchError: true,
|
|
194
|
+
isError: true,
|
|
195
|
+
error: true,
|
|
196
|
+
errorType: error?.errorType || 'FETCH_UNKNOWN_ERROR',
|
|
197
|
+
message: error?.message || 'Unknown ChannelFetchUtil error',
|
|
198
|
+
|
|
199
|
+
channelName: fetchMetadata.channelName,
|
|
200
|
+
url: fetchMetadata.url,
|
|
201
|
+
responseType: fetchMetadata.responseType,
|
|
202
|
+
status: fetchMetadata.status,
|
|
203
|
+
statusText: fetchMetadata.statusText,
|
|
204
|
+
contentType: fetchMetadata.contentType,
|
|
205
|
+
ok: fetchMetadata.ok,
|
|
206
|
+
|
|
207
|
+
rawBodyPreview: ChannelFetchUtil.truncateBody(error?.rawBody),
|
|
208
|
+
|
|
209
|
+
originalErrorMessage: error?.originalError?.message
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
static truncateBody(body = '', maxLength = 500) {
|
|
214
|
+
if (typeof body !== 'string') {
|
|
215
|
+
return ''
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return body.length > maxLength
|
|
219
|
+
? `${body.slice(0, maxLength)}...`
|
|
220
|
+
: body
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
static async safeReadText(response) {
|
|
224
|
+
try {
|
|
225
|
+
return await response.text()
|
|
226
|
+
} catch (error) {
|
|
227
|
+
return ''
|
|
228
|
+
}
|
|
229
|
+
}
|
|
230
|
+
|
|
124
231
|
static setMapFn(opts) {
|
|
125
|
-
const getFn = compose(defaultTo(
|
|
232
|
+
const getFn = compose(defaultTo(p => p), prop('mapFn'))
|
|
126
233
|
return getFn(opts)
|
|
127
234
|
}
|
|
128
235
|
|
|
129
236
|
static setUrl(opts) {
|
|
130
237
|
const url = prop('url', opts)
|
|
238
|
+
|
|
131
239
|
if (url === undefined) {
|
|
132
|
-
|
|
240
|
+
spyneWarn('SPYNE WARNING: URL is undefined for data channel')
|
|
133
241
|
}
|
|
242
|
+
|
|
134
243
|
return url
|
|
135
244
|
}
|
|
136
245
|
|
|
@@ -155,16 +264,47 @@ export class ChannelFetchUtil {
|
|
|
155
264
|
}
|
|
156
265
|
|
|
157
266
|
static stringifyBodyIfItExists(obj) {
|
|
158
|
-
const
|
|
267
|
+
const body = obj?.body
|
|
268
|
+
|
|
269
|
+
const shouldStringifyBody =
|
|
270
|
+
body !== null &&
|
|
271
|
+
typeof body === 'object' &&
|
|
272
|
+
body.constructor === Object
|
|
273
|
+
|
|
274
|
+
if (shouldStringifyBody !== true) {
|
|
275
|
+
return obj
|
|
276
|
+
}
|
|
159
277
|
|
|
160
|
-
|
|
278
|
+
const headers = new Headers(obj.headers || {})
|
|
279
|
+
|
|
280
|
+
if (headers.has('Content-Type') !== true) {
|
|
281
|
+
headers.set('Content-Type', 'application/json')
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
return {
|
|
285
|
+
...obj,
|
|
286
|
+
headers,
|
|
287
|
+
body: JSON.stringify(body)
|
|
288
|
+
}
|
|
161
289
|
}
|
|
162
290
|
|
|
163
291
|
static updateMethodWhenBodyExists(opts) {
|
|
164
|
-
const hasBody =
|
|
165
|
-
const
|
|
166
|
-
|
|
167
|
-
|
|
292
|
+
const hasBody = opts?.body !== undefined
|
|
293
|
+
const method = opts?.method || 'GET'
|
|
294
|
+
|
|
295
|
+
if (hasBody && method.toUpperCase() === 'GET') {
|
|
296
|
+
spyneWarn(
|
|
297
|
+
'SPYNE WARNING: Fetch body was provided with method GET. Changing method to POST.',
|
|
298
|
+
opts
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
return {
|
|
302
|
+
...opts,
|
|
303
|
+
method: 'POST'
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
return opts
|
|
168
308
|
}
|
|
169
309
|
|
|
170
310
|
static setServerOptions(opts) {
|
|
@@ -181,26 +321,30 @@ export class ChannelFetchUtil {
|
|
|
181
321
|
'integrity',
|
|
182
322
|
'keepalive'
|
|
183
323
|
], opts)
|
|
324
|
+
|
|
184
325
|
let mergedOptions = mergeDeepRight(ChannelFetchUtil.baseOptions(), options)
|
|
326
|
+
|
|
185
327
|
mergedOptions = ChannelFetchUtil.updateMethodWhenBodyExists(mergedOptions)
|
|
186
328
|
mergedOptions = ChannelFetchUtil.stringifyBodyIfItExists(mergedOptions)
|
|
329
|
+
|
|
187
330
|
return mergedOptions
|
|
188
331
|
}
|
|
189
332
|
|
|
190
333
|
static baseOptions() {
|
|
191
334
|
return {
|
|
192
|
-
method: 'GET',
|
|
335
|
+
method: 'GET',
|
|
336
|
+
|
|
193
337
|
headers: new Headers({
|
|
194
338
|
Accept: 'application/json, text/plain, */*'
|
|
195
339
|
}),
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
referrerPolicy: 'no-referrer-when-downgrade',
|
|
202
|
-
integrity: '',
|
|
203
|
-
keepalive: false
|
|
340
|
+
|
|
341
|
+
mode: 'cors',
|
|
342
|
+
credentials: 'same-origin',
|
|
343
|
+
cache: 'default',
|
|
344
|
+
redirect: 'follow',
|
|
345
|
+
referrerPolicy: 'no-referrer-when-downgrade',
|
|
346
|
+
integrity: '',
|
|
347
|
+
keepalive: false
|
|
204
348
|
}
|
|
205
349
|
}
|
|
206
350
|
}
|