diff --git a/docs/resources/collection.md b/docs/resources/collection.md index eada9a3..944804b 100644 --- a/docs/resources/collection.md +++ b/docs/resources/collection.md @@ -14,8 +14,7 @@ Group of related documents which are roughly equivalent to a table in a relation ```terraform resource "typesense_collection" "my_collection" { - name = "my-collection" - default_sorting_field = "" //if not needed, should be set empty string to match Typesense collection schema + name = "my-collection" fields { facet = true @@ -40,11 +39,11 @@ resource "typesense_collection" "my_collection" { ### Required -- `default_sorting_field` (String) Default sorting field - `name` (String) Collection name ### Optional +- `default_sorting_field` (String) Default sorting field - `enable_nested_fields` (Boolean) Enable nested fields, must be enabled to use object/object[] types - `fields` (Block List) (see [below for nested schema](#nestedblock--fields)) diff --git a/docs/resources/synonym.md b/docs/resources/synonym.md new file mode 100644 index 0000000..bdeb0b0 --- /dev/null +++ b/docs/resources/synonym.md @@ -0,0 +1,48 @@ +--- +# generated by https://github.com/hashicorp/terraform-plugin-docs +page_title: "typesense_synonym Resource - typesense" +subcategory: "" +description: |- + The synonyms feature allows you to define search terms that should be considered equivalent. For eg: when you define a synonym for sneaker as shoe, searching for sneaker will now return all records with the word shoe in them, in addition to records with the word sneaker. +--- + +# typesense_synonym (Resource) + +The synonyms feature allows you to define search terms that should be considered equivalent. For eg: when you define a synonym for sneaker as shoe, searching for sneaker will now return all records with the word shoe in them, in addition to records with the word sneaker. + +## Example Usage + +```terraform +resource "typesense_synonym" "my_synonym" { + name = "smart-phone-synonym" + collection_name = typesense_collection.my_collection.name + root = "smart phone" + + synonyms = ["iphone", "android"] +} +``` + + +## Schema + +### Required + +- `collection_name` (String) Collection name +- `name` (String) Name identifier +- `synonyms` (List of String) Array of words that should be considered as synonyms. + +### Optional + +- `root` (String) For 1-way synonyms, indicates the root word that words in the synonyms parameter map to + +### Read-Only + +- `id` (String) Id identifier + +## Import + +Import is supported using the following syntax: + +```shell +terraform import typesense_synonym.my_synonym my-synonym +``` diff --git a/examples/provider-install-verification/main.tf b/examples/provider-install-verification/main.tf index 1616849..dea81a9 100644 --- a/examples/provider-install-verification/main.tf +++ b/examples/provider-install-verification/main.tf @@ -10,8 +10,7 @@ terraform { provider "typesense" { } resource "typesense_collection" "test_collection" { - name = "adanylenko-test-collection-v2" - default_sorting_field = "" + name = "adanylenko-test-collection-v2" fields { facet = true @@ -29,12 +28,12 @@ resource "typesense_collection" "test_collection" { type = "string" } - fields { - facet = true - index = true - name = "test_field_2_updated" - optional = true - type = "object" - } +} + + +resource "typesense_synonym" "test" { + name = "test" + collection_name = typesense_collection.test_collection.name + synonyms = ["updated1", "value2", "value3"] } diff --git a/examples/resources/typesense_collection/resource.tf b/examples/resources/typesense_collection/resource.tf index a9ffa51..d9526e7 100644 --- a/examples/resources/typesense_collection/resource.tf +++ b/examples/resources/typesense_collection/resource.tf @@ -1,6 +1,5 @@ resource "typesense_collection" "my_collection" { - name = "my-collection" - default_sorting_field = "" //if not needed, should be set empty string to match Typesense collection schema + name = "my-collection" fields { facet = true diff --git a/examples/resources/typesense_synonym/import.sh b/examples/resources/typesense_synonym/import.sh new file mode 100644 index 0000000..dbd7e10 --- /dev/null +++ b/examples/resources/typesense_synonym/import.sh @@ -0,0 +1 @@ +terraform import typesense_synonym.my_synonym my-synonym diff --git a/examples/resources/typesense_synonym/resource.tf b/examples/resources/typesense_synonym/resource.tf new file mode 100644 index 0000000..ff0f073 --- /dev/null +++ b/examples/resources/typesense_synonym/resource.tf @@ -0,0 +1,7 @@ +resource "typesense_synonym" "my_synonym" { + name = "my-synonym" + collection_name = typesense_collection.my_collection.name + root = "smart phone" + + synonyms = ["iphone", "android"] +} diff --git a/internal/provider/provider.go b/internal/provider/provider.go index d68ef73..ae66f1b 100644 --- a/internal/provider/provider.go +++ b/internal/provider/provider.go @@ -131,6 +131,7 @@ func (p *TypesenseProvider) Configure(ctx context.Context, req provider.Configur func (p *TypesenseProvider) Resources(_ context.Context) []func() resource.Resource { return []func() resource.Resource{ NewCollectionResource, + NewSynonymResource, } } diff --git a/internal/provider/resource_collection.go b/internal/provider/resource_collection.go index 49a248f..b59ee2f 100644 --- a/internal/provider/resource_collection.go +++ b/internal/provider/resource_collection.go @@ -3,6 +3,7 @@ package provider import ( "context" "fmt" + "strings" "github.com/hashicorp/terraform-plugin-framework-validators/stringvalidator" "github.com/hashicorp/terraform-plugin-framework/path" @@ -71,7 +72,7 @@ func (r *CollectionResource) Schema(ctx context.Context, req resource.SchemaRequ }, }, "default_sorting_field": schema.StringAttribute{ - Required: true, + Optional: true, MarkdownDescription: "Default sorting field", PlanModifiers: []planmodifier.String{ stringplanmodifier.RequiresReplace(), @@ -188,7 +189,11 @@ func (r *CollectionResource) Create(ctx context.Context, req resource.CreateRequ data.Id = types.StringValue(collection.Name) data.Name = types.StringValue(collection.Name) - data.DefaultSortingField = types.StringPointerValue(collection.DefaultSortingField) + + if collection.DefaultSortingField != nil && *collection.DefaultSortingField != "" { + data.DefaultSortingField = types.StringPointerValue(collection.DefaultSortingField) + } + data.EnableNestedFields = types.BoolPointerValue(collection.EnableNestedFields) data.Fields = flattenCollectionFields(collection.Fields) @@ -210,8 +215,12 @@ func (r *CollectionResource) Read(ctx context.Context, req resource.ReadRequest, collection, err := r.client.Collection(id).Retrieve(ctx) if err != nil { - data.Id = types.StringValue("") - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to retrieve collection, got error: %s", err)) + if strings.Contains(err.Error(), "Not Found") { + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to retrieve collection, got error: %s", err)) + } + return } @@ -219,7 +228,11 @@ func (r *CollectionResource) Read(ctx context.Context, req resource.ReadRequest, data.Id = types.StringValue(collection.Name) data.Name = types.StringValue(collection.Name) - data.DefaultSortingField = types.StringPointerValue(collection.DefaultSortingField) + + if collection.DefaultSortingField != nil && *collection.DefaultSortingField != "" { + data.DefaultSortingField = types.StringPointerValue(collection.DefaultSortingField) + } + data.EnableNestedFields = types.BoolPointerValue(collection.EnableNestedFields) data.Fields = flattenCollectionFields(collection.Fields) @@ -331,12 +344,17 @@ func (r *CollectionResource) Delete(ctx context.Context, req resource.DeleteRequ _, err := r.client.Collection(data.Id.ValueString()).Delete(ctx) - data.Id = types.StringValue("") - if err != nil { - resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete example, got error: %s", err)) + if strings.Contains(err.Error(), "Not Found") { + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete collection, got error: %s", err)) + } + return } + + data.Id = types.StringValue("") } func (r *CollectionResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { diff --git a/internal/provider/resource_synonym.go b/internal/provider/resource_synonym.go new file mode 100644 index 0000000..a283429 --- /dev/null +++ b/internal/provider/resource_synonym.go @@ -0,0 +1,234 @@ +package provider + +import ( + "context" + "fmt" + "strings" + + "github.com/hashicorp/terraform-plugin-framework-validators/listvalidator" + "github.com/hashicorp/terraform-plugin-framework/path" + "github.com/hashicorp/terraform-plugin-framework/resource" + "github.com/hashicorp/terraform-plugin-framework/resource/schema" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/planmodifier" + "github.com/hashicorp/terraform-plugin-framework/resource/schema/stringplanmodifier" + "github.com/hashicorp/terraform-plugin-framework/schema/validator" + "github.com/hashicorp/terraform-plugin-framework/types" + "github.com/hashicorp/terraform-plugin-log/tflog" + + "github.com/typesense/typesense-go/typesense" + "github.com/typesense/typesense-go/typesense/api" +) + +// Ensure provider defined types fully satisfy framework interfaces. +var _ resource.Resource = &SynonymResource{} +var _ resource.ResourceWithImportState = &SynonymResource{} + +func NewSynonymResource() resource.Resource { + return &SynonymResource{} +} + +type SynonymResource struct { + client *typesense.Client +} + +type SynonymResourceModel struct { + Id types.String `tfsdk:"id"` + Name types.String `tfsdk:"name"` + CollectionName types.String `tfsdk:"collection_name"` + Root types.String `tfsdk:"root"` + Synonyms []types.String `tfsdk:"synonyms"` +} + +func (r *SynonymResource) Metadata(ctx context.Context, req resource.MetadataRequest, resp *resource.MetadataResponse) { + resp.TypeName = req.ProviderTypeName + "_synonym" +} + +func (r *SynonymResource) Schema(ctx context.Context, req resource.SchemaRequest, resp *resource.SchemaResponse) { + resp.Schema = schema.Schema{ + MarkdownDescription: "The synonyms feature allows you to define search terms that should be considered equivalent. For eg: when you define a synonym for sneaker as shoe, searching for sneaker will now return all records with the word shoe in them, in addition to records with the word sneaker.", + + Attributes: map[string]schema.Attribute{ + "id": schema.StringAttribute{ + Computed: true, + MarkdownDescription: "Id identifier", + PlanModifiers: []planmodifier.String{ + stringplanmodifier.UseStateForUnknown(), + }, + }, + "name": schema.StringAttribute{ + MarkdownDescription: "Name identifier", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "collection_name": schema.StringAttribute{ + MarkdownDescription: "Collection name", + Required: true, + PlanModifiers: []planmodifier.String{ + stringplanmodifier.RequiresReplace(), + }, + }, + "root": schema.StringAttribute{ + Optional: true, + MarkdownDescription: "For 1-way synonyms, indicates the root word that words in the synonyms parameter map to", + }, + "synonyms": schema.ListAttribute{ + Required: true, + ElementType: types.StringType, + Validators: []validator.List{listvalidator.SizeAtLeast(1)}, + MarkdownDescription: "Array of words that should be considered as synonyms.", + }, + }, + } +} + +func (r *SynonymResource) Configure(ctx context.Context, req resource.ConfigureRequest, resp *resource.ConfigureResponse) { + // Prevent panic if the provider has not been configured. + if req.ProviderData == nil { + return + } + + client, ok := req.ProviderData.(*typesense.Client) + + if !ok { + resp.Diagnostics.AddError( + "Unexpected Resource Configure Type", + fmt.Sprintf("Expected *http.Client, got: %T. Please report this issue to the provider developers.", req.ProviderData), + ) + + return + } + + r.client = client +} + +func (r *SynonymResource) Create(ctx context.Context, req resource.CreateRequest, resp *resource.CreateResponse) { + var data SynonymResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + schema := &api.SearchSynonymSchema{} + + schema.Root = data.Root.ValueStringPointer() + schema.Synonyms = convertTerraformArrayToStringArray(data.Synonyms) + + tflog.Info(ctx, "synonyms: "+fmt.Sprint(schema.Synonyms)) + tflog.Info(ctx, "collection name: "+data.CollectionName.ValueString()) + + synonym, err := r.client.Collection(data.CollectionName.ValueString()).Synonyms().Upsert(ctx, data.Name.ValueString(), schema) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create collection, got error: %s", err)) + return + } + + data.Id = types.StringPointerValue(synonym.Id) + data.Name = types.StringPointerValue(synonym.Id) + data.Root = types.StringPointerValue(synonym.Root) + data.Synonyms = convertStringArrayToTerraformArray(synonym.Synonyms) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SynonymResource) Read(ctx context.Context, req resource.ReadRequest, resp *resource.ReadResponse) { + var data SynonymResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + synonym, err := r.client.Collection(data.CollectionName.ValueString()).Synonym(data.Id.ValueString()).Retrieve(ctx) + + if err != nil { + if strings.Contains(err.Error(), "Not Found") { + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to retrieve synonym, got error: %s", err)) + } + + return + } + + data.Id = types.StringPointerValue(synonym.Id) + data.Name = types.StringPointerValue(synonym.Id) + data.Synonyms = convertStringArrayToTerraformArray(synonym.Synonyms) + + if synonym.Root != nil && *synonym.Root != "" { + data.Root = types.StringPointerValue(synonym.Root) + } + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SynonymResource) Update(ctx context.Context, req resource.UpdateRequest, resp *resource.UpdateResponse) { + var data SynonymResourceModel + + // Read Terraform plan data into the model + resp.Diagnostics.Append(req.Plan.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + schema := &api.SearchSynonymSchema{} + + schema.Root = data.Root.ValueStringPointer() + schema.Synonyms = convertTerraformArrayToStringArray(data.Synonyms) + + tflog.Info(ctx, "synonyms: "+fmt.Sprint(schema.Synonyms)) + tflog.Info(ctx, "collection name: "+data.CollectionName.ValueString()) + + synonym, err := r.client.Collection(data.CollectionName.ValueString()).Synonyms().Upsert(ctx, data.Id.ValueString(), schema) + + if err != nil { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to create collection, got error: %s", err)) + return + } + + data.Id = types.StringPointerValue(synonym.Id) + data.Name = types.StringPointerValue(synonym.Id) + data.Root = types.StringPointerValue(synonym.Root) + data.Synonyms = convertStringArrayToTerraformArray(synonym.Synonyms) + + resp.Diagnostics.Append(resp.State.Set(ctx, &data)...) +} + +func (r *SynonymResource) Delete(ctx context.Context, req resource.DeleteRequest, resp *resource.DeleteResponse) { + var data SynonymResourceModel + + // Read Terraform prior state data into the model + resp.Diagnostics.Append(req.State.Get(ctx, &data)...) + + if resp.Diagnostics.HasError() { + return + } + + tflog.Warn(ctx, "###Delete Synonym with id="+data.Id.ValueString()) + + _, err := r.client.Collection(data.CollectionName.ValueString()).Synonym(data.Id.ValueString()).Delete(ctx) + + if err != nil { + if strings.Contains(err.Error(), "Not Found") { + resp.State.RemoveResource(ctx) + } else { + resp.Diagnostics.AddError("Client Error", fmt.Sprintf("Unable to delete synonym, got error: %s", err)) + } + + return + } + + data.Id = types.StringValue("") +} + +func (r *SynonymResource) ImportState(ctx context.Context, req resource.ImportStateRequest, resp *resource.ImportStateResponse) { + resource.ImportStatePassthroughID(ctx, path.Root("id"), req, resp) +} diff --git a/internal/provider/util.go b/internal/provider/util.go new file mode 100644 index 0000000..434e0b6 --- /dev/null +++ b/internal/provider/util.go @@ -0,0 +1,23 @@ +package provider + +import ( + "github.com/hashicorp/terraform-plugin-framework/types" +) + +// convert []types.String to []string +func convertTerraformArrayToStringArray(array []types.String) []string { + arrayString := make([]string, len(array)) + for i, item := range array { + arrayString[i] = item.ValueString() + } + return arrayString +} + +// convert []string to []types.String +func convertStringArrayToTerraformArray(array []string) []types.String { + arrayString := make([]types.String, len(array)) + for i, item := range array { + arrayString[i] = types.StringValue(item) + } + return arrayString +}