Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft: params.clones #24

Closed
wants to merge 17 commits into from
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions docs/guide/service-stores.md
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,7 @@ These special getters accept arguments that allow you to query data from the sto

### `findInStore(params)`

Performs reactive queries against the data in `itemsById`. To also include data from `tempsById`, set `params.temps` to `true`.
Performs reactive queries against the data in `itemsById`. To also include data from `tempsById`, set `params.temps` to `true`. To return clones, if existent from `clonesById`, set `params.clones` to `true`.

> Tip: use `findInStore` and enjoy [Automatic Instance Hydration](#automatic-instance-hydration)

Expand All @@ -156,7 +156,7 @@ returns the `total` returned from `findInStore`, without returning the data.

### `getFromStore(id, params)`

Works similar to `.get` requests in a Feathers service object. It only returns records currently populated in the store.
Works similar to `.get` requests in a Feathers service object. It only returns records currently populated in the store. It can also return temp data from `tempsById` if the provided `id` is a `__tempId`. It can also return a clone, if `params.clones` is `true`.

> Tip: use `getFromStore` and enjoy [Automatic Instance Hydration](#automatic-instance-hydration)

Expand Down
8 changes: 4 additions & 4 deletions src/handle-clones.ts
Original file line number Diff line number Diff line change
Expand Up @@ -60,7 +60,7 @@ export function handleClones(props: any, options: HandleClonesOptions = {}) {
if (item.value == null) {
return null
}
const id = getAnyId(item.value)
const id = getAnyId(item.value, store.idField)
const existingClone = store.clonesById[id]
if (
existingClone &&
Expand All @@ -73,10 +73,10 @@ export function handleClones(props: any, options: HandleClonesOptions = {}) {
})
watch(
// Since `item` can change, watch the reactive `item` instead of non-reactive `item`
() => item.value && getAnyId(item.value),
() => item.value && getAnyId(item.value, store.idField),
(id) => {
// Update the clones and handlers
if (!clones[key] || id !== getAnyId(clones[key].value)) {
if (!clones[key] || id !== getAnyId(clones[key].value, store.idField)) {
clones[key] = clone
/**
* Each save_handler has the same name as the prop, prepended with `save_`.
Expand Down Expand Up @@ -104,7 +104,7 @@ export function handleClones(props: any, options: HandleClonesOptions = {}) {
propOrCollection: any,
opts: SaveHandlerOpts = {},
) {
const original = store.getFromStore(getAnyId(item.value))
const original = store.getFromStore(getAnyId(item.value, store.idField))
const isArray = Array.isArray(propOrCollection)
const isString = typeof propOrCollection === 'string'
const isObject = !isArray && !isString && propOrCollection != null
Expand Down
3 changes: 2 additions & 1 deletion src/service-store/base-model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ export class BaseModel {
Object.assign(this, instanceDefaults(data, { models, store }))
Object.assign(this, setupInstance(data, { models, store }))
this.__isClone = !!options.clone

return this
}

Expand Down Expand Up @@ -131,7 +132,7 @@ export class BaseModel {
public reset(): this {
const { store } = this.constructor as typeof BaseModel

return (store as any).resetCopy(this)
return (store as any).resetClone(this)
}

/**
Expand Down
64 changes: 48 additions & 16 deletions src/service-store/make-actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -273,12 +273,12 @@ export function makeActions(options: ServiceOptions): ServiceActions {
// Assure each item is an instance
items.forEach((item: any, index: number) => {
if (this.isSsr || !(item instanceof options.Model)) {
const classes = { [this.servicePath]: options.Model }
items[index] = new classes[this.servicePath](item)
items[index] = new options.Model(item)
}
})

// Move items with both __tempId and idField from tempsById to itemsById
// Move clones with both __tempId and idField in clonesById
const withBoth = items.filter((i: any) => getId(i) != null && getTempId(i) != null)
withBoth.forEach((item: any) => {
const id = getId(item)
Expand All @@ -289,6 +289,20 @@ export function makeActions(options: ServiceOptions): ServiceActions {
delete this.tempsById[item.__tempId]
delete this.itemsById[id].__tempId
}

const existingClone = this.clonesById[item.__tempId]
if (existingClone) {
//assign id
const { idField } = this;
existingClone[idField] = id;

//move
delete this.clonesById[item.__tempId]
this.clonesById[id] = existingClone

delete this.clonesById[id].__tempId
}

delete item.__tempId
})

Expand All @@ -312,19 +326,16 @@ export function makeActions(options: ServiceOptions): ServiceActions {
},

clone(item: any, data = {}) {
const placeToStore = item.__tempId != null ? 'tempsById' : 'itemsById'
const id = getAnyId(item)
const originalItem = this[placeToStore][id]
const existing = this.clonesById[getAnyId(item)]
if (existing && existing.constructor.name === originalItem.constructor.name) {
const readyToReset = Object.assign(existing, originalItem, data)
Object.keys(readyToReset).forEach((key) => {
if (!hasOwn(originalItem, key)) {
delete readyToReset[key]
}
})
return readyToReset
// reset existing clone
const resetted = this.resetClone(item, data);
if (resetted) {
return resetted;
} else {
// clone not existing
const placeToStore = item.__tempId != null ? 'tempsById' : 'itemsById'
const id = getAnyId(item, this.idField)
const originalItem = this[placeToStore][id]

const clone = fastCopy(originalItem)
Object.defineProperty(clone, '__isClone', {
value: true,
Expand All @@ -336,15 +347,36 @@ export function makeActions(options: ServiceOptions): ServiceActions {
return this.clonesById[id] // Must return the item from the store
}
},

commit(item: any) {
const id = getAnyId(item)
const id = getAnyId(item, this.idField)
if (id != null) {
const placeToStore = item.__tempId != null ? 'tempsById' : 'itemsById'
this[placeToStore][id] = fastCopy(this.clonesById[id])
return this.itemsById[id]
return this[placeToStore][id]
}
},

resetClone(item: any, data = {}) {
const placeToStore = item.__tempId != null ? 'tempsById' : 'itemsById'
const id = getAnyId(item, this.idField)
const originalItem = this[placeToStore][id]
const existing = this.clonesById[getAnyId(item, this.idFIeld)]

if (!existing || existing.constructor.name !== originalItem.constructor.name) {
return;
}

const readyToReset = Object.assign(existing, originalItem, data)
Object.keys(readyToReset).forEach((key) => {
if (!hasOwn(originalItem, key)) {
delete readyToReset[key]
}
})

return readyToReset
},

/**
* Stores pagination data on state.pagination based on the query identifier
* (qid) The qid must be manually assigned to `params.qid`
Expand Down
65 changes: 49 additions & 16 deletions src/service-store/make-getters.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import { _ } from '@feathersjs/commons'
import { filterQuery, sorter, select } from '@feathersjs/adapter-commons'
import { unref } from 'vue-demi'
import fastCopy from 'fast-copy'
import { getAnyId, getId, getTempId } from '../utils'

const FILTERS = ['$sort', '$limit', '$skip', '$select']
const additionalOperators = ['$elemMatch']
Expand All @@ -31,39 +32,63 @@ export function makeGetters(options: ServiceOptions): ServiceGetters {
return !!ssr
},
itemIds() {
return this.items.map((item: any) => item[this.idField])
return this.items.map((item: any) => getId(item, this.idField))
},
items() {
return Object.values(this.itemsById)
},
tempIds() {
return this.temps.map((temp: any) => temp.__tempId)
return this.temps.map((temp: any) => getTempId(temp))
},
temps() {
return Object.values(this.tempsById)
},
cloneIds() {
return this.clones.map((clone: any) => clone[this.idField])
return this.clones.map((clone: any) => getAnyId(clone, this.idField))
},
clones() {
return Object.values(this.clonesById)
},
itemsAndTemps() {
return this.items.concat(this.temps);
},
itemsAndClones() {
return this.items.map((item: any) => {
const id = getId(item, this.idField)
return this.clonesById[id] || item
});
},
itemsTempsAndClones() {
return this.itemsAndTemps.map((item: any) => {
const id = getId(item, this.idField)

return (id != null && this.clonesById[id]) ||
(item.__tempId != null && this.clonesById[item.__tempId]) ||
item;
})
},
operators() {
return additionalOperators
.concat(this.whitelist)
.concat(this.service.options?.allow || this.service.options?.whitelist || [])
},
findInStore() {
return (params: Params) => {
params = { ...unref(params) } || {}

const { paramsForServer, whitelist, itemsById } = this
const q = _.omit(params.query || {}, paramsForServer)
const q = _.omit(params.query || {}, this.paramsForServer)

const { query, filters } = filterQuery(q, {
operators: additionalOperators
.concat(whitelist)
.concat(this.service.options?.allow || this.service.options?.whitelist || []),
})
let values = _.values(itemsById)
const { query, filters } = filterQuery(q, { operators: this.operators })

if (params.temps) {
values.push(..._.values(this.tempsById))
let values: any[]
if (!params.temps && !params.clones) {
values = this.items
} else if (params.temps && !params.clones) {
values = this.itemsAndTemps
} else if (!params.temps && params.clones) {
values = this.itemsAndClones
} else {
values = this.itemsTempsAndClones
}

values = values.filter(sift(query))
Expand All @@ -82,8 +107,12 @@ export function makeGetters(options: ServiceOptions): ServiceGetters {
values = values.slice(0, filters.$limit)
}

// keep transformed items by $select separately
// so `addOrUpdate` can transform the original item to an instance
// otherwise the picked Values would go through `addOrUpdate` every time
let pickedValues: any[] | undefined = undefined;
if (filters.$select) {
values = values.map((value) => _.pick(value, ...filters.$select.slice()))
pickedValues = values.map((value) => _.pick(value, ...filters.$select))
}

// Make sure items are instances
Expand All @@ -98,7 +127,7 @@ export function makeGetters(options: ServiceOptions): ServiceGetters {
total,
limit: filters.$limit || 0,
skip: filters.$skip || 0,
data: values,
data: pickedValues || values,
}
}
},
Expand All @@ -120,7 +149,11 @@ export function makeGetters(options: ServiceOptions): ServiceGetters {
id = unref(id)
params = fastCopy(unref(params) || {})

let item = this.itemsById[id] && select(params, this.idField)(this.itemsById[id])
let item
if (params.clones) {
item = this.clonesById[id] && select(params, this.idField)(this.clonesById[id])
}
if (!item) item = this.itemsById[id] && select(params, this.idField)(this.itemsById[id])
if (!item) item = this.tempsById[id] && select(params, '__tempId')(this.tempsById[id])

// Make sure item is an instance
Expand Down
2 changes: 1 addition & 1 deletion src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,7 @@ export interface Params extends AnyData {
route?: Record<string, string>
headers?: Record<string, any>
temps?: boolean
copies?: boolean
clones?: boolean
qid?: string
skipRequestIfExists?: boolean
data?: any
Expand Down
5 changes: 3 additions & 2 deletions src/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,9 @@ export function getTempId(item: any) {
return stringifyIfObject(item.__tempId)
}
}
export function getAnyId(item: any) {
return getId(item) != undefined ? getId(item) : getTempId(item)
export function getAnyId(item: any, idField?: string) {
const id = getId(item, idField);
return (id != undefined) ? id : getTempId(item);
}

export function getQueryInfo(
Expand Down
72 changes: 72 additions & 0 deletions tests/lists.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -51,4 +51,76 @@ describe('Lists', () => {
messagesService.items.forEach((item: any) => item.clone())
expect(messagesService.cloneIds).toStrictEqual([0, 1, 2, 3, 4, 5, 6])
});

test('itemsAndTemps getter returns items and temps in itemsById and tempsById', async () => {
const { items, temps } = messagesService;

items.forEach((item: any, i: number) => {
if (i % 2 === 0) { return; }
item.clone()
})
temps.forEach((temp: any, i: number) => {
if (i % 2 === 0) { return; }
temp.clone()
})

const itemsAndTemps = [...items, ...temps];

expect(messagesService.itemsAndTemps.length).toBe(14);
messagesService.itemsAndTemps.forEach((item: any) => {
expect(item.__isClone).toBe(false);
});
expect(messagesService.itemsAndTemps.sort()).toStrictEqual(itemsAndTemps.sort());
})

test('itemsAndClones getter returns items and clones in itemsById and clonesById', async () => {
const { items, temps, clonesById } = messagesService;

items.forEach((item: any, i: number) => {
item.i = i;
if (i % 2 === 0) { return; }
item.clone()
})
temps.forEach((temp: any, i: number) => {
temp.i = i;
if (i % 2 === 0) { return; }
temp.clone()
})

const itemsAndClones = items.map((item: any) => clonesById[item.id] || item)

expect(messagesService.itemsAndClones.length).toBe(7);
messagesService.itemsAndClones.forEach((item: any) => {
expect(item).toHaveProperty('__isClone');
if (item.i % 2 === 0) {
expect(item.__isClone).toBe(false);
} else {
expect(item.__isClone).toBe(true);
}
});
expect(messagesService.itemsAndClones.sort()).toStrictEqual(itemsAndClones.sort());
})

test('itemsTempsAndClones getter returns items, temps and clones in itemsById, tempsById and clonesById', async () => {
const { items, temps, itemsAndTemps, clonesById } = messagesService;

items.forEach((item: any, i: number) => {
item.i = i;
if (i % 2 === 0) { return; }
item.clone()
})
temps.forEach((temp: any, i: number) => {
temp.i = i;
if (i % 2 === 0) { return; }
temp.clone()
})

const itemsTempsAndClones = itemsAndTemps.map((item: any) => {
if (clonesById[item.id]) { return clonesById[item.id] }
if (item.__isTemp && clonesById[item.__tempId]) { return clonesById[item.__tempId] }
return item;
})

expect(messagesService.itemsTempsAndClones.sort()).toStrictEqual(itemsTempsAndClones.sort());
})
})
Loading