Skip to content

Commit 2a63369

Browse files
committed
Fixing isActive bug with api revisions
1 parent d00cd64 commit 2a63369

2 files changed

Lines changed: 155 additions & 4 deletions

File tree

src/services/api-publisher.ts

Lines changed: 54 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,22 @@ export async function publishApi(
5757
}
5858

5959
// Step 2: Find and publish revisions in numeric order
60-
await publishApiRevisions(client, store, context, descriptor, config);
60+
const publishedRevisionCount = await publishApiRevisions(client, store, context, descriptor, config);
61+
62+
// Step 2b: Align root API only when source marks it as current.
63+
// Source of truth is properties.isCurrent in root apiInformation.json.
64+
if (publishedRevisionCount > 0 && rootResult.isCurrent === true) {
65+
const alignResult = await alignActiveRevisionWithSource(
66+
client,
67+
store,
68+
context,
69+
descriptor,
70+
config
71+
);
72+
if (alignResult.status !== 'success') {
73+
return alignResult;
74+
}
75+
}
6176

6277
// Step 3: Publish child resources in parallel
6378
// When a spec was imported, operations and schemas are auto-created by APIM
@@ -105,6 +120,11 @@ function getImportFormat(specFormat: string, _apiType?: string): string | undefi
105120
interface RootApiResult {
106121
status: 'success' | 'skipped';
107122
specImported: boolean;
123+
isCurrent?: boolean;
124+
}
125+
126+
interface PublishRootApiOptions {
127+
includeSpecification?: boolean;
108128
}
109129

