Skip to content

Commit a08d447

Browse files
committed
feat: dynamic collection name
1 parent 71b4d48 commit a08d447

File tree

4 files changed

+369
-28
lines changed

4 files changed

+369
-28
lines changed

docs/guide/data/query.md

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -401,3 +401,48 @@ declare module '@rstore/vue' {
401401

402402
export {}
403403
```
404+
405+
## Dynamic collection name <Badge text="New in v0.8.2" />
406+
407+
You can access a collection with a dynamic name by using a string, a ref or a getter with the `$collection` method on the store instead of accessing it directly as a property.
408+
409+
When the collection name changes, the query will automatically call its `refresh` method to fetch data from the new collection.
410+
411+
Example with a ref:
412+
413+
```ts
414+
const collectionName = ref('posts')
415+
416+
const {
417+
data: items
418+
} = await store.$collection(collectionName).query(q => q.many())
419+
420+
// some time later...
421+
422+
collectionName.value = 'comments'
423+
// The query will automatically refresh
424+
// to fetch the comments instead of the posts
425+
```
426+
427+
Example with a getter:
428+
429+
```ts
430+
const route = useRoute()
431+
432+
const {
433+
data: items
434+
} = await store.$collection(() => route.params.collectionName as string)
435+
.query(q => q.many())
436+
// When the route changes, the query will automatically refresh
437+
// to fetch the new collection
438+
```
439+
440+
If you can't need reactivity but don't know the collection name in advance, you can also pass a simple string:
441+
442+
```ts
443+
const query = await store.$collection('posts').query(q => q.many())
444+
```
445+
446+
::: warning
447+
Using `$collection` will not provide type inference for the collection API. You will need to manually type the result if needed.
448+
:::

packages/vue/src/api.ts

Lines changed: 84 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -277,13 +277,26 @@ export interface VueCollectionApi<
277277
) => void
278278
}
279279

280+
export interface CreateCollectionApiOptions<
281+
TCollection extends Collection,
282+
TCollectionDefaults extends CollectionDefaults,
283+
TSchema extends StoreSchema,
284+
> {
285+
store: VueStore<TSchema, TCollectionDefaults>
286+
getCollection: () => ResolvedCollection<TCollection, TCollectionDefaults, TSchema>
287+
onInvalidate?: (cb: () => unknown) => { off: () => void }
288+
}
289+
280290
export function createCollectionApi<
281291
TCollection extends Collection,
282292
TCollectionDefaults extends CollectionDefaults,
283293
TSchema extends StoreSchema,
284294
>(
285-
store: VueStore<TSchema, TCollectionDefaults>,
286-
collection: ResolvedCollection<TCollection, TCollectionDefaults, TSchema>,
295+
{
296+
store,
297+
getCollection,
298+
onInvalidate,
299+
}: CreateCollectionApiOptions<TCollection, TCollectionDefaults, TSchema>,
287300
): VueCollectionApi<TCollection, TCollectionDefaults, TSchema, WrappedItem<TCollection, TCollectionDefaults, TSchema>> {
288301
/**
289302
* Bind the options getter to track the type of query (`first` or `many`).
@@ -333,11 +346,37 @@ export function createCollectionApi<
333346
return createQuery<TCollection, TCollectionDefaults, TSchema, any, WrappedItem<TCollection, TCollectionDefaults, TSchema> | null | Array<WrappedItem<TCollection, TCollectionDefaults, TSchema>>>({
334347
store,
335348
fetchMethod: (options, meta) => toValue(type) === 'first'
336-
? (options ? findFirst({ store, collection, findOptions: options, meta }).then(r => r.result) : Promise.resolve(null))
337-
: findMany({ store, collection, findOptions: options, meta }).then(r => r.result),
349+
? (options
350+
? findFirst({
351+
store,
352+
collection: getCollection(),
353+
findOptions: options,
354+
meta,
355+
}).then(r => r.result)
356+
: Promise.resolve(null))
357+
: findMany({
358+
store,
359+
collection: getCollection(),
360+
findOptions: options,
361+
meta,
362+
}).then(r => r.result),
338363
cacheMethod: (options, meta) => toValue(type) === 'first'
339-
? (options ? peekFirst({ store, collection, findOptions: options, meta, force: true }).result : null)
340-
: peekMany({ store, collection, findOptions: options, meta, force: true }).result,
364+
? (options
365+
? peekFirst({
366+
store,
367+
collection: getCollection(),
368+
findOptions: options,
369+
meta,
370+
force: true,
371+
}).result
372+
: null)
373+
: peekMany({
374+
store,
375+
collection: getCollection(),
376+
findOptions: options,
377+
meta,
378+
force: true,
379+
}).result,
341380
defaultValue: () => toValue(type) === 'first' ? null : [],
342381
options: boundOptionsGetter,
343382
})
@@ -365,7 +404,7 @@ export function createCollectionApi<
365404
unsubscribe({
366405
store,
367406
meta: meta.value,
368-
collection,
407+
collection: getCollection(),
369408
subscriptionId,
370409
key: previousKey,
371410
findOptions: previousFindOptions,
@@ -387,7 +426,7 @@ export function createCollectionApi<
387426
await subscribe({
388427
store,
389428
meta: meta.value,
390-
collection,
429+
collection: getCollection(),
391430
subscriptionId,
392431
key,
393432
findOptions,
@@ -402,6 +441,15 @@ export function createCollectionApi<
402441

403442
tryOnScopeDispose(unsub)
404443

444+
if (onInvalidate) {
445+
const { off } = onInvalidate(() => {
446+
return sub(toValue(keyOrFindOptions))
447+
})
448+
tryOnScopeDispose(() => {
449+
off()
450+
})
451+
}
452+
405453
return {
406454
unsubscribe: unsub,
407455
meta,
@@ -430,34 +478,44 @@ export function createCollectionApi<
430478
if (meta) {
431479
Object.assign(query.meta.value, meta)
432480
}
481+
482+
if (onInvalidate) {
483+
const { off } = onInvalidate(() => {
484+
query.refresh()
485+
})
486+
tryOnScopeDispose(() => {
487+
off()
488+
})
489+
}
490+
433491
return query as ReturnType<Api['query']>
434492
}
435493

436494
type Api = VueCollectionApi<TCollection, TCollectionDefaults, TSchema, WrappedItem<TCollection, TCollectionDefaults, TSchema>>
437495
const api: Api = {
438496
peekFirst: findOptions => peekFirst({
439497
store,
440-
collection,
498+
collection: getCollection(),
441499
findOptions,
442500
force: true,
443501
}).result,
444502

445503
findFirst: findOptions => findFirst({
446504
store,
447-
collection,
505+
collection: getCollection(),
448506
findOptions,
449507
}).then(r => r.result),
450508

451509
peekMany: findOptions => peekMany({
452510
store,
453-
collection,
511+
collection: getCollection(),
454512
findOptions,
455513
force: true,
456514
}).result,
457515

458516
findMany: findOptions => findMany({
459517
store,
460-
collection,
518+
collection: getCollection(),
461519
findOptions,
462520
}).then(r => r.result),
463521

@@ -470,14 +528,14 @@ export function createCollectionApi<
470528
create: (item, options) => createItem({
471529
...options,
472530
store,
473-
collection,
531+
collection: getCollection(),
474532
item,
475533
}),
476534

477535
createMany: (items, options) => createMany({
478536
...options,
479537
store,
480-
collection,
538+
collection: getCollection(),
481539
items,
482540
}),
483541

@@ -487,7 +545,7 @@ export function createCollectionApi<
487545
ResolvedCollectionItem<TCollection, TCollectionDefaults, TSchema>
488546
>({
489547
defaultValues: formOptions?.defaultValues,
490-
schema: formOptions?.schema ?? collection.formSchema.create,
548+
schema: formOptions?.schema ?? getCollection().formSchema.create,
491549
submit: data => api.create(data, {
492550
optimistic: formOptions?.optimistic,
493551
}),
@@ -500,14 +558,14 @@ export function createCollectionApi<
500558
update: (item, updateOptions) => updateItem({
501559
...updateOptions,
502560
store,
503-
collection,
561+
collection: getCollection(),
504562
item,
505563
}),
506564

507565
updateMany: (items, options) => updateMany({
508566
...options,
509567
store,
510-
collection,
568+
collection: getCollection(),
511569
items,
512570
}),
513571

@@ -531,7 +589,7 @@ export function createCollectionApi<
531589
...formOptions?.defaultValues?.() as Partial<ResolvedCollectionItem<TCollection, TCollectionDefaults, TSchema>>,
532590
...initialData,
533591
}),
534-
schema: formOptions?.schema ?? collection.formSchema.update,
592+
schema: formOptions?.schema ?? getCollection().formSchema.update,
535593
resetDefaultValues: () => getFormData(),
536594
// Only use changed props
537595
transformData: (form) => {
@@ -550,7 +608,7 @@ export function createCollectionApi<
550608
return data
551609
},
552610
submit: data => api.update(data, {
553-
key: collection.getKey(initialData),
611+
key: getCollection().getKey(initialData),
554612
optimistic: formOptions?.optimistic,
555613
}),
556614
resetOnSuccess: formOptions?.resetOnSuccess,
@@ -560,6 +618,8 @@ export function createCollectionApi<
560618
},
561619

562620
delete: (keyOrItem, options) => {
621+
const collection = getCollection()
622+
563623
let key: string | number | number
564624
if (typeof keyOrItem !== 'string' && typeof keyOrItem !== 'number') {
565625
const result = collection.getKey(keyOrItem)
@@ -581,6 +641,8 @@ export function createCollectionApi<
581641
},
582642

583643
deleteMany: (keysOrItems, options) => {
644+
const collection = getCollection()
645+
584646
const keys = keysOrItems.map((keyOrItem) => {
585647
if (typeof keyOrItem !== 'string' && typeof keyOrItem !== 'number') {
586648
const result = collection.getKey(keyOrItem)
@@ -602,9 +664,10 @@ export function createCollectionApi<
602664
})
603665
},
604666

605-
getKey: item => collection.getKey(item),
667+
getKey: item => getCollection().getKey(item),
606668

607669
writeItem: (item) => {
670+
const collection = getCollection()
608671
const key = collection.getKey(item)
609672
if (key == null) {
610673
throw new Error('Item write failed: key is not defined')
@@ -622,7 +685,7 @@ export function createCollectionApi<
622685

623686
clearItem: (key) => {
624687
store.$cache.deleteItem({
625-
collection,
688+
collection: getCollection(),
626689
key,
627690
})
628691
},

packages/vue/src/store.ts

Lines changed: 37 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
11
import type { Collection, CollectionDefaults, CollectionsFromStoreSchema, FindOptions, Plugin, ResolvedModule, StoreCore, StoreSchema, WrappedItem } from '@rstore/shared'
22
import { createStoreCore, normalizeCollectionRelations, resolveCollection } from '@rstore/core'
33
import { createHooks } from '@rstore/shared'
4-
import { createEventHook } from '@vueuse/core'
5-
import { reactive, ref } from 'vue'
4+
import { createEventHook, tryOnScopeDispose } from '@vueuse/core'
5+
import { type MaybeRefOrGetter, reactive, ref, toValue, watch } from 'vue'
66
import { createCollectionApi, type VueCollectionApi } from './api'
77
import { createCache } from './cache'
88

@@ -51,7 +51,7 @@ export type VueStore<
5151
TSchema extends StoreSchema = StoreSchema,
5252
TCollectionDefaults extends CollectionDefaults = CollectionDefaults,
5353
> = StoreCore<TSchema, TCollectionDefaults> & VueStoreCollectionApiProxy<TSchema, TCollectionDefaults> & {
54-
$collection: (collectionName: string) => VueCollectionApi<any, TCollectionDefaults, TSchema, WrappedItem<any, TCollectionDefaults, TSchema>>
54+
$collection: (collectionName: MaybeRefOrGetter<string>) => VueCollectionApi<any, TCollectionDefaults, TSchema, WrappedItem<any, TCollectionDefaults, TSchema>>
5555
$onCacheReset: (callback: () => void) => () => void
5656
$experimentalGarbageCollection?: boolean
5757
$modulesCache: WeakMap<(...args: any[]) => ResolvedModule<any, any>, ResolvedModule<any, any>>
@@ -85,13 +85,16 @@ export async function createStore<
8585

8686
const queryCache: Map<string, VueCollectionApi<Collection, TCollectionDefaults, TSchema, WrappedItem<Collection, TCollectionDefaults, TSchema>>> = new Map()
8787

88-
function getApi(key: string) {
88+
function getCachedApiByKey(key: string) {
8989
if (!queryCache.has(key)) {
9090
const collection = storeProxy.$collections.find(m => m.name === key)
9191
if (!collection) {
9292
throw new Error(`Collection ${key} not found`)
9393
}
94-
queryCache.set(key, createCollectionApi(storeProxy, collection))
94+
queryCache.set(key, createCollectionApi({
95+
store: storeProxy,
96+
getCollection: () => collection,
97+
}))
9598
}
9699
return queryCache.get(key)
97100
}
@@ -109,11 +112,38 @@ export async function createStore<
109112
storeProxy = new Proxy(store, {
110113
get(_, key) {
111114
if (typeof key === 'string' && privateStore.$_collectionNames.has(key)) {
112-
return getApi(key)
115+
return getCachedApiByKey(key)
113116
}
114117

115118
if (key === '$collection') {
116-
return (collectionName: string) => getApi(collectionName)
119+
return (collectionName: MaybeRefOrGetter<string>) => {
120+
if (typeof collectionName === 'string') {
121+
return getCachedApiByKey(collectionName)
122+
}
123+
124+
const invalidateEvent = createEventHook()
125+
126+
tryOnScopeDispose(() => {
127+
invalidateEvent.clear()
128+
})
129+
130+
watch(collectionName, () => {
131+
invalidateEvent.trigger()
132+
})
133+
134+
return createCollectionApi({
135+
store: storeProxy,
136+
getCollection: () => {
137+
const name = toValue(collectionName)
138+
const collection = storeProxy.$collections.find(m => m.name === name)
139+
if (!collection) {
140+
throw new Error(`Collection ${name} not found`)
141+
}
142+
return collection
143+
},
144+
onInvalidate: invalidateEvent.on,
145+
})
146+
}
117147
}
118148

119149
if (key === '$onCacheReset') {

0 commit comments

Comments
 (0)