Skip to content

Commit

Permalink
Add generics to for storeData contents
Browse files Browse the repository at this point in the history
  • Loading branch information
manuelmeister committed Apr 25, 2024
1 parent 3759b6a commit a9255d5
Show file tree
Hide file tree
Showing 12 changed files with 101 additions and 99 deletions.
22 changes: 11 additions & 11 deletions src/Collection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,53 +8,53 @@ import Resource from './Resource'
/**
* Filter out items that are marked as deleting (eager removal)
*/
function filterDeleting (array: Array<ResourceInterface>): Array<ResourceInterface> {
function filterDeleting<StoreType> (array: Array<ResourceInterface<StoreType>>): Array<ResourceInterface<StoreType>> {
return array.filter(entry => !entry._meta.deleting)
}

class Collection extends Resource<StoreDataCollection> {
class Collection<StoreType> extends Resource<StoreType, StoreDataCollection<StoreType>> {
/**
* Get items excluding ones marked as 'deleting' (eager remove)
* The items property should always be a getter, in order to make the call to mapArrayOfEntityReferences
* lazy, since that potentially fetches a large number of entities from the API.
*/
public get items (): Array<ResourceInterface> {
public get items (): Array<ResourceInterface<StoreType>> {
return filterDeleting(this._mapArrayOfEntityReferences(this._storeData.items))
}

/**
* Get all items including ones marked as 'deleting' (lazy remove)
*/
public get allItems (): Array<ResourceInterface> {
public get allItems (): Array<ResourceInterface<StoreType>> {
return this._mapArrayOfEntityReferences(this._storeData.items)
}

/**
* Returns a promise that resolves to the collection object, once all items have been loaded
*/
public $loadItems () :Promise<CollectionInterface> {
public $loadItems () :Promise<CollectionInterface<StoreType>> {
return this._itemLoader(this._storeData.items)
}

/**
* Returns a promise that resolves to the collection object, once all items have been loaded
*/
private _itemLoader (array: Array<Link>) : Promise<CollectionInterface> {
private _itemLoader (array: Array<Link>) : Promise<CollectionInterface<StoreType>> {
if (!this._containsUnknownEntityReference(array)) {
return Promise.resolve(this as unknown as CollectionInterface) // we know that this object must be of type CollectionInterface
return Promise.resolve(this as unknown as CollectionInterface<StoreType>) // we know that this object must be of type CollectionInterface
}

// eager loading of 'fetchAllUri' (e.g. parent for embedded collections)
if (this.config.avoidNPlusOneRequests) {
return this.apiActions.reload(this as unknown as CollectionInterface) as Promise<CollectionInterface> // we know that reload resolves to a type CollectionInterface
return this.apiActions.reload(this as unknown as CollectionInterface<StoreType>) as Promise<CollectionInterface<StoreType>> // we know that reload resolves to a type CollectionInterface

// no eager loading: replace each reference (Link) with a Resource (ResourceInterface)
} else {
const arrayWithReplacedReferences = this._replaceEntityReferences(array)

return Promise.all(
arrayWithReplacedReferences.map(entry => entry._meta.load)
).then(() => this as unknown as CollectionInterface) // we know that this object must be of type CollectionInterface
).then(() => this as unknown as CollectionInterface<StoreType>) // we know that this object must be of type CollectionInterface
}
}

Expand All @@ -68,7 +68,7 @@ class Collection extends Resource<StoreDataCollection> {
* @returns array the new array with replaced items, or a LoadingCollection if any of the array
* elements is still loading.
*/
private _mapArrayOfEntityReferences (array: Array<Link>): Array<ResourceInterface> {
private _mapArrayOfEntityReferences (array: Array<Link>): Array<ResourceInterface<StoreType>> {
if (!this._containsUnknownEntityReference(array)) {
return this._replaceEntityReferences(array)
}
Expand All @@ -88,7 +88,7 @@ class Collection extends Resource<StoreDataCollection> {
/**
* Replace each item in array with a proper Resource (or LoadingResource)
*/
private _replaceEntityReferences (array: Array<Link>): Array<ResourceInterface> {
private _replaceEntityReferences (array: Array<Link>): Array<ResourceInterface<StoreType>> {
return array
.filter(entry => isEntityReference(entry))
.map(entry => this.apiActions.get(entry.href))
Expand Down
6 changes: 3 additions & 3 deletions src/LoadingCollection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ class LoadingCollection {
* @param loadArray Promise that resolves once the array has finished loading
* @param existingContent optionally set the elements that are already known, for random access
*/
static create (loadArray: Promise<Array<ResourceInterface> | undefined>, existingContent: Array<ResourceInterface> = []): Array<ResourceInterface> {
static create<StoreType> (loadArray: Promise<Array<ResourceInterface<StoreType>> | undefined>, existingContent: Array<ResourceInterface<StoreType>> = []): Array<ResourceInterface<StoreType>> {
// if Promsise resolves to undefined, provide empty array
// this could happen if items is accessed from a LoadingResource, which resolves to a normal entity without 'items'
const loadArraySafely = loadArray.then(array => array ?? [])
Expand All @@ -19,7 +19,7 @@ class LoadingCollection {
singleResultFunctions.forEach(func => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
existingContent[func] = (...args: any[]) => {
const resultLoaded = loadArraySafely.then(array => array[func](...args) as ResourceInterface)
const resultLoaded = loadArraySafely.then(array => array[func](...args) as ResourceInterface<StoreType>)
return new LoadingResource(resultLoaded)
}
})
Expand All @@ -29,7 +29,7 @@ class LoadingCollection {
arrayResultFunctions.forEach(func => {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
existingContent[func] = (...args: any[]) => {
const resultLoaded = loadArraySafely.then(array => array[func](...args) as Array<ResourceInterface>) // TODO: return type for .map() is not necessarily an Array<ResourceInterface>
const resultLoaded = loadArraySafely.then(array => array[func](...args) as Array<ResourceInterface<StoreType>>) // TODO: return type for .map() is not necessarily an Array<ResourceInterface>
return LoadingCollection.create(resultLoaded)
}
})
Expand Down
28 changes: 14 additions & 14 deletions src/LoadingResource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,15 +15,15 @@ import { InternalConfig } from './interfaces/Config'
* let user = new LoadingResource(...)
* 'The "' + user + '" is called "' + user.name + '"' // gives 'The "" is called ""'
*/
class LoadingResource implements ResourceInterface {
class LoadingResource<StoreType> implements ResourceInterface<StoreType> {
public _meta: {
self: string | null,
selfUrl: string | null,
load: Promise<ResourceInterface>
load: Promise<ResourceInterface<StoreType>>
loading: boolean
}

private loadResource: Promise<ResourceInterface>
private loadResource: Promise<ResourceInterface<StoreType>>

/**
* @param loadResource a Promise that resolves to a Resource when the entity has finished
Expand All @@ -32,7 +32,7 @@ class LoadingResource implements ResourceInterface {
* returned LoadingResource will return it in calls to .self and ._meta.self
* @param config configuration of this instance of hal-json-vuex
*/
constructor (loadResource: Promise<ResourceInterface>, self: string | null = null, config: InternalConfig | null = null) {
constructor (loadResource: Promise<ResourceInterface<StoreType>>, self: string | null = null, config: InternalConfig | null = null) {
this._meta = {
self: self,
selfUrl: self ? config?.apiRoot + self : null,
Expand All @@ -43,7 +43,7 @@ class LoadingResource implements ResourceInterface {
this.loadResource = loadResource

const handler = {
get: function (target: LoadingResource, prop: string | number | symbol) {
get: function (target: LoadingResource<StoreType>, prop: string | number | symbol) {
// This is necessary so that Vue's reactivity system understands to treat this LoadingResource
// like a normal object.
if (prop === Symbol.toPrimitive) {
Expand Down Expand Up @@ -80,28 +80,28 @@ class LoadingResource implements ResourceInterface {
return new Proxy(this, handler)
}

get items (): Array<ResourceInterface> {
return LoadingCollection.create(this.loadResource.then(resource => (resource as CollectionInterface).items))
get items (): Array<ResourceInterface<StoreType>> {
return LoadingCollection.create(this.loadResource.then(resource => (resource as CollectionInterface<StoreType>).items))
}

get allItems (): Array<ResourceInterface> {
return LoadingCollection.create(this.loadResource.then(resource => (resource as CollectionInterface).allItems))
get allItems (): Array<ResourceInterface<StoreType>> {
return LoadingCollection.create(this.loadResource.then(resource => (resource as CollectionInterface<StoreType>).allItems))
}

public $reload (): Promise<ResourceInterface> {
public $reload (): Promise<ResourceInterface<StoreType>> {
// Skip reloading entities that are already loading
return this._meta.load
}

public $loadItems (): Promise<CollectionInterface> {
return this._meta.load.then(resource => (resource as CollectionInterface).$loadItems())
public $loadItems (): Promise<CollectionInterface<StoreType>> {
return this._meta.load.then(resource => (resource as CollectionInterface<StoreType>).$loadItems())
}

public $post (data: unknown): Promise<ResourceInterface | null> {
public $post (data: unknown): Promise<ResourceInterface<StoreType> | null> {
return this._meta.load.then(resource => resource.$post(data))
}

public $patch (data: unknown): Promise<ResourceInterface> {
public $patch (data: unknown): Promise<ResourceInterface<StoreType>> {
return this._meta.load.then(resource => resource.$patch(data))
}

Expand Down
14 changes: 7 additions & 7 deletions src/Resource.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import urltemplate from 'url-template'
import { isTemplatedLink, isVirtualLink, isEntityReference } from './halHelpers'
import ResourceInterface from './interfaces/ResourceInterface'
import ApiActions from './interfaces/ApiActions'
import { StoreData } from './interfaces/StoreData'
import { StoreData, StoreDataEntity } from './interfaces/StoreData'
import ResourceCreator from './ResourceCreator'
import { InternalConfig } from './interfaces/Config'

Expand All @@ -11,11 +11,11 @@ import { InternalConfig } from './interfaces/Config'
* If the storeData has been loaded into the store before but is currently reloading, the old storeData will be
* returned, along with a ._meta.load promise that resolves when the reload is complete.
*/
class Resource<Store extends StoreData> implements ResourceInterface {
class Resource<StoreType, Store extends StoreData<StoreType> = StoreDataEntity<StoreType>> implements ResourceInterface<StoreType> {
public _meta: {
self: string,
selfUrl: string,
load: Promise<ResourceInterface>
load: Promise<ResourceInterface<StoreType>>
loading: boolean
}

Expand Down Expand Up @@ -59,7 +59,7 @@ class Resource<Store extends StoreData> implements ResourceInterface {

// Use a trivial load promise to break endless recursion, except if we are currently reloading the storeData from the API
const loadResource = storeData._meta.reloading
? (storeData._meta.load as Promise<StoreData>).then(reloadedData => resourceCreator.wrap(reloadedData))
? (storeData._meta.load as Promise<Store>).then(reloadedData => resourceCreator.wrap(reloadedData))
: Promise.resolve(this)

// Use a shallow clone of _meta, since we don't want to overwrite the ._meta.load promise or self link in the Vuex store
Expand All @@ -71,15 +71,15 @@ class Resource<Store extends StoreData> implements ResourceInterface {
}
}

$reload (): Promise<ResourceInterface> {
$reload (): Promise<ResourceInterface<StoreType>> {
return this.apiActions.reload(this)
}

$post (data: unknown): Promise<ResourceInterface | null> {
$post (data: unknown): Promise<ResourceInterface<StoreType> | null> {
return this.apiActions.post(this._meta.self, data)
}

$patch (data: unknown): Promise<ResourceInterface> {
$patch (data: unknown): Promise<ResourceInterface<StoreType>> {
return this.apiActions.patch(this._meta.self, data)
}

Expand Down
15 changes: 8 additions & 7 deletions src/ResourceCreator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import { StoreData } from './interfaces/StoreData'
import ResourceInterface from './interfaces/ResourceInterface'
import Collection from './Collection'
import { isCollection } from './halHelpers'
import CollectionInterface from '@/interfaces/CollectionInterface'

class ResourceCreator {
private config: InternalConfig
Expand Down Expand Up @@ -43,28 +44,28 @@ class ResourceCreator {
* @param data entity data from the Vuex store
* @returns object wrapped entity ready for use in a frontend component
*/
wrap (data: StoreData): ResourceInterface {
wrap<StoreType> (data: StoreData<StoreType>): ResourceInterface<StoreType> {
const meta = data._meta || { load: Promise.resolve(), loading: false }

// Resource is loading --> return LoadingResource
if (meta.loading) {
const loadResource = (meta.load as Promise<StoreData>).then(storeData => this.wrapData(storeData))
const loadResource = (meta.load as Promise<StoreData<StoreType>>).then(storeData => this.wrapData<StoreType>(storeData))
return new LoadingResource(loadResource, meta.self, this.config)

// Resource is not loading --> wrap actual data
} else {
return this.wrapData(data)
return this.wrapData<StoreType>(data)
}
}

wrapData (data: StoreData): ResourceInterface {
wrapData<StoreType> (data: StoreData<StoreType>): ResourceInterface<StoreType> | CollectionInterface<StoreType> {
// Store data looks like a collection --> return CollectionInterface
if (isCollection(data)) {
return new Collection(data, this.apiActions, this, this.config) // these parameters are passed to Resource constructor
if (isCollection<StoreType>(data)) {
return new Collection<StoreType>(data, this.apiActions, this, this.config) // these parameters are passed to Resource constructor

// else Store Data looks like an entity --> return normal Resource
} else {
return new Resource(data, this.apiActions, this, this.config)
return new Resource<StoreType>(data, this.apiActions, this, this.config)
}
}
}
Expand Down
6 changes: 3 additions & 3 deletions src/halHelpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -47,16 +47,16 @@ function isVirtualLink (object: keyValueObject): object is VirtualLink {
* @param resource
* @returns boolean true if resource is a VirtualResource
*/
function isVirtualResource (resource: ResourceInterface): resource is VirtualResource {
return (resource as VirtualResource)._storeData?._meta?.virtual
function isVirtualResource<StoreType> (resource: ResourceInterface<StoreType>): resource is VirtualResource<StoreType> {
return (resource as VirtualResource<StoreType>)._storeData?._meta?.virtual as boolean
}

/**
* A standalone collection in the Vuex store has an items property that is an array.
* @param object to be examined
* @returns boolean true if the object looks like a standalone collection, false otherwise
*/
function isCollection (object: keyValueObject): object is StoreDataCollection {
function isCollection<StoreType> (object: keyValueObject): object is StoreDataCollection<StoreType> {
return !!(object && Array.isArray(object.items))
}

Expand Down

0 comments on commit a9255d5

Please sign in to comment.