Skip to content

Commit 80b1c12

Browse files
committed
feat(pinia-orm): add updateOrCreate & firstOrCreate repository methods
Laravel-style helpers: firstOrCreate returns the first record matching the given attributes or persists a new one from the merged attributes and values. updateOrCreate updates the first matching record with the given values or creates it. closes #1833
1 parent 018acee commit 80b1c12

4 files changed

Lines changed: 220 additions & 0 deletions

File tree

docs/content/1.guide/4.repository/4.updating-data.md

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,21 @@ useRepo(Flight)
6262
```
6363

6464
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.
65+
66+
## Update Or Create
67+
68+
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`.
69+
70+
```js
71+
// Update the age of the first user named "Jane Doe",
72+
// or create a new user with that name and age 41.
73+
useRepo(User).updateOrCreate({ name: 'Jane Doe' }, { age: 41 })
74+
```
75+
76+
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.
77+
78+
```js
79+
// Returns the existing "Jane Doe" unchanged,
80+
// or creates her with age 40.
81+
const user = useRepo(User).firstOrCreate({ name: 'Jane Doe' }, { age: 40 })
82+
```

packages/pinia-orm/src/repository/Repository.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -423,6 +423,41 @@ export class Repository<M extends Model = Model> {
423423
return this.query().save(records)
424424
}
425425

