Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
---
changeKind: feature
packages:
- "@azure-tools/typespec-azure-resource-manager"
---

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.
57 changes: 57 additions & 0 deletions packages/typespec-autorest/test/arm/resources.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -860,3 +860,60 @@ it("allows sync and async provider actions with unknown body", async () => {
schema: {},
});
});

it("emits correct subscription-level list path for child resource using ArmListBySubscriptionScope", async () => {
const openApi = await compileOpenAPI(
`
@armProviderNamespace
namespace Microsoft.ContosoProviderhub;

model Test is TrackedResource<{}> {
...ResourceNameParameter<Test>;
}

@parentResource(Test)
model Employee is ProxyResource<EmployeeProperties> {
...ResourceNameParameter<Employee>;
}

model EmployeeProperties {
age?: int32;
city?: string;
}

@armResourceOperations
interface Tests {
get is ArmResourceRead<Test>;
createOrUpdate is ArmResourceCreateOrReplaceAsync<Test>;
delete is ArmResourceDeleteWithoutOkAsync<Test>;
listByResourceGroup is ArmResourceListByParent<Test>;
}

@armResourceOperations
interface Employees {
get is ArmResourceRead<Employee>;
createOrUpdate is ArmResourceCreateOrReplaceSync<Employee>;
delete is ArmResourceDeleteSync<Employee>;
listByParent is ArmResourceListByParent<Employee>;
listBySubscription is ArmListBySubscriptionScope<Employee>;
}
`,
{ preset: "azure" },
);

// Verify the subscription-level list path is correct (no parent resource path segments)
const subscriptionListPath =
"/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderhub/employees";
ok(
openApi.paths[subscriptionListPath]?.get,
`Expected subscription-level list path ${subscriptionListPath} to exist`,
);

// Verify the parent-level list path also exists
const parentListPath =
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderhub/tests/{testName}/employees";
ok(
openApi.paths[parentListPath]?.get,
`Expected parent-level list path ${parentListPath} to exist`,
);
});
27 changes: 27 additions & 0 deletions packages/typespec-azure-resource-manager/lib/operations.tsp
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,33 @@ op ArmListBySubscription<
Error extends {} = ErrorResponse
> is ArmReadOperation<SubscriptionScope<Resource> & Parameters, Response, Error>;

/**
* A resource list operation, at the subscription scope, for any resource.
* This template generates a standard resource list operation at the subscription level,
* @template Resource the resource being listed
* @template Parameters Optional. Additional query or header parameters
* @template Response Optional. The success response for the list operation
* @template Error Optional. The error response, if non-standard.
*/
@autoRoute
@doc("List {name} resources by subscription ID", Resource)
@list
@listsResource(Resource)
@segmentOf(Resource)
@armResourceList(Resource)
@get
@Private.enforceConstraint(Resource, Foundations.Resource)
op ArmListBySubscriptionScope<
Resource extends Foundations.SimpleResource,
Parameters extends {} = {},
Response extends {} = ArmResponse<ResourceListResult<Resource>>,
Error extends {} = ErrorResponse
> is ArmReadOperation<
SubscriptionBaseParameters & ProviderNamespace<Resource> & Parameters,
Response,
Error
>;

