Compare commits

...

20 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
11 changed files with 288 additions and 269 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
=====
@ -169,7 +171,7 @@ p1.username = "deavmi";
p1.password = "bigbruh1111";
p1.passwordConfirm = "bigbruh1111";
p1 = pb.createRecord("dummy_auth", p1, true);
p1 = pb.createRecordAuth("dummy_auth", p1);
pb.deleteRecord("dummy_auth", p1);
```

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,9 +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",
"libs":["curl"]
"targetType": "library"
}

View File

@ -46,7 +46,7 @@
"schema": [
{
"id": "psy7unkl",
"name": "bruh",
"name": "name",
"type": "text",
"system": false,
"required": false,
@ -56,6 +56,18 @@
"max": null,
"pattern": ""
}
},
{
"id": "jroziygp",
"name": "age",
"type": "number",
"system": false,
"required": false,
"unique": false,
"options": {
"min": null,
"max": null
}
}
],
"listRule": "",

View File

@ -1,129 +0,0 @@
module libpb.deserialization;
import std.json;
import std.traits : FieldTypeTuple, FieldNameTuple;
public RecordType fromJSON(RecordType)(JSONValue jsonIn)
{
RecordType record;
// 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;
}
unittest
{
import std.string : cmp;
import std.stdio : writeln;
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 = 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
}

View File

@ -6,8 +6,7 @@ import std.net.curl;
import std.conv : to;
import std.string : cmp;
import libpb.exceptions;
import libpb.serialization;
import libpb.deserialization;
import jstruct : fromJSON, SerializationError, serializeRecord;
private mixin template AuthTokenHeader(alias http, PocketBase pbInstance)
@ -56,16 +55,51 @@ public class PocketBase
}
/**
* List all of the records in the given table
* 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();
@ -105,8 +139,24 @@ public class PocketBase
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);
}
@ -137,17 +187,14 @@ public class PocketBase
{
throw new PocketBaseParsingException();
}
}
public RecordType createRecordAuth(string, RecordType)(string table, RecordType item)
{
mixin isAuthable!(RecordType);
return createRecord(table, item, true);
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
* Creates a record in the given table
* Creates a record in the given authentication table
*
* Params:
* table = the table to create the record in
@ -155,7 +202,38 @@ public class PocketBase
*
* Returns: An instance of the created <code>RecordType</code>
*/
public RecordType createRecord(string, RecordType)(string table, RecordType item, bool isAuthCollection = false)
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);
@ -199,6 +277,11 @@ public class PocketBase
}
catch(HTTPStatusException e)
{
debug(dbg)
{
writeln("createRecord_internal: "~e.toString());
}
if(e.status == 403)
{
throw new NotAuthorized(table, item.id);
@ -221,10 +304,25 @@ public class PocketBase
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
// TODO: Add comment
/**
* 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);
@ -257,7 +355,6 @@ public class PocketBase
recordResponse["email"] = "";
}
recordOut = fromJSON!(RecordType)(recordResponse);
// Store the token
@ -286,10 +383,14 @@ public class PocketBase
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
* View the given record by id
* View the given record by id (base collections)
*
* Params:
* table = the table to lookup the record in
@ -298,6 +399,37 @@ public class PocketBase
* 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;
@ -311,6 +443,21 @@ public class PocketBase
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;
@ -335,11 +482,30 @@ public class PocketBase
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
* Updates the given record in the given table, returning the
* updated record
* 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
@ -348,6 +514,23 @@ public class PocketBase
* 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);
@ -369,6 +552,21 @@ public class PocketBase
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;
@ -401,6 +599,10 @@ public class PocketBase
{
throw new PocketBaseParsingException();
}
catch(SerializationError e)
{
throw new RemoteFieldMissing();
}
}
/**
@ -452,7 +654,7 @@ public class PocketBase
deleteRecord(table, record.id);
}
mixin template MemberAndType(alias record, alias typeEnforce, string memberName)
private mixin template MemberAndType(alias record, alias typeEnforce, string memberName)
{
static if(__traits(hasMember, record, memberName))
{
@ -638,6 +840,30 @@ unittest
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);
@ -646,14 +872,11 @@ unittest
writeln("Token: "~tokenIn);
// Ensure we get our person back
assert(cmp(authPerson.name, p1.name) == 0);
assert(authPerson.age == p1.age);
assert(cmp(authPerson.email, p1.email) == 0);
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);
}

View File

@ -44,7 +44,11 @@ public final class ValidationRequired : PBException
}
/**
* NetworkException
*
* Thrown on an unhandled curl error
*/
public final class NetworkException : PBException
{
this()
@ -57,3 +61,12 @@ public final class PocketBaseParsingException : PBException
{
}
public final class RemoteFieldMissing : PBException
{
this()
{
}
}

View File

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

View File

@ -1,109 +0,0 @@
module libpb.serialization;
import std.json;
import std.conv : to;
import std.traits : FieldTypeTuple, FieldNameTuple;
public JSONValue serializeRecord(RecordType)(RecordType record)
{
// 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;
}
// Test serialization of a struct to JSON
private enum EnumType
{
DOG,
CAT
}
unittest
{
import std.algorithm.searching : canFind;
import std.string : cmp;
import std.stdio : writeln;
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 = 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());
}
}