diff --git a/packages/framework-provider-azure/src/helpers/query-helper.ts b/packages/framework-provider-azure/src/helpers/query-helper.ts index 195ef1894..88676de29 100644 --- a/packages/framework-provider-azure/src/helpers/query-helper.ts +++ b/packages/framework-provider-azure/src/helpers/query-helper.ts @@ -225,7 +225,7 @@ function buildProjections(projections: ProjectionFor | string = '*'): s } // Group fields by the root property - const groupedFields: any = {} + const groupedFields: { [key: string]: string[] } = {} Object.values(projections).forEach((field: string) => { const root: string = field.split('.')[0] if (!groupedFields[root]) { @@ -246,7 +246,31 @@ function buildProjections(projections: ProjectionFor | string = '*'): s return `c.${fields[0]}` } else { // Nested object fields - return fields.map((f: string) => `c.${f} AS "${f}"`).join(', ') + const nestedFields: { [key: string]: string[] } = {} + fields.forEach((f: string) => { + const parts = f.split('.').slice(1) + if (parts.length > 0) { + const nestedRoot = parts[0] + if (!nestedFields[nestedRoot]) { + nestedFields[nestedRoot] = [] + } + nestedFields[nestedRoot].push(parts.join('.')) + } + }) + + return Object.keys(nestedFields) + .map((nestedRoot: string) => { + const subFields = nestedFields[nestedRoot].map((f: string) => `c.${root}.${f} AS "${root}.${f}"`).join(', ') + if (nestedRoot.endsWith('[]')) { + const arrayNestedRoot = nestedRoot.slice(0, -2) + const subArrayFields = nestedFields[nestedRoot] + .map((f: string) => `item.${f.split('.').slice(1).join('.')}`) + .join(', ') + return `ARRAY(SELECT ${subArrayFields} FROM item IN c.${root}.${arrayNestedRoot}) AS "${root}.${arrayNestedRoot}"` + } + return subFields + }) + .join(', ') } }) .join(', ') diff --git a/packages/framework-provider-azure/test/helpers/query-helper.test.ts b/packages/framework-provider-azure/test/helpers/query-helper.test.ts index 0828f7787..a6b1058f3 100644 --- a/packages/framework-provider-azure/test/helpers/query-helper.test.ts +++ b/packages/framework-provider-azure/test/helpers/query-helper.test.ts @@ -146,6 +146,7 @@ describe('Query helper', () => { 'a.b.c2', 'arr[].x.y', 'arr[].x.z', + 'foo.items[].bar', ] as ProjectionFor ) @@ -159,7 +160,10 @@ describe('Query helper', () => { ).to.have.been.calledWith( match({ query: - 'SELECT c.id, c.other, ARRAY(SELECT item.prop1, item.prop2 FROM item IN c.arrayProp) AS arrayProp, c.a.b.c1 AS "a.b.c1", c.a.b.c2 AS "a.b.c2", ARRAY(SELECT item.x.y, item.x.z FROM item IN c.arr) AS arr FROM c ', + 'SELECT c.id, c.other, ARRAY(SELECT item.prop1, item.prop2 FROM item IN c.arrayProp) AS arrayProp, ' + + 'c.a.b.c1 AS "a.b.c1", c.a.b.c2 AS "a.b.c2", ARRAY(SELECT item.x.y, item.x.z FROM item IN c.arr) AS arr, ' + + 'ARRAY(SELECT item.bar FROM item IN c.foo.items) AS "foo.items" ' + + 'FROM c ', parameters: [], }) ) diff --git a/packages/framework-provider-local/src/services/read-model-registry.ts b/packages/framework-provider-local/src/services/read-model-registry.ts index 8d61ca4a4..e5f1a5820 100644 --- a/packages/framework-provider-local/src/services/read-model-registry.ts +++ b/packages/framework-provider-local/src/services/read-model-registry.ts @@ -51,18 +51,34 @@ export class ReadModelRegistry { cursor = cursor.limit(limit) } - const arrayFields: { [key: string]: string[] } = {} + const arrayFields: { [key: string]: any } = {} select?.forEach((field: string) => { const parts = field.split('.') - const topLevelField = parts[0] - if (topLevelField.endsWith('[]')) { - const arrayField = topLevelField.slice(0, -2) - if (!arrayFields[arrayField]) { - arrayFields[arrayField] = [] - } - const subField = parts.slice(1).join('.') - if (subField) { - arrayFields[arrayField].push(subField) + let currentLevel = arrayFields + let isArrayField = false + for (let i = 0; i < parts.length; ++i) { + const part = parts[i] + if (part.endsWith('[]')) { + const arrayField = part.slice(0, -2) + if (!currentLevel[arrayField]) { + currentLevel[arrayField] = {} + } + currentLevel = currentLevel[arrayField] + isArrayField = true + } else { + if (i === parts.length - 1) { + if (isArrayField) { + if (!currentLevel['__fields']) { + currentLevel['__fields'] = [] + } + currentLevel['__fields'].push(part) + } + } else { + if (!currentLevel[part]) { + currentLevel[part] = {} + } + currentLevel = currentLevel[part] + } } } }) @@ -72,20 +88,7 @@ export class ReadModelRegistry { // Process each result to filter the array fields results.forEach((result: any) => { - Object.keys(arrayFields).forEach((arrayField) => { - const subFields = arrayFields[arrayField] - if (result.value && Array.isArray(result.value[arrayField])) { - result.value[arrayField] = result.value[arrayField].map((item: any) => { - const filteredItem: { [key: string]: any } = {} - subFields.forEach((subField) => { - if (subField in item) { - filteredItem[subField] = item[subField] - } - }) - return filteredItem - }) - } - }) + result.value = this.filterObjectByArrayFields(result.value, arrayFields) }) return results @@ -156,25 +159,70 @@ export class ReadModelRegistry { const result: LocalSelectFor = {} const seenFields = new Set() - return select.reduce((acc: LocalSelectFor, field: string) => { - // Split the field into parts + select.forEach((field: string) => { const parts = field.split('.') const topLevelField = parts[0] - // Check if the field is an array field if (topLevelField.endsWith('[]')) { const arrayField = `value.${topLevelField.slice(0, -2)}` - - // Only add the array field if it hasn't been added yet if (!seenFields.has(arrayField)) { seenFields.add(arrayField) - return { ...acc, [arrayField]: 1} + result[arrayField] = 1 + } + } else { + if (parts.some((part) => part.endsWith('[]'))) { + const arrayIndex = parts.findIndex((part) => part.endsWith('[]')) + const arrayField = `value.${parts + .slice(0, arrayIndex + 1) + .join('.') + .slice(0, -2)}` + if (!seenFields.has(arrayField)) { + seenFields.add(arrayField) + result[arrayField] = 1 + } + } else { + const fullPath = `value.${field}` + if (!seenFields.has(fullPath)) { + seenFields.add(fullPath) + result[fullPath] = 1 + } + } + } + }) + + return result + } + + filterArrayFields(item: any, fields: { [key: string]: any; __fields?: string[] }): any { + const filteredItem: { [key: string]: any } = {} + if (fields.__fields) { + fields.__fields.forEach((field) => { + if (field in item) { + filteredItem[field] = item[field] + } + }) + } + Object.keys(fields).forEach((key) => { + if (key !== '__fields' && item[key] && Array.isArray(item[key])) { + filteredItem[key] = item[key].map((subItem: any) => this.filterArrayFields(subItem, fields[key])) + } + }) + return filteredItem + } + + filterObjectByArrayFields(obj: any, arrayFields: { [key: string]: any; __fields?: string[] }): any { + const filteredObj: { [key: string]: any } = {} + Object.keys(obj).forEach((key) => { + if (key in arrayFields) { + if (Array.isArray(obj[key])) { + filteredObj[key] = obj[key].map((item: any) => this.filterArrayFields(item, arrayFields[key])) + } else { + filteredObj[key] = this.filterObjectByArrayFields(obj[key], arrayFields[key]) } } else { - // Handle non-array fields normally - return { ...acc, [`value.${field}`]: 1 } + filteredObj[key] = obj[key] } - return acc - }, result) + }) + return filteredObj } } diff --git a/packages/framework-provider-local/test/helpers/read-model-helper.ts b/packages/framework-provider-local/test/helpers/read-model-helper.ts index 311ddcfe2..9db43e897 100644 --- a/packages/framework-provider-local/test/helpers/read-model-helper.ts +++ b/packages/framework-provider-local/test/helpers/read-model-helper.ts @@ -23,6 +23,18 @@ export function createMockReadModelEnvelope(): ReadModelEnvelope { name: random.word(), }, ], + prop: { + items: [ + { + id: random.uuid(), + name: random.word(), + }, + { + id: random.uuid(), + name: random.word(), + }, + ], + }, }, typeName: random.word(), } diff --git a/packages/framework-provider-local/test/services/read-model-registry.test.ts b/packages/framework-provider-local/test/services/read-model-registry.test.ts index 26c0c8d42..a3db8b8bf 100644 --- a/packages/framework-provider-local/test/services/read-model-registry.test.ts +++ b/packages/framework-provider-local/test/services/read-model-registry.test.ts @@ -211,7 +211,7 @@ describe('the read model registry', () => { undefined, undefined, undefined, - ['id', 'age', 'arr[].id'] as ProjectionFor + ['id', 'age', 'arr[].id', 'prop.items[].name'] as ProjectionFor ) expect(result.length).to.be.equal(1) @@ -220,8 +220,11 @@ describe('the read model registry', () => { id: mockReadModel.value.id, age: mockReadModel.value.age, arr: mockReadModel.value.arr.map((item: any) => ({ id: item.id })), + prop: { items: mockReadModel.value.prop.items.map((item: any) => ({ name: item.name })) }, }, } + console.log(`**** ${JSON.stringify(result[0], null, 2)}`) + console.log(`**** ${JSON.stringify(expectedReadModel, null, 2)}`) expect(result[0]).to.deep.include(expectedReadModel) }) })