Compare commits

..
Author SHA1 Message Date
forge-recovery 32e6146e08 ci: enroll shared Gitea lifecycle
QRun-IO/ci-policy: ci / fork-guard (push) Skipped
QRun-IO/ci-policy: ci / head-profile (push) Skipped
QRun-IO/ci-policy: ci / application-template-0 (push) Skipped
QRun-IO/ci-policy: ci / gradle-1 (push) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (push) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (push) Skipped
QRun-IO/ci-policy: ci / node-8 (push) Skipped
QRun-IO/ci-policy: ci / node-9 (push) Skipped
QRun-IO/ci-policy: ci / node-10 (push) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (push) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (push) Skipped
QRun-IO/ci-policy: ci / source-config-13 (push) Skipped
QRun-IO/ci-policy: ci / source-config-14 (push) Skipped
QRun-IO/ci-policy: ci / android-0 (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Skipped
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
Shared Java Maven CI with Docker / secrets (push) Successful in 27s
QRun-IO/ci-policy: native / native-result (push) Successful in 8s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (push) Successful in 26s
QRun-IO/ci-policy: ci / repository-policy (push) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (push) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (push) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (push) Skipped
Shared Java Maven CI with Docker / verify (push) Successful in 1m38s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (push) Successful in 1m54s
Shared Java Maven CI with Docker / gate (push) Successful in 21s
qrun-release / release (push) Successful in 23m31s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (push) Successful in 19s
QRun-IO/ci-policy: ci / java-docker-3 (push) Successful in 23m32s
QRun-IO/ci-policy: ci / ci-result (push) Successful in 13s
2026-11-02 22:11:14 +00:00
18b3b64839 ci: add the isolated postpublish job to the QRun release caller
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / head-profile (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-10 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-11 (pull_request) Skipped
QRun-IO/ci-policy: ci / repository-policy-12 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-13 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-14 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
Shared Java Maven CI with Docker / secrets (push) Successful in 13s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / repository-policy (pull_request) Skipped
QRun-IO/ci-policy: ci / java-core-2-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-docker-3-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-6-head (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-7-head (pull_request) Skipped
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 4s
Shared Java Maven CI with Docker / verify (push) Successful in 1m17s
Shared Java Maven CI with Docker / gate (push) Successful in 10s
qrun-release / release (push) Successful in 2h34m17s
qrun-release / postpublish (push) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Successful in 1m11s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Successful in 11s
QRun-IO/ci-policy: ci / java-docker-3 (pull_request) Successful in 2h35m28s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 6s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 15:47:02 +00:00
4993c0f837 ci: move the QRun release caller to the thin v2 contract
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / application-template-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-1 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-frontend-browser-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-6 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-7 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-playwright-8 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-9 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: native / fork-deny (pull_request) Skipped
QRun-IO/ci-policy: native / dotnet-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / ci-result (pull_request) Failing after 3s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Canceled after 0s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Canceled after 0s
QRun-IO/ci-policy: ci / java-docker-2 (pull_request) Canceled after 1h22m31s
Shared Java Maven CI with Docker / verify (push) Canceled after 0s
qrun-release / release (push) Canceled after 1h22m35s
Shared Java Maven CI with Docker / gate (push) Canceled after 0s
Shared Java Maven CI with Docker / secrets (push) Successful in 23s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 22s
QRun-IO/ci-policy: native / native-result (pull_request) Successful in 8s
Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 06:16:36 +00:00
forge-recoveryandCodex 710f480549 ci: add the identical QRun release caller
QRun-IO/ci-policy: ci / fork-guard (pull_request) Skipped
QRun-IO/ci-policy: ci / gradle-0 (pull_request) Skipped
QRun-IO/ci-policy: ci / node-3 (pull_request) Skipped
QRun-IO/ci-policy: ci / java-maven-2 (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / secrets (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / node-4 (pull_request) Skipped
QRun-IO/ci-policy: ci / source-config-5 (pull_request) Skipped
QRun-IO/ci-policy: ci / not-enrolled (pull_request) Skipped
QRun-IO/ci-policy: Shared Java Maven CI with Docker / verify (pull_request) Successful in 1m19s
QRun-IO/ci-policy: Shared Java Maven CI with Docker / gate (pull_request) Successful in 12s
QRun-IO/ci-policy: ci / java-docker-1 (pull_request) Successful in 14m10s
QRun-IO/ci-policy: ci / ci-result (pull_request) Successful in 5s
Co-Authored-By: Codex <noreply@openai.com>
2026-11-02 03:26:03 +00:00
james.maesandCodex 5f87b54ae3 ci: enroll in the shared QQQ orb (#41)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 19:50:20 -05:00
james.maesandCodex 2d24bf8bf2 ci: use shared orb pipelines and source scanning (#36)
Co-authored-by: Codex <noreply@openai.com>
2026-10-21 10:43:00 -05:00
james.maes ba23ccebbe Merge pull request #11 from QRun-IO/develop
release: bring 0.1.1 (TagRuleEngine PVS fix) into main
2026-10-12 20:09:19 -05:00
james.maes dd9442f37c Merge pull request #10 from QRun-IO/fix/tag-rule-pvs-table
fix(tag-rules): resolve joined table through the FK field's possible-value source
2026-10-12 19:49:43 -05:00
james.maesandClaude Opus 5.5 26e62f3bcc fix(purge): resolve tag-rule joined tables via the FK field's possible-value source
{blog.slug} used the prefix as both the FK field name and the table name, so
apps with prefixed tables (cms_blog) silently lost those purge paths. The
joined table now comes from the <prefix>Id field's possible-value source,
falling back to the bare prefix. A missing joined table is logged once.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 19:41:32 -05:00
CircleCI 29a39a491c Bump version to 0.1.0 [skip ci] 2026-10-12 21:31:19 +00:00
CircleCI 77d2fdd55f Bump version to 0.1.0-RC.1 [skip ci] 2026-10-12 21:24:51 +00:00
james.maes a64b1223d2 Merge pull request #8 from QRun-IO/feature/ga-only-release-tags
ci: publish only GA tags (vX.Y.Z), never RC tags
2026-10-12 16:01:41 -05:00
james.maesandClaude Opus 5.5 922b53c82f ci: publish only GA tags (vX.Y.Z), never RC tags
An RC tag matched /v.*/ and the orb republished it as the next RC.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-12 15:58:56 -05:00
james.maes 925c2c9134 Merge pull request #3 from QRun-IO/feature/cdn-qbit-complete
fix(app): CDN home without tables; build: qqq-bom 4.1.0-RC.4 profile
2026-10-12 14:59:25 -05:00
9 changed files with 228 additions and 76 deletions

No files matched your search

+11 -68
View File
@@ -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+$/
+25
View File
@@ -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
+23
View File
@@ -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
View File
@@ -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
+5
View File
@@ -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
+12
View File
@@ -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
+1 -1
View File
@@ -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.