426+
/**
427+
* Get the first record matching the given attributes or persist a new
428+
* record made from the merged attributes and values.
429+
*/
430+
firstOrCreate (attributes: Element, values: Element = {}): M {
431+
const record = this.matching(attributes)
432+
433+
return record ?? this.save({ ...attributes, ...values })
434+
}
435+
436+
/**
437+
* Update the first record matching the given attributes with the given
438+
* values or persist a new record made from the merged attributes and values.
439+
*/
440+
updateOrCreate (attributes: Element, values: Element = {}): M {
441+
const record = this.matching(attributes)
442+
443+
if (!record) { return this.save({ ...attributes, ...values }) }
444+
445+
const primaryKey = this.getModel().$primaryKey()
446+
const keyValues = (isArray(primaryKey) ? primaryKey : [primaryKey]).reduce<Element>((keys, key) => {
447+
keys[key] = record[key as keyof M] as any
448+
return keys
449+
}, {})
450+
451+
return this.save({ ...keyValues, ...values })
452+
}
453+
454+
/**
455+
* Get the first record matching the given attributes.
456+
*/
457+
protected matching (attributes: Element): Item<M> {
458+
return this.query().where((model: M) => Object.entries(attributes).every(([field, value]) => model[field as keyof M] === value)).first()
459+
}
460+
426461
/**
427462
* Create and persist model with default values.
428463
*/
Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { Model, useRepo } from '../../../src'
4+
import { Attr, Num, Str } from '../../../src/decorators'
5+
import { assertState } from '../../helpers'
6+
7+
describe('feature/repository/first_or_create', () => {
8+
class User extends Model {
9+
static entity = 'users'
10+
11+
@Attr() id!: any
12+
@Str('') name!: string
13+
@Num(0) age!: number
14+
}
15+
16+
it('returns the first record matching the attributes', () => {
17+
const userRepo = useRepo(User)
18+
19+
userRepo.save([
20+
{ id: 1, name: 'John Doe', age: 30 },
21+
{ id: 2, name: 'Jane Doe', age: 40 },
22+
])
23+
24+
const user = userRepo.firstOrCreate({ name: 'Jane Doe' }, { age: 50 })
25+
26+
expect(user.id).toBe(2)
27+
expect(user.age).toBe(40)
28+
29+
assertState({
30+
users: {
31+
1: { id: 1, name: 'John Doe', age: 30 },
32+
2: { id: 2, name: 'Jane Doe', age: 40 },
33+
},
34+
})
35+
})
36+
37+
it('creates a new record from the merged attributes and values if none matches', () => {
38+
const userRepo = useRepo(User)
39+
40+
userRepo.save({ id: 1, name: 'John Doe', age: 30 })
41+
42+
const user = userRepo.firstOrCreate({ id: 2, name: 'Jane Doe' }, { age: 40 })
43+
44+
expect(user.id).toBe(2)
45+
46+
assertState({
47+
users: {
48+
1: { id: 1, name: 'John Doe', age: 30 },
49+
2: { id: 2, name: 'Jane Doe', age: 40 },
50+
},
51+
})
52+
})
53+
})
Lines changed: 114 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,114 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { Model, useRepo } from '../../../src'
4+
import { Attr, Num, Str, Uid } from '../../../src/decorators'
5+
import { assertState } from '../../helpers'
6+
7+
describe('feature/repository/update_or_create', () => {
8+
class User extends Model {
9+
static entity = 'users'
10+
11+
@Attr() id!: any
12+
@Str('') name!: string
13+
@Num(0) age!: number
14+
}
15+
16+
it('creates a new record if no record matches the attributes', () => {
17+
const userRepo = useRepo(User)
18+
19+
userRepo.save({ id: 1, name: 'John Doe', age: 30 })
20+
21+
const user = userRepo.updateOrCreate({ id: 2 }, { name: 'Jane Doe', age: 40 })
22+
23+
expect(user.id).toBe(2)
24+
25+
assertState({
26+
users: {
27+
1: { id: 1, name: 'John Doe', age: 30 },
28+
2: { id: 2, name: 'Jane Doe', age: 40 },
29+
},
30+
})
31+
})
32+
33+
it('updates the matching record with the given values', () => {
34+
const userRepo = useRepo(User)
35+
36+
userRepo.save([
37+
{ id: 1, name: 'John Doe', age: 30 },
38+
{ id: 2, name: 'Jane Doe', age: 40 },
39+
])
40+
41+
const user = userRepo.updateOrCreate({ name: 'Jane Doe' }, { age: 41 })
42+
43+
expect(user.id).toBe(2)
44+
45+
assertState({
46+
users: {
47+
1: { id: 1, name: 'John Doe', age: 30 },
48+
2: { id: 2, name: 'Jane Doe', age: 41 },
49+
},
50+
})
51+
})
52+
53+
it('matches records by multiple attributes', () => {
54+
const userRepo = useRepo(User)
55+
56+
userRepo.save([
57+
{ id: 1, name: 'John Doe', age: 30 },
58+
{ id: 2, name: 'John Doe', age: 40 },
59+
])
60+
61+
userRepo.updateOrCreate({ name: 'John Doe', age: 40 }, { name: 'Johnny Doe' })
62+
63+
assertState({
64+
users: {
65+
1: { id: 1, name: 'John Doe', age: 30 },
66+
2: { id: 2, name: 'Johnny Doe', age: 40 },
67+
},
68+
})
69+
})
70+
71+
it('updates records with a composite primary key', () => {
72+
class RoleUser extends Model {
73+
static entity = 'roleUser'
74+
75+
static primaryKey = ['role_id', 'user_id']
76+
77+
@Attr(null) role_id!: number | null
78+
@Attr(null) user_id!: number | null
79+
@Num(0) level!: number
80+
}
81+
82+
const roleUserRepo = useRepo(RoleUser)
83+
84+
roleUserRepo.save([
85+
{ role_id: 1, user_id: 1, level: 1 },
86+
{ role_id: 2, user_id: 1, level: 2 },
87+
])
88+
89+
roleUserRepo.updateOrCreate({ role_id: 2, user_id: 1 }, { level: 5 })
90+
91+
assertState({
92+
roleUser: {
93+
'[2,1]': { role_id: 2, user_id: 1, level: 5 },
94+
'[1,1]': { role_id: 1, user_id: 1, level: 1 },
95+
},
96+
})
97+
})
98+
99+
it('creates a record with a generated uid', () => {
100+
class Tag extends Model {
101+
static entity = 'tags'
102+
103+
@Uid() id!: string
104+
@Str('') name!: string
105+
}
106+
107+
const tagRepo = useRepo(Tag)
108+
109+
const tag = tagRepo.updateOrCreate({ name: 'news' })
110+
111+
expect(tag.id).not.toBeNull()
112+
expect(tagRepo.all().length).toBe(1)
113+
})
114+
})

0 commit comments

Comments
 (0)