Skip to content

Allow nested metadata #364

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

Closed
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
76 changes: 76 additions & 0 deletions src/BaseControllerV2.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -280,6 +280,47 @@ describe('getAnonymizedState', () => {

expect(anonymizedState).toEqual({ txMeta: { history: [{ value: 9 }], value: 10 } });
});

it('should accept nested metadata', () => {
const anonymizeTransactionHistory = (history: { hash: string; value: number }[]) => {
return history.map((entry) => {
return { value: entry.value };
});
};

const anonymizedState = getAnonymizedState(
{
txMeta: {
hash: '0x123',
history: [
{
hash: '0x123',
value: 9,
},
],
value: 10,
},
},
{
txMeta: {
hash: {
anonymous: false,
persist: false,
},
history: {
anonymous: anonymizeTransactionHistory,
persist: false,
},
value: {
anonymous: true,
persist: false,
},
},
},
);

expect(anonymizedState).toEqual({ txMeta: { history: [{ value: 9 }], value: 10 } });
});
});

describe('getPersistentState', () => {
Expand Down Expand Up @@ -321,4 +362,39 @@ describe('getPersistentState', () => {
);
expect(persistentState).toEqual({ password: 'secret password', privateKey: '123' });
});

it('should accept nested metadata', () => {
const anonymizedState = getPersistentState(
{
txMeta: {
hash: '0x123',
history: [
{
hash: '0x123',
value: 9,
},
],
value: 10,
},
},
{
txMeta: {
hash: {
anonymous: false,
persist: false,
},
history: {
anonymous: false,
persist: true,
},
value: {
anonymous: false,
persist: true,
},
},
},
);

expect(anonymizedState).toEqual({ txMeta: { history: [{ hash: '0x123', value: 9 }], value: 10 } });
});
});
76 changes: 65 additions & 11 deletions src/BaseControllerV2.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,9 @@ export type Anonymizer<T> = (value: T) => T extends Primitive ? T : RecursivePar
* get an anonymized representation of the state.
*/
export type StateMetadata<T> = {
[P in keyof T]: StatePropertyMetadata<T[P]>;
[P in keyof T]: T[P] extends Primitive
? StatePropertyMetadata<T[P]>
: StateMetadata<T[P]> | StatePropertyMetadata<T[P]>;
};

/**
Expand Down Expand Up @@ -153,10 +155,23 @@ export class BaseController<S extends Record<string, unknown>> {
}

// This function acts as a type guard. Using a `typeof` conditional didn't seem to work.
function isAnonymizingFunction<T>(x: boolean | Anonymizer<T>): x is Anonymizer<T> {
function isAnonymizingFunction<T>(x: boolean | Anonymizer<T> | StateMetadata<T>): x is Anonymizer<T> {
return typeof x === 'function';
}

function isStatePropertyMetadata<T>(x: StatePropertyMetadata<T> | StateMetadata<T>): x is StatePropertyMetadata<T> {
const sortedKeys = Object.keys(x).sort();
return sortedKeys.length === 2 && sortedKeys[0] === 'anonymous' && sortedKeys[1] === 'persist';
}

function isStateMetadata<T>(x: StatePropertyMetadata<T> | StateMetadata<T>): x is StateMetadata<T> {
return !isStatePropertyMetadata(x);
}

function isPrimitive<T>(x: Primitive | RecursivePartial<T>): x is Primitive {
return ['boolean', 'string', 'number', 'null'].includes(typeof x);
}

/**
* Returns an anonymized representation of the controller state.
*
Expand All @@ -174,11 +189,28 @@ export function getAnonymizedState<S extends Record<string, any>>(
): RecursivePartial<S> {
return Object.keys(state).reduce((anonymizedState, _key) => {
const key: keyof S = _key; // https://stackoverflow.com/questions/63893394/string-cannot-be-used-to-index-type-t
const metadataValue = metadata[key].anonymous;
if (isAnonymizingFunction(metadataValue)) {
anonymizedState[key] = metadataValue(state[key]);
} else if (metadataValue) {
anonymizedState[key] = state[key];
const propertyMetadata = metadata[key];
const propertyValue = state[key];
// Ignore statement required because 'else' case is unreachable due to type
// The 'else if' condition is still required because it acts as a type guard
/* istanbul ignore else */
if (isStateMetadata(propertyMetadata)) {
// Ignore statement required because this case is unreachable due to type
// This condition is still required because it acts as a type guard
/* istanbul ignore next */
if (isPrimitive(propertyValue) || Array.isArray(propertyValue)) {
throw new Error(`Cannot assign metadata object to primitive type or array`);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
anonymizedState[key] = getAnonymizedState(propertyValue, propertyMetadata);
Copy link
Member Author

Choose a reason for hiding this comment

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

This line is where the error is:

Type 'RecursivePartial<S[keyof S]>' is not assignable to type 'S[keyof S] extends (infer U)[] ? RecursivePartial[] : S[keyof S] extends Primitive ? S[keyof S] : RecursivePartial<S[keyof S]>'. ts(2322)

I tried to resolve this by adding type guards to "prove" that the first two conditions were not met.

Copy link
Member Author

Choose a reason for hiding this comment

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

My guess is that it failed because TypeScript doesn't understand that this is the same S[keyof S] as the other S[keyof S]? That is, these could be referring to two different properties that have different types. But it is the same property here, and I don't know how to tell TypeScript this.

Copy link
Member Author

Choose a reason for hiding this comment

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

Well, I simplified the RecursivePartial type locally to test this theory and now I'm less sure. It looks like the Primitive type guard may not be working. I'm not 100% sure though.

Copy link
Member Author

Choose a reason for hiding this comment

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

I got this working for now with a // ts-ignore comment 😬

} else if (isStatePropertyMetadata(propertyMetadata)) {
const metadataValue = propertyMetadata.anonymous;
if (isAnonymizingFunction(metadataValue)) {
anonymizedState[key] = metadataValue(state[key]);
} else if (metadataValue) {
anonymizedState[key] = state[key];
}
}
return anonymizedState;
}, {} as RecursivePartial<S>);
Expand All @@ -191,12 +223,34 @@ export function getAnonymizedState<S extends Record<string, any>>(
* @param metadata - The controller state metadata, which describes which pieces of state should be persisted
* @returns The subset of controller state that should be persisted
*/
export function getPersistentState<S extends Record<string, any>>(state: S, metadata: StateMetadata<S>): Partial<S> {
export function getPersistentState<S extends Record<string, any>>(
state: S,
metadata: StateMetadata<S>,
): RecursivePartial<S> {
return Object.keys(state).reduce((persistedState, _key) => {
const key: keyof S = _key; // https://stackoverflow.com/questions/63893394/string-cannot-be-used-to-index-type-t
if (metadata[key].persist) {
persistedState[key] = state[key];
const propertyMetadata = metadata[key];
const propertyValue = state[key];

// Ignore statement required because 'else' case is unreachable due to type
// The 'else if' condition is still required because it acts as a type guard
/* istanbul ignore else */
if (isStateMetadata(propertyMetadata)) {
// Ignore statement required because this case is unreachable due to type
// This condition is still required because it acts as a type guard
/* istanbul ignore next */
if (isPrimitive(propertyValue) || Array.isArray(propertyValue)) {
throw new Error(`Cannot assign metadata object to primitive type or array`);
}
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
persistedState[key] = getPersistentState(propertyValue, propertyMetadata);
} else if (isStatePropertyMetadata(propertyMetadata)) {
if (propertyMetadata.persist) {
persistedState[key] = state[key];
}
}

return persistedState;
}, {} as Partial<S>);
}, {} as RecursivePartial<S>);
}