Implement apix-registry with IoT sunset/decommission lifecycle and full BDD suite
- REST API: register, patch, O-level, replacements, history, search endpoints - IoT lifecycle validations: future sunset, lock-before-release, sunset-passed-before-decommission - DB schema: Liquibase changesets 001–008 (services, versions, replacements, sunset-at column) - @ColumnTransformer(write="?::jsonb") on bsm_payload fields to avoid JDBC varchar→jsonb rejection - Jandex plugin on apix-common + quarkus.index-dependency so @NotBlank validators resolve at runtime - quarkus-logging-json extension added; quarkus.log.console.json=false is now a recognised key - Fix requireSunsetBeforeLockRelease: Boolean.TRUE.equals instead of !Boolean.FALSE.equals (null guard) - BDD suite: 27 scenarios / 213 steps across 5 feature files (sunset-lock, decommission, replacement, discovery, anonymity) - Test infrastructure: JDBC TRUNCATE in @Before for DB isolation, Arc.container() for clock control — no test endpoints in production code - sunsetAt truncated to microseconds in BDD steps to match Postgres timestamptz precision - Cucumber step fixes: singular/plural candidate(s), lastResponse propagation in replacementsReturnsNCandidates Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,35 @@
|
||||
# ── Jandex — include apix-common in the index so bean validation constraints work ──
|
||||
quarkus.index-dependency.apix-common.group-id=org.botstandards
|
||||
quarkus.index-dependency.apix-common.artifact-id=apix-common
|
||||
|
||||
# ── Datasource ────────────────────────────────────────────────────────────────
|
||||
quarkus.datasource.db-kind=postgresql
|
||||
quarkus.datasource.jdbc.url=${QUARKUS_DATASOURCE_JDBC_URL:jdbc:postgresql://localhost:5432/apix}
|
||||
quarkus.datasource.username=${QUARKUS_DATASOURCE_USERNAME:apix}
|
||||
quarkus.datasource.password=${QUARKUS_DATASOURCE_PASSWORD:apix}
|
||||
|
||||
# ── ORM ───────────────────────────────────────────────────────────────────────
|
||||
# Liquibase owns schema creation; Hibernate must not touch DDL
|
||||
quarkus.hibernate-orm.database.generation=none
|
||||
|
||||
# ── Liquibase ─────────────────────────────────────────────────────────────────
|
||||
quarkus.liquibase.migrate-at-start=true
|
||||
quarkus.liquibase.change-log=db/changelog/db.changelog-master.xml
|
||||
|
||||
# ── HTTP ──────────────────────────────────────────────────────────────────────
|
||||
quarkus.http.port=8180
|
||||
|
||||
# ── Security — API key for write endpoints ───────────────────────────────────
|
||||
apix.api-key=${APIX_API_KEY:dev-insecure-key-change-in-prod}
|
||||
|
||||
# ── Verification ──────────────────────────────────────────────────────────────
|
||||
apix.gleif.api-url=${GLEIF_API_URL:https://api.gleif.org/api/v1}
|
||||
apix.opencorporates.api-key=${OPENCORPORATES_API_KEY:}
|
||||
apix.sanctions.cache-path=${SANCTIONS_CACHE_PATH:./sanctions-cache}
|
||||
|
||||
# ── Logging ───────────────────────────────────────────────────────────────────
|
||||
quarkus.log.level=${LOG_LEVEL:DEBUG}
|
||||
quarkus.log.console.json=false
|
||||
|
||||
# ── Health ────────────────────────────────────────────────────────────────────
|
||||
quarkus.smallrye-health.root-path=/q/health
|
||||
@@ -0,0 +1,36 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="001" author="apix">
|
||||
<createTable tableName="services">
|
||||
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="endpoint_url" type="text">
|
||||
<constraints nullable="false" unique="true" uniqueConstraintName="uq_services_endpoint_url"/>
|
||||
</column>
|
||||
<!-- Full BSM document stored as JSONB for flexible querying -->
|
||||
<column name="bsm_payload" type="jsonb">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="olevel" type="varchar(50)" defaultValue="UNVERIFIED">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="slevel" type="varchar(50)"/>
|
||||
<column name="liveness_status" type="varchar(50)" defaultValue="PENDING">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="registered_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
</createTable>
|
||||
|
||||
<!-- GIN index on bsm_payload for capability search (@> operator) -->
|
||||
<sql>CREATE INDEX idx_services_bsm_payload_gin ON services USING gin (bsm_payload);</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="002" author="apix">
|
||||
<addColumn tableName="services">
|
||||
<column name="verification_status" type="varchar(50)"/>
|
||||
<column name="olevel_checked_at" type="timestamptz"/>
|
||||
<!-- null = not yet screened; false = hit; true = clear -->
|
||||
<column name="sanctions_cleared" type="boolean"/>
|
||||
<column name="gleif_lei" type="text"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="003" author="apix">
|
||||
<addColumn tableName="services">
|
||||
<column name="last_checked_at" type="timestamptz"/>
|
||||
<column name="uptime_30d_percent" type="numeric(5,2)"/>
|
||||
<column name="avg_response_ms" type="integer"/>
|
||||
<column name="consecutive_failures" type="integer" defaultValueNumeric="0"/>
|
||||
</addColumn>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,40 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="004" author="apix">
|
||||
<addColumn tableName="services">
|
||||
<!-- Registrant type — top-level column, not buried in JSONB, for direct filtering -->
|
||||
<column name="registrant_org_type" type="varchar(50)" defaultValue="INDIVIDUAL">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Registrant-declared lifecycle stage — controls search visibility -->
|
||||
<column name="service_stage" type="varchar(50)" defaultValue="DEVELOPMENT">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- BSF-controlled administrative state — always applied before stage filter -->
|
||||
<column name="registry_status" type="varchar(50)" defaultValue="ACTIVE">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Monotonically increasing per-service counter; links to service_versions -->
|
||||
<column name="version" type="integer" defaultValueNumeric="1">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Timestamp of last non-liveness update (liveness writes go to 003 columns) -->
|
||||
<column name="last_updated_at" type="timestamptz"/>
|
||||
</addColumn>
|
||||
|
||||
<!-- service_stage is the primary search filter dimension after capability -->
|
||||
<createIndex tableName="services" indexName="idx_services_service_stage">
|
||||
<column name="service_stage"/>
|
||||
</createIndex>
|
||||
<!-- registry_status is applied to every public query; index keeps it cheap -->
|
||||
<createIndex tableName="services" indexName="idx_services_registry_status">
|
||||
<column name="registry_status"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="005" author="apix">
|
||||
<!-- Append-only snapshot table. Rows are never updated or deleted.
|
||||
Each row captures the complete service state at the moment of a meaningful change.
|
||||
Diffs are computed from adjacent versions at read time, not stored. -->
|
||||
<createTable tableName="service_versions">
|
||||
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<column name="service_id" type="uuid">
|
||||
<constraints nullable="false"
|
||||
foreignKeyName="fk_sv_service_id"
|
||||
references="services(id)"/>
|
||||
</column>
|
||||
<column name="version" type="integer">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="recorded_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- REGISTERED | BSM_UPDATED | ORG_TYPE_CHANGED | OLEVEL_CHANGED |
|
||||
STAGE_CHANGED | OWNERSHIP_TRANSFERRED | REGISTRY_STATUS_CHANGED -->
|
||||
<column name="change_type" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Full BSM at this version — enables complete before/after comparison -->
|
||||
<column name="bsm_payload" type="jsonb">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="registrant_org_type" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="olevel" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="service_stage" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<column name="registry_status" type="varchar(50)">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Optional human-readable context for the change, e.g.
|
||||
"Incorporated as GmbH; transitioning from individual registration" -->
|
||||
<column name="note" type="text"/>
|
||||
</createTable>
|
||||
|
||||
<addUniqueConstraint
|
||||
tableName="service_versions"
|
||||
columnNames="service_id, version"
|
||||
constraintName="uq_sv_service_version"/>
|
||||
|
||||
<!-- Primary access pattern: all versions for a given service, ordered by version -->
|
||||
<createIndex tableName="service_versions" indexName="idx_sv_service_id">
|
||||
<column name="service_id"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,33 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="006" author="apix">
|
||||
<addColumn tableName="services">
|
||||
<!-- null = lock concept not applicable (non-IoT service)
|
||||
true = device locked to this template owner; migration blocked
|
||||
false = lock released; device owner may migrate freely
|
||||
Default null so existing records are not incorrectly treated as locked. -->
|
||||
<column name="locked" type="boolean"/>
|
||||
|
||||
<!-- ISO date when the service goes permanently offline.
|
||||
Set together with service_stage = DEPRECATED. -->
|
||||
<column name="sunset_date" type="date"/>
|
||||
|
||||
<!-- Provider-hosted migration documentation URL. -->
|
||||
<column name="migration_guide_url" type="text"/>
|
||||
</addColumn>
|
||||
|
||||
<!-- Targeted index: only rows with a sunset date (small, DEPRECATED subset) -->
|
||||
<sql>CREATE INDEX idx_services_sunset_date ON services (sunset_date)
|
||||
WHERE sunset_date IS NOT NULL;</sql>
|
||||
|
||||
<!-- Targeted index: only rows where locked is explicitly set -->
|
||||
<sql>CREATE INDEX idx_services_locked ON services (locked)
|
||||
WHERE locked IS NOT NULL;</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,49 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="007" author="apix">
|
||||
<!-- Declared compatibility index.
|
||||
Replacement providers list deprecated service IDs in BSM.replacesServiceIds[].
|
||||
The registry extracts those declarations here for efficient lookup.
|
||||
These rows are derived from BSM content and re-synced on every BSM update —
|
||||
never edit directly. -->
|
||||
<createTable tableName="service_replacements">
|
||||
<column name="id" type="uuid" defaultValueComputed="gen_random_uuid()">
|
||||
<constraints primaryKey="true" nullable="false"/>
|
||||
</column>
|
||||
<!-- The deprecated service being replaced -->
|
||||
<column name="deprecated_service_id" type="uuid">
|
||||
<constraints nullable="false"
|
||||
foreignKeyName="fk_sr_deprecated"
|
||||
references="services(id)"/>
|
||||
</column>
|
||||
<!-- The service declaring it can replace the deprecated one -->
|
||||
<column name="replacement_service_id" type="uuid">
|
||||
<constraints nullable="false"
|
||||
foreignKeyName="fk_sr_replacement"
|
||||
references="services(id)"/>
|
||||
</column>
|
||||
<column name="declared_at" type="timestamptz" defaultValueComputed="now()">
|
||||
<constraints nullable="false"/>
|
||||
</column>
|
||||
<!-- Optional human-readable compatibility notes from the replacement provider -->
|
||||
<column name="compatibility_notes" type="text"/>
|
||||
</createTable>
|
||||
|
||||
<!-- A replacement service can declare compatibility with each deprecated service once -->
|
||||
<addUniqueConstraint
|
||||
tableName="service_replacements"
|
||||
columnNames="deprecated_service_id, replacement_service_id"
|
||||
constraintName="uq_sr_deprecated_replacement"/>
|
||||
|
||||
<!-- Primary query path: GET /services/{deprecatedId}/replacements -->
|
||||
<createIndex tableName="service_replacements" indexName="idx_sr_deprecated_service_id">
|
||||
<column name="deprecated_service_id"/>
|
||||
</createIndex>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,24 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<changeSet id="008" author="apix">
|
||||
<!-- Drop old date-only index before renaming the column -->
|
||||
<sql>DROP INDEX IF EXISTS idx_services_sunset_date;</sql>
|
||||
|
||||
<!-- Rename and retype: date → timestamptz (exclusive boundary, UTC).
|
||||
Existing date values are cast to midnight UTC of that date. -->
|
||||
<sql>ALTER TABLE services RENAME COLUMN sunset_date TO sunset_at;</sql>
|
||||
<sql>ALTER TABLE services
|
||||
ALTER COLUMN sunset_at TYPE timestamptz
|
||||
USING sunset_at::timestamptz;</sql>
|
||||
|
||||
<!-- Recreate targeted index under new column name -->
|
||||
<sql>CREATE INDEX idx_services_sunset_at ON services (sunset_at)
|
||||
WHERE sunset_at IS NOT NULL;</sql>
|
||||
</changeSet>
|
||||
|
||||
</databaseChangeLog>
|
||||
@@ -0,0 +1,17 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<databaseChangeLog
|
||||
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
|
||||
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog
|
||||
http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-4.27.xsd">
|
||||
|
||||
<include file="changes/001-initial-schema.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/002-verification-columns.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/003-liveness-metrics.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/004-org-stage-status.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/005-version-history.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/006-sunset-lock.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/007-service-replacements.xml" relativeToChangelogFile="true"/>
|
||||
<include file="changes/008-sunset-at.xml" relativeToChangelogFile="true"/>
|
||||
|
||||
</databaseChangeLog>
|
||||
Reference in New Issue
Block a user