The Data & Security guide at Parse.com describes how to control access on class and object level. I am looking for a way to restrict write permissions on certain properties of a class for all users. How can this be achieved?
My idea would be to use a One-to-One relationship and restrict the access on the related class. However, this doesn't feel like the canonical way...
The easiest way to do this is to use cloud code. Here is how you could do it :
Parse.Cloud.beforeSave("YourObject", function(request, response)
{
// Find a way to decide if writing is allowed.
var writingForbidden = (request.master === false);
if (writingForbidden)
{
var readOnlyProperties = ["property1", "property2", "property3", "..."];
var dirtyProperties = request.object.dirtyKeys();
for (i = 0 ; i < dirtyProperties.length ; i++) {
var dirtyProperty = dirtyProperties[i];
if (readOnlyProperties.indexOf(dirtyProperty) > -1) {
return response.error("Trying to change a readonly property : " + dirtyProperty);
}
}
}
return response.success();
}
Related
By default App Insights use page title as event name. Having dynamic page names, like "Order 32424", creates insane amount of event types.
Documentation on the matter says to use trackEvent method, but there are no examples.
appInsights.trackEvent("Edit button clicked", { "Source URL": "http://www.contoso.com/index" })
What is the best approach? It would be perfect to have some sort of map/filter which would allow to modify event name for some pages to the shared name, like "Order 23424" => "Order", at the same time to leave most pages as they are.
You should be able to leverage telemetry initializer approach to replace certain pattern in the event name with the more "common" version of that name.
Here is the example from Application Insights JS SDK GitHub on how to modify pageView's data before it's sent out. With the slight modification you may use it to change event names based on their appearance:
window.appInsights = appInsights;
...
// Add telemetry initializer
appInsights.queue.push(function () {
appInsights.context.addTelemetryInitializer(function (envelope) {
var telemetryItem = envelope.data.baseData;
// To check the telemetry item’s type:
if (envelope.name === Microsoft.ApplicationInsights.Telemetry.PageView.envelopeType) {
// this statement removes url from all page view documents
telemetryItem.url = "URL CENSORED";
}
// To set custom properties:
telemetryItem.properties = telemetryItem.properties || {};
telemetryItem.properties["globalProperty"] = "boo";
// To set custom metrics:
telemetryItem.measurements = telemetryItem.measurements || {};
telemetryItem.measurements["globalMetric"] = 100;
});
});
// end
...
appInsights.trackPageView();
appInsights.trackEvent(...);
With help of Dmitry Matveev I've came with the following final code:
var appInsights = window.appInsights;
if (appInsights && appInsights.queue) {
function adjustPageName(item) {
var name = item.name.replace("AppName", "");
if (name.indexOf("Order") !== -1)
return "Order";
if (name.indexOf("Product") !== -1)
return "Shop";
// And so on...
return name;
}
// Add telemetry initializer
appInsights.queue.push(function () {
appInsights.context.addTelemetryInitializer(function (envelope) {
var telemetryItem = envelope.data.baseData;
// To check the telemetry item’s type:
if (envelope.name === Microsoft.ApplicationInsights.Telemetry.PageView.envelopeType || envelope.name === Microsoft.ApplicationInsights.Telemetry.PageViewPerformance.envelopeType) {
// Do not track admin pages
if (telemetryItem.name.indexOf("Admin") !== -1)
return false;
telemetryItem.name = adjustPageName(telemetryItem);
}
});
});
}
Why this code is important? Because App Insights use page titles by default as Name for PageView, so you would have hundreds and thousands of different events, like "Order 123132" which would make further analysis (funnel, flows, events) meaningless.
Key highlights:
var name = item.name.replace("AppName", ""); If you put your App/Product name in title, you probably want to remove it from you event name, because it would just repeat itself everywhere.
appInsights && appInsights.queue you should check for appInsights.queue because for some reason it can be not defined and it would cause an error.
if (telemetryItem.name.indexOf("Admin") !== -1) return false; returning false will cause event to be not recorded at all. There certain events/pages you most likely do not want to track, like admin part of website.
There are two types of events which use page title as event name: PageView
and PageViewPerformance. It makes sense to modify both of them.
Here's one work-around, if you're using templates to render your /orders/12345 pages:
appInsights.trackPageView({name: TEMPLATE_NAME });
Another option, perhaps better suited for a SPA with react-router:
const Tracker = () => {
let {pathname} = useLocation();
pathname = pathname.replace(/([/]orders[/])([^/]+), "$1*"); // handle /orders/NN/whatever
pathname = pathname.replace(/([/]foo[/]bar[/])([^/]+)(.*)/, "$1*"); // handle /foo/bar/NN/whatever
useEffect(() => {
appInsights.trackPageView({uri: pathname});
}, [pathname]);
return null;
}
CakePHP Version 3.5.5
The id is visible in the address bar for view and edit which for my application creates a security risk. Any logged in user at the same company can change the id in the address bar and view or edit the details
of users they are not allowed to.
IE: https://localhost/crm/users/edit/1378 can be manually changed in the address bar to https://localhost/crm/users/edit/1215 and entered. This would display the details of user 1215 which is not allowed.
To overcome this I am selecting the ids which the user is allowed to edit and checking that the id from the url is one of these ids with the following code:
public function view($id = null)
{
if ($this->request->is('get')) {
// Select the permitted ids.
if (superuser) { // example to explain only
$query = $this->Users->find()
->where(['companyid' => $cid])
->andWhere(['status' => 1])
->toArray();
}
elseif (manager) { // example to explain only
$query = $this->Users->find()
->where(['areaid' => $areaid])
->andWhere(['status' => 1])
->toArray();
}
elseif (team leader) { // example to explain only
$query = $this->Users->find()
->where(['teamid' => $teamid])
->andWhere(['status' => 1])
->toArray();
}
// Check if the edit id is in the array of permitted ids.
$ids = array_column($query, 'id');
$foundKey = array_search($id, $ids);
// If the edit id is not in the array of permitted ids redirect to blank.
if (empty($foundKey)) {
// Handle error.
}
$user = $this->Users->get($id);
$this->set('user', $user);
$this->set('_serialize', ['user']);
}
else {
// Handle error.
}
}
My question: Is the above code the best cake way of achieving this or is there a better way to do it?
This code does work but because it's to do with security I'd appreciate any input which would improve it or point out it's weakness/es.
/////////////////////////////////////////////////////////////////////////////
As requested by cgTag please see below.
My app has superusers, managers, team leaders and users.
Managers manage one area which can contain many teams.
Team Leaders lead one team and must belong to an area.
Users are assigned to an area or a team.
For example:
Area is UK
Team is England
Team is Scotland
Team is Wales
Area is USA
Team is Florida
Team is California
Team is Texas
On index - superusers see all the superusers, managers, team leaders and users in the company.
On index - managers see themself and users in their area, team leaders in their area and users in the teams.
On index - team leaders see themself and users in their team
My problem is say the manager of area UK clicks edit on one of the records and that record is displayed with a url of https://localhost/crm/users/edit/1378
Then say this disgruntled manager makes a guess and changes the url to https://localhost/crm/users/edit/1215 and submits it then this record is displayed. (This record could be anyone, a superuser, another manager, a team leader who is not in their area or a user not in their area.
This manager could then change say the email address and submit this and it's this type of situation that I need to protect against.
My fix is to reiterate the find for the superuser, manager and team leader I've done on index in the view and edit class. This ensures that say a manager can only view or edit someone in their area.
Hopefully I've explained it well enough but if not just let me know and I'll have another go.
Thanks. Z.
/////////////////////////////////////////////////////////////////////////////
Thanks cgTag, I feel a lot more confident with this approach but I cannot use this code because you have correctly assumed that I am using an id to select all the companies results but I'm using a 40 char string. I do this so I can make my sql queries more robust.
It's impossible for you to help me unless you have all the info required so I have posted an accurate representation below:
public function view($id = null)
{
if(!$this->request->is('get') || !$id) {
//throw new ForbiddenException();
echo 'in request is NOT get or id NOT set ' . '<hr />';
}
$user_id = $this->Auth->user('id');
// regular users can never view other users.
if($user_id !== $id) {
//throw new ForbiddenException();
echo 'in $user_id !== $id ' . '<hr />';
}
// Declare client id 1.
if ($this->cid1() === false) {
echo 'in throw exception ' . '<hr />';
}
else {
$c1 = null;
$c1 = $this->cid1();
}
$company_ids = $this->getCompanyIds($c1);
$area_ids = $this->getAreaIds($user_id, $c1);
$team_ids = $this->getTeamIds($user_id, $c1);
// company_id does not exist which will cause an unknown column error.
// The column I select by is cid_1 so I have changed this column to cid_1 as shown below.
$user = $this->Users->find()
->where([
'id' => $id,
'cid_1 IN' => $company_ids,
'area_id IN' => $area_ids,
'team_id IN' => $team_ids,
'status' => 1
])
->firstOrFail();
$this->set(compact('user'));
}
The functions:
public function cid1()
{
$session = $this->request->session();
if ($session->check('Cid.one')) {
$c1 = null;
$c1 = $session->read('Cid.one');
if (!is_string($c1) || is_numeric($c1) || (strlen($c1) !== 40)) {
return false;
}
return $c1;
}
return false;
}
public function getCompanyIds($c1 = null)
{
$query = $this->Users->find()
->where(['status' => 1])
->andWhere(['cid_1' => $c1]);
return $query;
}
public function getAreaIds($c1 = null, $user_id = null)
{
$query = $this->Users->find()
->where(['status' => 1])
->andWhere(['cid_1' => $c1])
->andWhere(['area_id' => $user_id]);
return $query;
}
public function getTeamIds($c1 = null, $user_id = null)
{
$query = $this->Users->find()
->where(['status' => 1])
->andWhere(['cid_1' => $c1])
->andWhere(['team_id' => $user_id]);
return $query;
}
With this code I get the following error:
Error: SQLSTATE[21000]: Cardinality violation: 1241 Operand should contain 1 column(s)
I don't know if your example will work with this new information but at least you have all the information now.
If it can be ammended great but if not I really don't mind. And I do appreciate the time you've put aside to try to help.
Thanks Z
/////////////////////////////////////////////////////////////////////////////
#tarikul05 - Thanks for the input.
Your suggestion is very similar to my first effort at addressing this security issue but I went for security through obscurity and hid the id in a 80 char string, example below.
// In a cell
public function display($id = null)
{
// Encrypt the id to pass with view and edit links.
$idArray = str_split($id);
foreach($idArray as $arrkey => $arrVal) {
$id0 = "$idArray[0]";
$id1 = "$idArray[1]";
$id2 = "$idArray[2]";
$id3 = "$idArray[3]";
}
// Generate string for the id to be obscured in.
$enc1 = null;
$enc1 = sha1(uniqid(mt_rand(), true));
$enc2 = null;
$enc2 = sha1(uniqid(mt_rand(), true));
$encIdStr = $enc1 . $enc2;
// Split the string.
$encIdArray = null;
$encIdArray = str_split($encIdStr);
// Generate the coded sequence.
$codedSequence = null;
$codedSequence = array(9 => "$id0", 23 => "$id1", 54 => "$id2", 76 => "$id3");
// Replace the id in the random string.
$idTemp = null;
$idTemp = array_replace($encIdArray, $codedSequence);
// Implode the array.
$encryptedId = null;
$encryptedId = implode("",$idTemp);
// Send the encrypted id to the view.
$this->set('encryptedId', $encryptedId);
}
And then decrypted with
// In function in the app controller
public function decryptTheId($encryptedId = null)
{
$idArray = str_split($encryptedId);
foreach($idArray as $arrkey => $arrVal) {
$id0 = "$idArray[9]";
$id1 = "$idArray[23]";
$id2 = "$idArray[54]";
$id3 = "$idArray[76]";
}
$id = null;
$id = $id0.$id1.$id2.$id3;
return $id;
}
The problem with this was that when testing I managed to get the script to error which revealed the array positions which would of undermined the security by obscurity principle and made it a lot easier for a hacker.
Your suggestion is neater than my obscurity method but I believe md5 has been cracked therefore it should not be used.
I'm no security expert but in my opinion checking the view and edit id against an array of permitted ids is the most secure way to address this.
Maybe I'm wrong but if I do it this way there's is no way a hacker no matter what they try in the address bar can see or edit data they are not meant to and it keeps the url cleaner.
What I was originally looking/hoping for was a Cake method/function which addressed this but I couldn't find anything in the cookbook.
Thanks anyway. Z.
I would simplify your code so that the SQL that fetches the user record only finds that record if the current user has permissions. When you're dependent upon associated data for those conditions. Follow this approach even if you have to use joins.
You create the SQL conditions and then call firstOrFail() on the query. This throws a NotFoundException if there is no match for the record.
public function view($id = null) {
if(!$this->request->is('get') || !$id) {
throw new ForbiddenException();
}
$user_id = $this->Auth->user('id');
// regular users can never view other users.
if($user_id !== $id) {
throw new ForbiddenException();
}
$company_ids = $this->getCompanyIds($user_id);
$area_ids = $this->getAreaIds($user_id);
$team_ids = $this->getTeamIds($user_id);
$user = $this->Users->find()
->where([
'id' => $id
'company_id IN' => $company_ids,
'area_id IN' => $area_ids,
'team_id IN' => $team_ids,
'status' => 1
])
->firstOrFail();
$this->set(compact('user'));
}
The above logic should be sound when a user belongsTo a hierarchical structure of data. Where by, they can view many users but only if those users belong to one of the upper associations they have access too.
It works because of the IN clause of the where conditions.
Note: The IN operator throws an error if the array is empty. When you have users who can see all "teams" just exclude that where condition instead of using an empty array.
The key here is to have functions which return an array of allowed parent associations such as; getCompanyIds($user_id) would return just the company IDs the current user is allowed access too.
I think if you implement it this way then the logic is easy to understand, the security is solid and a simple firstOrFail() prevents access.
I am offloading my search feature on a relational database to Azure Search. My Products tables contains columns like serialNumber, PartNumber etc.. (there can be multiple serialNumbers with the same partNumber).
I want to create a suggestor that can autocomplete partNumbers. But in my scenario I am getting a lot of duplicates in the suggestions because the partNumber match was found in multiple entries.
How can I solve this problem ?
The Suggest API suggests documents, not queries. If you repeat the partNumber information for each serialNumber in your index and then suggest based on partNumber, you will get a result for each matching document. You can see this more clearly by including the key field in the $select parameter. Azure Search will eliminate duplicates within the same document, but not across documents. You will have to do that on the client side, or build a secondary index of partNumbers just for suggestions.
See this forum thread for a more in-depth discussion.
Also, feel free to vote on this UserVoice item to help us prioritize improvements to Suggestions.
I'm facing this problem myself. My solution does not involve a new index (this will only get messy and cost us money).
My take on this is a while-loop adding 'UserIdentity' (in your case, 'partNumber') to a filter, and re-search until my take/top-limit is met or no more suggestions exists:
public async Task<List<MachineSuggestionDTO>> SuggestMachineUser(string searchText, int take, string[] searchFields)
{
var indexClientMachine = _searchServiceClient.Indexes.GetClient(INDEX_MACHINE);
var suggestions = new List<MachineSuggestionDTO>();
var sp = new SuggestParameters
{
UseFuzzyMatching = true,
Top = 100 // Get maximum result for a chance to reduce search calls.
};
// Add searchfields if set
if (searchFields != null && searchFields.Count() != 0)
{
sp.SearchFields = searchFields;
}
// Loop until you get the desired ammount of suggestions, or if under desired ammount, the maximum.
while (suggestions.Count < take)
{
if (!await DistinctSuggestMachineUser(searchText, take, searchFields, suggestions, indexClientMachine, sp))
{
// If no more suggestions is found, we break the while-loop
break;
}
}
// Since the list might me bigger then the take, we return a narrowed list
return suggestions.Take(take).ToList();
}
private async Task<bool> DistinctSuggestMachineUser(string searchText, int take, string[] searchFields, List<MachineSuggestionDTO> suggestions, ISearchIndexClient indexClientMachine, SuggestParameters sp)
{
var response = await indexClientMachine.Documents.SuggestAsync<MachineSearchDocument>(searchText, SUGGESTION_MACHINE, sp);
if(response.Results.Count > 0){
// Fix filter if search is triggered once more
if (!string.IsNullOrEmpty(sp.Filter))
{
sp.Filter += " and ";
}
foreach (var result in response.Results.DistinctBy(r => new { r.Document.UserIdentity, r.Document.UserName, r.Document.UserCode}).Take(take))
{
var d = result.Document;
suggestions.Add(new MachineSuggestionDTO { Id = d.UserIdentity, Namn = d.UserNamn, Hkod = d.UserHkod, Intnr = d.UserIntnr });
// Add found UserIdentity to filter
sp.Filter += $"UserIdentity ne '{d.UserIdentity}' and ";
}
// Remove end of filter if it is run once more
if (sp.Filter.EndsWith(" and "))
{
sp.Filter = sp.Filter.Substring(0, sp.Filter.LastIndexOf(" and ", StringComparison.Ordinal));
}
}
// Returns false if no more suggestions is found
return response.Results.Count > 0;
}
public async Task<List<string>> SuggestionsAsync(bool highlights, bool fuzzy, string term)
{
SuggestParameters sp = new SuggestParameters()
{
UseFuzzyMatching = fuzzy,
Top = 100
};
if (highlights)
{
sp.HighlightPreTag = "<em>";
sp.HighlightPostTag = "</em>";
}
var suggestResult = await searchConfig.IndexClient.Documents.SuggestAsync(term, "mysuggestion", sp);
// Convert the suggest query results to a list that can be displayed in the client.
return suggestResult.Results.Select(x => x.Text).Distinct().Take(10).ToList();
}
After getting top 100 and using distinct it works for me.
You can use the Autocomplete API for that where does the grouping by default. However, if you need more fields together with the result, like, the partNo plus description it doesn't support it. The partNo will be distinct though.
I am aware of how to create getters and setters for properties whose names one already knows, by doing something like this:
// A trivial example:
function MyObject(val){
this.count = 0;
this.value = val;
}
MyObject.prototype = {
get value(){
return this.count < 2 ? "Go away" : this._value;
},
set value(val){
this._value = val + (++this.count);
}
};
var a = new MyObject('foo');
alert(a.value); // --> "Go away"
a.value = 'bar';
alert(a.value); // --> "bar2"
Now, my question is, is it possible to define sort of catch-all getters and setters like these? I.e., create getters and setters for any property name which isn't already defined.
The concept is possible in PHP using the __get() and __set() magic methods (see the PHP documentation for information on these), so I'm really asking is there a JavaScript equivalent to these?
Needless to say, I'd ideally like a solution that is cross-browser compatible.
This changed as of the ES2015 (aka "ES6") specification: JavaScript now has proxies. Proxies let you create objects that are true proxies for (facades on) other objects. Here's a simple example that turns any property values that are strings to all caps on retrieval, and returns "missing" instead of undefined for a property that doesn't exist:
"use strict";
if (typeof Proxy == "undefined") {
throw new Error("This browser doesn't support Proxy");
}
let original = {
example: "value",
};
let proxy = new Proxy(original, {
get(target, name, receiver) {
if (Reflect.has(target, name)) {
let rv = Reflect.get(target, name, receiver);
if (typeof rv === "string") {
rv = rv.toUpperCase();
}
return rv;
}
return "missing";
}
});
console.log(`original.example = ${original.example}`); // "original.example = value"
console.log(`proxy.example = ${proxy.example}`); // "proxy.example = VALUE"
console.log(`proxy.unknown = ${proxy.unknown}`); // "proxy.unknown = missing"
original.example = "updated";
console.log(`original.example = ${original.example}`); // "original.example = updated"
console.log(`proxy.example = ${proxy.example}`); // "proxy.example = UPDATED"
Operations you don't override have their default behavior. In the above, all we override is get, but there's a whole list of operations you can hook into.
In the get handler function's arguments list:
target is the object being proxied (original, in our case).
name is (of course) the name of the property being retrieved, which is usually a string but could also be a Symbol.
receiver is the object that should be used as this in the getter function if the property is an accessor rather than a data property. In the normal case this is the proxy or something that inherits from it, but it can be anything since the trap may be triggered by Reflect.get.
This lets you create an object with the catch-all getter and setter feature you want:
"use strict";
if (typeof Proxy == "undefined") {
throw new Error("This browser doesn't support Proxy");
}
let obj = new Proxy({}, {
get(target, name, receiver) {
if (!Reflect.has(target, name)) {
console.log("Getting non-existent property '" + name + "'");
return undefined;
}
return Reflect.get(target, name, receiver);
},
set(target, name, value, receiver) {
if (!Reflect.has(target, name)) {
console.log(`Setting non-existent property '${name}', initial value: ${value}`);
}
return Reflect.set(target, name, value, receiver);
}
});
console.log(`[before] obj.example = ${obj.example}`);
obj.example = "value";
console.log(`[after] obj.example = ${obj.example}`);
The output of the above is:
Getting non-existent property 'example'
[before] obj.example = undefined
Setting non-existent property 'example', initial value: value
[after] obj.example = value
Note how we get the "non-existent" message when we try to retrieve example when it doesn't yet exist, and again when we create it, but not after that.
Answer from 2011 (obsoleted by the above, still relevant to environments limited to ES5 features like Internet Explorer):
No, JavaScript doesn't have a catch-all property feature. The accessor syntax you're using is covered in Section 11.1.5 of the spec, and doesn't offer any wildcard or something like that.
You could, of course, implement a function to do it, but I'm guessing you probably don't want to use f = obj.prop("example"); rather than f = obj.example; and obj.prop("example", value); rather than obj.example = value; (which would be necessary for the function to handle unknown properties).
FWIW, the getter function (I didn't bother with setter logic) would look something like this:
MyObject.prototype.prop = function(propName) {
if (propName in this) {
// This object or its prototype already has this property,
// return the existing value.
return this[propName];
}
// ...Catch-all, deal with undefined property here...
};
But again, I can't imagine you'd really want to do that, because of how it changes how you use the object.
Preface:
T.J. Crowder's answer mentions a Proxy, which will be needed for a catch-all getter/setter for properties which don't exist, as the OP was asking for. Depending on what behavior is actually wanted with dynamic getters/setters, a Proxy may not actually be necessary though; or, potentially, you may want to use a combination of a Proxy with what I'll show you below.
(P.S. I have experimented with Proxy thoroughly in Firefox on Linux recently and have found it to be very capable, but also somewhat confusing/difficult to work with and get right. More importantly, I have also found it to be quite slow (at least in relation to how optimized JavaScript tends to be nowadays) - I'm talking in the realm of deca-multiples slower.)
To implement dynamically created getters and setters specifically, you can use Object.defineProperty() or Object.defineProperties(). This is also quite fast.
The gist is that you can define a getter and/or setter on an object like so:
let obj = {};
let val = 0;
Object.defineProperty(obj, 'prop', { //<- This object is called a "property descriptor".
//Alternatively, use: `get() {}`
get: function() {
return val;
},
//Alternatively, use: `set(newValue) {}`
set: function(newValue) {
val = newValue;
}
});
//Calls the getter function.
console.log(obj.prop);
let copy = obj.prop;
//Etc.
//Calls the setter function.
obj.prop = 10;
++obj.prop;
//Etc.
Several things to note here:
You cannot use the value property in the property descriptor (not shown above) simultaneously with get and/or set; from the docs:
Property descriptors present in objects come in two main flavors: data descriptors and accessor descriptors. A data descriptor is a property that has a value, which may or may not be writable. An accessor descriptor is a property described by a getter-setter pair of functions. A descriptor must be one of these two flavors; it cannot be both.
Thus, you'll note that I created a val property outside of the Object.defineProperty() call/property descriptor. This is standard behavior.
As per the error here, don't set writable to true in the property descriptor if you use get or set.
You might want to consider setting configurable and enumerable, however, depending on what you're after; from the docs:
configurable
true if and only if the type of this property descriptor may be changed and if the property may be deleted from the corresponding object.
Defaults to false.
enumerable
true if and only if this property shows up during enumeration of the properties on the corresponding object.
Defaults to false.
On this note, these may also be of interest:
Object.getOwnPropertyNames(obj): gets all properties of an object, even non-enumerable ones (AFAIK this is the only way to do so!).
Object.getOwnPropertyDescriptor(obj, prop): gets the property descriptor of an object, the object that was passed to Object.defineProperty() above.
obj.propertyIsEnumerable(prop);: for an individual property on a specific object instance, call this function on the object instance to determine whether the specific property is enumerable or not.
The following could be an original approach to this problem:
var obj = {
emptyValue: null,
get: function(prop){
if(typeof this[prop] == "undefined")
return this.emptyValue;
else
return this[prop];
},
set: function(prop,value){
this[prop] = value;
}
}
In order to use it the properties should be passed as strings.
So here is an example of how it works:
//To set a property
obj.set('myProperty','myValue');
//To get a property
var myVar = obj.get('myProperty');
Edit:
An improved, more object-oriented approach based on what I proposed is the following:
function MyObject() {
var emptyValue = null;
var obj = {};
this.get = function(prop){
return (typeof obj[prop] == "undefined") ? emptyValue : obj[prop];
};
this.set = function(prop,value){
obj[prop] = value;
};
}
var newObj = new MyObject();
newObj.set('myProperty','MyValue');
alert(newObj.get('myProperty'));
You can see it working here.
I was looking for something and I figured out on my own.
/*
This function takes an object and converts to a proxy object.
It also takes care of proxying nested objectsa and array.
*/
let getProxy = (original) => {
return new Proxy(original, {
get(target, name, receiver) {
let rv = Reflect.get(target, name, receiver);
return rv;
},
set(target, name, value, receiver) {
// Proxies new objects
if(typeof value === "object"){
value = getProxy(value);
}
return Reflect.set(target, name, value, receiver);
}
})
}
let first = {};
let proxy = getProxy(first);
/*
Here are the tests
*/
proxy.name={} // object
proxy.name.first={} // nested object
proxy.name.first.names=[] // nested array
proxy.name.first.names[0]={first:"vetri"} // nested array with an object
/*
Here are the serialised values
*/
console.log(JSON.stringify(first)) // {"name":{"first":{"names":[{"first":"vetri"}]}}}
console.log(JSON.stringify(proxy)) // {"name":{"first":{"names":[{"first":"vetri"}]}}}
var x={}
var propName = 'value'
var get = Function("return this['" + propName + "']")
var set = Function("newValue", "this['" + propName + "'] = newValue")
var handler = { 'get': get, 'set': set, enumerable: true, configurable: true }
Object.defineProperty(x, propName, handler)
this works for me
Iam using the ServiceStack.Text JsonObject parser to map into my domain model. I basically have anthing working, except when using Linq to filter on ArrayObject and the try to convert it using convertAll. Iam cannot come arround actuall after using link, adding element by element to an JsonArrayObjects list and then pass it.
var tmpList = x.Object("references").ArrayObjects("image").Where(y => y.Get<int>("type") != 1).ToList();
JsonArrayObjects tmpStorage = new JsonArrayObjects();
foreach (var pic in tmpList) {
tmpStorage.Add(pic);
}
if (tmpStorage.Count > 0) {
GalleryPictures = tmpStorage.ConvertAll(RestJsonToModelMapper.jsonToImage);
}
Question:
Is there a more elegant way to get from IEnumarable back to JsonArrayObjects?
Casting will not work, since where copys elements into a list, instead of manipulating the old one, therefor the result is not an downcasted JsonArrayObjects, rather a new List object.
Best
Considering this more elegant is arguable, but I would probably do:
var tmpStorage = new JsonArrayObjects();
tmpList.ForEach(pic => tmpStorage.Add(RestJsonToModelMapper.jsonToImage(pic)));
And if this kind of conversion is used frequently, you may create an extension method:
public static JsonArrayObjects ToJsonArrayObjects(this IEnumerable<JsonObject> pics)
{
var tmpStorage = new JsonArrayObjects();
foreach(var pic in pics)
{
tmpStorage.Add(RestJsonToModelMapper.jsonToImage(pic));
}
return tmpStorage;
}
This way you would end up with simpler consumer code:
var tmpStorage = x.Object("references")
.ArrayObjects("image")
.Where(y => y.Get<int>("type") != 1)
.ToJsonArrayObjects();
Like this?
var pictures = x.Object("references")
.ArrayObjects("image")
.Where(y => y.Get<int>("type") != 1)
.Select(RestJsonToModelMapper.JsonToImage)
.ToList();