Skip to content

Commit 6f03215

Browse files
committed
refactor: compare documents instead of operations
1 parent cf1464e commit 6f03215

File tree

20 files changed

+470
-306
lines changed

20 files changed

+470
-306
lines changed

src/apitypes/graphql/graphql.changes.ts

Lines changed: 109 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -15,11 +15,116 @@
1515
*/
1616

1717
import { VersionGraphQLOperation } from './graphql.types'
18-
import { removeComponents, takeIf } from '../../utils'
19-
import { apiDiff, COMPARE_MODE_OPERATION, Diff } from '@netcracker/qubership-apihub-api-diff'
20-
import { NORMALIZE_OPTIONS } from '../../consts'
21-
import { GraphApiSchema } from '@netcracker/qubership-apihub-graphapi'
18+
import { isEmpty, removeComponents, removeFirstSlash, slugify, takeIf } from '../../utils'
19+
import {
20+
apiDiff,
21+
COMPARE_MODE_OPERATION,
22+
DEFAULT_DIFFS_AGGREGATED_META_KEY,
23+
Diff,
24+
DIFF_META_KEY,
25+
} from '@netcracker/qubership-apihub-api-diff'
26+
import { NORMALIZE_OPTIONS, ORIGINS_SYMBOL } from '../../consts'
27+
import { GraphApiOperation, GraphApiSchema } from '@netcracker/qubership-apihub-graphapi'
28+
import { buildSchema } from 'graphql/utilities'
29+
import { buildGraphQLDocument } from './graphql.document'
30+
import {
31+
CompareContext,
32+
FILE_KIND,
33+
NormalizedOperationId,
34+
OperationChanges,
35+
OperationsApiType,
36+
ResolvedOperation,
37+
ResolvedVersionDocument,
38+
WithAggregatedDiffs,
39+
WithDiffMetaRecord,
40+
} from '../../types'
41+
import { GRAPHQL_TYPE, GRAPHQL_TYPE_KEYS } from './graphql.consts'
42+
import { createOperationChange, getOperationTags } from '../../components'
2243

