Skip to content

Commit 3472d3e

Browse files
authored
Merge pull request #260 from usu/feat/embedded-collection-standalone
Fix: embedded collections without standalone link
2 parents 020cfe5 + 8ad7908 commit 3472d3e

26 files changed

+16352
-5131
lines changed

package-lock.json

Lines changed: 15602 additions & 4704 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

package.json

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -42,16 +42,15 @@
4242
},
4343
"homepage": "https://github.com/ecamp/hal-json-vuex#readme",
4444
"dependencies": {
45-
"hal-json-normalizer": "^4.0.3",
46-
"url-template": "^2.0.8"
45+
"hal-json-normalizer": "4.1.1",
46+
"url-template": "2.0.8"
4747
},
4848
"devDependencies": {
4949
"@babel/plugin-transform-regenerator": "7.16.7",
5050
"@babel/plugin-transform-runtime": "7.16.10",
5151
"@babel/preset-env": "7.16.11",
5252
"@babel/preset-typescript": "7.16.7",
5353
"@nuxt/types": "2.15.8",
54-
"@types/url-template": "2.0.28",
5554
"@typescript-eslint/eslint-plugin": "5.10.1",
5655
"@typescript-eslint/parser": "5.10.1",
5756
"@vue/eslint-config-standard": "6.1.0",

src/Collection.ts

Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
import { isEntityReference } from './halHelpers'
2+
import LoadingCollection from './LoadingCollection'
3+
import ResourceInterface from './interfaces/ResourceInterface'
4+
import CollectionInterface from './interfaces/CollectionInterface'
5+
import { Link } from './interfaces/StoreData'
6+
import Resource from './Resource'
7+
8+
/**
9+
* Filter out items that are marked as deleting (eager removal)
10+
*/
11+
function filterDeleting (array: Array<ResourceInterface>): Array<ResourceInterface> {
12+
return array.filter(entry => !entry._meta.deleting)
13+
}
14+
15+
class Collection extends Resource {
16+
/**
17+
* Get items excluding ones marked as 'deleting' (eager remove)
18+
* The items property should always be a getter, in order to make the call to mapArrayOfEntityReferences
19+
* lazy, since that potentially fetches a large number of entities from the API.
20+
*/
21+
public get items (): Array<ResourceInterface> {
22+
return filterDeleting(this._mapArrayOfEntityReferences(this._storeData.items))
23+
}
24+
25+
/**
26+
* Get all items including ones marked as 'deleting' (lazy remove)
27+
*/
28+
public get allItems (): Array<ResourceInterface> {
29+
return this._mapArrayOfEntityReferences(this._storeData.items)
30+
}
31+
32+
/**
33+
* Returns a promise that resolves to the collection object, once all items have been loaded
34+
*/
35+
public $loadItems () :Promise<CollectionInterface> {
36+
return this._itemLoader(this._storeData.items)
37+
}
38+
39+
/**
40+
* Returns a promise that resolves to the collection object, once all items have been loaded
41+
*/
42+
private _itemLoader (array: Array<Link>) : Promise<CollectionInterface> {
43+
if (!this._containsUnknownEntityReference(array)) {
44+
return Promise.resolve(this as unknown as CollectionInterface) // we know that this object must be of type CollectionInterface
45+
}
46+
47+
// eager loading of 'fetchAllUri' (e.g. parent for embedded collections)
48+
if (this.config.avoidNPlusOneRequests) {
49+
return this.apiActions.reload(this as unknown as CollectionInterface) as Promise<CollectionInterface> // we know that reload resolves to a type CollectionInterface
50+
51+
// no eager loading: replace each reference (Link) with a Resource (ResourceInterface)
52+
} else {
53+
const arrayWithReplacedReferences = this._replaceEntityReferences(array)
54+
55+
return Promise.all(
56+
arrayWithReplacedReferences.map(entry => entry._meta.load)
57+
).then(() => this as unknown as CollectionInterface) // we know that this object must be of type CollectionInterface
58+
}
59+
}
60+
61+
/**
62+
* Given an array, replaces any entity references in the array with the entity loaded from the Vuex store
63+
* (or from the API if necessary), and returns that as a new array. In case some of the entity references in
64+
* the array have not finished loading yet, returns a LoadingCollection instead.
65+
* @param array possibly mixed array of values and references
66+
* @param fetchAllUri URI that allows fetching all array items in a single network request, if known
67+
* @param fetchAllProperty property in the entity from fetchAllUri that will contain the array
68+
* @returns array the new array with replaced items, or a LoadingCollection if any of the array
69+
* elements is still loading.
70+
*/
71+
private _mapArrayOfEntityReferences (array: Array<Link>): Array<ResourceInterface> {
72+
if (!this._containsUnknownEntityReference(array)) {
73+
return this._replaceEntityReferences(array)
74+
}
75+
76+
const itemsLoaded = this._itemLoader(array).then(() => this._replaceEntityReferences(array))
77+
78+
// eager loading of 'fetchAllUri' (e.g. parent for embedded collections)
79+
if (this.config.avoidNPlusOneRequests) {
80+
return LoadingCollection.create(itemsLoaded)
81+
82+
// no eager loading: replace each reference (Link) with a Resource (ResourceInterface)
83+
} else {
84+
return LoadingCollection.create(itemsLoaded, this._replaceEntityReferences(array))
85+
}
86+
}
87+
88+
/**
89+
* Replace each item in array with a proper Resource (or LoadingResource)
90+
*/
91+
private _replaceEntityReferences (array: Array<Link>): Array<ResourceInterface> {
92+
return array
93+
.filter(entry => isEntityReference(entry))
94+
.map(entry => this.apiActions.get(entry.href))
95+
}
96+
97+
/**
98+
* Returns true if any of the items within 'array' is not yet known to the API (meaning it has never been loaded)
99+
*/
100+
private _containsUnknownEntityReference (array: Array<Link>): boolean {
101+
return array.some(entry => isEntityReference(entry) && this.apiActions.isUnknown(entry.href))
102+
}
103+
}
104+
105+
export default Collection

src/EmbeddedCollection.ts

Lines changed: 0 additions & 45 deletions
This file was deleted.

src/HasItems.ts

Lines changed: 0 additions & 122 deletions
This file was deleted.
Lines changed: 12 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,40 +1,40 @@
1-
import LoadingStoreValue from './LoadingStoreValue'
2-
import Resource from './interfaces/Resource'
1+
import LoadingResource from './LoadingResource'
2+
import ResourceInterface from './interfaces/ResourceInterface'
33

4-
class LoadingStoreCollection {
4+
class LoadingCollection {
55
/**
66
* Returns a placeholder for an array that has not yet finished loading from the API. The array placeholder
77
* will respond to functional calls (like .find(), .map(), etc.) with further LoadingStoreCollections or
88
* LoadingStoreValues. If passed the existingContent argument, random access and .length will also work.
99
* @param loadArray Promise that resolves once the array has finished loading
1010
* @param existingContent optionally set the elements that are already known, for random access
1111
*/
12-
static create (loadArray: Promise<Array<Resource> | undefined>, existingContent: Array<Resource> = []): Array<Resource> {
12+
static create (loadArray: Promise<Array<ResourceInterface> | undefined>, existingContent: Array<ResourceInterface> = []): Array<ResourceInterface> {
1313
// if Promsise resolves to undefined, provide empty array
14-
// this could happen if items is accessed from a LoadingStoreValue, which resolves to a normal entity without 'items'
14+
// this could happen if items is accessed from a LoadingResource, which resolves to a normal entity without 'items'
1515
const loadArraySafely = loadArray.then(array => array ?? [])
1616

17-
// proxy array function 'find' to a LoadingStoreValue (Resource)
17+
// proxy array function 'find' to a LoadingResource (Resource)
1818
const singleResultFunctions = ['find']
1919
singleResultFunctions.forEach(func => {
2020
// eslint-disable-next-line @typescript-eslint/no-explicit-any
2121
existingContent[func] = (...args: any[]) => {
22-
const resultLoaded = loadArraySafely.then(array => array[func](...args) as Resource)
23-
return new LoadingStoreValue(resultLoaded)
22+
const resultLoaded = loadArraySafely.then(array => array[func](...args) as ResourceInterface)
23+
return new LoadingResource(resultLoaded)
2424
}
2525
})
2626

27-
// proxy array functions with multiple results to a LoadingStoreCollection (Array<Resource>)
27+
// proxy array functions with multiple results to a LoadingCollection (Array<ResourceInterface>)
2828
const arrayResultFunctions = ['map', 'flatMap', 'filter']
2929
arrayResultFunctions.forEach(func => {
3030
// eslint-disable-next-line @typescript-eslint/no-explicit-any
3131
existingContent[func] = (...args: any[]) => {
32-
const resultLoaded = loadArraySafely.then(array => array[func](...args) as Array<Resource>) // TODO: return type for .map() is not necessarily an Array<Resource>
33-
return LoadingStoreCollection.create(resultLoaded)
32+
const resultLoaded = loadArraySafely.then(array => array[func](...args) as Array<ResourceInterface>) // TODO: return type for .map() is not necessarily an Array<ResourceInterface>
33+
return LoadingCollection.create(resultLoaded)
3434
}
3535
})
3636
return existingContent
3737
}
3838
}
3939

40-
export default LoadingStoreCollection
40+
export default LoadingCollection

0 commit comments

Comments
 (0)