Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix(tokens-info): Refactor delete missing keys for keep internal keys #285

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
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
23 changes: 13 additions & 10 deletions scripts/update-rtdb.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import { diffString } from 'json-diff'
import { loadUpdateRTDBConfig } from '../src/config'
import { FirebaseClient } from '../src/clients/firebase-client'
import { deleteMissingKeysUpdateRequest } from '../src/utils/utils'
import { keepInternalKeys } from '../src/utils/utils'
import { getCeloRTDBMetadata } from '../src'

async function main() {
Expand All @@ -28,17 +28,20 @@ async function main() {
await firebaseClient.writeToPath(rtdbLocation, data)
console.log(`Wrote data to ${rtdbLocation}.`)
} else {
let updateRequest = data
if (overrideType.deleteMissingKeys) {
const deleteMissingKeys = deleteMissingKeysUpdateRequest(
Copy link
Contributor Author

@dievazqu dievazqu Mar 7, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The name of this might a bit misleading, but this was only removing missing keys in the root level, so basically it was removing the whole token data if not present.

data,
rtdbData,
const updateRequest = keepInternalKeys(
data,
rtdbData,
overrideType.keptInternalKeys,
)
const updateDiff = diffString(rtdbData, updateRequest)
if (!updateDiff) {
console.log(
`Diff is empty for data at ${rtdbLocation}, skipping...`,
)
updateRequest = { ...updateRequest, ...deleteMissingKeys }
} else {
await firebaseClient.writeToPath(rtdbLocation, updateRequest)
console.log(`Updated data at ${rtdbLocation}.`)
}
// TODO: Avoid updating the node if we already know there aren't changes
await firebaseClient.updateToPath(rtdbLocation, updateRequest)
console.log(`Updated data at ${rtdbLocation}.`)
}
}
}
Expand Down
6 changes: 0 additions & 6 deletions src/clients/firebase-client.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import * as admin from 'firebase-admin'
import { mapNestedJsonIntoPlain } from '../utils/utils'
import { UpdateRTDBConfig } from '../types'

export class FirebaseClient {
Expand All @@ -21,11 +20,6 @@ export class FirebaseClient {
return data.val()
}

async updateToPath(path: string, data: any): Promise<void> {
const ref = await this.firebaseDb.ref(path)
await ref.update(mapNestedJsonIntoPlain(data))
}

async writeToPath(path: string, data: any): Promise<void> {
const ref = await this.firebaseDb.ref(path)
await ref.set(data)
Expand Down
6 changes: 5 additions & 1 deletion src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,11 @@ export function getCeloRTDBMetadata(environment: Environment): RTDBMetadata[] {
data: transformCeloTokensForRTDB(tokensInfo),
schema: RTDBAddressToTokenInfoSchema,
rtdbLocation: 'tokensInfo',
overrideType: OverrideType.DeleteMissingKeysAndUpdate,
overrideType: OverrideType.KeepInternalKeys([
'historicalUsdPrices',
'usdPrice',
'priceFetchedAt',
]),
},
]
}
Expand Down
8 changes: 4 additions & 4 deletions src/types.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import Joi from 'joi'

export class OverrideType {
static readonly OverrideAll = new OverrideType(true, true)
static readonly OnlyUpdates = new OverrideType(false, false)
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This wasn't used.

static readonly DeleteMissingKeysAndUpdate = new OverrideType(false, true)
static readonly OverrideAll = new OverrideType(true, [])
static readonly KeepInternalKeys = (keepInternalKeys?: string[]) =>
new OverrideType(false, keepInternalKeys)

// private to disallow creating other instances of this type
private constructor(
public readonly shouldOverride: boolean,
public readonly deleteMissingKeys: boolean,
public readonly keptInternalKeys: string[] = [],
) {}
}

Expand Down
195 changes: 79 additions & 116 deletions src/utils/utils.test.ts
Original file line number Diff line number Diff line change
@@ -1,128 +1,91 @@
import { deleteMissingKeysUpdateRequest, mapNestedJsonIntoPlain } from './utils'
import { keepInternalKeys } from './utils'

describe('Keep Internal Keys', () => {
const valuesToUpdate = {
'0x1234': {
address: '0x1234',
decimal: 18,
imageUrl: 'https://example.com',
},
'0x1235': {
address: '0x1235',
decimal: 8,
imageUrl: 'https://example2.com',
},
'0x1236': {
address: '0x1236',
decimal: 18,
imageUrl: 'https://example3.com',
},
}

const currentValues = {
'0x1234': {
address: '0x1234old',
decimal: 18,
imageUrl: 'https://example.com',
usdPrice: '1',
historicalUsdPrices: {
lastDay: {
price: '1.2',
},
},
},
'0x1235': {
address: '0x1235old',
decimal: 8,
imageUrl: 'https://example2.com',
usdPrice: '2',
historicalUsdPrices: {
lastDay: {
price: '2.2',
},
},
},
}

describe('Map Nested Json Into', () => {
it('should map correctly when the json contains one level', () => {
const inputJson = {
key1: 'stringValue',
key2: 3,
}
const expectedJson = inputJson
it("returns the expected object when there aren't keys to keep", () => {
const updateObject = keepInternalKeys(valuesToUpdate, currentValues, [])

const result = mapNestedJsonIntoPlain(inputJson)
expect(result).toMatchObject(expectedJson)
expect(updateObject).toMatchObject(valuesToUpdate)
})

it('should map correctly when the json contains a nested level', () => {
const inputJson = {
key1: {
key3: 4,
key4: 'stringValue',
it('returns the expected object when there are internal keys to keep', () => {
const updateObject = keepInternalKeys(valuesToUpdate, currentValues, [
'usdPrice',
'historicalUsdPrices',
])

const expectedObject = {
'0x1234': {
address: '0x1234',
decimal: 18,
imageUrl: 'https://example.com',
usdPrice: '1',
historicalUsdPrices: {
lastDay: {
price: '1.2',
},
},
},
key2: 3,
}
const expectedJson = {
'key1/key3': 4,
'key1/key4': 'stringValue',
key2: 3,
}

const result = mapNestedJsonIntoPlain(inputJson)
expect(result).toMatchObject(expectedJson)
})

it('should map correctly when the json contains multiple nested level', () => {
const inputJson = {
key1: {
key3: 4,
key4: 5,
key5: {
key6: 'stringValue',
key7: {
key8: 9,
'0x1235': {
address: '0x1235',
decimal: 8,
imageUrl: 'https://example2.com',
usdPrice: '2',
historicalUsdPrices: {
lastDay: {
price: '2.2',
},
},
},
key2: 3,
}
const expectedJson = {
'key1/key3': 4,
'key1/key4': 5,
'key1/key5/key6': 'stringValue',
'key1/key5/key7/key8': 9,
key2: 3,
}

const result = mapNestedJsonIntoPlain(inputJson)
expect(result).toMatchObject(expectedJson)
})

it('should map array to object with indexes as key', () => {
const inputJson = {
key1: ['this', 'is', 'a', 'test'],
key2: 3,
}

const expectedJson = {
'key1/0': 'this',
'key1/1': 'is',
'key1/2': 'a',
'key1/3': 'test',
key2: 3,
}

const result = mapNestedJsonIntoPlain(inputJson)
expect(result).toMatchObject(expectedJson)
})
})

describe('Delete Missing Keys UpdateRequest', () => {
it('return expected update request when there are missing keys', () => {
const expected = {
key1: 1,
key2: 'value',
}

const current = {
key1: 2,
key3: 'shouldBeDeleted',
}

const updateObject = deleteMissingKeysUpdateRequest(expected, current)

const expectedResult = {
key3: null,
}

expect(updateObject).toMatchObject(expectedResult)
})

it('return empty update request when there are not missing keys', () => {
const expected = {
key1: 1,
key2: 'value',
key3: 'extra',
}

const current = {
key1: 2,
key2: 'shouldNotBeDeleted',
}

const updateObject = deleteMissingKeysUpdateRequest(expected, current)

expect(updateObject).toMatchObject({})
})

it('creates all keys if the RTDB instance is clean', () => {
const expected = {
key1: 1,
key2: 'value',
'0x1236': {
address: '0x1236',
decimal: 18,
imageUrl: 'https://example3.com',
},
}

const current = null

const updateObject = deleteMissingKeysUpdateRequest(expected, current)

expect(updateObject).toMatchObject({})
expect(expectedObject).toMatchObject(updateObject)
})
})
50 changes: 14 additions & 36 deletions src/utils/utils.ts
Original file line number Diff line number Diff line change
@@ -1,41 +1,19 @@
export function mapNestedJsonIntoPlain(json: any): any {
if (!isNonEmptyObject(json)) {
return json
}
export function keepInternalKeys(
expected: any,
current: any,
keptInternalKeys: string[],
) {
const result: any = {}

return Object.keys(json).reduce(
(result, current) => ({
...result,
...prependKey(current, mapNestedJsonIntoPlain(json[current])),
}),
{},
)
}
for (const key of Object.keys(expected)) {
result[key] = expected[key]

function prependKey(key: string, json: any) {
if (!isNonEmptyObject(json)) {
return { [key]: json }
for (const internalKey of keptInternalKeys) {
if (current[key] && current[key][internalKey]) {
result[key][internalKey] = current[key][internalKey]
}
}
}

return Object.keys(json).reduce(
(result, current) => ({
...result,
[`${key}/${current}`]: json[current],
}),
{},
)
}

function isNonEmptyObject(json: any): boolean {
return typeof json === 'object' && json !== null
}

export function deleteMissingKeysUpdateRequest(expected: any, current: any) {
if (!isNonEmptyObject(current)) return {}
const expectedKeys = new Set(Object.keys(expected))
const currentKeys = Object.keys(current)

return currentKeys
.filter((key) => !expectedKeys.has(key))
.reduce((result, key) => ({ ...result, [key]: null }), {})
return result
}