Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
50 changes: 50 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -560,6 +560,56 @@ Create or replace a JSON Schema. Authentication required.
| `400` | Invalid JSON or invalid JSON Schema |
| `401` | Missing or invalid Bearer token |

**Example:**

```bash
curl -s -X PUT http://127.0.0.1:8080/schema/product.v1 \
-H "Authorization: Bearer $TOKEN" \
-H 'Content-Type: application/json' \
-d '{
"$schema": "http://json-schema.org/draft-07/schema#",
"$id": "product.v1",
"type": "object",
"properties": {
"name": {"type": "string"},
"price": {"type": "number", "minimum": 0}
},
"required": ["name", "price"]
}'
```

---

### `DELETE /schema/{schemaID}`

Permanently delete a JSON Schema. Requires `write` permission.

Existing entities stored under that schema ID are **not** affected. Future entity
writes will proceed without validation until a new template is uploaded.

**Path parameter:** `schemaID` — the schema identifier (e.g., `chat.v1`).

**Response:**

```json
{ "status": "ok" }
```

**Error responses:**

| HTTP code | Meaning |
|-----------|---------|
| `401` | Missing or invalid Bearer token |
| `403` | Token lacks `write` permission |
| `404` | Schema not found |

**Example:**

```bash
curl -s -X DELETE http://127.0.0.1:8080/schema/product.v1 \
-H "Authorization: Bearer $TOKEN"
```

---

## JSON Schema Templates
Expand Down
50 changes: 50 additions & 0 deletions api/openapi.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -661,6 +661,56 @@ paths:
schema:
$ref: "#/components/schemas/Error"

delete:
operationId: deleteSchema
summary: Delete a JSON Schema
description: |
Permanently delete a JSON Schema document by its ID. Requires `write`
permission.

Once deleted, entities that were previously validated against this schema
are unaffected (they remain stored), but future `PUT` requests to the
same `schema_id` will no longer be validated until a new schema is
uploaded. To update a schema instead of deleting it, use `PUT`.

