ソースを参照

backend: APIv1

Joystream Stats 4 年 前
コミット
9a29caad15

+ 54 - 0
server/api/accounts.ts

@@ -0,0 +1,54 @@
+const router = require('express').Router()
+import { Account } from '../db/models'
+
+router.get('/', async (req: any, res: any, next: any) => {
+  try {
+    Account.findAllWithIncludes().then((a: any) => res.json(a))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.get('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Account.findByIdWithIncludes(req.params.id).then((a: any) => res.json(a))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/', async (req: any, res: any, next: any) => {
+  try {
+    Account.create(req.body).then((account: any) =>
+      Account.findByIdWithIncludes(account.id).then((a: any) => res.json(a))
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.put('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Account.findByPk(req.params.id).then((account: any) =>
+      account
+        .update(req.body)
+        .then(() =>
+          Account.findByIdWithIncludes(req.params.id).then((a: any) =>
+            res.json(a)
+          )
+        )
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/:id/delete', async (req: any, res: any, next: any) => {
+  try {
+    //Account.findByPk(req.params.id).then((account:any)=>res.json(account.delete())
+  } catch (err) {
+    next(err)
+  }
+})
+
+module.exports = router

+ 55 - 0
server/api/categories.ts

@@ -0,0 +1,55 @@
+const router = require('express').Router()
+import { Category } from '../db/models'
+
+router.get('/', async (req: any, res: any, next: any) => {
+  try {
+    Category.findAllWithIncludes().then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.get('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Category.findByIdWithIncludes(req.params.id).then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/', async (req: any, res: any, next: any) => {
+  try {
+    Category.create(req.body).then((category: any) =>
+      Category.findByIdWithIncludes(category.id).then((p: any) => res.json(p))
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.put('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Category.findByPk(req.params.id).then((category: any) =>
+      category
+        .update(req.body)
+        .then(() =>
+          Category.findByIdWithIncludes(req.params.id).then((p: any) =>
+            res.json(p)
+          )
+        )
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/:id/delete', async (req: any, res: any, next: any) => {
+  try {
+    //Category.findByPk(req.params.id).then((category:any)=>res.json(category.delete())
+  } catch (err) {
+    next(err)
+  }
+})
+
+module.exports = router
+

+ 55 - 0
server/api/channels.ts

@@ -0,0 +1,55 @@
+const router = require('express').Router()
+import { Channel } from '../db/models'
+
+router.get('/', async (req: any, res: any, next: any) => {
+  try {
+    Channel.findAllWithIncludes().then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.get('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Channel.findByIdWithIncludes(req.params.id).then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/', async (req: any, res: any, next: any) => {
+  try {
+    Channel.create(req.body).then((channel: any) =>
+      Channel.findByIdWithIncludes(channel.id).then((p: any) => res.json(p))
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.put('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Channel.findByPk(req.params.id).then((channel: any) =>
+      channel
+        .update(req.body)
+        .then(() =>
+          Channel.findByIdWithIncludes(req.params.id).then((p: any) =>
+            res.json(p)
+          )
+        )
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/:id/delete', async (req: any, res: any, next: any) => {
+  try {
+    //Channel.findByPk(req.params.id).then((channel:any)=>res.json(channel.delete())
+  } catch (err) {
+    next(err)
+  }
+})
+
+module.exports = router
+

+ 59 - 0
server/api/councils.ts

@@ -0,0 +1,59 @@
+const router = require('express').Router()
+import { Council } from '../db/models'
+
+router.get('/', async (req: any, res: any, next: any) => {
+  try {
+    Council.findAllWithIncludes().then((m: any) => res.json(m))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.get('/:id', async (req: any, res: any, next: any) => {
+  try {
+    if (req.params.id > 0)
+      Council.findByIdWithIncludes(req.params.id).then((m: any) => res.json(m))
+    else
+      Council.findWithIncludes({
+        where: { handle: req.params.id },
+      }).then((m: any) => res.json(m))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/', async (req: any, res: any, next: any) => {
+  try {
+    Council.create(req.body).then((member: any) =>
+      Council.findByIdWithIncludes(member.id).then((m: any) => res.json(m))
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.put('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Council.findByPk(req.params.id).then((member: any) =>
+      member
+        .update(req.body)
+        .then(() =>
+          Council.findByIdWithIncludes(req.params.id).then((m: any) =>
+            res.json(m)
+          )
+        )
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/:id/delete', async (req: any, res: any, next: any) => {
+  try {
+    //Council.findByPk(req.params.id).then((member:any)=>res.json(member.delete())
+  } catch (err) {
+    next(err)
+  }
+})
+
+module.exports = router

+ 42 - 0
server/api/index.ts

@@ -0,0 +1,42 @@
+const router = require('express').Router()
+import intro from '../intro'
+
+router.use('/v1/blocks', require('./blocks'))
+router.use('/v1/events', require('./events'))
+router.use('/v1/eras', require('./eras'))
+router.use('/v1/accounts', require('./accounts'))
+router.use('/v1/councils', require('./councils'))
+router.use('/v1/members', require('./members'))
+router.use('/v1/proposals', require('./proposals'))
+router.use('/v1/channels', require('./channels'))
+router.use('/v1/categories', require('./categories'))
+router.use('/v1/threads', require('./threads'))
+router.use('/v1/posts', require('./posts'))
+
+router.get('/v1', (req: any, res: any, next: any) => {
+  try {
+    res.send(`<pre>${intro}</pre>`)
+  } catch (err) {
+    console.log(err)
+    next()
+  }
+})
+
+router.get('/', (req: any, res: any, next: any) => {
+  try {
+    const versions = ['v1']
+    res.send(`Available versions: <ul><li><a href='/api/v1'>/v1</a></li></ul>`)
+  } catch (err) {
+    console.log(err)
+    next()
+  }
+})
+
+router.use((req: any, res: any, next: any) => {
+  const error = new Error(`Not Found: /api${req.url}`)
+  //console.log(req)
+  //error.status = 404
+  next(error)
+})
+
+module.exports = router

+ 120 - 0
server/api/members.ts

@@ -0,0 +1,120 @@
+const router = require('express').Router()
+import {
+  Member,
+  Post,
+  Proposal,
+  Council,
+  Consul,
+  ConsulStake,
+  ProposalVote,
+} from '../db/models'
+
+const findMember = (handle: number | string) =>
+  handle > 0 ? Member.findByPk(handle) : Member.findOne({ where: { handle } })
+
+router.get('/', async (req: any, res: any, next: any) => {
+  try {
+    Member.findAll().then((m: any) => res.json(m))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.get('/:id', async (req: any, res: any, next: any) => {
+  try {
+    res.json(await findMember(req.params.id))
+  } catch (err) {
+    next(err)
+  }
+})
+router.get('/:id/posts', async (req: any, res: any, next: any) => {
+  try {
+    const { id } = await findMember(req.params.id)
+    if (!id) res.json({})
+    const posts = await Post.findWithIncludes({ where: { authorId: id } })
+    res.json(posts)
+  } catch (err) {
+    next(err)
+  }
+})
+router.get('/:id/proposals', async (req: any, res: any, next: any) => {
+  try {
+    const { id } = await findMember(req.params.id)
+    if (!id) res.json({})
+    const proposals = await Proposal.findWithIncludes({
+      where: { authorId: id },
+    })
+    res.json(proposals)
+  } catch (err) {
+    next(err)
+  }
+})
+router.get('/:id/terms', async (req: any, res: any, next: any) => {
+  try {
+    const { id } = await findMember(req.params.id)
+    if (!id) res.json({})
+    const terms = await Consul.findWithIncludes({ where: { memberId: id } })
+    res.json(terms)
+  } catch (err) {
+    next(err)
+  }
+})
+router.get('/:id/votes', async (req: any, res: any, next: any) => {
+  try {
+    const member = await findMember(req.params.id)
+    if (!member) res.json({})
+    const memberId = member.id
+    const proposals = await Consul.findAll({
+      where: { memberId },
+      include: [
+        {
+          association: 'votes',
+          include: [
+            {
+              model: Proposal,
+              attributes: ['title'],
+            },
+          ],
+        },
+      ],
+    })
+
+    const councils = await Consul.findAll({
+      include: [
+        { model: Member, attributes: ['handle'] },
+        { association: 'voters', required: true, where: { memberId } },
+      ],
+    })
+    return res.json({ councils, proposals })
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/', async (req: any, res: any, next: any) => {
+  try {
+    //Member.create(req.body).then((member: any) => res.json(member))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.put('/:id', async (req: any, res: any, next: any) => {
+  try {
+    // Member.findByPk(req.params.id).then((member: any) =>
+    //   member.update(req.body).then((m: any) => res.json(m))
+    // )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/:id/delete', async (req: any, res: any, next: any) => {
+  try {
+    //Member.findByPk(req.params.id).then((member:any)=>res.json(member.delete())
+  } catch (err) {
+    next(err)
+  }
+})
+
+module.exports = router

+ 55 - 0
server/api/posts.ts

@@ -0,0 +1,55 @@
+const router = require('express').Router()
+import { Post } from '../db/models'
+
+router.get('/', async (req: any, res: any, next: any) => {
+  try {
+    Post.findAllWithIncludes().then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.get('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Post.findByIdWithIncludes(req.params.id).then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/', async (req: any, res: any, next: any) => {
+  try {
+    Post.create(req.body).then((post: any) =>
+      Post.findByIdWithIncludes(post.id).then((p: any) => res.json(p))
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.put('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Post.findByPk(req.params.id).then((post: any) =>
+      post
+        .update(req.body)
+        .then(() =>
+          Post.findByIdWithIncludes(req.params.id).then((p: any) =>
+            res.json(p)
+          )
+        )
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/:id/delete', async (req: any, res: any, next: any) => {
+  try {
+    //Post.findByPk(req.params.id).then((post:any)=>res.json(post.delete())
+  } catch (err) {
+    next(err)
+  }
+})
+
+module.exports = router
+

+ 55 - 0
server/api/proposals.ts

@@ -0,0 +1,55 @@
+const router = require('express').Router()
+import { Proposal } from '../db/models'
+
+router.get('/', async (req: any, res: any, next: any) => {
+  try {
+    Proposal.findAllWithIncludes().then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.get('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Proposal.findByIdWithIncludes(req.params.id).then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/', async (req: any, res: any, next: any) => {
+  try {
+    Proposal.create(req.body).then((proposal: any) =>
+      Proposal.findByIdWithIncludes(proposal.id).then((p: any) => res.json(p))
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.put('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Proposal.findByPk(req.params.id).then((proposal: any) =>
+      proposal
+        .update(req.body)
+        .then(() =>
+          Proposal.findByIdWithIncludes(req.params.id).then((p: any) =>
+            res.json(p)
+          )
+        )
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/:id/delete', async (req: any, res: any, next: any) => {
+  try {
+    //Proposal.findByPk(req.params.id).then((proposal:any)=>res.json(proposal.delete())
+  } catch (err) {
+    next(err)
+  }
+})
+
+module.exports = router
+

+ 55 - 0
server/api/threads.ts

@@ -0,0 +1,55 @@
+const router = require('express').Router()
+import { Thread } from '../db/models'
+
+router.get('/', async (req: any, res: any, next: any) => {
+  try {
+    Thread.findAllWithIncludes().then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.get('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Thread.findByIdWithIncludes(req.params.id).then((p: any) => res.json(p))
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/', async (req: any, res: any, next: any) => {
+  try {
+    Thread.create(req.body).then((thread: any) =>
+      Thread.findByIdWithIncludes(thread.id).then((p: any) => res.json(p))
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.put('/:id', async (req: any, res: any, next: any) => {
+  try {
+    Thread.findByPk(req.params.id).then((thread: any) =>
+      thread
+        .update(req.body)
+        .then(() =>
+          Thread.findByIdWithIncludes(req.params.id).then((p: any) =>
+            res.json(p)
+          )
+        )
+    )
+  } catch (err) {
+    next(err)
+  }
+})
+
+router.post('/:id/delete', async (req: any, res: any, next: any) => {
+  try {
+    //Thread.findByPk(req.params.id).then((thread:any)=>res.json(thread.delete())
+  } catch (err) {
+    next(err)
+  }
+})
+
+module.exports = router
+

+ 12 - 0
server/ascii.ts

@@ -0,0 +1,12 @@
+const jsstats = String.raw`
+       _                 _                             _____ _        _
+      | |               | |                           / ____| |      | | APIv1     
+      | | ___  _   _ ___| |_ _ __ ___  __ _ _ __ ___ | (___ | |_ __ _| |_ ___ 
+  _   | |/ _ \| | | / __| __| '__/ _ \/ _  | '_   _ \ \___ \| __/ _  | __/ __|
+ | |__| | (_) | |_| \__ \ |_| | |  __/ (_| | | | | | |____) | || (_| | |_\__ \
+  \____/ \___/ \__, |___/\__|_|  \___|\__,_|_| |_| |_|_____/ \__\__,_|\__|___/
+                __/ |                                                         
+               |___/ .live/api
+`
+
+export default jsstats

+ 1 - 1
server/config.json

@@ -1,3 +1,3 @@
 {
-    "domain": "https://testnet.joystream.org",
+    "domain": "https://testnet.joystream.org"
 }

+ 13 - 0
server/db/models/account.ts

@@ -7,4 +7,17 @@ const Account = db.define('account', {
   about: DataTypes.TEXT,
 })
 
+Account.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [{ model: db.models.member }, { association: 'vote' }],
+  })
+}
+
+Account.findWithIncludes = function (args: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [{ model: db.models.member }, { association: 'vote' }],
+  })
+}
+
 export default Account

+ 12 - 2
server/db/models/block.ts

@@ -15,7 +15,17 @@ Block.findAllWithIncludes = function () {
     include: [
       { model: db.models.era },
       { model: db.models.event },
-      { association: 'author' },
+      { association: 'validator' },
+    ],
+  })
+}
+
+Block.findByIdWithIncludes = function (id: number) {
+  return this.findByPk(id, {
+    include: [
+      { model: db.models.era },
+      { model: db.models.event },
+      { association: 'validator' },
     ],
   })
 }
@@ -26,7 +36,7 @@ Block.findWithIncludes = function (args: { where: any }) {
     include: [
       { model: db.models.era },
       { model: db.models.event },
-      { association: 'author' },
+      { association: 'validator' },
     ],
   })
 }

+ 50 - 0
server/db/models/category.ts

@@ -14,4 +14,54 @@ const Category = db.define('category', {
   archived: DataTypes.BOOLEAN,
 })
 
+Category.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      {
+        model: db.models.thread,
+        include: [
+          { model: db.models.post, include: [{ association: 'author' }] },
+          { association: 'author' },
+          { association: 'moderator' },
+        ],
+      },
+      { association: 'moderator' },
+    ],
+  })
+}
+
+Category.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+    include: [
+      {
+        model: db.models.thread,
+        include: [
+          { model: db.models.post, include: [{ association: 'author' }] },
+          { association: 'author' },
+          { association: 'moderator' },
+        ],
+      },
+      { association: 'moderator' },
+    ],
+  })
+}
+
+Category.findWithIncludes = function (args?: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      {
+        model: db.models.thread,
+        include: [
+          { model: db.models.post, include: [{ association: 'author' }] },
+          { association: 'author' },
+          { association: 'moderator' },
+        ],
+      },
+      { association: 'moderator' },
+    ],
+  })
+}
+
 export default Category

+ 70 - 3
server/db/models/council.ts

@@ -2,12 +2,79 @@ import db from '../db'
 import { DataTypes } from 'sequelize'
 
 const Council = db.define('council', {
-  id: {
+  round: {
     type: DataTypes.INTEGER,
     primaryKey: true,
   },
-  block: DataTypes.INTEGER,
-  round: DataTypes.INTEGER,
+  start: DataTypes.INTEGER,
+  startDate: DataTypes.DATE,
+  end: DataTypes.INTEGER,
+  endDate: DataTypes.DATE,
 })
 
+Council.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      {
+        model: db.models.consul,
+        include: [
+          { model: db.models.member, attributes: ['handle'] },
+          {
+            association: 'votes',
+            include: [{ model: db.models.proposal, attributes: ['title'] }],
+          },
+          {
+            association: 'voters',
+            include: [{ model: db.models.member, attributes: ['handle'] }],
+          },
+        ],
+      },
+    ],
+  })
+}
+
+Council.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+    include: [
+      {
+        model: db.models.consul,
+        include: [
+          { model: db.models.member, attributes: ['handle'] },
+          {
+            association: 'votes',
+            include: [{ model: db.models.proposal, attributes: ['title'] }],
+          },
+          {
+            association: 'voters',
+            include: [{ model: db.models.member, attributes: ['handle'] }],
+          },
+        ],
+      },
+    ],
+  })
+}
+
+Council.findWithIncludes = function (args?: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      {
+        model: db.models.consul,
+        include: [
+          { model: db.models.member, attributes: ['handle'] },
+          {
+            association: 'votes',
+            include: [{ model: db.models.proposal, attributes: ['title'] }],
+          },
+          {
+            association: 'voters',
+            include: [{ model: db.models.member, attributes: ['handle'] }],
+          },
+        ],
+      },
+    ],
+  })
+}
+
 export default Council

+ 61 - 0
server/db/models/councilseat.ts

@@ -0,0 +1,61 @@
+import db from '../db'
+import { DataTypes } from 'sequelize'
+
+const Seat = db.define('consul', {
+  stake: DataTypes.INTEGER,
+})
+
+Seat.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      { model: db.models.council },
+      { model: db.models.member },
+      {
+        association: 'votes',
+        include: [{ model: db.models.proposal, attributes: ['title'] }],
+      },
+      {
+        association: 'voters',
+        include: [{ model: db.models.member, attributes: ['handle'] }],
+      },
+    ],
+  })
+}
+
+Seat.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+    include: [
+      { model: db.models.council },
+      { model: db.models.member },
+      {
+        association: 'votes',
+        include: [{ model: db.models.proposal, attributes: ['title'] }],
+      },
+      {
+        association: 'voters',
+        include: [{ model: db.models.member, attributes: ['handle'] }],
+      },
+    ],
+  })
+}
+
+Seat.findWithIncludes = function (args: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      { model: db.models.council },
+      { model: db.models.member },
+      {
+        association: 'votes',
+        include: [{ model: db.models.proposal, attributes: ['title'] }],
+      },
+      {
+        association: 'voters',
+        include: [{ model: db.models.member, attributes: ['handle'] }],
+      },
+    ],
+  })
+}
+
+export default Seat

+ 28 - 0
server/db/models/councilstake.ts

@@ -0,0 +1,28 @@
+import db from '../db'
+import { DataTypes } from 'sequelize'
+
+const Stake = db.define('consulstake', {
+  stake: DataTypes.INTEGER,
+})
+
+Stake.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [{ model: db.models.consul }, { model: db.models.member }],
+  })
+}
+
+Stake.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+    include: [{ model: db.models.consul }, { model: db.models.member }],
+  })
+}
+
+Stake.findWithIncludes = function (args: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [{ model: db.models.consul }, { model: db.models.member }],
+  })
+}
+
+export default Stake

+ 34 - 0
server/db/models/era.ts

@@ -12,4 +12,38 @@ const Era = db.define('era', {
   timestamp: DataTypes.DATE,
 })
 
+Era.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      {
+        model: db.models.block,
+        include: [{ association: 'author' }, { model: db.models.event }],
+      },
+    ],
+  })
+}
+
+Era.findByIdWithIncludes = function (id: number) {
+  return this.findByPk(id, {
+    include: [
+      {
+        model: db.models.block,
+        include: [{ association: 'author' }, { model: db.models.event }],
+      },
+    ],
+  })
+}
+
+Era.findWithIncludes = function (args: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      {
+        model: db.models.block,
+        include: [{ association: 'author' }, { model: db.models.event }],
+      },
+    ],
+  })
+}
+
 export default Era

