signalkin 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,401 @@
1
+ import { Signal } from '@angular/core';
2
+ import { SignalStoreFeatureResult, SignalStoreFeature } from '@ngrx/signals';
3
+ import { EntityId, SelectEntityId, EntityMap, EntityChanges } from '@ngrx/signals/entities';
4
+
5
+ /**
6
+ * How many entities of one side relate to a single entity of the other side.
7
+ * A literal `1` means single; any other number (e.g. `2`) or `'many'` means multiple.
8
+ * Pass a literal so the generated key names and return shapes resolve correctly.
9
+ */
10
+ type Cardinality = number | 'many';
11
+ type IsSingle<Count extends Cardinality> = Count extends 1 ? true : false;
12
+ /** True when a side relates to MORE THAN ONE of the other (any number >= 2, or `'many'`) — i.e. not `1`. */
13
+ type IsMulti<Count extends Cardinality> = IsSingle<Count> extends true ? false : true;
14
+ /**
15
+ * True when a side's count is a FIXED number of 2+ (not `1`, not `'many'`). A fixed-size set of that
16
+ * side's ids acts as a composite key that pins one of the other side, so a lookup keyed by it is single.
17
+ */
18
+ type IsFixedCount<Count extends Cardinality> = Count extends 'many' ? false : Count extends 1 ? false : true;
19
+ /** A single entity for a 1-side (or `undefined` when the link is missing), an array for a many-side. */
20
+ type Related<Entity, Count extends Cardinality> = IsSingle<Count> extends true ? Entity | undefined : Entity[];
21
+ /** Leading noun of a generated key: the bare name for a 1-side, the (given or default `+s`) plural for a many-side. */
22
+ type Lead<Name extends string, Plural extends string, Count extends Cardinality> = IsSingle<Count> extends true ? Name : Plural;
23
+ /** The id-returning noun for a side: `${Name}Id` for a 1-side, `${Name}Ids` for a many/number-side. */
24
+ type IdLead<Name extends string, Count extends Cardinality> = IsSingle<Count> extends true ? `${Name}Id` : `${Name}Ids`;
25
+ /**
26
+ * Whether a composite lookup keyed by a SET of one side's ids returns a single entity. It does when the
27
+ * set is a candidate key — the keyed (input) side has a fixed count of 2+ — OR when only one of the
28
+ * returned side can ever match (the returned side is a 1-side). Otherwise more than one may match, so
29
+ * the lookup returns an array.
30
+ */
31
+ type CompositeSingle<InputCount extends Cardinality, ReturnCount extends Cardinality> = IsFixedCount<InputCount> extends true ? true : IsSingle<ReturnCount>;
32
+ /** A single entity when the composite lookup is single, else an array of all matches. */
33
+ type CompositeResult<Entity, InputCount extends Cardinality, ReturnCount extends Cardinality> = CompositeSingle<InputCount, ReturnCount> extends true ? Entity | undefined : Entity[];
34
+ /** A single id when the composite lookup is single, else an array of all matching ids. */
35
+ type CompositeIdResult<Id extends EntityId, InputCount extends Cardinality, ReturnCount extends Cardinality> = CompositeSingle<InputCount, ReturnCount> extends true ? Id | undefined : Id[];
36
+ /** The leading noun of a composite lookup's name: singular when it returns one, plural when it returns an array. */
37
+ type CompositeLead<Name extends string, Plural extends string, InputCount extends Cardinality, ReturnCount extends Cardinality> = CompositeSingle<InputCount, ReturnCount> extends true ? Name : Plural;
38
+ /** The id-returning noun of a composite lookup's name: `${Name}Id` when single, `${Name}Ids` when an array. */
39
+ type CompositeIdLead<Name extends string, InputCount extends Cardinality, ReturnCount extends Cardinality> = CompositeSingle<InputCount, ReturnCount> extends true ? `${Name}Id` : `${Name}Ids`;
40
+ /**
41
+ * The state a feature REQUIRES: the named collections' entity maps must already exist. Intersected
42
+ * into a feature's `Input` so `signalStore` refuses to compile it unless placed AFTER those
43
+ * `withEntities` calls (ordering enforcement), and so the real entity types can be read back out.
44
+ */
45
+ type RequiredEntityMaps<Names extends string> = {
46
+ state: Record<`${Names}EntityMap`, Record<EntityId, unknown>>;
47
+ };
48
+ /** {@link RequiredEntityMaps} for a relationship's two sides. */
49
+ type RequiredEntityStore<FromName extends string, ToName extends string> = RequiredEntityMaps<FromName | ToName>;
50
+ /** The collection names present on a store's state, read off its `${name}EntityMap` keys. */
51
+ type CollectionNames<State> = {
52
+ [Key in keyof State & string]: Key extends `${infer Name}EntityMap` ? Name : never;
53
+ }[keyof State & string];
54
+ /**
55
+ * Extracts a collection's entity type from the inferred input store, by collection name — e.g.
56
+ * `EntityOf<Store, 'book'>` reads `Store['state']['bookEntityMap']` and yields `Book`.
57
+ * Reads the RAW state map (`Record<EntityId, Entity>`), which is what lets a caller pass only the name.
58
+ */
59
+ type EntityOf<Store, Name extends string> = Store extends {
60
+ state: infer State;
61
+ } ? State extends Record<`${Name}EntityMap`, Record<EntityId, infer Entity>> ? Entity : never : never;
62
+ /** A test an entity either passes or fails — used to pick which entities a bulk update/remove touches. */
63
+ type EntityPredicate<Entity> = (entity: Entity) => boolean;
64
+ /**
65
+ * A property name to read, or a getter that returns an id — mirrors ngrx `selectId`, but a key string
66
+ * also works. Either form carries a branded id like `OptionId` into the generated lookups: a typed
67
+ * getter (e.g. `(entity) => entity.id2`) through its return type, a key (e.g. `'id2'`) through the
68
+ * property type it names. Omitting it falls back to `EntityId`.
69
+ */
70
+ type IdAccessor<Id extends EntityId = EntityId> = string | ((entity: any) => Id);
71
+ /**
72
+ * The id type a `selectId` accessor yields: a getter's return type, or the type of the property a key
73
+ * string names, so a branded id like `OptionId` flows into the generated lookups. Falls back to
74
+ * `EntityId` when no accessor is given, when a getter is untyped, or when a key does not resolve to an
75
+ * id. A key resolves against `Entity`, which is why the accessor's own type is captured and passed in —
76
+ * ngrx widens `selectId` to `EntityId` on an `entityConfig`, so re-reading the property is what keeps
77
+ * the brand when a config is spread into a relationship side.
78
+ */
79
+ type IdOf<Accessor, Entity = unknown> = EntityId & (Accessor extends (entity: any) => infer Id ? Id : Accessor extends keyof Entity ? Entity[Accessor] : EntityId);
80
+ /**
81
+ * Like {@link IdAccessor}, but for a foreign key that may be absent or plural — a getter may return a
82
+ * single id, an array of ids (for a side that links to more than one of the other), or `undefined`.
83
+ */
84
+ type ForeignIdAccessor = string | ((entity: any) => EntityId | EntityId[] | undefined);
85
+ /**
86
+ * One side of a relationship: the `collection` name, how many of it relate to one of the other side,
87
+ * and how its ids are read. The shape matches an `entityConfig(...)` result (`collection`, `selectId`),
88
+ * so `{ ...bookConfig, count: 'many' }` works — you only add the relationship-specific `count` (and
89
+ * optionally `plural` / `selectForeignId`).
90
+ */
91
+ type RelationshipSide<Name extends string, Count extends Cardinality> = {
92
+ /** The ngrx collection name; the entity type is inferred from the store by this name. */
93
+ collection: Name;
94
+ /** How many of this side relate to a single entity of the other side. */
95
+ count: Count;
96
+ /** The plural of `collection`, used in every generated member that pluralizes it. Default: `` `${collection}s` ``. */
97
+ plural?: string;
98
+ /** How to read THIS entity's own id; keep it matching the collection's `selectId`. Default: the `'id'` property. */
99
+ selectId?: IdAccessor;
100
+ /**
101
+ * How to read THIS entity's foreign key to the other side — set ONLY if this entity actually stores it.
102
+ * Default key: `` `${otherName}Id` `` when this side links to one of the other, `` `${otherName}Ids` ``
103
+ * (an array, e.g. a book's `aisleIds`) when it links to more than one.
104
+ */
105
+ selectForeignId?: ForeignIdAccessor;
106
+ };
107
+ /** Flags to turn optional behavior on or off. */
108
+ type RelationshipOptions = {
109
+ /** Auto-populate the index from foreign keys as entities change. Default: `true`. Set `false` for explicit links only. */
110
+ autoIndex?: boolean;
111
+ /**
112
+ * Let `remove`/`move` mutations and the ordering methods (`swap`/`reorder`/`move…ToIndex`) write the
113
+ * entities' foreign keys, so a change sticks for FK-derived links: `removeBookFromLibrary` clears the
114
+ * book's `libraryId`, `move` re-points it, and reordering an owner's members writes the new order into
115
+ * the owner's own array key (e.g. `cell.optionIds`). Default: `true`. Set `false` to never write
116
+ * entities (the FK stays the source of truth and those operations affect manual links only). Only works
117
+ * on a side whose `selectForeignId` is a property NAME (or defaulted) — a getter can't be written back,
118
+ * so getter sides are skipped; and reordering only writes a key the owner actually holds as an array.
119
+ */
120
+ syncForeignKeys?: boolean;
121
+ /**
122
+ * Generate the composite set-lookups (`bookByRows(...)` / `authorByBooks(...)`) for each side that
123
+ * relates to more than one of the other. Default: `true`. Set `false` to skip building those indexes.
124
+ */
125
+ compositeLookups?: boolean;
126
+ };
127
+
128
+ /** What withEntityAccessors needs from a config: the collection name, its `selectId` if not the `id` property, and its `plural` if not `+s`. An `entityConfig(...)` result works as-is. */
129
+ type EntityAccessorConfig = {
130
+ entity?: unknown;
131
+ collection: string;
132
+ selectId?: SelectEntityId<any>;
133
+ plural?: string;
134
+ };
135
+ /** The collections a call targets: the passed configs' collections, or EVERY collection on the store when none are passed. */
136
+ type TargetNames<Store extends SignalStoreFeatureResult, Configs extends readonly EntityAccessorConfig[]> = Configs extends readonly [] ? CollectionNames<Store['state']> : Configs[number]['collection'];
137
+ /** A collection's plural, from its passed config if one names it, else `+s`. */
138
+ type PluralOf<Configs extends readonly EntityAccessorConfig[], Name extends string> = [
139
+ Extract<Configs[number], {
140
+ collection: Name;
141
+ }>
142
+ ] extends [never] ? `${Name}s` : Extract<Configs[number], {
143
+ collection: Name;
144
+ }> extends {
145
+ plural: infer Plural extends string;
146
+ } ? Plural : `${Name}s`;
147
+ /** Convenience signals added alongside ngrx's own `${collection}EntityMap` / `${collection}Ids` / `${collection}Entities`. */
148
+ type EntityAccessorProps<Store, Configs extends readonly EntityAccessorConfig[], Names extends string> = {
149
+ [Name in Names as PluralOf<Configs, Name>]: Signal<EntityOf<Store, Name>[]>;
150
+ } & {
151
+ [Name in Names as `${Name}Map`]: Signal<EntityMap<EntityOf<Store, Name>>>;
152
+ } & {
153
+ [Name in Names as `${Name}Count`]: Signal<number>;
154
+ };
155
+ /** The standard collection methods, one per ngrx updater plus lookups, named by collection. */
156
+ type EntityAccessorMethods<Store, Configs extends readonly EntityAccessorConfig[], Names extends string> = {
157
+ [Name in Names as `add${Capitalize<Name>}`]: (entity: EntityOf<Store, Name>) => void;
158
+ } & {
159
+ [Name in Names as `add${Capitalize<PluralOf<Configs, Name>>}`]: (entities: EntityOf<Store, Name>[]) => void;
160
+ } & {
161
+ [Name in Names as `prepend${Capitalize<Name>}`]: (entity: EntityOf<Store, Name>) => void;
162
+ } & {
163
+ [Name in Names as `prepend${Capitalize<PluralOf<Configs, Name>>}`]: (entities: EntityOf<Store, Name>[]) => void;
164
+ } & {
165
+ [Name in Names as `set${Capitalize<Name>}`]: (entity: EntityOf<Store, Name>) => void;
166
+ } & {
167
+ [Name in Names as `set${Capitalize<PluralOf<Configs, Name>>}`]: (entities: EntityOf<Store, Name>[]) => void;
168
+ } & {
169
+ [Name in Names as `setAll${Capitalize<PluralOf<Configs, Name>>}`]: (entities: EntityOf<Store, Name>[]) => void;
170
+ } & {
171
+ [Name in Names as `upsert${Capitalize<Name>}`]: (entity: EntityOf<Store, Name>) => void;
172
+ } & {
173
+ [Name in Names as `upsert${Capitalize<PluralOf<Configs, Name>>}`]: (entities: EntityOf<Store, Name>[]) => void;
174
+ } & {
175
+ [Name in Names as `update${Capitalize<Name>}`]: (entity: EntityOf<Store, Name> | EntityId, changes: EntityChanges<EntityOf<Store, Name>>) => void;
176
+ } & {
177
+ [Name in Names as `update${Capitalize<PluralOf<Configs, Name>>}`]: (entitiesOrPredicate: (EntityOf<Store, Name> | EntityId)[] | EntityPredicate<EntityOf<Store, Name>>, changes: EntityChanges<EntityOf<Store, Name>>) => void;
178
+ } & {
179
+ [Name in Names as `updateAll${Capitalize<PluralOf<Configs, Name>>}`]: (changes: EntityChanges<EntityOf<Store, Name>>) => void;
180
+ } & {
181
+ [Name in Names as `remove${Capitalize<Name>}`]: (entity: EntityOf<Store, Name> | EntityId) => void;
182
+ } & {
183
+ [Name in Names as `remove${Capitalize<PluralOf<Configs, Name>>}`]: (entitiesOrPredicate: (EntityOf<Store, Name> | EntityId)[] | EntityPredicate<EntityOf<Store, Name>>) => void;
184
+ } & {
185
+ [Name in Names as `removeAll${Capitalize<PluralOf<Configs, Name>>}`]: () => void;
186
+ } & {
187
+ [Name in Names as `swap${Capitalize<PluralOf<Configs, Name>>}`]: (entityA: EntityOf<Store, Name> | EntityId, entityB: EntityOf<Store, Name> | EntityId) => void;
188
+ } & {
189
+ [Name in Names as `reorder${Capitalize<PluralOf<Configs, Name>>}`]: (orderedEntities: (EntityOf<Store, Name> | EntityId)[]) => void;
190
+ } & {
191
+ [Name in Names as `move${Capitalize<Name>}ToIndex`]: (entity: EntityOf<Store, Name> | EntityId, index: number) => void;
192
+ } & {
193
+ [Name in Names as `${Name}ById`]: (id: EntityId) => EntityOf<Store, Name> | undefined;
194
+ } & {
195
+ [Name in Names as `has${Capitalize<Name>}`]: (entity: EntityOf<Store, Name> | EntityId) => boolean;
196
+ };
197
+ type EntityAccessorsOutput<Store, Configs extends readonly EntityAccessorConfig[], Names extends string> = {
198
+ state: {};
199
+ props: EntityAccessorProps<Store, Configs, Names>;
200
+ methods: EntityAccessorMethods<Store, Configs, Names>;
201
+ };
202
+ /**
203
+ * Standard collection methods and convenience signals for the store's entities, named by collection — no args covers every collection, or pass configs to narrow and to honor a custom `selectId` / `plural`.
204
+ *
205
+ * @example
206
+ * signalStore(
207
+ * withEntities(bookConfig),
208
+ * withEntityAccessors(bookConfig),
209
+ * );
210
+ * // books(), bookMap(), bookCount(), bookById(id), hasBook(book | id)
211
+ * // addBook, addBooks, prependBook, setBook, setAllBooks, upsertBook
212
+ * // updateBook(book | id, changes), updateAllBooks(changes), removeBook(book | id), removeAllBooks()
213
+ * // swapBooks(a, b), reorderBooks(orderedBooks), moveBookToIndex(book | id, index) reorder the collection
214
+ */
215
+ declare const withEntityAccessors: <const Configs extends readonly EntityAccessorConfig[], Store extends SignalStoreFeatureResult & RequiredEntityMaps<Configs[number]["collection"]>>(...configs: Configs) => SignalStoreFeature<Store, EntityAccessorsOutput<Store, Configs, TargetNames<Store, Configs> & string>>;
216
+
217
+ type LinkState<FromName extends string, ToName extends string> = {
218
+ [Key in `_${FromName}${Capitalize<ToName>}Links`]: Record<EntityId, EntityId[]>;
219
+ } & {
220
+ [Key in `_${ToName}${Capitalize<FromName>}Links`]: Record<EntityId, EntityId[]>;
221
+ };
222
+ type IndexProps<FromName extends string, ToName extends string> = {
223
+ [Key in `${FromName}${Capitalize<ToName>}Map`]: Signal<Record<EntityId, EntityId[]>>;
224
+ } & {
225
+ [Key in `${ToName}${Capitalize<FromName>}Map`]: Signal<Record<EntityId, EntityId[]>>;
226
+ };
227
+ /**
228
+ * The two get-by lookups for one direction: return the related entity(ies) or their id(s), keyed by
229
+ * ONE entity of the other side — passed as the entity OR its id. The returned noun pluralizes by the
230
+ * returned side's own count; the shape is a single value for a 1-side, an array otherwise.
231
+ */
232
+ type DirectionalLookup<ReturnEntity, ReturnName extends string, ReturnPlural extends string, ReturnCount extends Cardinality, ReturnId extends EntityId, InputEntity, InputName extends string> = {
233
+ [Key in `${Lead<ReturnName, ReturnPlural, ReturnCount>}By${Capitalize<InputName>}`]: (input: InputEntity | EntityId) => Related<ReturnEntity, ReturnCount>;
234
+ } & {
235
+ [Key in `${IdLead<ReturnName, ReturnCount>}By${Capitalize<InputName>}`]: (input: InputEntity | EntityId) => Related<ReturnId, ReturnCount>;
236
+ };
237
+ /** Existence checks and counts across the link, both directions. Each entity argument accepts the entity or its id. */
238
+ type HasCountMethods<FromEntity, FromName extends string, ToEntity, ToName extends string> = {
239
+ [Key in `${FromName}Has${Capitalize<ToName>}`]: (from: FromEntity | EntityId, to: ToEntity | EntityId) => boolean;
240
+ } & {
241
+ [Key in `${ToName}Has${Capitalize<FromName>}`]: (to: ToEntity | EntityId, from: FromEntity | EntityId) => boolean;
242
+ } & {
243
+ [Key in `${ToName}CountBy${Capitalize<FromName>}`]: (from: FromEntity | EntityId) => number;
244
+ } & {
245
+ [Key in `${FromName}CountBy${Capitalize<ToName>}`]: (to: ToEntity | EntityId) => number;
246
+ };
247
+ type LookupMethods<FromEntity, FromName extends string, FromPlural extends string, FromCount extends Cardinality, FromId extends EntityId, ToEntity, ToName extends string, ToPlural extends string, ToCount extends Cardinality, ToId extends EntityId> = DirectionalLookup<ToEntity, ToName, ToPlural, ToCount, ToId, FromEntity, FromName> & DirectionalLookup<FromEntity, FromName, FromPlural, FromCount, FromId, ToEntity, ToName> & HasCountMethods<FromEntity, FromName, ToEntity, ToName>;
248
+ /**
249
+ * When the INPUT side relates to MORE THAN ONE of the other (count is a number 2+ or `'many'`), the
250
+ * SET of that side's ids for a given other-side entity can be used as a key. This generates the
251
+ * variadic set-lookups: pass those entities or ids (any order), get the matching other-side
252
+ * entity(ies) or id(s). The result is a single value when the set is a candidate key (the keyed side
253
+ * has a fixed count 2+) or only one of the returned side can match (returned side is a 1-side);
254
+ * otherwise it's an array of every match. The method name goes singular / plural to match.
255
+ */
256
+ type CompositeSetLookup<ReturnEntity, ReturnName extends string, ReturnPlural extends string, ReturnId extends EntityId, ReturnCount extends Cardinality, InputEntity, InputPlural extends string, InputCount extends Cardinality> = IsMulti<InputCount> extends true ? {
257
+ [Key in `${CompositeLead<ReturnName, ReturnPlural, InputCount, ReturnCount>}By${Capitalize<InputPlural>}`]: (...refs: (InputEntity | EntityId)[]) => CompositeResult<ReturnEntity, InputCount, ReturnCount>;
258
+ } & {
259
+ [Key in `${CompositeIdLead<ReturnName, InputCount, ReturnCount>}By${Capitalize<InputPlural>}`]: (...refs: (InputEntity | EntityId)[]) => CompositeIdResult<ReturnId, InputCount, ReturnCount>;
260
+ } : {};
261
+ type MutationMethods<FromEntity, FromName extends string, ToEntity, ToName extends string> = {
262
+ [Key in `add${Capitalize<ToName>}To${Capitalize<FromName>}`]: (from: FromEntity | EntityId, to: ToEntity | EntityId) => void;
263
+ } & {
264
+ [Key in `remove${Capitalize<ToName>}From${Capitalize<FromName>}`]: (from: FromEntity | EntityId, to: ToEntity | EntityId) => void;
265
+ } & {
266
+ [Key in `move${Capitalize<ToName>}To${Capitalize<FromName>}`]: (to: ToEntity | EntityId, from: FromEntity | EntityId) => void;
267
+ };
268
+ /**
269
+ * The position of a member entity within its owner's ordered related list (as returned by the owner's
270
+ * get-by lookup), or -1 when the member is not linked. Takes only the member — the owner is the single
271
+ * entity the member belongs to. Generated only where the member has exactly one owner (the owner side
272
+ * is count 1) and that owner holds many members (the member side is `'many'` or a fixed count 2+); a
273
+ * many-to-many has no single owner to infer, so it gets none. The member is passed as the entity or its id.
274
+ */
275
+ type IndexOfMethod<OwnerName extends string, OwnerCount extends Cardinality, MemberName extends string, MemberEntity, MemberCount extends Cardinality> = IsSingle<OwnerCount> extends true ? IsMulti<MemberCount> extends true ? {
276
+ [Key in `indexOf${Capitalize<OwnerName>}${Capitalize<MemberName>}`]: (member: MemberEntity | EntityId) => number;
277
+ } : {} : {};
278
+ /**
279
+ * Ordering mutations for a member within its owner's related list: swap two members, set the full
280
+ * order, or move one to an index. Generated in the same direction as {@link IndexOfMethod} — only where
281
+ * the member has a single owner (owner side is count 1) that holds many members. `reorder` takes the
282
+ * complete set of that owner's members and no-ops (with a warning) on an incomplete or foreign list.
283
+ * Members are passed as the entity or its id.
284
+ */
285
+ type OrderingMethods<OwnerName extends string, OwnerCount extends Cardinality, MemberName extends string, MemberPlural extends string, MemberEntity, MemberCount extends Cardinality> = IsSingle<OwnerCount> extends true ? IsMulti<MemberCount> extends true ? {
286
+ [Key in `swap${Capitalize<OwnerName>}${Capitalize<MemberPlural>}`]: (memberA: MemberEntity | EntityId, memberB: MemberEntity | EntityId) => void;
287
+ } & {
288
+ [Key in `reorder${Capitalize<OwnerName>}${Capitalize<MemberPlural>}`]: (orderedMembers: (MemberEntity | EntityId)[]) => void;
289
+ } & {
290
+ [Key in `move${Capitalize<OwnerName>}${Capitalize<MemberName>}ToIndex`]: (member: MemberEntity | EntityId, index: number) => void;
291
+ } : {} : {};
292
+ /**
293
+ * The By-form of {@link OrderingMethods} plus indexOf, for a member that has MORE THAN ONE owner (owner
294
+ * side is not count 1) and belongs to more than one of each owner (member side is multi — a fixed count
295
+ * 2+ or `'many'`). The owner can't be inferred, so it is named explicitly as the last argument:
296
+ * `indexOfCellByOption(cell, option)`, or `indexOfOptionByCell(option, cell)` in the mirror direction.
297
+ * Members are passed as the entity or its id; `reorder` requires that owner's complete member set.
298
+ */
299
+ type ByOrderingMethods<OwnerName extends string, OwnerEntity, OwnerCount extends Cardinality, MemberName extends string, MemberPlural extends string, MemberEntity, MemberCount extends Cardinality> = IsMulti<OwnerCount> extends true ? IsMulti<MemberCount> extends true ? {
300
+ [Key in `indexOf${Capitalize<MemberName>}By${Capitalize<OwnerName>}`]: (member: MemberEntity | EntityId, owner: OwnerEntity | EntityId) => number;
301
+ } & {
302
+ [Key in `swap${Capitalize<MemberPlural>}By${Capitalize<OwnerName>}`]: (memberA: MemberEntity | EntityId, memberB: MemberEntity | EntityId, owner: OwnerEntity | EntityId) => void;
303
+ } & {
304
+ [Key in `reorder${Capitalize<MemberPlural>}By${Capitalize<OwnerName>}`]: (orderedMembers: (MemberEntity | EntityId)[], owner: OwnerEntity | EntityId) => void;
305
+ } & {
306
+ [Key in `move${Capitalize<MemberName>}By${Capitalize<OwnerName>}ToIndex`]: (member: MemberEntity | EntityId, index: number, owner: OwnerEntity | EntityId) => void;
307
+ } : {} : {};
308
+ /**
309
+ * Filtered directional lookups that keep only the owners where the member sits at EXACTLY the given
310
+ * index in that owner's ordered member list, so index 0 is the first slot and a member showing up at
311
+ * index 1 is ignored. Generated when BOTH sides are multi (neither is count 1): the owner then holds an
312
+ * ordered array of two-or-more members (a fixed count 2+ or `'many'`), and the member belongs to more
313
+ * than one owner, so the same member can occupy a different slot in each — which is what makes filtering
314
+ * by index meaningful. Both the entity and the id form are returned, in the same order as the base
315
+ * `${OwnerPlural}By${MemberName}` lookup. The member is passed as the entity or its id.
316
+ *
317
+ * @example ensemblesByMusicianAtIndex(musician, 3) — ensembles where that musician plays the 4th chair.
318
+ */
319
+ type IndexedLookupMethods<OwnerName extends string, OwnerPlural extends string, OwnerEntity, OwnerId extends EntityId, OwnerCount extends Cardinality, MemberName extends string, MemberEntity, MemberCount extends Cardinality> = IsMulti<OwnerCount> extends true ? IsMulti<MemberCount> extends true ? {
320
+ [Key in `${OwnerPlural}By${Capitalize<MemberName>}AtIndex`]: (member: MemberEntity | EntityId, index: number) => OwnerEntity[];
321
+ } & {
322
+ [Key in `${OwnerName}IdsBy${Capitalize<MemberName>}AtIndex`]: (member: MemberEntity | EntityId, index: number) => OwnerId[];
323
+ } : {} : {};
324
+ type RelationshipOutput<FromEntity, FromName extends string, FromPlural extends string, FromCount extends Cardinality, FromId extends EntityId, ToEntity, ToName extends string, ToPlural extends string, ToCount extends Cardinality, ToId extends EntityId> = {
325
+ state: LinkState<FromName, ToName>;
326
+ props: IndexProps<FromName, ToName>;
327
+ methods: LookupMethods<FromEntity, FromName, FromPlural, FromCount, FromId, ToEntity, ToName, ToPlural, ToCount, ToId> & CompositeSetLookup<ToEntity, ToName, ToPlural, ToId, ToCount, FromEntity, FromPlural, FromCount> & CompositeSetLookup<FromEntity, FromName, FromPlural, FromId, FromCount, ToEntity, ToPlural, ToCount> & IndexOfMethod<FromName, FromCount, ToName, ToEntity, ToCount> & IndexOfMethod<ToName, ToCount, FromName, FromEntity, FromCount> & OrderingMethods<FromName, FromCount, ToName, ToPlural, ToEntity, ToCount> & OrderingMethods<ToName, ToCount, FromName, FromPlural, FromEntity, FromCount> & ByOrderingMethods<FromName, FromEntity, FromCount, ToName, ToPlural, ToEntity, ToCount> & ByOrderingMethods<ToName, ToEntity, ToCount, FromName, FromPlural, FromEntity, FromCount> & IndexedLookupMethods<FromName, FromPlural, FromEntity, FromId, FromCount, ToName, ToEntity, ToCount> & IndexedLookupMethods<ToName, ToPlural, ToEntity, ToId, ToCount, FromName, FromEntity, FromCount> & MutationMethods<FromEntity, FromName, ToEntity, ToName>;
328
+ };
329
+ /**
330
+ * Relates two entity collections into typed cross-lookups, counts, composite set-lookups, and link mutations named by each side's `collection`; `count` (`1`, a number, or `'many'`) drives the names and shapes, and every lookup takes the entity or its id.
331
+ *
332
+ * @example
333
+ * signalStore(
334
+ * withEntities(libraryConfig),
335
+ * withEntities(bookConfig),
336
+ * withEntityRelationship({ collection: 'library', count: 1 }, { collection: 'book', count: 'many' }),
337
+ * );
338
+ * // booksByLibrary(library | id) -> Book[]
339
+ * // libraryByBook(book | id) -> Library | undefined
340
+ * // libraryHasBook, bookCountByLibrary, addBookToLibrary, removeBookFromLibrary, moveBookToLibrary
341
+ * // indexOfLibraryBook(book | id) -> number (the book's position in its library, -1 if absent)
342
+ * // swapLibraryBooks(a, b), reorderLibraryBooks(orderedBooks), moveLibraryBookToIndex(book, index) reorder a library's books
343
+ * // book side is 'many', so a set-lookup keyed by a full book-set: libraryByBooks(...books | ids) -> Library
344
+ */
345
+ declare const withEntityRelationship: <FromName extends string, FromCount extends Cardinality, ToName extends string, ToCount extends Cardinality, Store extends SignalStoreFeatureResult & RequiredEntityStore<FromName, ToName>, FromSelect extends IdAccessor = IdAccessor, ToSelect extends IdAccessor = IdAccessor, FromPlural extends string = `${FromName}s`, ToPlural extends string = `${ToName}s`>(from: {
346
+ collection: FromName;
347
+ count: FromCount;
348
+ plural?: FromPlural;
349
+ selectId?: FromSelect;
350
+ selectForeignId?: ForeignIdAccessor;
351
+ }, to: {
352
+ collection: ToName;
353
+ count: ToCount;
354
+ plural?: ToPlural;
355
+ selectId?: ToSelect;
356
+ selectForeignId?: ForeignIdAccessor;
357
+ }, options?: RelationshipOptions) => SignalStoreFeature<Store, RelationshipOutput<EntityOf<Store, FromName>, FromName, FromPlural, FromCount, IdOf<FromSelect, EntityOf<Store, FromName>>, EntityOf<Store, ToName>, ToName, ToPlural, ToCount, IdOf<ToSelect, EntityOf<Store, ToName>>>>;
358
+
359
+ /**
360
+ * The read-only lookups a transitive relationship generates, both directions, arrays throughout
361
+ * (a chained walk can reach more than one of either endpoint). Each lookup accepts the endpoint
362
+ * entity OR its id, and returns the far endpoint entities or their ids.
363
+ */
364
+ type TransitiveOutput<FromEntity, FromName extends string, FromPlural extends string, FromId extends EntityId, ToEntity, ToName extends string, ToPlural extends string, ToId extends EntityId> = {
365
+ state: {};
366
+ props: {};
367
+ methods: {
368
+ [Key in `${ToPlural}By${Capitalize<FromName>}`]: (from: FromEntity | EntityId) => ToEntity[];
369
+ } & {
370
+ [Key in `${ToName}IdsBy${Capitalize<FromName>}`]: (from: FromEntity | EntityId) => ToId[];
371
+ } & {
372
+ [Key in `${FromPlural}By${Capitalize<ToName>}`]: (to: ToEntity | EntityId) => FromEntity[];
373
+ } & {
374
+ [Key in `${FromName}IdsBy${Capitalize<ToName>}`]: (to: ToEntity | EntityId) => FromId[];
375
+ };
376
+ };
377
+ /**
378
+ * Read-only lookups between two collections that share no foreign key, by walking the intermediary collection(s) between them (`through` a single name or an ordered list); returns arrays both directions.
379
+ *
380
+ * @example
381
+ * signalStore(
382
+ * withEntities(libraryConfig), withEntities(shelfConfig), withEntities(bookConfig),
383
+ * withEntityRelationship({ collection: 'library', count: 1 }, { collection: 'shelf', count: 'many' }),
384
+ * withEntityRelationship({ collection: 'shelf', count: 1 }, { collection: 'book', count: 'many' }),
385
+ * withTransitiveRelationship({ from: 'library', to: 'book', through: 'shelf' }),
386
+ * );
387
+ * // booksByLibrary(library | id) -> Book[]
388
+ * // librariesByBook(book | id) -> Library[]
389
+ */
390
+ declare const withTransitiveRelationship: <FromName extends string, ToName extends string, Store extends SignalStoreFeatureResult & RequiredEntityStore<FromName, ToName>, FromSelect extends IdAccessor = IdAccessor, ToSelect extends IdAccessor = IdAccessor, FromPlural extends string = `${FromName}s`, ToPlural extends string = `${ToName}s`>(config: {
391
+ from: FromName;
392
+ to: ToName;
393
+ through: string | string[];
394
+ fromPlural?: FromPlural;
395
+ toPlural?: ToPlural;
396
+ selectFromId?: FromSelect;
397
+ selectToId?: ToSelect;
398
+ }) => SignalStoreFeature<Store, TransitiveOutput<EntityOf<Store, FromName>, FromName, FromPlural, IdOf<FromSelect>, EntityOf<Store, ToName>, ToName, ToPlural, IdOf<ToSelect>>>;
399
+
400
+ export { withEntityAccessors, withEntityRelationship, withTransitiveRelationship };
401
+ export type { Cardinality, EntityAccessorConfig, EntityPredicate, ForeignIdAccessor, IdAccessor, RelationshipOptions, RelationshipSide };