Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
b02ab70
feat(database): add attribute schema tables and soft-delete triggers
Filip-sz-informed Sep 4, 2026
354467d
test(persistency): add Testcontainers Postgres base for repository tests
Filip-sz-informed Sep 4, 2026
b5f8661
feat(persistency): add AttributeScope entity and repository
Filip-sz-informed Sep 4, 2026
15ef690
feat(persistency): add AttributeDefinition entity and repository
Filip-sz-informed Sep 4, 2026
7ed8515
feat(persistency): add AttributeDefinitionScope entity and repository
Filip-sz-informed Sep 4, 2026
36eb44f
feat(persistency): add AttributeValue entity and repository
Filip-sz-informed Sep 4, 2026
2cd05b0
test(persistency): cover soft-delete triggers for attribute_value
Filip-sz-informed Sep 4, 2026
8ad5a33
docs(schema): document policy attribute schema tables
Filip-sz-informed Sep 4, 2026
5c4809f
fix(persistency): start the shared Postgres container eagerly, not vi…
Filip-sz-informed Sep 4, 2026
ed6b230
refactor(persistency): extract shared audit columns into AttributeAud…
Filip-sz-informed Sep 4, 2026
2c0e145
chore(deps): bump bundled Tomcat to 10.1.59 to resolve CRITICAL CVEs
Filip-sz-informed Sep 4, 2026
1cbff6e
feat(filter): add closed filter DSL for dynamic config filtering
Filip-sz-informed Sep 4, 2026
3bd49ef
feat(filter): add producer/consumer resource attribute registry
Filip-sz-informed Sep 4, 2026
d00ae3c
feat(filter): add SpecificationPredicateCompiler for producer/consume…
Filip-sz-informed Sep 4, 2026
e4a7c7f
feat(config): wire Specification filtering through repository and ser…
Filip-sz-informed Sep 4, 2026
ea68b44
feat(config): wire optional filter query parameter into configuration…
Filip-sz-informed Sep 4, 2026
e37586a
test(config): add end-to-end filtering coverage against real Postgres
Filip-sz-informed Sep 4, 2026
b6b8e9a
refactor(filter): drop unused FilterNode.Literal and tidy compiler ge…
Filip-sz-informed Sep 4, 2026
6084620
fix(config): fix code-review findings in dynamic-config-filtering
Filip-sz-informed Sep 4, 2026
99c1e8a
fix(config): use compareTo for BigDecimal zero-validity check
Filip-sz-informed Sep 4, 2026
f2365c6
fix(config): resolve 20 SonarCloud code smells on PR #69
Filip-sz-informed Sep 4, 2026
d836ed7
test(config): fix field-shadowing smell and close compiler coverage gaps
Filip-sz-informed Sep 4, 2026
e0ae142
refactor(filter): remove the filter query-param DSL package
Filip-sz-informed Sep 8, 2026
ee12f2e
test(filter): remove tests for the deleted filter query-param DSL
Filip-sz-informed Sep 8, 2026
1ee74d7
refactor(config): drop the filter query param from configuration endp…
Filip-sz-informed Sep 8, 2026
a6f88d8
refactor(config): drop the FilterNode overloads from ConfigurationPro…
Filip-sz-informed Sep 8, 2026
644c8d8
refactor(config): drop the Specification-based filter overloads
Filip-sz-informed Sep 8, 2026
40aaf0f
refactor(persistency): drop the filter-only AttributeDefinitionScope …
Filip-sz-informed Sep 8, 2026
004ab1c
refactor(config): drop the FilterCompilationException handler
Filip-sz-informed Sep 8, 2026
d9343b9
style(config): fix trailing blank line left by filter-feature removal
Filip-sz-informed Sep 8, 2026
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
148 changes: 148 additions & 0 deletions docs/DATABASE_SCHEMA.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ erDiagram
CONSUMER ||--o{ PRODUCT_CONSUMER : consumes
PRODUCT_CONSUMER ||--o{ PRODUCT_CONSUMER_ATTRIBUTE : has
PRODUCT_TYPE ||--o{ PRODUCT : categorizes
ATTRIBUTE_DEFINITION ||--o{ ATTRIBUTE_DEFINITION_SCOPE : "bound via"
ATTRIBUTE_SCOPE ||--o{ ATTRIBUTE_DEFINITION_SCOPE : "bound via"
ATTRIBUTE_DEFINITION_SCOPE ||--o{ ATTRIBUTE_VALUE : has

ORGANISATION {
BIGSERIAL id PK
Expand Down Expand Up @@ -116,6 +119,53 @@ erDiagram
TIMESTAMP event_time
VARCHAR performed_by
}
ATTRIBUTE_SCOPE {
BIGSERIAL id PK
VARCHAR code
VARCHAR table_name
VARCHAR description
}
ATTRIBUTE_DEFINITION {
BIGSERIAL id PK
VARCHAR namespace
VARCHAR name
VARCHAR display_name
TEXT description
VARCHAR data_type
BOOLEAN multi_valued
JSONB allowed_values
VARCHAR validation_pattern
JSONB classification
BOOLEAN sensitive
BOOLEAN is_deleted
TIMESTAMP created_at
VARCHAR created_by
TIMESTAMP updated_at
VARCHAR updated_by
}
ATTRIBUTE_DEFINITION_SCOPE {
BIGSERIAL id PK
BIGINT attribute_definition_id FK
BIGINT attribute_scope_id FK
BOOLEAN required
JSONB default_value
BOOLEAN is_deleted
TIMESTAMP created_at
VARCHAR created_by
TIMESTAMP updated_at
VARCHAR updated_by
}
ATTRIBUTE_VALUE {
BIGSERIAL id PK
BIGINT attribute_definition_scope_id FK
BIGINT entity_id
JSONB value
BOOLEAN is_deleted
TIMESTAMP created_at
VARCHAR created_by
TIMESTAMP updated_at
VARCHAR updated_by
}
```

---
Expand Down Expand Up @@ -284,6 +334,104 @@ Usage:

---

### attribute_scope
Which core entity types may carry dynamic policy attributes, and the table `attribute_value.entity_id` resolves against for that scope.

Columns:
- `id` BIGSERIAL, primary key
- `code` VARCHAR(50), not null — unique scope identifier (e.g. `PRODUCT`)
- `table_name` VARCHAR(150), not null — the table `attribute_value.entity_id` is a row id in, for this scope
- `description` VARCHAR(500), nullable

Constraints:
- UNIQUE on `code` (`uq_attribute_scope__code`)

Usage:
- Seeded by migration with one row per core entity type: `ORGANISATION` (`organisation`), `CONSUMER` (`consumer`), `PRODUCER` (`producer`), `PRODUCT` (`product`), `SUBSCRIPTION` (`product_consumer`).
- Referenced by `attribute_definition_scope` to say which scopes an attribute definition applies to.

---

### attribute_definition
Vocabulary of policy attributes: name, type, and validation metadata, independent of which scope(s) it applies to.

Columns:
- `id` BIGSERIAL, primary key
- `namespace` VARCHAR(150), not null
- `name` VARCHAR(150), not null
- `display_name` VARCHAR(255), nullable
- `description` TEXT, not null
- `data_type` VARCHAR(50), not null
- `multi_valued` BOOLEAN, not null, default FALSE
- `allowed_values` JSONB, nullable
- `validation_pattern` VARCHAR(500), nullable
- `classification` JSONB, nullable
- `sensitive` BOOLEAN, not null, default FALSE
- `is_deleted` BOOLEAN, not null, default FALSE
- `created_at` TIMESTAMP, not null, default `now()`
- `created_by` VARCHAR(255), not null
- `updated_at` TIMESTAMP, nullable
- `updated_by` VARCHAR(255), nullable

Constraints:
- UNIQUE on (`namespace`, `name`) (`uq_attribute_definition__namespace_name`)

Usage:
- Defines the shape of a policy attribute (e.g. data type, whether it can hold multiple values, allowed values, sensitivity) independently of where it can be attached.

---

### attribute_definition_scope
Which scopes an `attribute_definition` is valid on, whether required there, and its default value.

Columns:
- `id` BIGSERIAL, primary key
- `attribute_definition_id` BIGINT, not null, foreign key → `attribute_definition(id)`
- `attribute_scope_id` BIGINT, not null, foreign key → `attribute_scope(id)`
- `required` BOOLEAN, not null, default FALSE
- `default_value` JSONB, nullable
- `is_deleted` BOOLEAN, not null, default FALSE
- `created_at` TIMESTAMP, not null, default `now()`
- `created_by` VARCHAR(255), not null
- `updated_at` TIMESTAMP, nullable
- `updated_by` VARCHAR(255), nullable

Constraints:
- UNIQUE on (`attribute_definition_id`, `attribute_scope_id`) (`uq_attribute_definition_scope__definition_scope`)
- Index on `attribute_definition_id` (`idx_attribute_definition_scope__attribute_definition_id`)
- Index on `attribute_scope_id` (`idx_attribute_definition_scope__attribute_scope_id`)

Usage:
- Binds a definition to one or more scopes, controlling per-scope requiredness and default.

---

### attribute_value
Actual policy attribute values recorded against a specific entity.

Columns:
- `id` BIGSERIAL, primary key
- `attribute_definition_scope_id` BIGINT, not null, foreign key → `attribute_definition_scope(id)`
- `entity_id` BIGINT, not null — polymorphic reference: the primary key of the row in the table named by the value's `attribute_scope.table_name`. Not a declared foreign key, since the target table varies by scope.
- `value` JSONB, not null
- `is_deleted` BOOLEAN, not null, default FALSE
- `created_at` TIMESTAMP, not null, default `now()`
- `created_by` VARCHAR(255), not null
- `updated_at` TIMESTAMP, nullable
- `updated_by` VARCHAR(255), nullable

Constraints:
- Index on `entity_id` (`idx_attribute_value__entity_id`)
- Partial UNIQUE index on (`attribute_definition_scope_id`, `entity_id`, `value`) WHERE `is_deleted = FALSE` (`uq_attr_value_live`) — an idempotency guard against persisting an exact-duplicate live value; it does not by itself enforce "one live value per entity" for single-valued attributes (that check spans `attribute_definition.multi_valued` and is left to the service layer that writes these rows)

Soft-delete triggers:
- `trg_organisation_attribute_value_soft_delete`, `trg_consumer_attribute_value_soft_delete`, `trg_producer_attribute_value_soft_delete`, `trg_product_attribute_value_soft_delete`, `trg_product_consumer_attribute_value_soft_delete` — one `AFTER DELETE` trigger per owning table (`organisation`, `consumer`, `producer`, `product`, `product_consumer`), all calling the shared function `fn_attribute_value_soft_delete_on_entity_delete()`. When a row in one of those tables is deleted, every live (`is_deleted = FALSE`) `attribute_value` row scoped to that table and entity id is set `is_deleted = TRUE` rather than deleted or left orphaned.

Usage:
- Stores the actual attribute values used to build the OPA data bundle for policy decisions, keyed by which entity (organisation, consumer, producer, product, or subscription) they describe.

---

## Migration Notes
- Schema is versioned and applied with Flyway on application startup.
- Foreign keys enforce referential integrity among core entities.
Expand Down
11 changes: 11 additions & 0 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@
<bouncycastle.version>1.84</bouncycastle.version>
<spring-security.version>6.5.9</spring-security.version>
<httpcore5.version>5.4.3</httpcore5.version>
<tomcat.version>10.1.59</tomcat.version>
<sonar.coverage.exclusions>**/config/**,
**/dto/**,
**/entity/**,
Expand Down Expand Up @@ -197,6 +198,16 @@
<artifactId>commons-lang3</artifactId>
<version>3.18.0</version>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>postgresql</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.testcontainers</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
/*
* SPDX-License-Identifier: Apache-2.0
* © Crown Copyright 2026. This work has been developed by the National Digital Twin Programme and is legally
* attributed to the Department for Business and Trade (UK) as the governing entity.
*/

package uk.gov.dbt.ndtp.ia.node.management.persistency.entity;

import jakarta.persistence.Column;
import jakarta.persistence.MappedSuperclass;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import java.sql.Timestamp;
import lombok.Getter;
import lombok.Setter;

/**
* Shared soft-delete and audit columns for the policy attribute schema entities
* ({@link AttributeDefinition}, {@link AttributeDefinitionScope}, {@link AttributeValue}).
*/
@Getter
@Setter
@MappedSuperclass
public abstract class AttributeAuditFields {

@NotNull
@Column(name = "is_deleted", nullable = false)
private Boolean isDeleted = false;

@NotNull
@Column(name = "created_at", nullable = false)
private Timestamp createdAt;

@Size(max = 255)
@NotNull
@Column(name = "created_by", nullable = false, length = 255)
private String createdBy;

@Column(name = "updated_at")
private Timestamp updatedAt;

@Size(max = 255)
@Column(name = "updated_by", length = 255)
private String updatedBy;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* SPDX-License-Identifier: Apache-2.0
* © Crown Copyright 2026. This work has been developed by the National Digital Twin Programme and is legally
* attributed to the Department for Business and Trade (UK) as the governing entity.
*/

package uk.gov.dbt.ndtp.ia.node.management.persistency.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

@Getter
@Setter
@Entity
@Table(name = "attribute_definition")
public class AttributeDefinition extends AttributeAuditFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

@Size(max = 150)
@NotNull
@Column(name = "namespace", nullable = false, length = 150)
private String namespace;

@Size(max = 150)
@NotNull
@Column(name = "name", nullable = false, length = 150)
private String name;

@Size(max = 255)
@Column(name = "display_name", length = 255)
private String displayName;

@NotNull
@Column(name = "description", nullable = false)
private String description;

@Size(max = 50)
@NotNull
@Column(name = "data_type", nullable = false, length = 50)
private String dataType;

@NotNull
@Column(name = "multi_valued", nullable = false)
private Boolean multiValued = false;

@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "allowed_values")
private String allowedValues;

@Size(max = 500)
@Column(name = "validation_pattern", length = 500)
private String validationPattern;

@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "classification")
private String classification;

@NotNull
@Column(name = "sensitive", nullable = false)
private Boolean sensitive = false;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
/*
* SPDX-License-Identifier: Apache-2.0
* © Crown Copyright 2026. This work has been developed by the National Digital Twin Programme and is legally
* attributed to the Department for Business and Trade (UK) as the governing entity.
*/

package uk.gov.dbt.ndtp.ia.node.management.persistency.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import lombok.Getter;
import lombok.Setter;
import org.hibernate.annotations.JdbcTypeCode;
import org.hibernate.type.SqlTypes;

@Getter
@Setter
@Entity
@Table(name = "attribute_definition_scope")
public class AttributeDefinitionScope extends AttributeAuditFields {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "attribute_definition_id", nullable = false)
private AttributeDefinition attributeDefinition;

@NotNull
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "attribute_scope_id", nullable = false)
private AttributeScope attributeScope;

@NotNull
@Column(name = "required", nullable = false)
private Boolean required = false;

@JdbcTypeCode(SqlTypes.JSON)
@Column(name = "default_value")
private String defaultValue;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
/*
* SPDX-License-Identifier: Apache-2.0
* © Crown Copyright 2026. This work has been developed by the National Digital Twin Programme and is legally
* attributed to the Department for Business and Trade (UK) as the governing entity.
*/

package uk.gov.dbt.ndtp.ia.node.management.persistency.entity;

import jakarta.persistence.*;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Size;
import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Entity
@Table(name = "attribute_scope")
public class AttributeScope {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", nullable = false)
private Long id;

@Size(max = 50)
@NotNull
@Column(name = "code", nullable = false, length = 50)
private String code;

@Size(max = 150)
@NotNull
@Column(name = "table_name", nullable = false, length = 150)
private String tableName;

@Size(max = 500)
@Column(name = "description", length = 500)
private String description;
}
Loading
Loading