Skip to content

Java · PostgreSQL · GraphQL

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 — no credit card required.

// 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!
}

Programmatically declare your GraphQL schema

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.

Only expose what is needed

Adding fields for querying your database is easy, and so is adding custom fields for everything else.

Add common query options with a single line

Relationships to join, where or orderBy expression, all with type-safety.

Everything you need in one endpoint

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" }
        }]
      }
    ]
  }
}

Fetch the data you need in one request

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.

No overfetching

Don't call a large endpoint and discard most of the data, instead fetch exactly what you need and no more.

No N+1 queries

One root field, many root fields, relations, it doesn't matter; it's all fetched in one go.

One round-trip one transaction

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.

Generated code ensures type-safety

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.

Plain java business logic

Use the types generated to look at what is going on and apply your business logic in plain java.

Minimize runtime errors

Loppi makes most invalid state impossible to represent.

Developer-friendly API

Most objects are immutable and thread-safe, and method calls never accept or return null.

Newsletter

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.