Browse Source

add task search apis

Aneurin Barker Snook 1 year ago
parent
commit
bf40fcf5de
4 changed files with 70 additions and 3 deletions
  1. 2 0
      src/http.ts
  2. 62 1
      src/task/api.ts
  3. 2 0
      src/task/model.ts
  4. 4 2
      src/task/types.ts

+ 2 - 0
src/http.ts

@@ -28,6 +28,8 @@ export function createExpress(ctx: Context) {
   app.delete(`${prefix}/herd/:id`, herd.deleteHerd(ctx))
 
   // Task APIs
+  app.get(`${prefix}/tasks`, task.searchTasks(ctx))
+  app.get(`${prefix}/herd/:herd/tasks`, task.searchTasks(ctx))
   app.post(`${prefix}/task`, task.createTask(ctx))
   app.get(`${prefix}/task/:id`, task.getTask(ctx))
   app.put(`${prefix}/task/:id`, task.updateTask(ctx))

+ 62 - 1
src/task/api.ts

@@ -1,9 +1,10 @@
 import type { AuthRequestHandler } from '../auth'
 import type { Context } from '../types'
 import { ObjectId } from 'mongodb'
+import type { SearchResult } from '../api'
 import type { WithId } from 'mongodb'
-import { validate as v } from '@edge/misc-utils'
 import type { Task, TaskCreate, TaskUpdate } from './types'
+import { query, validate as v } from '@edge/misc-utils'
 import { sendBadRequest, sendForbidden, sendNotFound, sendUnauthorized } from '../http'
 
 /** Create a task. */
@@ -21,6 +22,7 @@ export function createTask({ model }: Context): AuthRequestHandler {
       _herd: v.seq(v.str, v.exactLength(24)),
       _account: v.seq(v.str, v.exactLength(24)),
       description: v.seq(v.str, v.minLength(1)),
+      position: v.seq(v.numeric, v.min(1)),
     },
   })
 
@@ -113,6 +115,64 @@ export function getTask({ model }: Context): AuthRequestHandler {
   }
 }
 
+
+/** Search tasks. */
+export function searchTasks({ model }: Context): AuthRequestHandler {
+  type ResponseData = SearchResult<{
+    task: WithId<Task>
+  }>
+
+  return async function (req, res, next) {
+    if (!req.account) return sendUnauthorized(res, next)
+
+    // Read parameters
+    const herd = req.params.herd || undefined
+    const limit = query.integer(req.query.limit, 1, 100) || 10
+    const page = query.integer(req.query.page, 1) || 1
+    const search = query.str(req.query.search)
+    const sort = query.sorts(req.query.sort, ['description', 'position'], ['position', 'ASC'])
+
+    // Build filters and skip
+    const filter: Record<string, unknown> = {
+      _account: req.account._id,
+    }
+    if (herd) {
+      // Add herd filter if set.
+      // We don't need to verify access as it is implicitly asserted by the _account filter
+      try {
+        filter._herd = new ObjectId(herd)
+      } catch (err) {
+        return sendBadRequest(res, next, { reason: 'invalid herd' })
+      }
+    }
+    if (search) filter.$text = { $search: search }
+    const skip = (page - 1) * limit
+
+    try {
+      // Get total documents count for filter
+      const totalCount = await model.herd.collection.countDocuments(filter)
+
+      // Build cursor
+      let cursor = model.task.collection.find(filter)
+      for (const [prop, dir] of sort) {
+        cursor = cursor.sort(prop, dir === 'ASC' ? 1 : -1)
+      }
+      cursor = cursor.skip(skip).limit(limit)
+
+      // Get results and send output
+      const data = await cursor.toArray()
+      const output: ResponseData = {
+        results: data.map(task => ({ task })),
+        metadata: { limit, page, totalCount },
+      }
+      res.send(output)
+      next()
+    } catch (err) {
+      next(err)
+    }
+  }
+}
+
 /** Update a task. */
 export function updateTask({ model }: Context): AuthRequestHandler {
   interface RequestData {
@@ -128,6 +188,7 @@ export function updateTask({ model }: Context): AuthRequestHandler {
       _herd: v.seq(v.optional, v.str, v.exactLength(24)),
       _account: v.seq(v.optional, v.str, v.exactLength(24)),
       description: v.seq(v.optional, v.str, v.minLength(1)),
+      position: v.seq(v.optional, v.numeric, v.min(1)),
     },
   })
 

+ 2 - 0
src/task/model.ts

@@ -15,6 +15,7 @@ async function createTaskModel(ctx: Context) {
       _herd: input._herd,
       _account: input._account,
       description: input.description,
+      position: input.position,
     })
 
     return await collection.findOne({ _id: result.insertedId })
@@ -33,6 +34,7 @@ async function createTaskModel(ctx: Context) {
     if (input._herd) changes._herd = input._herd
     if (input._account) changes._account = input._account
     if (input.description) changes.description = input.description
+    if (input.position !== undefined) changes.position = input.position
 
     return collection.findOneAndUpdate(
       { _id: new ObjectId(id) },

+ 4 - 2
src/task/types.ts

@@ -8,10 +8,12 @@ export interface Task<T extends ObjectId | string = ObjectId> {
   _account: T
   /** Description. */
   description: string
+  /** Position in herd. */
+  position: number
 }
 
 /** Subset of task data when creating a new task. */
-export type TaskCreate<T extends ObjectId | string = ObjectId> = Pick<Task<T>, '_herd' | '_account' | 'description'>
+export type TaskCreate<T extends ObjectId | string = ObjectId> = Pick<Task<T>, '_herd' | '_account' | 'description' | 'position'>
 
 /** Subset of task data when updating a task. */
-export type TaskUpdate<T extends ObjectId | string = ObjectId> = Partial<Pick<Task<T>, '_herd' | '_account' | 'description'>>
+export type TaskUpdate<T extends ObjectId | string = ObjectId> = Partial<Pick<Task<T>, '_herd' | '_account' | 'description' | 'position'>>