Browse Source

add herd model, apis

Aneurin Barker Snook 1 year ago
parent
commit
e6ddc632d1
6 changed files with 251 additions and 1 deletions
  1. 3 1
      src/account/model.ts
  2. 4 0
      src/db.ts
  3. 165 0
      src/herd/api.ts
  4. 57 0
      src/herd/model.ts
  5. 15 0
      src/herd/types.ts
  6. 7 0
      src/http.ts

+ 3 - 1
src/account/model.ts

@@ -39,7 +39,9 @@ async function createAccountModel(ctx: Context) {
 
   /** Initialize the account collection. */
   async function init() {
-    const exists = await collection.indexExists('unq_email')
+    await ctx.db.createCollection('account')
+
+    const exists = await collection.indexExists('email_1')
     if (!exists) {
       await collection.createIndex({ email: 1 }, { unique: true })
     }

+ 4 - 0
src/db.ts

@@ -1,7 +1,9 @@
 import type { AccountModel } from './account/model'
 import type { Context } from './types'
+import type { HerdModel } from './herd/model'
 import { MongoClient } from 'mongodb'
 import createAccountModel from './account/model'
+import createHerdModel from './herd/model'
 
 /**
  * Models context.
@@ -9,6 +11,7 @@ import createAccountModel from './account/model'
  */
 export interface Models {
   account: AccountModel
+  herd: HerdModel
 }
 
 /** Create a MongoDB connection and initialize models. */
@@ -22,6 +25,7 @@ async function createDatabase(ctx: Context) {
   const dbCtx = { ...ctx, mongo, db }
   const model = <Models>{
     account: await createAccountModel(dbCtx),
+    herd: await createHerdModel(dbCtx),
   }
 
   return { mongo, db, model }

+ 165 - 0
src/herd/api.ts

@@ -0,0 +1,165 @@
+import * as v from '../validate'
+import type { AuthRequestHandler } from '../auth'
+import type { Context } from '../types'
+import { ObjectId } from 'mongodb'
+import type { WithId } from 'mongodb'
+import type { Herd, HerdCreate, HerdUpdate } from './types'
+import { sendBadRequest, sendForbidden, sendNotFound, sendUnauthorized } from '../http'
+
+/**
+ * Create a herd.
+ * The request must be verified, and the user may only create a herd in their own account.
+ */
+export function createHerd({ model }: Context): AuthRequestHandler {
+  interface RequestData {
+    herd: Omit<HerdCreate, '_account'>
+  }
+
+  interface ResponseData {
+    herd: WithId<Herd>
+  }
+
+  const readRequestData = v.validate<RequestData>({
+    herd: {
+      name: v.minLength(1),
+    },
+  })
+
+  return async function (req, res, next) {
+    if (!req.verified) return sendUnauthorized(res, next)
+
+    try {
+      const account = await model.account.collection.findOne({ _id: req.accountId })
+      if (!account) return sendUnauthorized(res, next)
+
+      const input = readRequestData(req.body)
+
+      const herd = await model.herd.create({ ...input.herd, _account: req.accountId as ObjectId })
+      if (!herd) return sendNotFound(res, next, { reason: 'unexpectedly failed to get new herd' })
+
+      const output: ResponseData = { herd }
+      res.send(output)
+      next()
+    } catch (err) {
+      const name = (err as Error).name
+      if (name === 'ValidateError') {
+        const ve = err as v.ValidateError
+        return sendBadRequest(res, next, { param: ve.param, reason: ve.message })
+      }
+      return next(err)
+    }
+  }
+}
+
+/**
+ * Delete a herd.
+ * The request must be verified, and the user may not modify any herd other than their own.
+ */
+export function deleteHerd({ model }: Context): AuthRequestHandler {
+  interface ResponseData {
+    herd: WithId<Herd>
+  }
+
+  return async function (req, res, next) {
+    if (!req.verified) return sendUnauthorized(res, next)
+
+    const id = req.params.id
+    if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
+
+    try {
+      const herd = await model.herd.collection.findOne({ _id: new ObjectId(id) })
+      if (!herd) return sendNotFound(res, next)
+      if (!req.accountId?.equals(herd._account)) return sendForbidden(res, next)
+
+      /** @todo delete related data */
+      await model.herd.collection.deleteOne({ _id: new ObjectId(id) })
+
+      const output: ResponseData = { herd }
+      res.send(output)
+      next()
+    } catch (err) {
+      next(err)
+    }
+  }
+}
+
+/**
+ * Get a herd.
+ * The request must be verified, and the user may not access any herd other than their own.
+ */
+export function getHerd({ model }: Context): AuthRequestHandler {
+  interface ResponseData {
+    herd: WithId<Herd>
+  }
+
+  return async function (req, res, next) {
+    if (!req.verified) return sendUnauthorized(res, next)
+
+    const id = req.params.id || req.accountId
+    if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
+
+    try {
+      const herd = await model.herd.collection.findOne({ _id: new ObjectId(id) })
+      if (!herd) return sendNotFound(res, next)
+      if (!req.accountId?.equals(herd._account)) return sendForbidden(res, next)
+
+      const output: ResponseData = { herd }
+      res.send(output)
+      next()
+    } catch (err) {
+      return next(err)
+    }
+  }
+}
+
+/**
+ * Update a herd.
+ * The request must be verified, and the user may not modify any herd other than their own.
+ */
+export function updateHerd({ model }: Context): AuthRequestHandler {
+  interface RequestData {
+    herd: HerdUpdate
+  }
+
+  interface ResponseData {
+    herd: WithId<Herd>
+  }
+
+  const readRequestData = v.validate<RequestData>({
+    herd: {
+      name: v.minLength(1),
+    },
+  })
+
+  return async function (req, res, next) {
+    if (!req.verified) return sendUnauthorized(res, next)
+
+    const id = req.params.id || req.accountId
+    if (!id) return sendBadRequest(res, next, { reason: 'no ID' })
+
+    try {
+      const input = readRequestData(req.body)
+      if (!input.herd.name) {
+        return sendBadRequest(res, next, { reason: 'no changes' })
+      }
+
+      let herd = await model.herd.collection.findOne({ _id: new ObjectId(id) })
+      if (!herd) return sendNotFound(res, next)
+      if (!req.accountId?.equals(herd._account)) return sendForbidden(res, next)
+
+      herd = await model.herd.update(id, input.herd)
+      if (!herd) return sendNotFound(res, next)
+
+      const output: ResponseData = { herd }
+      res.send(output)
+      next()
+    } catch (err) {
+      const name = (err as Error).name
+      if (name === 'ValidateError') {
+        const ve = err as v.ValidateError
+        return sendBadRequest(res, next, { param: ve.param, reason: ve.message })
+      }
+      return next(err)
+    }
+  }
+}

+ 57 - 0
src/herd/model.ts

@@ -0,0 +1,57 @@
+import type { Context } from '../types'
+import { ObjectId } from 'mongodb'
+import type { Herd, HerdCreate, HerdUpdate } from './types'
+
+/** Model for accessing and managing herds. */
+export type HerdModel = Awaited<ReturnType<typeof createHerdModel>>
+
+/** Create a herd model. */
+async function createHerdModel(ctx: Context) {
+  const collection = ctx.db.collection<Herd>('herd')
+
+  /** Create a herd. */
+  async function create(input: HerdCreate) {
+    const result = await collection.insertOne({
+      _account: input._account,
+      name: input.name,
+    })
+
+    return await collection.findOne({ _id: result.insertedId })
+  }
+
+  /** Initialize the herd collection. */
+  async function init() {
+    await ctx.db.createCollection('herd')
+
+    const exists = await collection.indexExists('_account_1_name_1')
+    if (!exists) {
+      await collection.createIndex({ _account: 1, name: 1 }, { unique: true })
+    }
+  }
+
+  /**
+   * Update a herd.
+   */
+  async function update(id: ObjectId | string, input: HerdUpdate) {
+    const changes = <Partial<Herd>>{}
+    if (input.name) changes.name = input.name
+
+    return collection.findOneAndUpdate(
+      { _id: new ObjectId(id) },
+      { $set: changes },
+      { returnDocument: 'after' },
+    )
+  }
+
+  // Initialize on startup
+  await init()
+
+  return {
+    collection,
+    create,
+    init,
+    update,
+  }
+}
+
+export default createHerdModel

+ 15 - 0
src/herd/types.ts

@@ -0,0 +1,15 @@
+import type { ObjectId } from 'mongodb'
+
+/** Herd data. */
+export interface Herd {
+  /** Account ID. */
+  _account: ObjectId
+  /** Name. */
+  name: string
+}
+
+/** Subset of herd data when creating a new herd. */
+export type HerdCreate = Pick<Herd, '_account' | 'name'>
+
+/** Subset of herd data when updating a herd. */
+export type HerdUpdate = Partial<Pick<Herd, 'name'>>

+ 7 - 0
src/http.ts

@@ -1,4 +1,5 @@
 import * as account from './account/api'
+import * as herd from './herd/api'
 import type { Context } from './types'
 import express from 'express'
 import type { ErrorRequestHandler, NextFunction, Response } from 'express'
@@ -28,6 +29,12 @@ export function createExpress(ctx: Context) {
   app.put(`${prefix}/account/:id?`, account.updateAccount(ctx))
   app.delete(`${prefix}/account/:id?`, account.deleteAccount(ctx))
 
+  // Herd APIs
+  app.post(`${prefix}/herd`, herd.createHerd(ctx))
+  app.get(`${prefix}/herd/:id?`, herd.getHerd(ctx))
+  app.put(`${prefix}/herd/:id?`, herd.updateHerd(ctx))
+  app.delete(`${prefix}/herd/:id?`, herd.deleteHerd(ctx))
+
   // Authentication APIs
   app.post(`${prefix}/login/account`, account.loginAccount(ctx))