Compare commits
14
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
32e6146e08
|
||
|
|
18b3b64839
|
||
|
|
4993c0f837
|
||
|
|
710f480549
|
||
|
|
5f87b54ae3 | ||
|
|
2d24bf8bf2 | ||
|
|
ba23ccebbe | ||
|
|
dd9442f37c | ||
|
|
26e62f3bcc
|
||
|
|
29a39a491c | ||
|
|
77d2fdd55f | ||
|
|
a64b1223d2 | ||
|
|
922b53c82f | ||
|
|
925c2c9134 |
No files matched your search
+11
-68
@@ -1,71 +1,14 @@
|
||||
version: 2.1
|
||||
|
||||
setup: true
|
||||
orbs:
|
||||
qqq-orb: kingsrook/qqq-orb@0.6.8
|
||||
|
||||
commands:
|
||||
configure-package-install:
|
||||
steps:
|
||||
- run:
|
||||
name: Keep package installation unattended
|
||||
command: |
|
||||
# These build machines are discarded after the job.
|
||||
sudo install -d /etc/needrestart/conf.d
|
||||
printf '%s\n' '$nrconf{restart} = "l";' | sudo tee /etc/needrestart/conf.d/99-qqq-ci.conf >/dev/null
|
||||
|
||||
qqq-orb: kingsrook/qqq-orb@0.8
|
||||
workflows:
|
||||
test_only:
|
||||
jobs:
|
||||
- qqq-orb/mvn_test_only:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
filters:
|
||||
branches:
|
||||
ignore: /(develop|release\/.*|hotfix\/.*)/
|
||||
|
||||
publish_snapshot:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: snapshot
|
||||
filters:
|
||||
branches:
|
||||
only: [develop]
|
||||
|
||||
publish_release_candidate:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release_candidate
|
||||
filters:
|
||||
branches:
|
||||
only: [/release\/.*/]
|
||||
|
||||
publish_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: release
|
||||
filters:
|
||||
branches:
|
||||
ignore: /.*/
|
||||
tags:
|
||||
only: [/v.*/]
|
||||
|
||||
publish_hotfix_release:
|
||||
jobs:
|
||||
- qqq-orb/mvn_publish:
|
||||
pre-steps:
|
||||
- configure-package-install
|
||||
context: [qqq-maven-registry-credentials]
|
||||
branch_type: hotfix
|
||||
filters:
|
||||
branches:
|
||||
only: [/hotfix\/.*/]
|
||||
setup:
|
||||
jobs:
|
||||
- qqq-orb/generate_pipeline:
|
||||
orb_version: '0.8'
|
||||
filters:
|
||||
branches:
|
||||
ignore: automation/qbit-rc-pending
|
||||
tags:
|
||||
only: /^v\d+\.\d+\.\d+$/
|
||||
@@ -0,0 +1,25 @@
|
||||
# Standard QRun release caller (v2, thin contract): every push and manual dispatch delegates to the protected qrun-release-v1 channel. Do not edit per repository.
|
||||
name: qrun-release
|
||||
on:
|
||||
push: {}
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
jobs:
|
||||
release:
|
||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/java-docker.yaml@qrun-release-v1
|
||||
with:
|
||||
runner: linux-small
|
||||
docker-runner: container-build
|
||||
browsers: true
|
||||
browser-runner: browser-build
|
||||
postpublish:
|
||||
if: ${{ needs.release.outputs.capture == 'true' }}
|
||||
needs: release
|
||||
permissions:
|
||||
contents: write
|
||||
actions: write
|
||||
uses: self:KofTwentyTwo/platform-ci/.gitea/workflows/qrun-source-version.yaml@qrun-release-v1
|
||||
with:
|
||||
runner: linux-small
|
||||
@@ -0,0 +1,23 @@
|
||||
name: CodeQL
|
||||
'on':
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
- develop
|
||||
- release/**
|
||||
- hotfix/**
|
||||
pull_request: {}
|
||||
schedule:
|
||||
- cron: 41 16 * * 0
|
||||
workflow_dispatch: {}
|
||||
permissions:
|
||||
contents: read
|
||||
actions: read
|
||||
security-events: write
|
||||
packages: read
|
||||
concurrency:
|
||||
group: codeql-${{ github.event.pull_request.number || github.ref }}
|
||||
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
|
||||
jobs:
|
||||
analyze:
|
||||
uses: QRun-IO/qqq-orb/.github/workflows/qbit-codeql.yml@c288a8864fe9054f68410e1d78f5863aea4c4d22
|
||||
+16
@@ -0,0 +1,16 @@
|
||||
profile: qbit
|
||||
orb_version: '0.8'
|
||||
default_branch: develop
|
||||
java_version: '21'
|
||||
browsers: true
|
||||
api_checks: false
|
||||
docs_only: false
|
||||
release_on_main: false
|
||||
release_tag_policy: stable
|
||||
test_branch_policy: non-release-no-hotfix
|
||||
publish:
|
||||
snapshot: true
|
||||
release_candidate: true
|
||||
release: true
|
||||
hotfix: true
|
||||
asciidoc: false
|
||||
@@ -2,6 +2,11 @@
|
||||
|
||||
All notable changes to this project are documented here. The format follows Keep a Changelog; versions follow semantic versioning.
|
||||
|
||||
## [0.1.1] - Unreleased
|
||||
|
||||
### Fixed
|
||||
- Tag rule `{prefix.field}` placeholders now resolve the joined table from the `<prefix>Id` field's possible-value source, so prefixed tables (`cms_blog`) work; a missing joined table is logged instead of silently skipped.
|
||||
|
||||
## [0.1.0] - Unreleased
|
||||
|
||||
### Added
|
||||
|
||||
@@ -31,6 +31,18 @@ Design: QRun-IO/qqq `docs/specs/2026-10-12-qbit-cdn-design.md`.
|
||||
NONE. See `docs/cloudfront-expected-config.md`.
|
||||
- **Permissions:** `cdn.view`, `cdn.purge`, `cdn.purgeAll`, `cdn.admin`.
|
||||
|
||||
## Purge-path placeholders
|
||||
|
||||
`cdn_table_tag_rule` templates (`tagTemplates`, `invalidationPaths`) can use `{field}` for the changed record's own fields and `{prefix.field}` for a joined record, e.g. `/blog/{blog.slug}/{slug}*`.
|
||||
|
||||
A joined placeholder is resolved like this:
|
||||
|
||||
1. The foreign-key field is `<prefix>Id` on the rule's table (`blogId` for `{blog.slug}`).
|
||||
2. The joined table is the one behind that field's possible-value source: the source's table name for a table-type source, or the source name when it names a table. So `{blog.slug}` reads `cms_blog` when `blogId` has a possible-value source on `cms_blog`.
|
||||
3. With no such field or possible-value source, the bare prefix is the table name (`blog`).
|
||||
|
||||
If the joined table does not exist, the placeholder is skipped (the rest of the template is dropped, as for any missing value) and a warning is logged once per table per rule evaluation.
|
||||
|
||||
## Install
|
||||
|
||||
```java
|
||||
|
||||
@@ -18,7 +18,7 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
|
||||
<properties>
|
||||
<revision>0.1.0-SNAPSHOT</revision>
|
||||
<revision>0.1.0</revision>
|
||||
<maven.compiler.source>21</maven.compiler.source>
|
||||
<maven.compiler.target>21</maven.compiler.target>
|
||||
<jacoco.version>0.8.15</jacoco.version>
|
||||
|
||||
@@ -34,11 +34,16 @@ import com.kingsrook.qbits.cdn.tags.TagTemplate;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.GetAction;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.logging.QLogger;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.get.GetInput;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSourceType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.utils.StringUtils;
|
||||
import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
import static com.kingsrook.qqq.backend.core.logging.LogUtils.logPair;
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
@@ -54,13 +59,20 @@ import com.kingsrook.qqq.backend.core.utils.ValueUtils;
|
||||
** purges nothing.
|
||||
**
|
||||
** A dotted placeholder "{blog.slug}" reads field "slug" of the record that
|
||||
** field "blogId" points to in table "blog" (the xId convention).
|
||||
** field "blogId" points to (the xId convention). The joined table is the one
|
||||
** named by that field's possible-value source (its table name, or the source
|
||||
** name when that names a table), so apps that prefix their tables ("cms_blog")
|
||||
** work; with no field or possible-value source, the bare prefix ("blog") is
|
||||
** the table name. An unresolvable join is skipped, and logged once.
|
||||
*******************************************************************************/
|
||||
public class TagRuleEngine
|
||||
{
|
||||
private static final Pattern PLACEHOLDER = Pattern.compile("\\{([A-Za-z0-9_]+)\\.([A-Za-z0-9_]+)}");
|
||||
|
||||
private final Map<String, QRecord> joinCache = new HashMap<>();
|
||||
private static final QLogger LOG = QLogger.getLogger(TagRuleEngine.class);
|
||||
|
||||
private final Map<String, QRecord> joinCache = new HashMap<>();
|
||||
private final Set<String> warnedMissing = new LinkedHashSet<>();
|
||||
|
||||
|
||||
|
||||
@@ -172,7 +184,7 @@ public class TagRuleEngine
|
||||
{
|
||||
continue;
|
||||
}
|
||||
QRecord target = lookup(joined, foreignKey);
|
||||
QRecord target = lookup(rule.getValueString("tableName"), joined, foreignKey);
|
||||
if(target != null)
|
||||
{
|
||||
values.put(joined + "." + field, target.getValue(field));
|
||||
@@ -184,17 +196,57 @@ public class TagRuleEngine
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One joined record, cached for the life of this engine.
|
||||
** The table a placeholder prefix joins to: the table behind the possible-value
|
||||
** source of the prefix's "xId" field on the source table, else the bare
|
||||
** prefix.
|
||||
*******************************************************************************/
|
||||
private QRecord lookup(String tableName, Serializable id) throws QException
|
||||
static String joinedTableName(String sourceTable, String prefix)
|
||||
{
|
||||
String key = tableName + "#" + id;
|
||||
QFieldMetaData field = null;
|
||||
if(sourceTable != null)
|
||||
{
|
||||
QTableMetaData source = QContext.getQInstance().getTable(sourceTable);
|
||||
field = source == null ? null : source.getFields().get(prefix + "Id");
|
||||
}
|
||||
String pvsName = field == null ? null : field.getPossibleValueSourceName();
|
||||
if(pvsName == null)
|
||||
{
|
||||
return (prefix);
|
||||
}
|
||||
QPossibleValueSource pvs = QContext.getQInstance().getPossibleValueSource(pvsName);
|
||||
if(pvs != null && pvs.getType() == QPossibleValueSourceType.TABLE && StringUtils.hasContent(pvs.getTableName()))
|
||||
{
|
||||
return (pvs.getTableName());
|
||||
}
|
||||
return (pvsName);
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** One joined record, cached for the life of this engine. A missing table is
|
||||
** logged once and yields null.
|
||||
*******************************************************************************/
|
||||
private QRecord lookup(String sourceTable, String prefix, Serializable id) throws QException
|
||||
{
|
||||
String tableName = joinedTableName(sourceTable, prefix);
|
||||
String key = tableName + "#" + id;
|
||||
if(joinCache.containsKey(key))
|
||||
{
|
||||
return (joinCache.get(key));
|
||||
}
|
||||
QTableMetaData table = QContext.getQInstance().getTable(tableName);
|
||||
QRecord found = table == null ? null : new GetAction().executeForRecord(new GetInput(tableName).withPrimaryKey(id));
|
||||
if(table == null)
|
||||
{
|
||||
if(warnedMissing.add(tableName))
|
||||
{
|
||||
LOG.warn("A CDN tag rule placeholder joins to a table that does not exist; its values are skipped",
|
||||
logPair("placeholder", prefix), logPair("sourceTable", sourceTable), logPair("table", tableName));
|
||||
}
|
||||
joinCache.put(key, null);
|
||||
return (null);
|
||||
}
|
||||
QRecord found = new GetAction().executeForRecord(new GetInput(tableName).withPrimaryKey(id));
|
||||
joinCache.put(key, found);
|
||||
return (found);
|
||||
}
|
||||
|
||||
@@ -18,6 +18,11 @@ package com.kingsrook.qbits.cdn.purge;
|
||||
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValueSource;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qbits.cdn.CdnTestSupport;
|
||||
import com.kingsrook.qbits.cdn.planning.InvalidationPath;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
@@ -58,6 +63,77 @@ class TagRuleEngineTest
|
||||
}
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** The joined table comes from the FK field's table possible-value source, so
|
||||
** a prefixed table resolves {blog.slug}; a PVS named for a table also works.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void resolvesPrefixedTableViaPossibleValueSource() throws Exception
|
||||
{
|
||||
CdnTestSupport.setUp();
|
||||
var instance = QContext.getQInstance();
|
||||
instance.addTable(new QTableMetaData().withName("cms_blog").withBackendName(CdnTestSupport.MEMORY).withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("slug", QFieldType.STRING)));
|
||||
instance.addPossibleValueSource(QPossibleValueSource.newForTable("cms_blog").withName("blogPvs"));
|
||||
instance.getTable("post").getFields().get("blogId").setPossibleValueSourceName("blogPvs");
|
||||
CdnTestSupport.insert("cms_blog", new QRecord().withValue("slug", "prefixed"));
|
||||
|
||||
QRecord rule = new QRecord().withValue("tableName", "post").withValue("invalidationPaths", "/blog/{blog.slug}/{slug}*");
|
||||
QRecord post = new QRecord().withValue("id", 1).withValue("slug", "a").withValue("blogId", 1);
|
||||
assertThat(new TagRuleEngine().evaluate(rule, List.of(post), List.of()).paths()).containsExactly(InvalidationPath.leaf("/blog/prefixed/a*"));
|
||||
|
||||
////////////////////////////////////////////////////////////
|
||||
// a PVS whose name is itself a table (not TABLE-typed) //
|
||||
////////////////////////////////////////////////////////////
|
||||
instance.getTable("post").getFields().get("blogId").setPossibleValueSourceName("cms_blog");
|
||||
assertThat(TagRuleEngine.joinedTableName("post", "blog")).isEqualTo("cms_blog");
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** No field, no PVS, or no source table: the bare prefix is the table name.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void fallsBackToBarePrefix() throws Exception
|
||||
{
|
||||
CdnTestSupport.setUp();
|
||||
assertThat(TagRuleEngine.joinedTableName("post", "blog")).isEqualTo("blog");
|
||||
assertThat(TagRuleEngine.joinedTableName("post", "nofield")).isEqualTo("nofield");
|
||||
assertThat(TagRuleEngine.joinedTableName("nosuchtable", "blog")).isEqualTo("blog");
|
||||
assertThat(TagRuleEngine.joinedTableName(null, "blog")).isEqualTo("blog");
|
||||
CdnTestSupport.insert("blog", new QRecord().withValue("slug", "bare"));
|
||||
QRecord rule = new QRecord().withValue("tableName", "post").withValue("invalidationPaths", "/blog/{blog.slug}");
|
||||
QRecord post = new QRecord().withValue("id", 1).withValue("blogId", 1);
|
||||
assertThat(new TagRuleEngine().evaluate(rule, List.of(post), List.of()).paths()).containsExactly(InvalidationPath.leaf("/blog/bare"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** A missing FK field value, or a missing joined table, skips the placeholder
|
||||
** without throwing.
|
||||
*******************************************************************************/
|
||||
@Test
|
||||
void missingFieldOrTableIsSkipped() throws Exception
|
||||
{
|
||||
CdnTestSupport.setUp();
|
||||
QRecord rule = new QRecord().withValue("tableName", "post").withValue("invalidationPaths", "/blog/{blog.slug}\n/x/{ghost.slug}\n/y/{slug}");
|
||||
|
||||
QRecord noFk = new QRecord().withValue("id", 1).withValue("slug", "s");
|
||||
assertThat(new TagRuleEngine().evaluate(rule, List.of(noFk), List.of()).paths()).containsExactly(InvalidationPath.leaf("/y/s"));
|
||||
|
||||
QRecord dangling = new QRecord().withValue("id", 2).withValue("slug", "s").withValue("ghostId", 7);
|
||||
TagRuleEngine engine = new TagRuleEngine();
|
||||
assertThat(engine.evaluate(rule, List.of(dangling, dangling), List.of()).paths()).containsExactly(InvalidationPath.leaf("/y/s"));
|
||||
|
||||
QContext.getQInstance().getTable("post").getFields().get("blogId").setPossibleValueSourceName("nowhere");
|
||||
QRecord toNowhere = new QRecord().withValue("id", 3).withValue("slug", "s").withValue("blogId", 1);
|
||||
assertThat(new TagRuleEngine().evaluate(rule, List.of(toNowhere), List.of()).paths()).containsExactly(InvalidationPath.leaf("/y/s"));
|
||||
}
|
||||
|
||||
|
||||
|
||||
/*******************************************************************************
|
||||
** Stored text round-trips; invalid lines are dropped.
|
||||
|
||||
Reference in new issue
Block a user