Compare commits

...

43 Commits

Author SHA1 Message Date
Tristan B. Velloza Kildaire e78b201aea libpb
- Require minimum version `0.1.2` of `jstruct`
2023-06-18 13:33:54 +02:00
Tristan B. Velloza Kildaire aa77afccac - Added logo 2023-01-09 12:21:52 +02:00
Tristan B. Velloza Kildaire da22ae1c61 - Now using `jstruct` for serialization/deserialization (see Hax-io/jstruct for details)
- Updated .gitignore to exclude dub.selections.json
2023-01-09 11:19:11 +02:00
Tristan B. Velloza Kildaire 4fc4242c1e - Moved back to more clean templating method (for now till a fix can be found for the type lookups for issue #7) 2023-01-09 10:28:56 +02:00
Tristan B. Velloza Kildaire c89d6bd8b6 Updated .gitignore 2023-01-08 13:36:28 +02:00
Tristan B. Velloza Kildaire 707a1b9373 - `fromJSON()` now throws `RemoteFieldMissing` exception if the input JSON does not have a field found in the input struct type `RecordType`
- Added a unittest to test the above
2023-01-08 13:36:01 +02:00
Tristan B. Velloza Kildaire 3d9b9a4976 - Attempt to mixin properly 2023-01-06 16:42:40 +02:00
Tristan B. Velloza Kildaire b10c7d928d Attempt at fixing enum type lookups (anything not implicitly in scope of mixin such as built-in types) 2023-01-06 16:38:34 +02:00
Tristan B. Velloza Kildaire 2f5710fafb - Removed uneeded public imports 2023-01-06 16:38:03 +02:00
Tristan B. Velloza Kildaire d89b7159dd - Ensure that `serializeRecord()` and `fromJSON()` are publically imported as part of the package import to fix user-defined types lookups (such as enums) 2023-01-06 15:30:59 +02:00
Tristan B. Velloza Kildaire 5c0dcfe15b Updated testing DB schema 2023-01-05 13:01:53 +02:00
Tristan B. Velloza Kildaire b9a2254a66 - Removed uneeded default arguments 2023-01-05 13:01:13 +02:00
Tristan B. Velloza Kildaire 1b3ef337a5 - Now `updateRecord()` is for base collections and `updateRecordAuth()` is for auth collections 2023-01-05 12:58:51 +02:00
Tristan B. Velloza Kildaire 571e8dea81 - Mixin template `MemberAndType` should be private (not user-accessibe) 2023-01-05 12:50:36 +02:00
Tristan B. Velloza Kildaire 199b1f0120 - Now `listRecords()` is for base collections and `listRecordsAuth()` is for auth collections
- Now `viewRecord()` is for base collections and `viewRecordAuth()` is for auth collections
- Updated unittests accordingly
2023-01-05 12:36:36 +02:00
Tristan B. Velloza Kildaire 6da8696926 - Added documentation string to `NetworkException` 2023-01-05 11:33:00 +02:00
Tristan B. Velloza Kildaire 2c8141f5b6 Added all integral types 2023-01-05 11:31:42 +02:00
Tristan B. Velloza Kildaire 4a287864ee - Fixed API call in usage example 2023-01-05 11:29:11 +02:00
Tristan B. Velloza Kildaire 8483a60da8 - Added missing documentaion string for `authWithPassword()` 2023-01-05 11:28:12 +02:00
Tristan B. Velloza Kildaire 6b47078b89 - Fixed up API to have seperate `createRecord()` (for base collections) and `createRecordAuth()` (for auth collections)
- Privatized and renamed the old `createRecord()` -> `createRecord_internal()`
2023-01-05 11:25:24 +02:00
Tristan B. Velloza Kildaire f23a4c40bb - Implemented `createRecordAuth()` which is to be used for creating authentication records as it has compile-time guarantees about the fields required in a struct for such a procedure 2023-01-05 11:22:35 +02:00
Tristan B. Velloza Kildaire 6a5e1d60ee - Implemented `authWithPassword()` which provides a token back after providing authentication details
- Added unittest for `authWithPassword()`
2023-01-05 11:19:38 +02:00
Tristan B. Velloza Kildaire cda2e6e54f - Fixed the use of `serializeRecord` and `fromJSON` in README.md serialization/deserialization examples 2023-01-02 16:38:08 +02:00
Tristan B. Velloza Kildaire 78100faaef - Privatized static method `isAbleCheck()` in PocketBase 2023-01-02 16:32:08 +02:00
Tristan B. Velloza Kildaire 2550e12496 - Privatised testing enum `EnumType` in serialization.d unit test 2023-01-02 16:28:04 +02:00
Tristan B. Velloza Kildaire 87b564f4e5 - Cleaned up serialization.d and deserialization.d imports 2023-01-02 16:21:20 +02:00
Tristan B. Velloza Kildaire eeabd9173e Merge branch 'master' of github.com:Hax-io/libpb 2023-01-02 16:16:00 +02:00
Tristan B. Velloza Kildaire 15fe95f532 Refactored various components into their own modules 2023-01-02 16:14:47 +02:00
Tristan B. Velloza Kildaire f88ea15f38
Added link to full API docs 2023-01-02 15:59:19 +02:00
Tristan B. Velloza Kildaire 3e853e7431 - Added authentication token support on a per-instance basis, this adds `setAuthToken(string)` and `string getAuthToken()`
- Authentication tokens that are `""` (empty) will not cause headers to be added
2023-01-02 15:55:36 +02:00
Tristan B. Velloza Kildaire 4bb0c225af - Removed stray `writeln()` without a wrapping `debug(dbg)` 2023-01-02 14:17:49 +02:00
Tristan B. Velloza Kildaire f06dcaf8f5 - Fixed the filtering in `listRecords()`, now escaping is automatically done
- Added test cases for testing the filtering in `listRecords()`
- `libcurl` is now a dependency, for linking against the object files (see `.dub.json`)
2023-01-02 14:15:44 +02:00
Tristan B. Velloza Kildaire 889e07040e Added filtering support to \'listRecords()\' 2023-01-01 08:36:27 +02:00
Tristan B. Velloza Kildaire 960eef4934 Removed stray debug print 2022-12-30 16:10:19 +02:00
Tristan B. Velloza Kildaire 4db1d40759 Fixed creation of auth-type records in `createRecord()` 2022-12-30 16:09:44 +02:00
Tristan B. Velloza Kildaire e44fbf3189 Added more exception types for handling various error codes 2022-12-30 14:23:56 +02:00
Tristan B. Velloza Kildaire 1027e5580f - Improved error handling with more specific errors
- Error 404 (when a record is not found) is now translated into `RecordNotFoundException`
- Added documentation
- Added unittest updates to test the usage of `RecordNotFoundException` in `deleteRecord()`, `viewRecord()` and `updateRecord()`
2022-12-30 14:01:04 +02:00
Tristan B. Velloza Kildaire 0b0a8d2b31
Updated usage 2022-12-30 13:18:16 +02:00
Tristan B. Velloza Kildaire 98723c0772 SIlent when not using -ddbg 2022-12-30 13:14:20 +02:00
Tristan B. Velloza Kildaire c651b9d515 - `listRecords()` now returns an array of records
- Added unittests for `listRecords()`
2022-12-30 13:10:10 +02:00
Tristan B. Velloza Kildaire 1594943561 Updated usage 2022-12-29 21:37:28 +02:00
Tristan B. Velloza Kildaire c2597788d7 - Implemented `viewRecord()`
- Added unittests for `viewRecord()`
2022-12-29 21:30:52 +02:00
Tristan B. Velloza Kildaire 9cfed9b33d - `updateRecord()` now returns the updated record 2022-12-29 19:56:40 +02:00
10 changed files with 1121 additions and 430 deletions

2
.gitignore vendored
View File

@ -14,3 +14,5 @@ libpb-test-*
*.o
*.obj
*.lst
liblibpb.a
dub.selections.json

View File

@ -1,3 +1,5 @@
![](branding/logo.png)
libpb
=====
@ -7,6 +9,8 @@ libpb
## Example usage
View the full API documentation (methods etc.) [here](https://libpb.dpldocs.info/libpb.html).
### Server initiation
Firstly we create a new PocketBase instance to manage our server:
@ -40,7 +44,7 @@ p1.list = ["1", "2", "3"];
p1.extraJSON = parseJSON(`{"item":1, "items":[1,2,3]}`);
p1.eType = EnumType.CAT;
JSONValue serialized = PocketBase.serializeRecord(p1);
JSONValue serialized = serializeRecord(p1);
string[] keys = serialized.object().keys();
assert(canFind(keys, "firstname") && cmp(serialized["firstname"].str(), "Tristan") == 0);
@ -79,7 +83,7 @@ JSONValue json = parseJSON(`{
}
`);
Person person = PocketBase.fromJSON!(Person)(json);
Person person = fromJSON!(Person)(json);
debug(dbg)
{
@ -97,6 +101,8 @@ assert(person.obj["bruh"].integer() == 1);
### Record management
#### Normal collections
Below we have a few calls like create and delete:
```d
@ -116,12 +122,66 @@ p1.age = 23;
Person recordStored = pb.createRecord("dummy", p1);
pb.deleteRecord("dummy", recordStored.id);
recordStored = pb.createRecord("dummy", p1);
recordStored.age = 46;
recordStored = pb.updateRecord("dummy", recordStored);
Person recordFetched = pb.viewRecord!(Person)("dummy", recordStored.id);
pb.deleteRecord("dummy", recordStored);
Person[] people = [Person(), Person()];
people[0].name = "Abby";
people[1].name = "Becky";
people[0] = pb.createRecord("dummy", people[0]);
people[1] = pb.createRecord("dummy", people[1]);
Person[] returnedPeople = pb.listRecords!(Person)("dummy");
foreach(Person returnedPerson; returnedPeople)
{
writeln(returnedPerson);
pb.deleteRecord("dummy", returnedPerson);
}
```
#### `auth` collections
Auth collections require that certain calls, such as `createRecord(table, record, isAuthCollection)` have the last argument se to `true`.
```d
import core.thread : Thread, dur;
import std.string : cmp;
PocketBase pb = new PocketBase();
struct Person
{
string id;
string email;
string username;
string password;
string passwordConfirm;
}
Person p1;
p1.email = "deavmi@redxen.eu";
p1.username = "deavmi";
p1.password = "bigbruh1111";
p1.passwordConfirm = "bigbruh1111";
p1 = pb.createRecordAuth("dummy_auth", p1);
pb.deleteRecord("dummy_auth", p1);
```
## Development
### Dependencies
This requires that you have the `libcurl` libraries available for
linking against.
### Unit tests
To run tests you will want to enable the `pragma`s and `writeln`s. therefore pass the `dbg` flag in as such:

BIN
branding/logo.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.4 KiB

BIN
branding/logo.xcf Normal file

Binary file not shown.

View File

@ -3,8 +3,14 @@
"Tristan B. Velloza Kildaire"
],
"copyright": "Copyright © 2022, Tristan B. Velloza Kildaire",
"dependencies": {
"jstruct": ">=0.1.2"
},
"description": "PocketBase wrapper with serializer/deserializer support",
"libs": [
"curl"
],
"license": "LGPL v3.0",
"name": "libpb",
"targetType" : "library"
"targetType": "library"
}

View File

@ -1,4 +1,5 @@
{
[
{
"id": "iir9gleipa4n6lf",
"name": "dummy",
"type": "base",
@ -36,5 +37,53 @@
"updateRule": "",
"deleteRule": "",
"options": {}
},
{
"id": "fw99z50dwcfjwn7",
"name": "dummy_auth",
"type": "auth",
"system": false,
"schema": [
{
"id": "psy7unkl",
"name": "name",
"type": "text",
"system": false,
"required": false,
"unique": false,
"options": {
"min": null,
"max": null,
"pattern": ""
}
},
{
"id": "jroziygp",
"name": "age",
"type": "number",
"system": false,
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
}
],
"listRule": "",
"viewRule": "",
"createRule": "",
"updateRule": "",
"deleteRule": "",
"options": {
"allowEmailAuth": true,
"allowOAuth2Auth": false,
"allowUsernameAuth": true,
"exceptEmailDomains": null,
"manageRule": null,
"minPasswordLength": 8,
"onlyEmailDomains": null,
"requireEmail": false
}
}
}
]

View File

@ -1,425 +0,0 @@
module libpb;
import std.json;
import std.stdio;
import std.net.curl;
import std.conv : to;
public final class PBException : Exception
{
public enum ErrorType
{
CURL_NETWORK_ERROR,
JSON_PARSE_ERROR
}
private ErrorType errType;
this(ErrorType errType, string msg)
{
this.errType = errType;
super("PBException("~to!(string)(errType)~"): "~msg);
}
}
public class PocketBase
{
private string pocketBaseURL;
this(string pocketBaseURL = "http://127.0.0.1:8090/api/")
{
this.pocketBaseURL = pocketBaseURL;
}
public JSONValue listRecords(string table, ulong page = 1, ulong perPage = 30)
{
// Compute the query string
string queryStr = "page="~to!(string)(page)~"&perPage="~to!(string)(perPage);
try
{
string responseData = cast(string)get(pocketBaseURL~"collections/"~table~"/records?"~queryStr);
JSONValue responseJSON = parseJSON(responseData);
return responseJSON;
}
catch(CurlException e)
{
throw new PBException(PBException.ErrorType.CURL_NETWORK_ERROR, e.msg);
}
catch(JSONException e)
{
throw new PBException(PBException.ErrorType.JSON_PARSE_ERROR, e.msg);
}
}
public RecordType createRecord(string, RecordType)(string table, RecordType item)
{
idAbleCheck(item);
RecordType recordOut;
HTTP httpSettings = HTTP();
httpSettings.addRequestHeader("Content-Type", "application/json");
// Serialize the record instance
JSONValue serialized = serializeRecord(item);
try
{
string responseData = cast(string)post(pocketBaseURL~"collections/"~table~"/records", serialized.toString(), httpSettings);
JSONValue responseJSON = parseJSON(responseData);
recordOut = fromJSON!(RecordType)(responseJSON);
return recordOut;
}
catch(CurlException e)
{
throw new PBException(PBException.ErrorType.CURL_NETWORK_ERROR, e.msg);
}
catch(JSONException e)
{
throw new PBException(PBException.ErrorType.JSON_PARSE_ERROR, e.msg);
}
}
public JSONValue updateRecord(string, RecordType)(string table, RecordType item)
{
idAbleCheck(item);
HTTP httpSettings = HTTP();
httpSettings.addRequestHeader("Content-Type", "application/json");
// Serialize the record instance
JSONValue serialized = serializeRecord(item);
try
{
string responseData = cast(string)patch(pocketBaseURL~"collections/"~table~"/records/"~item.id, serialized.toString(), httpSettings);
JSONValue responseJSON = parseJSON(responseData);
return responseJSON;
}
catch(CurlException e)
{
throw new PBException(PBException.ErrorType.CURL_NETWORK_ERROR, e.msg);
}
catch(JSONException e)
{
throw new PBException(PBException.ErrorType.JSON_PARSE_ERROR, e.msg);
}
}
public void deleteRecord(string table, string id)
{
try
{
del(pocketBaseURL~"collections/"~table~"/records/"~id);
}
catch(CurlException e)
{
throw new PBException(PBException.ErrorType.CURL_NETWORK_ERROR, e.msg);
}
}
public static void idAbleCheck(RecordType)(RecordType record)
{
static if(__traits(hasMember, record, "id"))
{
static if(__traits(isSame, typeof(record.id), string))
{
// Do nothing as it is a-okay
}
else
{
// Must be a string
pragma(msg, "The `id` field of the record provided must be of type string");
static assert(false);
}
}
else
{
// An id field is required (TODO: ensure not a function identifier)
pragma(msg, "The provided record must have a `id` field");
static assert(false);
}
}
//TODO: Here and upate record we must enforce the `.id`
public void deleteRecord(string, RecordType)(string table, RecordType record)
{
idAbleCheck(record);
deleteRecord(table, record.id);
}
public static JSONValue serializeRecord(RecordType)(RecordType record)
{
import std.traits;
import std.meta : AliasSeq;
// Final JSON to submit
JSONValue builtJSON;
// Alias as to only expand later when used in compile-time
alias structTypes = FieldTypeTuple!(RecordType);
alias structNames = FieldNameTuple!(RecordType);
alias structValues = record.tupleof;
static foreach(cnt; 0..structTypes.length)
{
debug(dbg)
{
pragma(msg, structTypes[cnt]);
pragma(msg, structNames[cnt]);
// pragma(msg, structValues[cnt]);
}
static if(__traits(isSame, mixin(structTypes[cnt]), int))
{
builtJSON[structNames[cnt]] = structValues[cnt];
}
else static if(__traits(isSame, mixin(structTypes[cnt]), uint))
{
builtJSON[structNames[cnt]] = structValues[cnt];
}
else static if(__traits(isSame, mixin(structTypes[cnt]), ulong))
{
builtJSON[structNames[cnt]] = structValues[cnt];
}
else static if(__traits(isSame, mixin(structTypes[cnt]), long))
{
builtJSON[structNames[cnt]] = structValues[cnt];
}
else static if(__traits(isSame, mixin(structTypes[cnt]), string))
{
builtJSON[structNames[cnt]] = structValues[cnt];
}
else static if(__traits(isSame, mixin(structTypes[cnt]), JSONValue))
{
builtJSON[structNames[cnt]] = structValues[cnt];
}
else static if(__traits(isSame, mixin(structTypes[cnt]), bool))
{
builtJSON[structNames[cnt]] = structValues[cnt];
}
else
{
debug(dbg)
{
pragma(msg, "Yaa");
}
builtJSON[structNames[cnt]] = to!(string)(structValues[cnt]);
}
}
return builtJSON;
}
public static fromJSON(RecordType)(JSONValue jsonIn)
{
RecordType record;
import std.traits;
import std.meta : AliasSeq;
// Alias as to only expand later when used in compile-time
alias structTypes = FieldTypeTuple!(RecordType);
alias structNames = FieldNameTuple!(RecordType);
alias structValues = record.tupleof;
static foreach(cnt; 0..structTypes.length)
{
debug(dbg)
{
pragma(msg, structTypes[cnt]);
pragma(msg, structNames[cnt]);
// pragma(msg, structValues[cnt]);
}
//TODO: Add all integral types
static if(__traits(isSame, mixin(structTypes[cnt]), int))
{
mixin("record."~structNames[cnt]) = cast(int)jsonIn[structNames[cnt]].integer();
}
else static if(__traits(isSame, mixin(structTypes[cnt]), uint))
{
mixin("record."~structNames[cnt]) = cast(uint)jsonIn[structNames[cnt]].integer();
}
else static if(__traits(isSame, mixin(structTypes[cnt]), ulong))
{
mixin("record."~structNames[cnt]) = cast(ulong)jsonIn[structNames[cnt]].integer();
}
else static if(__traits(isSame, mixin(structTypes[cnt]), long))
{
mixin("record."~structNames[cnt]) = cast(long)jsonIn[structNames[cnt]].integer();
}
else static if(__traits(isSame, mixin(structTypes[cnt]), string))
{
mixin("record."~structNames[cnt]) = jsonIn[structNames[cnt]].str();
debug(dbg)
{
pragma(msg,"record."~structNames[cnt]);
}
}
else static if(__traits(isSame, mixin(structTypes[cnt]), JSONValue))
{
mixin("record."~structNames[cnt]) = jsonIn[structNames[cnt]];
debug(dbg)
{
pragma(msg,"record."~structNames[cnt]);
}
}
else static if(__traits(isSame, mixin(structTypes[cnt]), bool))
{
mixin("record."~structNames[cnt]) = jsonIn[structNames[cnt]].boolean();
debug(dbg)
{
pragma(msg,"record."~structNames[cnt]);
}
}
//FIXME: Not sure how to get array support going, very new to meta programming
else static if(__traits(isSame, mixin(structTypes[cnt]), mixin(structTypes[cnt])[]))
{
mixin("record."~structNames[cnt]) = jsonIn[structNames[cnt]].boolean();
debug(dbg)
{
pragma(msg,"record."~structNames[cnt]);
}
}
else
{
// throw new
//TODO: Throw error
debug(dbg)
{
pragma(msg, "Unknown type for de-serialization");
}
}
}
return record;
}
}
public enum EnumType
{
DOG,
CAT
}
// Test serialization of a struct to JSON
unittest
{
import std.algorithm.searching : canFind;
import std.string : cmp;
struct Person
{
public string firstname, lastname;
public int age;
public string[] list;
public JSONValue extraJSON;
public EnumType eType;
}
Person p1;
p1.firstname = "Tristan";
p1.lastname = "Kildaire";
p1.age = 23;
p1.list = ["1", "2", "3"];
p1.extraJSON = parseJSON(`{"item":1, "items":[1,2,3]}`);
p1.eType = EnumType.CAT;
JSONValue serialized = PocketBase.serializeRecord(p1);
string[] keys = serialized.object().keys();
assert(canFind(keys, "firstname") && cmp(serialized["firstname"].str(), "Tristan") == 0);
assert(canFind(keys, "lastname") && cmp(serialized["lastname"].str(), "Kildaire") == 0);
assert(canFind(keys, "age") && serialized["age"].integer() == 23);
debug(dbg)
{
writeln(serialized.toPrettyString());
}
}
unittest
{
import std.string : cmp;
struct Person
{
public string firstname, lastname;
public int age;
public bool isMale;
public JSONValue obj;
public int[] list;
}
JSONValue json = parseJSON(`{
"firstname" : "Tristan",
"lastname": "Kildaire",
"age": 23,
"obj" : {"bruh":1},
"isMale": true,
"list": [1,2,3]
}
`);
Person person = PocketBase.fromJSON!(Person)(json);
debug(dbg)
{
writeln(person);
}
assert(cmp(person.firstname, "Tristan") == 0);
assert(cmp(person.lastname, "Kildaire") == 0);
assert(person.age == 23);
assert(person.isMale == true);
assert(person.obj["bruh"].integer() == 1);
//TODO: list test case
}
unittest
{
import core.thread : Thread, dur;
PocketBase pb = new PocketBase();
struct Person
{
string id;
string name;
int age;
}
Person p1 = Person();
p1.name = "Tristan Gonzales";
p1.age = 23;
Person recordStored = pb.createRecord("dummy", p1);
pb.deleteRecord("dummy", recordStored.id);
recordStored = pb.createRecord("dummy", p1);
Thread.sleep(dur!("seconds")(3));
recordStored.age = 46;
pb.updateRecord("dummy", recordStored);
Thread.sleep(dur!("seconds")(3));
pb.deleteRecord("dummy", recordStored);
}

923
source/libpb/driver.d Normal file
View File

@ -0,0 +1,923 @@
module libpb.driver;
import std.json;
import std.stdio;
import std.net.curl;
import std.conv : to;
import std.string : cmp;
import libpb.exceptions;
import jstruct : fromJSON, SerializationError, serializeRecord;
private mixin template AuthTokenHeader(alias http, PocketBase pbInstance)
{
// Must be an instance of HTTP from `std.curl`
static assert(__traits(isSame, typeof(http), HTTP));
void InitializeAuthHeader()
{
// Check if the given PocketBase instance as an authToken
if(pbInstance.authToken.length > 0)
{
// Then add the authaorization header
http.addRequestHeader("Authorization", pbInstance.getAuthToken());
}
}
}
public class PocketBase
{
private string pocketBaseURL;
private string authToken;
/**
* Constructs a new PocketBase instance with
* the default settings
*/
this(string pocketBaseURL = "http://127.0.0.1:8090/api/", string authToken = "")
{
this.pocketBaseURL = pocketBaseURL;
this.authToken = authToken;
}
public void setAuthToken(string authToken)
{
if(cmp(authToken, "") != 0)
{
this.authToken = authToken;
}
}
public string getAuthToken()
{
return this.authToken;
}
/**
* List all of the records in the given table (base collection)
*
* Params:
* table = the table to list from
* page = the page to look at (default is 1)
* perPage = the number of items to return per page (default is 30)
* filter = the predicate to filter by
*
* Returns: A list of type <code>RecordType</code>
*/
public RecordType[] listRecords(RecordType)(string table, ulong page = 1, ulong perPage = 30, string filter = "")
{
return listRecords_internal!(RecordType)(table, page, perPage, filter, false);
}
/**
* List all of the records in the given table (auth collection)
*
* Params:
* table = the table to list from
* page = the page to look at (default is 1)
* perPage = the number of items to return per page (default is 30)
* filter = the predicate to filter by
*
* Returns: A list of type <code>RecordType</code>
*/
public RecordType[] listRecordsAuth(RecordType)(string table, ulong page = 1, ulong perPage = 30, string filter = "")
{
return listRecords_internal!(RecordType)(table, page, perPage, filter, true);
}
/**
* List all of the records in the given table (internals)
*
* Params:
* table = the table to list from
* page = the page to look at (default is 1)
* perPage = the number of items to return per page (default is 30)
* filter = the predicate to filter by
* isAuthCollection = true if this is an auth collection, false
* for base collection
*
* Returns: A list of type <code>RecordType</code>
*/
private RecordType[] listRecords_internal(RecordType)(string table, ulong page, ulong perPage, string filter, bool isAuthCollection)
{
// Set authorization token if setup
HTTP httpSettings = HTTP();
mixin AuthTokenHeader!(httpSettings, this);
InitializeAuthHeader();
RecordType[] recordsOut;
// Compute the query string
string queryStr = "page="~to!(string)(page)~"&perPage="~to!(string)(perPage);
// If there is a filter then perform the needed escaping
if(cmp(filter, "") != 0)
{
// For the filter, make sure to add URL escaping to the `filter` parameter
import etc.c.curl : curl_escape;
import std.string : toStringz, fromStringz;
char* escapedParameter = curl_escape(toStringz(filter), cast(int)filter.length);
if(escapedParameter is null)
{
debug(dbg)
{
writeln("Invalid return from curl_easy_escape");
}
throw new NetworkException();
}
// Convert back to D-string (the filter)
filter = cast(string)fromStringz(escapedParameter);
}
// Append the filter
queryStr ~= cmp(filter, "") == 0 ? "" : "&filter="~filter;
try
{
string responseData = cast(string)get(pocketBaseURL~"collections/"~table~"/records?"~queryStr, httpSettings);
JSONValue responseJSON = parseJSON(responseData);
JSONValue[] returnedItems = responseJSON["items"].array();
foreach(JSONValue returnedItem; returnedItems)
{
// If this is an authable record (meaning it has email, password and passwordConfirm)
// well then the latter two will not be returned so fill them in. Secondly, the email
// will only be returned if `emailVisibility` is true.
if(isAuthCollection)
{
returnedItem["password"] = "";
returnedItem["passwordConfirm"] = "";
// If email is invisible make a fake field to prevent crash
if(!returnedItem["emailVisibility"].boolean())
{
returnedItem["email"] = "";
}
}
recordsOut ~= fromJSON!(RecordType)(returnedItem);
}
return recordsOut;
}
catch(HTTPStatusException e)
{
if(e.status == 403)
{
throw new NotAuthorized(table, null);
}
else
{
throw new NetworkException();
}
}
catch(CurlException e)
{
debug(dbg)
{
writeln("curl");
writeln(e);
}
throw new NetworkException();
}
catch(JSONException e)
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
* Creates a record in the given authentication table
*
* Params:
* table = the table to create the record in
* item = The Record to create
*
* Returns: An instance of the created <code>RecordType</code>
*/
public RecordType createRecordAuth(string, RecordType)(string table, RecordType item)
{
mixin isAuthable!(RecordType);
return createRecord_internal(table, item, true);
}
/**
* Creates a record in the given base table
*
* Params:
* table = the table to create the record in
* item = The Record to create
*
* Returns: An instance of the created <code>RecordType</code>
*/
public RecordType createRecord(string, RecordType)(string table, RecordType item)
{
return createRecord_internal(table, item, false);
}
/**
* Creates a record in the given table (internal method)
*
* Params:
* table = the table to create the record in
* item = The Record to create
* isAuthCollection = whether or not this collection is auth or not (base)
*
* Returns: An instance of the created <code>RecordType</code>
*/
private RecordType createRecord_internal(string, RecordType)(string table, RecordType item, bool isAuthCollection)
{
idAbleCheck(item);
RecordType recordOut;
// Set authorization token if setup
HTTP httpSettings = HTTP();
mixin AuthTokenHeader!(httpSettings, this);
InitializeAuthHeader();
// Set the content type
httpSettings.addRequestHeader("Content-Type", "application/json");
// Serialize the record instance
JSONValue serialized = serializeRecord(item);
try
{
string responseData = cast(string)post(pocketBaseURL~"collections/"~table~"/records", serialized.toString(), httpSettings);
JSONValue responseJSON = parseJSON(responseData);
// On creation of a record in an "auth" collection the email visibility
// will initially be false, therefore fill in a blank for it temporarily
// now as to not make `fromJSON` crash when it sees an email field in
// a struct and tries to look the the JSON key "email" when it isn't present
//
// A password is never returned (so `password` and `passwordConfirm` will be left out)
//
// The above are all assumed to be strings, if not then a runtime error will occur
// See (issue #3)
if(isAuthCollection)
{
responseJSON["email"] = "";
responseJSON["password"] = "";
responseJSON["passwordConfirm"] = "";
}
recordOut = fromJSON!(RecordType)(responseJSON);
return recordOut;
}
catch(HTTPStatusException e)
{
debug(dbg)
{
writeln("createRecord_internal: "~e.toString());
}
if(e.status == 403)
{
throw new NotAuthorized(table, item.id);
}
else if(e.status == 400)
{
throw new ValidationRequired(table, item.id);
}
else
{
// TODO: Fix this
throw new NetworkException();
}
}
catch(CurlException e)
{
throw new NetworkException();
}
catch(JSONException e)
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
* Authenticates on the given auth table with the provided
* credentials, returning a JWT token in the reference parameter.
* Finally returning the record of the authenticated user.
*
* Params:
* table = the auth collection to use
* identity = the user's identity
* password = the user's password
* token = the variable to return into
*
* Returns: An instance of `RecordType`
*/
public RecordType authWithPassword(RecordType)(string table, string identity, string password, ref string token)
{
mixin isAuthable!(RecordType);
RecordType recordOut;
// Set the content type
HTTP httpSettings = HTTP();
httpSettings.addRequestHeader("Content-Type", "application/json");
// Construct the authentication record
JSONValue authRecord;
authRecord["identity"] = identity;
authRecord["password"] = password;
try
{
string responseData = cast(string)post(pocketBaseURL~"collections/"~table~"/auth-with-password", authRecord.toString(), httpSettings);
JSONValue responseJSON = parseJSON(responseData);
JSONValue recordResponse = responseJSON["record"];
// In the case we are doing auth, we won't get password, passwordConfirm sent back
// set them to empty
recordResponse["password"] = "";
recordResponse["passwordConfirm"] = "";
// If email is invisible make a fake field to prevent crash
if(!recordResponse["emailVisibility"].boolean())
{
recordResponse["email"] = "";
}
recordOut = fromJSON!(RecordType)(recordResponse);
// Store the token
token = responseJSON["token"].str();
return recordOut;
}
catch(HTTPStatusException e)
{
if(e.status == 400)
{
// TODO: Update this error
throw new NotAuthorized(table, null);
}
else
{
// TODO: Fix this
throw new NetworkException();
}
}
catch(CurlException e)
{
throw new NetworkException();
}
catch(JSONException e)
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
* View the given record by id (base collections)
*
* Params:
* table = the table to lookup the record in
* id = the id to lookup the record by
*
* Returns: The found record of type <code>RecordType</code>
*/
public RecordType viewRecord(RecordType)(string table, string id)
{
return viewRecord_internal!(RecordType)(table, id, false);
}
/**
* View the given record by id (auth collections)
*
* Params:
* table = the table to lookup the record in
* id = the id to lookup the record by
*
* Returns: The found record of type <code>RecordType</code>
*/
public RecordType viewRecordAuth(RecordType)(string table, string id)
{
return viewRecord_internal!(RecordType)(table, id, true);
}
/**
* View the given record by id (internal)
*
* Params:
* table = the table to lookup the record in
* id = the id to lookup the record by
* isAuthCollection = true if this is an auth collection, false
* for base collection
*
* Returns: The found record of type <code>RecordType</code>
*/
private RecordType viewRecord_internal(RecordType)(string table, string id, bool isAuthCollection)
{
RecordType recordOut;
// Set authorization token if setup
HTTP httpSettings = HTTP();
mixin AuthTokenHeader!(httpSettings, this);
InitializeAuthHeader();
try
{
string responseData = cast(string)get(pocketBaseURL~"collections/"~table~"/records/"~id, httpSettings);
JSONValue responseJSON = parseJSON(responseData);
// If this is an authable record (meaning it has email, password and passwordConfirm)
// well then the latter two will not be returned so fill them in. Secondly, the email
// will only be returned if `emailVisibility` is true.
if(isAuthCollection)
{
responseJSON["password"] = "";
responseJSON["passwordConfirm"] = "";
// If email is invisible make a fake field to prevent crash
if(!responseJSON["emailVisibility"].boolean())
{
responseJSON["email"] = "";
}
}
recordOut = fromJSON!(RecordType)(responseJSON);
return recordOut;
}
catch(HTTPStatusException e)
{
if(e.status == 404)
{
throw new RecordNotFoundException(table, id);
}
else
{
// TODO: Fix this
throw new NetworkException();
}
}
catch(CurlException e)
{
throw new NetworkException();
}
catch(JSONException e)
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
* Updates the given record in the given table, returning the
* updated record (auth collections)
*
* Params:
* table = tabe table to update the record in
* item = the record of type <code>RecordType</code> to update
*
* Returns: The updated <code>RecordType</code>
*/
public RecordType updateRecordAuth(string, RecordType)(string table, RecordType item)
{
return updateRecord_internal(table, item, true);
}
/**
* Updates the given record in the given table, returning the
* updated record (base collections)
*
* Params:
* table = tabe table to update the record in
* item = the record of type <code>RecordType</code> to update
*
* Returns: The updated <code>RecordType</code>
*/
public RecordType updateRecord(string, RecordType)(string table, RecordType item)
{
return updateRecord_internal(table, item, false);
}
/**
* Updates the given record in the given table, returning the
* updated record (internal)
*
* Params:
* table = tabe table to update the record in
* item = the record of type <code>RecordType</code> to update
* isAuthCollection = true if this is an auth collection, false
* for base collection
*
* Returns: The updated <code>RecordType</code>
*/
private RecordType updateRecord_internal(string, RecordType)(string table, RecordType item, bool isAuthCollection)
{
idAbleCheck(item);
RecordType recordOut;
// Set authorization token if setup
HTTP httpSettings = HTTP();
mixin AuthTokenHeader!(httpSettings, this);
InitializeAuthHeader();
// Set the content type
httpSettings.addRequestHeader("Content-Type", "application/json");
// Serialize the record instance
JSONValue serialized = serializeRecord(item);
try
{
string responseData = cast(string)patch(pocketBaseURL~"collections/"~table~"/records/"~item.id, serialized.toString(), httpSettings);
JSONValue responseJSON = parseJSON(responseData);
// If this is an authable record (meaning it has email, password and passwordConfirm)
// well then the latter two will not be returned so fill them in. Secondly, the email
// will only be returned if `emailVisibility` is true.
if(isAuthCollection)
{
responseJSON["password"] = "";
responseJSON["passwordConfirm"] = "";
// If email is invisible make a fake field to prevent crash
if(!responseJSON["emailVisibility"].boolean())
{
responseJSON["email"] = "";
}
}
recordOut = fromJSON!(RecordType)(responseJSON);
return recordOut;
}
catch(HTTPStatusException e)
{
if(e.status == 404)
{
throw new RecordNotFoundException(table, item.id);
}
else if(e.status == 403)
{
throw new NotAuthorized(table, item.id);
}
else if(e.status == 400)
{
throw new ValidationRequired(table, item.id);
}
else
{
// TODO: Fix this
throw new NetworkException();
}
}
catch(CurlException e)
{
throw new NetworkException();
}
catch(JSONException e)
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
* Deletes the provided record by id from the given table
*
* Params:
* table = the table to delete the record from
* id = the id of the record to delete
*/
public void deleteRecord(string table, string id)
{
// Set authorization token if setup
HTTP httpSettings = HTTP();
mixin AuthTokenHeader!(httpSettings, this);
InitializeAuthHeader();
try
{
del(pocketBaseURL~"collections/"~table~"/records/"~id, httpSettings);
}
catch(HTTPStatusException e)
{
if(e.status == 404)
{
throw new RecordNotFoundException(table, id);
}
else
{
// TODO: Fix this
throw new NetworkException();
}
}
catch(CurlException e)
{
throw new NetworkException();
}
}
/**
* Deletes the provided record from the given table
*
* Params:
* table = the table to delete from
* record = the record of type <code>RecordType</code> to delete
*/
public void deleteRecord(string, RecordType)(string table, RecordType record)
{
idAbleCheck(record);
deleteRecord(table, record.id);
}
private mixin template MemberAndType(alias record, alias typeEnforce, string memberName)
{
static if(__traits(hasMember, record, memberName))
{
static if(__traits(isSame, typeof(mixin("record."~memberName)), typeEnforce))
{
}
else
{
pragma(msg, "Member '"~memberName~"' not of type '"~typeEnforce~"'");
static assert(false);
}
}
else
{
pragma(msg, "Record does not have member '"~memberName~"'");
static assert(false);
}
}
private static void isAuthable(RecordType)(RecordType record)
{
mixin MemberAndType!(record, string, "email");
mixin MemberAndType!(record, string, "password");
mixin MemberAndType!(record, string, "passwordConfirm");
}
private static void idAbleCheck(RecordType)(RecordType record)
{
static if(__traits(hasMember, record, "id"))
{
static if(__traits(isSame, typeof(record.id), string))
{
// Do nothing as it is a-okay
}
else
{
// Must be a string
pragma(msg, "The `id` field of the record provided must be of type string");
static assert(false);
}
}
else
{
// An id field is required (TODO: ensure not a function identifier)
pragma(msg, "The provided record must have a `id` field");
static assert(false);
}
}
// TODO: Implement the streaming functionality
private void stream(string table)
{
}
}
unittest
{
import core.thread : Thread, dur;
import std.string : cmp;
PocketBase pb = new PocketBase();
struct Person
{
string id;
string name;
int age;
}
Person p1 = Person();
p1.name = "Tristan Gonzales";
p1.age = 23;
Person recordStored = pb.createRecord("dummy", p1);
pb.deleteRecord("dummy", recordStored.id);
recordStored = pb.createRecord("dummy", p1);
Thread.sleep(dur!("seconds")(3));
recordStored.age = 46;
recordStored = pb.updateRecord("dummy", recordStored);
assert(recordStored.age == 46);
Thread.sleep(dur!("seconds")(3));
Person recordFetched = pb.viewRecord!(Person)("dummy", recordStored.id);
assert(recordFetched.age == 46);
assert(cmp(recordFetched.name, "Tristan Gonzales") == 0);
assert(cmp(recordFetched.id, recordStored.id) == 0);
pb.deleteRecord("dummy", recordStored);
Person[] people = [Person(), Person()];
people[0].name = "Abby";
people[1].name = "Becky";
people[0] = pb.createRecord("dummy", people[0]);
people[1] = pb.createRecord("dummy", people[1]);
Person[] returnedPeople = pb.listRecords!(Person)("dummy");
foreach(Person returnedPerson; returnedPeople)
{
debug(dbg)
{
writeln(returnedPerson);
}
pb.deleteRecord("dummy", returnedPerson);
}
try
{
recordFetched = pb.viewRecord!(Person)("dummy", people[0].id);
assert(false);
}
catch(RecordNotFoundException e)
{
assert(cmp(e.offendingTable, "dummy") == 0 && e.offendingId == people[0].id);
}
catch(Exception e)
{
assert(false);
}
try
{
recordFetched = pb.updateRecord("dummy", people[0]);
assert(false);
}
catch(RecordNotFoundException e)
{
assert(cmp(e.offendingTable, "dummy") == 0 && e.offendingId == people[0].id);
}
catch(Exception e)
{
assert(false);
}
try
{
pb.deleteRecord("dummy", people[0]);
assert(false);
}
catch(RecordNotFoundException e)
{
assert(cmp(e.offendingTable, "dummy") == 0 && e.offendingId == people[0].id);
}
catch(Exception e)
{
assert(false);
}
}
unittest
{
import core.thread : Thread, dur;
import std.string : cmp;
PocketBase pb = new PocketBase();
struct Person
{
string id;
string email;
string username;
string password;
string passwordConfirm;
string name;
int age;
}
// Set the password to use
string passwordToUse = "bigbruh1111";
Person p1;
p1.email = "deavmi@redxen.eu";
p1.username = "deavmi";
p1.password = passwordToUse;
p1.passwordConfirm = passwordToUse;
p1.name = "Tristaniha";
p1.age = 29;
p1 = pb.createRecordAuth("dummy_auth", p1);
Person[] people = pb.listRecordsAuth!(Person)("dummy_auth", 1, 30, "(id='"~p1.id~"')");
assert(people.length == 1);
// Ensure we get our person back
assert(cmp(people[0].name, p1.name) == 0);
assert(people[0].age == p1.age);
// assert(cmp(people[0].email, p1.email) == 0);
Person person = pb.viewRecordAuth!(Person)("dummy_auth", p1.id);
// Ensure we get our person back
assert(cmp(people[0].name, p1.name) == 0);
assert(people[0].age == p1.age);
// assert(cmp(people[0].email, p1.email) == 0);
string newName = "Bababooey";
person.name = newName;
person = pb.updateRecordAuth("dummy_auth", person);
assert(cmp(person.name, newName) == 0);
string tokenIn;
Person authPerson = pb.authWithPassword!(Person)("dummy_auth", p1.username, passwordToUse, tokenIn);
// Ensure a non-empty token
assert(cmp(tokenIn, "") != 0);
writeln("Token: "~tokenIn);
// Ensure we get our person back
assert(cmp(authPerson.name, person.name) == 0);
assert(authPerson.age == person.age);
assert(cmp(authPerson.email, person.email) == 0);
// Delete the record
pb.deleteRecord("dummy_auth", p1);
}
unittest
{
import core.thread : Thread, dur;
import std.string : cmp;
PocketBase pb = new PocketBase();
struct Person
{
string id;
string name;
int age;
}
Person p1 = Person();
p1.name = "Tristan Gonzales";
p1.age = 23;
Person p2 = Person();
p2.name = p1.name~"2";
p2.age = p1.age;
p1 = pb.createRecord("dummy", p1);
p2 = pb.createRecord("dummy", p2);
Person[] people = pb.listRecords!(Person)("dummy", 1, 30, "(id='"~p1.id~"')");
assert(people.length == 1);
assert(cmp(people[0].id, p1.id) == 0);
pb.deleteRecord("dummy", p1);
people = pb.listRecords!(Person)("dummy", 1, 30, "(id='"~p1.id~"')");
assert(people.length == 0);
people = pb.listRecords!(Person)("dummy", 1, 30, "(id='"~p2.id~"' && age=24)");
assert(people.length == 0);
people = pb.listRecords!(Person)("dummy", 1, 30, "(id='"~p2.id~"' && age=23)");
assert(people.length == 1 && cmp(people[0].id, p2.id) == 0);
pb.deleteRecord("dummy", p2);
}

72
source/libpb/exceptions.d Normal file
View File

@ -0,0 +1,72 @@
module libpb.exceptions;
public abstract class PBException : Exception
{
this(string message = "")
{
super("PBException: "~message);
}
}
public final class RecordNotFoundException : PBException
{
public const string offendingTable;
public const string offendingId;
this(string table, string id)
{
this.offendingTable = table;
this.offendingId = id;
super("Could not find record '"~id~"' in table '"~offendingTable~"'");
}
}
public final class NotAuthorized : PBException
{
public const string offendingTable;
public const string offendingId;
this(string table, string id)
{
this.offendingTable = table;
this.offendingId = id;
}
}
public final class ValidationRequired : PBException
{
public const string offendingTable;
public const string offendingId;
this(string table, string id)
{
this.offendingTable = table;
this.offendingId = id;
}
}
/**
* NetworkException
*
* Thrown on an unhandled curl error
*/
public final class NetworkException : PBException
{
this()
{
}
}
public final class PocketBaseParsingException : PBException
{
}
public final class RemoteFieldMissing : PBException
{
this()
{
}
}

4
source/libpb/package.d Normal file
View File

@ -0,0 +1,4 @@
module libpb;
public import libpb.exceptions;
public import libpb.driver;