aboutsummaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorEugene Sokolov <eug-vs@keemail.me>2020-06-25 14:39:21 +0300
committerGitHub <noreply@github.com>2020-06-25 14:39:21 +0300
commit0afe8f1530affad1e58a65385b2a4bf888ab86cb (patch)
tree3bd5599bc5c3ce23777c5b369eaadd744f3c5c1f
parente488591b9548264d0305a5f34786138bd6c6622b (diff)
parent64f5f8c3f9660f649dfdaad07d84aa8c26b9661e (diff)
downloadwhich-api-0afe8f1530affad1e58a65385b2a4bf888ab86cb.tar.gz
Merge pull request #8 from which-ecosystem/votes
Votes
-rw-r--r--app.ts2
-rw-r--r--config/default.json2
-rw-r--r--hooks/convertPoll.ts43
-rw-r--r--hooks/expandAuthor.ts32
-rw-r--r--hooks/requireAuth.ts7
-rw-r--r--hooks/tryAuthenticate.ts8
-rw-r--r--models/polls/poll.model.ts15
-rw-r--r--models/polls/poll.schema.ts22
-rw-r--r--models/users/user.schema.ts17
-rw-r--r--models/votes/vote.model.ts7
-rw-r--r--models/votes/vote.schema.ts25
-rw-r--r--package-lock.json6
-rw-r--r--package.json3
-rw-r--r--populateDb.ts41
-rw-r--r--services/auth/auth.service.ts2
-rw-r--r--services/index.ts10
-rw-r--r--services/polls/polls.hooks.ts8
-rw-r--r--services/profiles/profiles.hooks.ts6
-rw-r--r--services/users/users.hooks.ts10
-rw-r--r--services/votes/votes.hooks.ts15
-rw-r--r--services/votes/votes.service.ts13
21 files changed, 210 insertions, 84 deletions
diff --git a/app.ts b/app.ts
index 0623ef3..4d699fb 100644
--- a/app.ts
+++ b/app.ts
@@ -1,7 +1,7 @@
import feathers from '@feathersjs/feathers';
import express from '@feathersjs/express';
import socketio from '@feathersjs/socketio';
-import configuration from '@feathersjs/configuration'
+import configuration from '@feathersjs/configuration';
import '@feathersjs/transport-commons';
import cors from 'cors';
diff --git a/config/default.json b/config/default.json
index e112f3e..3d8ffd2 100644
--- a/config/default.json
+++ b/config/default.json
@@ -8,7 +8,7 @@
"local"
],
"local": {
- "usernameField": "name",
+ "usernameField": "username",
"passwordField": "password"
}
}
diff --git a/hooks/convertPoll.ts b/hooks/convertPoll.ts
new file mode 100644
index 0000000..1c32851
--- /dev/null
+++ b/hooks/convertPoll.ts
@@ -0,0 +1,43 @@
+import { HookContext } from '@feathersjs/feathers';
+import { Types } from 'mongoose';
+import bluebird from 'bluebird';
+import _ from 'lodash';
+import { Poll, User, Vote } from 'which-types';
+
+import { PollSchema } from '../models/polls/poll.schema';
+import VoteModel from '../models/votes/vote.model';
+
+
+export default async (context: HookContext): Promise<HookContext> => {
+ const { app, result, params: { user } } = context;
+
+ const convert = async (poll: PollSchema): Promise<Poll | null> => {
+ const author = await app.service('users').get(poll.authorId);
+
+ const contents = await VoteModel.aggregate([
+ { $match: { pollId: Types.ObjectId(poll._id) } },
+ { $group: { _id: '$which', total: { $sum: 1 } } }
+ ]).then(groups => groups.reduce(
+ (acc, group) => _.set(acc, group._id + '.votes', group.total),
+ { left: { votes: 0 }, right: { votes: 0 } }
+ ));
+
+ const userChoice = await VoteModel.findOne(
+ { pollId: poll._id, userId: user?._id }
+ ).then(vote => vote?.which);
+
+ return _.merge(
+ _.omit(poll, ['authorId']),
+ { author, contents, userChoice }
+ );
+ };
+
+ if (Array.isArray(result)) {
+ const polls = await bluebird.map(result, (poll: PollSchema) => convert(poll));
+ context.result = _.compact(polls);
+ } else {
+ context.result = await convert(result);
+ }
+ return context;
+};
+
diff --git a/hooks/expandAuthor.ts b/hooks/expandAuthor.ts
deleted file mode 100644
index 3b3e3df..0000000
--- a/hooks/expandAuthor.ts
+++ /dev/null
@@ -1,32 +0,0 @@
-import { HookContext } from '@feathersjs/feathers';
-import bluebird from 'bluebird';
-import _ from 'lodash';
-
-import { Poll, PollSchema } from '../models/polls/poll.schema';
-import { User } from '../models/users/user.schema';
-import UserModel from '../models/users/user.model';
-
-const expandAuthor = async (poll: PollSchema): Promise<Poll | null> => {
- return UserModel.findById(poll.authorId)
- .lean<User>()
- .exec()
- .then((author: User | null): Poll | null => {
- return author && _.merge(_.omit(poll, 'authorId'), { author });
- })
- .catch(err => {
- console.error(err);
- return err;
- });
-};
-
-export const expandAuthorHook = async (context: HookContext): Promise<HookContext> => {
- context.result = await expandAuthor(context.result);
- return context;
-};
-
-export const expandAuthorManyHook = async (context: HookContext): Promise<HookContext> => {
- const polls = await bluebird.map(context.result, (poll: PollSchema) => expandAuthor(poll));
- context.result = _.compact(polls);
- return context;
-};
-
diff --git a/hooks/requireAuth.ts b/hooks/requireAuth.ts
new file mode 100644
index 0000000..a7b0e96
--- /dev/null
+++ b/hooks/requireAuth.ts
@@ -0,0 +1,7 @@
+import { HookContext } from '@feathersjs/feathers';
+
+export default async (context: HookContext): Promise<HookContext> => {
+ if (!context.params.user) throw new Error('This endpoint requires auth!');
+ return context;
+};
+
diff --git a/hooks/tryAuthenticate.ts b/hooks/tryAuthenticate.ts
new file mode 100644
index 0000000..e179417
--- /dev/null
+++ b/hooks/tryAuthenticate.ts
@@ -0,0 +1,8 @@
+import { HookContext } from '@feathersjs/feathers';
+import { authenticate } from '@feathersjs/authentication';
+
+
+export default async (context: HookContext): Promise<HookContext> => {
+ return authenticate('jwt')(context).catch(() => context);
+};
+
diff --git a/models/polls/poll.model.ts b/models/polls/poll.model.ts
index 7f6be9a..6749e5c 100644
--- a/models/polls/poll.model.ts
+++ b/models/polls/poll.model.ts
@@ -1,5 +1,20 @@
import { Model, model } from 'mongoose';
import { PollSchema, pollSchema } from './poll.schema';
+import { Types } from 'mongoose';
+import { Which } from 'which-types';
+
+pollSchema.methods.vote = function(userId: string, which: Which): PollSchema {
+ const participants: Types.ObjectId[] = ['left', 'right'].reduce((acc, option) => {
+ const { votes } = this.contents[option];
+ return acc.concat(votes);
+ }, []);
+
+ if (!participants.some(user => user.equals(userId))) {
+ this.contents[which].votes.push(userId);
+ }
+
+ return this.save();
+}
export default model<PollSchema, Model<PollSchema>>('Poll', pollSchema);
diff --git a/models/polls/poll.schema.ts b/models/polls/poll.schema.ts
index bc6d497..4d1d762 100644
--- a/models/polls/poll.schema.ts
+++ b/models/polls/poll.schema.ts
@@ -1,27 +1,21 @@
import { Document, Schema, Types } from 'mongoose';
-import { User } from '../users/user.schema';
-export interface ImageData {
+export interface ImageDataSchema {
url: string;
- votes: number;
}
-export interface Poll {
- author: User;
+export interface PollSchema extends Document {
contents: {
- left: ImageData;
- right: ImageData;
+ left: ImageDataSchema;
+ right: ImageDataSchema;
};
-}
-
-export interface PollSchema extends Document, Omit<Poll, 'author'> {
+ createdAt: Date;
authorId: string;
+ vote: (userId: string, which: 'left' | 'right') => PollSchema;
}
-
-const imageDataSchema = {
+export const imageDataSchema = {
url: String,
- votes: Number
};
export const pollSchema = new Schema({
@@ -33,5 +27,5 @@ export const pollSchema = new Schema({
type: Types.ObjectId,
ref: 'User'
}
-});
+}, { timestamps: true });
diff --git a/models/users/user.schema.ts b/models/users/user.schema.ts
index fd7d1e1..ff6cbe9 100644
--- a/models/users/user.schema.ts
+++ b/models/users/user.schema.ts
@@ -1,24 +1,21 @@
import { Document, Schema } from 'mongoose';
+import { User } from 'which-types';
-export interface User {
- name: string;
- avatarUrl?: string;
- age?: number;
-}
-
-export interface UserSchema extends Document, User {
+export interface UserSchema extends Document, Omit<User, '_id'> {
password: string;
}
export const userSchema = new Schema({
- name: String,
+ username: String,
password: String,
+ email: String,
avatarUrl: {
type: String,
required: false
},
age: {
- type: Number
+ type: Number,
+ required: false
}
-});
+}, { timestamps: true });
diff --git a/models/votes/vote.model.ts b/models/votes/vote.model.ts
new file mode 100644
index 0000000..df2307e
--- /dev/null
+++ b/models/votes/vote.model.ts
@@ -0,0 +1,7 @@
+import { Model, model } from 'mongoose';
+import { VoteSchema, voteSchema } from './vote.schema';
+
+voteSchema.index({ pollId: 1, userId: 1 }, { unique: true }); // Unique together
+
+export default model<VoteSchema, Model<VoteSchema>>('Vote', voteSchema);
+
diff --git a/models/votes/vote.schema.ts b/models/votes/vote.schema.ts
new file mode 100644
index 0000000..63ba212
--- /dev/null
+++ b/models/votes/vote.schema.ts
@@ -0,0 +1,25 @@
+import { Document, Schema, Types } from 'mongoose';
+import { Vote } from 'which-types';
+
+export interface VoteSchema extends Document, Omit<Vote, '_id'> {
+ password: string;
+}
+
+export const voteSchema = new Schema({
+ userId: {
+ type: Types.ObjectId,
+ ref: 'user',
+ required: true
+ },
+ pollId: {
+ type: Types.ObjectId,
+ ref: 'poll',
+ required: true
+ },
+ which: {
+ type: String,
+ enum: ['left', 'right'],
+ required: true
+ }
+}, { timestamps: true });
+
diff --git a/package-lock.json b/package-lock.json
index 771797b..1402c27 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -3050,6 +3050,12 @@
"isexe": "^2.0.0"
}
},
+ "which-types": {
+ "version": "1.4.0",
+ "resolved": "https://registry.npmjs.org/which-types/-/which-types-1.4.0.tgz",
+ "integrity": "sha512-dq5f7AmZeataiQZxko+Dw1xRvWdzSXTwXrjKxtyL5zqLgS29J1PXjTgcAWMfbkZhbd8+iJLkfxKvYZ42itmxeA==",
+ "dev": true
+ },
"word-wrap": {
"version": "1.2.3",
"resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz",
diff --git a/package.json b/package.json
index 7116faa..ecdd31d 100644
--- a/package.json
+++ b/package.json
@@ -42,6 +42,7 @@
"eslint": "^7.2.0",
"eslint-config-airbnb-typescript": "^8.0.2",
"eslint-plugin-import": "^2.21.2",
- "typescript": "^3.9.5"
+ "typescript": "^3.9.5",
+ "which-types": "^1.4.0"
}
}
diff --git a/populateDb.ts b/populateDb.ts
index a21b669..757280a 100644
--- a/populateDb.ts
+++ b/populateDb.ts
@@ -1,9 +1,9 @@
import mongoose from 'mongoose';
import bluebird from 'bluebird';
import _ from 'lodash';
+import { User, Poll, Vote } from 'which-types';
+
import app from './app';
-import { UserSchema } from './models/users/user.schema';
-import { PollSchema, ImageData } from './models/polls/poll.schema';
mongoose.connect('mongodb://localhost:27017/which', { useNewUrlParser: true });
@@ -28,12 +28,17 @@ const names: string[] = [
'William'
];
-const generateImageData = (): ImageData => ({
- url: _.sample(imageUrls) || '',
- votes: Math.floor(Math.random() * 101)
-});
+const choices = [
+ 'left',
+ 'right'
+];
+
+
+const createPoll = (authorId: string): Promise<Poll> => {
+ const generateImageData = () => ({
+ url: _.sample(imageUrls) || '',
+ });
-const createPoll = (authorId: string): Promise<PollSchema> => {
return app.service('polls').create({
contents: {
left: generateImageData(),
@@ -43,21 +48,33 @@ const createPoll = (authorId: string): Promise<PollSchema> => {
});
};
-const createUser = (name: string): Promise<UserSchema> => {
+const createUser = (username: string): Promise<User> => {
return app.service('users').create({
avatarUrl: _.sample(imageUrls) || '',
password: 'supersecret',
- name
+ username
});
};
+const createVote = (userId: string, pollId: string): Promise<Vote> => {
+ return app.service('votes').create({
+ pollId,
+ which: _.sample(choices)
+ }, { user: { _id: userId } });
+}
+
const populate = async () => {
const users = await bluebird.map(names, name => createUser(name));
- await bluebird.mapSeries(new Array(POLLS_AMOUNT), async () => {
- const sampleUser = _.sample(users);
- return createPoll(sampleUser?._id);
+ const polls = await bluebird.mapSeries(new Array(POLLS_AMOUNT), async () => {
+ const user = _.sample(users);
+ return createPoll(user?._id || '');
+ });
+
+ const votes = await bluebird.map(users, user => {
+ const pollsToVote = _.sampleSize(polls, _.random(0, POLLS_AMOUNT));
+ return bluebird.map(pollsToVote, poll => createVote(user?._id || '', poll?._id || ''));
});
};
diff --git a/services/auth/auth.service.ts b/services/auth/auth.service.ts
index 42846b0..826357c 100644
--- a/services/auth/auth.service.ts
+++ b/services/auth/auth.service.ts
@@ -1,6 +1,6 @@
import {
AuthenticationService,
- JWTStrategy
+ JWTStrategy
} from '@feathersjs/authentication';
import { LocalStrategy } from '@feathersjs/authentication-local';
import { Application } from '@feathersjs/express';
diff --git a/services/index.ts b/services/index.ts
index f000837..fe5ffdc 100644
--- a/services/index.ts
+++ b/services/index.ts
@@ -2,12 +2,22 @@ import { Application } from '@feathersjs/express';
import Users from './users/users.service';
import Polls from './polls/polls.service';
import Profiles from './profiles/profiles.service';
+import Votes from './votes/votes.service';
import Auth from './auth/auth.service';
+import tryAuthenticate from '../hooks/tryAuthenticate';
+
export default (app: Application): void => {
app.configure(Auth);
app.configure(Users);
app.configure(Polls);
app.configure(Profiles);
+ app.configure(Votes);
+
+ app.hooks({
+ before: {
+ all: tryAuthenticate
+ }
+ })
};
diff --git a/services/polls/polls.hooks.ts b/services/polls/polls.hooks.ts
index 0637320..eba3e63 100644
--- a/services/polls/polls.hooks.ts
+++ b/services/polls/polls.hooks.ts
@@ -1,12 +1,8 @@
-import {
- expandAuthorHook,
- expandAuthorManyHook
-} from '../../hooks/expandAuthor';
+import convertPoll from '../../hooks/convertPoll';
export default {
after: {
- get: [expandAuthorHook],
- find: [expandAuthorManyHook]
+ all: [convertPoll],
}
};
diff --git a/services/profiles/profiles.hooks.ts b/services/profiles/profiles.hooks.ts
index bb05d94..13d6f63 100644
--- a/services/profiles/profiles.hooks.ts
+++ b/services/profiles/profiles.hooks.ts
@@ -1,10 +1,8 @@
-import {
- expandAuthorManyHook
-} from '../../hooks/expandAuthor';
+import convertPoll from '../../hooks/convertPoll';
export default {
after: {
- get: [expandAuthorManyHook]
+ all: [convertPoll]
}
};
diff --git a/services/users/users.hooks.ts b/services/users/users.hooks.ts
index fc17ed7..8eecca3 100644
--- a/services/users/users.hooks.ts
+++ b/services/users/users.hooks.ts
@@ -1,11 +1,17 @@
import { hooks } from '@feathersjs/authentication-local';
+import { HookContext } from '@feathersjs/feathers';
const hashPassword = hooks.hashPassword('password');
-const protectPassword = hooks.protect('password');
+
+const localDispatch = async (context: HookContext): Promise<HookContext> => {
+ context.result = context.dispatch;
+ return context;
+}
export default {
after: {
- all: [protectPassword]
+ all: [hooks.protect('password')],
+ get: [localDispatch] // Protect password from local get's
},
before: {
create: [hashPassword],
diff --git a/services/votes/votes.hooks.ts b/services/votes/votes.hooks.ts
new file mode 100644
index 0000000..1cf7261
--- /dev/null
+++ b/services/votes/votes.hooks.ts
@@ -0,0 +1,15 @@
+import { HookContext } from '@feathersjs/feathers';
+import requireAuth from '../../hooks/requireAuth';
+
+const addUserId = async (context: HookContext): Promise<HookContext> => {
+ const { params: { user} } = context;
+ context.data.userId = user._id;
+ return context;
+};
+
+export default {
+ before: {
+ create: [requireAuth, addUserId]
+ }
+};
+
diff --git a/services/votes/votes.service.ts b/services/votes/votes.service.ts
new file mode 100644
index 0000000..cb40c1a
--- /dev/null
+++ b/services/votes/votes.service.ts
@@ -0,0 +1,13 @@
+import { Application } from '@feathersjs/express';
+import service from 'feathers-mongoose';
+import Model from '../../models/votes/vote.model';
+
+import hooks from './votes.hooks';
+
+const VoteService = service({ Model });
+
+export default (app: Application): void => {
+ app.use('/votes', VoteService);
+ app.service('votes').hooks(hooks);
+};
+