From 3b28447fc42364b302382e4990ee188b209523b3 Mon Sep 17 00:00:00 2001 From: Pouria Abdolmaleki Date: Sun, 3 Jan 2021 23:49:22 +0200 Subject: [PATCH] sync from cli - basic stuff --- backend/codegen.yml | 2 +- .../20201231122158_init/migration.sql | 15 ++ backend/prisma/schema.prisma | 21 ++- backend/src/graphql/resolvers-types.ts | 88 +++++++++-- backend/src/graphql/resolvers.ts | 4 +- backend/src/server/express.ts | 3 + backend/src/timeRecord/addTimeRecords.ts | 30 ++++ backend/src/timeRecord/getTimeRecords.ts | 9 ++ backend/src/timeRecord/timeRecord.gql | 24 +++ backend/src/timeRecord/timeRecord.resolver.ts | 14 ++ cli/lib/communicate.js | 147 ++++++++++-------- cli/lib/constants.json | 7 +- cli/lib/db.js | 25 +-- 13 files changed, 283 insertions(+), 106 deletions(-) create mode 100644 backend/src/timeRecord/addTimeRecords.ts create mode 100644 backend/src/timeRecord/getTimeRecords.ts create mode 100644 backend/src/timeRecord/timeRecord.gql create mode 100644 backend/src/timeRecord/timeRecord.resolver.ts diff --git a/backend/codegen.yml b/backend/codegen.yml index a7cf8d7d..7728aa95 100644 --- a/backend/codegen.yml +++ b/backend/codegen.yml @@ -1,7 +1,7 @@ schema: '**.gql' hooks: afterAllFileWrite: - - prettier --write + - npx prettier --write generates: ./src/graphql/resolvers-types.ts: config: diff --git a/backend/prisma/migrations/20201231122158_init/migration.sql b/backend/prisma/migrations/20201231122158_init/migration.sql index 84cce7f3..99b41c30 100644 --- a/backend/prisma/migrations/20201231122158_init/migration.sql +++ b/backend/prisma/migrations/20201231122158_init/migration.sql @@ -7,6 +7,18 @@ CREATE TABLE "Project" ( PRIMARY KEY ("id") ); +-- CreateTable +CREATE TABLE "TimeRecord" ( +"id" SERIAL, + "date" TEXT NOT NULL, + "start" TEXT NOT NULL, + "end" TEXT NOT NULL, + "breakDuration" INTEGER NOT NULL, + "userId" INTEGER NOT NULL, + + PRIMARY KEY ("id") +); + -- CreateTable CREATE TABLE "User" ( "id" SERIAL, @@ -25,3 +37,6 @@ CREATE UNIQUE INDEX "User.email_unique" ON "User"("email"); -- AddForeignKey ALTER TABLE "Project" ADD FOREIGN KEY("userId")REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; + +-- AddForeignKey +ALTER TABLE "TimeRecord" ADD FOREIGN KEY("userId")REFERENCES "User"("id") ON DELETE CASCADE ON UPDATE CASCADE; diff --git a/backend/prisma/schema.prisma b/backend/prisma/schema.prisma index f54434b8..9f30ca5e 100644 --- a/backend/prisma/schema.prisma +++ b/backend/prisma/schema.prisma @@ -16,10 +16,21 @@ model Project { userId Int @unique } +model TimeRecord { + id Int @id @default(autoincrement()) + date String + start String + end String + breakDuration Int + user User @relation(fields: [userId], references: [id]) + userId Int +} + model User { - id Int @id @default(autoincrement()) - email String @unique - name String? - password String? - projects Project? + id Int @id @default(autoincrement()) + email String @unique + name String? + password String? + projects Project? + timeRecords TimeRecord[] } diff --git a/backend/src/graphql/resolvers-types.ts b/backend/src/graphql/resolvers-types.ts index dc9984b6..dbc8b91f 100644 --- a/backend/src/graphql/resolvers-types.ts +++ b/backend/src/graphql/resolvers-types.ts @@ -27,32 +27,56 @@ export type Project = { export type Query = { __typename?: 'Query'; projects?: Maybe>>; + timeRecords?: Maybe>>; user?: Maybe; users?: Maybe>>; }; -export type UserInput = { - email: Scalars['String']; - name?: Maybe; - password?: Maybe; +export type TimeRecordInput = { + date: Scalars['String']; + start: Scalars['String']; + end: Scalars['String']; + breakDuration: Scalars['Int']; }; -export type User = { - __typename?: 'User'; +export type TimeRecord = { + __typename?: 'TimeRecord'; id?: Maybe; - email?: Maybe; - name?: Maybe; + date?: Maybe; + start?: Maybe; + end?: Maybe; + breakDuration?: Maybe; + user?: Maybe; + userId?: Maybe; }; export type Mutation = { __typename?: 'Mutation'; + addTimeRecords?: Maybe; createUser?: Maybe; }; +export type MutationAddTimeRecordsArgs = { + timeRecords: Array; +}; + export type MutationCreateUserArgs = { user: UserInput; }; +export type UserInput = { + email: Scalars['String']; + name?: Maybe; + password?: Maybe; +}; + +export type User = { + __typename?: 'User'; + id?: Maybe; + email?: Maybe; + name?: Maybe; +}; + export type WithIndex = TObject & Record; export type ResolversObject = WithIndex; @@ -158,10 +182,12 @@ export type ResolversTypes = ResolversObject<{ Int: ResolverTypeWrapper; String: ResolverTypeWrapper; Query: ResolverTypeWrapper<{}>; - UserInput: UserInput; - User: ResolverTypeWrapper; + TimeRecordInput: TimeRecordInput; + TimeRecord: ResolverTypeWrapper; Mutation: ResolverTypeWrapper<{}>; Boolean: ResolverTypeWrapper; + UserInput: UserInput; + User: ResolverTypeWrapper; }>; /** Mapping between all available schema types and the resolvers parents */ @@ -170,10 +196,12 @@ export type ResolversParentTypes = ResolversObject<{ Int: Scalars['Int']; String: Scalars['String']; Query: {}; - UserInput: UserInput; - User: User; + TimeRecordInput: TimeRecordInput; + TimeRecord: TimeRecord; Mutation: {}; Boolean: Scalars['Boolean']; + UserInput: UserInput; + User: User; }>; export type IsAuthenticatedDirectiveArgs = {}; @@ -213,17 +241,26 @@ export type QueryResolvers< ParentType, ContextType >; + timeRecords?: Resolver< + Maybe>>, + ParentType, + ContextType + >; user?: Resolver, ParentType, ContextType>; users?: Resolver>>, ParentType, ContextType>; }>; -export type UserResolvers< +export type TimeRecordResolvers< ContextType = ApolloContext, - ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] + ParentType extends ResolversParentTypes['TimeRecord'] = ResolversParentTypes['TimeRecord'] > = ResolversObject<{ id?: Resolver, ParentType, ContextType>; - email?: Resolver, ParentType, ContextType>; - name?: Resolver, ParentType, ContextType>; + date?: Resolver, ParentType, ContextType>; + start?: Resolver, ParentType, ContextType>; + end?: Resolver, ParentType, ContextType>; + breakDuration?: Resolver, ParentType, ContextType>; + user?: Resolver, ParentType, ContextType>; + userId?: Resolver, ParentType, ContextType>; __isTypeOf?: IsTypeOfResolverFn; }>; @@ -231,6 +268,12 @@ export type MutationResolvers< ContextType = ApolloContext, ParentType extends ResolversParentTypes['Mutation'] = ResolversParentTypes['Mutation'] > = ResolversObject<{ + addTimeRecords?: Resolver< + Maybe, + ParentType, + ContextType, + RequireFields + >; createUser?: Resolver< Maybe, ParentType, @@ -239,11 +282,22 @@ export type MutationResolvers< >; }>; +export type UserResolvers< + ContextType = ApolloContext, + ParentType extends ResolversParentTypes['User'] = ResolversParentTypes['User'] +> = ResolversObject<{ + id?: Resolver, ParentType, ContextType>; + email?: Resolver, ParentType, ContextType>; + name?: Resolver, ParentType, ContextType>; + __isTypeOf?: IsTypeOfResolverFn; +}>; + export type Resolvers = ResolversObject<{ Project?: ProjectResolvers; Query?: QueryResolvers; - User?: UserResolvers; + TimeRecord?: TimeRecordResolvers; Mutation?: MutationResolvers; + User?: UserResolvers; }>; /** diff --git a/backend/src/graphql/resolvers.ts b/backend/src/graphql/resolvers.ts index 0e1ab319..06bf1c27 100644 --- a/backend/src/graphql/resolvers.ts +++ b/backend/src/graphql/resolvers.ts @@ -1,5 +1,5 @@ import { mergeResolvers } from '@graphql-tools/merge'; import userResolver from '../user/user.resolver'; -import projectResolver from '../project/project.resolver'; +import timeRecordResolver from '../timeRecord/timeRecord.resolver'; -export default mergeResolvers([userResolver, projectResolver]); +export default mergeResolvers([userResolver, timeRecordResolver]); diff --git a/backend/src/server/express.ts b/backend/src/server/express.ts index 471c3eef..a18a3890 100644 --- a/backend/src/server/express.ts +++ b/backend/src/server/express.ts @@ -21,6 +21,9 @@ export const startExpress = (): void => { }), ); + // server health check + app.get('/server-health', (req, res) => res.sendStatus(200)); + // attach apollo server to express getApolloServer(apolloContext).applyMiddleware({ app }); diff --git a/backend/src/timeRecord/addTimeRecords.ts b/backend/src/timeRecord/addTimeRecords.ts new file mode 100644 index 00000000..8d16b96e --- /dev/null +++ b/backend/src/timeRecord/addTimeRecords.ts @@ -0,0 +1,30 @@ +import { MutationResolvers } from 'src/graphql/resolvers-types'; + +export const addTimeRecords: MutationResolvers['addTimeRecords'] = async ( + parent, + args, + { prisma }, +) => { + const inserts = args.timeRecords.map((record) => { + const data = { + date: record.date, + start: record.start, + end: record.end, + breakDuration: record.breakDuration, + user: { + connect: { + id: 1, + }, + }, + }; + + return prisma.timeRecord.create({ data }); + }); + try { + await Promise.all(inserts); + return true; + } catch (error) { + console.log('There was an error while adding time records to db', error); + return false; + } +}; diff --git a/backend/src/timeRecord/getTimeRecords.ts b/backend/src/timeRecord/getTimeRecords.ts new file mode 100644 index 00000000..44302375 --- /dev/null +++ b/backend/src/timeRecord/getTimeRecords.ts @@ -0,0 +1,9 @@ +import { QueryResolvers } from 'src/graphql/resolvers-types'; + +export const getTimeRecords: QueryResolvers['timeRecords'] = ( + parent, + args, + { prisma }, +) => { + return prisma.timeRecord.findMany({ where: { userId: 1 } }); +}; diff --git a/backend/src/timeRecord/timeRecord.gql b/backend/src/timeRecord/timeRecord.gql new file mode 100644 index 00000000..2dec8a53 --- /dev/null +++ b/backend/src/timeRecord/timeRecord.gql @@ -0,0 +1,24 @@ +input TimeRecordInput { + date: String! + start: String! + end: String! + breakDuration: Int! +} + +type TimeRecord { + id: Int + date: String + start: String + end: String + breakDuration: Int + user: User + userId: Int +} + +type Query { + timeRecords: [TimeRecord] +} + +type Mutation { + addTimeRecords(timeRecords: [TimeRecordInput!]!): Boolean +} diff --git a/backend/src/timeRecord/timeRecord.resolver.ts b/backend/src/timeRecord/timeRecord.resolver.ts new file mode 100644 index 00000000..5e16dfce --- /dev/null +++ b/backend/src/timeRecord/timeRecord.resolver.ts @@ -0,0 +1,14 @@ +import { Resolvers } from '../graphql/resolvers-types'; +import { getTimeRecords } from './getTimeRecords'; +import { addTimeRecords } from './addTimeRecords'; + +const resolver: Resolvers = { + Query: { + timeRecords: getTimeRecords, + }, + Mutation: { + addTimeRecords, + }, +}; + +export default resolver; diff --git a/cli/lib/communicate.js b/cli/lib/communicate.js index 0cd79e49..47dcb1e2 100644 --- a/cli/lib/communicate.js +++ b/cli/lib/communicate.js @@ -1,78 +1,99 @@ 'use strict'; -const moment = require('moment'); const axios = require('axios').default; const db = require('./db.js'); const constants = require('./constants.json'); -const TODAY = moment().format('YYYY-MM-DD'); -const NOW = moment().format('HH:mm'); - -const test = (args, options, logger) => { - axios - .get(constants.SYNC_SERVER) - .then((response) => { - console.log(response.data); - }) - .catch((error) => { - console.log(error); - }) - .then(() => { - process.exit(0); - }); +const testServerStatus = () => + axios.get(constants.PING_SERVER).then((res) => console.log(res.data)); + +const getLastLogDataOfType = (type) => db.getLastLogData(db.knex, type); + +const getItemsAfterLastLog = (type, lastLog) => { + const lastLogId = lastLog.length ? lastLog[0].id : 0; + console.log('lastLogId', lastLogId); + return db.getRowsAfter(db.knex, type, lastLogId); +}; + +const sendItemsToServer = (type, timeRecords) => { + switch (type) { + case constants.TABLES.records: + return sendTimeRecordsToServer(type, timeRecords); + + // TODO: Support sync notes + // case constants.TABLES.notes: + // return sendNotesToServer(type, recentItems); + } }; -const syncTable = (type) => { - return new Promise((resolve, reject) => { - axios - .post(constants.SYNC_SERVER) - .then((response) => { - console.log(response.data); - // check the 'log' table exists - db.getLastLogData(db.knex, type).then((lastLog) => { - // lastLog = Get last row of 'log' table - let lastLogId = 0; - if (lastLog.length) lastLogId = lastLog[0].id; - console.log(lastLogId); - // brings up items later than 'lastLog.date' - db.getRowsAfter(db.knex, type, lastLogId).then((recentLogs) => { - // send them to the server - if (recentLogs[recentLogs.length - 1]) { - console.log(constants.TEXT.sync.syncing + type + '...'); - axios - .post(constants.SYNC_SERVER, { type, records: recentLogs }) - .then((response) => { - console.log(response.data); - // save a new log in 'logs' table if successful - db.setLogData(db.knex, { - type, - lastLogId: recentLogs[recentLogs.length - 1].id, - }).then((result) => { - console.log(constants.TEXT.sync.insertLogTable, result); - resolve(type); - }); - }); - } else { - console.log(constants.TEXT.sync.nothingToSyncPrefix + type); - resolve(type); - } - }); - }); - }) - .catch((err) => { - console.log(constants.TEXT.sync.serverUnavailable); - reject(err); - }); +const sendTimeRecordsToServer = (type, timeRecords) => { + const query = ` + mutation addTimeRecords($timeRecords:[TimeRecordInput!]!){ + addTimeRecords(timeRecords:$timeRecords) + } + `; + const variables = { + timeRecords: timeRecords.map((record) => ({ + date: record.date, + start: record.start, + end: record.end, + breakDuration: record.breakDuration, + })), + }; + + return axios.post(constants.SYNC_SERVER, JSON.stringify({ query, variables }), { + headers: { 'Content-Type': 'application/json' }, }); }; -const sync = (args, options, logger) => { - syncTable('records').then(() => { - syncTable('notes').then(() => { - console.log(constants.TEXT.sync.allDone); - process.exit(0); - }); +const saveSyncLog = (type, lastLogId) => + db.setLogData(db.knex, { + type, + lastLogId, }); + +const syncTable = async (type) => { + try { + await testServerStatus(); + const lastSyncedLog = await getLastLogDataOfType(type); + console.log('lastSyncedLog', lastSyncedLog); + const recentItems = await getItemsAfterLastLog(type, lastSyncedLog); + console.log('recentItems', recentItems); + const lastItem = recentItems[recentItems.length - 1]; + console.log('lastItem', lastItem); + if (lastItem) { + console.log(constants.TEXT.sync.syncing + type + '...'); + await sendItemsToServer(type, recentItems); + console.log('lastItem.id', lastItem.id); + await saveSyncLog(type, lastItem.id); + console.log(constants.TEXT.sync.insertLogTable); + } else { + console.log(constants.TEXT.sync.nothingToSyncPrefix + type); + } + } catch (error) { + console.log(error); + process.exit(0); + } +}; + +const syncRecords = () => syncTable(constants.TABLES.records); + +const syncNotes = () => syncTable('notes'); + +const syncFinish = () => { + console.log(constants.TEXT.sync.allDone); + process.exit(0); +}; + +const sync = async (args, options, logger) => { + await syncRecords(); + // await syncNotes(); + syncFinish(); +}; + +const test = async () => { + await testServerStatus(); + process.exit(0); }; module.exports = { diff --git a/cli/lib/constants.json b/cli/lib/constants.json index 62bfc395..f32a5d4f 100644 --- a/cli/lib/constants.json +++ b/cli/lib/constants.json @@ -1,7 +1,12 @@ { "DB_FILE_MAIN": ".moro-data.db", "DB_FILE_FOR_TESTS": ".moro-tests.db", - "SYNC_SERVER": "http://localhost:3000/test", + "SYNC_SERVER": "http://localhost:3000/graphql", + "PING_SERVER": "http://localhost:3000/server-health", + "TABLES": { + "records": "records", + "notes": "notes" + }, "TEXT": { "about": "If you like Moro please consider donating to an NGO or humanitarian organization on our behalf.\n", "addNoteSuccess": "Your note is added! see it in reports\\o/ ", diff --git a/cli/lib/db.js b/cli/lib/db.js index 8b0dc78a..cc4b194c 100644 --- a/cli/lib/db.js +++ b/cli/lib/db.js @@ -197,30 +197,21 @@ const createLogTable = (knex) => } }); -const getLastLogData = (knex, type) => { - return createLogTable(knex) - .then(() => { - return knex +const getLastLogData = (knex, type) => + createLogTable(knex) + .then(() => + knex .select('*') .from('logs') .where({ type }) .orderBy('lastLogId', 'desc') .limit(1) - .then((rows) => { - return rows; - }) - .catch((e) => spinner.fail(`Errors in fetch - ${e}`)); - }) + .catch((e) => spinner.fail(`Errors in fetch - ${e}`)), + ) .catch((e) => spinner.fail(`Errors in createLogTable - ${e}`)); -}; -const setLogData = (knex, data) => { - return createLogTable(knex) - .then(() => { - return knex.insert(data).into('logs'); - }) - .catch((err) => console.log(error)); -}; +const setLogData = (knex, data) => + createLogTable(knex).then(() => knex.insert(data).into('logs')); module.exports = { createTable,