+ 26 - 5
server/db/models/event.ts

@@ -2,14 +2,35 @@ import db from '../db'
 import { DataTypes } from 'sequelize'
 
 const Event = db.define('event', {
-  id: {
-    type: DataTypes.INTEGER,
-    autoIncrement: true,
-    primaryKey: true,
-  },
   section: DataTypes.STRING,
   method: DataTypes.STRING,
   data: DataTypes.TEXT,
 })
 
+Event.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      { model: db.models.block, include: [{ association: 'validator' }] },
+    ],
+  })
+}
+
+Event.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+    include: [
+      { model: db.models.block, include: [{ association: 'validator' }] },
+    ],
+  })
+}
+
+Event.findWithIncludes = function (args?: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      { model: db.models.block, include: [{ association: 'validator' }] },
+    ],
+  })
+}
+
 export default Event

+ 33 - 3
server/db/models/index.ts

@@ -3,16 +3,28 @@ import Balance from './balance'
 import Block from './block'
 import Channel from './channel'
 import Council from './council'
+import Consul from './councilseat'
+import ConsulStake from './councilstake'
 import Era from './era'
 import Event from './event'
 import Proposal from './proposal'
+import ProposalVote from './proposalVote'
 import Member from './member'
 import Category from './category'
 import Thread from './thread'
 import Post from './post'
 
