From a9c721c02aa2d1f80fb299f3583fd3ca5099a146 Mon Sep 17 00:00:00 2001 From: Patrick Costello Date: Fri, 5 May 2023 13:51:21 -0700 Subject: [PATCH] Introduce google_firestore_field resource. (#7853) * Introduce google_firestore_field resource. Introduces a single resource for managing Firestore fields, matching the REST API. Introduces 2 primary divergences from the proto/REST API: 1) Firestore fields do not support deletion (this is because all fields implicitly can exist in a schemaless system). Deletion of a terraform resource will clear any "overrides" for this field (TTL or indexing). 2) Single field indexes in the proto API were overly nested to allow reusing the same resource for composite indexes. The terraform resource unnests the 1-length field in the indexes list. fixes https://github.com/hashicorp/terraform-provider-google/issues/12151 fixes https://github.com/hashicorp/terraform-provider-google/issues/11419 * Address feedback in review (mostly minor cleanups). * Fix whitespace and add database to basic example. --- mmv1/products/firestore/Field.yaml | 166 ++++++++++++++++++ .../firestore_field.go.erb | 29 +++ .../firestore_field_delete.go.erb | 48 +++++ .../custom_expand/empty_object_if_set.go.erb | 20 +++ .../firestore_field_index_config.go.erb | 52 ++++++ .../firestore_field_index_config.go.erb | 42 +++++ .../custom_import/firestore_field.go.erb | 29 +++ .../terraform/encoders/firestore_field.go.erb | 9 + .../examples/firestore_field_basic.tf.erb | 18 ++ .../firestore_field_complex_field_name.tf.erb | 16 ++ .../firestore_field_match_override.tf.erb | 17 ++ .../examples/firestore_field_timestamp.tf.erb | 9 + .../tests/resource_firestore_field_test.go | 135 ++++++++++++++ 13 files changed, 590 insertions(+) create mode 100644 mmv1/products/firestore/Field.yaml create mode 100644 mmv1/templates/terraform/custom_check_destroy/firestore_field.go.erb create mode 100644 mmv1/templates/terraform/custom_delete/firestore_field_delete.go.erb create mode 100644 mmv1/templates/terraform/custom_expand/empty_object_if_set.go.erb create mode 100644 mmv1/templates/terraform/custom_expand/firestore_field_index_config.go.erb create mode 100644 mmv1/templates/terraform/custom_flatten/firestore_field_index_config.go.erb create mode 100644 mmv1/templates/terraform/custom_import/firestore_field.go.erb create mode 100644 mmv1/templates/terraform/encoders/firestore_field.go.erb create mode 100644 mmv1/templates/terraform/examples/firestore_field_basic.tf.erb create mode 100644 mmv1/templates/terraform/examples/firestore_field_complex_field_name.tf.erb create mode 100644 mmv1/templates/terraform/examples/firestore_field_match_override.tf.erb create mode 100644 mmv1/templates/terraform/examples/firestore_field_timestamp.tf.erb create mode 100644 mmv1/third_party/terraform/tests/resource_firestore_field_test.go diff --git a/mmv1/products/firestore/Field.yaml b/mmv1/products/firestore/Field.yaml new file mode 100644 index 000000000000..0b02c5abb140 --- /dev/null +++ b/mmv1/products/firestore/Field.yaml @@ -0,0 +1,166 @@ +# Copyright 2023 Google Inc. +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +--- !ruby/object:Api::Resource +name: 'Field' +base_url: projects/{{project}}/databases/{{database}}/collectionGroups/{{collection}}/fields +create_url: projects/{{project}}/databases/{{database}}/collectionGroups/{{collection}}/fields/{{field}} +self_link: '{{name}}' +immutable: false +update_verb: :PATCH +update_mask: true +create_verb: :PATCH +description: | + Represents a single field in the database. + Fields are grouped by their "Collection Group", which represent all collections + in the database with the same id. +references: !ruby/object:Api::Resource::ReferenceLinks + guides: + 'Official Documentation': 'https://cloud.google.com/firestore/docs/query-data/indexing' + api: 'https://cloud.google.com/firestore/docs/reference/rest/v1/projects.databases.collectionGroups.fields' +async: !ruby/object:Api::OpAsync + operation: !ruby/object:Api::OpAsync::Operation + path: 'name' + base_url: '{{op_id}}' + wait_ms: 1000 + result: !ruby/object:Api::OpAsync::Result + path: 'response' + resource_inside_response: true + status: !ruby/object:Api::OpAsync::Status + path: 'done' + complete: true + allowed: + - true + - false + error: !ruby/object:Api::OpAsync::Error + path: 'error' + message: 'message' +autogen_async: true +skip_sweeper: true +import_format: ["{{name}}"] +docs: !ruby/object:Provider::Terraform::Docs + warning: | + This resource creates a Firestore Single Field override on a project that + already has a Firestore database. If you haven't already created it, you may + create a `google_firestore_database` resource with `location_id` set to your + chosen location. +examples: + - !ruby/object:Provider::Terraform::Examples + name: "firestore_field_basic" + primary_resource_id: "basic" + test_env_vars: + project_id: :FIRESTORE_PROJECT_NAME + - !ruby/object:Provider::Terraform::Examples + name: "firestore_field_timestamp" + primary_resource_id: "timestamp" + test_env_vars: + project_id: :FIRESTORE_PROJECT_NAME + - !ruby/object:Provider::Terraform::Examples + name: "firestore_field_match_override" + primary_resource_id: "match_override" + test_env_vars: + project_id: :FIRESTORE_PROJECT_NAME +custom_code: !ruby/object:Provider::Terraform::CustomCode + custom_import: templates/terraform/custom_import/index_self_link_as_name_set_project.go.erb + encoder: templates/terraform/encoders/firestore_field.go.erb + custom_delete: templates/terraform/custom_delete/firestore_field_delete.go.erb + test_check_destroy: templates/terraform/custom_check_destroy/firestore_field.go.erb +properties: + - !ruby/object:Api::Type::String + name: 'database' + default_value: '(default)' + description: | + The Firestore database id. Defaults to `"(default)"`. + url_param_only: true + - !ruby/object:Api::Type::String + name: 'collection' + description: | + The id of the collection group to configure. + required: true + url_param_only: true + - !ruby/object:Api::Type::String + name: 'field' + description: | + The id of the field to configure. + required: true + url_param_only: true + - !ruby/object:Api::Type::String + name: name + output: true + description: | + The name of this field. Format: + `projects/{{project}}/databases/{{database}}/collectionGroups/{{collection}}/fields/{{field}}` + - !ruby/object:Api::Type::NestedObject + name: indexConfig + description: | + The single field index configuration for this field. + Creating an index configuration for this field will override any inherited configuration with the + indexes specified. Configuring the index configuration with an empty block disables all indexes on + the field. + send_empty_value: true + custom_expand: templates/terraform/custom_expand/firestore_field_index_config.go.erb + custom_flatten: templates/terraform/custom_flatten/firestore_field_index_config.go.erb + properties: + - !ruby/object:Api::Type::Array + name: indexes + description: The indexes to configure on the field. Order or array contains must be specified. + is_set: true + item_type: !ruby/object:Api::Type::NestedObject + properties: + - !ruby/object:Api::Type::Enum + name: queryScope + description: | + The scope at which a query is run. Collection scoped queries require you specify + the collection at query time. Collection group scope allows queries across all + collections with the same id. + default_value: :COLLECTION + values: + - :COLLECTION + - :COLLECTION_GROUP + - !ruby/object:Api::Type::Enum + name: 'order' + description: | + Indicates that this field supports ordering by the specified order or comparing using =, <, <=, >, >=, !=. + Only one of `order` and `arrayConfig` can be specified. + values: + - :ASCENDING + - :DESCENDING + exactly_one_of: + - order + - arrayConfig + - !ruby/object:Api::Type::Enum + name: 'arrayConfig' + description: | + Indicates that this field supports operations on arrayValues. Only one of `order` and `arrayConfig` can + be specified. + values: + - :CONTAINS + exactly_one_of: + - order + - arrayConfig + - !ruby/object:Api::Type::NestedObject + name: ttlConfig + custom_expand: templates/terraform/custom_expand/empty_object_if_set.go.erb + description: | + If set, this field is configured for TTL deletion. + send_empty_value: true + properties: + - !ruby/object:Api::Type::Enum + name: state + description: | + The state of the TTL configuration. + output: true + values: + - :CREATING + - :ACTIVE + - :NEEDS_REPAIR \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_check_destroy/firestore_field.go.erb b/mmv1/templates/terraform/custom_check_destroy/firestore_field.go.erb new file mode 100644 index 000000000000..7ba2e3ae5b48 --- /dev/null +++ b/mmv1/templates/terraform/custom_check_destroy/firestore_field.go.erb @@ -0,0 +1,29 @@ +// Firestore fields are not deletable. We consider the field deleted if: +// 1) the index configuration has no overrides and matches the ancestor configuration. +// 2) the ttl configuration is unset. + +config := GoogleProviderConfig(t) + +url, err := replaceVarsForTest(config, rs, "{{FirestoreBasePath}}projects/{{project}}/databases/{{database}}/collectionGroups/{{collection}}/fields/{{field}}") +if err != nil { + return err +} + +res, err := SendRequest(config, "GET", "", url, config.UserAgent, nil) +if err != nil { + return err +} + +if v := res["indexConfig"]; v != nil { + indexConfig := v.(map[string]interface{}) + + usesAncestorConfig, ok := indexConfig["usesAncestorConfig"].(bool) + + if !ok || !usesAncestorConfig { + return fmt.Errorf("Index configuration is not using the ancestor config %s.", url) + } +} + +if res["ttlConfig"] != nil { + return fmt.Errorf("TTL configuration was not deleted at %s.", url) +} \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_delete/firestore_field_delete.go.erb b/mmv1/templates/terraform/custom_delete/firestore_field_delete.go.erb new file mode 100644 index 000000000000..cbea55cb5861 --- /dev/null +++ b/mmv1/templates/terraform/custom_delete/firestore_field_delete.go.erb @@ -0,0 +1,48 @@ +// Firestore fields cannot be deleted, instead we clear the indexConfig and ttlConfig. + +log.Printf("[DEBUG] Deleting Field %q", d.Id()) + +billingProject := "" + +project, err := getProject(d, config) +if err != nil { + return fmt.Errorf("Error fetching project for App: %s", err) +} +billingProject = project + +url, err := ReplaceVars(d, config, "{{FirestoreBasePath}}{{name}}") +if err != nil { + return err +} + +updateMask := []string{"indexConfig", "ttlConfig"} + +url, err = AddQueryParams(url, map[string]string{"updateMask": strings.Join(updateMask, ",")}) +if err != nil { + return err +} + + +// err == nil indicates that the billing_project value was found +if bp, err := getBillingProject(d, config); err == nil { + billingProject = bp +} + +// Clear fields by sending an empty PATCH request with appropriate update mask. +req := make(map[string]interface{}) +res, err := SendRequestWithTimeout(config, "PATCH", billingProject, url, userAgent, req, d.Timeout(schema.TimeoutUpdate)) + +if err != nil { + return fmt.Errorf("Error deleting Field %q: %s", d.Id(), err) +} + +err = FirestoreOperationWaitTime( + config, res, project, "Deleting Field", userAgent, + d.Timeout(schema.TimeoutDelete)) + +if err != nil { + return err +} + +log.Printf("[DEBUG] Finished deleting Field %q", d.Id()) +return nil \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_expand/empty_object_if_set.go.erb b/mmv1/templates/terraform/custom_expand/empty_object_if_set.go.erb new file mode 100644 index 000000000000..49f29d58bf29 --- /dev/null +++ b/mmv1/templates/terraform/custom_expand/empty_object_if_set.go.erb @@ -0,0 +1,20 @@ +/* + * Expands an empty terraform config into an empty object. + * + * Used to differentate a user specifying an empty block versus a null/unset block. + * + * This is unique from send_empty_value, which will send an explicit null value + * for empty configuration blocks. + */ +func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + if v == nil { + return nil, nil + } + + l := v.([]interface{}) + if len(l) == 0 { + return nil, nil + } + // A set, but empty object. + return struct{}{}, nil +} \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_expand/firestore_field_index_config.go.erb b/mmv1/templates/terraform/custom_expand/firestore_field_index_config.go.erb new file mode 100644 index 000000000000..f7e59d0c2e8b --- /dev/null +++ b/mmv1/templates/terraform/custom_expand/firestore_field_index_config.go.erb @@ -0,0 +1,52 @@ +func expand<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d TerraformResourceData, config *transport_tpg.Config) (interface{}, error) { + // We drop all output only fields as they are unnecessary. + if v == nil { + return nil, nil + } + + l := v.([]interface{}) + if len(l) == 0 { + return nil, nil + } + + transformedIndexConfig := make(map[string]interface{}) + + // A configured, but empty, index_config block should be sent. This is how a user would remove all indexes. + if l[0] == nil { + return transformedIndexConfig, nil + } + + indexConfig := l[0].(map[string]interface{}) + + // For Single field indexes, we put the field configuration on the index to avoid forced nesting. + // Push all order/arrayConfig down into a single element fields list. + l = indexConfig["indexes"].(*schema.Set).List() + transformedIndexes := make([]interface{}, 0, len(l)) + for _, raw := range l { + if raw == nil { + continue + } + original := raw.(map[string]interface{}) + transformed := make(map[string]interface{}) + transformedField := make(map[string]interface{}) + + if val := reflect.ValueOf(original["query_scope"]); val.IsValid() && !isEmptyValue(val) { + transformed["queryScope"] = original["query_scope"] + } + + if val := reflect.ValueOf(original["order"]); val.IsValid() && !isEmptyValue(val) { + transformedField["order"] = original["order"] + } + + if val := reflect.ValueOf(original["array_config"]); val.IsValid() && !isEmptyValue(val) { + transformedField["arrayConfig"] = original["array_config"] + } + transformed["fields"] = [1]interface{}{ + transformedField, + } + + transformedIndexes = append(transformedIndexes, transformed) + } + transformedIndexConfig["indexes"] = transformedIndexes + return transformedIndexConfig, nil +} \ No newline at end of file diff --git a/mmv1/templates/terraform/custom_flatten/firestore_field_index_config.go.erb b/mmv1/templates/terraform/custom_flatten/firestore_field_index_config.go.erb new file mode 100644 index 000000000000..eb14a63c2735 --- /dev/null +++ b/mmv1/templates/terraform/custom_flatten/firestore_field_index_config.go.erb @@ -0,0 +1,42 @@ +func flatten<%= prefix -%><%= titlelize_property(property) -%>(v interface{}, d TerraformResourceData, config *transport_tpg.Config) (interface{}) { + if v == nil { + return v + } + indexConfig := v.(map[string]interface{}) + + usesAncestorConfig := false + if indexConfig["usesAncestorConfig"] != nil { + usesAncestorConfig = indexConfig["usesAncestorConfig"].(bool) + } + + if usesAncestorConfig { + // The intent when uses_ancestor_config is no config. + return []interface{}{} + } + + if indexConfig["indexes"] == nil { + // No indexes, return an existing, but empty index config. + return [1]interface{}{nil} + } + + // For Single field indexes, we put the field configuration on the index to avoid forced nesting. + l := indexConfig["indexes"].([]interface{}) + transformed := make(map[string]interface{}) + transformedIndexes := make([]interface{}, 0, len(l)) + for _, raw := range l { + original := raw.(map[string]interface{}) + if len(original) < 1 { + // Do not include empty json objects coming back from the api + continue + } + fields := original["fields"].([]interface{}) + sfi := fields[0].(map[string]interface{}) + transformedIndexes = append(transformedIndexes, map[string]interface{}{ + "query_scope": original["queryScope"], + "order": sfi["order"], + "array_config": sfi["arrayConfig"], + }) + } + transformed["indexes"] = transformedIndexes + return []interface{}{transformed} +} diff --git a/mmv1/templates/terraform/custom_import/firestore_field.go.erb b/mmv1/templates/terraform/custom_import/firestore_field.go.erb new file mode 100644 index 000000000000..1ccd66835994 --- /dev/null +++ b/mmv1/templates/terraform/custom_import/firestore_field.go.erb @@ -0,0 +1,29 @@ + +config := meta.(*transport_tpg.Config) + +// current import_formats can't import fields with forward slashes in their value +if err := ParseImportId([]string{"(?P.+)"}, d, config); err != nil { + return nil, err +} + +// Re-populate split fields from the name. +re := regexp.MustCompile("^projects/([^/]+)/databases/([^/]+)/collectionGroups/([^/]+)/fields/(.+)$") +match := re.FindStringSubmatch(d.Get("name").(string)) +if len(match) > 0 { + if err := d.Set("project", match[1]); err != nil { + return nil, fmt.Errorf("Error setting project: %s", err) + } + if err := d.Set("database", match[2]); err != nil { + return nil, fmt.Errorf("Error setting database: %s", err) + } + if err := d.Set("collection", match[3]); err != nil { + return nil, fmt.Errorf("Error setting collection: %s", err) + } + if err := d.Set("field", match[4]); err != nil { + return nil, fmt.Errorf("Error setting field: %s", err) + } +} else { + return nil, fmt.Errorf("import did not match the regex ^projects/([^/]+)/databases/([^/]+)/collectionGroups/([^/]+)/fields/(.+)$") +} + +return []*schema.ResourceData{d}, nil diff --git a/mmv1/templates/terraform/encoders/firestore_field.go.erb b/mmv1/templates/terraform/encoders/firestore_field.go.erb new file mode 100644 index 000000000000..96383af29f6e --- /dev/null +++ b/mmv1/templates/terraform/encoders/firestore_field.go.erb @@ -0,0 +1,9 @@ + +// We've added project / database / collection / field as split fields of the name, but +// the API doesn't expect them. Make sure we remove them from any requests. + +delete(obj, "project") +delete(obj, "database") +delete(obj, "collection") +delete(obj, "field") +return obj, nil diff --git a/mmv1/templates/terraform/examples/firestore_field_basic.tf.erb b/mmv1/templates/terraform/examples/firestore_field_basic.tf.erb new file mode 100644 index 000000000000..71b23926b04f --- /dev/null +++ b/mmv1/templates/terraform/examples/firestore_field_basic.tf.erb @@ -0,0 +1,18 @@ +resource "google_firestore_field" "<%= ctx[:primary_resource_id] %>" { + project = "<%= ctx[:test_env_vars]['project_id'] %>" + database = "(default)" + collection = "chatrooms_%{random_suffix}" + field = "basic" + + index_config { + indexes { + order = "ASCENDING" + query_scope = "COLLECTION_GROUP" + } + indexes { + array_config = "CONTAINS" + } + } + + ttl_config {} +} diff --git a/mmv1/templates/terraform/examples/firestore_field_complex_field_name.tf.erb b/mmv1/templates/terraform/examples/firestore_field_complex_field_name.tf.erb new file mode 100644 index 000000000000..8f0c7ccb1688 --- /dev/null +++ b/mmv1/templates/terraform/examples/firestore_field_complex_field_name.tf.erb @@ -0,0 +1,16 @@ +resource "google_firestore_field" "<%= ctx[:primary_resource_id] %>" { + project = "<%= ctx[:test_env_vars]['project_id'] %>" + collection = "chatrooms_%{random_suffix}" + field = "`*`" + + index_config { + indexes { + order = "ASCENDING" + query_scope = "COLLECTION_GROUP" + } + indexes { + array_config = "CONTAINS" + } + } + ttl_config {} +} diff --git a/mmv1/templates/terraform/examples/firestore_field_match_override.tf.erb b/mmv1/templates/terraform/examples/firestore_field_match_override.tf.erb new file mode 100644 index 000000000000..cebafc386f33 --- /dev/null +++ b/mmv1/templates/terraform/examples/firestore_field_match_override.tf.erb @@ -0,0 +1,17 @@ +resource "google_firestore_field" "<%= ctx[:primary_resource_id] %>" { + project = "<%= ctx[:test_env_vars]['project_id'] %>" + collection = "chatrooms_%{random_suffix}" + field = "field_with_same_configuration_as_ancestor" + + index_config { + indexes { + order = "ASCENDING" + } + indexes { + order = "DESCENDING" + } + indexes { + array_config = "CONTAINS" + } + } +} \ No newline at end of file diff --git a/mmv1/templates/terraform/examples/firestore_field_timestamp.tf.erb b/mmv1/templates/terraform/examples/firestore_field_timestamp.tf.erb new file mode 100644 index 000000000000..6defbde35498 --- /dev/null +++ b/mmv1/templates/terraform/examples/firestore_field_timestamp.tf.erb @@ -0,0 +1,9 @@ +resource "google_firestore_field" "<%= ctx[:primary_resource_id] %>" { + project = "<%= ctx[:test_env_vars]['project_id'] %>" + collection = "chatrooms_%{random_suffix}" + field = "timestamp" + + // Disable all single field indexes for the timestamp property. + index_config {} + ttl_config {} +} \ No newline at end of file diff --git a/mmv1/third_party/terraform/tests/resource_firestore_field_test.go b/mmv1/third_party/terraform/tests/resource_firestore_field_test.go new file mode 100644 index 000000000000..1a6fd0108eaa --- /dev/null +++ b/mmv1/third_party/terraform/tests/resource_firestore_field_test.go @@ -0,0 +1,135 @@ +package google + +import ( + "fmt" + "testing" + + "github.com/hashicorp/terraform-plugin-sdk/v2/helper/resource" +) + +func TestAccFirestoreField_firestoreFieldUpdateAddIndexExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project_id": GetTestFirestoreProjectFromEnv(t), + "random_suffix": RandString(t, 10), + "resource_name": "add_index", + } + testAccFirestoreField_runUpdateTest(testAccFirestoreField_firestoreFieldUpdateAddIndexExample(context), t, context) +} + +func TestAccFirestoreField_firestoreFieldUpdateAddTTLExample(t *testing.T) { + t.Parallel() + + context := map[string]interface{}{ + "project_id": GetTestFirestoreProjectFromEnv(t), + "random_suffix": RandString(t, 10), + "resource_name": "add_ttl", + } + testAccFirestoreField_runUpdateTest(testAccFirestoreField_firestoreFieldUpdateAddTTLExample(context), t, context) +} + +func testAccFirestoreField_runUpdateTest(updateConfig string, t *testing.T, context map[string]interface{}) { + resourceName := context["resource_name"].(string) + + VcrTest(t, resource.TestCase{ + PreCheck: func() { AccTestPreCheck(t) }, + ProtoV5ProviderFactories: ProtoV5ProviderFactories(t), + CheckDestroy: testAccCheckFirestoreFieldDestroyProducer(t), + Steps: []resource.TestStep{ + { + Config: testAccFirestoreField_firestoreFieldUpdateInitialExample(context), + }, + { + ResourceName: fmt.Sprintf("google_firestore_field.%s", resourceName), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"database", "collection", "field"}, + }, + { + Config: updateConfig, + }, + { + ResourceName: fmt.Sprintf("google_firestore_field.%s", resourceName), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"database", "collection", "field"}, + }, + { + Config: testAccFirestoreField_firestoreFieldUpdateInitialExample(context), + }, + { + ResourceName: fmt.Sprintf("google_firestore_field.%s", resourceName), + ImportState: true, + ImportStateVerify: true, + ImportStateVerifyIgnore: []string{"database", "collection", "field"}, + }, + }, + }) +} + +func testAccFirestoreField_firestoreFieldUpdateInitialExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_firestore_field" "%{resource_name}" { + project = "%{project_id}" + collection = "chatrooms_%{random_suffix}" + field = "%{resource_name}" + + index_config { + indexes { + order = "ASCENDING" + query_scope = "COLLECTION_GROUP" + } + indexes { + array_config = "CONTAINS" + } + } +} +`, context) +} + +func testAccFirestoreField_firestoreFieldUpdateAddTTLExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_firestore_field" "%{resource_name}" { + project = "%{project_id}" + collection = "chatrooms_%{random_suffix}" + field = "%{resource_name}" + + index_config { + indexes { + order = "ASCENDING" + query_scope = "COLLECTION_GROUP" + } + indexes { + array_config = "CONTAINS" + } + } + + ttl_config {} +} +`, context) +} + +func testAccFirestoreField_firestoreFieldUpdateAddIndexExample(context map[string]interface{}) string { + return Nprintf(` +resource "google_firestore_field" "%{resource_name}" { + project = "%{project_id}" + collection = "chatrooms_%{random_suffix}" + field = "%{resource_name}" + + index_config { + indexes { + order = "ASCENDING" + query_scope = "COLLECTION_GROUP" + } + indexes { + array_config = "CONTAINS" + } + indexes { + order = "DESCENDING" + query_scope = "COLLECTION_GROUP" + } + } +} +`, context) +}