Skip to content

Commit 46c3aa0

Browse files
authored
refactor: derive WORKSPACE_SUPPORTED_TYPES from RESOURCE_TYPE_METADATA
Add `workspaceSupported?: boolean` to ResourceTypeMetadata interface and mark the 12 workspace-capable types in RESOURCE_TYPE_METADATA. Replace the hardcoded WORKSPACE_SUPPORTED_TYPES array in workspace-extractor.ts with a dynamic derivation filtered from RESOURCE_TYPE_METADATA in enum declaration order. Add tests validating the flag and derived type list."
1 parent 593c15a commit 46c3aa0

4 files changed

Lines changed: 112 additions & 23 deletions

File tree

src/models/resource-types.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,12 @@ export interface ResourceTypeMetadata {
6767
* individual GET per item to fetch the complete resource.
6868
*/
6969
readonly listOmitsFields?: boolean;
70+
/**
71+
* True when this resource type is supported at the workspace scope.
72+
* Used by workspace-extractor to derive the set of types to extract
73+
* without maintaining a separate hardcoded list.
74+
*/
75+
readonly workspaceSupported?: boolean;
7076
}
7177

7278
export const RESOURCE_TYPE_METADATA: Record<ResourceType, ResourceTypeMetadata> = {
@@ -75,12 +81,14 @@ export const RESOURCE_TYPE_METADATA: Record<ResourceType, ResourceTypeMetadata>
7581
artifactDirectory: 'namedValues/{0}',
7682
infoFile: 'namedValueInformation.json',
7783
supportsGet: true,
84+
workspaceSupported: true,
7885
},
7986
[ResourceType.Tag]: {
8087
armPathSuffix: 'tags/{0}',
8188
artifactDirectory: 'tags/{0}',
8289
infoFile: 'tagInformation.json',
8390
supportsGet: true,
91+
workspaceSupported: true,
8492
},
8593
[ResourceType.Gateway]: {
8694
armPathSuffix: 'gateways/{0}',
@@ -99,30 +107,35 @@ export const RESOURCE_TYPE_METADATA: Record<ResourceType, ResourceTypeMetadata>
99107
artifactDirectory: 'backends/{0}',
100108
infoFile: 'backendInformation.json',
101109
supportsGet: true,
110+
workspaceSupported: true,
102111
},
103112
[ResourceType.Logger]: {
104113
armPathSuffix: 'loggers/{0}',
105114
artifactDirectory: 'loggers/{0}',
106115
infoFile: 'loggerInformation.json',
107116
supportsGet: true,
117+
workspaceSupported: true,
108118
},
109119
[ResourceType.Group]: {
110120
armPathSuffix: 'groups/{0}',
111121
artifactDirectory: 'groups/{0}',
112122
infoFile: 'groupInformation.json',
113123
supportsGet: true,
124+
workspaceSupported: true,
114125
},
115126
[ResourceType.Diagnostic]: {
116127
armPathSuffix: 'diagnostics/{0}',
117128
artifactDirectory: 'diagnostics/{0}',
118129
infoFile: 'diagnosticInformation.json',
119130
supportsGet: true,
131+
workspaceSupported: true,
120132
},
121133
[ResourceType.PolicyFragment]: {
122134
armPathSuffix: 'policyFragments/{0}',
123135
artifactDirectory: 'policyFragments/{0}',
124136
infoFile: 'policyFragmentInformation.json',
125137
supportsGet: true,
138+
workspaceSupported: true,
126139
},
127140
[ResourceType.ServicePolicy]: {
128141
armPathSuffix: 'policies/policy',
@@ -135,6 +148,7 @@ export const RESOURCE_TYPE_METADATA: Record<ResourceType, ResourceTypeMetadata>
135148
artifactDirectory: 'products/{0}',
136149
infoFile: 'productInformation.json',
137150
supportsGet: true,
151+
workspaceSupported: true,
138152
},
139153
[ResourceType.ProductPolicy]: {
140154
armPathSuffix: 'products/{0}/policies/policy',
@@ -165,6 +179,7 @@ export const RESOURCE_TYPE_METADATA: Record<ResourceType, ResourceTypeMetadata>
165179
artifactDirectory: 'apis/{0}',
166180
infoFile: 'apiInformation.json',
167181
supportsGet: true,
182+
workspaceSupported: true,
168183
},
169184
[ResourceType.ApiPolicy]: {
170185
armPathSuffix: 'apis/{0}/policies/policy',
@@ -207,12 +222,14 @@ export const RESOURCE_TYPE_METADATA: Record<ResourceType, ResourceTypeMetadata>
207222
artifactDirectory: 'subscriptions/{0}',
208223
infoFile: 'subscriptionInformation.json',
209224
supportsGet: true,
225+
workspaceSupported: true,
210226
},
211227
[ResourceType.GlobalSchema]: {
212228
armPathSuffix: 'schemas/{0}',
213229
artifactDirectory: 'schemas/{0}',
214230
infoFile: 'schemaInformation.json',
215231
supportsGet: true,
232+
workspaceSupported: true,
216233
},
217234
[ResourceType.PolicyRestriction]: {
218235
armPathSuffix: 'policyRestrictions/{0}',
@@ -225,6 +242,7 @@ export const RESOURCE_TYPE_METADATA: Record<ResourceType, ResourceTypeMetadata>
225242
artifactDirectory: 'documentations/{0}',
226243
infoFile: 'documentationInformation.json',
227244
supportsGet: true,
245+
workspaceSupported: true,
228246
},
229247
[ResourceType.ApiSchema]: {
230248
armPathSuffix: 'apis/{0}/schemas/{1}',

src/services/workspace-extractor.ts

Lines changed: 7 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -9,31 +9,21 @@
99
import { IApimClient } from '../clients/iapim-client.js';
1010
import { IArtifactStore } from '../clients/iartifact-store.js';
1111
import { ApimServiceContext } from '../models/types.js';
12-
import { ResourceType } from '../models/resource-types.js';
12+
import { ResourceType, RESOURCE_TYPE_METADATA } from '../models/resource-types.js';
1313
import { FilterConfig } from '../models/config.js';
1414
import { extractResourceType } from './resource-extractor.js';
1515
import { extractApiResources } from './api-extractor.js';
1616
import { extractProductResources } from './product-extractor.js';
1717
import { logger } from '../lib/logger.js';
1818

1919
/**
20-
* Types that can exist at the workspace level.
21-
* Not all APIM resource types support workspace scoping.
20+
* Types that can exist at the workspace level, derived from RESOURCE_TYPE_METADATA.
21+
* Enumerated in declaration order for deterministic iteration.
22+
* A type is workspace-capable when its metadata has `workspaceSupported: true`.
2223
*/
23-
const WORKSPACE_SUPPORTED_TYPES: ResourceType[] = [
24-
ResourceType.NamedValue,
25-
ResourceType.Tag,
26-
ResourceType.Backend,
27-
ResourceType.Logger,
28-
ResourceType.Diagnostic,
29-
ResourceType.PolicyFragment,
30-
ResourceType.Product,
31-
ResourceType.Api,
32-
ResourceType.Subscription,
33-
ResourceType.GlobalSchema,
34-
ResourceType.Documentation,
35-
ResourceType.Group,
36-
];
24+
const WORKSPACE_SUPPORTED_TYPES: ResourceType[] = Object.values(ResourceType).filter(
25+
(type) => RESOURCE_TYPE_METADATA[type].workspaceSupported === true
26+
);
3727

3828
/**
3929
* Result of extracting a single workspace.

tests/unit/models/resource-types.test.ts

Lines changed: 41 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -82,15 +82,51 @@ describe('RESOURCE_TYPE_METADATA', () => {
8282
}
8383
});
8484

85-
it('non-association resource types have supportsGet=true', () => {
86-
const nonAssociationTypes = [
85+
it('workspaceSupported is optional and boolean when present', () => {
86+
for (const [type, meta] of Object.entries(RESOURCE_TYPE_METADATA)) {
87+
if (meta.workspaceSupported !== undefined) {
88+
expect(typeof meta.workspaceSupported, `${type} workspaceSupported should be boolean`).toBe('boolean');
89+
}
90+
}
91+
});
92+
93+
it('workspace-capable types have workspaceSupported=true', () => {
94+
const expectedWorkspaceTypes = [
95+
ResourceType.NamedValue,
96+
ResourceType.Tag,
97+
ResourceType.Backend,
98+
ResourceType.Logger,
99+
ResourceType.Group,
100+
ResourceType.Diagnostic,
101+
ResourceType.PolicyFragment,
87102
ResourceType.Product,
88103
ResourceType.Api,
89-
ResourceType.Tag,
104+
ResourceType.Subscription,
105+
ResourceType.GlobalSchema,
106+
ResourceType.Documentation,
107+
];
108+
for (const type of expectedWorkspaceTypes) {
109+
expect(
110+
RESOURCE_TYPE_METADATA[type].workspaceSupported,
111+
`${type} should have workspaceSupported=true`
112+
).toBe(true);
113+
}
114+
});
115+
116+
it('non-workspace types do not have workspaceSupported=true', () => {
117+
const nonWorkspaceTypes = [
90118
ResourceType.ServicePolicy,
119+
ResourceType.ProductPolicy,
120+
ResourceType.ProductApi,
121+
ResourceType.GatewayApi,
122+
ResourceType.ApiPolicy,
123+
ResourceType.ApiOperation,
91124
];
92-
for (const type of nonAssociationTypes) {
93-
expect(RESOURCE_TYPE_METADATA[type].supportsGet, `${type} should have supportsGet=true`).toBe(true);
125+
for (const type of nonWorkspaceTypes) {
126+
expect(
127+
RESOURCE_TYPE_METADATA[type].workspaceSupported,
128+
`${type} should not have workspaceSupported=true`
129+
).not.toBe(true);
94130
}
95131
});
96132
});

tests/unit/services/workspace-extractor.test.ts

Lines changed: 46 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
*/
66

77
import { describe, it, expect, vi } from 'vitest';
8-
import { ResourceType } from '../../../src/models/resource-types.js';
8+
import { ResourceType, RESOURCE_TYPE_METADATA } from '../../../src/models/resource-types.js';
99
import { ApimServiceContext } from '../../../src/models/types.js';
1010
import { FilterConfig } from '../../../src/models/config.js';
1111
import { extractWorkspaces } from '../../../src/services/workspace-extractor.js';
@@ -42,6 +42,51 @@ function createMockStore() {
4242
};
4343
}
4444