+Member.hasMany(Account)
+Member.hasMany(Consul, { as: 'terms' })
+Member.hasMany(ConsulStake, { as: 'votes' })
+Member.hasMany(Category, { as: 'categories' })
+Member.hasMany(Thread, { as: 'threads' })
+Member.hasMany(Post, { as: 'posts' })
+Member.hasMany(Proposal, { as: 'proposals' })
+
 Account.belongsTo(Member)
 Account.hasMany(Balance)
+Account.hasMany(Block, { foreignKey: 'validatorId' })
 
 Balance.belongsTo(Account)
 Balance.belongsTo(Era)
@@ -20,11 +32,19 @@ Balance.belongsTo(Era)
 Era.hasMany(Balance)
 Era.hasMany(Block)
 
+Block.belongsTo(Account, { as: 'validator' })
 Block.belongsTo(Era)
-Block.belongsTo(Member, { as: 'author' })
 Block.hasMany(Event)
+Event.belongsTo(Block)
 
-Council.hasMany(Member, { as: 'seat' })
+Council.hasMany(Consul)
+Council.hasMany(Proposal)
+Consul.belongsTo(Council)
+Consul.belongsTo(Member)
+Consul.hasMany(ConsulStake, { as: 'voters' })
+Consul.hasMany(ProposalVote, { as: 'votes' })
+ConsulStake.belongsTo(Consul)
+ConsulStake.belongsTo(Member)
 
 Channel.belongsTo(Member, { as: 'owner' })
 
