fusion-framework 1.2.2 → 1.2.3

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.2',
137
+ ['X-Fusion-Version']: '1.2.3',
138
138
  }
139
139
 
140
140
  function isThenable(value) {
@@ -487,7 +487,9 @@ function readSwaggerSettings() {
487
487
  const navbar = {
488
488
  enabled: truthyEnabled(navbarRaw.enabled, true),
489
489
  showUrlInput: truthyEnabled(navbarRaw.showUrlInput, true),
490
+ showUrlInputSet: Object.prototype.hasOwnProperty.call(navbarRaw, 'showUrlInput'),
490
491
  urls: Array.isArray(navbarRaw.urls) ? navbarRaw.urls : null,
492
+ urlsSet: Array.isArray(navbarRaw.urls),
491
493
  }
492
494
 
493
495
  const ui = {
@@ -532,6 +534,60 @@ function readSwaggerSettings() {
532
534
  }
533
535
  }
534
536
 
537
+ const UNVERSIONED_SWAGGER_NAME = 'default'
538
+
539
+ function normalizeVersionLabel(value) {
540
+ return String(value || '')
541
+ .trim()
542
+ .replace(/^\/+|\/+$/g, '')
543
+ }
544
+
545
+ function collectRouteVersions() {
546
+ const versions = []
547
+ let hasUnversioned = false
548
+ for (const item of registry) {
549
+ const version = normalizeVersionLabel(item.version_prefix)
550
+ if (!version) {
551
+ hasUnversioned = true
552
+ continue
553
+ }
554
+ if (!versions.includes(version)) versions.push(version)
555
+ }
556
+ return { versions, hasUnversioned }
557
+ }
558
+
559
+ function swaggerVersionUrls(prefix) {
560
+ const { versions, hasUnversioned } = collectRouteVersions()
561
+ const urls = versions.map((label) => ({
562
+ url: `${prefix}/${label}/openapi.json`,
563
+ name: label,
564
+ }))
565
+ if (hasUnversioned && urls.length) {
566
+ urls.push({
567
+ url: `${prefix}/${UNVERSIONED_SWAGGER_NAME}/openapi.json`,
568
+ name: UNVERSIONED_SWAGGER_NAME,
569
+ })
570
+ }
571
+ return urls
572
+ }
573
+
574
+ function applyVersionNavbar(swagger) {
575
+ const autoUrls = swaggerVersionUrls(swagger.path)
576
+ if (!swagger.navbar.urlsSet && autoUrls.length) {
577
+ swagger.navbar.urls = autoUrls
578
+ if (!swagger.navbar.showUrlInputSet) swagger.navbar.showUrlInput = false
579
+ }
580
+ return autoUrls.map((item) => item.name)
581
+ }
582
+
583
+ function routeMatchesVersion(item, filter) {
584
+ const version = normalizeVersionLabel(item.version_prefix)
585
+ if (filter == null) return true
586
+ const label = normalizeVersionLabel(filter)
587
+ if (!label || label.toLowerCase() === UNVERSIONED_SWAGGER_NAME) return !version
588
+ return version.toLowerCase() === label.toLowerCase()
589
+ }
590
+
535
591
  function applySwaggerOpenApi(openapi, swagger) {
536
592
  openapi.info = { ...asObject(openapi.info), ...swagger.info }
537
593
  if (swagger.servers?.length) openapi.servers = swagger.servers
@@ -546,7 +602,66 @@ function applySwaggerOpenApi(openapi, swagger) {
546
602
  return openapi
547
603
  }
548
604
 
549
- function swaggerUiHtml(swagger, openapiUrl) {
605
+ function fillOpenApiPaths(openapi, versionFilter = null) {
606
+ const parsePathParams = (pattern) => {
607
+ return String(pattern)
608
+ .split('/')
609
+ .filter((seg) => (seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('[') && seg.endsWith(']')))
610
+ .map((seg) => seg.slice(1, -1))
611
+ }
612
+
613
+ for (const item of registry) {
614
+ if (!routeMatchesVersion(item, versionFilter)) continue
615
+ const { path: p, ApiClass, swagger: routeSwagger } = item
616
+ const pathParams = parsePathParams(p)
617
+ const resolvedPath = p.startsWith('/') ? p : `/${p}`
618
+
619
+ if (!openapi.paths[resolvedPath]) openapi.paths[resolvedPath] = {}
620
+
621
+ for (const methodName of HTTP_METHODS) {
622
+ if (!definesMethod(ApiClass, methodName)) continue
623
+
624
+ const methodUpper = String(methodName).toUpperCase()
625
+ const methodLower = String(methodName).toLowerCase()
626
+
627
+ const params = pathParams.map((name) => ({
628
+ name,
629
+ in: 'path',
630
+ required: true,
631
+ schema: { type: 'string' },
632
+ }))
633
+
634
+ openapi.paths[resolvedPath][methodLower] = {
635
+ tags: routeSwagger?.tags?.length ? routeSwagger.tags : [],
636
+ summary: routeSwagger?.title ?? `${ApiClass.name}.${methodUpper}`,
637
+ description: routeSwagger?.description ?? '',
638
+ deprecated: !!routeSwagger?.deprecated,
639
+ operationId: `${ApiClass.name}_${methodLower}`,
640
+ parameters: params,
641
+ responses: { 200: { description: 'OK' } },
642
+ }
643
+ }
644
+ }
645
+ return openapi
646
+ }
647
+
648
+ function buildOpenApi(swagger, version = null) {
649
+ const openapi = applySwaggerOpenApi(
650
+ {
651
+ openapi: '3.0.3',
652
+ info: { ...swagger.info },
653
+ paths: {},
654
+ },
655
+ swagger,
656
+ )
657
+ const label = normalizeVersionLabel(version)
658
+ if (label && label !== UNVERSIONED_SWAGGER_NAME) {
659
+ openapi.info = { ...openapi.info, version: label }
660
+ }
661
+ return fillOpenApiPaths(openapi, version)
662
+ }
663
+
664
+ function swaggerUiHtml(swagger, openapiUrl, primaryName = null) {
550
665
  const uiOpts = { ...swagger.ui }
551
666
  delete uiOpts.presets
552
667
  delete uiOpts.plugins
@@ -555,9 +670,12 @@ function swaggerUiHtml(swagger, openapiUrl) {
555
670
  if (swagger.navbar?.urls?.length) {
556
671
  delete uiOpts.url
557
672
  uiOpts.urls = swagger.navbar.urls
673
+ const name = primaryName || swagger.navbar.urls[0]?.name
674
+ if (name) uiOpts['urls.primaryName'] = name
558
675
  } else {
559
676
  uiOpts.url = openapiUrl
560
677
  delete uiOpts.urls
678
+ delete uiOpts['urls.primaryName']
561
679
  }
562
680
  uiOpts.dom_id = '#swagger-ui'
563
681
 
@@ -573,7 +691,7 @@ function swaggerUiHtml(swagger, openapiUrl) {
573
691
  const showUrlInput = swagger.navbar?.showUrlInput !== false
574
692
  const hideUrlCss =
575
693
  navbarEnabled && !showUrlInput
576
- ? `<style>.topbar .download-url-wrapper { display: none !important; }</style>`
694
+ ? `<style>.topbar form { display: none !important; }</style>`
577
695
  : ''
578
696
  const standaloneScript = navbarEnabled
579
697
  ? `<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>`
@@ -657,64 +775,26 @@ class FusionApp {
657
775
  const swagger = readSwaggerSettings()
658
776
  if (swagger.enabled) {
659
777
  const prefix = swagger.path
778
+ const labels = applyVersionNavbar(swagger)
779
+ const combined = buildOpenApi(swagger)
660
780
 
661
- const openapi = applySwaggerOpenApi(
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 = () => ({
781
+ const htmlEnvelope = (primaryName = null) => ({
710
782
  status: 200,
711
- body: swaggerUiHtml(swagger, `${prefix}/openapi.json`),
783
+ body: swaggerUiHtml(swagger, `${prefix}/openapi.json`, primaryName),
712
784
  headers: { 'content-type': 'text/html' },
713
785
  })
714
- this.engine.route('GET', `${prefix}/openapi.json`, () => openapi)
715
- this.engine.route('GET', prefix, uiEnvelope)
786
+
787
+ this.engine.route('GET', `${prefix}/openapi.json`, () => combined)
788
+ this.engine.route('GET', prefix, () => htmlEnvelope())
716
789
  if (prefix !== '/') {
717
- this.engine.route('GET', `${prefix}/`, uiEnvelope)
790
+ this.engine.route('GET', `${prefix}/`, () => htmlEnvelope())
791
+ }
792
+
793
+ for (const label of labels) {
794
+ const spec = buildOpenApi(swagger, label)
795
+ this.engine.route('GET', `${prefix}/${label}/openapi.json`, () => spec)
796
+ this.engine.route('GET', `${prefix}/${label}`, () => htmlEnvelope(label))
797
+ this.engine.route('GET', `${prefix}/${label}/`, () => htmlEnvelope(label))
718
798
  }
719
799
  }
720
800
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fusion-framework",
3
- "version": "1.2.2",
3
+ "version": "1.2.3",
4
4
  "description": "Class-based HTTP framework for Node.js, powered by a shared Rust core via N-API",
5
5
  "keywords": [
6
6
  "http",