diff --git a/.travis.yml b/.travis.yml index e62eae864..6849a4c01 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: node_js node_js: - "stable" +services: +- docker +before_install: +- docker run --name todo_neo4j -p7474:7474 -p7687:7687 -d --env NEO4J_AUTH=neo4j/password neo4j:latest jobs: include: - stage: "Backend" @@ -16,3 +20,4 @@ jobs: script: - npm test - npm run lint + diff --git a/ToDoApp/Backend/README.md b/ToDoApp/Backend/README.md new file mode 100644 index 000000000..16cdaac03 --- /dev/null +++ b/ToDoApp/Backend/README.md @@ -0,0 +1,34 @@ +# Backend + + +1. Update packages +- npm install +2. Download [Neo4j Desktop](https://neo4j.com/download/neo4j-desktop/?edition=desktop&flavour=unix&release=1.2.3&offline=true) +3. Create database (*TodoApp*) with password (*password*) +4. Start database, view Neo4j web interface on: + + - bolt://localhost:7687 + - http://localhost:7474 + - https://localhost:7473 + +5. Start backend + + \\Start with seed + node index.js --seed + + \\Start without seed + node index.js + +6. Write queries from GraphQL web interface (http://localhost:4000/) + +## Tips + +##### View whole graph in Neo4j web interface + MATCH (n) + RETURN n + +##### Clear database from everything + start r=relationship(*) delete r; + + MATCH (n) + DETACH DELETE n \ No newline at end of file diff --git a/ToDoApp/Backend/index.js b/ToDoApp/Backend/index.js index ad16ba475..acd84b169 100644 --- a/ToDoApp/Backend/index.js +++ b/ToDoApp/Backend/index.js @@ -1,17 +1,40 @@ -const { ApolloServer } = require('apollo-server'); +const { ApolloServer, makeExecutableSchema } = require('apollo-server'); const { mergeResolvers } = require("merge-graphql-schemas"); +const neo4j = require('neo4j-driver'); +const { augmentSchema } = require("neo4j-graphql-js"); +const { seedDatabase } = require("./src/db/seed"); +const { getAuth } = require("./src/utils/auth"); +const { applyMiddleware } = require ('graphql-middleware'); +const { permissions } = require ('./src/config/permissions'); + +const driver = neo4j.driver( + 'bolt://localhost', + neo4j.auth.basic('neo4j', 'password'), + { disableLosslessIntegers: true } +) // The ApolloServer constructor requires two parameters: your schema // definition and your set of resolvers. const { typeDefs } = require('./src/schema/typeDefs'); - const { userResolver } = require("./src/resolvers/user/userResolver"); const { todoResolver } = require("./src/resolvers/todo/todoResolver"); -const resolvers = mergeResolvers([userResolver, todoResolver]); +const { eventResolver } = require("./src/resolvers/event/eventResolver"); +const resolvers = mergeResolvers([userResolver, todoResolver, eventResolver]); +const schema = makeExecutableSchema({ typeDefs, resolvers }); +const augmentedSchema = augmentSchema(applyMiddleware(schema, permissions)); + +const server = new ApolloServer({ schema: augmentedSchema, context:({req}) => { + return({ + driver, + auth: getAuth(req), + })} +}); -const server = new ApolloServer({ typeDefs, resolvers}); +if (process.argv.length === 3 && process.argv[2] === "--seed") { + seedDatabase(driver) +} // The `listen` method launches a web server. server.listen().then(({ url }) => { diff --git a/ToDoApp/Backend/jest.config.js b/ToDoApp/Backend/jest.config.js new file mode 100644 index 000000000..fa2e545ab --- /dev/null +++ b/ToDoApp/Backend/jest.config.js @@ -0,0 +1,18 @@ +module.exports = { + verbose: true, + moduleFileExtensions: [ + "js", + "json" + ], + transform: { + "^.+\\.js$": "/node_modules/babel-jest" + }, + collectCoverage: true, + collectCoverageFrom: [ + "src/components/*.{js}", + "!**/node_modules/**" + ], + coverageReporters: [ + "text-summary" + ], +} diff --git a/ToDoApp/Backend/package-lock.json b/ToDoApp/Backend/package-lock.json index e2e7d4eac..ba9c066eb 100644 --- a/ToDoApp/Backend/package-lock.json +++ b/ToDoApp/Backend/package-lock.json @@ -236,6 +236,42 @@ "@babel/helper-plugin-utils": "^7.0.0" } }, + "@babel/runtime": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.6.tgz", + "integrity": "sha512-BWAJxpNVa0QlE5gZdWjSxXtemZyZ9RmrmVozxt3NUXeZhVIJ5ANyqmMc0JDrivBZyxUuQvFxlvH4OWWOogGfUw==", + "requires": { + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + } + } + }, + "@babel/runtime-corejs2": { + "version": "7.7.6", + "resolved": "https://registry.npmjs.org/@babel/runtime-corejs2/-/runtime-corejs2-7.7.6.tgz", + "integrity": "sha512-QYp/8xdH8iMin3pH5gtT/rUuttVfIcOhWBC3wh9Eh/qs4jEe39+3DpCDLgWXhMQgiCTOH8mrLSvQ0OHOCcox9g==", + "requires": { + "core-js": "^2.6.5", + "regenerator-runtime": "^0.13.2" + }, + "dependencies": { + "core-js": { + "version": "2.6.11", + "resolved": "https://registry.npmjs.org/core-js/-/core-js-2.6.11.tgz", + "integrity": "sha512-5wjnpaT/3dV+XB4borEsnAYQchn00XSgTAWKDkEqv+K8KevjbzmofK6hfJ9TZIlpj2N0xQpazy7PiRQiWHqzWg==" + }, + "regenerator-runtime": { + "version": "0.13.3", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", + "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==" + } + } + }, "@babel/template": { "version": "7.7.4", "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.7.4.tgz", @@ -1031,6 +1067,11 @@ "integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==", "dev": true }, + "@types/yup": { + "version": "0.26.27", + "resolved": "https://registry.npmjs.org/@types/yup/-/yup-0.26.27.tgz", + "integrity": "sha512-Rlsq3DExOHfWur75nQUAa5I0fXA2vSrw0u0qK3SI4PAkyOWjNzZsTaK+U9/sofWm3ttwWYn+C92pSq0s4rob4w==" + }, "@wry/equality": { "version": "0.1.9", "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.9.tgz", @@ -1218,6 +1259,15 @@ "sha.js": "^2.4.11" } }, + "apollo-errors": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/apollo-errors/-/apollo-errors-1.9.0.tgz", + "integrity": "sha512-XVukHd0KLvgY6tNjsPS3/Re3U6RQlTKrTbIpqqeTMo2N34uQMr+H1UheV21o8hOZBAFosvBORVricJiP5vfmrw==", + "requires": { + "assert": "^1.4.1", + "extendable-error": "^0.1.5" + } + }, "apollo-graphql": { "version": "0.3.6", "resolved": "https://registry.npmjs.org/apollo-graphql/-/apollo-graphql-0.3.6.tgz", @@ -1301,9 +1351,9 @@ "integrity": "sha512-Y0PKQvkrb2Kd18d1NPlHdSqmlr8TgqJ7JQcNIfhNDgdb45CnqZlxL1abuIRhr8tiw8OhVOcFxz2KyglBi8TKdA==" }, "apollo-server-express": { - "version": "2.9.13", - "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.9.13.tgz", - "integrity": "sha512-M306e07dpZ8YpZx4VBYa0FWlt+wopj4Bwn0Iy1iJ6VjaRyGx2HCUJvLpHZ+D0TIXtQ2nX3DTYeOouVaDDwJeqQ==", + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/apollo-server-express/-/apollo-server-express-2.9.15.tgz", + "integrity": "sha512-RrPFAW6QqxAGAlubdvxjluGc7SOr70H69ElLxDgXy3HREXN25Y4XZoCE+L3PoURwFy2mNtITZeDO7JKW1cbHNg==", "requires": { "@apollographql/graphql-playground-html": "1.6.24", "@types/accepts": "^1.3.5", @@ -1311,8 +1361,8 @@ "@types/cors": "^2.8.4", "@types/express": "4.17.1", "accepts": "^1.3.5", - "apollo-server-core": "^2.9.13", - "apollo-server-types": "^0.2.8", + "apollo-server-core": "^2.9.15", + "apollo-server-types": "^0.2.10", "body-parser": "^1.18.3", "cors": "^2.8.4", "express": "^4.17.1", @@ -1332,6 +1382,112 @@ "@types/express-serve-static-core": "*", "@types/serve-static": "*" } + }, + "apollo-cache-control": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/apollo-cache-control/-/apollo-cache-control-0.8.10.tgz", + "integrity": "sha512-1/CAEidIoY1PWw2mhwZChchK4fvLdHfdoB7AEikWMYFSbjYY6ZiayT+Q3z48wsiS/LTNugF/YJDJHQi4+qjuug==", + "requires": { + "apollo-server-env": "^2.4.3", + "graphql-extensions": "^0.10.9" + } + }, + "apollo-datasource": { + "version": "0.6.4", + "resolved": "https://registry.npmjs.org/apollo-datasource/-/apollo-datasource-0.6.4.tgz", + "integrity": "sha512-u4eu6Q94q6KuZacZfdo4vCevA81F4QWeTYEXUvoksQMJpiacPHHe0DJrofKVKvxngUp5kCi1RnPXSc6kBY+/oA==", + "requires": { + "apollo-server-caching": "^0.5.1", + "apollo-server-env": "^2.4.3" + } + }, + "apollo-engine-reporting": { + "version": "1.4.13", + "resolved": "https://registry.npmjs.org/apollo-engine-reporting/-/apollo-engine-reporting-1.4.13.tgz", + "integrity": "sha512-p5fVmQigNGXoxCHnu8Bb6itNfwtjGnoyLf9i4oP0vfdSnTjaNI8KM7zXv16t1YnE/wsqYuieCnleoPSBReTy9Q==", + "requires": { + "apollo-engine-reporting-protobuf": "^0.4.4", + "apollo-graphql": "^0.3.4", + "apollo-server-caching": "^0.5.1", + "apollo-server-env": "^2.4.3", + "apollo-server-errors": "^2.3.4", + "apollo-server-types": "^0.2.10", + "async-retry": "^1.2.1", + "graphql-extensions": "^0.10.9" + } + }, + "apollo-server-caching": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/apollo-server-caching/-/apollo-server-caching-0.5.1.tgz", + "integrity": "sha512-L7LHZ3k9Ao5OSf2WStvQhxdsNVplRQi7kCAPfqf9Z3GBEnQ2uaL0EgO0hSmtVHfXTbk5CTRziMT1Pe87bXrFIw==", + "requires": { + "lru-cache": "^5.0.0" + } + }, + "apollo-server-core": { + "version": "2.9.15", + "resolved": "https://registry.npmjs.org/apollo-server-core/-/apollo-server-core-2.9.15.tgz", + "integrity": "sha512-MgqtxZkUO2u7cSDQjp8feQlyHT/iZlKv3TV5kNy+xa9ewbfiR/qjziMsz46x+oVPBah+VH9WbGShSbVO0b2TJA==", + "requires": { + "@apollographql/apollo-tools": "^0.4.0", + "@apollographql/graphql-playground-html": "1.6.24", + "@types/graphql-upload": "^8.0.0", + "@types/ws": "^6.0.0", + "apollo-cache-control": "^0.8.10", + "apollo-datasource": "^0.6.4", + "apollo-engine-reporting": "^1.4.13", + "apollo-server-caching": "^0.5.1", + "apollo-server-env": "^2.4.3", + "apollo-server-errors": "^2.3.4", + "apollo-server-plugin-base": "^0.6.10", + "apollo-server-types": "^0.2.10", + "apollo-tracing": "^0.8.10", + "fast-json-stable-stringify": "^2.0.0", + "graphql-extensions": "^0.10.9", + "graphql-tag": "^2.9.2", + "graphql-tools": "^4.0.0", + "graphql-upload": "^8.0.2", + "sha.js": "^2.4.11", + "subscriptions-transport-ws": "^0.9.11", + "ws": "^6.0.0" + } + }, + "apollo-server-plugin-base": { + "version": "0.6.10", + "resolved": "https://registry.npmjs.org/apollo-server-plugin-base/-/apollo-server-plugin-base-0.6.10.tgz", + "integrity": "sha512-/xT7UT/tbCDIoTQ4lcEQsJ0ACh7h7QG0BDmeSlDXjwDuENRI50bQ2QoluCMPitZXGe+FCQfLhvzFgzbsZGT0IA==", + "requires": { + "apollo-server-types": "^0.2.10" + } + }, + "apollo-server-types": { + "version": "0.2.10", + "resolved": "https://registry.npmjs.org/apollo-server-types/-/apollo-server-types-0.2.10.tgz", + "integrity": "sha512-ke9ViPEWfW+2XLe66CaKGVZdS7duSLbamSKSprmmeMBd8s6tmjf0FumUVxV7X4quxPZi0OPo8x0LoLU7GWsmaA==", + "requires": { + "apollo-engine-reporting-protobuf": "^0.4.4", + "apollo-server-caching": "^0.5.1", + "apollo-server-env": "^2.4.3" + } + }, + "apollo-tracing": { + "version": "0.8.10", + "resolved": "https://registry.npmjs.org/apollo-tracing/-/apollo-tracing-0.8.10.tgz", + "integrity": "sha512-EjToHqHdDrjj0xBRnbie57lz3U81Onrs55YqHhvzaMv8gDAeKX3rnXiw5EfYZNxR7uutoyXH0iNKaYPyolWbZA==", + "requires": { + "apollo-server-env": "^2.4.3", + "graphql-extensions": "^0.10.9" + } + }, + "graphql-extensions": { + "version": "0.10.9", + "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.10.9.tgz", + "integrity": "sha512-NJdV8aGVGCrcm1Pbq8IbmV5FfLCAEPxgplh9EJU7qAP+Z4PenkuH6V6RAPRqIwwZ287m8/aDXKW+X0qFe8gyUQ==", + "requires": { + "@apollographql/apollo-tools": "^0.4.0", + "apollo-server-env": "^2.4.3", + "apollo-server-types": "^0.2.10" + } } } }, @@ -3978,6 +4134,11 @@ } } }, + "extendable-error": { + "version": "0.1.5", + "resolved": "https://registry.npmjs.org/extendable-error/-/extendable-error-0.1.5.tgz", + "integrity": "sha1-EiMIpwl7yJomOyxPvwiceBQOO20=" + }, "extglob": { "version": "2.0.4", "resolved": "https://registry.npmjs.org/extglob/-/extglob-2.0.4.tgz", @@ -4163,6 +4324,11 @@ "locate-path": "^2.0.0" } }, + "fn-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/fn-name/-/fn-name-2.0.1.tgz", + "integrity": "sha1-UhTXU3pNBqSjAcDMJi/rhBiAAuc=" + }, "follow-redirects": { "version": "1.9.0", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.9.0.tgz", @@ -4804,6 +4970,16 @@ "iterall": "^1.2.2" } }, + "graphql-auth-directives": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/graphql-auth-directives/-/graphql-auth-directives-2.1.0.tgz", + "integrity": "sha512-mRVsjeMeMABPyjxyzl9mhkcW02YBwSj7dnu7C6wy2dIhiby6xTKy6Q54C8KeqXSYsy6ua4VmBH++d7GKqpvIoA==", + "requires": { + "apollo-errors": "^1.9.0", + "graphql-tools": "^4.0.4", + "jsonwebtoken": "^8.3.0" + } + }, "graphql-extensions": { "version": "0.10.7", "resolved": "https://registry.npmjs.org/graphql-extensions/-/graphql-extensions-0.10.7.tgz", @@ -4814,6 +4990,24 @@ "apollo-server-types": "^0.2.8" } }, + "graphql-middleware": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/graphql-middleware/-/graphql-middleware-4.0.2.tgz", + "integrity": "sha512-ESVDvMXeN00S1BNsjNS18uExcR16J8zbT31CuKcpyeBa7IMbidG0Pnqnu5P1wKkJLmPmKOfCljWlhXpD/Fawqg==", + "requires": { + "graphql-tools": "^4.0.5" + } + }, + "graphql-shield": { + "version": "7.0.7", + "resolved": "https://registry.npmjs.org/graphql-shield/-/graphql-shield-7.0.7.tgz", + "integrity": "sha512-T7Ds9ailm9dFQ/u7E4pmyE/nu6I7RbG4L2Bice6zqz3ajuV4AvMGB57mCg9xB5RFZ3wpVHd+s9aFLPB0FpWtjg==", + "requires": { + "@types/yup": "0.26.27", + "object-hash": "^2.0.0", + "yup": "^0.28.0" + } + }, "graphql-subscriptions": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/graphql-subscriptions/-/graphql-subscriptions-1.1.0.tgz", @@ -7627,6 +7821,56 @@ "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==" }, + "neo4j-driver": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/neo4j-driver/-/neo4j-driver-4.0.0.tgz", + "integrity": "sha512-hUYZm1bdsE16c+wCCXSDETxV1u6yUkba0JVfIv/knmvBOJTg/3IDR5DFUAHuq9C6arJM+0Pyr1/9UcR1SEdXKA==", + "requires": { + "@babel/runtime": "^7.5.5", + "rxjs": "^6.5.2", + "text-encoding-utf-8": "^1.0.2", + "uri-js": "^4.2.2" + } + }, + "neo4j-graphql-js": { + "version": "2.10.2", + "resolved": "https://registry.npmjs.org/neo4j-graphql-js/-/neo4j-graphql-js-2.10.2.tgz", + "integrity": "sha512-CgtKEgrWgSJBjuKQ5CEPt4tcG1z14oAB3UWQjX8scDlUag0iWofgzpPlrc3brn+RitfeEc3FuMSru8E9dVDJPg==", + "requires": { + "@babel/runtime": "^7.5.5", + "@babel/runtime-corejs2": "^7.5.5", + "debug": "^4.1.1", + "graphql": "^14.2.1", + "graphql-auth-directives": "^2.1.0", + "lodash": "^4.17.15", + "neo4j-driver": "^1.7.3" + }, + "dependencies": { + "debug": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.1.1.tgz", + "integrity": "sha512-pYAIzeRo8J6KPEaJ0VWOh5Pzkbw/RetuzehGM7QRRX5he4fPHx2rdKMB256ehJCkX+XRQm16eZLqLNS8RSZXZw==", + "requires": { + "ms": "^2.1.1" + } + }, + "ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "neo4j-driver": { + "version": "1.7.6", + "resolved": "https://registry.npmjs.org/neo4j-driver/-/neo4j-driver-1.7.6.tgz", + "integrity": "sha512-6c3ALO3vYDfUqNoCy8OFzq+fQ7q/ab3LCuJrmm8P04M7RmyRCCnUtJ8IzSTGbiZvyhcehGK+azNDAEJhxPV/hA==", + "requires": { + "@babel/runtime": "^7.5.5", + "text-encoding-utf-8": "^1.0.2", + "uri-js": "^4.2.2" + } + } + } + }, "next-tick": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/next-tick/-/next-tick-1.0.0.tgz", @@ -7776,6 +8020,11 @@ } } }, + "object-hash": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/object-hash/-/object-hash-2.0.1.tgz", + "integrity": "sha512-HgcGMooY4JC2PBt9sdUdJ6PMzpin+YtY3r/7wg0uTifP+HJWW8rammseSEHuyt0UeShI183UGssCJqm1bJR7QA==" + }, "object-inspect": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.7.0.tgz", @@ -8342,6 +8591,11 @@ "sisteransi": "^1.0.3" } }, + "property-expr": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/property-expr/-/property-expr-1.5.1.tgz", + "integrity": "sha512-CGuc0VUTGthpJXL36ydB6jnbyOf/rAHFvmVrJlH+Rg0DqqLFQGAP6hIaxD/G0OAmBJPhXDHuEJigrp0e0wFV6g==" + }, "proxy-addr": { "version": "2.0.5", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.5.tgz", @@ -8802,6 +9056,14 @@ "integrity": "sha512-nfMOlASu9OnRJo1mbEk2cz0D56a1MBNrJ7orjRZQG10XDyuvwksKbuXNp6qa+kbn839HwjwhBzhFmdsaEAfauA==", "dev": true }, + "rxjs": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.5.3.tgz", + "integrity": "sha512-wuYsAYYFdWTAnAaPoKGNhfpWwKZbJW+HgAJ+mImp+Epl7BG8oNWBCTyRM8gba9k4lk8BgWdoYm21Mo/RYhhbgA==", + "requires": { + "tslib": "^1.9.0" + } + }, "safe-buffer": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.0.tgz", @@ -9562,6 +9824,11 @@ "integrity": "sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==", "dev": true }, + "synchronous-promise": { + "version": "2.0.10", + "resolved": "https://registry.npmjs.org/synchronous-promise/-/synchronous-promise-2.0.10.tgz", + "integrity": "sha512-6PC+JRGmNjiG3kJ56ZMNWDPL8hjyghF5cMXIFOKg+NiwwEZZIvxTWd0pinWKyD227odg9ygF8xVhhz7gb8Uq7A==" + }, "tapable": { "version": "0.2.9", "resolved": "https://registry.npmjs.org/tapable/-/tapable-0.2.9.tgz", @@ -9688,6 +9955,11 @@ } } }, + "text-encoding-utf-8": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/text-encoding-utf-8/-/text-encoding-utf-8-1.0.2.tgz", + "integrity": "sha512-8bw4MY9WjdsD2aMtO0OzOCY3pXGYNx2d2FfHRVUKkiCPDWjKuOlhLVASS+pD7VkLTVjW268LYJHwsnPFlBpbAg==" + }, "throat": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/throat/-/throat-4.1.0.tgz", @@ -9762,6 +10034,11 @@ "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.0.tgz", "integrity": "sha512-yaOH/Pk/VEhBWWTlhI+qXxDFXlejDGcQipMlyxda9nthulaxLZUNcUqFxokp0vcYnvteJln5FNQDRrxj3YcbVw==" }, + "toposort": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/toposort/-/toposort-2.0.2.tgz", + "integrity": "sha1-riF2gXXRVZ1IvvNUILL0li8JwzA=" + }, "tough-cookie": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.5.0.tgz", @@ -10555,6 +10832,19 @@ } } }, + "yup": { + "version": "0.28.0", + "resolved": "https://registry.npmjs.org/yup/-/yup-0.28.0.tgz", + "integrity": "sha512-9ZmsB/PT6/m+oUKF8rT9lWhMMGfx5s/aNCCf8pMu/GEQA0Ro2tLOc+aX12GjfL67Vif5a3c7eZVuxGFqFScnJQ==", + "requires": { + "@babel/runtime": "^7.0.0", + "fn-name": "~2.0.1", + "lodash": "^4.17.11", + "property-expr": "^1.5.0", + "synchronous-promise": "^2.0.6", + "toposort": "^2.0.2" + } + }, "zen-observable": { "version": "0.8.15", "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", diff --git a/ToDoApp/Backend/package.json b/ToDoApp/Backend/package.json index 152455b46..4f09a4e4a 100644 --- a/ToDoApp/Backend/package.json +++ b/ToDoApp/Backend/package.json @@ -8,18 +8,22 @@ "scripts": { "dev": "cross-env NODE_ENV=development webpack-dev-server --open --hot", "build": "cross-env NODE_ENV=production webpack --progress --hide-modules", - "test": "jest" + "test": "jest --detectOpenHandles --forceExit" }, "dependencies": { "apollo-server": "^2.9.9", - "apollo-server-express": "^2.9.12", "apollo-server-testing": "^2.9.9", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "express": "^4.17.1", "graphql": "^14.5.8", + "graphql-middleware": "^4.0.2", + "graphql-shield": "^7.0.7", "jsonwebtoken": "^8.5.1", "merge-graphql-schemas": "^1.7.3", + "neo4j-driver": "^4.0.0", + "neo4j-graphql-js": "^2.10.2", + "uuid": "^3.3.3", "webpack": "^3.6.0", "webpack-dev-server": "^2.9.1" }, diff --git a/ToDoApp/Backend/src/config/permissions.js b/ToDoApp/Backend/src/config/permissions.js new file mode 100644 index 000000000..557b5cde5 --- /dev/null +++ b/ToDoApp/Backend/src/config/permissions.js @@ -0,0 +1,21 @@ +const { rule, shield, not } = require('graphql-shield'); +const { isEmptyObject } = require('../utils/helpers'); + +const isAuthenticated = rule()(async (parent, args, context) => { + return context.auth !== null && context.auth !== undefined && !isEmptyObject(context.auth) +}); + +const permissions = shield({ + Query: { + }, + Mutation: { + addTodo: isAuthenticated, + updateTodo: isAuthenticated, + deleteTodo: isAuthenticated, + login: not(isAuthenticated), + }, + } + +); + +module.exports.permissions = permissions; \ No newline at end of file diff --git a/ToDoApp/Backend/src/db/data.js b/ToDoApp/Backend/src/db/data.js new file mode 100644 index 000000000..8bb6b1b6f --- /dev/null +++ b/ToDoApp/Backend/src/db/data.js @@ -0,0 +1,43 @@ +let todos = [{ + id: '1', + message: 'first todo', + finished: false, + publishedBy: '1' +}, + { + id: 2, + message: 'second todo', + finished: true, + publishedBy: '2' + }, +]; + +let users = [{ + id: '1', + name: 'eva testuser', + email: 'your@email.com', + password: '$2y$10$3IUx11G0mcJfSpFWn3Lru.xac9OqHDzqLOAhdZovaUyKa2DhgCaOS' // "password" +}, + { + id: '2', + name: 'anna testuser', + email: '2your@email.com', + password: '$2y$10$3IUx11G0mcJfSpFWn3Lru.xac9OqHDzqLOAhdZovaUyKa2DhgCaOS' // "password" + } +]; + +let events = [{ + id: '1', + motto: '90s Party', + date:"31.01.2020" + }, + { + id: '2', + motto: 'SDF Vortrag', + date:"15.01.2020" + } +] + +module.exports.todos = todos; +module.exports.users = users; +module.exports.events = events; \ No newline at end of file diff --git a/ToDoApp/Backend/src/db/seed.js b/ToDoApp/Backend/src/db/seed.js new file mode 100644 index 000000000..f41192bac --- /dev/null +++ b/ToDoApp/Backend/src/db/seed.js @@ -0,0 +1,46 @@ +const {users, todos, events} = require('./data'); + +const createUsers = async(driver) =>{ + users.forEach(async user => { + console.log("creating user") + const session = driver.session(); + await session.run( + 'CREATE (:User {id: $id, name: $name, email: $email, password: $password})', + user + ); + await session.close(); + }) +} +const createTodos = (driver) =>{ + todos.forEach(async todo => { + console.log("creating todo") + const session = driver.session(); + await session.run( + 'CREATE (t:Todo {id: $id, message: $message, finished: $finished})\n' + + 'WITH t \n' + + 'MATCH (u:User{id:$publishedBy})\n' + + 'MERGE (u)-[:PUBLISHED]->(t)\n', + todo + ); + await session.close(); + }) +} +const createEvents = (driver) =>{ + events.forEach(async event => { + console.log("creating event") + const session = driver.session(); + await session.run( + 'CREATE (e:Event {id: $id, motto: $motto, date: $date})\n' , + event + ); + await session.close(); + }) +} + +const seedDatabase = async(driver) => { + await createUsers(driver); + await createTodos(driver); + await createEvents(driver); +} + +module.exports.seedDatabase = seedDatabase; \ No newline at end of file diff --git a/ToDoApp/Backend/src/resolvers/event/eventResolver.js b/ToDoApp/Backend/src/resolvers/event/eventResolver.js new file mode 100644 index 000000000..73504882d --- /dev/null +++ b/ToDoApp/Backend/src/resolvers/event/eventResolver.js @@ -0,0 +1,60 @@ +const { find } = require('lodash'); +const { generateUUID } = require("../../../utils.js"); +const { CONFIG } = require("../../config/config"); +const bcrypt = require('bcryptjs') +const jwt = require('jsonwebtoken') +const { neo4jgraphql } = require('neo4j-graphql-js'); +const { getToken } = require('../../utils/auth') + +const eventResolver = { + Query: { + allEvents: async (parent, args, context) => { + const session = context.driver.session(); + const queryResults = await session.run('MATCH(e:Event)RETURN e '); + const events = queryResults.records.map(event => event.get(`e`).properties); + session.close() + return events; + }, + eventByMotto: async (root, { + motto + }, context) => { + const session = context.driver.session(); + const queryResults = await session.run('MATCH(e:Event) WHERE e.motto = $motto RETURN e ', { + motto:motto + }); + session.close(); + return queryResults.records[0].get("e").properties; + + }, + }, + Mutation: { + addEvent: async (_, { + motto, + date + }, context) => { + const event = { + id: generateUUID(), + motto: motto, + date: date + } + const session = context.driver.session(); + const queryResults = await session.run( + 'MATCH (e:Event {motto:$motto}) RETURN e', { + motto + } + ); + res = queryResults.records.map(event => event.get(`e`).properties)[0]; + if (res != null) { + throw new Error(`There is already an event with this motto`); + } else { + await session.run( + 'CREATE (e:Event {id: $id, motto: $motto, date: $date}) RETURN e', + {...event} + ) + session.close(); + return event; + } + } + } +} +module.exports.eventResolver = eventResolver; \ No newline at end of file diff --git a/ToDoApp/Backend/src/resolvers/event/events.spec.js b/ToDoApp/Backend/src/resolvers/event/events.spec.js new file mode 100644 index 000000000..1fa6068a3 --- /dev/null +++ b/ToDoApp/Backend/src/resolvers/event/events.spec.js @@ -0,0 +1,85 @@ +const { createTestClient } = require('apollo-server-testing'); +const { gql} = require('apollo-server'); +const { getTestApolloServer,cleanDatabase,createUser,createTodo,createEvent } = require('../../utils/testHelper'); + +const {query, mutate} = createTestClient(getTestApolloServer(true)); + +afterEach(async (done) => { + await cleanDatabase() + done() +}) + +beforeEach(async (done) => { + await createEvent({id: "1",motto: '90s Party',date: "31.01.2020" }) + await createEvent({id: "2", motto: 'SDF Vortrag', date: "15.01.2020" }) + done() +}) + +describe('Events', () => { + + it('adding event with same motto returns an Error', async () => { + const res = await mutate({ + mutation: CREATE_EVENT, + variables: { + motto: "SDF Vortrag", + date: "12.01.2020" + } + }); + expect(res.errors[0].message).toEqual("There is already an event with this motto"); + }); + + it('adding a new event', async () => { + const res = await mutate({ + mutation: CREATE_EVENT, + variables: { + motto: "Hochzeit", + date: "12.01.2020" + } + }); + expect(res.data.addEvent).toMatchObject({ + motto: "Hochzeit", + date: "12.01.2020" + }); + }); + it('query event by motto', async () => { + const res = await mutate({ + mutation: EVENT_BY_MOTTO, + variables: { + motto: "SDF Vortrag" + } + }); + expect(res.data.eventByMotto.motto).toEqual("SDF Vortrag"); + }); +}); + +const GET_ALL_EVENTS = gql ` + query { + allEvents { + id + motto + date + } + } + `; + + + + +const CREATE_EVENT = gql ` + mutation addEvent($motto: String, $date: String){ + addEvent(motto: $motto, date: $date) + { + motto, + date + } + } + `; + +const EVENT_BY_MOTTO = gql ` + query eventByMotto($motto:String!){ + eventByMotto(motto:$motto) { + motto, + date + } + } + `; \ No newline at end of file diff --git a/ToDoApp/Backend/src/resolvers/todo/todoResolver.js b/ToDoApp/Backend/src/resolvers/todo/todoResolver.js index 002063c2a..ac5cbe98c 100644 --- a/ToDoApp/Backend/src/resolvers/todo/todoResolver.js +++ b/ToDoApp/Backend/src/resolvers/todo/todoResolver.js @@ -1,68 +1,89 @@ -const { find }= require('lodash'); -const { generateIntID }= require("../../../utils.js"); -const jwt = require('jsonwebtoken') -const { CONFIG }= require("../../config/config"); +const { generateUUID }= require("../../../utils.js"); -let todos = [{ - id: 1, - message: 'first todo', - finished: false, - creator: 1 - }, - { - id: 2, - message: 'second todo', - finished: true, - creator: 2 - }, -]; - - -// Resolvers define the technique for fetching the types defined in the -// schema. This resolver retrieves books from the "books" array above. const todoResolver = { Query: { - allTodos: () => todos, - todoById: (root, args,) => { - return find(todos, { id: args.id }); + allTodos: async (parent, args, context) => { + const session = context.driver.session(); + const queryResults = await session.run('MATCH(t:Todo) RETURN t LIMIT 10'); + const todos = queryResults.records.map(todo => todo.get('t').properties); + session.close() + return todos; }, + todoById: async (root, {id}, context) => { + const session = context.driver.session(); + const queryResults = await session.run('MATCH (t:Todo {id: $id}) RETURN t', + { + id:id + }); + session.close(); + const todo = queryResults.records.map(todo => todo.get('t').properties) + return todo[0]; + }, }, Mutation: { - addTodo: (_, { message, token }) => { - const decoded = jwt.verify(token, CONFIG.JWT_SECRET) - const userId = decoded.id - todos.push({ - id: generateIntID(), - message: message, - finished: false, - creator: userId - }); - return todos; + addTodo: async (parent, { message, token }, context) => { + const userId = context.auth.userId; + const session = context.driver.session(); + const todo = { + id: generateUUID(), + message: message, + finished: false + } + await session.run( + 'CREATE (t:Todo {id: $id, message: $message, finished: $finished})\n' + + 'WITH t \n' + + 'MATCH (u:User{id:$userId})\n' + + 'MERGE (u)-[:PUBLISHED]->(t)\n', + {...todo, userId} + ); + session.close() + return [todo]; }, - updateTodo: (_, {id, token, message, finished}) => { - const decoded = jwt.verify(token, CONFIG.JWT_SECRET) - const userId = decoded.id - const todo = find(todos, {id: id}); - if (todo.creator !== userId) { - throw new Error(`Your are not the creator of todo: id ${id}`); - } - todo.message = message; - todo.finished = finished; - return todo; - }, - deleteTodo: (_, {id, token}) => { - const decoded = jwt.verify(token, CONFIG.JWT_SECRET) - const userId = decoded.id - const todo= find(todos, {id: id }) - if(todo.creator === userId){ - const index = todos.map(todo => { - return todo.id; - }).indexOf(id); - if (index > -1) { - return todos.splice(index, 1); + updateTodo: async (_, {id, token, message, finished}, context) => { + const userId = context.auth.userId; + const session = context.driver.session(); + const queryResults = await session.run( + 'MATCH (t:Todo{id:$todoId}) <-[r:PUBLISHED]-(u:User{id:$userId}) RETURN t', + { + todoId: id, + userId } + ) + const todo = queryResults.records.map(todo => todo.get('t').properties)[0]; + if (todo == null) { + throw new Error(`Your are not the creator of todo: id ${id}`); + } + let updatedTodo = {...todo}; + updatedTodo.message = message; + updatedTodo.finished = finished; + await session.run( + 'MATCH (t:Todo{id:$id}) \n' + + 'SET t = {id: $id, message: $message, finished:$finished}', + updatedTodo + ) + session.close(); + return updatedTodo; + }, + deleteTodo: async (_, {id, token}, context) => { + const userId = context.auth.userId; + const session = context.driver.session(); + const queryResults = await session.run( + 'MATCH (t:Todo{id:$todoId}) <-[r:PUBLISHED]-(u:User{id:$userId}) RETURN t', + { + todoId: id, + userId + } + ) + const todo = queryResults.records.map(todo => todo.get(`t`).properties)[0]; + if (todo == null) { + throw new Error(`Your are not the creator of todo or this node has been deleted: id ${id}`); } - return todos; + await session.run( + 'MATCH (t:Todo{id:$todoId}) DETACH DELETE t', + { todoId: id } + ) + session.close(); + return todo; } } }; diff --git a/ToDoApp/Backend/src/resolvers/todo/todos.spec.js b/ToDoApp/Backend/src/resolvers/todo/todos.spec.js index 362b6d35a..daaad4ac1 100644 --- a/ToDoApp/Backend/src/resolvers/todo/todos.spec.js +++ b/ToDoApp/Backend/src/resolvers/todo/todos.spec.js @@ -1,121 +1,100 @@ -const { mergeResolvers } = require("merge-graphql-schemas"); const { createTestClient } = require('apollo-server-testing'); -const { ApolloServer, gql } = require('apollo-server'); -const { typeDefs } = require("../../schema/typeDefs"); -const { userResolver } = require("../user/userResolver"); -const { todoResolver } = require("./todoResolver"); +const { gql} = require('apollo-server'); +const { getTestApolloServer,cleanDatabase,createUser,createTodo } = require('../../utils/testHelper'); -const resolvers = mergeResolvers([userResolver, todoResolver]); +const clientLoggedIn = createTestClient(getTestApolloServer(true)); +const clientLoggedOut = createTestClient(getTestApolloServer(false)); -const server = new ApolloServer({typeDefs, resolvers}); -const { query, mutate } = createTestClient(server); +afterEach(async (done) => { + await cleanDatabase() + done() +}) -describe('query', () => { - describe('todos', () => { - it('todo_list has 2 initial todos', async () => { - const res = await query({ - query: GET_ALL_TODOS - }); - expect(res.data.allTodos).toHaveLength(2); - }); - - it('returns todo-message for given id', async () => { - const res = await query({ - query: GET_FIRST_TODOMESSAGE - }); - expect(res.data.todoById).toMatchObject({message: "first todo"}); - }); - }); -}); - -describe('mutate', () => { - var logInToken = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MSwiZW1haWwiOiJ5b3VyQGVtYWlsLmNvbSIsImlhdCI6MTU3NTQ0NjYzNCwiZXhwIjoxNjA3MDA0MjM0fQ.cHeP2Ih8Dxeyyxw3rePvfVCeJJFFnO7A9BrV1QL4hcA"; - describe('given user is not logged in/no token', () => { +beforeEach(async (done) => { + await createUser({ id: "1", name: 'First Testuser', email: 'first@email.com', password: 'password' }) + await createTodo({ id: "1",message: 'first todo', finished: false,userId: "2" }) + await createTodo({ id: "2",message: 'second todo', finished: false, userId: "1" }) + done() +}) +describe('Todos', () => { + describe('user is not logged in', () => { it('creating a todo returns an error', async () => { - const res = await mutate({ + await expect(clientLoggedOut.mutate({ mutation: CREATE_TODO, variables: { - message: "neues Todo", - token: "" + message: "neues Todo" } - }); - expect(res.errors).toHaveLength(1); + })).resolves.toMatchObject({ + data: {addTodo: null} + }) }); - it('logIn with correct credentials returns token-String', async () => { - const res = await mutate({ - mutation: LOGIN, + it('query returns todo-message for given id', async () => { + await expect(clientLoggedOut.query({ + query: GET_TODOMESSAGE_BYID, variables: { - email: "your@email.com", - password: "password" + id: "1" } + })).resolves.toMatchObject({ + data: { + todoById: { + message: "first todo" + } + }, + errors: undefined + }) + }); + it('query all todos ', async () => { + const res = await clientLoggedOut.query({ + query: GET_ALL_TODOS }); - expect(res.data.login).toBeDefined(); + expect(res.data.allTodos).toHaveLength(2); }); }); + describe('give user is loggedIn', () => { it('adds a todo', async () => { - const res_allTodos=await query ({ - query: GET_ALL_TODOS - }); - const res = await mutate({ + const res = await clientLoggedIn.mutate({ mutation: CREATE_TODO, variables: { - message: "neues Todo", - token: logInToken + message: "neues Todo" } }); - expect(res.data.addTodo).toHaveLength(res_allTodos.data.allTodos.length+1); - expect(res.data.addTodo[res.data.addTodo.length-1].message).toEqual("neues Todo"); - + expect(res.data.addTodo[0].message).toEqual("neues Todo"); }); - describe('Modifying Todos', () => { - describe('given the loggedIn-User is the creator of the todo (allowed to modify)', () => { - - it('updates todo message ', async () => { - const res = await mutate({ - mutation: UPDATE_TODO_MESSAGE, - variables: { - id: 1, - message: "kleines Update", - finished: false, - token: logInToken - } - }); - expect(res).toMatchObject({ - "data": { - "updateTodo": { - "message": "kleines Update" - } - } - }); - }); - it('deletes todo ', async () => { - const res = await mutate({ - mutation: DELETE_TODO, - variables: { - id: 1, - token: logInToken + describe('given the loggedIn-User is the creator of the todo (allowed to modify)', () => { + it('deletes todo ', async () => { + await expect(clientLoggedIn.mutate({ + mutation: DELETE_TODO, + variables: { + id: "2" + } + })).resolves.toMatchObject({ + data: { + deleteTodo: { + id: "2" } - }); - expect(res.data.deleteTodo).toHaveLength(1); - }); + }, + errors: undefined + }) }); - describe('given the loggedIn-User is NOT the creator of the todo (NOT allowed to modify)', () => { - it('does not delete the todo', async () => { - const res = await mutate({ - mutation: DELETE_TODO, - variables: { - id: 2, - token: logInToken - } - }); - expect(res.data.deleteTodo.toBeUndefined); - }); + }); + + describe('given the loggedIn-User is NOT the creator of the todo (NOT allowed to modify)', () => { + it('does not delete the todo', async () => { + await expect(clientLoggedIn.mutate({ + mutation: DELETE_TODO, + variables: { + id: "1" + } + })).resolves.toMatchObject({ + data: { + deleteTodo: null + } + }) }); }); }); }); - const GET_ALL_TODOS = gql ` query { allTodos { @@ -125,17 +104,18 @@ const GET_ALL_TODOS = gql ` } `; -const GET_FIRST_TODOMESSAGE = gql ` - query { - todoById(id:1) { - message +const GET_TODOMESSAGE_BYID = gql ` + query addTodo($id: String!){ + todoById(id: $id) { + message + } } - } + `; const CREATE_TODO = gql ` - mutation addTodo($message: String, $token: String){ - addTodo(message: $message , token: $token) + mutation addTodo($message: String){ + addTodo(message: $message) { message } @@ -143,8 +123,8 @@ const CREATE_TODO = gql ` `; const DELETE_TODO = gql ` - mutation deleteTodo($id: Int, $token:String) { - deleteTodo(id: $id, token:$token) + mutation deleteTodo($id: String) { + deleteTodo(id: $id) { id message @@ -153,16 +133,10 @@ const DELETE_TODO = gql ` `; const UPDATE_TODO_MESSAGE = gql ` - mutation updateTodo( $id: Int, $message: String, $finished: Boolean, $token:String) { - updateTodo(id: $id, message: $message, finished:$finished, token:$token) + mutation updateTodo( $id: String, $message: String, $finished: Boolean) { + updateTodo(id: $id, message: $message, finished:$finished) { message } } - `; - -const LOGIN = gql ` - mutation login($email: String!, $password: String!) { - login(email: $email, password: $password) - } - `; + `; \ No newline at end of file diff --git a/ToDoApp/Backend/src/resolvers/user/userResolver.js b/ToDoApp/Backend/src/resolvers/user/userResolver.js index b4f735843..b7b4abfb0 100644 --- a/ToDoApp/Backend/src/resolvers/user/userResolver.js +++ b/ToDoApp/Backend/src/resolvers/user/userResolver.js @@ -1,56 +1,83 @@ -const { find }= require('lodash'); -const { generateIntID }= require("../../../utils.js"); -const { CONFIG }= require("../../config/config"); +const { find } = require('lodash'); +const { generateUUID } = require("../../../utils.js"); +const { CONFIG } = require("../../config/config"); const bcrypt = require('bcryptjs') const jwt = require('jsonwebtoken') - -const users = [{ - id: 1, - name: 'testuser', - email: 'your@email.com', - password: '$2y$10$3IUx11G0mcJfSpFWn3Lru.xac9OqHDzqLOAhdZovaUyKa2DhgCaOS' // "password" -}, -{ - id: 2, - name: '2. testuser', - email: '2your@email.com', - password: '$2y$10$3IUx11G0mcJfSpFWn3Lru.xac9OqHDzqLOAhdZovaUyKa2DhgCaOS' // "password" -} -] +const { neo4jgraphql } = require('neo4j-graphql-js'); +const { users } = require('../../db/data') +const { getToken } = require('../../utils/auth') const userResolver = { - Query: { - allUsers: () => users, - }, - Mutation: { - signup: (_, {name, email, password}) => { - const userID = generateIntID() - const userFromEmail = find(users, {email: email}); - if (userFromEmail) { - throw new Error(`This email is already used by another user: ${email}`); - } - users.push({ - id: userID, - name: name, - email: email, - password: bcrypt.hash(password, 10), - }); - const token = jwt.sign({id: userID, email: email}, CONFIG.JWT_SECRET, {expiresIn: '1y'}); - return token; + Query: { + allUsers: async (parent, args, context) => { + const session = context.driver.session(); + const queryResults = await session.run('MATCH(u:User)RETURN u ORDER BY u.name ASC '); + const users = queryResults.records.map(user => user.get(`u`).properties); + session.close() + return users; + }, + userByEmail: async (root, { + email + }, context) => { + const session = context.driver.session(); + const queryResults = await session.run('MATCH(u:User) WHERE u.email = $email RETURN u', { + email: email + }); + session.close(); + return queryResults.records[0].get("u").properties; + + }, }, - login: (_, {email, password}) => { - const userFromEmail = find(users, {email: email}); - if (!userFromEmail) { - throw new Error(`Couldn’t find a user with the email: ${email}`); - } - const valid = bcrypt.compare(password, userFromEmail.password); - if (!valid) { - throw new Error('Incorrect password'); - } - const token = jwt.sign({id: userFromEmail.id, email: userFromEmail.email}, CONFIG.JWT_SECRET, {expiresIn: '1y'}); - return token; + Mutation: { + signup: async (_, { + name, + email, + password + }, context) => { + const userID = generateUUID(); + const session = context.driver.session(); + const queryResults = await session.run( + 'MATCH (u:User {email:$email}) RETURN u', { + email + } + ); + const userFromEmail = queryResults.records.map(user => user.get(`u`).properties)[0]; + if (userFromEmail != null) { + throw new Error(`This email is already used by another user: ${email}`); + } else { + await session.run( + 'CREATE (u:User {id: $id, name: $name,email: $email ,password: $password }) RETURN u', { + id: userID, + name: name, + email: email, + password: bcrypt.hash(password, 10).toString() + } + ); + } + session.close(); + return getToken(userID, email); + }, + login: async (_, { + email, + password + }, context) => { + const session = context.driver.session(); + const queryResults = await session.run( + 'MATCH (u:User {email:$email}) RETURN u', { + email + } + ); + const userFromEmail = queryResults.records.map(user => user.get(`u`).properties)[0]; + if (userFromEmail == null) { + throw new Error(`Incorrect password or email address`); + } + const valid = bcrypt.compare(password, userFromEmail.password); + if (!valid) { + throw new Error('Incorrect password'); + } + session.close(); + return getToken(userFromEmail.id, userFromEmail.email); + } } - } -}; - -module.exports.userResolver = userResolver; +} +module.exports.userResolver = userResolver; \ No newline at end of file diff --git a/ToDoApp/Backend/src/resolvers/user/users.spec.js b/ToDoApp/Backend/src/resolvers/user/users.spec.js new file mode 100644 index 000000000..57ec84b3e --- /dev/null +++ b/ToDoApp/Backend/src/resolvers/user/users.spec.js @@ -0,0 +1,109 @@ +const { createTestClient } = require('apollo-server-testing'); +const { gql} = require('apollo-server'); +const { getTestApolloServer,cleanDatabase,createUser } = require('../../utils/testHelper'); + + +const {query, mutate} = createTestClient(getTestApolloServer(true)); + +afterEach(async (done) => { + await cleanDatabase() + done() +}) + +beforeEach(async (done) => { + await createUser ({id:"11",name: 'X Testuser', email: 'xtester@email.com' , password:'password'}) + done() +}) + +describe('User', () => { + describe('query', () =>{ + it('by Email', async () => { + const res = await query({ + query: USER_BY_EMAIL, + variables: { + email: "xtester@email.com" + } + }); + expect(res.data.userByEmail.name).toEqual("X Testuser"); + }); + it('orders List by Name ASC ', async () => { + await createUser ({id:"99",name: 'A Testuser', email: 'atester@email.com' ,password:'password'}) + const res = await query({ + query: GET_ALL_USERS + }); + expect(res.data.allUsers[0].name).toEqual("A Testuser"); + }); + }); + describe('mutate', () => { + it('signup returns error when email is already in use', async () => { + const res = await mutate({ + mutation: SIGNUP, + variables: { + name: "eva", + email: "xtester@email.com", + password: "password" + } + }); + expect(res.errors).toHaveLength(1); + expect(res.data).tobeNull; + }); + it('signup returns token-String when email is not already in use', async () => { + const res = await mutate({ + mutation: SIGNUP, + variables: { + name: "eva", + email: "eva@email.com", + password: "password" + } + }); + expect(res.data.signup).toBeDefined(); + }); + + const {query, mutate} = createTestClient(getTestApolloServer(false)); + it('login with correct credentials returns token-String', async () => { + const res = await mutate({ + mutation: LOGIN, + variables: { + email: "xtester@email.com", + password: "password" + } + }); + expect(res.data.login).toBeDefined(); + }); + }); +}); + +const GET_ALL_USERS = gql ` + query { + allUsers { + id, + name, + email + } + } + `; + + +const SIGNUP = gql ` + mutation signup($name: String!, $email: String!, $password:String!){ + signup(name: $name , email:$email, password: $password) + + } + `; + + + +const LOGIN = gql ` + mutation login($email: String!, $password: String!) { + login(email: $email, password: $password) + } + `; + +const USER_BY_EMAIL = gql ` + query userByEmail($email:String!){ + userByEmail(email:$email) { + email, + name + } + } + `; \ No newline at end of file diff --git a/ToDoApp/Backend/src/schema/typeDefs.js b/ToDoApp/Backend/src/schema/typeDefs.js index ae3a9579d..880171306 100644 --- a/ToDoApp/Backend/src/schema/typeDefs.js +++ b/ToDoApp/Backend/src/schema/typeDefs.js @@ -4,26 +4,36 @@ const { gql } = require('apollo-server'); // your data. const typeDefs = gql` type Todo { - id: Int! + id: String! message: String finished: Boolean - creator: [User] + creator: User + event: Event } type User { - id: Int! + id: String! name: String email: String password: String } + type Event { + id: String! + motto: String + date: String + } type Query { - todoById(id: Int!): Todo + todoById(id: String!): Todo + eventByMotto(motto: String!): Event + userByEmail(email: String!):User allTodos: [Todo] allUsers: [User] + allEvents:[Event] } type Mutation { - addTodo( message: String, token: String): [Todo] - updateTodo( id: Int, message: String , finished: Boolean, token: String): Todo - deleteTodo(id: Int, token: String): [Todo] + addTodo( message: String): [Todo] + addEvent( motto: String, date: String): Event + updateTodo( id: String, message: String , finished: Boolean): Todo + deleteTodo(id: String): Todo login(email: String!, password: String!): String! signup(name: String!, email: String!, password: String!): String! } diff --git a/ToDoApp/Backend/src/utils/auth.js b/ToDoApp/Backend/src/utils/auth.js new file mode 100644 index 000000000..76c3835a6 --- /dev/null +++ b/ToDoApp/Backend/src/utils/auth.js @@ -0,0 +1,31 @@ +const {AuthenticationError} = require("apollo-server-errors"); +const jwt = require('jsonwebtoken') +const { CONFIG }= require("../config/config"); + +const getAuth = (req) => { + const authToken = req.get('authToken'); + if(authToken == null){ + return {}; + } + return getContextAuthorizationObject(authToken); +} + +const getToken = (userID, email) => { + return jwt.sign({ + id: userID, + email: email + }, CONFIG.JWT_SECRET, { + expiresIn: '1y' + }); +} + +const getContextAuthorizationObject = (authToken) => { + const decoded = jwt.verify(authToken, CONFIG.JWT_SECRET) + return({ + token: authToken, + userId: decoded.id, + }) +} + +module.exports.getToken = getToken; +module.exports.getAuth = getAuth; \ No newline at end of file diff --git a/ToDoApp/Backend/src/utils/helpers.js b/ToDoApp/Backend/src/utils/helpers.js new file mode 100644 index 000000000..37e9bf085 --- /dev/null +++ b/ToDoApp/Backend/src/utils/helpers.js @@ -0,0 +1,5 @@ +const isEmptyObject = (obj) => { + return Object.entries(obj).length === 0 && obj.constructor === Object +} + +module.exports.isEmptyObject = isEmptyObject; \ No newline at end of file diff --git a/ToDoApp/Backend/src/utils/testHelper.js b/ToDoApp/Backend/src/utils/testHelper.js new file mode 100644 index 000000000..7f42b29f3 --- /dev/null +++ b/ToDoApp/Backend/src/utils/testHelper.js @@ -0,0 +1,130 @@ +const { mergeResolvers } = require("merge-graphql-schemas"); +const { ApolloServer, makeExecutableSchema} = require('apollo-server'); +const { typeDefs } = require("../schema/typeDefs"); +const { userResolver } = require("../resolvers/user/userResolver"); +const { todoResolver } = require("../resolvers/todo/todoResolver"); +const { eventResolver } = require("../resolvers/event/eventResolver"); +const { augmentSchema } = require("neo4j-graphql-js"); +const neo4j = require('neo4j-driver'); +const { getToken } = require('./auth') +const { users } = require('../db/data') +const { applyMiddleware } = require ('graphql-middleware'); +const { permissions } = require ('../config/permissions'); +const bcrypt = require('bcryptjs') +const { generateUUID } = require("../../utils.js"); + + +const driver = neo4j.driver( + 'bolt://localhost', + neo4j.auth.basic('neo4j', 'password'), { + disableLosslessIntegers: true + } +) + +const resolvers = mergeResolvers([userResolver, todoResolver,eventResolver]); + +const schema = makeExecutableSchema({ + typeDefs, + resolvers +}); +const augmentedSchema = augmentSchema(applyMiddleware(schema, permissions)); + +const getTestApolloServer = (loggedIn ) => { + return new ApolloServer({ + schema: augmentedSchema, + context: { + driver, + auth: loggedIn ? getTestAuthorizationObject() : {} + } + }) +} +const getTestAuthorizationObject = () => { + return { + token: getToken(users[0].id, users[0].email), + userId: users[0].id, + } +} + +const cleanDatabase = async (options={}) => { + getTestApolloServer(true); + const session = driver.session() + try { + await session.writeTransaction(transaction => { + return transaction.run( ` + MATCH (everything) + DETACH DELETE everything + `, + ) + }) + } finally { + session.close() + } +} +const createUser = async (params) => { + getTestApolloServer(true); + const session = driver.session() + if (params.id == null) userID = generateUUID(); + else userID = params.id; + + try { + await session.writeTransaction(transaction => { + return transaction.run( + 'CREATE (u:User {id: $id, name: $name, email: $email ,password: $password })', { + id: userID, + name: params.name, + email: params.email, + password: bcrypt.hash(params.password, 10).toString(), + }) + }) + } finally { + await session.close() + } +} + +const createTodo = async (params) => { + getTestApolloServer(true); + const session = driver.session() + if (params.id == null) todoID = generateUUID(); + else todoID = params.id; + try { + await session.writeTransaction(transaction => { + return transaction.run( + 'CREATE (t: Todo {id: $id, message: $message, finished: $finished}) WITH t MATCH (u:User{id:$userId}) MERGE (u)-[:PUBLISHED]->(t)',{ + id: todoID, + message: params.message, + finished: params.finished, + userId:params.userId + + } + ) + }) + } finally { + await session.close() + } +} + +const createEvent = async (params) => { + getTestApolloServer(true); + const session = driver.session() + if (params.id == null) eventID = generateUUID(); + else eventID = params.id; + try { + await session.writeTransaction(transaction => { + return transaction.run( + 'CREATE (e:Event {id: $id, motto: $motto, date: $date}) RETURN e',{ + id: eventID, + motto: params.motto, + date: params.date + } + ) + }) + } finally { + await session.close() + } +} + +module.exports.getTestApolloServer = getTestApolloServer; +module.exports.cleanDatabase = cleanDatabase; +module.exports.createUser = createUser; +module.exports.createTodo = createTodo; +module.exports.createEvent = createEvent; \ No newline at end of file diff --git a/ToDoApp/Backend/utils.js b/ToDoApp/Backend/utils.js index 4ee9917d3..5484b30d7 100644 --- a/ToDoApp/Backend/utils.js +++ b/ToDoApp/Backend/utils.js @@ -1,4 +1,10 @@ +const uuidv1 = require('uuid/v1'); + module.exports.generateIntID=()=>{ const i = new Date().getTime(); return i & 0xffffffff; } + +module.exports.generateUUID=()=>{ + return uuidv1(); +} \ No newline at end of file diff --git a/ToDoApp/Frontend/package-lock.json b/ToDoApp/Frontend/package-lock.json index acdf15384..9577e8b0f 100644 --- a/ToDoApp/Frontend/package-lock.json +++ b/ToDoApp/Frontend/package-lock.json @@ -789,6 +789,11 @@ "@types/istanbul-lib-report": "*" } }, + "@types/node": { + "version": "13.1.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-13.1.2.tgz", + "integrity": "sha512-B8emQA1qeKerqd1dmIsQYnXi+mmAzTB7flExjmy5X1aVAKFNNNDubkavwR13kR6JnpeLp3aLoJhwn9trWPAyFQ==" + }, "@types/stack-utils": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@types/stack-utils/-/stack-utils-1.0.1.tgz", @@ -822,6 +827,11 @@ "integrity": "sha512-gCubfBUZ6KxzoibJ+SCUc/57Ms1jz5NjHe4+dI2krNmU5zCPAphyLJYyTOg06ueIyfj+SaCUqmzun7ImlxDcKg==", "dev": true }, + "@types/zen-observable": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@types/zen-observable/-/zen-observable-0.8.0.tgz", + "integrity": "sha512-te5lMAWii1uEJ4FwLjzdlbw3+n0FZNOvFXHxQDKeT0dilh7HOzdMzV2TrJVUzq8ep7J4Na8OUYPRLSQkJHAlrg==" + }, "@vue/test-utils": { "version": "1.0.0-beta.29", "resolved": "https://registry.npmjs.org/@vue/test-utils/-/test-utils-1.0.0-beta.29.tgz", @@ -832,6 +842,23 @@ "lodash": "^4.17.4" } }, + "@wry/context": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/@wry/context/-/context-0.4.4.tgz", + "integrity": "sha512-LrKVLove/zw6h2Md/KZyWxIkFM6AoyKp71OqpH9Hiip1csjPVoD3tPxlbQUNxEnHENks3UGgNpSBCAfq9KWuag==", + "requires": { + "@types/node": ">=6", + "tslib": "^1.9.3" + } + }, + "@wry/equality": { + "version": "0.1.9", + "resolved": "https://registry.npmjs.org/@wry/equality/-/equality-0.1.9.tgz", + "integrity": "sha512-mB6ceGjpMGz1ZTza8HYnrPGos2mC6So4NhS1PtZ8s4Qt0K7fBiIGhpSxUbQmhwcSWE3no+bYxmI2OL6KuXYmoQ==", + "requires": { + "tslib": "^1.9.3" + } + }, "abab": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/abab/-/abab-2.0.2.tgz", @@ -1007,6 +1034,103 @@ } } }, + "apollo-cache": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/apollo-cache/-/apollo-cache-1.3.4.tgz", + "integrity": "sha512-7X5aGbqaOWYG+SSkCzJNHTz2ZKDcyRwtmvW4mGVLRqdQs+HxfXS4dUS2CcwrAj449se6tZ6NLUMnjko4KMt3KA==", + "requires": { + "apollo-utilities": "^1.3.3", + "tslib": "^1.10.0" + } + }, + "apollo-cache-inmemory": { + "version": "1.6.5", + "resolved": "https://registry.npmjs.org/apollo-cache-inmemory/-/apollo-cache-inmemory-1.6.5.tgz", + "integrity": "sha512-koB76JUDJaycfejHmrXBbWIN9pRKM0Z9CJGQcBzIOtmte1JhEBSuzsOUu7NQgiXKYI4iGoMREcnaWffsosZynA==", + "requires": { + "apollo-cache": "^1.3.4", + "apollo-utilities": "^1.3.3", + "optimism": "^0.10.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0" + } + }, + "apollo-client": { + "version": "2.6.8", + "resolved": "https://registry.npmjs.org/apollo-client/-/apollo-client-2.6.8.tgz", + "integrity": "sha512-0zvJtAcONiozpa5z5zgou83iEKkBaXhhSSXJebFHRXs100SecDojyUWKjwTtBPn9HbM6o5xrvC5mo9VQ5fgAjw==", + "requires": { + "@types/zen-observable": "^0.8.0", + "apollo-cache": "1.3.4", + "apollo-link": "^1.0.0", + "apollo-utilities": "1.3.3", + "symbol-observable": "^1.0.2", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0", + "zen-observable": "^0.8.0" + } + }, + "apollo-link": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/apollo-link/-/apollo-link-1.2.13.tgz", + "integrity": "sha512-+iBMcYeevMm1JpYgwDEIDt/y0BB7VWyvlm/7x+TIPNLHCTCMgcEgDuW5kH86iQZWo0I7mNwQiTOz+/3ShPFmBw==", + "requires": { + "apollo-utilities": "^1.3.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3", + "zen-observable-ts": "^0.8.20" + } + }, + "apollo-link-context": { + "version": "1.0.19", + "resolved": "https://registry.npmjs.org/apollo-link-context/-/apollo-link-context-1.0.19.tgz", + "integrity": "sha512-TUi5TyufU84hEiGkpt+5gdH5HkB3Gx46npNfoxR4of3DKBCMuItGERt36RCaryGcU/C3u2zsICU3tJ+Z9LjFoQ==", + "requires": { + "apollo-link": "^1.2.13", + "tslib": "^1.9.3" + } + }, + "apollo-link-error": { + "version": "1.1.12", + "resolved": "https://registry.npmjs.org/apollo-link-error/-/apollo-link-error-1.1.12.tgz", + "integrity": "sha512-psNmHyuy3valGikt/XHJfe0pKJnRX19tLLs6P6EHRxg+6q6JMXNVLYPaQBkL0FkwdTCB0cbFJAGRYCBviG8TDA==", + "requires": { + "apollo-link": "^1.2.13", + "apollo-link-http-common": "^0.2.15", + "tslib": "^1.9.3" + } + }, + "apollo-link-http": { + "version": "1.5.16", + "resolved": "https://registry.npmjs.org/apollo-link-http/-/apollo-link-http-1.5.16.tgz", + "integrity": "sha512-IA3xA/OcrOzINRZEECI6IdhRp/Twom5X5L9jMehfzEo2AXdeRwAMlH5LuvTZHgKD8V1MBnXdM6YXawXkTDSmJw==", + "requires": { + "apollo-link": "^1.2.13", + "apollo-link-http-common": "^0.2.15", + "tslib": "^1.9.3" + } + }, + "apollo-link-http-common": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/apollo-link-http-common/-/apollo-link-http-common-0.2.15.tgz", + "integrity": "sha512-+Heey4S2IPsPyTf8Ag3PugUupASJMW894iVps6hXbvwtg1aHSNMXUYO5VG7iRHkPzqpuzT4HMBanCTXPjtGzxg==", + "requires": { + "apollo-link": "^1.2.13", + "ts-invariant": "^0.4.0", + "tslib": "^1.9.3" + } + }, + "apollo-utilities": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/apollo-utilities/-/apollo-utilities-1.3.3.tgz", + "integrity": "sha512-F14aX2R/fKNYMvhuP2t9GD9fggID7zp5I96MF5QeKYWDWTrkRdHRp4+SVfXUVN+cXOaB/IebfvRtzPf25CM0zw==", + "requires": { + "@wry/equality": "^0.1.2", + "fast-json-stable-stringify": "^2.0.0", + "ts-invariant": "^0.4.0", + "tslib": "^1.10.0" + } + }, "aproba": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/aproba/-/aproba-1.2.0.tgz", @@ -4306,8 +4430,7 @@ "fast-json-stable-stringify": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.0.0.tgz", - "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=", - "dev": true + "integrity": "sha1-1RQsDK7msRifh9OnYREGT4bIu/I=" }, "fast-levenshtein": { "version": "2.0.6", @@ -4601,14 +4724,12 @@ "balanced-match": { "version": "1.0.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "brace-expansion": { "version": "1.1.11", "bundled": true, "dev": true, - "optional": true, "requires": { "balanced-match": "^1.0.0", "concat-map": "0.0.1" @@ -4623,20 +4744,17 @@ "code-point-at": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "concat-map": { "version": "0.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "console-control-strings": { "version": "1.1.0", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "core-util-is": { "version": "1.0.2", @@ -4753,8 +4871,7 @@ "inherits": { "version": "2.0.3", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "ini": { "version": "1.3.5", @@ -4766,7 +4883,6 @@ "version": "1.0.0", "bundled": true, "dev": true, - "optional": true, "requires": { "number-is-nan": "^1.0.0" } @@ -4781,7 +4897,6 @@ "version": "3.0.4", "bundled": true, "dev": true, - "optional": true, "requires": { "brace-expansion": "^1.1.7" } @@ -4789,14 +4904,12 @@ "minimist": { "version": "0.0.8", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "minipass": { "version": "2.3.5", "bundled": true, "dev": true, - "optional": true, "requires": { "safe-buffer": "^5.1.2", "yallist": "^3.0.0" @@ -4815,7 +4928,6 @@ "version": "0.5.1", "bundled": true, "dev": true, - "optional": true, "requires": { "minimist": "0.0.8" } @@ -4896,8 +5008,7 @@ "number-is-nan": { "version": "1.0.1", "bundled": true, - "dev": true, - "optional": true + "dev": true }, "object-assign": { "version": "4.1.1", @@ -4909,7 +5020,6 @@ "version": "1.4.0", "bundled": true, "dev": true, - "optional": true, "requires": { "wrappy": "1" } @@ -5031,7 +5141,6 @@ "version": "1.0.2", "bundled": true, "dev": true, - "optional": true, "requires": { "code-point-at": "^1.0.0", "is-fullwidth-code-point": "^1.0.0", @@ -5294,6 +5403,19 @@ "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==", "dev": true }, + "graphql": { + "version": "14.5.8", + "resolved": "https://registry.npmjs.org/graphql/-/graphql-14.5.8.tgz", + "integrity": "sha512-MMwmi0zlVLQKLdGiMfWkgQD7dY/TUKt4L+zgJ/aR0Howebod3aNgP5JkgvAULiR2HPVZaP2VEElqtdidHweLkg==", + "requires": { + "iterall": "^1.2.2" + } + }, + "graphql-tag": { + "version": "2.10.1", + "resolved": "https://registry.npmjs.org/graphql-tag/-/graphql-tag-2.10.1.tgz", + "integrity": "sha512-jApXqWBzNXQ8jYa/HLkZJaVw9jgwNqZkywa2zfFn16Iv1Zb7ELNHkJaXHR7Quvd5SIGsy6Ny7SUKATgnu05uEg==" + }, "growly": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/growly/-/growly-1.3.0.tgz", @@ -6454,6 +6576,11 @@ "handlebars": "^4.1.2" } }, + "iterall": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/iterall/-/iterall-1.2.2.tgz", + "integrity": "sha512-yynBb1g+RFUPY64fTrFv7nsjRrENBQJaX2UL+2Szc9REFrSNm1rpSXHGzhmAy7a9uv3vlvgBlXnf9RqmPH1/DA==" + }, "jest": { "version": "24.9.0", "resolved": "https://registry.npmjs.org/jest/-/jest-24.9.0.tgz", @@ -8909,6 +9036,14 @@ "is-wsl": "^1.1.0" } }, + "optimism": { + "version": "0.10.3", + "resolved": "https://registry.npmjs.org/optimism/-/optimism-0.10.3.tgz", + "integrity": "sha512-9A5pqGoQk49H6Vhjb9kPgAeeECfUDF6aIICbMDL23kDLStBn1MWk3YvcZ4xWF9CsSf6XEgvRLkXy4xof/56vVw==", + "requires": { + "@wry/context": "^0.4.0" + } + }, "optimist": { "version": "0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", @@ -10741,6 +10876,11 @@ } } }, + "serialize-javascript": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz", + "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==" + }, "serve-index": { "version": "1.9.1", "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", @@ -11451,6 +11591,11 @@ } } }, + "symbol-observable": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/symbol-observable/-/symbol-observable-1.2.0.tgz", + "integrity": "sha512-e900nM8RRtGhlV36KGEU9k65K3mPb1WV70OdjfxlG2EAuM1noi/E/BaW/uMhL7bPEssK8QV57vN3esixjUvcXQ==" + }, "symbol-tree": { "version": "3.2.4", "resolved": "https://registry.npmjs.org/symbol-tree/-/symbol-tree-3.2.4.tgz", @@ -11659,6 +11804,11 @@ "integrity": "sha1-iQN8vJLFarGJJua6TLsgDhVnKmo=", "dev": true }, + "throttle-debounce": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-2.1.0.tgz", + "integrity": "sha512-AOvyNahXQuU7NN+VVvOOX+uW6FPaWdAOdRP5HfwYxAfCzXTFKRMoIMk+n+po318+ktcChx+F1Dd91G3YHeMKyg==" + }, "through": { "version": "2.3.8", "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", @@ -11809,6 +11959,14 @@ "glob": "^7.1.2" } }, + "ts-invariant": { + "version": "0.4.4", + "resolved": "https://registry.npmjs.org/ts-invariant/-/ts-invariant-0.4.4.tgz", + "integrity": "sha512-uEtWkFM/sdZvRNNDL3Ehu4WVpwaulhwQszV8mrtcdeE8nN00BV9mAmQ88RkrBhFgl9gMgvjJLAQcZbnPXI9mlA==", + "requires": { + "tslib": "^1.9.3" + } + }, "tsconfig": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/tsconfig/-/tsconfig-7.0.0.tgz", @@ -11838,8 +11996,7 @@ "tslib": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.10.0.tgz", - "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==", - "dev": true + "integrity": "sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ==" }, "tty-browserify": { "version": "0.0.0", @@ -12199,6 +12356,49 @@ "resolved": "https://registry.npmjs.org/vue/-/vue-2.6.10.tgz", "integrity": "sha512-ImThpeNU9HbdZL3utgMCq0oiMzAkt1mcgy3/E6zWC/G6AaQoeuFdsl9nDhTDU3X1R6FK7nsIUuRACVcjI+A2GQ==" }, + "vue-apollo": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/vue-apollo/-/vue-apollo-3.0.2.tgz", + "integrity": "sha512-lrKyTT1L5mjDEp7nyqnTRJwD/kTpLDBIqFfZ+TGQVivjlUz6o5VA0pLYGCx5cGa1gEF/ERWc0AEdNSdKgs7Ygg==", + "requires": { + "chalk": "^2.4.2", + "serialize-javascript": "^2.1.0", + "throttle-debounce": "^2.1.0" + }, + "dependencies": { + "ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "requires": { + "color-convert": "^1.9.0" + } + }, + "chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "requires": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + } + }, + "has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha1-tdRU3CGZriJWmfNGfloH87lVuv0=" + }, + "supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "requires": { + "has-flag": "^3.0.0" + } + } + } + }, "vue-eslint-parser": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/vue-eslint-parser/-/vue-eslint-parser-6.0.4.tgz", @@ -13185,6 +13385,20 @@ "dev": true } } + }, + "zen-observable": { + "version": "0.8.15", + "resolved": "https://registry.npmjs.org/zen-observable/-/zen-observable-0.8.15.tgz", + "integrity": "sha512-PQ2PC7R9rslx84ndNBZB/Dkv8V8fZEpk83RLgXtYd0fwUgEjseMn1Dgajh2x6S8QbZAFa9p2qVCEuYZNgve0dQ==" + }, + "zen-observable-ts": { + "version": "0.8.20", + "resolved": "https://registry.npmjs.org/zen-observable-ts/-/zen-observable-ts-0.8.20.tgz", + "integrity": "sha512-2rkjiPALhOtRaDX6pWyNqK1fnP5KkJJybYebopNSn6wDG1lxBoFs2+nwwXKoA6glHIrtwrfBBy6da0stkKtTAA==", + "requires": { + "tslib": "^1.9.3", + "zen-observable": "^0.8.0" + } } } } diff --git a/ToDoApp/Frontend/package.json b/ToDoApp/Frontend/package.json index 675f9a7db..c4b6819f4 100644 --- a/ToDoApp/Frontend/package.json +++ b/ToDoApp/Frontend/package.json @@ -12,9 +12,17 @@ "lint": "eslint src" }, "dependencies": { + "apollo-cache-inmemory": "^1.6.5", + "apollo-client": "^2.6.8", + "apollo-link-context": "^1.0.19", + "apollo-link-error": "^1.1.12", + "apollo-link-http": "^1.5.16", "bootstrap": "^4.3.1", "bootstrap-vue": "^2.0.4", - "vue": "^2.6.10" + "graphql": "^14.5.8", + "graphql-tag": "^2.10.1", + "vue": "^2.6.10", + "vue-apollo": "^3.0.2" }, "browserslist": [ "> 1%", diff --git a/ToDoApp/Frontend/src/App.spec.js b/ToDoApp/Frontend/src/App.spec.js index 9624fee4f..2f5f73b83 100644 --- a/ToDoApp/Frontend/src/App.spec.js +++ b/ToDoApp/Frontend/src/App.spec.js @@ -1,10 +1,9 @@ -import { mount} from '@vue/test-utils' import {shallowMount} from '@vue/test-utils' import App from './App.vue' describe('Component', () => { it('is a Vue instance', () => { - const wrapper = mount(App); + const wrapper = shallowMount(App); expect(wrapper.isVueInstance()).toBeTruthy() }) it('is rendering welcome-heading', () =>{ diff --git a/ToDoApp/Frontend/src/App.vue b/ToDoApp/Frontend/src/App.vue index c38badd05..e8e0e1657 100644 --- a/ToDoApp/Frontend/src/App.vue +++ b/ToDoApp/Frontend/src/App.vue @@ -1,6 +1,9 @@