diff --git a/assets/greenstand.webp b/assets/greenstand.webp new file mode 100644 index 0000000..18486ad Binary files /dev/null and b/assets/greenstand.webp differ diff --git a/database/migrations/20230504224144-app-config.js b/database/migrations/20230504224144-app-config.js new file mode 100644 index 0000000..a2c24be --- /dev/null +++ b/database/migrations/20230504224144-app-config.js @@ -0,0 +1,53 @@ +'use strict'; + +var dbm; +var type; +var seed; +var fs = require('fs'); +var path = require('path'); +var Promise; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function(options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; + Promise = options.Promise; +}; + +exports.up = function(db) { + var filePath = path.join(__dirname, 'sqls', '20230504224144-app-config-up.sql'); + return new Promise( function( resolve, reject ) { + fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }) + .then(function(data) { + return db.runSql(data); + }); +}; + +exports.down = function(db) { + var filePath = path.join(__dirname, 'sqls', '20230504224144-app-config-down.sql'); + return new Promise( function( resolve, reject ) { + fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }) + .then(function(data) { + return db.runSql(data); + }); +}; + +exports._meta = { + "version": 1 +}; diff --git a/database/migrations/20230506070828-app-installation.js b/database/migrations/20230506070828-app-installation.js new file mode 100644 index 0000000..660d632 --- /dev/null +++ b/database/migrations/20230506070828-app-installation.js @@ -0,0 +1,53 @@ +'use strict'; + +var dbm; +var type; +var seed; +var fs = require('fs'); +var path = require('path'); +var Promise; + +/** + * We receive the dbmigrate dependency from dbmigrate initially. + * This enables us to not have to rely on NODE_PATH. + */ +exports.setup = function(options, seedLink) { + dbm = options.dbmigrate; + type = dbm.dataType; + seed = seedLink; + Promise = options.Promise; +}; + +exports.up = function(db) { + var filePath = path.join(__dirname, 'sqls', '20230506070828-app-installation-up.sql'); + return new Promise( function( resolve, reject ) { + fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }) + .then(function(data) { + return db.runSql(data); + }); +}; + +exports.down = function(db) { + var filePath = path.join(__dirname, 'sqls', '20230506070828-app-installation-down.sql'); + return new Promise( function( resolve, reject ) { + fs.readFile(filePath, {encoding: 'utf-8'}, function(err,data){ + if (err) return reject(err); + console.log('received data: ' + data); + + resolve(data); + }); + }) + .then(function(data) { + return db.runSql(data); + }); +}; + +exports._meta = { + "version": 1 +}; diff --git a/database/migrations/sqls/20230504224144-app-config-down.sql b/database/migrations/sqls/20230504224144-app-config-down.sql new file mode 100644 index 0000000..6fb7b04 --- /dev/null +++ b/database/migrations/sqls/20230504224144-app-config-down.sql @@ -0,0 +1 @@ +DROP TABLE app_config; \ No newline at end of file diff --git a/database/migrations/sqls/20230504224144-app-config-up.sql b/database/migrations/sqls/20230504224144-app-config-up.sql new file mode 100644 index 0000000..80aaa2e --- /dev/null +++ b/database/migrations/sqls/20230504224144-app-config-up.sql @@ -0,0 +1,10 @@ +CREATE TABLE app_config ( + id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), + config_code text NOT NULL UNIQUE, + stakeholder_id uuid NOT NULL REFERENCES stakeholder(id), + capture_flow, + capture_setup_flow, + active boolean DEFAULT true, + created_at timestamptz NOT NULL DEFAULT now(), + updated_at timestamptz NOT NULL DEFAULT now() +); \ No newline at end of file diff --git a/database/migrations/sqls/20230506070828-app-installation-down.sql b/database/migrations/sqls/20230506070828-app-installation-down.sql new file mode 100644 index 0000000..b4a779c --- /dev/null +++ b/database/migrations/sqls/20230506070828-app-installation-down.sql @@ -0,0 +1 @@ +DROP TABLE app_installation; \ No newline at end of file diff --git a/database/migrations/sqls/20230506070828-app-installation-up.sql b/database/migrations/sqls/20230506070828-app-installation-up.sql new file mode 100644 index 0000000..26ee1f9 --- /dev/null +++ b/database/migrations/sqls/20230506070828-app-installation-up.sql @@ -0,0 +1,10 @@ +CREATE TABLE app_installation( + id uuid NOT NULL PRIMARY KEY DEFAULT uuid_generate_v4(), + wallet varchar NOT NULL, + app_config_id uuid NOT NULL REFERENCES app_config(id), + created_at timestamptz NOT NULL DEFAULT now(), + latest_login_at timestamptz NOT NULL DEFAULT now() +); + +CREATE UNIQUE INDEX wallet_app_config + ON app_installation(wallet, app_config_id); \ No newline at end of file diff --git a/package-lock.json b/package-lock.json index d8076a5..33c8df2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -21,9 +21,11 @@ "express-async-handler": "^1.1.4", "express-validator": "^6.4.0", "joi": "^17.4.2", + "joi-to-swagger": "^6.2.0", "knex": "^0.95.13", "loglevel": "^1.6.8", "pg": "^8.7.1", + "swagger-ui-express": "^4.6.3", "uuid": "^8.2.0" }, "devDependencies": { @@ -48,7 +50,7 @@ "supertest": "^4.0.2" }, "engines": { - "node": "^16", + "node": ">=16", "npm": ">=6.0.0" } }, @@ -3698,6 +3700,20 @@ "@sideway/pinpoint": "^2.0.0" } }, + "node_modules/joi-to-swagger": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/joi-to-swagger/-/joi-to-swagger-6.2.0.tgz", + "integrity": "sha512-gwfIr1TsbbvZWozB/sFqiD7POFcXeaLKp6QJKGFkVgdom2ie/4f75QQAanZc/Wlbnyk66e6kTZXO28i6pN3oQA==", + "dependencies": { + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "joi": ">=17.1.1" + } + }, "node_modules/js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -6161,6 +6177,25 @@ "node": ">=8" } }, + "node_modules/swagger-ui-dist": { + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.18.3.tgz", + "integrity": "sha512-QW280Uvt234+TLo9NMPRa2Sj17RoorbQlR2eEY4R6Cs0LbdXhiO14YWX9OPBkBdiN64GQYz4zU8wlHLVi81lBg==" + }, + "node_modules/swagger-ui-express": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.3.tgz", + "integrity": "sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw==", + "dependencies": { + "swagger-ui-dist": ">=4.11.0" + }, + "engines": { + "node": ">= v0.10.32" + }, + "peerDependencies": { + "express": ">=4.0.0 || >=5.0.0-beta" + } + }, "node_modules/table": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/table/-/table-6.7.3.tgz", @@ -9729,6 +9764,14 @@ "@sideway/pinpoint": "^2.0.0" } }, + "joi-to-swagger": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/joi-to-swagger/-/joi-to-swagger-6.2.0.tgz", + "integrity": "sha512-gwfIr1TsbbvZWozB/sFqiD7POFcXeaLKp6QJKGFkVgdom2ie/4f75QQAanZc/Wlbnyk66e6kTZXO28i6pN3oQA==", + "requires": { + "lodash": "^4.17.21" + } + }, "js-tokens": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", @@ -11580,6 +11623,19 @@ "has-flag": "^4.0.0" } }, + "swagger-ui-dist": { + "version": "4.18.3", + "resolved": "https://registry.npmjs.org/swagger-ui-dist/-/swagger-ui-dist-4.18.3.tgz", + "integrity": "sha512-QW280Uvt234+TLo9NMPRa2Sj17RoorbQlR2eEY4R6Cs0LbdXhiO14YWX9OPBkBdiN64GQYz4zU8wlHLVi81lBg==" + }, + "swagger-ui-express": { + "version": "4.6.3", + "resolved": "https://registry.npmjs.org/swagger-ui-express/-/swagger-ui-express-4.6.3.tgz", + "integrity": "sha512-CDje4PndhTD2HkgyKH3pab+LKspDeB/NhPN2OF1j+piYIamQqBYwAXWESOT1Yju2xFg51bRW9sUng2WxDjzArw==", + "requires": { + "swagger-ui-dist": ">=4.11.0" + } + }, "table": { "version": "6.7.3", "resolved": "https://registry.npmjs.org/table/-/table-6.7.3.tgz", diff --git a/package.json b/package.json index f297248..2ac411e 100644 --- a/package.json +++ b/package.json @@ -46,9 +46,11 @@ "express-async-handler": "^1.1.4", "express-validator": "^6.4.0", "joi": "^17.4.2", + "joi-to-swagger": "^6.2.0", "knex": "^0.95.13", "loglevel": "^1.6.8", "pg": "^8.7.1", + "swagger-ui-express": "^4.6.3", "uuid": "^8.2.0" }, "devDependencies": { diff --git a/server/app.js b/server/app.js index 7d4cda6..2b3b071 100644 --- a/server/app.js +++ b/server/app.js @@ -1,8 +1,12 @@ const express = require('express'); const cors = require('cors'); const log = require('loglevel'); +const swaggerUi = require('swagger-ui-express'); +const { join } = require('path'); + const HttpError = require('./utils/HttpError'); const { handlerWrapper, errorHandler } = require('./utils/utils'); +const swaggerDocument = require('./handlers/swaggerDoc'); const router = require('./routes'); const app = express(); @@ -12,7 +16,6 @@ if (process.env.NODE_ENV === 'development') { app.use(cors()); } - /* * Check request */ @@ -34,6 +37,19 @@ app.use( }), ); +const options = { + customCss: ` + .topbar-wrapper img { + content:url('../assets/greenstand.webp'); + width:80px; + height:auto; + } + `, + explorer: true, +}; + +app.use('/assets', express.static(join(__dirname, '..', '/assets'))); +app.use('/docs', swaggerUi.serve, swaggerUi.setup(swaggerDocument, options)); app.use(express.urlencoded({ extended: false })); app.use(express.json()); diff --git a/server/handlers/candidateHander.js b/server/handlers/appConfigHandler.js similarity index 100% rename from server/handlers/candidateHander.js rename to server/handlers/appConfigHandler.js diff --git a/server/handlers/stakeholderHandler/docs.js b/server/handlers/stakeholderHandler/docs.js new file mode 100644 index 0000000..98a30c7 --- /dev/null +++ b/server/handlers/stakeholderHandler/docs.js @@ -0,0 +1,201 @@ +const j2s = require('joi-to-swagger'); +const { + stakeholderGetQuerySchema, + updateStakeholderSchema, + stakeholderDeleteSchema, + stakeholderPostSchema, +} = require('./schemas'); + +const { swagger: getSchema } = j2s(stakeholderGetQuerySchema); +const { swagger: patchSchema } = j2s(updateStakeholderSchema); +const { swagger: deleteSchema } = j2s(stakeholderDeleteSchema); +const { swagger: postSchema } = j2s(stakeholderPostSchema); + +const stakeholderResponses = { + content: { + 'application/json': { + schema: { + type: 'object', + properties: { + tags: { + type: 'array', + items: { + $ref: '#/components/schemas/Stakeholder', + }, + }, + }, + }, + }, + }, +}; + +const stakeholderSwagger = { + '/stakeholders': { + get: { + tags: ['stakeholders'], + summary: 'get all stakeholders', + parameters: [ + { + schema: { + ...getSchema, + }, + in: 'query', + name: 'query', + description: 'Allowed query parameters', + }, + ], + responses: { + 200: stakeholderResponses, + }, + }, + post: { + tags: ['stakeholders'], + summary: 'create a new stakeholder', + requestBody: { + content: { + 'application/json': { + schema: { ...postSchema }, + }, + }, + }, + responses: { + 201: stakeholderResponses, + }, + }, + patch: { + tags: ['stakeholders'], + summary: 'update a stakeholder', + requestBody: { + content: { + 'application/json': { + schema: { ...patchSchema }, + }, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Stakeholder', + }, + }, + }, + }, + }, + }, + delete: { + tags: ['stakeholders'], + summary: 'delete a stakeholder', + requestBody: { + content: { + 'application/json': { + schema: { ...deleteSchema }, + }, + }, + }, + responses: { + 200: stakeholderResponses, + }, + }, + }, + '/stakeholders/{id}': { + get: { + tags: ['stakeholders'], + summary: 'get all stakeholders', + parameters: [ + { + schema: { + ...getSchema, + }, + in: 'query', + name: 'query', + description: 'Allowed query parameters', + }, + ], + responses: { + 200: stakeholderResponses, + }, + }, + post: { + tags: ['stakeholders'], + summary: 'create a new stakeholder', + requestBody: { + content: { + 'application/json': { + schema: { ...postSchema }, + }, + }, + }, + responses: { + 201: stakeholderResponses, + }, + }, + patch: { + tags: ['stakeholders'], + summary: 'update a stakeholder', + requestBody: { + content: { + 'application/json': { + schema: { ...patchSchema }, + }, + }, + }, + responses: { + 200: { + content: { + 'application/json': { + schema: { + $ref: '#/components/schemas/Stakeholder', + }, + }, + }, + }, + }, + }, + delete: { + tags: ['stakeholders'], + summary: 'delete a stakeholder', + requestBody: { + content: { + 'application/json': { + schema: { ...deleteSchema }, + }, + }, + }, + responses: { + 200: stakeholderResponses, + }, + }, + }, +}; + +const stakeholder = { + id: { type: 'string', format: 'uuid' }, + type: { type: 'string' }, + org_name: { type: 'string' }, + first_name: { type: 'string' }, + last_name: { type: 'string' }, + email: { type: 'string', format: 'email' }, + phone: { type: 'string' }, + website: { type: 'string' }, + logo_url: { type: 'string' }, + map: { type: 'string' }, +}; + +const stakeholderComponent = { + type: 'object', + properties: { + ...stakeholder, + children: { + type: 'array', + items: { type: 'object', properties: { ...stakeholder } }, + }, + parents: { + type: 'array', + items: { type: 'object', properties: { ...stakeholder } }, + }, + }, +}; + +module.exports = { stakeholderSwagger, stakeholderComponent }; diff --git a/server/handlers/stakeholderHandler.js b/server/handlers/stakeholderHandler/index.js similarity index 64% rename from server/handlers/stakeholderHandler.js rename to server/handlers/stakeholderHandler/index.js index f0dd4c0..219d31f 100644 --- a/server/handlers/stakeholderHandler.js +++ b/server/handlers/stakeholderHandler/index.js @@ -1,67 +1,14 @@ -const Joi = require('joi'); -const StakeholderService = require('../services/StakeholderService'); +const StakeholderService = require('../../services/StakeholderService'); const { getFilterAndLimitOptions, generatePrevAndNext, -} = require('../utils/helper'); - -const stakeholderGetQuerySchema = Joi.object({ - id: Joi.string().uuid(), - type: Joi.string(), - logo_url: Joi.string(), - org_name: Joi.string(), - first_name: Joi.string(), - last_name: Joi.string(), - map: Joi.string(), - email: Joi.string(), - phone: Joi.string(), - website: Joi.string(), - search: Joi.string(), -}).unknown(false); - -const stakeholderPostSchema = Joi.object({ - email: Joi.string().email(), - first_name: Joi.string(), - last_name: Joi.string(), - org_name: Joi.string(), - logo_url: Joi.string().uri(), - map: Joi.string(), - phone: Joi.string(), - website: Joi.string().uri(), - image_url: Joi.string().allow('').uri(), - type: Joi.string().valid('Organization', 'Person'), - offering_pay_to_plant: Joi.boolean(), - relation: Joi.string().valid('parents', 'children'), - relation_id: Joi.string().uuid(), -}).unknown(false); - -const stakeholderDeleteSchema = Joi.object({ - id: Joi.string().uuid(), - type: Joi.string(), - linked: Joi.boolean(), - relation_id: Joi.string().uuid(), -}).unknown(false); - -const updateStakeholderSchema = Joi.object({ - id: Joi.string().uuid().required(), - email: Joi.string().email(), - org_name: Joi.string(), - first_name: Joi.string(), - last_name: Joi.string(), - logo_url: Joi.string(), - map: Joi.string(), - phone: Joi.string(), - website: Joi.string().uri(), - children: Joi.array().items(Joi.object()), - parents: Joi.array().items(Joi.object()), - image_url: Joi.string().allow(''), - type: Joi.string(), - created_at: Joi.string(), - updated_at: Joi.string(), -}) - .unknown(false) - .xor('org_name', 'first_name') - .xor('org_name', 'last_name'); +} = require('../../utils/helper'); +const { + stakeholderGetQuerySchema, + updateStakeholderSchema, + stakeholderDeleteSchema, + stakeholderPostSchema, +} = require('./schemas'); const stakeholderGetAll = async (req, res) => { await stakeholderGetQuerySchema.validateAsync(req.query, { diff --git a/server/handlers/stakeholderHandler/schemas.js b/server/handlers/stakeholderHandler/schemas.js new file mode 100644 index 0000000..fcb2ac6 --- /dev/null +++ b/server/handlers/stakeholderHandler/schemas.js @@ -0,0 +1,66 @@ +const Joi = require('joi'); + +const stakeholderGetQuerySchema = Joi.object({ + id: Joi.string().uuid(), + type: Joi.string(), + logo_url: Joi.string(), + org_name: Joi.string(), + first_name: Joi.string(), + last_name: Joi.string(), + map: Joi.string(), + email: Joi.string(), + phone: Joi.string(), + website: Joi.string(), + search: Joi.string(), +}).unknown(false); + +const stakeholderPostSchema = Joi.object({ + email: Joi.string().email(), + first_name: Joi.string(), + last_name: Joi.string(), + org_name: Joi.string(), + logo_url: Joi.string().uri(), + map: Joi.string(), + phone: Joi.string(), + website: Joi.string().uri(), + image_url: Joi.string().allow('').uri(), + type: Joi.string().valid('Organization', 'Person'), + offering_pay_to_plant: Joi.boolean(), + relation: Joi.string().valid('parents', 'children'), + relation_id: Joi.string().uuid(), +}).unknown(false); + +const stakeholderDeleteSchema = Joi.object({ + id: Joi.string().uuid(), + type: Joi.string(), + linked: Joi.boolean(), + relation_id: Joi.string().uuid(), +}).unknown(false); + +const updateStakeholderSchema = Joi.object({ + id: Joi.string().uuid().required(), + email: Joi.string().email(), + org_name: Joi.string(), + first_name: Joi.string(), + last_name: Joi.string(), + logo_url: Joi.string(), + map: Joi.string(), + phone: Joi.string(), + website: Joi.string().uri(), + children: Joi.array().items(Joi.object()), + parents: Joi.array().items(Joi.object()), + image_url: Joi.string().allow(''), + type: Joi.string(), + created_at: Joi.string(), + updated_at: Joi.string(), +}) + .unknown(false) + .xor('org_name', 'first_name') + .xor('org_name', 'last_name'); + +module.exports = { + stakeholderGetQuerySchema, + updateStakeholderSchema, + stakeholderDeleteSchema, + stakeholderPostSchema, +}; diff --git a/server/handlers/swaggerDoc.js b/server/handlers/swaggerDoc.js new file mode 100644 index 0000000..1292e83 --- /dev/null +++ b/server/handlers/swaggerDoc.js @@ -0,0 +1,26 @@ +const { + stakeholderSwagger, + stakeholderComponent, +} = require('./stakeholderHandler/docs'); + +const { version } = require('../../package.json'); + +const paths = { + ...stakeholderSwagger, +}; + +const swaggerDefinition = { + openapi: '3.0.0', + info: { + title: 'Treetracker API', + version, + }, + paths, + components: { + schemas: { + Stakeholder: { ...stakeholderComponent }, + }, + }, +}; + +module.exports = swaggerDefinition; diff --git a/server/models/Stakeholder.js b/server/models/Stakeholder.js index 11b592b..f01f78a 100644 --- a/server/models/Stakeholder.js +++ b/server/models/Stakeholder.js @@ -5,30 +5,6 @@ class Stakeholder { this._stakeholderRepository = new StakeholderRepository(session); } - static StakeholderPostObject({ - type, - org_name, - first_name, - last_name, - email, - phone, - website, - logo_url, - map, - }) { - return Object.freeze({ - type, - org_name, - first_name, - last_name, - email, - phone, - website, - logo_url, - map, - }); - } - static StakeholderTree({ id, type, @@ -202,10 +178,8 @@ class Stakeholder { } async createStakeholder(data) { - const stakeholderObj = this.constructor.StakeholderPostObject(data); - const stakeholder = await this._stakeholderRepository.createStakeholder( - stakeholderObj, + data, ); return this.stakeholderTree(stakeholder);