From 57fce98fc3900cf2866e66266846c8bd24b0d1f7 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 10:20:11 +0200
Subject: [PATCH 01/21] Add header comment
---
backend/src/lib/validation.js | 4 ++++
1 file changed, 4 insertions(+)
diff --git a/backend/src/lib/validation.js b/backend/src/lib/validation.js
index ea2fe16..8c15315 100644
--- a/backend/src/lib/validation.js
+++ b/backend/src/lib/validation.js
@@ -1,4 +1,8 @@
/* eslint-disable no-useless-escape */
+/**
+ * We are using the api of the library indicative to achieve user input validation.
+ * Please refer to the doc: http://indicative.adonisjs.com/
+ */
const { validateAll, configure } = require('indicative');
const { UserInputError } = require('apollo-server-express');
const {
From 2dc8c6d203b07119f678cbae2d968d10f6c4ba3c Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 10:22:35 +0200
Subject: [PATCH 02/21] Add header comments
---
backend/src/models/address.js | 1 +
backend/src/models/bankDetails.js | 1 +
2 files changed, 2 insertions(+)
diff --git a/backend/src/models/address.js b/backend/src/models/address.js
index c1290cf..e1ee616 100644
--- a/backend/src/models/address.js
+++ b/backend/src/models/address.js
@@ -1,3 +1,4 @@
+// This schema is meant to be a subdocument common to several documents
const mongoose = require('mongoose');
const { Schema } = mongoose;
diff --git a/backend/src/models/bankDetails.js b/backend/src/models/bankDetails.js
index 4c3c99c..5898f5a 100644
--- a/backend/src/models/bankDetails.js
+++ b/backend/src/models/bankDetails.js
@@ -1,3 +1,4 @@
+// This schema is meant to be a subdocument common to several documents
const mongoose = require('mongoose');
const { Schema } = mongoose;
From 34d7cf8e3fb8842552bbc6b325ce7b18c8b9fb64 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 10:36:40 +0200
Subject: [PATCH 03/21] Write JSDoc & add HASH_FACTOR env variable
---
backend/src/models/User.js | 20 ++++++++++++++++----
1 file changed, 16 insertions(+), 4 deletions(-)
diff --git a/backend/src/models/User.js b/backend/src/models/User.js
index dae711f..00c5b40 100644
--- a/backend/src/models/User.js
+++ b/backend/src/models/User.js
@@ -4,6 +4,8 @@ const bcrypt = require('bcrypt');
const address = require('./address');
const bankDetails = require('./bankDetails');
+const HASH_FACTOR = process.env.HASH_FACTOR || 10;
+
const { Schema } = mongoose;
const userSchema = new Schema({
@@ -30,6 +32,11 @@ const userSchema = new Schema({
]
});
+/**
+ * Mongoose middleware.
+ * Hash the password and check for email existance.
+ * @throws {Error} if email found
+ */
userSchema.pre('save', async function() {
const { email } = this;
// accessing the constructor to make request against the db
@@ -38,20 +45,25 @@ userSchema.pre('save', async function() {
throw new Error('Email already exists!');
}
if (this.isModified('password')) {
- // TODO Retrieve hash factor from env
- this.password = await bcrypt.hash(this.password, 10);
+ this.password = await bcrypt.hash(this.password, HASH_FACTOR);
}
});
+/**
+ * Mongoose middleware.
+ * Hash the password if provided.
+ */
userSchema.pre('findOneAndUpdate', async function() {
const input = this.getUpdate();
const { password } = input;
if (password) {
- // TODO Retrieve hash factor from env
- this.getUpdate().password = await bcrypt.hash(password, 10);
+ this.getUpdate().password = await bcrypt.hash(password, HASH_FACTOR);
}
});
+/**
+ * Expose bcrypt compare function.
+ */
userSchema.methods.rightPassword = async function(password) {
return bcrypt.compare(password, this.password);
};
From d811558f13cb5c91d8170f20b9f36ceb742b86b1 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 10:49:07 +0200
Subject: [PATCH 04/21] Add header comment
---
backend/src/models/Counter.js | 3 +++
1 file changed, 3 insertions(+)
diff --git a/backend/src/models/Counter.js b/backend/src/models/Counter.js
index 2a335d3..05589e2 100644
--- a/backend/src/models/Counter.js
+++ b/backend/src/models/Counter.js
@@ -1,3 +1,6 @@
+/**
+ * Model that creates unique sequential ids.
+ */
const mongoose = require('mongoose');
const { Schema } = mongoose;
From 9568de0d8b195696e1b785406cb2b2e192ca52e9 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 11:43:52 +0200
Subject: [PATCH 05/21] Document the invoice generation function
---
backend/src/invoiceFactory.js | 12 +++++++++++-
1 file changed, 11 insertions(+), 1 deletion(-)
diff --git a/backend/src/invoiceFactory.js b/backend/src/invoiceFactory.js
index a736234..7520839 100644
--- a/backend/src/invoiceFactory.js
+++ b/backend/src/invoiceFactory.js
@@ -2,6 +2,7 @@ const pdf = require('html-pdf');
const options = { format: 'Letter' };
+// TODO retrieve from the database
const organization = {
name: 'Open Knowledge Belgium vzw',
address: {
@@ -23,7 +24,16 @@ const renderContactDetails = contact => {
}`;
};
-module.exports = /* sender */ (details, metadata, receiver) => {
+/**
+ * Generate an invoice.
+ * @example
+ * generateInvoice({[description: "", amount: 0]}, {VAT: 21, date: 13/05/2019, noInvoice: "2019/00001"}, company: Company)
+ * @param {object} details - invoice details (see example)
+ * @param {object} metadata - invoice metadata (see example)
+ * @param {Company} company buying company
+ * @returns {Promise} Promise that represents the file stream or an error
+ */
+module.exports = (details, metadata, receiver) => {
const total = details.reduce((acc, current) => acc + current.amount, 0);
const amountVAT = (total / 100) * metadata.VAT;
const totalInclVAT = total + amountVAT;
From f83eb4473a165401ce0a3019f227f2b934aba302 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 11:49:30 +0200
Subject: [PATCH 06/21] Correct typo
---
backend/src/auth.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/src/auth.js b/backend/src/auth.js
index ca067b5..275e9d3 100644
--- a/backend/src/auth.js
+++ b/backend/src/auth.js
@@ -29,7 +29,7 @@ const loggedUser = async ({ token }, { User }) => {
/**
* Returns whether a user is logged in or not.
* @param { object } user - logged in user
- * @returns {boolean} True of false
+ * @returns {boolean} True or false
*/
const loggedIn = ({ user }) => !!user;
From 05c0d03e96316f65f427607cd72bbf03cc0bb630 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 11:58:34 +0200
Subject: [PATCH 07/21] Add more documentation
---
backend/src/constants.js | 5 +++++
backend/src/db.js | 9 +++++++++
backend/src/messages.js | 4 +++-
3 files changed, 17 insertions(+), 1 deletion(-)
diff --git a/backend/src/constants.js b/backend/src/constants.js
index 3a61baa..aca0f07 100644
--- a/backend/src/constants.js
+++ b/backend/src/constants.js
@@ -1,3 +1,8 @@
+/**
+ * Gather constants that are neither env variables nor messages.
+ * TR = transaction
+ */
+
exports.TR_FLOW = {
IN: 'IN',
OUT: 'OUT'
diff --git a/backend/src/db.js b/backend/src/db.js
index dec81a7..7a1390b 100644
--- a/backend/src/db.js
+++ b/backend/src/db.js
@@ -1,5 +1,8 @@
const mongoose = require('mongoose');
+/**
+ * Open connection to the database and creates collections.
+ */
module.exports.connect = async () => {
return mongoose
.connect(process.env.MONGODB_URI, {
@@ -21,6 +24,9 @@ module.exports.connect = async () => {
});
};
+/**
+ * Close the database connection. Used in tests.
+ */
module.exports.disconnect = async () => {
return mongoose
.disconnect()
@@ -30,4 +36,7 @@ module.exports.disconnect = async () => {
});
};
+/**
+ * Expose mongoose startSession function.
+ */
module.exports.startSession = async () => mongoose.startSession();
diff --git a/backend/src/messages.js b/backend/src/messages.js
index 66358f0..e014876 100644
--- a/backend/src/messages.js
+++ b/backend/src/messages.js
@@ -1,4 +1,6 @@
-// export constant messages
+/**
+ * Gather constant (format of) messages that are sent to the client.
+ */
module.exports = {
MUST_LOGIN: 'You must be logged in.',
MUST_LOGOUT: 'You must be logged out.',
From b381184cdbd706b2015daf1e9e0768c830a69c00 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 12:03:27 +0200
Subject: [PATCH 08/21] Remove todo comment
---
backend/src/index.js | 1 -
1 file changed, 1 deletion(-)
diff --git a/backend/src/index.js b/backend/src/index.js
index bede14d..7b821bd 100644
--- a/backend/src/index.js
+++ b/backend/src/index.js
@@ -72,7 +72,6 @@ if (process.env.NODE_ENV !== 'test') {
module.exports = {
models,
typeDefs,
- // TODO exports resolvers in their on folder via index
resolvers: {
Query,
Mutation
From 8d941e85a679e56ad9f95fdd619632eedcb44287 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 12:08:30 +0200
Subject: [PATCH 09/21] Update .env.example
---
backend/.env.example | 5 ++++-
1 file changed, 4 insertions(+), 1 deletion(-)
diff --git a/backend/.env.example b/backend/.env.example
index 9dffe09..7d68aaf 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -1,3 +1,6 @@
MONGODB_URI=mongodb+srv://:@cluster0-e7fdv.mongodb.net/test?retryWrites=true
DB_NAME=test
-APP_SECRET=this_is_a_secret
\ No newline at end of file
+APP_SECRET=this_is_a_secret
+CLOUDINARY_URL=cloudinary://:@
+STRING_MAX_CHAR=1000
+HASH_FACTOR=13
\ No newline at end of file
From 3e37ab0a4ec524386826a991a77c336309ecf4b5 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 12:28:15 +0200
Subject: [PATCH 10/21] Document test utils
---
backend/src/tests/utils.js | 37 +++++++++++--------------------------
1 file changed, 11 insertions(+), 26 deletions(-)
diff --git a/backend/src/tests/utils.js b/backend/src/tests/utils.js
index a21d16c..3bf4818 100644
--- a/backend/src/tests/utils.js
+++ b/backend/src/tests/utils.js
@@ -1,4 +1,9 @@
/* eslint-disable import/no-extraneous-dependencies */
+
+/**
+ * inspired by: https://github.com/apollographql/fullstack-tutorial/blob/master/final/server/src/__tests__/__utils.js
+ */
+
const { HttpLink } = require('apollo-link-http');
const fetch = require('node-fetch');
const { execute, toPromise } = require('apollo-link');
@@ -58,32 +63,11 @@ const startTestServer = async server => {
};
};
-// /**
-// * Upload server
-// */
-// const startTestUploadServer = async server => {
-// server.applyMiddleware({ app });
-// const httpServer = await app.listen(0);
-
-// const link = new HttpLink({
-// uri: `http://localhost:${httpServer.address().port}${server.graphqlPath}`,
-// fetch
-// });
-
-// const executeOperation = ({ query, variables = {} }) => execute(link, { query, variables });
-
-// return {
-// link,
-// stop: async () => {
-// httpServer.close();
-// },
-// graphql: executeOperation
-// };
-// };
-
module.exports.startTestServer = startTestServer;
-// clean database
+/**
+ * Cleans the database.
+ */
const clean = async () => {
const { User, Transaction, Company, Counter } = models;
await User.deleteMany({});
@@ -92,8 +76,9 @@ const clean = async () => {
await Counter.deleteMany({});
};
-// populate database
-
+/**
+ * Populates the database with mock data.
+ */
const populate = async () => {
const { User } = models;
From 1c9f1350ae960ad69fcdf702d174bc592aaa338c Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 13:40:02 +0200
Subject: [PATCH 11/21] Parse HASH_FACTOR
---
backend/src/models/User.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/backend/src/models/User.js b/backend/src/models/User.js
index 00c5b40..ab6be11 100644
--- a/backend/src/models/User.js
+++ b/backend/src/models/User.js
@@ -4,7 +4,7 @@ const bcrypt = require('bcrypt');
const address = require('./address');
const bankDetails = require('./bankDetails');
-const HASH_FACTOR = process.env.HASH_FACTOR || 10;
+const HASH_FACTOR = parseInt(process.env.HASH_FACTOR, 10) || 10;
const { Schema } = mongoose;
From bb44fb15ae411ee57eba9e257de500dba1062db5 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 13:41:17 +0200
Subject: [PATCH 12/21] Document mutations resolver
---
backend/src/graphql/resolvers/mutations.js | 30 +++++++++++++++++++---
1 file changed, 27 insertions(+), 3 deletions(-)
diff --git a/backend/src/graphql/resolvers/mutations.js b/backend/src/graphql/resolvers/mutations.js
index edf5505..fac6925 100644
--- a/backend/src/graphql/resolvers/mutations.js
+++ b/backend/src/graphql/resolvers/mutations.js
@@ -1,3 +1,12 @@
+/**
+ * Saves (updates) or retrieves a company.
+ * If id is set the company will only be retrieved
+ * If name is set (without id), the company will be updated.
+ * @param {Company} company company's data
+ * @param {Model} Company Mongoose model
+ * @param {boolean} upsert true will insert if not found when only a name is provided.
+ * @returns {Promise} Promise that represents the company
+ */
const saveOrRetrieveCompany = async (company, Company, upsert) => {
if (!company) return null;
const { name, id } = company;
@@ -11,17 +20,32 @@ const saveOrRetrieveCompany = async (company, Company, upsert) => {
return null;
};
+/**
+ * Returns an invoice reference.
+ * @param {string} id Counter document id
+ * @param {Model} Counter Mongoose model
+ * @returns {string} the reference
+ */
const getInvoiceRef = async (id, Counter) => {
const counter = await Counter.findByIdAndUpdate(
id,
{ $inc: { sequence: 1 } },
{ new: true, upsert: true }
);
- const INVOICE_REF_SIZE = process.env.INVOICE_REF_SIZE || 4;
+ const INVOICE_REF_SIZE = parseInt(process.env.INVOICE_REF_SIZE, 10) || 4;
const z = '0';
return `${id}/${`${z.repeat(INVOICE_REF_SIZE)}${counter.sequence}`.slice(-INVOICE_REF_SIZE)}`;
};
+/**
+ * Stores file by passing it to the cloud storage
+ * @param {Stream} file a file stream
+ * @param {string} tags tags to give to the file
+ * @param {string} folder folder in which to store the file
+ * @param {object} cloudinary cloudinary functions
+ * @param {boolean} generated flag that tells whether the file has been generated by the backend or not
+ * @returns {Promise} Promise that represents the file
+ */
const store = (file, tags, folder, cloudinary, generated) =>
new Promise((resolve, reject) => {
const uploadStream = cloudinary.uploader.upload_stream({ tags, folder }, (err, image) => {
@@ -139,7 +163,6 @@ module.exports = {
const { formatData, rules, messages } = uploadInvoiceValidation;
await validate(formatData({ ...invoice }), rules, messages);
- // look for a company (by id or name), if name is provided: update it or save it if doesn't exist
const company = await saveOrRetrieveCompany(invoice.company, Company, true);
invoice.company = company;
if (invoice.category && (invoice.category.name || invoice.category.id)) {
@@ -174,7 +197,6 @@ module.exports = {
) => {
const { formatData, rules, messages } = generateInvoiceValidation;
- // look for a company (by id or name), if name is provided: update it or save it if doesn't exist
const company = await saveOrRetrieveCompany(invoice.company, Company, true);
invoice.company = company;
@@ -184,6 +206,8 @@ module.exports = {
invoice.flow = TR_FLOW.OUT;
invoice.date = Date.now();
+ // TODO add category to the invoice
+
const noInvoice = await getInvoiceRef(new Date(invoice.date).getFullYear(), Counter);
invoice.ref = noInvoice;
From e9efcfbbdd681bb22ac56664881cce7a80ab1ae4 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 15:12:47 +0200
Subject: [PATCH 13/21] Document the api
---
backend/src/graphql/types.js | 33 ++++++++++++++++++++++++++++-----
1 file changed, 28 insertions(+), 5 deletions(-)
diff --git a/backend/src/graphql/types.js b/backend/src/graphql/types.js
index 144d91b..8a0f988 100644
--- a/backend/src/graphql/types.js
+++ b/backend/src/graphql/types.js
@@ -6,21 +6,31 @@ module.exports = gql`
# types
type Query {
- users: [User]! @auth
+ "Returns all the users - inaccessible for unauthenticated users"
+ users: [User]! @auth #TODO restrict access
+ "Returns the authenticated user profile"
me: User @auth
+ "Returns the authenticated user expenses"
myExpenses: [Transaction]! @auth
- transactions: [Transaction]! @auth
+ "Returns all transactions - inaccessible for unauthenticated users"
+ transactions: [Transaction]! @auth #TODO restrict access
+ "Returns all companies - inaccessible for unauthenticated users"
companies: [Company] @auth
}
type Mutation {
+ "Registers a user"
register(user: UserInput!): User! @guest
logout: Boolean!
login(email: String!, password: String!): User! @guest
+ "Handles expense claims - inaccessible for unauthenticated users"
expenseClaim(expense: Expense!): Transaction! @auth
+ "Handles profile updates - inaccessible for unauthenticated users"
updateProfile(user: UserUpdateInput!): User! @auth
- uploadInvoice(invoice: InvoiceUpload!): Transaction! @auth
- generateInvoice(invoice: GenerateInvoiceInput!): Transaction! @auth
+ "Handles invoice uploads - inaccessible for unauthenticated users"
+ uploadInvoice(invoice: InvoiceUpload!): Transaction! @auth #TODO restrict access
+ "Handles invoice generations - inaccessible for unauthenticated users"
+ generateInvoice(invoice: GenerateInvoiceInput!): Transaction! @auth #TODO restrict access
}
type Success {
status: Boolean!
@@ -32,7 +42,7 @@ module.exports = gql`
zipCode: Int!
}
- type BankDetails {
+ type BankDetails { # need attention
iban: String!
bic: String
}
@@ -58,6 +68,7 @@ module.exports = gql`
expDate: String
description: String
file: String
+ "VAT rate"
VAT: Int
type: TransactionType!
ref: String
@@ -72,6 +83,7 @@ module.exports = gql`
name: String!
email: String
phone: String
+ "VAT number"
VAT: String
bankDetails: BankDetails
address: Address
@@ -111,6 +123,7 @@ module.exports = gql`
date: String
description: String!
receipt: Upload!
+ "VAT rate"
VAT: Int
}
@@ -121,6 +134,7 @@ module.exports = gql`
company: CompanyInput
expDate: String
invoice: Upload!
+ "VAT rate"
VAT: Int
}
@@ -137,6 +151,8 @@ module.exports = gql`
input GenerateInvoiceInput {
company: CompanyInput!
details: [GenerateInvoiceDetailsInput!]!
+ # add category
+ "VAT rate"
VAT: Int!
}
@@ -146,13 +162,20 @@ module.exports = gql`
}
#enums
+
+ "Transaction flow"
enum Flow {
+ "Incoming transaction"
IN
+ "Outgoing transaction"
OUT
}
+ "Transaction state"
enum State {
+ "An unreviewed transaction"
UNCLEARED
+ "An accepted transaction"
CLEARED
PAID
REJECTED
From b10127d370d90dab70271cf9289a109f2431ca74 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 15:42:11 +0200
Subject: [PATCH 14/21] Change env variable name & complete example
---
backend/.env.example | 3 ++-
backend/src/models/User.js | 6 +++---
2 files changed, 5 insertions(+), 4 deletions(-)
diff --git a/backend/.env.example b/backend/.env.example
index 7d68aaf..717002f 100644
--- a/backend/.env.example
+++ b/backend/.env.example
@@ -3,4 +3,5 @@ DB_NAME=test
APP_SECRET=this_is_a_secret
CLOUDINARY_URL=cloudinary://:@
STRING_MAX_CHAR=1000
-HASH_FACTOR=13
\ No newline at end of file
+HASH_ROUNDS=13
+INVOICE_REF_SIZE=4
\ No newline at end of file
diff --git a/backend/src/models/User.js b/backend/src/models/User.js
index ab6be11..effe3b9 100644
--- a/backend/src/models/User.js
+++ b/backend/src/models/User.js
@@ -4,7 +4,7 @@ const bcrypt = require('bcrypt');
const address = require('./address');
const bankDetails = require('./bankDetails');
-const HASH_FACTOR = parseInt(process.env.HASH_FACTOR, 10) || 10;
+const HASH_ROUNDS = parseInt(process.env.HASH_ROUNDS, 10) || 10;
const { Schema } = mongoose;
@@ -45,7 +45,7 @@ userSchema.pre('save', async function() {
throw new Error('Email already exists!');
}
if (this.isModified('password')) {
- this.password = await bcrypt.hash(this.password, HASH_FACTOR);
+ this.password = await bcrypt.hash(this.password, HASH_ROUNDS);
}
});
@@ -57,7 +57,7 @@ userSchema.pre('findOneAndUpdate', async function() {
const input = this.getUpdate();
const { password } = input;
if (password) {
- this.getUpdate().password = await bcrypt.hash(password, HASH_FACTOR);
+ this.getUpdate().password = await bcrypt.hash(password, HASH_ROUNDS);
}
});
From a223449e6b4507f890cd2db69df1c94852da984b Mon Sep 17 00:00:00 2001
From: auloin <36740618+auloin@users.noreply.github.com>
Date: Mon, 13 May 2019 15:51:16 +0200
Subject: [PATCH 15/21] Update README.md
---
README.md | 14 +++++++++++---
1 file changed, 11 insertions(+), 3 deletions(-)
diff --git a/README.md b/README.md
index 1449579..2b9bd6a 100644
--- a/README.md
+++ b/README.md
@@ -2,10 +2,18 @@
A work in progress - Experiment in which we attempt to manage invoices, expenses, budgets and analysis in a transparent manner.
## .env
-```MONGODB_URI``` Path to reach your database (e.g.```mongodb+srv://:@cluster0-e7fdv.mongodb.net/test?retryWrites=true```)
+```MONGODB_URI``` Path to reach your database (e.g.```mongodb+srv://:@cluster0-e7fdv.mongodb.net/test?retryWrites=true```)
-```DB_NAME``` The database name (e.g. ```open-wallet``` or ```test```)
+```DB_NAME``` The database name (e.g. ```open-wallet``` or ```test```)
-```APP_SECRET``` A secret used by JWT to sign tokens, e.g. ```A secret is a secret``` (this is not a good secret)
+```APP_SECRET``` A secret used by JWT to sign tokens, e.g. ```A secret is a secret``` (this is not a good secret)
+
+```CLOUDINARY_URL``` e.g. ```cloudinary://:@```
+
+```STRING_MAX_CHAR``` maximum string length authorized for user inputs (```default 500```)
+
+```HASH_ROUNDS``` bcrypt hash rounds (```default 10```)
+
+```INVOICE_REF_SIZE``` number of characters of an invoice reference (```default 4```)
[See example](./backend/.env.example)
From 5ecb21e8e7dcbbb25d0d8ff40809d0c779b21a01 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 16:59:55 +0200
Subject: [PATCH 16/21] Enhance error handling
---
frontend/components/commons/ErrorMessage.jsx | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/frontend/components/commons/ErrorMessage.jsx b/frontend/components/commons/ErrorMessage.jsx
index 7b55e37..9c9863a 100644
--- a/frontend/components/commons/ErrorMessage.jsx
+++ b/frontend/components/commons/ErrorMessage.jsx
@@ -8,8 +8,13 @@ const ErrorMessage = ({ error }) => {
{error && (
- {error.message}
- Please make sure to provide all required fieds.
+ {error.networkError && (
+
+ Network error! Please check your internet connection and make sure to provide all
+ required values.
+
+ )}
+ {error.graphQLErrors && error.graphQLErrors.map(err => {err.message}
)}
)}
From 2344603a0e843c3e788c2fc023b039b632b94081 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Mon, 13 May 2019 17:07:48 +0200
Subject: [PATCH 17/21] Add missing prop validations
---
frontend/components/InvoiceCreationForm.jsx | 9 +++++++--
1 file changed, 7 insertions(+), 2 deletions(-)
diff --git a/frontend/components/InvoiceCreationForm.jsx b/frontend/components/InvoiceCreationForm.jsx
index a25ef58..c5c81a1 100644
--- a/frontend/components/InvoiceCreationForm.jsx
+++ b/frontend/components/InvoiceCreationForm.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/forbid-prop-types */
/* eslint-disable jsx-a11y/label-has-associated-control */
/* eslint-disable jsx-a11y/label-has-for */
import React, { useState } from 'react';
@@ -95,7 +96,9 @@ Details.propTypes = {
})
),
setDetail: PropTypes.func
- }).isRequired
+ }).isRequired,
+ // eslint-disable-next-line react/require-default-props
+ errors: PropTypes.object.isRequired
};
const Company = ({
@@ -256,7 +259,9 @@ Company.propTypes = {
selectedCompany: PropTypes.shape({
selectedCompany: companyType,
setSelectedCompany: PropTypes.func
- }).isRequired
+ }).isRequired,
+ // eslint-disable-next-line react/forbid-prop-types
+ errors: PropTypes.object.isRequired
};
const renderUI = (
From 3d31ae4127a653aff70b6a5e8750a6ab47631651 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Tue, 14 May 2019 14:06:54 +0200
Subject: [PATCH 18/21] Update comment
---
frontend/next.config.js | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/frontend/next.config.js b/frontend/next.config.js
index d4ad230..b0c314d 100644
--- a/frontend/next.config.js
+++ b/frontend/next.config.js
@@ -4,5 +4,5 @@ const withImages = require('next-images');
const withFonts = require('next-fonts');
// Don't enable CSS modules
-// To prevent blueprint classes from being hashed
+// To prevent css classes from being hashed
module.exports = withPlugins([[withCSS], [withFonts], [withImages]]);
From b7ba184fd78dcf706a9a2f21f3c98850d2e13e92 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Tue, 14 May 2019 14:30:06 +0200
Subject: [PATCH 19/21] Add comments
---
frontend/lib/checkLoggedIn.js | 9 +++++++++
frontend/lib/formatErrors.js | 5 +++++
frontend/lib/redirect.js | 3 +++
frontend/lib/validation.js | 5 +++++
frontend/lib/withApollo.js | 5 +++++
5 files changed, 27 insertions(+)
diff --git a/frontend/lib/checkLoggedIn.js b/frontend/lib/checkLoggedIn.js
index 38f39b5..aef8702 100644
--- a/frontend/lib/checkLoggedIn.js
+++ b/frontend/lib/checkLoggedIn.js
@@ -1,5 +1,14 @@
+/**
+ * inspired by = https://github.com/zeit/next.js/blob/canary/examples/with-apollo-auth/lib/checkLoggedIn.js
+ */
import { QUERY_ME } from '../graphql/queries';
+/**
+ * Retrieves the logged in user.
+ *
+ * @param {ApolloClient} apolloClient
+ * @returns {object} An object wrapping the logged in user.
+ */
export default apolloClient =>
apolloClient
.query({
diff --git a/frontend/lib/formatErrors.js b/frontend/lib/formatErrors.js
index 45a004e..86eb64b 100644
--- a/frontend/lib/formatErrors.js
+++ b/frontend/lib/formatErrors.js
@@ -1,3 +1,8 @@
+/**
+ * Transforms an array of errors into an object of errors.
+ * @param {array} errs An array of indicative validation errors
+ * @returns {object} An object
+ */
export default errs => {
const obj = {};
if (!Array.isArray(errs)) return obj;
diff --git a/frontend/lib/redirect.js b/frontend/lib/redirect.js
index 0412782..68a4478 100644
--- a/frontend/lib/redirect.js
+++ b/frontend/lib/redirect.js
@@ -1,3 +1,6 @@
+/**
+ * https://github.com/zeit/next.js/blob/canary/examples/with-apollo-auth/lib/redirect.js
+ */
import Router from 'next/router';
export default (context, target) => {
diff --git a/frontend/lib/validation.js b/frontend/lib/validation.js
index b4cb4ee..bd1c83d 100644
--- a/frontend/lib/validation.js
+++ b/frontend/lib/validation.js
@@ -1,3 +1,8 @@
+/**
+ * This file gathers some reused validation rules using the indicative validation api.
+ * see doc: https://indicative.adonisjs.com/
+ */
+
const MIN_PASSWORD_LENGTH = 8;
const REQUIRED = fieldName => `${fieldName} is required.`;
diff --git a/frontend/lib/withApollo.js b/frontend/lib/withApollo.js
index 23347f2..2b76280 100644
--- a/frontend/lib/withApollo.js
+++ b/frontend/lib/withApollo.js
@@ -1,3 +1,8 @@
+/**
+ * This file will configure an apollo client.
+ * inspiration: https://github.com/lfades/next-with-apollo/issues/13#issuecomment-390289449 + apollo client docs
+ */
+
import ApolloClient from 'apollo-client';
import { ApolloLink } from 'apollo-link';
import { createUploadLink } from 'apollo-upload-client';
From 1fb908fbce59e098b7d455b266f5c9e3522686fc Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Wed, 15 May 2019 12:45:32 +0200
Subject: [PATCH 20/21] Fix [react list child unique key]
---
frontend/components/commons/ErrorMessage.jsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/frontend/components/commons/ErrorMessage.jsx b/frontend/components/commons/ErrorMessage.jsx
index 9c9863a..d0187fe 100644
--- a/frontend/components/commons/ErrorMessage.jsx
+++ b/frontend/components/commons/ErrorMessage.jsx
@@ -1,3 +1,4 @@
+/* eslint-disable react/no-array-index-key */
import React from 'react';
import { Message, Icon } from 'semantic-ui-react';
import PropTypes from 'prop-types';
@@ -14,7 +15,7 @@ const ErrorMessage = ({ error }) => {
required values.
)}
- {error.graphQLErrors && error.graphQLErrors.map(err => {err.message}
)}
+ {error.graphQLErrors && error.graphQLErrors.map((err, i) => {err.message}
)}
)}
From 40b655c5ee4f9f88d1e9d085039b5c48df808509 Mon Sep 17 00:00:00 2001
From: Ismaila Abdoulahi
Date: Wed, 15 May 2019 14:09:10 +0200
Subject: [PATCH 21/21] Fix undefined property error, companies not being
loaded & input overflow
---
frontend/components/InvoiceCreationForm.jsx | 8 ++++++--
frontend/components/commons/InputField.jsx | 1 +
2 files changed, 7 insertions(+), 2 deletions(-)
diff --git a/frontend/components/InvoiceCreationForm.jsx b/frontend/components/InvoiceCreationForm.jsx
index c5c81a1..7523715 100644
--- a/frontend/components/InvoiceCreationForm.jsx
+++ b/frontend/components/InvoiceCreationForm.jsx
@@ -27,7 +27,11 @@ const CompanyDropdownStyle = styled.div`
const Details = ({ details: { details, setDetail }, errors }) => {
const addDetail = () => {
- setDetail([...details, { id: details[details.length - 1].id + 1 }]);
+ let id = 0;
+ if (details.length) {
+ id = details[details.length - 1].id + 1;
+ }
+ setDetail([...details, { id }]);
};
const removeDetail = idx => {
@@ -437,7 +441,7 @@ FormManager.propTypes = {
const Main = () => {
return (
- {({ data }) => }
+ {({ data, loading }) => !loading && }
);
};
diff --git a/frontend/components/commons/InputField.jsx b/frontend/components/commons/InputField.jsx
index 7e73045..13aebdc 100644
--- a/frontend/components/commons/InputField.jsx
+++ b/frontend/components/commons/InputField.jsx
@@ -22,6 +22,7 @@ const InputField = ({