diff --git a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java index 10f6d567..89507db3 100644 --- a/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java +++ b/qqq-backend-core/src/main/java/com/kingsrook/qqq/backend/core/model/metadata/fields/QFieldType.java @@ -98,4 +98,14 @@ public enum QFieldType { return this == QFieldType.STRING || this == QFieldType.TEXT || this == QFieldType.HTML || this == QFieldType.PASSWORD; } + + + + /******************************************************************************* + ** + *******************************************************************************/ + public boolean isNumeric() + { + return this == QFieldType.INTEGER || this == QFieldType.DECIMAL; + } } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java index 45b54fcf..f12c7962 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/actions/GenerateOpenApiSpecAction.java @@ -22,7 +22,11 @@ package com.kingsrook.qqq.api.actions; +import java.time.Instant; +import java.time.LocalDate; +import java.time.temporal.ChronoUnit; import java.util.ArrayList; +import java.util.Comparator; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -111,8 +115,8 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction()); @@ -125,17 +129,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction scopes = new LinkedHashMap<>(); - securitySchemes.put("OAuth2", new OAuth2() - .withFlows(MapBuilder.of("authorizationCode", new OAuth2Flow() - // todo - get from auth metadata - .withAuthorizationUrl("https://nutrifresh-one-development.us.auth0.com/authorize") - .withTokenUrl("https://nutrifresh-one-development.us.auth0.com/oauth/token") - .withScopes(scopes) - )) - ); securitySchemes.put("bearerAuth", new SecurityScheme() .withType("http") .withScheme("bearer") @@ -147,6 +143,13 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction scopes = new LinkedHashMap<>(); + securitySchemes.put("OAuth2", new OAuth2() + .withFlows(MapBuilder.of("clientCredentials", new OAuth2Flow() + .withTokenUrl("/api/oauth/token") + // .withScopes(scopes) + )) + ); componentSchemas.put("baseSearchResultFields", new Schema() .withType("object") .withProperties(MapBuilder.of( @@ -165,7 +168,9 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tables = new ArrayList<>(qInstance.getTables().values()); + tables.sort(Comparator.comparing(t -> t.getLabel())); + for(QTableMetaData table : tables) { String tableName = table.getName(); @@ -262,39 +267,25 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction tableFieldsWithoutPrimaryKey = new LinkedHashMap<>(); - Schema tableWithoutPrimaryKeySchema = new Schema() + ////////////////////////////////////// + // build the schemas for this table // + ////////////////////////////////////// + LinkedHashMap tableFields = new LinkedHashMap<>(); + Schema tableSchema = new Schema() .withType("object") - .withProperties(tableFieldsWithoutPrimaryKey); - componentSchemas.put(tableApiName + "WithoutPrimaryKey", tableWithoutPrimaryKeySchema); + .withProperties(tableFields); + componentSchemas.put(tableApiName, tableSchema); for(QFieldMetaData field : tableApiFields) { - if(primaryKeyName.equals(field.getName())) - { - continue; - } - Schema fieldSchema = getFieldSchema(table, field); - tableFieldsWithoutPrimaryKey.put(ApiFieldMetaData.getEffectiveApiFieldName(field), fieldSchema); + tableFields.put(ApiFieldMetaData.getEffectiveApiFieldName(field), fieldSchema); } ////////////////////////////////// // recursively add associations // ////////////////////////////////// - addAssociations(table, tableWithoutPrimaryKeySchema); - - ///////////////////////////////////////////////// - // full version of table (w/o pkey + the pkey) // - ///////////////////////////////////////////////// - componentSchemas.put(tableApiName, new Schema() - .withType("object") - .withAllOf(ListBuilder.of(new Schema().withRef("#/components/schemas/" + tableApiName + "WithoutPrimaryKey"))) - .withProperties(MapBuilder.of(primaryKeyApiName, getFieldSchema(table, table.getField(primaryKeyName))))); + addAssociations(table, tableSchema); ////////////////////////////////////////////////////////////////////////////// // table as a search result (the base search result, plus the table itself) // @@ -307,19 +298,37 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction - SQL-style, comma-separated list of field names, each optionally followed by ASC or DESC (defaults to ASC). - """) + .withDescription("How the results of the query should be sorted. SQL-style, comma-separated list of field names, each optionally followed by ASC or DESC (defaults to ASC).") .withIn("query") .withSchema(new Schema().withType("string")) .withExamples(buildOrderByExamples(primaryKeyApiName, tableApiFields)), @@ -351,14 +357,12 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction getComponentExamples() + { + Map rs = new LinkedHashMap<>(); + rs.put("criteriaNotQueried", new ExampleWithListValue().withSummary("no query on this field").withValue(ListBuilder.of(""))); + + rs.put("criteriaNumberEquals", new ExampleWithListValue().withSummary("equal to 47").withValue(ListBuilder.of("47"))); + rs.put("criteriaNumberNotEquals", new ExampleWithListValue().withSummary("not equal to 47").withValue(ListBuilder.of("!47"))); + rs.put("criteriaNumberLessThan", new ExampleWithListValue().withSummary("less than 47").withValue(ListBuilder.of("<47"))); + rs.put("criteriaNumberGreaterThan", new ExampleWithListValue().withSummary("greater than 47").withValue(ListBuilder.of(">47"))); + rs.put("criteriaNumberLessThanOrEquals", new ExampleWithListValue().withSummary("less than or equal to 47").withValue(ListBuilder.of("<=47"))); + rs.put("criteriaNumberGreaterThanOrEquals", new ExampleWithListValue().withSummary("greater than or equal to 47").withValue(ListBuilder.of(">=47"))); + rs.put("criteriaNumberEmpty", new ExampleWithListValue().withSummary("null value").withValue(ListBuilder.of("EMPTY"))); + rs.put("criteriaNumberNotEmpty", new ExampleWithListValue().withSummary("non-null value").withValue(ListBuilder.of("!EMPTY"))); + rs.put("criteriaNumberBetween", new ExampleWithListValue().withSummary("between 42 and 47").withValue(ListBuilder.of("BETWEEN 42,47"))); + rs.put("criteriaNumberNotBetween", new ExampleWithListValue().withSummary("not between 42 and 47").withValue(ListBuilder.of("!BETWEEN 42,47"))); + rs.put("criteriaNumberIn", new ExampleWithListValue().withSummary("any of 1701, 74205, or 74656").withValue(ListBuilder.of("IN 1701,74205,74656"))); + rs.put("criteriaNumberNotIn", new ExampleWithListValue().withSummary("not any of 1701, 74205, or 74656").withValue(ListBuilder.of("!IN 1701,74205,74656"))); + rs.put("criteriaNumberMultiple", new ExampleWithListValue().withSummary("multiple criteria: between 42 and 47 and not equal to 45").withValue(ListBuilder.of("BETWEEN 42,47", "!45"))); + + rs.put("criteriaBooleanEquals", new ExampleWithListValue().withSummary("equal to true").withValue(ListBuilder.of("true"))); + rs.put("criteriaBooleanNotEquals", new ExampleWithListValue().withSummary("not equal to true").withValue(ListBuilder.of("!true"))); + rs.put("criteriaBooleanEmpty", new ExampleWithListValue().withSummary("null value").withValue(ListBuilder.of("EMPTY"))); + rs.put("criteriaBooleanNotEmpty", new ExampleWithListValue().withSummary("non-null value").withValue(ListBuilder.of("!EMPTY"))); + + String now = Instant.now().truncatedTo(ChronoUnit.SECONDS).toString(); + String then = Instant.now().minus(90, ChronoUnit.DAYS).truncatedTo(ChronoUnit.SECONDS).toString(); + String when = Instant.now().plus(90, ChronoUnit.DAYS).truncatedTo(ChronoUnit.SECONDS).toString(); + rs.put("criteriaDateTimeEquals", new ExampleWithListValue().withSummary("equal to " + now).withValue(ListBuilder.of(now))); + rs.put("criteriaDateTimeNotEquals", new ExampleWithListValue().withSummary("not equal to " + now).withValue(ListBuilder.of("!" + now))); + rs.put("criteriaDateTimeLessThan", new ExampleWithListValue().withSummary("less than " + now).withValue(ListBuilder.of("<" + now))); + rs.put("criteriaDateTimeGreaterThan", new ExampleWithListValue().withSummary("greater than " + now).withValue(ListBuilder.of(">" + now))); + rs.put("criteriaDateTimeLessThanOrEquals", new ExampleWithListValue().withSummary("less than or equal to " + now).withValue(ListBuilder.of("<=" + now))); + rs.put("criteriaDateTimeGreaterThanOrEquals", new ExampleWithListValue().withSummary("greater than or equal to " + now).withValue(ListBuilder.of(">=" + now))); + rs.put("criteriaDateTimeEmpty", new ExampleWithListValue().withSummary("null value").withValue(ListBuilder.of("EMPTY"))); + rs.put("criteriaDateTimeNotEmpty", new ExampleWithListValue().withSummary("non-null value").withValue(ListBuilder.of("!EMPTY"))); + rs.put("criteriaDateTimeBetween", new ExampleWithListValue().withSummary("between " + then + " and " + now).withValue(ListBuilder.of("BETWEEN " + then + "," + now))); + rs.put("criteriaDateTimeNotBetween", new ExampleWithListValue().withSummary("not between " + then + " and " + now).withValue(ListBuilder.of("!BETWEEN " + then + "," + now))); + rs.put("criteriaDateTimeIn", new ExampleWithListValue().withSummary("any of " + then + ", " + now + ", or " + when).withValue(ListBuilder.of("IN " + then + "," + now + "," + when))); + rs.put("criteriaDateTimeNotIn", new ExampleWithListValue().withSummary("not any of " + then + ", " + now + ", or " + when).withValue(ListBuilder.of("!IN " + then + "," + now + "," + when))); + rs.put("criteriaDateTimeMultiple", new ExampleWithListValue().withSummary("multiple criteria: between " + then + " and " + when + " and not equal to " + now).withValue(ListBuilder.of("BETWEEN " + then + "," + when, "!" + now))); + + now = LocalDate.now().toString(); + then = LocalDate.now().minus(90, ChronoUnit.DAYS).toString(); + when = LocalDate.now().plus(90, ChronoUnit.DAYS).toString(); + rs.put("criteriaDateEquals", new ExampleWithListValue().withSummary("equal to " + now).withValue(ListBuilder.of(now))); + rs.put("criteriaDateNotEquals", new ExampleWithListValue().withSummary("not equal to " + now).withValue(ListBuilder.of("!" + now))); + rs.put("criteriaDateLessThan", new ExampleWithListValue().withSummary("less than " + now).withValue(ListBuilder.of("<" + now))); + rs.put("criteriaDateGreaterThan", new ExampleWithListValue().withSummary("greater than " + now).withValue(ListBuilder.of(">" + now))); + rs.put("criteriaDateLessThanOrEquals", new ExampleWithListValue().withSummary("less than or equal to " + now).withValue(ListBuilder.of("<=" + now))); + rs.put("criteriaDateGreaterThanOrEquals", new ExampleWithListValue().withSummary("greater than or equal to " + now).withValue(ListBuilder.of(">=" + now))); + rs.put("criteriaDateEmpty", new ExampleWithListValue().withSummary("null value").withValue(ListBuilder.of("EMPTY"))); + rs.put("criteriaDateNotEmpty", new ExampleWithListValue().withSummary("non-null value").withValue(ListBuilder.of("!EMPTY"))); + rs.put("criteriaDateBetween", new ExampleWithListValue().withSummary("between " + then + " and " + now).withValue(ListBuilder.of("BETWEEN " + then + "," + now))); + rs.put("criteriaDateNotBetween", new ExampleWithListValue().withSummary("not between " + then + " and " + now).withValue(ListBuilder.of("!BETWEEN " + then + "," + now))); + rs.put("criteriaDateIn", new ExampleWithListValue().withSummary("any of " + then + ", " + now + ", or " + when).withValue(ListBuilder.of("IN " + then + "," + now + "," + when))); + rs.put("criteriaDateNotIn", new ExampleWithListValue().withSummary("not any of " + then + ", " + now + ", or " + when).withValue(ListBuilder.of("!IN " + then + "," + now + "," + when))); + rs.put("criteriaDateMultiple", new ExampleWithListValue().withSummary("multiple criteria: between " + then + " and " + when + " and not equal to " + now).withValue(ListBuilder.of("BETWEEN " + then + "," + when, "!" + now))); + + rs.put("criteriaStringEquals", new ExampleWithListValue().withSummary("equal to foo").withValue(ListBuilder.of("foo"))); + rs.put("criteriaStringNotEquals", new ExampleWithListValue().withSummary("not equal to foo").withValue(ListBuilder.of("!foo"))); + rs.put("criteriaStringLessThan", new ExampleWithListValue().withSummary("less than foo").withValue(ListBuilder.of("foo"))); + rs.put("criteriaStringLessThanOrEquals", new ExampleWithListValue().withSummary("less than or equal to foo").withValue(ListBuilder.of("<=foo"))); + rs.put("criteriaStringGreaterThanOrEquals", new ExampleWithListValue().withSummary("greater than or equal to foo").withValue(ListBuilder.of(">=foo"))); + rs.put("criteriaStringEmpty", new ExampleWithListValue().withSummary("null value").withValue(ListBuilder.of("EMPTY"))); + rs.put("criteriaStringNotEmpty", new ExampleWithListValue().withSummary("non-null value").withValue(ListBuilder.of("!EMPTY"))); + rs.put("criteriaStringBetween", new ExampleWithListValue().withSummary("between bar and foo").withValue(ListBuilder.of("BETWEEN bar,foo"))); + rs.put("criteriaStringNotBetween", new ExampleWithListValue().withSummary("not between bar and foo").withValue(ListBuilder.of("!BETWEEN bar,foo"))); + rs.put("criteriaStringIn", new ExampleWithListValue().withSummary("any of foo, bar, or baz").withValue(ListBuilder.of("IN foo,bar,baz"))); + rs.put("criteriaStringNotIn", new ExampleWithListValue().withSummary("not any of foo, bar, or baz").withValue(ListBuilder.of("!IN foo,bar,baz"))); + rs.put("criteriaStringLike", new ExampleWithListValue().withSummary("starting with f").withValue(ListBuilder.of("LIKE f%"))); + rs.put("criteriaStringNotLike", new ExampleWithListValue().withSummary("not starting with f").withValue(ListBuilder.of("!LIKE f%"))); + rs.put("criteriaStringMultiple", new ExampleWithListValue().withSummary("multiple criteria: between bar and foo and not equal to baz").withValue(ListBuilder.of("BETWEEN bar,foo", "!baz"))); + + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static Map getCriteriaExamples(Map componentExamples, QFieldMetaData tableApiField) + { + List exampleRefs = new ArrayList<>(); + exampleRefs.add("criteriaNotQueried"); + + if(tableApiField.getType().isStringLike()) + { + componentExamples.keySet().stream().filter(s -> s.startsWith("criteriaString")).forEach(exampleRefs::add); + } + else if(tableApiField.getType().isNumeric()) + { + componentExamples.keySet().stream().filter(s -> s.startsWith("criteriaNumber")).forEach(exampleRefs::add); + } + else if(tableApiField.getType().equals(QFieldType.DATE_TIME)) + { + componentExamples.keySet().stream().filter(s -> s.startsWith("criteriaDateTime")).forEach(exampleRefs::add); + } + else if(tableApiField.getType().equals(QFieldType.DATE)) + { + componentExamples.keySet().stream().filter(s -> s.startsWith("criteriaDate") && !s.startsWith("criteriaDateTime")).forEach(exampleRefs::add); + } + else if(tableApiField.getType().equals(QFieldType.BOOLEAN)) + { + componentExamples.keySet().stream().filter(s -> s.startsWith("criteriaBoolean")).forEach(exampleRefs::add); + } + + Map rs = new LinkedHashMap<>(); + + for(String exampleRef : exampleRefs) + { + rs.put(exampleRef, new Example().withRef("#components/examples/" + exampleRef)); + } + + return (rs); + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void addAssociations(QTableMetaData table, Schema tableSchema) { for(Association association : CollectionUtils.nonNullList(table.getAssociations())) { @@ -577,7 +740,7 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction enumValues = new ArrayList<>(); + List enumValues = new ArrayList<>(); + List enumMapping = new ArrayList<>(); for(QPossibleValue enumValue : possibleValueSource.getEnumValues()) { - enumValues.add(enumValue.getId() + "=" + enumValue.getLabel()); + enumValues.add(String.valueOf(enumValue.getId())); + enumMapping.add(enumValue.getId() + "=" + enumValue.getLabel()); } fieldSchema.setEnumValues(enumValues); + fieldSchema.setDescription(fieldSchema.getDescription() + " Value definitions are: " + StringUtils.joinWithCommasAndAnd(enumMapping)); } else if(QPossibleValueSourceType.TABLE.equals(possibleValueSource.getType())) { @@ -672,7 +853,8 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction properties = new LinkedHashMap<>(); - properties.put("status", new Schema().withType("integer")); + properties.put("statusCode", new Schema().withType("integer")); + properties.put("statusText", new Schema().withType("string")); properties.put("error", new Schema().withType("string")); if(method.equalsIgnoreCase("post")) { @@ -726,17 +908,21 @@ public class GenerateOpenApiSpecAction extends AbstractQActionFunction { @@ -167,6 +171,7 @@ public class QJavalinApiHandler }); ApiBuilder.get("/api/versions.json", QJavalinApiHandler::doVersions); + ApiBuilder.get("/api/qqq-api-styles.css", QJavalinApiHandler::doStyles); ApiBuilder.before("/*", QJavalinApiHandler::setupCORS); @@ -177,6 +182,18 @@ public class QJavalinApiHandler ApiBuilder.delete("/api/*", QJavalinApiHandler::doPathNotFound); ApiBuilder.patch("/api/*", QJavalinApiHandler::doPathNotFound); ApiBuilder.post("/api/*", QJavalinApiHandler::doPathNotFound); + + /////////////////////////////////////////////////////////////////////////////////// + // if the main implementation class has a hot-swapper installed, use it here too // + /////////////////////////////////////////////////////////////////////////////////// + if(QJavalinImplementation.getQInstanceHotSwapSupplier() != null) + { + ApiBuilder.before((context) -> + { + QJavalinImplementation.hotSwapQInstance(context); + QJavalinApiHandler.qInstance = QJavalinImplementation.getQInstance(); + }); + } }); } @@ -239,8 +256,24 @@ public class QJavalinApiHandler try { QContext.init(qInstance, null); - String version = context.pathParam("version"); - GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(new GenerateOpenApiSpecInput().withVersion(version)); + String version = context.pathParam("version"); + + GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version); + try + { + if(StringUtils.hasContent(context.pathParam("tableName"))) + { + input.setTableName(context.pathParam("tableName")); + } + } + catch(Exception e) + { + /////////////////////////// + // leave table param out // + /////////////////////////// + } + + GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(input); context.contentType(ContentType.APPLICATION_YAML); context.result(output.getYaml()); } @@ -259,22 +292,45 @@ public class QJavalinApiHandler { try { - ////////////////////////////// - // validate required inputs // - ////////////////////////////// - String clientId = context.formParam("client_id"); - if(clientId == null) + //////////////////////////////////////////////////////////////////////////////////////////////////////// + // clientId & clientSecret may either be provided as formParams, or in an Authorization: Basic header // + //////////////////////////////////////////////////////////////////////////////////////////////////////// + String clientId; + String clientSecret; + String authorizationHeader = context.header("Authorization"); + if(authorizationHeader != null && authorizationHeader.startsWith("Basic ")) { - context.status(HttpStatus.BAD_REQUEST_400); - context.result("'client_id' must be provided."); - return; + try + { + byte[] credDecoded = Base64.getDecoder().decode(authorizationHeader.replace("Basic ", "")); + String credentials = new String(credDecoded, StandardCharsets.UTF_8); + String[] parts = credentials.split(":", 2); + clientId = parts[0]; + clientSecret = parts[1]; + } + catch(Exception e) + { + context.status(HttpStatus.BAD_REQUEST_400); + context.result("Could not parse client_id and client_secret from Basic Authorization header."); + return; + } } - String clientSecret = context.formParam("client_secret"); - if(clientSecret == null) + else { - context.status(HttpStatus.BAD_REQUEST_400); - context.result("'client_secret' must be provided."); - return; + clientId = context.formParam("client_id"); + if(clientId == null) + { + context.status(HttpStatus.BAD_REQUEST_400); + context.result("'client_id' must be provided."); + return; + } + clientSecret = context.formParam("client_secret"); + if(clientSecret == null) + { + context.status(HttpStatus.BAD_REQUEST_400); + context.result("'client_secret' must be provided."); + return; + } } //////////////////////////////////////////////////////// @@ -286,12 +342,12 @@ public class QJavalinApiHandler try { - ///////////////////////////////////////////////////////////////////////////////////////// - // make call to get access token data, if no exception thrown, assume 200OK and return // - ///////////////////////////////////////////////////////////////////////////////////////// + ////////////////////////////////////////////////////////////////////////////////////////// + // make call to get access token data, if no exception thrown, assume 200 OK and return // + ////////////////////////////////////////////////////////////////////////////////////////// QContext.init(qInstance, null); // hmm... String accessToken = authenticationModule.createAccessToken(metaData, clientId, clientSecret); - context.status(io.javalin.http.HttpStatus.OK); + context.status(HttpStatus.Code.OK.getCode()); context.result(accessToken); QJavalinAccessLogger.logEndSuccess(); return; @@ -335,9 +391,18 @@ public class QJavalinApiHandler String version = context.pathParam("version"); GenerateOpenApiSpecInput input = new GenerateOpenApiSpecInput().withVersion(version); - if(StringUtils.hasContent(context.pathParam("tableName"))) + try { - input.setTableName(context.pathParam("tableName")); + if(StringUtils.hasContent(context.pathParam("tableName"))) + { + input.setTableName(context.pathParam("tableName")); + } + } + catch(Exception e) + { + /////////////////////////// + // leave table param out // + /////////////////////////// } GenerateOpenApiSpecOutput output = new GenerateOpenApiSpecAction().execute(input); @@ -352,6 +417,139 @@ public class QJavalinApiHandler + /******************************************************************************* + ** + *******************************************************************************/ + private static void doSpecHtml(Context context) + { + try + { + QContext.init(qInstance, null); + + QBrandingMetaData branding = QContext.getQInstance().getBranding(); + String html = """ + + + + + + + + + + {navLogoImg} + + + + + """ + .replace("{version}", context.pathParam("version")) + .replace("{primaryColor}", branding == null ? "#FF791A" : branding.getAccentColor()); + + if(branding != null && StringUtils.hasContent(branding.getLogo())) + { + html = html.replace("{navLogoImg}", ""); + } + + context.contentType(ContentType.HTML); + context.result(html); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + + /******************************************************************************* + ** + *******************************************************************************/ + private static void doStyles(Context context) + { + try + { + QContext.init(qInstance, null); + + String css = """ + #api-info + { + margin-left: 0px !important; + } + + #api-info button + { + width: auto !important; + } + + #api-title span + { + font-size: 24px !important; + margin-left: 8px; + } + + .nav-scroll + { + padding-left: 16px; + } + + .tag-description.expanded + { + max-height: initial !important; + } + + .tag-description .m-markdown p + { + margin-block-end: 0.5em !important; + } + + api-response + { + margin-bottom: 50vh; + display: inline-block; + } + """; + + context.contentType(ContentType.CSS); + context.result(css); + } + catch(Exception e) + { + handleException(context, e); + } + } + + + /******************************************************************************* ** *******************************************************************************/ @@ -548,6 +746,10 @@ public class QJavalinApiHandler } } } + else + { + filter.withOrderBy(new QFilterOrderBy(table.getPrimaryKeyField(), false)); + } Set nonFilterParams = Set.of("pageSize", "pageNo", "orderBy", "booleanOperator", "includeCount"); @@ -610,19 +812,9 @@ public class QJavalinApiHandler queryInput.setFilter(filter); QueryOutput queryOutput = queryAction.execute(queryInput); - Map output = new HashMap<>(); - output.put("pageSize", pageSize); + Map output = new LinkedHashMap<>(); output.put("pageNo", pageNo); - - /////////////////////////////// - // map record fields for api // - /////////////////////////////// - ArrayList> records = new ArrayList<>(); - for(QRecord record : queryOutput.getRecords()) - { - records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, version)); - } - output.put("records", records); + output.put("pageSize", pageSize); ///////////////////////////// // optionally do the count // @@ -636,6 +828,16 @@ public class QJavalinApiHandler output.put("count", countOutput.getCount()); } + /////////////////////////////// + // map record fields for api // + /////////////////////////////// + ArrayList> records = new ArrayList<>(); + for(QRecord record : queryOutput.getRecords()) + { + records.add(QRecordApiAdapter.qRecordToApiMap(record, tableName, version)); + } + output.put("records", records); + QJavalinAccessLogger.logEndSuccess(logPair("recordCount", queryOutput.getRecords().size()), QJavalinAccessLogger.logPairIfSlow("filter", filter, SLOW_LOG_THRESHOLD_MS)); context.result(JsonUtils.toJson(output)); } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Example.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Example.java index 3af82bd0..ea3162de 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Example.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Example.java @@ -22,12 +22,16 @@ package com.kingsrook.qqq.api.model.openapi; +import com.fasterxml.jackson.annotation.JsonGetter; + + /******************************************************************************* ** *******************************************************************************/ -public abstract class Example +public class Example { private String summary; + private String ref; @@ -60,4 +64,36 @@ public abstract class Example return (this); } + + + /******************************************************************************* + ** Getter for ref + *******************************************************************************/ + @JsonGetter("$ref") + public String getRef() + { + return (this.ref); + } + + + + /******************************************************************************* + ** Setter for ref + *******************************************************************************/ + public void setRef(String ref) + { + this.ref = ref; + } + + + + /******************************************************************************* + ** Fluent setter for ref + *******************************************************************************/ + public Example withRef(String ref) + { + this.ref = ref; + return (this); + } + } diff --git a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java index eb55dc2e..228bae49 100644 --- a/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java +++ b/qqq-middleware-api/src/main/java/com/kingsrook/qqq/api/model/openapi/Schema.java @@ -41,6 +41,9 @@ public class Schema private Object example; private String ref; private List allOf; + private Boolean readOnly; + private Boolean nullable; + private Integer maxLength; @@ -344,4 +347,97 @@ public class Schema return (this); } + + + /******************************************************************************* + ** Getter for readOnly + *******************************************************************************/ + public Boolean getReadOnly() + { + return (this.readOnly); + } + + + + /******************************************************************************* + ** Setter for readOnly + *******************************************************************************/ + public void setReadOnly(Boolean readOnly) + { + this.readOnly = readOnly; + } + + + + /******************************************************************************* + ** Fluent setter for readOnly + *******************************************************************************/ + public Schema withReadOnly(Boolean readOnly) + { + this.readOnly = readOnly; + return (this); + } + + + + /******************************************************************************* + ** Getter for nullable + *******************************************************************************/ + public Boolean getNullable() + { + return (this.nullable); + } + + + + /******************************************************************************* + ** Setter for nullable + *******************************************************************************/ + public void setNullable(Boolean nullable) + { + this.nullable = nullable; + } + + + + /******************************************************************************* + ** Fluent setter for nullable + *******************************************************************************/ + public Schema withNullable(Boolean nullable) + { + this.nullable = nullable; + return (this); + } + + + + /******************************************************************************* + ** Getter for maxLength + *******************************************************************************/ + public Integer getMaxLength() + { + return (this.maxLength); + } + + + + /******************************************************************************* + ** Setter for maxLength + *******************************************************************************/ + public void setMaxLength(Integer maxLength) + { + this.maxLength = maxLength; + } + + + + /******************************************************************************* + ** Fluent setter for maxLength + *******************************************************************************/ + public Schema withMaxLength(Integer maxLength) + { + this.maxLength = maxLength; + return (this); + } + } diff --git a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java index 43b12f80..62c88550 100644 --- a/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java +++ b/qqq-middleware-javalin/src/main/java/com/kingsrook/qqq/backend/javalin/QJavalinImplementation.java @@ -187,7 +187,7 @@ public class QJavalinImplementation QJavalinImplementation.qInstance = qInstance; QJavalinImplementation.javalinMetaData = javalinMetaData; new QInstanceValidator().validate(qInstance); - this.startTime = System.currentTimeMillis(); + startTime = System.currentTimeMillis(); } @@ -1465,4 +1465,25 @@ public class QJavalinImplementation { QJavalinImplementation.javalinMetaData = javalinMetaData; } + + + + /******************************************************************************* + ** Getter for qInstanceHotSwapSupplier + *******************************************************************************/ + public static Supplier getQInstanceHotSwapSupplier() + { + return (QJavalinImplementation.qInstanceHotSwapSupplier); + } + + + + /******************************************************************************* + ** Getter for qInstanceHotSwapSupplier + *******************************************************************************/ + public static QInstance getQInstance() + { + return (QJavalinImplementation.qInstance); + } + }