org.restheart.graphql.models.builder.ObjectsMappings Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of restheart-graphql Show documentation
Show all versions of restheart-graphql Show documentation
RESTHeart MongoDB - GraphQL plugin
/*-
* ========================LICENSE_START=================================
* restheart-graphql
* %%
* Copyright (C) 2020 - 2024 SoftInstigate
* %%
* 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 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 .
* =========================LICENSE_END==================================
*/
package org.restheart.graphql.models.builder;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import org.bson.BsonBoolean;
import org.bson.BsonDocument;
import org.bson.BsonInt32;
import org.restheart.graphql.GraphQLIllegalAppDefinitionException;
import org.restheart.graphql.GraphQLService;
import org.restheart.graphql.models.AggregationMapping;
import org.restheart.graphql.models.DataLoaderSettings;
import org.restheart.graphql.models.FieldMapping;
import org.restheart.graphql.models.FieldRenaming;
import org.restheart.graphql.models.ObjectMapping;
import org.restheart.graphql.models.QueryMapping;
import org.restheart.graphql.models.TypeMapping;
import org.restheart.utils.LambdaUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import graphql.language.ObjectTypeDefinition;
import graphql.schema.idl.TypeDefinitionRegistry;
class ObjectsMappings extends Mappings {
private static final Logger LOGGER = LoggerFactory.getLogger(ObjectsMappings.class);
private static BsonInt32 defaultLimit = new BsonInt32(GraphQLService.DEFAULT_DEFAULT_LIMIT);
private static int maxLimit = GraphQLService.DEFAULT_MAX_LIMIT;
public static void setDefaultLimit(int _defaultLimit) {
defaultLimit = new BsonInt32(_defaultLimit);
}
public static void setMaxLimit(int _maxLimit) {
maxLimit = _maxLimit;
}
/**
*
* @param doc
* @param typeDefinitionRegistry
* @return the objects mappings
* @throws GraphQLIllegalAppDefinitionException
*/
static Map get(BsonDocument doc, TypeDefinitionRegistry typeDefinitionRegistry) throws GraphQLIllegalAppDefinitionException {
var ret = new HashMap();
var _wrongObjectMapping = doc.keySet().stream()
.filter(key -> isObject(key, typeDefinitionRegistry))
.filter(key -> !doc.get(key).isDocument())
.findFirst();
if (_wrongObjectMapping.isPresent()) {
var wrongObjectMapping = _wrongObjectMapping.get();
throw new GraphQLIllegalAppDefinitionException("Error with mappings of type: " + wrongObjectMapping + ". Type mappings must be of type Object but was " + doc.get(wrongObjectMapping).getBsonType());
}
doc.keySet().stream()
.filter(key -> isObject(key, typeDefinitionRegistry))
.filter(key -> doc.get(key).isDocument())
.forEach(type -> ret.put(type, new ObjectMapping(type, objectFieldMappings(type, typeDefinitionRegistry, doc.getDocument(type)))));
return ret;
}
/**
*
* @param type
* @param typeDoc
* @return the FieldMappings of the Object Type
*/
private static HashMap objectFieldMappings(String type, TypeDefinitionRegistry typeDefinitionRegistry, BsonDocument typeDoc) {
var typeMappings = new HashMap();
for (var field : typeDoc.keySet()) {
var fieldMapping = typeDoc.get(field);
switch (fieldMapping.getBsonType()) {
case STRING -> typeMappings.put(field, new FieldRenaming(field, fieldMapping.asString().getValue()));
case DOCUMENT -> {
var fieldMappingDoc = fieldMapping.asDocument();
// Check if document has "db" and "collection" keys and both are strings.
// These are common to both Aggregation and Query mapping.
if (fieldMappingDoc.containsKey("db")) {
if (!fieldMappingDoc.get("db").isString()) {
throwIllegalDefinitionException(field, type, "db", "string", fieldMappingDoc.get("db"));
}
} else {
throwIllegalDefinitionException(field, type, "db");
}
if (fieldMappingDoc.containsKey("collection")) {
if (!fieldMappingDoc.get("collection").isString()) {
throwIllegalDefinitionException(field, type, "db", "string", fieldMappingDoc.get("collection"));
LambdaUtils.throwsSneakyException(new GraphQLIllegalAppDefinitionException("The mapping for " + type + "." + field + " should be a string but is a " + fieldMappingDoc.get("collection")));
}
} else {
throwIllegalDefinitionException(field, type, "collection");
}
// if "stages" key is present -> Aggregation Mapping
// else it's Query Mapping
if (fieldMappingDoc.containsKey("stages")) {
if (fieldMappingDoc.get("stages").isArray()) {
var aggregationBuilder = new AggregationMapping.Builder();
aggregationBuilder
.fieldName(field)
.db(fieldMappingDoc.get("db").asString())
.collection(fieldMappingDoc.get("collection").asString())
.stages(fieldMappingDoc.get("stages").asArray())
.allowDiskUse(hasKeyOfType(fieldMappingDoc, "allowDiskUse", t -> t.isBoolean())
? fieldMappingDoc.get("allowDiskUse").asBoolean()
: BsonBoolean.FALSE);
// Check if dataloader settings are present
if (fieldMappingDoc.containsKey("dataLoader")) {
if (fieldMappingDoc.get("dataLoader").isDocument()) {
var settings = fieldMappingDoc.getDocument("dataLoader");
var dataLoaderBuilder = DataLoaderSettings.builder();
if (settings.containsKey("batching") && settings.get("batching").isBoolean()) {
dataLoaderBuilder.batching(settings.getBoolean("batching").getValue());
if (settings.containsKey("maxBatchSize") && settings.get("maxBatchSize").isNumber()) {
dataLoaderBuilder.max_batch_size(settings.getNumber("maxBatchSize").intValue());
}
}
if (settings.containsKey("caching") && settings.get("caching").isBoolean()) {
dataLoaderBuilder.caching(settings.getBoolean("caching").getValue());
}
aggregationBuilder.dataLoaderSettings(dataLoaderBuilder.build());
} else {
throwIllegalDefinitionException(field, type, "dataLoader", "Object", fieldMappingDoc.get("dataLoader"));
}
}
typeMappings.put(field, aggregationBuilder.build());
break;
} else {
throwIllegalDefinitionException(field, type, "db", "ARRAY", fieldMappingDoc.get("stages"));
}
} else {
var queryMappingBuilder = QueryMapping.newBuilder();
queryMappingBuilder.fieldName(field);
queryMappingBuilder.db(fieldMappingDoc.getString("db").getValue());
queryMappingBuilder.collection(fieldMappingDoc.getString("collection").getValue());
if (fieldMappingDoc.containsKey("find")) {
if (fieldMappingDoc.get("find").isDocument()) {
queryMappingBuilder.find(fieldMappingDoc.getDocument("find"));
} else {
throwIllegalDefinitionException(field, type, "find", "Object", fieldMappingDoc.get("find"));
}
}
if (fieldMappingDoc.containsKey("sort")) {
if (fieldMappingDoc.get("sort").isDocument()) {
queryMappingBuilder.sort(fieldMappingDoc.getDocument("sort"));
} else {
throwIllegalDefinitionException(field, type, "sort", "Object", fieldMappingDoc.get("sort"));
}
}
if (fieldMappingDoc.containsKey("limit")) {
if (fieldMappingDoc.get("limit").isDocument()) {
queryMappingBuilder.limit(fieldMappingDoc.getDocument("limit"));
} else if (fieldMappingDoc.get("limit").isInt32()) {
var ln = fieldMappingDoc.get("limit").asInt32();
if (ln.getValue() > maxLimit) {
LambdaUtils.throwsSneakyException(new GraphQLIllegalAppDefinitionException("Error with field 'limit' of type " + type + ", value cannot be grater than " + maxLimit));
} else {
queryMappingBuilder.limit(fieldMappingDoc.getNumber("limit"));
}
} else {
throwIllegalDefinitionException(field, type, "limit", "Object", fieldMappingDoc.get("limit"));
}
} else {
queryMappingBuilder.limit(defaultLimit);
}
if (fieldMappingDoc.containsKey("skip")) {
if (fieldMappingDoc.get("skip").isDocument()) {
queryMappingBuilder.skip(fieldMappingDoc.getDocument("skip"));
} else if (fieldMappingDoc.get("skip").isNumber()) {
queryMappingBuilder.skip(fieldMappingDoc.getNumber("skip"));
} else {
throwIllegalDefinitionException(field, type, "skip", "Object", fieldMappingDoc.get("skip"));
}
}
if (fieldMappingDoc.containsKey("dataLoader")) {
if (fieldMappingDoc.get("dataLoader").isDocument()) {
var settings = fieldMappingDoc.getDocument("dataLoader");
var dataLoaderBuilder = DataLoaderSettings.builder();
if (settings.containsKey("batching") && settings.get("batching").isBoolean()) {
dataLoaderBuilder.batching(settings.getBoolean("batching").getValue());
if (settings.containsKey("maxBatchSize") && settings.get("maxBatchSize").isNumber()) {
dataLoaderBuilder.max_batch_size(settings.getNumber("maxBatchSize").intValue());
}
}
if (settings.containsKey("caching") && settings.get("caching").isBoolean()) {
dataLoaderBuilder.caching(settings.getBoolean("caching").getValue());
}
queryMappingBuilder.DataLoaderSettings(dataLoaderBuilder.build());
} else {
throwIllegalDefinitionException(field, type, "dataLoader", "Object", fieldMappingDoc.get("dataLoader"));
}
}
typeMappings.put(field, queryMappingBuilder.build());
break;
}
break;
}
default -> LambdaUtils.throwsSneakyException(new GraphQLIllegalAppDefinitionException("The mapping for " + type + "." + field + " must be a String or a Document, but it is a " + fieldMapping.getBsonType()));
}
}
var defaultObjectFieldMappings = defaultObjectFieldMappings(type, typeDefinitionRegistry, typeDoc);
typeMappings.putAll(defaultObjectFieldMappings);
return typeMappings;
}
/**
* Provides a default mapping for fields that are not explicitly mapped.
*
* This is necessary to trigger the execution of GQLRenamingDataFetcher, even when no renaming is performed.
* The purpose of this method is to ensure that GQLDataFetcher.storeRootDoc() is executed, preventing the use of
* the default graphql.schema.PropertyDataFetcher, which would result in missing rootDoc for all executions
* involving only non-mapped fields at path level 2.
*/
public static HashMap defaultObjectFieldMappings(String type, TypeDefinitionRegistry typeDefinitionRegistry, BsonDocument typeDoc) {
var typeMappings = new HashMap();
typeDefinitionRegistry.types().entrySet().stream()
.filter(e -> !type.equals("Query") && type.equals(e.getKey()))
.filter(e -> e.getValue() instanceof ObjectTypeDefinition)
.map(e -> (ObjectTypeDefinition) e.getValue())
.map(e -> e.getFieldDefinitions()) // get fields
.flatMap(List::stream)
.map(fd -> fd.getName())
.filter(f -> !typeDoc.containsKey(f)) // field is not mapped
.peek(f -> LOGGER.trace("adding default field mapping for {}.{}", type, f))
.forEach(f -> typeMappings.put(f, new FieldRenaming(f, f))); // add default mapping
return typeMappings;
}
}