diff --git a/docs/modules/gcloud.md b/docs/modules/gcloud.md index 4b22fd1fa..3195a51f1 100644 --- a/docs/modules/gcloud.md +++ b/docs/modules/gcloud.md @@ -8,16 +8,16 @@ Testcontainers module for the Google Cloud Platform's [Cloud SDK](https://cloud. npm install @testcontainers/gcloud --save-dev ``` -The module now supports multiple emulators, including `firestore`, which offers both `native` and `datastore` modes. -To utilize these emulators, you should employ the following classes: +The module supports multiple emulators. Use the following classes: -Emulator | Class | Container Image +Emulator | Class | Container Image -|-|- -Firestore (Native mode) | FirestoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) -Firestore (Datastore mode) | DatastoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) +Firestore (Native mode) | FirestoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) +Firestore (Datastore mode) | DatastoreEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) Cloud PubSub | PubSubEmulatorContainer | [gcr.io/google.com/cloudsdktool/google-cloud-cli:emulators](https://gcr.io/google.com/cloudsdktool/google-cloud-cli) -Cloud Storage | CloudStorageEmulatorContainer | [https://hub.docker.com/r/fsouza/fake-gcs-server](https://hub.docker.com/r/fsouza/fake-gcs-server) -BigQuery | BigQueryEmulatorContainer | [ghcr.io/goccy/bigquery-emulator](ghcr.io/goccy/bigquery-emulator) +Cloud Storage | CloudStorageEmulatorContainer | [fsouza/fake-gcs-server:1.52.2](https://hub.docker.com/r/fsouza/fake-gcs-server) +BigQuery | BigQueryEmulatorContainer | [ghcr.io/goccy/bigquery-emulator:0.6.6](https://ghcr.io/goccy/bigquery-emulator) +Cloud Spanner | SpannerEmulatorContainer | [gcr.io/cloud-spanner-emulator/emulator:1.5.37](https://gcr.io/cloud-spanner-emulator/emulator:1.5.37) ## Examples @@ -49,16 +49,32 @@ BigQuery | BigQueryEmulatorContainer | [ghcr.io/goccy/bigquery-emulator](ghcr. ### Cloud Storage -The Cloud Storage container doesn't rely on a built-in emulator created by Google but instead depends on a fake Cloud Storage server implemented by [Francisco Souza](https://github.com/fsouza). The project is open-source, and the repository can be found at [fsouza/fake-gcs-server](https://github.com/fsouza/fake-gcs-server). +The Cloud Storage container uses a fake Cloud Storage server by [Francisco Souza](https://github.com/fsouza). + [Starting a Cloud Storage Emulator container with the default image](../../packages/modules/gcloud/src/cloudstorage-emulator-container.test.ts) inside_block:cloud-storage ### BigQuery -The BigQuery container doesn't rely on a built-in emulator created by Google, but instead depends on an implementation written in Go by [Masaaki Goshima](https://github.com/goccy). The project is open-source, and the repository can be found at [goccy/bigquery-emulator](https://github.com/goccy/bigquery-emulator). +The BigQuery emulator is by [Masaaki Goshima](https://github.com/goccy) and uses [go-zetasqlite](https://github.com/goccy/go-zetasqlite). -BigQuery emulator uses [go-zetasqlite](https://github.com/goccy/go-zetasqlite) to interpret ZetaSQL (the language used in BigQuery) and runs it in SQLite. The [README](https://github.com/goccy/go-zetasqlite?tab=readme-ov-file#status) lists BigQuery features currently supported. [Starting a BigQuery Emulator container with the default image](../../packages/modules/gcloud/src/bigquery-emulator-container.test.ts) + +### Cloud Spanner + +The Cloud Spanner emulator container wraps Google's official emulator image. + + +[Starting a Spanner Emulator container and exposing endpoints using explicitly configured client](../../packages/modules/gcloud/src/spanner-emulator-container.test.ts) inside_block:startupWithExplicitClient + + + +[Starting a Spanner Emulator container and exposing endpoints using projectId and SPANNER_EMULATOR_HOST](../../packages/modules/gcloud/src/spanner-emulator-container.test.ts) inside_block:startupWithEnvironmentVariable + + + +[Creating and deleting instance and database via helper](../../packages/modules/gcloud/src/spanner-emulator-helper.test.ts) inside_block:createAndDelete + diff --git a/package-lock.json b/package-lock.json index 668f09786..d41509f7b 100644 --- a/package-lock.json +++ b/package-lock.json @@ -4442,6 +4442,271 @@ "node": ">= 6" } }, + "node_modules/@google-cloud/spanner": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/spanner/-/spanner-8.0.0.tgz", + "integrity": "sha512-IJn+8A3QZJfe7FUtWqHVNo3xJs7KFpurCWGWCiCz3oEh+BkRymKZ1QxfAbU2yGMDzTytLGQ2IV6T2r3cuo75/w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@google-cloud/common": "^6.0.0", + "@google-cloud/precise-date": "^5.0.0", + "@google-cloud/projectify": "^5.0.0", + "@google-cloud/promisify": "^5.0.0", + "@grpc/proto-loader": "^0.7.13", + "@opentelemetry/api": "^1.9.0", + "@opentelemetry/context-async-hooks": "^2.0.0", + "@opentelemetry/core": "^2.0.0", + "@opentelemetry/semantic-conventions": "^1.30.0", + "@types/big.js": "^6.2.2", + "@types/stack-trace": "^0.0.33", + "big.js": "^7.0.0", + "checkpoint-stream": "^0.1.2", + "duplexify": "^4.1.3", + "events-intercept": "^2.0.0", + "extend": "^3.0.2", + "google-auth-library": "^10.0.0-rc.1", + "google-gax": "^5.0.1-rc.0", + "grpc-gcp": "^1.0.1", + "is": "^3.3.0", + "lodash.snakecase": "^4.1.1", + "merge-stream": "^2.0.0", + "p-queue": "^6.0.2", + "protobufjs": "^7.4.0", + "retry-request": "^8.0.0", + "split-array-stream": "^2.0.0", + "stack-trace": "0.0.10", + "stream-events": "^1.0.5", + "teeny-request": "^10.0.0", + "through2": "^4.0.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/@google-cloud/projectify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/projectify/-/projectify-5.0.0.tgz", + "integrity": "sha512-XXQLaIcLrOAMWvRrzz+mlUGtN6vlVNja3XQbMqRi/V7XJTAVwib3VcKd7oRwyZPkp7rBVlHGcaqdyGRrcnkhlA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/@google-cloud/promisify": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/@google-cloud/promisify/-/promisify-5.0.0.tgz", + "integrity": "sha512-N8qS6dlORGHwk7WjGXKOSsLjIjNINCPicsOX6gyyLiYk7mq3MtII96NZ9N2ahwA2vnkLmZODOIH9rlNniYWvCQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/@opentelemetry/core": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-2.0.1.tgz", + "integrity": "sha512-MaZk9SJIDgo1peKevlbhP6+IwIiNPNmswNL4AF0WaQJLbHXjr9SrZMgS12+iqr9ToV4ZVosCcc0f8Rg67LXjxw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.29.0" + }, + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, + "node_modules/@google-cloud/spanner/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@google-cloud/spanner/node_modules/big.js": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/big.js/-/big.js-7.0.1.tgz", + "integrity": "sha512-iFgV784tD8kq4ccF1xtNMZnXeZzVuXWWM+ERFzKQjv+A5G9HC8CY3DuV45vgzFFcW+u2tIvmF95+AzWgs6BjCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/bigjs" + } + }, + "node_modules/@google-cloud/spanner/node_modules/gaxios": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.1.tgz", + "integrity": "sha512-Odju3uBUJyVCkW64nLD4wKLhbh93bh6vIg/ZIXkWiLPBrdgtc65+tls/qml+un3pr6JqYVFDZbbmLDQT68rTOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/gaxios/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@google-cloud/spanner/node_modules/gcp-metadata": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-7.0.1.tgz", + "integrity": "sha512-UcO3kefx6dCcZkgcTGgVOTFb7b1LlQ02hY1omMjjrrBzkajRMCFgYOjs7J71WqnuG1k2b+9ppGL7FsOfhZMQKQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/google-auth-library": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.1.0.tgz", + "integrity": "sha512-GspVjZj1RbyRWpQ9FbAXMKjFGzZwDKnUHi66JJ+tcjcu5/xYAP1pdlWotCuIkMwjfVsxxDvsGZXGLzRt72D0sQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.0.0", + "gcp-metadata": "^7.0.0", + "google-logging-utils": "^1.0.0", + "gtoken": "^8.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/google-gax": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/google-gax/-/google-gax-5.0.1.tgz", + "integrity": "sha512-I8fTFXvIG8tYpiDxDXwCXoFsTVsvHJ2GA7DToH+eaRccU8r3nqPMFghVb2GdHSVcu4pq9ScRyB2S1BjO+vsa1Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.12.6", + "@grpc/proto-loader": "^0.7.13", + "abort-controller": "^3.0.0", + "duplexify": "^4.1.3", + "google-auth-library": "^10.1.0", + "google-logging-utils": "^1.1.1", + "node-fetch": "^3.3.2", + "object-hash": "^3.0.0", + "proto3-json-serializer": "^3.0.0", + "protobufjs": "^7.5.3", + "retry-request": "^8.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/gtoken": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-8.0.0.tgz", + "integrity": "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw==", + "dev": true, + "license": "MIT", + "dependencies": { + "gaxios": "^7.0.0", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/@google-cloud/spanner/node_modules/proto3-json-serializer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/proto3-json-serializer/-/proto3-json-serializer-3.0.1.tgz", + "integrity": "sha512-Rug90pDIefARAG9MgaFjd0yR/YP4bN3Fov00kckXMjTZa0x86c4WoWfCQFdSeWi9DvRXjhfLlPDIvODB5LOTfg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "protobufjs": "^7.4.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/retry-request": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/retry-request/-/retry-request-8.0.0.tgz", + "integrity": "sha512-dJkZNmyV9C8WKUmbdj1xcvVlXBSvsUQCkg89TCK8rD72RdSn9A2jlXlS2VuYSTHoPJjJEfUHhjNYrlvuksF9cg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/request": "^2.48.12", + "extend": "^3.0.2", + "teeny-request": "^10.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@google-cloud/spanner/node_modules/teeny-request": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/teeny-request/-/teeny-request-10.1.0.tgz", + "integrity": "sha512-3ZnLvgWF29jikg1sAQ1g0o+lr5JX6sVgYvfUJazn7ZjJroDBUTWp44/+cFVX0bULjv4vci+rBD+oGVAkWqhUbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "http-proxy-agent": "^5.0.0", + "https-proxy-agent": "^5.0.0", + "node-fetch": "^3.3.2", + "stream-events": "^1.0.5" + }, + "engines": { + "node": ">=18" + } + }, "node_modules/@google-cloud/storage": { "version": "7.16.0", "resolved": "https://registry.npmjs.org/@google-cloud/storage/-/storage-7.16.0.tgz", @@ -5669,6 +5934,19 @@ "node": ">=8.0.0" } }, + "node_modules/@opentelemetry/context-async-hooks": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/context-async-hooks/-/context-async-hooks-2.0.1.tgz", + "integrity": "sha512-XuY23lSI3d4PEqKA+7SLtAgwqIfc6E/E9eAQWLN1vlpC53ybO3o6jW4BsXo1xvz9lYyyWItfQDDLzezER01mCw==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": "^18.19.0 || >=20.6.0" + }, + "peerDependencies": { + "@opentelemetry/api": ">=1.0.0 <1.10.0" + } + }, "node_modules/@opentelemetry/core": { "version": "1.30.1", "resolved": "https://registry.npmjs.org/@opentelemetry/core/-/core-1.30.1.tgz", @@ -7319,6 +7597,13 @@ "integrity": "sha512-HlZ6Dcr205BmNhwkdXqrg2vkFMN2PluI7Lgr8In3B3wE5PiQHhjRqtW/lGdVU9gw+sM0JcIDx2AN+cW8oSWIcw==", "dev": true }, + "node_modules/@types/big.js": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/@types/big.js/-/big.js-6.2.2.tgz", + "integrity": "sha512-e2cOW9YlVzFY2iScnGBBkplKsrn2CsObHQ2Hiw4V1sSyiGbgWL8IyqE3zFi1Pt5o1pdAtYkDAIsF3KKUPjdzaA==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/body-parser": { "version": "1.19.5", "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.5.tgz", @@ -7424,6 +7709,16 @@ "@types/ssh2": "*" } }, + "node_modules/@types/duplexify": { + "version": "3.6.4", + "resolved": "https://registry.npmjs.org/@types/duplexify/-/duplexify-3.6.4.tgz", + "integrity": "sha512-2eahVPsd+dy3CL6FugAzJcxoraWhUghZGEQJns1kTKfCXWKJ5iG/VkaB05wRVrDKHfOFKqb0X0kXh91eE99RZg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", @@ -7578,6 +7873,17 @@ "integrity": "sha512-k4fBDScfZ9xQjIrZm0HcJlyWVZ5ltE9W8N2AIecsgFXfg5REhKKHEwmpRvSQvEPWnIEsL67K70P1tx7kGo+cjQ==", "dev": true }, + "node_modules/@types/pumpify": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/@types/pumpify/-/pumpify-1.4.4.tgz", + "integrity": "sha512-+cWbQUecD04MQYkjNBhPmcUIP368aloYmqm+ImdMKA8rMpxRNAhZAD6gIj+sAVTF1DliqrT/qUp6aGNi/9U3tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/duplexify": "*", + "@types/node": "*" + } + }, "node_modules/@types/qs": { "version": "6.9.14", "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.9.14.tgz", @@ -7702,6 +8008,13 @@ "@types/node": "*" } }, + "node_modules/@types/stack-trace": { + "version": "0.0.33", + "resolved": "https://registry.npmjs.org/@types/stack-trace/-/stack-trace-0.0.33.tgz", + "integrity": "sha512-O7in6531Bbvlb2KEsJ0dq0CHZvc3iWSR5ZYMtvGgnHA56VgriAN/AU2LorfmcvAl2xc9N5fbCTRyMRRl8nd74g==", + "dev": true, + "license": "MIT" + }, "node_modules/@types/statuses": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/@types/statuses/-/statuses-2.0.5.tgz", @@ -9556,6 +9869,92 @@ "node": ">= 16" } }, + "node_modules/checkpoint-stream": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/checkpoint-stream/-/checkpoint-stream-0.1.2.tgz", + "integrity": "sha512-eYXIcydL3mPjjEVLxHdi1ISgTwmxGJZ8vyJ3lYVvFTDRyTOZMTbKZdRJqiA7Gi1rPcwOyyzcrZmGLL8ff7e69w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/pumpify": "^1.4.1", + "events-intercept": "^2.0.0", + "pumpify": "^1.3.5", + "split-array-stream": "^1.0.0", + "through2": "^2.0.3" + } + }, + "node_modules/checkpoint-stream/node_modules/async": { + "version": "2.6.4", + "resolved": "https://registry.npmjs.org/async/-/async-2.6.4.tgz", + "integrity": "sha512-mzo5dfJYwAn29PeiJ0zvwTo04zj8HDJj0Mn8TD7sno7q12prdbnasKJHhkm2c1LgrhlJ0teaea8860oxi51mGA==", + "dev": true, + "license": "MIT", + "dependencies": { + "lodash": "^4.17.14" + } + }, + "node_modules/checkpoint-stream/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/checkpoint-stream/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/checkpoint-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/checkpoint-stream/node_modules/split-array-stream": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/split-array-stream/-/split-array-stream-1.0.3.tgz", + "integrity": "sha512-yGY35QmZFzZkWZ0eHE06RPBi63umym8m+pdtuC/dlO1ADhdKSfCj0uNn87BYCXBBDFxyTq4oTw0BgLYT0K5z/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "async": "^2.4.0", + "is-stream-ended": "^0.1.0" + } + }, + "node_modules/checkpoint-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/checkpoint-stream/node_modules/through2": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/through2/-/through2-2.0.5.tgz", + "integrity": "sha512-/mrRod8xqpA+IHSLyGCQ2s8SPHiCDEeQJSep1jqLYeEUClOFG2Qsh+4FU6G9VeqpZnGW/Su8LQGc4YKni5rYSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "readable-stream": "~2.3.6", + "xtend": "~4.0.1" + } + }, "node_modules/chownr": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", @@ -11430,6 +11829,13 @@ "node": ">=0.8.x" } }, + "node_modules/events-intercept": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/events-intercept/-/events-intercept-2.0.0.tgz", + "integrity": "sha512-blk1va0zol9QOrdZt0rFXo5KMkNPVSp92Eju/Qz8THwKWKRKeE0T8Br/1aW6+Edkyq9xHYgYxn2QtOnUKPUp+Q==", + "dev": true, + "license": "MIT" + }, "node_modules/expand-template": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", @@ -12516,6 +12922,19 @@ "node": ">= 6" } }, + "node_modules/grpc-gcp": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/grpc-gcp/-/grpc-gcp-1.0.1.tgz", + "integrity": "sha512-06r73IoGaAIpzT+DRPnw7V5BXvZ5mjy1OcKqSPX+ZHOgbLxT+lJfz8IN83z/sbA3t55ZX88MfDaaCjDGdveVIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@grpc/grpc-js": "^1.7.0" + }, + "engines": { + "node": ">=12" + } + }, "node_modules/gtoken": { "version": "7.1.0", "resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz", @@ -14859,6 +15278,13 @@ "readable-stream": "^3.4.0" } }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -16679,6 +17105,43 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-queue": { + "version": "6.6.2", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", + "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "eventemitter3": "^4.0.4", + "p-timeout": "^3.2.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-queue/node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", + "dev": true, + "license": "MIT" + }, + "node_modules/p-timeout": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", + "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-finally": "^1.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/package-json-from-dist": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/package-json-from-dist/-/package-json-from-dist-1.0.0.tgz", @@ -17330,9 +17793,9 @@ } }, "node_modules/protobufjs": { - "version": "7.5.2", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.2.tgz", - "integrity": "sha512-f2ls6rpO6G153Cy+o2XQ+Y0sARLOZ17+OGVLHrc3VUKcLHYKEKWbkSujdBWQXM7gKn5NTfp0XnRPZn1MIu8n9w==", + "version": "7.5.3", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.3.tgz", + "integrity": "sha512-sildjKwVqOI2kmFDiXQ6aEB0fjYTafpEvIBs8tOR8qI4spuL9OPROLVu2qZqi/xgCfsHIwVqlaF8JBjWFHnKbw==", "hasInstallScript": true, "license": "BSD-3-Clause", "dependencies": { @@ -17386,6 +17849,82 @@ "once": "^1.3.1" } }, + "node_modules/pumpify": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/pumpify/-/pumpify-1.5.1.tgz", + "integrity": "sha512-oClZI37HvuUJJxSKKrC17bZ9Cu0ZYhEAGPsPUy9KlMUmv9dKX2o77RUmq7f3XjIxbwyGwYzbzQ1L2Ks8sIradQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "duplexify": "^3.6.0", + "inherits": "^2.0.3", + "pump": "^2.0.0" + } + }, + "node_modules/pumpify/node_modules/duplexify": { + "version": "3.7.1", + "resolved": "https://registry.npmjs.org/duplexify/-/duplexify-3.7.1.tgz", + "integrity": "sha512-07z8uv2wMyS51kKhD1KsdXJg5WQ6t93RneqRxUHnskXVtlYYkLqM0gqStQZ3pj073g687jPCHrqNfCzawLYh5g==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.0.0", + "stream-shift": "^1.0.0" + } + }, + "node_modules/pumpify/node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/pumpify/node_modules/pump": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/pump/-/pump-2.0.1.tgz", + "integrity": "sha512-ruPMNRkN3MHP1cWJc9OWr+T/xDP0jhXYCLfJcBuX54hhfIBnaQmAUMfDcG4DM5UMWByBbJY69QSphm3jtDKIkA==", + "dev": true, + "license": "MIT", + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/pumpify/node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dev": true, + "license": "MIT", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/pumpify/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", + "dev": true, + "license": "MIT" + }, + "node_modules/pumpify/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dev": true, + "license": "MIT", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -18823,6 +19362,16 @@ "node": "^14.17.0 || ^16.13.0 || >=18.0.0" } }, + "node_modules/stack-trace": { + "version": "0.0.10", + "resolved": "https://registry.npmjs.org/stack-trace/-/stack-trace-0.0.10.tgz", + "integrity": "sha512-KGzahc7puUKkzyMt+IqAep+TVNbKP+k2Lmwhub39m1AsTSkaDutx56aDCo+HLDzf/D26BIHTJWNiTG1KAJiQCg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, "node_modules/stackback": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", @@ -21465,13 +22014,15 @@ "version": "11.2.1", "license": "MIT", "dependencies": { + "@grpc/grpc-js": "^1.13.4", "testcontainers": "^11.2.1" }, "devDependencies": { - "@google-cloud/bigquery": "^8.0.0", + "@google-cloud/bigquery": "^8.1.0", "@google-cloud/datastore": "^9.2.1", "@google-cloud/firestore": "7.11.2", "@google-cloud/pubsub": "^5.1.0", + "@google-cloud/spanner": "^8.0.0", "@google-cloud/storage": "^7.16.0", "firebase-admin": "13.4.0", "msw": "^2.7.3" diff --git a/packages/modules/gcloud/Dockerfile b/packages/modules/gcloud/Dockerfile index 3e2391622..78b5f9388 100644 --- a/packages/modules/gcloud/Dockerfile +++ b/packages/modules/gcloud/Dockerfile @@ -1,3 +1,4 @@ -FROM gcr.io/google.com/cloudsdktool/cloud-sdk:529.0.0-emulators +FROM gcr.io/google.com/cloudsdktool/google-cloud-cli:529.0.0-emulators FROM fsouza/fake-gcs-server:1.52.2 -FROM ghcr.io/goccy/bigquery-emulator:0.6.6 \ No newline at end of file +FROM ghcr.io/goccy/bigquery-emulator:0.6.6 +FROM gcr.io/cloud-spanner-emulator/emulator:1.5.37 \ No newline at end of file diff --git a/packages/modules/gcloud/package.json b/packages/modules/gcloud/package.json index d6f44a151..0efbb1bd8 100644 --- a/packages/modules/gcloud/package.json +++ b/packages/modules/gcloud/package.json @@ -9,6 +9,7 @@ "datastore", "pubsub", "cloudstorage", + "spanner", "gcs", "testing", "docker", @@ -36,13 +37,15 @@ "build": "tsc --project tsconfig.build.json" }, "dependencies": { + "@grpc/grpc-js": "^1.13.4", "testcontainers": "^11.2.1" }, "devDependencies": { - "@google-cloud/bigquery": "^8.0.0", + "@google-cloud/bigquery": "^8.1.0", "@google-cloud/datastore": "^9.2.1", "@google-cloud/firestore": "7.11.2", "@google-cloud/pubsub": "^5.1.0", + "@google-cloud/spanner": "^8.0.0", "@google-cloud/storage": "^7.16.0", "firebase-admin": "13.4.0", "msw": "^2.7.3" diff --git a/packages/modules/gcloud/src/bigquery-emulator-container.ts b/packages/modules/gcloud/src/bigquery-emulator-container.ts index 07f451015..2449ae2c7 100644 --- a/packages/modules/gcloud/src/bigquery-emulator-container.ts +++ b/packages/modules/gcloud/src/bigquery-emulator-container.ts @@ -8,6 +8,12 @@ export class BigQueryEmulatorContainer extends GenericContainer { constructor(image: string) { super(image); this.withExposedPorts(EMULATOR_PORT).withWaitStrategy(Wait.forListeningPorts()).withStartupTimeout(120_000); + + // The BigQuery emulator image is not multi platform + // so this fix is needed for ARM architectures + if (process.arch === "arm64") { + this.withPlatform("linux/amd64"); + } } public withProjectId(projectId: string): BigQueryEmulatorContainer { diff --git a/packages/modules/gcloud/src/index.ts b/packages/modules/gcloud/src/index.ts index 92f30cd49..ce218aad4 100644 --- a/packages/modules/gcloud/src/index.ts +++ b/packages/modules/gcloud/src/index.ts @@ -3,3 +3,5 @@ export { CloudStorageEmulatorContainer, StartedCloudStorageEmulatorContainer } f export { DatastoreEmulatorContainer, StartedDatastoreEmulatorContainer } from "./datastore-emulator-container"; export { FirestoreEmulatorContainer, StartedFirestoreEmulatorContainer } from "./firestore-emulator-container"; export { PubSubEmulatorContainer, StartedPubSubEmulatorContainer } from "./pubsub-emulator-container"; +export { SpannerEmulatorContainer, StartedSpannerEmulatorContainer } from "./spanner-emulator-container"; +export { SpannerEmulatorHelper } from "./spanner-emulator-helper"; diff --git a/packages/modules/gcloud/src/spanner-emulator-container.test.ts b/packages/modules/gcloud/src/spanner-emulator-container.test.ts new file mode 100644 index 000000000..09ce87c3d --- /dev/null +++ b/packages/modules/gcloud/src/spanner-emulator-container.test.ts @@ -0,0 +1,61 @@ +import { Spanner } from "@google-cloud/spanner"; +import { getImage } from "../../../testcontainers/src/utils/test-helper"; +import { SpannerEmulatorContainer } from "./spanner-emulator-container"; + +// select the fourth FROM in the Dockerfile (spanner emulator) +const IMAGE = getImage(__dirname, 3); + +describe("SpannerEmulatorContainer", { timeout: 240_000 }, () => { + // startupWithExplicitClient { + it("should start, expose endpoints and accept real client connections using explicitly configured client", async () => { + const container = await new SpannerEmulatorContainer(IMAGE).withProjectId("test-project").start(); + + const client = new Spanner({ + projectId: container.getProjectId(), + apiEndpoint: container.getHost(), + port: container.getGrpcPort(), + sslCreds: container.getSslCredentials(), + }); + + // list instance configs + const admin = client.getInstanceAdminClient(); + const [configs] = await admin.listInstanceConfigs({ + parent: admin.projectPath(container.getProjectId()), + }); + + // emulator always includes "emulator-config" + const expectedConfigName = admin.instanceConfigPath(container.getProjectId(), "emulator-config"); + expect(configs.map((c) => c.name)).toContain(expectedConfigName); + + await container.stop(); + }); + // } + + describe.sequential("Shared state", () => { + afterEach(() => { + process.env.SPANNER_EMULATOR_HOST = ""; + }); + + // startupWithEnvironmentVariable { + it("should start, expose endpoints and accept real client connections using projectId and SPANNER_EMULATOR_HOST", async () => { + const container = await new SpannerEmulatorContainer(IMAGE).withProjectId("test-project").start(); + + // configure the client to talk to our emulator + process.env.SPANNER_EMULATOR_HOST = container.getEmulatorGrpcEndpoint(); + const client = new Spanner({ projectId: container.getProjectId() }); + + // list instance configs + const admin = client.getInstanceAdminClient(); + const [configs] = await admin.listInstanceConfigs({ + parent: admin.projectPath(container.getProjectId()), + }); + + // emulator always includes "emulator-config" + const expectedConfigName = admin.instanceConfigPath(container.getProjectId(), "emulator-config"); + expect(configs.map((c) => c.name)).toContain(expectedConfigName); + + await container.stop(); + }); + // } + }); +}); diff --git a/packages/modules/gcloud/src/spanner-emulator-container.ts b/packages/modules/gcloud/src/spanner-emulator-container.ts new file mode 100644 index 000000000..9e50fa372 --- /dev/null +++ b/packages/modules/gcloud/src/spanner-emulator-container.ts @@ -0,0 +1,73 @@ +import { credentials } from "@grpc/grpc-js"; +import { AbstractStartedContainer, GenericContainer, StartedTestContainer, Wait } from "testcontainers"; + +const GRPC_PORT = 9010; + +/** + * SpannerEmulatorContainer runs the Cloud Spanner emulator via the GCloud CLI image. + */ +export class SpannerEmulatorContainer extends GenericContainer { + private projectId?: string; + + constructor(image: string) { + super(image); + + // only gRPC port is supported + this.withExposedPorts(GRPC_PORT).withWaitStrategy(Wait.forLogMessage(/.*Cloud Spanner emulator running\..*/, 1)); + } + + /** + * Sets the GCP project ID to use with the emulator. + */ + public withProjectId(projectId: string): this { + this.projectId = projectId; + return this; + } + + public override async start(): Promise { + const selectedProject = this.projectId ?? "test-project"; + + const started = await super.start(); + return new StartedSpannerEmulatorContainer(started, selectedProject); + } +} + +/** + * A running Spanner emulator instance with endpoint getters and helper access. + */ +export class StartedSpannerEmulatorContainer extends AbstractStartedContainer { + constructor( + startedTestContainer: StartedTestContainer, + private readonly projectId: string + ) { + super(startedTestContainer); + } + + /** + * @returns mapped port for gRPC. + */ + public getGrpcPort(): number { + return this.getMappedPort(GRPC_PORT); + } + + /** + * @returns host:port for gRPC. + */ + public getEmulatorGrpcEndpoint(): string { + return `${this.getHost()}:${this.getGrpcPort()}`; + } + + /** + * @returns the GCP project ID used by the emulator. + */ + public getProjectId(): string { + return this.projectId; + } + + /** + * @returns insecure credentials for emulator. + */ + public getSslCredentials() { + return credentials.createInsecure(); + } +} diff --git a/packages/modules/gcloud/src/spanner-emulator-helper.test.ts b/packages/modules/gcloud/src/spanner-emulator-helper.test.ts new file mode 100644 index 000000000..ecd47c866 --- /dev/null +++ b/packages/modules/gcloud/src/spanner-emulator-helper.test.ts @@ -0,0 +1,44 @@ +import { getImage } from "../../../testcontainers/src/utils/test-helper"; +import { SpannerEmulatorContainer } from "./spanner-emulator-container"; +import { SpannerEmulatorHelper } from "./spanner-emulator-helper"; + +// select the fourth FROM in the Dockerfile (spanner emulator) +const IMAGE = getImage(__dirname, 3); + +describe("SpannerEmulatorHelper", { timeout: 240_000 }, () => { + // createAndDelete { + it("should create and delete instance and database via helper", async () => { + const container = await new SpannerEmulatorContainer(IMAGE).start(); + const helper = new SpannerEmulatorHelper(container); + const instanceId = "test-instance"; + const databaseId = "test-db"; + + // create resources + await helper.createInstance(instanceId); + await helper.createDatabase(instanceId, databaseId); + + const client = helper.client; + + // verify instance exists + const [instanceExists] = await client.instance(instanceId).exists(); + expect(instanceExists).toBe(true); + + // verify database exists + const [dbExists] = await client.instance(instanceId).database(databaseId).exists(); + expect(dbExists).toBe(true); + + // delete resources + await helper.deleteDatabase(instanceId, databaseId); + await helper.deleteInstance(instanceId); + + // verify deletions + const [dbExistsAfter] = await client.instance(instanceId).database(databaseId).exists(); + expect(dbExistsAfter).toBe(false); + + const [instanceExistsAfter] = await client.instance(instanceId).exists(); + expect(instanceExistsAfter).toBe(false); + + await container.stop(); + }); + // } +}); diff --git a/packages/modules/gcloud/src/spanner-emulator-helper.ts b/packages/modules/gcloud/src/spanner-emulator-helper.ts new file mode 100644 index 000000000..2ff0bed29 --- /dev/null +++ b/packages/modules/gcloud/src/spanner-emulator-helper.ts @@ -0,0 +1,95 @@ +import { Spanner } from "@google-cloud/spanner"; +import type { IInstance } from "@google-cloud/spanner/build/src/instance"; +import type { StartedSpannerEmulatorContainer } from "./spanner-emulator-container"; + +/** + * Helper class that encapsulates all Spanner client interactions against the emulator. + * Clients and configs are lazily instantiated. + */ +export class SpannerEmulatorHelper { + private clientInstance?: Spanner; + private instanceAdminClientInstance?: ReturnType; + private databaseAdminClientInstance?: ReturnType; + private instanceConfigValue?: string; + + constructor(private readonly emulator: StartedSpannerEmulatorContainer) {} + + /** + * Lazily get or create the Spanner client. + */ + public get client(): Spanner { + this.clientInstance ??= new Spanner({ + projectId: this.emulator.getProjectId(), + apiEndpoint: this.emulator.getHost(), + port: this.emulator.getGrpcPort(), + sslCreds: this.emulator.getSslCredentials(), + }); + return this.clientInstance; + } + + /** + * Lazily get or create the InstanceAdminClient. + */ + private get instanceAdminClient(): ReturnType { + this.instanceAdminClientInstance ??= this.client.getInstanceAdminClient(); + return this.instanceAdminClientInstance; + } + + /** + * Lazily get or create the DatabaseAdminClient. + */ + private get databaseAdminClient(): ReturnType { + this.databaseAdminClientInstance ??= this.client.getDatabaseAdminClient(); + return this.databaseAdminClientInstance; + } + + /** + * Lazily compute the instanceConfig path. + */ + public get instanceConfig(): string { + this.instanceConfigValue ??= this.instanceAdminClient.instanceConfigPath( + this.emulator.getProjectId(), + "emulator-config" + ); + return this.instanceConfigValue; + } + + /** + * Creates a new Spanner instance in the emulator. + */ + public async createInstance(instanceId: string, options?: IInstance): Promise { + const [operation] = await this.instanceAdminClient.createInstance({ + instanceId, + parent: this.instanceAdminClient.projectPath(this.emulator.getProjectId()), + instance: options, + }); + const [result] = await operation.promise(); + return result; + } + + /** + * Deletes an existing Spanner instance in the emulator. + */ + public async deleteInstance(instanceId: string): Promise { + await this.client.instance(instanceId).delete(); + } + + /** + * Creates a new database under the specified instance in the emulator. + */ + public async createDatabase(instanceId: string, databaseId: string): Promise { + const [operation] = await this.databaseAdminClient.createDatabase({ + parent: this.databaseAdminClient.instancePath(this.emulator.getProjectId(), instanceId), + createStatement: `CREATE DATABASE \`${databaseId}\``, + }); + const [result] = await operation.promise(); + return result; + } + + /** + * Deletes a database under the specified instance in the emulator. + */ + public async deleteDatabase(instanceId: string, databaseId: string): Promise { + await this.client.instance(instanceId).database(databaseId).delete(); + } +}