@@ -32,12 +52,19 @@ Category.hasMany(Thread)
 Category.belongsTo(Member, { as: 'moderator' })
 
 Thread.belongsTo(Category)
-Thread.belongsTo(Member, { as: 'author' })
+Thread.belongsTo(Member, { as: 'creator' })
 Thread.belongsTo(Member, { as: 'moderator' })
 Thread.hasMany(Post)
 
 Post.belongsTo(Thread)
 Post.belongsTo(Member, { as: 'author' })
+Post.belongsTo(Member, { as: 'moderator' })
+
+Proposal.belongsTo(Member, { as: 'author' })
+Proposal.hasMany(ProposalVote, { as: 'votes' })
+ProposalVote.belongsTo(Proposal)
+ProposalVote.belongsTo(Consul)
+ProposalVote.belongsTo(Member)
 
 export {
   Account,
@@ -45,10 +72,13 @@ export {
   Block,
   Channel,
   Council,
+  Consul,
+  ConsulStake,
   Era,
   Event,
   Member,
   Proposal,
+  ProposalVote,
   Category,
   Thread,
   Post,

+ 80 - 0
server/db/models/member.ts

@@ -12,4 +12,84 @@ const Member = db.define('member', {
   about: DataTypes.TEXT,
 })
 
+Member.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      {
+        model: db.models.post,
+        required: false,
+        include: [{ model: db.models.thread }],
+      },
+      { model: db.models.thread, include: [{ model: db.models.category }] },
+      { model: db.models.proposal, include: [{ association: 'votes' }] },
+      { model: db.models.account },
+      {
+        association: 'terms',
+        include: [
+          {
+            association: 'votes',
+            include: [
+              {
+                model: db.models.proposal,
+                include: [{ association: 'author' }],
+              },
+            ],
+          },
+          { association: 'voters', include: [{ model: db.models.member }] },
+        ],
+      },
+      {
+        association: 'votes',
+        include: [
+          {
+            model: db.models.consul,
+            include: [{ model: db.models.member }],
+          },
+        ],
+      },
+    ],
+  })
+}
+
+Member.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+  })
+}
+
+Member.findWithIncludes = function (args: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      { model: db.models.post, include: [{ model: db.models.thread }] },
+      { model: db.models.proposal, include: [{ association: 'votes' }] },
+      { model: db.models.account },
+      {
+        association: 'terms',
+        include: [
+          {
+            association: 'votes',
+            include: [
+              {
+                model: db.models.proposal,
+                include: [{ association: 'author' }],
+              },
+            ],
+          },
+          { association: 'voters', include: [{ model: db.models.member }] },
+        ],
+      },
+      {
+        association: 'votes',
+        include: [
+          {
+            model: db.models.consul,
+            include: [{ model: db.models.member }],
+          },
+        ],
+      },
+    ],
+  })
+}
+
 export default Member

