From 8624ebe055c0749239f5fa8dc7158d364a54ff9a Mon Sep 17 00:00:00 2001 From: krrrr38 Date: Thu, 22 Aug 2024 21:08:10 +0900 Subject: [PATCH] wip --- internal/services/user/user/user_crud.go | 73 +++ internal/services/user/user/user_dto.go | 41 ++ .../services/user/user/users_data_source.go | 426 ++++++++++++++++++ internal/util/typesuser.go | 44 ++ 4 files changed, 584 insertions(+) create mode 100644 internal/services/user/user/user_crud.go create mode 100644 internal/services/user/user/user_dto.go create mode 100644 internal/services/user/user/users_data_source.go create mode 100644 internal/util/typesuser.go diff --git a/internal/services/user/user/user_crud.go b/internal/services/user/user/user_crud.go new file mode 100644 index 0000000..08ca85f --- /dev/null +++ b/internal/services/user/user/user_crud.go @@ -0,0 +1,73 @@ +package user + +import ( + "context" + "fmt" + + "github.com/folio-sec/terraform-provider-zoom/generated/api/zoomuser" + "github.com/folio-sec/terraform-provider-zoom/internal/util" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/samber/lo" +) + +func newCrud(client *zoomuser.Client) *crud { + return &crud{ + client: client, + } +} + +type crud struct { + client *zoomuser.Client +} + +func (c *crud) read(ctx context.Context, dto *readQueryDto) (*readDto, error) { + var user []*readDtoUser + nextPageToken := zoomuser.OptString{} + for { + ret, err := c.client.Users(ctx, zoomuser.UsersParams{ + PageSize: zoomuser.NewOptInt(300), // max 300 + NextPageToken: nextPageToken, + Status: util.ToUserOptString(dto.status), + RoleID: util.ToUserOptString(dto.roleID), + PageNumber: util.ToUserOptString(dto.pageNumber), + IncludeFields: util.ToUserOptString(dto.includeFields), + License: util.ToUserOptString(dto.license), + }) + if err != nil { + return nil, fmt.Errorf("unable to read user: %v", err) + } + user = append(user, lo.Map(ret.Users, func(item zoomuser.UsersOKUsersItem, _index int) *readDtoUser { + return &readDtoUser{ + id: util.FromOptString(item.ID), + email: types.StringValue(item.Email), + customAttributes: util.FromOptString(item.CustomAttributes), + dept: util.FromOptString(item.Dept), + employeeUniqueID: util.FromOptString(item.EmployeeUniqueID), + firstName: util.FromOptString(item.FirstName), + lastName: util.FromOptString(item.LastName), + groupIds: lo.Map(item.GroupIds, func(item string, index int) types.String { + return types.StringValue(item) + }), + hostKey: util.FromOptString(item.HostKey), + imGroupIds: lo.Map(item.ImGroupIds, func(item string, index int) types.String { + return types.StringValue(item) + }), + planUnitedType: util.FromOptString(item.PlanUnitedType), + pmi: util.FromOptInt64(item.Pmi), + roleID: util.FromOptString(item.RoleID), + status: util.FromOptString(item.Status), + typ: types.Int32Value(int32(item.Type)), + verified: util.FromOptInt(item.Verified), + displayName: util.FromOptString(item.DisplayName), + } + })...) + if ret.NextPageToken.Value == "" { + break + } + nextPageToken = ret.NextPageToken + } + + return &readDto{ + user: user, + }, nil +} diff --git a/internal/services/user/user/user_dto.go b/internal/services/user/user/user_dto.go new file mode 100644 index 0000000..c648b63 --- /dev/null +++ b/internal/services/user/user/user_dto.go @@ -0,0 +1,41 @@ +package user + +import "github.com/hashicorp/terraform-plugin-framework/types" + +type readQueryDto struct { + status types.String + roleID types.String + pageNumber types.String + includeFields types.String + license types.String +} + +type readDto struct { + user []*readDtoUser +} + +type readDtoUser struct { + id types.String + email types.String + customAttributes []*readDtoUserCustomAttributes + dept types.String + employeeUniqueID types.String + firstName types.String + lastName types.String + groupIds []types.String + hostKey types.String + imGroupIds []types.String + planUnitedType types.String + pmi types.Int64 + roleID types.String + status types.String + typ types.Int32 + verified types.Int32 + displayName types.String +} + +type readDtoUserCustomAttributes struct { + key types.String + name types.String + value types.String +} diff --git a/internal/services/user/user/users_data_source.go b/internal/services/user/user/users_data_source.go new file mode 100644 index 0000000..7bdc69d --- /dev/null +++ b/internal/services/user/user/users_data_source.go @@ -0,0 +1,426 @@ +package user + +import ( + "context" + "fmt" + "strings" + + "github.com/folio-sec/terraform-provider-zoom/internal/provider/shared" + "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" + "github.com/hashicorp/terraform-plugin-framework/datasource" + "github.com/hashicorp/terraform-plugin-framework/datasource/schema" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + "github.com/samber/lo" +) + +var ( + _ datasource.DataSource = &tfDataSource{} + _ datasource.DataSourceWithConfigure = &tfDataSource{} +) + +func NewUserDataSource() datasource.DataSource { + return &tfDataSource{} +} + +type tfDataSource struct { + crud *crud +} + +func (d *tfDataSource) Configure(_ context.Context, req datasource.ConfigureRequest, resp *datasource.ConfigureResponse) { + if req.ProviderData == nil { + return + } + data, ok := req.ProviderData.(*shared.ProviderData) + if !ok { + resp.Diagnostics.AddError( + "Unexpected ProviderData Source Configure Type", + fmt.Sprintf("Expected *provider.ProviderData, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + return + } + d.crud = newCrud(data.UserClient) +} + +func (d *tfDataSource) Metadata(_ context.Context, req datasource.MetadataRequest, resp *datasource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_phone_user" +} + +func (d *tfDataSource) Schema(_ context.Context, _ datasource.SchemaRequest, resp *datasource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: `Zoom Phone numbers in a Zoom account. + +## API Permissions +The following API permissions are required in order to use this resource. +This resource requires the ` + strings.Join([]string{ + "`phone:read:list_numbers:admin`", + }, ", "), + Attributes: map[string]schema.Attribute{ + "filter": schema.SingleNestedAttribute{ + Optional: true, + Attributes: map[string]schema.Attribute{ + "type": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The query response by number assignment. The value can be one of the following: + - assigned: The number has been assigned to either a user, a call queue, an auto-receptionist, or a common area in an account. + - unassigned: The number is not assigned to anyone. + - all: Include both assigned and unassigned numbers in the response. + - byoc: Include Bring Your Own Carrier (BYOC) numbers only in the response.`, + Validators: []validator.String{ + stringvalidator.OneOf("assigned", "unassigned", "all", "byoc"), + }, + }, + "extension_type": schema.StringAttribute{ + Optional: true, + MarkdownDescription: `The type of assignee to whom the number is assigned. The parameter can be set only if type parameter is set as assigned. The value can be one of the following: + - Allowed: user ┃ callQueue ┃ autoReceptionist ┃ commonArea ┃ emergencyNumberPool ┃ companyLocation ┃ meetingService`, + Validators: []validator.String{ + stringvalidator.OneOf("user", "callQueue", "autoReceptionist", "commonArea", "emergencyNumberPool", "companyLocation", "meetingService"), + }, + }, + "number_type": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The type of number. Values can be one of the following: `toll`, `tollfree`.", + Validators: []validator.String{ + stringvalidator.OneOf("toll", "tollfree"), + }, + }, + "pending_numbers": schema.BoolAttribute{ + Optional: true, + MarkdownDescription: "This field includes or excludes pending numbers in the response.", + }, + "site_id": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "The unique identifier of the site. Use this query parameter if you have enabled multiple sites and would like to filter the response of this API call by a specific phone site. See [Managing multiple sites](https://support.zoom.us/hc/en-us/articles/360020809672-Managing-multiple-sites) or [Adding a site](https://support.zoom.us/hc/en-us/articles/360020809672-Managing-multiple-sites#h_05c88e35-1593-491f-b1a8-b7139a75dc15) for details.", + }, + }, + }, + "user": schema.ListNestedAttribute{ + Computed: true, + Optional: true, + NestedObject: schema.NestedAttributeObject{ + Attributes: map[string]schema.Attribute{ + "assignee": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "extension_number": schema.Int64Attribute{ + Computed: true, + MarkdownDescription: "The extension number of the phone.", + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The unique identifier of the user to whom the number has been assigned.", + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the user to whom the number has been assigned.", + }, + "type": schema.StringAttribute{ + Computed: true, + MarkdownDescription: ` +This field indicates to whom the phone number belongs. + - user: Number has been assigned to an existing phone user that allows them to receive calls through their extension number or direct phone number. + - callQueue: Phone number has been assigned to a call queue. + - autoReceptionist: Phone number has been assigned to an auto receptionist. + - commonArea: Phone number has been assigned to a common area. + - emergencyNumberPool + - companyLocation + - meetingService`, + }, + }, + }, + "capability": schema.ListAttribute{ + Computed: true, + ElementType: types.StringType, + MarkdownDescription: "The capability for the phone number, whether it can take incoming calls, make outgoing calls, or both. Values include `incoming`, `outgoing`, or both values.", + }, + "carrier": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: "This field displays when the `type` request parameter is `byoc`.", + Attributes: map[string]schema.Attribute{ + "code": schema.Int32Attribute{ + Computed: true, + MarkdownDescription: "The carrier code.", + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the carrier to which the phone number is assigned.", + }, + }, + }, + "display_name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The display name for the phone number.", + }, + "emergency_address": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: "This field displays when the `type` request parameter is `byoc`.", + Attributes: map[string]schema.Attribute{ + "address_line1": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The address Line 1 of the [emergency address](https://support.zoom.us/hc/en-us/articles/360021062871-Setting-an-Emergency-Address) that consists of the house number and street name.", + }, + "address_line2": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Address Line 2 of the [emergency address](https://support.zoom.us/hc/en-us/articles/360021062871-Setting-an-Emergency-Address) that consists of building number, floor number, unit and so on.", + }, + "city": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The city of the [emergency address](https://support.zoom.us/hc/en-us/articles/360021062871-Setting-an-Emergency-Address).", + }, + "country": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The two-lettered country code (Aplha-2 code in ISO-3166 format) standard of the site's [emergency address](https://support.zoom.us/hc/en-us/articles/360021062871-Setting-an-Emergency-Address).", + }, + "state_code": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The state code of the [emergency address](https://support.zoom.us/hc/en-us/articles/360021062871-Setting-an-Emergency-Address).", + }, + "zip": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The zip code of the [emergency address](https://support.zoom.us/hc/en-us/articles/360021062871-Setting-an-Emergency-Address).", + }, + }, + }, + "emergency_address_status": schema.Int32Attribute{ + Computed: true, + MarkdownDescription: "This field displays when the `type` request parameter is `byoc`. The emergency address status: `1`-carrier update required, `2`-confirmed.", + }, + "emergency_address_update_time": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "This field displays when the `type` request parameter is `byoc`. The time of emergency address information update (format: 'yyyy-MM-ddThh:dd:ssZ').", + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The unique identifier of the phone number.", + }, + "location": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The location (city, state and country) where the phone number is assigned.", + }, + "number": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The phone number in E164 format.", + }, + "number_type": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The type of number. Values can be one of the following: `toll`, `tollfree`.", + }, + "sip_group": schema.SingleNestedAttribute{ + Computed: true, + MarkdownDescription: "This field displays when the `type` request parameter is `byoc`.", + Attributes: map[string]schema.Attribute{ + "display_name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the SIP group.", + }, + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The ID of the SIP group. See the **Creating SIP groups** section in [Creating a shared directory of external contacts](https://support.zoom.us/hc/en-us/articles/360037050092-Creating-a-shared-directory-of-external-contacts) for details.", + }, + }, + }, + "site": schema.SingleNestedAttribute{ + Computed: true, + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The target [site](https://support.zoom.us/hc/en-us/articles/360020809672-Managing-Multiple-Sites) in which the phone number was assigned. Sites allow you to organize the phone users in your organization. For example, you sites could be created based on different office locations.", + }, + "name": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The name of the site where the phone number is assigned.", + }, + }, + }, + "source": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The source of the phone number.", + }, + "status": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "The status of the number.", + }, + }, + }, + }, + }, + } +} + +type dataSourceModel struct { + Filter *dataSourceModelFilter `tfsdk:"filter"` + User []*dataSourceModelUser `tfsdk:"user"` +} + +type dataSourceModelUser struct { + Assignee *dataSourceModelAssignee `tfsdk:"assignee"` + Capability []types.String `tfsdk:"capability"` + Carrier *dataSourceModelCarrier `tfsdk:"carrier"` + DisplayName types.String `tfsdk:"display_name"` + EmergencyAddress *dataSourceModelEmergencyAddress `tfsdk:"emergency_address"` + EmergencyAddressStatus types.Int32 `tfsdk:"emergency_address_status"` + EmergencyAddressUpdateTime types.String `tfsdk:"emergency_address_update_time"` + ID types.String `tfsdk:"id"` + Location types.String `tfsdk:"location"` + Number types.String `tfsdk:"number"` + NumberType types.String `tfsdk:"number_type"` + SipGroup *dataSourceModelSipGroup `tfsdk:"sip_group"` + Site *dataSourceModelSite `tfsdk:"site"` + Source types.String `tfsdk:"source"` + Status types.String `tfsdk:"status"` +} + +type dataSourceModelFilter struct { + Type types.String `tfsdk:"type"` + ExtensionType types.String `tfsdk:"extension_type"` + NumberType types.String `tfsdk:"number_type"` + PendingNumbers types.Bool `tfsdk:"pending_numbers"` + SiteID types.String `tfsdk:"site_id"` +} + +type dataSourceModelAssignee struct { + ExtensionNumber types.Int64 `tfsdk:"extension_number"` + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + Type types.String `tfsdk:"type"` +} + +type dataSourceModelCarrier struct { + Code types.Int32 `tfsdk:"code"` + Name types.String `tfsdk:"name"` +} + +type dataSourceModelEmergencyAddress struct { + AddressLine1 types.String `tfsdk:"address_line1"` + AddressLine2 types.String `tfsdk:"address_line2"` + City types.String `tfsdk:"city"` + Country types.String `tfsdk:"country"` + StateCode types.String `tfsdk:"state_code"` + Zip types.String `tfsdk:"zip"` +} + +type dataSourceModelSipGroup struct { + DisplayName types.String `tfsdk:"display_name"` + ID types.String `tfsdk:"id"` +} + +type dataSourceModelSite struct { + ID types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` +} + +func (d *tfDataSource) Read(ctx context.Context, req datasource.ReadRequest, resp *datasource.ReadResponse) { + var data dataSourceModel + resp.Diagnostics.Append(req.Config.Get(ctx, &data)...) + if resp.Diagnostics.HasError() { + return + } + + typ := types.StringNull() + extensionType := types.StringNull() + numberType := types.StringNull() + pendingNumbers := types.BoolNull() + siteID := types.StringNull() + if data.Filter != nil { + typ = data.Filter.Type + extensionType = data.Filter.ExtensionType + numberType = data.Filter.NumberType + pendingNumbers = data.Filter.PendingNumbers + siteID = data.Filter.SiteID + } + dto, err := d.crud.read(ctx, &readQueryDto{ + typ: typ, + extensionType: extensionType, + numberType: numberType, + pendingNumbers: pendingNumbers, + siteID: siteID, + }) + if err != nil { + resp.Diagnostics.AddError("Error reading user", err.Error()) + return + } + + tflog.Info(ctx, "read user") + + var filter *dataSourceModelFilter + if data.Filter != nil { + filter = &dataSourceModelFilter{ + Type: data.Filter.Type, + ExtensionType: data.Filter.ExtensionType, + NumberType: data.Filter.NumberType, + PendingNumbers: data.Filter.PendingNumbers, + SiteID: data.Filter.SiteID, + } + } + output := dataSourceModel{ + Filter: filter, + User: lo.Map(dto.user, func(item *readDtoUser, _index int) *dataSourceModelUser { + var assignee *dataSourceModelAssignee + if item.assignee != nil { + assignee = &dataSourceModelAssignee{ + ExtensionNumber: item.assignee.extensionNumber, + ID: item.assignee.id, + Name: item.assignee.name, + Type: item.assignee.typ, + } + } + var carrier *dataSourceModelCarrier + if item.carrier != nil { + carrier = &dataSourceModelCarrier{ + Code: item.carrier.code, + Name: item.carrier.name, + } + } + var emergencyAddress *dataSourceModelEmergencyAddress + if item.emergencyAddress != nil { + emergencyAddress = &dataSourceModelEmergencyAddress{ + AddressLine1: item.emergencyAddress.addressLine1, + AddressLine2: item.emergencyAddress.addressLine2, + City: item.emergencyAddress.city, + Country: item.emergencyAddress.country, + StateCode: item.emergencyAddress.stateCode, + Zip: item.emergencyAddress.zip, + } + } + var sipGroup *dataSourceModelSipGroup + if item.sipGroup != nil { + sipGroup = &dataSourceModelSipGroup{ + DisplayName: item.sipGroup.displayName, + ID: item.sipGroup.id, + } + } + var site *dataSourceModelSite + if item.site != nil { + site = &dataSourceModelSite{ + ID: item.site.id, + Name: item.site.name, + } + } + return &dataSourceModelUser{ + Assignee: assignee, + Capability: item.capability, + Carrier: carrier, + DisplayName: item.displayName, + EmergencyAddress: emergencyAddress, + EmergencyAddressStatus: item.emergencyAddressStatus, + EmergencyAddressUpdateTime: item.emergencyAddressUpdateTime, + ID: item.id, + Location: item.location, + Number: item.number, + NumberType: item.numberType, + SipGroup: sipGroup, + Site: site, + Source: item.source, + Status: item.status, + } + }), + } + + diags := resp.State.Set(ctx, &output) + resp.Diagnostics.Append(diags...) + if resp.Diagnostics.HasError() { + return + } +} diff --git a/internal/util/typesuser.go b/internal/util/typesuser.go new file mode 100644 index 0000000..7604c75 --- /dev/null +++ b/internal/util/typesuser.go @@ -0,0 +1,44 @@ +package util + +import ( + "time" + + "github.com/folio-sec/terraform-provider-zoom/generated/api/zoomuser" + "github.com/hashicorp/terraform-plugin-framework/types" +) + +func ToUserOptBool(o types.Bool) zoomuser.OptBool { + if o.IsNull() || o.IsUnknown() { + return zoomuser.OptBool{} + } + return zoomuser.NewOptBool(o.ValueBool()) +} + +func ToUserOptString(o types.String) zoomuser.OptString { + if o.IsNull() || o.IsUnknown() { + return zoomuser.OptString{} + } + return zoomuser.NewOptString(o.ValueString()) +} + +func ToUserOptInt64(o types.Int64) zoomuser.OptInt64 { + if o.IsNull() || o.IsUnknown() { + return zoomuser.OptInt64{} + } + return zoomuser.NewOptInt64(o.ValueInt64()) +} + +func ToUserOptInt(o types.Int32) zoomuser.OptInt { + if o.IsNull() || o.IsUnknown() { + return zoomuser.OptInt{} + } + return zoomuser.NewOptInt(int(o.ValueInt32())) +} + +func ToUserOptDateTime(o types.String) zoomuser.OptDateTime { + if o.IsNull() || o.IsUnknown() { + return zoomuser.OptDateTime{} + } + value, _ := time.Parse(o.ValueString(), "2006-01-02 15:04:05.999999999 -0700 MST") + return zoomuser.NewOptDateTime(value) +}