45+
describe('workspace type selection', () => {
46+
it('should derive workspace types from RESOURCE_TYPE_METADATA workspaceSupported flag', () => {
47+
const derivedTypes = Object.values(ResourceType).filter(
48+
(type) => RESOURCE_TYPE_METADATA[type].workspaceSupported === true
49+
);
50+
expect(derivedTypes).toContain(ResourceType.NamedValue);
51+
expect(derivedTypes).toContain(ResourceType.Tag);
52+
expect(derivedTypes).toContain(ResourceType.Backend);
53+
expect(derivedTypes).toContain(ResourceType.Logger);
54+
expect(derivedTypes).toContain(ResourceType.Group);
55+
expect(derivedTypes).toContain(ResourceType.Diagnostic);
56+
expect(derivedTypes).toContain(ResourceType.PolicyFragment);
57+
expect(derivedTypes).toContain(ResourceType.Product);
58+
expect(derivedTypes).toContain(ResourceType.Api);
59+
expect(derivedTypes).toContain(ResourceType.Subscription);
60+
expect(derivedTypes).toContain(ResourceType.GlobalSchema);
61+
expect(derivedTypes).toContain(ResourceType.Documentation);
62+
expect(derivedTypes).toHaveLength(12);
63+
});
64+
65+
it('should not include non-workspace types', () => {
66+
const derivedTypes = Object.values(ResourceType).filter(
67+
(type) => RESOURCE_TYPE_METADATA[type].workspaceSupported === true
68+
);
69+
expect(derivedTypes).not.toContain(ResourceType.ServicePolicy);
70+
expect(derivedTypes).not.toContain(ResourceType.ProductApi);
71+
expect(derivedTypes).not.toContain(ResourceType.GatewayApi);
72+
expect(derivedTypes).not.toContain(ResourceType.ApiPolicy);
73+
});
74+
75+
it('should return types in enum declaration order', () => {
76+
const derivedTypes = Object.values(ResourceType).filter(
77+
(type) => RESOURCE_TYPE_METADATA[type].workspaceSupported === true
78+
);
79+
// Enum order: NamedValue, Tag, ..., Backend, Logger, Group, Diagnostic, PolicyFragment, ..., Product, Api, ..., Subscription, GlobalSchema, ..., Documentation
80+
const namedValueIdx = derivedTypes.indexOf(ResourceType.NamedValue);
81+
const tagIdx = derivedTypes.indexOf(ResourceType.Tag);
82+
const apiIdx = derivedTypes.indexOf(ResourceType.Api);
83+
const subscriptionIdx = derivedTypes.indexOf(ResourceType.Subscription);
84+
expect(namedValueIdx).toBeLessThan(tagIdx);
85+
expect(tagIdx).toBeLessThan(apiIdx);
86+
expect(apiIdx).toBeLessThan(subscriptionIdx);
87+
});
88+
});
89+
4590
describe('workspace-extractor', () => {
4691
describe('extractWorkspaces', () => {
4792
it('should skip extraction when no workspace names in filter', async () => {

0 commit comments

Comments
 (0)