Skip to content
GQLoom

MikroORM

MikroORM is a TypeScript ORM for Node.js that supports PostgreSQL, MySQL, MariaDB, SQLite, MongoDB, and more. It is based on the Data Mapper, Unit of Work, and Identity Map patterns, aiming to provide a powerful and easy-to-use database toolset.

@gqloom/mikro-orm provides integration between GQLoom and MikroORM:

  • Use MikroORM Entities as Silks;
  • Use resolver factories to quickly generate CRUD operations from MikroORM.

Installation

Please refer to MikroORM's Quick Start guide to install MikroORM and the corresponding database driver.

After completing the MikroORM installation, install @gqloom/mikro-orm:

sh
npm i graphql @gqloom/core @gqloom/mikro-orm
sh
pnpm add graphql @gqloom/core @gqloom/mikro-orm
sh
yarn add graphql @gqloom/core @gqloom/mikro-orm
sh
bun add graphql @gqloom/core @gqloom/mikro-orm
sh
deno add npm:graphql npm:@gqloom/core npm:@gqloom/mikro-orm

Using Silks

Wrap MikroORM Entities with mikroSilk to use them as Silks. There are two ways to define entities:

When using defineEntity to define entities, wrap them with mikroSilk. Before using them in resolvers you need to initialize MikroORM; example below:

ts
import { 
mikroSilk
} from "@gqloom/mikro-orm"
import { type
InferEntity
,
defineEntity
} from "@mikro-orm/core"
const
UserEntity
=
defineEntity
({
name
: "User",
properties
: (
p
) => ({
id
:
p
.
integer
().
primary
().
autoincrement
(),
createdAt
:
p
.
datetime
().
onCreate
(() => new
Date
()),
email
:
p
.
string
(),
name
:
p
.
string
(),
role
:
p
.
string
().
$type
<"admin" | "user">().
default
("user"),
posts
: () =>
p
.
oneToMany
(
PostEntity
).
mappedBy
("author"),
}), }) export interface IUser extends
InferEntity
<typeof
UserEntity
> {}
const
PostEntity
=
defineEntity
({
name
: "Post",
properties
: (
p
) => ({
id
:
p
.
integer
().
primary
().
autoincrement
(),
createdAt
:
p
.
datetime
().
onCreate
(() => new
Date
()),
updatedAt
:
p
.
datetime
()
.
onCreate
(() => new
Date
())
.
onUpdate
(() => new
Date
()),
published
:
p
.
boolean
().
default
(false),
title
:
p
.
string
(),
author
: () =>
p
.
manyToOne
(
UserEntity
).
ref
(),
}), }) export interface IPost extends
InferEntity
<typeof
PostEntity
> {}
export const
User
=
mikroSilk
(
UserEntity
)
export const
Post
=
mikroSilk
(
PostEntity
)
ts
import { 
createMemoization
,
useResolvingFields
} from "@gqloom/core/context"
import {
MikroORM
} from "@mikro-orm/libsql"
import {
Post
,
User
} from "./entities"
export const
orm
=
MikroORM
.
initSync
({
entities
: [
User
,
Post
],
dbName
: ":memory:",
}) export const
useEm
=
createMemoization
(() =>
orm
.
em
.
fork
())
export const
useSelectedFields
= () => {
return
Array
.
from
(
useResolvingFields
()?.
selectedFields
?? ["*"]) as []
}
ts
import { 
weave
} from "@gqloom/core"
import {
MikroResolverFactory
} from "@gqloom/mikro-orm"
import {
Post
,
User
} from "./entities"
import {
useEm
} from "./provider"
const
userResolver
= new
MikroResolverFactory
(
User
,
useEm
).
resolver
()
const
postResolver
= new
MikroResolverFactory
(
Post
,
useEm
).
resolver
()
export const
schema
=
weave
(
userResolver
,
postResolver
)

Now we can use them in resolvers:

Manual Resolver

You can use MikroORM entities wrapped with mikroSilk directly in the resolver:

