From b2a9c3323f2954eb74226e3f3a3a3a18e8d913d8 Mon Sep 17 00:00:00 2001 From: Evgeny Gamov Date: Tue, 16 Jun 2026 20:48:10 +0500 Subject: [PATCH] feat(esmole): mapping flatten --- packages/esmole-mcp/src/es/mapping.ts | 37 +++++++++++++++++ .../esmole-mcp/test/unit/es/mapping.test.ts | 40 +++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 packages/esmole-mcp/src/es/mapping.ts create mode 100644 packages/esmole-mcp/test/unit/es/mapping.test.ts diff --git a/packages/esmole-mcp/src/es/mapping.ts b/packages/esmole-mcp/src/es/mapping.ts new file mode 100644 index 0000000..09dbc6b --- /dev/null +++ b/packages/esmole-mcp/src/es/mapping.ts @@ -0,0 +1,37 @@ +export type MappingRow = { index?: string; field: string; type: string } + +type Props = Record + +const walk = (props: Props, prefix: string, rows: MappingRow[], index?: string): void => { + for (const [name, def] of Object.entries(props)) { + const field = prefix ? prefix + '.' + name : name + if (def.type) { + rows.push(index ? { index, field, type: def.type } : { field, type: def.type }) + } + if (def.fields) { + walk(def.fields, field, rows, index) + } + if (def.properties) { + walk(def.properties, field, rows, index) + } + } +} + +// Flattens an ES `GET /{index}/_mapping` response into dotted field:type rows. +// With a single index the `index` key is omitted; with multiple indices each +// row carries its index name. +export const flattenMapping = (raw: unknown): MappingRow[] => { + const rows: MappingRow[] = [] + if (typeof raw !== 'object' || raw === null) { + return rows + } + const indices = Object.entries(raw as Record) + const multi = indices.length > 1 + for (const [indexName, body] of indices) { + const props = body?.mappings?.properties + if (props) { + walk(props, '', rows, multi ? indexName : undefined) + } + } + return rows +} diff --git a/packages/esmole-mcp/test/unit/es/mapping.test.ts b/packages/esmole-mcp/test/unit/es/mapping.test.ts new file mode 100644 index 0000000..0702983 --- /dev/null +++ b/packages/esmole-mcp/test/unit/es/mapping.test.ts @@ -0,0 +1,40 @@ +import { describe, expect, it } from 'vitest' +import { flattenMapping } from '../../../src/es/mapping.js' + +const raw = { + myindex: { + mappings: { + properties: { + id: { type: 'keyword' }, + title: { type: 'text', fields: { raw: { type: 'keyword' } } }, + author: { properties: { name: { type: 'text' }, age: { type: 'integer' } } } + } + } + } +} + +describe('flattenMapping', () => { + it('flattens nested properties to dotted field:type rows', () => { + const rows = flattenMapping(raw) + expect(rows).toContainEqual({ field: 'id', type: 'keyword' }) + expect(rows).toContainEqual({ field: 'title', type: 'text' }) + expect(rows).toContainEqual({ field: 'title.raw', type: 'keyword' }) + expect(rows).toContainEqual({ field: 'author.name', type: 'text' }) + expect(rows).toContainEqual({ field: 'author.age', type: 'integer' }) + }) + it('handles multiple indices by prefixing the index name', () => { + const rows = flattenMapping({ + a: { mappings: { properties: { x: { type: 'long' } } } }, + b: { mappings: { properties: { y: { type: 'date' } } } } + }) + expect(rows).toContainEqual({ index: 'a', field: 'x', type: 'long' }) + expect(rows).toContainEqual({ index: 'b', field: 'y', type: 'date' }) + }) + it('returns empty array for an empty mapping', () => { + expect(flattenMapping({ idx: { mappings: {} } })).toEqual([]) + }) + it('returns empty array for a non-object input', () => { + expect(flattenMapping(null)).toEqual([]) + expect(flattenMapping('nope')).toEqual([]) + }) +})