ORM and GraphQL server for Java and PostgreSQL
Simplify working with relational data and creating network services. Loppi autogenerates code so you can have a GraphQL schema and database that you can use with type-safety.
Currently in private beta.
Simplify working with relational data and creating network services. Loppi autogenerates code so you can have a GraphQL schema and database that you can use with type-safety.
Currently in private beta.
// There is a table in the database called "product". This block of code will
// configure a GraphQL query field. ProductMeta is autogenerated.
GraphQLTablePluralQueryConfiguration<ProductResult> productQueryConfiguration =
new GraphQLTablePluralQueryConfiguration<ProductResult>()
.setIncludeField(true)
.setPluralFieldDescription("Returns the available products.")
// The fields that a customer will be allowed to order by in the queries.
.setIncludeOrderBy(Set.of(productMeta.stockCode,
productMeta.productId,
productMeta.currentUnitPrice,
productMeta.createdAt))
// The fields that a customer will be allowed to filter by in the queries.
.setIncludeWhere(Set.of(productMeta.productId,
productMeta.currentUnitPrice,
productMeta.description,
productMeta.stockCode,
productMeta.createdAt))
.setIncludePagination(true);
GraphQLTableQueriesConfiguration<ProductResult> queriesConfiguration =
new GraphQLTableQueriesConfiguration<>(
// The fields that a customer with be allowed to include in the queries.
Set.of(productMeta.productId,
productMeta.description,
productMeta.stockCode,
productMeta.currentUnitPrice,
productMeta.createdAt))
.setIncludePluralTableQuery(productQueryConfiguration);
return new GraphQLTableConfiguration<>(productMeta.getEntityMetadata())
.setIncludeQueries(queriesConfiguration);
# GraphQL SDL of the schema generated
"Returns the available products."
products(
"Limit the number of results."
limit: Int!,
"Order by argument for the result list. Cannot be null."
orderBy: [ProductOrderBy!],
"""This adds certain where-criteria. When next page is selected it works
such that only elements after lastSeenId are returned. When previous page
is selected it only returns elements before lastSeenId. When using
pagination, you must also specify orderBy with exactly one or two entries.
The last entry in the orderBy must be the productId field."""
pagination: ProductPagination,
"""Boolean expression for determining which results should be included.
Cannot be null."""
where: ProductCriteria
): [ProductResult!]!
type ProductResult {
createdAt: OffsetDateTime!
currentUnitPrice: BigDecimal!
description: String!
productId: Int!
stockCode: String!
}
Declare what tables, columns and relations you want to expose, how they are allowed to be ordered and filtered, and how they can be mutated. Fields that do not map directly to database tables can be declared as custom fields.
Adding fields for querying your database is easy, and so is adding custom fields for everything else.
Relationships to join, where or orderBy expression, all with type-safety.
Maintain and secure a handful of fields instead of hundreds of endpoints.
# Fetches the 5 latest purchase orders for the
# current user, and the first 8 orderlines for
# each purchase order.
query myOrders {
purchaseOrders(limit:5, orderBy: {createdAt:desc}){
purchaseOrderId
createdAt
orderStatus
# This fetches orderlines for the purchase
# order, but does not cause multiple fetches,
# round-trips or the N+1 problem.
orderLines(limit: 8 orderBy: { orderLineId: desc})
{
unitPrice
quantity
product {
productId
description
}
}
}
}
{
"data": {
"purchaseOrders": [
{
"purchaseOrderId": 581796,
"createdAt": "2023-08-21T10:38:19.952764+00:00",
"orderStatus": "NEW",
"orderLines": [{
"unitPrice": "0.12", "quantity": 1,
"product": { "productId": 134, "description": "DISCO BALL CHRISTMAS DECORATION" }
}]
},
{
"purchaseOrderId": 581795,
"createdAt": "2023-08-21T10:37:22.637079+00:00",
"orderStatus": "NEW",
"orderLines": [{
"unitPrice": "0.21", "quantity": 1,
"product": { "productId": 28, "description": "ANIMAL STICKERS" }
}]
}
]
}
}
With one schema there are many ways to query it. Fetch multiple root fields, their relations and their relations again. No need for several http requests to get the data for one page.
Don't call a large endpoint and discard most of the data, instead fetch exactly what you need and no more.
One root field, many root fields, relations, it doesn't matter; it's all fetched in one go.
From Loppi to the database, the query or mutation is executed in one round-trip and one transaction.
# Example of the mutations part of an e-commerce GraphQL schema
"Root mutation type."
type MutationRoot {
"""Login to the service. Requires solved captcha, with token sent via
cookie "hCaptchaToken". If login is successful the response will include
a set-cookie for "sessionToken". After four unsuccessful username/password
combinations, you need to solve captcha again."""
login(passwordBcrypt: String!, username: String!): LoginResult!
"""Place an order for purchase. Set customerId to associate order with
logged in account. Maximum order lines: 50."""
placeOrder(purchaseOrderToInsert: PurchaseOrderToInsert!): PurchaseOrderMutationResult!
"Signs out of the service."
signOut: IgnoredResult!
"""Signs up for the service. Requires solved captcha, with token sent
via cookie "hCaptchaToken"."""
signUp(name: String!, passwordBcrypt: String!, username: String!): SignUpResult!
"""Checks with a third party if the submitted token is a valid captcha
response. If successful sets cookie "hCaptchaToken" and returns the token.
Does no mutations"""
verifyCaptcha(token: String!): UUIDTokenResult!
}
// Here is a snippet from how the autogenerated GraphQL code could be handled.
CustomerMutationRootField mutationField =
checkOnlyOneMutation(mutationRequest);
rateLimiter.consumeRegularGraphQL(userInfo.remoteAddress(), 3);
MutationFieldAndCookies authorized = switch (mutationField) {
// These Field types are autogenerated.
case PlaceOrderMutationField placeOrderMutationField ->
orderDomain.handlePlaceOrder(userInfo, placeOrderMutationField);
case SignUpMutationField signUp ->
userDomain.handleSignUp(userInfo, signUp);
case SignOutMutationField signOut ->
loginSessionDomain.handleSignOut(userInfo, signOut);
case LoginMutationField login ->
loginSessionDomain.handleLogin(userInfo, login);
case UpdatePasswordMutationField updatePassword ->
userDomain.handleUpdatePassword(userInfo, updatePassword);
case VerifyCaptchaMutationField verifyCaptchaField ->
captchaVerifier.verifyHCaptchaTokenMutation(verifyCaptchaField);
};
GraphQLExecutionResult executionResult =
graphQLService.executeMutation(List.of(authorized.mutationField()));
public MutationFieldAndCookies handlePlaceOrder(
UserInfo userInfo, PlaceOrderMutationField placeOrderMutationField)
throws GraphQLRequestException {
PurchaseOrderInsertMutationInput input = placeOrderMutationField.input();
PurchaseOrderToInsert purchaseOrderToInsert = input.purchaseOrderToInsert();
// Validate address
String address = purchaseOrderToInsert.getAddress();
if(address.trim().isEmpty()){
throw new GraphQLValidationException("Address cannot be empty.");
}
// Validate name
String name = purchaseOrderToInsert.getName();
if(name.trim().isEmpty()){
throw new GraphQLValidationException("Name cannot be empty.");
}
// Additional code
public MutationFieldAndCookies verifyHCaptchaTokenMutation(
VerifyCaptchaMutationField verifyCaptchaField)
throws GraphQLRequestException {
VerifyCaptchaMutationInput input = verifyCaptchaField.input();
String responseToken = input.token();
if(responseToken.length() > 5_000){
throw new GraphQLRequestException("Invalid captcha token.",
GraphQLErrorTypeCategory.OTHER);
}
// Additional code.
Have the compiler and IDE hold your hand while you are building your application. Instead of spending time writing mapping code, you get all the typing you need automatically from the schema.
Use the types generated to look at what is going on and apply your business logic in plain java.
Loppi makes most invalid state impossible to represent.
Most objects are immutable and thread-safe, and method calls never accept or return null.
Subscribe to our newsletter by sending
subject subscribe to newsletter@loppi.io or click
here.
Unsubscribe by sending unsubscribe to the same address, or click on any of the unsubscribe links in the received mail.