From 45305ad8debbe52e94d0fef7f6e0e1c2412c2704 Mon Sep 17 00:00:00 2001 From: bertrand <bpinel@ippon.fr> Date: Thu, 27 Dec 2018 22:02:24 +0100 Subject: [PATCH] First implementation dealing with relationships --- blueprints/ember-aws-ehipster/index.js | 12 +- blueprints/entity-factory/README.md | 6 +- .../controllers/entity-factory/__name__.js | 10 +- .../routes/entity-factory/__name__.js | 8 +- blueprints/entity-factory/index.js | 112 ++++-- blueprints/jdl-importer/index.js | 2 +- cloud/lambda-jsonapi-test.js | 245 ++++++++----- cloud/lambda/lambda-jsonapi.js | 341 ++++++++++-------- package.json | 2 +- 9 files changed, 431 insertions(+), 307 deletions(-) diff --git a/blueprints/ember-aws-ehipster/index.js b/blueprints/ember-aws-ehipster/index.js index 42eed58..f651c31 100644 --- a/blueprints/ember-aws-ehipster/index.js +++ b/blueprints/ember-aws-ehipster/index.js @@ -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 diff --git a/blueprints/entity-factory/README.md b/blueprints/entity-factory/README.md index 2e4d353..18da950 100644 --- a/blueprints/entity-factory/README.md +++ b/blueprints/entity-factory/README.md @@ -1,7 +1,7 @@ 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 diff --git a/blueprints/entity-factory/files/__root__/controllers/entity-factory/__name__.js b/blueprints/entity-factory/files/__root__/controllers/entity-factory/__name__.js index 132bca7..a4cdad7 100644 --- a/blueprints/entity-factory/files/__root__/controllers/entity-factory/__name__.js +++ b/blueprints/entity-factory/files/__root__/controllers/entity-factory/__name__.js @@ -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); diff --git a/blueprints/entity-factory/files/__root__/routes/entity-factory/__name__.js b/blueprints/entity-factory/files/__root__/routes/entity-factory/__name__.js index c52f4ff..ab6b83d 100644 --- a/blueprints/entity-factory/files/__root__/routes/entity-factory/__name__.js +++ b/blueprints/entity-factory/files/__root__/routes/entity-factory/__name__.js @@ -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 diff --git a/blueprints/entity-factory/index.js b/blueprints/entity-factory/index.js index af1ad96..2e9506e 100644 --- a/blueprints/entity-factory/index.js +++ b/blueprints/entity-factory/index.js @@ -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': diff --git a/blueprints/jdl-importer/index.js b/blueprints/jdl-importer/index.js index 969b020..60c9b9b 100644 --- a/blueprints/jdl-importer/index.js +++ b/blueprints/jdl-importer/index.js @@ -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) } diff --git a/cloud/lambda-jsonapi-test.js b/cloud/lambda-jsonapi-test.js index 90e3836..5747004 100644 --- a/cloud/lambda-jsonapi-test.js +++ b/cloud/lambda-jsonapi-test.js @@ -1,71 +1,18 @@ '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", + "source-ip": "78.119.97.214", + "user": "", + "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", + "user-arn": "", + "request-id": "1d5b3483-0954-11e9-b697-131cc1d237db", + "resource-id": "cmo3pj", + "resource-path": "/{type}" + } +}; + +getMethod(msgGet, {}, callback); +//putMethod(msgPost, {}, callback); \ No newline at end of file diff --git a/cloud/lambda/lambda-jsonapi.js b/cloud/lambda/lambda-jsonapi.js index 1e111b6..99b21c4 100644 --- a/cloud/lambda/lambda-jsonapi.js +++ b/cloud/lambda/lambda-jsonapi.js @@ -2,21 +2,22 @@ const AWS = require('aws-sdk'); const dynamo = new AWS.DynamoDB.DocumentClient(); const tableName = 'JsonApiTable'; -const EPOCH = 1300000000000; +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) { - var ts = new Date().getTime() - EPOCH; // limit to recent - // 41 bits for time in milliseconds (gives us 41 years of IDs with a custom epoch - var randid = Math.floor(Math.random() * 512); - ts = (ts * 64); // bit-shift << 6 - // Given shard (if any...) - ts = ts + subid; - // random value - return (ts * 512) + (randid % 512); -} - -const createObject = (obj) => { + var ts = new Date().getTime() - EPOCH; // limit to recent + // 41 bits for time in milliseconds (gives us 41 years of IDs with a custom epoch + var randid = Math.floor(Math.random() * 512); + ts = (ts * 64); // bit-shift << 6 + // Given shard (if any...) + ts = ts + subid; + // random value + return (ts * 512) + (randid % 512); + } + + const createObject = (obj) => { let objout = { "type": obj.ObjectType, "id": obj.Id, "attributes": {}, @@ -28,177 +29,203 @@ const createObject = (obj) => { 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]; } } - console.log('JSONAPILambda Return object is '+JSON.stringify(objout)); + console.log('Return object is '+JSON.stringify(objout)); return objout; } -const createData = (data) => { - if (Array.isArray(data)) { - let outdata = []; - for (let i=0;i<data.length;i++){ - outdata.push(createObject(data[i])); - } - return outdata; - } else { - return createObject(data); - } -} + const handlingData = (data) => { + if (Array.isArray(data)) { + let outdata = []; + for (let i=0;i<data.length;i++){ + outdata.push(createObject(data[i])); + } + return outdata; + } else { + return createObject(data); + } + } + + const createRelationships= (obj) => { -const createRelationships = (data) => { - return {}; -} - -const createResponse = (statusCode, body) => { - console.log("JSONAPILambda Body is "+JSON.stringify(body)); - return { - 'statusCode': statusCode, - 'data': createData(body), - 'relationships': createRelationships(body) - } -}; - -const getMethod = (event, context, callback) => { - - let params = { - TableName: tableName, - }, - type = event.params.path.type, - id = Number(event.params.path.id), - dbGet = {}; - - if (id) { - params.Key = { - 'ObjectType': type, - 'Id': id - }; - dbGet = (params) => { return dynamo.get(params).promise() }; - console.log('JSONAPILambda Lambda GET single value with params: ', params); + } + const handlingRelationships = (data) => { + if (Array.isArray(data)) { + console.warn("Not handling correctly relationship when retrieving list of objects"); } else { - params.KeyConditionExpression = 'ObjectType = :objectType'; - params.ExpressionAttributeValues = { ':objectType': type }; - dbGet = (params) => { return dynamo.query(params).promise() }; - console.log('JSONAPILambda Lambda GET multiple values with params: ', params); - } - - dbGet(params).then( (data) => { - console.log('JSONAPILambda Lambda GET data received: ', data); - - if (id && !data.Item) { - callback(null, createResponse(404, "ITEM NOT FOUND")); - return; - } else if (id && data.Item) { - console.log(`JSONAPILambda RETRIEVED ITEM SUCCESSFULLY WITH doc = ${data.Item}`); - callback(null, createResponse(200, data.Item)); - } else { - console.log(`JSONAPILambda RETRIEVED ITEMS SUCCESSFULLY WITH doc = ${data.Items}`); - callback(null, createResponse(200, data.Items)); - } - - }).catch( (err) => { - console.log(`JSONAPILambda GET ITEM FAILED FOR Entry = ${params}, WITH ERROR: ${err}`); - callback(null, createResponse(500, err)); - }); -}; -const putMethod = (event, context, callback) => { - const body=event['body-json']; - const attrs = body.data.attributes; - const relations = body.data.relationships; - // Without any body content, there is nothing to put... - if (!body || !attrs) { - callback(null, createResponse(500, 'No content found in body')); - return; - } - // Retrieving the type and generating a new id for created item - let type = event.params.path.type, - id = generateRowId(1); - // Final content contains at least these two fields + the atributes - let content = { - "ObjectType": type, - "Id": id - }; - // Adding attributes as column in dynamoDb - for (var prop in attrs) { - content[prop] = attrs[prop]; } - // Dealing with relationships if any - if (relations){ - for (var relName in relations) { + return {}; + } + + const createResponse = (statusCode, body) => { + console.log("Body is "+JSON.stringify(body)); + return { + 'statusCode': statusCode, + 'data': handlingData(body), + 'relationships': handlingRelationships(body) + } + }; + + const getMethod = (event, context, callback) => { + + let params = { + TableName: tableName, + }, + type = event.params.path.type, + id = Number(event.params.path.id), + dbGet = {}; + + if (id) { + params.Key = { + 'ObjectType': type, + 'Id': id + }; + dbGet = (params) => { return dynamo.get(params).promise() }; + console.log('EmberDataServerless lambda GET single value with params: ', params); + + } else { + params.KeyConditionExpression = 'ObjectType = :objectType'; + params.ExpressionAttributeValues = { ':objectType': type }; + dbGet = (params) => { return dynamo.query(params).promise() }; + console.log('EmberDataServerless lambda GET multiple values with params: ', params); + } + + dbGet(params).then( (data) => { + console.log('EmberDataServerless lambda GET data received: ', data); + + if (id && !data.Item) { + callback(null, createResponse(404, "ITEM NOT FOUND")); + return; + } else if (id && data.Item) { + console.log(`RETRIEVED ITEM SUCCESSFULLY WITH doc = ${data.Item}`); + callback(null, createResponse(200, data.Item)); + } else { + console.log('SCANNING TABLE'); + console.log(`RETRIEVED ITEMS SUCCESSFULLY WITH doc = ${data.Items}`); + callback(null, createResponse(200, data.Items)); + } + + }).catch( (err) => { + console.log(`GET ITEM FAILED FOR Entry = ${params}, WITH ERROR: ${err}`); + callback(null, createResponse(500, err)); + }); + }; + + const putMethod = (event, context, callback) => { + const body=event['body-json']; + const attrs = body.data.attributes; + const relations = body.data.relationships; + // Without any body content, there is nothing to put... + if (!body || !attrs) { + callback(null, createResponse(500, 'No content found in body')); + return; + } + // Retrieving the type and generating a new id for created item + let type = event.params.path.type, + id = generateRowId(1); + // Final content contains at least these two fields + the atributes + let content = { + "ObjectType": type, + "Id": id + }; + // Adding attributes as column in dynamoDb + for (var prop in attrs) { + content[prop] = attrs[prop]; + } + // Dealing with relationships if any + if (relations){ + for (var relName in relations) { + let newCol = relName+'_'+type+'_id'; let relData = relations[relName]["data"]; - let newCol; if (!Array.isArray(relData)) { - newCol = relName+'_'+relData["type"]+'_id'; - content[newCol] = relData["id"]; + content[newCol] = relData["id"]; } else { + let ids = []; for (var i=0; i<relData.length;i++){ let currentData = relData[i]; - newCol = relName+'_'+currentData["type"]+'_id'; - content[newCol] = currentData["id"]; + ids.push(currentData["id"]); } + content[newCol] = '['+ids.join(',')+']'; } } } - + const entry = { TableName: tableName, Item: content }; - console.log('JSONAPILambda 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) { - if (err) { - console.log("JSONAPILambda Error", err); - callback(null, createResponse(500, 'Error '+err)); - } else { - body.data.id = id; - body['statusCode'] = 200; - console.log(`JSONAPILambda PUT ITEM SUCCEEDED WITH data=`+JSON.stringify(body)); - callback(null, body); - } - }); -}; - -const deleteMethod = (event, context, callback) => { - let type = event.params.path.type, - id = Number(event.params.path.id), - params = { - 'TableName': tableName, - 'Key': { - 'ObjectType': type, - 'Id': id - }, - 'ReturnValues': 'ALL_OLD' - }; - const body=event['body-json']; - - let dbDelete = (params) => { return dynamo.delete(params).promise() }; - dbDelete(params).then( (data) => { - if (!data.Attributes) { - callback(null, createResponse(404, "ITEM NOT FOUND FOR DELETION")); - return; - } - console.log(`JSONAPILambda DELETED ITEM OF TYPE ${type} SUCCESSFULLY WITH id = ${id}`); - callback(null, body); - }).catch( (err) => { - console.log(`JSONAPILambda DELETE ITEM OF TYPE ${type} FAILED FOR id = ${id}, WITH ERROR: ${err}`); - callback(null, createResponse(500, err)); - }); -}; + 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) { + if (err) { + console.log("Error", err); + callback(null, createResponse(500, 'Error '+err)); + } else { + body.data.id = id; + body['statusCode'] = 200; + console.log(`PUT ITEM SUCCEEDED WITH data=`+JSON.stringify(body)); + callback(null, body); + } + }); + }; + + const deleteMethod = (event, context, callback) => { + let type = event.params.path.type, + id = Number(event.params.path.id), + params = { + 'TableName': tableName, + 'Key': { + 'ObjectType': type, + 'Id': id + }, + 'ReturnValues': 'ALL_OLD' + }; + const body=event['body-json']; + + let dbDelete = (params) => { return dynamo.delete(params).promise() }; + dbDelete(params).then( (data) => { + if (!data.Attributes) { + callback(null, createResponse(404, "ITEM NOT FOUND FOR DELETION")); + return; + } + console.log(`DELETED ITEM OF TYPE ${type} SUCCESSFULLY WITH id = ${id}`); + callback(null, body); + }).catch( (err) => { + console.log(`DELETE ITEM OF TYPE ${type} FAILED FOR id = ${id}, WITH ERROR: ${err}`); + callback(null, createResponse(500, err)); + }); + }; + exports.handler = (event, context, callback) => { console.log("JSONAPILambda ********************** Received Event *******************\n"+ JSON.stringify(event)); diff --git a/package.json b/package.json index 23e2006..7126e88 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-aws-ehipster", - "version": "0.3.17", + "version": "0.3.21", "description": "Attempt to build a complete web application using serverless architecture on AWS", "keywords": [ "ember-addon", -- GitLab