diff --git a/docs/content/2.api/3.query/update.md b/docs/content/2.api/3.query/update.md new file mode 100644 index 000000000..538568f40 --- /dev/null +++ b/docs/content/2.api/3.query/update.md @@ -0,0 +1,24 @@ +--- +title: 'update()' +description: 'Update the record matching the query chain.' +--- + +# `update()` + +## Usage + +````ts +import { useRepo } from 'pinia-orm' +import User from './models/User' + +const userRepo = useRepo(User) + +// update all records by query with the given properties +userRepo.where('name', 'Jane Doe').update({ age: 50 }) +```` + +## Typescript Declarations + +````ts +function update(record: Element): Collection +```` diff --git a/docs/content/2.api/4.repository/update.md b/docs/content/2.api/4.repository/update.md new file mode 100644 index 000000000..06fc854e7 --- /dev/null +++ b/docs/content/2.api/4.repository/update.md @@ -0,0 +1,30 @@ +--- +title: 'update()' +description: 'Tries to update given record or records by their id.' +--- + +# `update()` + +## Usage + +````ts +import { useRepo } from 'pinia-orm' +import User from './models/User' + +const userRepo = useRepo(User) + +// update specific record +userRepo.update({ id: 1, age: 50 }) + +// update specific records +userRepo.update([{ id: 1, age: 50 }, { id: 2, age: 50 }]) + +// throws an warning if the record to update is not found +userRepo.update({ id: 999, age: 50 }) +```` + +## Typescript Declarations + +````ts +function update(records: Element | Element[]): M | M[] +```` diff --git a/packages/pinia-orm/src/composables/useStoreActions.ts b/packages/pinia-orm/src/composables/useStoreActions.ts index 238302e55..90071c463 100644 --- a/packages/pinia-orm/src/composables/useStoreActions.ts +++ b/packages/pinia-orm/src/composables/useStoreActions.ts @@ -15,16 +15,6 @@ export function useStoreActions() { fresh(this: DataStore, records: Elements) { this.data = records }, - destroy(this: DataStore, ids: string[]): void { - const data: Elements = {} - - for (const id in this.data) { - if (!ids.includes(id)) - data[id] = this.data[id] - } - - this.data = data - }, /** * Commit `delete` change to the store. */ diff --git a/packages/pinia-orm/src/query/Query.ts b/packages/pinia-orm/src/query/Query.ts index 339562901..8cd1e0264 100644 --- a/packages/pinia-orm/src/query/Query.ts +++ b/packages/pinia-orm/src/query/Query.ts @@ -5,7 +5,7 @@ import { isArray, isEmpty, isFunction, - orderBy, + orderBy, throwWarning, } from '../support/Utils' import type { Collection, Element, Elements, GroupedCollection, Item, NormalizedData } from '../data/Data' import type { Database } from '../database/Database' @@ -33,6 +33,8 @@ import type { WhereSecondaryClosure, } from './Options' +export type SaveModes = Array<'create' | 'update' | 'insert'> + export class Query { /** * The database instance. @@ -701,8 +703,14 @@ export class Query { * Save the given records to the store with data normalization. */ save(records: Element[]): M[] - save(record: Element): M + save(records: Element): M save(records: Element | Element[]): M | M[] { + return this.processSavingElements(records) + } + + processSavingElements(records: Element[], modes?: SaveModes): M[] + processSavingElements(records: Element, modes?: SaveModes): M + processSavingElements(records: Element | Element[], modes: SaveModes = ['create', 'update', 'insert']): M | M[] { let processedData: [Element | Element[], NormalizedData] = this.newInterpreter().process(records) const modelTypes = this.model.$types() const isChildEntity = this.model.$baseEntity() !== this.model.$entity() @@ -735,7 +743,7 @@ export class Query { const query = this.newQuery(entity) const elements = entities[entity] - query.saveElements(elements) + query.saveElements(elements, modes) } return this.revive(data) as M | M[] } @@ -743,7 +751,7 @@ export class Query { /** * Save the given elements to the store. */ - saveElements(elements: Elements): void { + protected saveElements(elements: Elements, modes: SaveModes): void { const newData = {} as Elements const currentData = this.commit('all') const afterSavingHooks = [] @@ -751,23 +759,44 @@ export class Query { for (const id in elements) { const record = elements[id] const existing = currentData[id] - const model = existing - ? this.hydrate({ ...existing, ...record }, { operation: 'set', action: 'update' }) - : this.hydrate(record, { operation: 'set', action: 'save' }) + let model: M | null = null + if (existing) { + if (modes.includes('update')) + model = this.hydrate({ ...existing, ...record }, { operation: 'set', action: 'update' }) - const isSaving = model.$self().saving(model, record) - const isUpdatingOrCreating = existing ? model.$self().updating(model, record) : model.$self().creating(model, record) - if (isSaving === false || isUpdatingOrCreating === false) - continue + if (!model && modes.includes('insert')) + throwWarning(['Inserting a record which already exist.'], 'Existing', existing, 'New Record', record) + } + else { + if (modes.includes('create') || modes.includes('insert')) + model = this.hydrate(record, { operation: 'set', action: 'save' }) + + if (!model && modes.includes('update')) + throwWarning(['Updating a record which does not exist.'], record) + } - afterSavingHooks.push(() => model.$self().saved(model, record)) - afterSavingHooks.push(() => existing ? model.$self().updated(model, record) : model.$self().created(model, record)) - newData[id] = model.$getAttributes() - if (Object.values(model.$types()).length > 0 && !newData[id][model.$typeKey()]) - newData[id][model.$typeKey()] = record[model.$typeKey()] + if (model) { + const isSaving = model.$self().saving(model, record) + const isUpdatingOrCreating = existing ? model.$self().updating(model, record) : model.$self().creating(model, record) + if (isSaving === false || isUpdatingOrCreating === false) + continue + + // @ts-expect-error model is not null + afterSavingHooks.push(() => model.$self().saved(model, record)) + // @ts-expect-error model is not null + afterSavingHooks.push(() => existing ? model.$self().updated(model, record) : model.$self().created(model, record)) + newData[id] = model.$getAttributes() + if (Object.values(model.$types()).length > 0 && !newData[id][model.$typeKey()]) + newData[id][model.$typeKey()] = record[model.$typeKey()] + } } + if (Object.keys(newData).length > 0) { - this.commit('save', newData) + if (modes.length === 1) + this.commit(modes[0], newData) + else + this.commit('save', newData) + afterSavingHooks.forEach(hook => hook()) } } @@ -778,11 +807,7 @@ export class Query { insert(records: Element[]): Collection insert(record: Element): M insert(records: Element | Element[]): M | Collection { - const models = this.hydrate(records) - - this.commit('insert', this.compile(models)) - - return models + return this.processSavingElements(records, ['insert']) } /** @@ -799,7 +824,7 @@ export class Query { } /** - * Update the reocrd matching the query chain. + * Update the record matching the query chain. */ update(record: Element): Collection { const models = this.get(false) @@ -842,7 +867,7 @@ export class Query { const [afterHooks, removeIds] = this.dispatchDeleteHooks(model) if (!removeIds.includes(model.$getIndexId())) { - this.commit('destroy', [model.$getIndexId()]) + this.commit('delete', [model.$getIndexId()]) afterHooks.forEach(hook => hook()) } @@ -858,7 +883,7 @@ export class Query { const [afterHooks, removeIds] = this.dispatchDeleteHooks(models) const checkedIds = this.getIndexIdsFromCollection(models).filter(id => !removeIds.includes(id)) - this.commit('destroy', checkedIds) + this.commit('delete', checkedIds) afterHooks.forEach(hook => hook()) return models diff --git a/packages/pinia-orm/src/repository/Repository.ts b/packages/pinia-orm/src/repository/Repository.ts index 8b01a2d6f..4731c8785 100644 --- a/packages/pinia-orm/src/repository/Repository.ts +++ b/packages/pinia-orm/src/repository/Repository.ts @@ -343,10 +343,19 @@ export class Repository { */ save(records: Element[]): M[] save(record: Element): M - public save(records: Element | Element[]): M | M[] { + save(records: Element | Element[]): M | M[] { return this.query().save(records) } + /** + * Tries to update given record or records by their id. + */ + update(records: Element[]): M[] + update(record: Element): M + update(records: Element | Element[]): M | M[] { + return this.query().processSavingElements(records, ['update']) + } + /** * Create and persist model with default values. */ diff --git a/packages/pinia-orm/src/support/Utils.ts b/packages/pinia-orm/src/support/Utils.ts index 51d8f8f5f..6fccece3a 100644 --- a/packages/pinia-orm/src/support/Utils.ts +++ b/packages/pinia-orm/src/support/Utils.ts @@ -202,6 +202,11 @@ export function throwError( throw new Error(['[Pinia ORM]'].concat(message).join(' ')) } +export function throwWarning(message: string[], ...variables: any) { + // eslint-disable-next-line no-console + console.warn(['[Pinia ORM]'].concat(message).join(' '), ...variables) +} + /** * Asserts that the condition is truthy, throwing immediately if not. */ diff --git a/packages/pinia-orm/tests/feature/repository/insert.spec.ts b/packages/pinia-orm/tests/feature/repository/insert.spec.ts index 347f8d746..d2fa5e7ec 100644 --- a/packages/pinia-orm/tests/feature/repository/insert.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/insert.spec.ts @@ -1,15 +1,24 @@ -import { describe, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Model, useRepo } from '../../../src' -import { Attr, Str } from '../../../src/decorators' +import { Attr, HasMany, Num, Str } from '../../../src/decorators' import { assertState } from '../../helpers' describe('feature/repository/insert', () => { class User extends Model { static entity = 'users' - @Attr() id!: any - @Str('') name!: string + @Attr() declare id: any + @Str('') declare name: string + @HasMany(() => Post, 'userId') declare posts: Post[] + } + + class Post extends Model { + static entity = 'posts' + + @Num(0) declare id: number + @Attr() declare userId: number + @Str('') declare title: string } it('inserts a record to the store', () => { @@ -44,6 +53,38 @@ describe('feature/repository/insert', () => { const userRepo = useRepo(User) userRepo.insert([]) - assertState({ users: {} }) + assertState({}) + }) + + it('inserts with relation', () => { + const userRepo = useRepo(User) + + userRepo.insert({ id: 1, name: 'John Doe', posts: [{ id: 1, title: 'New Post' }] }) + assertState({ + users: { + 1: { id: 1, name: 'John Doe' }, + }, + posts: { + 1: { id: 1, userId: 1, title: 'New Post' }, + }, + }) + }) + + it('throws a warning if the same ids are inserted', () => { + const userRepo = useRepo(User) + const logger = vi.spyOn(console, 'warn') + + userRepo.insert({ id: 1, name: 'John Doe', posts: [{ id: 1, title: 'New Post' }] }) + userRepo.insert({ id: 1, name: 'John Doe2', posts: [{ id: 1, title: 'New Post 2' }] }) + + expect(logger).toBeCalledTimes(2) + assertState({ + users: { + 1: { id: 1, name: 'John Doe' }, + }, + posts: { + 1: { id: 1, userId: 1, title: 'New Post' }, + }, + }) }) }) diff --git a/packages/pinia-orm/tests/feature/repository/update.spec.ts b/packages/pinia-orm/tests/feature/repository/update.spec.ts index 2a52ab10d..d8daf5d54 100644 --- a/packages/pinia-orm/tests/feature/repository/update.spec.ts +++ b/packages/pinia-orm/tests/feature/repository/update.spec.ts @@ -1,4 +1,4 @@ -import { describe, expect, it } from 'vitest' +import { describe, expect, it, vi } from 'vitest' import { Model, useRepo } from '../../../src' import { Attr, Num, Str } from '../../../src/decorators' @@ -81,4 +81,51 @@ describe('feature/repository/update', () => { }, }) }) + + it('updates with repository update', () => { + const userRepo = useRepo(User) + + fillState({ + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 }, + }, + }) + + userRepo.update({ id: 1, age: 50 }) + + assertState({ + users: { + 1: { id: 1, name: 'John Doe', age: 50 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 }, + }, + }) + }) + + it('throws warning if no updatable record found', () => { + const userRepo = useRepo(User) + const logger = vi.spyOn(console, 'warn') + + fillState({ + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 }, + }, + }) + + userRepo.update({ id: 4, age: 50 }) + + expect(logger).toBeCalledTimes(1) + + assertState({ + users: { + 1: { id: 1, name: 'John Doe', age: 40 }, + 2: { id: 2, name: 'Jane Doe', age: 30 }, + 3: { id: 3, name: 'Johnny Doe', age: 20 }, + }, + }) + }) })