Added pagination.
This commit is contained in:
parent
3fa2938f48
commit
33089b3b75
|
@ -39,12 +39,23 @@ PathHandler mapApiHandlers() {
|
|||
a.addMapping(Method.DELETE, PROFILE_PATH, &handleDeleteProfile);
|
||||
a.addMapping(Method.GET, PROFILE_PATH ~ "/properties", &handleGetProperties);
|
||||
|
||||
// Account endpoints:
|
||||
import account.api;
|
||||
a.addMapping(Method.GET, PROFILE_PATH ~ "/accounts", &handleGetAccounts);
|
||||
a.addMapping(Method.POST, PROFILE_PATH ~ "/accounts", &handleCreateAccount);
|
||||
a.addMapping(Method.GET, PROFILE_PATH ~ "/accounts/:accountId:ulong", &handleGetAccount);
|
||||
a.addMapping(Method.DELETE, PROFILE_PATH ~ "/accounts/:accountId:ulong", &handleDeleteAccount);
|
||||
|
||||
import transaction.api;
|
||||
// Transaction vendor endpoints:
|
||||
a.addMapping(Method.GET, PROFILE_PATH ~ "/vendors", &getVendors);
|
||||
a.addMapping(Method.GET, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &getVendor);
|
||||
a.addMapping(Method.POST, PROFILE_PATH ~ "/vendors", &createVendor);
|
||||
a.addMapping(Method.PUT, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &updateVendor);
|
||||
a.addMapping(Method.DELETE, PROFILE_PATH ~ "/vendors/:vendorId:ulong", &deleteVendor);
|
||||
|
||||
a.addMapping(Method.GET, PROFILE_PATH ~ "/transactions", &getTransactions);
|
||||
|
||||
// Protect all authenticated paths with a token filter.
|
||||
import auth.service : TokenAuthenticationFilter, SECRET;
|
||||
HttpRequestFilter tokenAuthenticationFilter = new TokenAuthenticationFilter(SECRET);
|
||||
|
@ -60,8 +71,7 @@ private void getStatus(ref HttpRequestContext ctx) {
|
|||
ctx.response.writeBodyString("online");
|
||||
}
|
||||
|
||||
private void getOptions(ref HttpRequestContext ctx) {
|
||||
}
|
||||
private void getOptions(ref HttpRequestContext ctx) {}
|
||||
|
||||
private void sampleDataEndpoint(ref HttpRequestContext ctx) {
|
||||
import util.sample_data;
|
||||
|
|
|
@ -8,8 +8,8 @@ module auth.model;
|
|||
* or more profiles.
|
||||
*/
|
||||
struct User {
|
||||
const string username;
|
||||
const string passwordHash;
|
||||
immutable string username;
|
||||
immutable string passwordHash;
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
@ -68,19 +68,16 @@ SQL";
|
|||
}
|
||||
|
||||
static HistoryItem parseItem(Row row) {
|
||||
HistoryItem item;
|
||||
item.id = row.peek!ulong(0);
|
||||
item.historyId = row.peek!ulong(1);
|
||||
item.timestamp = SysTime.fromISOExtString(row.peek!string(2));
|
||||
item.type = getHistoryItemType(row.peek!string(3));
|
||||
return item;
|
||||
return HistoryItem(
|
||||
row.peek!ulong(0),
|
||||
row.peek!ulong(1),
|
||||
parseISOTimestamp(row, 2),
|
||||
getHistoryItemType(row.peek!(string, PeekMode.slice)(3))
|
||||
);
|
||||
}
|
||||
|
||||
static HistoryItemText parseTextItem(Row row) {
|
||||
HistoryItemText item;
|
||||
item.itemId = row.peek!ulong(0);
|
||||
item.content = row.peek!string(1);
|
||||
return item;
|
||||
return HistoryItemText(row.peek!ulong(0), row.peek!string(1));
|
||||
}
|
||||
|
||||
private ulong addItem(ulong historyId, SysTime timestamp, HistoryItemType type) {
|
||||
|
|
|
@ -2,10 +2,18 @@ module history.model;
|
|||
|
||||
import std.datetime.systime;
|
||||
|
||||
/**
|
||||
* A history containing a series of items, which all usually pertain to a
|
||||
* certain target entity.
|
||||
*/
|
||||
struct History {
|
||||
ulong id;
|
||||
immutable ulong id;
|
||||
}
|
||||
|
||||
/**
|
||||
* The type of history item. This can be used as a discriminator value to treat
|
||||
* different history types separately.
|
||||
*/
|
||||
enum HistoryItemType : string {
|
||||
TEXT = "TEXT"
|
||||
}
|
||||
|
@ -18,14 +26,21 @@ HistoryItemType getHistoryItemType(string text) {
|
|||
throw new Exception("Unknown history item type: " ~ text);
|
||||
}
|
||||
|
||||
/**
|
||||
* A single item in a history. It has a UTC timestamp and a type. From the type,
|
||||
* one can get more specific information.
|
||||
*/
|
||||
struct HistoryItem {
|
||||
ulong id;
|
||||
ulong historyId;
|
||||
SysTime timestamp;
|
||||
HistoryItemType type;
|
||||
immutable ulong id;
|
||||
immutable ulong historyId;
|
||||
immutable SysTime timestamp;
|
||||
immutable HistoryItemType type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Additional data for history items with the TEXT type.
|
||||
*/
|
||||
struct HistoryItemText {
|
||||
ulong itemId;
|
||||
string content;
|
||||
immutable ulong itemId;
|
||||
immutable string content;
|
||||
}
|
||||
|
|
|
@ -125,9 +125,10 @@ class SqlitePropertiesRepository : PropertiesRepository {
|
|||
ResultRange result = stmt.execute();
|
||||
ProfileProperty[] props;
|
||||
foreach (Row row; result) {
|
||||
ProfileProperty prop;
|
||||
prop.property = row.peek!string("property");
|
||||
prop.value = row.peek!string("value");
|
||||
ProfileProperty prop = ProfileProperty(
|
||||
row.peek!string("property"),
|
||||
row.peek!string("value")
|
||||
);
|
||||
props ~= prop;
|
||||
}
|
||||
return props;
|
||||
|
|
|
@ -22,7 +22,12 @@ class Profile {
|
|||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* A string-based key-value-pair used to store an arbitrary piece of
|
||||
* information in a profile's database, intended for settings and other one-off
|
||||
* data that a normal table would be overkill for.
|
||||
*/
|
||||
struct ProfileProperty {
|
||||
string property;
|
||||
string value;
|
||||
immutable string property;
|
||||
immutable string value;
|
||||
}
|
||||
|
|
|
@ -0,0 +1,97 @@
|
|||
module transaction.api;
|
||||
|
||||
import handy_httpd;
|
||||
|
||||
import transaction.model;
|
||||
import transaction.data;
|
||||
import transaction.service;
|
||||
import profile.data;
|
||||
import profile.service;
|
||||
import util.json;
|
||||
import util.money;
|
||||
import util.pagination;
|
||||
|
||||
immutable DEFAULT_TRANSACTION_PAGE = PageRequest(0, 10, [Sort("created_at", SortDir.DESC)]);
|
||||
|
||||
void getTransactions(ref HttpRequestContext ctx) {
|
||||
ProfileDataSource ds = getProfileDataSource(ctx);
|
||||
PageRequest pr = PageRequest.parse(ctx, DEFAULT_TRANSACTION_PAGE);
|
||||
Page!Transaction page = searchTransactions(ds, pr);
|
||||
}
|
||||
|
||||
void getVendors(ref HttpRequestContext ctx) {
|
||||
ProfileDataSource ds = getProfileDataSource(ctx);
|
||||
auto vendorRepo = ds.getTransactionVendorRepository();
|
||||
TransactionVendor[] vendors = vendorRepo.findAll();
|
||||
writeJsonBody(ctx, vendors);
|
||||
}
|
||||
|
||||
void getVendor(ref HttpRequestContext ctx) {
|
||||
long vendorId = ctx.request.getPathParamAs!long("vendorId", -1);
|
||||
if (vendorId == -1) {
|
||||
ctx.response.status = HttpStatus.NOT_FOUND;
|
||||
ctx.response.writeBodyString("Missing vendorId path parameter.");
|
||||
return;
|
||||
}
|
||||
ProfileDataSource ds = getProfileDataSource(ctx);
|
||||
auto vendorRepo = ds.getTransactionVendorRepository();
|
||||
TransactionVendor vendor = vendorRepo.findById(vendorId)
|
||||
.orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
|
||||
writeJsonBody(ctx, vendor);
|
||||
}
|
||||
|
||||
struct VendorPayload {
|
||||
string name;
|
||||
string description;
|
||||
}
|
||||
|
||||
void createVendor(ref HttpRequestContext ctx) {
|
||||
VendorPayload payload = readJsonPayload!VendorPayload(ctx);
|
||||
ProfileDataSource ds = getProfileDataSource(ctx);
|
||||
auto vendorRepo = ds.getTransactionVendorRepository();
|
||||
if (vendorRepo.existsByName(payload.name)) {
|
||||
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||
ctx.response.writeBodyString("Vendor name is already in use.");
|
||||
return;
|
||||
}
|
||||
TransactionVendor vendor = vendorRepo.insert(payload.name, payload.description);
|
||||
writeJsonBody(ctx, vendor);
|
||||
}
|
||||
|
||||
void updateVendor(ref HttpRequestContext ctx) {
|
||||
VendorPayload payload = readJsonPayload!VendorPayload(ctx);
|
||||
long vendorId = ctx.request.getPathParamAs!long("vendorId", -1);
|
||||
if (vendorId == -1) {
|
||||
ctx.response.status = HttpStatus.NOT_FOUND;
|
||||
ctx.response.writeBodyString("Missing vendorId path parameter.");
|
||||
return;
|
||||
}
|
||||
ProfileDataSource ds = getProfileDataSource(ctx);
|
||||
auto vendorRepo = ds.getTransactionVendorRepository();
|
||||
TransactionVendor existingVendor = vendorRepo.findById(vendorId)
|
||||
.orElseThrow(() => new HttpStatusException(HttpStatus.NOT_FOUND));
|
||||
if (payload.name != existingVendor.name && vendorRepo.existsByName(payload.name)) {
|
||||
ctx.response.status = HttpStatus.BAD_REQUEST;
|
||||
ctx.response.writeBodyString("Vendor name is already in use.");
|
||||
}
|
||||
TransactionVendor updated = vendorRepo.updateById(
|
||||
vendorId,
|
||||
payload.name,
|
||||
payload.description
|
||||
);
|
||||
writeJsonBody(ctx, updated);
|
||||
}
|
||||
|
||||
void deleteVendor(ref HttpRequestContext ctx) {
|
||||
long vendorId = ctx.request.getPathParamAs!long("vendorId", -1);
|
||||
if (vendorId == -1) {
|
||||
ctx.response.status = HttpStatus.NOT_FOUND;
|
||||
ctx.response.writeBodyString("Missing vendorId path parameter.");
|
||||
return;
|
||||
}
|
||||
ProfileDataSource ds = getProfileDataSource(ctx);
|
||||
auto vendorRepo = ds.getTransactionVendorRepository();
|
||||
vendorRepo.deleteById(vendorId);
|
||||
}
|
||||
|
||||
|
|
@ -6,8 +6,9 @@ import std.datetime;
|
|||
import transaction.model;
|
||||
import transaction.data;
|
||||
import profile.data;
|
||||
import util.money;
|
||||
import account.model;
|
||||
import util.money;
|
||||
import util.pagination;
|
||||
|
||||
void addTransaction(
|
||||
ProfileDataSource ds,
|
||||
|
@ -60,3 +61,7 @@ void addTransaction(
|
|||
}
|
||||
});
|
||||
}
|
||||
|
||||
Page!Transaction searchTransactions(ProfileDataSource ds, PageRequest pr) {
|
||||
|
||||
}
|
||||
|
|
|
@ -0,0 +1,99 @@
|
|||
module util.pagination;
|
||||
|
||||
import handy_httpd;
|
||||
import handy_httpd.components.multivalue_map;
|
||||
import handy_httpd.components.optional;
|
||||
|
||||
import std.conv;
|
||||
|
||||
enum SortDir : string {
|
||||
ASC = "ASC",
|
||||
DESC = "DESC"
|
||||
}
|
||||
|
||||
struct Sort {
|
||||
immutable string attribute;
|
||||
immutable SortDir dir;
|
||||
|
||||
static Optional!Sort parse(string expr) {
|
||||
import std.string;
|
||||
string[] parts = expr.split(",");
|
||||
if (parts.length == 1) return Optional!Sort.of(Sort(parts[0], SortDir.ASC));
|
||||
if (parts.length != 2) return Optional!Sort.empty;
|
||||
string attr = parts[0];
|
||||
string dirExpr = parts[1];
|
||||
SortDir d;
|
||||
if (dirExpr == SortDir.ASC) {
|
||||
d = SortDir.ASC;
|
||||
} else if (dirExpr == SortDir.DESC) {
|
||||
d = SortDir.DESC;
|
||||
} else {
|
||||
return Optional!Sort.empty;
|
||||
}
|
||||
return Optional!Sort.of(Sort(attr, d));
|
||||
}
|
||||
}
|
||||
|
||||
struct PageRequest {
|
||||
immutable uint page;
|
||||
immutable ushort size;
|
||||
immutable Sort[] sorts;
|
||||
|
||||
bool isUnpaged() const {
|
||||
return page < 1;
|
||||
}
|
||||
|
||||
static PageRequest unpaged() {
|
||||
return PageRequest(0, 0, []);
|
||||
}
|
||||
|
||||
static PageRequest parse(ref HttpRequestContext ctx, PageRequest defaults) {
|
||||
import std.algorithm;
|
||||
import std.array;
|
||||
const(StringMultiValueMap) params = ctx.request.queryParams;
|
||||
uint pg = ctx.request.getParamAs!uint("page", defaults.page);
|
||||
ushort sz = ctx.request.getParamAs!ushort("size", defaults.size);
|
||||
Sort[] s = params.getAll("sort")
|
||||
.map!(Sort.parse)
|
||||
.filter!(o => !o.isNull)
|
||||
.map!(o => o.value)
|
||||
.array;
|
||||
return PageRequest(pg, sz, s.idup);
|
||||
}
|
||||
|
||||
string toSql() const {
|
||||
import std.array;
|
||||
auto app = appender!string;
|
||||
|
||||
if (sorts.length > 0) {
|
||||
app ~= "ORDER BY ";
|
||||
for (size_t i = 0; i < sorts.length; i++) {
|
||||
app ~= sorts[i].attribute;
|
||||
app ~= " ";
|
||||
app ~= cast(string) sorts[i].dir;
|
||||
if (i + 1 < sorts.length) app ~= ",";
|
||||
}
|
||||
app ~= " ";
|
||||
}
|
||||
app ~= "LIMIT ";
|
||||
app ~= size.to!string;
|
||||
app ~= " OFFSET ";
|
||||
app ~= page.to!string;
|
||||
return app[];
|
||||
}
|
||||
|
||||
PageRequest next() const {
|
||||
if (isUnpaged) return this;
|
||||
return PageRequest(page + 1, size, sorts);
|
||||
}
|
||||
|
||||
PageRequest prev() const {
|
||||
if (isUnpaged) return this;
|
||||
return PageRequest(page - 1, size, sorts);
|
||||
}
|
||||
}
|
||||
|
||||
struct Page(T) {
|
||||
T[] items;
|
||||
PageRequest pageRequest;
|
||||
}
|
|
@ -112,7 +112,7 @@ void generateRandomTransactions(ProfileDataSource ds) {
|
|||
SysTime now = Clock.currTime(UTC());
|
||||
SysTime timestamp = Clock.currTime(UTC()) - seconds(1);
|
||||
|
||||
for (int i = 0; i < 1000; i++) {
|
||||
for (int i = 0; i < 100; i++) {
|
||||
Optional!ulong vendorId;
|
||||
if (hasVendor) {
|
||||
vendorId = Optional!ulong.of(choice(vendors).id);
|
||||
|
|
Loading…
Reference in New Issue