Skip to content
Merged
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
18 changes: 18 additions & 0 deletions docs/content/1.guide/4.repository/4.updating-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,3 +62,21 @@ useRepo(Flight)
```

As opposed to updating records by the `save` method, it only accepts an object as the argument (not an array). Also, it will not normalize the data, and any nested relationships will be ignored.

## Update Or Create

The `updateOrCreate` method updates the first record matching the attributes given as the first argument with the values given as the second argument. If no record matches, a new record is created from the merged attributes and values. This works like Laravel's `updateOrCreate`.

```js
// Update the age of the first user named "Jane Doe",
// or create a new user with that name and age 41.
useRepo(User).updateOrCreate({ name: 'Jane Doe' }, { age: 41 })
```

If you only need to create the record when it doesn't exist yet — without touching an existing one — use `firstOrCreate` instead. It returns the first record matching the attributes, or creates one from the merged attributes and values.

```js
// Returns the existing "Jane Doe" unchanged,
// or creates her with age 40.
const user = useRepo(User).firstOrCreate({ name: 'Jane Doe' }, { age: 40 })
```
35 changes: 35 additions & 0 deletions packages/pinia-orm/src/repository/Repository.ts
Original file line number Diff line number Diff line change
Expand Up @@ -423,6 +423,41 @@ export class Repository<M extends Model = Model> {
return this.query().save(records)
}

/**
* Get the first record matching the given attributes or persist a new
* record made from the merged attributes and values.
*/
firstOrCreate (attributes: Element, values: Element = {}): M {
const record = this.matching(attributes)

return record ?? this.save({ ...attributes, ...values })
}

/**
* Update the first record matching the given attributes with the given
* values or persist a new record made from the merged attributes and values.
*/
updateOrCreate (attributes: Element, values: Element = {}): M {
const record = this.matching(attributes)

if (!record) { return this.save({ ...attributes, ...values }) }

const primaryKey = this.getModel().$primaryKey()
const keyValues = (isArray(primaryKey) ? primaryKey : [primaryKey]).reduce<Element>((keys, key) => {
keys[key] = record[key as keyof M] as any
return keys
}, {})

return this.save({ ...keyValues, ...values })
}

/**
* Get the first record matching the given attributes.
*/
protected matching (attributes: Element): Item<M> {
return this.query().where((model: M) => Object.entries(attributes).every(([field, value]) => model[field as keyof M] === value)).first()
}

/**
* Create and persist model with default values.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import { describe, expect, it } from 'vitest'

import { Model, useRepo } from '../../../src'
import { Attr, Num, Str } from '../../../src/decorators'
import { assertState } from '../../helpers'

describe('feature/repository/first_or_create', () => {
class User extends Model {
static entity = 'users'

@Attr() id!: any
@Str('') name!: string
@Num(0) age!: number
}

it('returns the first record matching the attributes', () => {
const userRepo = useRepo(User)

userRepo.save([
{ id: 1, name: 'John Doe', age: 30 },
{ id: 2, name: 'Jane Doe', age: 40 },
])

const user = userRepo.firstOrCreate({ name: 'Jane Doe' }, { age: 50 })

expect(user.id).toBe(2)
expect(user.age).toBe(40)

assertState({
users: {
1: { id: 1, name: 'John Doe', age: 30 },
2: { id: 2, name: 'Jane Doe', age: 40 },
},
})
})

it('creates a new record from the merged attributes and values if none matches', () => {
const userRepo = useRepo(User)

userRepo.save({ id: 1, name: 'John Doe', age: 30 })

const user = userRepo.firstOrCreate({ id: 2, name: 'Jane Doe' }, { age: 40 })

expect(user.id).toBe(2)

assertState({
users: {
1: { id: 1, name: 'John Doe', age: 30 },
2: { id: 2, name: 'Jane Doe', age: 40 },
},
})
})
})
114 changes: 114 additions & 0 deletions packages/pinia-orm/tests/feature/repository/update_or_create.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
import { describe, expect, it } from 'vitest'

import { Model, useRepo } from '../../../src'
import { Attr, Num, Str, Uid } from '../../../src/decorators'
import { assertState } from '../../helpers'

describe('feature/repository/update_or_create', () => {
class User extends Model {
static entity = 'users'

@Attr() id!: any
@Str('') name!: string
@Num(0) age!: number
}

it('creates a new record if no record matches the attributes', () => {
const userRepo = useRepo(User)

userRepo.save({ id: 1, name: 'John Doe', age: 30 })

const user = userRepo.updateOrCreate({ id: 2 }, { name: 'Jane Doe', age: 40 })

expect(user.id).toBe(2)

assertState({
users: {
1: { id: 1, name: 'John Doe', age: 30 },
2: { id: 2, name: 'Jane Doe', age: 40 },
},
})
})

it('updates the matching record with the given values', () => {
const userRepo = useRepo(User)

userRepo.save([
{ id: 1, name: 'John Doe', age: 30 },
{ id: 2, name: 'Jane Doe', age: 40 },
])

const user = userRepo.updateOrCreate({ name: 'Jane Doe' }, { age: 41 })

expect(user.id).toBe(2)

assertState({
users: {
1: { id: 1, name: 'John Doe', age: 30 },
2: { id: 2, name: 'Jane Doe', age: 41 },
},
})
})

it('matches records by multiple attributes', () => {
const userRepo = useRepo(User)

userRepo.save([
{ id: 1, name: 'John Doe', age: 30 },
{ id: 2, name: 'John Doe', age: 40 },
])

userRepo.updateOrCreate({ name: 'John Doe', age: 40 }, { name: 'Johnny Doe' })

assertState({
users: {
1: { id: 1, name: 'John Doe', age: 30 },
2: { id: 2, name: 'Johnny Doe', age: 40 },
},
})
})

it('updates records with a composite primary key', () => {
class RoleUser extends Model {
static entity = 'roleUser'

static primaryKey = ['role_id', 'user_id']

@Attr(null) role_id!: number | null
@Attr(null) user_id!: number | null
@Num(0) level!: number
}

const roleUserRepo = useRepo(RoleUser)

roleUserRepo.save([
{ role_id: 1, user_id: 1, level: 1 },
{ role_id: 2, user_id: 1, level: 2 },
])

roleUserRepo.updateOrCreate({ role_id: 2, user_id: 1 }, { level: 5 })

assertState({
roleUser: {
'[2,1]': { role_id: 2, user_id: 1, level: 5 },
'[1,1]': { role_id: 1, user_id: 1, level: 1 },
},
})
})

it('creates a record with a generated uid', () => {
class Tag extends Model {
static entity = 'tags'

@Uid() id!: string
@Str('') name!: string
}

const tagRepo = useRepo(Tag)

const tag = tagRepo.updateOrCreate({ name: 'news' })

expect(tag.id).not.toBeNull()
expect(tagRepo.all().length).toBe(1)
})
})
Loading