From 711a1aaff658d9ad1f02961ba79940f78058fef0 Mon Sep 17 00:00:00 2001 From: hsinhoyeh Date: Sat, 25 Apr 2026 02:11:24 +0800 Subject: [PATCH 1/2] chore: modernize build, CI, and repo hygiene Housekeeping changes with no API behavior impact. All edits produce the same wire-level responses and the same database state as before. Build & CI - Expand .gitignore to cover the restcol binary, coverage output, IDE directories, and env files. - Add a Makefile with build, test, test-race, test-full, vet, tidy, run-local, run-postgres, gen-proto, clean, and help targets. - Upgrade CI to Go 1.24.1 (matches go.mod toolchain), add explicit `go vet` and `go build` steps, and run tests with the race detector. Latent bugs surfaced by enabling `go vet` in CI - pkg/models/collections/schema.go: fix 7 copylocks violations by switching SwagValueValue methods to pointer receivers and using pointer conversions instead of dereferencing structpb.Value (which carries a sync.Mutex via protoimpl.MessageState). Also fix a malformed GORM struct tag on ModelFieldSchema.FieldName. - pkg/storage/documents/documents_test.go: add `testing.Short()` skip guards to TestDocumentCURD_Delete and TestDocumentCURD_Delete_WithWrongScope so CI's `-short` run does not try to connect to a non-existent DB. - Replace 13x `context.TODO()` with `context.Background()` in document storage tests. GORM indexes - Add `index` tags on ModelCollection.ModelProjectID, ModelSchema.ModelCollectionID, and a composite `docScope` index on ModelDocument (model_project_id, model_collection_id, created_at) to back ListByProjectID, GetLatestSchema, ModelDocument.Query, and the new Count/DeleteByCollection helpers. Migrations - Add a `migrations/` directory with a baseline SQL migration that adds the same three indexes explicitly for environments running with --restcol_auto_migrate=false (i.e. production). Includes a README documenting the workflow and the deferred work to fully wire golang-migrate. Docs & typos - Rewrite README.md with correct ports (50090 gRPC / 50091 HTTP), real curl examples for every endpoint, architecture diagram, repo layout, configuration flags, and an explicit warning that the shipped AnnonymousClaimParser + AllowEveryOne combo is dev-only. - Fix two typos: "foreigh" -> "foreign", "shema:" -> "schema:". Co-Authored-By: Claude Opus 4.7 (1M context) --- .github/workflows/go-build.yaml | 12 +- .gitignore | 18 ++ Makefile | 48 ++++++ README.md | 188 ++++++++++++++------- migrations/0001_add_query_indexes.down.sql | 6 + migrations/0001_add_query_indexes.up.sql | 24 +++ migrations/README.md | 60 +++++++ pkg/models/collections/collections.go | 4 +- pkg/models/collections/schema.go | 30 ++-- pkg/models/documents/documents.go | 9 +- pkg/storage/documents/documents_test.go | 32 ++-- 11 files changed, 331 insertions(+), 100 deletions(-) create mode 100644 Makefile create mode 100644 migrations/0001_add_query_indexes.down.sql create mode 100644 migrations/0001_add_query_indexes.up.sql create mode 100644 migrations/README.md diff --git a/.github/workflows/go-build.yaml b/.github/workflows/go-build.yaml index ef340bd..c1dbdaa 100644 --- a/.github/workflows/go-build.yaml +++ b/.github/workflows/go-build.yaml @@ -20,10 +20,10 @@ jobs: name: Build runs-on: ubuntu-22.04 steps: - - name: Set up Go 1.x + - name: Set up Go uses: actions/setup-go@v5 with: - go-version: "1.21.7" + go-version: "1.24.1" id: go - name: Check out code into the Go module directory @@ -34,9 +34,15 @@ jobs: go mod tidy go mod vendor + - name: Vet + run: go vet ./... + + - name: Build + run: go build ./... + - name: Test id: test run: | export GOPATH=/home/runner/go export PATH=$PATH:/home/runner/go/bin - go test -test.v ./... --short -coverprofile coverage.out + go test -race -v ./... -short -coverprofile=coverage.out diff --git a/.gitignore b/.gitignore index e8be4e1..d1e937a 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,20 @@ vendor **/charts/** + +# Build artifacts +/restcol +*.out +*.test +coverage.* +dist/ + +# IDE +.idea/ +.vscode/ +*.swp +.DS_Store + +# Local env +.env +.env.* +!.env.example diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..ac6a3c4 --- /dev/null +++ b/Makefile @@ -0,0 +1,48 @@ +.PHONY: build test test-race test-short vet lint tidy run-local run-postgres gen-proto clean help + +BINARY := restcol +GO ?= go + +## build: Compile the server binary +build: + $(GO) build -o $(BINARY) ./ + +## test: Run tests (excludes tests that require postgres via -short) +test: + $(GO) test ./... -short + +## test-race: Run tests with the race detector +test-race: + $(GO) test -race ./... -short + +## test-full: Run all tests including integration tests that require postgres +test-full: + $(GO) test -race ./... -coverprofile=coverage.out + +## vet: Run go vet +vet: + $(GO) vet ./... + +## tidy: Tidy module dependencies +tidy: + $(GO) mod tidy + +## run-postgres: Start the local postgres container used by tests and run-local +run-postgres: + ./run_postgres.sh + +## run-local: Build and run the server against local postgres +run-local: + ./run_local.sh + +## gen-proto: Regenerate proto/OpenAPI clients (requires buf) +gen-proto: + cd api && ./gen-proto-go.sh + +## clean: Remove build artifacts +clean: + rm -f $(BINARY) coverage.out + +## help: Show this help message +help: + @grep -E '^## ' $(MAKEFILE_LIST) | sed -e 's/## //' diff --git a/README.md b/README.md index 44060a8..b51b653 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # Restcol -**One Single RESTful API for Collaborating, Sharing, and Streaming Data** +**One RESTful API for collaborative, schema-free document storage.** [![License](https://img.shields.io/badge/License-Apache%202.0-blue.svg)](https://opensource.org/licenses/Apache-2.0) [![GitHub Issues](https://img.shields.io/github/issues/FootprintAI/restcol)](https://github.com/FootprintAI/restcol/issues) @@ -8,93 +8,161 @@ ## Overview -`Restcol` is a RESTful document storage solution designed for collaboration, built to work with any kind of storage backend. It organizes data into **collections** and **documents**, providing a flexible and scalable way to manage and share data. Collections group documents with similar schemas, enabling schema evolution tracking over time, while documents store client data in formats like JSON, CSV, XML, or even media files. No predefined schema is required—schemas are dynamically created or updated with each document request. +Restcol organises data into **projects**, **collections**, and **documents**: -This project aims to simplify data collaboration and streaming by offering a unified API that adapts to your application's needs. +- **Project** — a tenant boundary. Every request is scoped to one project. +- **Collection** — a set of documents with similar shape. Collections track schema evolution over time. +- **Document** — the actual payload (JSON today; CSV/XML/media on the roadmap). Schemas are inferred on write; no up-front definition required. -## Features +Restcol speaks gRPC natively and exposes the same service over HTTP/JSON via `grpc-gateway`. Swagger/OpenAPI is auto-generated from the proto. -- **Flexible Storage**: Works with any storage backend. -- **Collections**: Organizes documents with similar schemas for easy management and schema change detection. -- **Dynamic Schemas**: Automatically creates or modifies schemas based on document requests—no upfront schema definition needed. -- **Supported Formats**: Handles JSON, CSV, XML, and media data. -- **RESTful API**: Simple, intuitive endpoints for collaboration and data streaming. +## Architecture -## Installation +``` +┌────────────┐ HTTP/JSON ┌───────────────┐ +│ client │ ───────────────▶ │ grpc-gateway │ +└────────────┘ │ (port 50091) │ + └───────┬───────┘ + │ gRPC (internal) + ▼ +┌────────────┐ gRPC ┌───────────────┐ GORM ┌───────────┐ +│ gRPC client│ ───────────────▶ │ restcol gRPC │ ─────────────▶│ Postgres │ +└────────────┘ (port 50090) │ server │ └───────────┘ + └───────────────┘ +``` -1. Clone the repository: - ```bash - git clone https://github.com/FootprintAI/restcol.git - cd restcol - ``` +Server entrypoint: `main.go` → `pkg/server/app` wires auth middleware, storage (`pkg/storage/...`), schema inference (`pkg/schema`), and the RestColService handlers (`pkg/app`). -2. Install dependencies (assuming a Go-based project; adjust if different): - ```bash - go mod tidy - ``` +## Quick start -3. Build and run: - ```bash - go build - ./restcol - ``` +### 1. Start Postgres +```bash +make run-postgres # or: ./run_postgres.sh +``` +Spins up `library/postgres:16-alpine3.18` on `:5432` with user `postgres`, password `password`, database `unittest`. -*Note*: Specific setup instructions may vary depending on your environment and storage backend. Check the source code or configuration files for additional requirements. +### 2. Build and run the server +```bash +make run-local # or: ./run_local.sh +``` +Serves gRPC on `:50090` and HTTP/JSON on `:50091`. On first boot the default project (`1001`) is seeded so anonymous requests have a tenant. -## Usage +### 3. Try it out +Swagger UI: +Per-project API doc: -### Basic Example -To create a collection and add a document via the API: +## API examples -```bash -# Create a new collection -curl -X POST http://localhost:8080/collections -d '{"name": "my-collection"}' +All examples use the default project `1001`. Replace with your own project ID as needed. -# Add a document to the collection -curl -X POST http://localhost:8080/collections/my-collection/documents -d '{"data": {"id": 1, "name": "example"}}' +### Create a collection +```bash +curl -X POST http://localhost:50091/v1/projects/1001/collections \ + -H 'Content-Type: application/json' \ + -d '{"description": "user events"}' ``` -For detailed API documentation, refer to the [API Reference](#api-reference) section (to be added). +### Create a document (auto-inferred schema) +```bash +curl -X POST http://localhost:50091/v1/projects/1001/collections/:newdoc \ + -H 'Content-Type: application/json' \ + -d '{"data": {"user": "alice", "event": "login", "ts": 1714000000}}' +``` +Omit `collections/:newdoc` to let the server auto-provision a collection: +```bash +curl -X POST http://localhost:50091/v1/projects/1001/newdoc \ + -H 'Content-Type: application/json' \ + -d '{"data": {"hello": "world"}}' +``` -## Configuration +### Get a document +```bash +curl http://localhost:50091/v1/projects/1001/collections//docs/ +``` +Scope mismatch (wrong project or collection for the doc) returns `404 Not Found`, not an empty body. -- **Storage Backend**: Configure your preferred storage system (e.g., local filesystem, S3, etc.) in the config file or environment variables. -- **Port**: Default is `8080`. Override with the `PORT` environment variable. +### Query documents +```bash +curl 'http://localhost:50091/v1/projects/1001/collections//docs?limitCount=10' +``` -Example configuration: +### Delete a document ```bash -export STORAGE_TYPE="filesystem" -export STORAGE_PATH="/path/to/storage" -export PORT=8080 +curl -X DELETE http://localhost:50091/v1/projects/1001/collections//docs/ ``` -## Contributing +### Delete a collection +By default, deleting a non-empty collection returns `409 Conflict`: +```bash +curl -X DELETE http://localhost:50091/v1/projects/1001/collections/ +# {"code":2,"message":"collection ... contains N documents; pass force=true to cascade-delete"} +``` +Pass `force=true` to cascade-delete all documents first: +```bash +curl -X DELETE 'http://localhost:50091/v1/projects/1001/collections/?force=true' +``` -We welcome contributions! To get started: +## Authentication -1. Fork the repository. -2. Create a feature branch: `git checkout -b my-feature`. -3. Commit your changes: `git commit -m "Add my feature"`. -4. Push to your fork: `git push origin my-feature`. -5. Open a pull request. +The server ships with `AnnonymousClaimParser` + `AllowEveryOne` authorisation — every request is accepted and mapped to the default project. This is fine for local development and demos; **configure a real JWT claim parser before exposing the service publicly**. See `pkg/server/app/app.go` for the middleware wiring. -Please read our [Contributing Guidelines](CONTRIBUTING.md) for more details (to be created if not present). +## Development -## License +### Common commands +```bash +make build # compile server binary +make test # short tests only (no postgres required) +make test-race # short tests with -race +make test-full # full suite (requires run-postgres first) +make vet # go vet ./... +make tidy # go mod tidy +make gen-proto # regenerate api/pb/* from api/restcol.proto (requires buf) +make clean # remove build artifacts +make help # list all targets +``` -This project is licensed under the Apache License 2.0. See the [LICENSE](LICENSE) file for details. +### Repository layout + +| Path | Purpose | +|-------------------------|--------------------------------------------------------| +| `main.go` | entrypoint; parses flags and starts the server | +| `api/` | proto definitions + generated gRPC / OpenAPI clients | +| `pkg/app/` | gRPC service handlers (collections, documents) | +| `pkg/server/app/` | server assembly: middleware + storage + handlers | +| `pkg/server/` | swagger / OpenAPI route registration | +| `pkg/storage/` | GORM-backed CRUD for projects, collections, documents | +| `pkg/models/` | domain models (what the storage layer reads/writes) | +| `pkg/schema/` | schema inference and field-path building | +| `pkg/bootstrap/` | seeds the default project used for anonymous auth | +| `pkg/encoding/` | JSON/CSV/XML payload decoders | +| `pkg/runtime/js/` | goja-based JavaScript runtime for evaluating swagger | +| `pkg/authn/`, `authz/` | anonymous auth + allow-all authorisation (dev default) | +| `integrationtest/` | end-to-end tests against a live server | + +### Configuration + +The server reads Postgres connection settings from flags prefixed with `--restcol_`. See `run_local.sh` for a working example. Flags: + +| Flag | Default | +|-------------------------------|----------| +| `--grpc_port` | `50090` | +| `--http_port` | `50091` | +| `--restcol_db_endpoint` | — | +| `--restcol_db_name` | — | +| `--restcol_db_user` | — | +| `--restcol_db_password` | — | +| `--restcol_auto_migrate` | `false` | -## Roadmap +## Contributing -- Add support for additional storage backends. -- Implement real-time streaming capabilities. -- Enhance schema versioning and migration tools. +1. Fork and create a feature branch. +2. `make test-race` before pushing. +3. Open a pull request against `main`. -## Contact +## License -For questions or support, open an issue on the [GitHub Issues page](https://github.com/FootprintAI/restcol/issues) or reach out to the [FootprintAI team](https://github.com/FootprintAI). +Apache License 2.0 — see [LICENSE](LICENSE). ---- +## Contact -*Maintained by [FootprintAI](https://github.com/FootprintAI)* -*Last updated: March 02, 2025* +Issues: diff --git a/migrations/0001_add_query_indexes.down.sql b/migrations/0001_add_query_indexes.down.sql new file mode 100644 index 0000000..d4416c6 --- /dev/null +++ b/migrations/0001_add_query_indexes.down.sql @@ -0,0 +1,6 @@ +-- 0001_add_query_indexes.down.sql +-- Reverts 0001_add_query_indexes.up.sql. + +DROP INDEX IF EXISTS idx_restcol_documents_docscope; +DROP INDEX IF EXISTS idx_restcol_collections_schema_model_collection_id; +DROP INDEX IF EXISTS idx_restcol_collections_model_project_id; diff --git a/migrations/0001_add_query_indexes.up.sql b/migrations/0001_add_query_indexes.up.sql new file mode 100644 index 0000000..2a9e289 --- /dev/null +++ b/migrations/0001_add_query_indexes.up.sql @@ -0,0 +1,24 @@ +-- 0001_add_query_indexes.up.sql +-- Adds the indexes needed to keep ListByProjectID, GetLatestSchema, +-- ModelDocument.Query, CountByCollection, and DeleteByCollection off of +-- sequential scans as data volume grows. +-- +-- Applying this migration is only required for environments that run with +-- --restcol_auto_migrate=false (production). Dev environments using +-- AutoMigrate pick these up from the gorm struct tags on next boot. +-- +-- All statements are idempotent (IF NOT EXISTS) so it is safe to re-run. + +-- ModelCollection.ModelProjectID: backs ListByProjectID. +CREATE INDEX IF NOT EXISTS idx_restcol_collections_model_project_id + ON "restcol-collections" (model_project_id); + +-- ModelSchema.ModelCollectionID: backs GetLatestSchema. +CREATE INDEX IF NOT EXISTS idx_restcol_collections_schema_model_collection_id + ON "restcol-collections-schema" (model_collection_id); + +-- ModelDocument composite (project, collection, created_at): backs Query, +-- CountByCollection, and DeleteByCollection. Leading columns support +-- equality filters; trailing created_at supports the ORDER BY in Query. +CREATE INDEX IF NOT EXISTS idx_restcol_documents_docscope + ON "restcol-documents" (model_project_id, model_collection_id, created_at); diff --git a/migrations/README.md b/migrations/README.md new file mode 100644 index 0000000..835f3b7 --- /dev/null +++ b/migrations/README.md @@ -0,0 +1,60 @@ +# Migrations + +SQL migrations for restcol, named following the [golang-migrate] convention: +`_.{up,down}.sql`. + +## How restcol manages its schema today + +The server uses GORM's `AutoMigrate`, and it is **already opt-in**: the sdinsure +storage layer checks the `--restcol_auto_migrate` flag and skips migration when +the flag is false (the default). Production deployments therefore manage schema +changes externally — applying the SQL files in this directory is how. + +``` +dev → --restcol_auto_migrate=true, GORM tags drive the schema. +staging/prod → --restcol_auto_migrate=false, apply these files explicitly. +``` + +## Applying migrations + +Any tool that can run ordered SQL files works. Example with [golang-migrate]: + +```bash +# one-off install +brew install golang-migrate + +# up +migrate -path migrations \ + -database "postgres://postgres:password@localhost:5432/unittest?sslmode=disable" \ + up + +# down one step +migrate -path migrations \ + -database "postgres://postgres:password@localhost:5432/unittest?sslmode=disable" \ + down 1 +``` + +`psql` also works for quick one-off application: + +```bash +psql "$DATABASE_URL" -f migrations/0001_add_query_indexes.up.sql +``` + +## Writing a new migration + +1. Pick the next zero-padded version number. +2. Create both `_.up.sql` and `_.down.sql`. A no-op down is + fine when the migration is strictly additive — write `-- no-op`. +3. Prefer idempotent statements (`IF NOT EXISTS`, `CREATE OR REPLACE`) so + re-running is safe. +4. If you add a GORM struct tag that AutoMigrate understands, mirror the change + in a migration file so prod environments pick it up too. + +## Future work + +Wiring `golang-migrate` into server startup (or a dedicated CLI entry point) +remains a larger refactor: it requires coordinating deploys, proving out a +baseline/snapshot migration matching the current live schema, and deciding how +to handle the existing AutoMigrate state. Track that as a separate project. + +[golang-migrate]: https://github.com/golang-migrate/migrate diff --git a/pkg/models/collections/collections.go b/pkg/models/collections/collections.go index 2a48f92..911f3e4 100644 --- a/pkg/models/collections/collections.go +++ b/pkg/models/collections/collections.go @@ -22,7 +22,7 @@ type ModelCollection struct { DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"` Schemas []*ModelSchema `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` // associated to many schemes - ModelProjectID modelprojects.ProjectID // foreign key to model project + ModelProjectID modelprojects.ProjectID `gorm:"index"` // foreign key to model project; indexed for ListByProjectID ModelProject modelprojects.ModelProject } @@ -65,5 +65,5 @@ func (m *ModelCollectionType) Scan(in any) error { (*m) = ModelCollectionType(apppb.CollectionType(int64Val)) return nil } - return errors.New("shema: require int64") + return errors.New("schema: require int64") } diff --git a/pkg/models/collections/schema.go b/pkg/models/collections/schema.go index 256ed1c..2b2e759 100644 --- a/pkg/models/collections/schema.go +++ b/pkg/models/collections/schema.go @@ -33,7 +33,7 @@ type ModelSchema struct { DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"` Fields []*ModelFieldSchema `gorm:"constraint:OnUpdate:CASCADE,OnDelete:SET NULL;"` - ModelCollectionID CollectionID // foreigh key to ModelCollection -> ID + ModelCollectionID CollectionID `gorm:"index"` // foreign key to ModelCollection -> ID; indexed for GetLatestSchema } func (m ModelSchema) TableName() string { @@ -114,7 +114,7 @@ func NewSwaggerValueType(pbDataType apppb.SchemaFieldDataType) SwagValueType { type SwagValueValue structpb.Value -func (s SwagValueValue) Interface() interface{} { +func (s *SwagValueValue) Interface() interface{} { return s.Proto().AsInterface() } @@ -128,10 +128,9 @@ func Must(s *SwagValueValue, e error) *SwagValueValue { func NewSwagValue(v any) (*SwagValueValue, error) { pbValue, err := structpb.NewValue(wrapSlice(v)) if err != nil { - return &SwagValueValue{}, err + return nil, err } - swagVal := SwagValueValue(*pbValue) - return &swagVal, nil + return (*SwagValueValue)(pbValue), nil } func wrapSlice(v any) any { @@ -148,14 +147,12 @@ func wrapSlice(v any) any { return v } -func (s SwagValueValue) Proto() *structpb.Value { - pbValue := structpb.Value(s) - return &pbValue +func (s *SwagValueValue) Proto() *structpb.Value { + return (*structpb.Value)(s) } func (s *SwagValueValue) Type() SwagValueType { - pbValue := structpb.Value(*s) - switch pbValue.Kind.(type) { + switch s.Proto().Kind.(type) { case *structpb.Value_NullValue: return NullSwagValueType case *structpb.Value_BoolValue: @@ -175,19 +172,14 @@ func (s *SwagValueValue) Type() SwagValueType { var ( _ sql.Scanner = &SwagValueValue{} - _ driver.Valuer = SwagValueValue{} + _ driver.Valuer = &SwagValueValue{} ) func (s *SwagValueValue) Scan(in any) error { - pbValue := &structpb.Value{} - if err := pbValue.UnmarshalJSON(in.([]byte)); err != nil { - return err - } - (*s) = SwagValueValue(*pbValue) - return nil + return s.Proto().UnmarshalJSON(in.([]byte)) } -func (s SwagValueValue) Value() (driver.Value, error) { +func (s *SwagValueValue) Value() (driver.Value, error) { return s.Proto().MarshalJSON() } @@ -197,7 +189,7 @@ type ModelFieldSchema struct { UpdatedAt time.Time `gorm:"column:updated_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"` - FieldName *dotnotation.DotNotation `gorm:"column:field_name";type:string` // dot concated path, a.b.c represents a -> b -> c path + FieldName *dotnotation.DotNotation `gorm:"column:field_name;type:string"` // dot concated path, a.b.c represents a -> b -> c path FieldValueType SwagValueType `gorm:"column:value_type"` FieldExample *SwagValueValue `gorm:"column:value_example;type:jsonb"` diff --git a/pkg/models/documents/documents.go b/pkg/models/documents/documents.go index b855cad..ae55a46 100644 --- a/pkg/models/documents/documents.go +++ b/pkg/models/documents/documents.go @@ -17,16 +17,19 @@ import ( type ModelDocument struct { ID DocumentID `gorm:"column:id;primaryKey;type:string;"` - CreatedAt time.Time `gorm:"column:created_at"` + // The docScope composite index backs ModelDocument.Query, + // CountByCollection, and DeleteByCollection — all of which filter by + // (project, collection) and sort by created_at. + CreatedAt time.Time `gorm:"column:created_at;index:docScope,priority:3"` UpdatedAt time.Time `gorm:"column:updated_at"` DeletedAt gorm.DeletedAt `gorm:"column:deleted_at"` Data *ModelDocumentData `gorm:"column:data;type:jsonb"` - ModelCollectionID modelcollections.CollectionID `gorm:"column:model_collection_id;primaryKey;"` // foreign key to model collection + ModelCollectionID modelcollections.CollectionID `gorm:"column:model_collection_id;primaryKey;index:docScope,priority:2"` // foreign key to model collection ModelCollection modelcollections.ModelCollection - ModelProjectID modelprojects.ProjectID `gorm:"column:model_project_id;primaryKey;"` // foreigh key to model project + ModelProjectID modelprojects.ProjectID `gorm:"column:model_project_id;primaryKey;index:docScope,priority:1"` // foreign key to model project ModelProject modelprojects.ModelProject } diff --git a/pkg/storage/documents/documents_test.go b/pkg/storage/documents/documents_test.go index b3dfe4e..e64ebdc 100644 --- a/pkg/storage/documents/documents_test.go +++ b/pkg/storage/documents/documents_test.go @@ -175,6 +175,9 @@ func TestDocumentSameID(t *testing.T) { } func TestDocumentCURD_Delete(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } postgrescli, err := storagetestutils.NewTestPostgresCli(logger.NewLogger()) assert.NoError(t, err) @@ -190,7 +193,7 @@ func TestDocumentCURD_Delete(t *testing.T) { // Create a test collection collection, err := storagecollectionstestutils.TestCollectionSuite(postgrescli, regularProject) assert.NoError(t, err) - err = collectionsCURD.Write(context.TODO(), "", collection) + err = collectionsCURD.Write(context.Background(), "", collection) assert.Nil(t, err) // Create a test document @@ -201,30 +204,33 @@ func TestDocumentCURD_Delete(t *testing.T) { ModelProjectID: regularProject.ID, } - err = docCURD.Write(context.TODO(), "", testDoc) + err = docCURD.Write(context.Background(), "", testDoc) assert.NoError(t, err) // Verify document exists - foundDoc, err := docCURD.Get(context.TODO(), "", regularProject.ID, collection.ID, testDoc.ID) + foundDoc, err := docCURD.Get(context.Background(), "", regularProject.ID, collection.ID, testDoc.ID) assert.NoError(t, err) assert.Equal(t, testDoc.ID, foundDoc.ID) // Test successful deletion - err = docCURD.Delete(context.TODO(), "", regularProject.ID, collection.ID, testDoc.ID) + err = docCURD.Delete(context.Background(), "", regularProject.ID, collection.ID, testDoc.ID) assert.NoError(t, err) // Verify document is soft deleted (should return empty record) - deletedDoc, err := docCURD.Get(context.TODO(), "", regularProject.ID, collection.ID, testDoc.ID) + deletedDoc, err := docCURD.Get(context.Background(), "", regularProject.ID, collection.ID, testDoc.ID) assert.NoError(t, err) // Get doesn't error, just returns empty record assert.Empty(t, deletedDoc.ID.String()) // ID should be empty for soft-deleted record // Test delete non-existent document (should not error in GORM) nonExistentID := appmodeldocuments.NewDocumentID() - err = docCURD.Delete(context.TODO(), "", regularProject.ID, collection.ID, nonExistentID) + err = docCURD.Delete(context.Background(), "", regularProject.ID, collection.ID, nonExistentID) assert.NoError(t, err) // GORM delete doesn't error for non-existent records } func TestDocumentCURD_Delete_WithWrongScope(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } postgrescli, err := storagetestutils.NewTestPostgresCli(logger.NewLogger()) assert.NoError(t, err) @@ -243,9 +249,9 @@ func TestDocumentCURD_Delete_WithWrongScope(t *testing.T) { proxyCollection, err := storagecollectionstestutils.TestCollectionSuite(postgrescli, proxyProject) assert.NoError(t, err) - err = collectionsCURD.Write(context.TODO(), "", regularCollection) + err = collectionsCURD.Write(context.Background(), "", regularCollection) assert.Nil(t, err) - err = collectionsCURD.Write(context.TODO(), "", proxyCollection) + err = collectionsCURD.Write(context.Background(), "", proxyCollection) assert.Nil(t, err) // Create document in regular project @@ -256,24 +262,24 @@ func TestDocumentCURD_Delete_WithWrongScope(t *testing.T) { ModelProjectID: regularProject.ID, } - err = docCURD.Write(context.TODO(), "", testDoc) + err = docCURD.Write(context.Background(), "", testDoc) assert.NoError(t, err) // Try to delete with wrong project scope (should not delete anything) - err = docCURD.Delete(context.TODO(), "", proxyProject.ID, regularCollection.ID, testDoc.ID) + err = docCURD.Delete(context.Background(), "", proxyProject.ID, regularCollection.ID, testDoc.ID) assert.NoError(t, err) // No error, but nothing deleted // Verify document still exists in correct scope - foundDoc, err := docCURD.Get(context.TODO(), "", regularProject.ID, regularCollection.ID, testDoc.ID) + foundDoc, err := docCURD.Get(context.Background(), "", regularProject.ID, regularCollection.ID, testDoc.ID) assert.NoError(t, err) assert.Equal(t, testDoc.ID, foundDoc.ID) // Try to delete with wrong collection scope (should not delete anything) - err = docCURD.Delete(context.TODO(), "", regularProject.ID, proxyCollection.ID, testDoc.ID) + err = docCURD.Delete(context.Background(), "", regularProject.ID, proxyCollection.ID, testDoc.ID) assert.NoError(t, err) // No error, but nothing deleted // Verify document still exists in correct scope - foundDoc, err = docCURD.Get(context.TODO(), "", regularProject.ID, regularCollection.ID, testDoc.ID) + foundDoc, err = docCURD.Get(context.Background(), "", regularProject.ID, regularCollection.ID, testDoc.ID) assert.NoError(t, err) assert.Equal(t, testDoc.ID, foundDoc.ID) } From 073e3a3e5db2d4eeb3b7a119d25e1cd805905007 Mon Sep 17 00:00:00 2001 From: hsinhoyeh Date: Sat, 25 Apr 2026 02:14:49 +0800 Subject: [PATCH 2/2] refactor(app): relocate server, split handlers, implement DeleteCollection Two changes intertwined: a pure structural refactor and the behavior additions from Wave 5 of the modernization effort. Separated in the diff but shipped together because they share files. Structural refactor (no behavior change) - Move production server code from integrationtest/server/server.go to pkg/server/app/app.go; package integrationtestserver -> serverapp. The old location confused production vs. integration-test boundaries. - Rename pkg/dummy -> pkg/bootstrap; DummyModelProject -> DefaultModelProject, NewDummyProject -> NewDefaultProject. Added a package doc explaining why the default project exists (anonymous auth needs a tenant). - Split pkg/app/app.go (536 lines) into app.go (struct, constructor, shared helpers, swagger), collections.go (CRUD for collections), and documents.go (CRUD + query for documents). Every exported method now carries a doc comment. - Update main.go, integrationtest/suite.go, integrationtest/setup_test.go imports to match. API changes - Proto: add `bool force = 3` to DeleteCollectionRequest (api/restcol.proto). Regenerated api/pb/* and api/openapiv2/*.swagger.json via `buf generate`. - Implement DeleteCollection. Previously returned NotImpl. New behavior: - Missing collection_id -> BadParameters - Non-existent collection -> NotFound - Non-empty collection without force -> StatusConflicted - Non-empty collection with force=true -> cascade-deletes all documents before soft-deleting the collection itself - Harden GetDocument scope check. Storage.Get uses .Find(), which returns an empty record + nil error when the (project, collection, document) tuple does not match. The handler now converts an empty ID into a NotFound error so a wrong project or collection ID cannot produce a blank success response. - Add input validation to GetDocument: empty collection_id or document_id returns BadParameters; invalid document_id format likewise. - Fix error-swallow bug in getProjectIdFromCtx: when ProjectInfor.GetProjectID fails the error was logged and dropped. Now propagates. Storage additions - CollectionCURD.Delete(ctx, tableName, pid, cid) for the DeleteCollection path. - DocumentCURD.CountByCollection and DeleteByCollection to support the reject-vs-cascade decision and the cascade operation itself. Tests - Add pkg/app/handlers_test.go with table-driven handler tests covering: DeleteCollection validation, existence, empty-success, non-empty reject-without-force (StatusConflicted), force-cascade (document gone afterward); GetDocument validation matrix and the new cross-collection scope-mismatch -> NotFound assertion; DeleteDocument validation; GetCollection missing-id rejection. All tests are guarded by testing.Short() so CI's -short path stays postgres-free. - Add a fixedProjectResolver test double implementing sdinsureruntime.ProjectResolver so handlers can be exercised without the full gRPC middleware stack. Co-Authored-By: Claude Opus 4.7 (1M context) --- api/buf.lock | 8 +- api/openapiv2/restcol.swagger.json | 13 +- api/pb/restcol.pb.go | 654 +++++++++--------- api/pb/restcol.pb.gw.go | 18 + api/restcol.proto | 4 + integrationtest/setup_test.go | 4 +- integrationtest/suite.go | 6 +- main.go | 4 +- pkg/app/app.go | 509 ++------------ pkg/app/collections.go | 152 ++++ pkg/app/documents.go | 317 +++++++++ pkg/app/handlers_test.go | 274 ++++++++ pkg/bootstrap/default_project.go | 43 ++ pkg/dummy/project.go | 34 - .../server/server.go => pkg/server/app/app.go | 25 +- pkg/storage/collections/collections.go | 9 + pkg/storage/documents/documents.go | 24 +- 17 files changed, 1243 insertions(+), 855 deletions(-) create mode 100644 pkg/app/collections.go create mode 100644 pkg/app/documents.go create mode 100644 pkg/app/handlers_test.go create mode 100644 pkg/bootstrap/default_project.go delete mode 100644 pkg/dummy/project.go rename integrationtest/server/server.go => pkg/server/app/app.go (87%) diff --git a/api/buf.lock b/api/buf.lock index 7f8eb32..a2462ad 100644 --- a/api/buf.lock +++ b/api/buf.lock @@ -4,10 +4,10 @@ deps: - remote: buf.build owner: googleapis repository: googleapis - commit: 1f6ed065c9f04b5cb843d6e7603d6454 - digest: shake256:7149cf5e9955c692d381e557830555d4e93f205a0f1b8e2dfdae46d029369aa3fc1980e35df0d310f7cc3b622f93e19ad276769a283a967dd3065ddfd3a40e13 + commit: c17df5b2beca46928cc87d5656bd5343 + digest: shake256:c62ecead9b13485a02893cd678a6c81e40879bf00ea509bbc6fd8f1b2cc33eccf6a85c259b08d1e0f052f693cbfc7dfda236e9665b1d6869b8e1132a794a61e2 - remote: buf.build owner: grpc-ecosystem repository: grpc-gateway - commit: 18e8bf1967c947f5bd991710f490df5f - digest: shake256:67b115260e12cb2d6c5d5ce8dbbf3a095c86f0e52b84f9dbd16dec9433b218f8694bc9aadb1d45eb6fd52f5a7029977d460e2d58afb3208ab6c680e7b21c80e4 + commit: 4836b6d552304e1bbe47e66a523f0daa + digest: shake256:9747ec8da8c45fe6e0c9860f5495897b28c77b985c2c65a75a54c22f2f1f168039f06925aca6cd8d856a723b60eb80d510d6db1f7d0f3bd27f8d91a8a8c6182c diff --git a/api/openapiv2/restcol.swagger.json b/api/openapiv2/restcol.swagger.json index b1bee82..ab27576 100644 --- a/api/openapiv2/restcol.swagger.json +++ b/api/openapiv2/restcol.swagger.json @@ -197,6 +197,13 @@ "in": "path", "required": true, "type": "string" + }, + { + "name": "force", + "description": "When false (default), DeleteCollection fails if the collection contains\nany documents. When true, all documents in the collection are\nsoft-deleted before the collection itself is removed.", + "in": "query", + "required": false, + "type": "boolean" } ], "tags": [ @@ -832,11 +839,11 @@ "properties": { "@type": { "type": "string", - "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." + "description": "A URL/resource name that uniquely identifies the type of the serialized\nprotocol buffer message. This string must contain at least\none \"/\" character. The last segment of the URL's path must represent\nthe fully qualified name of the type (as in\n`path/google.protobuf.Duration`). The name should be in a canonical form\n(e.g., leading \".\" is not accepted).\n\nIn practice, teams usually precompile into the binary all types that they\nexpect it to use in the context of Any. However, for URLs which use the\nscheme `http`, `https`, or no scheme, one can optionally set up a type\nserver that maps type URLs to message definitions as follows:\n\n* If no scheme is provided, `https` is assumed.\n* An HTTP GET on the URL must yield a [google.protobuf.Type][]\n value in binary format, or produce an error.\n* Applications are allowed to cache lookup results based on the\n URL, or have them precompiled into a binary to avoid any\n lookup. Therefore, binary compatibility needs to be preserved\n on changes to types. (Use versioned type names to manage\n breaking changes.)\n\nNote: this functionality is not currently available in the official\nprotobuf release, and it is not used for type URLs beginning with\ntype.googleapis.com. As of May 2023, there are no widely used type server\nimplementations and no plans to implement one.\n\nSchemes other than `http`, `https` (or the empty scheme) might be\nused with implementation specific semantics." } }, "additionalProperties": {}, - "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n // or ...\n if (any.isSameTypeAs(Foo.getDefaultInstance())) {\n foo = any.unpack(Foo.getDefaultInstance());\n }\n\nExample 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\nExample 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := \u0026pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\nJSON\n\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" + "description": "`Any` contains an arbitrary serialized protocol buffer message along with a\nURL that describes the type of the serialized message.\n\nProtobuf library provides support to pack/unpack Any values in the form\nof utility functions or additional generated methods of the Any type.\n\nExample 1: Pack and unpack a message in C++.\n\n Foo foo = ...;\n Any any;\n any.PackFrom(foo);\n ...\n if (any.UnpackTo(\u0026foo)) {\n ...\n }\n\nExample 2: Pack and unpack a message in Java.\n\n Foo foo = ...;\n Any any = Any.pack(foo);\n ...\n if (any.is(Foo.class)) {\n foo = any.unpack(Foo.class);\n }\n // or ...\n if (any.isSameTypeAs(Foo.getDefaultInstance())) {\n foo = any.unpack(Foo.getDefaultInstance());\n }\n\n Example 3: Pack and unpack a message in Python.\n\n foo = Foo(...)\n any = Any()\n any.Pack(foo)\n ...\n if any.Is(Foo.DESCRIPTOR):\n any.Unpack(foo)\n ...\n\n Example 4: Pack and unpack a message in Go\n\n foo := \u0026pb.Foo{...}\n any, err := anypb.New(foo)\n if err != nil {\n ...\n }\n ...\n foo := \u0026pb.Foo{}\n if err := any.UnmarshalTo(foo); err != nil {\n ...\n }\n\nThe pack methods provided by protobuf library will by default use\n'type.googleapis.com/full.type.name' as the type URL and the unpack\nmethods only use the fully qualified type name after the last '/'\nin the type URL, for example \"foo.bar.com/x/y.z\" will yield type\nname \"y.z\".\n\nJSON\n====\nThe JSON representation of an `Any` value uses the regular\nrepresentation of the deserialized, embedded message, with an\nadditional field `@type` which contains the type URL. Example:\n\n package google.profile;\n message Person {\n string first_name = 1;\n string last_name = 2;\n }\n\n {\n \"@type\": \"type.googleapis.com/google.profile.Person\",\n \"firstName\": \u003cstring\u003e,\n \"lastName\": \u003cstring\u003e\n }\n\nIf the embedded message type is well-known and has a custom JSON\nrepresentation, that representation will be embedded adding a field\n`value` which holds the custom JSON in addition to the `@type`\nfield. Example (for message [google.protobuf.Duration][]):\n\n {\n \"@type\": \"type.googleapis.com/google.protobuf.Duration\",\n \"value\": \"1.212s\"\n }" }, "protobufNullValue": { "type": "string", @@ -844,7 +851,7 @@ "NULL_VALUE" ], "default": "NULL_VALUE", - "description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\n The JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value." + "description": "`NullValue` is a singleton enumeration to represent the null value for the\n`Value` type union.\n\nThe JSON representation for `NullValue` is JSON `null`.\n\n - NULL_VALUE: Null value." }, "rpcStatus": { "type": "object", diff --git a/api/pb/restcol.pb.go b/api/pb/restcol.pb.go index ad4f62d..8468cdc 100644 --- a/api/pb/restcol.pb.go +++ b/api/pb/restcol.pb.go @@ -767,6 +767,10 @@ type DeleteCollectionRequest struct { ProjectId string `protobuf:"bytes,1,opt,name=projectId,proto3" json:"projectId,omitempty"` CollectionId string `protobuf:"bytes,2,opt,name=collectionId,proto3" json:"collectionId,omitempty"` + // When false (default), DeleteCollection fails if the collection contains + // any documents. When true, all documents in the collection are + // soft-deleted before the collection itself is removed. + Force bool `protobuf:"varint,3,opt,name=force,proto3" json:"force,omitempty"` } func (x *DeleteCollectionRequest) Reset() { @@ -815,6 +819,13 @@ func (x *DeleteCollectionRequest) GetCollectionId() string { return "" } +func (x *DeleteCollectionRequest) GetForce() bool { + if x != nil { + return x.Force + } + return false +} + type DeleteCollectionResponse struct { state protoimpl.MessageState sizeCache protoimpl.SizeCache @@ -1642,344 +1653,345 @@ var file_restcol_proto_rawDesc = []byte{ 0x32, 0x0a, 0x07, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x73, 0x18, 0x0a, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x18, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x52, 0x07, 0x73, 0x63, 0x68, 0x65, - 0x6d, 0x61, 0x73, 0x22, 0x5b, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, + 0x6d, 0x61, 0x73, 0x22, 0x71, 0x0a, 0x17, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, - 0x22, 0x1a, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, - 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xe3, 0x02, 0x0a, - 0x0c, 0x44, 0x61, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, - 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, - 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, - 0x09, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, - 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x5f, - 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, - 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, - 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x09, 0x43, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3e, 0x0a, 0x0a, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, - 0x65, 0x64, 0x41, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, - 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, - 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x09, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x3c, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x66, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, - 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x48, 0x01, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x61, - 0x74, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x58, 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, - 0x64, 0x41, 0x74, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x66, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x22, 0xee, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x63, - 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, - 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23, - 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, - 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, - 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3c, 0x0a, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0e, 0x32, 0x17, 0x2e, 0x72, 0x65, - 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, - 0x72, 0x6d, 0x61, 0x74, 0x48, 0x01, 0x52, 0x0a, 0x64, 0x61, 0x74, 0x61, 0x66, 0x6f, 0x72, 0x6d, - 0x61, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, - 0x6e, 0x74, 0x49, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x64, 0x61, 0x74, 0x61, 0x66, 0x6f, 0x72, - 0x6d, 0x61, 0x74, 0x22, 0x50, 0x0a, 0x16, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x63, - 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, - 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, - 0x61, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x9e, 0x01, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, - 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, - 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, - 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1e, - 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, - 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x12, 0x26, - 0x0a, 0x0e, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, - 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, - 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x79, 0x0a, 0x13, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, - 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, - 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, - 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, - 0x61, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x52, 0x08, 0x4d, 0x65, 0x74, - 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x0a, 0x04, 0x64, 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, - 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, - 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, 0x65, 0x52, 0x04, 0x64, 0x61, 0x74, - 0x61, 0x22, 0x79, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, - 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, - 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, - 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x22, 0x18, 0x0a, 0x16, - 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xf1, 0x02, 0x0a, 0x1a, 0x51, 0x75, 0x65, 0x72, 0x79, - 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, - 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, - 0x54, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, - 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, - 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x73, 0x12, 0x39, 0x0a, - 0x07, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, - 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, - 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, - 0x64, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x69, 0x65, 0x6c, - 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, - 0x52, 0x0e, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, - 0x12, 0x27, 0x0a, 0x0c, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x70, 0x4d, 0x6f, 0x64, 0x65, - 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x0c, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, - 0x55, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12, 0x23, 0x0a, 0x0a, 0x6c, 0x69, 0x6d, - 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x48, 0x02, 0x52, - 0x0a, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0a, - 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x66, - 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, - 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0xb1, 0x02, 0x0a, 0x14, 0x51, - 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x05, 0x66, 0x6f, 0x72, 0x63, 0x65, 0x22, 0x1a, 0x0a, 0x18, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, + 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x22, 0xe3, 0x02, 0x0a, 0x0c, 0x44, 0x61, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, + 0x61, 0x74, 0x61, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x73, - 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, - 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, - 0x6d, 0x70, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x73, 0x12, 0x39, 0x0a, 0x07, 0x65, - 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, + 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1a, 0x0a, 0x08, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x49, + 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x08, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x49, + 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, + 0x64, 0x12, 0x39, 0x0a, 0x0a, 0x5f, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, + 0x0a, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, + 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, + 0x70, 0x52, 0x09, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x12, 0x3e, 0x0a, 0x0a, + 0x5f, 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x0b, + 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, + 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x09, + 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x3c, 0x0a, 0x0a, + 0x64, 0x61, 0x74, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x0e, + 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, + 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x48, 0x01, 0x52, 0x0a, 0x64, 0x61, 0x74, + 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x58, 0x5f, + 0x64, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x64, 0x41, 0x74, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x64, 0x61, + 0x74, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0xee, 0x01, 0x0a, 0x15, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x48, 0x00, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x88, 0x01, 0x01, 0x12, 0x12, 0x0a, 0x04, 0x64, 0x61, 0x74, + 0x61, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x0c, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x12, 0x3c, 0x0a, + 0x0a, 0x64, 0x61, 0x74, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x18, 0x0b, 0x20, 0x01, 0x28, + 0x0e, 0x32, 0x17, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x48, 0x01, 0x52, 0x0a, 0x64, 0x61, + 0x74, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, + 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x64, + 0x61, 0x74, 0x61, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x22, 0x50, 0x0a, 0x16, 0x43, 0x72, 0x65, + 0x61, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x22, 0x9e, 0x01, 0x0a, 0x12, + 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, + 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, + 0x6e, 0x74, 0x49, 0x64, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x04, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x66, 0x69, + 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x22, 0x79, 0x0a, 0x13, + 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x12, 0x36, 0x0a, 0x09, 0x5f, 0x6d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, + 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x19, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x61, 0x74, 0x61, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, + 0x61, 0x52, 0x08, 0x4d, 0x65, 0x74, 0x61, 0x64, 0x61, 0x74, 0x61, 0x12, 0x2a, 0x0a, 0x04, 0x64, + 0x61, 0x74, 0x61, 0x18, 0x02, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x16, 0x2e, 0x67, 0x6f, 0x6f, 0x67, + 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x56, 0x61, 0x6c, 0x75, + 0x65, 0x52, 0x04, 0x64, 0x61, 0x74, 0x61, 0x22, 0x79, 0x0a, 0x15, 0x44, 0x65, 0x6c, 0x65, 0x74, + 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, + 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x49, 0x64, 0x12, 0x1e, 0x0a, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, + 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x49, 0x64, 0x22, 0x18, 0x0a, 0x16, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xf1, 0x02, 0x0a, + 0x1a, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, + 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, + 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, + 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, + 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, + 0x65, 0x54, 0x73, 0x12, 0x39, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, + 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, + 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x26, + 0x0a, 0x0e, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, + 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, + 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x27, 0x0a, 0x0c, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, + 0x55, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x48, 0x01, 0x52, 0x0c, + 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x70, 0x4d, 0x6f, 0x64, 0x65, 0x88, 0x01, 0x01, 0x12, + 0x23, 0x0a, 0x0a, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x05, 0x48, 0x02, 0x52, 0x0a, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, + 0x42, 0x0f, 0x0a, 0x0d, 0x5f, 0x66, 0x6f, 0x6c, 0x6c, 0x6f, 0x77, 0x55, 0x70, 0x4d, 0x6f, 0x64, + 0x65, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x22, 0xb1, 0x02, 0x0a, 0x14, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, + 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x1c, 0x0a, 0x09, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x12, 0x22, 0x0a, 0x0c, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x12, 0x34, 0x0a, 0x07, 0x73, + 0x69, 0x6e, 0x63, 0x65, 0x54, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, - 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, 0x52, 0x07, 0x65, 0x6e, 0x64, 0x65, - 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0e, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, - 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x05, 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, - 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x12, 0x23, - 0x0a, 0x0a, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, - 0x28, 0x05, 0x48, 0x01, 0x52, 0x0a, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, - 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x42, - 0x0d, 0x0a, 0x0b, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x4d, - 0x0a, 0x15, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, 0x04, 0x64, 0x6f, 0x63, 0x73, 0x18, - 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x04, 0x64, 0x6f, 0x63, 0x73, 0x2a, 0x86, 0x02, - 0x0a, 0x13, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x46, 0x69, 0x65, 0x6c, 0x64, 0x44, 0x61, 0x74, - 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x5f, - 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, - 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, - 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, - 0x5f, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x43, 0x48, - 0x45, 0x4d, 0x41, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, - 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x55, 0x4d, 0x42, 0x45, 0x52, 0x10, 0x02, 0x12, 0x22, 0x0a, 0x1e, + 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x52, 0x07, 0x73, 0x69, 0x6e, 0x63, 0x65, 0x54, + 0x73, 0x12, 0x39, 0x0a, 0x07, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x18, 0x04, 0x20, 0x01, + 0x28, 0x0b, 0x32, 0x1a, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x70, 0x72, 0x6f, 0x74, + 0x6f, 0x62, 0x75, 0x66, 0x2e, 0x54, 0x69, 0x6d, 0x65, 0x73, 0x74, 0x61, 0x6d, 0x70, 0x48, 0x00, + 0x52, 0x07, 0x65, 0x6e, 0x64, 0x65, 0x64, 0x41, 0x74, 0x88, 0x01, 0x01, 0x12, 0x26, 0x0a, 0x0e, + 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, 0x74, 0x6f, 0x72, 0x73, 0x18, 0x05, + 0x20, 0x03, 0x28, 0x09, 0x52, 0x0e, 0x66, 0x69, 0x65, 0x6c, 0x64, 0x53, 0x65, 0x6c, 0x65, 0x63, + 0x74, 0x6f, 0x72, 0x73, 0x12, 0x23, 0x0a, 0x0a, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x48, 0x01, 0x52, 0x0a, 0x6c, 0x69, 0x6d, 0x69, + 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x88, 0x01, 0x01, 0x42, 0x0a, 0x0a, 0x08, 0x5f, 0x65, 0x6e, + 0x64, 0x65, 0x64, 0x41, 0x74, 0x42, 0x0d, 0x0a, 0x0b, 0x5f, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x43, + 0x6f, 0x75, 0x6e, 0x74, 0x22, 0x4d, 0x0a, 0x15, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, + 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x34, 0x0a, + 0x04, 0x64, 0x6f, 0x63, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x20, 0x2e, 0x72, 0x65, + 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, + 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x52, 0x04, 0x64, + 0x6f, 0x63, 0x73, 0x2a, 0x86, 0x02, 0x0a, 0x13, 0x53, 0x63, 0x68, 0x65, 0x6d, 0x61, 0x46, 0x69, + 0x65, 0x6c, 0x64, 0x44, 0x61, 0x74, 0x61, 0x54, 0x79, 0x70, 0x65, 0x12, 0x1f, 0x0a, 0x1b, 0x53, + 0x43, 0x48, 0x45, 0x4d, 0x41, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, + 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, - 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, 0x45, 0x47, 0x45, 0x52, 0x10, 0x03, - 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, - 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x42, 0x4f, 0x4f, 0x4c, 0x10, - 0x04, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x5f, 0x46, 0x49, 0x45, 0x4c, - 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4f, 0x42, 0x4a, 0x45, - 0x43, 0x54, 0x10, 0x05, 0x12, 0x20, 0x0a, 0x1c, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x5f, 0x46, - 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, - 0x52, 0x52, 0x41, 0x59, 0x10, 0x06, 0x2a, 0xc5, 0x01, 0x0a, 0x0e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4c, - 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, - 0x45, 0x10, 0x00, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x47, 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x46, - 0x49, 0x4c, 0x45, 0x53, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x49, 0x4d, 0x45, 0x53, 0x45, - 0x52, 0x49, 0x45, 0x53, 0x10, 0x02, 0x12, 0x1f, 0x0a, 0x1b, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, - 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x54, 0x52, 0x41, 0x4e, 0x53, 0x41, - 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x1a, 0x0a, 0x16, 0x43, 0x4f, 0x4c, 0x4c, 0x45, - 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x56, 0x45, 0x43, 0x54, 0x4f, - 0x52, 0x10, 0x04, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, - 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x52, 0x4f, 0x58, 0x59, 0x10, 0x05, 0x2a, 0xa7, - 0x01, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x12, 0x17, 0x0a, - 0x13, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x55, 0x4e, 0x4b, - 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, - 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x41, 0x55, 0x54, 0x4f, 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, - 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, - 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, - 0x54, 0x5f, 0x43, 0x53, 0x56, 0x10, 0x03, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f, - 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x58, 0x4d, 0x4c, 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, - 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x55, 0x52, 0x4c, 0x10, - 0x05, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, - 0x5f, 0x4d, 0x45, 0x44, 0x49, 0x41, 0x10, 0x06, 0x32, 0xc2, 0x12, 0x0a, 0x0e, 0x52, 0x65, 0x73, - 0x74, 0x43, 0x6f, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0xd7, 0x01, 0x0a, 0x0d, - 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x44, 0x6f, 0x63, 0x12, 0x21, 0x2e, - 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x53, - 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x44, 0x6f, 0x63, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x48, 0x74, - 0x74, 0x70, 0x42, 0x6f, 0x64, 0x79, 0x22, 0x8c, 0x01, 0x92, 0x41, 0x24, 0x0a, 0x07, 0x73, 0x77, - 0x61, 0x67, 0x67, 0x65, 0x72, 0x12, 0x19, 0x52, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x41, 0x50, - 0x49, 0x20, 0x44, 0x6f, 0x63, 0x20, 0x69, 0x6e, 0x20, 0x53, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x5f, 0x5a, 0x3c, 0x12, 0x3a, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, - 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x2f, 0x61, 0x70, - 0x69, 0x64, 0x6f, 0x63, 0x12, 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x61, - 0x70, 0x69, 0x64, 0x6f, 0x63, 0x12, 0xef, 0x01, 0x0a, 0x10, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, - 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, - 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8d, 0x01, 0x92, 0x41, 0x5b, 0x0a, 0x0a, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4d, 0x41, 0x64, 0x64, 0x20, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x2c, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x73, 0x65, 0x74, 0x20, 0x6f, 0x66, 0x20, 0x64, 0x6f, 0x63, - 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x73, 0x63, 0x68, 0x65, - 0x6d, 0x65, 0x2d, 0x66, 0x72, 0x65, 0x65, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x29, 0x3a, 0x01, - 0x2a, 0x22, 0x24, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, - 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, - 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0xb4, 0x01, 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, - 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x23, 0x2e, 0x72, 0x65, - 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, - 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x56, 0x92, 0x41, 0x27, 0x0a, 0x0a, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x6c, 0x69, 0x73, 0x74, 0x20, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, - 0x6e, 0x74, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x12, 0x24, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, - 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, - 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0xf7, - 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x12, 0x21, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, - 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, - 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9e, 0x01, 0x92, 0x41, 0x60, 0x0a, 0x0a, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x52, 0x72, 0x65, 0x74, 0x72, 0x69, - 0x65, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x69, 0x6e, 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, - 0x6c, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x6e, 0x66, - 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x64, 0x6f, 0x63, - 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6b, 0x65, 0x79, 0x73, 0x20, 0x61, 0x73, 0x73, 0x6f, 0x63, - 0x61, 0x69, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, 0x20, 0x69, 0x74, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x35, 0x12, 0x33, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, - 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, - 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x12, 0xe4, 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, - 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x2e, - 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, - 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, - 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x82, 0x01, 0x92, 0x41, 0x44, - 0x0a, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, 0x36, 0x72, 0x65, - 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x69, 0x6e, 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, - 0x61, 0x6c, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x6e, - 0x64, 0x20, 0x69, 0x74, 0x73, 0x20, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, - 0x20, 0x64, 0x6f, 0x63, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x35, 0x2a, 0x33, 0x2f, 0x76, 0x31, 0x2f, - 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x12, - 0xf9, 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, - 0x6e, 0x74, 0x12, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, - 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, - 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, - 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9d, 0x01, 0x92, 0x41, - 0x2f, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x23, 0x63, 0x72, 0x65, - 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x74, - 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x65, 0x3a, 0x01, 0x2a, 0x5a, 0x3f, 0x3a, 0x01, 0x2a, 0x22, 0x3a, + 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x53, 0x54, 0x52, 0x49, 0x4e, 0x47, 0x10, 0x01, 0x12, + 0x21, 0x0a, 0x1d, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, + 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x4e, 0x55, 0x4d, 0x42, 0x45, 0x52, + 0x10, 0x02, 0x12, 0x22, 0x0a, 0x1e, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, 0x5f, 0x46, 0x49, 0x45, + 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x49, 0x4e, 0x54, + 0x45, 0x47, 0x45, 0x52, 0x10, 0x03, 0x12, 0x1f, 0x0a, 0x1b, 0x53, 0x43, 0x48, 0x45, 0x4d, 0x41, + 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x42, 0x4f, 0x4f, 0x4c, 0x10, 0x04, 0x12, 0x21, 0x0a, 0x1d, 0x53, 0x43, 0x48, 0x45, 0x4d, + 0x41, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x54, 0x59, 0x50, + 0x45, 0x5f, 0x4f, 0x42, 0x4a, 0x45, 0x43, 0x54, 0x10, 0x05, 0x12, 0x20, 0x0a, 0x1c, 0x53, 0x43, + 0x48, 0x45, 0x4d, 0x41, 0x5f, 0x46, 0x49, 0x45, 0x4c, 0x44, 0x5f, 0x44, 0x41, 0x54, 0x41, 0x5f, + 0x54, 0x59, 0x50, 0x45, 0x5f, 0x41, 0x52, 0x52, 0x41, 0x59, 0x10, 0x06, 0x2a, 0xc5, 0x01, 0x0a, + 0x0e, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x54, 0x79, 0x70, 0x65, 0x12, + 0x18, 0x0a, 0x14, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, + 0x50, 0x45, 0x5f, 0x4e, 0x4f, 0x4e, 0x45, 0x10, 0x00, 0x12, 0x21, 0x0a, 0x1d, 0x43, 0x4f, 0x4c, + 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x52, 0x45, 0x47, + 0x55, 0x4c, 0x41, 0x52, 0x5f, 0x46, 0x49, 0x4c, 0x45, 0x53, 0x10, 0x01, 0x12, 0x1e, 0x0a, 0x1a, + 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x54, 0x49, 0x4d, 0x45, 0x53, 0x45, 0x52, 0x49, 0x45, 0x53, 0x10, 0x02, 0x12, 0x1f, 0x0a, 0x1b, + 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, + 0x54, 0x52, 0x41, 0x4e, 0x53, 0x41, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x10, 0x03, 0x12, 0x1a, 0x0a, + 0x16, 0x43, 0x4f, 0x4c, 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, + 0x5f, 0x56, 0x45, 0x43, 0x54, 0x4f, 0x52, 0x10, 0x04, 0x12, 0x19, 0x0a, 0x15, 0x43, 0x4f, 0x4c, + 0x4c, 0x45, 0x43, 0x54, 0x49, 0x4f, 0x4e, 0x5f, 0x54, 0x59, 0x50, 0x45, 0x5f, 0x50, 0x52, 0x4f, + 0x58, 0x59, 0x10, 0x05, 0x2a, 0xa7, 0x01, 0x0a, 0x0a, 0x44, 0x61, 0x74, 0x61, 0x46, 0x6f, 0x72, + 0x6d, 0x61, 0x74, 0x12, 0x17, 0x0a, 0x13, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, + 0x41, 0x54, 0x5f, 0x55, 0x4e, 0x4b, 0x4e, 0x4f, 0x57, 0x4e, 0x10, 0x00, 0x12, 0x14, 0x0a, 0x10, + 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x41, 0x55, 0x54, 0x4f, + 0x10, 0x01, 0x12, 0x14, 0x0a, 0x10, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, + 0x54, 0x5f, 0x4a, 0x53, 0x4f, 0x4e, 0x10, 0x02, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x41, 0x54, 0x41, + 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x43, 0x53, 0x56, 0x10, 0x03, 0x12, 0x13, 0x0a, + 0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x58, 0x4d, 0x4c, + 0x10, 0x04, 0x12, 0x13, 0x0a, 0x0f, 0x44, 0x41, 0x54, 0x41, 0x5f, 0x46, 0x4f, 0x52, 0x4d, 0x41, + 0x54, 0x5f, 0x55, 0x52, 0x4c, 0x10, 0x05, 0x12, 0x15, 0x0a, 0x11, 0x44, 0x41, 0x54, 0x41, 0x5f, + 0x46, 0x4f, 0x52, 0x4d, 0x41, 0x54, 0x5f, 0x4d, 0x45, 0x44, 0x49, 0x41, 0x10, 0x06, 0x32, 0xc2, + 0x12, 0x0a, 0x0e, 0x52, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, + 0x65, 0x12, 0xd7, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, + 0x44, 0x6f, 0x63, 0x12, 0x21, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x47, 0x65, 0x74, 0x53, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x44, 0x6f, 0x63, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x14, 0x2e, 0x67, 0x6f, 0x6f, 0x67, 0x6c, 0x65, 0x2e, + 0x61, 0x70, 0x69, 0x2e, 0x48, 0x74, 0x74, 0x70, 0x42, 0x6f, 0x64, 0x79, 0x22, 0x8c, 0x01, 0x92, + 0x41, 0x24, 0x0a, 0x07, 0x73, 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x12, 0x19, 0x52, 0x65, 0x74, + 0x75, 0x72, 0x6e, 0x20, 0x41, 0x50, 0x49, 0x20, 0x44, 0x6f, 0x63, 0x20, 0x69, 0x6e, 0x20, 0x53, + 0x77, 0x61, 0x67, 0x67, 0x65, 0x72, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x5f, 0x5a, 0x3c, 0x12, 0x3a, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x49, 0x64, 0x7d, 0x3a, 0x6e, 0x65, 0x77, 0x64, 0x6f, 0x63, 0x22, 0x1f, 0x2f, 0x76, 0x31, 0x2f, - 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x6e, 0x65, 0x77, 0x64, 0x6f, 0x63, 0x12, 0xe3, 0x01, 0x0a, 0x0b, - 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x1f, 0x2e, 0x72, 0x65, - 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, - 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, - 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x6f, - 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x90, - 0x01, 0x92, 0x41, 0x40, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x34, - 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, - 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, - 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, - 0x69, 0x6f, 0x6e, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x47, 0x12, 0x45, 0x2f, 0x76, 0x31, 0x2f, + 0x49, 0x64, 0x7d, 0x2f, 0x61, 0x70, 0x69, 0x64, 0x6f, 0x63, 0x12, 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, - 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, - 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x2f, - 0x64, 0x6f, 0x63, 0x73, 0x2f, 0x7b, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, - 0x7d, 0x12, 0xe8, 0x01, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, - 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, - 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, - 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, - 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8c, 0x01, - 0x92, 0x41, 0x3c, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x30, 0x52, - 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, - 0x69, 0x63, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x66, 0x72, 0x6f, 0x6d, - 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x82, - 0xd3, 0xe4, 0x93, 0x02, 0x47, 0x2a, 0x45, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, - 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, - 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, - 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2f, - 0x7b, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x7d, 0x12, 0x91, 0x02, 0x0a, - 0x14, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x53, - 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x27, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, - 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, - 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, - 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x22, 0xab, 0x01, 0x92, 0x41, 0x61, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, - 0x1a, 0x55, 0x72, 0x75, 0x6e, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x61, 0x67, 0x61, 0x69, - 0x6e, 0x73, 0x74, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, - 0x2c, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, - 0x74, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x20, - 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x20, 0x74, 0x68, - 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x41, 0x12, 0x3f, 0x2f, - 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, - 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, - 0x64, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x3a, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x30, 0x01, - 0x12, 0xea, 0x01, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, - 0x6e, 0x74, 0x12, 0x21, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, - 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, - 0x61, 0x70, 0x69, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, - 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x91, 0x01, 0x92, 0x41, 0x4e, 0x0a, - 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x42, 0x72, 0x75, 0x6e, 0x20, 0x71, - 0x75, 0x65, 0x72, 0x79, 0x20, 0x61, 0x67, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x20, 0x61, 0x20, 0x63, - 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2c, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, - 0x6e, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x6d, 0x61, 0x74, 0x63, + 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x61, 0x70, 0x69, 0x64, 0x6f, 0x63, 0x12, 0xef, 0x01, 0x0a, 0x10, + 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, + 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x8d, 0x01, + 0x92, 0x41, 0x5b, 0x0a, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, + 0x4d, 0x41, 0x64, 0x64, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, + 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x2c, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x69, 0x73, 0x20, 0x61, 0x20, 0x73, 0x65, 0x74, 0x20, + 0x6f, 0x66, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x77, 0x69, 0x74, + 0x68, 0x20, 0x73, 0x63, 0x68, 0x65, 0x6d, 0x65, 0x2d, 0x66, 0x72, 0x65, 0x65, 0x2e, 0x82, 0xd3, + 0xe4, 0x93, 0x02, 0x29, 0x3a, 0x01, 0x2a, 0x22, 0x24, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, + 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, + 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0xb4, 0x01, + 0x0a, 0x0f, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x12, 0x23, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x56, 0x92, 0x41, + 0x27, 0x0a, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x19, 0x6c, + 0x69, 0x73, 0x74, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x20, + 0x65, 0x6e, 0x64, 0x70, 0x6f, 0x69, 0x6e, 0x74, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x26, 0x12, 0x24, + 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, + 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x73, 0x12, 0xf7, 0x01, 0x0a, 0x0d, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x21, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, + 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x74, + 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x43, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x9e, 0x01, + 0x92, 0x41, 0x60, 0x0a, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x1a, + 0x52, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x69, 0x6e, 0x64, + 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6c, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x20, 0x69, 0x6e, 0x66, 0x6f, 0x72, 0x6d, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x61, + 0x6e, 0x64, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x6b, 0x65, 0x79, 0x73, + 0x20, 0x61, 0x73, 0x73, 0x6f, 0x63, 0x61, 0x69, 0x74, 0x65, 0x64, 0x20, 0x77, 0x69, 0x74, 0x68, + 0x20, 0x69, 0x74, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x35, 0x12, 0x33, 0x2f, 0x76, 0x31, 0x2f, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, + 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x12, 0xe4, + 0x01, 0x0a, 0x10, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x12, 0x24, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, + 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x25, 0x2e, 0x72, 0x65, 0x73, 0x74, + 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x43, 0x6f, + 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x22, 0x82, 0x01, 0x92, 0x41, 0x44, 0x0a, 0x0a, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x1a, 0x36, 0x72, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x61, 0x6e, 0x20, 0x69, 0x6e, + 0x64, 0x69, 0x76, 0x69, 0x64, 0x75, 0x61, 0x6c, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, + 0x69, 0x6f, 0x6e, 0x20, 0x61, 0x6e, 0x64, 0x20, 0x69, 0x74, 0x73, 0x20, 0x61, 0x73, 0x73, 0x6f, + 0x63, 0x69, 0x61, 0x74, 0x65, 0x64, 0x20, 0x64, 0x6f, 0x63, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x35, + 0x2a, 0x33, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x12, 0xf9, 0x01, 0x0a, 0x0e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, + 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, + 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, 0x65, 0x44, 0x6f, 0x63, + 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, 0x2e, 0x72, + 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x43, 0x72, 0x65, 0x61, 0x74, + 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, + 0x65, 0x22, 0x9d, 0x01, 0x92, 0x41, 0x2f, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x1a, 0x23, 0x63, 0x72, 0x65, 0x61, 0x74, 0x65, 0x20, 0x61, 0x20, 0x64, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x74, 0x6f, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x65, 0x3a, 0x01, 0x2a, 0x5a, + 0x3f, 0x3a, 0x01, 0x2a, 0x22, 0x3a, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, + 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x3a, 0x6e, 0x65, 0x77, 0x64, 0x6f, 0x63, + 0x22, 0x1f, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x6e, 0x65, 0x77, 0x64, 0x6f, + 0x63, 0x12, 0xe3, 0x01, 0x0a, 0x0b, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x12, 0x1f, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, + 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, + 0x2e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, + 0x6f, 0x6e, 0x73, 0x65, 0x22, 0x90, 0x01, 0x92, 0x41, 0x40, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x34, 0x72, 0x65, 0x74, 0x72, 0x69, 0x65, 0x76, 0x65, 0x20, 0x61, + 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x20, 0x69, 0x6e, 0x66, 0x6f, 0x72, 0x6d, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, + 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x47, + 0x12, 0x45, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, + 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, + 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2f, 0x7b, 0x64, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x49, 0x64, 0x7d, 0x12, 0xe8, 0x01, 0x0a, 0x0e, 0x44, 0x65, 0x6c, 0x65, + 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x22, 0x2e, 0x72, 0x65, 0x73, + 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, 0x65, 0x74, 0x65, 0x44, + 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x23, + 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x44, 0x65, 0x6c, + 0x65, 0x74, 0x65, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x22, 0x8c, 0x01, 0x92, 0x41, 0x3c, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, + 0x65, 0x6e, 0x74, 0x1a, 0x30, 0x52, 0x65, 0x6d, 0x6f, 0x76, 0x65, 0x20, 0x74, 0x68, 0x65, 0x20, + 0x73, 0x70, 0x65, 0x63, 0x69, 0x66, 0x69, 0x63, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x20, 0x66, 0x72, 0x6f, 0x6d, 0x20, 0x74, 0x68, 0x65, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x47, 0x2a, 0x45, 0x2f, 0x76, 0x31, + 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, + 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, + 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, + 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x2f, 0x7b, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x49, + 0x64, 0x7d, 0x12, 0x91, 0x02, 0x0a, 0x14, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x12, 0x27, 0x2e, 0x72, 0x65, + 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, + 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6d, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x1a, 0x20, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, + 0x70, 0x69, 0x2e, 0x47, 0x65, 0x74, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, + 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, 0xab, 0x01, 0x92, 0x41, 0x61, 0x0a, 0x08, 0x64, 0x6f, + 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x1a, 0x55, 0x72, 0x75, 0x6e, 0x20, 0x71, 0x75, 0x65, 0x72, + 0x79, 0x20, 0x61, 0x67, 0x61, 0x69, 0x6e, 0x73, 0x74, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x6c, 0x6c, + 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2c, 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x64, + 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x73, 0x20, 0x69, 0x6e, 0x20, 0x73, 0x74, 0x72, 0x65, + 0x61, 0x6d, 0x69, 0x6e, 0x67, 0x20, 0x77, 0x68, 0x69, 0x63, 0x68, 0x20, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x82, 0xd3, 0xe4, - 0x93, 0x02, 0x3a, 0x12, 0x38, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x93, 0x02, 0x41, 0x12, 0x3f, 0x2f, 0x76, 0x31, 0x2f, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, - 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x42, 0x80, 0x03, - 0x92, 0x41, 0xd5, 0x02, 0x12, 0xc1, 0x01, 0x0a, 0x1a, 0x52, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6c, - 0x20, 0x41, 0x50, 0x49, 0x20, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x73, 0x22, 0x44, 0x0a, 0x1a, 0x52, 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x20, 0x41, - 0x50, 0x49, 0x20, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, - 0x73, 0x12, 0x26, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x6f, 0x74, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x61, - 0x69, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2a, 0x58, 0x0a, 0x14, 0x42, 0x53, 0x44, - 0x20, 0x33, 0x2d, 0x43, 0x6c, 0x61, 0x75, 0x73, 0x65, 0x20, 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, - 0x65, 0x12, 0x40, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x6f, 0x74, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x61, - 0x69, 0x2f, 0x67, 0x72, 0x61, 0x6e, 0x64, 0x74, 0x75, 0x72, 0x6b, 0x2f, 0x62, 0x6c, 0x6f, 0x62, - 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x2f, 0x4c, 0x49, 0x43, 0x45, 0x4e, 0x53, 0x45, 0x2e, - 0x74, 0x78, 0x74, 0x32, 0x03, 0x31, 0x2e, 0x30, 0x2a, 0x01, 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, - 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, - 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x5a, - 0x23, 0x0a, 0x21, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, - 0x13, 0x08, 0x02, 0x1a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, - 0x6f, 0x6e, 0x20, 0x02, 0x62, 0x10, 0x0a, 0x0e, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, - 0x41, 0x75, 0x74, 0x68, 0x12, 0x00, 0x72, 0x31, 0x0a, 0x07, 0x72, 0x65, 0x73, 0x74, 0x61, 0x70, - 0x69, 0x12, 0x26, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x6f, 0x74, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x61, - 0x69, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x5a, 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, - 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x6f, 0x74, 0x70, 0x72, 0x69, 0x6e, 0x74, 0x61, - 0x69, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, - 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x2f, 0x64, 0x6f, 0x63, 0x73, 0x3a, 0x73, 0x74, + 0x72, 0x65, 0x61, 0x6d, 0x30, 0x01, 0x12, 0xea, 0x01, 0x0a, 0x0d, 0x51, 0x75, 0x65, 0x72, 0x79, + 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x12, 0x21, 0x2e, 0x72, 0x65, 0x73, 0x74, 0x63, + 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, 0x6f, 0x63, 0x75, + 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x22, 0x2e, 0x72, 0x65, + 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2e, 0x61, 0x70, 0x69, 0x2e, 0x51, 0x75, 0x65, 0x72, 0x79, 0x44, + 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x22, + 0x91, 0x01, 0x92, 0x41, 0x4e, 0x0a, 0x08, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, 0x1a, + 0x42, 0x72, 0x75, 0x6e, 0x20, 0x71, 0x75, 0x65, 0x72, 0x79, 0x20, 0x61, 0x67, 0x61, 0x69, 0x6e, + 0x73, 0x74, 0x20, 0x61, 0x20, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x2c, + 0x20, 0x72, 0x65, 0x74, 0x75, 0x72, 0x6e, 0x20, 0x64, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, 0x74, + 0x73, 0x20, 0x6d, 0x61, 0x74, 0x63, 0x68, 0x65, 0x64, 0x20, 0x74, 0x68, 0x65, 0x20, 0x71, 0x75, + 0x65, 0x72, 0x79, 0x82, 0xd3, 0xe4, 0x93, 0x02, 0x3a, 0x12, 0x38, 0x2f, 0x76, 0x31, 0x2f, 0x70, + 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, 0x73, 0x2f, 0x7b, 0x70, 0x72, 0x6f, 0x6a, 0x65, 0x63, 0x74, + 0x49, 0x64, 0x7d, 0x2f, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x2f, + 0x7b, 0x63, 0x6f, 0x6c, 0x6c, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x49, 0x64, 0x7d, 0x2f, 0x64, + 0x6f, 0x63, 0x73, 0x42, 0x80, 0x03, 0x92, 0x41, 0xd5, 0x02, 0x12, 0xc1, 0x01, 0x0a, 0x1a, 0x52, + 0x65, 0x73, 0x74, 0x43, 0x6f, 0x6c, 0x20, 0x41, 0x50, 0x49, 0x20, 0x44, 0x6f, 0x63, 0x75, 0x6d, + 0x65, 0x6e, 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x22, 0x44, 0x0a, 0x1a, 0x52, 0x65, 0x73, + 0x74, 0x43, 0x6f, 0x6c, 0x20, 0x41, 0x50, 0x49, 0x20, 0x44, 0x6f, 0x63, 0x75, 0x6d, 0x65, 0x6e, + 0x74, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, 0x26, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, + 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x6f, 0x74, + 0x70, 0x72, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2a, + 0x58, 0x0a, 0x14, 0x42, 0x53, 0x44, 0x20, 0x33, 0x2d, 0x43, 0x6c, 0x61, 0x75, 0x73, 0x65, 0x20, + 0x4c, 0x69, 0x63, 0x65, 0x6e, 0x73, 0x65, 0x12, 0x40, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, + 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x6f, 0x74, + 0x70, 0x72, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x2f, 0x67, 0x72, 0x61, 0x6e, 0x64, 0x74, 0x75, 0x72, + 0x6b, 0x2f, 0x62, 0x6c, 0x6f, 0x62, 0x2f, 0x6d, 0x61, 0x73, 0x74, 0x65, 0x72, 0x2f, 0x4c, 0x49, + 0x43, 0x45, 0x4e, 0x53, 0x45, 0x2e, 0x74, 0x78, 0x74, 0x32, 0x03, 0x31, 0x2e, 0x30, 0x2a, 0x01, + 0x02, 0x32, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x2f, 0x6a, + 0x73, 0x6f, 0x6e, 0x3a, 0x10, 0x61, 0x70, 0x70, 0x6c, 0x69, 0x63, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x2f, 0x6a, 0x73, 0x6f, 0x6e, 0x5a, 0x23, 0x0a, 0x21, 0x0a, 0x0a, 0x41, 0x70, 0x69, 0x4b, 0x65, + 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x13, 0x08, 0x02, 0x1a, 0x0d, 0x41, 0x75, 0x74, 0x68, 0x6f, + 0x72, 0x69, 0x7a, 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x20, 0x02, 0x62, 0x10, 0x0a, 0x0e, 0x0a, 0x0a, + 0x41, 0x70, 0x69, 0x4b, 0x65, 0x79, 0x41, 0x75, 0x74, 0x68, 0x12, 0x00, 0x72, 0x31, 0x0a, 0x07, + 0x72, 0x65, 0x73, 0x74, 0x61, 0x70, 0x69, 0x12, 0x26, 0x68, 0x74, 0x74, 0x70, 0x73, 0x3a, 0x2f, + 0x2f, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x6f, 0x74, + 0x70, 0x72, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x5a, + 0x25, 0x67, 0x69, 0x74, 0x68, 0x75, 0x62, 0x2e, 0x63, 0x6f, 0x6d, 0x2f, 0x66, 0x6f, 0x6f, 0x74, + 0x70, 0x72, 0x69, 0x6e, 0x74, 0x61, 0x69, 0x2f, 0x72, 0x65, 0x73, 0x74, 0x63, 0x6f, 0x6c, 0x2f, + 0x61, 0x70, 0x69, 0x2f, 0x70, 0x62, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( diff --git a/api/pb/restcol.pb.gw.go b/api/pb/restcol.pb.gw.go index 0d7c460..839784d 100644 --- a/api/pb/restcol.pb.gw.go +++ b/api/pb/restcol.pb.gw.go @@ -357,6 +357,10 @@ func local_request_RestColService_GetCollection_0(ctx context.Context, marshaler } +var ( + filter_RestColService_DeleteCollection_0 = &utilities.DoubleArray{Encoding: map[string]int{"projectId": 0, "collectionId": 1}, Base: []int{1, 1, 2, 0, 0}, Check: []int{0, 1, 1, 2, 3}} +) + func request_RestColService_DeleteCollection_0(ctx context.Context, marshaler runtime.Marshaler, client RestColServiceClient, req *http.Request, pathParams map[string]string) (proto.Message, runtime.ServerMetadata, error) { var protoReq DeleteCollectionRequest var metadata runtime.ServerMetadata @@ -388,6 +392,13 @@ func request_RestColService_DeleteCollection_0(ctx context.Context, marshaler ru return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionId", err) } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RestColService_DeleteCollection_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := client.DeleteCollection(ctx, &protoReq, grpc.Header(&metadata.HeaderMD), grpc.Trailer(&metadata.TrailerMD)) return msg, metadata, err @@ -424,6 +435,13 @@ func local_request_RestColService_DeleteCollection_0(ctx context.Context, marsha return nil, metadata, status.Errorf(codes.InvalidArgument, "type mismatch, parameter: %s, error: %v", "collectionId", err) } + if err := req.ParseForm(); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + if err := runtime.PopulateQueryParameters(&protoReq, req.Form, filter_RestColService_DeleteCollection_0); err != nil { + return nil, metadata, status.Errorf(codes.InvalidArgument, "%v", err) + } + msg, err := server.DeleteCollection(ctx, &protoReq) return msg, metadata, err diff --git a/api/restcol.proto b/api/restcol.proto index bc2175f..f5263b2 100644 --- a/api/restcol.proto +++ b/api/restcol.proto @@ -248,6 +248,10 @@ message GetCollectionResponse{ message DeleteCollectionRequest { string projectId = 1; string collectionId = 2; + // When false (default), DeleteCollection fails if the collection contains + // any documents. When true, all documents in the collection are + // soft-deleted before the collection itself is removed. + bool force = 3; } message DeleteCollectionResponse {} diff --git a/integrationtest/setup_test.go b/integrationtest/setup_test.go index abe18a3..484680a 100644 --- a/integrationtest/setup_test.go +++ b/integrationtest/setup_test.go @@ -6,12 +6,12 @@ import ( restcolopenapicollections "github.com/footprintai/restcol/api/go-openapiv2/client/collections" restcolopenapimodels "github.com/footprintai/restcol/api/go-openapiv2/models" - restcoldummy "github.com/footprintai/restcol/pkg/dummy" + bootstrap "github.com/footprintai/restcol/pkg/bootstrap" "github.com/stretchr/testify/assert" ) var ( - projectId = restcoldummy.DummyModelProject.ID.String() + projectId = bootstrap.DefaultModelProject.ID.String() cid string ) diff --git a/integrationtest/suite.go b/integrationtest/suite.go index 4c5a74e..6b342df 100644 --- a/integrationtest/suite.go +++ b/integrationtest/suite.go @@ -11,7 +11,7 @@ import ( restcolgohttpclient "github.com/footprintai/restcol/api/go-http-client" restcolopenapi "github.com/footprintai/restcol/api/go-openapiv2/client" - integrationtestserver "github.com/footprintai/restcol/integrationtest/server" + serverapp "github.com/footprintai/restcol/pkg/server/app" ) type SuiteCloser interface { @@ -19,7 +19,7 @@ type SuiteCloser interface { } type suite struct { - svr *integrationtestserver.Server + svr *serverapp.Server } func (s *suite) Close() error { @@ -36,7 +36,7 @@ func SetupTest(t *testing.T) *suite { if err != nil { assert.NoError(t, err) } - svr, err := integrationtestserver.NewServer(50050, 50051, postgresDb, log) + svr, err := serverapp.NewServer(50050, 50051, postgresDb, log) if err != nil { log.Fatal("%+v", err) } diff --git a/main.go b/main.go index edff540..8116535 100644 --- a/main.go +++ b/main.go @@ -7,7 +7,7 @@ import ( storageflags "github.com/sdinsure/agent/pkg/storage/flags" postgresstorage "github.com/sdinsure/agent/pkg/storage/postgres" - integrationtestserver "github.com/footprintai/restcol/integrationtest/server" + serverapp "github.com/footprintai/restcol/pkg/server/app" "github.com/footprintai/restcol/pkg/version" ) @@ -29,7 +29,7 @@ func main() { log.Fatal("failed to init dsn, err:%+v\n", err) } - svr, err := integrationtestserver.NewServer(*grpcPort, *httpPort, postgresDb, log) + svr, err := serverapp.NewServer(*grpcPort, *httpPort, postgresDb, log) if err != nil { log.Fatal("failed to init dsn, err:%+v\n", err) } diff --git a/pkg/app/app.go b/pkg/app/app.go index e79a834..d4980e6 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -1,22 +1,18 @@ +// Package app implements the gRPC RestColService handlers. Handlers are split +// by resource into app.go (shared plumbing + swagger), collections.go, and +// documents.go. package app import ( "context" "errors" - "fmt" - "strings" - "time" - sderrors "github.com/sdinsure/agent/pkg/errors" "github.com/sdinsure/agent/pkg/logger" sdinsureruntime "github.com/sdinsure/agent/pkg/runtime" "google.golang.org/genproto/googleapis/api/httpbody" - "google.golang.org/protobuf/types/known/structpb" - "google.golang.org/protobuf/types/known/timestamppb" apppb "github.com/footprintai/restcol/api/pb" collectionsmodel "github.com/footprintai/restcol/pkg/models/collections" - documentsmodel "github.com/footprintai/restcol/pkg/models/documents" projectsmodel "github.com/footprintai/restcol/pkg/models/projects" schemafinder "github.com/footprintai/restcol/pkg/schema" collectionsstorage "github.com/footprintai/restcol/pkg/storage/collections" @@ -24,6 +20,21 @@ import ( collectionsswagger "github.com/footprintai/restcol/pkg/swagger/collections" ) +// RestColServiceServerService implements apppb.RestColServiceServer. +type RestColServiceServerService struct { + apppb.UnimplementedRestColServiceServer + + log logger.Logger + collectionCURD *collectionsstorage.CollectionCURD + documentCURD *documentsstorage.DocumentCURD + + schemaBuilder *schemafinder.SchemaBuilder + + defaultProjectResolver sdinsureruntime.ProjectResolver +} + +// NewRestColServiceServerService wires a new service handler. Call +// SetDefaultProjectResolver before serving requests. func NewRestColServiceServerService( log logger.Logger, collectionCURD *collectionsstorage.CollectionCURD, @@ -38,499 +49,61 @@ func NewRestColServiceServerService( } } -type RestColServiceServerService struct { - apppb.UnimplementedRestColServiceServer - - log logger.Logger - collectionCURD *collectionsstorage.CollectionCURD - documentCURD *documentsstorage.DocumentCURD - - schemaBuilder *schemafinder.SchemaBuilder - - //optional - defaultProjectResolver sdinsureruntime.ProjectResolver -} - +// SetDefaultProjectResolver installs the resolver that maps incoming requests +// to a project tenant. Without it, every handler returns an error. func (r *RestColServiceServerService) SetDefaultProjectResolver(projectResolver sdinsureruntime.ProjectResolver) { r.defaultProjectResolver = projectResolver } -func (r *RestColServiceServerService) GetSwaggerDoc(ctx context.Context, req *apppb.GetSwaggerDocRequest) (*httpbody.HttpBody, error) { - var err error - projectId, err := r.getProjectIdFromCtx(ctx) - if err != nil { - return nil, err - } - - var collectionList []*collectionsmodel.ModelCollection - if len(req.CollectionId) > 0 { - var selectedCollection *collectionsmodel.ModelCollection - cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) - selectedCollection, err = r.collectionCURD.GetLatestSchema(ctx, "", projectId, cid) - if err != nil { - return nil, err - } - collectionList = append(collectionList, selectedCollection) - } else { - // query collections from the project - collectionList, err = r.collectionCURD.ListByProjectID(ctx, "", projectId) - if err != nil { - return nil, err - } - } - colSwaggerDoc := collectionsswagger.NewCollectionSwaggerDoc(collectionList...) - colSwaggerDocInStr, err := colSwaggerDoc.RenderDoc() - if err != nil { - return nil, err - } - return &httpbody.HttpBody{ - ContentType: "application/json", - Data: []byte(colSwaggerDocInStr), - }, nil -} - -func (r *RestColServiceServerService) CreateCollection(ctx context.Context, req *apppb.CreateCollectionRequest) (*apppb.CreateCollectionResponse, error) { - projectId, err := r.getProjectIdFromCtx(ctx) - if err != nil { - return nil, err - } - var cid collectionsmodel.CollectionID = collectionsmodel.NewCollectionID() - if req.CollectionId != nil { - cid = collectionsmodel.NewCollectionIDFromStr(*req.CollectionId) - } - collectionType := apppb.CollectionType_COLLECTION_TYPE_REGULAR_FILES - if req.CollectionType != nil { - collectionType = *req.CollectionType - } - var summary string - if req.Description != nil { - summary = *req.Description - } - var modelSchemaSlice []*collectionsmodel.ModelSchema - if len(req.Schemas) > 0 { - // request is with a specific schema, use it - modelSchema, _ := collectionsmodel.NewModelSchema(req.Schemas) - modelSchemaSlice = append(modelSchemaSlice, modelSchema) - } - - mc := collectionsmodel.NewModelCollection( - projectId, - cid, - collectionType, - summary, - modelSchemaSlice, - ) - if err := r.collectionCURD.Write(ctx, "", &mc); err != nil { - return nil, err - } - - resp := &apppb.CreateCollectionResponse{ - XMetadata: collectionsmodel.NewPbCollectionMetadata(&mc), - Description: mc.Summary, - CollectionType: mc.Type.Proto(), - } - if len(mc.Schemas) > 0 { - resp.Schemas = collectionsmodel.NewPbSchemaFields(mc.Schemas[0]) - } - - return resp, nil -} - -func (r *RestColServiceServerService) getProjectIdFromCtx(ctx context.Context) (pid projectsmodel.ProjectID, reterr error) { +// getProjectIdFromCtx extracts the tenant project from the request context +// populated by identity middleware. +func (r *RestColServiceServerService) getProjectIdFromCtx(ctx context.Context) (projectsmodel.ProjectID, error) { if r.defaultProjectResolver == nil { - pid = projectsmodel.ProjectID("invalid") - reterr = errors.New("no project resolver") r.log.Error("getProjectIdFromCtx: no valid project resolver\n") - return + return projectsmodel.ProjectID("invalid"), errors.New("no project resolver") } projectInfor, found := r.defaultProjectResolver.ProjectInfo(ctx) if !found { - r.log.Info("no valid project id found, use default: %+v\n", pid) - return + r.log.Info("no valid project id found\n") + return "", nil } rawPid, err := projectInfor.GetProjectID() if err != nil { r.log.Error("getProjectIdFromCtx: get projectid failed, err:%+v\n", err) - return + return "", err } return projectsmodel.ProjectID(rawPid), nil } -// TODO getCollectionIDFromSchemas would lookup collection id with schema list given -// This should scan all collections and match by its schema and return the right collection id -// For now, we create a new collection with auto-generated summary -func (r *RestColServiceServerService) getCollectionIDFromSchemas(ctx context.Context, projectId projectsmodel.ProjectID) (collectionsmodel.CollectionID, error) { - cid := collectionsmodel.NewCollectionID() - - // Auto-create a collection with default settings - mc := collectionsmodel.NewModelCollection( - projectId, - cid, - apppb.CollectionType_COLLECTION_TYPE_REGULAR_FILES, // Default type - "auto created collection", // Auto-generated summary - []*collectionsmodel.ModelSchema{}, // Empty schemas initially - ) - - err := r.collectionCURD.Write(ctx, "", &mc) - if err != nil { - return collectionsmodel.CollectionID(""), err - } - - return cid, nil -} - -func (r *RestColServiceServerService) ListCollections(ctx context.Context, req *apppb.ListCollectionsRequest) (*apppb.ListCollectionsResponse, error) { - return nil, sderrors.NewNotImplError(errors.New("not implemented")) -} - -func (r *RestColServiceServerService) GetCollection(ctx context.Context, req *apppb.GetCollectionRequest) (*apppb.GetCollectionResponse, error) { - var cid collectionsmodel.CollectionID - if len(req.CollectionId) == 0 { - return nil, sderrors.NewBadParamsError(errors.New("missing required field")) - } +// GetSwaggerDoc renders the OpenAPI document for one or all collections of the +// caller's project. +func (r *RestColServiceServerService) GetSwaggerDoc(ctx context.Context, req *apppb.GetSwaggerDocRequest) (*httpbody.HttpBody, error) { projectId, err := r.getProjectIdFromCtx(ctx) if err != nil { return nil, err } - cid = collectionsmodel.NewCollectionIDFromStr(req.CollectionId) - mc, err := r.collectionCURD.GetLatestSchema(ctx, "", projectId, cid) - if err != nil { - ismyerr, myerr := sderrors.As(err) - if ismyerr && myerr.Code() == sderrors.CodeNotFound { - return &apppb.GetCollectionResponse{}, nil - } - return nil, err - } - resp := &apppb.GetCollectionResponse{ - XMetadata: collectionsmodel.NewPbCollectionMetadata(mc), - } - if mc == nil { - return resp, nil - } - resp.Description = mc.Summary - resp.CollectionType = mc.Type.Proto() - if mc.Schemas != nil { - resp.Schemas = collectionsmodel.NewPbSchemaFields(mc.Schemas[0]) - } - return resp, nil -} - -func (r *RestColServiceServerService) DeleteCollection(ctx context.Context, req *apppb.DeleteCollectionRequest) (*apppb.DeleteCollectionResponse, error) { - return nil, sderrors.NewNotImplError(errors.New("not implemented")) -} -func (r *RestColServiceServerService) CreateDocument(ctx context.Context, req *apppb.CreateDocumentRequest) (*apppb.CreateDocumentResponse, error) { - projectId, err := r.getProjectIdFromCtx(ctx) - if err != nil { - return nil, err - } - var cid collectionsmodel.CollectionID - if req.CollectionId != "" { - cid = collectionsmodel.NewCollectionIDFromStr(req.CollectionId) - } else { - cid, err = r.getCollectionIDFromSchemas(ctx, projectId) + var collectionList []*collectionsmodel.ModelCollection + if len(req.CollectionId) > 0 { + cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) + selected, err := r.collectionCURD.GetLatestSchema(ctx, "", projectId, cid) if err != nil { return nil, err } - } - - modelCollection, err := r.collectionCURD.GetLatestSchema(ctx, "", projectId, cid) - if err != nil { - return nil, err - } - - var docSchema *collectionsmodel.ModelSchema - - // auto detect schema - _, inputDataSchema, valueHolder, err := r.schemaBuilder.Parse(req.Data) - if err != nil { - r.log.Error("failed to convert into modelschema, err:%+v\n", err) - return nil, err - } - // check whether we need to create a new schema mapping - // or use existing schema - if len(modelCollection.Schemas) == 0 { - // no previous schema, use the latest data - docSchema = inputDataSchema + collectionList = append(collectionList, selected) } else { - // has schema under the collection - if r.schemaBuilder.Equals(modelCollection.Schemas[0], inputDataSchema) { - docSchema = modelCollection.Schemas[0] - } else { - docSchema = inputDataSchema - } - } - var docId documentsmodel.DocumentID - if req.DocumentId == nil { - docId = documentsmodel.NewDocumentID() - } else { - docId, err = documentsmodel.Parse(*req.DocumentId) + collectionList, err = r.collectionCURD.ListByProjectID(ctx, "", projectId) if err != nil { return nil, err } } - - docModel := &documentsmodel.ModelDocument{ - ID: docId, - Data: documentsmodel.NewModelDocumentData(valueHolder), - ModelCollectionID: cid, - ModelCollection: collectionsmodel.NewModelCollection( - projectId, - cid, - apppb.CollectionType_COLLECTION_TYPE_REGULAR_FILES, - "auto created collection", - []*collectionsmodel.ModelSchema{ - docSchema, - }, - ), - ModelProjectID: projectId, - } - if err := r.documentCURD.Write(ctx, "", docModel); err != nil { - r.log.Error("failed to write docmodel, err:%+v\n", err) - return nil, err - } - return &apppb.CreateDocumentResponse{ - XMetadata: documentsmodel.NewPbDocumentMetadata(docModel), - }, nil -} -func (r *RestColServiceServerService) GetDocument(ctx context.Context, req *apppb.GetDocumentRequest) (*apppb.GetDocumentResponse, error) { - // TODO: use pid and cid for permission checking - // as for retrieving data, did is only required field - projectId, err := r.getProjectIdFromCtx(ctx) - if err != nil { - return nil, err - } - cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) - - did, err := documentsmodel.Parse(req.DocumentId) - if err != nil { - return nil, err - } - docModel, err := r.documentCURD.Get(ctx, "", projectId, cid, did) - if err != nil { - return nil, err - } - filteredDoc, err := r.filterDocWithSelectedFields(docModel, req.FieldSelectors) + colSwaggerDoc := collectionsswagger.NewCollectionSwaggerDoc(collectionList...) + docStr, err := colSwaggerDoc.RenderDoc() if err != nil { return nil, err } - - return &apppb.GetDocumentResponse{ - XMetadata: documentsmodel.NewPbDocumentMetadata(docModel), - Data: filteredDoc, + return &httpbody.HttpBody{ + ContentType: "application/json", + Data: []byte(docStr), }, nil - -} - -func (r *RestColServiceServerService) filterDocWithSelectedFields(doc *documentsmodel.ModelDocument, selectedFields []string) (*structpb.Value, error) { - r.log.Info("query doc with fields:%+v\n", selectedFields) - - if doc.Data == nil { - return nil, nil - } - - if len(selectedFields) == 0 { - // no selectedFields, return all - - return structpb.NewValue(doc.Data.MapValue) - } - - // get associated schema - modelSchema, err := r.schemaBuilder.Flatten(doc.Data.MapValue) - if err != nil { - return nil, err - } - - // make selectedFields into a lookup map - lookupMap := make(map[string]struct{}) - for _, selectedField := range selectedFields { - r.log.Info("query doc, add field:%s\n", strings.ToLower(selectedField)) - lookupMap[strings.ToLower(selectedField)] = struct{}{} - } - - var fieldsInSelected []*collectionsmodel.ModelFieldSchema - for _, dataField := range modelSchema.Fields { - r.log.Info("query doc, select field:%s\n", dataField.FieldName.String()) - _, exist := lookupMap[dataField.FieldName.String()] - if exist { - r.log.Info("query doc, added field:%s\n", dataField.FieldName.String()) - fieldsInSelected = append(fieldsInSelected, dataField) - } - } - // construct the whole struct with fieldsInSelected - structWithSelectedFields, err := schemafinder.Build(fieldsInSelected) - if err != nil { - return nil, err - } - return structpb.NewValue(structWithSelectedFields) -} - -func (r *RestColServiceServerService) DeleteDocument(ctx context.Context, req *apppb.DeleteDocumentRequest) (*apppb.DeleteDocumentResponse, error) { - // 1. Extract project ID from context (auth/authz) - projectId, err := r.getProjectIdFromCtx(ctx) - if err != nil { - return nil, err - } - - // 2. Validate required parameters - if req.CollectionId == "" { - return nil, sderrors.NewBadParamsError(errors.New("collection_id is required")) - } - if req.DocumentId == "" { - return nil, sderrors.NewBadParamsError(errors.New("document_id is required")) - } - - // 3. Parse and validate IDs - cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) - did, err := documentsmodel.Parse(req.DocumentId) - if err != nil { - return nil, sderrors.NewBadParamsError(fmt.Errorf("invalid document_id: %w", err)) - } - - // 4. Check if document exists before deletion - _, err = r.documentCURD.Get(ctx, "", projectId, cid, did) - if err != nil { - return nil, err // Will return appropriate error (not found, etc.) - } - - // 5. Perform deletion - err = r.documentCURD.Delete(ctx, "", projectId, cid, did) - if err != nil { - return nil, err - } - - // 6. Return empty response (as defined in proto) - return &apppb.DeleteDocumentResponse{}, nil -} - -func (r *RestColServiceServerService) QueryDocumentsStream(req *apppb.QueryDocumentStreamRequest, stream apppb.RestColService_QueryDocumentsStreamServer) error { - ctx := stream.Context() - projectId, err := r.getProjectIdFromCtx(ctx) - if err != nil { - return err - } - cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) - startedAt := req.SinceTs - endedAt := req.EndedAt - - needsFollowUp := false - if req.FollowUpMode != nil && *req.FollowUpMode { - needsFollowUp = true - } - - if !needsFollowUp { - queryDocs, err := r.documentCURD.Query( - ctx, - "", - projectId, - cid, - makeQueryConditioner(startedAt, endedAt, req.LimitCount)..., - ) - if err != nil { - return err - } - // TODO: apply field selector - for _, doc := range queryDocs { - filteredDoc, err := r.filterDocWithSelectedFields(doc, req.FieldSelectors) - if err != nil { - return err - } - - if err := stream.Send(&apppb.GetDocumentResponse{ - XMetadata: documentsmodel.NewPbDocumentMetadata(doc), - Data: filteredDoc, - }); err != nil { - return err - } - } - return nil - } - // enter followup mode, keep looking data - sendingCount := 0 - for { - r.log.Info("query with time range[%+v -> %+v], cid:%+v\n", startedAt.AsTime(), endedAt.AsTime(), cid) - queryDocs, err := r.documentCURD.Query( - ctx, - "", - projectId, - cid, - makeQueryConditioner(startedAt, endedAt, req.LimitCount)..., - ) - if err != nil { - return err - } - // TODO: apply field selector - for _, doc := range queryDocs { - filteredDoc, err := r.filterDocWithSelectedFields(doc, req.FieldSelectors) - if err != nil { - return err - } - - sendingCount = sendingCount + 1 - if err := stream.Send(&apppb.GetDocumentResponse{ - XMetadata: documentsmodel.NewPbDocumentMetadata(doc), - Data: filteredDoc, - }); err != nil { - return err - } - } - // update startedAt to be the last record of previous query - if len(queryDocs) > 0 { - startedAt = timestamppb.New(queryDocs[len(queryDocs)-1].CreatedAt) - } - - // take a nap and would query again - select { - case <-ctx.Done(): - r.log.Info("query is done as ctx is done\n") - return nil - case <-time.After(1 * time.Second): - } - } - return nil -} - -func makeQueryConditioner(startedAt *timestamppb.Timestamp, endedAt *timestamppb.Timestamp, limitCount *int32) []documentsstorage.QueryConditioner { - var cnds []documentsstorage.QueryConditioner - if startedAt != nil { - cnds = append(cnds, documentsstorage.WithStartedAt(startedAt.AsTime())) - } - if endedAt != nil { - cnds = append(cnds, documentsstorage.WithEndedAt(endedAt.AsTime())) - } - if limitCount != nil { - cnds = append(cnds, documentsstorage.WithLimitCount(*limitCount)) - } - return cnds -} - -func (r *RestColServiceServerService) QueryDocument(ctx context.Context, req *apppb.QueryDocumentRequest) (*apppb.QueryDocumentResponse, error) { - projectId, err := r.getProjectIdFromCtx(ctx) - if err != nil { - return nil, err - } - cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) - queryDocs, err := r.documentCURD.Query( - ctx, - "", - projectId, - cid, - makeQueryConditioner(req.SinceTs, req.EndedAt, req.LimitCount)..., - ) - if err != nil { - return nil, err - } - resp := &apppb.QueryDocumentResponse{} - for _, doc := range queryDocs { - filteredDocBytes, err := r.filterDocWithSelectedFields(doc, req.FieldSelectors) - if err != nil { - return nil, err - } - resp.Docs = append(resp.Docs, &apppb.GetDocumentResponse{ - XMetadata: documentsmodel.NewPbDocumentMetadata(doc), - Data: filteredDocBytes, - }) - } - return resp, nil - } diff --git a/pkg/app/collections.go b/pkg/app/collections.go new file mode 100644 index 0000000..85eea37 --- /dev/null +++ b/pkg/app/collections.go @@ -0,0 +1,152 @@ +package app + +import ( + "context" + "errors" + "fmt" + + sderrors "github.com/sdinsure/agent/pkg/errors" + + apppb "github.com/footprintai/restcol/api/pb" + collectionsmodel "github.com/footprintai/restcol/pkg/models/collections" + projectsmodel "github.com/footprintai/restcol/pkg/models/projects" +) + +// CreateCollection creates a new collection under the caller's project. +func (r *RestColServiceServerService) CreateCollection(ctx context.Context, req *apppb.CreateCollectionRequest) (*apppb.CreateCollectionResponse, error) { + projectId, err := r.getProjectIdFromCtx(ctx) + if err != nil { + return nil, err + } + cid := collectionsmodel.NewCollectionID() + if req.CollectionId != nil { + cid = collectionsmodel.NewCollectionIDFromStr(*req.CollectionId) + } + collectionType := apppb.CollectionType_COLLECTION_TYPE_REGULAR_FILES + if req.CollectionType != nil { + collectionType = *req.CollectionType + } + var summary string + if req.Description != nil { + summary = *req.Description + } + var modelSchemaSlice []*collectionsmodel.ModelSchema + if len(req.Schemas) > 0 { + modelSchema, _ := collectionsmodel.NewModelSchema(req.Schemas) + modelSchemaSlice = append(modelSchemaSlice, modelSchema) + } + + mc := collectionsmodel.NewModelCollection( + projectId, + cid, + collectionType, + summary, + modelSchemaSlice, + ) + if err := r.collectionCURD.Write(ctx, "", &mc); err != nil { + return nil, err + } + + resp := &apppb.CreateCollectionResponse{ + XMetadata: collectionsmodel.NewPbCollectionMetadata(&mc), + Description: mc.Summary, + CollectionType: mc.Type.Proto(), + } + if len(mc.Schemas) > 0 { + resp.Schemas = collectionsmodel.NewPbSchemaFields(mc.Schemas[0]) + } + return resp, nil +} + +// ListCollections is not yet implemented. +func (r *RestColServiceServerService) ListCollections(ctx context.Context, req *apppb.ListCollectionsRequest) (*apppb.ListCollectionsResponse, error) { + return nil, sderrors.NewNotImplError(errors.New("not implemented")) +} + +// GetCollection returns metadata plus the latest schema for the requested +// collection. A not-found is mapped to an empty response (not an error) to +// match existing client expectations. +func (r *RestColServiceServerService) GetCollection(ctx context.Context, req *apppb.GetCollectionRequest) (*apppb.GetCollectionResponse, error) { + if len(req.CollectionId) == 0 { + return nil, sderrors.NewBadParamsError(errors.New("missing required field")) + } + projectId, err := r.getProjectIdFromCtx(ctx) + if err != nil { + return nil, err + } + cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) + mc, err := r.collectionCURD.GetLatestSchema(ctx, "", projectId, cid) + if err != nil { + if ismyerr, myerr := sderrors.As(err); ismyerr && myerr.Code() == sderrors.CodeNotFound { + return &apppb.GetCollectionResponse{}, nil + } + return nil, err + } + resp := &apppb.GetCollectionResponse{ + XMetadata: collectionsmodel.NewPbCollectionMetadata(mc), + } + if mc == nil { + return resp, nil + } + resp.Description = mc.Summary + resp.CollectionType = mc.Type.Proto() + if mc.Schemas != nil { + resp.Schemas = collectionsmodel.NewPbSchemaFields(mc.Schemas[0]) + } + return resp, nil +} + +// DeleteCollection soft-deletes a collection. If the collection still contains +// documents the call fails with FailedPrecondition unless req.Force is true, in +// which case all documents in the collection are soft-deleted first. +func (r *RestColServiceServerService) DeleteCollection(ctx context.Context, req *apppb.DeleteCollectionRequest) (*apppb.DeleteCollectionResponse, error) { + if len(req.CollectionId) == 0 { + return nil, sderrors.NewBadParamsError(errors.New("collection_id is required")) + } + projectId, err := r.getProjectIdFromCtx(ctx) + if err != nil { + return nil, err + } + cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) + + if _, err := r.collectionCURD.Get(ctx, "", projectId, cid, collectionsmodel.NullSchemaID); err != nil { + return nil, err + } + + count, err := r.documentCURD.CountByCollection(ctx, "", projectId, cid) + if err != nil { + return nil, err + } + if count > 0 && !req.Force { + return nil, sderrors.NewStatusConflicted(fmt.Errorf("collection %s contains %d documents; pass force=true to cascade-delete", cid.String(), count)) + } + if count > 0 { + if err := r.documentCURD.DeleteByCollection(ctx, "", projectId, cid); err != nil { + return nil, err + } + } + if err := r.collectionCURD.Delete(ctx, "", projectId, cid); err != nil { + return nil, err + } + return &apppb.DeleteCollectionResponse{}, nil +} + +// getCollectionIDFromSchemas auto-provisions a collection when a document is +// written without an explicit collection ID. +// +// TODO: match incoming data against existing collections by schema similarity +// instead of always creating a new collection. +func (r *RestColServiceServerService) getCollectionIDFromSchemas(ctx context.Context, projectId projectsmodel.ProjectID) (collectionsmodel.CollectionID, error) { + cid := collectionsmodel.NewCollectionID() + mc := collectionsmodel.NewModelCollection( + projectId, + cid, + apppb.CollectionType_COLLECTION_TYPE_REGULAR_FILES, + "auto created collection", + []*collectionsmodel.ModelSchema{}, + ) + if err := r.collectionCURD.Write(ctx, "", &mc); err != nil { + return collectionsmodel.CollectionID(""), err + } + return cid, nil +} diff --git a/pkg/app/documents.go b/pkg/app/documents.go new file mode 100644 index 0000000..8407e71 --- /dev/null +++ b/pkg/app/documents.go @@ -0,0 +1,317 @@ +package app + +import ( + "context" + "errors" + "fmt" + "strings" + "time" + + sderrors "github.com/sdinsure/agent/pkg/errors" + "google.golang.org/protobuf/types/known/structpb" + "google.golang.org/protobuf/types/known/timestamppb" + + apppb "github.com/footprintai/restcol/api/pb" + collectionsmodel "github.com/footprintai/restcol/pkg/models/collections" + documentsmodel "github.com/footprintai/restcol/pkg/models/documents" + schemafinder "github.com/footprintai/restcol/pkg/schema" + documentsstorage "github.com/footprintai/restcol/pkg/storage/documents" +) + +// CreateDocument writes a document, auto-detecting its schema against the +// collection's latest known schema. When no collection is supplied, one is +// provisioned on the fly via getCollectionIDFromSchemas. +func (r *RestColServiceServerService) CreateDocument(ctx context.Context, req *apppb.CreateDocumentRequest) (*apppb.CreateDocumentResponse, error) { + projectId, err := r.getProjectIdFromCtx(ctx) + if err != nil { + return nil, err + } + var cid collectionsmodel.CollectionID + if req.CollectionId != "" { + cid = collectionsmodel.NewCollectionIDFromStr(req.CollectionId) + } else { + cid, err = r.getCollectionIDFromSchemas(ctx, projectId) + if err != nil { + return nil, err + } + } + + modelCollection, err := r.collectionCURD.GetLatestSchema(ctx, "", projectId, cid) + if err != nil { + return nil, err + } + + _, inputDataSchema, valueHolder, err := r.schemaBuilder.Parse(req.Data) + if err != nil { + r.log.Error("failed to convert into modelschema, err:%+v\n", err) + return nil, err + } + + // Reuse the existing schema when it matches; otherwise attach the newly + // parsed schema so schema evolution is tracked per-document. + var docSchema *collectionsmodel.ModelSchema + if len(modelCollection.Schemas) == 0 { + docSchema = inputDataSchema + } else if r.schemaBuilder.Equals(modelCollection.Schemas[0], inputDataSchema) { + docSchema = modelCollection.Schemas[0] + } else { + docSchema = inputDataSchema + } + + var docId documentsmodel.DocumentID + if req.DocumentId == nil { + docId = documentsmodel.NewDocumentID() + } else { + docId, err = documentsmodel.Parse(*req.DocumentId) + if err != nil { + return nil, err + } + } + + docModel := &documentsmodel.ModelDocument{ + ID: docId, + Data: documentsmodel.NewModelDocumentData(valueHolder), + ModelCollectionID: cid, + ModelCollection: collectionsmodel.NewModelCollection( + projectId, + cid, + apppb.CollectionType_COLLECTION_TYPE_REGULAR_FILES, + "auto created collection", + []*collectionsmodel.ModelSchema{docSchema}, + ), + ModelProjectID: projectId, + } + if err := r.documentCURD.Write(ctx, "", docModel); err != nil { + r.log.Error("failed to write docmodel, err:%+v\n", err) + return nil, err + } + return &apppb.CreateDocumentResponse{ + XMetadata: documentsmodel.NewPbDocumentMetadata(docModel), + }, nil +} + +// GetDocument returns a single document, optionally filtered to selected +// fields. A mismatch between the request's (project, collection) scope and the +// stored document surfaces as NotFound — callers never leak cross-tenant data. +func (r *RestColServiceServerService) GetDocument(ctx context.Context, req *apppb.GetDocumentRequest) (*apppb.GetDocumentResponse, error) { + if req.CollectionId == "" { + return nil, sderrors.NewBadParamsError(errors.New("collection_id is required")) + } + if req.DocumentId == "" { + return nil, sderrors.NewBadParamsError(errors.New("document_id is required")) + } + projectId, err := r.getProjectIdFromCtx(ctx) + if err != nil { + return nil, err + } + cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) + did, err := documentsmodel.Parse(req.DocumentId) + if err != nil { + return nil, sderrors.NewBadParamsError(fmt.Errorf("invalid document_id: %w", err)) + } + docModel, err := r.documentCURD.Get(ctx, "", projectId, cid, did) + if err != nil { + return nil, err + } + // Storage .Get returns an empty record (no error) for soft-deleted or + // scope-mismatched rows. Convert that into NotFound so the handler cannot + // leak a blank response under the wrong project/collection. + if docModel.ID.String() == "" { + return nil, sderrors.NewNotFoundError(fmt.Errorf("document %s not found in collection %s", did.String(), cid.String())) + } + filteredDoc, err := r.filterDocWithSelectedFields(docModel, req.FieldSelectors) + if err != nil { + return nil, err + } + return &apppb.GetDocumentResponse{ + XMetadata: documentsmodel.NewPbDocumentMetadata(docModel), + Data: filteredDoc, + }, nil +} + +// DeleteDocument removes the named document; returns NotFound if it does not +// exist in the given (project, collection) scope. +func (r *RestColServiceServerService) DeleteDocument(ctx context.Context, req *apppb.DeleteDocumentRequest) (*apppb.DeleteDocumentResponse, error) { + projectId, err := r.getProjectIdFromCtx(ctx) + if err != nil { + return nil, err + } + if req.CollectionId == "" { + return nil, sderrors.NewBadParamsError(errors.New("collection_id is required")) + } + if req.DocumentId == "" { + return nil, sderrors.NewBadParamsError(errors.New("document_id is required")) + } + cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) + did, err := documentsmodel.Parse(req.DocumentId) + if err != nil { + return nil, sderrors.NewBadParamsError(fmt.Errorf("invalid document_id: %w", err)) + } + + if _, err := r.documentCURD.Get(ctx, "", projectId, cid, did); err != nil { + return nil, err + } + if err := r.documentCURD.Delete(ctx, "", projectId, cid, did); err != nil { + return nil, err + } + return &apppb.DeleteDocumentResponse{}, nil +} + +// QueryDocument returns a page of documents matching the request's time range +// and limit. +func (r *RestColServiceServerService) QueryDocument(ctx context.Context, req *apppb.QueryDocumentRequest) (*apppb.QueryDocumentResponse, error) { + projectId, err := r.getProjectIdFromCtx(ctx) + if err != nil { + return nil, err + } + cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) + queryDocs, err := r.documentCURD.Query( + ctx, + "", + projectId, + cid, + makeQueryConditioner(req.SinceTs, req.EndedAt, req.LimitCount)..., + ) + if err != nil { + return nil, err + } + resp := &apppb.QueryDocumentResponse{} + for _, doc := range queryDocs { + filteredDoc, err := r.filterDocWithSelectedFields(doc, req.FieldSelectors) + if err != nil { + return nil, err + } + resp.Docs = append(resp.Docs, &apppb.GetDocumentResponse{ + XMetadata: documentsmodel.NewPbDocumentMetadata(doc), + Data: filteredDoc, + }) + } + return resp, nil +} + +// QueryDocumentsStream streams matching documents. In follow-up mode it keeps +// polling until the client cancels the context. +func (r *RestColServiceServerService) QueryDocumentsStream(req *apppb.QueryDocumentStreamRequest, stream apppb.RestColService_QueryDocumentsStreamServer) error { + ctx := stream.Context() + projectId, err := r.getProjectIdFromCtx(ctx) + if err != nil { + return err + } + cid := collectionsmodel.NewCollectionIDFromStr(req.CollectionId) + startedAt := req.SinceTs + endedAt := req.EndedAt + + needsFollowUp := req.FollowUpMode != nil && *req.FollowUpMode + + if !needsFollowUp { + queryDocs, err := r.documentCURD.Query( + ctx, + "", + projectId, + cid, + makeQueryConditioner(startedAt, endedAt, req.LimitCount)..., + ) + if err != nil { + return err + } + for _, doc := range queryDocs { + filteredDoc, err := r.filterDocWithSelectedFields(doc, req.FieldSelectors) + if err != nil { + return err + } + if err := stream.Send(&apppb.GetDocumentResponse{ + XMetadata: documentsmodel.NewPbDocumentMetadata(doc), + Data: filteredDoc, + }); err != nil { + return err + } + } + return nil + } + + for { + r.log.Info("query with time range[%+v -> %+v], cid:%+v\n", startedAt.AsTime(), endedAt.AsTime(), cid) + queryDocs, err := r.documentCURD.Query( + ctx, + "", + projectId, + cid, + makeQueryConditioner(startedAt, endedAt, req.LimitCount)..., + ) + if err != nil { + return err + } + for _, doc := range queryDocs { + filteredDoc, err := r.filterDocWithSelectedFields(doc, req.FieldSelectors) + if err != nil { + return err + } + if err := stream.Send(&apppb.GetDocumentResponse{ + XMetadata: documentsmodel.NewPbDocumentMetadata(doc), + Data: filteredDoc, + }); err != nil { + return err + } + } + if len(queryDocs) > 0 { + startedAt = timestamppb.New(queryDocs[len(queryDocs)-1].CreatedAt) + } + select { + case <-ctx.Done(): + r.log.Info("query is done as ctx is done\n") + return nil + case <-time.After(1 * time.Second): + } + } +} + +// filterDocWithSelectedFields returns either the full document value or a +// pruned view containing only the field paths named in selectedFields. +func (r *RestColServiceServerService) filterDocWithSelectedFields(doc *documentsmodel.ModelDocument, selectedFields []string) (*structpb.Value, error) { + r.log.Info("query doc with fields:%+v\n", selectedFields) + + if doc.Data == nil { + return nil, nil + } + if len(selectedFields) == 0 { + return structpb.NewValue(doc.Data.MapValue) + } + + modelSchema, err := r.schemaBuilder.Flatten(doc.Data.MapValue) + if err != nil { + return nil, err + } + + lookup := make(map[string]struct{}, len(selectedFields)) + for _, f := range selectedFields { + lookup[strings.ToLower(f)] = struct{}{} + } + + var fieldsInSelected []*collectionsmodel.ModelFieldSchema + for _, dataField := range modelSchema.Fields { + if _, ok := lookup[dataField.FieldName.String()]; ok { + fieldsInSelected = append(fieldsInSelected, dataField) + } + } + structWithSelectedFields, err := schemafinder.Build(fieldsInSelected) + if err != nil { + return nil, err + } + return structpb.NewValue(structWithSelectedFields) +} + +// makeQueryConditioner translates optional proto filters into the storage +// layer's variadic query options. +func makeQueryConditioner(startedAt *timestamppb.Timestamp, endedAt *timestamppb.Timestamp, limitCount *int32) []documentsstorage.QueryConditioner { + var cnds []documentsstorage.QueryConditioner + if startedAt != nil { + cnds = append(cnds, documentsstorage.WithStartedAt(startedAt.AsTime())) + } + if endedAt != nil { + cnds = append(cnds, documentsstorage.WithEndedAt(endedAt.AsTime())) + } + if limitCount != nil { + cnds = append(cnds, documentsstorage.WithLimitCount(*limitCount)) + } + return cnds +} diff --git a/pkg/app/handlers_test.go b/pkg/app/handlers_test.go new file mode 100644 index 0000000..c0175c5 --- /dev/null +++ b/pkg/app/handlers_test.go @@ -0,0 +1,274 @@ +package app + +import ( + "context" + "testing" + + sderrors "github.com/sdinsure/agent/pkg/errors" + "github.com/sdinsure/agent/pkg/logger" + sdinsureruntime "github.com/sdinsure/agent/pkg/runtime" + storagetestutils "github.com/sdinsure/agent/pkg/storage/testutils" + "github.com/stretchr/testify/assert" + + apppb "github.com/footprintai/restcol/api/pb" + collectionsmodel "github.com/footprintai/restcol/pkg/models/collections" + documentsmodel "github.com/footprintai/restcol/pkg/models/documents" + projectsmodel "github.com/footprintai/restcol/pkg/models/projects" + schemafinder "github.com/footprintai/restcol/pkg/schema" + collectionsstorage "github.com/footprintai/restcol/pkg/storage/collections" + documentsstorage "github.com/footprintai/restcol/pkg/storage/documents" + projectsstorage "github.com/footprintai/restcol/pkg/storage/projects" +) + +// fixedProjectResolver implements sdinsureruntime.ProjectResolver for tests: +// every call returns the configured project, so handlers see a stable tenant. +type fixedProjectResolver struct { + pid projectsmodel.ProjectID +} + +func (f *fixedProjectResolver) WithProjectInfo(ctx context.Context, _ string) context.Context { + return ctx +} + +func (f *fixedProjectResolver) ProjectInfo(_ context.Context) (sdinsureruntime.ProjectInfor, bool) { + return &fixedProjectInfor{pid: f.pid}, true +} + +type fixedProjectInfor struct { + pid projectsmodel.ProjectID +} + +func (f *fixedProjectInfor) GetProjectID() (string, error) { return f.pid.String(), nil } +func (f *fixedProjectInfor) GetProject(any) error { return nil } + +// newTestService spins up a full handler against a real postgres. Tests using +// it must be guarded with testing.Short() — CI runs -short. +func newTestService(t *testing.T) (*RestColServiceServerService, *projectsmodel.ModelProject) { + t.Helper() + log := logger.NewLogger() + db, err := storagetestutils.NewTestPostgresCli(log) + assert.NoError(t, err) + + projectCURD := projectsstorage.NewProjectCURD(db) + assert.NoError(t, projectCURD.AutoMigrate()) + collectionCURD := collectionsstorage.NewCollectionCURD(db) + assert.NoError(t, collectionCURD.AutoMigrate()) + documentCURD := documentsstorage.NewDocumentCURD(db) + assert.NoError(t, documentCURD.AutoMigrate()) + + proj := &projectsmodel.ModelProject{ + ID: projectsmodel.NewProjectID(9001), + Type: projectsmodel.RegularProjectType, + } + assert.NoError(t, projectCURD.Write(context.Background(), "", proj)) + + svc := NewRestColServiceServerService(log, collectionCURD, documentCURD, schemafinder.NewSchemaBuilder(log)) + svc.SetDefaultProjectResolver(&fixedProjectResolver{pid: proj.ID}) + return svc, proj +} + +func assertErrorCode(t *testing.T, err error, want sderrors.Code) { + t.Helper() + if !assert.Error(t, err) { + return + } + ok, myerr := sderrors.As(err) + if !assert.True(t, ok, "expected sderrors.Error, got %T: %v", err, err) { + return + } + assert.Equal(t, want, myerr.Code(), "error code mismatch; message: %s", myerr.Error()) +} + +func TestDeleteCollection_ValidationAndExistence(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } + svc, _ := newTestService(t) + ctx := context.Background() + + _, err := svc.DeleteCollection(ctx, &apppb.DeleteCollectionRequest{CollectionId: ""}) + assertErrorCode(t, err, sderrors.CodeBadParameters) + + _, err = svc.DeleteCollection(ctx, &apppb.DeleteCollectionRequest{ + CollectionId: collectionsmodel.NewCollectionID().String(), + }) + assertErrorCode(t, err, sderrors.CodeNotFound) +} + +func TestDeleteCollection_EmptyCollectionSucceeds(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } + svc, _ := newTestService(t) + ctx := context.Background() + + created, err := svc.CreateCollection(ctx, &apppb.CreateCollectionRequest{ + Description: strPtr("empty-coll"), + }) + assert.NoError(t, err) + cid := created.XMetadata.CollectionId + + _, err = svc.DeleteCollection(ctx, &apppb.DeleteCollectionRequest{CollectionId: cid}) + assert.NoError(t, err) +} + +func TestDeleteCollection_NonEmptyRejectedWithoutForce(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } + svc, _ := newTestService(t) + ctx := context.Background() + + created, err := svc.CreateCollection(ctx, &apppb.CreateCollectionRequest{Description: strPtr("non-empty")}) + assert.NoError(t, err) + cid := created.XMetadata.CollectionId + + _, err = svc.CreateDocument(ctx, &apppb.CreateDocumentRequest{ + CollectionId: cid, + Data: []byte(`{"k":"v"}`), + }) + assert.NoError(t, err) + + _, err = svc.DeleteCollection(ctx, &apppb.DeleteCollectionRequest{CollectionId: cid, Force: false}) + assertErrorCode(t, err, sderrors.CodeStatusConflicted) +} + +func TestDeleteCollection_ForceCascades(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } + svc, _ := newTestService(t) + ctx := context.Background() + + created, err := svc.CreateCollection(ctx, &apppb.CreateCollectionRequest{Description: strPtr("cascade")}) + assert.NoError(t, err) + cid := created.XMetadata.CollectionId + + createdDoc, err := svc.CreateDocument(ctx, &apppb.CreateDocumentRequest{ + CollectionId: cid, + Data: []byte(`{"k":"v"}`), + }) + assert.NoError(t, err) + did := createdDoc.XMetadata.DocumentId + + _, err = svc.DeleteCollection(ctx, &apppb.DeleteCollectionRequest{CollectionId: cid, Force: true}) + assert.NoError(t, err) + + // Document should no longer be retrievable after cascade. + _, err = svc.GetDocument(ctx, &apppb.GetDocumentRequest{CollectionId: cid, DocumentId: did}) + assertErrorCode(t, err, sderrors.CodeNotFound) +} + +func TestGetDocument_ValidationAndScope(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } + svc, _ := newTestService(t) + ctx := context.Background() + + cases := []struct { + name string + req *apppb.GetDocumentRequest + code sderrors.Code + }{ + { + name: "missing collection id", + req: &apppb.GetDocumentRequest{DocumentId: documentsmodel.NewDocumentID().String()}, + code: sderrors.CodeBadParameters, + }, + { + name: "missing document id", + req: &apppb.GetDocumentRequest{CollectionId: collectionsmodel.NewCollectionID().String()}, + code: sderrors.CodeBadParameters, + }, + { + name: "malformed document id", + req: &apppb.GetDocumentRequest{ + CollectionId: collectionsmodel.NewCollectionID().String(), + DocumentId: "not-a-valid-doc-id", + }, + code: sderrors.CodeBadParameters, + }, + } + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + _, err := svc.GetDocument(ctx, tc.req) + assertErrorCode(t, err, tc.code) + }) + } +} + +func TestGetDocument_WrongCollectionScopeReturnsNotFound(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } + svc, _ := newTestService(t) + ctx := context.Background() + + // Create two collections and a document in the first. + c1, err := svc.CreateCollection(ctx, &apppb.CreateCollectionRequest{Description: strPtr("c1")}) + assert.NoError(t, err) + c2, err := svc.CreateCollection(ctx, &apppb.CreateCollectionRequest{Description: strPtr("c2")}) + assert.NoError(t, err) + + doc, err := svc.CreateDocument(ctx, &apppb.CreateDocumentRequest{ + CollectionId: c1.XMetadata.CollectionId, + Data: []byte(`{"k":"v"}`), + }) + assert.NoError(t, err) + + // Reading the doc under the wrong collection must NOT leak an empty + // response — it must surface as NotFound. + _, err = svc.GetDocument(ctx, &apppb.GetDocumentRequest{ + CollectionId: c2.XMetadata.CollectionId, + DocumentId: doc.XMetadata.DocumentId, + }) + assertErrorCode(t, err, sderrors.CodeNotFound) + + // And the happy path still works. + got, err := svc.GetDocument(ctx, &apppb.GetDocumentRequest{ + CollectionId: c1.XMetadata.CollectionId, + DocumentId: doc.XMetadata.DocumentId, + }) + assert.NoError(t, err) + assert.Equal(t, doc.XMetadata.DocumentId, got.XMetadata.DocumentId) +} + +func TestDeleteDocument_Validation(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } + svc, _ := newTestService(t) + ctx := context.Background() + + _, err := svc.DeleteDocument(ctx, &apppb.DeleteDocumentRequest{ + CollectionId: "", + DocumentId: documentsmodel.NewDocumentID().String(), + }) + assertErrorCode(t, err, sderrors.CodeBadParameters) + + _, err = svc.DeleteDocument(ctx, &apppb.DeleteDocumentRequest{ + CollectionId: collectionsmodel.NewCollectionID().String(), + DocumentId: "", + }) + assertErrorCode(t, err, sderrors.CodeBadParameters) + + _, err = svc.DeleteDocument(ctx, &apppb.DeleteDocumentRequest{ + CollectionId: collectionsmodel.NewCollectionID().String(), + DocumentId: "not-a-valid-doc-id", + }) + assertErrorCode(t, err, sderrors.CodeBadParameters) +} + +func TestGetCollection_MissingIDRejected(t *testing.T) { + if testing.Short() { + t.Skip("requires a local postgres; skipping under -short") + } + svc, _ := newTestService(t) + ctx := context.Background() + + _, err := svc.GetCollection(ctx, &apppb.GetCollectionRequest{CollectionId: ""}) + assertErrorCode(t, err, sderrors.CodeBadParameters) +} + +func strPtr(s string) *string { return &s } diff --git a/pkg/bootstrap/default_project.go b/pkg/bootstrap/default_project.go new file mode 100644 index 0000000..988789f --- /dev/null +++ b/pkg/bootstrap/default_project.go @@ -0,0 +1,43 @@ +// Package bootstrap seeds the database with the baseline records required for +// the server to start. It currently provisions the default project that backs +// anonymous authentication so requests without an explicit project have a +// tenant to attach to. +package bootstrap + +import ( + "context" + + projectsmodel "github.com/footprintai/restcol/pkg/models/projects" + projectsstorage "github.com/footprintai/restcol/pkg/storage/projects" +) + +// DefaultModelProject is the project used as the fallback tenant for anonymous +// requests. Its ID is stable so swagger/OpenAPI URLs like +// /v1/projects/1001/apidoc remain valid across restarts. +var DefaultModelProject = projectsmodel.ModelProject{ + ID: projectsmodel.NewProjectID(1001), + Type: projectsmodel.ProxyProjectType, +} + +// DefaultProject upserts and retrieves DefaultModelProject. +type DefaultProject struct { + projectcurd *projectsstorage.ProjectCURD +} + +// NewDefaultProject wires a DefaultProject against the given storage handle. +func NewDefaultProject(projectcurd *projectsstorage.ProjectCURD) *DefaultProject { + return &DefaultProject{ + projectcurd: projectcurd, + } +} + +// Init persists DefaultModelProject so downstream code can resolve it by ID. +// Safe to call on every startup. +func (d *DefaultProject) Init(ctx context.Context) error { + return d.projectcurd.Write(ctx, "", &DefaultModelProject) +} + +// GetProject returns the project record identified by pid. +func (d *DefaultProject) GetProject(ctx context.Context, pid projectsmodel.ProjectID) (*projectsmodel.ModelProject, error) { + return d.projectcurd.Get(ctx, "", pid) +} diff --git a/pkg/dummy/project.go b/pkg/dummy/project.go deleted file mode 100644 index 9917ff1..0000000 --- a/pkg/dummy/project.go +++ /dev/null @@ -1,34 +0,0 @@ -package dummy - -import ( - "context" - - projectsmodel "github.com/footprintai/restcol/pkg/models/projects" - projectsstorage "github.com/footprintai/restcol/pkg/storage/projects" -) - -var ( - DummyModelProject = projectsmodel.ModelProject{ - ID: projectsmodel.NewProjectID(1001), - Type: projectsmodel.ProxyProjectType, - } -) - -type DummyProject struct { - projectcurd *projectsstorage.ProjectCURD -} - -func NewDummyProject(projectcurd *projectsstorage.ProjectCURD) *DummyProject { - return &DummyProject{ - projectcurd: projectcurd, - } -} - -func (d *DummyProject) Init(ctx context.Context) error { - return d.projectcurd.Write(ctx, "", &DummyModelProject) -} - -func (d *DummyProject) GetProject(ctx context.Context, pid projectsmodel.ProjectID) (*projectsmodel.ModelProject, error) { - docModel, err := d.projectcurd.Get(ctx, "", pid) - return docModel, err -} diff --git a/integrationtest/server/server.go b/pkg/server/app/app.go similarity index 87% rename from integrationtest/server/server.go rename to pkg/server/app/app.go index 9c7ff3d..ab931ba 100644 --- a/integrationtest/server/server.go +++ b/pkg/server/app/app.go @@ -1,4 +1,6 @@ -package integrationtestserver +// Package serverapp wires together authentication, authorization, storage, +// and the gRPC/HTTP gateway to produce a runnable restcol server. +package serverapp import ( "context" @@ -10,7 +12,7 @@ import ( appapp "github.com/footprintai/restcol/pkg/app" appauthn "github.com/footprintai/restcol/pkg/authn" appauthz "github.com/footprintai/restcol/pkg/authz" - dummy "github.com/footprintai/restcol/pkg/dummy" + bootstrap "github.com/footprintai/restcol/pkg/bootstrap" appmiddleware "github.com/footprintai/restcol/pkg/middleware" runtimeprojectgetter "github.com/footprintai/restcol/pkg/runtime/getter" schemafinder "github.com/footprintai/restcol/pkg/schema" @@ -72,8 +74,8 @@ func makeServerService( if err := documentCURD.AutoMigrate(); err != nil { return nil, err } - dummyProject := dummy.NewDummyProject(projectCURD) - if err := dummyProject.Init(context.Background()); err != nil { + defaultProject := bootstrap.NewDefaultProject(projectCURD) + if err := defaultProject.Init(context.Background()); err != nil { return nil, err } @@ -84,20 +86,9 @@ func makeServerService( &appauthn.AnnonymousClaimParser{}, authnmiddleware.EnableAnnonymous(true), ) - //authnMiddleware := grpc_auth.UnaryServerInterceptor(.AuthFunc) authZMiddleware := authzmiddleware.NewAuthZMiddleware( log, appmiddleware.NewAuthzMiddlwareAdaptor(&appauthz.AllowEveryOne{}), - //middleware.WithSkippedAuthZPaths([]middleware.HttpPath{ - // middleware.HttpPath{ - // RawPath: "/v1/login", - // RawMethod: http.MethodPost, - // }, - // middleware.HttpPath{ - // RawPath: "/v1/user", - // RawMethod: http.MethodPost, - // }, - //}), ) projectIdentityMiddleware := identitymiddleware.NewProjectIdentityMiddleware(projectResolver) @@ -144,12 +135,12 @@ type Server struct { httpPort int } -// Start starts Server and blocks forever +// Start starts the server and blocks until Stop is called. func (s *Server) Start() error { return s.server.Start() } -// Stop stops server +// Stop terminates the server. func (s *Server) Stop() error { return s.server.Stop() } diff --git a/pkg/storage/collections/collections.go b/pkg/storage/collections/collections.go index 52ee3c4..3be5f7f 100644 --- a/pkg/storage/collections/collections.go +++ b/pkg/storage/collections/collections.go @@ -67,6 +67,15 @@ func (c *CollectionCURD) ListByProjectID(ctx context.Context, tableName string, return cs, storage.WrapStorageError(err) } +// Delete soft-deletes the named collection. Associated schemas remain because +// the Schemas relationship is declared with OnDelete:SET NULL. +func (c *CollectionCURD) Delete(ctx context.Context, tableName string, pid appmodelprojects.ProjectID, cid appmodelcollections.CollectionID) error { + err := c.With(ctx, tableName). + Where("id = ? AND model_project_id = ?", cid.String(), pid.String()). + Delete(&appmodelcollections.ModelCollection{}).Error + return storage.WrapStorageError(err) +} + func (c *CollectionCURD) Get(ctx context.Context, tableName string, pid appmodelprojects.ProjectID, cid appmodelcollections.CollectionID, sid appmodelcollections.SchemaID) (*appmodelcollections.ModelCollection, error) { record := &appmodelcollections.ModelCollection{} db := c.With(ctx, tableName) diff --git a/pkg/storage/documents/documents.go b/pkg/storage/documents/documents.go index 2e40293..eb5c762 100644 --- a/pkg/storage/documents/documents.go +++ b/pkg/storage/documents/documents.go @@ -160,7 +160,29 @@ func (c *DocumentCURD) Query(ctx context.Context, } func (c *DocumentCURD) Delete(ctx context.Context, tableName string, pid appmodelprojects.ProjectID, cid appmodelcollections.CollectionID, did appmodeldocuments.DocumentID) error { - err := c.With(ctx, tableName).Where("id = ? AND model_collection_id = ? AND model_project_id = ?", + err := c.With(ctx, tableName).Where("id = ? AND model_collection_id = ? AND model_project_id = ?", did.String(), cid.String(), pid.String()).Delete(&appmodeldocuments.ModelDocument{}).Error return storage.WrapStorageError(err) } + +// CountByCollection returns the number of active (non-soft-deleted) documents +// in the given collection. +func (c *DocumentCURD) CountByCollection(ctx context.Context, tableName string, pid appmodelprojects.ProjectID, cid appmodelcollections.CollectionID) (int64, error) { + var count int64 + err := c.With(ctx, tableName). + Model(&appmodeldocuments.ModelDocument{}). + Where("model_project_id = ? AND model_collection_id = ?", pid.String(), cid.String()). + Count(&count).Error + if err != nil { + return 0, storage.WrapStorageError(err) + } + return count, nil +} + +// DeleteByCollection soft-deletes every document in the given collection. +func (c *DocumentCURD) DeleteByCollection(ctx context.Context, tableName string, pid appmodelprojects.ProjectID, cid appmodelcollections.CollectionID) error { + err := c.With(ctx, tableName). + Where("model_project_id = ? AND model_collection_id = ?", pid.String(), cid.String()). + Delete(&appmodeldocuments.ModelDocument{}).Error + return storage.WrapStorageError(err) +}