@@ -13,6 +13,7 @@ import { ResourceType } from '../models/resource-types.js';
1313import { publishResource , type ResourcePublishResult } from './resource-publisher.js' ;
1414import { logger } from '../lib/logger.js' ;
1515import { getNamePart } from '../lib/resource-path.js' ;
16+ import { parseArmUri } from '../lib/resource-uri.js' ;
1617
1718/**
1819 * Publish a Product with all its associations (APIs, Groups, Tags).
@@ -27,13 +28,18 @@ export async function publishProduct(
2728) : Promise < ResourcePublishResult > {
2829 try {
2930 const productName = getNamePart ( descriptor . nameParts , 0 ) ;
31+ const productExisted = ( await client . getResource ( context , descriptor ) ) !== undefined ;
3032
3133 // Step 1: Publish the Product itself
3234 const productResult = await publishResource ( client , store , context , descriptor , config ) ;
3335 if ( productResult . status !== 'success' ) {
3436 return productResult ;
3537 }
3638
39+ if ( ! productExisted ) {
40+ await cleanupAutoCreatedProductResources ( client , context , descriptor ) ;
41+ }
42+
3743 // Step 2: Publish ProductApi associations
3844 await publishProductAssociations (
3945 client ,
@@ -88,6 +94,125 @@ export async function publishProduct(
8894 }
8995}
9096
97+ async function cleanupAutoCreatedProductResources (
98+ client : IApimClient ,
99+ context : ApimServiceContext ,
100+ productDescriptor : ResourceDescriptor
101+ ) : Promise < void > {
102+ await cleanupProductSubscriptions ( client , context , productDescriptor ) ;
103+ await cleanupProductGroups ( client , context , productDescriptor ) ;
104+ }
105+
106+ async function cleanupProductSubscriptions (
107+ client : IApimClient ,
108+ context : ApimServiceContext ,
109+ productDescriptor : ResourceDescriptor
110+ ) : Promise < void > {
111+ const productName = getNamePart ( productDescriptor . nameParts , 0 ) ;
112+ const expectedScopeSuffix = `/products/${ productName } ` ;
113+
114+ let deleted = 0 ;
115+
116+ for await ( const subscription of client . listResources ( context , ResourceType . Subscription ) ) {
117+ const descriptor = parseSubscriptionDescriptor ( subscription , context ) ;
118+ if ( ! descriptor || descriptor . workspace !== productDescriptor . workspace ) {
119+ continue ;
120+ }
121+
122+ const props = subscription . properties as Record < string , unknown > | undefined ;
123+ const scope = typeof props ?. scope === 'string' ? props . scope : '' ;
124+ if ( ! scope . endsWith ( expectedScopeSuffix ) ) {
125+ continue ;
126+ }
127+
128+ try {
129+ const removed = await client . deleteResource ( context , descriptor ) ;
130+ if ( removed ) {
131+ deleted ++ ;
132+ logger . debug ( `Deleted APIM auto-created product subscription: ${ descriptor . nameParts [ 0 ] } ` ) ;
133+ }
134+ } catch ( error ) {
135+ logger . warn (
136+ `Failed to delete APIM auto-created product subscription ${ descriptor . nameParts [ 0 ] } : ${ String ( error ) } `
137+ ) ;
138+ }
139+ }
140+
141+ if ( deleted > 0 ) {
142+ logger . info ( `Deleted ${ deleted } auto-created subscription(s) for product: ${ productName } ` ) ;
143+ }
144+ }
145+
146+ async function cleanupProductGroups (
147+ client : IApimClient ,
148+ context : ApimServiceContext ,
149+ productDescriptor : ResourceDescriptor
150+ ) : Promise < void > {
151+ const productName = getNamePart ( productDescriptor . nameParts , 0 ) ;
152+ let deleted = 0 ;
153+
154+ for await ( const productGroup of client . listResources (
155+ context ,
156+ ResourceType . ProductGroup ,
157+ productDescriptor
158+ ) ) {
159+ const descriptor = parseProductGroupDescriptor ( productGroup , context ) ;
160+ if ( ! descriptor || descriptor . workspace !== productDescriptor . workspace ) {
161+ continue ;
162+ }
163+
164+ try {
165+ const removed = await client . deleteResource ( context , descriptor ) ;
166+ if ( removed ) {
167+ deleted ++ ;
168+ }
169+ } catch ( error ) {
170+ logger . warn (
171+ `Failed to delete auto-created product group ${ descriptor . nameParts . join ( '/' ) } : ${ String ( error ) } `
172+ ) ;
173+ }
174+ }
175+
176+ if ( deleted > 0 ) {
177+ logger . info ( `Deleted ${ deleted } auto-created product group(s) for product: ${ productName } ` ) ;
178+ }
179+ }
180+
181+ function parseSubscriptionDescriptor (
182+ subscription : Record < string , unknown > ,
183+ context : ApimServiceContext
184+ ) : ResourceDescriptor | undefined {
185+ if ( typeof subscription . id === 'string' ) {
186+ const parsed = parseArmUri ( subscription . id , context ) ;
187+ if ( parsed ?. type === ResourceType . Subscription ) {
188+ return parsed ;
189+ }
190+ }
191+
192+ if ( typeof subscription . name === 'string' && subscription . name . length > 0 ) {
193+ return {
194+ type : ResourceType . Subscription ,
195+ nameParts : [ subscription . name ] ,
196+ } ;
197+ }
198+
199+ return undefined ;
200+ }
201+
202+ function parseProductGroupDescriptor (
203+ productGroup : Record < string , unknown > ,
204+ context : ApimServiceContext
205+ ) : ResourceDescriptor | undefined {
206+ if ( typeof productGroup . id === 'string' ) {
207+ const parsed = parseArmUri ( productGroup . id , context ) ;
208+ if ( parsed ?. type === ResourceType . ProductGroup ) {
209+ return parsed ;
210+ }
211+ }
212+
213+ return undefined ;
214+ }
215+
91216/**
92217 * Publish associations (ProductApi or ProductGroup) for a product
93218 */
0 commit comments