ts
import { 
field
,
mutation
,
query
,
resolver
} from "@gqloom/core"
import * as
v
from "valibot"
import {
Post
,
User
} from "./entities"
import {
flusher
,
useEm
,
useSelectedFields
} from "./provider"
export const
userResolver
=
resolver
.
of
(
User
, {
user
:
query
(
User
.
nullable
())
.
input
({
id
:
v
.
number
() })
.
resolve
(async ({
id
}) => {
const
user
= await
useEm
().
findOne
(
User
,
{
id
},
{
fields
:
useSelectedFields
() }
) return
user
}),
users
:
query
(
User
.
list
()).
resolve
(() => {
return
useEm
().
findAll
(
User
, {
fields
:
useSelectedFields
() })
}),
createUser
:
mutation
(
User
)
.
input
({
data
:
v
.
object
({
name
:
v
.
string
(),
email
:
v
.
string
(),
}), }) .
use
(
flusher
)
.
resolve
(async ({
data
}) => {
const
user
=
useEm
().
create
(
User
,
data
)
useEm
().
persist
(
user
)
return
user
}),
posts
:
field
(
Post
.
list
())
.
derivedFrom
("id")
.
resolve
((
user
) => {
return
useEm
().
find
(
Post
,
{
author
:
user
.
id
},
{
fields
:
useSelectedFields
() }
) }), })

As shown in the code above, we can use MikroORM entities wrapped with mikroSilk directly in the resolver. Here we use User as the parent type for resolver.of, and define two queries user and users, plus a createUser mutation.

Key points

  • Entity Manager: Use useEm() to get the request-scoped Entity Manager for database operations.
  • Auto persist: Use the flusher middleware to call em.flush() after a successful mutation.
  • Performance: Use useSelectedFields() so only the columns requested in the GraphQL query are selected; this function requires enabling context.

Derived Fields

Add derived fields to database entities:

ts
import { 
field
,
resolver
} from "@gqloom/core"
import * as
v
from "valibot"
import { type IUser,
User
} from "./entities"
export const
userResolver
=
resolver
.
of
(
User
, {
display
:
field
(
v
.
string
())
.
derivedFrom
("name", "email")
.
resolve
((
user
) => {
return `${
user
.
name
} <${
user
.
email
}>`
}), })

Note

Derived fields must use derivedFrom to declare the columns they depend on, so that useSelectedFields can select them correctly.

Hiding Fields

@gqloom/mikro-orm exposes all fields by default. To hide sensitive fields (e.g. password), use field.hidden:

ts
import { 
field
,
resolver
} from "@gqloom/core"
import {
User
} from "./entities"
export const
userResolver
=
resolver
.
of
(
User
, {
password
:
field
.
hidden
,
})

With password: field.hidden, that field will not appear in the generated GraphQL schema.

Mixing Fields

For fields such as json or enum, to get consistent type inference in both TypeScript and GraphQL you can use valibot or zod:

ts
import { 
mikroSilk
} from "@gqloom/mikro-orm"
import {
asEnumType
} from "@gqloom/valibot"
import {
defineEntity
, type
InferEntity
,
p
} from "@mikro-orm/core"
import * as
v
from "valibot"
const
Role
=
v
.
pipe
(
v
.
picklist
(["admin", "user"]),
asEnumType
({
name
: "Role",
valuesConfig
: {
admin
: {
description
: "Admin user" },
user
: {
description
: "Regular user" },
}, }) ) const
ContactInformation
=
v
.
object
({
email
:
v
.
nullish
(
v
.
string
()),
phone
:
v
.
nullish
(
v
.
string
()),
address
:
v
.
nullish
(
v
.
string
()),
}) const
UserEntity
=
defineEntity
({
name
: "User",
properties
: {
id
:
p
.
integer
().
primary
().
autoincrement
(),
createdAt
:
p
.
datetime
().
onCreate
(() => new
Date
()),
name
:
p
.
string
(),
role
:
p
.
enum
(
Role
.
options
).
onCreate
(() => "user"),
contactInformation
:
p
.
json
<
v
.
InferOutput
<typeof
ContactInformation
>>()
.
nullable
(),
}, }) export interface IUser extends
InferEntity
<typeof
UserEntity
> {}
export const
User
=
mikroSilk
(
UserEntity
, {
fields
: {
role
:
Role
,
contactInformation
:
v
.
nullish
(
ContactInformation
),
}, })

Resolver Factory

