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.inflight
API, 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);
};