feat(qbits): wire demo qbits into core and full profiles (#6) #16
No files matched your search
@@ -0,0 +1,33 @@
|
||||
-- Add the webhook qbit's protected resources to the demo OIDC roles.
|
||||
-- Runs after C6's 02-auth-and-roles.sql during PostgreSQL initialization.
|
||||
INSERT INTO permission (name, object_type, object_label)
|
||||
SELECT table_name || '.' || action, 'table', table_name
|
||||
FROM (VALUES
|
||||
('webhook'), ('webhookSubscription'), ('webhookEvent'),
|
||||
('webhookEventContent'), ('webhookEventSendLog'),
|
||||
('scheduledJob'), ('scheduledJobParameter')
|
||||
) AS tables(table_name)
|
||||
CROSS JOIN (VALUES ('read'), ('insert'), ('edit'), ('delete')) AS actions(action)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
INSERT INTO permission (name, object_type, object_label)
|
||||
SELECT process_name || '.hasAccess', 'process', process_name
|
||||
FROM (VALUES
|
||||
('SendWebhookEvent'), ('SendTestEventToWebhook'), ('ManageWebhookHealth')
|
||||
) AS processes(process_name)
|
||||
ON CONFLICT (name) DO NOTHING;
|
||||
|
||||
INSERT INTO role_permission_int (role_id, permission_id)
|
||||
SELECT role.id, permission.id
|
||||
FROM role CROSS JOIN permission
|
||||
WHERE (role.name = 'admin' OR (role.name = 'viewer' AND permission.name LIKE '%.read'))
|
||||
AND (permission.name LIKE 'webhook.%'
|
||||
OR permission.name LIKE 'webhookSubscription.%'
|
||||
OR permission.name LIKE 'webhookEvent.%'
|
||||
OR permission.name LIKE 'webhookEventContent.%'
|
||||
OR permission.name LIKE 'webhookEventSendLog.%'
|
||||
OR permission.name LIKE 'scheduledJob.%'
|
||||
OR permission.name LIKE 'scheduledJobParameter.%'
|
||||
OR permission.name IN ('SendWebhookEvent.hasAccess',
|
||||
'SendTestEventToWebhook.hasAccess', 'ManageWebhookHealth.hasAccess'))
|
||||
ON CONFLICT (role_id, permission_id) DO NOTHING;
|
||||
@@ -9,7 +9,9 @@ java -jar qqq-all-app/target/qqq-all-app-4.1.0-SNAPSHOT.jar
|
||||
|
||||
The core app binds to `127.0.0.1` and serves the Next dashboard at `http://127.0.0.1:8080/` and health at `/health`. Set `QQQ_ALL_PORT` to change the port and `QQQ_ALL_DATA_DIR` to change the default `./data` directory. On first start it seeds customers and orders in H2, order lines in SQLite, and products as JSON files. Artemis runs in the same JVM; inserts and updates to `order` publish to the `orderEvents` topic and trigger `syncOrder`.
|
||||
|
||||
Core uses mock authentication. The `sessionId` cookie selects the two fixed demo identities: `aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa` for Admin and `dddddddd-dddd-4ddd-8ddd-dddddddddddd` for Demo User. Any other session ID uses Demo User. These identities are for local demonstrations; role-based permissions are wired in task C5.
|
||||
Core uses mock authentication. The `sessionId` cookie selects the two fixed demo identities: `aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa` for Admin and `dddddddd-dddd-4ddd-8ddd-dddddddddddd` for Demo User. Any other session ID uses Demo User. Admin can read and write the customer, order, order-line, product, and address tables; Demo User can read them but cannot write them. These identities are for local demonstrations. Quick search is disabled in core because it requires OpenSearch; the customer list remains available through the normal table query API.
|
||||
|
||||
The core demo seeds a customer table view, a shipping address linked to the geo country/state/city tables, and an order review workflow. The `RunRecordWorkflow` process can run that workflow against an order; `syncOrder` writes a process trace after an order event. It also seeds an active `orderStored` webhook subscription to its loopback receiver at `/demo/order-webhook`. Insert an order as Admin, run `SendWebhookEvent` as Admin, and inspect `/demo/order-webhook-receipts` to see the delivered payload. Demo User can read webhook records but cannot edit their configuration or invoke the delivery process. The receiver is available only in the loopback-bound core profile.
|
||||
|
||||
## Full profile
|
||||
|
||||
@@ -27,5 +29,6 @@ Configure the following environment variables before starting the same jar:
|
||||
| ESB | `QQQ_ALL_ARTEMIS_URL`, `QQQ_ALL_RABBITMQ_URL`, `QQQ_ALL_RABBITMQ_USER`, `QQQ_ALL_RABBITMQ_PASSWORD` | — |
|
||||
| Mailpit SMTP | `QQQ_ALL_SMTP_HOST` | `QQQ_ALL_SMTP_PORT` (1025) |
|
||||
| Keycloak OIDC | `QQQ_ALL_OIDC_BASE_URL`, `QQQ_ALL_OIDC_CLIENT_ID`, `QQQ_ALL_OIDC_CLIENT_SECRET` | `QQQ_ALL_OIDC_EXTERNAL_BASE_URL` (base URL), `QQQ_ALL_OIDC_SCOPES` (`openid email profile`) |
|
||||
| OpenSearch quick search | `QQQ_ALL_OPENSEARCH_HOST` | `QQQ_ALL_OPENSEARCH_PORT` (9200), `QQQ_ALL_OPENSEARCH_INDEX` (`qqq-all-customers`), `QQQ_ALL_OPENSEARCH_USER`, `QQQ_ALL_OPENSEARCH_PASSWORD`, `QQQ_ALL_OPENSEARCH_SSL` (false) |
|
||||
|
||||
The full app adds sample tables on each external backend, `orderEvents` on Artemis, and `orderSyncEvents` on RabbitMQ. Keycloak realm roles resolve by name against the PostgreSQL `role` table from `qbit-user-role-permissions`, then become QQQ session permissions. Unknown roles receive no permissions. PostgreSQL must also contain the qbit permission tables and OAuth session and redirect-state tables. External database schema, demo roles, the MinIO bucket, and the service stack are provisioned by the forthcoming compose and seed work in C6; this C4 profile expects those resources to exist.
|
||||
The full app adds sample tables on each external backend, `orderEvents` on Artemis, and `orderSyncEvents` on RabbitMQ. Quick search is configured for customer `name` and `email`; run `quickSearchFullReindex` after OpenSearch and the customer data are ready. The SFTP integration adds its import source and staging tables plus `SFTPImportFileSyncProcess`; `externalImportFile` remains the direct SFTP file sample. Keycloak realm roles resolve by name against the PostgreSQL `role` table from `qbit-user-role-permissions`, then become QQQ session permissions. Unknown roles receive no permissions. Webhook tables and delivery processes require explicit permissions: the viewer role receives read access, while the admin role can edit destinations and run delivery. The additive `infra/postgres/03-webhook-permissions.sql` grant file runs after C6's role seed. PostgreSQL must also contain the qbit permission tables and OAuth session and redirect-state tables. External database schema, demo records, demo roles, the MinIO bucket, and the service stack are provisioned by the forthcoming compose and seed work in C6; this profile expects those resources to exist.
|
||||
@@ -39,6 +39,13 @@
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-esb</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qqq</groupId><artifactId>qqq-frontend-next</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-user-role-permissions</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-quick-search</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-customizable-table-views</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-standard-process-trace</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-webhooks</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-workflows</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-geo-data</artifactId></dependency>
|
||||
<dependency><groupId>com.kingsrook.qbits</groupId><artifactId>qbit-sftp-data-integration</artifactId></dependency>
|
||||
<dependency><groupId>org.postgresql</groupId><artifactId>postgresql</artifactId></dependency>
|
||||
<dependency><groupId>com.mysql</groupId><artifactId>mysql-connector-j</artifactId></dependency>
|
||||
<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId></dependency>
|
||||
|
||||
@@ -9,6 +9,9 @@ package com.kingsrook.qqq.all;
|
||||
import java.io.IOException;
|
||||
import java.net.ServerSocket;
|
||||
import java.nio.file.Path;
|
||||
import com.kingsrook.qbits.webhooks.actions.WebhookSubscriptionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSystemUserSession;
|
||||
import com.kingsrook.qqq.middleware.javalin.QApplicationLauncher;
|
||||
import com.kingsrook.qqq.middleware.javalin.QApplicationLauncherConfig;
|
||||
import org.apache.activemq.artemis.core.config.Configuration;
|
||||
@@ -36,6 +39,8 @@ final class CoreRuntime implements AutoCloseable
|
||||
Integer brokerPort = findFreePort();
|
||||
Path brokerDirectory = dataDirectory.resolve("artemis").toAbsolutePath();
|
||||
String brokerUrl = "tcp://127.0.0.1:" + brokerPort;
|
||||
DemoOrderWebhookReceiver.clear();
|
||||
DemoQBitData.ensureSchema(dataDirectory, new QqqAllApplication(dataDirectory, brokerUrl).defineQInstance(), port);
|
||||
Configuration configuration = new ConfigurationImpl()
|
||||
.setPersistenceEnabled(false).setSecurityEnabled(false).setJMXManagementEnabled(false)
|
||||
.setBindingsDirectory(brokerDirectory.resolve("bindings").toString())
|
||||
@@ -52,6 +57,15 @@ final class CoreRuntime implements AutoCloseable
|
||||
new QApplicationLauncherConfig().withRegisterShutdownHook(false)
|
||||
.withServerCustomizer(server -> server.withPort(port)
|
||||
.withJavalinConfigCustomizer(config -> config.jetty.host = "127.0.0.1")));
|
||||
QContext.init(launcher.getQInstance(), new QSystemUserSession());
|
||||
try
|
||||
{
|
||||
WebhookSubscriptionsHelper.clearMemoizations();
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
return new CoreRuntime(broker, launcher);
|
||||
}
|
||||
catch(Exception | LinkageError e)
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
* 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
|
||||
*/
|
||||
package com.kingsrook.qqq.all;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.webhooks.actions.WebhookEventTypeCustomizerInterface;
|
||||
import com.kingsrook.qbits.webhooks.model.WebhookEventContent;
|
||||
import com.kingsrook.qqq.backend.core.model.data.QRecord;
|
||||
import com.kingsrook.qqq.backend.core.utils.JsonUtils;
|
||||
|
||||
/** Builds a self-contained demo payload without requiring a separate public API definition. */
|
||||
public class DemoOrderWebhookContent implements WebhookEventTypeCustomizerInterface
|
||||
{
|
||||
@Override
|
||||
public WebhookEventContent buildEventContent(QRecord record, String eventType, String apiName, String apiVersion)
|
||||
{
|
||||
Map<String, Object> body = new LinkedHashMap<>();
|
||||
body.put("eventType", eventType);
|
||||
body.put("orderNo", record.getValueString("orderNo"));
|
||||
body.put("orderId", record.getValueInteger("id"));
|
||||
return new WebhookEventContent().withPostBody(JsonUtils.toJson(body));
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,55 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
* 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
|
||||
*/
|
||||
package com.kingsrook.qqq.all;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CopyOnWriteArrayList;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.middleware.javalin.QJavalinRouteProviderInterface;
|
||||
import io.javalin.config.JavalinConfig;
|
||||
import static io.javalin.apibuilder.ApiBuilder.get;
|
||||
import static io.javalin.apibuilder.ApiBuilder.post;
|
||||
|
||||
/** Local receiver that makes successful order webhook delivery visible in the core demo. */
|
||||
public class DemoOrderWebhookReceiver implements QJavalinRouteProviderInterface
|
||||
{
|
||||
private static final List<String> RECEIPTS = new CopyOnWriteArrayList<>();
|
||||
|
||||
static void clear()
|
||||
{
|
||||
RECEIPTS.clear();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setQInstance(QInstance instance)
|
||||
{
|
||||
}
|
||||
|
||||
@Override
|
||||
public void acceptJavalinConfig(JavalinConfig config)
|
||||
{
|
||||
config.routes.apiBuilder(() ->
|
||||
{
|
||||
post("/demo/order-webhook", context ->
|
||||
{
|
||||
String body = context.body();
|
||||
if(body.length() > 8192)
|
||||
{
|
||||
context.status(413);
|
||||
return;
|
||||
}
|
||||
RECEIPTS.add(body);
|
||||
if(RECEIPTS.size() > 20)
|
||||
{
|
||||
RECEIPTS.removeFirst();
|
||||
}
|
||||
context.status(200).result("ok");
|
||||
});
|
||||
get("/demo/order-webhook-receipts", context -> context.json(List.copyOf(RECEIPTS)));
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,118 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
* 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
|
||||
*/
|
||||
package com.kingsrook.qqq.all;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.sql.Connection;
|
||||
import java.sql.DriverManager;
|
||||
import java.sql.SQLException;
|
||||
import java.sql.Statement;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
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.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
|
||||
|
||||
/** Creates repeatable local tables for the qbit metadata exposed by core. */
|
||||
final class DemoQBitData
|
||||
{
|
||||
private DemoQBitData()
|
||||
{
|
||||
}
|
||||
|
||||
static void ensureSchema(Path dataDirectory, QInstance instance, Integer port) throws SQLException
|
||||
{
|
||||
try(Connection connection = DriverManager.getConnection(
|
||||
"jdbc:h2:file:" + CoreData.h2DatabaseName(dataDirectory) + ";MODE=MySQL;DB_CLOSE_DELAY=-1", "sa", "");
|
||||
Statement statement = connection.createStatement())
|
||||
{
|
||||
for(QTableMetaData table : instance.getTables().values())
|
||||
{
|
||||
if(!"h2".equals(table.getBackendName()) || "customer".equals(table.getName()) || "order".equals(table.getName()))
|
||||
{
|
||||
continue;
|
||||
}
|
||||
String tableName = table.getBackendDetails() instanceof RDBMSTableBackendDetails details
|
||||
? details.getTableName() : table.getName();
|
||||
List<String> columns = new ArrayList<>();
|
||||
for(QFieldMetaData field : table.getFields().values())
|
||||
{
|
||||
String name = field.getBackendName() == null ? field.getName() : field.getBackendName();
|
||||
String type = sqlType(field.getType());
|
||||
String identity = field.getName().equals(table.getPrimaryKeyField())
|
||||
&& (field.getType() == QFieldType.INTEGER || field.getType() == QFieldType.LONG)
|
||||
? " GENERATED BY DEFAULT AS IDENTITY" : "";
|
||||
String primaryKey = field.getName().equals(table.getPrimaryKeyField()) ? " PRIMARY KEY" : "";
|
||||
columns.add(identifier(name) + " " + type + identity + primaryKey);
|
||||
}
|
||||
if(!columns.isEmpty())
|
||||
{
|
||||
statement.execute("CREATE TABLE IF NOT EXISTS " + identifier(tableName)
|
||||
+ " (" + String.join(", ", columns) + ")");
|
||||
}
|
||||
}
|
||||
statement.execute("MERGE INTO shipping_country (id, name, alpha2_code) KEY (id) VALUES (1, 'United States', 'US')");
|
||||
statement.execute("MERGE INTO shipping_stateProvince (id, name, country_id) KEY (id) VALUES (1, 'Illinois', 1)");
|
||||
statement.execute("MERGE INTO shipping_city (id, name, state_province_id, latitude, longitude) KEY (id) "
|
||||
+ "VALUES (1, 'Chicago', 1, 41.8781, -87.6298)");
|
||||
statement.execute("MERGE INTO address (id, customer_id, line1, city_id) KEY (id) "
|
||||
+ "VALUES (1, 1, '1 Demo Street', 1)");
|
||||
statement.execute("MERGE INTO customizable_table (id, table_name, is_active, default_table_view_id) KEY (id) "
|
||||
+ "VALUES (1, 'customer', true, 1)");
|
||||
statement.execute("MERGE INTO table_view (id, customizable_table_id, name) KEY (id) "
|
||||
+ "VALUES (1, 1, 'Customer Directory')");
|
||||
statement.execute("MERGE INTO table_view_field (id, table_view_id, field_name, access_level) KEY (id) VALUES "
|
||||
+ "(1, 1, 'customer.id', 'READ_ONLY'), "
|
||||
+ "(2, 1, 'customer.name', 'EDITABLE_OPTIONAL'), "
|
||||
+ "(3, 1, 'customer.email', 'EDITABLE_OPTIONAL')");
|
||||
statement.execute("MERGE INTO workflow (id, name, workflow_type_name, table_name, current_workflow_revision_id) "
|
||||
+ "KEY (id) VALUES (1, 'Order Status Review', 'RecordWorkflow', 'order', 1)");
|
||||
statement.execute("MERGE INTO workflow_revision (id, workflow_id, version_no, start_step_no, commit_message, author) "
|
||||
+ "KEY (id) VALUES (1, 1, 1, 1, 'Demo order review', 'qqq-all')");
|
||||
statement.execute("MERGE INTO workflow_step (id, workflow_revision_id, step_no, workflow_step_type_name, summary, input_values_json) "
|
||||
+ "KEY (id) VALUES (1, 1, 1, 'updateInputRecordField', 'Mark order reviewed', "
|
||||
+ "'{\"fieldName\":\"status\",\"value\":\"REVIEWED\"}')");
|
||||
statement.execute("MERGE INTO webhook (id, name, url, active_status_id, health_status_id) KEY (id) "
|
||||
+ "VALUES (1, 'Local Order Receiver', 'http://127.0.0.1:" + port + "/demo/order-webhook', 1, 1)");
|
||||
statement.execute("MERGE INTO webhook_subscription (id, webhook_id, webhook_event_type_name, "
|
||||
+ "active_status_id, api_name, api_version, create_date) KEY (id) "
|
||||
+ "VALUES (1, 1, 'orderStored', 1, 'none', 'none', CURRENT_TIMESTAMP)");
|
||||
}
|
||||
}
|
||||
|
||||
private static String identifier(String value)
|
||||
{
|
||||
if(value == null || !value.matches("[A-Za-z][A-Za-z0-9_]*"))
|
||||
{
|
||||
throw new IllegalArgumentException("Unsupported demo schema identifier");
|
||||
}
|
||||
if("value".equalsIgnoreCase(value) || "key".equalsIgnoreCase(value))
|
||||
{
|
||||
return "`" + value + "`";
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private static String sqlType(QFieldType type)
|
||||
{
|
||||
return switch(type)
|
||||
{
|
||||
case INTEGER -> "INTEGER";
|
||||
case LONG -> "BIGINT";
|
||||
case DECIMAL -> "DECIMAL(20,6)";
|
||||
case BOOLEAN -> "BOOLEAN";
|
||||
case DATE -> "DATE";
|
||||
case TIME -> "TIME";
|
||||
case DATE_TIME -> "TIMESTAMP";
|
||||
case BLOB -> "BLOB";
|
||||
case STRING, PASSWORD -> "VARCHAR(4000)";
|
||||
case TEXT, HTML -> "CLOB";
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,184 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
* 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
|
||||
*/
|
||||
package com.kingsrook.qqq.all;
|
||||
|
||||
import java.util.List;
|
||||
import com.kingsrook.qbits.customizabletableviews.CustomizableTableViewsQBitConfig;
|
||||
import com.kingsrook.qbits.customizabletableviews.CustomizableTableViewsQBitProducer;
|
||||
import com.kingsrook.qbits.quicksearch.QuickSearchQBitConfig;
|
||||
import com.kingsrook.qbits.quicksearch.QuickSearchQBitProducer;
|
||||
import com.kingsrook.qbits.quicksearch.SearchableFieldConfig;
|
||||
import com.kingsrook.qbits.sftpdataintegration.SFTPDataIntegrationQBitConfig;
|
||||
import com.kingsrook.qbits.sftpdataintegration.SFTPDataIntegrationQBitProducer;
|
||||
import com.kingsrook.qbits.geodata.GeoDataQBitConfig;
|
||||
import com.kingsrook.qbits.geodata.GeoDataQBitProducer;
|
||||
import com.kingsrook.qbits.standardprocesstrace.StandardProcessTraceQBitConfig;
|
||||
import com.kingsrook.qbits.standardprocesstrace.StandardProcessTraceQBitProducer;
|
||||
import com.kingsrook.qbits.standardprocesstrace.utils.StandardProcessTracer;
|
||||
import com.kingsrook.qbits.webhooks.WebhooksQBitConfig;
|
||||
import com.kingsrook.qbits.webhooks.WebhooksQBitProducer;
|
||||
import com.kingsrook.qbits.webhooks.model.WebhookEventCategory;
|
||||
import com.kingsrook.qbits.webhooks.registry.WebhookEventType;
|
||||
import com.kingsrook.qbits.webhooks.registry.WebhooksRegistry;
|
||||
import com.kingsrook.qbits.workflows.WorkflowsQBitConfig;
|
||||
import com.kingsrook.qbits.workflows.WorkflowsQBitProducer;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceEnricher;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.producers.MetaDataCustomizerInterface;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference;
|
||||
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.qbits.ProvidedOrSuppliedTableConfig;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.qbits.QBitMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.possiblevalues.QPossibleValue;
|
||||
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.TablesPossibleValueSourceMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.processes.QQQProcessesMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.tables.QQQTablesMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.savedbulkloadprofiles.SavedBulkLoadProfileMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.savedreports.SavedReportsMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.scheduledjobs.ScheduledJobsMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.common.TimeZonePossibleValueSourceMetaDataProvider;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.scheduleing.simple.SimpleSchedulerMetaData;
|
||||
import com.kingsrook.qqq.backend.module.rdbms.model.metadata.RDBMSTableBackendDetails;
|
||||
|
||||
/** Registers the locally usable qbits against the demo's relational backend. */
|
||||
final class DemoQBits
|
||||
{
|
||||
private DemoQBits()
|
||||
{
|
||||
}
|
||||
|
||||
static void addCore(QInstance instance, String backendName) throws QException
|
||||
{
|
||||
new QQQTablesMetaDataProvider().defineAll(instance, backendName, backendName, null);
|
||||
new QQQProcessesMetaDataProvider().defineAll(instance, backendName, backendName, null);
|
||||
instance.addScheduler(new SimpleSchedulerMetaData().withName("demoScheduler"));
|
||||
new ScheduledJobsMetaDataProvider().defineAll(instance, backendName, null);
|
||||
instance.addPossibleValueSource(new TimeZonePossibleValueSourceMetaDataProvider().produce());
|
||||
instance.addPossibleValueSource(TablesPossibleValueSourceMetaDataProvider.defineTablesPossibleValueSource(instance));
|
||||
instance.addPossibleValueSource(new QPossibleValueSource().withName("apiName")
|
||||
.withType(QPossibleValueSourceType.ENUM).withIdType(QFieldType.STRING)
|
||||
.withEnumValues(List.of(new QPossibleValue<>("none", "No API in this demo"))));
|
||||
instance.addPossibleValueSource(new QPossibleValueSource().withName("apiVersion")
|
||||
.withType(QPossibleValueSourceType.ENUM).withIdType(QFieldType.STRING)
|
||||
.withEnumValues(List.of(new QPossibleValue<>("none", "No API version"))));
|
||||
if(instance.getPossibleValueSource("role") == null)
|
||||
{
|
||||
instance.addPossibleValueSource(new QPossibleValueSource().withName("role")
|
||||
.withType(QPossibleValueSourceType.ENUM).withIdType(QFieldType.INTEGER)
|
||||
.withEnumValues(List.of(new QPossibleValue<>(0, "No role"))));
|
||||
}
|
||||
|
||||
MetaDataCustomizerInterface<QTableMetaData> tableCustomizer =
|
||||
(i, table) ->
|
||||
{
|
||||
table.setBackendName(backendName);
|
||||
table.setBackendDetails(new RDBMSTableBackendDetails()
|
||||
.withTableName(QInstanceEnricher.inferBackendName(table.getName())));
|
||||
QInstanceEnricher.setInferredFieldBackendNames(table);
|
||||
return table;
|
||||
};
|
||||
|
||||
new CustomizableTableViewsQBitProducer()
|
||||
.withQBitConfig(new CustomizableTableViewsQBitConfig()
|
||||
.withTableMetaDataCustomizer(tableCustomizer))
|
||||
.produce(instance).addSelfToInstance(instance);
|
||||
CustomizableTableViewsQBitProducer.activateTableMetaDataPersonalizerInterface(instance);
|
||||
|
||||
StandardProcessTraceQBitConfig traceConfig = new StandardProcessTraceQBitConfig()
|
||||
.withTableMetaDataCustomizer(tableCustomizer);
|
||||
StandardProcessTracer.setStandardProcessTraceQBitConfig(traceConfig);
|
||||
new StandardProcessTraceQBitProducer()
|
||||
.withStandardProcessTraceQBitConfig(traceConfig)
|
||||
.produce(instance).addSelfToInstance(instance);
|
||||
instance.getProcess("syncOrder").setProcessTracerCodeReference(new QCodeReference(StandardProcessTracer.class));
|
||||
|
||||
new WebhooksQBitProducer()
|
||||
.withQBitConfig(new WebhooksQBitConfig().withDefaultBackendNameForTables(backendName)
|
||||
.withSchedulerName("demoScheduler")
|
||||
.withTableMetaDataCustomizer(tableCustomizer))
|
||||
.produce(instance).addSelfToInstance(instance);
|
||||
WebhooksRegistry.ofOrWithNew(instance).registerWebhookEventType(new WebhookEventType()
|
||||
.withName("orderStored").withLabel("Order Stored")
|
||||
.withCategory(WebhookEventCategory.STORE).withTableName("order")
|
||||
.withCustomizer(new QCodeReference(DemoOrderWebhookContent.class)));
|
||||
|
||||
new WorkflowsQBitProducer()
|
||||
.withQBitConfig(new WorkflowsQBitConfig().withTableMetaDataCustomizer(tableCustomizer))
|
||||
.produce(instance).addSelfToInstance(instance);
|
||||
|
||||
new GeoDataQBitProducer()
|
||||
.withConfig(new GeoDataQBitConfig().withBackendName(backendName).withTableNamePrefix("shipping"))
|
||||
.produce(instance, "shipping-geo");
|
||||
instance.addTable(new QTableMetaData().withName("shipping_country").withLabel("Country")
|
||||
.withBackendName(backendName).withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("alpha2Code", QFieldType.STRING).withBackendName("alpha2_code")));
|
||||
instance.addTable(new QTableMetaData().withName("shipping_stateProvince").withLabel("State Province")
|
||||
.withBackendName(backendName).withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("countryId", QFieldType.INTEGER).withBackendName("country_id")
|
||||
.withPossibleValueSourceName("shipping_country")));
|
||||
instance.addTable(new QTableMetaData().withName("shipping_city").withLabel("City")
|
||||
.withBackendName(backendName).withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("stateProvinceId", QFieldType.INTEGER).withBackendName("state_province_id")
|
||||
.withPossibleValueSourceName("shipping_stateProvince"))
|
||||
.withField(new QFieldMetaData("latitude", QFieldType.DECIMAL))
|
||||
.withField(new QFieldMetaData("longitude", QFieldType.DECIMAL)));
|
||||
instance.addTable(new QTableMetaData().withName("address").withLabel("Address")
|
||||
.withBackendName(backendName).withPrimaryKeyField("id")
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("customerId", QFieldType.INTEGER).withBackendName("customer_id"))
|
||||
.withField(new QFieldMetaData("line1", QFieldType.STRING))
|
||||
.withField(new QFieldMetaData("cityId", QFieldType.INTEGER).withBackendName("city_id")
|
||||
.withPossibleValueSourceName("shipping_city")));
|
||||
}
|
||||
|
||||
static void addFull(QInstance instance, FullProfileSettings settings) throws QException
|
||||
{
|
||||
QuickSearchQBitConfig searchConfig = new QuickSearchQBitConfig()
|
||||
.withBackendName("postgres")
|
||||
.withOpensearchHost(settings.get("OPENSEARCH_HOST"))
|
||||
.withOpensearchPort(settings.port("OPENSEARCH_PORT", "9200"))
|
||||
.withOpensearchIndexName(settings.getOrDefault("OPENSEARCH_INDEX", "qqq-all-customers"))
|
||||
.withOpensearchUsername(settings.get("OPENSEARCH_USER"))
|
||||
.withOpensearchPassword(settings.get("OPENSEARCH_PASSWORD"))
|
||||
.withUseSsl(Boolean.parseBoolean(settings.getOrDefault("OPENSEARCH_SSL", "false")))
|
||||
.withSearchableTable("customer", List.of(new SearchableFieldConfig("name"),
|
||||
new SearchableFieldConfig("email")));
|
||||
new QuickSearchQBitProducer().withConfig(searchConfig).produce(instance);
|
||||
instance.addQBit(new QBitMetaData().withGroupId("com.kingsrook.qbits")
|
||||
.withArtifactId("quick-search").withVersion("0.2.1-SNAPSHOT").withConfig(searchConfig));
|
||||
|
||||
new SavedBulkLoadProfileMetaDataProvider().defineAll(instance, "postgres", null);
|
||||
new SavedReportsMetaDataProvider().defineAll(instance, "postgres", "sftp", null);
|
||||
new SFTPDataIntegrationQBitProducer().withSftpDataIntegrationQBitConfig(new SFTPDataIntegrationQBitConfig()
|
||||
.withSourceFileTableConfig(ProvidedOrSuppliedTableConfig.provideTableUsingBackendNamed("sftp"))
|
||||
.withDestinationFileTableConfig(ProvidedOrSuppliedTableConfig.provideTableUsingBackendNamed("sftp"))
|
||||
.withStagingFileTableConfig(ProvidedOrSuppliedTableConfig.provideTableUsingBackendNamed("files"))
|
||||
.withSchedulerName("demoScheduler")
|
||||
.withTableMetaDataCustomizer((i, table) ->
|
||||
{
|
||||
if(table.getBackendName() == null)
|
||||
{
|
||||
table.setBackendName("postgres");
|
||||
table.setBackendDetails(new RDBMSTableBackendDetails()
|
||||
.withTableName(QInstanceEnricher.inferBackendName(table.getName())));
|
||||
QInstanceEnricher.setInferredFieldBackendNames(table);
|
||||
}
|
||||
return table;
|
||||
})).produce(instance, "demo-import");
|
||||
}
|
||||
}
|
||||
@@ -7,8 +7,11 @@
|
||||
package com.kingsrook.qqq.all;
|
||||
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.QPermissionRules;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModuleCustomizerInterface;
|
||||
|
||||
@@ -17,6 +20,35 @@ public class DemoUsers implements QAuthenticationModuleCustomizerInterface
|
||||
{
|
||||
public static final String ADMIN_SESSION = "aaaaaaaa-aaaa-4aaa-8aaa-aaaaaaaaaaaa";
|
||||
public static final String DEMO_SESSION = "dddddddd-dddd-4ddd-8ddd-dddddddddddd";
|
||||
private static final List<String> DEMO_TABLES = List.of("customer", "order", "orderLine", "product", "address");
|
||||
private static final List<String> WEBHOOK_TABLES = List.of("webhook", "webhookSubscription",
|
||||
"webhookEvent", "webhookEventContent", "webhookEventSendLog", "scheduledJob", "scheduledJobParameter");
|
||||
private static final List<String> WEBHOOK_PROCESSES = List.of("SendWebhookEvent",
|
||||
"SendTestEventToWebhook", "ManageWebhookHealth");
|
||||
|
||||
static void protectDemoTables(QInstance instance)
|
||||
{
|
||||
for(String tableName : DEMO_TABLES)
|
||||
{
|
||||
instance.getTable(tableName).setPermissionRules(new QPermissionRules()
|
||||
.withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS));
|
||||
}
|
||||
protectWebhookResources(instance);
|
||||
}
|
||||
|
||||
static void protectWebhookResources(QInstance instance)
|
||||
{
|
||||
for(String tableName : WEBHOOK_TABLES)
|
||||
{
|
||||
instance.getTable(tableName).setPermissionRules(new QPermissionRules()
|
||||
.withLevel(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS));
|
||||
}
|
||||
for(String processName : WEBHOOK_PROCESSES)
|
||||
{
|
||||
instance.getProcess(processName).setPermissionRules(new QPermissionRules()
|
||||
.withLevel(PermissionLevel.HAS_ACCESS_PERMISSION));
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void customizeSession(QInstance instance, QSession session, Map<String, Object> context)
|
||||
@@ -26,6 +58,29 @@ public class DemoUsers implements QAuthenticationModuleCustomizerInterface
|
||||
String userId = admin ? "core:admin" : "core:demo";
|
||||
session.getUser().setIdReference(userId);
|
||||
session.getUser().setFullName(name);
|
||||
for(String tableName : DEMO_TABLES)
|
||||
{
|
||||
session.withPermission(tableName + ".read");
|
||||
if(admin)
|
||||
{
|
||||
session.withPermissions(tableName + ".insert", tableName + ".edit", tableName + ".delete");
|
||||
}
|
||||
}
|
||||
for(String tableName : WEBHOOK_TABLES)
|
||||
{
|
||||
session.withPermission(tableName + ".read");
|
||||
if(admin)
|
||||
{
|
||||
session.withPermissions(tableName + ".insert", tableName + ".edit", tableName + ".delete");
|
||||
}
|
||||
}
|
||||
if(admin)
|
||||
{
|
||||
for(String processName : WEBHOOK_PROCESSES)
|
||||
{
|
||||
session.withPermission(processName + ".hasAccess");
|
||||
}
|
||||
}
|
||||
session.withValueForFrontend("user", new LinkedHashMap<>(Map.of("name", name, "email", userId + "@example.test")));
|
||||
}
|
||||
}
|
||||
@@ -58,11 +58,14 @@ final class FullProfileApplication extends AbstractQQQApplication
|
||||
@Override
|
||||
public QInstance defineQInstance() throws QException
|
||||
{
|
||||
QInstance instance = new QqqAllApplication(dataDirectory, settings.get("ARTEMIS_URL")).defineQInstance();
|
||||
QInstance instance = new QqqAllApplication(dataDirectory, settings.get("ARTEMIS_URL"), false).defineQInstance();
|
||||
addBackends(instance);
|
||||
addTables(instance);
|
||||
addAuthentication(instance);
|
||||
addEsb(instance);
|
||||
DemoQBits.addCore(instance, "postgres");
|
||||
DemoQBits.addFull(instance, settings);
|
||||
DemoUsers.protectWebhookResources(instance);
|
||||
|
||||
EmailMessagingProviderMetaData mail = new EmailMessagingProviderMetaData()
|
||||
.withSmtpServer(settings.get("SMTP_HOST"))
|
||||
@@ -122,7 +125,7 @@ final class FullProfileApplication extends AbstractQQQApplication
|
||||
.withRecordFormat(RecordFormat.JSON).withCardinality(Cardinality.MANY).withGlob("*.json"))
|
||||
.withField(new QFieldMetaData("id", QFieldType.INTEGER))
|
||||
.withField(new QFieldMetaData("name", QFieldType.STRING)));
|
||||
instance.addTable(new QTableMetaData().withName("importFile").withLabel("Import File")
|
||||
instance.addTable(new QTableMetaData().withName("externalImportFile").withLabel("External Import File")
|
||||
.withBackendName("sftp").withPrimaryKeyField("id")
|
||||
.withBackendDetails(new SFTPTableBackendDetails().withBasePath("imports")
|
||||
.withRecordFormat(RecordFormat.JSON).withCardinality(Cardinality.MANY).withGlob("*.json"))
|
||||
|
||||
@@ -20,7 +20,8 @@ final class FullProfileSettings
|
||||
"S3_ENDPOINT", "S3_BUCKET", "S3_ACCESS_KEY", "S3_SECRET_KEY",
|
||||
"SFTP_HOST", "SFTP_USER", "SFTP_PASSWORD",
|
||||
"ARTEMIS_URL", "RABBITMQ_URL", "RABBITMQ_USER", "RABBITMQ_PASSWORD",
|
||||
"SMTP_HOST", "OIDC_BASE_URL", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET");
|
||||
"SMTP_HOST", "OIDC_BASE_URL", "OIDC_CLIENT_ID", "OIDC_CLIENT_SECRET",
|
||||
"OPENSEARCH_HOST");
|
||||
|
||||
private final Map<String, String> values;
|
||||
|
||||
|
||||
@@ -51,11 +51,18 @@ public final class QqqAllApplication extends AbstractQQQApplication
|
||||
{
|
||||
private final Path dataDirectory;
|
||||
private final String brokerUrl;
|
||||
private final boolean includeDemoQbits;
|
||||
|
||||
public QqqAllApplication(Path dataDirectory, String brokerUrl)
|
||||
{
|
||||
this(dataDirectory, brokerUrl, true);
|
||||
}
|
||||
|
||||
QqqAllApplication(Path dataDirectory, String brokerUrl, boolean includeDemoQbits)
|
||||
{
|
||||
this.dataDirectory = dataDirectory;
|
||||
this.brokerUrl = brokerUrl;
|
||||
this.includeDemoQbits = includeDemoQbits;
|
||||
}
|
||||
|
||||
public static void main(String[] args) throws Exception
|
||||
@@ -130,6 +137,13 @@ public final class QqqAllApplication extends AbstractQQQApplication
|
||||
.withEndpointPath("/health").withIndicators(List.of(new MemoryHealthIndicator().withThreshold(99))));
|
||||
QJavalinMetaData.ofOrWithNew(instance)
|
||||
.withAdditionalRouteProviderReference(new QCodeReference(JavalinHealthRouteProvider.class));
|
||||
if(includeDemoQbits)
|
||||
{
|
||||
DemoQBits.addCore(instance, "h2");
|
||||
DemoUsers.protectDemoTables(instance);
|
||||
QJavalinMetaData.ofOrWithNew(instance)
|
||||
.withAdditionalRouteProviderReference(new QCodeReference(DemoOrderWebhookReceiver.class));
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,12 @@ import java.nio.file.Path;
|
||||
import java.time.Duration;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import com.kingsrook.qbits.webhooks.model.WebhookEventStatus;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.InsertAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.tables.QueryAction;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.actions.processes.QProcessCallbackFactory;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
@@ -62,6 +66,28 @@ class CoreApplicationTest
|
||||
assertEquals(200, dashboard.statusCode());
|
||||
assertTrue(dashboard.body().contains("html"));
|
||||
|
||||
HttpResponse<String> demoRead = client.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:" + port + "/qqq/v1/table/order/query"))
|
||||
.header("Cookie", "sessionId=" + DemoUsers.DEMO_SESSION)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{}"))
|
||||
.build(), HttpResponse.BodyHandlers.ofString());
|
||||
assertEquals(200, demoRead.statusCode(), demoRead.body());
|
||||
HttpResponse<String> demoInsert = insertOrder(client, port, DemoUsers.DEMO_SESSION, "ORD-DENIED");
|
||||
assertEquals(403, demoInsert.statusCode());
|
||||
assertEquals(403, updateWebhook(client, port, DemoUsers.DEMO_SESSION,
|
||||
"webhook", "{\"url\":\"http://127.0.0.1:1/\"}").statusCode());
|
||||
assertEquals(403, updateWebhook(client, port, DemoUsers.DEMO_SESSION,
|
||||
"webhookSubscription", "{\"activeStatusId\":3}").statusCode());
|
||||
assertEquals(403, runWebhookProcess(client, port, DemoUsers.DEMO_SESSION).statusCode());
|
||||
assertEquals(200, updateWebhook(client, port, DemoUsers.ADMIN_SESSION,
|
||||
"webhook", "{\"url\":\"http://127.0.0.1:" + port + "/demo/order-webhook\"}").statusCode());
|
||||
assertEquals(200, updateWebhook(client, port, DemoUsers.ADMIN_SESSION,
|
||||
"webhookSubscription", "{\"activeStatusId\":1}").statusCode());
|
||||
HttpResponse<String> adminInsert = insertOrder(client, port, DemoUsers.ADMIN_SESSION, "ORD-ADMIN");
|
||||
assertEquals(200, adminInsert.statusCode(), adminInsert.body());
|
||||
assertEquals(200, runWebhookProcess(client, port, DemoUsers.ADMIN_SESSION).statusCode());
|
||||
|
||||
QContext.init(runtime.getLauncher().getQInstance(), new QSystemUserSession());
|
||||
try
|
||||
{
|
||||
@@ -71,10 +97,14 @@ class CoreApplicationTest
|
||||
assertEquals("core:demo", mockAuth.createSession(runtime.getLauncher().getQInstance(),
|
||||
Map.of("sessionId", DemoUsers.DEMO_SESSION)).getUser().getIdReference());
|
||||
|
||||
for(String table : List.of("customer", "order", "orderLine", "product"))
|
||||
for(String table : List.of("customer", "order", "orderLine", "product", "address", "shipping_city"))
|
||||
{
|
||||
assertFalse(new QueryAction().execute(new QueryInput(table)).getRecords().isEmpty(), table);
|
||||
}
|
||||
assertEquals("Customer Directory", new QueryAction().execute(new QueryInput("TableView"))
|
||||
.getRecords().getFirst().getValueString("name"));
|
||||
assertEquals("Order Status Review", new QueryAction().execute(new QueryInput("workflow"))
|
||||
.getRecords().getFirst().getValueString("name"));
|
||||
|
||||
waitFor(() -> QEsbRuntime.getInstance().getRunner("syncOrder.orderEvents") != null
|
||||
&& QEsbRuntime.getInstance().getRunner("syncOrder.orderEvents").getState() == EsbTriggerState.RUNNING);
|
||||
@@ -82,6 +112,27 @@ class CoreApplicationTest
|
||||
new QRecord().withValue("orderNo", "ORD-1002")
|
||||
.withValue("customerId", 2).withValue("status", "NEW"))));
|
||||
waitFor(() -> QqqAllApplication.SyncOrderStep.getRunCount() > 0);
|
||||
waitFor(() -> !new QueryAction().execute(new QueryInput("processTrace")).getRecords().isEmpty());
|
||||
waitFor(() -> !new QueryAction().execute(new QueryInput("webhookEvent")).getRecords().isEmpty());
|
||||
|
||||
HttpResponse<String> receipts = client.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:" + port + "/demo/order-webhook-receipts"))
|
||||
.build(), HttpResponse.BodyHandlers.ofString());
|
||||
assertEquals(200, receipts.statusCode(), receipts.body());
|
||||
waitFor(() -> client.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:" + port + "/demo/order-webhook-receipts"))
|
||||
.build(), HttpResponse.BodyHandlers.ofString()).body().contains("ORD-ADMIN"));
|
||||
waitFor(() -> new QueryAction().execute(new QueryInput("webhookEvent")).getRecords().stream()
|
||||
.anyMatch(event -> WebhookEventStatus.DELIVERED.getId().equals(event.getValueInteger("eventStatusId"))));
|
||||
|
||||
RunProcessInput workflow = new RunProcessInput();
|
||||
workflow.setProcessName("RunRecordWorkflow");
|
||||
workflow.setCallback(QProcessCallbackFactory.forPrimaryKey("id", 1));
|
||||
workflow.addValue("tableName", "order");
|
||||
workflow.addValue("workflowId", 1);
|
||||
workflow.setFrontendStepBehavior(RunProcessInput.FrontendStepBehavior.SKIP);
|
||||
new RunProcessAction().execute(workflow);
|
||||
assertFalse(new QueryAction().execute(new QueryInput("workflowRunLog")).getRecords().isEmpty());
|
||||
}
|
||||
finally
|
||||
{
|
||||
@@ -90,13 +141,47 @@ class CoreApplicationTest
|
||||
}
|
||||
}
|
||||
|
||||
private static void waitFor(java.util.function.BooleanSupplier condition) throws Exception
|
||||
private static HttpResponse<String> updateWebhook(HttpClient client, Integer port, String sessionId,
|
||||
String tableName, String body) throws Exception
|
||||
{
|
||||
return client.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:" + port + "/data/" + tableName + "/1"))
|
||||
.header("Cookie", "sessionId=" + sessionId)
|
||||
.header("Content-Type", "application/json")
|
||||
.method("PATCH", HttpRequest.BodyPublishers.ofString(body))
|
||||
.build(), HttpResponse.BodyHandlers.ofString());
|
||||
}
|
||||
|
||||
private static HttpResponse<String> runWebhookProcess(HttpClient client, Integer port, String sessionId)
|
||||
throws Exception
|
||||
{
|
||||
return client.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:" + port + "/processes/SendWebhookEvent/run?webhookId=1"))
|
||||
.header("Cookie", "sessionId=" + sessionId)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{}"))
|
||||
.build(), HttpResponse.BodyHandlers.ofString());
|
||||
}
|
||||
|
||||
private static HttpResponse<String> insertOrder(HttpClient client, Integer port, String sessionId, String orderNo)
|
||||
throws Exception
|
||||
{
|
||||
return client.send(HttpRequest.newBuilder()
|
||||
.uri(URI.create("http://127.0.0.1:" + port + "/data/order/"))
|
||||
.header("Cookie", "sessionId=" + sessionId)
|
||||
.header("Content-Type", "application/json")
|
||||
.POST(HttpRequest.BodyPublishers.ofString("{\"orderNo\":\"" + orderNo
|
||||
+ "\",\"customerId\":1,\"status\":\"NEW\"}"))
|
||||
.build(), HttpResponse.BodyHandlers.ofString());
|
||||
}
|
||||
|
||||
private static void waitFor(java.util.concurrent.Callable<Boolean> condition) throws Exception
|
||||
{
|
||||
long deadline = System.nanoTime() + Duration.ofSeconds(15).toNanos();
|
||||
while(!condition.getAsBoolean() && System.nanoTime() < deadline)
|
||||
while(!condition.call() && System.nanoTime() < deadline)
|
||||
{
|
||||
Thread.sleep(50);
|
||||
}
|
||||
assertTrue(condition.getAsBoolean());
|
||||
assertTrue(condition.call());
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
* 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
|
||||
*/
|
||||
package com.kingsrook.qqq.all;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.webhooks.registry.WebhooksRegistry;
|
||||
import com.kingsrook.qbits.workflows.definition.WorkflowsRegistry;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class CoreQBitsTest
|
||||
{
|
||||
@TempDir Path dataDirectory;
|
||||
|
||||
@Test
|
||||
void coreExposesLocalQbitsAndOrderTrace() throws Exception
|
||||
{
|
||||
QInstance instance = new QqqAllApplication(dataDirectory, "tcp://127.0.0.1:61616").defineQInstance();
|
||||
new QInstanceValidator().validate(instance);
|
||||
Set<String> artifacts = instance.getQBits().values().stream()
|
||||
.map(qbit -> qbit.getArtifactId()).collect(Collectors.toSet());
|
||||
assertTrue(artifacts.containsAll(Set.of("customizable-table-views", "standard-process-trace",
|
||||
"webhooks", "workflows", "qbit-geo-data")));
|
||||
assertNotNull(instance.getTable("TableView"));
|
||||
assertNotNull(instance.getTable("processTrace"));
|
||||
assertNotNull(instance.getTable("webhook"));
|
||||
assertNotNull(instance.getTable("workflow"));
|
||||
assertNotNull(instance.getProcess("syncOrder").getProcessTracerCodeReference());
|
||||
assertNotNull(WebhooksRegistry.of(instance).getWebhookEventType("orderStored"));
|
||||
assertNotNull(WorkflowsRegistry.of(instance).getWorkflowType("RecordWorkflow"));
|
||||
assertNotNull(instance.getTable("address").getField("cityId").getPossibleValueSourceName());
|
||||
assertTrue(artifacts.stream().noneMatch("quick-search"::equals));
|
||||
}
|
||||
}
|
||||
@@ -52,7 +52,7 @@ class FullProfileTest
|
||||
assertEquals("http://minio.example.test:9000",
|
||||
((MinioBackendMetaData) instance.getBackend("s3")).getEndpoint());
|
||||
assertEquals("s3", instance.getTable("document").getBackendName());
|
||||
assertEquals("sftp", instance.getTable("importFile").getBackendName());
|
||||
assertEquals("sftp", instance.getTable("externalImportFile").getBackendName());
|
||||
|
||||
EsbInstanceMetaData esb = EsbInstanceMetaData.of(instance);
|
||||
assertEquals("tcp://artemis.example.test:61616", esb.getProvider("artemis").getUrl());
|
||||
@@ -101,6 +101,8 @@ class FullProfileTest
|
||||
values.put("QQQ_ALL_OIDC_BASE_URL", "https://keycloak.example.test/realms/demo");
|
||||
values.put("QQQ_ALL_OIDC_CLIENT_ID", "qqq-all");
|
||||
values.put("QQQ_ALL_OIDC_CLIENT_SECRET", "fixture");
|
||||
values.put("QQQ_ALL_OPENSEARCH_HOST", "127.0.0.1");
|
||||
values.put("QQQ_ALL_OPENSEARCH_PORT", "1");
|
||||
return values;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,114 @@
|
||||
/*
|
||||
* Copyright 2026 Kingsrook, LLC
|
||||
* 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
|
||||
*/
|
||||
package com.kingsrook.qqq.all;
|
||||
|
||||
import java.nio.file.Path;
|
||||
import java.util.Set;
|
||||
import java.util.stream.Collectors;
|
||||
import com.kingsrook.qbits.quicksearch.QuickSearchQBitConfig;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.PermissionsHelper;
|
||||
import com.kingsrook.qqq.backend.core.actions.permissions.TablePermissionSubType;
|
||||
import com.kingsrook.qqq.backend.core.context.QContext;
|
||||
import com.kingsrook.qqq.backend.core.exceptions.QPermissionDeniedException;
|
||||
import com.kingsrook.qqq.backend.core.instances.QInstanceValidator;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput;
|
||||
import com.kingsrook.qqq.backend.core.model.actions.tables.query.QueryInput;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.QInstance;
|
||||
import com.kingsrook.qqq.backend.core.model.metadata.permissions.PermissionLevel;
|
||||
import com.kingsrook.qqq.backend.core.model.session.QSession;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.io.TempDir;
|
||||
import static org.junit.jupiter.api.Assertions.assertEquals;
|
||||
import static org.junit.jupiter.api.Assertions.assertFalse;
|
||||
import static org.junit.jupiter.api.Assertions.assertNotNull;
|
||||
import static org.junit.jupiter.api.Assertions.assertThrows;
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
class FullQBitsTest
|
||||
{
|
||||
@TempDir Path dataDirectory;
|
||||
|
||||
@Test
|
||||
void fullAddsCustomerSearchAndSftpImportToTheDemo() throws Exception
|
||||
{
|
||||
FullProfileSettings settings = FullProfileSettings.from(FullProfileTest.environment());
|
||||
QInstance instance = new FullProfileApplication(dataDirectory, settings).defineQInstance();
|
||||
new QInstanceValidator().validate(instance);
|
||||
|
||||
Set<String> artifacts = instance.getQBits().values().stream()
|
||||
.map(qbit -> qbit.getArtifactId()).collect(Collectors.toSet());
|
||||
assertTrue(artifacts.containsAll(Set.of("quick-search", "user-role-permissions",
|
||||
"customizable-table-views", "standard-process-trace", "webhooks", "workflows",
|
||||
"qbit-geo-data", "sftp-data-integration")));
|
||||
assertNotNull(instance.getApp("quickSearchAdmin"));
|
||||
assertNotNull(instance.getProcess("quickSearchFullReindex"));
|
||||
assertNotNull(instance.getProcess("quickSearchReconcileIndex"));
|
||||
assertNotNull(instance.getProcess("SFTPImportFileSyncProcess"));
|
||||
assertNotNull(instance.getTable("SFTPImportSourceFileTable"));
|
||||
assertEquals("sftp", instance.getTable("SFTPImportSourceFileTable").getBackendName());
|
||||
for(String tableName : Set.of("webhook", "webhookSubscription", "webhookEvent",
|
||||
"webhookEventContent", "webhookEventSendLog", "scheduledJob", "scheduledJobParameter"))
|
||||
{
|
||||
assertEquals(PermissionLevel.READ_INSERT_EDIT_DELETE_PERMISSIONS,
|
||||
instance.getTable(tableName).getPermissionRules().getLevel(), tableName);
|
||||
}
|
||||
for(String processName : Set.of("SendWebhookEvent", "SendTestEventToWebhook", "ManageWebhookHealth"))
|
||||
{
|
||||
assertEquals(PermissionLevel.HAS_ACCESS_PERMISSION,
|
||||
instance.getProcess(processName).getPermissionRules().getLevel(), processName);
|
||||
}
|
||||
QuickSearchQBitConfig search = (QuickSearchQBitConfig) instance.getQBits().values().stream()
|
||||
.filter(qbit -> "quick-search".equals(qbit.getArtifactId())).findFirst().orElseThrow().getConfig();
|
||||
assertEquals("customer", search.getSearchableTables().getFirst().getTableName());
|
||||
assertEquals(Set.of("name", "email"), search.getSearchableTables().getFirst().getFields().stream()
|
||||
.map(field -> field.getFieldName()).collect(Collectors.toSet()));
|
||||
}
|
||||
|
||||
@Test
|
||||
void viewerPermissionsCannotChangeWebhookDestinationOrSendEvent() throws Exception
|
||||
{
|
||||
QInstance instance = new FullProfileApplication(dataDirectory,
|
||||
FullProfileSettings.from(FullProfileTest.environment())).defineQInstance();
|
||||
QSession viewer = new QSession().withPermissions("webhook.read", "webhookSubscription.read");
|
||||
QContext.init(instance, viewer);
|
||||
try
|
||||
{
|
||||
assertFalse(PermissionsHelper.hasTablePermission(new QueryInput("webhook"),
|
||||
"webhook", TablePermissionSubType.EDIT));
|
||||
assertFalse(PermissionsHelper.hasTablePermission(new QueryInput("webhookSubscription"),
|
||||
"webhookSubscription", TablePermissionSubType.EDIT));
|
||||
assertThrows(QPermissionDeniedException.class,
|
||||
() -> PermissionsHelper.checkProcessPermissionThrowing(new RunProcessInput(), "SendWebhookEvent"));
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
void adminPermissionsCanChangeWebhookDestinationAndSendEvent() throws Exception
|
||||
{
|
||||
QInstance instance = new FullProfileApplication(dataDirectory,
|
||||
FullProfileSettings.from(FullProfileTest.environment())).defineQInstance();
|
||||
QSession admin = new QSession().withPermissions("webhook.edit", "webhookSubscription.edit",
|
||||
"SendWebhookEvent.hasAccess");
|
||||
QContext.init(instance, admin);
|
||||
try
|
||||
{
|
||||
assertTrue(PermissionsHelper.hasTablePermission(new QueryInput("webhook"),
|
||||
"webhook", TablePermissionSubType.EDIT));
|
||||
assertTrue(PermissionsHelper.hasTablePermission(new QueryInput("webhookSubscription"),
|
||||
"webhookSubscription", TablePermissionSubType.EDIT));
|
||||
PermissionsHelper.checkProcessPermissionThrowing(new RunProcessInput(), "SendWebhookEvent");
|
||||
}
|
||||
finally
|
||||
{
|
||||
QContext.clear();
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user