Besides manual resolvers, @gqloom/mikro-orm provides MikroResolverFactory. It greatly reduces boilerplate and quickly generates common queries, mutations, and relation fields from entity metadata.

ts
import { 
MikroResolverFactory
} from "@gqloom/mikro-orm"
import {
Post
,
User
} from "./entities"
import {
useEm
} from "./provider"
export const
userResolverFactory
= new
MikroResolverFactory
(
User
,
useEm
)
export const
postResolverFactory
= new
MikroResolverFactory
(
Post
,
useEm
)

The MikroResolverFactory constructor supports two forms:

  1. new MikroResolverFactory(Entity, getEntityManager): pass the entity and a function that returns an EntityManager.
  2. new MikroResolverFactory(Entity, options): pass the entity and an options object { getEntityManager, input? }.

Note

The input option configures each field’s visibility and validation in filter / create / update.

Relation Fields

The resolver factory provides referenceField and collectionField to define relation fields:

ts
import { 
field
,
query
,
resolver
} from "@gqloom/core"
import {
MikroResolverFactory
} from "@gqloom/mikro-orm"
import * as
v
from "valibot"
import {
Post
,
User
} from "./entities"
import {
useEm
} from "./provider"
export const
userResolverFactory
= new
MikroResolverFactory
(
User
,
useEm
)
export const
postResolverFactory
= new
MikroResolverFactory
(
Post
,
useEm
)
export const
userResolver
=
resolver
.
of
(
User
, {
user
:
userResolverFactory
.
findOneQuery
(),
posts
:
userResolverFactory
.
collectionField
('posts'),
}) export const
postResolver
=
resolver
.
of
(
Post
, {
author
:
postResolverFactory
.
referenceField
('author'),
})

In the code above we use userResolverFactory.collectionField('posts') and postResolverFactory.referenceField('author') to define relation fields. collectionField is for one-to-many and many-to-many relations; referenceField is for many-to-one and one-to-one relations.

Queries

The resolver factory provides preset query methods that call the corresponding EntityManager methods:

The where argument generates a Filter type. The dialect option in MikroWeaver.config controls whether PostgreSQL-only operators (e.g. ilike, overlap) are exposed, so you get a compatible API across databases.

You can use them directly:

ts
import { 
query
,
resolver
} from "@gqloom/core"
import {
MikroResolverFactory
} from "@gqloom/mikro-orm"
import * as
v
from "valibot"
import {
User
} from "./entities"
import {
useEm
} from "./provider"
export const
userResolverFactory
= new
MikroResolverFactory
(
User
,
useEm
)
export const
userResolver
=
resolver
.
of
(
User
, {
user
:
userResolverFactory
.
findOneQuery
(),
posts
:
userResolverFactory
.
collectionField
('posts'),
})

In the code above we use userResolverFactory.findOneQuery() to define the user query. The resolver factory will automatically create the input type and resolver function.

Mutations

The resolver factory provides preset mutation methods:

Built-in persist

The factory’s mutation methods already call em.flush(); you usually do not need to add a flusher middleware manually.

You can use them directly:

ts
import { 
resolver
} from "@gqloom/core"
import {
MikroResolverFactory
} from "@gqloom/mikro-orm"
import {
Post
} from "./entities"
import {
useEm
} from "./provider"
export const
postResolverFactory
= new
MikroResolverFactory
(
Post
,
useEm
)
export const
postResolver
=
resolver
.
of
(
Post
, {
createPost
:
postResolverFactory
.
createMutation
(),
author
:
postResolverFactory
.
referenceField
('author'),
})

In the code above we use postResolverFactory.createMutation() to define the createPost mutation. The factory will automatically create the input type and resolver function.

Custom Input Fields

Via the input option in the constructor, you can configure each field’s validation and visibility per operation:

ts
import { 
field
} from "@gqloom/core"
import {
MikroResolverFactory
} from "@gqloom/mikro-orm"
import * as
v
from "valibot"
const
userFactory
= new
MikroResolverFactory
(
User
, {
getEntityManager
:
useEm
,
input
: {
email
:
v
.
pipe
(
v
.
string
(),
v
.
email
()), // Validate email format
password
: {
filters
:
field
.
hidden
, // Hide this field in query filters
create
:
v
.
pipe
(
v
.
string
(),
v
.
minLength
(6)), // Validate min length 6 on create
update
:
v
.
pipe
(
v
.
string
(),
v
.
minLength
(6)), // Validate min length 6 on update
}, }, })

