fusion-framework 1.2.1 → 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.1',
137
+ ['X-Fusion-Version']: '1.2.3',
138
138
  }
139
139
 
140
140
  function isThenable(value) {
@@ -170,7 +170,7 @@ function frameworkHeaders() {
170
170
 
171
171
  class FusionBaseApi {
172
172
  constructor(request) {
173
- this.request = request
173
+ this.request = request && typeof request === 'object' ? request : emptyRequest()
174
174
  }
175
175
 
176
176
  get method() {
@@ -219,7 +219,39 @@ function resolveRoutePath(routePath, ApiClass) {
219
219
  return native.resolveRoutePathJs(routePath, ApiClass.name)
220
220
  }
221
221
 
222
+ function emptyRequest() {
223
+ return {
224
+ method: '',
225
+ path: '',
226
+ body: '',
227
+ headers: {},
228
+ params: {},
229
+ query: {},
230
+ state: {},
231
+ }
232
+ }
233
+
234
+ /**
235
+ * N-API threadsafe callbacks may be `(request)` (Fatal) or `(err, request)` (CalleeHandled).
236
+ * Always return a real request object so middleware never sees `null`.
237
+ */
238
+ function nativeRequestArg(errOrRequest, maybeRequest) {
239
+ if (maybeRequest != null && typeof maybeRequest === 'object') {
240
+ return maybeRequest
241
+ }
242
+ if (errOrRequest instanceof Error) {
243
+ throw errOrRequest
244
+ }
245
+ if (errOrRequest != null && typeof errOrRequest === 'object') {
246
+ return errOrRequest
247
+ }
248
+ return emptyRequest()
249
+ }
250
+
222
251
  function ensureState(request) {
252
+ if (!request || typeof request !== 'object') {
253
+ return {}
254
+ }
223
255
  if (!request.state || typeof request.state !== 'object') {
224
256
  request.state = {}
225
257
  }
@@ -455,7 +487,9 @@ function readSwaggerSettings() {
455
487
  const navbar = {
456
488
  enabled: truthyEnabled(navbarRaw.enabled, true),
457
489
  showUrlInput: truthyEnabled(navbarRaw.showUrlInput, true),
490
+ showUrlInputSet: Object.prototype.hasOwnProperty.call(navbarRaw, 'showUrlInput'),
458
491
  urls: Array.isArray(navbarRaw.urls) ? navbarRaw.urls : null,
492
+ urlsSet: Array.isArray(navbarRaw.urls),
459
493
  }
460
494
 
461
495
  const ui = {
@@ -500,6 +534,60 @@ function readSwaggerSettings() {
500
534
  }
501
535
  }
502
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
+
503
591
  function applySwaggerOpenApi(openapi, swagger) {
504
592
  openapi.info = { ...asObject(openapi.info), ...swagger.info }
505
593
  if (swagger.servers?.length) openapi.servers = swagger.servers
@@ -514,7 +602,66 @@ function applySwaggerOpenApi(openapi, swagger) {
514
602
  return openapi
515
603
  }
516
604
 
517
- 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) {
518
665
  const uiOpts = { ...swagger.ui }
519
666
  delete uiOpts.presets
520
667
  delete uiOpts.plugins
@@ -523,9 +670,12 @@ function swaggerUiHtml(swagger, openapiUrl) {
523
670
  if (swagger.navbar?.urls?.length) {
524
671
  delete uiOpts.url
525
672
  uiOpts.urls = swagger.navbar.urls
673
+ const name = primaryName || swagger.navbar.urls[0]?.name
674
+ if (name) uiOpts['urls.primaryName'] = name
526
675
  } else {
527
676
  uiOpts.url = openapiUrl
528
677
  delete uiOpts.urls
678
+ delete uiOpts['urls.primaryName']
529
679
  }
530
680
  uiOpts.dom_id = '#swagger-ui'
531
681
 
@@ -541,7 +691,7 @@ function swaggerUiHtml(swagger, openapiUrl) {
541
691
  const showUrlInput = swagger.navbar?.showUrlInput !== false
542
692
  const hideUrlCss =
543
693
  navbarEnabled && !showUrlInput
544
- ? `<style>.topbar .download-url-wrapper { display: none !important; }</style>`
694
+ ? `<style>.topbar form { display: none !important; }</style>`
545
695
  : ''
546
696
  const standaloneScript = navbarEnabled
547
697
  ? `<script src="https://unpkg.com/swagger-ui-dist/swagger-ui-standalone-preset.js"></script>`
@@ -604,11 +754,12 @@ class FusionApp {
604
754
  for (const { path: routePath, ApiClass, middleware: routeMiddleware = [] } of registry) {
605
755
  for (const methodName of HTTP_METHODS) {
606
756
  if (!definesMethod(ApiClass, methodName)) continue
607
- this.engine.route(methodName.toUpperCase(), routePath, async (request) => {
757
+ this.engine.route(methodName.toUpperCase(), routePath, (errOrRequest, maybeRequest) => {
758
+ const request = nativeRequestArg(errOrRequest, maybeRequest)
608
759
  const chain = [...activeGlobalMiddleware, ...routeMiddleware]
609
760
  const handler = async (req) => {
610
761
  try {
611
- const instance = new ApiClass(req)
762
+ const instance = new ApiClass(req || emptyRequest())
612
763
  const fn = instance[methodName]
613
764
  return await Promise.resolve(fn.call(instance))
614
765
  } catch (err) {
@@ -624,61 +775,27 @@ class FusionApp {
624
775
  const swagger = readSwaggerSettings()
625
776
  if (swagger.enabled) {
626
777
  const prefix = swagger.path
778
+ const labels = applyVersionNavbar(swagger)
779
+ const combined = buildOpenApi(swagger)
627
780
 
628
- const openapi = applySwaggerOpenApi(
629
- {
630
- openapi: '3.0.3',
631
- info: { ...swagger.info },
632
- paths: {},
633
- },
634
- swagger,
635
- )
781
+ const htmlEnvelope = (primaryName = null) => ({
782
+ status: 200,
783
+ body: swaggerUiHtml(swagger, `${prefix}/openapi.json`, primaryName),
784
+ headers: { 'content-type': 'text/html' },
785
+ })
636
786
 
637
- const parsePathParams = (pattern) => {
638
- return String(pattern)
639
- .split('/')
640
- .filter((seg) => (seg.startsWith('{') && seg.endsWith('}')) || (seg.startsWith('[') && seg.endsWith(']')))
641
- .map((seg) => seg.slice(1, -1))
787
+ this.engine.route('GET', `${prefix}/openapi.json`, () => combined)
788
+ this.engine.route('GET', prefix, () => htmlEnvelope())
789
+ if (prefix !== '/') {
790
+ this.engine.route('GET', `${prefix}/`, () => htmlEnvelope())
642
791
  }
643
792
 
644
- for (const item of registry) {
645
- const { path: p, ApiClass, swagger: routeSwagger } = item
646
- const pathParams = parsePathParams(p)
647
- const resolvedPath = p.startsWith('/') ? p : `/${p}`
648
-
649
- if (!openapi.paths[resolvedPath]) openapi.paths[resolvedPath] = {}
650
-
651
- for (const methodName of HTTP_METHODS) {
652
- if (!definesMethod(ApiClass, methodName)) continue
653
-
654
- const methodUpper = String(methodName).toUpperCase()
655
- const methodLower = String(methodName).toLowerCase()
656
-
657
- const params = pathParams.map((name) => ({
658
- name,
659
- in: 'path',
660
- required: true,
661
- schema: { type: 'string' },
662
- }))
663
-
664
- openapi.paths[resolvedPath][methodLower] = {
665
- tags: routeSwagger?.tags?.length ? routeSwagger.tags : [],
666
- summary: routeSwagger?.title ?? `${ApiClass.name}.${methodUpper}`,
667
- description: routeSwagger?.description ?? '',
668
- deprecated: !!routeSwagger?.deprecated,
669
- operationId: `${ApiClass.name}_${methodLower}`,
670
- parameters: params,
671
- responses: { '200': { description: 'OK' } },
672
- }
673
- }
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))
674
798
  }
675
-
676
- this.engine.route('GET', `${prefix}/openapi.json`, async () => openapi)
677
- this.engine.route('GET', prefix, async () => ({
678
- status: 200,
679
- body: swaggerUiHtml(swagger, `${prefix}/openapi.json`),
680
- headers: { 'content-type': 'text/html' },
681
- }))
682
799
  }
683
800
 
684
801
  this.mounted = true
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fusion-framework",
3
- "version": "1.2.1",
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",