Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,7 @@
"@elastic/elasticsearch": "7.13.0",
"@nestjs/common": "10.3.10",
"@nestjs/elasticsearch": "10.0.1",
"class-transformer": "0.5.1",
"class-validator": "0.14.1",
"ramda": "0.30.1",
"reflect-metadata": "0.2.2"
Expand Down
55 changes: 55 additions & 0 deletions src/lib/aggregations/get-nested.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
import { HomeDocument } from 'test/module'
import { TEST_ELASTICSEARCH_NODE } from 'test/constants'
import { setupNestApplication } from 'test/toolkit'
import { ElasticsearchModule } from 'module/elasticsearch.module'
import { ElasticsearchService } from 'module/elasticsearch.service'
import { getNestedAggregation } from './get-nested'

describe('getNestedAggregation', () => {
const { app } = setupNestApplication({
imports: [
ElasticsearchModule.register({
node: TEST_ELASTICSEARCH_NODE
})
]
})

it('accepts only an array of objects schema field', () => {
const query = getNestedAggregation<HomeDocument>('animals')

expect(query).toEqual({
nested: {
path: 'animals'
}
})
})

it('should query elasticsearch for nested aggregation ', async () => {
const service = app.get(ElasticsearchService)

const result = await service.search(HomeDocument, {
size: 0,
aggregations: {
nestedAggregation: getNestedAggregation('animals')
}
})

expect(result.aggregations.nestedAggregation.doc_count).toBeDefined()
expect(result.aggregations.nestedAggregation.doc_count).not.toEqual(0)
})

it('should return doc_count 0 after passing string field which is not an array of objects type', async () => {
const service = app.get(ElasticsearchService)

const result = await service.search(HomeDocument, {
size: 0,
aggregations: {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
nestedAggregation: getNestedAggregation('address' as any)
}
})

expect(result.aggregations.nestedAggregation.doc_count).toBeDefined()
expect(result.aggregations.nestedAggregation.doc_count).toEqual(0)
})
})
15 changes: 15 additions & 0 deletions src/lib/aggregations/get-nested.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
import { Document, ArrayOfObjectsField } from 'lib/common'

export type NestedAggregationBody<TDocument extends Document> = {
path: ArrayOfObjectsField<TDocument>
}

export type NestedAggregation<TDocument extends Document> = {
nested: NestedAggregationBody<TDocument>
}

export const getNestedAggregation = <TDocument extends Document>(path: ArrayOfObjectsField<TDocument>): NestedAggregation<TDocument> => ({
nested: {
path
}
})
1 change: 1 addition & 0 deletions src/lib/aggregations/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,3 +14,4 @@ export * from './get-cardinality'
export * from './get-top-hits'
export * from './get-composite'
export * from './get-stats-bucket'
export * from './get-nested'
2 changes: 2 additions & 0 deletions src/lib/aggregations/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ import { MaxAggregation } from './get-max'
import { CardinalityAggregation } from './get-cardinality'
import { MinAggregation } from './get-min'
import { StatsBucketAggregation } from './get-stats-bucket'
import { NestedAggregation } from './get-nested'

export type AggregationList<TDocument extends Document> =
| AvgAggregation<TDocument>
Expand All @@ -32,6 +33,7 @@ export type AggregationList<TDocument extends Document> =
| CardinalityAggregation<TDocument>
| CompositeAggregation<TDocument>
| StatsBucketAggregation
| NestedAggregation<TDocument>

export type AggregationsContainer<TDocument extends Document> = Record<string, Aggregations<TDocument>>

