Commit 45305ad8 authored by Bertrand PINEL's avatar Bertrand PINEL
Browse files

First implementation dealing with relationships

parent fcc13046
......@@ -46,12 +46,12 @@ module.exports = {
addLineToFile(this, configPath, /'use strict';/, proxy);
addLineToFile(this, configPath, /when it is created/, "\t\tproxy: usingProxy(),");
let validatedFormConfig = '\t"ember-validated-form": {\n\t\tlabel: {\n\t\t\tsubmit: "label.save"\n\t\t},\n'+
'\t\tcss: {\n\t\t\tgroup: "form-group",\n\t\t\tradio: "radio",\n\t\t\tcontrol: "form-control",\n'+
'\t\t\tlabel: "col-form-label",\n\t\t\thelp: "small form-text text-danger",'+
'\n\t\t\thint: "small form-text text-muted",\n\t\t\tcheckbox: "checkbox",\n\t\t\tbutton: "btn btn-default",'+
'\n\t\t\tsubmit: "btn btn-primary",\n\t\t\tloading: "loading",\n\t\t\tvalid: "is-valid",'+
'\n\t\t\terror: "is-invalid"\n\t\t}\n\t},\n';
let validatedFormConfig = '\t\t"ember-validated-form": {\n\t\t\tlabel: {\n\t\t\t\tsubmit: "label.save"\n\t\t\t},\n'+
'\t\t\tcss: {\n\t\t\t\tgroup: "form-group",\n\t\t\t\tradio: "radio",\n\t\t\t\tcontrol: "form-control",\n'+
'\t\t\t\tlabel: "col-form-label",\n\t\t\t\thelp: "small form-text text-danger",'+
'\n\t\t\t\thint: "small form-text text-muted",\n\t\t\t\tcheckbox: "checkbox",\n\t\t\t\tbutton: "btn btn-default",'+
'\n\t\t\t\tsubmit: "btn btn-primary",\n\t\t\t\tloading: "loading",\n\t\t\t\tvalid: "is-valid",'+
'\n\t\t\t\terror: "is-invalid"\n\t\t\t}\n\t\t},\n';
addLineToFile(this, configPath, /let ENV = {/, validatedFormConfig);
// Add import of ember_aws_ehipster.css to apps.css
......
This blueprint uses the same signature as the model blueprint for defining the list of attributes of the entity and their type.
The following attribute types are supported: :array :boolean :date :object :number :string :your-custom-transform :belongs-to: :has-many:
ember model <name> <attr:type>
ember model <name> <attr:type> [<constraint>]
Generates an ember-data model.
You may generate models with as many attrs as you would like to pass. The following attribute types are supported:
<attr-name>
......@@ -27,4 +27,6 @@ The following attribute types are supported: :array :boolean :date :object :numb
name: DS.attr('string'),
price: DS.attr('number'),
misc: DS.attr()
});
\ No newline at end of file
});
Constraint can be added
\ No newline at end of file
......@@ -8,18 +8,16 @@ import { task } from "ember-concurrency";
export default Controller.extend({
isAddingEntry: false,
newEntry: EmberObject.create({<%=controllerInitEntity%>}),
<%=capitalizeEntityName%>Validations,
<%=relationshipHandling%>
<%=controllerModelTable%>
actions: {
submit() {
event.preventDefault();
let newEntity = this.store.createRecord('<%=singularEntityName%>',{<%=controllerCreateEntity%>});
newEntity.save();
this.set('addEntryModal', false).then((entry) => {
console.log("new entry of id "+entry.get('id')+" created");
});
console.log("action submit");
},
deleteRecord (record) {
console.log('record is '+ record);
......
......@@ -2,7 +2,13 @@ import Route from '@ember/routing/route';
export default Route.extend({
model() {
return this.store.findAll('<%=singularEntityName%>');
return Ember.RSVP.hash({
<%=routeLoadModels%>
});
},
setupController(controller, models) {
controller.setProperties(models);
}
});
\ No newline at end of file
......@@ -82,8 +82,10 @@ module.exports = {
entityModel: entityModel(camelizedName, options.entity.options),
mirageFactory: mirageFactory(camelizedName, options.entity.options),
mirageModel: mirageModel(camelizedName, options.entity.options),
routeLoadModels: routeLoadModels(camelizedName, options.entity.options),
controllerInitEntity: controllerInitEntity(camelizedName, options.entity.options),
controllerCreateEntity: controllerCreateEntity(camelizedName, options.entity.options),
relationshipHandling: relationshipHandling(camelizedName, options.entity.options),
controllerModelTable: controllerModelTable(camelizedName, options.entity.options),
templateEntityForm: templateEntityForm(camelizedName, options.entity.options)
};
......@@ -161,35 +163,15 @@ function addLineToFile(ctx, filePath, markerString, addedLine) {
}
};
function templateEntityForm(name, options) {
let capitalizeName = stringUtils.capitalize(name);
let form = ['{{#validated-form model = (changeset newEntry '+capitalizeName+'Validations) on-submit = (perform submitEntry) as |f|}}'];
let attributes = [];
function relationshipHandling(name, options) {
let fetchedRelations = [];
for (var prop in options) {
let type = options[prop].split(':')[0];
let targetEntity = options[prop].split(':')[1];
switch (type) {
case "string":
case "number":
form.push('{{f.input label="'+prop+'" name="'+prop+'" }}');
break;
case "boolean":
form.push('\t\t{{f.input type="checkbox" label="'+prop+'" name="'+prop+'" }}');
break;
case "date":
form.push('\t\t{{#f.input label="'+prop+'" name="'+prop+'" as |fi|}}\n' +
'\t\t\t{{pikaday-input useUTC=true format="DD/MM/YYYY" onSelection=fi.update}}\n' +
'\t\t{{/f.input}}');
break;
case 'belongs-to':
// todo How to deal with relationships
break;
case 'has-many':
// todo How to deal with relationships
fetchedRelations.push("\t"+targetEntity+"List: computed('"+prop+"', function() {\n\t\tthis.store.findAll('"+targetEntity+"');\n\t}),");
break;
default:
......@@ -197,37 +179,47 @@ function templateEntityForm(name, options) {
break;
}
}
form.push('\t\t{{f.submit class="btn btn-primary" label="Save"}}\n\t{{/validated-form}}');
return form.join('\n');
};
return fetchedRelations.join('\n');
}
function templateEntityForm2(name, options) {
let form = ['{{#bs-form formLayout="vertical" model=this onSubmit=(action "submit") as |form|}}'];
function templateEntityForm(name, options) {
let capitalizeName = stringUtils.capitalize(name);
let form = ['{{#validated-form model = (changeset newEntry '+capitalizeName+'Validations) on-submit = (perform submitEntry) as |f|}}'];
let attributes = [];
for (var prop in options) {
let type = options[prop].split(':')[0];
let targetEntity = options[prop].split(':')[1];
let optionArray = options[prop].split(':');
let type = optionArray[0];
let targetEntity = optionArray[1];
let mapBy = optionArray[2];
if (!mapBy) {
mapBy='id';
}
switch (type) {
case "string":
case "number":
form.push('{{form.element controlType="text" label=\"' + prop + '\" value=' + prop + ' onChange=(action (mut ' + prop + ')) placeholder=\"' + prop + '\"}}');
form.push('\t\t\t{{f.input label="'+prop+'" name="'+prop+'" }}');
break;
case "boolean":
form.push('{{form.element controlType="checkbox" label=\"' + prop + '\" value=' + prop + ' onChange=(action (mut ' + prop + ')) placeholder=\"' + prop + '\"}}');
form.push('\t\t\t{{f.input type="checkbox" label="'+prop+'" name="'+prop+'" }}');
break;
case "date":
form.push('<div class="row form-group"><label class="col-form-label col-md-4">' + prop +
'</label>{{pikaday-input useUTC=true format="DD/MM/YYYY" onSelection=(action (mut ' + prop + '))}}</div>');
form.push('\t\t\t{{#f.input label="'+prop+'" name="'+prop+'" as |fi|}}\n' +
'\t\t\t\t{{pikaday-input useUTC=true format="DD/MM/YYYY" onSelection=fi.update}}\n' +
'\t\t\t{{/f.input}}');
break;
case 'belongs-to':
// todo How to deal with relationships
form.push('\t\t\t{{f.input type="select" label="'+prop+'" name="'+prop+ " \n"+
'options='+inflection.pluralize(targetEntity)+' optionLabelPath="'+mapBy+'" optionValuePath="id" \n'+
'includeBlank= "Please choose..." promptIsSelectable=false}}');
break;
case 'has-many':
// todo How to deal with relationships
form.push('\t\t\t{{f.input type="select" label="'+prop+'" name="'+prop+ " multiple=true \n"+
'options='+inflection.pluralize(targetEntity)+' optionLabelPath="'+mapBy+'" optionValuePath="id" \n'+
'includeBlank= "Please choose..." promptIsSelectable=false}}');
break;
default:
......@@ -235,7 +227,7 @@ function templateEntityForm2(name, options) {
break;
}
}
form.push("{{/bs-form}}");
form.push('\t\t{{f.submit class="btn btn-primary" label="Save"}}\n\t{{/validated-form}}');
return form.join('\n');
};
......@@ -310,6 +302,30 @@ function templateEntityForm2(name, options) {
return content;
};
function routeLoadModels(name, options) {
let content = "";
let findAll = [];
// Add main object loading
findAll.push("\t\t\t"+inflection.pluralize(name)+": this.store.findAll('"+name+"')");
for (var prop in options) {
let type = options[prop].split(':')[0];
let targetEntity = options[prop].split(':')[1];
switch (type) {
case 'belongs-to':
case 'has-many':
findAll.push("\t\t\t"+inflection.pluralize(targetEntity)+": this.store.findAll('"+targetEntity+"')");
break;
default:
// do nothing
break;
}
}
content += findAll.join(',\n');
return content;
}
function controllerInitEntity(name, options) {
let attributes = [];
for (var prop in options) {
......@@ -387,9 +403,9 @@ function templateEntityForm2(name, options) {
break;
}
}
content += attributes.join('\n') + name + "TableColumns: computed(function() {\n\tvar col = A([\n";
content += attributes.join('\n') + '\n' + name + "TableColumns: computed(function() {\n\tvar col = A([\n";
content += tablecols.join(',\n') + "]);\n\tcol.pushObject({\n\t\ttitle: 'Delete',\n\t\tcomponent: 'delete-row'\n\t});\n\treturn col;\t}),\n";
content += name + "TableContent: computed(function() {\n\treturn this.get('model');\n}),";
content += name + "TableContent: computed(function() {\n\treturn this.get('"+inflection.pluralize(name)+"');\n}),";
return content;
};
......@@ -427,19 +443,31 @@ function templateEntityForm2(name, options) {
};
function entityValidation(name, options) {
let content = "import {validatePresence, validateLength} from 'ember-changeset-validations/validators';\n\n" +
let content = "import {validatePresence, validateLength, validateFormat} from 'ember-changeset-validations/validators';\n\n" +
"export default {\n";
for (var prop in options) {
let attr = options[prop].split(':')[1];
if (attr) { // there is at least one constraint
let constraints = [];
let lengthConstraint = [];
let blocks = attr.split(',');
for (var i = 0; i < blocks.length; i++) {
let constraint = blocks[i];
if (constraint.startsWith('required')) {
constraints.push('validatePresence(true)');
} else if (constraint.startsWith('minlength')) {
lengthConstraint.push('min: '+searchArgument(constraint))
} else if (constraint.startsWith('minlength')) {
lengthConstraint.push('max: '+searchArgument(constraint))
}
}
// A rule using property name just to try to be a little clever about validation..$.
if (prop.toLowerCase().includes('mail')){
constraints.push("validateFormat({ type: 'email' })")
}
if (lengthConstraint.length>0) {
constraints.push('validateLength({'+lengthConstraint.join(',')+'})');
}
if (constraints.length>0)
content += "\t" + prop + ": [" + constraints.join(',') + "],\n"
}
......@@ -448,6 +476,10 @@ function templateEntityForm2(name, options) {
return content;
}
function searchArgument(constraint) {
return constraint.substring(constraint.indexOf('§')+1);
}
function dsAttr(name, type) {
switch (type) {
case 'belongs-to':
......
......@@ -68,7 +68,7 @@ module.exports = {
validation = validations[k];
let validationRule = validation.key;
if (validation.value) {
validationRule +='§' + validation.value;
validationRule +='§'+validation.value;
}
validationRules.push(validationRule)
}
......
'use strict';
const AWS = require('aws-sdk');
//const inflection = require('inflection');
// BEGIN : To be removed from lambda deployment
AWS.config.update({region:'us-east-1'});
const msgPut = {
"body-json": {
"data": {
"type": "photos",
"attributes": {
"title": "Ember Hamster",
"src": "http://example.com/images/productivity.png"
},
"relationships": {
"photographer": {
"data": { "type": "people", "id": "9" }
}
}
}
},
"params": {
"path": {
"type": "photos"
},
"querystring": {},
"header": {
"Accept": "*/*",
"accept-encoding": "gzip, deflate",
"cache-control": "no-cache",
"Content-Type": "application/json",
"Host": "9o6yk638sk.execute-api.us-east-1.amazonaws.com",
"Postman-Token": "ca9ccefd-42e1-4bad-85bd-620050ef1770",
"User-Agent": "PostmanRuntime/7.4.0",
"X-Amz-Date": "20181122T091255Z",
"X-Amzn-Trace-Id": "Root=1-5bf67317-3e732e202ed67512573c7ca4",
"X-Forwarded-For": "195.5.224.170",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https"
}
},
"stage-variables": {},
"context": {
"account-id": "",
"api-id": "9o6yk638sk",
"api-key": "",
"authorizer-principal-id": "",
"caller": "",
"cognito-authentication-provider": "",
"cognito-authentication-type": "",
"cognito-identity-id": "",
"cognito-identity-pool-id": "",
"httpMethod": "POST",
"stage": "default",
"source-ip": "195.5.224.170",
"user": "",
"user-agent": "PostmanRuntime/7.4.0",
"user-arn": "",
"request-id": "cc5001d5-ee36-11e8-8ae7-273051a62acd",
"resource-id": "anwfak",
"resource-path": "/EmberDataServerless/{type}"
}
};
// END
const dynamo = new AWS.DynamoDB.DocumentClient();
const tableName = 'EmberDataServerlessTable';
const EPOCH = 1300000000000;
const tableName = 'JsonApiTable';
const EPOCH = 1545907609050;
const MAX_OBJECTS=1000; // Max number of objects returned by finder
// Instagram inspired --> https://instagram-engineering.com/sharding-ids-at-instagram-1cf5a71e5a5c
function generateRowId(subid) {
......@@ -91,15 +38,30 @@ function generateRowId(subid) {
objout.attributes[attr] = obj[attr];
} else {
let relationDetails = attr.split('_');
let relationId = Number(obj[attr]);
objout.relationships[relationDetails[0]] = {
"links": {
"self": "/"+obj.ObjectType+"/"+obj.Id+"/relationships/"+relationDetails[0],
"related": "/"+obj.ObjectType+"/"+obj.Id+"/"+relationDetails[0]
},
"data": {"type": relationDetails[1], "id": relationId}
};
// TODO what's about if relation is not a belongsTo but a hasMany...
if (obj[attr].startsWith('[')) {
// hasMany relationship
let idsArray = obj[attr].substring(1, obj[attr].length-1).split(',');
let relData = [];
for (let l=0;l<idsArray.length;l++) {
let relObject = {"type": relationDetails[0], "id": Number(idsArray[l]) };
relData.push(relObject); }
objout.relationships[relationDetails[0]] = {
"links": {
"self": "/"+obj.ObjectType+"/"+obj.Id+"/relationships/"+relationDetails[0],
"related": "/"+obj.ObjectType+"/"+obj.Id+"/"+relationDetails[0]
},
"data": relData
};
} else {
// belongsTo relationship
objout.relationships[relationDetails[0]] = {
"links": {
"self": "/"+obj.ObjectType+"/"+obj.Id+"/relationships/"+relationDetails[0],
"related": "/"+obj.ObjectType+"/"+obj.Id+"/"+relationDetails[0]
},
"data": {"type": relationDetails[1], "id": Number(obj[attr])}
};
}
}
} else {
objout[(attr==='ObjectType')?'type':'id'] = obj[attr];
......@@ -109,7 +71,7 @@ function generateRowId(subid) {
return objout;
}
const createData = (data) => {
const handlingData = (data) => {
if (Array.isArray(data)) {
let outdata = [];
for (let i=0;i<data.length;i++){
......@@ -121,16 +83,25 @@ function generateRowId(subid) {
}
}
const createRelationships = (data) => {
return {};
const createRelationships= (obj) => {
}
const handlingRelationships = (data) => {
if (Array.isArray(data)) {
console.warn("Not handling correctly relationship when retrieving list of objects");
} else {
}
return {};
}
const createResponse = (statusCode, body) => {
console.log("Body is "+JSON.stringify(body));
return {
'statusCode': statusCode,
'data': createData(body),
'relationships': createRelationships(body)
'data': handlingData(body),
'relationships': handlingRelationships(body)
}
};
......@@ -203,25 +174,25 @@ function generateRowId(subid) {
// Dealing with relationships if any
if (relations){
for (var relName in relations) {
let relData = relations[relName]["data"];
let newCol;
if (!Array.isArray(relData)) {
newCol = relName+'_'+relData["type"]+'_id';
content[newCol] = relData["id"];
} else {
for (var i=0; i<relData.length;i++){
let currentData = relData[i];
newCol = relName+'_'+currentData["type"]+'_id';
content[newCol] = currentData["id"];
}
}
}
}
let newCol = relName+'_'+type+'_id';
let relData = relations[relName]["data"];
if (!Array.isArray(relData)) {
content[newCol] = relData["id"];
} else {
let ids = [];
for (var i=0; i<relData.length;i++){
let currentData = relData[i];
ids.push(currentData["id"]);
}
content[newCol] = '['+ids.join(',')+']';
}
}
}
const entry = {
TableName: 'EmberDataServerlessTable',
Item: content
};
const entry = {
TableName: tableName,
Item: content
};
console.log('Try saving entity of type '+type+' and content '+JSON.stringify(entry));
//let dbPut = (entry) => { return dynamo.put(entry).promise() };
dynamo.put(entry, function(err, data) {
......@@ -272,7 +243,7 @@ const callback = (evt, msg) => {console.log(msg);};
const msgGet = {
"params": {
"path": {
"type": "photos"
"type": "users"
},
"querystring": {},
"header": {}
......@@ -299,4 +270,92 @@ const msgGet = {
}
};
getMethod(msgGet,{},callback)
\ No newline at end of file
const msgPost = {
"body-json": {
"data": {
"attributes": {
"login": "freebox",
"password-hash": "sgsUIHSFDK",
"first-name": "Laure",
"last-name": "PINEL",
"email": "laurepinel@laposte.net",
"activated": 0,
"created-by": "Laure PINEL",
"created-date": "2018-12-26T21:17:32.332Z",
"last-modified-date": "2018-12-26T21:17:32.332Z"
},
"relationships": {
"authorities": {
"data": [
{
"type": "authorities",
"id": "8018931201376941"
},
{
"type": "authorities",
"id": "8053142279390032"
}
]
}
},
"type": "users"
}
},
"params": {
"path": {
"type": "users"
},
"querystring": {},
"header": {
"Accept": "application/vnd.api+json",
"Accept-Encoding": "gzip, deflate, br",
"Accept-Language": "fr-FR,fr;q=0.9,en-US;q=0.8,en;q=0.7",
"cache-control": "no-cache",
"CloudFront-Forwarded-Proto": "https",
"CloudFront-Is-Desktop-Viewer": "true",
"CloudFront-Is-Mobile-Viewer": "false",
"CloudFront-Is-SmartTV-Viewer": "false",
"CloudFront-Is-Tablet-Viewer": "false",
"CloudFront-Viewer-Country": "FR",
"content-type": "application/json; charset=UTF-8",
"Cookie": "G_AUTHUSER_H=0; io=INjGzOkni2CdTpLCAAAA; remember-me=QzhwSTVZZmpkTDFxaHJYSkJ5cUs6Znl0MGlnRk1YTGhGbnAySGpEbzE",
"Host": "dcftkivcqe.execute-api.us-east-1.amazonaws.com",
"origin": "http://localhost:4200",
"pragma": "no-cache",
"Referer": "http://localhost:4200/entity-factory/user?login=freebox&passwordHash=sgsgq+%22%27a%28fqgg&firstName=Laure&lastName=PINEL&email=laurepinel%40laposte.net&imageUrl=&activated=0&createdBy=Laure+PINEL&lastModifiedBy=&authorities=GUEST&authorities=GATSBY",
"User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/71.0.3578.98 Safari/537.36",
"Via": "1.1 cbf986a33f2676c4c9c2ef12cabb1a32.cloudfront.net (CloudFront)",
"X-Amz-Cf-Id": "Z5P_b1_CJqTWY6ZCy1BTPS2E0SBU_uJn3NHjGTj9DfCTgC0Ps_oiQw==",
"X-Amzn-Trace-Id": "Root=1-5c23f0b0-94250540b6531d00e8bf9600",
"x-broccoli": "[object Object]",
"X-Forwarded-For": "::1, 78.119.97.214, 52.46.15.94",
"x-forwarded-host": "localhost:4200",
"X-Forwarded-Port": "443",
"X-Forwarded-Proto": "https",
"x-requested-with": "XMLHttpRequest"
}
},
"context": {
"httpMethod": "POST",
"account-id": "",
"api-id": "dcftkivcqe",
"api-key": "",
"authorizer-principal-id": "",
"caller": "",
"cognito-authentication-provider": "",
"cognito-authentication-type": "",
"cognito-identity-id": "",
"cognito-identity-pool-id": "",
"stage": "staging",<