Compare commits

..
Author SHA1 Message Date
james.maesandCodex 7d19cc9b79 ci: adopt current shared orb standard
Co-Authored-By: Codex <noreply@openai.com>
2026-10-21 14:46:45 -05:00
14 changed files with 29 additions and 465 deletions

No files matched your search

-1
View File
@@ -1 +0,0 @@
Central Java verification, tests and secret scan are supplied by the organization-scoped workflow.
+2 -2
View File
@@ -1,12 +1,12 @@
version: 2.1
setup: true
orbs:
qqq-orb: kingsrook/qqq-orb@0.8
qqq-orb: kingsrook/qqq-orb@0.7.18
workflows:
setup:
jobs:
- qqq-orb/generate_pipeline:
orb_version: '0.8'
orb_version: 0.7.18
filters:
branches:
ignore: automation/qbit-rc-pending
-7
View File
@@ -1,7 +0,0 @@
name: build
on:
push:
jobs:
maven:
uses: self:KofTwentyTwo/ci-workflows/.gitea/workflows/maven.yaml@v1
secrets: inherit
-1
View File
@@ -1,6 +1,5 @@
# Maven
target/
.flattened-pom.xml
*.jar
*.war
*.ear
+1 -1
View File
@@ -1,5 +1,5 @@
profile: qbit
orb_version: '0.8'
orb_version: 0.7.18
default_branch: main
java_version: '21'
browsers: true
-33
View File
@@ -7,39 +7,6 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
## [Unreleased]
## [0.2.0] - 2026-11-06
Published as `com.kingsrook.qbits:qbit-geo-data:0.2.0` on the QRun-IO Gitea Maven
registry. Requires Java 21 and QQQ 4.1 (built against `qqq-bom-pom` 4.1.0-RC.21).
This is the first release that works on QQQ 4.1; 0.1.0 was built against QQQ 0.35.0.
### Changed
- Build against the published QQQ 4.1 core (`qqq-bom-pom` 4.1.0-RC.21) instead of 0.35.0.
- `GeoDataQBitProducer.VERSION` is `0.2.0`.
- Import the project's `junit-bom` 5.10.0 ahead of the QQQ BOM so every JUnit artifact
converges on 5.10.0 and Surefire discovers the tests.
- CI runs on Gitea through the shared `KofTwentyTwo/ci-workflows` Maven workflow
(`main` verifies; a `vX.Y.Z` tag publishes to the Gitea registry).
### Fixed
- `Country`, `StateProvince` and `City` ask for their tables (`produceTableMetaData`)
and expose the setters `QRecordEntity` needs to discover fields, so the producer
creates `<prefix>_country`, `<prefix>_stateProvince` and `<prefix>_city` on core 4.1
instead of zero tables. The producer tolerates a table without fields.
- `GeoDataSyncStep` resolves the bundled `countryAlpha2` / `stateCode` source codes to
the persisted `countryId` / `stateProvinceId` before matching, so states and cities
are persisted (0.1.0 reported success after writing 250 countries and zero states or
cities because those source columns do not exist in the schema).
- Cities match on `stateProvinceId`, `name`, `latitude` and `longitude` at the schema's
seven-decimal precision; city names are not unique within a subdivision.
- Numeric comparisons use `BigDecimal` value equality, so a second run reports zero
inserts, updates or deactivations.
- An inactive existing row whose source entry returns is reactivated.
- Insert and update results are checked; a row rejected by the backend fails the sync
with a `QException` instead of reporting success.
## [0.1.0] - 2025-12-28
### Added
- Multi-module structure (qbit-geo-data-core + tools)
- Country, StateProvince, City entities with sync process
+2 -19
View File
@@ -7,33 +7,16 @@ Geographic reference data QBit providing countries, states/provinces, and cities
## Quick Start
The current release is **0.2.0**. It requires Java 21 and QQQ 4.1 (built against
`qqq-bom-pom` 4.1.0-RC.21). See [CHANGELOG.md](CHANGELOG.md).
Add the dependency to your `pom.xml`:
```xml
<dependency>
<groupId>com.kingsrook.qbits</groupId>
<artifactId>qbit-geo-data</artifactId>
<version>0.2.0</version>
<artifactId>qbit-geo-data-core</artifactId>
<version>${qbit-geo-data.version}</version>
</dependency>
```
Releases are published to the QRun-IO Gitea Maven registry, which requires
authentication for reads as well as writes. Add the repository and a matching
`<server>` entry (with your own Gitea username and token) in your private Maven
`settings.xml`:
```xml
<repository>
<id>qrun-io</id>
<url>https://git.kof22.com/api/packages/QRun-IO/maven</url>
<releases><enabled>true</enabled></releases>
<snapshots><enabled>false</enabled></snapshots>
</repository>
```
## What You Get
This QBit provides three entities with pre-populated reference data:
+6 -48
View File
@@ -50,8 +50,7 @@
</modules>
<properties>
<revision>0.2.0</revision>
<qqq.version>4.1.0-RC.21</qqq.version>
<revision>0.1.0</revision>
<maven.compiler.source>21</maven.compiler.source>
<maven.compiler.target>21</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
@@ -59,10 +58,11 @@
<dependencyManagement>
<dependencies>
<!-- One JUnit line. The QQQ 4.1 BOM inherits junit-jupiter-engine/params at 6.0.2 from qqq-parent-project, while this pom's JUnit BOM
manages junit-jupiter, the API and the platform at 5.10.0; the mixed engine fails discovery ("TestEngine with ID 'junit-jupiter'
failed to discover tests") and no test runs. Import this pom's own JUnit version first (first import wins) so every JUnit
artifact converges on 5.10.0. -->
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
</dependency>
<dependency>
<groupId>org.junit</groupId>
<artifactId>junit-bom</artifactId>
@@ -70,19 +70,6 @@
<type>pom</type>
<scope>import</scope>
</dependency>
<!-- Published QQQ 4.1 core (minimum QQQ 4.1), imported ahead of the parent's BOM. -->
<dependency>
<groupId>com.kingsrook.qqq</groupId>
<artifactId>qqq-bom-pom</artifactId>
<version>${qqq.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.json</groupId>
<artifactId>json</artifactId>
<version>20231013</version>
</dependency>
<dependency>
<groupId>org.assertj</groupId>
<artifactId>assertj-core</artifactId>
@@ -92,35 +79,6 @@
</dependencyManagement>
<build>
<plugins>
<!-- Resolve the CI-friendly ${revision} in the installed and deployed POMs so -->
<!-- consumers of the published artifacts can resolve the parent chain. -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>flatten-maven-plugin</artifactId>
<version>1.7.3</version>
<configuration>
<updatePomFile>true</updatePomFile>
<flattenMode>resolveCiFriendliesOnly</flattenMode>
</configuration>
<executions>
<execution>
<id>flatten</id>
<phase>process-resources</phase>
<goals>
<goal>flatten</goal>
</goals>
</execution>
<execution>
<id>flatten-clean</id>
<phase>clean</phase>
<goals>
<goal>clean</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
<pluginManagement>
<plugins>
<plugin>
@@ -28,7 +28,7 @@ public class GeoDataQBitProducer implements QBitProducer
{
public static final String GROUP_ID = "com.kingsrook.qbits";
public static final String ARTIFACT_ID = "qbit-geo-data";
public static final String VERSION = "0.2.0";
public static final String VERSION = "0.1.0";
private GeoDataQBitConfig config;
@@ -126,10 +126,6 @@ public class GeoDataQBitProducer implements QBitProducer
*******************************************************************************/
private void updateForeignKeyReferences(QTableMetaData table)
{
if(table.getFields() == null)
{
return;
}
table.getFields().values().forEach(field ->
{
if(field.getPossibleValueSourceName() != null)
@@ -14,7 +14,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
@QMetaDataProducingEntity(producePossibleValueSource = true)
public class City extends QRecordEntity
{
public static final String TABLE_NAME = "city";
@@ -204,103 +204,4 @@ public class City extends QRecordEntity
{
return modifyDate;
}
/*******************************************************************************
** Setter for id (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Setter for stateProvinceId (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setStateProvinceId(Integer stateProvinceId)
{
this.stateProvinceId = stateProvinceId;
}
/*******************************************************************************
** Setter for name (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Setter for asciiName (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setAsciiName(String asciiName)
{
this.asciiName = asciiName;
}
/*******************************************************************************
** Setter for population (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setPopulation(Integer population)
{
this.population = population;
}
/*******************************************************************************
** Setter for latitude (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setLatitude(BigDecimal latitude)
{
this.latitude = latitude;
}
/*******************************************************************************
** Setter for longitude (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setLongitude(BigDecimal longitude)
{
this.longitude = longitude;
}
/*******************************************************************************
** Setter for timezone (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setTimezone(String timezone)
{
this.timezone = timezone;
}
/*******************************************************************************
** Setter for isActive (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setIsActive(Boolean isActive)
{
this.isActive = isActive;
}
/*******************************************************************************
** Setter for createDate (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Setter for modifyDate (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
}
@@ -13,7 +13,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
@QMetaDataProducingEntity(producePossibleValueSource = true)
public class Country extends QRecordEntity
{
public static final String TABLE_NAME = "country";
@@ -171,85 +171,4 @@ public class Country extends QRecordEntity
{
return modifyDate;
}
/*******************************************************************************
** Setter for id (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Setter for alpha2Code (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setAlpha2Code(String alpha2Code)
{
this.alpha2Code = alpha2Code;
}
/*******************************************************************************
** Setter for alpha3Code (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setAlpha3Code(String alpha3Code)
{
this.alpha3Code = alpha3Code;
}
/*******************************************************************************
** Setter for numericCode (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setNumericCode(Integer numericCode)
{
this.numericCode = numericCode;
}
/*******************************************************************************
** Setter for name (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Setter for officialName (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setOfficialName(String officialName)
{
this.officialName = officialName;
}
/*******************************************************************************
** Setter for isActive (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setIsActive(Boolean isActive)
{
this.isActive = isActive;
}
/*******************************************************************************
** Setter for createDate (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Setter for modifyDate (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
}
@@ -13,7 +13,7 @@ import com.kingsrook.qqq.backend.core.model.data.QRecordEntity;
import com.kingsrook.qqq.backend.core.model.metadata.producers.annotations.QMetaDataProducingEntity;
@QMetaDataProducingEntity(produceTableMetaData = true, producePossibleValueSource = true)
@QMetaDataProducingEntity(producePossibleValueSource = true)
public class StateProvince extends QRecordEntity
{
public static final String TABLE_NAME = "stateProvince";
@@ -155,76 +155,4 @@ public class StateProvince extends QRecordEntity
{
return modifyDate;
}
/*******************************************************************************
** Setter for id (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setId(Integer id)
{
this.id = id;
}
/*******************************************************************************
** Setter for countryId (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setCountryId(Integer countryId)
{
this.countryId = countryId;
}
/*******************************************************************************
** Setter for code (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setCode(String code)
{
this.code = code;
}
/*******************************************************************************
** Setter for name (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setName(String name)
{
this.name = name;
}
/*******************************************************************************
** Setter for subdivisionType (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setSubdivisionType(String subdivisionType)
{
this.subdivisionType = subdivisionType;
}
/*******************************************************************************
** Setter for isActive (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setIsActive(Boolean isActive)
{
this.isActive = isActive;
}
/*******************************************************************************
** Setter for createDate (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setCreateDate(Instant createDate)
{
this.createDate = createDate;
}
/*******************************************************************************
** Setter for modifyDate (QRecordEntity discovers a field only from a getter and setter pair).
*******************************************************************************/
public void setModifyDate(Instant modifyDate)
{
this.modifyDate = modifyDate;
}
}
@@ -4,15 +4,13 @@
** Syncs all three entity types (Country, StateProvince, City) from bundled
** JSON files into database tables. Performs natural key upsert:
** - Country: by alpha2Code
** - StateProvince: by countryId + code (countryAlpha2 resolved to the persisted country)
** - City: by stateProvinceId + name + latitude + longitude (codes resolved to the persisted state)
** - StateProvince: by countryAlpha2 + code
** - City: by countryAlpha2 + stateCode + name
*******************************************************************************/
package com.kingsrook.qbits.geodata.sync;
import java.io.InputStream;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
@@ -64,39 +62,9 @@ public class GeoDataSyncStep implements BackendStep
// Sync in order: countries first (no dependencies), then states, then //
// cities (which may reference states) //
//////////////////////////////////////////////////////////////////////////
int countriesInserted = syncTable(prefix + "_country", loadJsonData("/data/countries.json"), List.of("alpha2Code"));
Map<String, QRecord> countries = queryExisting(prefix + "_country", List.of("alpha2Code"));
List<QRecord> states = loadJsonData("/data/states.json");
for(QRecord state : states)
{
QRecord country = countries.get(state.getValueString("countryAlpha2"));
if(country == null)
{
throw new QException("Geo Data state references an unavailable country");
}
state.setValue("countryId", country.getValue("id"));
state.getValues().remove("countryAlpha2");
}
int statesInserted = syncTable(prefix + "_stateProvince", states, List.of("countryId", "code"));
Map<String, QRecord> persistedStates = queryExisting(prefix + "_stateProvince", List.of("countryId", "code"));
List<QRecord> cities = loadJsonData("/data/cities.json");
for(QRecord city : cities)
{
QRecord country = countries.get(city.getValueString("countryAlpha2"));
QRecord state = country == null ? null : persistedStates.get(country.getValue("id") + "|" + city.getValueString("stateCode"));
if(state == null)
{
throw new QException("Geo Data city references an unavailable state");
}
city.setValue("stateProvinceId", state.getValue("id"));
for(String coordinate : List.of("latitude", "longitude"))
{
city.setValue(coordinate, new BigDecimal(city.getValue(coordinate).toString()).setScale(7, RoundingMode.HALF_UP));
}
city.getValues().remove("countryAlpha2");
city.getValues().remove("stateCode");
}
int citiesInserted = syncTable(prefix + "_city", cities, List.of("stateProvinceId", "name", "latitude", "longitude"));
int countriesInserted = syncTable(prefix + "_country", "/data/countries.json", List.of("alpha2Code"));
int statesInserted = syncTable(prefix + "_stateProvince", "/data/states.json", List.of("countryAlpha2", "code"));
int citiesInserted = syncTable(prefix + "_city", "/data/cities.json", List.of("countryAlpha2", "stateCode", "name"));
LOG.info("Geo data sync complete",
logPair("prefix", prefix),
@@ -115,15 +83,17 @@ public class GeoDataSyncStep implements BackendStep
** Sync a single table from JSON resource.
** Returns count of records processed.
*******************************************************************************/
private int syncTable(String tableName, List<QRecord> sourceRecords, List<String> naturalKeyFields) throws QException
private int syncTable(String tableName, String resourcePath, List<String> naturalKeyFields) throws QException
{
LOG.info("Syncing table",
logPair("table", tableName),
logPair("resource", resourcePath),
logPair("naturalKey", naturalKeyFields));
/////////////////////////////////////////////////////////////////////////
// 1. Load source data from classpath JSON //
/////////////////////////////////////////////////////////////////////////
List<QRecord> sourceRecords = loadJsonData(resourcePath);
LOG.info("Loaded source data", logPair("count", sourceRecords.size()));
/////////////////////////////////////////////////////////////////////////
@@ -149,7 +119,7 @@ public class GeoDataSyncStep implements BackendStep
source.setValue("isActive", true);
toInsert.add(source);
}
else if(hasChanges(source, existing) || !Boolean.TRUE.equals(existing.getValueBoolean("isActive")))
else if(hasChanges(source, existing))
{
source.setValue("id", existing.getValue("id"));
source.setValue("isActive", true);
@@ -212,7 +182,7 @@ public class GeoDataSyncStep implements BackendStep
key.append("|");
}
Object value = record.getValue(field);
key.append(value instanceof Number ? new BigDecimal(value.toString()).stripTrailingZeros().toPlainString() : value != null ? value.toString() : "");
key.append(value != null ? value.toString() : "");
}
return key.toString();
}
@@ -295,14 +265,7 @@ public class GeoDataSyncStep implements BackendStep
}
Object sourceValue = source.getValue(fieldName);
Object existingValue = existing.getValue(fieldName);
if(sourceValue instanceof Number && existingValue instanceof Number)
{
if(new BigDecimal(sourceValue.toString()).compareTo(new BigDecimal(existingValue.toString())) != 0)
{
return true;
}
}
else if(!Objects.equals(sourceValue, existingValue))
if(!Objects.equals(sourceValue, existingValue))
{
return true;
}
@@ -320,7 +283,7 @@ public class GeoDataSyncStep implements BackendStep
InsertInput insertInput = new InsertInput();
insertInput.setTableName(tableName);
insertInput.setRecords(records);
requireSuccess(tableName, records.size(), new InsertAction().execute(insertInput).getRecords());
new InsertAction().execute(insertInput);
}
@@ -333,16 +296,6 @@ public class GeoDataSyncStep implements BackendStep
UpdateInput updateInput = new UpdateInput();
updateInput.setTableName(tableName);
updateInput.setRecords(records);
requireSuccess(tableName, records.size(), new UpdateAction().execute(updateInput).getRecords());
}
/*******************************************************************************
** Do not report a successful sync when a native action rejected rows.
*******************************************************************************/
private void requireSuccess(String tableName, int expected, List<QRecord> records) throws QException
{
if(records == null || records.size() != expected || records.stream().anyMatch(record -> !record.getErrors().isEmpty()))
{
throw new QException("Geo Data persistence failed for table " + tableName);
}
new UpdateAction().execute(updateInput);
}
}
@@ -78,38 +78,6 @@ class GeoDataQBitProducerTest
/*******************************************************************************
** The enabled entities produce their tables, on the configured backend, with the
** prefix; the possible value sources point at tables that exist. (On core 4.1 an
** entity annotation only produces a table when it asks for it.)
*******************************************************************************/
@Test
void testProduce_createsTablesAndPossibleValueSourcesPointAtThem() throws QException
{
QInstance qInstance = new QInstance();
qInstance.addBackend(new QBackendMetaData().withName("rdbms"));
new GeoDataQBitProducer()
.withConfig(new GeoDataQBitConfig().withBackendName("rdbms").withTableNamePrefix("geo")
.withEnableCountries(true).withEnableStateProvinces(true).withEnableCities(true))
.produce(qInstance, "geo");
assertThat(qInstance.getTables().keySet()).contains("geo_country", "geo_stateProvince", "geo_city");
assertThat(qInstance.getTable("geo_city").getBackendName()).isEqualTo("rdbms");
assertThat(qInstance.getTable("geo_country").getPrimaryKeyField()).isEqualTo("id");
assertThat(qInstance.getTable("geo_country").getFields().keySet()).contains("alpha2Code", "name", "isActive");
assertThat(qInstance.getTable("geo_city").getFields().keySet()).contains("name");
for(var pvs : qInstance.getPossibleValueSources().values())
{
if(pvs.getTableName() != null)
{
assertThat(qInstance.getTable(pvs.getTableName())).as(pvs.getName()).isNotNull();
}
}
}
/*******************************************************************************
** Test that producer stores config in QBit metadata.
*******************************************************************************/
@@ -165,6 +133,6 @@ class GeoDataQBitProducerTest
{
assertThat(GeoDataQBitProducer.GROUP_ID).isEqualTo("com.kingsrook.qbits");
assertThat(GeoDataQBitProducer.ARTIFACT_ID).isEqualTo("qbit-geo-data");
assertThat(GeoDataQBitProducer.VERSION).isEqualTo("0.2.0");
assertThat(GeoDataQBitProducer.VERSION).isEqualTo("0.1.0");
}
}