Skip to content

Commit 1038da9

Browse files
committed
Migrate vault entries to new schema (#2092)
* Migrate existing vault entries to new schema * Read+write new vault schema * Drop VaultEntry table * Refactor vaultPrismaFragments * Remove wrong comment * Remove TODO * Fix possible race condition on update of vault key * Remove lib/object.js
1 parent 4620160 commit 1038da9

File tree

6 files changed

+444
-95
lines changed

6 files changed

+444
-95
lines changed

api/resolvers/vault.js

Lines changed: 41 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,42 +1,61 @@
11
import { E_VAULT_KEY_EXISTS, GqlAuthenticationError, GqlInputError } from '@/lib/error'
2+
import { getWalletByType } from '@/wallets/common'
3+
import { deleteVault, hasVault, vaultNewSchematoTypedef, vaultPrismaFragments } from '@/wallets/vault'
24

35
export default {
46
Query: {
57
getVaultEntries: async (parent, args, { me, models }) => {
68
if (!me) throw new GqlAuthenticationError()
79

8-
return await models.vaultEntry.findMany({ where: { userId: me.id } })
10+
const wallets = await models.wallet.findMany({
11+
where: { userId: me.id },
12+
include: vaultPrismaFragments.include()
13+
})
14+
15+
const vaultEntries = []
16+
for (const wallet of wallets) {
17+
vaultEntries.push(...vaultNewSchematoTypedef(wallet).vaultEntries)
18+
}
19+
20+
return vaultEntries
921
}
1022
},
1123
Mutation: {
1224
// atomic vault migration
1325
updateVaultKey: async (parent, { entries, hash }, { me, models }) => {
1426
if (!me) throw new GqlAuthenticationError()
1527
if (!hash) throw new GqlInputError('hash required')
16-
const txs = []
1728

1829
const { vaultKeyHash: oldKeyHash } = await models.user.findUnique({ where: { id: me.id } })
1930
if (oldKeyHash) {
20-
if (oldKeyHash !== hash) {
21-
throw new GqlInputError('vault key already set', E_VAULT_KEY_EXISTS)
22-
} else {
31+
if (oldKeyHash === hash) {
2332
return true
2433
}
25-
} else {
26-
txs.push(models.user.update({
27-
where: { id: me.id },
28-
data: { vaultKeyHash: hash }
29-
}))
34+
throw new GqlInputError('vault key already set', E_VAULT_KEY_EXISTS)
3035
}
3136

32-
for (const entry of entries) {
33-
txs.push(models.vaultEntry.update({
34-
where: { userId_key: { userId: me.id, key: entry.key } },
35-
data: { value: entry.value, iv: entry.iv }
36-
}))
37-
}
38-
await models.$transaction(txs)
39-
return true
37+
return await models.$transaction(async tx => {
38+
const wallets = await tx.wallet.findMany({ where: { userId: me.id } })
39+
for (const wallet of wallets) {
40+
const def = getWalletByType(wallet.type)
41+
await tx.wallet.update({
42+
where: { id: wallet.id },
43+
data: {
44+
[def.walletField]: {
45+
update: vaultPrismaFragments.upsert({ ...wallet, vaultEntries: entries })
46+
}
47+
}
48+
})
49+
}
50+
51+
// optimistic concurrency control: make sure the user's vault key didn't change while we were updating the wallets
52+
await tx.user.update({
53+
where: { id: me.id, vaultKeyHash: oldKeyHash },
54+
data: { vaultKeyHash: hash }
55+
})
56+
57+
return true
58+
})
4059
},
4160
clearVault: async (parent, args, { me, models }) => {
4261
if (!me) throw new GqlAuthenticationError()
@@ -45,7 +64,10 @@ export default {
4564
where: { id: me.id },
4665
data: { vaultKeyHash: '' }
4766
}))
48-
txs.push(models.vaultEntry.deleteMany({ where: { userId: me.id } }))
67+
68+
const wallets = await models.wallet.findMany({ where: { userId: me.id } })
69+
txs.push(...wallets.filter(hasVault).map(wallet => deleteVault(models, wallet)))
70+
4971
await models.$transaction(txs)
5072
return true
5173
}

api/resolvers/wallet.js

Lines changed: 32 additions & 62 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@ import { canReceive, getWalletByType } from '@/wallets/common'
2828
import performPaidAction from '../paidAction'
2929
import performPayingAction from '../payingAction'
3030
import { timeoutSignal, withTimeout } from '@/lib/time'
31+
import { deleteVault, hasVault, vaultNewSchematoTypedef, vaultPrismaFragments } from '@/wallets/vault'
3132

3233
function injectResolvers (resolvers) {
3334
console.group('injected GraphQL resolvers:')
@@ -42,11 +43,13 @@ function injectResolvers (resolvers) {
4243
// this mutation was sent from an unsynced client
4344
// to pass validation, we need to add the existing vault entries for validation
4445
// in case the client is removing the receiving config
45-
existingVaultEntries = await models.vaultEntry.findMany({
46+
const wallet = await models.wallet.findUnique({
4647
where: {
47-
walletId: Number(data.id)
48-
}
48+
id: Number(data.id)
49+
},
50+
include: vaultPrismaFragments.include()
4951
})
52+
existingVaultEntries = vaultNewSchematoTypedef(wallet).vaultEntries
5053
}
5154

5255
const validData = await validateWallet(walletDef,
@@ -158,17 +161,17 @@ const resolvers = {
158161
throw new GqlAuthenticationError()
159162
}
160163

161-
return await models.wallet.findMany({
162-
include: {
163-
vaultEntries: true
164-
},
164+
const wallets = await models.wallet.findMany({
165+
include: vaultPrismaFragments.include(),
165166
where: {
166167
userId: me.id
167168
},
168169
orderBy: {
169170
priority: 'asc'
170171
}
171172
})
173+
174+
return wallets.map(vaultNewSchematoTypedef)
172175
},
173176
withdrawl: getWithdrawl,
174177
direct: async (parent, { id }, { me, models }) => {
@@ -554,7 +557,11 @@ const resolvers = {
554557
}
555558

556559
const logger = walletLogger({ wallet, models })
557-
await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
560+
561+
await models.$transaction([
562+
hasVault(wallet) ? deleteVault(models, wallet) : null,
563+
models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
564+
].filter(Boolean))
558565

559566
if (canReceive({ def: getWalletByType(wallet.type), config: wallet.wallet })) {
560567
logger.info('details for receiving deleted')
@@ -823,78 +830,41 @@ async function upsertWallet (
823830

824831
const txs = []
825832

826-
if (id) {
827-
const oldVaultEntries = await models.vaultEntry.findMany({ where: { userId: me.id, walletId: Number(id) } })
833+
const walletWithVault = { ...wallet, vaultEntries }
828834

829-
// createMany is the set difference of the new - old
830-
// deleteMany is the set difference of the old - new
831-
// updateMany is the intersection of the old and new
832-
const difference = (a = [], b = [], key = 'key') => a.filter(x => !b.find(y => y[key] === x[key]))
833-
const intersectionMerge = (a = [], b = [], key = 'key') => a.filter(x => b.find(y => y[key] === x[key]))
834-
.map(x => ({ [key]: x[key], ...b.find(y => y[key] === x[key]) }))
835+
if (id) {
836+
const dbWallet = await models.wallet.findUnique({
837+
where: { id: Number(id), userId: me.id }
838+
})
835839

836840
txs.push(
837841
models.wallet.update({
838842
where: { id: Number(id), userId: me.id },
839843
data: {
840844
enabled,
841845
priority,
842-
// client only wallets have no receive config and thus don't have their own table
843-
...(Object.keys(recvConfig).length > 0
844-
? {
845-
[wallet.field]: {
846-
upsert: {
847-
create: recvConfig,
848-
update: recvConfig
849-
}
850-
}
851-
}
852-
: {}),
853-
...(vaultEntries
854-
? {
855-
vaultEntries: {
856-
deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({
857-
userId: me.id, key
858-
})),
859-
create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, iv, value }) => ({
860-
key, iv, value, userId: me.id
861-
})),
862-
update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, iv, value }) => ({
863-
where: { userId_key: { userId: me.id, key } },
864-
data: { value, iv }
865-
}))
866-
}
867-
}
868-
: {})
869-
846+
[wallet.field]: {
847+
upsert: {
848+
create: { ...recvConfig, ...vaultPrismaFragments.create(walletWithVault) },
849+
update: { ...recvConfig, ...vaultPrismaFragments.upsert(walletWithVault) }
850+
},
851+
// XXX the check is required because the update would fail if there is no row to delete ...
852+
update: hasVault(dbWallet) ? vaultPrismaFragments.deleteMissing(walletWithVault) : undefined
853+
}
870854
},
871-
include: {
872-
vaultEntries: true
873-
}
855+
include: vaultPrismaFragments.include(walletWithVault)
874856
})
875857
)
876858
} else {
877859
txs.push(
878860
models.wallet.create({
879-
include: {
880-
vaultEntries: true
881-
},
861+
include: vaultPrismaFragments.include(walletWithVault),
882862
data: {
883863
enabled,
884864
priority,
885865
userId: me.id,
886866
type: wallet.type,
887-
// client only wallets have no receive config and thus don't have their own table
888-
...(Object.keys(recvConfig).length > 0 ? { [wallet.field]: { create: recvConfig } } : {}),
889-
...(vaultEntries
890-
? {
891-
vaultEntries: {
892-
createMany: {
893-
data: vaultEntries?.map(({ key, iv, value }) => ({ key, iv, value, userId: me.id }))
894-
}
895-
}
896-
}
897-
: {})
867+
[wallet.field]: { create: { ...recvConfig, ...vaultPrismaFragments.create(walletWithVault) } }
898868
}
899869
})
900870
)
@@ -931,7 +901,7 @@ async function upsertWallet (
931901
}
932902

933903
const [upsertedWallet] = await models.$transaction(txs)
934-
return upsertedWallet
904+
return vaultNewSchematoTypedef(upsertedWallet)
935905
}
936906

937907
export async function createWithdrawal (parent, { invoice, maxFee }, { me, models, lnd, headers, wallet, logger }) {

0 commit comments

Comments
 (0)