I have an existing service written with the .NET Web API.
As an example, this service returns JSON in the following format:
[
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Jane" }
]
However, as per the Ember.js Rest Adapter documentation, Ember would expect JSON in the following format:
{
"persons": [
{ "id": 1, "name": "John" },
{ "id": 2, "name": "Jane" }
]
}
Because of this, Ember is returning the following error:
Your server returned a hash with the key 0 but you have no mapping for it
By no means do I plan on changing my service API and how it returns data.
Would it be possible to get Ember.js (latest version) to work with the existing data my service is returning? And, if so, how can I implement that?
Ember is very flexible in that sense, giving the ability to extend the adapter and serializer in order to integrate your app with any backend API.
You should get the WebAPIAdapter, which is part of the Ember.js Template for Web API.
Additionally, you might wanna take a look into this project I wrote as an example, based on that same template (with some modifications I did on my own). It's still under development and it doesn't feature all the best practices (yet), but I'd say it's a valid example.
You should also take a look into this repo / library (You can install it via NuGet too), which allows you to pre-compile your Handlebars templates directly into Ember.TEMPLATES collection.
in the web api just return new { object }
var persons = _personbService.GetPeople;
return new { persons };
this will wrap your object in an object of the same name. If you need to call the object something else just do
new { SomeOtherName = persons; }
I was having this issue in ember and found the best solution for me was to build a new serializer and override the normalizePayload method. code below:
export default DS.RESTSerializer.extend({
normalizePayload: function(payload) {
var root = "posts";
var output = {};
output[root] = payload;
return output;
}
});
This wraps the initial response and adds the root to it, hope it helps!
Related
Background
I have decided to deploy Strapi as a headless CMS/backend for my project and have been quite happy with the ease of setup out of the box. However, a part of my project requires capturing user input through a simple form and I thought extending Strapi's REST API through writing custom plugin and utilizing the same backend is the way to go instead of spinning up another express server with its own DB. I was able to find documentation on generating a plugin and I have a good understanding of how the file structure and logic of the different plugin files work (model, controllers, policies, config, routes etc...). However, for the life of me, I have not been able to find the most basic resources or any adequate documentation on how to write logic allowing my controllers to hook into the basic CRUD methods that Strapi has created for my custom collection ('messages' in my case).
What I have done so far
Generated a plugin
Strapi generate:plugin contact-book
In plugins\contact-form\config\routes.json. Created a /postMessage route which I tested successfully after allowing public access to it from the Admin panel
{ "routes":
[
{
"method": "POST",
"path": "/postMessage",
"handler": "contact-form.postMessage"
}
]
}
In plugins\contact-form\models. I created an empty model file Message.js & Message.settings.json containing the definition of my model 'message' which defines a collectionType collection names 'message' with its fields as follows:
{
"kind": "collectionType",
"connection": "default",
"info": {
"name": "message",
"description": "This represents the Message Model"
},
"attributes": {
"name": {
"default": "",
"type": "string",
"required": true
},
"email": {
"default": "",
"type": "email",
"required": true
},
"message": {
"default": "",
"type": "text",
"required": true
}
}
}
Upon dev server restart, Strapi has already recognized my plugin and has reflected the 'message' model in its DB and I can see the collection from the admin panel correctly.
What I need help with
What I would like to do, is to extend the plugin's REST API endpoints to provide the same functionality as the out of the box end points that Strapi builds whenever a new collection/entity is created, while adding a custom layer of business logic to it.
I would appreciate any one pointing me towards an example or a resource that shows the methods or functions that Strapi exposes to plugins which can be hooked into or invoked to achieve this.
[Update] 14-June
So after hours of logging and inspecting 10s of objects that Strapi exposes to the controller, I was able to find that the strapi object exposes a query method which accepts 2 parameters, the first being the model and second is the plugin name. So simply speaking, the following enabled me to perform a write to my collection:
postMessage: async (ctx) => {
const testData = {
name: 'John Doe',
email: 'john#doe.com',
message: 'Hello World!'
}
result = await strapi.query("message","contact-form").create(data)
}
However, I still do not consider this an answer since I would like to find a more comprehensive approach through which built in policies and services can be used. Furthermore, I am still not sure if invoking this method bypasses any layers of middleware that Strapi sets up for default controllers and therefore exposes the app to security or stability risks.
The above can be achieved by utilizing the methods exposed by the built-in Strapi entityService. By examining how the stack handles create operations via the automatically build API endpoints, I was able to identify the entityService as the appropriate module for this functionality. However, when passing the plugin's model name to strapi.entityService.create({data} , {model: 'modelName') , the service was not able to locate the modelName. So I examined the service's source code and found out that it accepts the model UID instead. So in the case of a custom plugin, instead of just passing the modelName we need to pass the modelUID which is formatted as such:
plugins::plugin-name.modelName
In summary, for a create operation against the example in my question, this is how it would be:
const result = await strapi.entityService.create(
{ data: ctx.request.body },
{ model: "plugins::contact-form.message" }
)
all other CRUD operations that Strapi supports are also exposed by the entityService and can be accessed in a similar fashion (Create, Update, Find, FindOne, Etc...). You can find all these methods under the Strapi documentation > Concepts > Controllers > Core Controllers.
I have also created a YouTube video covering how this works:
https://www.youtube.com/watch?v=kIZHzbmnhnU
Due to some extraordinary circumstances I am currently busy with creating a client-only, server-less app using AngularJS. Its single purpose (for the moment) is to read a .json file, containing an array of Activity objects as follows:
[{
"id": "sta",
"title": "IT Strategy & Governance",
"type": "management",
"order": 1
},
{
"id": "por",
"title": "Portfolio Management",
"type": "management",
"order": 2
},
{
"id": "org",
"title": "Organization & Process Management",
"type": "management",
"order": 3
}]
Please keep in mind that this file is static and not generated by any kind of backend. I would now like to achieve to objectives.
Objective 1 is to load this complete list of objects. I accomplish this using an Angular $resource as follows, and it works beautifully:
Service:
pexServices.factory('Activities', function($resource) {
return $resource('data/activities.json', {});
});
Controller:
pexControllers.controller('ActivityCtrl',
function($scope, Activities) {
$scope.activities = Activities.query();
}
);
Objective 2 is to get one individual record out of this array, selecting it via its ID. As before, the data shall be pulled out of the one, big data file including all data objects.
My idea is too add a custom get function to the $resource already introduced above and include some transformResponse code that extracts the requested element:
pexServices.factory('Activities', function($resource) {
return $resource('data/activities.json', {}, {
get: {
method: "GET",
transformResponse: function(rawData, headersGetter) {
var jsonData = jQuery.parseJSON(rawData);
var requestedObject = jsonData.filter(function(activity) {
return activity.id == "THEREQUESTEDID";
});
return requestedObject.shift();
}
}
});
});
This works great, but only as long as I hard code the requested ID at THEREQUESTEDID. However, I obviously want to pass a variable parameter into the get function in order to pull the needed object, like this:
$scope.activity = Activities.get({activityId: "sta"});
Unfortunately, this is the point where I have been stuck for a while now. How do I enable a parameter to pass all the way from my controller to the transformResponse function, where I can use it to filter the array?
As said before, the file itself is static and won't react to any parameters, so I need to do the filtering manually on the client.
Thanks a lot for your support and for keeping in mind my limited JS skills. :-)
(In case you have another entirely different solution to my problem in mind, please feel free to share it as well! This is just my first shot.)
Not this way! You need to create a service with methods and wrap your resource object inside it. The service would be something like this
pexServices.factory('Activities', function ($resource) {
var service = {};
var cachedActivities = [];
service.getAll = function () {
cachedActivities = $resource('data/activities.json', {});
return cacheActivities;
}
service.getActivity = function (id) {
var requestedObject = jsonData.filter(function (activity) {
return cachedActivities.id == id;
});
return requestedObject.shift();
}
return service;
});
Now your service has 2 methods, one for list and one for specific activity. Also the activities retrieved from server are cached in a local variable. The only issue here is that getActivity will only work if you call getAll first. If you want to fix it, move to a promise based approach.
Can't find records from my model
Ember version:
DEBUG: -------------------------------
DEBUG: Ember : 1.0.0
DEBUG: Ember Data : 1.0.0-beta.2
DEBUG: Handlebars : 1.0.0 ember.js
DEBUG: jQuery : 1.9.1 ember.js
DEBUG: -------------------------------
Model:
App.Concert = DS.Model.extend({
tour_id: DS.attr(),
tickets: DS.attr()
});
Finding:
this.get("store").find("concert", {tour_id: 1}).then(function(result) {
console.log("--------------");
console.log(result.content.length);
console.log("--------------");
}, function(error) {
console.log("broken");
});
Console:
-------------
length: 0
--------------
I can't find records from my model by properties. Anybody can help?
I know what records with tour_id: 1 exist
Edu is actually partially correct (see below), but the real reason that you don't get back any records is most likely the format that your API returns. Ember Data is very strict about the type of data it expects and how it is structured. If you query your API for concerts with a tour_id of 1, then this is how the response should look like. Notice that the root object is the pluralized model name and the key is an array of concerts.
{
"concerts": [
{
"id": 1,
"tour_id": 1,
"name": "cool concert"
},
{
"id": 5,
"tour_id": 1,
"name": "awesome concert"
}
]
}
Regarding Edu's comment, if you invoke the store's find method, Ember Data doesn't perform a regular find. Under the hood, findQuery is invoked and it returns an instance of DS.RecordArray. This isn't a regular JavaScript array so you need to respect how Ember works and how it deals with arrays.
I recommend reading up on DS.RecordArray in the Ember guides. Also, if you plan on using Ember Data, you shouldn't be scared by the idea of exploring its source. Ember Data is a ambitious project and the documentation is still very much a work in progress. Ember Data's source is therefore your most important source of information.
try using getters...
console.log(result.get('length'));
The Ember.js REST Adapter expects the JSON to be returned as:
{
"person": {
"first_name": "Barack",
"last_name": "Obama",
"is_person_of_the_year": true
}
}
But my API returns the data without a root element:
{
"first_name": "Barack",
"last_name": "Obama",
"is_person_of_the_year": true
}
Is it possible to customize the REST Adapter so that it accepts my JSON data? Right now it's showing "Assertion failed: Your server returned a hash with the key 0 but you have no mapping for it"
UPDATE:
Based on Sherwin Yu's answer below, this is what I came up with, seems to work so far: https://gist.github.com/richardkall/5910875
You could also normalize it into something ember would expect.
App.PersonSerializer = DS.RESTSerializer.extend({
normalizePayload: function(type, payload) {
var typeKey = type.typeKey;
return {
typeKey: payload
}
}
});
Yes, you can write your own custom REST adapter. Take a look at the source code in the JSONSerializer, RESTSerializer (which extends the JSONSerializer), and the REST adapter.
Basically, the you need to override the extract* methods from the JSONSerializer.
Currently, it looks something like this:
extract: function(loader, json, type, record) {
var root = this.rootForType(type);
this.sideload(loader, type, json, root);
this.extractMeta(loader, type, json);
if (json[root]) {
if (record) { loader.updateId(record, json[root]); }
this.extractRecordRepresentation(loader, type, json[root]);
}
},
Notice how it checks json[root] -- you'd have to write your custom method based on your expected API response.
Another approach would be to "preprocess" the json from the API to use a root element. You could do this by finding out what methods call extract* (which passes it the json) and before it does so, modify the json to contain the root element.
Hope this helps, please let me know if it's unclear.
I solved this by extending DS.RESTSerializer. extractArray method needs to be overloaded when server response is array type.
App.PersonSerializer = DS.RESTSerializer.extend({
extractSingle: function (store, type, payload, id) {
var wrappedObj = {};
wrappedObj[type.typeKey] = payload;
return this._super(store, type, wrappedObj, id);
}});
The easiest way is to not use the RESTSerializer but the much simpler JSONSerializer, which does not expect a root element.
Good resources on understanding which serializer to use for a given API can be found in these two blog posts:
http://thejsguy.com/2015/12/05/which-ember-data-serializer-should-i-use.html
http://emberigniter.com/fit-any-backend-into-ember-custom-adapters-serializers/
I'm loading two sets of data separately but I'd like them to be related. Allow me to explain.
Firstly, I'm not using Ember-data but am instead using a simple $.ajax wrapper as outlined in this post from one of the Discourse team.
I have a concept of channels and programmes.
Channels JSON:
[{
"ID":94,
"Name":"BBC1"
},
{
"ID":105,
"Name":"BBC2"
}]
I have to gather the IDs from this JSON to be able to then request the programmes for those channels. So a response from the programmes endpoint will look a bit like this:
Programmes JSON:
{
"Channels": [
{
"Listings": [
{
"Id": "wcy2g",
"Genres": "Education",
"S": "2013-04-26T10:45",
"E": "2013-04-26T11:15",
"T": "Crime Scene Rescue"
}
]
},
{
"Listings": [
{
"Id": "wcwbs",
"Genres": "Current affairs,News",
"S": "2013-04-26T11:00",
"E": "2013-04-26T12:00",
"PID": "nyg",
"T": "Daily Politics"
}
]
}
]
}
Each Listings array can contain x amount of programmes and the objects in the Channels array relate to the order in which they are requested (by the IDs from the Channels.json) so in this case Channels[0] is BBC1 and Channels[1] is BBC2.
What I'd like is to request these two data sets as a single JSON request each but then somehow relate them. So having a channel controller that has x amount of programme models. I also need to render the channels and their programmes in two different templates
Was thinking I could iterate through the channels.json and use the index of the item to look up the relevant items in programmes.json and create the relationship that way.
Not too sure how to use Ember to achieve this though.
Thanks
I did something very similar to this and got it working in ember. I'll sketch out what I did, using your objects. Note that I'm fairly new to ember so a grain of salt may be necessary.
First, you'll want to have model objects for "Channels", "Channel" and "Programme". This will eventually let you have Controllers and Routers for each of those things, matching up nicely with ember's naming conventions. The ChannelsData will have many ChannelData objects in it, and each ChannelData will have many ProgrammeData objects. How do you get these populated?
In your ChannelsRoute class you can have a model() function which returns the model data for that route. Your model function can call create() on ChannelsData to create an instance, and then call a loadAll function on ChannelsData. ChannelsData implements loadAll() using your preferred flavor of ajax. The dead-simple easiest thing to do is to have that function do both of your ajax calls and build the entire tree of data.
You will then find that you'll run into trouble if your ChannelRoute class tries to call its model(), for instance if you enter a path like #/channels/105 directly into the browser. To work around that, make a simple object store of your own on your App object, something like App.ChannelsStore = {}, and when you create each Channel put a reference to it in your ChannelsStore (by id). Then your ChannelRoute.model function can look up its model from that store. But only if ChannelsRoute.model has completed first!
If the user entered that #/channels/105 route as the very first entry into your app, then your code will go through the ChannelsRoute.model() method, and immediately go through the ChannelRoute.model() method, probably before your ajax has completed. In that case you can have the ChannelRoute.model() method create a temporary Channel (with no programmes, for instance) and put that in the App.ChannelsStore. Your logic for building up the whole tree of data should then be willing to check the ChannelsStore to see if an object with a given id already exists, and if so to update it. You end up with something like:
App.ChannelRoute = Ember.Route.extend({
model: function(params) {
var channel = App.ChannelsStore[params.channel_id];
// create stub version if not found
if (!channel) {
channel = App.ChannelData.create({ID: params.channel_id});
App.ChannelsStore[params.channel_id] = channel;
}
return channel;
}
});
(You may end up building a ProgrammeStore similarly, but that's just more of the same.)
The updating of the temporary object actually demonstrates a very cool aspect of ember, which is that your ui may be presented with the values from the temporary object, but then when your ajax call completes and the Channels and Programmes are all loaded - your ui will update properly. Just make sure you update your objects with the set() method, and that your ui templates are happy to work with partial data.