From a2e267fe40e38a934b2ac18c052f9c635b018a61 Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Thu, 4 Aug 2022 11:34:19 -0500 Subject: [PATCH] Changes pushed to qqq-backend-core (solo-repo) in 0.2 support --- .../backend/core/actions/ActionHelper.java | 5 +- .../core/actions/QBackendTransaction.java | 82 ++++++ .../actions/interfaces/InsertInterface.java | 10 + .../core/actions/reporting/RecordPipe.java | 21 +- .../core/actions/tables/InsertAction.java | 30 ++- .../core/adapters/CsvToQRecordAdapter.java | 83 +++++- .../actions/tables/insert/InsertInput.java | 35 +++ .../tables/query/QueryOutputRecordPipe.java | 33 +-- .../Auth0AuthenticationModule.java | 91 +++++-- .../FullyAnonymousAuthenticationModule.java | 2 +- .../MockAuthenticationModule.java | 2 +- .../QAuthenticationModuleInterface.java | 2 +- .../etl/basic/BasicETLExtractFunction.java | 32 ++- .../etl/basic/BasicETLLoadFunction.java | 17 +- .../etl/streamed/StreamedETLBackendStep.java | 240 ++++++++++++++++++ .../etl/streamed/StreamedETLProcess.java | 75 ++++++ .../actions/reporting/ReportActionTest.java | 2 +- .../Auth0AuthenticationModuleTest.java | 89 +++++-- ...ullyAnonymousAuthenticationModuleTest.java | 4 +- .../etl/streamed/StreamedETLProcessTest.java | 89 +++++++ 20 files changed, 840 insertions(+), 104 deletions(-) create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/QBackendTransaction.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLBackendStep.java create mode 100644 qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLProcess.java create mode 100644 qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLProcessTest.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java index d5ab7318..11ef0eeb 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/ActionHelper.java @@ -34,6 +34,9 @@ import com.kingsrook.qqq.backend.core.modules.authentication.QAuthenticationModu *******************************************************************************/ public class ActionHelper { + private int f; + + /******************************************************************************* ** @@ -42,7 +45,7 @@ public class ActionHelper { QAuthenticationModuleDispatcher qAuthenticationModuleDispatcher = new QAuthenticationModuleDispatcher(); QAuthenticationModuleInterface authenticationModule = qAuthenticationModuleDispatcher.getQModule(request.getAuthenticationMetaData()); - if(!authenticationModule.isSessionValid(request.getSession())) + if(!authenticationModule.isSessionValid(request.getInstance(), request.getSession())) { throw new QAuthenticationException("Invalid session in request"); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/QBackendTransaction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/QBackendTransaction.java new file mode 100644 index 00000000..4220ec90 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/QBackendTransaction.java @@ -0,0 +1,82 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.actions; + + +import java.io.IOException; +import com.kingsrook.qqq.backend.core.exceptions.QException; + + +/******************************************************************************* + ** Container wherein backend modules can track data and/or objects that are + ** part of a transaction. + ** + ** Most obvious use-case would be a JDBC Connection. See subclass in rdbms module. + *******************************************************************************/ +public class QBackendTransaction +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public void commit() throws QException + { + //////////////////////// + // noop in base class // + //////////////////////// + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public void rollback() throws QException + { + //////////////////////// + // noop in base class // + //////////////////////// + } + + + + /******************************************************************************* + * Closes this stream and releases any system resources associated + * with it. If the stream is already closed then invoking this + * method has no effect. + * + *

As noted in {@link AutoCloseable#close()}, cases where the + * close may fail require careful attention. It is strongly advised + * to relinquish the underlying resources and to internally + * mark the {@code Closeable} as closed, prior to throwing + * the {@code IOException}. + * + * @throws IOException + * if an I/O error occurs + *******************************************************************************/ + public void close() + { + //////////////////////// + // noop in base class // + //////////////////////// + } +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/InsertInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/InsertInterface.java index 34e79045..dc90de66 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/InsertInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/interfaces/InsertInterface.java @@ -22,6 +22,7 @@ package com.kingsrook.qqq.backend.core.actions.interfaces; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; @@ -37,4 +38,13 @@ public interface InsertInterface ** *******************************************************************************/ InsertOutput execute(InsertInput insertInput) throws QException; + + /******************************************************************************* + ** + *******************************************************************************/ + default QBackendTransaction openTransaction(InsertInput insertInput) throws QException + { + return (new QBackendTransaction()); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java index cb8e3b6d..3703a841 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/reporting/RecordPipe.java @@ -25,7 +25,11 @@ package com.kingsrook.qqq.backend.core.actions.reporting; import java.util.ArrayList; import java.util.List; import java.util.concurrent.ArrayBlockingQueue; +import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; /******************************************************************************* @@ -34,16 +38,25 @@ import com.kingsrook.qqq.backend.core.model.data.QRecord; *******************************************************************************/ public class RecordPipe { - private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(10_000); + private static final Logger LOG = LogManager.getLogger(RecordPipe.class); + + private ArrayBlockingQueue queue = new ArrayBlockingQueue<>(1_000); /******************************************************************************* ** Add a record to the pipe ** Returns true iff the record fit in the pipe; false if the pipe is currently full. *******************************************************************************/ - public boolean addRecord(QRecord record) + public void addRecord(QRecord record) { - return (queue.offer(record)); + boolean offerResult = queue.offer(record); + + while(!offerResult) + { + LOG.debug("Record pipe.add failed (due to full pipe). Blocking."); + SleepUtils.sleep(100, TimeUnit.MILLISECONDS); + offerResult = queue.offer(record); + } } @@ -53,7 +66,7 @@ public class RecordPipe *******************************************************************************/ public void addRecords(List records) { - queue.addAll(records); + records.forEach(this::addRecord); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java index d4f6cd92..54ebcfb2 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/actions/tables/InsertAction.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.actions.tables; import com.kingsrook.qqq.backend.core.actions.ActionHelper; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertOutput; @@ -47,14 +48,35 @@ public class InsertAction *******************************************************************************/ public InsertOutput execute(InsertInput insertInput) throws QException { - ActionHelper.validateSession(insertInput); - - QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); - QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(insertInput.getBackend()); + QBackendModuleInterface qModule = getBackendModuleInterface(insertInput); // todo pre-customization - just get to modify the request? InsertOutput insertOutput = qModule.getInsertInterface().execute(insertInput); // todo post-customization - can do whatever w/ the result if you want return insertOutput; } + + + /******************************************************************************* + ** + *******************************************************************************/ + private QBackendModuleInterface getBackendModuleInterface(InsertInput insertInput) throws QException + { + ActionHelper.validateSession(insertInput); + QBackendModuleDispatcher qBackendModuleDispatcher = new QBackendModuleDispatcher(); + QBackendModuleInterface qModule = qBackendModuleDispatcher.getQBackendModule(insertInput.getBackend()); + return (qModule); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QBackendTransaction openTransaction(InsertInput insertInput) throws QException + { + QBackendModuleInterface qModule = getBackendModuleInterface(insertInput); + return (qModule.getInsertInterface().openTransaction(insertInput)); + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java index 38b7f58a..3d5493a3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/adapters/CsvToQRecordAdapter.java @@ -28,6 +28,8 @@ import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Map; +import java.util.function.Consumer; +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.AbstractQFieldMapping; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.fields.QFieldMetaData; @@ -41,9 +43,44 @@ import org.apache.commons.csv.CSVRecord; /******************************************************************************* ** Adapter class to convert a CSV string into a list of QRecords. ** + ** Based on which method is called, can either take a pipe, and stream records + ** into it - or return a list of all records from the file. Either way, at this + ** time, the full CSV string is read & parsed - a future optimization might read + ** the CSV content from a stream as well. *******************************************************************************/ public class CsvToQRecordAdapter { + private RecordPipe recordPipe = null; + private List recordList = null; + + + + /******************************************************************************* + ** stream records from a CSV String into a RecordPipe, for a given table, optionally + ** using a given mapping. + ** + *******************************************************************************/ + public void buildRecordsFromCsv(RecordPipe recordPipe, String csv, QTableMetaData table, AbstractQFieldMapping mapping, Consumer recordCustomizer) + { + this.recordPipe = recordPipe; + doBuildRecordsFromCsv(csv, table, mapping, recordCustomizer); + } + + + + /******************************************************************************* + ** convert a CSV String into a List of QRecords, for a given table, optionally + ** using a given mapping. + ** + *******************************************************************************/ + public List buildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping mapping) + { + this.recordList = new ArrayList<>(); + doBuildRecordsFromCsv(csv, table, mapping, null); + return (recordList); + } + + /******************************************************************************* ** convert a CSV String into a List of QRecords, for a given table, optionally @@ -51,14 +88,13 @@ public class CsvToQRecordAdapter ** ** todo - meta-data validation, type handling *******************************************************************************/ - public List buildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping mapping) + public void doBuildRecordsFromCsv(String csv, QTableMetaData table, AbstractQFieldMapping mapping, Consumer recordCustomizer) { if(!StringUtils.hasContent(csv)) { throw (new IllegalArgumentException("Empty csv value was provided.")); } - List rs = new ArrayList<>(); try { /////////////////////////////////////////////////////////////////////////////////////////////////////////////// @@ -82,7 +118,7 @@ public class CsvToQRecordAdapter // put values from the CSV record into a map of header -> value // ////////////////////////////////////////////////////////////////// Map csvValues = new HashMap<>(); - for(int i=0; i recordCustomizer, QRecord qRecord) + { + if(recordCustomizer != null) + { + recordCustomizer.accept(qRecord); + } } @@ -165,7 +216,7 @@ public class CsvToQRecordAdapter for(String header : headers) { - String headerToUse = header; + String headerToUse = header; String headerWithoutSuffix = header.replaceFirst(" \\d+$", ""); if(countsByHeader.containsKey(headerWithoutSuffix)) @@ -183,4 +234,22 @@ public class CsvToQRecordAdapter return (rs); } + + + /******************************************************************************* + ** Add a record - either to the pipe, or list, whichever we're building. + *******************************************************************************/ + private void addRecord(QRecord record) + { + if(recordPipe != null) + { + recordPipe.addRecord(record); + } + + if(recordList != null) + { + recordList.add(record); + } + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/insert/InsertInput.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/insert/InsertInput.java index d2ec4f15..24320d98 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/insert/InsertInput.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/insert/InsertInput.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.insert; import java.util.List; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.model.actions.AbstractTableActionInput; import com.kingsrook.qqq.backend.core.model.data.QRecord; import com.kingsrook.qqq.backend.core.model.metadata.QInstance; @@ -34,6 +35,7 @@ import com.kingsrook.qqq.backend.core.model.metadata.QInstance; *******************************************************************************/ public class InsertInput extends AbstractTableActionInput { + private QBackendTransaction transaction; private List records; @@ -57,6 +59,39 @@ public class InsertInput extends AbstractTableActionInput + /******************************************************************************* + ** Getter for transaction + ** + *******************************************************************************/ + public QBackendTransaction getTransaction() + { + return transaction; + } + + + + /******************************************************************************* + ** Setter for transaction + ** + *******************************************************************************/ + public void setTransaction(QBackendTransaction transaction) + { + this.transaction = transaction; + } + + + /******************************************************************************* + ** Fluent setter for transaction + ** + *******************************************************************************/ + public InsertInput withTransaction(QBackendTransaction transaction) + { + this.transaction = transaction; + return (this); + } + + + /******************************************************************************* ** Getter for records ** diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputRecordPipe.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputRecordPipe.java index 9e65ade9..6f8ce386 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputRecordPipe.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/actions/tables/query/QueryOutputRecordPipe.java @@ -23,10 +23,8 @@ package com.kingsrook.qqq.backend.core.model.actions.tables.query; import java.util.List; -import java.util.concurrent.TimeUnit; import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.model.data.QRecord; -import com.kingsrook.qqq.backend.core.utils.SleepUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; @@ -58,35 +56,7 @@ class QueryOutputRecordPipe implements QueryOutputStorageInterface @Override public void addRecord(QRecord record) { - if(!recordPipe.addRecord(record)) - { - do - { - LOG.debug("Record pipe.add failed (due to full pipe). Blocking."); - SleepUtils.sleep(10, TimeUnit.MILLISECONDS); - } - while(!recordPipe.addRecord(record)); - LOG.debug("Done blocking."); - } - } - - - - /******************************************************************************* - ** - *******************************************************************************/ - private void blockIfPipeIsTooFull() - { - if(recordPipe.countAvailableRecords() >= 100_000) - { - LOG.info("Record pipe is kinda full. Blocking for a bit"); - do - { - SleepUtils.sleep(10, TimeUnit.MILLISECONDS); - } - while(recordPipe.countAvailableRecords() >= 10_000); - LOG.info("Done blocking."); - } + recordPipe.addRecord(record); } @@ -98,7 +68,6 @@ class QueryOutputRecordPipe implements QueryOutputStorageInterface public void addRecords(List records) { recordPipe.addRecords(records); - blockIfPipeIsTooFull(); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java index 79bc0d73..435a13ce 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModule.java @@ -57,20 +57,25 @@ import org.json.JSONObject; *******************************************************************************/ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface { - private static final Logger logger = LogManager.getLogger(Auth0AuthenticationModule.class); + private static final Logger LOG = LogManager.getLogger(Auth0AuthenticationModule.class); - private static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 300; + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + // 30 minutes - ideally this would be lower, but right now we've been dealing with re-validation issues... // + ///////////////////////////////////////////////////////////////////////////////////////////////////////////// + public static final int ID_TOKEN_VALIDATION_INTERVAL_SECONDS = 1800; public static final String AUTH0_ID_TOKEN_KEY = "sessionId"; public static final String TOKEN_NOT_PROVIDED_ERROR = "Id Token was not provided"; - public static final String COULD_NOT_DECODE_ERROR = "Unable to decode id token"; - public static final String EXPIRED_TOKEN_ERROR = "Token has expired"; - public static final String INVALID_TOKEN_ERROR = "An invalid token was provided"; + public static final String COULD_NOT_DECODE_ERROR = "Unable to decode id token"; + public static final String EXPIRED_TOKEN_ERROR = "Token has expired"; + public static final String INVALID_TOKEN_ERROR = "An invalid token was provided"; private Instant now; + + /******************************************************************************* ** *******************************************************************************/ @@ -83,7 +88,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface String idToken = context.get(AUTH0_ID_TOKEN_KEY); if(idToken == null) { - logger.warn(TOKEN_NOT_PROVIDED_ERROR); + LOG.warn(TOKEN_NOT_PROVIDED_ERROR); throw (new QAuthenticationException(TOKEN_NOT_PROVIDED_ERROR)); } @@ -97,7 +102,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // then call method to check more session validity // ///////////////////////////////////////////////////// QSession qSession = buildQSessionFromToken(idToken); - if(isSessionValid(qSession)) + if(isSessionValid(qInstance, qSession)) { return (qSession); } @@ -112,7 +117,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // put now into state so we dont check until next interval passes // /////////////////////////////////////////////////////////////////// StateProviderInterface spi = getStateProvider(); - Auth0StateKey key = new Auth0StateKey(qSession.getIdReference()); + Auth0StateKey key = new Auth0StateKey(qSession.getIdReference()); spi.put(key, Instant.now()); return (qSession); @@ -122,12 +127,12 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface //////////////////////////////// // could not decode the token // //////////////////////////////// - logger.warn(COULD_NOT_DECODE_ERROR, jde); + LOG.warn(COULD_NOT_DECODE_ERROR, jde); throw (new QAuthenticationException(COULD_NOT_DECODE_ERROR)); } catch(TokenExpiredException tee) { - logger.info(EXPIRED_TOKEN_ERROR, tee); + LOG.info(EXPIRED_TOKEN_ERROR, tee); throw (new QAuthenticationException(EXPIRED_TOKEN_ERROR)); } catch(JWTVerificationException | JwkException jve) @@ -135,7 +140,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface /////////////////////////////////////////// // token had invalid signature or claims // /////////////////////////////////////////// - logger.warn(INVALID_TOKEN_ERROR, jve); + LOG.warn(INVALID_TOKEN_ERROR, jve); throw (new QAuthenticationException(INVALID_TOKEN_ERROR)); } catch(Exception e) @@ -144,7 +149,7 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // ¯\_(ツ)_/¯ // //////////////// String message = "An unknown error occurred"; - logger.error(message, e); + LOG.error(message, e); throw (new QAuthenticationException(message)); } } @@ -155,16 +160,16 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface ** *******************************************************************************/ @Override - public boolean isSessionValid(QSession session) + public boolean isSessionValid(QInstance instance, QSession session) { if(session == null) { return (false); } - StateProviderInterface spi = getStateProvider(); - Auth0StateKey key = new Auth0StateKey(session.getIdReference()); - Optional lastTimeCheckedOptional = spi.get(Instant.class, key); + StateProviderInterface spi = getStateProvider(); + Auth0StateKey key = new Auth0StateKey(session.getIdReference()); + Optional lastTimeCheckedOptional = spi.get(Instant.class, key); if(lastTimeCheckedOptional.isPresent()) { Instant lastTimeChecked = lastTimeCheckedOptional.get(); @@ -174,7 +179,28 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface // - so this is basically saying, if the time between the last time we checked the token and // // right now is more than ID_TOKEN_VALIDATION_INTERVAL_SECTIONS, then session needs revalidated // /////////////////////////////////////////////////////////////////////////////////////////////////// - return (Duration.between(lastTimeChecked, Instant.now()).compareTo(Duration.ofSeconds(ID_TOKEN_VALIDATION_INTERVAL_SECONDS)) < 0); + if(Duration.between(lastTimeChecked, Instant.now()).compareTo(Duration.ofSeconds(ID_TOKEN_VALIDATION_INTERVAL_SECONDS)) < 0) + { + return (true); + } + + try + { + LOG.debug("Re-validating token due to validation interval being passed: " + session.getIdReference()); + revalidateToken(instance, session.getIdReference()); + + ////////////////////////////////////////////////////////////////// + // update the timestamp in state provider, to avoid re-checking // + ////////////////////////////////////////////////////////////////// + spi.put(key, Instant.now()); + + return (true); + } + catch(Exception e) + { + LOG.warn(INVALID_TOKEN_ERROR, e); + return (false); + } } return (false); @@ -190,10 +216,10 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface { Auth0AuthenticationMetaData metaData = (Auth0AuthenticationMetaData) qInstance.getAuthentication(); - DecodedJWT jwt = JWT.decode(idToken); - JwkProvider provider = new UrlJwkProvider(metaData.getBaseUrl()); - Jwk jwk = provider.get(jwt.getKeyId()); - Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null); + DecodedJWT jwt = JWT.decode(idToken); + JwkProvider provider = new UrlJwkProvider(metaData.getBaseUrl()); + Jwk jwk = provider.get(jwt.getKeyId()); + Algorithm algorithm = Algorithm.RSA256((RSAPublicKey) jwk.getPublicKey(), null); JWTVerifier verifier = JWT.require(algorithm) .withIssuer(metaData.getBaseUrl()) .build(); @@ -217,20 +243,31 @@ public class Auth0AuthenticationModule implements QAuthenticationModuleInterface //////////////////////////////////// // decode and extract the payload // //////////////////////////////////// - DecodedJWT jwt = JWT.decode(idToken); - Base64.Decoder decoder = Base64.getUrlDecoder(); - String payloadString = new String(decoder.decode(jwt.getPayload())); - JSONObject payload = new JSONObject(payloadString); + DecodedJWT jwt = JWT.decode(idToken); + Base64.Decoder decoder = Base64.getUrlDecoder(); + String payloadString = new String(decoder.decode(jwt.getPayload())); + JSONObject payload = new JSONObject(payloadString); QUser qUser = new QUser(); - qUser.setFullName(payload.getString("name")); + if(payload.has("name")) + { + qUser.setFullName(payload.getString("name")); + } + else + { + qUser.setFullName("Unknown"); + } + if(payload.has("email")) { qUser.setIdReference(payload.getString("email")); } else { - qUser.setIdReference(payload.getString("nickname")); + if(payload.has("sub")) + { + qUser.setIdReference(payload.getString("sub")); + } } QSession qSession = new QSession(); diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java index 442941b5..8a0e7078 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModule.java @@ -66,7 +66,7 @@ public class FullyAnonymousAuthenticationModule implements QAuthenticationModule ** *******************************************************************************/ @Override - public boolean isSessionValid(QSession session) + public boolean isSessionValid(QInstance instance, QSession session) { return session != null; } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java index 21b25c2a..f490d0a4 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/MockAuthenticationModule.java @@ -63,7 +63,7 @@ public class MockAuthenticationModule implements QAuthenticationModuleInterface ** *******************************************************************************/ @Override - public boolean isSessionValid(QSession session) + public boolean isSessionValid(QInstance instance, QSession session) { if(session == null) { diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java index ec5db589..75c1535f 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/modules/authentication/QAuthenticationModuleInterface.java @@ -43,5 +43,5 @@ public interface QAuthenticationModuleInterface /******************************************************************************* ** *******************************************************************************/ - boolean isSessionValid(QSession session); + boolean isSessionValid(QInstance instance, QSession session); } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLExtractFunction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLExtractFunction.java index 20c27808..9b0d0687 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLExtractFunction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLExtractFunction.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.basic; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; import com.kingsrook.qqq.backend.core.actions.tables.QueryAction; import com.kingsrook.qqq.backend.core.exceptions.QException; import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; @@ -40,6 +41,8 @@ public class BasicETLExtractFunction implements BackendStep { private static final Logger LOG = LogManager.getLogger(BasicETLExtractFunction.class); + private RecordPipe recordPipe = null; + /******************************************************************************* @@ -64,10 +67,35 @@ public class BasicETLExtractFunction implements BackendStep // queryRequest.setFilter(JsonUtils.toObject(filter, QQueryFilter.class)); // } + ////////////////////////////////////////////////////////////////////// + // if the caller gave us a record pipe, pass it to the query action // + ////////////////////////////////////////////////////////////////////// + if (recordPipe != null) + { + queryInput.setRecordPipe(recordPipe); + } + QueryAction queryAction = new QueryAction(); QueryOutput queryOutput = queryAction.execute(queryInput); - runBackendStepOutput.setRecords(queryOutput.getRecords()); - LOG.info("Query on table " + tableName + " produced " + queryOutput.getRecords().size() + " records."); + if (recordPipe == null) + { + //////////////////////////////////////////////////////////////////////////// + // only return the records (and log about them) if there's no record pipe // + //////////////////////////////////////////////////////////////////////////// + runBackendStepOutput.setRecords(queryOutput.getRecords()); + LOG.info("Query on table " + tableName + " produced " + queryOutput.getRecords().size() + " records."); + } + } + + + + /******************************************************************************* + ** Setter for recordPipe + ** + *******************************************************************************/ + public void setRecordPipe(RecordPipe recordPipe) + { + this.recordPipe = recordPipe; } } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java index 997e377b..0ce1c572 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/basic/BasicETLLoadFunction.java @@ -24,6 +24,7 @@ package com.kingsrook.qqq.backend.core.processes.implementations.etl.basic; import java.util.ArrayList; import java.util.List; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; import com.kingsrook.qqq.backend.core.exceptions.QException; @@ -44,6 +45,8 @@ public class BasicETLLoadFunction implements BackendStep { private static final Logger LOG = LogManager.getLogger(BasicETLLoadFunction.class); + private QBackendTransaction transaction; + /******************************************************************************* @@ -86,10 +89,11 @@ public class BasicETLLoadFunction implements BackendStep insertInput.setSession(runBackendStepInput.getSession()); insertInput.setTableName(table); insertInput.setRecords(page); + insertInput.setTransaction(transaction); InsertAction insertAction = new InsertAction(); InsertOutput insertOutput = insertAction.execute(insertInput); - outputRecords.addAll(insertOutput.getRecords()); + // todo - this is to avoid garbage leak in state provider... outputRecords.addAll(insertOutput.getRecords()); recordsInserted += insertOutput.getRecords().size(); } @@ -97,4 +101,15 @@ public class BasicETLLoadFunction implements BackendStep runBackendStepOutput.addValue(BasicETLProcess.FIELD_RECORD_COUNT, recordsInserted); } + + + /******************************************************************************* + ** Setter for transaction + ** + *******************************************************************************/ + public void setTransaction(QBackendTransaction transaction) + { + this.transaction = transaction; + } + } diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLBackendStep.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLBackendStep.java new file mode 100644 index 00000000..bb4337dc --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLBackendStep.java @@ -0,0 +1,240 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed; + + +import java.util.List; +import java.util.Optional; +import java.util.concurrent.TimeUnit; +import com.kingsrook.qqq.backend.core.actions.QBackendTransaction; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobManager; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobState; +import com.kingsrook.qqq.backend.core.actions.async.AsyncJobStatus; +import com.kingsrook.qqq.backend.core.actions.processes.BackendStep; +import com.kingsrook.qqq.backend.core.actions.reporting.RecordPipe; +import com.kingsrook.qqq.backend.core.actions.tables.InsertAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunBackendStepOutput; +import com.kingsrook.qqq.backend.core.model.actions.tables.insert.InsertInput; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLExtractFunction; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLLoadFunction; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLProcess; +import com.kingsrook.qqq.backend.core.processes.implementations.etl.basic.BasicETLTransformFunction; +import com.kingsrook.qqq.backend.core.utils.SleepUtils; +import org.apache.logging.log4j.LogManager; +import org.apache.logging.log4j.Logger; + + +/******************************************************************************* + ** Backend step to do a streamed ETL + *******************************************************************************/ +public class StreamedETLBackendStep implements BackendStep +{ + private static final Logger LOG = LogManager.getLogger(StreamedETLBackendStep.class); + + private static final int TIMEOUT_AFTER_NO_RECORDS_MS = 10 * 60 * 1000; + + private static final int MAX_SLEEP_MS = 1000; + private static final int INIT_SLEEP_MS = 10; + + + + /******************************************************************************* + ** + *******************************************************************************/ + @Override + public void run(RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) throws QException + { + QBackendTransaction transaction = openTransaction(runBackendStepInput); + + try + { + RecordPipe recordPipe = new RecordPipe(); + BasicETLExtractFunction basicETLExtractFunction = new BasicETLExtractFunction(); + basicETLExtractFunction.setRecordPipe(recordPipe); + + ////////////////////////////////////////// + // run the query action as an async job // + ////////////////////////////////////////// + AsyncJobManager asyncJobManager = new AsyncJobManager(); + String queryJobUUID = asyncJobManager.startJob("ReportAction>QueryAction", (status) -> + { + basicETLExtractFunction.run(runBackendStepInput, runBackendStepOutput); + return (runBackendStepOutput); + }); + LOG.info("Started query job [" + queryJobUUID + "] for report"); + + AsyncJobState queryJobState = AsyncJobState.RUNNING; + AsyncJobStatus asyncJobStatus = null; + + long recordCount = 0; + int nextSleepMillis = INIT_SLEEP_MS; + long lastReceivedRecordsAt = System.currentTimeMillis(); + long jobStartTime = System.currentTimeMillis(); + + while(queryJobState.equals(AsyncJobState.RUNNING)) + { + if(recordPipe.countAvailableRecords() == 0) + { + /////////////////////////////////////////////////////////// + // if the pipe is empty, sleep to let the producer work. // + // todo - smarter sleep? like get notified vs. sleep? // + /////////////////////////////////////////////////////////// + LOG.info("No records are available in the pipe. Sleeping [" + nextSleepMillis + "] ms to give producer a chance to work"); + SleepUtils.sleep(nextSleepMillis, TimeUnit.MILLISECONDS); + nextSleepMillis = Math.min(nextSleepMillis * 2, MAX_SLEEP_MS); + + long timeSinceLastReceivedRecord = System.currentTimeMillis() - lastReceivedRecordsAt; + if(timeSinceLastReceivedRecord > TIMEOUT_AFTER_NO_RECORDS_MS) + { + throw (new QException("Query action appears to have stopped producing records (last record received " + timeSinceLastReceivedRecord + " ms ago).")); + } + } + else + { + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // if the pipe has records, consume them. reset the sleep timer so if we sleep again it'll be short. // + //////////////////////////////////////////////////////////////////////////////////////////////////////// + lastReceivedRecordsAt = System.currentTimeMillis(); + nextSleepMillis = INIT_SLEEP_MS; + + recordCount += consumeRecordsFromPipe(recordPipe, runBackendStepInput, runBackendStepOutput, transaction); + + LOG.info(String.format("Processed %,d records so far", recordCount)); + } + + //////////////////////////////////// + // refresh the query job's status // + //////////////////////////////////// + Optional optionalAsyncJobStatus = asyncJobManager.getJobStatus(queryJobUUID); + if(optionalAsyncJobStatus.isEmpty()) + { + ///////////////////////////////////////////////// + // todo - ... maybe some version of try-again? // + ///////////////////////////////////////////////// + throw (new QException("Could not get status of report query job [" + queryJobUUID + "]")); + } + asyncJobStatus = optionalAsyncJobStatus.get(); + queryJobState = asyncJobStatus.getState(); + } + + LOG.info("Query job [" + queryJobUUID + "] for ETL completed with status: " + asyncJobStatus); + + ////////////////////////////////////////////////////// + // send the final records to transform & load steps // + ////////////////////////////////////////////////////// + recordCount += consumeRecordsFromPipe(recordPipe, runBackendStepInput, runBackendStepOutput, transaction); + + ///////////////////// + // commit the work // + ///////////////////// + transaction.commit(); + + long reportEndTime = System.currentTimeMillis(); + LOG.info(String.format("Processed %,d records", recordCount) + + String.format(" at end of ETL job in %,d ms (%.2f records/second).", (reportEndTime - jobStartTime), 1000d * (recordCount / (.001d + (reportEndTime - jobStartTime))))); + + runBackendStepOutput.addValue(StreamedETLProcess.FIELD_RECORD_COUNT, recordCount); + } + catch(Exception e) + { + //////////////////////////////////////////////////////////////////////////////// + // rollback the work, then re-throw the error for up-stream to catch & report // + //////////////////////////////////////////////////////////////////////////////// + transaction.rollback(); + throw (e); + } + finally + { + //////////////////////////////////////////////////////////// + // always close our transactions (e.g., jdbc connections) // + //////////////////////////////////////////////////////////// + transaction.close(); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private QBackendTransaction openTransaction(RunBackendStepInput runBackendStepInput) throws QException + { + InsertInput insertInput = new InsertInput(runBackendStepInput.getInstance()); + + insertInput.setSession(runBackendStepInput.getSession()); + insertInput.setTableName(runBackendStepInput.getValueString(BasicETLProcess.FIELD_DESTINATION_TABLE)); + + return new InsertAction().openTransaction(insertInput); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private int consumeRecordsFromPipe(RecordPipe recordPipe, RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput, QBackendTransaction transaction) throws QException + { + List qRecords = recordPipe.consumeAvailableRecords(); + + preTransform(qRecords, runBackendStepInput, runBackendStepOutput); + + runBackendStepInput.setRecords(qRecords); + new BasicETLTransformFunction().run(runBackendStepInput, runBackendStepOutput); + + postTransform(qRecords, runBackendStepInput, runBackendStepOutput); + + runBackendStepInput.setRecords(runBackendStepOutput.getRecords()); + BasicETLLoadFunction basicETLLoadFunction = new BasicETLLoadFunction(); + basicETLLoadFunction.setTransaction(transaction); + basicETLLoadFunction.run(runBackendStepInput, runBackendStepOutput); + + return (qRecords.size()); + } + + + + /******************************************************************************* + ** Customization point for subclasses of this step. + *******************************************************************************/ + protected void preTransform(List qRecords, RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) + { + //////////////////////// + // noop in base class // + //////////////////////// + } + + + + /******************************************************************************* + ** Customization point for subclasses of this step. + *******************************************************************************/ + protected void postTransform(List qRecords, RunBackendStepInput runBackendStepInput, RunBackendStepOutput runBackendStepOutput) + { + //////////////////////// + // noop in base class // + //////////////////////// + } + +} diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLProcess.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLProcess.java new file mode 100644 index 00000000..2fa636e7 --- /dev/null +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLProcess.java @@ -0,0 +1,75 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed; + + +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeReference; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeType; +import com.kingsrook.qqq.backend.core.model.metadata.code.QCodeUsage; +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.processes.QBackendStepMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionInputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QFunctionOutputMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QProcessMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.processes.QStepMetaData; + + +/******************************************************************************* + ** Definition for Streamed ETL process. + *******************************************************************************/ +public class StreamedETLProcess +{ + public static final String PROCESS_NAME = "etl.streamed"; + + public static final String FUNCTION_NAME_ETL = "streamedETL"; + + public static final String FIELD_SOURCE_TABLE = "sourceTable"; + public static final String FIELD_DESTINATION_TABLE = "destinationTable"; + public static final String FIELD_MAPPING_JSON = "mappingJSON"; + public static final String FIELD_RECORD_COUNT = "recordCount"; + + + + /******************************************************************************* + ** + *******************************************************************************/ + public QProcessMetaData defineProcessMetaData() + { + QStepMetaData etlFunction = new QBackendStepMetaData() + .withName(FUNCTION_NAME_ETL) + .withCode(new QCodeReference() + .withName(StreamedETLBackendStep.class.getName()) + .withCodeType(QCodeType.JAVA) + .withCodeUsage(QCodeUsage.BACKEND_STEP)) + .withInputData(new QFunctionInputMetaData() + .withField(new QFieldMetaData(FIELD_SOURCE_TABLE, QFieldType.STRING)) + .withField(new QFieldMetaData(FIELD_MAPPING_JSON, QFieldType.STRING)) + .withField(new QFieldMetaData(FIELD_DESTINATION_TABLE, QFieldType.STRING))) + .withOutputMetaData(new QFunctionOutputMetaData() + .addField(new QFieldMetaData(FIELD_RECORD_COUNT, QFieldType.INTEGER))); + + return new QProcessMetaData() + .withName(PROCESS_NAME) + .addStep(etlFunction); + } +} diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportActionTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportActionTest.java index f29a62fa..71d2807f 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportActionTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/actions/reporting/ReportActionTest.java @@ -80,7 +80,7 @@ class ReportActionTest public void testBigger() throws Exception { // int recordCount = 2_000_000; // to really stress locally, use this. - int recordCount = 200_000; + int recordCount = 50_000; String filename = "/tmp/ReportActionTest.csv"; runReport(recordCount, filename, ReportFormat.CSV, false); diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java index 99cbb6b6..049d3c57 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/Auth0AuthenticationModuleTest.java @@ -23,6 +23,7 @@ package com.kingsrook.qqq.backend.core.modules.authentication; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.HashMap; import java.util.Map; import com.kingsrook.qqq.backend.core.exceptions.QAuthenticationException; @@ -31,7 +32,6 @@ import com.kingsrook.qqq.backend.core.model.session.QSession; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.Auth0AuthenticationMetaData; import com.kingsrook.qqq.backend.core.modules.authentication.metadata.QAuthenticationMetaData; import com.kingsrook.qqq.backend.core.state.InMemoryStateProvider; -import com.kingsrook.qqq.backend.core.state.StateProviderInterface; import com.kingsrook.qqq.backend.core.utils.TestUtils; import org.junit.jupiter.api.Test; import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.AUTH0_ID_TOKEN_KEY; @@ -40,7 +40,8 @@ import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0Authent import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.INVALID_TOKEN_ERROR; import static com.kingsrook.qqq.backend.core.modules.authentication.Auth0AuthenticationModule.TOKEN_NOT_PROVIDED_ERROR; import static org.assertj.core.api.Assertions.assertThat; -import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; +import static org.junit.jupiter.api.Assertions.assertTrue; import static org.junit.jupiter.api.Assertions.fail; @@ -49,9 +50,9 @@ import static org.junit.jupiter.api.Assertions.fail; *******************************************************************************/ public class Auth0AuthenticationModuleTest { - private static final String VALID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE5VDE2OjI0OjQ1LjgyMloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MjQ3OTAyLCJleHAiOjE2NTgyODM5MDIsIm5vbmNlIjoiZUhOdFMxbEtUR2N5ZG5KS1VVY3RkRTFVT0ZKNmJFNUxVVkEwZEdsRGVXOXZkVkl4UW41eVRrUlJlZz09In0.hib7JR8NDU2kx8Fj1bnzo3IUuabE6Hb-Z7HHZAJPQuF_Zdg3L1KDypn6SY7HAd_dsz2N8RkXfvQto-Y2g2ukuz7FxzNFgcVL99cyEO3YqmyCa6JTOTCrxdeaIE8QZpCEKvC28oeJBv0wO1Dwc--OVJMsK2vSzyxj1WNok64YYjWKLL4c0dFf-nj0KWFr1IU-tMiyWLDDiJw2Sa8M4YxXZYqdlkgNmrBPExgcm9l9SiT2l3Ts3Sgc_IyMVyMrnV8XX50EWdsm6vuCOSUcqf0XhjDQ7urZveoVwVLnYq3GcLhVBcy1Hr9RL8zPdPynOzsbX6uCww2Esrv6iwWrgQ5zBA"; - private static final String INVALID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE5VDE2OjI0OjQ1LjgyMloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MjQ3OTAyLCJleHAiOjE2NTgyODM5MDIsIm5vbmNlIjoiZUhOdFMxbEtUR2N5ZG5KS1VVY3RkRTFVT0ZKNmJFNUxVVkEwZEdsRGVXOXZkVkl4UW41eVRrUlJlZz09In0.hib7JR8NDU2kx8Fj1bnzo3IUuabE6Hb-Z7HHZAJPQuF_Zdg3L1KDypn6SY7HAd_dsz2N8RkXfvQto-Y2g2ukuz7FxzNFgcVL99cyEO3YqmyCa6JTOTCrxdeaIE8QZpCEKvC28oeJBv0wO1Dwc--OVJMsK2vSzyxj1WNok64YYjWKLL4c0dFf-nj0KWFr1IU-tMiyWLDDiJw2Sa8M4YxXZYqdlkgNmrBPExgcm9l9SiT2l3Ts3Sgc_IyMVyMrnV8XX50EWdsm6vuCOSUcqf0XhjDQ7urZveoVwVLnYq3GcLhVBcy1Hr9RL8zPdPynOzsbX6uCww2Esrv6iwWrgQ5zBA-thismakesinvalid"; - private static final String EXPIRED_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE4VDIxOjM4OjE1LjM4NloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MTgwNDc3LCJleHAiOjE2NTgyMTY0NzcsIm5vbmNlIjoiVkZkQlYzWmplR2hvY1cwMk9WZEtabHBLU0c1K1ZXbElhMEV3VkZaeFpVdEJVMDErZUZaT1RtMTNiZz09In0.fU7EwUgNrupOPz_PX_aQKON2xG1-LWD85xVo1Bn41WNEek-iMyJoch8l6NUihi7Bou14BoOfeWIG_sMqsLHqI2Pk7el7l1kigsjURx0wpiXadBt8piMxdIlxdToZEMuZCBzg7eJvXh4sM8tlV5cm0gPa6FT9Ih3VGJajNlXi5BcYS_JRpIvFvHn8-Bxj4KiAlZ5XPPkopjnDgP8kFfc4cMn_nxDkqWYlhj-5TaGW2xCLC9Qr_9UNxX0fm-CkKjYs3Z5ezbiXNkc-bxrCYvxeBeDPf8-T3EqrxCRVqCZSJ85BHdOc_E7UZC_g8bNj0umoplGwlCbzO4XIuOO-KlIaOg"; + private static final String VALID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE5VDE2OjI0OjQ1LjgyMloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MjQ3OTAyLCJleHAiOjE2NTgyODM5MDIsIm5vbmNlIjoiZUhOdFMxbEtUR2N5ZG5KS1VVY3RkRTFVT0ZKNmJFNUxVVkEwZEdsRGVXOXZkVkl4UW41eVRrUlJlZz09In0.hib7JR8NDU2kx8Fj1bnzo3IUuabE6Hb-Z7HHZAJPQuF_Zdg3L1KDypn6SY7HAd_dsz2N8RkXfvQto-Y2g2ukuz7FxzNFgcVL99cyEO3YqmyCa6JTOTCrxdeaIE8QZpCEKvC28oeJBv0wO1Dwc--OVJMsK2vSzyxj1WNok64YYjWKLL4c0dFf-nj0KWFr1IU-tMiyWLDDiJw2Sa8M4YxXZYqdlkgNmrBPExgcm9l9SiT2l3Ts3Sgc_IyMVyMrnV8XX50EWdsm6vuCOSUcqf0XhjDQ7urZveoVwVLnYq3GcLhVBcy1Hr9RL8zPdPynOzsbX6uCww2Esrv6iwWrgQ5zBA"; + private static final String INVALID_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE5VDE2OjI0OjQ1LjgyMloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MjQ3OTAyLCJleHAiOjE2NTgyODM5MDIsIm5vbmNlIjoiZUhOdFMxbEtUR2N5ZG5KS1VVY3RkRTFVT0ZKNmJFNUxVVkEwZEdsRGVXOXZkVkl4UW41eVRrUlJlZz09In0.hib7JR8NDU2kx8Fj1bnzo3IUuabE6Hb-Z7HHZAJPQuF_Zdg3L1KDypn6SY7HAd_dsz2N8RkXfvQto-Y2g2ukuz7FxzNFgcVL99cyEO3YqmyCa6JTOTCrxdeaIE8QZpCEKvC28oeJBv0wO1Dwc--OVJMsK2vSzyxj1WNok64YYjWKLL4c0dFf-nj0KWFr1IU-tMiyWLDDiJw2Sa8M4YxXZYqdlkgNmrBPExgcm9l9SiT2l3Ts3Sgc_IyMVyMrnV8XX50EWdsm6vuCOSUcqf0XhjDQ7urZveoVwVLnYq3GcLhVBcy1Hr9RL8zPdPynOzsbX6uCww2Esrv6iwWrgQ5zBA-thismakesinvalid"; + private static final String EXPIRED_TOKEN = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6IllrY2FkWTA0Q3RFVUFxQUdLNTk3ayJ9.eyJnaXZlbl9uYW1lIjoiVGltIiwiZmFtaWx5X25hbWUiOiJDaGFtYmVybGFpbiIsIm5pY2tuYW1lIjoidGltLmNoYW1iZXJsYWluIiwibmFtZSI6IlRpbSBDaGFtYmVybGFpbiIsInBpY3R1cmUiOiJodHRwczovL2xoMy5nb29nbGV1c2VyY29udGVudC5jb20vYS0vQUZkWnVjcXVSaUFvTzk1RG9URklnbUtseVA1akVBVnZmWXFnS0lHTkVubzE9czk2LWMiLCJsb2NhbGUiOiJlbiIsInVwZGF0ZWRfYXQiOiIyMDIyLTA3LTE4VDIxOjM4OjE1LjM4NloiLCJlbWFpbCI6InRpbS5jaGFtYmVybGFpbkBraW5nc3Jvb2suY29tIiwiZW1haWxfdmVyaWZpZWQiOnRydWUsImlzcyI6Imh0dHBzOi8va2luZ3Nyb29rLnVzLmF1dGgwLmNvbS8iLCJzdWIiOiJnb29nbGUtb2F1dGgyfDEwODk2NDEyNjE3MjY1NzAzNDg2NyIsImF1ZCI6InNwQ1NtczAzcHpVZGRYN1BocHN4ZDlUd2FLMDlZZmNxIiwiaWF0IjoxNjU4MTgwNDc3LCJleHAiOjE2NTgyMTY0NzcsIm5vbmNlIjoiVkZkQlYzWmplR2hvY1cwMk9WZEtabHBLU0c1K1ZXbElhMEV3VkZaeFpVdEJVMDErZUZaT1RtMTNiZz09In0.fU7EwUgNrupOPz_PX_aQKON2xG1-LWD85xVo1Bn41WNEek-iMyJoch8l6NUihi7Bou14BoOfeWIG_sMqsLHqI2Pk7el7l1kigsjURx0wpiXadBt8piMxdIlxdToZEMuZCBzg7eJvXh4sM8tlV5cm0gPa6FT9Ih3VGJajNlXi5BcYS_JRpIvFvHn8-Bxj4KiAlZ5XPPkopjnDgP8kFfc4cMn_nxDkqWYlhj-5TaGW2xCLC9Qr_9UNxX0fm-CkKjYs3Z5ezbiXNkc-bxrCYvxeBeDPf8-T3EqrxCRVqCZSJ85BHdOc_E7UZC_g8bNj0umoplGwlCbzO4XIuOO-KlIaOg"; private static final String UNDECODABLE_TOKEN = "UNDECODABLE"; public static final String AUTH0_BASE_URL = "https://kingsrook.us.auth0.com/"; @@ -59,32 +60,79 @@ public class Auth0AuthenticationModuleTest /******************************************************************************* - ** Test a valid token where 'now' is set to a time that would be valid for it + ** Test an expired token where 'now' is set to a time that would not require it to be + ** re-checked, so it'll show as valid ** *******************************************************************************/ @Test - public void testLastTimeChecked() throws QAuthenticationException + public void testLastTimeCheckedNow() { - ////////////////////////////////////////////////////////// - // Tuesday, July 19, 2022 12:40:27.299 PM GMT-05:00 DST // - ////////////////////////////////////////////////////////// - Instant now = Instant.now(); + assertTrue(testLastTimeChecked(Instant.now(), UNDECODABLE_TOKEN), "A session just checked 'now' should always be valid"); + } - ///////////////////////////////////////////////////////// - // put the 'now' from the past into the state provider // - ///////////////////////////////////////////////////////// - StateProviderInterface spi = InMemoryStateProvider.getInstance(); - Auth0AuthenticationModule.Auth0StateKey key = new Auth0AuthenticationModule.Auth0StateKey(VALID_TOKEN); - spi.put(key, now); + + + /******************************************************************************* + ** Test an expired token where 'now' is set to a time that would not require it to be + ** re-checked, so it'll show as valid + ** + *******************************************************************************/ + @Test + public void testLastTimeCheckedJustUnderThreshold() + { + Instant underThreshold = Instant.now().minus(Auth0AuthenticationModule.ID_TOKEN_VALIDATION_INTERVAL_SECONDS - 60, ChronoUnit.SECONDS); + assertTrue(testLastTimeChecked(underThreshold, INVALID_TOKEN), "A session checked under threshold should be valid"); + } + + + + /******************************************************************************* + ** Test an expired token where 'now' is set to a time that would require it to be + ** re-checked + ** + *******************************************************************************/ + @Test + public void testLastTimeCheckedJustOverThreshold() + { + Instant overThreshold = Instant.now().minus(Auth0AuthenticationModule.ID_TOKEN_VALIDATION_INTERVAL_SECONDS + 60, ChronoUnit.SECONDS); + assertFalse(testLastTimeChecked(overThreshold, INVALID_TOKEN), "A session checked over threshold should be re-validated, and in this case, not be valid."); + } + + + + /******************************************************************************* + ** Test an expired token where 'now' is set to a time that would require it to be + ** re-checked + ** + *******************************************************************************/ + @Test + public void testLastTimeCheckedOverThresholdAndUndecodable() + { + Instant overThreshold = Instant.now().minus(Auth0AuthenticationModule.ID_TOKEN_VALIDATION_INTERVAL_SECONDS + 60, ChronoUnit.SECONDS); + assertFalse(testLastTimeChecked(overThreshold, UNDECODABLE_TOKEN), "A session checked over threshold should be re-validated, and in this case, not be valid."); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private boolean testLastTimeChecked(Instant lastTimeChecked, String token) + { + ///////////////////////////////////////////////////////////// + // put the input last-time-checked into the state provider // + ///////////////////////////////////////////////////////////// + Auth0AuthenticationModule.Auth0StateKey key = new Auth0AuthenticationModule.Auth0StateKey(token); + InMemoryStateProvider.getInstance().put(key, lastTimeChecked); ////////////////////// // build up session // ////////////////////// QSession session = new QSession(); - session.setIdReference(VALID_TOKEN); + session.setIdReference(token); Auth0AuthenticationModule auth0AuthenticationModule = new Auth0AuthenticationModule(); - assertEquals(true, auth0AuthenticationModule.isSessionValid(session), "Session should return as still valid."); + return (auth0AuthenticationModule.isSessionValid(getQInstance(), session)); } @@ -114,7 +162,7 @@ public class Auth0AuthenticationModuleTest /******************************************************************************* - ** Test failure case, token cant be decoded + ** Test failure case, token can't be decoded ** *******************************************************************************/ @Test @@ -220,4 +268,5 @@ public class Auth0AuthenticationModuleTest qInstance.setAuthentication(authenticationMetaData); return (qInstance); } + } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java index 68c1e7d8..f775511f 100644 --- a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/modules/authentication/FullyAnonymousAuthenticationModuleTest.java @@ -49,8 +49,8 @@ public class FullyAnonymousAuthenticationModuleTest assertNotNull(session.getIdReference(), "Session id ref should not be null"); assertNotNull(session.getUser(), "Session User should not be null"); assertNotNull(session.getUser().getIdReference(), "Session User id ref should not be null"); - assertTrue(fullyAnonymousAuthenticationModule.isSessionValid(session), "Any session should be valid"); - assertFalse(fullyAnonymousAuthenticationModule.isSessionValid(null), "null should be not valid"); + assertTrue(fullyAnonymousAuthenticationModule.isSessionValid(null, session), "Any session should be valid"); + assertFalse(fullyAnonymousAuthenticationModule.isSessionValid(null, null), "null should be not valid"); } } diff --git a/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLProcessTest.java b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLProcessTest.java new file mode 100644 index 00000000..97d756c9 --- /dev/null +++ b/qqq-backend-core/src/test/java/com/kingsrook/qqq/backend/core/processes/implementations/etl/streamed/StreamedETLProcessTest.java @@ -0,0 +1,89 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2022. Kingsrook, LLC + * 651 N Broad St Ste 205 # 6917 | Middletown DE 19709 | United States + * contact@kingsrook.com + * https://github.com/Kingsrook/ + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Affero General Public License as + * published by the Free Software Foundation, either version 3 of the + * License, or (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU Affero General Public License for more details. + * + * You should have received a copy of the GNU Affero General Public License + * along with this program. If not, see . + */ + +package com.kingsrook.qqq.backend.core.processes.implementations.etl.streamed; + + +import com.kingsrook.qqq.backend.core.actions.processes.RunProcessAction; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessInput; +import com.kingsrook.qqq.backend.core.model.actions.processes.RunProcessOutput; +import com.kingsrook.qqq.backend.core.model.actions.shared.mapping.QKeyBasedFieldMapping; +import com.kingsrook.qqq.backend.core.utils.JsonUtils; +import com.kingsrook.qqq.backend.core.utils.TestUtils; +import org.junit.jupiter.api.Test; +import static org.junit.jupiter.api.Assertions.assertNotNull; +import static org.junit.jupiter.api.Assertions.assertTrue; + + +/******************************************************************************* + ** Unit test for BasicETLProcess + *******************************************************************************/ +class StreamedETLProcessTest +{ + + /******************************************************************************* + ** Simplest happy path + *******************************************************************************/ + @Test + public void test() throws QException + { + RunProcessInput request = new RunProcessInput(TestUtils.defineInstance()); + request.setSession(TestUtils.getMockSession()); + request.setProcessName(StreamedETLProcess.PROCESS_NAME); + request.addValue(StreamedETLProcess.FIELD_SOURCE_TABLE, TestUtils.defineTablePerson().getName()); + request.addValue(StreamedETLProcess.FIELD_DESTINATION_TABLE, TestUtils.definePersonFileTable().getName()); + request.addValue(StreamedETLProcess.FIELD_MAPPING_JSON, ""); + + RunProcessOutput result = new RunProcessAction().execute(request); + assertNotNull(result); + assertTrue(result.getRecords().stream().allMatch(r -> r.getValues().containsKey("id")), "records should have an id, set by the process"); + assertTrue(result.getException().isEmpty()); + } + + + + /******************************************************************************* + ** Basic example of doing a mapping transformation + *******************************************************************************/ + @Test + public void testMappingTransformation() throws QException + { + RunProcessInput request = new RunProcessInput(TestUtils.defineInstance()); + request.setSession(TestUtils.getMockSession()); + request.setProcessName(StreamedETLProcess.PROCESS_NAME); + request.addValue(StreamedETLProcess.FIELD_SOURCE_TABLE, TestUtils.definePersonFileTable().getName()); + request.addValue(StreamedETLProcess.FIELD_DESTINATION_TABLE, TestUtils.defineTableIdAndNameOnly().getName()); + + /////////////////////////////////////////////////////////////////////////////////////// + // define our mapping from destination-table field names to source-table field names // + /////////////////////////////////////////////////////////////////////////////////////// + QKeyBasedFieldMapping mapping = new QKeyBasedFieldMapping().withMapping("name", "firstName"); + // request.addValue(StreamedETLProcess.FIELD_MAPPING_JSON, JsonUtils.toJson(mapping.getMapping())); + request.addValue(StreamedETLProcess.FIELD_MAPPING_JSON, JsonUtils.toJson(mapping)); + + RunProcessOutput result = new RunProcessAction().execute(request); + assertNotNull(result); + assertTrue(result.getRecords().stream().allMatch(r -> r.getValues().containsKey("id")), "records should have an id, set by the process"); + assertTrue(result.getException().isEmpty()); + } + +} \ No newline at end of file