@nordcraft/core 1.0.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/README.md +5 -0
- package/dist/api/LegacyToddleApi.d.ts +34 -0
- package/dist/api/LegacyToddleApi.js +178 -0
- package/dist/api/LegacyToddleApi.js.map +1 -0
- package/dist/api/ToddleApiV2.d.ts +77 -0
- package/dist/api/ToddleApiV2.js +271 -0
- package/dist/api/ToddleApiV2.js.map +1 -0
- package/dist/api/api.d.ts +49 -0
- package/dist/api/api.js +276 -0
- package/dist/api/api.js.map +1 -0
- package/dist/api/apiTypes.d.ts +125 -0
- package/dist/api/apiTypes.js +11 -0
- package/dist/api/apiTypes.js.map +1 -0
- package/dist/api/headers.d.ts +10 -0
- package/dist/api/headers.js +36 -0
- package/dist/api/headers.js.map +1 -0
- package/dist/api/template.d.ts +5 -0
- package/dist/api/template.js +7 -0
- package/dist/api/template.js.map +1 -0
- package/dist/component/ToddleComponent.d.ts +45 -0
- package/dist/component/ToddleComponent.js +373 -0
- package/dist/component/ToddleComponent.js.map +1 -0
- package/dist/component/actionUtils.d.ts +2 -0
- package/dist/component/actionUtils.js +56 -0
- package/dist/component/actionUtils.js.map +1 -0
- package/dist/component/component.types.d.ts +313 -0
- package/dist/component/component.types.js +9 -0
- package/dist/component/component.types.js.map +1 -0
- package/dist/component/isPageComponent.d.ts +2 -0
- package/dist/component/isPageComponent.js +3 -0
- package/dist/component/isPageComponent.js.map +1 -0
- package/dist/formula/ToddleFormula.d.ts +15 -0
- package/dist/formula/ToddleFormula.js +20 -0
- package/dist/formula/ToddleFormula.js.map +1 -0
- package/dist/formula/formula.d.ts +103 -0
- package/dist/formula/formula.js +186 -0
- package/dist/formula/formula.js.map +1 -0
- package/dist/formula/formulaTypes.d.ts +30 -0
- package/dist/formula/formulaTypes.js +2 -0
- package/dist/formula/formulaTypes.js.map +1 -0
- package/dist/formula/formulaUtils.d.ts +18 -0
- package/dist/formula/formulaUtils.js +240 -0
- package/dist/formula/formulaUtils.js.map +1 -0
- package/dist/styling/className.d.ts +2 -0
- package/dist/styling/className.js +52 -0
- package/dist/styling/className.js.map +1 -0
- package/dist/styling/style.css.d.ts +5 -0
- package/dist/styling/style.css.js +242 -0
- package/dist/styling/style.css.js.map +1 -0
- package/dist/styling/theme.const.d.ts +3 -0
- package/dist/styling/theme.const.js +381 -0
- package/dist/styling/theme.const.js.map +1 -0
- package/dist/styling/theme.d.ts +72 -0
- package/dist/styling/theme.js +200 -0
- package/dist/styling/theme.js.map +1 -0
- package/dist/styling/variantSelector.d.ts +123 -0
- package/dist/styling/variantSelector.js +25 -0
- package/dist/styling/variantSelector.js.map +1 -0
- package/dist/types.d.ts +97 -0
- package/dist/types.js +2 -0
- package/dist/types.js.map +1 -0
- package/dist/utils/collections.d.ts +21 -0
- package/dist/utils/collections.js +75 -0
- package/dist/utils/collections.js.map +1 -0
- package/dist/utils/customElements.d.ts +6 -0
- package/dist/utils/customElements.js +15 -0
- package/dist/utils/customElements.js.map +1 -0
- package/dist/utils/hash.d.ts +1 -0
- package/dist/utils/hash.js +17 -0
- package/dist/utils/hash.js.map +1 -0
- package/dist/utils/json.d.ts +5 -0
- package/dist/utils/json.js +16 -0
- package/dist/utils/json.js.map +1 -0
- package/dist/utils/sha1.d.ts +2 -0
- package/dist/utils/sha1.js +13 -0
- package/dist/utils/sha1.js.map +1 -0
- package/dist/utils/url.d.ts +5 -0
- package/dist/utils/url.js +27 -0
- package/dist/utils/url.js.map +1 -0
- package/dist/utils/util.d.ts +3 -0
- package/dist/utils/util.js +4 -0
- package/dist/utils/util.js.map +1 -0
- package/package.json +18 -0
- package/src/api/LegacyToddleApi.ts +205 -0
- package/src/api/ToddleApiV2.ts +331 -0
- package/src/api/api.test.ts +319 -0
- package/src/api/api.ts +414 -0
- package/src/api/apiTypes.ts +145 -0
- package/src/api/headers.ts +41 -0
- package/src/api/template.ts +10 -0
- package/src/component/ToddleComponent.actionReferences.test.ts +75 -0
- package/src/component/ToddleComponent.formulasInComponent.test.ts +234 -0
- package/src/component/ToddleComponent.ts +470 -0
- package/src/component/actionUtils.ts +61 -0
- package/src/component/component.types.ts +362 -0
- package/src/component/isPageComponent.ts +6 -0
- package/src/formula/ToddleFormula.ts +30 -0
- package/src/formula/formula.ts +355 -0
- package/src/formula/formulaTypes.ts +45 -0
- package/src/formula/formulaUtils.ts +287 -0
- package/src/styling/className.test.ts +19 -0
- package/src/styling/className.ts +73 -0
- package/src/styling/style.css.ts +309 -0
- package/src/styling/theme.const.ts +390 -0
- package/src/styling/theme.ts +339 -0
- package/src/styling/variantSelector.ts +168 -0
- package/src/types.ts +158 -0
- package/src/utils/collections.test.ts +57 -0
- package/src/utils/collections.ts +122 -0
- package/src/utils/customElements.test.ts +40 -0
- package/src/utils/customElements.ts +16 -0
- package/src/utils/hash.test.ts +32 -0
- package/src/utils/hash.ts +18 -0
- package/src/utils/json.ts +18 -0
- package/src/utils/sha1.test.ts +50 -0
- package/src/utils/sha1.ts +17 -0
- package/src/utils/url.test.ts +17 -0
- package/src/utils/url.ts +33 -0
- package/src/utils/util.ts +8 -0
package/src/api/api.ts
ADDED
|
@@ -0,0 +1,414 @@
|
|
|
1
|
+
import type { Formula, FormulaContext } from '../formula/formula'
|
|
2
|
+
import { applyFormula } from '../formula/formula'
|
|
3
|
+
import { omitKeys, sortObjectEntries } from '../utils/collections'
|
|
4
|
+
import { hash } from '../utils/hash'
|
|
5
|
+
import { isDefined, isObject, toBoolean } from '../utils/util'
|
|
6
|
+
import type {
|
|
7
|
+
ApiBase,
|
|
8
|
+
ApiPerformance,
|
|
9
|
+
ApiRequest,
|
|
10
|
+
ComponentAPI,
|
|
11
|
+
LegacyComponentAPI,
|
|
12
|
+
ToddleRequestInit,
|
|
13
|
+
} from './apiTypes'
|
|
14
|
+
import { ApiMethod } from './apiTypes'
|
|
15
|
+
import { LegacyToddleApi } from './LegacyToddleApi'
|
|
16
|
+
import { ToddleApiV2 } from './ToddleApiV2'
|
|
17
|
+
|
|
18
|
+
export const NON_BODY_RESPONSE_CODES = [101, 204, 205, 304]
|
|
19
|
+
|
|
20
|
+
export const isLegacyApi = <Handler>(
|
|
21
|
+
api: ComponentAPI | LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
|
|
22
|
+
): api is LegacyComponentAPI | LegacyToddleApi<Handler> =>
|
|
23
|
+
api instanceof LegacyToddleApi ? true : !('version' in api)
|
|
24
|
+
|
|
25
|
+
export const createApiRequest = <Handler>({
|
|
26
|
+
api,
|
|
27
|
+
formulaContext,
|
|
28
|
+
baseUrl,
|
|
29
|
+
defaultHeaders,
|
|
30
|
+
}: {
|
|
31
|
+
api: ApiRequest | ToddleApiV2<Handler>
|
|
32
|
+
formulaContext: FormulaContext
|
|
33
|
+
baseUrl?: string
|
|
34
|
+
defaultHeaders: Headers | undefined
|
|
35
|
+
}) => {
|
|
36
|
+
const url = getUrl(api, formulaContext, baseUrl)
|
|
37
|
+
const requestSettings = getRequestSettings({
|
|
38
|
+
api,
|
|
39
|
+
formulaContext,
|
|
40
|
+
defaultHeaders,
|
|
41
|
+
})
|
|
42
|
+
|
|
43
|
+
return { url, requestSettings }
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export const getUrl = (
|
|
47
|
+
api: ApiBase,
|
|
48
|
+
formulaContext: FormulaContext,
|
|
49
|
+
baseUrl?: string,
|
|
50
|
+
): URL => {
|
|
51
|
+
let urlPathname = ''
|
|
52
|
+
let urlQueryParams = new URLSearchParams()
|
|
53
|
+
let parsedUrl: URL | undefined
|
|
54
|
+
const url = applyFormula(api.url, formulaContext)
|
|
55
|
+
if (['string', 'number'].includes(typeof url)) {
|
|
56
|
+
const urlInput = typeof url === 'number' ? String(url) : url
|
|
57
|
+
try {
|
|
58
|
+
// Try to parse the URL to extract potential path and query parameters
|
|
59
|
+
parsedUrl = new URL(urlInput, baseUrl)
|
|
60
|
+
urlPathname = parsedUrl.pathname
|
|
61
|
+
urlQueryParams = parsedUrl.searchParams
|
|
62
|
+
// eslint-disable-next-line no-empty
|
|
63
|
+
} catch {}
|
|
64
|
+
}
|
|
65
|
+
const pathParams = getRequestPath(api.path, formulaContext)
|
|
66
|
+
// Combine potential path parameters from the url declaration with the actual path parameters
|
|
67
|
+
const path = `${urlPathname}${pathParams.length > 0 && !urlPathname.endsWith('/') ? '/' : ''}${pathParams}`
|
|
68
|
+
// Combine potential query parameters from the url declaration with the actual query parameters
|
|
69
|
+
const queryParams = new URLSearchParams([
|
|
70
|
+
...urlQueryParams,
|
|
71
|
+
...getRequestQueryParams(api.queryParams, formulaContext),
|
|
72
|
+
])
|
|
73
|
+
const queryString =
|
|
74
|
+
[...queryParams.entries()].length > 0 ? `?${queryParams.toString()}` : ''
|
|
75
|
+
if (parsedUrl) {
|
|
76
|
+
const combinedUrl = new URL(parsedUrl.origin, baseUrl)
|
|
77
|
+
combinedUrl.pathname = path
|
|
78
|
+
combinedUrl.search = queryParams.toString()
|
|
79
|
+
return combinedUrl
|
|
80
|
+
} else {
|
|
81
|
+
return new URL(`${path}${queryString}`, baseUrl)
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
const HttpMethodsWithAllowedBody: ApiMethod[] = [
|
|
86
|
+
ApiMethod.POST,
|
|
87
|
+
ApiMethod.DELETE,
|
|
88
|
+
ApiMethod.PUT,
|
|
89
|
+
ApiMethod.PATCH,
|
|
90
|
+
ApiMethod.OPTIONS,
|
|
91
|
+
]
|
|
92
|
+
|
|
93
|
+
export const applyAbortSignal = (
|
|
94
|
+
api: ApiRequest,
|
|
95
|
+
requestSettings: RequestInit,
|
|
96
|
+
formulaContext: FormulaContext,
|
|
97
|
+
) => {
|
|
98
|
+
if (api.timeout) {
|
|
99
|
+
const timeout = applyFormula(api.timeout.formula, formulaContext)
|
|
100
|
+
if (typeof timeout === 'number' && !Number.isNaN(timeout) && timeout > 0) {
|
|
101
|
+
requestSettings.signal = AbortSignal.timeout(timeout)
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const getRequestSettings = ({
|
|
107
|
+
api,
|
|
108
|
+
formulaContext,
|
|
109
|
+
defaultHeaders,
|
|
110
|
+
}: {
|
|
111
|
+
api: ApiRequest
|
|
112
|
+
formulaContext: FormulaContext
|
|
113
|
+
defaultHeaders: Headers | undefined
|
|
114
|
+
}): ToddleRequestInit => {
|
|
115
|
+
const method = Object.values(ApiMethod).includes(api.method as ApiMethod)
|
|
116
|
+
? (api.method as ApiMethod)
|
|
117
|
+
: ApiMethod.GET
|
|
118
|
+
const headers = getRequestHeaders({
|
|
119
|
+
apiHeaders: api.headers,
|
|
120
|
+
formulaContext,
|
|
121
|
+
defaultHeaders,
|
|
122
|
+
})
|
|
123
|
+
const body = getRequestBody({ api, formulaContext, headers, method })
|
|
124
|
+
if (headers.get('content-type') === 'multipart/form-data') {
|
|
125
|
+
headers.delete('content-type')
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const requestSettings: ToddleRequestInit = {
|
|
129
|
+
method,
|
|
130
|
+
headers,
|
|
131
|
+
body,
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
applyAbortSignal(api, requestSettings, formulaContext)
|
|
135
|
+
|
|
136
|
+
return requestSettings
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export const getRequestPath = (
|
|
140
|
+
path: ApiRequest['path'],
|
|
141
|
+
formulaContext: FormulaContext,
|
|
142
|
+
): string =>
|
|
143
|
+
sortObjectEntries(path ?? {}, ([_, p]) => p.index)
|
|
144
|
+
.map(([_, p]) => applyFormula(p.formula, formulaContext))
|
|
145
|
+
.join('/')
|
|
146
|
+
|
|
147
|
+
export const getRequestQueryParams = (
|
|
148
|
+
params: ApiRequest['queryParams'],
|
|
149
|
+
formulaContext: FormulaContext,
|
|
150
|
+
): URLSearchParams => {
|
|
151
|
+
const queryParams = new URLSearchParams()
|
|
152
|
+
Object.entries(params ?? {}).forEach(([key, param]) => {
|
|
153
|
+
const enabled = isDefined(param.enabled)
|
|
154
|
+
? applyFormula(param.enabled, formulaContext)
|
|
155
|
+
: true
|
|
156
|
+
if (!enabled) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const value = applyFormula(param.formula, formulaContext)
|
|
161
|
+
if (!isDefined(value)) {
|
|
162
|
+
// Ignore null/undefined values
|
|
163
|
+
return
|
|
164
|
+
}
|
|
165
|
+
if (Array.isArray(value)) {
|
|
166
|
+
// Support encoding 1-dimensional arrays
|
|
167
|
+
value.forEach((v) => queryParams.append(key, String(v)))
|
|
168
|
+
} else if (isObject(value)) {
|
|
169
|
+
// Support encoding (nested) objects, but cast any non-object to a String
|
|
170
|
+
const encodeObject = (obj: Record<string, any>, prefix: string) => {
|
|
171
|
+
Object.entries(obj).forEach(([key, val]) => {
|
|
172
|
+
if (!Array.isArray(val) && isObject(val)) {
|
|
173
|
+
return encodeObject(val, `${prefix}[${key}]`)
|
|
174
|
+
} else {
|
|
175
|
+
queryParams.set(`${prefix}[${key}]`, String(val))
|
|
176
|
+
}
|
|
177
|
+
})
|
|
178
|
+
}
|
|
179
|
+
encodeObject(value, key)
|
|
180
|
+
} else {
|
|
181
|
+
queryParams.set(key, String(value))
|
|
182
|
+
}
|
|
183
|
+
})
|
|
184
|
+
return queryParams
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export const getRequestHeaders = ({
|
|
188
|
+
apiHeaders,
|
|
189
|
+
formulaContext,
|
|
190
|
+
defaultHeaders,
|
|
191
|
+
}: {
|
|
192
|
+
apiHeaders: ApiRequest['headers']
|
|
193
|
+
formulaContext: FormulaContext
|
|
194
|
+
defaultHeaders: Headers | undefined
|
|
195
|
+
}) => {
|
|
196
|
+
const headers = new Headers(defaultHeaders)
|
|
197
|
+
Object.entries(apiHeaders ?? {}).forEach(([key, param]) => {
|
|
198
|
+
const enabled = isDefined(param.enabled)
|
|
199
|
+
? applyFormula(param.enabled, formulaContext)
|
|
200
|
+
: true
|
|
201
|
+
if (enabled) {
|
|
202
|
+
const value = applyFormula(param.formula, formulaContext)
|
|
203
|
+
if (isDefined(value)) {
|
|
204
|
+
try {
|
|
205
|
+
headers.set(
|
|
206
|
+
key.trim(),
|
|
207
|
+
(typeof value === 'string' ? value : String(value)).trim(),
|
|
208
|
+
)
|
|
209
|
+
// eslint-disable-next-line no-empty
|
|
210
|
+
} catch {}
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
})
|
|
214
|
+
return headers
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export const getBaseUrl = ({
|
|
218
|
+
origin,
|
|
219
|
+
url,
|
|
220
|
+
}: {
|
|
221
|
+
origin: string
|
|
222
|
+
url?: string
|
|
223
|
+
}) =>
|
|
224
|
+
!isDefined(url) || url === '' || url.startsWith('/') ? origin + url : url
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Calculate the hash of a Request object based on its properties
|
|
228
|
+
*/
|
|
229
|
+
export const requestHash = (url: URL, request: RequestInit) =>
|
|
230
|
+
hash(
|
|
231
|
+
JSON.stringify({
|
|
232
|
+
url: url.href,
|
|
233
|
+
method: request.method,
|
|
234
|
+
headers: omitKeys(
|
|
235
|
+
Object.fromEntries(Object.entries(request.headers ?? {})),
|
|
236
|
+
['host', 'cookie'],
|
|
237
|
+
),
|
|
238
|
+
body: request.body ?? null,
|
|
239
|
+
}),
|
|
240
|
+
)
|
|
241
|
+
|
|
242
|
+
export const isApiError = ({
|
|
243
|
+
apiName,
|
|
244
|
+
response,
|
|
245
|
+
formulaContext,
|
|
246
|
+
errorFormula,
|
|
247
|
+
performance,
|
|
248
|
+
}: {
|
|
249
|
+
apiName: string
|
|
250
|
+
response: {
|
|
251
|
+
ok: boolean
|
|
252
|
+
status?: number
|
|
253
|
+
headers?: Record<string, string> | null
|
|
254
|
+
body: unknown
|
|
255
|
+
}
|
|
256
|
+
formulaContext: FormulaContext
|
|
257
|
+
performance: ApiPerformance
|
|
258
|
+
errorFormula?: { formula: Formula } | null
|
|
259
|
+
}) => {
|
|
260
|
+
const errorFormulaRes = errorFormula
|
|
261
|
+
? applyFormula(errorFormula.formula, {
|
|
262
|
+
component: formulaContext.component,
|
|
263
|
+
package: formulaContext.package,
|
|
264
|
+
toddle: formulaContext.toddle,
|
|
265
|
+
data: {
|
|
266
|
+
Attributes: {},
|
|
267
|
+
Args: formulaContext.data.Args,
|
|
268
|
+
Apis: {
|
|
269
|
+
// The errorFormula will only have access to the data of the current API
|
|
270
|
+
[apiName]: {
|
|
271
|
+
isLoading: false,
|
|
272
|
+
data: response.body,
|
|
273
|
+
error: null,
|
|
274
|
+
response: {
|
|
275
|
+
status: response.status,
|
|
276
|
+
headers: response.headers,
|
|
277
|
+
performance,
|
|
278
|
+
},
|
|
279
|
+
},
|
|
280
|
+
},
|
|
281
|
+
},
|
|
282
|
+
env: formulaContext.env,
|
|
283
|
+
})
|
|
284
|
+
: null
|
|
285
|
+
|
|
286
|
+
if (errorFormulaRes === null || errorFormulaRes === undefined) {
|
|
287
|
+
return !response.ok
|
|
288
|
+
}
|
|
289
|
+
return toBoolean(errorFormulaRes)
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
const getRequestBody = ({
|
|
293
|
+
api,
|
|
294
|
+
formulaContext,
|
|
295
|
+
headers,
|
|
296
|
+
method,
|
|
297
|
+
}: {
|
|
298
|
+
api: ApiRequest
|
|
299
|
+
formulaContext: FormulaContext
|
|
300
|
+
headers: Headers
|
|
301
|
+
method: ApiMethod
|
|
302
|
+
}): FormData | string | undefined => {
|
|
303
|
+
if (!api.body || !HttpMethodsWithAllowedBody.includes(method)) {
|
|
304
|
+
return
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const body = applyFormula(api.body, formulaContext)
|
|
308
|
+
if (!body) {
|
|
309
|
+
return
|
|
310
|
+
}
|
|
311
|
+
switch (headers.get('content-type')) {
|
|
312
|
+
case 'application/x-www-form-urlencoded': {
|
|
313
|
+
if (typeof body === 'object' && body !== null) {
|
|
314
|
+
return Object.entries(body)
|
|
315
|
+
.map(([key, value]) => {
|
|
316
|
+
if (Array.isArray(value)) {
|
|
317
|
+
return value
|
|
318
|
+
.map(
|
|
319
|
+
(v) => `${encodeURIComponent(key)}=${encodeURIComponent(v)}`,
|
|
320
|
+
)
|
|
321
|
+
.join('&')
|
|
322
|
+
} else {
|
|
323
|
+
return `${encodeURIComponent(key)}=${encodeURIComponent(
|
|
324
|
+
String(value),
|
|
325
|
+
)}`
|
|
326
|
+
}
|
|
327
|
+
})
|
|
328
|
+
.join('&')
|
|
329
|
+
}
|
|
330
|
+
return ''
|
|
331
|
+
}
|
|
332
|
+
case 'multipart/form-data': {
|
|
333
|
+
const formData = new FormData()
|
|
334
|
+
if (typeof body === 'object' && body !== null) {
|
|
335
|
+
Object.entries(body).forEach(([key, value]) => {
|
|
336
|
+
formData.set(key, value as string | Blob)
|
|
337
|
+
})
|
|
338
|
+
}
|
|
339
|
+
return formData
|
|
340
|
+
}
|
|
341
|
+
case 'text/plain':
|
|
342
|
+
return String(body)
|
|
343
|
+
default:
|
|
344
|
+
return JSON.stringify(body)
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
export const createApiEvent = (
|
|
349
|
+
eventName: 'message' | 'success' | 'failed',
|
|
350
|
+
detail: any,
|
|
351
|
+
) => {
|
|
352
|
+
return new CustomEvent(eventName, {
|
|
353
|
+
detail,
|
|
354
|
+
})
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
const compareApiDependencies = <Handler>(
|
|
358
|
+
a: LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
|
|
359
|
+
b: LegacyToddleApi<Handler> | ToddleApiV2<Handler>,
|
|
360
|
+
) => {
|
|
361
|
+
const isADependentOnB = a.apiReferences.has(b.name)
|
|
362
|
+
const isBDependentOnA = b.apiReferences.has(a.name)
|
|
363
|
+
if (isADependentOnB === isBDependentOnA) {
|
|
364
|
+
return 0
|
|
365
|
+
}
|
|
366
|
+
// 1 means A goes last - hence B is evaluated before A
|
|
367
|
+
return isADependentOnB ? 1 : -1
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
export const sortApiObjects = <Handler>(
|
|
371
|
+
apis: Array<[string, ComponentAPI]>,
|
|
372
|
+
) => {
|
|
373
|
+
const apiMap = new Map<
|
|
374
|
+
string,
|
|
375
|
+
LegacyToddleApi<Handler> | ToddleApiV2<Handler>
|
|
376
|
+
>()
|
|
377
|
+
const getApi = (apiObj: ComponentAPI, key: string) => {
|
|
378
|
+
let api = apiMap.get(key)
|
|
379
|
+
if (!api) {
|
|
380
|
+
api =
|
|
381
|
+
apiObj.version === 2
|
|
382
|
+
? new ToddleApiV2<Handler>(
|
|
383
|
+
apiObj,
|
|
384
|
+
key,
|
|
385
|
+
// global formulas are not required for sorting
|
|
386
|
+
{
|
|
387
|
+
formulas: {},
|
|
388
|
+
packages: {},
|
|
389
|
+
},
|
|
390
|
+
)
|
|
391
|
+
: new LegacyToddleApi<Handler>(
|
|
392
|
+
apiObj,
|
|
393
|
+
key,
|
|
394
|
+
// global formulas are not required for sorting
|
|
395
|
+
{
|
|
396
|
+
formulas: {},
|
|
397
|
+
packages: {},
|
|
398
|
+
},
|
|
399
|
+
)
|
|
400
|
+
apiMap.set(key, api)
|
|
401
|
+
}
|
|
402
|
+
return api
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
return [...apis].sort(([aKey, aObj], [bKey, bObj]) => {
|
|
406
|
+
const a = getApi(aObj, aKey)
|
|
407
|
+
const b = getApi(bObj, bKey)
|
|
408
|
+
return compareApiDependencies(a, b)
|
|
409
|
+
})
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
export const sortApiEntries = <Handler>(
|
|
413
|
+
apis: Array<[string, LegacyToddleApi<Handler> | ToddleApiV2<Handler>]>,
|
|
414
|
+
) => [...apis].sort(([_, a], [__, b]) => compareApiDependencies(a, b))
|
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
import type { EventModel } from '../component/component.types'
|
|
2
|
+
import type { Formula } from '../formula/formula'
|
|
3
|
+
import type { ToddleMetadata } from '../types'
|
|
4
|
+
|
|
5
|
+
export type ComponentAPI = LegacyComponentAPI | ApiRequest
|
|
6
|
+
|
|
7
|
+
export interface LegacyComponentAPI {
|
|
8
|
+
name: string
|
|
9
|
+
type: 'REST'
|
|
10
|
+
autoFetch?: Formula | null
|
|
11
|
+
url?: Formula
|
|
12
|
+
path?: { formula: Formula }[]
|
|
13
|
+
proxy?: boolean
|
|
14
|
+
queryParams?: Record<string, { name: string; formula: Formula }>
|
|
15
|
+
headers?: Record<string, Formula> | Formula
|
|
16
|
+
method?: 'GET' | 'POST' | 'DELETE' | 'PUT'
|
|
17
|
+
body?: Formula
|
|
18
|
+
auth?: {
|
|
19
|
+
type: 'Bearer id_token' | 'Bearer access_token'
|
|
20
|
+
}
|
|
21
|
+
throttle?: number | null
|
|
22
|
+
debounce?: number | null
|
|
23
|
+
onCompleted: EventModel | null
|
|
24
|
+
onFailed: EventModel | null
|
|
25
|
+
version?: never
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface LegacyApiStatus {
|
|
29
|
+
data: unknown
|
|
30
|
+
isLoading: boolean
|
|
31
|
+
error: unknown
|
|
32
|
+
response?: never
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export enum ApiMethod {
|
|
36
|
+
GET = 'GET',
|
|
37
|
+
POST = 'POST',
|
|
38
|
+
DELETE = 'DELETE',
|
|
39
|
+
PUT = 'PUT',
|
|
40
|
+
PATCH = 'PATCH',
|
|
41
|
+
HEAD = 'HEAD',
|
|
42
|
+
OPTIONS = 'OPTIONS',
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export type RedirectStatusCode = 300 | 301 | 302 | 303 | 304 | 307 | 308
|
|
46
|
+
|
|
47
|
+
export type ApiParserMode =
|
|
48
|
+
| 'auto'
|
|
49
|
+
| 'text'
|
|
50
|
+
| 'json'
|
|
51
|
+
| 'event-stream'
|
|
52
|
+
| 'json-stream'
|
|
53
|
+
| 'blob'
|
|
54
|
+
|
|
55
|
+
export interface ApiBase extends ToddleMetadata {
|
|
56
|
+
url?: Formula
|
|
57
|
+
path?: Record<string, { formula: Formula; index: number }>
|
|
58
|
+
queryParams?: Record<
|
|
59
|
+
string,
|
|
60
|
+
// The enabled formula is used to determine if the query parameter should be included in the request or not
|
|
61
|
+
{ formula: Formula; enabled?: Formula | null }
|
|
62
|
+
>
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export interface ApiRequest extends ApiBase {
|
|
66
|
+
version: 2
|
|
67
|
+
name: string
|
|
68
|
+
type: 'http' | 'ws' // The structure for web sockets might look different
|
|
69
|
+
autoFetch?: Formula | null
|
|
70
|
+
headers?: Record<string, { formula: Formula; enabled?: Formula | null }>
|
|
71
|
+
method?: ApiMethod
|
|
72
|
+
body?: Formula
|
|
73
|
+
// inputs for an API request
|
|
74
|
+
inputs: Record<string, { formula: Formula | null }>
|
|
75
|
+
service?: string | null
|
|
76
|
+
servicePath?: string | null
|
|
77
|
+
server?: {
|
|
78
|
+
// We should only accept server side proxy requests if proxy is defined
|
|
79
|
+
proxy?: {
|
|
80
|
+
enabled: { formula: Formula }
|
|
81
|
+
} | null
|
|
82
|
+
ssr?: {
|
|
83
|
+
// We should only fetch a request server side during SSR if this is true
|
|
84
|
+
// it should probably be true by default for autofetch APIs
|
|
85
|
+
// During SSR we will only fetch autoFetch requests
|
|
86
|
+
enabled?: { formula: Formula } | null
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
client?: {
|
|
90
|
+
debounce?: { formula: Formula } | null
|
|
91
|
+
onCompleted?: EventModel | null
|
|
92
|
+
onFailed?: EventModel | null
|
|
93
|
+
onMessage?: EventModel | null
|
|
94
|
+
// parserMode is used to determine how the response should be handled
|
|
95
|
+
// auto: The response will be handled based on the content type of the response
|
|
96
|
+
// stream: The response will be handled as a stream
|
|
97
|
+
parserMode: ApiParserMode
|
|
98
|
+
}
|
|
99
|
+
// Shared logic for client/server 👇
|
|
100
|
+
// The user could distinguish using an environment
|
|
101
|
+
// variable e.g. IS_SERVER when they declare the formula
|
|
102
|
+
|
|
103
|
+
// Rules for redirecting the user to a different page
|
|
104
|
+
// These rules will run both on server+client - mostly used for 401 response -> 302 redirect
|
|
105
|
+
// We allow multiple rules since it makes it easier to setup multiple conditions/redirect locations
|
|
106
|
+
redirectRules?: Record<
|
|
107
|
+
string,
|
|
108
|
+
{
|
|
109
|
+
// The formula will receive the response from the server including a status code
|
|
110
|
+
// A redirect response will be returned if the formula returns a valid url
|
|
111
|
+
formula: Formula
|
|
112
|
+
// The status code used in the redirect response. Only relevant server side
|
|
113
|
+
statusCode?: RedirectStatusCode | null
|
|
114
|
+
index: number
|
|
115
|
+
}
|
|
116
|
+
> | null
|
|
117
|
+
// Formula for determining whether the response is an error or not. Receives the API response + status code/headers as input
|
|
118
|
+
// The response is considered an error if the formula returns true
|
|
119
|
+
// Default behavior is to check if the status code is 400 or higher
|
|
120
|
+
isError?: { formula: Formula } | null
|
|
121
|
+
// Formula for determining when the request should time out
|
|
122
|
+
timeout?: { formula: Formula } | null
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
export interface ApiStatus {
|
|
126
|
+
data: unknown
|
|
127
|
+
isLoading: boolean
|
|
128
|
+
error: unknown
|
|
129
|
+
response?: {
|
|
130
|
+
status?: number
|
|
131
|
+
headers?: Record<string, string> | null
|
|
132
|
+
performance?: ApiPerformance
|
|
133
|
+
debug?: null | unknown
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
export interface ApiPerformance {
|
|
138
|
+
requestStart: number | null
|
|
139
|
+
responseStart: number | null
|
|
140
|
+
responseEnd: number | null
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface ToddleRequestInit extends RequestInit {
|
|
144
|
+
headers: Headers
|
|
145
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Checks if a header is a json (content-type) header
|
|
3
|
+
* Also supports edge cases like application/vnd.api+json and application/vnd.contentful.delivery.v1+json
|
|
4
|
+
* See https://jsonapi.org/#mime-types
|
|
5
|
+
*/
|
|
6
|
+
export const isJsonHeader = (header?: string | null) => {
|
|
7
|
+
if (typeof header !== 'string') {
|
|
8
|
+
return false
|
|
9
|
+
}
|
|
10
|
+
return /^application\/(json|.*\+json)/.test(header)
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const isTextHeader = (header?: string | null) => {
|
|
14
|
+
if (typeof header !== 'string') {
|
|
15
|
+
return false
|
|
16
|
+
}
|
|
17
|
+
return /^(text\/|application\/x-www-form-urlencoded|application\/(xml|.*\+xml))/.test(
|
|
18
|
+
header,
|
|
19
|
+
)
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export const isEventStreamHeader = (header?: string | null) => {
|
|
23
|
+
if (typeof header !== 'string') {
|
|
24
|
+
return false
|
|
25
|
+
}
|
|
26
|
+
return /^text\/event-stream/.test(header)
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export const isJsonStreamHeader = (header?: string | null) => {
|
|
30
|
+
if (typeof header !== 'string') {
|
|
31
|
+
return false
|
|
32
|
+
}
|
|
33
|
+
return /^(application\/stream\+json|application\/x-ndjson)/.test(header)
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export const isImageHeader = (header?: string | null) => {
|
|
37
|
+
if (typeof header !== 'string') {
|
|
38
|
+
return false
|
|
39
|
+
}
|
|
40
|
+
return /^image\//.test(header)
|
|
41
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { ToddleComponent } from './ToddleComponent'
|
|
2
|
+
|
|
3
|
+
describe('ToddleComponent.actionReferences', () => {
|
|
4
|
+
test('it return custom actions used in the component', () => {
|
|
5
|
+
const demo = new ToddleComponent({
|
|
6
|
+
component: {
|
|
7
|
+
name: 'demo',
|
|
8
|
+
apis: {},
|
|
9
|
+
attributes: {},
|
|
10
|
+
nodes: {},
|
|
11
|
+
variables: {},
|
|
12
|
+
workflows: {
|
|
13
|
+
'7XLoA3': {
|
|
14
|
+
name: 'my-workflow',
|
|
15
|
+
actions: [
|
|
16
|
+
{
|
|
17
|
+
type: 'Custom',
|
|
18
|
+
name: 'MyCustomAction',
|
|
19
|
+
},
|
|
20
|
+
{
|
|
21
|
+
name: 'MyLegacyCustomAction',
|
|
22
|
+
},
|
|
23
|
+
],
|
|
24
|
+
parameters: [],
|
|
25
|
+
},
|
|
26
|
+
},
|
|
27
|
+
},
|
|
28
|
+
getComponent: () => undefined,
|
|
29
|
+
packageName: 'demo',
|
|
30
|
+
globalFormulas: { formulas: {}, packages: {} },
|
|
31
|
+
})
|
|
32
|
+
const actions = Array.from(demo.actionReferences)
|
|
33
|
+
expect(actions).toEqual(['MyCustomAction', 'MyLegacyCustomAction'])
|
|
34
|
+
})
|
|
35
|
+
test('it should not include non-custom actions', () => {
|
|
36
|
+
const demo = new ToddleComponent({
|
|
37
|
+
component: {
|
|
38
|
+
name: 'demo',
|
|
39
|
+
apis: {},
|
|
40
|
+
attributes: {},
|
|
41
|
+
nodes: {},
|
|
42
|
+
variables: {},
|
|
43
|
+
workflows: {
|
|
44
|
+
'7XLoA3': {
|
|
45
|
+
name: 'my-workflow',
|
|
46
|
+
actions: [
|
|
47
|
+
{
|
|
48
|
+
type: 'SetVariable',
|
|
49
|
+
data: {
|
|
50
|
+
type: 'value',
|
|
51
|
+
value: 'Hello World',
|
|
52
|
+
},
|
|
53
|
+
variable: 'my-variable',
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
type: 'TriggerEvent',
|
|
57
|
+
event: 'my-event',
|
|
58
|
+
data: {
|
|
59
|
+
type: 'value',
|
|
60
|
+
value: 'Hello World',
|
|
61
|
+
},
|
|
62
|
+
},
|
|
63
|
+
],
|
|
64
|
+
parameters: [],
|
|
65
|
+
},
|
|
66
|
+
},
|
|
67
|
+
},
|
|
68
|
+
getComponent: () => undefined,
|
|
69
|
+
packageName: 'demo',
|
|
70
|
+
globalFormulas: { formulas: {}, packages: {} },
|
|
71
|
+
})
|
|
72
|
+
const actions = Array.from(demo.actionReferences)
|
|
73
|
+
expect(actions).toEqual([])
|
|
74
|
+
})
|
|
75
|
+
})
|