Skip to content

Commit 34385e9

Browse files
authored
feat: 0.10.0 (#202)
## Feature * feat(collection): added `chunkBy` method ## Fix * fix(services)(**BREAKING CHANGE**): updated default `arrayFormat` * previous configuration caused query string structure issues by not specifying array indexes * fix(model): removed unnecessary `resetQueryParameters` in `findMany` ## Documentation * docs(collection): documented `chunkBy` method ## Chore * chore: incremented version * chore(deps-dev): locked ts eslint version * Locked while the following resolves: * typescript-eslint/typescript-eslint#4569 ## Performance * perf(collection): removed redundant construct calls ## Continuous Integration * ci: added remote before pushing * ci: run build test on PRs going into main too
1 parent 2eaedb8 commit 34385e9

File tree

14 files changed

+201
-68
lines changed

14 files changed

+201
-68
lines changed

.github/workflows/build.yml

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ on:
99
- 'src/**/*.ts'
1010
- 'package-lock.json'
1111
branches:
12+
- main
1213
- 'release/*'
1314

1415
jobs:

.github/workflows/deploy-api-docs.yml

+2-2
Original file line numberDiff line numberDiff line change
@@ -30,5 +30,5 @@ jobs:
3030
git add -f .nojekyll
3131
git add -f index.html
3232
git add -f modules.html
33-
git commit -m "Updates from ${{ github.ref }}" --no-verify
34-
git push -u origin gh-pages
33+
git commit -m "Updates from ${{ github.ref }} - {{ github.sha }}" --no-verify
34+
git push origin -u gh-pages

docs/helpers/collection.md

+33
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,39 @@ collection.chunk(2); // Collection[Collection[1, 2], Collection[3, 4], Collectio
216216
collection.chunk(5); // Collection[Collection[1, 2, 3, 4, 5]]
217217
```
218218

219+
#### chunkBy
220+
221+
The `chunkBy` method returns an object where the keys are the resolved key values. The method also accepts a callback function which can be used to chunk by deeply nested key values.
222+
```js
223+
import { Collection } from '@upfrontjs/framework';
224+
225+
const collection = new Collection([
226+
{ id: 0, parentId: 5 },
227+
{ id: 1, parentId: 5 },
228+
{ id: 2, parentId: 3 },
229+
{ id: 3, parentId: 3 },
230+
{ id: 4, parentId: 3 }
231+
]);
232+
233+
/**
234+
* Will result in:
235+
* {
236+
* '5': Collection[{ id: 0, parentId: 5 },{ id: 1, parentId: 5 }],
237+
* '3': Collection[{ id: 2, parentId: 3 },{ id: 3, parentId: 3 },{ id: 4, parentId: 3 }],
238+
* }
239+
*/
240+
collection.chunkBy('parentId');
241+
242+
/**
243+
* Will result in:
244+
* {
245+
* '5': Collection[{ id: 0, parentId: 5 },{ id: 1, parentId: 5 }],
246+
* '3': Collection[{ id: 2, parentId: 3 },{ id: 3, parentId: 3 },{ id: 4, parentId: 3 }],
247+
* }
248+
*/
249+
collection.chunkBy(item => item.parentKey);
250+
```
251+
219252
#### when
220253

221254
The `when` method executes the given method if the first argument evaluates to true. The method has to return the collection.

package-lock.json

+4-4
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

+3-3
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@upfrontjs/framework",
3-
"version": "0.9.1",
3+
"version": "0.10.0",
44
"description": "Data handling framework complementary to backend model systems.",
55
"main": "index.min.js",
66
"module": "index.es.min.js",
@@ -89,8 +89,8 @@
8989
"@types/qs": "^6.9.5",
9090
"@types/semantic-release": "^17.2.1",
9191
"@types/uuid": "^8.3.0",
92-
"@typescript-eslint/eslint-plugin": "^5.4.0",
93-
"@typescript-eslint/parser": "^5.4.0",
92+
"@typescript-eslint/eslint-plugin": "5.11.0",
93+
"@typescript-eslint/parser": "5.11.0",
9494
"commitlint": "^16.0.0",
9595
"conventional-changelog-conventionalcommits": "4.6.0",
9696
"eslint": "^8.3.0",

src/Calliope/Model.ts

+1-4
Original file line numberDiff line numberDiff line change
@@ -316,10 +316,7 @@ export default class Model extends SoftDeletes implements HasFactory {
316316
* @param {string[]|number[]} ids
317317
*/
318318
public async findMany<T extends this>(ids: (number | string)[]): Promise<ModelCollection<T>> {
319-
let response = await this
320-
.resetQueryParameters()
321-
.whereKey(ids)
322-
.get<T>();
319+
let response = await this.whereKey(ids).get<T>();
323320

324321
if (response instanceof Model) {
325322
response = new ModelCollection([response]);

src/Services/API.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ export default class API implements ApiCaller {
2626
* @see https://www.npmjs.com/package/qs
2727
*/
2828
protected readonly getParamEncodingOptions: qs.IStringifyOptions = {
29-
arrayFormat: 'brackets', // comma does not work currently https://github.com/ljharb/qs/issues/410
29+
arrayFormat: 'indices',
3030
strictNullHandling: true,
3131
indices: true,
3232
encodeValuesOnly: true,

src/Support/Collection.ts

+57-34
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import type Jsonable from '../Contracts/Jsonable';
44
import LogicException from '../Exceptions/LogicException';
55
import { isObjectLiteral } from './function';
66
import type { MaybeArray } from './type';
7+
import InvalidOffsetException from '../Exceptions/InvalidOffsetException';
78

89
export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T>, ArrayLike<T> {
910
/**
@@ -296,7 +297,7 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
296297
}
297298

298299
/**
299-
* Remove all items the are deep equal to the argument.
300+
* Remove all items that are deep equal to the argument.
300301
*
301302
* @param {any} item
302303
*
@@ -314,7 +315,7 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
314315
* @return {this}
315316
*/
316317
public nth(every: number): this {
317-
return this._newInstance(this.toArray().filter((_item, index) => (index + 1) % every === 0));
318+
return this.filter((_item, index) => (index + 1) % every === 0);
318319
}
319320

320321
/**
@@ -323,7 +324,7 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
323324
* @return {this}
324325
*/
325326
public withoutEmpty(): this {
326-
return this._newInstance(this.toArray().filter(item => item !== undefined && item !== null));
327+
return this.filter(item => item !== undefined && item !== null);
327328
}
328329

329330
/**
@@ -415,7 +416,7 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
415416
? values
416417
: new Collection(Array.isArray(values) ? values : [values]);
417418

418-
return this._newInstance(this.toArray().filter(item => argCollection.includes(item)));
419+
return this.filter(item => argCollection.includes(item));
419420
}
420421

421422
/**
@@ -435,12 +436,47 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
435436
continue;
436437
}
437438

438-
result.push(this._newInstance(this.slice(start, start + size).toArray()));
439+
result.push(this.slice(start, start + size));
439440
}
440441

441442
return new Collection(result);
442443
}
443444

445+
/**
446+
* Chunk the collection by the specified key.
447+
*/
448+
public chunkBy<K extends keyof T>(key: K | ((item: T) => PropertyKey)): Record<PropertyKey, Collection<T>> {
449+
if (!this._allAreObjects()) {
450+
throw new TypeError('Every item needs to be an object to be able to access its properties.');
451+
}
452+
453+
const result: Record<PropertyKey, Collection<T>> = {};
454+
455+
if (typeof key === 'string' || typeof key === 'number' || typeof key === 'symbol') {
456+
if (!this.every(obj => key in obj)) {
457+
throw new InvalidOffsetException(
458+
'\'' + String(key) + '\' is not present in every item of the collection.'
459+
);
460+
}
461+
462+
this.pluck(String(key)).unique().forEach(value => {
463+
result[value as PropertyKey] = this.filter(item => item[key] === value);
464+
});
465+
} else {
466+
this.forEach(item => {
467+
const propertyKey = key(item);
468+
469+
if (!result.hasOwnProperty(propertyKey)) {
470+
result[propertyKey] = new Collection();
471+
}
472+
473+
result[propertyKey]!.push(item);
474+
});
475+
}
476+
477+
return result;
478+
}
479+
444480
/**
445481
* Call a callback on the collection
446482
* when the first argument is Boolean(true) or
@@ -535,18 +571,13 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
535571
return this;
536572
}
537573

538-
const array = this.toArray();
539-
540574
if (count < 0) {
541-
return this._newInstance(
542-
array
543-
.reverse()
544-
.filter((_item, index) => index + 1 <= Math.abs(count))
545-
.reverse()
546-
);
575+
return this.reverse()
576+
.filter((_item, index) => index + 1 <= Math.abs(count))
577+
.reverse();
547578
}
548579

549-
return this._newInstance(array.filter((_item, index) => index + 1 <= count));
580+
return this.filter((_item, index) => index + 1 <= count);
550581
}
551582

552583
/**
@@ -600,18 +631,13 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
600631
return this._newInstance(this.toArray());
601632
}
602633

603-
const array = this.toArray();
604-
605634
if (count < 0) {
606-
return this._newInstance(
607-
array
608-
.reverse()
609-
.filter((_item, index) => index >= Math.abs(count))
610-
.reverse()
611-
);
635+
return this.reverse()
636+
.filter((_item, index) => index >= Math.abs(count))
637+
.reverse();
612638
}
613639

614-
return this._newInstance(array.filter((_item, index) => index >= count));
640+
return this.filter((_item, index) => index >= count);
615641
}
616642

617643
/**
@@ -663,23 +689,20 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
663689
public pluck<Keys extends Readonly<string[]> | string[]>(properties: Keys): Collection<Record<Keys[number], any>>;
664690
public pluck(properties: MaybeArray<string>): Collection<any> {
665691
if (!this._allAreObjects()) {
666-
throw new TypeError('Every item needs to be an object to be able to access its properties');
692+
throw new TypeError('Every item needs to be an object to be able to access its properties.');
667693
}
668694

669695
if (Array.isArray(properties)) {
670-
return new Collection(
671-
this.map((item: Record<string, unknown>) => {
672-
const obj: Record<string, unknown> = {};
696+
return this.map((item: Record<string, unknown>) => {
697+
const obj: Record<string, unknown> = {};
673698

674-
properties.forEach(property => obj[property] = item[property]);
699+
properties.forEach(property => obj[property] = item[property]);
675700

676-
return obj;
677-
})
678-
.toArray()
679-
);
701+
return obj;
702+
});
680703
}
681704

682-
return new Collection(this.map((item: Record<string, unknown>) => item[properties]).toArray());
705+
return this.map((item: Record<string, unknown>) => item[properties]);
683706
}
684707

685708
/**
@@ -752,7 +775,7 @@ export default class Collection<T> implements Jsonable, Arrayable<T>, Iterable<T
752775
}
753776

754777
/**
755-
* Get the summative of the collection values.
778+
* Get summative of the collection values.
756779
*
757780
* @param {string|function} key
758781
*/

tests/Calliope/Concerns/CallsApi.test.ts

+8-8
Original file line numberDiff line numberDiff line change
@@ -316,8 +316,8 @@ describe('CallsApi', () => {
316316
newKey: 'new value'
317317
});
318318
expect(getLastRequest()!.url).toBe(
319-
finish(config.get('baseEndPoint')!, '/') + caller.getEndpoint()
320-
+ '?wheres[][column]=id&wheres[][operator]=%3D&wheres[][value]=1&wheres[][boolean]=and&key=value'
319+
finish(config.get('baseEndPoint')!, '/') + caller.getEndpoint() +
320+
'?wheres[0][column]=id&wheres[0][operator]=%3D&wheres[0][value]=1&wheres[0][boolean]=and&key=value'
321321
);
322322

323323
config.unset('requestMiddleware');
@@ -560,7 +560,7 @@ describe('CallsApi', () => {
560560

561561
expect(getLastRequest()?.url)
562562
.toBe(`${config.get('baseEndPoint')!}/${caller.getEndpoint()}`
563-
+ '?wheres[][column]=id&wheres[][operator]=%3D&wheres[][value]=2&wheres[][boolean]=and');
563+
+ '?wheres[0][column]=id&wheres[0][operator]=%3D&wheres[0][value]=2&wheres[0][boolean]=and');
564564
});
565565

566566
it('should send query parameters in the request', async () => {
@@ -570,7 +570,7 @@ describe('CallsApi', () => {
570570
expect(getLastRequest()?.url).toBe(
571571
String(config.get('baseEndPoint')) + '/'
572572
+ caller.getEndpoint()
573-
+ '?wheres[][column]=id&wheres[][operator]=%3D&wheres[][value]=43&wheres[][boolean]=and'
573+
+ '?wheres[0][column]=id&wheres[0][operator]=%3D&wheres[0][value]=43&wheres[0][boolean]=and'
574574
);
575575
});
576576

@@ -642,7 +642,7 @@ describe('CallsApi', () => {
642642
expect(getLastRequest()?.body).toStrictEqual({ key: 'value' });
643643
expect(getLastRequest()?.url).toBe(
644644
`${config.get('baseEndPoint')!}/${caller.getEndpoint()}`
645-
+ '?wheres[][column]=id&wheres[][operator]=%3D&wheres[][value]=43&wheres[][boolean]=and'
645+
+ '?wheres[0][column]=id&wheres[0][operator]=%3D&wheres[0][value]=43&wheres[0][boolean]=and'
646646
);
647647
});
648648
});
@@ -683,7 +683,7 @@ describe('CallsApi', () => {
683683
expect(getLastRequest()?.body).toStrictEqual({ key: 'value' });
684684
expect(getLastRequest()?.url).toBe(
685685
`${config.get('baseEndPoint')!}/${caller.getEndpoint()}`
686-
+ '?wheres[][column]=id&wheres[][operator]=%3D&wheres[][value]=43&wheres[][boolean]=and'
686+
+ '?wheres[0][column]=id&wheres[0][operator]=%3D&wheres[0][value]=43&wheres[0][boolean]=and'
687687
);
688688
});
689689
});
@@ -724,7 +724,7 @@ describe('CallsApi', () => {
724724
expect(getLastRequest()?.body).toStrictEqual({ key: 'value' });
725725
expect(getLastRequest()?.url).toBe(
726726
`${config.get('baseEndPoint')!}/${caller.getEndpoint()}`
727-
+ '?wheres[][column]=id&wheres[][operator]=%3D&wheres[][value]=43&wheres[][boolean]=and'
727+
+ '?wheres[0][column]=id&wheres[0][operator]=%3D&wheres[0][value]=43&wheres[0][boolean]=and'
728728
);
729729
});
730730
});
@@ -773,7 +773,7 @@ describe('CallsApi', () => {
773773

774774
expect(getLastRequest()?.url).toBe(
775775
`${config.get('baseEndPoint')!}/${caller.getEndpoint()}`
776-
+ '?wheres[][column]=id&wheres[][operator]=%3D&wheres[][value]=43&wheres[][boolean]=and'
776+
+ '?wheres[0][column]=id&wheres[0][operator]=%3D&wheres[0][value]=43&wheres[0][boolean]=and'
777777
);
778778
});
779779
});

tests/Calliope/Concerns/HasRelations.test.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -332,7 +332,7 @@ describe('HasRelations', () => {
332332
String(config.get('baseEndPoint'))
333333
+ '/' + String(hasRelations.getEndpoint())
334334
+ '/' + String(hasRelations.getKey())
335-
+ '?' + 'with[]=file&with[]=files'
335+
+ '?' + 'with[0]=file&with[1]=files'
336336
);
337337
}
338338
);

tests/Calliope/Concerns/HasTimestamps.test.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -129,8 +129,8 @@ describe('HasTimestamps', () => {
129129
expect(getLastRequest()?.url).toBe(
130130
String(config.get('baseEndPoint'))
131131
+ finish(start(hasTimestamps.getEndpoint(), '/'), '/' + String(hasTimestamps.getKey()))
132-
+ '?wheres[][column]=id&wheres[][operator]=%3D&wheres[][value]=1&wheres[][boolean]=and'
133-
+ '&columns[]=createdAt&columns[]=updatedAt'
132+
+ '?wheres[0][column]=id&wheres[0][operator]=%3D&wheres[0][value]=1&wheres[0][boolean]=and'
133+
+ '&columns[0]=createdAt&columns[1]=updatedAt'
134134
);
135135
});
136136

0 commit comments

Comments
 (0)