Skip to content

Commit c6ff371

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 4d8743b commit c6ff371

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
@@ -29,6 +29,7 @@ import { canReceive, getWalletByType } from '@/wallets/common'
2929
import performPaidAction from '../paidAction'
3030
import performPayingAction from '../payingAction'
3131
import { timeoutSignal, withTimeout } from '@/lib/time'
32+
import { deleteVault, hasVault, vaultNewSchematoTypedef, vaultPrismaFragments } from '@/wallets/vault'
3233

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

5356
const validData = await validateWallet(walletDef,
@@ -159,17 +162,17 @@ const resolvers = {
159162
throw new GqlAuthenticationError()
160163
}
161164

162-
return await models.wallet.findMany({
163-
include: {
164-
vaultEntries: true
165-
},
165+
const wallets = await models.wallet.findMany({
166+
include: vaultPrismaFragments.include(),
166167
where: {
167168
userId: me.id
168169
},
169170
orderBy: {
170171
priority: 'asc'
171172
}
172173
})
174+
175+
return wallets.map(vaultNewSchematoTypedef)
173176
},
174177
withdrawl: getWithdrawl,
175178
direct: async (parent, { id }, { me, models }) => {
@@ -569,7 +572,11 @@ const resolvers = {
569572
}
570573

571574
const logger = walletLogger({ wallet, models })
572-
await models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
575+
576+
await models.$transaction([
577+
hasVault(wallet) ? deleteVault(models, wallet) : null,
578+
models.wallet.delete({ where: { userId: me.id, id: Number(id) } })
579+
].filter(Boolean))
573580

574581
if (canReceive({ def: getWalletByType(wallet.type), config: wallet.wallet })) {
575582
logger.info('details for receiving deleted')
@@ -838,78 +845,41 @@ async function upsertWallet (
838845

839846
const txs = []
840847

841-
if (id) {
842-
const oldVaultEntries = await models.vaultEntry.findMany({ where: { userId: me.id, walletId: Number(id) } })
848+
const walletWithVault = { ...wallet, vaultEntries }
843849

844-
// createMany is the set difference of the new - old
845-
// deleteMany is the set difference of the old - new
846-
// updateMany is the intersection of the old and new
847-
const difference = (a = [], b = [], key = 'key') => a.filter(x => !b.find(y => y[key] === x[key]))
848-
const intersectionMerge = (a = [], b = [], key = 'key') => a.filter(x => b.find(y => y[key] === x[key]))
849-
.map(x => ({ [key]: x[key], ...b.find(y => y[key] === x[key]) }))
850+
if (id) {
851+
const dbWallet = await models.wallet.findUnique({
852+
where: { id: Number(id), userId: me.id }
853+
})
850854

851855
txs.push(
852856
models.wallet.update({
853857
where: { id: Number(id), userId: me.id },
854858
data: {
855859
enabled,
856860
priority,
857-
// client only wallets have no receive config and thus don't have their own table
858-
...(Object.keys(recvConfig).length > 0
859-
? {
860-
[wallet.field]: {
861-
upsert: {
862-
create: recvConfig,
863-
update: recvConfig
864-
}
865-
}
866-
}
867-
: {}),
868-
...(vaultEntries
869-
? {
870-
vaultEntries: {
871-
deleteMany: difference(oldVaultEntries, vaultEntries, 'key').map(({ key }) => ({
872-
userId: me.id, key
873-
})),
874-
create: difference(vaultEntries, oldVaultEntries, 'key').map(({ key, iv, value }) => ({
875-
key, iv, value, userId: me.id
876-
})),
877-
update: intersectionMerge(oldVaultEntries, vaultEntries, 'key').map(({ key, iv, value }) => ({
878-
where: { userId_key: { userId: me.id, key } },
879-
data: { value, iv }
880-
}))
881-
}
882-
}
883-
: {})
884-
861+
[wallet.field]: {
862+
upsert: {
863+
create: { ...recvConfig, ...vaultPrismaFragments.create(walletWithVault) },
864+
update: { ...recvConfig, ...vaultPrismaFragments.upsert(walletWithVault) }
865+
},
866+
// XXX the check is required because the update would fail if there is no row to delete ...
867+
update: hasVault(dbWallet) ? vaultPrismaFragments.deleteMissing(walletWithVault) : undefined
868+
}
885869
},
886-
include: {
887-
vaultEntries: true
888-
}
870+
include: vaultPrismaFragments.include(walletWithVault)
889871
})
890872
)
891873
} else {
892874
txs.push(
893875
models.wallet.create({
894-
include: {
895-
vaultEntries: true
896-
},
876+
include: vaultPrismaFragments.include(walletWithVault),
897877
data: {
898878
enabled,
899879
priority,
900880
userId: me.id,
901881
type: wallet.type,
902-
// client only wallets have no receive config and thus don't have their own table
903-
...(Object.keys(recvConfig).length > 0 ? { [wallet.field]: { create: recvConfig } } : {}),
904-
...(vaultEntries
905-
? {
906-
vaultEntries: {
907-
createMany: {
908-
data: vaultEntries?.map(({ key, iv, value }) => ({ key, iv, value, userId: me.id }))
909-
}
910-
}
911-
}
912-
: {})
882+
[wallet.field]: { create: { ...recvConfig, ...vaultPrismaFragments.create(walletWithVault) } }
913883
}
914884
})
915885
)
@@ -946,7 +916,7 @@ async function upsertWallet (
946916
}
947917

948918
const [upsertedWallet] = await models.$transaction(txs)
949-
return upsertedWallet
919+
return vaultNewSchematoTypedef(upsertedWallet)
950920
}
951921

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

0 commit comments

Comments
 (0)