Files
qbit-document-generation/foundation/src/main/java/com/kingsrook/qbits/documentgeneration/GenerationPreviewSources.java
T
ee782b7d6e
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-3 (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-business (push) Skipped
QRun-IO/ci-policy: ci / original-mcp (push) Skipped
QRun-IO/ci-policy: ci / original-voyage (push) Skipped
QRun-IO/ci-policy: ci / original-cli (push) Skipped
QRun-IO/ci-policy: ci / not-enrolled (push) Failing after 7s
QRun-IO/ci-policy: native / fork-deny (push) Skipped
QRun-IO/ci-policy: native / dotnet-0 (push) Skipped
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
QRun-IO/ci-policy: ci / ci-result (push) Failing after 5s
QRun-IO/ci-policy: native / native-result (push) Successful in 6s
chore: preserve unfinished document generation source and status
Checkpoint implementation at user-requested stop; document known failures and incomplete release qualification.

Co-Authored-By: Claude <noreply@anthropic.com>

Co-Authored-By: GPT-6 Codex <noreply@openai.com>
2026-11-04 10:45:09 -06:00

148 lines
7.4 KiB
Java

/*
* QQQ - Low-code Application Framework for Engineers.
* Copyright (C) 2021-2025. Kingsrook, LLC
* 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States
* contact@kingsrook.com
* https://github.com/Kingsrook/
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.kingsrook.qbits.documentgeneration;
import java.io.Serializable;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionEnforcement;
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
import com.kingsrook.qqq.backend.core.actions.values.QCustomPossibleValueProvider;
import com.kingsrook.qqq.backend.core.actions.values.QValueFormatter;
import com.kingsrook.qqq.backend.core.context.QContext;
import com.kingsrook.qqq.backend.core.exceptions.QBadRequestException;
import com.kingsrook.qqq.backend.core.exceptions.QException;
import com.kingsrook.qqq.backend.core.exceptions.QNotFoundException;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QCriteriaOperator;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QFilterOrderBy;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QQueryFilter;
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
import com.kingsrook.qqq.backend.core.model.actions.values.SearchPossibleValueSourceInput;
import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldType;
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
/***************************************************************************
** Preview selection reads the configured source through current native authority.
***************************************************************************/
public class GenerationPreviewSources implements QCustomPossibleValueProvider<Serializable>
{
/***************************************************************************
** A bare ID has no source identity; exact refresh uses the named search source.
***************************************************************************/
@Override
public QPossibleValue<Serializable> getPossibleValue(Serializable id) throws QException
{
throw new QBadRequestException("Resolve preview records through their source-specific selector.");
}
/***************************************************************************
** One bounded page, with enforced query and exact read checks; never raw totals.
***************************************************************************/
@Override
public List<QPossibleValue<Serializable>> search(SearchPossibleValueSourceInput input) throws QException
{
GenerationSecurity.require("configure");
GenerationSecurity.require("generate");
var instance = QContext.getQInstance();
var source = instance.getPossibleValueSource(input.getPossibleValueSourceName());
var config = DocumentGenerationQBitConfig.from(instance);
String sourceTable = config.getSourceTables().stream()
.filter(name -> GenerationAuthoringProcesses.sourceRecordChoices(name).equals(input.getPossibleValueSourceName()))
.findFirst().orElse(null);
if(GenerationAuthoringProcesses.PREVIEW_SOURCE.equals(input.getPossibleValueSourceName()))
{
// The standard process route is keyed by the static field; the form says which source table is selected.
// Only a configured table is accepted, and every record below is still read under the caller's own authority.
Object selected = input.getOtherValues() == null ? null : input.getOtherValues().get("sourceTable");
sourceTable = selected instanceof String name && config.getSourceTables().contains(name) ? name : null;
}
if(source == null || sourceTable == null)
{
throw new QBadRequestException("Unknown preview source.");
}
var table = instance.getTable(sourceTable);
String term = Objects.requireNonNullElse(input.getSearchTerm(), "");
int skip = Objects.requireNonNullElse(input.getSkip(), 0);
if(term.length() > 128 || skip < 0 || skip > 10000 || input.getIdList() != null && input.getIdList().size() > 50)
{
throw new QBadRequestException("Narrow the preview record search.");
}
var filter = new QQueryFilter().withLimit(Math.max(1, Math.min(50, Objects.requireNonNullElse(input.getLimit(), 50))))
.withSkip(skip).withOrderBy(new QFilterOrderBy(table.getPrimaryKeyField()));
List<String> labels = Objects.requireNonNullElse(table.getRecordLabelFields(), List.of());
List<String> safeLabels = labels.stream().filter(name -> table.getField(name) != null && !Boolean.TRUE.equals(table.getField(name).getIsHidden())
&& table.getField(name).getType() == QFieldType.STRING).toList();
if(input.getIdList() != null)
{
filter.withCriteria(table.getPrimaryKeyField(), QCriteriaOperator.IN, input.getIdList());
}
else if(!term.isBlank())
{
if(safeLabels.isEmpty())
{
// Without a text label the only searchable key is the primary key; a term that is not a valid key matches nothing.
try
{
filter.withCriteria(table.getPrimaryKeyField(), QCriteriaOperator.EQUALS,
com.kingsrook.qqq.backend.core.utils.ValueUtils.getValueAsFieldType(table.getField(table.getPrimaryKeyField()).getType(), term));
}
catch(Exception notAKey)
{
return (new ArrayList<>());
}
}
else
{
var search = new QQueryFilter().withBooleanOperator(QQueryFilter.BooleanOperator.OR);
safeLabels.forEach(name -> search.withCriteria(name, QCriteriaOperator.CONTAINS, term));
filter.withSubFilter(search);
}
}
QueryInput query = new QueryInput(table.getName()).withFilter(filter).withShouldFetchHeavyFields(false);
query.setPermissionEnforcement(PermissionEnforcement.ENFORCE);
List<QPossibleValue<Serializable>> result = new ArrayList<>();
for(var candidate : new QueryAction().execute(query).getRecords())
{
Serializable id = candidate.getValue(table.getPrimaryKeyField());
if(id == null)
{
continue;
}
try
{
var record = GenerationSecurity.source(new GenerationPorts.Source(table.getName(), id.toString()));
String label = labels.isEmpty() || !safeLabels.containsAll(labels) || !record.getValues().keySet().containsAll(labels)
? id.toString() : QValueFormatter.formatRecordLabel(table, record);
result.add(new QPossibleValue<>(id, label));
}
catch(QNotFoundException denied)
{
// Exact Get customizers may narrow Query results; denied labels are omitted.
}
}
return (result);
}
}