Skip to content

Commit d6a33e0

Browse files
Copilotmarkcowl
andauthored
Add ArmListBySubscriptionScope operation template for subscription-level listing (#4185)
- [x] Add `ArmListBySubscriptionScope` operation template in `operations.tsp` - [x] Add test in `resource-resolution.test.ts` verifying resolveArmResources for child resource subscription scope - [x] Add test in `typespec-autorest/test/arm/resources.test.ts` verifying correct OpenAPI path output - [x] Update user documentation in `resource-operations.md` (table recommends `ArmListBySubscriptionScope`) - [x] Rebuild reference documentation (regen-docs) - [x] Apply review feedback: update JSDoc comments per suggestions - [x] Apply review feedback: simplify docs table (remove separate children row) - [x] Apply review feedback: change table to recommend `ArmListBySubscriptionScope` instead of `ArmListBySubscription` - [x] Create changeset for the change - [x] Rebased onto latest main — no merge conflicts, only PR changes in diff - [x] All 52 resource-resolution tests pass, all 16 autorest ARM tests pass - [x] All files correctly formatted Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: markcowl <1054056+markcowl@users.noreply.github.com>
1 parent 27235e9 commit d6a33e0

7 files changed

Lines changed: 220 additions & 5 deletions

File tree

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
---
2+
changeKind: feature
3+
packages:
4+
- "@azure-tools/typespec-azure-resource-manager"
5+
---
6+
7+
Add `ArmListBySubscriptionScope` operation template for listing resources at the subscription scope with a flat path, useful for child resources that need a subscription-level list operation without parent path segments.

packages/typespec-autorest/test/arm/resources.test.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -860,3 +860,60 @@ it("allows sync and async provider actions with unknown body", async () => {
860860
schema: {},
861861
});
862862
});
863+
864+
it("emits correct subscription-level list path for child resource using ArmListBySubscriptionScope", async () => {
865+
const openApi = await compileOpenAPI(
866+
`
867+
@armProviderNamespace
868+
namespace Microsoft.ContosoProviderhub;
869+
870+
model Test is TrackedResource<{}> {
871+
...ResourceNameParameter<Test>;
872+
}
873+
874+
@parentResource(Test)
875+
model Employee is ProxyResource<EmployeeProperties> {
876+
...ResourceNameParameter<Employee>;
877+
}
878+
879+
model EmployeeProperties {
880+
age?: int32;
881+
city?: string;
882+
}
883+
884+
@armResourceOperations
885+
interface Tests {
886+
get is ArmResourceRead<Test>;
887+
createOrUpdate is ArmResourceCreateOrReplaceAsync<Test>;
888+
delete is ArmResourceDeleteWithoutOkAsync<Test>;
889+
listByResourceGroup is ArmResourceListByParent<Test>;
890+
}
891+
892+
@armResourceOperations
893+
interface Employees {
894+
get is ArmResourceRead<Employee>;
895+
createOrUpdate is ArmResourceCreateOrReplaceSync<Employee>;
896+
delete is ArmResourceDeleteSync<Employee>;
897+
listByParent is ArmResourceListByParent<Employee>;
898+
listBySubscription is ArmListBySubscriptionScope<Employee>;
899+
}
900+
`,
901+
{ preset: "azure" },
902+
);
903+
904+
// Verify the subscription-level list path is correct (no parent resource path segments)
905+
const subscriptionListPath =
906+
"/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderhub/employees";
907+
ok(
908+
openApi.paths[subscriptionListPath]?.get,
909+
`Expected subscription-level list path ${subscriptionListPath} to exist`,
910+
);
911+
912+
// Verify the parent-level list path also exists
913+
const parentListPath =
914+
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderhub/tests/{testName}/employees";
915+
ok(
916+
openApi.paths[parentListPath]?.get,
917+
`Expected parent-level list path ${parentListPath} to exist`,
918+
);
919+
});

packages/typespec-azure-resource-manager/lib/operations.tsp

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,33 @@ op ArmListBySubscription<
2929
Error extends {} = ErrorResponse
3030
> is ArmReadOperation<SubscriptionScope<Resource> & Parameters, Response, Error>;
3131