+ 31 - 0
server/db/models/post.ts

@@ -10,4 +10,35 @@ const Post = db.define('post', {
   createdAt: DataTypes.INTEGER,
 })
 
+Post.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      { model: db.models.thread, include: [{ model: db.models.category }] },
+      { association: 'author' },
+      { association: 'moderator' },
+    ],
+  })
+}
+
+Post.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+    include: [
+      { model: db.models.thread, include: [{ model: db.models.category }] },
+      { association: 'moderator' },
+    ],
+  })
+}
+
+Post.findWithIncludes = function (args: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      { model: db.models.thread, include: [{ model: db.models.category }] },
+      { model: db.models.thread },
+      { association: 'moderator' },
+    ],
+  })
+}
+
 export default Post

+ 42 - 0
server/db/models/proposal.ts

@@ -14,6 +14,48 @@ const Proposal = db.define('proposal', {
   result: DataTypes.STRING,
   executed: DataTypes.STRING,
   parameters: DataTypes.STRING,
+  description: DataTypes.TEXT,
 })
 
+Proposal.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      { association: 'author', attributes: ['handle'] },
+      {
+        association: 'votes',
+        attributes: ['id', 'vote'],
+        include: [{ model: db.models.member, attributes: ['id', 'handle'] }],
+      },
+    ],
+  })
+}
+
+Proposal.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+    include: [
+      { association: 'author', attributes: ['handle'] },
+      {
+        association: 'votes',
+        attributes: ['id', 'vote'],
+        include: [{ model: db.models.member, attributes: ['id', 'handle'] }],
+      },
+    ],
+  })
+}
+
+Proposal.findWithIncludes = function (args: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      { association: 'author', attributes: ['handle'] },
+      {
+        association: 'votes',
+        attributes: ['id', 'vote'],
+        include: [{ model: db.models.member, attributes: ['id', 'handle'] }],
+      },
+    ],
+  })
+}
+
 export default Proposal

+ 8 - 0
server/db/models/proposalVote.ts

@@ -0,0 +1,8 @@
+import db from '../db'
+import { DataTypes } from 'sequelize'
+
+const ProposalVote = db.define('proposalvote', {
+  vote: DataTypes.STRING,
+})
+
+export default ProposalVote

+ 35 - 0
server/db/models/thread.ts

@@ -11,4 +11,39 @@ const Thread = db.define('thread', {
   nrInCategory: DataTypes.INTEGER,
 })
 
+Thread.findAllWithIncludes = function () {
+  return this.findAll({
+    include: [
+      { model: db.models.category },
+      { model: db.models.post, include: [{ association: 'author' }] },
+      { association: 'creator' },
+      { association: 'moderator' },
+    ],
+  })
+}
+
+Thread.findByIdWithIncludes = function (id: number, args?: { where: any }) {
+  return this.findByPk(id, {
+    ...args,
+    include: [
+      { model: db.models.category },
+      { model: db.models.post, include: [{ association: 'author' }] },
+      { association: 'creator' },
+      { association: 'moderator' },
+    ],
+  })
+}
+
+Thread.findWithIncludes = function (args?: { where: any }) {
+  return this.findAll({
+    ...args,
+    include: [
+      { model: db.models.category },
+      { model: db.models.post, include: [{ association: 'author' }] },
+      { association: 'creator' },
+      { association: 'moderator' },
+    ],
+  })
+}
+
 export default Thread

+ 4 - 1
server/db/seed.ts

@@ -1,16 +1,19 @@
 import db from './db'
 import {
   Block,
+  Event,
   Category,
   Channel,
   Council,
+  Consul,
+  ConsulStake,
   Member,
   Post,
   Proposal,
   Thread,
 } from './models'
 
