User Story 15 - Task List with Api Gateway and Redis
Status: Draft, Expected released on 2023/03/16
The following code is an initial implementation of TaskList with api gateway and a redis db
New language and SDK features it introduces
-  bring untyped
- bring external npm package (axios)
- bring an internal nodejs stdlib (RegEx)
- How does untyped works with numeric operations?
- Do we cast untyped?
 
-  enum & duration that can be included inside json(Json should not include these)
-  It leverages setting explicit permissions (using the this.inflightAPI, described here)
- Using interface
- use redis instead of bucket
- code that updates estimation and duration from REST put command
- console requirements
- Optionality ? and ??
- redis is packaged inside our SDK
-  I wanted to use ioredis as an inflight member but there are 2 issues:
- Missing inflight init (used lazy getter style method instead)
- ioredis is from type any (redis.IRedisClient)
 
Developer Experience
This section focuses on the develoepr experience
Redis on localhost
There is a range of possibilities here:
One option is to have a a complete battery included solution that handle installing and running redis instance w/wo docker.
Another option is taking the do-it-yourself approach that require the developer to setup the instance listening on a configured port.
Code
tasklist.w
bring cloud;
bring redis;
let EMPTY_JSON = Json { empty: "https://github.com/winglang/wing/issues/1947" };
interface IMyRegExp {
  inflight test(s: str): bool;
}
enum Status {
  UNCOMPLETED,
  COMPLETED
}
interface ITaskList {
  inflight get(id: str): Json;
  inflight add(title: str): str;
  inflight remove(id: str); 
  inflight find(r: IMyRegExp): Array<str>;
  inflight set_status(id: str, status: Status): str;
}
resource TaskList impl ITaskList {
  _redis: redis.Redis;
  
  extern "./tasklist_helper.js" static inflight uuid(): str; 
  // Workaround for https://github.com/winglang/wing/issues/1669 - changed method to be non-static
  extern "./tasklist_helper.js" inflight get_data(url: str): Json;
  extern "./tasklist_helper.js" inflight create_regex(s: str): IMyRegExp;
  init() {
    this._redis = new redis.Redis();
  }
  
  inflight get(id: str): Json {
    return Json.parse(this._redis.get(id) ?? "");
  }
  
  inflight _add(id: str, j: Json): str {
    this._redis.set(id , Json.stringify(j));
    this._redis.sadd("todo", id);
    return id;
  } 
    
  inflight add(title: str): str {
    let id = TaskList.uuid();
    let j = Json { 
      title: title, 
      status: "uncompleted"
      };
    log("adding task ${id} with data: ${j}"); 
    return this._add(id, j);
  }
      
  inflight remove(id: str) {
    log("removing task ${id}");
    this._redis.del(id);
  }
      
  inflight find(r: IMyRegExp): Array<str> { 
    let result = MutArray<str>[]; 
    let ids = this._redis.smembers("todo");
    for id in ids {
      let j = Json.parse(this._redis.get(id) ?? "");
      if r.test(str.from_json(j.get("title"))) {
        result.push(id);
      }
    }
    return result.copy();
  }
      
  inflight set_status(id: str, status: Status): str {
    let j = Json.deepCopyMut(this.get(id));
    if status == Status.COMPLETED {
      j.set("status", "completed");
    } else {
      j.set("status", "uncompleted");
    }
    this._add(id, Json.deepCopyMut(j));
    return id;
  }
        
}
      
resource TaskListApi {
  api: cloud.Api;
  task_list: TaskList;
        
  init(task_list: TaskList) {
    this.task_list = task_list;
    this.api = new cloud.Api();
        
    this.api.post("/tasks", inflight (req: cloud.ApiRequest): cloud.ApiResponse => {
      let body = req.body ?? EMPTY_JSON;
      let var title = str.from_json(body.get("title"));
      // Easter Egg - if you add a todo with the single word "random" as the title, 
      //              the system will fetch a random task from the internet
      if title == "random" {
        // Workaround for https://github.com/winglang/wing/issues/1969 - calling task_list directly instead of via `this.`
        let data: Json = task_list.get_data("https://www.boredapi.com/api/activity");
        title = str.from_json(data.get("activity")); 
      } 
      let id = task_list.add(title);
      return cloud.ApiResponse { status:201, body: id };
    });
        
    this.api.put("/tasks/{id}", inflight (req: cloud.ApiRequest): cloud.ApiResponse => {
      let vars = req.vars ?? Map<str>{};
      let body = req.body ?? EMPTY_JSON;
      let id = str.from_json(vars.get("id"));
      if bool.from_json(body.get("completed")) {
        task_list.set_status(id, Status.COMPLETED);
      } else {
        task_list.set_status(id, Status.UNCOMPLETED);
      }
      try {
        let title = task_list.get(id);
        return cloud.ApiResponse { status:200, body: title };
      } catch {
        return cloud.ApiResponse { status: 400 };
      }
    });
    this.api.get("/tasks/{id}", inflight (req: cloud.ApiRequest): cloud.ApiResponse => {
      let vars = req.vars ?? Map<str>{};
      let id = str.from_json(vars.get("id"));
      try {
        let title = task_list.get(id);
        return cloud.ApiResponse { status:200, body: title };
      } catch {
        return cloud.ApiResponse { status: 400 };
      }
    });
    
    this.api.delete("/tasks/{id}", inflight (req: cloud.ApiRequest): cloud.ApiResponse => {
      let vars = req.vars ?? Map<str>{};
      let id = str.from_json(vars.get("id"));
      try {
        task_list.remove(id);
        return cloud.ApiResponse { status: 204 };
      } catch {
        return cloud.ApiResponse { status: 400 };
      }
    });
    this.api.get("/tasks", inflight (req: cloud.ApiRequest): cloud.ApiResponse => {
      let search = req.query.get("search");
      let results = task_list.find(task_list.create_regex(search));
      return cloud.ApiResponse { status: 200, body: results };
    });
  }
}
let task_list = new TaskList();
let t = new TaskListApi(task_list);
tasklist_helper.js
const axios = require("axios");
exports.get_data = async function(url) {
  const response = await axios.get(url);
  return response.data; // returns a JSON
};
exports.create_regex = function (s) {
  return new RegExp(s);
};
exports.uuid = function () {
  return "" + Math.floor(Math.random() * 100000000000);
};