diff --git a/README.md b/README.md index 31ba1c2..18023a0 100644 --- a/README.md +++ b/README.md @@ -5,3 +5,7 @@ _Kotlin library to request postgres with native SQL queries and return JSON_ [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=postgres-json&metric=alert_status)](https://sonarcloud.io/dashboard?id=postgres-json) [![Coverage](https://sonarcloud.io/api/project_badges/measure?project=postgres-json&metric=coverage)](https://sonarcloud.io/dashboard?id=postgres-json) [![Lines of Code](https://sonarcloud.io/api/project_badges/measure?project=postgres-json&metric=ncloc)](https://sonarcloud.io/dashboard?id=postgres-json) + +* [Installation](./docs/installation.md) +* [Migrations](./docs/migrations/migrations.md) +* [Usage](./docs/usage/usage.md) diff --git a/docs/installation.md b/docs/installation.md new file mode 100644 index 0000000..9c2d9a5 --- /dev/null +++ b/docs/installation.md @@ -0,0 +1,9 @@ +# Installation + +## Gradle +```kotlin +// build.gradle.kts +dependencies { + implementation("com.github.flecomte:postgres-json:+") +} +``` \ No newline at end of file diff --git a/docs/migrations/migrations-application.md b/docs/migrations/migrations-application.md new file mode 100644 index 0000000..406dfb5 --- /dev/null +++ b/docs/migrations/migrations-application.md @@ -0,0 +1,16 @@ +# Execute migration in application +```kotlin +import fr.postgresjson.migration.Migrations +import fr.postgresjson.connexion.Connection + +val conn: Connection = TODO() +val migrations = Migrations( + conn, + this::class.java.getResource("/sql/migrations")?.toURI() ?: error("No migrations found"), + this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found") +) + +migrations.status() // Show executed and not executed migrations +migrations.runDry() // Execute migration in transaction and rollback at the end +migrations.run() // Execute migration in transaction and commit if no error +``` \ No newline at end of file diff --git a/docs/migrations/migrations-gradle.md b/docs/migrations/migrations-gradle.md new file mode 100644 index 0000000..3c82ae5 --- /dev/null +++ b/docs/migrations/migrations-gradle.md @@ -0,0 +1,37 @@ +# Execute Migrations with Gradle + +You can execute migration with a Gradle task like this: + +```kotlin +// build.gradle.kts +import fr.postgresjson.connexion.Connection +import fr.postgresjson.connexion.Requester +import fr.postgresjson.migration.Migrations + +buildscript { + dependencies { + classpath("com.github.flecomte:postgres-json:+") + } +} + +val migration by tasks.registering { + doLast { + val connection = Connection( + host = "localhost", + port = 5432, + database = "database", + username = "username", + password = "password" + ) + Migrations( + connection, + file("$buildDir/resources/main/sql/migrations").toURI(), + file("$buildDir/resources/main/sql/functions").toURI() + ).run() + } +} +``` + +```shell +$ gradle migration +``` \ No newline at end of file diff --git a/docs/migrations/migrations.md b/docs/migrations/migrations.md new file mode 100644 index 0000000..c3f019d --- /dev/null +++ b/docs/migrations/migrations.md @@ -0,0 +1,71 @@ +# Migration +## Schemas migration +Migrations are just manually written `*.sql` files that represent the database schemas. +Each file is executed one after the other in alphabetical order. +Each execution is stored in the `migration.history` table. + +A migration contains a `*.up.sql` file and a `*.down.sql` file to rollback the migration. +The content of the `*.down.sql` file is also stored in the database. +This allows the `*.down.sql` to be executed even if the code is already rollback. + +Example: +```postgresql +-- resources/sql/migrations/0000-init_schema.up.sql +create table "user" +( + id uuid default uuid_generate_v4() not null primary key, + created_at timestamptz default now() not null, + blocked_at timestamptz default null null, + username varchar(64) not null check ( username != '' and lower(username) = username) unique, + password text not null check ( password != '' ), + roles text[] default '{}' not null +); +``` + +```postgresql +-- resources/sql/migrations/0000-init_schema.down.sql +drop table if exists "user"; +``` +## Stored procedure migrations + +Migrations are also stored procedures and other functions. +Each function is updated with each migration. + +Example: +```postgresql +-- resources/sql/functions/insert_user.sql +create or replace function insert_user(inout resource json) language plpgsql as +$$ +declare + new_id uuid; +begin + insert into "user" (id, username, password, blocked_at, roles) + select + coalesce(t.id, uuid_generate_v4()), + t.username, + crypt(resource->>'password', gen_salt('bf', 8)), + case when t.blocked_at is not null then now() else null end, + t.roles + from json_populate_record(null::"user", resource) t + returning id into new_id; + + select find_user_by_id(new_id) into resource; +end; +$$; +``` + +```postgresql +-- resources/sql/functions/find_user_by_id.sql +create or replace function find_user_by_id(in _id uuid, out resource json) language plpgsql as +$$ +begin + select to_jsonb(u) - 'password' into resource + from "user" as u + where u.id = _id; +end; +$$; +``` + +* [Execute migrations in application](./migrations-application.md) +* [Execute migrations with gradle](./migrations-gradle.md) + diff --git a/docs/usage/init-connection.md b/docs/usage/init-connection.md new file mode 100644 index 0000000..6ce73d4 --- /dev/null +++ b/docs/usage/init-connection.md @@ -0,0 +1,14 @@ +# Init connection + +Before execute any query you must instantiate the connection. +```kotlin +import fr.postgresjson.connexion.Connection + +val connection = Connection( + host = "localhost", + port = 5432, + database = "mydb", + username = "john", + password = "azerty" +) +``` \ No newline at end of file diff --git a/docs/usage/raw-request.md b/docs/usage/raw-request.md new file mode 100644 index 0000000..5638ba8 --- /dev/null +++ b/docs/usage/raw-request.md @@ -0,0 +1,46 @@ +# Raw request +You can execute query directly from the code like this: +(*see [Init connection](./init-connection.md) before*) + +```kotlin +import fr.postgresjson.connexion.Connection + +val connection: Connection = TODO() + +val result: QueryResult = connection.exec( + "SELECT id FROM inventor WHERE name = :name", + mapOf("name" to "Nikola Tesla") +) +val id: String = result.rows[0].getString(0) +``` + +And if you must map the query result with an entity, you can do it like this: +```kotlin +import java.util.UUID +import fr.postgresjson.entity.Serializable +import fr.postgresjson.connexion.Connection + +val connection: Connection = TODO() + +data class Inventor( + val id: UUID = UUID.randomUUID(), + val name: String +): Serializable + +val result: Inventor? = connection.selectOne( + """ + SELECT json_build_object( + 'id', '9e65de49-712e-47ce-8bf2-dfffae53a82e', + 'name', :name + ) + """, + mapOf("name" to "Nikola Tesla") +) + +val inventor = connection.selectOne("SELECT * FROM mytable WHERE id = :id") + +val inventors: List = connection.select("SELECT * FROM mytable WHERE status = 'done'") +``` + + +See [ConnectionTest.kt](/src/test/kotlin/fr/postgresjson/ConnectionTest.kt) for more examples. \ No newline at end of file diff --git a/docs/usage/stored-procedure.md b/docs/usage/stored-procedure.md new file mode 100644 index 0000000..a5a95b1 --- /dev/null +++ b/docs/usage/stored-procedure.md @@ -0,0 +1,85 @@ +# Stored Procedure +*Execute stored procedure with requester* + +You can execute a stored procedure (previously defined in a migration) via the Requester + +To do that: + +1. First, instantiate the requester +```kotlin +import fr.postgresjson.connexion.Requester +import fr.postgresjson.connexion.Connection + +val connection: Connection = TODO() + +val requester = Requester.RequesterFactory( + connection = connection, + functionsDirectory = this::class.java.getResource("/sql/functions")?.toURI() ?: error("No sql function found") +).createRequester() +``` + +2. then, define Entities +```kotlin +import java.util.UUID +import org.joda.time.DateTime +import fr.postgresjson.entity.Serializable + +enum class Roles { ROLE_USER, ROLE_ADMIN } + +class User( + id: UUID = UUID.randomUUID(), + override var username: String, + var blockedAt: DateTime? = null, + var roles: List = emptyList() +): Serializable + +class UserForCreate( + id: UUID = UUID.randomUUID(), + username: String, + val password: String, + blockedAt: DateTime? = null, + roles: List = emptyList() +): Serializable +``` +3. and, define Repositories +*[See SQL function](./migrations.md#Stored procedure migrations)* + +```kotlin +import fr.postgresjson.connexion.Requester +import fr.postgresjson.repository.RepositoryI +import java.util.UUID + +class UserRepository(override var requester: Requester): RepositoryI { + fun findById(id: UUID): User { + return requester + .getFunction("find_user_by_id") // Use the name of the function + .selectOne( + "id" to id // You can pass parameters by their names. The underscore prefix on parameters is not required to be mapped. + ) ?: throw UserNotFound(id) // Throw exception if user not found + } + + fun insert(user: UserForCreate): User { + return requester + .getFunction("insert_user") + .selectOne("resource" to user) + } + + class UserNotFound(override val message: String?, override val cause: Throwable?): Throwable(message, cause) { + constructor(id: UUID): this("No User with ID $id", null) + } +} +``` + +4. And at last, execute queries +```kotlin +import fr.postgresjson.connexion.Requester +import java.util.UUID + +val requester: Requester = TODO() +val userRepo = UserRepository(requester) + +val user: User = userRepo.findById(UUID.fromString(id)) + +val newUser: UserForCreate = TODO() +val userInserted: User = userRepo.insert(newUser) +``` \ No newline at end of file diff --git a/docs/usage/usage.md b/docs/usage/usage.md new file mode 100644 index 0000000..cd28f73 --- /dev/null +++ b/docs/usage/usage.md @@ -0,0 +1,5 @@ +## Usage + +1. [Init connection](./init-connection.md) +2. [Raw request](./raw-request.md) +3. [Stored Procedure](./stored-procedure.md) \ No newline at end of file diff --git a/src/main/kotlin/fr/postgresjson/connexion/EmbedExecutable.kt b/src/main/kotlin/fr/postgresjson/connexion/EmbedExecutable.kt index 8060843..cf8355b 100644 --- a/src/main/kotlin/fr/postgresjson/connexion/EmbedExecutable.kt +++ b/src/main/kotlin/fr/postgresjson/connexion/EmbedExecutable.kt @@ -25,7 +25,7 @@ interface EmbedExecutable { block: SelectOneCallback = {} ): R? - /* Select Miltiples */ + /* Select Multiples */ fun select( typeReference: TypeReference>, values: List = emptyList(),