44+
export const compareDocuments = async (apiType: OperationsApiType, operationsMap: Record<NormalizedOperationId, {
45+
previous?: ResolvedOperation
46+
current?: ResolvedOperation
47+
}>, prevFile: File, currFile: File, currDoc: ResolvedVersionDocument, prevDoc: ResolvedVersionDocument, ctx: CompareContext): Promise<{
48+
operationChanges: OperationChanges[]
49+
tags: string[]
50+
}> => {
51+
const prevDocSchema = buildSchema(await prevFile.text(), { noLocation: true })
52+
const currDocSchema = buildSchema(await currFile.text(), { noLocation: true })
53+
54+
const prevDocData = (await buildGraphQLDocument({
55+
...prevDoc,
56+
source: prevFile,
57+
kind: FILE_KIND.TEXT,
58+
data: prevDocSchema,
59+
}, prevDoc)).data
60+
const currDocData = (await buildGraphQLDocument({
61+
...currDoc,
62+
source: currFile,
63+
kind: FILE_KIND.TEXT,
64+
data: currDocSchema,
65+
}, currDoc)).data
66+
67+
const { merged, diffs } = apiDiff(
68+
prevDocData,
69+
currDocData,
70+
{
71+
...NORMALIZE_OPTIONS,
72+
metaKey: DIFF_META_KEY,
73+
originsFlag: ORIGINS_SYMBOL,
74+
diffsAggregatedFlag: DEFAULT_DIFFS_AGGREGATED_META_KEY,
75+
// mode: COMPARE_MODE_OPERATION,
76+
normalizedResult: true,
77+
},
78+
) as {merged: GraphApiSchema; diffs: Diff[]}
79+
80+
if (isEmpty(diffs)) {
81+
return { operationChanges: [], tags: [] }
82+
}
83+
84+
let operationDiffs: Diff[] = []
85+
86+
const { currentGroup, previousGroup } = ctx.config
87+
const currGroupSlug = slugify(removeFirstSlash(currentGroup || ''))
88+
const prevGroupSlug = slugify(removeFirstSlash(previousGroup || ''))
89+
90+
const tags = new Set<string>()
91+
const changedOperations: OperationChanges[] = []
92+
93+
for (const type of GRAPHQL_TYPE_KEYS) {
94+
const operationsByType = merged[type]
95+
if (!operationsByType) { continue }
96+
97+
for (const operationKey of Object.keys(operationsByType)) {
98+
const operationId = slugify(`${GRAPHQL_TYPE[type]}-${operationKey}`)
99+
const methodData = operationsByType[operationKey]
100+
101+
const { current, previous } = operationsMap[operationId] ?? {}
102+
if (current && previous) {
103+
operationDiffs = [...(methodData as WithAggregatedDiffs<GraphApiOperation>)[DEFAULT_DIFFS_AGGREGATED_META_KEY]]
104+
} else if (current || previous) {
105+
for (const type of GRAPHQL_TYPE_KEYS) {
106+
const operationsByType = (merged[type] as WithDiffMetaRecord<Record<string, GraphApiOperation>>)?.[DIFF_META_KEY]
107+
if (!operationsByType) { continue }
108+
operationDiffs.push(...Object.values(operationsByType))
109+
}
110+
if (isEmpty(operationDiffs)) {
111+
throw new Error('should not happen')
112+
}
113+
}
114+
115+
if (isEmpty(operationDiffs)) {
116+
continue
117+
}
118+
119+
changedOperations.push(createOperationChange(apiType, operationDiffs, previous, current, currGroupSlug, prevGroupSlug, currentGroup, previousGroup))
120+
getOperationTags(current ?? previous).forEach(tag => tags.add(tag))
121+
}
122+
}
123+
124+
return { operationChanges: changedOperations, tags: [...tags.values()] }
125+
}
126+
127+
/** @deprecated */
23128
export const graphqlOperationsCompare = async (current: VersionGraphQLOperation | undefined, previous: VersionGraphQLOperation | undefined): Promise<Diff[]> => {
24129
let previousOperation = removeComponents(previous?.data)
25130
let currentOperation = removeComponents(current?.data)

src/apitypes/graphql/graphql.consts.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
* See the License for the specific language governing permissions and
1414
* limitations under the License.
1515
*/
16+
import { ResolvedVersionDocument, ZippableDocument } from '../../types'
17+
import { GraphQLDocumentType } from './graphql.types'
1618

1719
export const GRAPHQL_API_TYPE = 'graphql' as const
1820

@@ -43,3 +45,7 @@ export const GRAPHQL_TYPE = {
4345
'mutations': 'mutation',
4446
'subscriptions': 'subscription',
4547
} as const
48+
49+
export function isGraphqlDocument(document: ZippableDocument | ResolvedVersionDocument): boolean {
50+
return Object.values(GRAPHQL_DOCUMENT_TYPE).includes(document.type as GraphQLDocumentType)
51+
}

src/apitypes/graphql/graphql.document.ts

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,13 +14,18 @@
1414
* limitations under the License.
1515
*/
1616

17-
import { buildFromIntrospection, buildFromSchema, GraphApiSchema, printGraphApi } from '@netcracker/qubership-apihub-graphapi'
17+
import {
18+
buildFromIntrospection,
19+
buildFromSchema,
20+
GraphApiSchema,
21+
printGraphApi,
22+
} from '@netcracker/qubership-apihub-graphapi'
1823
import type { GraphQLSchema, IntrospectionQuery } from 'graphql'
1924

20-
import type { DocumentBuilder, DocumentDumper } from '../../types'
25+
import { BuildConfigFile, DocumentDumper, TextFile, VersionDocument } from '../../types'
2126
import { GRAPHQL_DOCUMENT_TYPE } from './graphql.consts'
2227

23-
export const buildGraphQLDocument: DocumentBuilder<GraphApiSchema> = async (parsedFile, file) => {
28+
export const buildGraphQLDocument: (parsedFile: TextFile, file: BuildConfigFile) => Promise<VersionDocument<GraphApiSchema>> = async (parsedFile, file) => {
2429
let graphapi: GraphApiSchema
2530
if (parsedFile.type === GRAPHQL_DOCUMENT_TYPE.INTROSPECTION) {
2631
const introspection = (parsedFile?.data && '__schema' in parsedFile.data ? parsedFile?.data : parsedFile.data?.data) as IntrospectionQuery

src/apitypes/graphql/graphql.operation.ts

Lines changed: 2 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -88,18 +88,13 @@ export const buildGraphQLOperation = (
8888

8989
const apiKind = document.apiKind || API_KIND.BWC
9090

91-
92-
const dataHash = syncDebugPerformance('[ModelsAndOperationHashing]', () => {
91+
syncDebugPerformance('[ModelsAndOperationHashing]', () => {
9392
calculateSpecRefs(document.data, singleOperationRefsOnlySpec, singleOperationSpec)
94-
const dataHash = calculateObjectHash(singleOperationSpec)
95-
return dataHash
9693
}, debugCtx)
9794

98-
99-
10095
return {
10196
operationId,
102-
dataHash,
97+
dataHash: 'dataHash is to be removed',
10398
apiType: GRAPHQL_API_TYPE,
10499
apiKind: rawToApiKind(apiKind),
105100
deprecated: !!singleOperationEffectiveSpec[type]?.[method]?.directives?.deprecated,

src/apitypes/graphql/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ import { buildGraphQLOperations } from './graphql.operations'
2121
import { GRAPHQL_API_TYPE, GRAPHQL_DOCUMENT_TYPE } from './graphql.consts'
2222
import { parseGraphQLFile } from './graphql.parser'
2323
import { ApiBuilder } from '../../types'
24-
import { graphqlOperationsCompare } from './graphql.changes'
24+
import { compareDocuments, graphqlOperationsCompare } from './graphql.changes'
2525

2626
export * from './graphql.consts'
2727

@@ -33,4 +33,5 @@ export const graphqlApiBuilder: ApiBuilder<GraphApiSchema> = {
3333
buildOperations: buildGraphQLOperations,
3434
dumpDocument: dumpGraphQLDocument,
3535
compareOperationsData: graphqlOperationsCompare,
36+
compareDocuments: compareDocuments,
3637
}

src/apitypes/rest/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ import { OpenAPIV3 } from 'openapi-types'
1818

1919
import { buildRestDocument, createRestExportDocument, dumpRestDocument } from './rest.document'
2020
import { REST_API_TYPE, REST_DOCUMENT_TYPE } from './rest.consts'
21-
import { compareRestOperationsData } from './rest.changes'
21+
import { compareDocuments, compareRestOperationsData } from './rest.changes'
2222
import { buildRestOperations, createNormalizedOperationId } from './rest.operations'
2323
import { parseRestFile } from './rest.parser'
2424

@@ -34,6 +34,7 @@ export const restApiBuilder: ApiBuilder<OpenAPIV3.Document> = {
3434
buildOperations: buildRestOperations,
3535
dumpDocument: dumpRestDocument,
3636
compareOperationsData: compareRestOperationsData,
37+
compareDocuments: compareDocuments,
3738
createNormalizedOperationId: createNormalizedOperationId,
3839
createExportDocument: createRestExportDocument,
3940
}

src/apitypes/rest/rest.changes.ts

Lines changed: 122 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,28 +15,148 @@
1515
*/
1616

1717
import { RestOperationData, VersionRestOperation } from './rest.types'
18-
import { areDeprecatedOriginsNotEmpty, isOperationRemove, removeComponents } from '../../utils'
18+
import {
19+
areDeprecatedOriginsNotEmpty,
20+
IGNORE_PATH_PARAM_UNIFIED_PLACEHOLDER,
21+
isEmpty,
22+
isOperationRemove,
23+
normalizePath,
24+
removeComponents,
25+
removeFirstSlash,
26+
slugify,
27+
} from '../../utils'
1928
import {
2029
apiDiff,
2130
breaking,
2231
COMPARE_MODE_OPERATION,
32+
DEFAULT_DIFFS_AGGREGATED_META_KEY,
2333
Diff,
34+
DIFF_META_KEY,
2435
DiffAction,
2536
risky,
2637
} from '@netcracker/qubership-apihub-api-diff'
2738
import { MESSAGE_SEVERITY, NORMALIZE_OPTIONS, ORIGINS_SYMBOL } from '../../consts'
2839
import {
2940
BREAKING_CHANGE_TYPE,
41+
CompareContext,
3042
CompareOperationsPairContext,
43+
NormalizedOperationId,
44+
OperationChanges,
45+
OperationsApiType,
46+
ResolvedOperation,
47+
ResolvedVersionDocument,
3148
RISKY_CHANGE_TYPE,
49+
WithAggregatedDiffs,
50+
WithDiffMetaRecord,
3251
} from '../../types'
3352
import { isObject } from '@netcracker/qubership-apihub-json-crawl'
3453
import { areDeclarationPathsEqual } from '../../utils/path'
35-
import { JSON_SCHEMA_PROPERTY_DEPRECATED, pathItemToFullPath, resolveOrigins } from '@netcracker/qubership-apihub-api-unifier'
54+
import {
55+
JSON_SCHEMA_PROPERTY_DEPRECATED,
56+
pathItemToFullPath,
57+
resolveOrigins,
58+
} from '@netcracker/qubership-apihub-api-unifier'
3659
import { findRequiredRemovedProperties } from './rest.required'
3760
import { calculateObjectHash } from '../../utils/hashes'
3861
import { REST_API_TYPE } from './rest.consts'
62+
import { OpenAPIV3 } from 'openapi-types'
63+
import { extractServersDiffs, getOperationBasePath } from './rest.utils'
64+
import { createOperationChange, getOperationTags, takeSubstringIf } from '../../components'
65+
66+
export const compareDocuments = async (apiType: OperationsApiType, operationsMap: Record<NormalizedOperationId, {
67+
previous?: ResolvedOperation
68+
current?: ResolvedOperation
69+
}>, prevFile: File, currFile: File, currDoc: ResolvedVersionDocument, prevDoc: ResolvedVersionDocument, ctx: CompareContext): Promise<{
70+
operationChanges: OperationChanges[]
71+
tags: string[]
72+
}> => {
73+
const prevDocData = JSON.parse(await prevFile.text())
74+
const currDocData = JSON.parse(await currFile.text())
75+
76+
const { merged, diffs } = apiDiff(
77+
prevDocData,
78+
currDocData,
79+
{
80+
...NORMALIZE_OPTIONS,
81+
metaKey: DIFF_META_KEY,
82+
originsFlag: ORIGINS_SYMBOL,
83+
diffsAggregatedFlag: DEFAULT_DIFFS_AGGREGATED_META_KEY,
84+
// mode: COMPARE_MODE_OPERATION,
85+
normalizedResult: true,
86+
},
87+
) as { merged: OpenAPIV3.Document; diffs: Diff[] }
88+
89+
if (isEmpty(diffs)) {
90+
return { operationChanges: [], tags: [] }
91+
}
92+
93+
// todo reclassify
94+
// const olnyBreaking = diffs.filter((diff) => diff.type === breaking)
95+
// if (olnyBreaking.length > 0 && previous?.operationId) {
96+
// await reclassifyBreakingChanges(previous.operationId, diffResult.merged, olnyBreaking, ctx)
97+
// }
98+
99+
const { currentGroup, previousGroup } = ctx.config
100+
const currGroupSlug = slugify(removeFirstSlash(currentGroup || ''))
101+
const prevGroupSlug = slugify(removeFirstSlash(previousGroup || ''))
102+
103+
const tags = new Set<string>()
104+
const changedOperations: OperationChanges[] = []
105+
106+
for (const path of Object.keys((merged as OpenAPIV3.Document).paths)) {
107+
const pathData = (merged as OpenAPIV3.Document).paths[path]
108+
if (typeof pathData !== 'object' || !pathData) { continue }
109+
110+
for (const key of Object.keys(pathData)) {
111+
const inferredMethod = key as OpenAPIV3.HttpMethods
112+
113+
// check if field is a valid openapi http method defined in OpenAPIV3.HttpMethods
114+
if (!Object.values(OpenAPIV3.HttpMethods).includes(inferredMethod)) {
115+
continue
116+
}
117+
118+
const methodData = pathData[inferredMethod]
119+
const basePath = getOperationBasePath(methodData?.servers || pathData?.servers || merged.servers || [])
120+
const operationPath = basePath + path
121+
const operationId = slugify(`${removeFirstSlash(operationPath)}-${inferredMethod}`)
122+
const normalizedOperationId = slugify(`${normalizePath(basePath + path)}-${inferredMethod}`, [], IGNORE_PATH_PARAM_UNIFIED_PLACEHOLDER)
123+
// todo what's with prevslug? which tests affected? which slug to slice prev or curr?
124+
const qwe = takeSubstringIf(!!currGroupSlug, normalizedOperationId, currGroupSlug.length)
125+
126+
const { current, previous } = operationsMap[qwe] ?? operationsMap[operationId] ?? {}
127+
128+
let operationDiffs: Diff[] = []
129+
if (current && previous) {
130+
operationDiffs = [
131+
...(methodData as WithAggregatedDiffs<OpenAPIV3.OperationObject>)[DEFAULT_DIFFS_AGGREGATED_META_KEY],
132+
// todo what about security? add test
133+
...extractServersDiffs(merged),
134+
]
135+
136+
const pathParamRenameDiff = (merged.paths as WithDiffMetaRecord<OpenAPIV3.PathsObject>)[DIFF_META_KEY]?.[path]
137+
pathParamRenameDiff && operationDiffs.push(pathParamRenameDiff)
138+
} else if (current || previous) {
139+
const operationDiff = (merged.paths as WithDiffMetaRecord<OpenAPIV3.PathsObject>)[DIFF_META_KEY]?.[path]
140+
if (!operationDiff) {
141+
throw new Error('should not happen')
142+
}
143+
operationDiffs.push(operationDiff)
144+
}
145+
146+
if (isEmpty(operationDiffs)) {
147+
continue
148+
}
149+
150+
// todo operationDiffs can be [undefined] in 'type error must not appear during build'
151+
changedOperations.push(createOperationChange(apiType, operationDiffs, previous, current, currGroupSlug, prevGroupSlug, currentGroup, previousGroup))
152+
getOperationTags(current ?? previous).forEach(tag => tags.add(tag))
153+
}
154+
}
155+
156+
return { operationChanges: changedOperations, tags: [...tags.values()] }
157+
}
39158

159+
/** @deprecated */
40160
export const compareRestOperationsData = async (current: VersionRestOperation | undefined, previous: VersionRestOperation | undefined, ctx: CompareOperationsPairContext): Promise<Diff[]> => {
41161

42162
let previousOperation = removeComponents(previous?.data)

src/apitypes/rest/rest.operation.ts

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ export const buildRestOperation = (
133133

134134
const models: Record<string, string> = {}
135135
const apiKind = effectiveOperationObject[REST_KIND_KEY] || document.apiKind || API_KIND.BWC
136-
const [specWithSingleOperation, dataHash] = syncDebugPerformance('[ModelsAndOperationHashing]', () => {
136+
const [specWithSingleOperation] = syncDebugPerformance('[ModelsAndOperationHashing]', () => {
137137
const specWithSingleOperation = createSingleOperationSpec(
138138
document.data,
139139
path,
@@ -144,8 +144,7 @@ export const buildRestOperation = (
144144
components?.securitySchemes,
145145
)
146146
calculateSpecRefs(document.data, refsOnlySingleOperationSpec, specWithSingleOperation, models, componentsHashMap)
147-
const dataHash = calculateObjectHash(specWithSingleOperation)
148-
return [specWithSingleOperation, dataHash]
147+
return [specWithSingleOperation]
149148
}, debugCtx)
150149

151150
const deprecatedOperationItem = deprecatedItems.find(isDeprecatedOperationItem)
@@ -156,7 +155,7 @@ export const buildRestOperation = (
156155

157156
return {
158157
operationId,
159-
dataHash,
158+
dataHash: 'dataHash is to be removed',
160159
apiType: REST_API_TYPE,
161160
apiKind: rawToApiKind(apiKind),
162161
deprecated: !!effectiveOperationObject.deprecated,

0 commit comments

Comments
 (0)