diff --git a/README.md b/README.md index 936936d..fe28e44 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,6 @@ # SurveyJS + NodeJS + PostgreSQL Demo Example -This demo shows how to integrate [SurveyJS](https://surveyjs.io/) components with a NodeJS backend with PostgreSQL database as a storage. +This demo shows how to integrate [SurveyJS](https://surveyjs.io/) components with a NodeJS backend using a PostgreSQL database as a storage. [View Demo Online](https://surveyjs-nodejs.azurewebsites.net/) @@ -10,18 +10,41 @@ This demo must not be used as a real service as it doesn't cover such real-world ## Run the Application -Install [NodeJS](https://nodejs.org/) on your machine. -Install [Docker Desktop](https://docs.docker.com/desktop/) on your machine. -After that, run the following commands: +1. Install [NodeJS](https://nodejs.org/) and [Docker Desktop](https://docs.docker.com/desktop/) on your machine. -```bash -git clone https://github.com/surveyjs/surveyjs-nodejs-complex.git -cd surveyjs-nodejs-complex -docker compose up -d -``` +2. Run the following commands: -Open http://localhost:9080 in your web browser. + ```bash + git clone https://github.com/surveyjs/surveyjs-nodejs-postgresql.git + cd surveyjs-nodejs-postgresql + docker compose up -d + ``` + +3. Open http://localhost:9080 in your web browser. ## Client-Side App -The client-side part is the `surveyjs-react-client` React application. The current project includes only the application's build artifacts in the /public folder. Refer to the [surveyjs-react-client](https://github.com/surveyjs/surveyjs-react-client) repo for full code and information about the application. +The client-side part is the `surveyjs-react-client` React application. The current project includes only the application's build artifacts in the [public](./public/) directory. Refer to the [`surveyjs-react-client`](https://github.com/surveyjs/surveyjs-react-client) repo for full code and information about the application. + +## Integrate SurveyJS with PostgreSQL + +SurveyJS communicates with any database using JSON objects that contain either survey schemas or user responses. An SQL database should have two tables to store these objects: `surveys` and `results`. You can use the following SQL script to create them: [`surveyjs.sql`](postgres/initdb/surveyjs.sql). The diagram below shows the structure of these tables: + +![SurveyJS: The structure of database tables](https://github.com/surveyjs/surveyjs-nodejs-postgresql/assets/18551316/176a0e1d-963c-4ec0-a11d-33631aa05770) + +To modify data in the `surveys` and `results` tables, you need to implement several JavaScript functions. According to the tasks they perform, these functions can be split into three modules: + +- **SQL query builder** +JS functions that construct CRUD SQL queries (see the [`sql-crud-adapter.js`](express-app/db-adapters/sql-crud-adapter.js) file). + +- **SQL query runner** +JS functions that execute queries in an SQL database. The repository you are viewing includes a query runner for PostgreSQL databases (see the [`postgres.js`](express-app/db-adapters/postgres.js) file). You can use it as an example to create a query runner for any other SQL database. + +- **Survey storage** +JS functions that provide an API for working with survey schemas and user responses (see the [`survey-storage.js`](express-app/db-adapters/survey-storage.js) file). This API is used by the NodeJS application router (see the [`index.js`](express-app/index.js) file). + +These modules interact with each other as shown on the following diagram: + +![SurveyJS PostgreSQL Integration](https://github.com/surveyjs/surveyjs-nodejs-postgresql/assets/18551316/2676bb99-d5a1-40fb-b0bb-c780c382e177) + +If you want to integrate SurveyJS with other databases, you can modify or replace the query builder and query runner without changing the survey storage module. This approach is applied to MongoDB integration in the following repository: [`surveyjs-nodejs-mongodb`](https://github.com/surveyjs/surveyjs-nodejs-mongodb). diff --git a/express-app/db-adapters/demo-surveys.js b/express-app/db-adapters/demo-surveys.js deleted file mode 100644 index bc8427c..0000000 --- a/express-app/db-adapters/demo-surveys.js +++ /dev/null @@ -1,636 +0,0 @@ -const surveys = [{ - "id": "1", - "name": "Product Feedback Survey", - "json": { - "pages": [{ - "elements": [{ - "type": "matrix", - "name": "Quality", - "title": "Please indicate if you agree or disagree with the following statements", - "columns": [{ - "value": 1, - "text": "Strongly disagree" - }, { - "value": 2, - "text": "Disagree" - }, { - "value": 3, - "text": "Neutral" - }, { - "value": 4, - "text": "Agree" - }, { - "value": 5, - "text": "Strongly agree" - }], - "rows": [{ - "value": "affordable", - "text": "Product is affordable" - }, { - "value": "does what it claims", - "text": "Product does what it claims" - }, { - "value": "better then others", - "text": "Product is better than other products on the market" - }, { - "value": "easy to use", - "text": "Product is easy to use" - }] - }, { - "type": "rating", - "name": "satisfaction", - "title": "How satisfied are you with the product?", - "mininumRateDescription": "Not satisfied", - "maximumRateDescription": "Completely satisfied" - }, { - "type": "rating", - "name": "recommend friends", - "visibleIf": "{satisfaction} > 3", - "title": "How likely are you to recommend the product to a friend or colleague?", - "mininumRateDescription": "Won't recommend", - "maximumRateDescription": "Will recommend" - }, { - "type": "comment", - "name": "suggestions", - "title": "What would make you more satisfied with the product?" - }] - }, { - "elements": [{ - "type": "radiogroup", - "name": "price to competitors", - "title": "Compared to our competitors, do you feel the product is", - "choices": [ - "Less expensive", - "Priced about the same", - "More expensive", - "Not sure" - ] - }, { - "type": "radiogroup", - "name": "price", - "title": "Do you feel our current price is merited by our product?", - "choices": [ - "correct|Yes, the price is about right", - "low|No, the price is too low", - "high|No, the price is too high" - ] - }, { - "type": "multipletext", - "name": "pricelimit", - "title": "What is the... ", - "items": [{ - "name": "mostamount", - "title": "Most amount you would pay for a product like ours" - }, { - "name": "leastamount", - "title": "The least amount you would feel comfortable paying" - }] - }] - }, { - "elements": [{ - "type": "text", - "name": "email", - "title": 'Thank you for taking our survey. Please enter your email address and press the "Submit" button.' - }] - }] - } -}, { - "id": "2", - "name": "Customer and their partner income survey", - "json": { - "completeText": "Finish", - "pageNextText": "Continue", - "pagePrevText": "Previous", - "pages": [{ - "elements": [{ - "type": "panel", - "elements": [{ - "type": "html", - "name": "income_intro", - "html": - "Income. In this section, you will be asked about your current employment status and other ways you and your partner receive income. It will be handy to have the following in front of you: payslip (for employment details), latest statement from any payments (from Centrelink or other authority), a current Centrelink Schedule for any account-based pension from super, annuities, or other income stream products that you may own. If you don't have a current one, you can get these schedules by contacting your income stream provider." - }], - "name": "panel1" - }], - "name": "page0" - }, { - "elements": [{ - "type": "panel", - "elements": [{ - "type": "radiogroup", - "choices": [ - "Married", - "In a registered relationship", - "Living with my partner", - "Widowed", - "Single" - ], - "name": "maritalstatus_c", - "title": " " - }], - "name": "panel13", - "title": "What is your marital status?" - }], - "name": "page1" - }, { - "elements": [{ - "type": "panel", - "elements": [{ - "type": "panel", - "elements": [{ - "type": "radiogroup", - "choices": [{ - "value": "1", - "text": "Yes" - }, { - "value": "0", - "text": "No" - }], - "colCount": 2, - "isRequired": true, - "name": "member_receives_income_from_employment", - "title": " " - }, { - "type": "checkbox", - "name": "member_type_of_employment", - "visible": false, - "visibleIf": "{member_receives_income_from_employment} =1", - "title": " ", - "isRequired": true, - "choices": [ - "Self-employed", - "Other types of employment" - ] - }], - "name": "panel2", - "title": "You" - }, { - "type": "panel", - "elements": [{ - "type": "radiogroup", - "choices": [{ - "value": "1", - "text": "Yes" - }, { - "value": "0", - "text": "No" - }], - "colCount": 2, - "isRequired": true, - "name": "partner_receives_income_from_employment", - "title": " " - }, { - "type": "checkbox", - "name": "partner_type_of_employment", - "visible": false, - "visibleIf": "{partner_receives_income_from_employment} =1", - "title": " ", - "isRequired": true, - "choices": [ - "Self-employed", - "Other types of employment" - ] - }], - "name": "panel1", - "startWithNewLine": false, - "title": "Your Partner", - "visibleIf": - "{maritalstatus_c} = 'Married' or {maritalstatus_c} = 'In a registered relationship' or {maritalstatus_c} = 'Living with my partner'" - }], - "name": "panel5", - "title": "Do you and/or your partner currently receive income from employment?" - }], - "name": "page2" - }, { - "elements": [{ - "type": "panel", - "elements": [{ - "type": "panel", - "elements": [{ - "type": "paneldynamic", - "minPanelCount": 1, - "name": "member_array_employer_names", - "valueName": "member_array_employer", - "title": "Enter information about your employers", - "panelAddText": "Add another employer", - "panelCount": 1, - "templateElements": [{ - "type": "text", - "name": "member_employer_name", - "valueName": "name", - "title": "Employer name" - }] - }], - "name": "panel2", - "title": "You", - "visible": false, - "visibleIf": "{member_type_of_employment} contains 'Other types of employment'" - }, { - "type": "panel", - "elements": [{ - "type": "paneldynamic", - "minPanelCount": 1, - "name": "partner_array_employer_names", - "valueName": "partner_array_employer", - "title": "Enter information about employers of your partner", - "panelAddText": "Add another employer", - "panelCount": 1, - "templateElements": [{ - "type": "text", - "name": "partner_employer_name", - "valueName": "name", - "title": "Employer name" - }] - }], - "name": "panel8", - "startWithNewLine": false, - "title": "Your Partner", - "visible": false, - "visibleIf": - "{partner_type_of_employment} contains 'Other types of employment'" - }], - "name": "panel6", - "title": "Employers" - }], - "name": "page3.1", - "visible": false, - "visibleIf": - "{member_type_of_employment} contains 'Other types of employment' or {partner_type_of_employment} contains 'Other types of employment'" - }, { - "elements": [{ - "type": "panel", - "elements": [{ - "type": "panel", - "elements": [{ - "type": "paneldynamic", - "renderMode": "progressTop", - "allowAddPanel": false, - "allowRemovePanel": false, - "name": "member_array_employer_info", - "title": "Your employers", - "valueName": "member_array_employer", - "panelCount": 1, - "templateElements": [{ - "type": "panel", - "name": "panel_member_employer_address", - "title": "Contacts", - "elements": [{ - "type": "text", - "name": "member_employer_address", - "valueName": "address", - "title": "Address:" - }, { - "type": "text", - "name": "member_employer_phone", - "valueName": "phone", - "title": "Phone number:" - }, { - "type": "text", - "name": "member_employer_abn", - "valueName": "abn", - "title": "ABN:" - }] - }, { - "type": "panel", - "name": "panel_member_employer_role", - "title": "Are you a full time worker?", - "elements": [{ - "type": "radiogroup", - "choices": [ - "Full-time", - "Part-time", - "Casual", - "Seasonal" - ], - "name": "member_employer_role", - "title": " ", - "valueName": "role" - }] - }, { - "type": "panel", - "name": "panel_member_employer_hours_work", - "title": "How many hours do you work?", - "elements": [{ - "type": "text", - "inputType": "number", - "name": "member_employer_hours_worked", - "valueName": "hours_worked", - "title": "Hours:" - }, { - "type": "dropdown", - "name": "member_employer_hours_worked_frequency", - "title": "Work frequency:", - "valueName": "hours_worked_frequency", - "startWithNewLine": false, - "defaultValue": "Day", - "choices": [ - "Day", - "Week", - "Fortnight", - "Month", - "Year" - ] - }] - }, { - "type": "panel", - "name": "panel_member_employer_income", - "title": "What is your income?", - "elements": [{ - "type": "text", - "inputType": "number", - "name": "member_employer_income", - "valueName": "income", - "title": "Income:" - }, { - "type": "dropdown", - "name": "member_employer_income_frequency", - "title": "Income frequency:", - "valueName": "income_frequency", - "startWithNewLine": false, - "defaultValue": "Month", - "choices": [ - "Day", - "Week", - "Fortnight", - "Month", - "Year" - ] - }] - }], - "templateTitle": "Employer name: {panel.name}" - }], - "name": "panel17", - "title": "You", - "visibleIf": "{member_type_of_employment} contains 'Other types of employment'" - }, { - "type": "panel", - "elements": [{ - "type": "paneldynamic", - "renderMode": "progressTop", - "allowAddPanel": false, - "allowRemovePanel": false, - "name": "partner_array_employer_info", - "title": "Employers", - "valueName": "partner_array_employer", - "panelCount": 1, - "templateElements": [{ - "type": "panel", - "name": "panel_partner_employer_address", - "title": "Contacts", - "elements": [{ - "type": "text", - "name": "partner_employer_address", - "valueName": "address", - "title": "Address:" - }, { - "type": "text", - "name": "partner_employer_phone", - "valueName": "phone", - "title": "Phone number:" - }, { - "type": "text", - "name": "partner_employer_abn", - "valueName": "abn", - "title": "ABN:" - }] - }, { - "type": "panel", - "name": "panel_partner_employer_role", - "title": "Are you a full time worker?", - "elements": [{ - "type": "radiogroup", - "choices": [ - "Full-time", - "Part-time", - "Casual", - "Seasonal" - ], - "name": "partner_employer_role", - "title": "Your role", - "valueName": "role" - }] - }, { - "type": "panel", - "name": "panel_partner_employer_hours_work", - "title": "How many hours do you work?", - "elements": [{ - "type": "text", - "inputType": "number", - "name": "partner_employer_hours_worked", - "valueName": "hours_worked", - "title": "Hours:" - }, { - "type": "dropdown", - "name": "partner_employer_hours_worked_frequency", - "valueName": "hours_worked_frequency", - "title": "Work frequency:", - "startWithNewLine": false, - "defaultValue": "Day", - "choices": [ - "Day", - "Week", - "Fortnight", - "Month", - "Year" - ] - }] - }, { - "type": "panel", - "name": "panel_partner_employer_income", - "title": "What is your income?", - "elements": [{ - "type": "text", - "inputType": "number", - "name": "partner_employer_income", - "valueName": "income", - "title": "Income:" - }, { - "type": "dropdown", - "name": "partner_employer_income_frequency", - "valueName": "income_frequency", - "title": "Income frequency:", - "startWithNewLine": false, - "defaultValue": "Month", - "choices": [ - "Day", - "Week", - "Fortnight", - "Month", - "Year" - ] - }] - }], - "templateTitle": "Employer name: {panel.name}" - }], - "name": "panel18", - "startWithNewLine": false, - "title": "You partner", - "visibleIf": "{partner_type_of_employment} contains 'Other types of employment'" - }], - "name": "panel16", - "title": "Enter information about your employers" - }], - "name": "page3.2", - "visibleIf": - "{member_type_of_employment} contains 'Other types of employment' or {partner_type_of_employment} contains 'Other types of employment'" - }, { - "elements": [{ - "type": "panel", - "elements": [{ - "type": "panel", - "elements": [{ - "type": "radiogroup", - "choices": [{ - "value": "1", - "text": "Yes" - }, { - "value": "0", - "text": "No" - }], - "colCount": 2, - "isRequired": true, - "name": "member_receive_fringe_benefits", - "title": " " - }, { - "type": "panel", - "elements": [{ - "type": "text", - "name": "member_fringe_benefits_type" - }, { - "type": "text", - "name": "member_fringe_benefits_value" - }, { - "type": "radiogroup", - "choices": ["Grossed up", "Not grossed up"], - "name": "member_fringe_benefits_grossing" - }], - "name": "panel11", - "visible": false, - "visibleIf": "{member_receive_fringe_benefits} = 1" - }], - "name": "panel2", - "title": "You", - "visible": false, - "visibleIf": "{member_type_of_employment} contains 'Other types of employment'" - }, { - "type": "panel", - "elements": [{ - "type": "radiogroup", - "choices": [{ - "value": "1", - "text": "Yes" - }, { - "value": "0", - "text": "No" - }], - "colCount": 2, - "isRequired": true, - "name": "partner_receive_fringe_benefits", - "title": " " - }, { - "type": "panel", - "elements": [{ - "type": "text", - "name": "partner_fringe_benefits_type" - }, { - "type": "text", - "name": "partner_fringe_benefits_value" - }, { - "type": "radiogroup", - "choices": ["Grossed up", "Not grossed up"], - "name": "partner_fringe_benefits_grossing" - }], - "name": "panel12", - "visible": false, - "visibleIf": "{partner_receive_fringe_benefits} = 1" - }], - "name": "panel1", - "startWithNewLine": false, - "title": "Your Partner", - "visible": false, - "visibleIf": "{partner_type_of_employment} contains 'Other types of employment'" - }], - "name": "panel9", - "title": "Do any of your employers provide you with fringe benefits?" - }], - "name": "page4", - "visible": false, - "visibleIf": - "{member_type_of_employment} contains 'Other types of employment' or {partner_type_of_employment} contains 'Other types of employment'" - }, { - "elements": [{ - "type": "panel", - "elements": [{ - "type": "panel", - "elements": [{ - "type": "radiogroup", - "choices": [{ - "value": "1", - "text": "Yes" - }, { - "value": "0", - "text": "No" - }], - "colCount": 2, - "isRequired": true, - "name": "member_seasonal_intermittent_or_contract_work", - "title": " " - }], - "name": "panel2", - "title": "You", - "visible": false, - "visibleIf": "{member_receives_income_from_employment} = 1" - }, { - "type": "panel", - "elements": [{ - "type": "radiogroup", - "choices": [{ - "value": "1", - "text": "Yes" - }, { - "value": "0", - "text": "No" - }], - "colCount": 2, - "isRequired": true, - "name": "partner_seasonal_intermittent_or_contract_work", - "title": " " - }], - "name": "panel1", - "startWithNewLine": false, - "title": "Your Partner", - "visible": false, - "visibleIf": "{partner_receives_income_from_employment} =1 " - }], - "name": "panel10", - "title": "In the last 6 months, have you done any seasonal, intermittent or contract work?" - }], - "name": "page5", - "visible": false, - "visibleIf": "{member_receives_income_from_employment} = 1 or {partner_receives_income_from_employment} =1 " - }], - "requiredText": "", - "showQuestionNumbers": "off", - "storeOthersAsComment": false - } -}]; - -const results = [{ - "id": "1", - "data": [ - { "Quality": { "affordable": "5", "better then others": "5", "does what it claims": "5", "easy to use": "5" }, "satisfaction": 5, "recommend friends": 5, "suggestions": "I am happy!", "price to competitors": "Not sure", "price": "low", "pricelimit": { "mostamount": "100", "leastamount": "100" } }, - { "Quality": { "affordable": "3", "does what it claims": "2", "better then others": "2", "easy to use": "3" }, "satisfaction": 3, "suggestions": "better support", "price to competitors": "Not sure", "price": "high", "pricelimit": { "mostamount": "60", "leastamount": "10" } } - ] -}, { - "id": "2", - "data": [ - { "member_array_employer": [{}], "partner_array_employer": [{}], "maritalstatus_c": "Married", "member_receives_income_from_employment": "0", "partner_receives_income_from_employment": "0" }, - { "member_array_employer": [{}], "partner_array_employer": [{}], "maritalstatus_c": "Single", "member_receives_income_from_employment": "1", "member_type_of_employment": ["Self-employed"], "member_seasonal_intermittent_or_contract_work": "0" } - ] -}]; - -module.exports = { - surveys: surveys, - results: results, - defaultName: "New Survey" -}; diff --git a/express-app/db-adapters/in-memory.js b/express-app/db-adapters/in-memory.js deleted file mode 100644 index d1ffad7..0000000 --- a/express-app/db-adapters/in-memory.js +++ /dev/null @@ -1,121 +0,0 @@ -var demoData = require("./demo-surveys"); - -var currentId = demoData.surveys.length + 1; - -function InMemoryStorage(session) { - function getTable(tableName) { - var table = session[tableName]; - if (!table) { - table = []; - session[tableName] = table; - } - return table; - } - - function getObjectsFromStorage(tableName, callback) { - var table = getTable(tableName); - callback(table); - } - - function findById(objects, id) { - return objects.filter(function (o) { return o.id === id; })[0]; - } - - function addSurvey(name, callback) { - var table = getTable("surveys"); - var newObj = { - id: '' + currentId++, - name: name || demoData.defaultName + " " + currentId, - json: "{}" - }; - table.push(newObj); - callback(newObj); - } - - function postResults(postId, json, callback) { - var table = getTable("results"); - var results = findById(table, postId); - if (!results) { - results = { - id: postId, - data: [] - } - table.push(results); - } - results.data.push(json); - callback({}); - } - - function getResults(postId, callback) { - var table = getTable("results"); - var results = findById(table, postId); - callback(results); - } - - function deleteSurvey(surveyId, callback) { - var table = getTable("surveys"); - var survey = findById(table, surveyId); - table.splice(table.indexOf(survey), 1); - callback(survey); - } - - function storeSurvey(id, name, json, callback) { - var table = getTable("surveys"); - var survey = findById(table, id); - if (!!survey) { - survey.json = json; - } else { - survey = { - id: id, - name: name || id, - json: json - }; - table.push(survey); - } - callback && callback(survey); - } - - function changeName(id, name, callback) { - var table = getTable("surveys"); - var survey = findById(table, id); - if (!!survey) { - survey.name = name; - } - callback && callback(survey); - } - - function getSurveys(callback) { - getObjectsFromStorage("surveys", function (objects) { - if (objects.length > 0) { - callback(objects); - } else { - var surveys = getTable("surveys"); - demoData.surveys.forEach(function (survey) { - surveys.push(JSON.parse(JSON.stringify(survey))); - }) - var results = getTable("results"); - demoData.results.forEach(function (result) { - results.push(JSON.parse(JSON.stringify(result))); - }) - getObjectsFromStorage("surveys", callback); - } - }); - } - - return { - addSurvey: addSurvey, - getSurvey: function (surveyId, callback) { - getSurveys(function (surveys) { - callback(findById(surveys, surveyId)); - }); - }, - storeSurvey: storeSurvey, - getSurveys: getSurveys, - deleteSurvey: deleteSurvey, - postResults: postResults, - getResults: getResults, - changeName: changeName - }; -} - -module.exports = InMemoryStorage; diff --git a/express-app/db-adapters/postgres.js b/express-app/db-adapters/postgres.js index 7a8447f..1cdc58a 100644 --- a/express-app/db-adapters/postgres.js +++ b/express-app/db-adapters/postgres.js @@ -1,5 +1,5 @@ const fs = require("fs"); -const SQLCRUDAdapter = require("./sql-crud-adapter"); +const SqlCrudAdapter = require("./sql-crud-adapter"); const SurveyStorage = require("./survey-storage"); const readFileSync = filename => fs.readFileSync(filename).toString("utf8"); @@ -17,7 +17,7 @@ const dbConfig = { const Pool = require('pg').Pool const pool = new Pool(dbConfig); -function PostgresStorage(session) { +function PostgresStorage () { function queryExecutorFunction() { if(!!process.env.DATABASE_LOG) { console.log(arguments[0]); @@ -25,7 +25,7 @@ function PostgresStorage(session) { } return pool.query.apply(pool, arguments); } - const dbQueryAdapter = new SQLCRUDAdapter(queryExecutorFunction); + const dbQueryAdapter = new SqlCrudAdapter(queryExecutorFunction); return new SurveyStorage(dbQueryAdapter); } diff --git a/express-app/db-adapters/sql-crud-adapter.js b/express-app/db-adapters/sql-crud-adapter.js index fad0ebf..757fdc6 100644 --- a/express-app/db-adapters/sql-crud-adapter.js +++ b/express-app/db-adapters/sql-crud-adapter.js @@ -1,8 +1,8 @@ -function SQLCRUDAdapter(queryExecutorFunction) { - function getObjects(tableName, filter, callback) { +function SqlCrudAdapter (queryExecutorFunction) { + function getObjects (tableName, filter, callback) { filter = filter || []; let where = ""; - if(filter.length > 0) { + if (filter.length > 0) { where += " WHERE " + filter.map(fi => "" + fi.name + fi.op + fi.value).join(" AND "); } @@ -15,7 +15,7 @@ function SQLCRUDAdapter(queryExecutorFunction) { }); } - function deleteObject(tableName, idValue, callback) { + function deleteObject (tableName, idValue, callback) { const command = "DELETE FROM " + tableName + " WHERE id='" + idValue + "'"; queryExecutorFunction(command, (error, results) => { if (error) { @@ -25,12 +25,12 @@ function SQLCRUDAdapter(queryExecutorFunction) { }); } - function createObject(tableName, object, callback) { + function createObject (tableName, object, callback) { const valueNames = []; const valueIndexes = []; const values = []; Object.keys(object).forEach((key, index) => { - if(object[key] !== undefined) { + if (object[key] !== undefined) { valueNames.push(key); valueIndexes.push("$" + (index + 1)); values.push(object[key]); @@ -41,22 +41,21 @@ function SQLCRUDAdapter(queryExecutorFunction) { if (error) { throw error; } - // console.log(JSON.stringify(results)); callback(results.rows[0].id); }); } - function updateObject(tableName, object, callback) { + function updateObject (tableName, object, callback) { const valueNames = []; const values = []; Object.keys(object).forEach((key, index) => { - if(object[key] !== undefined) { + if (object[key] !== undefined) { valueNames.push(key + " = $" + (index + 1)); values.push(object[key]); } }); const command = "UPDATE " + tableName + " SET " + valueNames.join(", ") + " WHERE id = '" + object.id + "'"; - queryExecutorFunction(command, values, (error, results) => { + queryExecutorFunction(command, values, (error) => { if (error) { throw error; } @@ -72,4 +71,4 @@ function SQLCRUDAdapter(queryExecutorFunction) { } } -module.exports = SQLCRUDAdapter; \ No newline at end of file +module.exports = SqlCrudAdapter; \ No newline at end of file diff --git a/express-app/db-adapters/survey-storage.js b/express-app/db-adapters/survey-storage.js index 4e19c07..546c6f0 100644 --- a/express-app/db-adapters/survey-storage.js +++ b/express-app/db-adapters/survey-storage.js @@ -1,52 +1,50 @@ let currentId = 1; -function SurveyStorage(dbQueryAdapter) { - - function addSurvey(name, callback) { - var newObj = { - name: name || ("New Survey" + " " + currentId++), - json: "{}" - }; - dbQueryAdapter.create("surveys", newObj, id => { - newObj.id = id; - callback(newObj); - }); - } - - function postResults(postId, json, callback) { - var newObj = { - postid: postId, - json: json - }; - console.log(JSON.stringify(newObj)); - dbQueryAdapter.create("results", newObj, id => { - newObj.id = id; - callback(newObj); - }); - } - - return { - addSurvey: addSurvey, - getSurvey: function (surveyId, callback) { - dbQueryAdapter.retrieve("surveys", [{ name: "id", op: "=", value: "'" + surveyId + "'" }], function (results) { callback(results[0]); }); - }, - storeSurvey: function (id, name, json, callback) { - dbQueryAdapter.update("surveys", { id: id, json: json }, function (results) { callback(results); }); - }, - getSurveys: function (callback) { - dbQueryAdapter.retrieve("surveys", [], function (results) { callback(results); }); - }, - deleteSurvey: function (surveyId, callback) { - dbQueryAdapter.delete("surveys", surveyId, function (results) { callback(results); }); - }, - postResults: postResults, - getResults: function (postId, callback) { - dbQueryAdapter.retrieve("results", [{ name: "postid", op: "=", value: "'" + postId + "'" }], function (results) { callback({ id: postId, data: results.map(r => r.json)}); }); - }, - changeName: function (id, name, callback) { - dbQueryAdapter.update("surveys", { id: id, name: name }, function (results) { callback(results); }); - } +function SurveyStorage (dbQueryAdapter) { + function addSurvey (name, callback) { + const newObj = { + name: name || ("New Survey" + " " + currentId++), + json: "{}" }; + dbQueryAdapter.create("surveys", newObj, id => { + newObj.id = id; + callback(newObj); + }); } - module.exports = SurveyStorage; \ No newline at end of file + function postResults (postId, json, callback) { + const newObj = { + postid: postId, + json: json + }; + dbQueryAdapter.create("results", newObj, id => { + newObj.id = id; + callback(newObj); + }); + } + + return { + addSurvey: addSurvey, + getSurvey: (surveyId, callback) => { + dbQueryAdapter.retrieve("surveys", [{ name: "id", op: "=", value: "'" + surveyId + "'" }], (results) => { callback(results[0]); }); + }, + storeSurvey: (id, _, json, callback) => { + dbQueryAdapter.update("surveys", { id: id, json: json }, (results) => { callback(results); }); + }, + getSurveys: (callback) => { + dbQueryAdapter.retrieve("surveys", [], (results) => { callback(results); }); + }, + deleteSurvey: (surveyId, callback) => { + dbQueryAdapter.delete("surveys", surveyId, (results) => { callback(results); }); + }, + postResults: postResults, + getResults: (postId, callback) => { + dbQueryAdapter.retrieve("results", [{ name: "postid", op: "=", value: "'" + postId + "'" }], (results) => { callback({ id: postId, data: results.map(r => r.json)}); }); + }, + changeName: (id, name, callback) => { + dbQueryAdapter.update("surveys", { id: id, name: name }, (results) => { callback(results); }); + } + }; +} + +module.exports = SurveyStorage; \ No newline at end of file diff --git a/express-app/index.js b/express-app/index.js index 1f7c182..9a73dbb 100644 --- a/express-app/index.js +++ b/express-app/index.js @@ -1,104 +1,104 @@ -var express = require("express"); -var bodyParser = require("body-parser"); -var session = require("express-session"); -var PostgresSurveyStorage = require("./db-adapters/postgres"); -var apiBaseAddress = "/api"; +const express = require("express"); +const bodyParser = require("body-parser"); +const session = require("express-session"); +const PostgresSurveyStorage = require("./db-adapters/postgres"); +const apiBaseAddress = "/api"; -var app = express(); +const app = express(); app.use( session({ secret: "mysecret", resave: true, saveUninitialized: true, - //cookie: { secure: true } + // cookie: { secure: true } }) ); app.use(bodyParser.json()); app.use(bodyParser.urlencoded({ extended: true })); -function getStorage(req) { - var storage = new PostgresSurveyStorage(req.session); +function getStorage (req) { + const storage = new PostgresSurveyStorage(req.session); return storage; } -function sendJsonResult(res, obj) { +function sendJsonResult (res, obj) { res.setHeader("Content-Type", "application/json"); res.send(JSON.stringify(obj)); } -app.get(apiBaseAddress + "/getActive", function (req, res) { - var storage = getStorage(req); - storage.getSurveys(function (result) { +app.get(apiBaseAddress + "/getActive", (req, res) => { + const storage = getStorage(req); + storage.getSurveys((result) => { sendJsonResult(res, result); }); }); -app.get(apiBaseAddress + "/getSurvey", function (req, res) { - var storage = getStorage(req); - var surveyId = req.query["surveyId"]; - storage.getSurvey(surveyId, function (result) { +app.get(apiBaseAddress + "/getSurvey", (req, res) => { + const storage = getStorage(req); + const surveyId = req.query["surveyId"]; + storage.getSurvey(surveyId, (result) => { sendJsonResult(res, result); }); }); -app.get(apiBaseAddress + "/changeName", function (req, res) { - var storage = getStorage(req); - var id = req.query["id"]; - var name = req.query["name"]; - storage.changeName(id, name, function (result) { +app.get(apiBaseAddress + "/changeName", (req, res) => { + const storage = getStorage(req); + const id = req.query["id"]; + const name = req.query["name"]; + storage.changeName(id, name, (result) => { sendJsonResult(res, result); }); }); -app.get(apiBaseAddress + "/create", function (req, res) { - var storage = getStorage(req); - var name = req.query["name"]; - storage.addSurvey(name, function (survey) { +app.get(apiBaseAddress + "/create", (req, res) => { + const storage = getStorage(req); + const name = req.query["name"]; + storage.addSurvey(name, (survey) => { sendJsonResult(res, survey); }); }); -app.post(apiBaseAddress + "/changeJson", function (req, res) { - var storage = getStorage(req); - var id = req.body.id; - var json = req.body.json; - storage.storeSurvey(id, null, json, function (survey) { +app.post(apiBaseAddress + "/changeJson", (req, res) => { + const storage = getStorage(req); + const id = req.body.id; + const json = req.body.json; + storage.storeSurvey(id, null, json, (survey) => { sendJsonResult(res, survey); }); }); -app.post(apiBaseAddress + "/post", function (req, res) { - var storage = getStorage(req); - var postId = req.body.postId; - var surveyResult = req.body.surveyResult; - storage.postResults(postId, surveyResult, function (result) { +app.post(apiBaseAddress + "/post", (req, res) => { + const storage = getStorage(req); + const postId = req.body.postId; + const surveyResult = req.body.surveyResult; + storage.postResults(postId, surveyResult, (result) => { sendJsonResult(res, result.json); }); }); -app.get(apiBaseAddress + "/delete", function (req, res) { - var storage = getStorage(req); - var id = req.query["id"]; - storage.deleteSurvey(id, function (result) { +app.get(apiBaseAddress + "/delete", (req, res) => { + const storage = getStorage(req); + const id = req.query["id"]; + storage.deleteSurvey(id, () => { sendJsonResult(res, { id: id }); }); }); -app.get(apiBaseAddress + "/results", function (req, res) { - var storage = getStorage(req); - var postId = req.query["postId"]; - storage.getResults(postId, function (result) { +app.get(apiBaseAddress + "/results", (req, res) => { + const storage = getStorage(req); + const postId = req.query["postId"]; + storage.getResults(postId, (result) => { sendJsonResult(res, result); }); }); -app.get(["/", "/about", "/run/*", "/edit/*", "/results/*"], function (req, res, next) { +app.get(["/", "/about", "/run/*", "/edit/*", "/results/*"], (_, res) => { res.sendFile("index.html", { root: __dirname + "/../public" }); }); app.use(express.static(__dirname + "/../public")); -var port = process.env.PORT || 3000; -app.listen(port, function () { +const port = process.env.PORT || 3000; +app.listen(port, () => { console.log("Listening on port: " + port + "..."); });