Skip to content

Commit 4ccb963

Browse files
jtomaszewskiclaude
andcommitted
feat(query-mikroorm): add MikroORM adapter package
Add `@ptc-org/nestjs-query-mikroorm` package that provides MikroORM integration: - MikroOrmQueryService implementing full QueryService interface - NestjsQueryMikroOrmModule.forFeature() for easy module registration - FilterQueryBuilder for converting nestjs-query filters to MikroORM queries - Support for all filter operations (eq, neq, gt, gte, lt, lte, in, notIn, like, iLike, is, isNot, and, or) - Support for sorting with nulls first/last - Support for pagination - Relation queries (queryRelations, countRelations, findRelation) - Relation mutations (addRelations, setRelations, setRelation, removeRelations, removeRelation) - Soft delete support - Comprehensive test suite with 62 tests 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <[email protected]>
1 parent 31e4131 commit 4ccb963

24 files changed

+18976
-25792
lines changed

package.json

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,9 @@
4646
"@docusaurus/module-type-aliases": "3.9.1",
4747
"@docusaurus/preset-classic": "3.9.1",
4848
"@jscutlery/semver": "5.7.1",
49+
"@mikro-orm/core": "^6.6.2",
50+
"@mikro-orm/nestjs": "^6.1.1",
51+
"@mikro-orm/sqlite": "^6.6.2",
4952
"@nestjs/apollo": "^13.2.1",
5053
"@nestjs/cli": "11.0.10",
5154
"@nestjs/schematics": "11.0.8",
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import { MikroORM, Options, EntityManager } from '@mikro-orm/core'
2+
import { SqliteDriver } from '@mikro-orm/sqlite'
3+
4+
import { TestEntity } from './test.entity'
5+
import { TestRelation } from './test-relation.entity'
6+
import { TestSoftDeleteEntity } from './test-soft-delete.entity'
7+
import { seed } from './seeds'
8+
9+
export const CONNECTION_OPTIONS: Options<SqliteDriver> = {
10+
driver: SqliteDriver,
11+
dbName: ':memory:',
12+
entities: [TestEntity, TestRelation, TestSoftDeleteEntity],
13+
allowGlobalContext: true
14+
}
15+
16+
export async function createTestConnection(): Promise<MikroORM<SqliteDriver>> {
17+
const orm = await MikroORM.init(CONNECTION_OPTIONS)
18+
const generator = orm.getSchemaGenerator()
19+
await generator.createSchema()
20+
return orm
21+
}
22+
23+
export async function truncate(em: EntityManager): Promise<void> {
24+
const connection = em.getConnection()
25+
await connection.execute('DELETE FROM test_relation')
26+
await connection.execute('DELETE FROM test_entity_many_test_relations')
27+
await connection.execute('DELETE FROM test_entity')
28+
await connection.execute('DELETE FROM test_soft_delete_entity')
29+
}
30+
31+
export async function refresh(em: EntityManager): Promise<void> {
32+
await truncate(em)
33+
await seed(em.fork())
34+
}
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
import { MikroORM, EntityManager } from '@mikro-orm/core'
2+
3+
import { TestEntity } from './test.entity'
4+
import { TestRelation } from './test-relation.entity'
5+
import { TestSoftDeleteEntity } from './test-soft-delete.entity'
6+
7+
export const TEST_ENTITIES: TestEntity[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => {
8+
const testEntity = new TestEntity()
9+
testEntity.testEntityPk = `test-entity-${i}`
10+
testEntity.stringType = `foo${i}`
11+
testEntity.boolType = i % 2 === 0
12+
testEntity.numberType = i
13+
testEntity.dateType = new Date(`2020-02-${i.toString().padStart(2, '0')}`)
14+
return testEntity
15+
})
16+
17+
export const TEST_RELATIONS: TestRelation[] = TEST_ENTITIES.reduce((relations, te) => {
18+
return [
19+
...relations,
20+
...[1, 2, 3].map((i) => {
21+
const relation = new TestRelation()
22+
relation.testRelationPk = `test-relations-${te.testEntityPk}-${i}`
23+
relation.relationName = `${te.stringType}-relation-${i}`
24+
relation.testEntityId = te.testEntityPk
25+
return relation
26+
})
27+
]
28+
}, [] as TestRelation[])
29+
30+
export const TEST_SOFT_DELETE_ENTITIES: TestSoftDeleteEntity[] = [1, 2, 3, 4, 5].map((i) => {
31+
const entity = new TestSoftDeleteEntity()
32+
entity.testEntityPk = `test-soft-delete-entity-${i}`
33+
entity.stringType = `foo${i}`
34+
return entity
35+
})
36+
37+
export async function seed(em: EntityManager): Promise<void> {
38+
for (const entity of TEST_ENTITIES) {
39+
const newEntity = em.create(TestEntity, {
40+
testEntityPk: entity.testEntityPk,
41+
stringType: entity.stringType,
42+
boolType: entity.boolType,
43+
numberType: entity.numberType,
44+
dateType: entity.dateType
45+
})
46+
em.persist(newEntity)
47+
}
48+
49+
await em.flush()
50+
51+
for (const relation of TEST_RELATIONS) {
52+
const testEntity = await em.findOne(TestEntity, { testEntityPk: relation.testEntityId })
53+
const newRelation = em.create(TestRelation, {
54+
testRelationPk: relation.testRelationPk,
55+
relationName: relation.relationName,
56+
testEntityId: relation.testEntityId,
57+
testEntity
58+
})
59+
em.persist(newRelation)
60+
}
61+
62+
await em.flush()
63+
64+
for (const entity of TEST_SOFT_DELETE_ENTITIES) {
65+
const newEntity = em.create(TestSoftDeleteEntity, {
66+
testEntityPk: entity.testEntityPk,
67+
stringType: entity.stringType
68+
})
69+
em.persist(newEntity)
70+
}
71+
72+
await em.flush()
73+
}
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import { Entity, PrimaryKey, Property, ManyToOne, ManyToMany, OneToOne, Collection } from '@mikro-orm/core'
2+
3+
import { TestEntity } from './test.entity'
4+
5+
@Entity()
6+
export class TestRelation {
7+
@PrimaryKey()
8+
testRelationPk!: string
9+
10+
@Property()
11+
relationName!: string
12+
13+
@Property({ nullable: true })
14+
testEntityId?: string
15+
16+
@ManyToOne(() => TestEntity, { nullable: true })
17+
testEntity?: TestEntity
18+
19+
@ManyToMany(() => TestEntity, (entity) => entity.manyTestRelations)
20+
manyTestEntities = new Collection<TestEntity>(this)
21+
22+
@OneToOne(() => TestEntity, (entity) => entity.oneTestRelation, { nullable: true })
23+
oneTestEntity?: TestEntity
24+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import { Entity, PrimaryKey, Property } from '@mikro-orm/core'
2+
3+
@Entity()
4+
export class TestSoftDeleteEntity {
5+
@PrimaryKey()
6+
testEntityPk!: string
7+
8+
@Property()
9+
stringType!: string
10+
11+
@Property({ nullable: true })
12+
deletedAt?: Date
13+
}
Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import { Entity, PrimaryKey, Property, ManyToOne, OneToMany, ManyToMany, OneToOne, Collection } from '@mikro-orm/core'
2+
3+
import { TestRelation } from './test-relation.entity'
4+
5+
@Entity()
6+
export class TestEntity {
7+
@PrimaryKey()
8+
testEntityPk!: string
9+
10+
@Property()
11+
stringType!: string
12+
13+
@Property()
14+
boolType!: boolean
15+
16+
@Property()
17+
numberType!: number
18+
19+
@Property()
20+
dateType!: Date
21+
22+
@OneToMany(() => TestRelation, (relation) => relation.testEntity)
23+
testRelations = new Collection<TestRelation>(this)
24+
25+
@ManyToOne(() => TestRelation, { nullable: true })
26+
manyToOneRelation?: TestRelation
27+
28+
@ManyToMany(() => TestRelation, (relation) => relation.manyTestEntities, { owner: true })
29+
manyTestRelations = new Collection<TestRelation>(this)
30+
31+
@OneToOne(() => TestRelation, (relation) => relation.oneTestEntity, { owner: true, nullable: true })
32+
oneTestRelation?: TestRelation
33+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import { NestjsQueryMikroOrmModule } from '../src'
2+
import { TestEntity } from './__fixtures__/test.entity'
3+
4+
describe('NestjsQueryMikroOrmModule', () => {
5+
it('should create a module', () => {
6+
const mikroOrmModule = NestjsQueryMikroOrmModule.forFeature([TestEntity])
7+
expect(mikroOrmModule.imports).toHaveLength(1)
8+
expect(mikroOrmModule.module).toBe(NestjsQueryMikroOrmModule)
9+
expect(mikroOrmModule.providers).toHaveLength(1)
10+
expect(mikroOrmModule.exports).toHaveLength(2)
11+
})
12+
13+
it('should support contextName parameter', () => {
14+
const mikroOrmModule = NestjsQueryMikroOrmModule.forFeature([TestEntity], 'connection2')
15+
expect(mikroOrmModule.imports).toHaveLength(1)
16+
expect(mikroOrmModule.module).toBe(NestjsQueryMikroOrmModule)
17+
expect(mikroOrmModule.providers).toHaveLength(1)
18+
expect(mikroOrmModule.exports).toHaveLength(2)
19+
})
20+
})
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { getQueryServiceToken } from '@ptc-org/nestjs-query-core'
2+
3+
import { createMikroOrmQueryServiceProviders } from '../src/providers'
4+
import { TestEntity } from './__fixtures__/test.entity'
5+
import { TestRelation } from './__fixtures__/test-relation.entity'
6+
7+
describe('createMikroOrmQueryServiceProviders', () => {
8+
it('should create a provider for a single entity', () => {
9+
const providers = createMikroOrmQueryServiceProviders([TestEntity])
10+
expect(providers).toHaveLength(1)
11+
expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity))
12+
expect(providers[0].useFactory).toBeDefined()
13+
expect(providers[0].inject).toHaveLength(1)
14+
})
15+
16+
it('should create providers for multiple entities', () => {
17+
const providers = createMikroOrmQueryServiceProviders([TestEntity, TestRelation])
18+
expect(providers).toHaveLength(2)
19+
expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity))
20+
expect(providers[1].provide).toBe(getQueryServiceToken(TestRelation))
21+
})
22+
23+
it('should support contextName parameter', () => {
24+
const providers = createMikroOrmQueryServiceProviders([TestEntity], 'customConnection')
25+
expect(providers).toHaveLength(1)
26+
expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity))
27+
})
28+
})

0 commit comments

Comments
 (0)