Custom Input Object

To specify the full input type (including transform) for a given query or mutation, use the .input() method:

ts
import { 
resolver
} from "@gqloom/core"
import {
MikroResolverFactory
} from "@gqloom/mikro-orm"
import * as
v
from "valibot"
import {
User
} from "./entities"
import {
useEm
} from "./provider"
export const
userResolverFactory
= new
MikroResolverFactory
(
User
,
useEm
)
export const
userResolver
=
resolver
.
of
(
User
, {
user
:
userResolverFactory
.
findOneQuery
().
input
(
v
.
pipe
(
v
.
object
({
id
:
v
.
number
() }),
v
.
transform
(({
id
}) => ({
where
: {
id
} }))
) ), })

The example above transforms the input into MikroORM query parameters.

Adding Middleware

Preset queries, mutations, and fields all support the use method for middleware such as auth or logging:

ts
import { 
resolver
} from "@gqloom/core"
import {
createMemoization
} from "@gqloom/core/context"
import {
MikroResolverFactory
} from "@gqloom/mikro-orm"
import {
GraphQLError
} from "graphql"
import {
Post
} from "./entities"
import {
useEm
} from "./provider"
const
postResolverFactory
= new
MikroResolverFactory
(
Post
,
useEm
)
const
useAuthedUser
=
createMemoization
(async () => ({
id
: 1,
name
: "test" }))
const
postResolver
=
resolver
.
of
(
Post
, {
createPost
:
postResolverFactory
.
createMutation
().
use
(async (
next
) => {
const
user
= await
useAuthedUser
()
if (
user
== null) throw new
GraphQLError
("Please login first")
return
next
()
}), })

In the code above we use the use method to add middleware. useAuthedUser() is a custom function to get the current user; if not logged in it throws, otherwise it calls next() to continue.

Complete Resolver

You can generate a resolver that includes all preset operations directly from the factory:

ts
import { 
MikroResolverFactory
} from "@gqloom/mikro-orm"
import {
User
} from "./entities"
import {
useEm
} from "./provider"
export const
userResolverFactory
= new
MikroResolverFactory
(
User
,
useEm
)
// Readonly Resolver const
userQueriesResolver
=
userResolverFactory
.
queriesResolver
()
// Full Resolver const
userResolver
=
userResolverFactory
.
resolver
()

MikroResolverFactory provides two methods to generate a Resolver:

  • queriesResolver(name?): Creates a resolver that only contains queries and relation fields.
  • resolver(name?): Adds mutation fields (e.g. createUser, updateUser) on top of queries and relation fields.

Note

The optional name argument controls the field name prefix. For example, passing "User" yields findOneUser, createUser, etc.

Weaver Config and Custom Type Mapping

Configure weaving behavior via MikroWeaver.config. Set it once in your app and pass it into weave:

  • presetGraphQLType(property): Override the default type mapping.
  • dialect: Set the database dialect (e.g. "PostgreSQL", "MySQL", "SQLite", "MongoDB") to narrow Filter operators.

Example: map datetime to GraphQLDateTime.

ts
import { 
MikroWeaver
} from "@gqloom/mikro-orm"
import {
GraphQLDateTime
} from "graphql-scalars"
export const
mikroWeaverConfig
=
MikroWeaver
.
config
({
presetGraphQLType
: (
property
) => {
if (
property
.
type
=== "datetime") {
return
GraphQLDateTime
} }, })

Pass this config when weaving the GraphQL schema:

ts
export const schema = weave(mikroWeaverConfig, userResolver, postResolver)

Default Type Mapping

GQLoom maps MikroORM property types to GraphQL types by default:

MikroORM TypeGraphQL Type
(primary)GraphQLID
stringGraphQLString
numberGraphQLFloat
floatGraphQLFloat
doubleGraphQLFloat
decimalGraphQLFloat
integerGraphQLInt
smallintGraphQLInt
mediumintGraphQLInt
tinyintGraphQLInt
bigintGraphQLInt
booleanGraphQLBoolean
(other)GraphQLString