diff --git a/.gitignore b/.gitignore deleted file mode 100644 index 1a5f6b3..0000000 --- a/.gitignore +++ /dev/null @@ -1,18 +0,0 @@ -.dub -docs.json -__dummy.html -docs/ -/libpb -libpb.so -libpb.dylib -libpb.dll -libpb.a -libpb.lib -libpb-test-* -*.exe -*.pdb -*.o -*.obj -*.lst -liblibpb.a -dub.selections.json diff --git a/LICENSE b/LICENSE deleted file mode 100644 index 0a04128..0000000 --- a/LICENSE +++ /dev/null @@ -1,165 +0,0 @@ - GNU LESSER GENERAL PUBLIC LICENSE - Version 3, 29 June 2007 - - Copyright (C) 2007 Free Software Foundation, Inc. - Everyone is permitted to copy and distribute verbatim copies - of this license document, but changing it is not allowed. - - - This version of the GNU Lesser General Public License incorporates -the terms and conditions of version 3 of the GNU General Public -License, supplemented by the additional permissions listed below. - - 0. Additional Definitions. - - As used herein, "this License" refers to version 3 of the GNU Lesser -General Public License, and the "GNU GPL" refers to version 3 of the GNU -General Public License. - - "The Library" refers to a covered work governed by this License, -other than an Application or a Combined Work as defined below. - - An "Application" is any work that makes use of an interface provided -by the Library, but which is not otherwise based on the Library. -Defining a subclass of a class defined by the Library is deemed a mode -of using an interface provided by the Library. - - A "Combined Work" is a work produced by combining or linking an -Application with the Library. The particular version of the Library -with which the Combined Work was made is also called the "Linked -Version". - - The "Minimal Corresponding Source" for a Combined Work means the -Corresponding Source for the Combined Work, excluding any source code -for portions of the Combined Work that, considered in isolation, are -based on the Application, and not on the Linked Version. - - The "Corresponding Application Code" for a Combined Work means the -object code and/or source code for the Application, including any data -and utility programs needed for reproducing the Combined Work from the -Application, but excluding the System Libraries of the Combined Work. - - 1. Exception to Section 3 of the GNU GPL. - - You may convey a covered work under sections 3 and 4 of this License -without being bound by section 3 of the GNU GPL. - - 2. Conveying Modified Versions. - - If you modify a copy of the Library, and, in your modifications, a -facility refers to a function or data to be supplied by an Application -that uses the facility (other than as an argument passed when the -facility is invoked), then you may convey a copy of the modified -version: - - a) under this License, provided that you make a good faith effort to - ensure that, in the event an Application does not supply the - function or data, the facility still operates, and performs - whatever part of its purpose remains meaningful, or - - b) under the GNU GPL, with none of the additional permissions of - this License applicable to that copy. - - 3. Object Code Incorporating Material from Library Header Files. - - The object code form of an Application may incorporate material from -a header file that is part of the Library. You may convey such object -code under terms of your choice, provided that, if the incorporated -material is not limited to numerical parameters, data structure -layouts and accessors, or small macros, inline functions and templates -(ten or fewer lines in length), you do both of the following: - - a) Give prominent notice with each copy of the object code that the - Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the object code with a copy of the GNU GPL and this license - document. - - 4. Combined Works. - - You may convey a Combined Work under terms of your choice that, -taken together, effectively do not restrict modification of the -portions of the Library contained in the Combined Work and reverse -engineering for debugging such modifications, if you also do each of -the following: - - a) Give prominent notice with each copy of the Combined Work that - the Library is used in it and that the Library and its use are - covered by this License. - - b) Accompany the Combined Work with a copy of the GNU GPL and this license - document. - - c) For a Combined Work that displays copyright notices during - execution, include the copyright notice for the Library among - these notices, as well as a reference directing the user to the - copies of the GNU GPL and this license document. - - d) Do one of the following: - - 0) Convey the Minimal Corresponding Source under the terms of this - License, and the Corresponding Application Code in a form - suitable for, and under terms that permit, the user to - recombine or relink the Application with a modified version of - the Linked Version to produce a modified Combined Work, in the - manner specified by section 6 of the GNU GPL for conveying - Corresponding Source. - - 1) Use a suitable shared library mechanism for linking with the - Library. A suitable mechanism is one that (a) uses at run time - a copy of the Library already present on the user's computer - system, and (b) will operate properly with a modified version - of the Library that is interface-compatible with the Linked - Version. - - e) Provide Installation Information, but only if you would otherwise - be required to provide such information under section 6 of the - GNU GPL, and only to the extent that such information is - necessary to install and execute a modified version of the - Combined Work produced by recombining or relinking the - Application with a modified version of the Linked Version. (If - you use option 4d0, the Installation Information must accompany - the Minimal Corresponding Source and Corresponding Application - Code. If you use option 4d1, you must provide the Installation - Information in the manner specified by section 6 of the GNU GPL - for conveying Corresponding Source.) - - 5. Combined Libraries. - - You may place library facilities that are a work based on the -Library side by side in a single library together with other library -facilities that are not Applications and are not covered by this -License, and convey such a combined library under terms of your -choice, if you do both of the following: - - a) Accompany the combined library with a copy of the same work based - on the Library, uncombined with any other library facilities, - conveyed under the terms of this License. - - b) Give prominent notice with the combined library that part of it - is a work based on the Library, and explaining where to find the - accompanying uncombined form of the same work. - - 6. Revised Versions of the GNU Lesser General Public License. - - The Free Software Foundation may publish revised and/or new versions -of the GNU Lesser General Public License from time to time. Such new -versions will be similar in spirit to the present version, but may -differ in detail to address new problems or concerns. - - Each version is given a distinguishing version number. If the -Library as you received it specifies that a certain numbered version -of the GNU Lesser General Public License "or any later version" -applies to it, you have the option of following the terms and -conditions either of that published version or of any later version -published by the Free Software Foundation. If the Library as you -received it does not specify a version number of the GNU Lesser -General Public License, you may choose any version of the GNU Lesser -General Public License ever published by the Free Software Foundation. - - If the Library as you received it specifies that a proxy can decide -whether future versions of the GNU Lesser General Public License shall -apply, that proxy's public statement of acceptance of any version is -permanent authorization for you to choose that version for the -Library. diff --git a/README.md b/README.md deleted file mode 100644 index 86f78cf..0000000 --- a/README.md +++ /dev/null @@ -1,197 +0,0 @@ -![](branding/logo.png) - -libpb -===== - -#### _PocketBase wrapper with serializer/deserializer support_ - ----- - -## 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: - -```d -PocketBase pb = new PocketBase("http://127.0.0.1:8090/api/"); -``` - -### Serialization - -This is just to show off the serialization method `serializeRecord(RecordType)` which returns a `JSONValue` struct: - -```d -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 = 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()); -} -``` - -### Deserialization - -This is to show off deserialization method `fromJSON(RecordType)(JSONValue jsonIn)` which returns a struct of type `RecordType` (so far most features are implemented): - -```d -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 = 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 -``` - -### Record management - -#### Normal collections - -Below we have a few calls like create and delete: - -```d -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); -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: - -```bash -dub test -ddbg -``` - -Run pocketbase on the default port and then use the schema provided as `dummy.json` to test with (in a collection named `dummy`). - -## License - -See [LICENSE](LICENSE) diff --git a/branding/logo.xcf b/branding/logo.xcf deleted file mode 100644 index 597795a..0000000 Binary files a/branding/logo.xcf and /dev/null differ diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..1e1b488 --- /dev/null +++ b/docs/index.md @@ -0,0 +1,29 @@ +libpb +===== + +#### _PocketBase wrapper with serializer/deserializer support_ + +---- + +## API + +For full API documentation see [DUB API Spec](https://libpb.dpldocs.info/index.html). + +## Installing + +In order to use libpb in your project simply run: + +```bash +dub add libpb +``` + +And then in your D program import as follows: + +```d +import libpb; +``` + +### Dependencies + +This requires that you have the `libcurl` libraries available for +linking against. \ No newline at end of file diff --git a/branding/logo.png b/docs/logo.png similarity index 100% rename from branding/logo.png rename to docs/logo.png diff --git a/docs/record management/auth collections.md b/docs/record management/auth collections.md new file mode 100644 index 0000000..3b59fe4 --- /dev/null +++ b/docs/record management/auth collections.md @@ -0,0 +1,29 @@ +`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); +``` \ No newline at end of file diff --git a/docs/record management/base collections.md b/docs/record management/base collections.md new file mode 100644 index 0000000..663403b --- /dev/null +++ b/docs/record management/base collections.md @@ -0,0 +1,45 @@ +Base collections +================ + +Below we have a few calls like create and delete: + +```d +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); +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); +} +``` \ No newline at end of file diff --git a/docs/server initialization.md b/docs/server initialization.md new file mode 100644 index 0000000..5d845b4 --- /dev/null +++ b/docs/server initialization.md @@ -0,0 +1,8 @@ +Server initiation +================= + +Firstly we create a new PocketBase instance to manage our server: + +```d +PocketBase pb = new PocketBase("http://127.0.0.1:8090/api/"); +``` \ No newline at end of file diff --git a/docs/translation/deserialization.md b/docs/translation/deserialization.md new file mode 100644 index 0000000..6fe8b8d --- /dev/null +++ b/docs/translation/deserialization.md @@ -0,0 +1,42 @@ +Deserialization +=============== + +This is to show off deserialization method `fromJSON(RecordType)(JSONValue jsonIn)` which returns a struct of type `RecordType` (so far most features are implemented): + +```d +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 = 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 +``` \ No newline at end of file diff --git a/docs/translation/serialization.md b/docs/translation/serialization.md new file mode 100644 index 0000000..c79d6e4 --- /dev/null +++ b/docs/translation/serialization.md @@ -0,0 +1,38 @@ +Serialization +============= + +This is just to show off the serialization method `serializeRecord(RecordType)` which returns a `JSONValue` struct: + +```d +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 = 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()); +} +``` \ No newline at end of file diff --git a/dub.json b/dub.json deleted file mode 100644 index e55e8ef..0000000 --- a/dub.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "authors": [ - "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" -} diff --git a/dummy.json b/dummy.json deleted file mode 100644 index adbc2ca..0000000 --- a/dummy.json +++ /dev/null @@ -1,89 +0,0 @@ -[ - { - "id": "iir9gleipa4n6lf", - "name": "dummy", - "type": "base", - "system": false, - "schema": [ - { - "id": "fkyktfrf", - "name": "name", - "type": "text", - "system": false, - "required": false, - "unique": false, - "options": { - "min": null, - "max": null, - "pattern": "" - } - }, - { - "id": "31mcl2nq", - "name": "age", - "type": "number", - "system": false, - "required": false, - "unique": false, - "options": { - "min": null, - "max": null - } - } - ], - "listRule": "", - "viewRule": "", - "createRule": "", - "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 - } - } -] \ No newline at end of file diff --git a/mkdocs.yml b/mkdocs.yml new file mode 100644 index 0000000..57230a0 --- /dev/null +++ b/mkdocs.yml @@ -0,0 +1,5 @@ +site_name: libpb +theme: alabaster + +extra: + logo: logo.png \ No newline at end of file diff --git a/source/libpb/driver.d b/source/libpb/driver.d deleted file mode 100644 index f470fb6..0000000 --- a/source/libpb/driver.d +++ /dev/null @@ -1,923 +0,0 @@ -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 RecordType - */ - 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 RecordType - */ - 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 RecordType - */ - 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 RecordType - */ - 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 RecordType - */ - 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 RecordType - */ - 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 RecordType - */ - 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 RecordType - */ - 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 RecordType - */ - 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 RecordType to update - * - * Returns: The updated RecordType - */ - 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 RecordType to update - * - * Returns: The updated RecordType - */ - 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 RecordType to update - * isAuthCollection = true if this is an auth collection, false - * for base collection - * - * Returns: The updated RecordType - */ - 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 RecordType 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); -} \ No newline at end of file diff --git a/source/libpb/exceptions.d b/source/libpb/exceptions.d deleted file mode 100644 index 6744988..0000000 --- a/source/libpb/exceptions.d +++ /dev/null @@ -1,72 +0,0 @@ -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() - { - - } -} \ No newline at end of file diff --git a/source/libpb/package.d b/source/libpb/package.d deleted file mode 100644 index 7bf3fe1..0000000 --- a/source/libpb/package.d +++ /dev/null @@ -1,4 +0,0 @@ -module libpb; - -public import libpb.exceptions; -public import libpb.driver; \ No newline at end of file