diff --git a/.github/actions/setup-step/action.yml b/.github/actions/setup-step/action.yml index e259bde38..288d6850b 100644 --- a/.github/actions/setup-step/action.yml +++ b/.github/actions/setup-step/action.yml @@ -5,6 +5,11 @@ author: 'Tycho Bokdam' runs: using: "composite" steps: + - name: Get Node version + id: node-version + shell: bash + run: echo "version=$(node --version)" >> $GITHUB_OUTPUT + - name: Cache node modules id: cache uses: actions/cache@v3 @@ -12,4 +17,9 @@ runs: path: | ~/.cache/mongodb-memory-server **/node_modules - key: cache-node-modules-${{ hashFiles('yarn.lock') }} + key: cache-node-modules-${{ steps.node-version.outputs.version }}-${{ hashFiles('yarn.lock') }} + + - name: Install dependencies + if: steps.cache.outputs.cache-hit != 'true' + shell: bash + run: yarn install --immutable diff --git a/.github/workflows/publish-preview.yml b/.github/workflows/publish-preview.yml new file mode 100644 index 000000000..c0482756e --- /dev/null +++ b/.github/workflows/publish-preview.yml @@ -0,0 +1,31 @@ +name: Publish Preview + +on: + pull_request: + workflow_dispatch: + +env: + NX_BRANCH: ${{ github.event.number }} + NX_RUN_GROUP: ${{ github.run_id }} + +jobs: + publish-preview: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Use Node.js + uses: actions/setup-node@v4 + with: + node-version: 22.x + + - name: Setup + uses: ./.github/actions/setup-step + + - name: Build + run: yarn nx run-many --target=build --all + + - name: Publish Preview + run: npx pkg-pr-new publish './dist/packages/*' diff --git a/package.json b/package.json index 50b0896db..024a9afc1 100644 --- a/package.json +++ b/package.json @@ -46,6 +46,9 @@ "@docusaurus/module-type-aliases": "3.9.1", "@docusaurus/preset-classic": "3.9.1", "@jscutlery/semver": "5.7.1", + "@mikro-orm/better-sqlite": "^6.4.0", + "@mikro-orm/core": "^6.4.0", + "@mikro-orm/nestjs": "^6.1.0", "@nestjs/apollo": "^13.2.1", "@nestjs/cli": "11.0.10", "@nestjs/schematics": "11.0.8", diff --git a/packages/query-mikro-orm/.eslintrc.json b/packages/query-mikro-orm/.eslintrc.json new file mode 100644 index 000000000..10e609fcf --- /dev/null +++ b/packages/query-mikro-orm/.eslintrc.json @@ -0,0 +1,36 @@ +{ + "extends": [ + "../../.eslintrc.json" + ], + "ignorePatterns": [ + "!**/*" + ], + "parserOptions": { + "project": "./tsconfig.json" + }, + "overrides": [ + { + "files": [ + "*.ts", + "*.tsx", + "*.js", + "*.jsx" + ], + "rules": {} + }, + { + "files": [ + "*.ts", + "*.tsx" + ], + "rules": {} + }, + { + "files": [ + "*.js", + "*.jsx" + ], + "rules": {} + } + ] +} diff --git a/packages/query-mikro-orm/__tests__/__fixtures__/connection.fixture.ts b/packages/query-mikro-orm/__tests__/__fixtures__/connection.fixture.ts new file mode 100644 index 000000000..c7e4bb94a --- /dev/null +++ b/packages/query-mikro-orm/__tests__/__fixtures__/connection.fixture.ts @@ -0,0 +1,32 @@ +import { BetterSqliteDriver } from '@mikro-orm/better-sqlite' +import { MikroORM, Options } from '@mikro-orm/core' + +import { seed } from './seeds' +import { TestEntity } from './test.entity' +import { TestRelation } from './test-relation.entity' + +export const CONNECTION_OPTIONS: Options = { + driver: BetterSqliteDriver, + dbName: ':memory:', + entities: [TestEntity, TestRelation], + allowGlobalContext: true +} + +export async function createTestConnection(): Promise> { + const orm = await MikroORM.init(CONNECTION_OPTIONS) + const generator = orm.getSchemaGenerator() + await generator.createSchema() + return orm +} + +export async function truncate(orm: MikroORM): Promise { + const em = orm.em.fork() + await em.nativeDelete(TestRelation, {}) + await em.nativeDelete(TestEntity, {}) +} + +export async function refresh(orm: MikroORM): Promise { + await truncate(orm) + const em = orm.em.fork() + await seed(em) +} diff --git a/packages/query-mikro-orm/__tests__/__fixtures__/index.ts b/packages/query-mikro-orm/__tests__/__fixtures__/index.ts new file mode 100644 index 000000000..f38bb1d2c --- /dev/null +++ b/packages/query-mikro-orm/__tests__/__fixtures__/index.ts @@ -0,0 +1,4 @@ +export * from './connection.fixture' +export * from './seeds' +export * from './test.entity' +export * from './test-relation.entity' diff --git a/packages/query-mikro-orm/__tests__/__fixtures__/seeds.ts b/packages/query-mikro-orm/__tests__/__fixtures__/seeds.ts new file mode 100644 index 000000000..a4aff4177 --- /dev/null +++ b/packages/query-mikro-orm/__tests__/__fixtures__/seeds.ts @@ -0,0 +1,72 @@ +import { EntityManager } from '@mikro-orm/core' + +import { TestEntity } from './test.entity' +import { TestRelation } from './test-relation.entity' + +export const TEST_ENTITIES: Omit[] = + [1, 2, 3, 4, 5, 6, 7, 8, 9, 10].map((i) => ({ + id: `test-entity-${i}`, + boolType: i % 2 === 0, + dateType: new Date(`2020-02-${String(i).padStart(2, '0')} 12:00`), + numberType: i, + stringType: `foo${i}` + })) + +export const TEST_RELATIONS: Omit[] = TEST_ENTITIES.flatMap( + (te) => [ + { + id: `test-relations-${te.id}-1`, + relationName: `${te.stringType}-test-relation-one` + }, + { + id: `test-relations-${te.id}-2`, + relationName: `${te.stringType}-test-relation-two` + }, + { + id: `test-relations-${te.id}-3`, + relationName: `${te.stringType}-test-relation-three` + } + ] +) + +export async function seed(em: EntityManager): Promise { + const testEntities = TEST_ENTITIES.map((data) => { + const entity = new TestEntity() + Object.assign(entity, data) + return entity + }) + + for (const entity of testEntities) { + em.persist(entity) + } + + const testRelations = TEST_RELATIONS.map((data, index) => { + const relation = new TestRelation() + Object.assign(relation, data) + const entityIndex = Math.floor(index / 3) + relation.testEntity = em.getReference(TestEntity, testEntities[entityIndex].id) + return relation + }) + + for (const relation of testRelations) { + em.persist(relation) + } + + await em.flush() + + for (let i = 0; i < testEntities.length; i++) { + const entity = testEntities[i] + const firstRelation = testRelations[i * 3] + + entity.oneTestRelation = em.getReference(TestRelation, firstRelation.id) + + if (entity.numberType % 2 === 0) { + const twoRelations = testRelations.filter((tr) => tr.relationName.endsWith('two')) + for (const rel of twoRelations) { + entity.manyTestRelations.add(rel) + } + } + } + + await em.flush() +} diff --git a/packages/query-mikro-orm/__tests__/__fixtures__/test-relation.entity.ts b/packages/query-mikro-orm/__tests__/__fixtures__/test-relation.entity.ts new file mode 100644 index 000000000..56bea7939 --- /dev/null +++ b/packages/query-mikro-orm/__tests__/__fixtures__/test-relation.entity.ts @@ -0,0 +1,21 @@ +import { Collection, Entity, ManyToMany, ManyToOne, OneToOne, PrimaryKey, Property } from '@mikro-orm/core' + +import { TestEntity } from './test.entity' + +@Entity() +export class TestRelation { + @PrimaryKey() + id!: string + + @Property() + relationName!: string + + @ManyToOne(() => TestEntity, { nullable: true }) + testEntity?: TestEntity + + @ManyToMany(() => TestEntity, (entity) => entity.manyTestRelations, { owner: true }) + manyTestEntities = new Collection(this) + + @OneToOne(() => TestEntity, (entity) => entity.oneTestRelation, { nullable: true }) + oneTestEntity?: TestEntity +} diff --git a/packages/query-mikro-orm/__tests__/__fixtures__/test.entity.ts b/packages/query-mikro-orm/__tests__/__fixtures__/test.entity.ts new file mode 100644 index 000000000..e17cee724 --- /dev/null +++ b/packages/query-mikro-orm/__tests__/__fixtures__/test.entity.ts @@ -0,0 +1,33 @@ +import { Collection, Entity, ManyToMany, ManyToOne, OneToMany, OneToOne, PrimaryKey, Property } from '@mikro-orm/core' + +import { TestRelation } from './test-relation.entity' + +@Entity() +export class TestEntity { + @PrimaryKey() + id!: string + + @Property() + stringType!: string + + @Property() + boolType!: boolean + + @Property() + numberType!: number + + @Property() + dateType!: Date + + @OneToMany(() => TestRelation, (relation) => relation.testEntity) + testRelations = new Collection(this) + + @ManyToOne(() => TestRelation, { nullable: true }) + manyToOneRelation?: TestRelation + + @ManyToMany(() => TestRelation, (relation) => relation.manyTestEntities) + manyTestRelations = new Collection(this) + + @OneToOne(() => TestRelation, (relation) => relation.oneTestEntity, { nullable: true, owner: true }) + oneTestRelation?: TestRelation +} diff --git a/packages/query-mikro-orm/__tests__/module.spec.ts b/packages/query-mikro-orm/__tests__/module.spec.ts new file mode 100644 index 000000000..8c1f214af --- /dev/null +++ b/packages/query-mikro-orm/__tests__/module.spec.ts @@ -0,0 +1,69 @@ +import { MikroORM } from '@mikro-orm/core' +import { MikroOrmModule } from '@mikro-orm/nestjs' +import { SqliteDriver } from '@mikro-orm/sqlite' +import { Test, TestingModule } from '@nestjs/testing' +import { getQueryServiceToken } from '@ptc-org/nestjs-query-core' + +import { MikroOrmQueryService, NestjsQueryMikroOrmModule } from '../src' +import { CONNECTION_OPTIONS, TestEntity, TestRelation } from './__fixtures__' + +describe('NestjsQueryMikroOrmModule', () => { + let moduleRef: TestingModule + let orm: MikroORM + + afterEach(async () => { + if (orm) { + await orm.close() + } + if (moduleRef) { + await moduleRef.close() + } + }) + + describe('forFeature', () => { + it('should create query services for entities', async () => { + moduleRef = await Test.createTestingModule({ + imports: [MikroOrmModule.forRoot(CONNECTION_OPTIONS), NestjsQueryMikroOrmModule.forFeature([TestEntity, TestRelation])] + }).compile() + + orm = moduleRef.get(MikroORM) + await orm.getSchemaGenerator().createSchema() + + const testEntityService = moduleRef.get>(getQueryServiceToken(TestEntity)) + const testRelationService = moduleRef.get>(getQueryServiceToken(TestRelation)) + + expect(testEntityService).toBeInstanceOf(MikroOrmQueryService) + expect(testRelationService).toBeInstanceOf(MikroOrmQueryService) + }) + + it('should create query service with custom DTO', async () => { + class TestEntityDTO { + id!: string + + stringType!: string + } + + moduleRef = await Test.createTestingModule({ + imports: [ + MikroOrmModule.forRoot(CONNECTION_OPTIONS), + NestjsQueryMikroOrmModule.forFeature([{ entity: TestEntity, dto: TestEntityDTO }]) + ] + }).compile() + + orm = moduleRef.get(MikroORM) + await orm.getSchemaGenerator().createSchema() + + const service = moduleRef.get>(getQueryServiceToken(TestEntityDTO)) + expect(service).toBeInstanceOf(MikroOrmQueryService) + }) + + it('should export MikroOrmModule', async () => { + moduleRef = await Test.createTestingModule({ + imports: [MikroOrmModule.forRoot(CONNECTION_OPTIONS), NestjsQueryMikroOrmModule.forFeature([TestEntity])] + }).compile() + + orm = moduleRef.get(MikroORM) + expect(orm).toBeDefined() + }) + }) +}) diff --git a/packages/query-mikro-orm/__tests__/providers.spec.ts b/packages/query-mikro-orm/__tests__/providers.spec.ts new file mode 100644 index 000000000..f82dafbd7 --- /dev/null +++ b/packages/query-mikro-orm/__tests__/providers.spec.ts @@ -0,0 +1,49 @@ +import { EntityRepository } from '@mikro-orm/core' +import { getRepositoryToken } from '@mikro-orm/nestjs' +import { getQueryServiceToken } from '@ptc-org/nestjs-query-core' + +import { createMikroOrmQueryServiceProviders, MikroOrmQueryService } from '../src' +import { TestEntity, TestRelation } from './__fixtures__' + +describe('createMikroOrmQueryServiceProviders', () => { + it('should create providers for entity classes', () => { + const providers = createMikroOrmQueryServiceProviders([TestEntity, TestRelation]) + + expect(providers).toHaveLength(2) + expect(providers[0].provide).toBe(getQueryServiceToken(TestEntity)) + expect(providers[1].provide).toBe(getQueryServiceToken(TestRelation)) + }) + + it('should create provider with custom DTO', () => { + class TestEntityDTO { + id!: string + } + + const providers = createMikroOrmQueryServiceProviders([{ entity: TestEntity, dto: TestEntityDTO }]) + + expect(providers).toHaveLength(1) + expect(providers[0].provide).toBe(getQueryServiceToken(TestEntityDTO)) + }) + + it('should inject repository token', () => { + const providers = createMikroOrmQueryServiceProviders([TestEntity]) + + expect(providers[0].inject).toContain(getRepositoryToken(TestEntity)) + }) + + it('should inject repository token with data source', () => { + const dataSource = 'custom-data-source' + const providers = createMikroOrmQueryServiceProviders([TestEntity], dataSource) + + expect(providers[0].inject).toContain(getRepositoryToken(TestEntity, dataSource)) + }) + + it('should create MikroOrmQueryService instance via factory', () => { + const providers = createMikroOrmQueryServiceProviders([TestEntity]) + const mockRepo = {} as EntityRepository + + const service = providers[0].useFactory(mockRepo) + + expect(service).toBeInstanceOf(MikroOrmQueryService) + }) +}) diff --git a/packages/query-mikro-orm/__tests__/services/mikro-orm-query.service.spec.ts b/packages/query-mikro-orm/__tests__/services/mikro-orm-query.service.spec.ts new file mode 100644 index 000000000..e66687de5 --- /dev/null +++ b/packages/query-mikro-orm/__tests__/services/mikro-orm-query.service.spec.ts @@ -0,0 +1,360 @@ +import { BetterSqliteDriver } from '@mikro-orm/better-sqlite' +import { EntityRepository, MikroORM } from '@mikro-orm/core' +import { SortDirection, SortNulls } from '@ptc-org/nestjs-query-core' + +import { MikroOrmQueryService } from '../../src' +import { createTestConnection, refresh, TEST_ENTITIES, TEST_RELATIONS, TestEntity, TestRelation } from '../__fixtures__' + +describe('MikroOrmQueryService', () => { + let orm: MikroORM + let testEntityRepo: EntityRepository + let testRelationRepo: EntityRepository + let queryService: MikroOrmQueryService + let relationQueryService: MikroOrmQueryService + + beforeAll(async () => { + orm = await createTestConnection() + }) + + afterAll(async () => { + await orm.close() + }) + + beforeEach(async () => { + await refresh(orm) + const em = orm.em.fork() + testEntityRepo = em.getRepository(TestEntity) + testRelationRepo = em.getRepository(TestRelation) + queryService = new MikroOrmQueryService(testEntityRepo) + relationQueryService = new MikroOrmQueryService(testRelationRepo) + }) + + describe('#query', () => { + it('should return all entities when no filter is provided', async () => { + const result = await queryService.query({}) + expect(result).toHaveLength(TEST_ENTITIES.length) + }) + + it('should filter by eq operator', async () => { + const result = await queryService.query({ filter: { stringType: { eq: 'foo1' } } }) + expect(result).toHaveLength(1) + expect(result[0].stringType).toBe('foo1') + }) + + it('should filter by neq operator', async () => { + const result = await queryService.query({ filter: { stringType: { neq: 'foo1' } } }) + expect(result).toHaveLength(TEST_ENTITIES.length - 1) + expect(result.every((e) => e.stringType !== 'foo1')).toBe(true) + }) + + it('should filter by gt operator', async () => { + const result = await queryService.query({ filter: { numberType: { gt: 5 } } }) + expect(result).toHaveLength(5) + expect(result.every((e) => e.numberType > 5)).toBe(true) + }) + + it('should filter by gte operator', async () => { + const result = await queryService.query({ filter: { numberType: { gte: 5 } } }) + expect(result).toHaveLength(6) + expect(result.every((e) => e.numberType >= 5)).toBe(true) + }) + + it('should filter by lt operator', async () => { + const result = await queryService.query({ filter: { numberType: { lt: 5 } } }) + expect(result).toHaveLength(4) + expect(result.every((e) => e.numberType < 5)).toBe(true) + }) + + it('should filter by lte operator', async () => { + const result = await queryService.query({ filter: { numberType: { lte: 5 } } }) + expect(result).toHaveLength(5) + expect(result.every((e) => e.numberType <= 5)).toBe(true) + }) + + it('should filter by in operator', async () => { + const result = await queryService.query({ filter: { numberType: { in: [1, 2, 3] } } }) + expect(result).toHaveLength(3) + expect(result.map((e) => e.numberType).sort()).toEqual([1, 2, 3]) + }) + + it('should filter by notIn operator', async () => { + const result = await queryService.query({ filter: { numberType: { notIn: [1, 2, 3] } } }) + expect(result).toHaveLength(7) + expect(result.every((e) => ![1, 2, 3].includes(e.numberType))).toBe(true) + }) + + it('should filter by like operator', async () => { + const result = await queryService.query({ filter: { stringType: { like: 'foo%' } } }) + expect(result).toHaveLength(TEST_ENTITIES.length) + }) + + it('should filter by like operator with specific pattern', async () => { + const result = await queryService.query({ filter: { stringType: { like: 'foo1%' } } }) + expect(result).toHaveLength(2) // foo1 and foo10 + }) + + it('should filter by is operator (null check)', async () => { + const result = await queryService.query({ filter: { boolType: { is: true } } }) + expect(result).toHaveLength(5) + expect(result.every((e) => e.boolType === true)).toBe(true) + }) + + it('should filter by isNot operator', async () => { + const result = await queryService.query({ filter: { boolType: { isNot: true } } }) + expect(result).toHaveLength(5) + expect(result.every((e) => e.boolType !== true)).toBe(true) + }) + + it('should filter with AND conditions', async () => { + const result = await queryService.query({ + filter: { + and: [{ numberType: { gt: 3 } }, { numberType: { lt: 7 } }] + } + }) + expect(result).toHaveLength(3) + expect(result.every((e) => e.numberType > 3 && e.numberType < 7)).toBe(true) + }) + + it('should filter with OR conditions', async () => { + const result = await queryService.query({ + filter: { + or: [{ numberType: { eq: 1 } }, { numberType: { eq: 10 } }] + } + }) + expect(result).toHaveLength(2) + expect(result.map((e) => e.numberType).sort((a, b) => a - b)).toEqual([1, 10]) + }) + + it('should apply paging with limit', async () => { + const result = await queryService.query({ + paging: { limit: 3 } + }) + expect(result).toHaveLength(3) + }) + + it('should apply paging with offset', async () => { + const result = await queryService.query({ + paging: { offset: 5, limit: 3 }, + sorting: [{ field: 'numberType', direction: SortDirection.ASC }] + }) + expect(result).toHaveLength(3) + expect(result[0].numberType).toBe(6) + }) + + it('should sort ascending', async () => { + const result = await queryService.query({ + sorting: [{ field: 'numberType', direction: SortDirection.ASC }] + }) + expect(result[0].numberType).toBe(1) + expect(result[result.length - 1].numberType).toBe(10) + }) + + it('should sort descending', async () => { + const result = await queryService.query({ + sorting: [{ field: 'numberType', direction: SortDirection.DESC }] + }) + expect(result[0].numberType).toBe(10) + expect(result[result.length - 1].numberType).toBe(1) + }) + + it('should sort with nulls first', async () => { + const result = await queryService.query({ + sorting: [{ field: 'numberType', direction: SortDirection.ASC, nulls: SortNulls.NULLS_FIRST }] + }) + expect(result[0].numberType).toBe(1) + }) + + it('should sort with nulls last', async () => { + const result = await queryService.query({ + sorting: [{ field: 'numberType', direction: SortDirection.ASC, nulls: SortNulls.NULLS_LAST }] + }) + expect(result[0].numberType).toBe(1) + }) + }) + + describe('#getById', () => { + it('should find entity by id', async () => { + const result = await queryService.getById('test-entity-1') + expect(result.id).toBe('test-entity-1') + expect(result.stringType).toBe('foo1') + }) + + it('should throw when entity not found', async () => { + await expect(queryService.getById('non-existent')).rejects.toThrow() + }) + + it('should apply filter when getting by id', async () => { + await expect( + queryService.getById('test-entity-1', { + filter: { numberType: { eq: 999 } } + }) + ).rejects.toThrow() + }) + }) + + describe('#findById', () => { + it('should find entity by id', async () => { + const result = await queryService.findById('test-entity-1') + expect(result).toBeDefined() + expect(result?.id).toBe('test-entity-1') + }) + + it('should return undefined when entity not found', async () => { + const result = await queryService.findById('non-existent') + expect(result).toBeUndefined() + }) + + it('should apply filter when finding by id', async () => { + const result = await queryService.findById('test-entity-1', { + filter: { numberType: { eq: 999 } } + }) + expect(result).toBeUndefined() + }) + }) + + describe('#findRelation', () => { + it('should find a single relation for an entity', async () => { + const entity = await queryService.getById('test-entity-1') + const relation = await queryService.findRelation(TestRelation, 'oneTestRelation', entity) + expect(relation).toBeDefined() + expect((relation as TestRelation).relationName).toContain('test-relation-one') + }) + + it('should find relations for multiple entities', async () => { + const entities = await queryService.query({ filter: { numberType: { in: [1, 2] } } }) + const relations = await queryService.findRelation(TestRelation, 'oneTestRelation', entities) + expect(relations).toBeInstanceOf(Map) + expect((relations as Map).size).toBe(2) + }) + + it('should return relation when filter matches', async () => { + const entity = await queryService.getById('test-entity-1') + const relation = await queryService.findRelation(TestRelation, 'oneTestRelation', entity, { + filter: { relationName: { like: '%one' } } + }) + expect(relation).toBeDefined() + expect((relation as TestRelation).relationName).toContain('test-relation-one') + }) + + it('should return undefined when filter does not match', async () => { + const entity = await queryService.getById('test-entity-1') + const relation = await queryService.findRelation(TestRelation, 'oneTestRelation', entity, { + filter: { relationName: { eq: 'non-existent' } } + }) + expect(relation).toBeUndefined() + }) + + it('should apply filter when finding relations for multiple entities', async () => { + const entities = await queryService.query({ filter: { numberType: { in: [1, 2] } } }) + const relations = await queryService.findRelation(TestRelation, 'oneTestRelation', entities, { + filter: { relationName: { like: '%one' } } + }) + expect(relations).toBeInstanceOf(Map) + const map = relations as Map + expect(map.size).toBe(2) + for (const [, rel] of map) { + expect(rel).toBeDefined() + expect(rel?.relationName).toContain('test-relation-one') + } + }) + + it('should return undefined for entities where filter does not match', async () => { + const entities = await queryService.query({ filter: { numberType: { in: [1, 2] } } }) + const relations = await queryService.findRelation(TestRelation, 'oneTestRelation', entities, { + filter: { relationName: { eq: 'non-existent' } } + }) + expect(relations).toBeInstanceOf(Map) + const map = relations as Map + expect(map.size).toBe(2) + for (const [, rel] of map) { + expect(rel).toBeUndefined() + } + }) + }) + + describe('#queryRelations', () => { + it('should query relations for a single entity', async () => { + const entity = await queryService.getById('test-entity-1') + const relations = await queryService.queryRelations(TestRelation, 'testRelations', entity, {}) + expect(Array.isArray(relations)).toBe(true) + expect(relations).toHaveLength(3) + }) + + it('should query relations for multiple entities', async () => { + const entities = await queryService.query({ filter: { numberType: { in: [1, 2] } } }) + const relations = await queryService.queryRelations(TestRelation, 'testRelations', entities, {}) + expect(relations).toBeInstanceOf(Map) + const map = relations + expect(map.size).toBe(2) + for (const [, rels] of map) { + expect(rels).toHaveLength(3) + } + }) + + it('should apply filter to relations query', async () => { + const entity = await queryService.getById('test-entity-1') + const relations = await queryService.queryRelations(TestRelation, 'testRelations', entity, { + filter: { relationName: { like: '%one' } } + }) + expect(relations).toHaveLength(1) + expect(relations[0].relationName).toContain('one') + }) + + it('should apply paging to relations query', async () => { + const entity = await queryService.getById('test-entity-1') + const relations = await queryService.queryRelations(TestRelation, 'testRelations', entity, { + paging: { limit: 2 } + }) + expect(relations).toHaveLength(2) + }) + + it('should apply sorting to relations query', async () => { + const entity = await queryService.getById('test-entity-1') + const relations = await queryService.queryRelations(TestRelation, 'testRelations', entity, { + sorting: [{ field: 'relationName', direction: SortDirection.DESC }] + }) + const sorted = relations + expect(sorted[0].relationName).toContain('two') + }) + }) + + describe('#countRelations', () => { + it('should count relations for a single entity', async () => { + const entity = await queryService.getById('test-entity-1') + const count = await queryService.countRelations(TestRelation, 'testRelations', entity, {}) + expect(count).toBe(3) + }) + + it('should count relations for multiple entities', async () => { + const entities = await queryService.query({ filter: { numberType: { in: [1, 2] } } }) + const counts = await queryService.countRelations(TestRelation, 'testRelations', entities, {}) + expect(counts).toBeInstanceOf(Map) + const map = counts + expect(map.size).toBe(2) + for (const [, count] of map) { + expect(count).toBe(3) + } + }) + + it('should apply filter when counting relations', async () => { + const entity = await queryService.getById('test-entity-1') + const count = await queryService.countRelations(TestRelation, 'testRelations', entity, { + relationName: { like: '%one' } + }) + expect(count).toBe(1) + }) + }) + + describe('filter conversion', () => { + it('should throw error when filter has both and/or and other properties', async () => { + await expect( + queryService.query({ + filter: { + and: [{ numberType: { eq: 1 } }], + numberType: { eq: 2 } + } as never + }) + ).rejects.toThrow('filter must contain either only `and` or `or` property, or other properties') + }) + }) +}) diff --git a/packages/query-mikro-orm/jest.config.ts b/packages/query-mikro-orm/jest.config.ts new file mode 100644 index 000000000..369f4e4d8 --- /dev/null +++ b/packages/query-mikro-orm/jest.config.ts @@ -0,0 +1,18 @@ +/* eslint-disable */ +// eslint-disable-next-line import/no-default-export +export default { + displayName: 'query-mikro-orm', + preset: '../../jest.preset.js', + globals: {}, + testEnvironment: 'node', + transform: { + '^.+\\.[tj]sx?$': [ + 'ts-jest', + { + tsconfig: '/tsconfig.spec.json' + } + ] + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx'], + coverageDirectory: '../../coverage/packages/query-mikro-orm' +} diff --git a/packages/query-mikro-orm/package.json b/packages/query-mikro-orm/package.json new file mode 100644 index 000000000..70380094f --- /dev/null +++ b/packages/query-mikro-orm/package.json @@ -0,0 +1,42 @@ +{ + "name": "@ptc-org/nestjs-query-mikro-orm", + "version": "9.3.0", + "description": "MikroORM adapter for @ptc-org/nestjs-query-core", + "homepage": "https://github.com/fullstackhouse/nestjs-query#readme", + "bugs": { + "url": "https://github.com/fullstackhouse/nestjs-query/issues" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/fullstackhouse/nestjs-query.git", + "directory": "packages/query-mikro-orm" + }, + "license": "MIT", + "main": "src/index.js", + "types": "src/index.d.ts", + "directories": { + "lib": "src", + "test": "__tests__" + }, + "files": [ + "src/**" + ], + "dependencies": { + "tslib": "^2.8.1" + }, + "peerDependencies": { + "@mikro-orm/core": "^6.0.0", + "@mikro-orm/nestjs": "^6.0.0", + "@nestjs/common": "^9.0.0 || ^10.0.0 || ^11.0.0", + "@ptc-org/nestjs-query-core": "^9.0.0" + }, + "devDependencies": { + "@mikro-orm/better-sqlite": "^6.4.0", + "@mikro-orm/core": "^6.4.0", + "@mikro-orm/nestjs": "^6.1.0", + "@mikro-orm/sqlite": "^6.4.0" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/packages/query-mikro-orm/project.json b/packages/query-mikro-orm/project.json new file mode 100644 index 000000000..bfa4b9435 --- /dev/null +++ b/packages/query-mikro-orm/project.json @@ -0,0 +1,43 @@ +{ + "name": "query-mikro-orm", + "$schema": "../../node_modules/nx/schemas/project-schema.json", + "sourceRoot": "packages/query-mikro-orm/src", + "projectType": "library", + "targets": { + "build": { + "executor": "@nx/js:tsc", + "outputs": ["{options.outputPath}"], + "options": { + "outputPath": "dist/packages/query-mikro-orm", + "tsConfig": "packages/query-mikro-orm/tsconfig.lib.json", + "packageJson": "packages/query-mikro-orm/package.json", + "main": "packages/query-mikro-orm/src/index.ts", + "assets": ["packages/query-mikro-orm/*.md"], + "updateBuildableProjectDepsInPackageJson": true + } + }, + "lint": { + "executor": "@nx/eslint:lint", + "outputs": ["{options.outputFile}"] + }, + "test": { + "executor": "@nx/jest:jest", + "outputs": ["{workspaceRoot}/coverage/packages/query-mikro-orm"], + "options": { + "jestConfig": "packages/query-mikro-orm/jest.config.ts" + } + }, + "version": { + "executor": "@jscutlery/semver:version", + "options": {} + }, + "publish": { + "executor": "nx:run-commands", + "options": { + "command": "npm publish ./dist/packages/query-mikro-orm --access public" + } + } + }, + "tags": [], + "implicitDependencies": ["core"] +} diff --git a/packages/query-mikro-orm/src/assemblers/index.ts b/packages/query-mikro-orm/src/assemblers/index.ts new file mode 100644 index 000000000..8df94d4ae --- /dev/null +++ b/packages/query-mikro-orm/src/assemblers/index.ts @@ -0,0 +1 @@ +export { MikroOrmAssembler } from './mikro-orm.assembler' diff --git a/packages/query-mikro-orm/src/assemblers/mikro-orm.assembler.ts b/packages/query-mikro-orm/src/assemblers/mikro-orm.assembler.ts new file mode 100644 index 000000000..8797c084e --- /dev/null +++ b/packages/query-mikro-orm/src/assemblers/mikro-orm.assembler.ts @@ -0,0 +1,38 @@ +import { AbstractAssembler, AggregateQuery, AggregateResponse, DeepPartial, Query } from '@ptc-org/nestjs-query-core' + +export class MikroOrmAssembler< + DTO, + Entity, + C = DeepPartial, + CE = DeepPartial, + U = C, + UE = CE +> extends AbstractAssembler { + convertToDTO(entity: Entity): DTO | Promise { + return entity as unknown as DTO + } + + convertToEntity(dto: DTO): Entity { + return dto as unknown as Entity + } + + convertQuery(query: Query): Query { + return query as unknown as Query + } + + convertAggregateQuery(aggregate: AggregateQuery): AggregateQuery { + return aggregate as unknown as AggregateQuery + } + + convertAggregateResponse(aggregate: AggregateResponse): AggregateResponse { + return aggregate as unknown as AggregateResponse + } + + convertToCreateEntity(create: C): CE { + return create as unknown as CE + } + + convertToUpdateEntity(update: U): UE { + return update as unknown as UE + } +} diff --git a/packages/query-mikro-orm/src/index.ts b/packages/query-mikro-orm/src/index.ts new file mode 100644 index 000000000..add1c7b83 --- /dev/null +++ b/packages/query-mikro-orm/src/index.ts @@ -0,0 +1,4 @@ +export { MikroOrmAssembler } from './assemblers' +export { NestjsQueryMikroOrmModule } from './module' +export { createMikroOrmQueryServiceProviders, EntityServiceOptions } from './providers' +export { MikroOrmQueryService } from './services' diff --git a/packages/query-mikro-orm/src/module.ts b/packages/query-mikro-orm/src/module.ts new file mode 100644 index 000000000..8ce03e108 --- /dev/null +++ b/packages/query-mikro-orm/src/module.ts @@ -0,0 +1,20 @@ +import { MikroOrmModule } from '@mikro-orm/nestjs' +import { DynamicModule } from '@nestjs/common' +import { Class } from '@ptc-org/nestjs-query-core' + +import { createMikroOrmQueryServiceProviders, EntityServiceOptions } from './providers' + +export class NestjsQueryMikroOrmModule { + static forFeature(entities: Array | EntityServiceOptions>, dataSource?: string): DynamicModule { + const queryServiceProviders = createMikroOrmQueryServiceProviders(entities, dataSource) + const entityClasses = entities.map((e) => (typeof e === 'object' && 'entity' in e ? e.entity : e)) + const mikroOrmModule = MikroOrmModule.forFeature(entityClasses, dataSource) + + return { + imports: [mikroOrmModule], + module: NestjsQueryMikroOrmModule, + providers: queryServiceProviders, + exports: [...queryServiceProviders, mikroOrmModule] + } + } +} diff --git a/packages/query-mikro-orm/src/providers.ts b/packages/query-mikro-orm/src/providers.ts new file mode 100644 index 000000000..6f4302d17 --- /dev/null +++ b/packages/query-mikro-orm/src/providers.ts @@ -0,0 +1,51 @@ +import { EntityRepository } from '@mikro-orm/core' +import { getRepositoryToken } from '@mikro-orm/nestjs' +import { FactoryProvider } from '@nestjs/common' +import { Assembler, AssemblerFactory, Class, getQueryServiceToken } from '@ptc-org/nestjs-query-core' + +import { MikroOrmQueryService } from './services' + +export interface EntityServiceOptions { + entity: Class + dto?: Class + assembler?: Class> +} + +function createMikroOrmQueryServiceProvider( + EntityClass: Class, + DTOClass?: Class, + AssemblerClass?: Class>, + dataSource?: string +): FactoryProvider { + return { + provide: getQueryServiceToken(DTOClass ?? EntityClass), + useFactory(repo: EntityRepository) { + if (AssemblerClass) { + const assembler = new AssemblerClass() + return new MikroOrmQueryService(repo, assembler) + } + + if (DTOClass) { + const assembler = AssemblerFactory.getAssembler(DTOClass, EntityClass) + if (assembler) { + return new MikroOrmQueryService(repo, assembler) + } + } + + return new MikroOrmQueryService(repo) + }, + inject: [getRepositoryToken(EntityClass, dataSource)] + } +} + +export function createMikroOrmQueryServiceProviders( + options: Array | EntityServiceOptions>, + dataSource?: string +): FactoryProvider[] { + return options.map((option) => { + if (typeof option === 'object' && 'entity' in option) { + return createMikroOrmQueryServiceProvider(option.entity, option.dto, option.assembler, dataSource) + } + return createMikroOrmQueryServiceProvider(option, undefined, undefined, dataSource) + }) +} diff --git a/packages/query-mikro-orm/src/services/index.ts b/packages/query-mikro-orm/src/services/index.ts new file mode 100644 index 000000000..e9ca41415 --- /dev/null +++ b/packages/query-mikro-orm/src/services/index.ts @@ -0,0 +1 @@ +export { MikroOrmQueryService } from './mikro-orm-query.service' diff --git a/packages/query-mikro-orm/src/services/mikro-orm-query.service.ts b/packages/query-mikro-orm/src/services/mikro-orm-query.service.ts new file mode 100644 index 000000000..ad9e7ee8a --- /dev/null +++ b/packages/query-mikro-orm/src/services/mikro-orm-query.service.ts @@ -0,0 +1,419 @@ +import { Collection, EntityKey, EntityRepository, FilterQuery, QueryOrder, QueryOrderMap, Reference, wrap } from '@mikro-orm/core' +import { OperatorMap } from '@mikro-orm/core/typings' +import { + Assembler, + AssemblerFactory, + Class, + Filter, + FilterComparisons, + FindByIdOptions, + FindRelationOptions, + GetByIdOptions, + NoOpQueryService, + Query, + QueryOptions, + QueryRelationsOptions, + SortDirection, + SortField, + SortNulls +} from '@ptc-org/nestjs-query-core' + +export class MikroOrmQueryService extends NoOpQueryService { + constructor( + protected repo: EntityRepository, + protected assembler?: Assembler + ) { + super() + } + + async getById(id: string | number, opts?: GetByIdOptions): Promise { + const where = this.convertFilter(opts?.filter) + const entity = await this.repo.findOneOrFail({ + ...where, + id + } as unknown as FilterQuery) + + if (this.assembler) { + return this.assembler.convertToDTO(entity) + } + return entity as unknown as DTO + } + + async findById(id: string | number, opts?: FindByIdOptions): Promise { + const where = this.convertFilter(opts?.filter) + const entity = await this.repo.findOne({ + ...where, + id + } as unknown as FilterQuery) + + if (!entity) return undefined + + if (this.assembler) { + return this.assembler.convertToDTO(entity) + } + return entity as unknown as DTO + } + + async query(query: Query, _opts?: QueryOptions): Promise { + const convertedQuery = this.assembler?.convertQuery?.(query) ?? query + const orderBy = this.convertSorting(convertedQuery.sorting as SortField[] | undefined) + const { limit, offset } = convertedQuery.paging ?? {} + const where = this.convertFilter(convertedQuery.filter) + const entities = await this.repo.findAll({ + orderBy, + limit, + offset, + where + }) + + if (this.assembler) { + return this.assembler.convertToDTOs(entities) + } + return entities as unknown as DTO[] + } + + protected convertFilter(filter: Filter | Filter | undefined): FilterQuery { + if (!filter) { + return {} as FilterQuery + } + + const convertedFilter = this.assembler?.convertQuery?.({ filter } as Query)?.filter ?? filter + + if ((convertedFilter?.and || convertedFilter?.or) && Object.keys(convertedFilter).length > 1) { + throw new Error('filter must contain either only `and` or `or` property, or other properties') + } + + if (convertedFilter?.and) { + return { + $and: convertedFilter.and.map((f) => this.convertFilter(f as Filter | Filter)) + } as FilterQuery + } + + if (convertedFilter?.or) { + return { + $or: convertedFilter.or.map((f) => this.convertFilter(f as Filter | Filter)) + } as FilterQuery + } + + return this.expandFilter(convertedFilter) + } + + protected expandFilter(comparisons: FilterComparisons): FilterQuery { + const filters = Object.entries(comparisons).map(([k, v]) => { + return this.expandFilterComparison(k, v) + }) + + return Object.fromEntries(filters) as FilterQuery + } + + protected expandFilterComparison(k: string, v: unknown): [string, unknown] { + if (k === 'eq' || k === 'is') { + return ['$eq', v as string] satisfies ['$eq', OperatorMap['$eq']] + } + + if (k === 'neq' || k === 'isNot') { + return ['$ne', v as string] satisfies ['$ne', OperatorMap['$ne']] + } + + if (k === 'gt') { + return ['$gt', v as string] satisfies ['$gt', OperatorMap['$gt']] + } + + if (k === 'gte') { + return ['$gte', v as string] satisfies ['$gte', OperatorMap['$gte']] + } + + if (k === 'lt') { + return ['$lt', v as string] satisfies ['$lt', OperatorMap['$lt']] + } + + if (k === 'lte') { + return ['$lte', v as string] satisfies ['$lte', OperatorMap['$lte']] + } + + if (k === 'in') { + return ['$in', v as string[]] satisfies ['$in', OperatorMap['$in']] + } + + if (k === 'notIn') { + return ['$nin', v as string[]] satisfies ['$nin', OperatorMap['$nin']] + } + + if (k === 'like') { + return ['$like', v as string] satisfies ['$like', OperatorMap['$like']] + } + + if (k === 'notLike') { + return ['$like', { $not: v as string }] + } + + if (k === 'iLike') { + return ['$ilike', v as string] satisfies ['$ilike', OperatorMap['$ilike']] + } + + if (k === 'notILike') { + return ['$ilike', { $not: v as string }] + } + + return [k, this.expandFilter(v as FilterComparisons)] + } + + async findRelation( + RelationClass: Class, + relationName: string, + entities: DTO | DTO[], + opts?: FindRelationOptions + ): Promise | Relation | undefined> { + if (!Array.isArray(entities)) { + const dto = entities + const entity = this.assembler ? await Promise.resolve(this.assembler.convertToEntity(dto)) : (dto as unknown as Entity) + const relation = await this.findRelationForEntity(entity, relationName, opts) + return relation + } + + const entries = await Promise.all( + entities.map(async (dto) => { + const entity = this.assembler ? await Promise.resolve(this.assembler.convertToEntity(dto)) : (dto as unknown as Entity) + const relation = await this.findRelationForEntity(entity, relationName, opts) + return [dto, relation] as const + }) + ) + + return new Map(entries) + } + + private async findRelationForEntity( + entity: Entity, + relationName: string, + opts?: FindRelationOptions + ): Promise { + if (opts?.withDeleted) { + throw new Error('MikroOrmQueryService does not support withDeleted on findRelation') + } + + const relation = await this.loadRelationForEntity(entity, relationName) + if (!relation) return undefined + + if (opts?.filter && Object.keys(opts.filter).length > 0) { + return this.matchesFilter(relation, opts.filter) + } + + return relation + } + + private async loadRelationForEntity( + entity: Entity, + relationName: string + ): Promise { + const relationRef = (entity as Record | Relation | undefined>)[relationName] + if (!relationRef) { + const em = this.repo.getEntityManager() + await em.populate(entity, [relationName as never]) + const loadedRef = (entity as Record | Relation | undefined>)[relationName] + if (!loadedRef) return undefined + if ('load' in loadedRef) { + return (await loadedRef.load()) ?? undefined + } + return loadedRef + } + if ('load' in relationRef) { + const relation = (await relationRef.load()) ?? undefined + return relation + } + const wrapped = wrap(relationRef) + if (!wrapped.isInitialized()) { + const em = this.repo.getEntityManager() + await em.refresh(relationRef) + } + return relationRef + } + + private async matchesFilter( + relation: Relation, + filter: Filter + ): Promise { + const em = this.repo.getEntityManager() + const where = this.convertFilter(filter as unknown as Filter) as unknown as FilterQuery + const wrapped = wrap(relation, true) + const pk = wrapped.getPrimaryKey() + + const found = await em.findOne( + relation.constructor as Class, + { + ...where, + [wrapped.__meta.primaryKeys[0]]: pk + } as FilterQuery + ) + + return (found as Relation) ?? undefined + } + + async countRelations( + RelationClass: Class, + relationName: string, + dto: DTO, + filter: Filter, + opts?: QueryRelationsOptions + ): Promise + async countRelations( + RelationClass: Class, + relationName: string, + dto: DTO[], + filter: Filter, + opts?: QueryRelationsOptions + ): Promise> + async countRelations( + RelationClass: Class, + relationName: string, + entities: DTO[] | DTO, + filter: Filter, + opts?: QueryRelationsOptions + ): Promise | number> { + if (opts?.withDeleted) { + throw new Error('MikroOrmQueryService does not support withDeleted on countRelations') + } + if (!Array.isArray(entities)) { + const dto = entities + const entity = this.assembler ? await Promise.resolve(this.assembler.convertToEntity(dto)) : (dto as unknown as Entity) + const count = await this.countRelationsForEntity(entity, relationName, filter) + return count + } + + const entries = await Promise.all( + entities.map(async (dto) => { + const entity = this.assembler ? await Promise.resolve(this.assembler.convertToEntity(dto)) : (dto as unknown as Entity) + const count = await this.countRelationsForEntity(entity, relationName, filter) + return [dto, count] as const + }) + ) + + return new Map(entries) + } + + private async countRelationsForEntity( + entity: Entity, + relationName: string, + filter: Filter + ): Promise { + const where = this.convertFilter(filter as unknown as Filter) as unknown as FilterQuery + + const collection = (entity as Record>)[relationName] + + const count = await collection.loadCount({ where }) + return count + } + + async queryRelations( + RelationClass: Class, + relationName: string, + entities: DTO, + query: Query, + opts?: QueryRelationsOptions + ): Promise + async queryRelations( + RelationClass: Class, + relationName: string, + entities: DTO[], + query: Query, + opts?: QueryRelationsOptions + ): Promise> + async queryRelations( + RelationClass: Class, + relationName: string, + entities: DTO[] | DTO, + query: Query, + opts?: QueryRelationsOptions + ): Promise | RelationDTO[]> { + if (opts?.withDeleted) { + throw new Error('MikroOrmQueryService does not support withDeleted on queryRelations') + } + if (!Array.isArray(entities)) { + const dto = entities + const entity = this.assembler ? await Promise.resolve(this.assembler.convertToEntity(dto)) : (dto as unknown as Entity) + const relations = await this.queryRelationsForEntity(RelationClass, entity, relationName, query) + return relations + } + + const entries = await Promise.all( + entities.map(async (dto) => { + const entity = this.assembler ? await Promise.resolve(this.assembler.convertToEntity(dto)) : (dto as unknown as Entity) + const relations = await this.queryRelationsForEntity(RelationClass, entity, relationName, query) + return [dto, relations] as const + }) + ) + + return new Map(entries) + } + + private async queryRelationsForEntity( + relationDtoClass: Class, + entity: Entity, + relationName: string, + query: Query + ): Promise { + const { offset, limit } = query.paging ?? {} + const where = this.convertFilter(query.filter as unknown as Filter) as unknown as FilterQuery + const orderBy = this.convertSorting(query.sorting as unknown as SortField[] | undefined) as unknown as Array< + QueryOrderMap + > + + const collection = (entity as Record>)[relationName] + const relationEntities = + !offset && !limit + ? await collection.loadItems({ orderBy, where }) + : await collection.matching({ + orderBy, + where, + offset, + limit + }) + + if (relationEntities.length === 0) { + return [] + } + const [relationEntity] = relationEntities + const entityClass = ( + relationEntity as unknown as { + __proto__: { + constructor: Class + } + } + ).__proto__.constructor + + if ((relationDtoClass as unknown) === (entityClass as unknown)) { + return relationEntities as unknown as RelationDTO[] + } + + const assembler = AssemblerFactory.getAssembler(relationDtoClass, entityClass) + const relationDtos = await assembler.convertToDTOs(relationEntities) + + return relationDtos + } + + convertSorting(sorting: Array> | undefined): Array> { + return (sorting ?? []).map((s): QueryOrderMap => { + const direction: QueryOrder = this.convertSortDirection(s) + return { + [s.field as EntityKey]: direction + } as unknown as Record, boolean> + }) + } + + private convertSortDirection(s: SortField): QueryOrder { + switch (s.direction) { + case SortDirection.ASC: + return s.nulls === SortNulls.NULLS_FIRST + ? QueryOrder.ASC_NULLS_FIRST + : s.nulls === SortNulls.NULLS_LAST + ? QueryOrder.ASC_NULLS_LAST + : QueryOrder.ASC + + case SortDirection.DESC: + return s.nulls === SortNulls.NULLS_FIRST + ? QueryOrder.DESC_NULLS_FIRST + : s.nulls === SortNulls.NULLS_LAST + ? QueryOrder.DESC_NULLS_LAST + : QueryOrder.DESC + } + } +} diff --git a/packages/query-mikro-orm/tsconfig.json b/packages/query-mikro-orm/tsconfig.json new file mode 100644 index 000000000..546e940e2 --- /dev/null +++ b/packages/query-mikro-orm/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "files": [], + "include": [], + "references": [ + { + "path": "./tsconfig.lib.json" + }, + { + "path": "./tsconfig.spec.json" + } + ] +} diff --git a/packages/query-mikro-orm/tsconfig.lib.json b/packages/query-mikro-orm/tsconfig.lib.json new file mode 100644 index 000000000..84752388e --- /dev/null +++ b/packages/query-mikro-orm/tsconfig.lib.json @@ -0,0 +1,20 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "module": "commonjs", + "outDir": "../../dist/out-tsc", + "declaration": true, + "types": [ + "node" + ] + }, + "exclude": [ + "jest.config.ts", + "**/*.spec.ts", + "**/*.test.ts", + "__tests__" + ], + "include": [ + "**/*.ts" + ] +} diff --git a/packages/query-mikro-orm/tsconfig.spec.json b/packages/query-mikro-orm/tsconfig.spec.json new file mode 100644 index 000000000..ee46e96db --- /dev/null +++ b/packages/query-mikro-orm/tsconfig.spec.json @@ -0,0 +1,23 @@ +{ + "extends": "./tsconfig.json", + "compilerOptions": { + "outDir": "../../dist/out-tsc", + "module": "commonjs", + "types": [ + "jest", + "node" + ] + }, + "include": [ + "jest.config.ts", + "**/*.test.ts", + "**/*.spec.ts", + "**/*.test.tsx", + "**/*.spec.tsx", + "**/*.test.js", + "**/*.spec.js", + "**/*.test.jsx", + "**/*.spec.jsx", + "**/*.d.ts" + ] +} diff --git a/yarn.lock b/yarn.lock index 7b8fa60eb..8b5c5912e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6055,6 +6055,83 @@ __metadata: languageName: node linkType: hard +"@mikro-orm/better-sqlite@npm:^6.4.0": + version: 6.6.1 + resolution: "@mikro-orm/better-sqlite@npm:6.6.1" + dependencies: + "@mikro-orm/knex": "npm:6.6.1" + better-sqlite3: "npm:11.10.0" + fs-extra: "npm:11.3.2" + sqlstring-sqlite: "npm:0.1.1" + peerDependencies: + "@mikro-orm/core": ^6.0.0 + checksum: 10/47f5b243857a68458199f0cb7ac493ccc7f8fbb00b9c1ea872a32eb554edf28bc8a8705a02e3766330c885c17cad6b58bc833a876423dacc1a9ef0a2694fc69f + languageName: node + linkType: hard + +"@mikro-orm/core@npm:^6.4.0": + version: 6.6.1 + resolution: "@mikro-orm/core@npm:6.6.1" + dependencies: + dataloader: "npm:2.2.3" + dotenv: "npm:17.2.3" + esprima: "npm:4.0.1" + fs-extra: "npm:11.3.2" + globby: "npm:11.1.0" + mikro-orm: "npm:6.6.1" + reflect-metadata: "npm:0.2.2" + checksum: 10/e6649478222effc24cc873cc8f19f5dab54903308d805043a22dc90bb1ae4c5a3c2165ba4e415079923e626ba5869144c792ca6140451f0b1433e2cde6e4b4e6 + languageName: node + linkType: hard + +"@mikro-orm/knex@npm:6.6.1": + version: 6.6.1 + resolution: "@mikro-orm/knex@npm:6.6.1" + dependencies: + fs-extra: "npm:11.3.2" + knex: "npm:3.1.0" + sqlstring: "npm:2.3.3" + peerDependencies: + "@mikro-orm/core": ^6.0.0 + better-sqlite3: "*" + libsql: "*" + mariadb: "*" + peerDependenciesMeta: + better-sqlite3: + optional: true + libsql: + optional: true + mariadb: + optional: true + checksum: 10/f2b564f876922eb9fea2b1277ce215b4ce9635cf3a4f5aef24dc930ca8df699763294de97d283fe149634362c6a94efa6ee23047ff40b322276b27dbf103fd03 + languageName: node + linkType: hard + +"@mikro-orm/nestjs@npm:^6.1.0": + version: 6.1.1 + resolution: "@mikro-orm/nestjs@npm:6.1.1" + peerDependencies: + "@mikro-orm/core": ^6.0.0 || ^6.0.0-dev.0 || ^7.0.0-dev.0 + "@nestjs/common": ^10.0.0 || ^11.0.5 + "@nestjs/core": ^10.0.0 || ^11.0.5 + checksum: 10/5da180b05b71abee40919d60aa6b93f4a22af4a704a17c15ac72946f6d2e13207d1baa3f3c83285a359bbe7afc11beb08b645e4a5287d3d534b39183885e50bc + languageName: node + linkType: hard + +"@mikro-orm/sqlite@npm:^6.4.0": + version: 6.6.1 + resolution: "@mikro-orm/sqlite@npm:6.6.1" + dependencies: + "@mikro-orm/knex": "npm:6.6.1" + fs-extra: "npm:11.3.2" + sqlite3: "npm:5.1.7" + sqlstring-sqlite: "npm:0.1.1" + peerDependencies: + "@mikro-orm/core": ^6.0.0 + checksum: 10/2ab26d960ed1e59a3b5f8b45edfeef29d524a8f0cf2f6470db636247a4eb059ab7f05ebf0f797ec115d92a6483d59eab884fb0854b3396b80e22af0ff2ac4799 + languageName: node + linkType: hard + "@mongodb-js/saslprep@npm:^1.1.0": version: 1.1.9 resolution: "@mongodb-js/saslprep@npm:1.1.9" @@ -6970,6 +7047,23 @@ __metadata: languageName: unknown linkType: soft +"@ptc-org/nestjs-query-mikro-orm@workspace:packages/query-mikro-orm": + version: 0.0.0-use.local + resolution: "@ptc-org/nestjs-query-mikro-orm@workspace:packages/query-mikro-orm" + dependencies: + "@mikro-orm/better-sqlite": "npm:^6.4.0" + "@mikro-orm/core": "npm:^6.4.0" + "@mikro-orm/nestjs": "npm:^6.1.0" + "@mikro-orm/sqlite": "npm:^6.4.0" + tslib: "npm:^2.8.1" + peerDependencies: + "@mikro-orm/core": ^6.0.0 + "@mikro-orm/nestjs": ^6.0.0 + "@nestjs/common": ^9.0.0 || ^10.0.0 || ^11.0.0 + "@ptc-org/nestjs-query-core": ^9.0.0 + languageName: unknown + linkType: soft + "@ptc-org/nestjs-query-mongoose@workspace:packages/query-mongoose": version: 0.0.0-use.local resolution: "@ptc-org/nestjs-query-mongoose@workspace:packages/query-mongoose" @@ -9765,6 +9859,17 @@ __metadata: languageName: node linkType: hard +"better-sqlite3@npm:11.10.0": + version: 11.10.0 + resolution: "better-sqlite3@npm:11.10.0" + dependencies: + bindings: "npm:^1.5.0" + node-gyp: "npm:latest" + prebuild-install: "npm:^7.1.1" + checksum: 10/5e4c7437c4fe6033335a79c82974d7ab29f33c51c36f48b73e87e087d21578468575de1c56a7badd4f76f17255e25abefddaeacf018e5eeb9e0cb8d6e3e4a5e1 + languageName: node + linkType: hard + "big.js@npm:^5.2.2": version: 5.2.2 resolution: "big.js@npm:5.2.2" @@ -10623,6 +10728,13 @@ __metadata: languageName: node linkType: hard +"colorette@npm:2.0.19": + version: 2.0.19 + resolution: "colorette@npm:2.0.19" + checksum: 10/6e2606435cd30e1cae8fc6601b024fdd809e20515c57ce1e588d0518403cff0c98abf807912ba543645a9188af36763b69b67e353d47397f24a1c961aba300bd + languageName: node + linkType: hard + "colorette@npm:^2.0.10": version: 2.0.20 resolution: "colorette@npm:2.0.20" @@ -11639,6 +11751,18 @@ __metadata: languageName: node linkType: hard +"debug@npm:4.3.4": + version: 4.3.4 + resolution: "debug@npm:4.3.4" + dependencies: + ms: "npm:2.1.2" + peerDependenciesMeta: + supports-color: + optional: true + checksum: 10/0073c3bcbd9cb7d71dd5f6b55be8701af42df3e56e911186dfa46fac3a5b9eb7ce7f377dd1d3be6db8977221f8eb333d945216f645cf56f6b688cd484837d255 + languageName: node + linkType: hard + "debug@npm:^3.2.7": version: 3.2.7 resolution: "debug@npm:3.2.7" @@ -12123,6 +12247,13 @@ __metadata: languageName: node linkType: hard +"dotenv@npm:17.2.3": + version: 17.2.3 + resolution: "dotenv@npm:17.2.3" + checksum: 10/f8b78626ebfff6e44420f634773375c9651808b3e1a33df6d4cc19120968eea53e100f59f04ec35f2a20b2beb334b6aba4f24040b2f8ad61773f158ac042a636 + languageName: node + linkType: hard + "dotenv@npm:^16.4.4, dotenv@npm:~16.4.5": version: 16.4.5 resolution: "dotenv@npm:16.4.5" @@ -12915,6 +13046,13 @@ __metadata: languageName: node linkType: hard +"esm@npm:^3.2.25": + version: 3.2.25 + resolution: "esm@npm:3.2.25" + checksum: 10/ee96b8202b76dd1841c55e8a066608d6f0ae0333012be5c77829ccadcd21114283b4d7bf9ac1b8c09853258829c7843e9c6d7e0594acbc5e813cb37d82728d4b + languageName: node + linkType: hard + "espree@npm:^9.0.0, espree@npm:^9.6.0, espree@npm:^9.6.1": version: 9.6.1 resolution: "espree@npm:9.6.1" @@ -12926,7 +13064,7 @@ __metadata: languageName: node linkType: hard -"esprima@npm:^4.0.0, esprima@npm:^4.0.1": +"esprima@npm:4.0.1, esprima@npm:^4.0.0, esprima@npm:^4.0.1": version: 4.0.1 resolution: "esprima@npm:4.0.1" bin: @@ -13724,6 +13862,17 @@ __metadata: languageName: node linkType: hard +"fs-extra@npm:11.3.2": + version: 11.3.2 + resolution: "fs-extra@npm:11.3.2" + dependencies: + graceful-fs: "npm:^4.2.0" + jsonfile: "npm:^6.0.1" + universalify: "npm:^2.0.0" + checksum: 10/d559545c73fda69c75aa786f345c2f738b623b42aea850200b1582e006a35278f63787179e3194ba19413c26a280441758952b0c7e88dd96762d497e365a6c3e + languageName: node + linkType: hard + "fs-extra@npm:^10.0.0": version: 10.1.0 resolution: "fs-extra@npm:10.1.0" @@ -13997,6 +14146,13 @@ __metadata: languageName: node linkType: hard +"getopts@npm:2.3.0": + version: 2.3.0 + resolution: "getopts@npm:2.3.0" + checksum: 10/64c7494d05d6b6205f3351336d9c000265e3f84975ab1bb2b500ff9488eb506bad1d04fa8d2687fd7d81379846e9a500409f8e4b9e20dc604c785abd9b5cf7fd + languageName: node + linkType: hard + "git-raw-commits@npm:^4.0.0": version: 4.0.0 resolution: "git-raw-commits@npm:4.0.0" @@ -14174,7 +14330,7 @@ __metadata: languageName: node linkType: hard -"globby@npm:^11.1.0": +"globby@npm:11.1.0, globby@npm:^11.1.0": version: 11.1.0 resolution: "globby@npm:11.1.0" dependencies: @@ -15191,6 +15347,13 @@ __metadata: languageName: node linkType: hard +"interpret@npm:^2.2.0": + version: 2.2.0 + resolution: "interpret@npm:2.2.0" + checksum: 10/a62d4de5c1f8ab1fd0ccc8a1a8cca8dc31e14928b70364f0787576fe4639c0c463bd79cfe58c9bd9f54db9b7e53d3e646e68fb7627c6b65e3b0e3893156c5126 + languageName: node + linkType: hard + "invariant@npm:^2.2.4": version: 2.2.4 resolution: "invariant@npm:2.2.4" @@ -16782,6 +16945,45 @@ __metadata: languageName: node linkType: hard +"knex@npm:3.1.0": + version: 3.1.0 + resolution: "knex@npm:3.1.0" + dependencies: + colorette: "npm:2.0.19" + commander: "npm:^10.0.0" + debug: "npm:4.3.4" + escalade: "npm:^3.1.1" + esm: "npm:^3.2.25" + get-package-type: "npm:^0.1.0" + getopts: "npm:2.3.0" + interpret: "npm:^2.2.0" + lodash: "npm:^4.17.21" + pg-connection-string: "npm:2.6.2" + rechoir: "npm:^0.8.0" + resolve-from: "npm:^5.0.0" + tarn: "npm:^3.0.2" + tildify: "npm:2.0.0" + peerDependenciesMeta: + better-sqlite3: + optional: true + mysql: + optional: true + mysql2: + optional: true + pg: + optional: true + pg-native: + optional: true + sqlite3: + optional: true + tedious: + optional: true + bin: + knex: bin/cli.js + checksum: 10/12eb978ebec9944d6d0225d33d31d44feb54046b3a02f9f14dfa33a4e665a54d784290991b17a68fd8141a14a3336b325c7706af35557f845dae9e500f3c8aae + languageName: node + linkType: hard + "latest-version@npm:^7.0.0": version: 7.0.0 resolution: "latest-version@npm:7.0.0" @@ -18202,6 +18404,13 @@ __metadata: languageName: node linkType: hard +"mikro-orm@npm:6.6.1": + version: 6.6.1 + resolution: "mikro-orm@npm:6.6.1" + checksum: 10/87537c90ef17fbeb5cb31c26031fb6e0ac4b8991fa4fadfd453c7ed1da37bae0bc1352daba21c907819be6dde05429978a3972d0886ae7ff8efee998bb992fdc + languageName: node + linkType: hard + "mime-db@npm:1.52.0": version: 1.52.0 resolution: "mime-db@npm:1.52.0" @@ -18695,6 +18904,13 @@ __metadata: languageName: node linkType: hard +"ms@npm:2.1.2": + version: 2.1.2 + resolution: "ms@npm:2.1.2" + checksum: 10/673cdb2c3133eb050c745908d8ce632ed2c02d85640e2edb3ace856a2266a813b30c613569bf3354fdf4ea7d1a1494add3bfa95e2713baa27d0c2c71fc44f58f + languageName: node + linkType: hard + "ms@npm:2.1.3, ms@npm:^2.0.0, ms@npm:^2.1.1, ms@npm:^2.1.3": version: 2.1.3 resolution: "ms@npm:2.1.3" @@ -18856,6 +19072,9 @@ __metadata: "@docusaurus/preset-classic": "npm:3.9.1" "@jscutlery/semver": "npm:5.7.1" "@m8a/nestjs-typegoose": "npm:12.0.1" + "@mikro-orm/better-sqlite": "npm:^6.4.0" + "@mikro-orm/core": "npm:^6.4.0" + "@mikro-orm/nestjs": "npm:^6.1.0" "@nestjs/apollo": "npm:^13.2.1" "@nestjs/cli": "npm:11.0.10" "@nestjs/common": "npm:11.1.6" @@ -19969,6 +20188,13 @@ __metadata: languageName: node linkType: hard +"pg-connection-string@npm:2.6.2": + version: 2.6.2 + resolution: "pg-connection-string@npm:2.6.2" + checksum: 10/22265882c3b6f2320785378d0760b051294a684989163d5a1cde4009e64e84448d7bf67d9a7b9e7f69440c3ee9e2212f9aa10dd17ad6773f6143c6020cebbcb5 + languageName: node + linkType: hard + "pg-connection-string@npm:^2.6.1": version: 2.7.0 resolution: "pg-connection-string@npm:2.7.0" @@ -21545,7 +21771,16 @@ __metadata: languageName: node linkType: hard -"reflect-metadata@npm:^0.2.2": +"rechoir@npm:^0.8.0": + version: 0.8.0 + resolution: "rechoir@npm:0.8.0" + dependencies: + resolve: "npm:^1.20.0" + checksum: 10/ad3caed8afdefbc33fbc30e6d22b86c35b3d51c2005546f4e79bcc03c074df804b3640ad18945e6bef9ed12caedc035655ec1082f64a5e94c849ff939dc0a788 + languageName: node + linkType: hard + +"reflect-metadata@npm:0.2.2, reflect-metadata@npm:^0.2.2": version: 0.2.2 resolution: "reflect-metadata@npm:0.2.2" checksum: 10/1c93f9ac790fea1c852fde80c91b2760420069f4862f28e6fae0c00c6937a56508716b0ed2419ab02869dd488d123c4ab92d062ae84e8739ea7417fae10c4745 @@ -23025,7 +23260,7 @@ __metadata: languageName: node linkType: hard -"sqlite3@npm:^5.1.7": +"sqlite3@npm:5.1.7, sqlite3@npm:^5.1.7": version: 5.1.7 resolution: "sqlite3@npm:5.1.7" dependencies: @@ -23046,7 +23281,14 @@ __metadata: languageName: node linkType: hard -"sqlstring@npm:^2.3.2": +"sqlstring-sqlite@npm:0.1.1": + version: 0.1.1 + resolution: "sqlstring-sqlite@npm:0.1.1" + checksum: 10/df99a60ee61586d4d9a21c4923e6ac824aef1850839311ffd16bb53b90a4ffdb3e12e454648328f4c5e4d67728c71d92c7b757c103b81a1a979ff30869ff5f2e + languageName: node + linkType: hard + +"sqlstring@npm:2.3.3, sqlstring@npm:^2.3.2": version: 2.3.3 resolution: "sqlstring@npm:2.3.3" checksum: 10/4e5a25af2d77a031fe00694034bf9fd822ddc3a483c9383124b120aa6b9ae9ab71e173cd29fba9c653998ebfef9e97be668957839960b9b3dc1afcb45f1ddb64 @@ -23606,6 +23848,13 @@ __metadata: languageName: node linkType: hard +"tarn@npm:^3.0.2": + version: 3.0.2 + resolution: "tarn@npm:3.0.2" + checksum: 10/7476ca83a39e0e4b1d951725b6c42071f16fdd65c456936c305500af00731861de0a20e41e59b54cf410b979722816db43acd137a5a580c3c8e48a73f389b523 + languageName: node + linkType: hard + "terser-webpack-plugin@npm:^5.3.10, terser-webpack-plugin@npm:^5.3.9": version: 5.3.10 resolution: "terser-webpack-plugin@npm:5.3.10" @@ -23742,6 +23991,13 @@ __metadata: languageName: node linkType: hard +"tildify@npm:2.0.0": + version: 2.0.0 + resolution: "tildify@npm:2.0.0" + checksum: 10/0f5fee93624c4afdf75ee224c3b65aece4817ba5317fd70f49eaf084ea720d73556a6ef3f50079425a773ba3b93805b4524d14057841d4e4336516fdbe80635b + languageName: node + linkType: hard + "tiny-invariant@npm:^1.0.2": version: 1.3.3 resolution: "tiny-invariant@npm:1.3.3"