From 6fbfbe9db243051f5f59e570c81c22cd4573fddf Mon Sep 17 00:00:00 2001 From: Darin Kelkhoff Date: Fri, 31 Mar 2023 16:29:30 -0500 Subject: [PATCH] API Logs! --- .../scripts/ScriptsMetaDataProvider.java | 2 +- .../qqq/api/javalin/QJavalinApiHandler.java | 161 +++++-- .../com/kingsrook/qqq/api/model/APILog.java | 393 ++++++++++++++++++ .../metadata/APILogMetaDataProvider.java | 119 ++++++ 4 files changed, 649 insertions(+), 26 deletions(-) create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java create mode 100644 qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java index be7030dc..d8235a2b 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/scripts/ScriptsMetaDataProvider.java @@ -386,7 +386,7 @@ public class ScriptsMetaDataProvider .withSection(new QFieldSection("changeManagement", new QIcon().withName("history"), Tier.T2, List.of("commitMessage", "author"))) .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("createDate", "modifyDate"))); - tableMetaData.getField("contents").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR)); + tableMetaData.getField("contents").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("javascript"))); tableMetaData.getField("scriptId").withFieldAdornment(AdornmentType.Size.LARGE.toAdornment()); return (tableMetaData); diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java index 05c68e45..a231f328 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/javalin/QJavalinApiHandler.java @@ -25,6 +25,7 @@ package com.kingsrook.qqq.api.javalin; import java.io.InputStream; import java.io.Serializable; import java.nio.charset.StandardCharsets; +import java.time.Instant; import java.util.ArrayList; import java.util.Arrays; import java.util.Base64; @@ -37,6 +38,7 @@ import java.util.Set; import java.util.stream.Collectors; import com.kingsrook.qqq.api.actions.GenerateOpenApiSpecAction; import com.kingsrook.qqq.api.actions.QRecordApiAdapter; +import com.kingsrook.qqq.api.model.APILog; import com.kingsrook.qqq.api.model.APIVersion; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecInput; import com.kingsrook.qqq.api.model.actions.GenerateOpenApiSpecOutput; @@ -276,7 +278,21 @@ public class QJavalinApiHandler *******************************************************************************/ private static void doPathNotFound(Context context) { - handleException(context, new QNotFoundException("Could not find any resources at path " + context.path())); + APILog apiLog = newAPILog(context); + + try + { + setupSession(context, null, null); + } + catch(Exception e) + { + ////////////////////////////////////////////////////////////////////////// + // if we don't have a session, we won't be able to store the api log... // + ////////////////////////////////////////////////////////////////////////// + LOG.debug("No session in a 404; will not create api log", e); + } + + handleException(context, new QNotFoundException("Could not find any resources at path " + context.path()), apiLog); } @@ -548,6 +564,7 @@ public class QJavalinApiHandler String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); String primaryKey = context.pathParam("primaryKey"); + APILog apiLog = newAPILog(context); try { @@ -585,12 +602,64 @@ public class QJavalinApiHandler Map outputRecord = QRecordApiAdapter.qRecordToApiMap(record, tableName, version); QJavalinAccessLogger.logEndSuccess(); - context.result(JsonUtils.toJson(outputRecord)); + String resultString = JsonUtils.toJson(outputRecord); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } catch(Exception e) { QJavalinAccessLogger.logEndFail(e); - handleException(context, e); + handleException(context, e, apiLog); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static APILog newAPILog(Context context) + { + APILog apiLog = new APILog() + .withTimestamp(Instant.now()) + .withMethod(context.req().getMethod()) + .withPath(context.path()) + .withQueryString(context.queryString()) + .withRequestBody(context.body()); + + try + { + apiLog.setVersion(context.pathParam("version")); + } + catch(Exception e) + { + ////////////////////////////////////////////////////////////////////////////////// + // pathParam throws if the param isn't found - in that case, just leave it null // + ////////////////////////////////////////////////////////////////////////////////// + } + + return (apiLog); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void storeApiLog(APILog apiLog) + { + try + { + InsertInput insertInput = new InsertInput(); + insertInput.setTableName(APILog.TABLE_NAME); + // todo - security fields!!!!! + // todo - user!!!! + insertInput.setRecords(List.of(apiLog.toQRecord())); + new InsertAction().executeAsync(insertInput); + } + catch(Exception e) + { + LOG.warn("Error storing API log", e); } } @@ -604,6 +673,7 @@ public class QJavalinApiHandler String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); QQueryFilter filter = null; + APILog apiLog = newAPILog(context); try { @@ -822,12 +892,14 @@ public class QJavalinApiHandler output.put("records", records); QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); - context.result(JsonUtils.toJson(output)); + String resultString = JsonUtils.toJson(output); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } catch(Exception e) { QJavalinAccessLogger.logEndFail(e, logPair("filter", filter)); - handleException(context, e); + handleException(context, e, apiLog); } } @@ -1041,6 +1113,7 @@ public class QJavalinApiHandler { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); + APILog apiLog = newAPILog(context); try { @@ -1090,12 +1163,14 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.CREATED.getCode()); - context.result(JsonUtils.toJson(outputRecord)); + String resultString = JsonUtils.toJson(outputRecord); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } catch(Exception e) { QJavalinAccessLogger.logEndFail(e); - handleException(context, e); + handleException(context, e, apiLog); } } @@ -1108,6 +1183,7 @@ public class QJavalinApiHandler { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); + APILog apiLog = newAPILog(context); try { @@ -1196,12 +1272,14 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); - context.result(JsonUtils.toJson(response)); + String resultString = JsonUtils.toJson(response); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } catch(Exception e) { QJavalinAccessLogger.logEndFail(e); - handleException(context, e); + handleException(context, e, apiLog); } } @@ -1214,6 +1292,7 @@ public class QJavalinApiHandler { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); + APILog apiLog = newAPILog(context); try { @@ -1325,12 +1404,14 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); - context.result(JsonUtils.toJson(response)); + String resultString = JsonUtils.toJson(response); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } catch(Exception e) { QJavalinAccessLogger.logEndFail(e); - handleException(context, e); + handleException(context, e, apiLog); } } @@ -1353,6 +1434,7 @@ public class QJavalinApiHandler { String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); + APILog apiLog = newAPILog(context); try { @@ -1463,12 +1545,14 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.MULTI_STATUS.getCode()); - context.result(JsonUtils.toJson(response)); + String resultString = JsonUtils.toJson(response); + context.result(resultString); + storeApiLog(apiLog.withStatusCode(context.statusCode()).withResponseBody(resultString)); } catch(Exception e) { QJavalinAccessLogger.logEndFail(e); - handleException(context, e); + handleException(context, e, apiLog); } } @@ -1482,6 +1566,7 @@ public class QJavalinApiHandler String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); String primaryKey = context.pathParam("primaryKey"); + APILog apiLog = newAPILog(context); try { @@ -1546,11 +1631,12 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.NO_CONTENT.getCode()); + storeApiLog(apiLog.withStatusCode(context.statusCode())); } catch(Exception e) { QJavalinAccessLogger.logEndFail(e); - handleException(context, e); + handleException(context, e, apiLog); } } @@ -1564,6 +1650,7 @@ public class QJavalinApiHandler String version = context.pathParam("version"); String tableApiName = context.pathParam("tableName"); String primaryKey = context.pathParam("primaryKey"); + APILog apiLog = newAPILog(context); try { @@ -1599,22 +1686,33 @@ public class QJavalinApiHandler QJavalinAccessLogger.logEndSuccess(); context.status(HttpStatus.Code.NO_CONTENT.getCode()); + storeApiLog(apiLog.withStatusCode(context.statusCode())); } catch(Exception e) { QJavalinAccessLogger.logEndFail(e); - handleException(context, e); + handleException(context, e, apiLog); } } + /******************************************************************************* + ** + *******************************************************************************/ + public static void handleException(Context context, Exception e, APILog apiLog) + { + handleException(null, context, e, apiLog); + } + + + /******************************************************************************* ** *******************************************************************************/ public static void handleException(Context context, Exception e) { - handleException(null, context, e); + handleException(null, context, e, null); } @@ -1622,13 +1720,13 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - public static void handleException(HttpStatus.Code statusCode, Context context, Exception e) + public static void handleException(HttpStatus.Code statusCode, Context context, Exception e, APILog apiLog) { QBadRequestException badRequestException = ExceptionUtils.findClassInRootChain(e, QBadRequestException.class); if(badRequestException != null) { statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.BAD_REQUEST); // 400 - respondWithError(context, statusCode, badRequestException.getMessage()); + respondWithError(context, statusCode, badRequestException.getMessage(), apiLog); return; } @@ -1638,26 +1736,28 @@ public class QJavalinApiHandler if(userFacingException instanceof QNotFoundException) { statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.NOT_FOUND); // 404 - respondWithError(context, statusCode, userFacingException.getMessage()); + respondWithError(context, statusCode, userFacingException.getMessage(), apiLog); + return; } else { LOG.info("User-facing exception", e); statusCode = Objects.requireNonNullElse(statusCode, HttpStatus.Code.INTERNAL_SERVER_ERROR); // 500 - respondWithError(context, statusCode, userFacingException.getMessage()); + respondWithError(context, statusCode, userFacingException.getMessage(), apiLog); + return; } } else { if(e instanceof QAuthenticationException) { - respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage()); // 401 + respondWithError(context, HttpStatus.Code.UNAUTHORIZED, e.getMessage(), apiLog); // 401 return; } if(e instanceof QPermissionDeniedException) { - respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage()); // 403 + respondWithError(context, HttpStatus.Code.FORBIDDEN, e.getMessage(), apiLog); // 403 return; } @@ -1665,7 +1765,8 @@ public class QJavalinApiHandler // default exception handling // //////////////////////////////// LOG.warn("Exception in javalin request", e); - respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")"); // 500 + respondWithError(context, HttpStatus.Code.INTERNAL_SERVER_ERROR, e.getClass().getSimpleName() + " (" + e.getMessage() + ")", apiLog); // 500 + return; } } @@ -1674,7 +1775,7 @@ public class QJavalinApiHandler /******************************************************************************* ** *******************************************************************************/ - public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage) + public static void respondWithError(Context context, HttpStatus.Code statusCode, String errorMessage, APILog apiLog) { context.status(statusCode.getCode()); @@ -1695,7 +1796,17 @@ public class QJavalinApiHandler /////////////////////////// } - context.result(JsonUtils.toJson(Map.of("error", errorMessage))); + String responseBody = JsonUtils.toJson(Map.of("error", errorMessage)); + context.result(responseBody); + + if(apiLog != null) + { + if(QContext.getQSession() != null && QContext.getQInstance() != null) + { + apiLog.withStatusCode(statusCode.getCode()).withResponseBody(responseBody); + storeApiLog(apiLog); + } + } } } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java new file mode 100644 index 00000000..af5c6a4f --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/APILog.java @@ -0,0 +1,393 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.api.model; + + +import java.time.Instant; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.data.QField; +import com.kingsrook.qqq.backend.core.model.data.QRecord; +import com.kingsrook.qqq.backend.core.model.data.QRecordEntity; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class APILog extends QRecordEntity +{ + public static final String TABLE_NAME = "apiLog"; + + @QField(isEditable = false) + private Integer id; + + @QField(isEditable = false) + private Instant timestamp; + + @QField() + private String method; + + @QField() + private Integer statusCode; + + @QField() + private String version; + + @QField() + private String path; + + @QField() + private String queryString; + + @QField() + private String requestBody; + + @QField() + private String responseBody; + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public APILog() + { + } + + + + /******************************************************************************* + ** Constructor + ** + *******************************************************************************/ + public APILog(QRecord qRecord) throws QException + { + populateFromQRecord(qRecord); + } + + + + /******************************************************************************* + ** Getter for id + ** + *******************************************************************************/ + public Integer getId() + { + return id; + } + + + + /******************************************************************************* + ** Setter for id + ** + *******************************************************************************/ + public void setId(Integer id) + { + this.id = id; + } + + + + /******************************************************************************* + ** Fluent setter for id + ** + *******************************************************************************/ + public APILog withId(Integer id) + { + this.id = id; + return (this); + } + + + + /******************************************************************************* + ** Getter for timestamp + ** + *******************************************************************************/ + public Instant getTimestamp() + { + return timestamp; + } + + + + /******************************************************************************* + ** Setter for timestamp + ** + *******************************************************************************/ + public void setTimestamp(Instant timestamp) + { + this.timestamp = timestamp; + } + + + + /******************************************************************************* + ** Fluent setter for timestamp + ** + *******************************************************************************/ + public APILog withTimestamp(Instant timestamp) + { + this.timestamp = timestamp; + return (this); + } + + + + /******************************************************************************* + ** Getter for method + ** + *******************************************************************************/ + public String getMethod() + { + return method; + } + + + + /******************************************************************************* + ** Setter for method + ** + *******************************************************************************/ + public void setMethod(String method) + { + this.method = method; + } + + + + /******************************************************************************* + ** Fluent setter for method + ** + *******************************************************************************/ + public APILog withMethod(String method) + { + this.method = method; + return (this); + } + + + + /******************************************************************************* + ** Getter for statusCode + ** + *******************************************************************************/ + public Integer getStatusCode() + { + return statusCode; + } + + + + /******************************************************************************* + ** Setter for statusCode + ** + *******************************************************************************/ + public void setStatusCode(Integer statusCode) + { + this.statusCode = statusCode; + } + + + + /******************************************************************************* + ** Fluent setter for statusCode + ** + *******************************************************************************/ + public APILog withStatusCode(Integer statusCode) + { + this.statusCode = statusCode; + return (this); + } + + + + /******************************************************************************* + ** Getter for version + ** + *******************************************************************************/ + public String getVersion() + { + return version; + } + + + + /******************************************************************************* + ** Setter for version + ** + *******************************************************************************/ + public void setVersion(String version) + { + this.version = version; + } + + + + /******************************************************************************* + ** Fluent setter for version + ** + *******************************************************************************/ + public APILog withVersion(String version) + { + this.version = version; + return (this); + } + + + + /******************************************************************************* + ** Getter for path + ** + *******************************************************************************/ + public String getPath() + { + return path; + } + + + + /******************************************************************************* + ** Setter for path + ** + *******************************************************************************/ + public void setPath(String path) + { + this.path = path; + } + + + + /******************************************************************************* + ** Fluent setter for path + ** + *******************************************************************************/ + public APILog withPath(String path) + { + this.path = path; + return (this); + } + + + + /******************************************************************************* + ** Getter for queryString + ** + *******************************************************************************/ + public String getQueryString() + { + return queryString; + } + + + + /******************************************************************************* + ** Setter for queryString + ** + *******************************************************************************/ + public void setQueryString(String queryString) + { + this.queryString = queryString; + } + + + + /******************************************************************************* + ** Fluent setter for queryString + ** + *******************************************************************************/ + public APILog withQueryString(String queryString) + { + this.queryString = queryString; + return (this); + } + + + + /******************************************************************************* + ** Getter for requestBody + ** + *******************************************************************************/ + public String getRequestBody() + { + return requestBody; + } + + + + /******************************************************************************* + ** Setter for requestBody + ** + *******************************************************************************/ + public void setRequestBody(String requestBody) + { + this.requestBody = requestBody; + } + + + + /******************************************************************************* + ** Fluent setter for requestBody + ** + *******************************************************************************/ + public APILog withRequestBody(String requestBody) + { + this.requestBody = requestBody; + return (this); + } + + + + /******************************************************************************* + ** Getter for responseBody + ** + *******************************************************************************/ + public String getResponseBody() + { + return responseBody; + } + + + + /******************************************************************************* + ** Setter for responseBody + ** + *******************************************************************************/ + public void setResponseBody(String responseBody) + { + this.responseBody = responseBody; + } + + + + /******************************************************************************* + ** Fluent setter for responseBody + ** + *******************************************************************************/ + public APILog withResponseBody(String responseBody) + { + this.responseBody = responseBody; + return (this); + } + +} diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java new file mode 100644 index 00000000..ef04deaf --- /dev/null +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/metadata/APILogMetaDataProvider.java @@ -0,0 +1,119 @@ +/* + * QQQ - Low-code Application Framework for Engineers. + * Copyright (C) 2021-2023. 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.api.model.metadata; + + +import java.util.List; +import java.util.function.Consumer; +import com.kingsrook.qqq.api.model.APILog; +import com.kingsrook.qqq.backend.core.exceptions.QException; +import com.kingsrook.qqq.backend.core.model.metadata.QInstance; +import com.kingsrook.qqq.backend.core.model.metadata.fields.AdornmentType; +import com.kingsrook.qqq.backend.core.model.metadata.fields.FieldAdornment; +import com.kingsrook.qqq.backend.core.model.metadata.fields.ValueTooLongBehavior; +import com.kingsrook.qqq.backend.core.model.metadata.layout.QIcon; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Capability; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QFieldSection; +import com.kingsrook.qqq.backend.core.model.metadata.tables.QTableMetaData; +import com.kingsrook.qqq.backend.core.model.metadata.tables.Tier; + + +/******************************************************************************* + ** + *******************************************************************************/ +public class APILogMetaDataProvider +{ + + /******************************************************************************* + ** + *******************************************************************************/ + public static void defineAll(QInstance qInstance, String backendName, Consumer backendDetailEnricher) throws QException + { + defineAPILogTable(qInstance, backendName, backendDetailEnricher); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void defineAPILogTable(QInstance qInstance, String backendName, Consumer backendDetailEnricher) throws QException + { + QTableMetaData tableMetaData = new QTableMetaData() + .withName("apiLog") + .withLabel("API Log") + .withIcon(new QIcon().withName("data_object")) + .withBackendName(backendName) + .withRecordLabelFormat("%s") + .withPrimaryKeyField("id") + .withFieldsFromEntity(APILog.class) + .withSection(new QFieldSection("identity", new QIcon().withName("badge"), Tier.T1, List.of("id"))) + .withSection(new QFieldSection("request", new QIcon().withName("arrow_upward"), Tier.T2, List.of("method", "version", "path", "queryString", "requestBody"))) + .withSection(new QFieldSection("response", new QIcon().withName("arrow_downward"), Tier.T2, List.of("statusCode", "responseBody"))) + .withSection(new QFieldSection("dates", new QIcon().withName("calendar_month"), Tier.T3, List.of("timestamp"))) + .withoutCapabilities(Capability.TABLE_INSERT, Capability.TABLE_UPDATE, Capability.TABLE_DELETE); + + tableMetaData.getField("requestBody").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("json"))); + tableMetaData.getField("responseBody").withFieldAdornment(new FieldAdornment(AdornmentType.CODE_EDITOR).withValue(AdornmentType.CodeEditorValues.languageMode("json"))); + + tableMetaData.getField("method").withFieldAdornment(new FieldAdornment(AdornmentType.CHIP) + .withValue(AdornmentType.ChipValues.colorValue("GET", AdornmentType.ChipValues.COLOR_INFO)) + .withValue(AdornmentType.ChipValues.colorValue("POST", AdornmentType.ChipValues.COLOR_SUCCESS)) + .withValue(AdornmentType.ChipValues.colorValue("DELETE", AdornmentType.ChipValues.COLOR_ERROR)) + .withValue(AdornmentType.ChipValues.colorValue("PATCH", AdornmentType.ChipValues.COLOR_WARNING))); + + tableMetaData.getField("statusCode").withFieldAdornment(new FieldAdornment(AdornmentType.CHIP) + .withValue(AdornmentType.ChipValues.colorValue(200, AdornmentType.ChipValues.COLOR_SUCCESS)) + .withValue(AdornmentType.ChipValues.colorValue(201, AdornmentType.ChipValues.COLOR_SUCCESS)) + .withValue(AdornmentType.ChipValues.colorValue(204, AdornmentType.ChipValues.COLOR_SUCCESS)) + .withValue(AdornmentType.ChipValues.colorValue(207, AdornmentType.ChipValues.COLOR_INFO)) + .withValue(AdornmentType.ChipValues.colorValue(400, AdornmentType.ChipValues.COLOR_ERROR)) + .withValue(AdornmentType.ChipValues.colorValue(401, AdornmentType.ChipValues.COLOR_ERROR)) + .withValue(AdornmentType.ChipValues.colorValue(403, AdornmentType.ChipValues.COLOR_ERROR)) + .withValue(AdornmentType.ChipValues.colorValue(404, AdornmentType.ChipValues.COLOR_ERROR)) + .withValue(AdornmentType.ChipValues.colorValue(429, AdornmentType.ChipValues.COLOR_ERROR)) + .withValue(AdornmentType.ChipValues.colorValue(500, AdornmentType.ChipValues.COLOR_ERROR))); + + /////////////////////////////////////////// + // these are the lengths of a MySQL TEXT // + /////////////////////////////////////////// + tableMetaData.getField("requestBody").withMaxLength(65_535).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS); + tableMetaData.getField("responseBody").withMaxLength(65_535).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS); + + ////////////////////////////////////////////////////////////////////////////////////////////// + // internet doesn't agree on max-length for a URL, but let's go with ... 4K on query string // + ////////////////////////////////////////////////////////////////////////////////////////////// + tableMetaData.getField("queryString").withMaxLength(4096).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS); + + //////////////////////////////////////// + // and we expect short paths, 100 max // + //////////////////////////////////////// + tableMetaData.getField("path").withMaxLength(100).withBehavior(ValueTooLongBehavior.TRUNCATE_ELLIPSIS); + + if(backendDetailEnricher != null) + { + backendDetailEnricher.accept(tableMetaData); + } + + qInstance.addTable(tableMetaData); + } +}