From 8cf8c76b47ef30f139c32a37d880ef37ea34510a Mon Sep 17 00:00:00 2001 From: Austen Stone Date: Tue, 22 Oct 2024 16:45:52 -0400 Subject: [PATCH] Add Jest configuration, survey model, and enhance backend API with survey routes --- backend/.env | 11 +- backend/__tests__/survey.test.ts | 48 ++++++ backend/jest.config.cjs | 8 + backend/package-lock.json | 142 ++++++++++++++++++ backend/package.json | 7 +- backend/src/app.ts | 5 +- backend/src/controllers/survery.controller.ts | 72 ++++++++- backend/src/controllers/webhook.controller.ts | 4 +- backend/src/database.ts | 25 ++- backend/src/models/survey.model.ts | 69 ++++++++- backend/src/routes/index.ts | 15 +- compose.yml | 6 +- frontend/src/app/app.config.ts | 8 +- .../src/app/copilot-survery.service.spec.ts | 16 ++ frontend/src/app/copilot-survery.service.ts | 34 +++++ .../copilot-survey.component.html | 8 +- .../copilot-survey.component.ts | 22 ++- frontend/src/app/models/survey.ts | 9 ++ package.json | 12 -- 19 files changed, 472 insertions(+), 49 deletions(-) create mode 100644 backend/__tests__/survey.test.ts create mode 100644 backend/jest.config.cjs create mode 100644 frontend/src/app/copilot-survery.service.spec.ts create mode 100644 frontend/src/app/copilot-survery.service.ts create mode 100644 frontend/src/app/models/survey.ts delete mode 100644 package.json diff --git a/backend/.env b/backend/.env index 5f18f3d..859059c 100644 --- a/backend/.env +++ b/backend/.env @@ -1,4 +1,13 @@ -GITHUB_WEBHOOK_PROXY_URL=https://smee.io/SxQC6L1O80NWJqdL +# Databse configuration +MYSQL_HOST=db +MYSQL_PORT=3306 +MYSQL_ROOT_PASSWORD=octocat +MYSQL_DATABASE=value + +# Base URL for the web server +WEB_URL=http://localhost + +# GitHub App configuration GITHUB_WEBHOOK_SECRET=bananas GITHUB_APP_ID=1028384 GITHUB_APP_CLIENT_ID=Iv23ctKxsNlsTbAt3NJY diff --git a/backend/__tests__/survey.test.ts b/backend/__tests__/survey.test.ts new file mode 100644 index 0000000..6e8ceea --- /dev/null +++ b/backend/__tests__/survey.test.ts @@ -0,0 +1,48 @@ +import 'dotenv/config' +import { Sequelize } from 'sequelize'; +import Survey from '../src/models/survey.model' +import sequelize from '../src/database'; + +beforeAll(async () => { + try { + await sequelize.authenticate(); + console.log('Connection has been established successfully.'); + } catch (error) { + console.error('Unable to connect to the database:', error); + } + await sequelize.sync({ force: true }); // Recreate the database schema +}); + +afterAll(async () => { + await sequelize.close(); +}); + +describe('Survey Model', () => { + it('should create a survey with valid data', async () => { + const survey = await Survey.create({ + daytime: new Date(), + userId: 1044, + usedCopilot: true, + pctTimesaved: 50, + timeUsedFor: 'Releases', + }); + + expect(survey).toBeDefined(); + expect(survey.userId).toBe(1044); + expect(survey.usedCopilot).toBe(true); + expect(survey.pctTimesaved).toBe(50); + expect(survey.timeUsedFor).toBe('Releases'); + }); + + it('should calculate timeSaved correctly', async () => { + const survey = await Survey.create({ + daytime: new Date(), + userId: 1044, + usedCopilot: true, + pctTimesaved: 50, + timeUsedFor: 'Releases', + }); + + expect(survey.timeSaved).toBe('50% saved for Releases'); + }); +}); \ No newline at end of file diff --git a/backend/jest.config.cjs b/backend/jest.config.cjs new file mode 100644 index 0000000..9864325 --- /dev/null +++ b/backend/jest.config.cjs @@ -0,0 +1,8 @@ +module.exports = { + preset: 'ts-jest', + testEnvironment: 'node', + transform: { + '^.+\\.tsx?$': 'ts-jest', + }, + moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], +}; \ No newline at end of file diff --git a/backend/package-lock.json b/backend/package-lock.json index 759438a..9e7321b 100644 --- a/backend/package-lock.json +++ b/backend/package-lock.json @@ -10,15 +10,18 @@ "license": "MIT", "dependencies": { "@octokit/webhooks": "^13.3.0", + "cors": "^2.8.5", "dotenv": "^16.4.5", "eventsource": "^2.0.2", "express": "^4.17.1", + "mysql2": "^3.11.3", "octokit": "^4.0.2", "sequelize": "^6.37.4", "smee-client": "^2.0.3", "sqlite3": "^5.0.2" }, "devDependencies": { + "@types/cors": "^2.8.17", "@types/eventsource": "^1.1.15", "@types/express": "^4.17.13", "@types/node": "^16.11.7", @@ -871,6 +874,15 @@ "@types/node": "*" } }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, "node_modules/@types/debug": { "version": "4.1.12", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", @@ -1124,6 +1136,14 @@ "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" }, + "node_modules/aws-ssl-profiles": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/aws-ssl-profiles/-/aws-ssl-profiles-1.1.2.tgz", + "integrity": "sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==", + "engines": { + "node": ">= 6.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -1414,6 +1434,18 @@ "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, "node_modules/create-require": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/create-require/-/create-require-1.1.1.tgz", @@ -1472,6 +1504,14 @@ "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", "optional": true }, + "node_modules/denque": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz", + "integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==", + "engines": { + "node": ">=0.10" + } + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -1827,6 +1867,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/generate-function": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz", + "integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==", + "dependencies": { + "is-property": "^1.0.2" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -2241,6 +2289,11 @@ "node": ">=0.12.0" } }, + "node_modules/is-property": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", + "integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==" + }, "node_modules/isexe": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", @@ -2258,6 +2311,11 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "node_modules/long": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz", + "integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q==" + }, "node_modules/lru-cache": { "version": "6.0.0", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz", @@ -2270,6 +2328,20 @@ "node": ">=10" } }, + "node_modules/lru.min": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/lru.min/-/lru.min-1.1.1.tgz", + "integrity": "sha512-FbAj6lXil6t8z4z3j0E5mfRlPzxkySotzUHwRXjlpRh10vc6AI6WN62ehZj82VG7M20rqogJ0GLwar2Xa05a8Q==", + "engines": { + "bun": ">=1.0.0", + "deno": ">=1.30.0", + "node": ">=8.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wellwelwel" + } + }, "node_modules/make-error": { "version": "1.3.6", "resolved": "https://registry.npmjs.org/make-error/-/make-error-1.3.6.tgz", @@ -2516,6 +2588,55 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" }, + "node_modules/mysql2": { + "version": "3.11.3", + "resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.11.3.tgz", + "integrity": "sha512-Qpu2ADfbKzyLdwC/5d4W7+5Yz7yBzCU05YWt5npWzACST37wJsB23wgOSo00qi043urkiRwXtEvJc9UnuLX/MQ==", + "dependencies": { + "aws-ssl-profiles": "^1.1.1", + "denque": "^2.1.0", + "generate-function": "^2.3.1", + "iconv-lite": "^0.6.3", + "long": "^5.2.1", + "lru.min": "^1.0.0", + "named-placeholders": "^1.1.3", + "seq-queue": "^0.0.5", + "sqlstring": "^2.3.2" + }, + "engines": { + "node": ">= 8.0" + } + }, + "node_modules/mysql2/node_modules/iconv-lite": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", + "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/named-placeholders": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz", + "integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==", + "dependencies": { + "lru-cache": "^7.14.1" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/named-placeholders/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "engines": { + "node": ">=12" + } + }, "node_modules/napi-build-utils": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-1.0.2.tgz", @@ -2675,6 +2796,14 @@ "node": "^12.13.0 || ^14.15.0 || >=16.0.0" } }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/object-inspect": { "version": "1.13.2", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.2.tgz", @@ -3033,6 +3162,11 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" }, + "node_modules/seq-queue": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz", + "integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==" + }, "node_modules/sequelize": { "version": "6.37.4", "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-6.37.4.tgz", @@ -3365,6 +3499,14 @@ } } }, + "node_modules/sqlstring": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz", + "integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==", + "engines": { + "node": ">= 0.6" + } + }, "node_modules/ssri": { "version": "8.0.1", "resolved": "https://registry.npmjs.org/ssri/-/ssri-8.0.1.tgz", diff --git a/backend/package.json b/backend/package.json index ef135c2..d260fb8 100644 --- a/backend/package.json +++ b/backend/package.json @@ -6,20 +6,25 @@ "main": "src/app.ts", "scripts": { "start": "tsx src/app.ts", + "test": "jest", "build": "tsc", - "dev": "nodemon src/app.ts" + "dev": "nodemon src/app.ts", + "proxy": "npx smee -t http://127.0.0.1:3000/api/github/webhooks" }, "dependencies": { "@octokit/webhooks": "^13.3.0", + "cors": "^2.8.5", "dotenv": "^16.4.5", "eventsource": "^2.0.2", "express": "^4.17.1", + "mysql2": "^3.11.3", "octokit": "^4.0.2", "sequelize": "^6.37.4", "smee-client": "^2.0.3", "sqlite3": "^5.0.2" }, "devDependencies": { + "@types/cors": "^2.8.17", "@types/eventsource": "^1.1.15", "@types/express": "^4.17.13", "@types/node": "^16.11.7", diff --git a/backend/src/app.ts b/backend/src/app.ts index c88d5ae..cc8f9cc 100644 --- a/backend/src/app.ts +++ b/backend/src/app.ts @@ -5,10 +5,13 @@ import apiRoutes from "./routes/index" import { createNodeMiddleware } from "octokit"; import { setupWebhookListeners } from './controllers/webhook.controller'; import octokit from './services/octokit'; +import cors from 'cors'; const app = express(); const PORT = Number(process.env.PORT) || 3000; +app.use(cors()); + // Setup webhook listeners setupWebhookListeners(octokit); app.use(createNodeMiddleware(octokit)); @@ -19,5 +22,5 @@ app.use(bodyParser.urlencoded({ extended: true })); app.use('/api', apiRoutes); app.listen(PORT, () => { - console.log(`Server is running on http://localhost:${PORT} ๐Ÿš€`); + console.log(`Server is running on port ${PORT}`); }); diff --git a/backend/src/controllers/survery.controller.ts b/backend/src/controllers/survery.controller.ts index a29ad7f..f82bae4 100644 --- a/backend/src/controllers/survery.controller.ts +++ b/backend/src/controllers/survery.controller.ts @@ -1,22 +1,78 @@ import { Request, Response } from 'express'; -import { Survey } from '../models/survey.model'; +import Survey from '../models/survey.model'; class SurveyController { - async createSurvey(req: Request, res: Response): Promise { + // Create a new survey ๐ŸŽจ + async createSurvey(req: Request, res: Response): Promise { + try { + const { daytime, userId, usedCopilot, pctTimesaved, timeUsedFor } = req.body; + const survey = await Survey.create({ daytime, userId, usedCopilot, pctTimesaved, timeUsedFor }); + res.status(201).json(survey); // ๐ŸŽ‰ Survey created! + } catch (error) { + res.status(500).json(error); // ๐Ÿšจ Error handling } + } - async getAllSurveys(req: Request, res: Response) { - return res.status(200).json({ message: 'All surveys' }); + // Get all surveys ๐Ÿ“‹ + async getAllSurveys(req: Request, res: Response): Promise { + try { + const surveys = await Survey.findAll(); + res.status(200).json(surveys); // ๐ŸŽ‰ All surveys retrieved! + } catch (error) { + res.status(500).json(error); // ๐Ÿšจ Error handling } + } - async getSurveyById(req: Request, res: Response): Promise { + // Get a survey by ID ๐Ÿ” + async getSurveyById(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const survey = await Survey.findByPk(id); + if (survey) { + res.status(200).json(survey); // ๐ŸŽ‰ Survey found! + } else { + res.status(404).json({ error: 'Survey not found' }); // ๐Ÿšจ Survey not found + } + } catch (error) { + res.status(500).json(error); // ๐Ÿšจ Error handling } + } - async updateSurvey(req: Request, res: Response): Promise { + // Update a survey โœ๏ธ + async updateSurvey(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const { daytime, userId, usedCopilot, pctTimesaved, timeUsedFor } = req.body; + const [updated] = await Survey.update({ daytime, userId, usedCopilot, pctTimesaved, timeUsedFor }, { + where: { id } + }); + if (updated) { + const updatedSurvey = await Survey.findByPk(id); + res.status(200).json(updatedSurvey); // ๐ŸŽ‰ Survey updated! + } else { + res.status(404).json({ error: 'Survey not found' }); // ๐Ÿšจ Survey not found + } + } catch (error) { + res.status(500).json(error); // ๐Ÿšจ Error handling } + } - async deleteSurvey(req: Request, res: Response): Promise { + // Delete a survey ๐Ÿ—‘๏ธ + async deleteSurvey(req: Request, res: Response): Promise { + try { + const { id } = req.params; + const deleted = await Survey.destroy({ + where: { id } + }); + if (deleted) { + res.status(204).send(); // ๐ŸŽ‰ Survey deleted! + } else { + res.status(404).json({ error: 'Survey not found' }); // ๐Ÿšจ Survey not found + } + } catch (error) { + res.status(500).json(error); // ๐Ÿšจ Error handling } + } } -export const surveyController = new SurveyController(); \ No newline at end of file +export default new SurveyController(); \ No newline at end of file diff --git a/backend/src/controllers/webhook.controller.ts b/backend/src/controllers/webhook.controller.ts index b325466..aa7cd5c 100644 --- a/backend/src/controllers/webhook.controller.ts +++ b/backend/src/controllers/webhook.controller.ts @@ -1,5 +1,6 @@ import { Webhooks } from '@octokit/webhooks'; import { App } from 'octokit'; +import { webUrl } from '../routes/index'; const webhooks = new Webhooks({ secret: process.env.GITHUB_WEBHOOK_SECRET || 'your-secret', @@ -11,14 +12,13 @@ webhooks.onAny(({ id, name, payload }) => { }); export const setupWebhookListeners = (octokit: App) => { - const baseURL = "http://localhost:3000"; octokit.webhooks.on("pull_request.opened", ({ octokit, payload }) => { console.log("Pull request opened", payload); return octokit.rest.issues.createComment({ owner: payload.repository.owner.login, repo: payload.repository.name, issue_number: payload.pull_request.number, - body: `Did you use copilot for this? Fill out this [survey](${baseURL}/survey)`, + body: `Hi @${payload.pull_request.user.login}! Please fill out this [survey](${webUrl}/copilot-survey) to help us understand if you leveraged Copilot in your pull request.` }); }); } diff --git a/backend/src/database.ts b/backend/src/database.ts index edec3c1..0042c5a 100644 --- a/backend/src/database.ts +++ b/backend/src/database.ts @@ -1,9 +1,28 @@ // backend/src/database.ts -import { Sequelize } from 'sequelize'; +import { DataTypes, Sequelize } from 'sequelize'; const sequelize = new Sequelize({ - host: 'localhost', - dialect: 'sqlite', + dialect: 'mysql', + database: process.env.MYSQL_DATABASE, + username: 'root', + password: process.env.MYSQL_ROOT_PASSWORD, + host: process.env.MYSQL_HOST, + port: parseInt(process.env.MYSQL_PORT || '3306'), }); +(async () => { + try { + console.log('Attempting to connect to the database...'); // ๐ŸŒ๐Ÿ” + await sequelize.authenticate(); + console.log('Connection has been established successfully.'); // ๐ŸŽ‰โœ… + } catch (error) { + console.error('Unable to connect to the database:', error); // ๐ŸšจโŒ + } + try { + await sequelize.sync(); // Recreate the database schema + } catch (error) { + console.error('Unable to sync the database:', error); + } +})(); + export default sequelize; \ No newline at end of file diff --git a/backend/src/models/survey.model.ts b/backend/src/models/survey.model.ts index bdba936..c33c244 100644 --- a/backend/src/models/survey.model.ts +++ b/backend/src/models/survey.model.ts @@ -1,7 +1,66 @@ +import { Model, DataTypes } from 'sequelize'; +import sequelize from '../database'; -export class Survey { - constructor(id: number) { - this.id = id; +class Survey extends Model { + public id!: number; + public daytime!: Date; + public userId!: number; + public usedCopilot!: boolean; + public pctTimesaved!: number; + public timeUsedFor!: string; + + // Virtual field for time saved + public get timeSaved(): string { + return `${this.pctTimesaved}% saved for ${this.timeUsedFor}`; + } + + public set timeSaved(value: string) { + const [pct, ...rest] = value.split(' '); + this.pctTimesaved = parseInt(pct.replace('%', ''), 10); + this.timeUsedFor = rest.join(' ').replace('saved for ', ''); } - id: number; -} \ No newline at end of file +} + +Survey.init({ + id: { + type: DataTypes.INTEGER, + autoIncrement: true, + primaryKey: true, + }, + daytime: { + type: DataTypes.DATE, + allowNull: false, + }, + userId: { + type: DataTypes.INTEGER, + allowNull: false, + }, + usedCopilot: { + type: DataTypes.BOOLEAN, + allowNull: false, + }, + pctTimesaved: { + type: DataTypes.INTEGER, + allowNull: false, + }, + timeUsedFor: { + type: DataTypes.STRING, + allowNull: false, + }, + timeSaved: { + type: DataTypes.VIRTUAL, + get() { + return `${this.getDataValue('pctTimesaved')}% saved for ${this.getDataValue('timeUsedFor')}`; + }, + set(value: string) { + const [pct, ...rest] = value.split(' '); + this.setDataValue('pctTimesaved', parseInt(pct.replace('%', ''), 10)); + this.setDataValue('timeUsedFor', rest.join(' ').replace('saved for ', '')); + }, + }, +}, { + sequelize, + modelName: 'Survey', +}); + +export default Survey; \ No newline at end of file diff --git a/backend/src/routes/index.ts b/backend/src/routes/index.ts index 3032941..64d8c1f 100644 --- a/backend/src/routes/index.ts +++ b/backend/src/routes/index.ts @@ -1,11 +1,16 @@ import { Router } from 'express'; -import { surveyController } from '../controllers/index'; +import SurveyController from '../controllers/survery.controller'; const router = Router(); -router.get('/', (req, res) => res.send('This is the API')); -// Define your routes here -router.get('/get-survey', surveyController.getAllSurveys); -router.post('/create-survey', surveyController.createSurvey); +router.get('/', (req, res) => res.send('๐ŸŽ‰ Welcome to the Survey API! ๐Ÿš€โœจ')); + +router.get('/get-survey', SurveyController.getAllSurveys); +router.post('/create-survey', SurveyController.createSurvey); +router.get('/get-survey/:id', SurveyController.getSurveyById); +router.put('/update-survey/:id', SurveyController.updateSurvey); +router.delete('/delete-survey/:id', SurveyController.deleteSurvey); + +export const webUrl = process.env.WEB_URL || 'http://localhost'; export default router; \ No newline at end of file diff --git a/compose.yml b/compose.yml index aa83c36..70225d1 100644 --- a/compose.yml +++ b/compose.yml @@ -9,8 +9,6 @@ services: - backend backend: - depends_on: - - db env_file: ./backend/.env build: context: ./backend @@ -24,6 +22,8 @@ services: DB_USER: root DB_PASSWORD: octocat DB_NAME: value + depends_on: + - db links: - db volumes: @@ -48,4 +48,4 @@ volumes: networks: default: - driver: bridge \ No newline at end of file + driver: bridge diff --git a/frontend/src/app/app.config.ts b/frontend/src/app/app.config.ts index 96116fd..077552c 100644 --- a/frontend/src/app/app.config.ts +++ b/frontend/src/app/app.config.ts @@ -3,7 +3,13 @@ import { provideRouter } from '@angular/router'; import { routes } from './app.routes'; import { provideAnimationsAsync } from '@angular/platform-browser/animations/async'; +import { provideHttpClient } from '@angular/common/http'; export const appConfig: ApplicationConfig = { - providers: [provideZoneChangeDetection({ eventCoalescing: true }), provideRouter(routes), provideAnimationsAsync()] + providers: [ + provideZoneChangeDetection({ eventCoalescing: true }), + provideRouter(routes), + provideAnimationsAsync(), + provideHttpClient(), + ] }; diff --git a/frontend/src/app/copilot-survery.service.spec.ts b/frontend/src/app/copilot-survery.service.spec.ts new file mode 100644 index 0000000..2747113 --- /dev/null +++ b/frontend/src/app/copilot-survery.service.spec.ts @@ -0,0 +1,16 @@ +import { TestBed } from '@angular/core/testing'; + +import { CopilotSurveryService } from './copilot-survery.service'; + +describe('CopilotSurveryService', () => { + let service: CopilotSurveryService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(CopilotSurveryService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/frontend/src/app/copilot-survery.service.ts b/frontend/src/app/copilot-survery.service.ts new file mode 100644 index 0000000..d0c9e82 --- /dev/null +++ b/frontend/src/app/copilot-survery.service.ts @@ -0,0 +1,34 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Observable } from 'rxjs'; +import { Survey } from './models/survey'; + +@Injectable({ + providedIn: 'root' +}) +export class CopilotSurveryService { + private apiUrl = `http://${window.location.hostname}:3000/api`; // Adjust the URL based on your backend setup ๐ŸŒ + + constructor(private http: HttpClient) { } + + createSurvey(survey: Survey) { + return this.http.post(`${this.apiUrl}/create-survey`, survey); + } + + getAllSurveys(): Observable { + return this.http.get(`${this.apiUrl}/get-survey`); + } + + getSurveyById(id: number): Observable { + return this.http.get(`${this.apiUrl}/get-survey/${id}`); + } + + updateSurvey(id: number, survey: any): Observable { + return this.http.put(`${this.apiUrl}/update-survey/${id}`, survey); + } + + deleteSurvey(id: number): Observable { + return this.http.delete(`${this.apiUrl}/delete-survey/${id}`); + } + +} diff --git a/frontend/src/app/copilot-survey/copilot-survey.component.html b/frontend/src/app/copilot-survey/copilot-survey.component.html index 0257db3..153b5d4 100644 --- a/frontend/src/app/copilot-survey/copilot-survey.component.html +++ b/frontend/src/app/copilot-survey/copilot-survey.component.html @@ -1,24 +1,24 @@

