44 ResourceSnapshot ,
55 ResourceStatus ,
66 Signal ,
7+ computed ,
78 isSignal ,
89 linkedSignal ,
910} from '@angular/core' ;
@@ -14,6 +15,7 @@ import {
1415 signalStoreFeature ,
1516 withComputed ,
1617 withLinkedState ,
18+ withProps ,
1719} from '@ngrx/signals' ;
1820import {
1921 EntityId ,
@@ -103,7 +105,16 @@ export function withEntityResources<
103105> (
104106 resourceFactory : (
105107 store : Input [ 'props' ] & Input [ 'methods' ] & StateSignals < Input [ 'state' ] > ,
106- ) => WidenedResource < TypedEntityResourceValue < Entity > > ,
108+ ) => ResourceRef < TypedEntityResourceValue < Entity > > ,
109+ ) : SignalStoreFeature < Input , EntityResourceRefResult < Entity > > ;
110+
111+ export function withEntityResources <
112+ Input extends SignalStoreFeatureResult ,
113+ Entity extends { id : EntityId } ,
114+ > (
115+ resourceFactory : (
116+ store : Input [ 'props' ] & Input [ 'methods' ] & StateSignals < Input [ 'state' ] > ,
117+ ) => Resource < TypedEntityResourceValue < Entity > > ,
107118) : SignalStoreFeature < Input , EntityResourceResult < Entity > > ;
108119
109120export function withEntityResources <
@@ -131,7 +142,7 @@ export function withEntityResources<
131142 } ) ;
132143
133144 if ( isResourceRef ( resourceOrDict ) ) {
134- return createUnnamedEntityResource ( resourceOrDict ) ( store ) ;
145+ return createUnnamedEntityResourceRef ( resourceOrDict ) ( store ) ;
135146 } else if ( isResource ( resourceOrDict ) ) {
136147 return createUnnamedEntityResource ( resourceOrDict ) ( store ) ;
137148 }
@@ -145,10 +156,8 @@ export function withEntityResources<
145156 * because {@link withResource} creates a Proxy around the resource value
146157 * to avoid the error throwing behavior of the Resource API.
147158 */
148- function createUnnamedEntityResource < E extends Entity > (
149- resource :
150- | ResourceRef < TypedEntityResourceValue < E > >
151- | Resource < TypedEntityResourceValue < E > > ,
159+ function createUnnamedEntityResourceRef < E extends Entity > (
160+ resource : ResourceRef < TypedEntityResourceValue < E > > ,
152161) {
153162 return signalStoreFeature (
154163 withResource ( ( ) => resource ) ,
@@ -182,6 +191,38 @@ function createUnnamedEntityResource<E extends Entity>(
182191 ) ;
183192}
184193
194+ function createUnnamedEntityResource < E extends Entity > (
195+ resource : Resource < TypedEntityResourceValue < E > > ,
196+ ) {
197+ return signalStoreFeature (
198+ withResource ( ( ) => resource ) ,
199+ withProps ( ( store ) => {
200+ const propsStore = store as {
201+ value : Signal < TypedEntityResourceValue < E > > ;
202+ status : Signal < ResourceStatus > ;
203+ error : Signal < Error | undefined > ;
204+ isLoading : Signal < boolean > ;
205+ snapshot : Signal < ResourceSnapshot < TypedEntityResourceValue < E > > > ;
206+ } ;
207+
208+ const resourceValue = propsStore . value as Signal < EntityResourceValue > ;
209+ if ( ! isSignal ( resourceValue ) ) {
210+ throw new Error ( `Resource's value signal does not exist` ) ;
211+ }
212+
213+ const { ids, entityMap } = createReadonlyEntityDerivations ( resourceValue ) ;
214+
215+ return {
216+ ids,
217+ entityMap,
218+ } ;
219+ } ) ,
220+ withComputed ( ( { ids, entityMap } ) => ( {
221+ entities : createComputedEntities ( ids , entityMap ) ,
222+ } ) ) ,
223+ ) ;
224+ }
225+
185226/**
186227 * See {@link createUnnamedEntityResource} for why we cannot use the value of `resource` directly.
187228 */
@@ -190,23 +231,46 @@ function createNamedEntityResources<Dictionary extends EntityDictionary>(
190231) {
191232 const keys = Object . keys ( dictionary ) ;
192233
193- const stateFactories = keys . map ( ( name ) => {
194- return ( store : Record < string , unknown > ) => {
195- const resourceValue = store [
196- `${ name } Value`
197- ] as Signal < EntityResourceValue > ;
198- if ( ! isSignal ( resourceValue ) ) {
199- throw new Error ( `Resource's value ${ name } Value does not exist` ) ;
200- }
234+ const stateFactories = keys
235+ . filter ( ( name ) => isResourceRef ( dictionary [ name ] ) )
236+ . map ( ( name ) => {
237+ return ( store : Record < string , unknown > ) => {
238+ const resourceValue = store [
239+ `${ name } Value`
240+ ] as Signal < EntityResourceValue > ;
241+ if ( ! isSignal ( resourceValue ) ) {
242+ throw new Error ( `Resource's value ${ name } Value does not exist` ) ;
243+ }
201244
202- const { ids, entityMap } = createEntityDerivations ( resourceValue ) ;
245+ const { ids, entityMap } = createEntityDerivations ( resourceValue ) ;
203246
204- return {
205- [ `${ name } EntityMap` ] : entityMap ,
206- [ `${ name } Ids` ] : ids ,
247+ return {
248+ [ `${ name } EntityMap` ] : entityMap ,
249+ [ `${ name } Ids` ] : ids ,
250+ } ;
207251 } ;
208- } ;
209- } ) ;
252+ } ) ;
253+
254+ const propsFactories = keys
255+ . filter ( ( name ) => ! isResourceRef ( dictionary [ name ] ) )
256+ . map ( ( name ) => {
257+ return ( store : Record < string , unknown > ) => {
258+ const resourceValue = store [
259+ `${ name } Value`
260+ ] as Signal < EntityResourceValue > ;
261+ if ( ! isSignal ( resourceValue ) ) {
262+ throw new Error ( `Resource's value ${ name } Value does not exist` ) ;
263+ }
264+
265+ const { ids, entityMap } =
266+ createReadonlyEntityDerivations ( resourceValue ) ;
267+
268+ return {
269+ [ `${ name } EntityMap` ] : entityMap ,
270+ [ `${ name } Ids` ] : ids ,
271+ } ;
272+ } ;
273+ } ) ;
210274
211275 const computedFactories = keys . map ( ( name ) => {
212276 return ( store : Record < string , unknown > ) => {
@@ -234,13 +298,28 @@ function createNamedEntityResources<Dictionary extends EntityDictionary>(
234298 withResource ( ( ) => dictionary ) ,
235299 withLinkedState ( ( store ) =>
236300 stateFactories . reduce (
237- ( acc , factory ) => ( { ...acc , ...factory ( store ) } ) ,
301+ ( acc , factory ) => ( {
302+ ...acc ,
303+ ...factory ( store ) ,
304+ } ) ,
305+ { } ,
306+ ) ,
307+ ) ,
308+ withProps ( ( store ) =>
309+ propsFactories . reduce (
310+ ( acc , factory ) => ( {
311+ ...acc ,
312+ ...factory ( store ) ,
313+ } ) ,
238314 { } ,
239315 ) ,
240316 ) ,
241317 withComputed ( ( store ) =>
242318 computedFactories . reduce (
243- ( acc , factory ) => ( { ...acc , ...factory ( store ) } ) ,
319+ ( acc , factory ) => ( {
320+ ...acc ,
321+ ...factory ( store ) ,
322+ } ) ,
244323 { } ,
245324 ) ,
246325 ) ,
@@ -268,12 +347,37 @@ function createNamedEntityResources<Dictionary extends EntityDictionary>(
268347 * - For named resources we return `NamedResourceResult<T>` intersected with
269348 * `NamedEntityState<E, Name>` and `NamedEntityProps<E, Name>` for each entry.
270349 */
271- export type EntityResourceResult < Entity > = {
350+ declare const NON_PATCHABLE_ENTITY_RESOURCE_STATE : unique symbol ;
351+
352+ type NonPatchableEntityResourceStateMarker = {
353+ [ NON_PATCHABLE_ENTITY_RESOURCE_STATE ] ?: never ;
354+ } ;
355+
356+ type ReadonlyEntityProps < E extends Entity > = {
357+ ids : Signal < EntityId [ ] > ;
358+ entityMap : Signal < Record < EntityId , E > > ;
359+ } ;
360+
361+ type NamedReadonlyEntityProps < E extends Entity , Name extends string > = {
362+ [ Prop in `${Name } Ids`] : Signal < EntityId [ ] > ;
363+ } & {
364+ [ Prop in `${Name } EntityMap`] : Signal < Record < EntityId , E > > ;
365+ } ;
366+
367+ export type EntityResourceRefResult < Entity > = {
272368 state : ResourceResult < Entity > [ 'state' ] & EntityState < Entity > ;
273369 props : ResourceResult < Entity > [ 'props' ] & EntityProps < Entity > ;
274370 methods : ResourceResult < Entity > [ 'methods' ] ;
275371} ;
276372
373+ export type EntityResourceResult < Entity extends { id : EntityId } > = {
374+ state : NonPatchableEntityResourceStateMarker ;
375+ props : ResourceResult < Entity > [ 'props' ] &
376+ EntityProps < Entity > &
377+ ReadonlyEntityProps < Entity > ;
378+ methods : ResourceResult < Entity > [ 'methods' ] ;
379+ } ;
380+
277381// Generic helpers for inferring entity types and merging unions
278382type ArrayElement < T > = T extends readonly ( infer E ) [ ] | ( infer E ) [ ] ? E : never ;
279383
@@ -300,15 +404,33 @@ export type EntityDictionary = Record<
300404type MergeNamedEntityStates < T extends EntityDictionary > = MergeUnion <
301405 {
302406 [ Prop in keyof T ] : Prop extends string
303- ? InferEntityFromSignal < T [ Prop ] [ 'value' ] > extends infer E
304- ? E extends never
305- ? never
306- : NamedEntityState < E , Prop >
407+ ? T [ Prop ] extends ResourceRef < unknown >
408+ ? InferEntityFromSignal < T [ Prop ] [ 'value' ] > extends infer E
409+ ? E extends never
410+ ? never
411+ : NamedEntityState < E , Prop >
412+ : never
307413 : never
308414 : never ;
309415 } [ keyof T ]
310416> ;
311417
418+ type MergeNamedReadonlyEntityProps < T extends EntityDictionary > = MergeUnion <
419+ {
420+ [ Prop in keyof T ] : Prop extends string
421+ ? T [ Prop ] extends ResourceRef < unknown >
422+ ? never
423+ : InferEntityFromSignal < T [ Prop ] [ 'value' ] > extends infer E
424+ ? E extends Entity
425+ ? NamedReadonlyEntityProps < E , Prop >
426+ : E extends never
427+ ? never
428+ : never
429+ : never
430+ : never ;
431+ } [ keyof T ]
432+ > ;
433+
312434type MergeNamedEntityProps < T extends EntityDictionary > = MergeUnion <
313435 {
314436 [ Prop in keyof T ] : Prop extends string
@@ -322,8 +444,12 @@ type MergeNamedEntityProps<T extends EntityDictionary> = MergeUnion<
322444> ;
323445
324446export type NamedEntityResourceResult < T extends EntityDictionary > = {
325- state : NamedResourceResult < T , false > [ 'state' ] & MergeNamedEntityStates < T > ;
326- props : NamedResourceResult < T , false > [ 'props' ] & MergeNamedEntityProps < T > ;
447+ state : NamedResourceResult < T , false > [ 'state' ] &
448+ MergeNamedEntityStates < T > &
449+ NonPatchableEntityResourceStateMarker ;
450+ props : NamedResourceResult < T , false > [ 'props' ] &
451+ MergeNamedEntityProps < T > &
452+ MergeNamedReadonlyEntityProps < T > ;
327453 methods : NamedResourceResult < T , false > [ 'methods' ] ;
328454} ;
329455
@@ -374,6 +500,22 @@ function createEntityDerivations<E extends Entity>(
374500 return { ids, entityMap } ;
375501}
376502
503+ function createReadonlyEntityDerivations < E extends Entity > (
504+ source : Signal < TypedEntityResourceValue < E > > ,
505+ ) {
506+ const ids = computed ( ( ) => ( source ( ) ?? [ ] ) . map ( ( e ) => e . id ) ) ;
507+
508+ const entityMap = computed ( ( ) => {
509+ const map = { } as Record < EntityId , E > ;
510+ for ( const item of source ( ) ?? [ ] ) {
511+ map [ item . id ] = item as E ;
512+ }
513+ return map ;
514+ } ) ;
515+
516+ return { ids, entityMap } ;
517+ }
518+
377519function createComputedEntities < E extends Entity > (
378520 ids : Signal < EntityId [ ] > ,
379521 entityMap : Signal < Record < EntityId , E > > ,
0 commit comments