-const blocks :any[]= [] //require('../../blocks.json')
+const blocks: any[] = [] //require('../../blocks.json')
 
 async function runSeed() {
   await db.sync({ force: true })

+ 3 - 2
server/index.ts

@@ -2,6 +2,7 @@ import express from 'express'
 import path from 'path'
 import morgan from 'morgan'
 import socketio from 'socket.io'
+import ascii from './ascii'
 
 import db from './db'
 const pg = require('pg')
@@ -15,7 +16,7 @@ const PORT: number = process.env.PORT ? +process.env.PORT : 3500
 
 const app = express()
 const server = app.listen(PORT, () =>
-  console.log(`[Express] Listening on port ${PORT}`)
+  console.log(`[Express] Listening on port ${PORT}`, ascii)
 )
 
 ;(async () => {
@@ -92,7 +93,7 @@ app.use(morgan('dev'))
 app.use(express.json())
 app.use(express.urlencoded({ extended: true }))
 app.use(require('body-parser').text())
-//app.use("/api", require("./api"));
+app.use('/api', require('./api'))
 //app.use('/auth', require('./auth'))
 app.use(
   '/static',

+ 45 - 0
server/intro.ts

@@ -0,0 +1,45 @@
+const intro = String.raw`Welcome to JoystreamStats.live APIv1!
+
+This API exposes json objects for following routes accessible via any browser:
+
+  /blocks	      returns all known blocks
+  /blocks/:id	      returns one block if found
+
+  /events	      returns all events
+  /events/:id	      returns one event if found
+  /events/sections    returns all sections found on events
+  /events/methods     returns all methods found on events
+  /events/{method|section}    all events using method / section, for example:
+  /events/{balances|transfer|reward|[era]payout|voted|[un]bonded|..}
+  /events/{:method}/:key     events using :method containing :key in data
+
+  /eras		      returns all known eras
+  /eras/:id	      returns one era if found
+
+  /members	      returns all known members
+  /members/:id	      returns one membership if found (id: number or handle)
+  /members/:id/posts  	      all posts by member
+  /members/:id/proposals      all proposals by member
+  /members/:id/termns  	      all council terms by member
+  /members/:id/votes  	      all proposal and coucnil votes by member
+
+  /councils	      returns all council terms including votes
+  /councils/:id	      returns specific term with votes
+
+  /proposals	      returns all known proposals
+  /proposals/:id      returns one proposal if found
+
+  /channels	      returns all known channels
+  /channels/:id	      returns one channel if found
+
+  /categories	      returns all known categories
+  /categories/:id     returns one category if found
+
+  /threads	      returns all known threads
+  /threads/:id	      returns one thread if found
+
+  /posts	      returns all known posts
+  /posts/:id	      returns one post if found
+`
+
+export default intro

+ 185 - 83
server/joystream/index.ts

@@ -1,3 +1,4 @@
+import { Op } from 'sequelize'
 import {
   Account,
   Balance,
@@ -5,13 +6,17 @@ import {
   Category,
   Channel,
   Council,
+  Consul,
+  ConsulStake,
   Era,
   Event,
   Member,
   Post,
   Proposal,
+  ProposalVote,
   Thread,
 } from '../db/models'
+
 import * as get from './lib/getters'
 //import {fetchReports} from './lib/github'
 import axios from 'axios'
@@ -19,7 +24,7 @@ import moment from 'moment'
 import chalk from 'chalk'
 
 import { VoteKind } from '@joystream/types/proposals'
-import { EventRecord } from '@polkadot/types/interfaces'
+import { Seats } from '@joystream/types/council'
 import { AccountInfo } from '@polkadot/types/interfaces/system'
 import {
   Api,
@@ -36,10 +41,20 @@ import {
   Status,
 } from '../types'
 
-import { AccountId, Moment, ActiveEraInfo } from '@polkadot/types/interfaces'
+import {
+  AccountId,
+  Moment,
+  ActiveEraInfo,
+  EventRecord,
+} from '@polkadot/types/interfaces'
 import Option from '@polkadot/types/codec/Option'
 import { Vec } from '@polkadot/types'
 
+// TODO fetch consts from db/chain
+const TERMDURATION = 144000
+const VOTINGDURATION = 57601
+const CYCLE = VOTINGDURATION + TERMDURATION
+
 const DELAY = 0 // ms
 let lastUpdate = 0
 let queuedAll = false
@@ -53,12 +68,22 @@ const getBlockHash = (api: Api, blockId: number) =>
 const getEraAtBlock = (api: Api, hash: string) =>
   api.query.staking.activeEra.at(hash)
 
+const getTimestamp = async (api: Api, hash?: string) =>
+  moment
+    .utc(
+      hash
+        ? await api.query.timestamp.now.at(hash)
+        : await api.query.timestamp.now()
+    )
+    .valueOf()
+
 const addBlock = async (
   api: Api,
   io: any,
   header: { number: number; author: string },
   status: Status = {
     era: 0,
+    round: 0,
     members: 0,
     channels: 0,
     categories: 0,
@@ -75,28 +100,28 @@ const addBlock = async (
     console.error(`TODO handle fork`, String(header.author))
     return status
   }
-  const timestamp = moment.utc(await api.query.timestamp.now()).valueOf()
+  const timestamp = await getTimestamp(api)
   const blocktime = last ? timestamp - last.timestamp : 6000
+  const address = header.author?.toString()
+  const account = await Account.findOrCreate({ where: { address } })
   const block = await Block.create({ id, timestamp, blocktime })
-  io.emit('block', block)
-
-  const author = header.author?.toString()
-  const member = await fetchMemberByAccount(api, author)
-  if (member && member.id) block.setAuthor(member.id)
-  updateBalances(api, id)
+  block.setValidator(account.id)
 
   const currentEra = Number(await api.query.staking.currentEra())
-  Era.findOrCreate({ where: { id: currentEra } }).then(() =>
-    block.setEra(currentEra)
-  )
+  const era = await Era.findOrCreate({ where: { id: currentEra } })
+  await block.setEra(currentEra)
+  io.emit('block', await Block.findByIdWithIncludes(block.id))
 
-  const handle = member ? member.handle : author
+  // logging
+  const member = await fetchMemberByAccount(api, address)
+  const handle = member ? member.handle : address
   const f = fetching !== '' ? `, fetching ${fetching}` : ''
   const q = queue.length ? ` (${queue.length} queued${f})` : ''
   console.log(`[Joystream] block ${block.id} ${handle}${q}`)
 
   processEvents(api, id)
   //updateEra(api, io, status, currentEra)
+  //updateBalances(api, id)
   return updateStatus(api, status, currentEra)
 }
 
@@ -105,13 +130,13 @@ const addBlockRange = async (
   startBlock: number,
   endBlock: number
 ) => {
-  const previousHash = await api.rpc.chain.getBlockHash(startBlock - 1)
+  const previousHash = await getBlockHash(api, startBlock - 1)
   let previousEra = (await api.query.staking.activeEra.at(
     previousHash
   )) as Option<ActiveEraInfo>
 
   for (let i = startBlock; i < endBlock; i++) {
-    const hash = await api.rpc.chain.getBlockHash(i)
+    const hash = await getBlockHash(api, i)
     const blockEra = (await api.query.staking.activeEra.at(
       hash
     )) as Option<ActiveEraInfo>
@@ -155,6 +180,7 @@ const addBlockRange = async (
 const updateStatus = async (api: Api, old: Status, era: number) => {
   const status = {
     era,
+    round: Number(await api.query.councilElection.round()),
     members: (await api.query.members.nextMemberId()) - 1,
     channels: await get.currentChannelId(api),
 
@@ -173,6 +199,8 @@ const updateStatus = async (api: Api, old: Status, era: number) => {
     status.proposals > old.proposals && fetchProposal(api, status.proposals)
     status.channels > old.channels && fetchChannel(api, status.channels)
     status.categories > old.categories && fetchCategory(api, status.categories)
+    status.proposalPosts > old.proposalPosts &&
+      fetchProposalPosts(api, status.proposalPosts)
   }
   return status
 }
@@ -183,18 +211,28 @@ const fetchAll = async (api: Api, status: Status) => {
   for (let id = status.members; id > 0; id--) {
     queue.push(() => fetchMember(api, id))
   }
-  for (let id = status.posts; id > 0; id--) {
-    queue.push(() => fetchPost(api, id))
+  for (let id = status.round; id > 0; id--) {
+    queue.push(() => fetchCouncil(api, id))
   }
+
   for (let id = status.proposals; id > 0; id--) {
     queue.push(() => fetchProposal(api, id))
   }
+  queue.push(() => fetchProposalPosts(api, status.proposalPosts))
+
   for (let id = status.channels; id > 0; id--) {
     queue.push(() => fetchChannel(api, id))
   }
   for (let id = status.categories; id > 0; id--) {
     queue.push(() => fetchCategory(api, id))
   }
+  for (let id = status.threads; id > 0; id--) {
+    queue.push(() => fetchThread(api, id))
+  }
+  for (let id = status.posts; id > 0; id--) {
+    queue.push(() => fetchPost(api, id))
+  }
+
   queuedAll = true
   processNext()
 }
@@ -202,7 +240,7 @@ const fetchAll = async (api: Api, status: Status) => {
 const processNext = async () => {
   if (processing) return
   processing = true
-  const task = queue.pop()
+  const task = queue.shift()
   if (!task) return
   const result = await task()
   processing = false
@@ -392,8 +430,14 @@ const fetchPost = async (api: Api, id: number) => {
   const thread = await fetchThread(api, threadId)
   if (thread) post.setThread(thread.id)
   const member = await fetchMemberByAccount(api, author)
-  if (member) post.setAuthor(member.id)
-  const mod = await fetchMemberByAccount(api, moderation)
+  if (member) {
+    post.setAuthor(member.id)
+    member.addPost(post.id)
+  }
+  if (moderation) {
+    const mod = await fetchMemberByAccount(api, moderation)
+    post.setModerator(mod)
+  }
   return post
 }
 
@@ -416,7 +460,7 @@ const fetchThread = async (api: Api, id: number) => {
   const category = await fetchCategory(api, +data.category_id)
   if (category) thread.setCategory(category.id)
   const author = await fetchMemberByAccount(api, account)
-  if (author) thread.setAuthor(author.id)
+  if (author) thread.setCreator(author.id)
   if (moderation) {
     /* TODO
   Error: Invalid value ModerationAction(3) [Map] {
@@ -431,92 +475,147 @@ const fetchThread = async (api: Api, id: number) => {
 [1]   'moderator_id'
 [1]   'rationale' => [String (Text): 'Irrelevant as posted in another thread.'] {
 */
+    //console.log(`thread mod`, moderation
     //const mod = await fetchMemberByAccount(api, moderation)
     //if (mod) thread.setModeration(mod.id)
   }
   return thread
 }
 
-const fetchCouncils = async (api: Api, lastBlock: number) => {
-  const round = await api.query.councilElection.round()
-  let councils: CouncilType[] = await Council.findAll()
-  const cycle = 201600
+const fetchCouncil = async (api: Api, round: number) => {
+  if (round <= 0) return console.log(chalk.red(`[fetchCouncil] round:${round}`))
+
+  const exists = await Council.findByPk(round)
+  if (exists) return exists
+
+  fetching = `council ${round}`
+  const start = 57601 + (round - 1) * CYCLE
+  const end = start + TERMDURATION
+  let council = { round, start, end, startDate: 0, endDate: 0 }
+  let seats: Seats
+  try {
+    const startHash = await getBlockHash(api, start)
+    council.startDate = await getTimestamp(api, startHash)
+    seats = await api.query.council.activeCouncil.at(startHash)
+  } catch (e) {
+    return console.log(`council term ${round} lies in the future ${start}`)
+  }
 
-  for (let round = 0; round < round; round++) {
-    const block = 57601 + round * cycle
-    if (councils.find((c) => c.round === round) || block > lastBlock) continue
-    fetchCouncil(api, block)
+  try {
+    const endHash = await getBlockHash(api, end)
+    council.endDate = await getTimestamp(api, endHash)
+  } catch (e) {
+    console.warn(`end of council term ${round} lies in the future ${end}`)
   }
-}
 
-const fetchCouncil = async (api: Api, block: number) => {
-  fetching = `council at block ${block}`
-  const blockHash = await api.rpc.chain.getBlockHash(block)
-  if (!blockHash)
-    return console.error(`Error: empty blockHash fetchCouncil ${block}`)
-  const council = await api.query.council.activeCouncil.at(blockHash)
-  return Council.create(council)
+  try {
+    Council.create(council).then(({ round }: any) =>
+      seats.map(({ member, stake, backers }) =>
+        fetchMemberByAccount(api, member.toHuman()).then((m: any) =>
+          Consul.create({
+            stake: Number(stake),
+            councilRound: round,
+            memberId: m.id,
+          }).then((consul: any) =>
+            backers.map(async ({ member, stake }) =>
+              fetchMemberByAccount(api, member.toHuman()).then(({ id }: any) =>
+                ConsulStake.create({
+                  stake: Number(stake),
+                  consulId: consul.id,
+                  memberId: id,
+                })
+              )
+            )
+          )
+        )
+      )
+    )
+  } catch (e) {
+    console.error(`Failed to save council ${round}`, e)
+  }
 }
 
 const fetchProposal = async (api: Api, id: number) => {
   if (id <= 0) return
   const exists = await Proposal.findByPk(+id)
-  if (exists) return exists
-
-  //if (exists && exists.stage === 'Finalized')
-  //if (exists.votesByAccount && exists.votesByAccount.length) return
-  //else return //TODO fetchVotesPerProposal(api, exists)
+  if (exists) {
+    fetchProposalVotes(api, exists)
+    return exists
+  }
 
   fetching = `proposal ${id}`
   const proposal = await get.proposalDetail(api, id)
+  fetchProposalVotes(api, proposal)
   return Proposal.create(proposal)
-  //TODO fetchVotesPerProposal(api, proposal)
 }
 
-const fetchVotesPerProposal = async (api: Api, proposal: ProposalDetail) => {
-  if (proposal.votesByAccount && proposal.votesByAccount.length) return
-
-  const proposals = await Proposal.findAll()
-  const councils = await Council.findAll()
+const fetchProposalPosts = async (api: Api, max: number) => {
+  console.log(`posts`, max)
+  let postId = 1
+  for (let threadId = 1; postId <= max; threadId++) {
+    fetching = `proposal posts ${threadId} ${postId}`
+    const post = await api.query.proposalsDiscussion.postThreadIdByPostId(
+      threadId,
+      postId
+    )
+    if (post.text.length) {
+      console.log(postId, threadId, post.text.toHuman())
+      postId++
+    }
+  }
+}
 
-  fetching = `proposal votes (${proposal.id})`
-  let members: MemberType[] = []
-  councils.map((seats: Seat[]) =>
-    seats.forEach(async (seat: Seat) => {
-      if (members.find((member) => member.account === seat.member)) return
-      const member = await Member.findOne({ where: { account: seat.member } })
-      member && members.push(member)
-    })
-  )
+const findCouncilAtBlock = (api: Api, block: number) =>
+  Council.findOne({
+    where: {
+      start: { [Op.lte]: block },
+      end: { [Op.gte]: block - VOTINGDURATION },
+    },
+  })
 
-  const { id } = proposal
-  const votesByAccount = await Promise.all(
-    members.map(async (member) => {
-      const vote = await fetchVoteByProposalByVoter(api, id, member.id)
-      return { vote, handle: member.handle }
+const fetchProposalVotes = async (api: Api, proposal: ProposalDetail) => {
+  if (!proposal) return console.error(`[fetchProposalVotes] empty proposal`)
+  fetching = `votes proposal ${proposal.id}`
+  const { createdAt } = proposal
+  if (!createdAt) return console.error(`empty start block`, proposal)
+  try {
+    const start = await findCouncilAtBlock(api, createdAt)
+    if (start) start.addProposal(proposal.id)
+    else return console.error(`no council found for proposal ${proposal.id}`)
+    // some proposals make it into a second term
+    const end = await findCouncilAtBlock(api, proposal.finalizedAt)
+    const councils = [start.round, end && end.round]
+    const consuls = await Consul.findAll({
+      where: { councilRound: { [Op.or]: councils } },
     })
-  )
-  Proposal.findByPk(id).then((p: any) => p.update({ votesByAccount }))
+    consuls.map(({ id, memberId }: any) =>
+      fetchProposalVoteByConsul(api, proposal.id, id, memberId)
+    )
+  } catch (e) {
+    console.log(`failed to fetch votes of proposal ${proposal.id}`, e)
+  }
 }
 
-const fetchVoteByProposalByVoter = async (
+const fetchProposalVoteByConsul = async (
   api: Api,
   proposalId: number,
-  voterId: number
-): Promise<string> => {
-  fetching = `vote by ${voterId} for proposal ${proposalId}`
-  const vote: VoteKind = await api.query.proposalsEngine.voteExistsByProposalByVoter(
-    proposalId,
-    voterId
-  )
-  const hasVoted: number = (
-    await api.query.proposalsEngine.voteExistsByProposalByVoter.size(
-      proposalId,
-      voterId
-    )
-  ).toNumber()
+  consulId: number,
+  memberId: number
+): Promise<any> => {
+  fetching = `vote by ${consulId} for proposal ${proposalId}`
+  const exists = await ProposalVote.findOne({
+    where: { proposalId, memberId, consulId },
+  })
+  if (exists) return exists
+
+  const query = api.query.proposalsEngine
+  const args = [proposalId, memberId]
+
+  const hasVoted = await query.voteExistsByProposalByVoter.size(...args)
+  if (!hasVoted.toNumber()) return
 
-  return hasVoted ? String(vote) : ''
+  const vote = (await query.voteExistsByProposalByVoter(...args)).toHuman()
+  return ProposalVote.create({ vote: vote, proposalId, consulId, memberId })
 }
 
 // accounts
@@ -524,9 +623,12 @@ const fetchMemberByAccount = async (
   api: Api,
   account: string
 ): Promise<MemberType | undefined> => {
+  if (!account) {
+    console.error(`fetchMemberByAccount called without account`)
+    return undefined
+  }
   const exists = await Member.findOne({ where: { account } })
   if (exists) return exists
-
   const id: number = Number(await get.memberIdByAccount(api, account))
   return id ? fetchMember(api, id) : undefined
 }
@@ -541,11 +643,11 @@ const fetchMember = async (
 
   fetching = `member ${id}`
   const membership = await get.membership(api, id)
-  const handle = String(membership.handle)
-  const account = String(membership.root_account)
   const about = String(membership.about)
+  const account = String(membership.root_account)
+  const handle = String(membership.handle)
   const createdAt = +membership.registered_at_block
-  return Member.create({ id, handle, createdAt, about })
+  return Member.create({ id, about, account, createdAt, handle })
 }
 
 module.exports = { addBlock, addBlockRange }

+ 1 - 1
server/joystream/lib/getters.ts

@@ -149,7 +149,7 @@ export const proposalDetail = async (
     stage,
     result,
     exec,
-    description,
+    description: description.toHuman(),
     votes: votingResults,
     type,
     author,

+ 2 - 2
server/joystream/ws.ts

@@ -9,11 +9,11 @@ const wsLocation =
 
 const connectUpstream = async (): Promise<ApiPromise> => {
     try {
-        console.debug(`[Joystream] Connecting to ${wsLocation}`)
+        //console.debug(`[Joystream] Connecting to ${wsLocation}`)
         const provider = new WsProvider(wsLocation)
         const api = await ApiPromise.create({ provider, types })
         await api.isReady
-        console.debug(`[Joystream] Connected.`)
+        console.debug(`[Joystream] Connected to ${wsLocation}`)
         return api
     } catch (e) {
         console.error(`[Joystream] upstream connection failed`, e)

+ 3 - 1
server/types.ts

@@ -105,7 +105,7 @@ export interface ProposalDetail {
   votes: VotingResults
   type: string
   votesByAccount?: Vote[]
-  author: string
+  author?: string
   authorId: number
 }
 
@@ -184,6 +184,7 @@ export interface MemberType {
   id: number
   registeredAt: number
   about: string
+  addPost: any
 }
 
 export interface Header {
@@ -266,6 +267,7 @@ export interface CalendarGroup {
 
 export interface Status {
   era: number
+  round: number
   members: number
   channels: number
   categories: number