Skip to content

Commit

Permalink
Add support for selecting fields in arrays which are not top-level pr…
Browse files Browse the repository at this point in the history
…operties of the read model
  • Loading branch information
Castro, Mario committed May 28, 2024
1 parent afd9d2b commit 8726201
Show file tree
Hide file tree
Showing 5 changed files with 129 additions and 38 deletions.
28 changes: 26 additions & 2 deletions packages/framework-provider-azure/src/helpers/query-helper.ts
Original file line number Diff line number Diff line change
Expand Up @@ -225,7 +225,7 @@ function buildProjections(projections: ProjectionFor<unknown> | 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]) {
Expand All @@ -246,7 +246,31 @@ function buildProjections(projections: ProjectionFor<unknown> | 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(', ')
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,6 +146,7 @@ describe('Query helper', () => {
'a.b.c2',
'arr[].x.y',
'arr[].x.z',
'foo.items[].bar',
] as ProjectionFor<unknown>
)

Expand All @@ -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: [],
})
)
Expand Down
116 changes: 82 additions & 34 deletions packages/framework-provider-local/src/services/read-model-registry.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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] = {}

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
}
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] = {}

Check warning

Code scanning / CodeQL

Prototype-polluting assignment Medium

This assignment may alter Object.prototype if a malicious '__proto__' string is injected from
library input
.
}
currentLevel = currentLevel[part]
}
}
}
})
Expand All @@ -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
Expand Down Expand Up @@ -156,25 +159,70 @@ export class ReadModelRegistry {
const result: LocalSelectFor = {}
const seenFields = new Set<string>()

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
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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(),
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -211,7 +211,7 @@ describe('the read model registry', () => {
undefined,
undefined,
undefined,
['id', 'age', 'arr[].id'] as ProjectionFor<unknown>
['id', 'age', 'arr[].id', 'prop.items[].name'] as ProjectionFor<unknown>
)

expect(result.length).to.be.equal(1)
Expand All @@ -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)
})
})
Expand Down

0 comments on commit 8726201

Please sign in to comment.