# Design Documents

## Table of Contents

- [Design Documents](#design-documents)
  - [Table of Contents](#table-of-contents)
  - [Introduction](#introduction)
  - [Motivation or Background](#motivation-or-background)
  - [Detailed Design](#detailed-design)
    - [Goals and Non-goals](#goals-and-non-goals)
    - [Domain Model](#domain-model)
      - [Entity Design](#entity-design)
      - [Model Design](#model-design)
    - [Controller API Design](#controller-api-design)
      - [1) Set Resource Tags](#1-set-resource-tags)
      - [2) Get Resource Tags](#2-get-resource-tags)
      - [3) Search Resources by Tags (AND)](#3-search-resources-by-tags-and)
    - [Service API Design](#service-api-design)
    - [Repo API Design](#repo-api-design)
    - [Query Strategy](#query-strategy)
      - [Forward index (Resource -\> Tags)](#forward-index-resource---tags)
      - [Reverse index (Tags -\> Resource, AND)](#reverse-index-tags---resource-and)
    - [Error Handling and Idempotency](#error-handling-and-idempotency)
  - [Test Design](#test-design)
    - [Functional Tests](#functional-tests)
    - [Scenario Tests](#scenario-tests)
    - [Compatibility Tests](#compatibility-tests)
  - [Impacts \& Risks](#impacts--risks)
  - [Unresolved Questions](#unresolved-questions)

## Introduction

This document describes the current implementation of `Resource Tag Service` in this repository.
It reflects the APIs and behaviors already provided by the existing controller/service/repo layers, without introducing future-phase capabilities.

## Motivation or Background

The service provides a shared tag storage and query capability for resources.
Current implementation focuses on three core operations:

1. Set (replace) full tag set for a resource.
2. Get current tags by resource key.
3. Search resources by tag conditions with AND semantics.

This enables upstream modules to reuse a single storage/query path instead of implementing tag persistence and reverse lookup repeatedly.

## Detailed Design

### Goals and Non-goals

Goals:

1. Provide public HTTP APIs for `SetResourceTags`, `GetResourceTags`, and `SearchResources`.
2. Support forward lookup by `resource_type + resource_id`.
3. Support reverse lookup by `resource_type + tags` using AND semantics.
4. Keep service and repo interfaces simple for direct reuse and testing.

Non-goals:

1. No create-only API (`ALREADY_EXISTS` semantics is not implemented).
2. No delete API endpoint for resource tags.
3. No pagination or `total` in search response.
4. No patch-style writes (single-tag add/remove/upsert).

### Domain Model

- Resource unique key: `resource_type + resource_id`
- Tag model: `key-value`
- One resource can have at most one value for the same `tag_key`.

Example:

- Resource: `resource_type=user/user_group`, `resource_id=10001`
- Tags:
  - `env=prod`
  - `owner=team-a`

#### Entity Design

```go
package resourcetag

type ResourceType string

type Tag struct {
    Key   string `json:"key"`
    Value string `json:"value"`
}

type ResourceRef struct {
    ResourceType ResourceType `json:"resource_type"`
    ResourceID   string       `json:"resource_id"`
}
```

#### Model Design

```go
package resourcetag

type ResourceTagBinding struct {
    ID           uint64 `gorm:"primary_key;not null"`
    ResourceType string `gorm:"type:VARCHAR(64);not null;uniqueIndex:uk_resource_tag_key;index:idx_tag_reverse_lookup,priority:1"`
    ResourceID   string `gorm:"type:VARCHAR(255);not null;uniqueIndex:uk_resource_tag_key;index:idx_tag_reverse_lookup,priority:4"`
    TagKey       string `gorm:"type:VARCHAR(128);not null;uniqueIndex:uk_resource_tag_key;index:idx_tag_reverse_lookup,priority:2"`
    TagValue     string `gorm:"type:VARCHAR(255);not null;index:idx_tag_reverse_lookup,priority:3"`
}

func (ResourceTagBinding) TableName() string {
    return "resource_tag_bindings"
}
```

Model notes:

1. `uk_resource_tag_key(resource_type, resource_id, tag_key)` ensures one value per tag key under one resource.
2. `idx_tag_reverse_lookup(resource_type, tag_key, tag_value, resource_id)` supports reverse search.

### Controller API Design

All routes are public routes under `/api/v1/devops` with auth middleware.

#### 1) Set Resource Tags

- Method: `POST`
- Path: `/api/v1/devops/resource-tag/resources/tags`
- Description: Set full tag set for target resource. Existing tags are fully replaced.

Request (JSON):

```json
{
  "resource_type": "user/user_group",
  "resource_id": "10001",
  "tags": [
    {"key": "env", "value": "prod"},
    {"key": "owner", "value": "team-a"}
  ]
}
```

Response (JSON):

```json
{
  "errcode": "OK",
  "errmsg": "",
  "trace_id": "xxx"
}
```

#### 2) Get Resource Tags

- Method: `GET`
- Path: `/api/v1/devops/resource-tag/resources/tags`
- Description: Get current tags for target resource.
- Query:
  - `resource_type`: target resource type
  - `resource_id`: target resource id

Response (JSON):

```json
{
  "errcode": "OK",
  "errmsg": "",
  "trace_id": "xxx",
  "ref": {
    "resource_type": "user/user_group",
    "resource_id": "10001"
  },
  "tags": [
    {"key": "env", "value": "prod"},
    {"key": "owner", "value": "team-a"}
  ]
}
```

#### 3) Search Resources by Tags (AND)

- Method: `POST`
- Path: `/api/v1/devops/resource-tag/resources/search`
- Description: Search resources by `resource_type` and tag conditions (AND semantics).

Request (JSON):

```json
{
  "resource_type": "user/user_group",
  "tags": [
    {"key": "env", "value": "prod"},
    {"key": "owner", "value": "team-a"}
  ]
}
```

Response (JSON):

```json
{
  "errcode": "OK",
  "errmsg": "",
  "trace_id": "xxx",
  "items": [
    {"resource_type": "user/user_group", "resource_id": "10001"},
    {"resource_type": "user/user_group", "resource_id": "10003"}
  ]
}
```

### Service API Design

Current service API:

```go
type ResourceTagServiceAPI interface {
    SetResourceTags(ctx context.Context, ref resourcetag.ResourceRef, tags []resourcetag.Tag) error
    GetResourceTags(ctx context.Context, ref resourcetag.ResourceRef) ([]resourcetag.Tag, error)
    SearchResources(ctx context.Context, typ resourcetag.ResourceType, tags []resourcetag.Tag) ([]resourcetag.ResourceRef, error)
}
```

Service behavior:

1. `SetResourceTags` validates `ResourceRef` and `tags`, then runs repo write in one transaction.
2. `GetResourceTags` returns tags for one resource key.
3. `SearchResources` delegates AND-search query to repo.

### Repo API Design

Current repository API:

```go
type ResourceTagRepo interface {
    SetResourceTags(ctx context.Context, ref resourcetag.ResourceRef, tags []resourcetag.Tag) error
    GetResourceTags(ctx context.Context, ref resourcetag.ResourceRef) ([]resourcetag.Tag, error)

    SearchResources(ctx context.Context, typ resourcetag.ResourceType, tags []resourcetag.Tag) ([]resourcetag.ResourceRef, error)
}
```

Repository behavior:

1. `SetResourceTags` executes `DELETE old bindings -> INSERT new bindings` for one resource.
2. When `tags` is empty, `SetResourceTags` only deletes old bindings.
3. `GetResourceTags` queries by `(resource_type, resource_id)` and returns tags ordered by `tag_key ASC`.
4. `SearchResources` returns empty list directly when `tags` is empty.

### Query Strategy

#### Forward index (Resource -> Tags)

- SQL pattern: `WHERE resource_type = ? AND resource_id = ?`
- Sorting: `ORDER BY tag_key ASC`
- Index usage: prefix of `uk_resource_tag_key`

#### Reverse index (Tags -> Resource, AND)

Given tag conditions `[(k1,v1), (k2,v2), ... (kn,vn)]`:

1. Filter by `resource_type = ?`.
2. Build OR clauses: `(tag_key = k1 AND tag_value = v1) OR ...`.
3. `GROUP BY resource_type, resource_id`.
4. `HAVING COUNT(*) = n` to enforce AND semantics.
5. Sort result by `resource_type ASC, resource_id ASC`.

### Error Handling and Idempotency

1. `SetResourceTags` validates inputs and returns invalid argument for invalid `resource_ref` or invalid tag list.
2. `GetResourceTags` and `SearchResources` return repository errors directly when query fails.
3. Repeated `SetResourceTags` with the same input is idempotent in effect.
4. `SearchResources` with empty `tags` returns an empty list.
5. Length limits are primarily constrained by DB schema column types.

## Test Design

### Functional Tests

1. Service tests should cover `Set/Get/Search` happy paths and validation failures.
2. Repository tests should cover:
   - Set replace semantics (`DELETE -> INSERT`),
   - get order by `tag_key ASC`,
   - search AND semantics via `GROUP BY + HAVING COUNT(*) = n`,
   - empty tags search returns empty list.
3. Controller tests should cover 3 routes' request binding and response schema (`errcode/errmsg/trace_id`, `ref/tags`, `items`).

### Scenario Tests

1. Multi-resource and multi-tag combinations to verify AND semantics correctness.
2. Repeated set on the same resource to verify stable replace behavior.
3. Empty-tag overwrite scenario (`tags=[]`) to verify old bindings are cleared.

### Compatibility Tests

1. Verify migration `202604071200` creates/maintains `resource_tag_bindings` schema expected by repository queries.
2. Verify API contract compatibility for current three endpoints (`Set/Get/Search`) under `/api/v1/devops/resource-tag`.
3. No additional upgrade/downgrade compatibility scope is introduced in this phase.

## Impacts & Risks

Impacts:

1. Provides reusable resource tag set/get/search capability in one shared service.
2. Keeps upstream integration simple with a minimal, consistent API surface.

Risks:

1. Full-set overwrite (`DELETE -> INSERT`) may increase contention under concurrent writes to the same resource.
2. Reverse search may become expensive for hot tags and large tag cardinality.
3. Search response currently has no pagination, which may amplify read load for broad conditions.

## Unresolved Questions

1. Whether search API should add pagination and total count in a later phase.
2. Whether separate create-only and delete APIs are needed for upstream usage.
3. Whether future phases should add richer query modes (for example key-only or partial-match).
