diff --git a/docs/content/1.guide/2.model/5.lifecycle-hooks.md b/docs/content/1.guide/2.model/5.lifecycle-hooks.md index 679d66faa..0b232d5cb 100644 --- a/docs/content/1.guide/2.model/5.lifecycle-hooks.md +++ b/docs/content/1.guide/2.model/5.lifecycle-hooks.md @@ -44,6 +44,8 @@ class User extends Model { Mutation lifecycle hooks are called when you mutate data in the store. The corresponding hook name are `creating`, `updating`, `saving`, `deleting`, `created`, `updated`, `deleted` and `saved`. +The `save`, `update`, `new`, `destroy`, `delete` and `fresh` methods fire the matching hooks. `fresh` fires `saving`/`creating` and `saved`/`created` for every record — pass `{ raw: true }` as second argument to persist the data without firing any hooks. Note that `insert` and `flush` do not fire any mutation hooks. + When in `creating`, `updating` or `saving`, you can modify the record directly to mutate the data to be saved. You also have access to the data passed to the model. (Also available in `created`, `updated` and `saved`) diff --git a/docs/content/1.guide/4.repository/3.inserting-data.md b/docs/content/1.guide/4.repository/3.inserting-data.md index 45d30fec3..3467f79c5 100644 --- a/docs/content/1.guide/4.repository/3.inserting-data.md +++ b/docs/content/1.guide/4.repository/3.inserting-data.md @@ -139,6 +139,12 @@ useRepo(User).fresh({ id: 3, name: 'Johnny Doe' }) } ``` +The `fresh` method fires the `saving`/`creating` and `saved`/`created` lifecycle hooks for every record. If you want to persist raw data without triggering any hooks — for example when initializing the store with backend data — pass `{ raw: true }` as second argument. + +```js +useRepo(User).fresh({ id: 3, name: 'Johnny Doe' }, { raw: true }) +``` + And of course, you may pass an array of records as well. ```js diff --git a/packages/pinia-orm/src/query/Query.ts b/packages/pinia-orm/src/query/Query.ts index da983d75b..d1acaa83b 100644 --- a/packages/pinia-orm/src/query/Query.ts +++ b/packages/pinia-orm/src/query/Query.ts @@ -880,14 +880,38 @@ export class Query { /** * Insert the given records to the store by replacing any existing records. + * The `saving`/`creating` and `saved`/`created` lifecycle hooks are fired + * for every record unless `raw` is set to `true`. Records for which a + * before hook returns `false` are not persisted. */ - fresh (records: Element[]): Collection - fresh (record: Element): M - fresh (records: Element | Element[]): M | Collection { + fresh (records: Element[], options?: { raw?: boolean }): Collection + fresh (record: Element, options?: { raw?: boolean }): M + fresh (records: Element | Element[], options: { raw?: boolean } = {}): M | Collection { this.hydratedDataCache.clear() const models = this.hydrate(records, { action: 'update' }) + const modelArray = isArray(models) ? models : [models] + const recordArray = isArray(records) ? records : [records] + + let persistableModels = modelArray + const afterHooks: (() => void)[] = [] + + if (!options.raw) { + persistableModels = modelArray.filter((model, index) => { + const record = recordArray[index] + const isSaving = model.$self().saving(model, record) + const isCreating = model.$self().creating(model, record) + if (isSaving === false || isCreating === false) { return false } + + afterHooks.push(() => { + model.$self().saved(model, record) + model.$self().created(model, record) + }) + return true + }) + } - this.commit('fresh', this.compile(models)) + this.commit('fresh', this.compile(persistableModels)) + 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 e7b7684d2..3812cffe1 100644 --- a/packages/pinia-orm/src/repository/Repository.ts +++ b/packages/pinia-orm/src/repository/Repository.ts @@ -442,10 +442,10 @@ export class Repository { /** * Insert the given records to the store by replacing any existing records. */ - fresh (records: Element[]): Collection - fresh (record: Element): M - fresh (records: Element | Element[]): M | Collection { - return this.query().fresh(records) + fresh (records: Element[], options?: { raw?: boolean }): Collection + fresh (record: Element, options?: { raw?: boolean }): M + fresh (records: Element | Element[], options?: { raw?: boolean }): M | Collection { + return this.query().fresh(records, options) } /** diff --git a/packages/pinia-orm/tests/feature/hooks/fresh.spec.ts b/packages/pinia-orm/tests/feature/hooks/fresh.spec.ts new file mode 100644 index 000000000..5a6778aab --- /dev/null +++ b/packages/pinia-orm/tests/feature/hooks/fresh.spec.ts @@ -0,0 +1,114 @@ +import { describe, expect, it, vi } from 'vitest' + +import { Model, useRepo } from '../../../src' +import { Attr, Str } from '../../../src/decorators' +import { assertState } from '../../helpers' + +describe('feature/hooks/fresh', () => { + it('fires the saving/creating and saved/created hooks', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + } + + const savingMethod = vi.spyOn(User, 'saving') + const creatingMethod = vi.spyOn(User, 'creating') + const savedMethod = vi.spyOn(User, 'saved') + const createdMethod = vi.spyOn(User, 'created') + + useRepo(User).fresh([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + ]) + + expect(savingMethod).toHaveBeenCalledTimes(2) + expect(creatingMethod).toHaveBeenCalledTimes(2) + expect(savedMethod).toHaveBeenCalledTimes(2) + expect(createdMethod).toHaveBeenCalledTimes(2) + + assertState({ + users: { + 1: { id: 1, name: 'John Doe' }, + 2: { id: 2, name: 'Jane Doe' }, + }, + }) + }) + + it('is able to change values in the saving hook', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + + static saving (model: Model) { + model.name = 'Modified' + } + } + + useRepo(User).fresh({ id: 1, name: 'John Doe' }) + + assertState({ + users: { + 1: { id: 1, name: 'Modified' }, + }, + }) + }) + + it('does not persist records for which a before hook returns false', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + + static creating (model: Model) { + return model.id !== 2 + } + } + + const createdMethod = vi.spyOn(User, 'created') + + useRepo(User).fresh([ + { id: 1, name: 'John Doe' }, + { id: 2, name: 'Jane Doe' }, + ]) + + expect(createdMethod).toHaveBeenCalledTimes(1) + + assertState({ + users: { + 1: { id: 1, name: 'John Doe' }, + }, + }) + }) + + it('fires no hooks when the raw option is set', () => { + class User extends Model { + static entity = 'users' + + @Attr() id!: any + @Str('') name!: string + } + + const savingMethod = vi.spyOn(User, 'saving') + const creatingMethod = vi.spyOn(User, 'creating') + const savedMethod = vi.spyOn(User, 'saved') + const createdMethod = vi.spyOn(User, 'created') + + useRepo(User).fresh([{ id: 1, name: 'John Doe' }], { raw: true }) + + expect(savingMethod).not.toHaveBeenCalled() + expect(creatingMethod).not.toHaveBeenCalled() + expect(savedMethod).not.toHaveBeenCalled() + expect(createdMethod).not.toHaveBeenCalled() + + assertState({ + users: { + 1: { id: 1, name: 'John Doe' }, + }, + }) + }) +})