Copilot Developer Survey

- + Yes No

- +

I chose {{surveyForm.get('timeSavingPercentage')?.value}}% because Copilot enabled me to... - + - + Faster PR's ๐Ÿš€ Faster Releases ๐Ÿ“ฆ Repo/Team Housekeeping ๐Ÿงน diff --git a/frontend/src/app/copilot-survey/copilot-survey.component.ts b/frontend/src/app/copilot-survey/copilot-survey.component.ts index 761ef99..1828a0f 100644 --- a/frontend/src/app/copilot-survey/copilot-survey.component.ts +++ b/frontend/src/app/copilot-survey/copilot-survey.component.ts @@ -1,12 +1,14 @@ import { Component, forwardRef } from '@angular/core'; import { AppModule } from '../app.module'; import { FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; +import { CopilotSurveryService } from '../copilot-survery.service'; +import { provideHttpClient } from '@angular/common/http'; @Component({ selector: 'app-copilot-survey', standalone: true, imports: [ - AppModule + AppModule, ], providers: [ { @@ -21,13 +23,27 @@ import { FormBuilder, FormGroup, NG_VALUE_ACCESSOR } from '@angular/forms'; export class CopilotSurveyComponent { surveyForm: FormGroup; - constructor(private fb: FormBuilder) { + constructor( + private fb: FormBuilder, + private copilotSurveyService: CopilotSurveryService + ) { this.surveyForm = this.fb.group({ - timeSavingPercentage: [30] + usedCopilot: [true], + pctTimesaved: [30], + timeUsedFor: ['writing code'], + timeSaved: ['30 minutes'], }); } onSubmit() { console.log(this.surveyForm.value); + this.copilotSurveyService.createSurvey({ + id: 0, + daytime: new Date(), + userId: 1, + ...this.surveyForm.value + }).subscribe((res) => { + console.log('Survey created successfully ๐ŸŽ‰'); + }); } } diff --git a/frontend/src/app/models/survey.ts b/frontend/src/app/models/survey.ts new file mode 100644 index 0000000..7c07c32 --- /dev/null +++ b/frontend/src/app/models/survey.ts @@ -0,0 +1,9 @@ +export interface Survey { + id: number; + daytime: Date; + userId: number; + usedCopilot: boolean; + pctTimesaved: number; + timeUsedFor: string; + timeSaved: string; +} \ No newline at end of file diff --git a/package.json b/package.json deleted file mode 100644 index 4c1a9dd..0000000 --- a/package.json +++ /dev/null @@ -1,12 +0,0 @@ -{ - "name": "github-value", - "version": "1.0.0", - "description": "Welcome to My Fullstack App! ๐ŸŽ‰ This project is a full-stack application built with Angular for the frontend and Node.js for the backend, all written in TypeScript. It uses SQLite as the database and is containerized using Docker Compose. ๐Ÿณ", - "main": "index.js", - "scripts": { - "dev-frontend": "cd ./frontend && npm run start", - "dev-backend": "cd ./backend && npm run start" - }, - "author": "", - "license": "ISC" -}