fusion-framework 1.2.2 → 1.2.4
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.
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/index.js
CHANGED
|
@@ -134,7 +134,7 @@ header.fingerprint = () =>
|
|
|
134
134
|
: {
|
|
135
135
|
'X-Powered-By': 'Fusion Framework',
|
|
136
136
|
'X-Framework': 'Fusion',
|
|
137
|
-
['X-Fusion-Version']: '1.2.
|
|
137
|
+
['X-Fusion-Version']: '1.2.4',
|
|
138
138
|
}
|
|
139
139
|
|
|
140
140
|
function isThenable(value) {
|
|
@@ -219,6 +219,121 @@ function resolveRoutePath(routePath, ApiClass) {
|
|
|
219
219
|
return native.resolveRoutePathJs(routePath, ApiClass.name)
|
|
220
220
|
}
|
|
221
221
|
|
|
222
|
+
function apiActionName(methodName) {
|
|
223
|
+
if (typeof native.apiActionNameJs === 'function') {
|
|
224
|
+
return native.apiActionNameJs(methodName)
|
|
225
|
+
}
|
|
226
|
+
let stem = methodName
|
|
227
|
+
if (methodName.endsWith('Action') && methodName.length > 6) stem = methodName.slice(0, -6)
|
|
228
|
+
else if (methodName.endsWith('ACTION') && methodName.length > 6) stem = methodName.slice(0, -6)
|
|
229
|
+
return stem.toLowerCase()
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function resolveMethodRoutePath(template, className, methodName) {
|
|
233
|
+
if (typeof native.resolveMethodRoutePathJs === 'function') {
|
|
234
|
+
return native.resolveMethodRoutePathJs(template, className, methodName)
|
|
235
|
+
}
|
|
236
|
+
return resolveRoutePath(template, { name: className }).replace(
|
|
237
|
+
/\[action\]/g,
|
|
238
|
+
apiActionName(methodName),
|
|
239
|
+
)
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
function joinRoutePaths(base, segment) {
|
|
243
|
+
const left = String(base || '').replace(/\/+$/, '')
|
|
244
|
+
const right = String(segment || '').replace(/^\/+|\/+$/g, '')
|
|
245
|
+
if (!right) return left || '/'
|
|
246
|
+
if (!left) return `/${right}`
|
|
247
|
+
return `${left}/${right}`
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function resolveHandlerRoute(classBasePath, template, className, methodName) {
|
|
251
|
+
if (typeof native.resolveHandlerRouteJs === 'function') {
|
|
252
|
+
return native.resolveHandlerRouteJs(classBasePath, template, className, methodName)
|
|
253
|
+
}
|
|
254
|
+
const resolved = resolveMethodRoutePath(template, className, methodName)
|
|
255
|
+
if (String(template).startsWith('/')) {
|
|
256
|
+
return joinRoutePaths('', resolved.replace(/^\/+/, ''))
|
|
257
|
+
}
|
|
258
|
+
return joinRoutePaths(classBasePath, resolved)
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
function httpRoute(method, route, options = {}) {
|
|
262
|
+
const meta = {
|
|
263
|
+
method: String(method).toLowerCase(),
|
|
264
|
+
template: route,
|
|
265
|
+
tags: Array.isArray(options.tags) ? options.tags : [],
|
|
266
|
+
desc: options.desc ?? null,
|
|
267
|
+
title: options.title ?? null,
|
|
268
|
+
deprecated: !!options.deprecated,
|
|
269
|
+
}
|
|
270
|
+
function wrap(fn) {
|
|
271
|
+
fn.__fusionHttpRoute = meta
|
|
272
|
+
return fn
|
|
273
|
+
}
|
|
274
|
+
return wrap
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
function httpGet(route, options = {}) {
|
|
278
|
+
return httpRoute('get', route, options)
|
|
279
|
+
}
|
|
280
|
+
function httpPost(route, options = {}) {
|
|
281
|
+
return httpRoute('post', route, options)
|
|
282
|
+
}
|
|
283
|
+
function httpPut(route, options = {}) {
|
|
284
|
+
return httpRoute('put', route, options)
|
|
285
|
+
}
|
|
286
|
+
function httpPatch(route, options = {}) {
|
|
287
|
+
return httpRoute('patch', route, options)
|
|
288
|
+
}
|
|
289
|
+
function httpDelete(route, options = {}) {
|
|
290
|
+
return httpRoute('delete', route, options)
|
|
291
|
+
}
|
|
292
|
+
function httpHead(route, options = {}) {
|
|
293
|
+
return httpRoute('head', route, options)
|
|
294
|
+
}
|
|
295
|
+
function httpOptions(route, options = {}) {
|
|
296
|
+
return httpRoute('options', route, options)
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
function collectRouteSlots(ApiClass, classBasePath, classSwagger) {
|
|
300
|
+
const slots = []
|
|
301
|
+
const custom = new Set()
|
|
302
|
+
const className = ApiClass.name
|
|
303
|
+
|
|
304
|
+
for (const key of Object.getOwnPropertyNames(ApiClass.prototype)) {
|
|
305
|
+
if (key === 'constructor' || key.startsWith('_')) continue
|
|
306
|
+
const fn = ApiClass.prototype[key]
|
|
307
|
+
if (typeof fn !== 'function' || !fn.__fusionHttpRoute) continue
|
|
308
|
+
custom.add(key)
|
|
309
|
+
const meta = fn.__fusionHttpRoute
|
|
310
|
+
slots.push({
|
|
311
|
+
path: resolveHandlerRoute(classBasePath, meta.template, className, key),
|
|
312
|
+
httpMethod: meta.method,
|
|
313
|
+
handlerMethod: key,
|
|
314
|
+
swagger: {
|
|
315
|
+
tags: meta.tags,
|
|
316
|
+
description: meta.desc,
|
|
317
|
+
title: meta.title,
|
|
318
|
+
deprecated: meta.deprecated,
|
|
319
|
+
},
|
|
320
|
+
})
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
for (const methodName of HTTP_METHODS) {
|
|
324
|
+
if (custom.has(methodName)) continue
|
|
325
|
+
if (!definesMethod(ApiClass, methodName)) continue
|
|
326
|
+
slots.push({
|
|
327
|
+
path: classBasePath,
|
|
328
|
+
httpMethod: methodName,
|
|
329
|
+
handlerMethod: methodName,
|
|
330
|
+
swagger: classSwagger,
|
|
331
|
+
})
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
return slots
|
|
335
|
+
}
|
|
336
|
+
|
|
222
337
|
function emptyRequest() {
|
|
223
338
|
return {
|
|
224
339
|
method: '',
|
|
@@ -372,17 +487,21 @@ function router(routePath, options = {}) {
|
|
|
372
487
|
)
|
|
373
488
|
}
|
|
374
489
|
|
|
490
|
+
const classSwagger = {
|
|
491
|
+
tags: Array.isArray(options.tags) ? options.tags : [],
|
|
492
|
+
description: options.desc ?? null,
|
|
493
|
+
title: options.title ?? null,
|
|
494
|
+
deprecated: !!options.deprecated,
|
|
495
|
+
}
|
|
496
|
+
|
|
375
497
|
registry.push({
|
|
376
498
|
path: resolved,
|
|
499
|
+
classBasePath: resolved,
|
|
377
500
|
ApiClass,
|
|
378
501
|
middleware: routeMiddleware,
|
|
379
|
-
swagger:
|
|
380
|
-
tags: Array.isArray(options.tags) ? options.tags : [],
|
|
381
|
-
description: options.desc ?? null,
|
|
382
|
-
title: options.title ?? null,
|
|
383
|
-
deprecated: !!options.deprecated,
|
|
384
|
-
},
|
|
502
|
+
swagger: classSwagger,
|
|
385
503
|
version_prefix: v,
|
|
504
|
+
slots: collectRouteSlots(ApiClass, resolved, classSwagger),
|
|
386
505
|
})
|
|
387
506
|
return ApiClass
|
|
388
507
|
}
|
|
@@ -487,7 +606,9 @@ function readSwaggerSettings() {
|
|
|
487
606
|
const navbar = {
|
|
488
607
|
enabled: truthyEnabled(navbarRaw.enabled, true),
|
|
489
608
|
showUrlInput: truthyEnabled(navbarRaw.showUrlInput, true),
|
|
609
|
+
showUrlInputSet: Object.prototype.hasOwnProperty.call(navbarRaw, 'showUrlInput'),
|
|
490
610
|
urls: Array.isArray(navbarRaw.urls) ? navbarRaw.urls : null,
|
|
611
|
+
urlsSet: Array.isArray(navbarRaw.urls),
|
|
491
612
|
}
|
|
492
613
|
|
|
493
614
|
const ui = {
|
|
@@ -532,6 +653,60 @@ function readSwaggerSettings() {
|
|
|
532
653
|
}
|
|
533
654
|
}
|
|
534
655
|
|
|
656
|
+
const UNVERSIONED_SWAGGER_NAME = 'default'
|
|
657
|
+
|
|
658
|
+
function normalizeVersionLabel(value) {
|
|
659
|
+
return String(value || '')
|
|
660
|
+
.trim()
|
|
661
|
+
.replace(/^\/+|\/+$/g, '')
|
|
662
|
+
}
|
|
663
|
+
|
|
664
|
+
function collectRouteVersions() {
|
|
665
|
+
const versions = []
|
|
666
|
+
let hasUnversioned = false
|
|
667
|
+
for (const item of registry) {
|
|
668
|
+
const version = normalizeVersionLabel(item.version_prefix)
|
|
669
|
+
if (!version) {
|
|
670
|
+
hasUnversioned = true
|
|
671
|
+
continue
|
|
672
|
+
}
|
|
673
|
+
if (!versions.includes(version)) versions.push(version)
|
|
674
|
+
}
|
|
675
|
+
return { versions, hasUnversioned }
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
function swaggerVersionUrls(prefix) {
|
|
679
|
+
const { versions, hasUnversioned } = collectRouteVersions()
|
|
680
|
+
const urls = versions.map((label) => ({
|
|
681
|
+
url: `${prefix}/${label}/openapi.json`,
|
|
682
|
+
name: label,
|
|
683
|
+
}))
|
|
684
|
+
if (hasUnversioned && urls.length) {
|
|
685
|
+
urls.push({
|
|
686
|
+
url: `${prefix}/${UNVERSIONED_SWAGGER_NAME}/openapi.json`,
|
|
687
|
+
name: UNVERSIONED_SWAGGER_NAME,
|
|
688
|
+
})
|
|
689
|
+
}
|
|
690
|
+
return urls
|
|
691
|
+
}
|
|
692
|
+
|
|
693
|
+
function applyVersionNavbar(swagger) {
|
|
694
|
+
const autoUrls = swaggerVersionUrls(swagger.path)
|
|
695
|
+
if (!swagger.navbar.urlsSet && autoUrls.length) {
|
|
696
|
+
swagger.navbar.urls = autoUrls
|
|
697
|
+
if (!swagger.navbar.showUrlInputSet) swagger.navbar.showUrlInput = false
|
|
698
|
+
}
|
|
699
|
+
return autoUrls.map((item) => item.name)
|
|
700
|
+
}
|
|
701
|
+
|
|
702
|
+
function routeMatchesVersion(item, filter) {
|
|
703
|
+
const version = normalizeVersionLabel(item.version_prefix)
|
|
704
|
+
if (filter == null) return true
|
|
705
|
+
const label = normalizeVersionLabel(filter)
|
|
706
|
+
if (!label || label.toLowerCase() === UNVERSIONED_SWAGGER_NAME) return !version
|
|
707
|
+
return version.toLowerCase() === label.toLowerCase()
|
|
708
|
+
}
|
|
709
|
+
|
|
535
710
|
function applySwaggerOpenApi(openapi, swagger) {
|
|
536
711
|
openapi.info = { ...asObject(openapi.info), ...swagger.info }
|
|
537
712
|
if (swagger.servers?.length) openapi.servers = swagger.servers
|
|
@@ -546,7 +721,66 @@ function applySwaggerOpenApi(openapi, swagger) {
|
|
|
546
721
|
return openapi
|
|
547
722
|
}
|
|
548
723
|
|
|
549
|
-
function
|
|
724
|
+
function fillOpenApiPaths(openapi, versionFilter = null) {
|
|
725
|
+
const parsePathParams = (pattern) => {
|
|
726
|
+
return String(pattern)
|
|
727
|
+
.split('/')
|
|
728
|
+
.filter((seg) => (seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('[') && seg.endsWith(']')))
|
|
729
|
+
.map((seg) => seg.slice(1, -1))
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
for (const item of registry) {
|
|
733
|
+
if (!routeMatchesVersion(item, versionFilter)) continue
|
|
734
|
+
const { ApiClass, swagger: routeSwagger } = item
|
|
735
|
+
const slots = item.slots || []
|
|
736
|
+
|
|
737
|
+
for (const slot of slots) {
|
|
738
|
+
const pathParams = parsePathParams(slot.path)
|
|
739
|
+
const resolvedPath = slot.path.startsWith('/') ? slot.path : `/${slot.path}`
|
|
740
|
+
const routeSwaggerEntry = slot.swagger || routeSwagger
|
|
741
|
+
|
|
742
|
+
if (!openapi.paths[resolvedPath]) openapi.paths[resolvedPath] = {}
|
|
743
|
+
|
|
744
|
+
const methodLower = String(slot.httpMethod).toLowerCase()
|
|
745
|
+
const methodUpper = methodLower.toUpperCase()
|
|
746
|
+
const params = pathParams.map((name) => ({
|
|
747
|
+
name,
|
|
748
|
+
in: 'path',
|
|
749
|
+
required: true,
|
|
750
|
+
schema: { type: 'string' },
|
|
751
|
+
}))
|
|
752
|
+
|
|
753
|
+
openapi.paths[resolvedPath][methodLower] = {
|
|
754
|
+
tags: routeSwaggerEntry?.tags?.length ? routeSwaggerEntry.tags : [],
|
|
755
|
+
summary: routeSwaggerEntry?.title ?? `${ApiClass.name}.${slot.handlerMethod}`,
|
|
756
|
+
description: routeSwaggerEntry?.description ?? '',
|
|
757
|
+
deprecated: !!routeSwaggerEntry?.deprecated,
|
|
758
|
+
operationId: `${ApiClass.name}_${slot.handlerMethod}`,
|
|
759
|
+
parameters: params,
|
|
760
|
+
responses: { 200: { description: 'OK' } },
|
|
761
|
+
}
|
|
762
|
+
}
|
|
763
|
+
}
|
|
764
|
+
return openapi
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
function buildOpenApi(swagger, version = null) {
|
|
768
|
+
const openapi = applySwaggerOpenApi(
|
|
769
|
+
{
|
|
770
|
+
openapi: '3.0.3',
|
|
771
|
+
info: { ...swagger.info },
|
|
772
|
+
paths: {},
|
|
773
|
+
},
|
|
774
|
+
swagger,
|
|
775
|
+
)
|
|
776
|
+
const label = normalizeVersionLabel(version)
|
|
777
|
+
if (label && label !== UNVERSIONED_SWAGGER_NAME) {
|
|
778
|
+
openapi.info = { ...openapi.info, version: label }
|
|
779
|
+
}
|
|
780
|
+
return fillOpenApiPaths(openapi, version)
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
function swaggerUiHtml(swagger, openapiUrl, primaryName = null) {
|
|
550
784
|
const uiOpts = { ...swagger.ui }
|
|
551
785
|
delete uiOpts.presets
|
|
552
786
|
delete uiOpts.plugins
|
|
@@ -555,9 +789,12 @@ function swaggerUiHtml(swagger, openapiUrl) {
|
|
|
555
789
|
if (swagger.navbar?.urls?.length) {
|
|
556
790
|
delete uiOpts.url
|
|
557
791
|
uiOpts.urls = swagger.navbar.urls
|
|
792
|
+
const name = primaryName || swagger.navbar.urls[0]?.name
|
|
793
|
+
if (name) uiOpts['urls.primaryName'] = name
|
|
558
794
|
} else {
|
|
559
795
|
uiOpts.url = openapiUrl
|
|
560
796
|
delete uiOpts.urls
|
|
797
|
+
delete uiOpts['urls.primaryName']
|
|
561
798
|
}
|
|
562
799
|
uiOpts.dom_id = '#swagger-ui'
|
|
563
800
|
|
|
@@ -573,7 +810,7 @@ function swaggerUiHtml(swagger, openapiUrl) {
|
|
|
573
810
|
const showUrlInput = swagger.navbar?.showUrlInput !== false
|
|
574
811
|
const hideUrlCss =
|
|
575
812
|
navbarEnabled && !showUrlInput
|
|
576
|
-
? `<style>.topbar
|
|
813
|
+
? `<style>.topbar form { display: none !important; }</style>`
|
|
577
814
|
: ''
|
|
578
815
|
const standaloneScript = navbarEnabled
|
|
579
816
|
? `<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>`
|
|
@@ -633,16 +870,16 @@ class FusionApp {
|
|
|
633
870
|
if (this.mounted) return
|
|
634
871
|
activeGlobalMiddleware = [...this._middleware]
|
|
635
872
|
|
|
636
|
-
for (const {
|
|
637
|
-
for (const
|
|
638
|
-
|
|
639
|
-
this.engine.route(
|
|
873
|
+
for (const { ApiClass, middleware: routeMiddleware = [], slots = [] } of registry) {
|
|
874
|
+
for (const slot of slots) {
|
|
875
|
+
const handlerMethod = slot.handlerMethod
|
|
876
|
+
this.engine.route(String(slot.httpMethod).toUpperCase(), slot.path, (errOrRequest, maybeRequest) => {
|
|
640
877
|
const request = nativeRequestArg(errOrRequest, maybeRequest)
|
|
641
878
|
const chain = [...activeGlobalMiddleware, ...routeMiddleware]
|
|
642
879
|
const handler = async (req) => {
|
|
643
880
|
try {
|
|
644
881
|
const instance = new ApiClass(req || emptyRequest())
|
|
645
|
-
const fn = instance[
|
|
882
|
+
const fn = instance[handlerMethod]
|
|
646
883
|
return await Promise.resolve(fn.call(instance))
|
|
647
884
|
} catch (err) {
|
|
648
885
|
if (err instanceof HTTPException) return err.toResponse()
|
|
@@ -657,64 +894,26 @@ class FusionApp {
|
|
|
657
894
|
const swagger = readSwaggerSettings()
|
|
658
895
|
if (swagger.enabled) {
|
|
659
896
|
const prefix = swagger.path
|
|
897
|
+
const labels = applyVersionNavbar(swagger)
|
|
898
|
+
const combined = buildOpenApi(swagger)
|
|
660
899
|
|
|
661
|
-
const
|
|
662
|
-
{
|
|
663
|
-
openapi: '3.0.3',
|
|
664
|
-
info: { ...swagger.info },
|
|
665
|
-
paths: {},
|
|
666
|
-
},
|
|
667
|
-
swagger,
|
|
668
|
-
)
|
|
669
|
-
|
|
670
|
-
const parsePathParams = (pattern) => {
|
|
671
|
-
return String(pattern)
|
|
672
|
-
.split('/')
|
|
673
|
-
.filter((seg) => (seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('[') && seg.endsWith(']')))
|
|
674
|
-
.map((seg) => seg.slice(1, -1))
|
|
675
|
-
}
|
|
676
|
-
|
|
677
|
-
for (const item of registry) {
|
|
678
|
-
const { path: p, ApiClass, swagger: routeSwagger } = item
|
|
679
|
-
const pathParams = parsePathParams(p)
|
|
680
|
-
const resolvedPath = p.startsWith('/') ? p : `/${p}`
|
|
681
|
-
|
|
682
|
-
if (!openapi.paths[resolvedPath]) openapi.paths[resolvedPath] = {}
|
|
683
|
-
|
|
684
|
-
for (const methodName of HTTP_METHODS) {
|
|
685
|
-
if (!definesMethod(ApiClass, methodName)) continue
|
|
686
|
-
|
|
687
|
-
const methodUpper = String(methodName).toUpperCase()
|
|
688
|
-
const methodLower = String(methodName).toLowerCase()
|
|
689
|
-
|
|
690
|
-
const params = pathParams.map((name) => ({
|
|
691
|
-
name,
|
|
692
|
-
in: 'path',
|
|
693
|
-
required: true,
|
|
694
|
-
schema: { type: 'string' },
|
|
695
|
-
}))
|
|
696
|
-
|
|
697
|
-
openapi.paths[resolvedPath][methodLower] = {
|
|
698
|
-
tags: routeSwagger?.tags?.length ? routeSwagger.tags : [],
|
|
699
|
-
summary: routeSwagger?.title ?? `${ApiClass.name}.${methodUpper}`,
|
|
700
|
-
description: routeSwagger?.description ?? '',
|
|
701
|
-
deprecated: !!routeSwagger?.deprecated,
|
|
702
|
-
operationId: `${ApiClass.name}_${methodLower}`,
|
|
703
|
-
parameters: params,
|
|
704
|
-
responses: { '200': { description: 'OK' } },
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
|
-
}
|
|
708
|
-
|
|
709
|
-
const uiEnvelope = () => ({
|
|
900
|
+
const htmlEnvelope = (primaryName = null) => ({
|
|
710
901
|
status: 200,
|
|
711
|
-
body: swaggerUiHtml(swagger, `${prefix}/openapi.json
|
|
902
|
+
body: swaggerUiHtml(swagger, `${prefix}/openapi.json`, primaryName),
|
|
712
903
|
headers: { 'content-type': 'text/html' },
|
|
713
904
|
})
|
|
714
|
-
|
|
715
|
-
this.engine.route('GET', prefix
|
|
905
|
+
|
|
906
|
+
this.engine.route('GET', `${prefix}/openapi.json`, () => combined)
|
|
907
|
+
this.engine.route('GET', prefix, () => htmlEnvelope())
|
|
716
908
|
if (prefix !== '/') {
|
|
717
|
-
this.engine.route('GET', `${prefix}/`,
|
|
909
|
+
this.engine.route('GET', `${prefix}/`, () => htmlEnvelope())
|
|
910
|
+
}
|
|
911
|
+
|
|
912
|
+
for (const label of labels) {
|
|
913
|
+
const spec = buildOpenApi(swagger, label)
|
|
914
|
+
this.engine.route('GET', `${prefix}/${label}/openapi.json`, () => spec)
|
|
915
|
+
this.engine.route('GET', `${prefix}/${label}`, () => htmlEnvelope(label))
|
|
916
|
+
this.engine.route('GET', `${prefix}/${label}/`, () => htmlEnvelope(label))
|
|
718
917
|
}
|
|
719
918
|
}
|
|
720
919
|
|
|
@@ -772,8 +971,17 @@ module.exports = {
|
|
|
772
971
|
HTTPException,
|
|
773
972
|
router,
|
|
774
973
|
route,
|
|
974
|
+
httpGet,
|
|
975
|
+
httpPost,
|
|
976
|
+
httpPut,
|
|
977
|
+
httpPatch,
|
|
978
|
+
httpDelete,
|
|
979
|
+
httpHead,
|
|
980
|
+
httpOptions,
|
|
775
981
|
apiResourceName,
|
|
776
982
|
resolveRoutePath,
|
|
983
|
+
resolveHandlerRoute,
|
|
984
|
+
apiActionName,
|
|
777
985
|
configure,
|
|
778
986
|
getSettings,
|
|
779
987
|
settings,
|