I'm new to the ES6 Proxy object, and am encountering an error I don't understand when attempting to call concat on an array that has been proxied.
Background:
I thought the ES6 Proxy would work perfectly as a way to verify the "purity" of a reducer function in my React/Redux application. I can wrap my state object in a proxy that throws an error if I ever attempt to mutate that object. I'm using something based on the on-change library to do this:
const triggersOnChange = (object, onChange) => {
const handler = {
get (target, property, receiver) {
try {
return new Proxy(target[property], handler)
} catch (err) {
return Reflect.get(target, property, receiver);
}
}
defineProperty (target, property, descriptor) {
onChange()
return Reflect.defineProperty(target, property, descriptor)
}
deleteProperty (target, property) {
onChange()
return Reflect.deleteProperty(target, property)
}
}
return new Proxy(object, handler)
}
And here's an example test of how I intend to use the proxy wrapper:
describe('reducer', () => {
test('it returns an updated state object', () => {
const state = triggersOnChange({ items: [] }, () => {
throw new Error('Oops! You mutated the state object')
})
const action = {
payload: { item: 'foobar' }
}
expect(reducer(state, action)).toEqual({
items: [action.payload.item]
})
})
})
If I implement a "bad" reducer that mutates the state object, my test throws an error as intended:
const reducer = (state, action) => {
state.items.push(action.payload.item) // bad
return state
}
// test throws error "Oops! You mutated the state object"
But when I "purify" my reducer by returning a new state object, I get a different error that I don't quite understand:
const reducer = (state, action) => {
return Object.assign({}, state, {
items: state.items.concat(action.payload.item)
})
}
/*
TypeError: 'get' on proxy: property 'prototype' is a read-only and
non-configurable data property on the proxy target but the proxy did
not return its actual value (expected '[object Array]' but got
'[object Object]')
at Proxy.concat (<anonymous>)
*/
Am I missing something about proxy behavior here? Or is this perhaps an issue with the proxy-chaining behavior that results from my get trap? I initially thought this was a problem with using a proxy within Object.assign, but I encountered the same error when debugging before my reducer's return statement, where I actually use Object.assign. Help!
Edit: Happy to revise this question to make it a little more generic, but I’m not 100% what the issue is so I’ll wait and see if I can get any answers.
Your problem can be replicated with the following code:
var obj = {};
Object.defineProperty(obj, "prop", {
configurable: false,
value: {},
});
var p = new Proxy(obj, {
get(target, property, receiver) {
return new Proxy(Reflect.get(target, property, receiver), {});
},
});
var val = p.prop;
The core of the issue is that objects have invariants that they must stay consistent with, even when accessed via a Proxy object, and in this case you are breaking one of those invariants. If you look at the specification for Proxy's get, it states:
[[Get]] for proxy objects enforces the following invariants:
The value reported for a property must be the same as the value of the corresponding target object property if the target object property is a non-writable, non-configurable own data property.
The value reported for a property must be undefined if the corresponding target object property is a non-configurable own accessor property that has undefined as its [[Get]] attribute.
and in your case, you are not maintaining that first invariant, because even when a property is non-writable and non-configurable, you are returning a wrapping Proxy. The easiest approach would be to ensure that the proper value is returned in that case.
While we're at it, I'll also recommend using typeof explicitly instead of using try/catch so it is clearer.
get(target, property, receiver) {
const desc = Object.getOwnPropertyDescriptor(target, property);
const value = Reflect.get(target, property, receiver);
if (desc && !desc.writable && !desc.configurable) return value;
if (typeof value === "object" && value !== null) return new Proxy(value, handler);
else return value;
},
Related
The two test cases blow both pass. I simply don't understand the behavior. It seems that JavaScript Proxy cannot trap property getting inside a getter function.
test('JS Proxy normal method', () => {
class Store {
hidden = false;
visible() {
return !this.hidden;
}
}
const accessList: PropertyKey[] = [];
const proxy = new Proxy<Store>(new Store(), {
get: (target: any, propertyKey: PropertyKey) => {
accessList.push(propertyKey);
return Reflect.get(target, propertyKey);
},
});
expect(proxy.visible()).toBe(true);
expect(accessList).toEqual(['visible', 'hidden']);
});
test('JS Proxy getter method', () => {
class Store {
hidden = false;
get visible() {
return !this.hidden;
}
}
const accessList: PropertyKey[] = [];
const proxy = new Proxy<Store>(new Store(), {
get: (target: any, propertyKey: PropertyKey) => {
accessList.push(propertyKey);
return Reflect.get(target, propertyKey);
},
});
expect(proxy.visible).toBe(true);
expect(accessList).toEqual(['visible']);
});
You're missing the receiver of the property access. The property might be defined on a different object than it is accessed on, and your Reflect.get call needs to take that into account. In particular, the receiver you get as a argument of the get trap is the proxy itself, and that's also the object you want to evaluate the getter against, so that its this value refers to the proxy. However, Reflect.get(target, propertyKey) is the same as target[propertyKey], where the this value in the getter is set to the target and the .hidden property access can't be detected by your proxy.
export const FilterUndefined = <T extends object>(obj: T): T => {
return Object.entries(obj).reduce((acc, [key, value]) => {
return value ? { ...acc, [key]: value } : acc;
}, {}) as T;
};
I'm migrating a database and part of cleaning up the old data structure, some of the values for some keys end up being literaly undefined. The key will still exist and will have the value undefined
I made this function but after modifying a class object with it, it will no longer be an instanceof the same class. How could I make this return an object that's instanceof the same class as the input parameter?
The as T makes TS compiler shut up but that's it.
I've also tried to get the prototype of that object and return new prototype(obj) or return new prototype.constructor(obj)
The console log of the prototype looks like this:
PROTOTYPE TestClass {}
I'm testing using this setup:
it('should return the same type that it receives', () => {
class TestClass {
name: string;
optionalProperty?: any;
}
let testObject = new TestClass();
testObject.name = 'My Name';
testObject.optionalProperty = undefined;
console.log(testObject instanceof TestClass);
testObject = FilterUndefined(testObject);
console.log(testObject instanceof TestClass);
console.log(testObject);
expect(testObject).instanceOf(TestClass);
});
EDIT: JSFiddle: https://jsfiddle.net/3sdg98xt/2/ but copy-pasted from vscode without any issues running it i'm getting an error 'execpted expression, got ';'
This solution will mutate the input object by removing the keys with undefined values.
function removeUndefined <T>(object: T): T {
for (const id in object) {
if (object[id] === undefined) {
delete object[id];
}
}
return object;
}
It seems it works for your test case: Test in typescript playground
Yes, with each iteration of reduce you are returning a new {} which is an instance Object.
So to make the return object of same instace as of argument you should make following changes.
export const FilterUndefined = (obj) => {
return Object.entries(obj).reduce((acc, [key, value]) => {
if (value) {acc[key] = value;}
else {delete acc[key]}
return acc;
}, new obj.constructor);
};
or you can use new obj.__proto__.constructor as per the target of typescript output you are using.
Reply in case you have typescript issues with this code snippet.
Is it possible to specify that a field in GraphQL should be a blackbox, similar to how Flow has an "any" type? I have a field in my schema that should be able to accept any arbitrary value, which could be a String, Boolean, Object, Array, etc.
I've come up with a middle-ground solution. Rather than trying to push this complexity onto GraphQL, I'm opting to just use the String type and JSON.stringifying my data before setting it on the field. So everything gets stringified, and later in my application when I need to consume this field, I JSON.parse the result to get back the desired object/array/boolean/ etc.
#mpen's answer is great, but I opted for a more compact solution:
const { GraphQLScalarType } = require('graphql')
const { Kind } = require('graphql/language')
const ObjectScalarType = new GraphQLScalarType({
name: 'Object',
description: 'Arbitrary object',
parseValue: (value) => {
return typeof value === 'object' ? value
: typeof value === 'string' ? JSON.parse(value)
: null
},
serialize: (value) => {
return typeof value === 'object' ? value
: typeof value === 'string' ? JSON.parse(value)
: null
},
parseLiteral: (ast) => {
switch (ast.kind) {
case Kind.STRING: return JSON.parse(ast.value)
case Kind.OBJECT: throw new Error(`Not sure what to do with OBJECT for ObjectScalarType`)
default: return null
}
}
})
Then my resolvers looks like:
{
Object: ObjectScalarType,
RootQuery: ...
RootMutation: ...
}
And my .gql looks like:
scalar Object
type Foo {
id: ID!
values: Object!
}
Yes. Just create a new GraphQLScalarType that allows anything.
Here's one I wrote that allows objects. You can extend it a bit to allow more root types.
import {GraphQLScalarType} from 'graphql';
import {Kind} from 'graphql/language';
import {log} from '../debug';
import Json5 from 'json5';
export default new GraphQLScalarType({
name: "Object",
description: "Represents an arbitrary object.",
parseValue: toObject,
serialize: toObject,
parseLiteral(ast) {
switch(ast.kind) {
case Kind.STRING:
return ast.value.charAt(0) === '{' ? Json5.parse(ast.value) : null;
case Kind.OBJECT:
return parseObject(ast);
}
return null;
}
});
function toObject(value) {
if(typeof value === 'object') {
return value;
}
if(typeof value === 'string' && value.charAt(0) === '{') {
return Json5.parse(value);
}
return null;
}
function parseObject(ast) {
const value = Object.create(null);
ast.fields.forEach((field) => {
value[field.name.value] = parseAst(field.value);
});
return value;
}
function parseAst(ast) {
switch (ast.kind) {
case Kind.STRING:
case Kind.BOOLEAN:
return ast.value;
case Kind.INT:
case Kind.FLOAT:
return parseFloat(ast.value);
case Kind.OBJECT:
return parseObject(ast);
case Kind.LIST:
return ast.values.map(parseAst);
default:
return null;
}
}
For most use cases, you can use a JSON scalar type to achieve this sort of functionality. There's a number of existing libraries you can just import rather than writing your own scalar -- for example, graphql-type-json.
If you need a more fine-tuned approach, than you'll want to write your own scalar type. Here's a simple example that you can start with:
const { GraphQLScalarType, Kind } = require('graphql')
const Anything = new GraphQLScalarType({
name: 'Anything',
description: 'Any value.',
parseValue: (value) => value,
parseLiteral,
serialize: (value) => value,
})
function parseLiteral (ast) {
switch (ast.kind) {
case Kind.BOOLEAN:
case Kind.STRING:
return ast.value
case Kind.INT:
case Kind.FLOAT:
return Number(ast.value)
case Kind.LIST:
return ast.values.map(parseLiteral)
case Kind.OBJECT:
return ast.fields.reduce((accumulator, field) => {
accumulator[field.name.value] = parseLiteral(field.value)
return accumulator
}, {})
case Kind.NULL:
return null
default:
throw new Error(`Unexpected kind in parseLiteral: ${ast.kind}`)
}
}
Note that scalars are used both as outputs (when returned in your response) and as inputs (when used as values for field arguments). The serialize method tells GraphQL how to serialize a value returned in a resolver into the data that's returned in the response. The parseLiteral method tells GraphQL what to do with a literal value that's passed to an argument (like "foo", or 4.2 or [12, 20]). The parseValue method tells GraphQL what to do with the value of a variable that's passed to an argument.
For parseValue and serialize we can just return the value we're given. Because parseLiteral is given an AST node object representing the literal value, we have to do a little bit of work to convert it into the appropriate format.
You can take the above scalar and customize it to your needs by adding validation logic as needed. In any of the three methods, you can throw an error to indicate an invalid value. For example, if we want to allow most values but don't want to serialize functions, we can do something like:
if (typeof value == 'function') {
throw new TypeError('Cannot serialize a function!')
}
return value
Using the above scalar in your schema is simple. If you're using vanilla GraphQL.js, then use it just like you would any of the other scalar types (GraphQLString, GraphQLInt, etc.) If you're using Apollo, you'll need to include the scalar in your resolver map as well as in your SDL:
const resolvers = {
...
// The property name here must match the name you specified in the constructor
Anything,
}
const typeDefs = `
# NOTE: The name here must match the name you specified in the constructor
scalar Anything
# the rest of your schema
`
Just send a stringified value via GraphQL and parse it on the other side, e.g. use this wrapper class.
export class Dynamic {
#Field(type => String)
private value: string;
getValue(): any {
return JSON.parse(this.value);
}
setValue(value: any) {
this.value = JSON.stringify(value);
}
}
For similar problem I've created schema like this:
"""`MetadataEntry` model"""
type MetadataEntry {
"""Key of the entry"""
key: String!
"""Value of the entry"""
value: String!
}
"""Object with metadata"""
type MyObjectWithMetadata {
"""
... rest of my object fields
"""
"""
Key-value entries that you can attach to an object. This can be useful for
storing additional information about the object in a structured format
"""
metadata: [MetadataEntry!]!
"""Returns value of `MetadataEntry` for given key if it exists"""
metadataValue(
"""`MetadataEntry` key"""
key: String!
): String
}
And my queries can look like this:
query {
listMyObjects {
# fetch meta values by key
meta1Value: metadataValue(key: "meta1")
meta2Value: metadataValue(key: "meta2")
# ... or list them all
metadata {
key
value
}
}
}
Imagine I have the following code:
const object = {};
// an error should be thrown
object.property.someMethod();
// an error should be thrown
object.foo;
Is it possible to throw an error when someMethod() is called or if any other non-existing property is called?
I guess that I need to do something with it's prototype, to throw an Error. However, I'm not sure what exactly I should do.
Any help would be appreciated.
Yes, using a Proxy with a handler.get() trap:
const object = new Proxy({}, {
get (target, key) {
throw new Error(`attempted access of nonexistent key \`${key}\``);
}
})
object.foo
If you want to modify an existing object with this behavior, you can use Reflect.has() to check for property existence and determine whether to forward the access using Reflect.get() or throw:
const object = new Proxy({
name: 'Fred',
age: 42,
get foo () { return this.bar }
}, {
get (target, key, receiver) {
if (Reflect.has(target, key)) {
return Reflect.get(target, key, receiver)
} else {
throw new Error(`attempted access of nonexistent key \`${key}\``)
}
}
})
console.log(object.name)
console.log(object.age)
console.log(object.foo)
I have an ES6 class that require a config object.
If a property is missing, I'd like to throw an error.
The solution if found to keep the code short and not add a if(!object.propN)... for every property is to do :
class myClass {
constructor(config) {
if (!config) {
throw new Error("You must provide a config object");
}
this.prop1 = config.prop1;
this.prop2 = config.prop2;
this.prop3 = config.prop3;
this.prop4 = config.prop4;
for (const key in this) {
if (!this[key]) {
throw new Error(`Config object miss the property ${key}`);
}
}
}
}
Is it OK to do this in javascript ?
For configs, we usually use the feature Destructuring assignment to check whether the properties are in an object or not by using the sugar of ES6 (ES2015).
And furthermore, we can also set default values for the configs by this feature.
{prop1, prop2, prop3: path='', prop4='helloworld', ...others} = config
After destructuring assignment has been done, just need to do a check before what we are going to do with the specific config. i.e.
if (!prop1) {
throw new Error(`Config object miss the property ${prop1}`);
}
doSomething(prop1);
But if you still want to check all the configs at the beginning, you can do something like this,
class myClass {
constructor(config) {
if (!config) {
throw new Error("You must provide a config object");
}
// You can assign the values to a new object like "tmpConfig" or just assign them back to "config" for the case config.propX === undefined
({prop1: config.prop1=undefined, prop2: config.prop2=undefined, prop3: config.prop3=undefined, prop4: config.prop4=undefined} = config);
for (const key in config) {
if (!config[key]) {
throw new Error(`Config object miss the property ${key}`);
}
}
Object.assign(this, config);
}
}
Or only using Object.assign() by setting default values,
class myClass {
constructor(config) {
if (!config) {
throw new Error("You must provide a config object");
}
let requiredConfigs = {
prop1: undefined,
prop2: undefined,
prop3: undefined,
prop4: undefined,
}
let tmpConfig = Object.assign(requiredConfigs, config);
for (const key in tmpConfig) {
if (!tmpConfig[key]) {
throw new Error(`Config object miss the property ${key}`);
}
}
Object.assign(this, tmpConfig);
}
}
=> We use Destructuring assignment and Object.assign() a lot for doing setting configs things.
To avoid premature and excessive setting of all config properties into this object you should place the check for an empty property occurrence beforehand. The solution using Array.some function:
class myClass {
constructor(config) {
if (!config || typeof config !== "object") {
throw new Error("You must provide a config object!");
} else if (Object.keys(config).some((k) => !config[k])) { // returns 'true' if any empty property is found
throw new Error(`Config object has empty property(ies)!`);
}
this.prop1 = config.prop1;
this.prop2 = config.prop2;
this.prop3 = config.prop3;
this.prop4 = config.prop4;
}
}