From abb810e553cb9f705c5580b04e2c7191950e225f Mon Sep 17 00:00:00 2001 From: bertrand <bpinel@ippon.fr> Date: Tue, 27 Nov 2018 13:46:01 +0100 Subject: [PATCH] Add delete feature --- addon/components/delete-row.js | 15 + addon/styles/ember-aws-ehipster.css | 39 ++ addon/templates/components/delete-row.hbs | 5 + app/components/delete-row.js | 1 + blueprints/ember-aws-ehipster/index.js | 6 + .../controllers/entity-factory/__name__.js | 8 + .../templates/entity-factory/__name__.hbs | 9 +- blueprints/entity-factory/index.js | 5 +- cloud/lambda-jsonapi.js | 384 +++++++++--------- package.json | 2 +- .../integration/components/delete-row-test.js | 26 ++ 11 files changed, 306 insertions(+), 194 deletions(-) create mode 100644 addon/components/delete-row.js create mode 100644 addon/styles/ember-aws-ehipster.css create mode 100644 addon/templates/components/delete-row.hbs create mode 100644 app/components/delete-row.js create mode 100644 tests/integration/components/delete-row-test.js diff --git a/addon/components/delete-row.js b/addon/components/delete-row.js new file mode 100644 index 0000000..6fe6652 --- /dev/null +++ b/addon/components/delete-row.js @@ -0,0 +1,15 @@ +import Component from '@ember/component'; +import {get} from '@ember/object'; +import layout from '../templates/components/delete-row'; + +export default Component.extend({ + layout, + sendAction: null, + record: null, + actions: { + sendAction(actionName, record, event) { + get(this, 'sendAction')(actionName, record); + event.stopPropagation(); + } + } +}); \ No newline at end of file diff --git a/addon/styles/ember-aws-ehipster.css b/addon/styles/ember-aws-ehipster.css new file mode 100644 index 0000000..f344568 --- /dev/null +++ b/addon/styles/ember-aws-ehipster.css @@ -0,0 +1,39 @@ +a:hover { + text-decoration: none; +} +a.active { + font-weight: 700; +} +pre { + text-align: left; + white-space: pre-line; +} +.table-footer { + border: 1px solid #ddd; + padding: 5px 0; +} + +.models-table-wrapper { + margin-bottom: 20px; +} + +.btn-default { + background-image: none !important; +} + +.columns-dropdown { + margin-bottom: 20px; +} + +.table-column-options.table > tbody > tr > td { + border-top-width: 0; +} + +tr.selected-row>td:not(.grouping-cell), tr.selected-expand>td:not(.grouping-cell) { + background: #C6E746; +} + +.navbar-text.gh { + margin-top: 13px !important; + margin-bottom: 11px !important; +} \ No newline at end of file diff --git a/addon/templates/components/delete-row.hbs b/addon/templates/components/delete-row.hbs new file mode 100644 index 0000000..94c03a1 --- /dev/null +++ b/addon/templates/components/delete-row.hbs @@ -0,0 +1,5 @@ +<button + class="btn btn-default" + onclick={{action "sendAction" "delete" record}}> + Delete +</button> diff --git a/app/components/delete-row.js b/app/components/delete-row.js new file mode 100644 index 0000000..d13ed0a --- /dev/null +++ b/app/components/delete-row.js @@ -0,0 +1 @@ +export { default } from 'ember-aws-ehipster/components/delete-row'; \ No newline at end of file diff --git a/blueprints/ember-aws-ehipster/index.js b/blueprints/ember-aws-ehipster/index.js index 5cd364b..13ad7a0 100644 --- a/blueprints/ember-aws-ehipster/index.js +++ b/blueprints/ember-aws-ehipster/index.js @@ -33,6 +33,12 @@ module.exports = { "\treturn usingProxyArg || hasGeneratedProxies;\n}\n"; addLineToFile(this, configPath, /'use strict';/, proxy); addLineToFile(this, configPath, /when it is created/, "\t\tproxy: usingProxy(),"); + + // Add import of ember_aws_ehipster.css to apps.css + let stylePath = (options.dummy) ? "tests/dummy/app/styles/app.css" : "app/styles/app.css"; + let importcss = "@import 'ember-aws-ehipster.css';\n"; + let fileContents = importcss + fs.readFileSync(stylePath, 'utf-8'); + fs.writeFileSync(stylePath, fileContents, 'utf-8'); } }; 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 b24e041..8a065d9 100644 --- a/blueprints/entity-factory/files/__root__/controllers/entity-factory/__name__.js +++ b/blueprints/entity-factory/files/__root__/controllers/entity-factory/__name__.js @@ -10,6 +10,10 @@ isAddingEntry: false, var col = A([ <%=tableCols%> ]); +col.pushObject({ + title: 'Delete', + component: 'delete-row' +}); return col; }), @@ -25,6 +29,10 @@ actions: { this.set('addEntryModal', false).then((entry) => { console.log("new entry of id "+entry.get('id')+" created"); }); + }, + deleteRecord (record) { + console.log('record is '+ record); + record.destroyRecord(); } } }); \ No newline at end of file diff --git a/blueprints/entity-factory/files/__root__/templates/entity-factory/__name__.hbs b/blueprints/entity-factory/files/__root__/templates/entity-factory/__name__.hbs index 25bb1d2..9db66f6 100644 --- a/blueprints/entity-factory/files/__root__/templates/entity-factory/__name__.hbs +++ b/blueprints/entity-factory/files/__root__/templates/entity-factory/__name__.hbs @@ -2,9 +2,12 @@ <div class="container-fluid"> <h2>List of <%=singularEntityName%> Entities</h2> -{{models-table - data=<%=singularEntityName%>TableContent - columns=<%=singularEntityName%>TableColumns}} + +{{#models-table data=<%=singularEntityName%>TableContent columns=<%=singularEntityName%>TableColumns delete="deleteRecord" as |mt|}} + {{mt.global-filter}} + {{mt.table}} + {{mt.footer}} +{{/models-table}} {{#bs-button onClick=(action (mut addEntryModal) true)}} Add new entry diff --git a/blueprints/entity-factory/index.js b/blueprints/entity-factory/index.js index 6f70157..359db11 100644 --- a/blueprints/entity-factory/index.js +++ b/blueprints/entity-factory/index.js @@ -161,7 +161,10 @@ module.exports = { // Complete /mirage/config.js let mirageConfigPath = (options.dummy) ? "tests/dummy/mirage/config.js" : "mirage/config.js"; - let configLine = "\t\tthis.get('/"+inflection.pluralize(entityName)+"', '"+inflection.pluralize(entityName)+"');\n"+ + let configLine = "\t\tthis.get('/"+inflection.pluralize(entityName)+"');\n"+ + "\t\tthis.get('/"+inflection.pluralize(entityName)+"/:id');\n"+ + "\t\tthis.delete('/"+inflection.pluralize(entityName)+"/:id');\n"+ + "\t\tthis.patch('/"+inflection.pluralize(entityName)+"/:id');\n"+ "\t\tthis.post('/"+inflection.pluralize(entityName)+"');"; if (!fs.existsSync(mirageConfigPath)) { this.ui.writeLine("Creating file "+mirageConfigPath); diff --git a/cloud/lambda-jsonapi.js b/cloud/lambda-jsonapi.js index 19013c5..90e3836 100644 --- a/cloud/lambda-jsonapi.js +++ b/cloud/lambda-jsonapi.js @@ -69,195 +69,201 @@ const EPOCH = 1300000000000; // 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) => { - let objout = { "type": obj.ObjectType, - "id": obj.Id, - "attributes": {}, - "relationships": {} - }; - for (var attr in obj) { - if (attr !== 'ObjectType' && attr !== 'Id') { - if(!attr.endsWith('_id')) { - 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... - } - } - } - 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 createRelationships = (data) => { - return {}; -} - -const createResponse = (statusCode, 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('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 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"]; - } - } - } - } - - const entry = { - TableName: 'EmberDataServerlessTable', - 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) { - 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' - }; - 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, createResponse(200,data)); - }).catch( (err) => { - console.log(`DELETE ITEM OF TYPE ${type} FAILED FOR id = ${id}, WITH ERROR: ${err}`); - callback(null, createResponse(500, err)); - }); -}; - + 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": {}, + "relationships": {} + }; + for (var attr in obj) { + if (attr !== 'ObjectType' && attr !== 'Id') { + if(!attr.endsWith('_id')) { + 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... + } + } else { + objout[(attr==='ObjectType')?'type':'id'] = obj[attr]; + } + } + 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 createRelationships = (data) => { + return {}; + } + + const createResponse = (statusCode, body) => { + console.log("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('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 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"]; + } + } + } + } + + const entry = { + TableName: 'EmberDataServerlessTable', + 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) { + 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)); + }); + }; + const callback = (evt, msg) => {console.log(msg);}; diff --git a/package.json b/package.json index b088e51..8786bdc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "ember-aws-ehipster", - "version": "0.1.1", + "version": "0.1.4", "description": "The default blueprint for ember-cli addons.", "keywords": [ "ember-addon" diff --git a/tests/integration/components/delete-row-test.js b/tests/integration/components/delete-row-test.js new file mode 100644 index 0000000..5600d15 --- /dev/null +++ b/tests/integration/components/delete-row-test.js @@ -0,0 +1,26 @@ +import { module, test } from 'qunit'; +import { setupRenderingTest } from 'ember-qunit'; +import { render } from '@ember/test-helpers'; +import hbs from 'htmlbars-inline-precompile'; + +module('Integration | Component | delete-row', function(hooks) { + setupRenderingTest(hooks); + + test('it renders', async function(assert) { + // Set any properties with this.set('myProperty', 'value'); + // Handle any actions with this.set('myAction', function(val) { ... }); + + await render(hbs`{{delete-row}}`); + + assert.equal(this.element.textContent.trim(), ''); + + // Template block usage: + await render(hbs` + {{#delete-row}} + template block text + {{/delete-row}} + `); + + assert.equal(this.element.textContent.trim(), 'template block text'); + }); +}); -- GitLab