@@ -12,7 +12,8 @@ import { IArtifactStore } from '../clients/iartifact-store.js';
1212import { ApimServiceContext , ResourceDescriptor } from '../models/types.js' ;
1313import { ResourceType , RESOURCE_TYPE_METADATA } from '../models/resource-types.js' ;
1414import { FilterConfig } from '../models/config.js' ;
15- import { shouldIncludeResource } from './filter-service.js' ;
15+ import * as yaml from 'js-yaml' ;
16+ import { extractRootApiName , shouldIncludeResource } from './filter-service.js' ;
1617import { extractResourceType , ExtractedResource } from './resource-extractor.js' ;
1718import { redactAndWarnPolicySecrets } from './secret-redactor.js' ;
1819import { logger } from '../lib/logger.js' ;
@@ -21,6 +22,22 @@ import { getNamePart } from '../lib/resource-path.js';
2122import { normalizeWsdl } from '../lib/wsdl-normalizer.js' ;
2223import { isWorkspaceScope , extractNameFromLink } from '../lib/workspace-link.js' ;
2324
25+ /**
26+ * `JSON.parse`'s reviver `context` argument and `JSON.rawJSON` are ES2024
27+ * runtime additions (supported by the project's Node.js target) not yet
28+ * reflected in the configured TS `lib`. Augment the global `JSON` interface
29+ * rather than casting through `unknown` at each call site.
30+ */
31+ declare global {
32+ interface JSON {
33+ parse (
34+ text : string ,
35+ reviver : ( this : unknown , key : string , value : unknown , context : { source ?: string } ) => unknown
36+ ) : unknown ;
37+ rawJSON ( text : string ) : unknown ;
38+ }
39+ }
40+
2441/**
2542 * Result of API-specific extraction for a single API.
2643 */
@@ -97,7 +114,7 @@ export async function extractApiResources(
97114 // Extract API specification (uses already-extracted schemas to detect
98115 // synthetic GraphQL without a second list call).
99116 const specificationResult = await extractApiSpecification (
100- client , store , context , apiDescriptor , apiJson , outputDir , result . schemas
117+ client , store , context , apiDescriptor , apiJson , outputDir , result . schemas , filter
101118 ) ;
102119 result . specification = specificationResult . extracted ;
103120 result . errorCount += specificationResult . errorCount ;
@@ -274,7 +291,8 @@ async function extractApiSpecification(
274291 apiDescriptor : ResourceDescriptor ,
275292 apiJson : Record < string , unknown > ,
276293 outputDir : string ,
277- extractedSchemas : ExtractedResource [ ]
294+ extractedSchemas : ExtractedResource [ ] ,
295+ filter ?: FilterConfig
278296) : Promise < { extracted : boolean ; errorCount : number } > {
279297 const properties = apiJson . properties as Record < string , unknown > | undefined ;
280298 const apiType = properties ?. type as string | undefined ;
@@ -319,9 +337,12 @@ async function extractApiSpecification(
319337 // APIM's WSDL export can emit wsdl:part references qualified with the wrong
320338 // namespace prefix and multiple service ports, which its own importer then
321339 // rejects. Normalize so the extracted artifact round-trips through publish.
322- const content = spec . format === 'wsdl'
340+ let content = spec . format === 'wsdl'
323341 ? normalizeWsdl ( spec . content )
324342 : spec . content ;
343+ if ( spec . format === 'yaml' || spec . format === 'json' ) {
344+ content = filterOpenApiOperations ( content , spec . format , apiDescriptor , filter ) ;
345+ }
325346
326347 await store . writeContent (
327348 outputDir ,
@@ -340,6 +361,190 @@ async function extractApiSpecification(
340361 }
341362}
342363
364+ /**
365+ * Apply the same operation filter to the specification as to operation artifacts.
366+ * Keep shared definitions and path metadata, but remove paths without selected methods.
367+ *
368+ * Parses and reserializes the document, taking care to avoid the precision/type
369+ * loss that `JSON.parse`/js-yaml's defaults would otherwise introduce for scalars
370+ * unrelated to the operations being removed (e.g. int64 examples round to the
371+ * nearest double, bare YAML date-like strings are coerced to `Date`). This is not
372+ * a full byte-for-byte preserving editor: YAML comments are not retained, and
373+ * integers originally written in hex/octal/binary notation are re-emitted in
374+ * decimal form, since the reserialization still rebuilds the whole document.
375+ */
376+ function filterOpenApiOperations (
377+ content : string ,
378+ format : 'yaml' | 'json' ,
379+ apiDescriptor : ResourceDescriptor ,
380+ filter ?: FilterConfig
381+ ) : string {
382+ const apiName = getNamePart ( apiDescriptor . nameParts , 0 ) ;
383+ const rootName = extractRootApiName ( apiName ) . toLowerCase ( ) ;
384+ const subFilterKey = Object . keys ( filter ?. apiSubFilters ?? { } ) . find (
385+ key => key . toLowerCase ( ) === rootName
386+ ) ;
387+ if ( ! subFilterKey || filter ?. apiSubFilters ?. [ subFilterKey ] . operations === undefined ) {
388+ return content ;
389+ }
390+
391+ const spec = parseOpenApiDocument ( content , format ) ;
392+ const methods = new Set ( [ 'get' , 'put' , 'post' , 'delete' , 'options' , 'head' , 'patch' , 'trace' ] ) ;
393+ let modified = false ;
394+
395+ for ( const field of [ 'paths' , 'x-ms-paths' ] ) {
396+ const paths = spec [ field ] ;
397+ if ( ! paths || typeof paths !== 'object' || Array . isArray ( paths ) ) continue ;
398+
399+ for ( const [ path , value ] of Object . entries ( paths ) ) {
400+ if ( ! path . startsWith ( '/' ) || ! value || typeof value !== 'object' || Array . isArray ( value ) ) continue ;
401+ const pathItem = value as Record < string , unknown > ;
402+ let hasSelectedOperation = false ;
403+
404+ for ( const method of Object . keys ( pathItem ) ) {
405+ if ( ! methods . has ( method ) ) continue ;
406+ const operation = pathItem [ method ] as Record < string , unknown > | null ;
407+ const operationId = operation ?. operationId ;
408+ if ( typeof operationId === 'string' && shouldIncludeResource ( {
409+ ...apiDescriptor ,
410+ type : ResourceType . ApiOperation ,
411+ nameParts : [ apiName , operationId ] ,
412+ } , filter ) ) {
413+ hasSelectedOperation = true ;
414+ } else {
415+ delete pathItem [ method ] ;
416+ modified = true ;
417+ }
418+ }
419+
420+ if ( ! hasSelectedOperation ) {
421+ delete ( paths as Record < string , unknown > ) [ path ] ;
422+ modified = true ;
423+ }
424+ }
425+ }
426+
427+ if ( ! modified ) return content ;
428+ return stringifyOpenApiDocument ( spec , format ) ;
429+ }
430+
431+ /**
432+ * Parse a JSON/YAML OpenAPI document without losing precision on unrelated
433+ * scalars. `JSON.parse`/`js-yaml` normally round very large integers (e.g.
434+ * int64 examples) to the nearest representable double and, for YAML, coerce
435+ * bare date-like scalars to `Date` objects. Both are avoided here so that
436+ * only the operations targeted by the filter are actually changed.
437+ */
438+ function parseOpenApiDocument ( content : string , format : 'yaml' | 'json' ) : Record < string , unknown > {
439+ if ( format === 'json' ) {
440+ const reviver = ( _key : string , value : unknown , context : { source ?: string } ) : unknown => {
441+ const source = context ?. source ;
442+ if ( typeof value === 'number' && source !== undefined ) {
443+ return JSON . rawJSON ( source ) ;
444+ }
445+ return value ;
446+ } ;
447+ return JSON . parse ( content , reviver ) as Record < string , unknown > ;
448+ }
449+ return yaml . load ( content , { schema : PRECISE_YAML_SCHEMA } ) as Record < string , unknown > ;
450+ }
451+
452+ /**
453+ * Reserialize a document previously parsed by {@link parseOpenApiDocument},
454+ * emitting the preserved raw/precise scalars back in their original form.
455+ */
456+ function stringifyOpenApiDocument ( spec : Record < string , unknown > , format : 'yaml' | 'json' ) : string {
457+ return format === 'json'
458+ ? JSON . stringify ( spec , null , 2 )
459+ : yaml . dump ( spec , { lineWidth : - 1 , schema : PRECISE_YAML_SCHEMA } ) ;
460+ }
461+
462+ /**
463+ * YAML `int` type that preserves full precision for integers outside the safe
464+ * integer range (e.g. int64 examples) by representing them as `BigInt`
465+ * instead of a lossy JS `number`. Built on the standard Core schema, which
466+ * (unlike the Default schema) does not auto-convert bare date-like scalars to
467+ * `Date` objects.
468+ */
469+ const PRECISE_YAML_SCHEMA = yaml . CORE_SCHEMA . extend ( {
470+ implicit : [
471+ new yaml . Type ( 'tag:yaml.org,2002:int' , {
472+ kind : 'scalar' ,
473+ resolve : resolveYamlInteger ,
474+ construct : ( data : string ) => parsePreciseYamlInteger ( data ) ,
475+ predicate : ( data : unknown ) => isPreciseYamlInteger ( data ) ,
476+ // Always represented in decimal: the original literal style (e.g. hex,
477+ // octal, binary) isn't retained on the plain number/BigInt values produced
478+ // by `construct`, so a multi-style representer couldn't be selected from
479+ // them. Non-decimal integers unrelated to the filtered operations are
480+ // therefore re-emitted in decimal form; see the module-level docstring.
481+ represent : ( data : unknown ) => castPreciseYamlValue ( data ) . toString ( 10 ) ,
482+ } ) ,
483+ ] ,
484+ } ) ;
485+
486+ function castPreciseYamlValue ( data : unknown ) : number | bigint {
487+ return data as number | bigint ;
488+ }
489+
490+ /** Ported from js-yaml's built-in int type resolver (not exported publicly). */
491+ function resolveYamlInteger ( data : string | null ) : boolean {
492+ if ( data === null || data . length === 0 ) return false ;
493+
494+ let index = 0 ;
495+ const max = data . length ;
496+ let ch = data [ index ] ;
497+
498+ if ( ch === '-' || ch === '+' ) {
499+ ch = data [ ++ index ] ;
500+ }
501+
502+ if ( ch === '0' ) {
503+ if ( index + 1 === max ) return true ;
504+ ch = data [ ++ index ] ;
505+
506+ if ( ch === 'b' ) return / ^ [ 0 1 ] + $ / . test ( data . slice ( index + 1 ) ) ;
507+ if ( ch === 'x' ) return / ^ [ 0 - 9 a - f A - F ] + $ / . test ( data . slice ( index + 1 ) ) ;
508+ if ( ch === 'o' ) return / ^ [ 0 - 7 ] + $ / . test ( data . slice ( index + 1 ) ) ;
509+ }
510+
511+ return / ^ [ 0 - 9 ] + $ / . test ( data . slice ( index ) ) ;
512+ }
513+
514+ /**
515+ * Parses a resolved YAML integer scalar, promoting to `BigInt` only when the
516+ * value would otherwise lose precision as a JS `number`.
517+ */
518+ function parsePreciseYamlInteger ( data : string ) : number | bigint {
519+ let value = data ;
520+ let sign = 1 ;
521+ const ch0 = value [ 0 ] ;
522+ if ( ch0 === '-' || ch0 === '+' ) {
523+ if ( ch0 === '-' ) sign = - 1 ;
524+ value = value . slice ( 1 ) ;
525+ }
526+ if ( value === '0' ) return 0 ;
527+
528+ let magnitude : number ;
529+ if ( value [ 0 ] === '0' && value [ 1 ] === 'b' ) magnitude = parseInt ( value . slice ( 2 ) , 2 ) ;
530+ else if ( value [ 0 ] === '0' && value [ 1 ] === 'x' ) magnitude = parseInt ( value . slice ( 2 ) , 16 ) ;
531+ else if ( value [ 0 ] === '0' && value [ 1 ] === 'o' ) magnitude = parseInt ( value . slice ( 2 ) , 8 ) ;
532+ else magnitude = parseInt ( value , 10 ) ;
533+
534+ const result = sign * magnitude ;
535+ if ( Number . isSafeInteger ( result ) ) return result ;
536+
537+ const preciseMagnitude = BigInt ( value ) ;
538+ return sign === - 1 ? - preciseMagnitude : preciseMagnitude ;
539+ }
540+
541+ function isPreciseYamlInteger ( data : unknown ) : boolean {
542+ if ( typeof data === 'bigint' ) return true ;
543+ return Object . prototype . toString . call ( data ) === '[object Number]' &&
544+ ( data as number ) % 1 === 0 &&
545+ ! Object . is ( data , - 0 ) ;
546+ }
547+
343548/**
344549 * Returns true if any already-extracted ApiSchema resource has a contentType
345550 * indicating a GraphQL schema. Used to distinguish synthetic GraphQL APIs
0 commit comments