Repository: harttle/liquidjs
Pull request #788 (2025-01-04 15:38:42)

fix: enumerate plain objects in where/where_exp, #785

coveralls: ## Pull Request Test Coverage Report for [Build 12611723801](https://coveralls.io/builds/71582202)


### Details

* **3** of **3**   **(100.0%)**  changed or added relevant lines in **1** file are covered.
* No unchanged relevant lines lost coverage.
* Overall coverage remained the same at **99.848%**

---



|  Totals | [![Coverage Status](https://coveralls.io/builds/71582202/badge)](https://coveralls.io/builds/71582202) |
| :-- | --: |
| Change from base [Build 12611375329](https://coveralls.io/builds/71582017): |  0.0% |
| Covered Lines: | 2854 |
| Relevant Lines: | 2856 |

---
##### 💛  - [Coveralls](https://coveralls.io)


Patch:
diff --git a/jest.config.js b/jest.config.js
index 1be9919ee..2f1c62ae2 100644
--- a/jest.config.js
+++ b/jest.config.js
@@ -1,6 +1,7 @@
 /** @type {import('ts-jest').JestConfigWithTsJest} */
 module.exports = {
   preset: 'ts-jest',
+  maxWorkers: '50%',
   testEnvironment: 'node',
   testMatch: ['**/*.spec.ts'],
   collectCoverageFrom: [
diff --git a/src/filters/array.ts b/src/filters/array.ts
index 8547fb80e..36b50eced 100644
--- a/src/filters/array.ts
+++ b/src/filters/array.ts
@@ -1,4 +1,4 @@
-import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, isArrayLike } from '../util'
+import { toArray, argumentsToValue, toValue, stringify, caseInsensitiveCompare, isArray, isNil, last as arrayLast, isArrayLike, toEnumerable } from '../util'
 import { arrayIncludes, equals, evalToken, isTruthy } from '../render'
 import { Value, FilterImpl } from '../template'
 import { Tokenizer } from '../parser'
@@ -148,7 +148,7 @@ export function * where_exp<T extends object> (this: FilterImpl, arr: T[], itemN
 
 export function * group_by<T extends object> (this: FilterImpl, arr: T[], property: string): IterableIterator<unknown> {
   const map = new Map()
-  arr = toArray(arr)
+  arr = toEnumerable(arr)
   const token = new Tokenizer(stringify(property)).readScopeValue()
   this.context.memoryLimit.use(arr.length)
   for (const item of arr) {
@@ -162,7 +162,7 @@ export function * group_by<T extends object> (this: FilterImpl, arr: T[], proper
 export function * group_by_exp<T extends object> (this: FilterImpl, arr: T[], itemName: string, exp: string): IterableIterator<unknown> {
   const map = new Map()
   const keyTemplate = new Value(stringify(exp), this.liquid)
-  arr = toArray(arr)
+  arr = toEnumerable(arr)
   this.context.memoryLimit.use(arr.length)
   for (const item of arr) {
     const key = yield keyTemplate.value(this.context.spawn({ [itemName]: item }))


Test patch:
diff --git a/test/e2e/issues.spec.ts b/test/e2e/issues.spec.ts
index d437379fd..619241f10 100644
--- a/test/e2e/issues.spec.ts
+++ b/test/e2e/issues.spec.ts
@@ -531,4 +531,31 @@ describe('Issues', function () {
     const tpl = `{% for i in (1..1000000000) %} {{'a'}} {% endfor %}`
     expect(() => engine.parseAndRenderSync(tpl)).toThrow('memory alloc limit exceeded, line:1, col:1')
   })
+  it('group_by_exp fails with object as input #785', () => {
+    const site = {
+      tags: {
+        CPP: [ 'page0' ],
+        PHP: [ 'page0', 'page2' ],
+        JavaScript: [ 'page1', 'page2', 'page3' ],
+        CSharp: [ 'page2', 'page4' ]
+      }
+    }
+    const tpl = `
+      {%- assign tags_by_size = site.tags | group_by_exp: 'tag', 'tag[1].size' | sort: 'name' | reverse -%}
+      {%- for tags_with_size in tags_by_size -%}
+        {%- for tag in tags_with_size.items -%}
+          {%- assign tag_name = tag[0] %}
+          {{ tag_name }} <sup>{{ tags_with_size.name }}</sup> Posts:
+          {%- for post in tag[1] -%}{{post}},{%- endfor -%}
+        {%- endfor -%}
+      {%- endfor -%}
+    `
+    const engine = new Liquid()
+    const html = engine.parseAndRenderSync(tpl, { site })
+    expect(html).toEqual(`
+          JavaScript <sup>3</sup> Posts:page1,page2,page3,
+          PHP <sup>2</sup> Posts:page0,page2,
+          CSharp <sup>2</sup> Posts:page2,page4,
+          CPP <sup>1</sup> Posts:page0,`)
+  })
 })
diff --git a/test/integration/filters/array.spec.ts b/test/integration/filters/array.spec.ts
index f633dc931..ede56efe1 100644
--- a/test/integration/filters/array.spec.ts
+++ b/test/integration/filters/array.spec.ts
@@ -554,6 +554,12 @@ describe('filters/array', function () {
       { graduation_year: 2014, name: 'John' },
       { graduation_year: 2009, name: 'Jack' }
     ]
+    const postsByTags = {
+      CPP: [ 'page0' ],
+      PHP: [ 'page0', 'page2' ],
+      JavaScript: [ 'page1', 'page2', 'page3' ],
+      CSharp: [ 'page2', 'page4' ]
+    }
     it('should support group by expression', function () {
       const expected = [{
         name: '201',
@@ -572,6 +578,29 @@ describe('filters/array', function () {
         { members },
         JSON.stringify(expected))
     })
+    it('should group key/values in plain object', function () {
+      const expected = [{
+        name: 3,
+        items: [
+          ['JavaScript', ['page1', 'page2', 'page3']]
+        ]
+      }, {
+        name: 2,
+        items: [
+          ['PHP', ['page0', 'page2']],
+          ['CSharp', ['page2', 'page4']]
+        ]
+      }, {
+        name: 1,
+        items: [
+          ['CPP', ['page0']]
+        ]
+      }]
+      return test(
+        `{{ postsByTags | group_by_exp: "tag", "tag[1].size" | sort: 'name' | reverse | json}}`,
+        { postsByTags },
+        JSON.stringify(expected))
+    })
   })
   describe('find', function () {
     const members = [