110130
/**
@@ -118,7 +138,8 @@ async function publishRootApi(
118138
store: IArtifactStore,
119139
context: ApimServiceContext,
120140
descriptor: ResourceDescriptor,
121-
config: PublishConfig
141+
config: PublishConfig,
142+
options?: PublishRootApiOptions
122143
): Promise<RootApiResult & ResourcePublishResult> {
123144
let json = await store.readResource(config.sourceDir, descriptor);
124145
if (!json) {
@@ -132,10 +153,14 @@ async function publishRootApi(
132153

133154
// Apply overrides
134155
json = applyOverrides(descriptor, json, config.overrides);
156+
const isCurrent = getApiIsCurrent(json);
135157

136158
// Try to read the specification file for this API
137159
let specImported = false;
138-
const specResult = await store.readContent(config.sourceDir, descriptor, 'specification');
160+
const includeSpecification = options?.includeSpecification ?? true;
161+
const specResult = includeSpecification
162+
? await store.readContent(config.sourceDir, descriptor, 'specification')
163+
: undefined;
139164
if (specResult) {
140165
const properties = json.properties as Record<string, unknown> | undefined;
141166
const apiType = properties?.type as string | undefined;
@@ -180,9 +205,26 @@ async function publishRootApi(
180205
status: 'success',
181206
action: 'put',
182207
specImported,
208+
isCurrent,
183209
};
184210
}
185211

212+
async function alignActiveRevisionWithSource(
213+
client: IApimClient,
214+
store: IArtifactStore,
215+
context: ApimServiceContext,
216+
descriptor: ResourceDescriptor,
217+
config: PublishConfig
218+
): Promise<RootApiResult & ResourcePublishResult> {
219+
logger.debug(
220+
`Source marks "${getNamePart(descriptor.nameParts, 0)}" as current; re-applying root metadata to align active revision`
221+
);
222+
223+
return publishRootApi(client, store, context, descriptor, config, {
224+
includeSpecification: false,
225+
});
226+
}
227+
186228
/**
187229
* Find and publish API revisions in numeric order
188230
*/
@@ -192,7 +234,7 @@ async function publishApiRevisions(
192234
context: ApimServiceContext,
193235
apiDescriptor: ResourceDescriptor,
194236
config: PublishConfig
195-
): Promise<void> {
237+
): Promise<number> {
196238
// List all resources from store
197239
const allDescriptors = await store.listResources(config.sourceDir);
198240

@@ -214,6 +256,8 @@ async function publishApiRevisions(
214256
for (const revDescriptor of sortedRevisions) {
215257
await publishResource(client, store, context, revDescriptor, config);
216258
}
259+
260+
return sortedRevisions.length;
217261
}
218262

219263
/**
@@ -398,3 +442,9 @@ function extractRevisionNumber(apiName: string): number {
398442
const match = /;rev=(\d+)/.exec(apiName);
399443
return match ? parseInt(match[1], 10) : 0;
400444
}
445+
446+
function getApiIsCurrent(json: Record<string, unknown>): boolean | undefined {
447+
const properties = json.properties as Record<string, unknown> | undefined;
448+
const isCurrent = properties?.isCurrent;
449+
return typeof isCurrent === 'boolean' ? isCurrent : undefined;
450+
}

tests/unit/services/api-publisher.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -207,6 +207,107 @@ describe('api-publisher', () => {
207207
expect(calls[0][3].nameParts[0]).toBe('orders-api;rev=1');
208208
expect(calls[1][3].nameParts[0]).toBe('orders-api;rev=2');
209209
expect(calls[2][3].nameParts[0]).toBe('orders-api;rev=3');
210+
211+
// Root API is only replayed when source marks it current (isCurrent=true).
212+
// Default mock root payload has no isCurrent flag, so only the initial PUT runs.
213+
expect(client.putResource).toHaveBeenCalledTimes(1);
214+
});
215+
216+
it('should replay root API without re-importing specification after revisions', async () => {
217+
const client = createMockClient();
218+
const revisions = [{ type: ResourceType.Api, nameParts: ['orders-api;rev=2'] }];
219+
const store = createMockStore(revisions);
220+
store.readResource.mockResolvedValue({
221+
name: 'orders-api',
222+
properties: { path: 'orders', isCurrent: true },
223+
});
224+
store.readContent.mockResolvedValue({
225+
content: 'openapi: "3.0.0"',
226+
format: 'yaml',
227+
});
228+
229+
const apiDescriptor: ResourceDescriptor = {
230+
type: ResourceType.Api,
231+
nameParts: ['orders-api'],
232+
};
233+
234+
await publishApi(client, store, testContext, apiDescriptor, testConfig);
235+
236+
// Spec is only read/injected on the first root publish.
237+
expect(store.readContent).toHaveBeenCalledTimes(1);
238+
expect(client.putResource).toHaveBeenCalledTimes(2);
239+
240+
const firstPayload = client.putResource.mock.calls[0][2] as Record<string, unknown>;
241+
const secondPayload = client.putResource.mock.calls[1][2] as Record<string, unknown>;
242+
const firstProps = firstPayload.properties as Record<string, unknown>;
243+
const secondProps = secondPayload.properties as Record<string, unknown>;
244+
245+
expect(firstProps).toHaveProperty('format', 'openapi');
246+
expect(firstProps).toHaveProperty('value', 'openapi: "3.0.0"');
247+
expect(secondProps).not.toHaveProperty('format');
248+
expect(secondProps).not.toHaveProperty('value');
249+
});
250+
251+
it('should not replay root API when source root is not current', async () => {
252+
const client = createMockClient();
253+
const revisions = [{ type: ResourceType.Api, nameParts: ['orders-api;rev=2'] }];
254+
const store = createMockStore(revisions);
255+
store.readResource.mockResolvedValue({
256+
name: 'orders-api',
257+
properties: {
258+
isCurrent: false,
259+
apiRevision: '2',
260+
serviceUrl: 'https://src-revisioned-backend-v2.example.com/api',
261+
},
262+
});
263+
264+
const apiDescriptor: ResourceDescriptor = {
265+
type: ResourceType.Api,
266+
nameParts: ['orders-api'],
267+
};
268+
269+
await publishApi(client, store, testContext, apiDescriptor, testConfig);
270+
271+
// Root is not current in source, so no root alignment replay is performed.
272+
expect(client.putResource).toHaveBeenCalledTimes(1);
273+
const alignedPayload = client.putResource.mock.calls[0][2] as Record<string, unknown>;
274+
const alignedProps = alignedPayload.properties as Record<string, unknown>;
275+
276+
expect(alignedProps).toHaveProperty('apiRevision', '2');
277+
expect(alignedProps).toHaveProperty('serviceUrl', 'https://src-revisioned-backend-v2.example.com/api');
278+
expect(alignedProps).not.toHaveProperty('format');
279+
expect(alignedProps).not.toHaveProperty('value');
280+
});
281+
282+
it('should align active revision from source when active revision is 1', async () => {
283+
const client = createMockClient();
284+
const revisions = [{ type: ResourceType.Api, nameParts: ['orders-api;rev=2'] }];
285+
const store = createMockStore(revisions);
286+
store.readResource.mockResolvedValue({
287+
name: 'orders-api',
288+
properties: {
289+
isCurrent: true,
290+
apiRevision: '1',
291+
serviceUrl: 'https://src-revisioned-backend.example.com/api',
292+
},
293+
});
294+
295+
const apiDescriptor: ResourceDescriptor = {
296+
type: ResourceType.Api,
297+
nameParts: ['orders-api'],
298+
};
299+
300+
await publishApi(client, store, testContext, apiDescriptor, testConfig);
301+
302+
// Second root PUT is the explicit active-revision alignment pass.
303+
expect(client.putResource).toHaveBeenCalledTimes(2);
304+
const alignedPayload = client.putResource.mock.calls[1][2] as Record<string, unknown>;
305+
const alignedProps = alignedPayload.properties as Record<string, unknown>;
306+
307+
expect(alignedProps).toHaveProperty('apiRevision', '1');
308+
expect(alignedProps).toHaveProperty('serviceUrl', 'https://src-revisioned-backend.example.com/api');
309+
expect(alignedProps).not.toHaveProperty('format');
310+
expect(alignedProps).not.toHaveProperty('value');
210311
});
211312

212313
it('should skip non-matching revisions when filtering by API name', async () => {

0 commit comments

Comments
 (0)