Schema IDs must not contain `/`, `\`, or `..` sequences.
tags: [schemas]
security:
- BearerAuth: []
parameters:
- name: schemaID
in: path
required: true
description: Schema identifier (e.g., `product.v1`).
schema:
type: string
example: product.v1
responses:
"200":
description: Schema deleted successfully.
content:
application/json:
schema:
$ref: "#/components/schemas/StatusOK"
"401":
description: Missing or invalid Bearer token.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"403":
description: Token lacks `write` permission.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"
"404":
description: Schema not found.
content:
application/json:
schema:
$ref: "#/components/schemas/Error"

# ---------------------------------------------------------------------------
# API Keys
# ---------------------------------------------------------------------------
Expand Down
4 changes: 3 additions & 1 deletion api/proto/worker.proto
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,9 @@ message ProcessRequest {
string database_name = 1;
string entity_key = 2;
string schema_id = 3;
string operation = 4; // GET, PUT, or DELETE
// Entity operations: GET, PUT, DELETE
// Schema template operations: SCHEMA_GET, SCHEMA_PUT, SCHEMA_DELETE
string operation = 4;
bytes payload = 5;
string token = 6;
}
Expand Down
77 changes: 73 additions & 4 deletions cmd/main-worker/server.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"crypto/sha256"
"encoding/hex"
"encoding/json"
"errors"
"fmt"
"log"
"net"
Expand Down Expand Up @@ -318,11 +319,63 @@ func (s *MainWorkerServer) Process(ctx context.Context, req *proto.ProcessReques
return nil, status.Error(codes.Unauthenticated, "invalid or expired token")
}

// ── Schema management operations ──────────────────────────────────────
// SCHEMA_GET / SCHEMA_PUT / SCHEMA_DELETE operate on the JSON Schema
// template registry, not on entity data.
switch op {
case "SCHEMA_GET":
if s.validator == nil {
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "error").Inc()
return nil, status.Error(codes.Unavailable, "schema management unavailable")
}
data, err := s.validator.GetTemplateData(req.GetSchemaId())
if err != nil {
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "error").Inc()
s.metrics.ProcessDurationSeconds.WithLabelValues(op, "error").Observe(time.Since(start).Seconds())
return nil, status.Error(codes.NotFound, "schema not found")
}
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "success").Inc()
s.metrics.ProcessDurationSeconds.WithLabelValues(op, "success").Observe(time.Since(start).Seconds())
return &proto.ProcessResponse{Status: "OK", Result: data}, nil

case "SCHEMA_PUT":
if s.validator == nil {
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "error").Inc()
return nil, status.Error(codes.Unavailable, "schema management unavailable")
}
if err := s.validator.SaveTemplate(req.GetSchemaId(), req.GetPayload()); err != nil {
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "error").Inc()
s.metrics.ProcessDurationSeconds.WithLabelValues(op, "error").Observe(time.Since(start).Seconds())
return nil, status.Error(codes.InvalidArgument, err.Error())
}
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "success").Inc()
s.metrics.ProcessDurationSeconds.WithLabelValues(op, "success").Observe(time.Since(start).Seconds())
return &proto.ProcessResponse{Status: "OK"}, nil

case "SCHEMA_DELETE":
if s.validator == nil {
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "error").Inc()
return nil, status.Error(codes.Unavailable, "schema management unavailable")
}
if err := s.validator.DeleteTemplate(req.GetSchemaId()); err != nil {
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "error").Inc()
s.metrics.ProcessDurationSeconds.WithLabelValues(op, "error").Observe(time.Since(start).Seconds())
if errors.Is(err, schema.ErrSchemaNotFound) {
return nil, status.Error(codes.NotFound, err.Error())
}
return nil, status.Error(codes.Internal, err.Error())
}
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "success").Inc()
s.metrics.ProcessDurationSeconds.WithLabelValues(op, "success").Observe(time.Since(start).Seconds())
return &proto.ProcessResponse{Status: "OK"}, nil
}

// ── Entity operations ─────────────────────────────────────────────────
// Validate operation
if op != "GET" && op != "PUT" && op != "DELETE" {
s.metrics.ProcessRequestsTotal.WithLabelValues(op, "error").Inc()
s.metrics.ProcessDurationSeconds.WithLabelValues(op, "error").Observe(time.Since(start).Seconds())
return nil, status.Error(codes.InvalidArgument, "operation must be GET, PUT, or DELETE")
return nil, status.Error(codes.InvalidArgument, "operation must be GET, PUT, DELETE, SCHEMA_GET, SCHEMA_PUT, or SCHEMA_DELETE")
}

if op == "GET" {
Expand Down Expand Up @@ -779,10 +832,11 @@ func (s *MainWorkerServer) handleAdminSchemas(w http.ResponseWriter, r *http.Req
json.NewEncoder(w).Encode(schemas) //nolint:errcheck
}

// handleSchema serves GET and PUT requests for /schema/{id}.
// handleSchema serves GET, PUT, and DELETE requests for /schema/{id}.
//
// GET /schema/{id} — retrieve a schema JSON (no authentication required).
// PUT /schema/{id} — create or replace a schema (authentication required).
// GET /schema/{id} — retrieve a schema JSON (no authentication required).
// PUT /schema/{id} — create or replace a schema (write permission required).
// DELETE /schema/{id} — permanently delete a schema (write permission required).
func (s *MainWorkerServer) handleSchema(w http.ResponseWriter, r *http.Request) {
schemaID := strings.TrimPrefix(r.URL.Path, "/schema/")
// Reject empty or path-traversal schema IDs.
Expand Down Expand Up @@ -825,6 +879,21 @@ func (s *MainWorkerServer) handleSchema(w http.ResponseWriter, r *http.Request)
w.WriteHeader(http.StatusOK)
json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) //nolint:errcheck

case http.MethodDelete:
if !s.requirePermission(w, r, auth.PermWrite) {
return
}
if err := s.validator.DeleteTemplate(schemaID); err != nil {
if errors.Is(err, schema.ErrSchemaNotFound) {
http.Error(w, `{"error":"not_found"}`, http.StatusNotFound)
} else {
http.Error(w, fmt.Sprintf(`{"error":%q}`, err.Error()), http.StatusInternalServerError)
}
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{"status": "ok"}) //nolint:errcheck

default:
http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
}
Expand Down
Loading
Loading