Skip to content

Commit ad6ec9d

Browse files
committed
refactor reload; allow reload of virtual link via string
1 parent 5305a5d commit ad6ec9d

File tree

6 files changed

+51
-40
lines changed

6 files changed

+51
-40
lines changed

src/Resource.ts

Lines changed: 4 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
import urltemplate from 'url-template'
2-
import { isTemplatedLink, isVirtualLink, isEntityReference } from './halHelpers'
2+
import { isTemplatedLink, isVirtualLink, isEntityReference, isVirtualResource } from './halHelpers'
33
import ResourceInterface from './interfaces/ResourceInterface'
44
import ApiActions from './interfaces/ApiActions'
55
import { StoreData } from './interfaces/StoreData'
@@ -74,23 +74,23 @@ class Resource implements ResourceInterface {
7474
}
7575

7676
$post (data: unknown): Promise<ResourceInterface | null> {
77-
if (this.isVirtual()) {
77+
if (isVirtualResource(this)) {
7878
throw new Error('$post is not implemented for virtual resources')
7979
}
8080

8181
return this.apiActions.post(this._meta.self, data)
8282
}
8383

8484
$patch (data: unknown): Promise<ResourceInterface> {
85-
if (this.isVirtual()) {
85+
if (isVirtualResource(this)) {
8686
throw new Error('$patch is not implemented for virtual resources')
8787
}
8888

8989
return this.apiActions.patch(this._meta.self, data)
9090
}
9191

9292
$del (): Promise<string | void> {
93-
if (this.isVirtual()) {
93+
if (isVirtualResource(this)) {
9494
throw new Error('$del is not implemented for virtual resources')
9595
}
9696

@@ -110,14 +110,6 @@ class Resource implements ResourceInterface {
110110
// alternatively: could also return '{}', as the data cannot be used directly, anyway
111111
return JSON.stringify(this._storeData)
112112
}
113-
114-
/**
115-
* returns true, if resource is only virtual generated an not an actual resource on the API
116-
*/
117-
private isVirtual (): boolean {
118-
const meta = this._storeData._meta
119-
return 'virtual' in meta && meta.virtual
120-
}
121113
}
122114

123115
export default Resource

src/halHelpers.ts

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { Link, VirtualLink, TemplatedLink, StoreDataCollection } from './interfaces/StoreData'
2+
import { ResourceInterface, VirtualResource } from './interfaces/ResourceInterface'
23

34
type keyValueObject = Record<string, unknown>
45

@@ -40,6 +41,16 @@ function isVirtualLink (object: keyValueObject): object is VirtualLink {
4041
return isEqualIgnoringOrder(Object.keys(object), ['href', 'virtual']) && (object.virtual === true)
4142
}
4243

44+
/**
45+
* A virtual resource contains a generated, virtual self link which points to another store key but
46+
* doesn't correspond to an actual resource on the API. Such resources have the _meta.virtual flag set to true.
47+
* @param resource
48+
* @returns boolean true if resource is a VirtualResource
49+
*/
50+
function isVirtualResource (resource: ResourceInterface): resource is VirtualResource {
51+
return (resource as VirtualResource)._storeData?._meta?.virtual
52+
}
53+
4354
/**
4455
* A standalone collection in the Vuex store has an items property that is an array.
4556
* @param object to be examined
@@ -49,4 +60,4 @@ function isCollection (object: keyValueObject): object is StoreDataCollection {
4960
return !!(object && Array.isArray(object.items))
5061
}
5162

52-
export { isTemplatedLink, isVirtualLink, isEntityReference, isCollection }
63+
export { isTemplatedLink, isVirtualLink, isEntityReference, isCollection, isVirtualResource }

src/index.ts

Lines changed: 13 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import { AxiosInstance, AxiosError } from 'axios'
1212
import ResourceInterface from './interfaces/ResourceInterface'
1313
import StoreData, { Link, SerializablePromise } from './interfaces/StoreData'
1414
import ApiActions from './interfaces/ApiActions'
15+
import { isVirtualResource } from './halHelpers'
1516

1617
/**
1718
* Defines the API store methods available in all Vue components. The methods can be called as follows:
@@ -132,19 +133,23 @@ function HalJsonVuex (store: Store<Record<string, State>>, axios: AxiosInstance,
132133
* in the Vuex store.
133134
*/
134135
async function reload (uriOrEntity: string | ResourceInterface): Promise<ResourceInterface> {
135-
if (uriOrEntity instanceof Resource && 'virtual' in uriOrEntity._storeData._meta && uriOrEntity._storeData._meta.virtual) {
136+
let resource: ResourceInterface
137+
138+
if (typeof uriOrEntity === 'string') {
139+
resource = get(uriOrEntity)
140+
} else {
141+
resource = uriOrEntity
142+
}
143+
144+
if (isVirtualResource(resource)) {
136145
// For embedded collections which had to reload the parent entity, unwrap the embedded collection after loading has finished
137-
const { owningResource, owningRelation } = uriOrEntity._storeData._meta
146+
const { owningResource, owningRelation } = resource._storeData._meta
138147
return reload(owningResource).then(owner => owner[owningRelation]())
139148
}
140149

141-
const uri = normalizeEntityUri(uriOrEntity, axios.defaults.baseURL)
150+
const uri = normalizeEntityUri(resource, axios.defaults.baseURL)
142151

143152
if (uri === null) {
144-
if (uriOrEntity instanceof LoadingResource) {
145-
// A LoadingResource is safe to return without breaking the UI.
146-
return uriOrEntity
147-
}
148153
// We don't know anything about the requested object, something is wrong.
149154
throw new Error(`Could not perform reload, "${uriOrEntity}" is not an entity or URI`)
150155
}
@@ -159,16 +164,6 @@ function HalJsonVuex (store: Store<Record<string, State>>, axios: AxiosInstance,
159164
return loadPromise.then(storeData => resourceCreator.wrap(storeData))
160165
}
161166

162-
async function reloadResourceFromStoreData (storeData: StoreData) {
163-
if ('virtual' in storeData._meta && storeData._meta.virtual) {
164-
const { owningResource, owningRelation } = storeData._meta
165-
166-
return reload(owningResource).then(owner => owner[owningRelation]())
167-
}
168-
169-
return reload(storeData._meta.self)
170-
}
171-
172167
/**
173168
* Returns true if uri doesn't exist in store (never loaded before)
174169
* @param uri
@@ -367,7 +362,7 @@ function HalJsonVuex (store: Store<Record<string, State>>, axios: AxiosInstance,
367362
.filter(outdatedEntity => !outdatedEntity._meta.deleting)
368363

369364
// reload outdated entities...
370-
.map(outdatedEntity => reloadResourceFromStoreData(outdatedEntity).catch(() => {
365+
.map(outdatedEntity => reload(outdatedEntity._meta.self).catch(() => {
371366
// ...but ignore any errors (such as 404 errors during reloading)
372367
// handleAxiosError will take care of recursively deleting cascade-deleted entities
373368
}))

src/interfaces/ResourceInterface.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import StoreData from './StoreData'
1+
import { StoreData, VirtualStoreData } from './StoreData'
22

33
/**
44
* Generic interface for a standalone ResourceInterface (e.g. a HAl resource with an own store entry and a self link)
@@ -12,7 +12,7 @@ interface ResourceInterface {
1212
deleting?: boolean
1313
}
1414

15-
_storeData?: StoreData
15+
_storeData?: StoreData // optional, because LoadingResource has no _storeData
1616

1717
$reload: () => Promise<ResourceInterface>
1818
$post: (data: unknown) => Promise<ResourceInterface | null>
@@ -21,5 +21,9 @@ interface ResourceInterface {
2121
$href: (relation: string, templateParams: Record<string, string | number | boolean>) => Promise<string | undefined>
2222
}
2323

24-
export { ResourceInterface }
24+
interface VirtualResource extends ResourceInterface {
25+
_storeData: VirtualStoreData
26+
}
27+
28+
export { ResourceInterface, VirtualResource }
2529
export default ResourceInterface

src/interfaces/StoreData.ts

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,14 +31,14 @@ type VirtualStoreDataMeta = StoreDataMeta & {
3131
}
3232
}
3333

34-
type StoreDataEntity = (StoreDataMeta | VirtualStoreDataMeta) & {
34+
type StoreDataEntity = StoreDataMeta & {
3535
items: never,
3636
_meta: {
3737
load: SerializablePromise<StoreDataEntity>
3838
}
3939
}
4040

41-
type StoreDataCollection = (StoreDataMeta | VirtualStoreDataMeta) & {
41+
type StoreDataCollection = StoreDataMeta & {
4242
items: Array<Link>,
4343
_meta: {
4444
load: SerializablePromise<StoreDataCollection>
@@ -47,6 +47,8 @@ type StoreDataCollection = (StoreDataMeta | VirtualStoreDataMeta) & {
4747

4848
type StoreData = StoreDataEntity | StoreDataCollection
4949

50-
export { StoreData, Link, VirtualLink, TemplatedLink, StoreDataEntity, StoreDataCollection, SerializablePromise }
50+
type VirtualStoreData = StoreData & VirtualStoreDataMeta
51+
52+
export { StoreData, VirtualStoreData, Link, VirtualLink, TemplatedLink, StoreDataEntity, StoreDataCollection, SerializablePromise }
5153

5254
export default StoreData

src/normalizeEntityUri.ts

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -36,9 +36,16 @@ function sortQueryParams (uri: string): string {
3636
* @param baseUrl common URI prefix to remove during normalization
3737
* @returns {null|string} normalized URI, or null if the uriOrEntity argument was not understood
3838
*/
39-
function normalizeEntityUri (uriOrEntity: string | ResourceInterface | StoreData = '', baseUrl = ''): string | null {
40-
if (typeof uriOrEntity === 'string') return normalizeUri(uriOrEntity, baseUrl)
41-
return normalizeUri(((uriOrEntity || {})._meta || {}).self, baseUrl)
39+
function normalizeEntityUri (uriOrEntity: string | ResourceInterface | null = '', baseUrl = ''): string | null {
40+
let uri
41+
42+
if (typeof uriOrEntity === 'string') {
43+
uri = uriOrEntity
44+
} else {
45+
uri = uriOrEntity?._meta?.self
46+
}
47+
48+
return normalizeUri(uri, baseUrl)
4249
}
4350

4451
/**

0 commit comments

Comments
 (0)