Expand Down
3 changes: 3 additions & 0 deletions src/lib/common/field.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,6 @@ export type FieldType<TDocument extends Document, TField extends Key<TDocument>
export type NumericField<TDocument extends Document> = {
[K in keyof TDocument]: Exclude<TDocument[K], undefined> extends number | null ? K : never
}[keyof TDocument]
export type ArrayOfObjectsField<TDocument extends Document> = {
[K in keyof TDocument]: TDocument[K] extends Array<object | null> ? K : never
}[keyof TDocument]
1 change: 1 addition & 0 deletions src/lib/responses/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './search'
export * from './cluster-health'
export * from './missing-value-aggregation'
export * from './nested-aggregation'
3 changes: 3 additions & 0 deletions src/lib/responses/nested-aggregation.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
export type NestedAggregationResponse = {
doc_count: number
}
7 changes: 5 additions & 2 deletions src/lib/transformers/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ import {
MaxAggregation,
MinAggregation,
MissingValueAggregation,
NestedAggregation,
PercentileAggregation,
RangeAggregation,
StatsBucketAggregation,
Expand All @@ -20,7 +21,7 @@ import {
TopHitsAggregation,
ValueCountAggregation
} from 'lib/aggregations'
import { MissingValueAggregationResponse } from 'lib/responses'
import { MissingValueAggregationResponse, NestedAggregationResponse } from 'lib/responses'

export type TransformAggregation<
TDocument extends Document,
Expand Down Expand Up @@ -50,7 +51,9 @@ export type TransformAggregation<
? MissingValueAggregationResponse
: TAggregation extends StatsBucketAggregation
? estypes.StatsAggregate
: `Unhandled aggregation type for name: ${TName & string}`
: TAggregation extends NestedAggregation<TDocument>
? NestedAggregationResponse & TransformedAggregations<TDocument, TAggregationsBody>
: `Unhandled aggregation type for name: ${TName & string}`

export type TransformedAggregation<
TDocument extends Document,
Expand Down
12 changes: 12 additions & 0 deletions src/test/module/animal.document.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
import { IsString } from 'class-validator'

export class AnimalDocument {
@IsString()
readonly id: string

@IsString()
readonly type: string

@IsString()
readonly color: string
}
11 changes: 10 additions & 1 deletion src/test/module/home.document.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
import { IsString, IsEnum, IsNumber, IsBoolean, IsOptional } from 'class-validator'
import { IsString, IsEnum, IsNumber, IsBoolean, IsOptional, ValidateNested, IsArray } from 'class-validator'
import { Type } from 'class-transformer'
import { RegisterIndex } from 'lib/decorators'
import { PropertyType } from './enums'
import { AnimalDocument } from './animal.document'

@RegisterIndex('homes')
export class HomeDocument {
Expand Down Expand Up @@ -41,4 +43,11 @@ export class HomeDocument {
@IsString()
@IsOptional()
readonly contractDate?: string

@IsArray()
@ValidateNested({
each: true
})
@Type(() => AnimalDocument)
readonly animals: Array<AnimalDocument>
}
1 change: 1 addition & 0 deletions src/test/module/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './home.document'
export * from './enums'
export * from './test.service'
export * from './animal.document'
16 changes: 15 additions & 1 deletion src/test/scripts/es-seed.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ import { Client } from '@elastic/elasticsearch'
import { join } from 'path'
import { TEST_ELASTICSEARCH_NODE } from 'test/constants'
import { readFile } from 'fs/promises'
import { DOCUMENTS_COUNT } from './generate-random-data'

const index = 'homes'
const client = new Client({
Expand All @@ -23,9 +24,22 @@ readFile(path)
await client.indices.delete({ index })
}

await client.indices.create({ index })

await client.indices.putMapping({
index,
body: {
properties: {
animals: {
type: 'nested'
}
}
}
})

await client.bulk({ body: records })

console.log('Seeded `homes` with:', records.length, 'results.')
console.log('Seeded `homes` with:', DOCUMENTS_COUNT, 'results.')
})
.catch(error => {
throw new Error(`Failed to load homes seed: ${error.message}`)
Expand Down
17 changes: 15 additions & 2 deletions src/test/scripts/generate-random-data.ts
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,18 @@ import { writeFileSync } from 'node:fs'
import { join } from 'node:path'
import { HomeDocument, PropertyType } from 'test/module'

export const DOCUMENTS_COUNT = 100
const ELASTICSEARCH_SEED_INDEX_FILENAME = join(process.cwd(), 'src/test/scripts/seeds/homes.seed.json')
const DOCUMENTS_COUNT = 100

const getAnimals = () => {
const animals = new Array(Math.floor(Math.random() * 5)).fill(undefined)

return animals.map(() => ({
id: faker.string.uuid(),
type: faker.animal.type(),
color: faker.color.human()
}))
}

const data = new Array(DOCUMENTS_COUNT).fill(null).map((_, index): HomeDocument => {
const id = faker.string.uuid()
Expand All @@ -19,6 +29,8 @@ const data = new Array(DOCUMENTS_COUNT).fill(null).map((_, index): HomeDocument
const areaSquared = faker.number.int({ min: 1, max: 1_000_000 })
const contractDate = faker.date.between({ from: '2023-01-01', to: '2023-12-31' }).toISOString()

const animals = getAnimals()

return {
id,
fullName: name,
Expand All @@ -35,7 +47,8 @@ const data = new Array(DOCUMENTS_COUNT).fill(null).map((_, index): HomeDocument
// eslint-disable-next-line @typescript-eslint/ban-types
propertyAreaSquaredAsString: hasProperty && hasPropertyAreaSquared ? areaSquared.toString() : (null as unknown as undefined),
// eslint-disable-next-line @typescript-eslint/ban-types
contractDate: hasProperty ? contractDate : (null as unknown as undefined)
contractDate: hasProperty ? contractDate : (null as unknown as undefined),
animals
}
})

Expand Down
Loading