Skip to content

Commit 306bc3a

Browse files
committed
chore: merge branch 'feature/performance-optimization' into feature/hash
2 parents 4cc36f9 + a898529 commit 306bc3a

File tree

13 files changed

+279
-33
lines changed

13 files changed

+279
-33
lines changed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
},
3434
"devDependencies": {
3535
"@netcracker/qubership-apihub-compatibility-suites": "dev",
36-
"@netcracker/qubership-apihub-graphapi": "1.0.8",
36+
"@netcracker/qubership-apihub-graphapi": "feature-performance-optimization",
3737
"@netcracker/qubership-apihub-npm-gitflow": "3.1.0",
3838
"@types/jest": "29.5.11",
3939
"@types/node": "20.11.6",

src/api.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,13 @@ import {
1616
SpecType,
1717
OpenApiSpecVersion,
1818
} from '@netcracker/qubership-apihub-api-unifier'
19-
import { DEFAULT_NORMALIZED_RESULT, DEFAULT_OPTION_DEFAULTS_META_KEY, DEFAULT_OPTION_ORIGINS_META_KEY, DIFF_META_KEY } from './core'
19+
import {
20+
DIFFS_AGGREGATED_META_KEY,
21+
DEFAULT_NORMALIZED_RESULT,
22+
DEFAULT_OPTION_DEFAULTS_META_KEY,
23+
DEFAULT_OPTION_ORIGINS_META_KEY,
24+
DIFF_META_KEY,
25+
} from './core'
2026

2127
function isOpenApiSpecVersion(specType: SpecType): specType is OpenApiSpecVersion {
2228
return specType === SPEC_TYPE_OPEN_API_30 || specType === SPEC_TYPE_OPEN_API_31
@@ -68,6 +74,7 @@ export function apiDiff(before: unknown, after: unknown, options: CompareOptions
6874
metaKey: DIFF_META_KEY,
6975
defaultsFlag: DEFAULT_OPTION_DEFAULTS_META_KEY,
7076
originsFlag: DEFAULT_OPTION_ORIGINS_META_KEY,
77+
diffsAggregatedFlag: DIFFS_AGGREGATED_META_KEY,
7178
compareScope: COMPARE_SCOPE_ROOT,
7279
mergedJsoCache: createEvaluationCacheService(),
7380
diffUniquenessCache: createEvaluationCacheService(),

src/core/compare.ts

Lines changed: 71 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
import { anyArrayKeys, getNodeRules, JsonPath, syncCrawl, SyncCrawlHook } from '@netcracker/qubership-apihub-json-crawl'
1+
import {
2+
anyArrayKeys,
3+
getNodeRules,
4+
JsonPath,
5+
syncClone,
6+
syncCrawl,
7+
SyncCrawlHook,
8+
} from '@netcracker/qubership-apihub-json-crawl'
29

310
import {
411
ChainItem,
@@ -16,6 +23,7 @@ import { deepEqual } from 'fast-equals'
1623
import {
1724
AdapterContext,
1825
AdapterResolver,
26+
AGGREGATE_DIFFS_HERE_RULE,
1927
CompareContext,
2028
CompareResult,
2129
CompareRule,
@@ -238,7 +246,7 @@ const useMergeFactory = (onDiff: DiffCallback, options: InternalCompareOptions):
238246

239247
const beforeKey = unsafeKey ?? (isArray(beforeJso) ? +Object.keys(keyMap).pop()! : Object.keys(keyMap).pop())
240248
const afterKey = keyMap[beforeKey]
241-
const mergeKey = isArray(mergedJso) && isNumber(beforeKey) ? beforeKey : afterKey //gitleaks:allow //THIS IS VERY FRAGILE. Cause this logic duplicate this line mergedJsoValue[keyInMerge] = afterValue[keyInAfter]
249+
const mergeKey = isArray(mergedJso) && isNumber(beforeKey) ? beforeKey : afterKey//THIS IS VERY FRAGILE. Cause this logic duplicate this line mergedJsoValue[keyInMerge] = afterValue[keyInAfter] #gitleaks:allow
242250

243251
// skip if node was removed
244252
if (!(beforeKey in keyMap)) {
@@ -476,7 +484,7 @@ export const compare = (before: unknown, after: unknown, options: InternalCompar
476484
return {
477485
diffs: rawDiffs,
478486
ownerDiffEntry: undefined,
479-
merged,
487+
merged: aggregateDiffs(merged, options),
480488
}
481489
}
482490
const diffFlags = Symbol('diffs')
@@ -494,8 +502,67 @@ export const compare = (before: unknown, after: unknown, options: InternalCompar
494502
return {
495503
diffs: denormalizedDiffs,
496504
ownerDiffEntry: undefined,
497-
merged,
505+
merged: merged,
506+
}
507+
}
508+
509+
export interface AggregateDiffsCrawlState {
510+
operationDiffs?: Set<Diff>
511+
}
512+
513+
export function aggregateDiffs(merged: unknown, options: InternalCompareOptions): unknown {
514+
let activeDataCycleGuard: Set<unknown> = new Set()
515+
516+
const collectCurrentNodeDiffs = (value: Record<string | symbol, unknown>, operationDiffs: Set<Diff>) => {
517+
if (options.metaKey in value) {
518+
const diffs = value[options.metaKey] as Record<PropertyKey, unknown> | undefined
519+
for (const key in diffs) {
520+
operationDiffs.add(diffs[key] as Diff)
521+
}
522+
}
498523
}
524+
525+
syncClone<AggregateDiffsCrawlState>(
526+
merged,
527+
[
528+
({ key, value, state, rules }) => {
529+
if (!isObject(value)) {
530+
return { value }
531+
}
532+
if (typeof key === 'symbol') {
533+
return { done: true }
534+
}
535+
if (activeDataCycleGuard.has(value)) {
536+
return { done: true }
537+
}
538+
activeDataCycleGuard.add(value)
539+
540+
if (state.operationDiffs) {
541+
collectCurrentNodeDiffs(value, state.operationDiffs)
542+
}
543+
544+
if (rules && AGGREGATE_DIFFS_HERE_RULE in rules) {
545+
activeDataCycleGuard = new Set()
546+
const operationDiffs = new Set<Diff>()
547+
collectCurrentNodeDiffs(value, operationDiffs)
548+
return {
549+
value,
550+
state: { ...state, operationDiffs },
551+
exitHook: () => {
552+
value[options.diffsAggregatedFlag] = operationDiffs
553+
},
554+
}
555+
}
556+
return { value }
557+
},
558+
],
559+
{
560+
state: {},
561+
rules: options.rules,
562+
},
563+
)
564+
565+
return merged
499566
}
500567

501568
export const nestedCompare = (before: unknown, after: unknown, options: InternalCompareOptions): CompareResult => {

src/core/constants.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { ClassifyRule } from '../types'
22

33
export const DIFF_META_KEY = Symbol('$diff')
4+
export const DIFFS_AGGREGATED_META_KEY = Symbol('$diffs-aggregated')
45
export const DEFAULT_NORMALIZED_RESULT = false
56
export const DEFAULT_OPTION_DEFAULTS_META_KEY = Symbol('$defaults')
67
export const DEFAULT_OPTION_ORIGINS_META_KEY = Symbol('$origins')

src/graphapi/graphapi.rules.ts

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import {
1818
deepEqualsUniqueItemsArrayMappingResolver
1919
} from '../core'
2020
import { resolveSchemaDescriptionTemplates } from '../jsonSchema'
21-
import type { ClassifyRule, CompareRules, DescriptionTemplates, MappingResolver } from '../types'
21+
import { AGGREGATE_DIFFS_HERE_RULE, ClassifyRule, CompareRules, DescriptionTemplates, MappingResolver } from '../types'
2222
import { graphApiSchemaAdapter as graphApiTypeAdapter, removeNotCorrectlySupportedInterfacesAdapter } from './graphapi.adapter'
2323
import { COMPARE_SCOPE_COMPONENTS, COMPARE_SCOPE_DIRECTIVE_USAGES, COMPARE_SCOPE_ARGS, COMPARE_SCOPE_OUTPUT } from './graphapi.const'
2424
import { complexTypeCompareResolver } from './graphapi.resolver'
@@ -335,18 +335,21 @@ export const graphApiRules = (): CompareRules => {
335335
'/queries': {
336336
'/*': {
337337
...methodRules,
338+
[AGGREGATE_DIFFS_HERE_RULE]: true,
338339
$: addNonBreaking
339340
},
340341
},
341342
'/mutations': {
342343
'/*': {
343344
...methodRules,
345+
[AGGREGATE_DIFFS_HERE_RULE]: true,
344346
$: addNonBreaking
345347
},
346348
},
347349
'/subscriptions': {
348350
'/*': {
349351
...methodRules,
352+
[AGGREGATE_DIFFS_HERE_RULE]: true,
350353
$: addNonBreaking
351354
}
352355
},

src/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
11
export { COMPARE_MODE_DEFAULT, COMPARE_MODE_OPERATION } from './types'
22

33
export {
4-
ClassifierType, DiffAction, DIFF_META_KEY, breaking, nonBreaking, unclassified, annotation, deprecated, risky,
4+
ClassifierType,
5+
DiffAction,
6+
DIFF_META_KEY,
7+
DIFFS_AGGREGATED_META_KEY,
8+
breaking,
9+
nonBreaking,
10+
unclassified,
11+
annotation,
12+
deprecated,
13+
risky,
514
} from './core'
615

716
export { apiDiff } from './api'

src/openapi/openapi3.classify.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ import {
55
breakingIfAfterTrue,
66
nonBreaking,
77
PARENT_JUMP,
8-
strictResolveValueFromContext,
98
reverseClassifyRule,
9+
strictResolveValueFromContext,
1010
transformClassifyRule,
1111
unclassified,
1212
} from '../core'
@@ -143,9 +143,12 @@ export const pathChangeClassifyRule: ClassifyRule = [
143143
({ before, after }) => {
144144
const beforePath = before.key as string
145145
const afterPath = after.key as string
146+
// todo uncomment
147+
// const unifiedBeforePath = createPathUnifier(before)(beforePath)
148+
// const unifiedAfterPath = createPathUnifier(after)(afterPath)
146149
const unifiedBeforePath = hidePathParamNames(beforePath)
147150
const unifiedAfterPath = hidePathParamNames(afterPath)
148-
151+
149152
// If unified paths are the same, it means only parameter names changed
150153
return unifiedBeforePath === unifiedAfterPath ? annotation : breaking
151154
}

src/openapi/openapi3.mapping.ts

Lines changed: 62 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,14 @@
1-
import type { MapKeysResult, MappingResolver } from '../types'
2-
import { getStringValue, objectKeys, onlyExistedArrayIndexes } from '../utils'
1+
import { MapKeysResult, MappingResolver, NodeContext } from '../types'
2+
import {
3+
difference,
4+
getStringValue,
5+
intersection,
6+
objectKeys,
7+
onlyExistedArrayIndexes,
8+
removeSlashes,
9+
} from '../utils'
310
import { mapPathParams } from './openapi3.utils'
11+
import { OpenAPIV3 } from 'openapi-types'
412

513
export const singleOperationPathMappingResolver: MappingResolver<string> = (before, after) => {
614

@@ -23,32 +31,40 @@ export const singleOperationPathMappingResolver: MappingResolver<string> = (befo
2331
return result
2432
}
2533

26-
export const pathMappingResolver: MappingResolver<string> = (before, after) => {
34+
export const pathMappingResolver: MappingResolver<string> = (before, after, ctx) => {
2735

2836
const result: MapKeysResult<string> = { added: [], removed: [], mapped: {} }
2937

30-
const originalBeforeKeys = objectKeys(before)
31-
const originalAfterKeys = objectKeys(after)
32-
const unifiedAfterKeys = originalAfterKeys.map(hidePathParamNames)
38+
const unifyBeforePath = createPathUnifier(ctx.before)
39+
const unifyAfterPath = createPathUnifier(ctx.after)
3340

34-
const notMappedAfterIndices = new Set(originalAfterKeys.keys())
41+
const unifiedBeforeKeyToKey = Object.fromEntries(objectKeys(before).map(key => [unifyBeforePath(key), key]))
42+
const unifiedAfterKeyToKey = Object.fromEntries(objectKeys(after).map(key => [unifyAfterPath(key), key]))
3543

36-
originalBeforeKeys.forEach(beforeKey => {
37-
const unifiedBeforePath = hidePathParamNames(beforeKey)
38-
const index = unifiedAfterKeys.indexOf(unifiedBeforePath)
44+
const unifiedBeforeKeys = Object.keys(unifiedBeforeKeyToKey)
45+
const unifiedAfterKeys = Object.keys(unifiedAfterKeyToKey)
3946

40-
if (index < 0) {
41-
// removed item
42-
result.removed.push(beforeKey)
43-
} else {
44-
// mapped items
45-
result.mapped[beforeKey] = originalAfterKeys[index]
46-
notMappedAfterIndices.delete(index)
47-
}
48-
})
47+
result.added = difference(unifiedAfterKeys, unifiedBeforeKeys).map(key => unifiedAfterKeyToKey[key])
48+
result.removed = difference(unifiedBeforeKeys, unifiedAfterKeys).map(key => unifiedBeforeKeyToKey[key])
49+
result.mapped = Object.fromEntries(
50+
intersection(unifiedBeforeKeys, unifiedAfterKeys).map(key => [unifiedBeforeKeyToKey[key], unifiedAfterKeyToKey[key]]),
51+
)
52+
53+
return result
54+
}
55+
56+
export const methodMappingResolver: MappingResolver<string> = (before, after) => {
57+
58+
const result: MapKeysResult<string> = { added: [], removed: [], mapped: {} }
59+
60+
const beforeKeys = objectKeys(before)
61+
const afterKeys = objectKeys(after)
4962

50-
// added items
51-
notMappedAfterIndices.forEach((notMappedIndex) => result.added.push(originalAfterKeys[notMappedIndex]))
63+
result.added = difference(afterKeys, beforeKeys)
64+
result.removed = difference(beforeKeys, afterKeys)
65+
66+
const mapped = intersection(beforeKeys, afterKeys)
67+
mapped.forEach(key => result.mapped[key] = key)
5268

5369
return result
5470
}
@@ -175,6 +191,31 @@ function isWildcardCompatible(beforeType: string, afterType: string): boolean {
175191
return true
176192
}
177193

194+
// todo copy-paste from api-processor
195+
export const extractOperationBasePath = (servers?: OpenAPIV3.ServerObject[]): string => {
196+
if (!Array.isArray(servers) || !servers.length) { return '' }
197+
198+
try {
199+
const [firstServer] = servers
200+
let serverUrl = firstServer.url
201+
const { variables = {} } = firstServer
202+
203+
for (const param of Object.keys(variables)) {
204+
serverUrl = serverUrl.replace(new RegExp(`{${param}}`, 'g'), variables[param].default)
205+
}
206+
207+
const { pathname } = new URL(serverUrl, 'https://localhost')
208+
return pathname.slice(-1) === '/' ? pathname.slice(0, -1) : pathname
209+
} catch (error) {
210+
return ''
211+
}
212+
}
213+
214+
export function createPathUnifier(nodeContext: NodeContext): (path: string) => string {
215+
const serverPrefix = extractOperationBasePath((nodeContext.root as OpenAPIV3.Document).servers) // /api/v2
216+
return (path) => removeSlashes(`${serverPrefix}${hidePathParamNames(path)}`)
217+
}
218+
178219
export function hidePathParamNames(path: string): string {
179220
return path.replace(PATH_PARAMETER_REGEXP, PATH_PARAM_UNIFIED_PLACEHOLDER)
180221
}

src/openapi/openapi3.rules.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ import {
3030
deepEqualsUniqueItemsArrayMappingResolver,
3131
} from '../core'
3232
import {
33+
AGGREGATE_DIFFS_HERE_RULE,
3334
COMPARE_MODE_OPERATION,
3435
CompareRules,
3536
DescriptionTemplates,
@@ -54,6 +55,7 @@ import {
5455
} from './openapi3.classify'
5556
import {
5657
contentMediaTypeMappingResolver,
58+
methodMappingResolver,
5759
paramMappingResolver,
5860
pathMappingResolver,
5961
singleOperationPathMappingResolver,
@@ -345,6 +347,7 @@ export const openApi3Rules = (options: OpenApi3RulesOptions): CompareRules => {
345347

346348
const operationRule: CompareRules = {
347349
$: [nonBreaking, breaking, unclassified],
350+
[AGGREGATE_DIFFS_HERE_RULE]: true,
348351
'/callbacks': {
349352
'/*': {
350353
//no support?
@@ -390,7 +393,7 @@ export const openApi3Rules = (options: OpenApi3RulesOptions): CompareRules => {
390393

391394
const pathItemObjectRules = (options: OpenApi3RulesOptions): CompareRules => ({
392395
$: pathChangeClassifyRule,
393-
mapping: options.mode === COMPARE_MODE_OPERATION ? singleOperationPathMappingResolver : pathMappingResolver,
396+
mapping: options.mode === COMPARE_MODE_OPERATION ? singleOperationPathMappingResolver : methodMappingResolver,
394397
'/description': { $: allAnnotation },
395398
'/parameters': {
396399
$: [nonBreaking, breaking, breaking],
@@ -431,6 +434,7 @@ export const openApi3Rules = (options: OpenApi3RulesOptions): CompareRules => {
431434
'/*': pathItemObjectRules(options),
432435
},
433436
'/securitySchemes': {
437+
[AGGREGATE_DIFFS_HERE_RULE]: true,
434438
$: [breaking, nonBreaking, breaking],
435439
'/*': {
436440
$: [breaking, nonBreaking, breaking],
@@ -452,14 +456,18 @@ export const openApi3Rules = (options: OpenApi3RulesOptions): CompareRules => {
452456
...documentAnnotationRule,
453457
'/**': documentAnnotationRule,
454458
},
455-
'/servers': serversRules,
459+
'/servers': {
460+
[AGGREGATE_DIFFS_HERE_RULE]: true,
461+
...serversRules,
462+
},
456463
'/paths': {
457464
$: allUnclassified,
458465
mapping: options.mode === COMPARE_MODE_OPERATION ? singleOperationPathMappingResolver : pathMappingResolver,
459466
'/*': pathItemObjectRules(options),
460467
},
461468
'/components': componentsRule,
462469
'/security': {
470+
[AGGREGATE_DIFFS_HERE_RULE]: true,
463471
$: globalSecurityClassifyRule,
464472
'/*': { $: globalSecurityItemClassifyRule },
465473
},

0 commit comments

Comments
 (0)