32+
/**
33+
* A resource list operation, at the subscription scope, for any resource.
34+
* This template generates a standard resource list operation at the subscription level,
35+
* @template Resource the resource being listed
36+
* @template Parameters Optional. Additional query or header parameters
37+
* @template Response Optional. The success response for the list operation
38+
* @template Error Optional. The error response, if non-standard.
39+
*/
40+
@autoRoute
41+
@doc("List {name} resources by subscription ID", Resource)
42+
@list
43+
@listsResource(Resource)
44+
@segmentOf(Resource)
45+
@armResourceList(Resource)
46+
@get
47+
@Private.enforceConstraint(Resource, Foundations.Resource)
48+
op ArmListBySubscriptionScope<
49+
Resource extends Foundations.SimpleResource,
50+
Parameters extends {} = {},
51+
Response extends {} = ArmResponse<ResourceListResult<Resource>>,
52+
Error extends {} = ErrorResponse
53+
> is ArmReadOperation<
54+
SubscriptionBaseParameters & ProviderNamespace<Resource> & Parameters,
55+
Response,
56+
Error
57+
>;
58+
3259
/**
3360
* A resource list operation, at the scope of the resource's parent
3461
* @template Resource the resource being patched

packages/typespec-azure-resource-manager/test/resource-resolution.test.ts

Lines changed: 101 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4460,4 +4460,105 @@ interface Employees {
44604460
expect(employee.singleton).toBeDefined();
44614461
expect(employee.singleton!.keyValue).toEqual(["salaried", "hourly"]);
44624462
});
4463+
4464+
it("collects list operations for child resource using ArmListBySubscriptionScope", async () => {
4465+
const { program } = await Tester.compile(`
4466+
using Azure.Core;
4467+
4468+
@armProviderNamespace
4469+
namespace Microsoft.ContosoProviderHub;
4470+
4471+
interface Operations extends Azure.ResourceManager.Operations {}
4472+
4473+
model Test is TrackedResource<{}> {
4474+
...ResourceNameParameter<Test>;
4475+
}
4476+
4477+
@parentResource(Test)
4478+
model Employee is ProxyResource<EmployeeProperties> {
4479+
...ResourceNameParameter<Employee>;
4480+
}
4481+
4482+
model EmployeeProperties {
4483+
age?: int32;
4484+
city?: string;
4485+
}
4486+
4487+
@armResourceOperations
4488+
interface Tests {
4489+
get is ArmResourceRead<Test>;
4490+
createOrUpdate is ArmResourceCreateOrReplaceAsync<Test>;
4491+
delete is ArmResourceDeleteWithoutOkAsync<Test>;
4492+
listByResourceGroup is ArmResourceListByParent<Test>;
4493+
listBySubscription is ArmListBySubscription<Test>;
4494+
}
4495+
4496+
@armResourceOperations
4497+
interface Employees {
4498+
get is ArmResourceRead<Employee>;
4499+
createOrUpdate is ArmResourceCreateOrReplaceSync<Employee>;
4500+
delete is ArmResourceDeleteSync<Employee>;
4501+
listByParent is ArmResourceListByParent<Employee>;
4502+
listBySubscription is ArmListBySubscriptionScope<Employee>;
4503+
}
4504+
`);
4505+
const provider = resolveArmResources(program);
4506+
expect(provider).toBeDefined();
4507+
expect(provider.resources).toBeDefined();
4508+
ok(provider.resources);
4509+
4510+
// Find the Employee resource at its normal scope
4511+
const employee = provider.resources.find(
4512+
(r) =>
4513+
r.resourceName === "Employee" &&
4514+
r.resourceInstancePath ===
4515+
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/tests/{testName}/employees/{employeeName}",
4516+
);
4517+
ok(employee);
4518+
expect(employee).toMatchObject({
4519+
providerNamespace: "Microsoft.ContosoProviderHub",
4520+
});
4521+
4522+
// Verify the listByParent operation is correctly resolved on the main resource
4523+
checkResolvedOperations(employee, {
4524+
operations: {
4525+
lifecycle: {
4526+
createOrUpdate: [
4527+
{ operationGroup: "Employees", name: "createOrUpdate", kind: "createOrUpdate" },
4528+
],
4529+
delete: [{ operationGroup: "Employees", name: "delete", kind: "delete" }],
4530+
read: [{ operationGroup: "Employees", name: "get", kind: "read" }],
4531+
},
4532+
lists: [
4533+
{
4534+
operationGroup: "Employees",
4535+
name: "listByParent",
4536+
kind: "list",
4537+
},
4538+
],
4539+
},
4540+
resourceType: {
4541+
provider: "Microsoft.ContosoProviderHub",
4542+
types: ["tests", "employees"],
4543+
},
4544+
resourceName: "Employee",
4545+
resourceInstancePath:
4546+
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/tests/{testName}/employees/{employeeName}",
4547+
});
4548+
4549+
// Verify a subscription-scoped employee resource entry was created for the subscription list
4550+
const subscriptionEmployee = provider.resources.find(
4551+
(r) =>
4552+
r.resourceInstancePath ===
4553+
"/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/employees/{name}",
4554+
);
4555+
ok(subscriptionEmployee);
4556+
expect(subscriptionEmployee.operations.lists).toHaveLength(1);
4557+
expect(subscriptionEmployee.operations.lists![0]).toMatchObject({
4558+
operationGroup: "Employees",
4559+
name: "listBySubscription",
4560+
kind: "list",
4561+
path: "/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/employees",
4562+
});
4563+
});
44634564
});

website/src/content/docs/docs/howtos/ARM/resource-operations.md

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -182,11 +182,15 @@ Arm Resource list operations return a list of Tracked or Proxy Resources at a pa
182182
- For **Child Resources**, this is at the scope of the resource parent.
183183
- Tracked resources _must_ include a list operation at the Subscription level.
184184

185-
| Operation | TypeSpec |
186-
| ------------------ | ----------------------------------------------------------- |
187-
| ListByParent | `listByWidget is ArmResourceListByParent<ResourceType>` |
188-
| ListBySubscription | `listBySubscription is ArmListBySubscription<ResourceType>` |
189-
| ListAtScope | `listAtScope is ArmResourceListAtScope<ResourceType>` |
185+
| Operation | TypeSpec |
186+
| ------------------ | ---------------------------------------------------------------- |
187+
| ListByParent | `listByWidget is ArmResourceListByParent<ResourceType>` |
188+
| ListBySubscription | `listBySubscription is ArmListBySubscriptionScope<ResourceType>` |
189+
| ListAtScope | `listAtScope is ArmResourceListAtScope<ResourceType>` |
190+
191+
The `ArmListBySubscriptionScope` template is used for listing a resource directly at the subscription
192+
scope, generating a flat subscription-level path regardless of the resource's parent hierarchy.
193+
Use this instead of `ArmListBySubscription` when you need a subscription-level list operation for a child resource.
190194

191195
The `ArmResourceListAtScope` template is used when the scope of the list operation is determined by
192196
the `BaseParameters` type parameter. This is useful for resources with custom scope requirements

website/src/content/docs/docs/libraries/azure-resource-manager/reference/index.mdx

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,7 @@ npm install --save-peer @azure-tools/typespec-azure-resource-manager
8989
- [`ArmCustomPatchAsync`](./interfaces.md#Azure.ResourceManager.ArmCustomPatchAsync)
9090
- [`ArmCustomPatchSync`](./interfaces.md#Azure.ResourceManager.ArmCustomPatchSync)
9191
- [`ArmListBySubscription`](./interfaces.md#Azure.ResourceManager.ArmListBySubscription)
92+
- [`ArmListBySubscriptionScope`](./interfaces.md#Azure.ResourceManager.ArmListBySubscriptionScope)
9293
- [`ArmProviderActionAsync`](./interfaces.md#Azure.ResourceManager.ArmProviderActionAsync)
9394
- [`ArmProviderActionSync`](./interfaces.md#Azure.ResourceManager.ArmProviderActionSync)
9495
- [`ArmResourceActionAsync`](./interfaces.md#Azure.ResourceManager.ArmResourceActionAsync)

website/src/content/docs/docs/libraries/azure-resource-manager/reference/interfaces.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -989,6 +989,24 @@ op Azure.ResourceManager.ArmListBySubscription(apiVersion: string, subscriptionI
989989
| Response | Optional. The success response for the list operation |
990990
| Error | Optional. The error response, if non-standard. |
991991

992+
### `ArmListBySubscriptionScope` {#Azure.ResourceManager.ArmListBySubscriptionScope}
993+
994+
A resource list operation, at the subscription scope, for any resource.
995+
This template generates a standard resource list operation at the subscription level,
996+
997+
```typespec
998+
op Azure.ResourceManager.ArmListBySubscriptionScope(apiVersion: string, subscriptionId: Azure.Core.uuid, provider: "Microsoft.ThisWillBeReplaced"): Response | Error
999+
```
1000+
1001+
#### Template Parameters
1002+
1003+
| Name | Description |
1004+
| ---------- | ----------------------------------------------------- |
1005+
| Resource | the resource being listed |
1006+
| Parameters | Optional. Additional query or header parameters |
1007+
| Response | Optional. The success response for the list operation |
1008+
| Error | Optional. The error response, if non-standard. |
1009+
9921010
### `ArmProviderActionAsync` {#Azure.ResourceManager.ArmProviderActionAsync}
9931011

9941012
```typespec

0 commit comments

Comments
 (0)