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
241 changes: 161 additions & 80 deletions api/centralconfig/v1/types.pb.go

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion cmd/server/main.go
Original file line number Diff line number Diff line change
Expand Up @@ -217,7 +217,7 @@ func run() int {

// Register services.
if srv.IsServiceEnabled("schema") {
schemaSvc := schema.NewService(schemaStoreVal, logger, schemaMetrics, validatorFactory.Cache())
schemaSvc := schema.NewService(schemaStoreVal, logger, schemaMetrics, validatorFactory)
pb.RegisterSchemaServiceServer(srv.GRPCServer(), schemaSvc)
srv.SetServiceHealthy("centralconfig.v1.SchemaService")
logger.InfoContext(ctx, "schema service enabled")
Expand Down
25 changes: 25 additions & 0 deletions cmd/server/openapi.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

19 changes: 11 additions & 8 deletions db/migrations/001_initial_schema.sql
Original file line number Diff line number Diff line change
Expand Up @@ -24,14 +24,17 @@ CREATE TABLE schemas (
);

CREATE TABLE schema_versions (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
schema_id UUID NOT NULL REFERENCES schemas(id) ON DELETE CASCADE,
version INT NOT NULL,
parent_version INT,
description TEXT,
checksum TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT false,
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
schema_id UUID NOT NULL REFERENCES schemas(id) ON DELETE CASCADE,
version INT NOT NULL,
parent_version INT,
description TEXT,
checksum TEXT NOT NULL,
published BOOLEAN NOT NULL DEFAULT false,
-- JSON array of {trigger_field, dependent_fields} entries encoding the
-- schema's dependentRequired rules. Empty array when no rules exist.
dependent_required JSONB NOT NULL DEFAULT '[]',
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
UNIQUE(schema_id, version)
);

Expand Down
4 changes: 2 additions & 2 deletions db/queries/schemas.sql
Original file line number Diff line number Diff line change
Expand Up @@ -18,8 +18,8 @@ LIMIT $1 OFFSET $2;
DELETE FROM schemas WHERE id = $1;

-- name: CreateSchemaVersion :one
INSERT INTO schema_versions (schema_id, version, parent_version, description, checksum)
VALUES ($1, $2, $3, $4, $5)
INSERT INTO schema_versions (schema_id, version, parent_version, description, checksum, dependent_required)
VALUES ($1, $2, $3, $4, $5, $6)
RETURNING *;

-- name: GetSchemaVersion :one
Expand Down
23 changes: 23 additions & 0 deletions docs/api/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
- [ConfigChange](#centralconfig-v1-ConfigChange)
- [ConfigValue](#centralconfig-v1-ConfigValue)
- [ConfigVersion](#centralconfig-v1-ConfigVersion)
- [DependentRequiredEntry](#centralconfig-v1-DependentRequiredEntry)
- [ExternalDocs](#centralconfig-v1-ExternalDocs)
- [FieldConstraints](#centralconfig-v1-FieldConstraints)
- [FieldExample](#centralconfig-v1-FieldExample)
Expand Down Expand Up @@ -225,6 +226,27 @@ full config at any version is the union of all deltas up to that version.



<a name="centralconfig-v1-DependentRequiredEntry"></a>

### DependentRequiredEntry
DependentRequiredEntry encodes one cross-field requirement: when the
trigger field has a non-null value, every dependent field path must also
have a non-null value. This is the proto wire form of JSON Schema 2020-12
dependentRequired, which uses a `map&lt;path, list&lt;path&gt;&gt;` shape — proto
maps cannot hold repeated values directly, so we use a repeated list of
entries.


| Field | Type | Label | Description |
| ----- | ---- | ----- | ----------- |
| trigger_field | [string](#string) | | Field path whose presence triggers the requirement. |
| dependent_fields | [string](#string) | repeated | Field paths that must be present when the trigger has a non-null value. |






<a name="centralconfig-v1-ExternalDocs"></a>

### ExternalDocs
Expand Down Expand Up @@ -321,6 +343,7 @@ Each schema is versioned — updates create new immutable versions.
| fields | [SchemaField](#centralconfig-v1-SchemaField) | repeated | The fields defined in this schema version. |
| created_at | [google.protobuf.Timestamp](#google-protobuf-Timestamp) | | When this version was created. |
| info | [SchemaInfo](#centralconfig-v1-SchemaInfo) | | Optional schema metadata: ownership, contact, labels. |
| dependent_required | [DependentRequiredEntry](#centralconfig-v1-DependentRequiredEntry) | repeated | Cross-field &#34;B required when A present&#34; rules. Each entry declares one trigger field whose presence (non-null value) makes a list of dependent field paths required (also non-null). Equivalent to JSON Schema 2020-12 dependentRequired, scoped to schema-level cross-field requirement. Lint-checked at ImportSchema time (every path must reference a real field; trigger may not appear in its own dependents). Enforced at every config write against the post-merge snapshot. |



Expand Down
25 changes: 25 additions & 0 deletions docs/api/openapi.swagger.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

21 changes: 21 additions & 0 deletions docs/concepts/schemas-and-fields.md
Original file line number Diff line number Diff line change
Expand Up @@ -180,6 +180,27 @@ constraints:
{"type": "object", "required": ["name"], "properties": {"name": {"type": "string"}}}
```

## Cross-field dependencies

Use the top-level `dependentRequired:` key to declare "if field A is set, field B must also be set". The keyword matches [JSON Schema 2020-12 `dependentRequired`](https://json-schema.org/understanding-json-schema/reference/conditionals#dependentrequired) — keys are trigger field paths, values are lists of dependent paths.

```yaml
fields:
payments.refunds_enabled: { type: bool }
payments.refund_window: { type: duration, nullable: true }

dependentRequired:
payments.refunds_enabled: [payments.refund_window]
```

Semantics:

- **Triggers on non-null.** A rule fires only when the trigger field has a non-null value in the post-merge configuration. Setting the trigger to null clears the requirement.
- **Lint at import.** `ImportSchema` rejects rules where the trigger or any dependent does not name a defined field, where a trigger lists itself as a dependent, or where a dependent appears twice under the same trigger.
- **Runtime enforcement.** Every config write (`SetField`, `SetFields`, `ImportConfig`, `RollbackToVersion`) evaluates all rules against the post-merge state inside the same transaction. A rule violation rejects the write with `InvalidArgument`.

For arithmetic or other cross-field invariants that `dependentRequired` cannot express (`min < max`, `start_at < end_at`), see the [CEL validation design](../../.agents/context/cel-validation.md) — that path uses the reserved `validations:` key.

## Field Options

| Option | Type | Default | Description |
Expand Down
Loading
Loading