/**
* A resource list operation, at the scope of the resource's parent
* @template Resource the resource being patched
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4210,4 +4210,105 @@ interface SupportTicketsNoSubscription {
expect(resource.operations.lists).toBeDefined();
expect(resource.operations.lists).toHaveLength(2);
});

it("collects list operations for child resource using ArmListBySubscriptionScope", async () => {
const { program } = await Tester.compile(`
using Azure.Core;

@armProviderNamespace
namespace Microsoft.ContosoProviderHub;

interface Operations extends Azure.ResourceManager.Operations {}

model Test is TrackedResource<{}> {
...ResourceNameParameter<Test>;
}

@parentResource(Test)
model Employee is ProxyResource<EmployeeProperties> {
...ResourceNameParameter<Employee>;
}

model EmployeeProperties {
age?: int32;
city?: string;
}

@armResourceOperations
interface Tests {
get is ArmResourceRead<Test>;
createOrUpdate is ArmResourceCreateOrReplaceAsync<Test>;
delete is ArmResourceDeleteWithoutOkAsync<Test>;
listByResourceGroup is ArmResourceListByParent<Test>;
listBySubscription is ArmListBySubscription<Test>;
}

@armResourceOperations
interface Employees {
get is ArmResourceRead<Employee>;
createOrUpdate is ArmResourceCreateOrReplaceSync<Employee>;
delete is ArmResourceDeleteSync<Employee>;
listByParent is ArmResourceListByParent<Employee>;
listBySubscription is ArmListBySubscriptionScope<Employee>;
}
`);
const provider = resolveArmResources(program);
expect(provider).toBeDefined();
expect(provider.resources).toBeDefined();
ok(provider.resources);

// Find the Employee resource at its normal scope
const employee = provider.resources.find(
(r) =>
r.resourceName === "Employee" &&
r.resourceInstancePath ===
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/tests/{testName}/employees/{employeeName}",
);
ok(employee);
expect(employee).toMatchObject({
providerNamespace: "Microsoft.ContosoProviderHub",
});

// Verify the listByParent operation is correctly resolved on the main resource
checkResolvedOperations(employee, {
operations: {
lifecycle: {
createOrUpdate: [
{ operationGroup: "Employees", name: "createOrUpdate", kind: "createOrUpdate" },
],
delete: [{ operationGroup: "Employees", name: "delete", kind: "delete" }],
read: [{ operationGroup: "Employees", name: "get", kind: "read" }],
},
lists: [
{
operationGroup: "Employees",
name: "listByParent",
kind: "list",
},
],
},
resourceType: {
provider: "Microsoft.ContosoProviderHub",
types: ["tests", "employees"],
},
resourceName: "Employee",
resourceInstancePath:
"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.ContosoProviderHub/tests/{testName}/employees/{employeeName}",
});

// Verify a subscription-scoped employee resource entry was created for the subscription list
const subscriptionEmployee = provider.resources.find(
(r) =>
r.resourceInstancePath ===
"/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/employees/{name}",
);
ok(subscriptionEmployee);
expect(subscriptionEmployee.operations.lists).toHaveLength(1);
expect(subscriptionEmployee.operations.lists![0]).toMatchObject({
operationGroup: "Employees",
name: "listBySubscription",
kind: "list",
path: "/subscriptions/{subscriptionId}/providers/Microsoft.ContosoProviderHub/employees",
});
});
});
Original file line number Diff line number Diff line change
Expand Up @@ -182,11 +182,15 @@ Arm Resource list operations return a list of Tracked or Proxy Resources at a pa
- For **Child Resources**, this is at the scope of the resource parent.
- Tracked resources _must_ include a list operation at the Subscription level.

| Operation | TypeSpec |
| ------------------ | ----------------------------------------------------------- |
| ListByParent | `listByWidget is ArmResourceListByParent<ResourceType>` |
| ListBySubscription | `listBySubscription is ArmListBySubscription<ResourceType>` |
| ListAtScope | `listAtScope is ArmResourceListAtScope<ResourceType>` |
| Operation | TypeSpec |
| ------------------ | ---------------------------------------------------------------- |
| ListByParent | `listByWidget is ArmResourceListByParent<ResourceType>` |
| ListBySubscription | `listBySubscription is ArmListBySubscriptionScope<ResourceType>` |
| ListAtScope | `listAtScope is ArmResourceListAtScope<ResourceType>` |

The `ArmListBySubscriptionScope` template is used for listing a resource directly at the subscription
scope, generating a flat subscription-level path regardless of the resource's parent hierarchy.
Use this instead of `ArmListBySubscription` when you need a subscription-level list operation for a child resource.

The `ArmResourceListAtScope` template is used when the scope of the list operation is determined by
the `BaseParameters` type parameter. This is useful for resources with custom scope requirements
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -89,6 +89,7 @@ npm install --save-peer @azure-tools/typespec-azure-resource-manager
- [`ArmCustomPatchAsync`](./interfaces.md#Azure.ResourceManager.ArmCustomPatchAsync)
- [`ArmCustomPatchSync`](./interfaces.md#Azure.ResourceManager.ArmCustomPatchSync)
- [`ArmListBySubscription`](./interfaces.md#Azure.ResourceManager.ArmListBySubscription)
- [`ArmListBySubscriptionScope`](./interfaces.md#Azure.ResourceManager.ArmListBySubscriptionScope)
- [`ArmProviderActionAsync`](./interfaces.md#Azure.ResourceManager.ArmProviderActionAsync)
- [`ArmProviderActionSync`](./interfaces.md#Azure.ResourceManager.ArmProviderActionSync)
- [`ArmResourceActionAsync`](./interfaces.md#Azure.ResourceManager.ArmResourceActionAsync)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -989,6 +989,24 @@ op Azure.ResourceManager.ArmListBySubscription(apiVersion: string, subscriptionI
| Response | Optional. The success response for the list operation |
| Error | Optional. The error response, if non-standard. |

### `ArmListBySubscriptionScope` {#Azure.ResourceManager.ArmListBySubscriptionScope}

A resource list operation, at the subscription scope, for any resource.
This template generates a standard resource list operation at the subscription level,

```typespec
op Azure.ResourceManager.ArmListBySubscriptionScope(apiVersion: string, subscriptionId: Azure.Core.uuid, provider: "Microsoft.ThisWillBeReplaced"): Response | Error
```

#### Template Parameters

| Name | Description |
| ---------- | ----------------------------------------------------- |
| Resource | the resource being listed |
| Parameters | Optional. Additional query or header parameters |
| Response | Optional. The success response for the list operation |
| Error | Optional. The error response, if non-standard. |

### `ArmProviderActionAsync` {#Azure.ResourceManager.ArmProviderActionAsync}

```typespec
Expand Down