From 6b5d043335449c8badebfd7863a8fa04cf0c8f8a Mon Sep 17 00:00:00 2001 From: ab Date: Tue, 7 Oct 2025 21:21:47 +0200 Subject: [PATCH 1/3] Testing with small_fix nano branch - not working --- .../sentinel/service/PostgreSqlService.java | 30 ++++++++++++------- .../controller/UserControllerTest.java | 6 ++-- 2 files changed, 22 insertions(+), 14 deletions(-) diff --git a/src/main/java/org/ab/sentinel/service/PostgreSqlService.java b/src/main/java/org/ab/sentinel/service/PostgreSqlService.java index 69ab7fe..41e7207 100644 --- a/src/main/java/org/ab/sentinel/service/PostgreSqlService.java +++ b/src/main/java/org/ab/sentinel/service/PostgreSqlService.java @@ -18,6 +18,7 @@ import org.ab.sentinel.jooq.tables.records.IntegrationsRecord; import org.ab.sentinel.jooq.tables.records.UsersRecord; import org.jooq.DSLContext; +import org.jooq.exception.DataAccessException; import org.jooq.impl.DSL; import org.nanonative.nano.core.model.Service; import org.nanonative.nano.helper.event.model.Event; @@ -66,7 +67,7 @@ public Object onFailure(final Event event) { @Override public void onEvent(final Event event) { - event.channel(AppEvents.ADD_USER).ifPresent(ev -> ev.respond(saveUser(ev.payload()))); + event.channel(AppEvents.ADD_USER).ifPresent(this::saveUser); event.channel(AppEvents.FETCH_USER).ifPresent(ev -> ev.respond(fetchUser(ev.payload()))); event.channel(AppEvents.FETCH_APPS).ifPresent(ev -> ev.respond(getApps())); event.channel(AppEvents.APP_INT_REQ).ifPresent(ev -> ev.respond(saveNewUserIntegration(ev.payload()))); @@ -92,15 +93,24 @@ private UsersRecord fetchUser(final String email) { return user; } - private UsersRecord saveUser(final UserDto user) { - final UsersRecord userRecord = dsl.transactionResult(configuration -> { - DSLContext ctx = DSL.using(configuration); - if (ctx.fetchExists(ctx.selectOne().from(USERS).where(USERS.EMAIL.eq(user.email())))) { - return null; - } - return ctx.insertInto(USERS).set(USERS.EMAIL, user.email()).set(USERS.NAME, user.name()).set(USERS.PASSWORD_HASH, user.passwordHash()).returning(USERS.ID).fetchOne(); - }); - return userRecord; + private void saveUser(final Event event) { + final UserDto user = event.payload(); + try { + dsl.transactionResult(configuration -> { + DSLContext ctx = DSL.using(configuration); + if (ctx.fetchExists(ctx.selectOne().from(USERS).where(USERS.EMAIL.eq(user.email())))) { + event.error(new RuntimeException("Email already registered")); + return null; + } else { + UsersRecord ur = ctx.insertInto(USERS).set(USERS.EMAIL, user.email()).set(USERS.NAME, user.name()).set(USERS.PASSWORD_HASH, user.passwordHash()).returning(USERS.ID).fetchOne(); + event.respond(ur); + return ur; + } + }); + } catch (DataAccessException dae) { + context.error(() -> "saveUser::jOOQ exception: {}", dae); + event.error(dae); + } } @Override diff --git a/src/test/java/org/ab/sentinel/controller/UserControllerTest.java b/src/test/java/org/ab/sentinel/controller/UserControllerTest.java index d519007..0250637 100644 --- a/src/test/java/org/ab/sentinel/controller/UserControllerTest.java +++ b/src/test/java/org/ab/sentinel/controller/UserControllerTest.java @@ -38,16 +38,14 @@ public static void initializeDB() { void registerUser() { final Nano nano = new Nano(Map.of("app_profiles", "dev"), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient()); - Context ctx = NanoUtils.readProfiles(nano.context(UserControllerTest.class)); - ctx.newEvent(EVENT_CONFIG_CHANGE, () -> Map.of( + nano.context().newEvent(EVENT_CONFIG_CHANGE, () -> Map.of( CONFIG_DB_HOST, dbProp.dbHost(), CONFIG_DB_PORT, dbProp.dbPort(), // TODO: Remove the below line as password is already mentioned in dev properties file and this is a work around CONFIG_DB_PASS, dbProp.dbPass() )).broadcast(true).send(); - ctx - .subscribeEvent(EVENT_HTTP_REQUEST, UserController::registerUser); + nano.subscribeEvent(EVENT_HTTP_REQUEST, UserController::registerUser); final HttpObject result = new HttpObject() .methodType(HttpMethod.POST) From a01eacd08554d8da038a41de0845a7a5a13df44f Mon Sep 17 00:00:00 2001 From: ab Date: Sun, 19 Oct 2025 02:53:30 +0200 Subject: [PATCH 2/3] Refactored PostgreSql svc to be generic --- pom.xml | 24 ++- src/main/java/org/ab/sentinel/App.java | 4 +- src/main/java/org/ab/sentinel/DbEvents.java | 32 ++++ .../sentinel/controller/UserController.java | 2 +- src/main/java/org/ab/sentinel/db/JooqDao.java | 101 +++++++++++++ .../ab/sentinel/db/api/DeleteByCondition.java | 8 + .../java/org/ab/sentinel/db/api/FetchMap.java | 8 + .../sentinel/db/api/FetchOneByCondition.java | 8 + .../ab/sentinel/db/api/InsertAndReturn.java | 9 ++ .../org/ab/sentinel/db/api/UpdateById.java | 10 ++ .../org/ab/sentinel/service/AppService.java | 137 ++++++++++++++++++ .../sentinel/service/PostgreSqlService.java | 129 +++++++---------- .../controller/UserControllerTest.java | 9 +- 13 files changed, 396 insertions(+), 85 deletions(-) create mode 100644 src/main/java/org/ab/sentinel/DbEvents.java create mode 100644 src/main/java/org/ab/sentinel/db/JooqDao.java create mode 100644 src/main/java/org/ab/sentinel/db/api/DeleteByCondition.java create mode 100644 src/main/java/org/ab/sentinel/db/api/FetchMap.java create mode 100644 src/main/java/org/ab/sentinel/db/api/FetchOneByCondition.java create mode 100644 src/main/java/org/ab/sentinel/db/api/InsertAndReturn.java create mode 100644 src/main/java/org/ab/sentinel/db/api/UpdateById.java create mode 100644 src/main/java/org/ab/sentinel/service/AppService.java diff --git a/pom.xml b/pom.xml index ffc9700..b983f70 100644 --- a/pom.xml +++ b/pom.xml @@ -15,7 +15,7 @@ 21 UTF-8 3.19.10 - 2025.1.3 + 2025.10.2750958 5.1.0 42.7.3 1.7.36 @@ -103,6 +103,11 @@ postgresql test + + org.nanonative + devconsole + 1.0.0 + @@ -150,6 +155,23 @@ + + io.github.absketches + codegen-concrete-classes-maven-plugin + 1.0.0 + + + nano-service-index + process-classes + + generate + + + true + + + + org.jooq diff --git a/src/main/java/org/ab/sentinel/App.java b/src/main/java/org/ab/sentinel/App.java index b8d4614..02297a2 100644 --- a/src/main/java/org/ab/sentinel/App.java +++ b/src/main/java/org/ab/sentinel/App.java @@ -1,9 +1,11 @@ package org.ab.sentinel; import org.ab.sentinel.controller.AppController; +import org.ab.sentinel.service.AppService; import org.ab.sentinel.service.PostgreSqlService; import org.ab.sentinel.controller.UserController; import org.ab.sentinel.service.integrations.GithubIntegrationService; +import org.nanonative.devconsole.service.DevConsoleService; import org.nanonative.nano.core.Nano; import org.nanonative.nano.services.http.HttpClient; import org.nanonative.nano.services.http.HttpServer; @@ -23,7 +25,7 @@ public static void main(String[] args) { CONFIG_LOG_LEVEL, DEBUG, CONFIG_LOG_FORMATTER, "console", CONFIG_SERVICE_HTTP_PORT, "8080" - ), new HttpServer(), new HttpClient(), new PostgreSqlService(), new GithubIntegrationService()); + ), new HttpServer(), new HttpClient(), new PostgreSqlService(), new DevConsoleService(), new GithubIntegrationService(), new AppService()); nano.subscribeEvent(EVENT_HTTP_REQUEST, event -> event.payloadOpt() .filter(HttpObject::isMethodOptions) diff --git a/src/main/java/org/ab/sentinel/DbEvents.java b/src/main/java/org/ab/sentinel/DbEvents.java new file mode 100644 index 0000000..5aeb183 --- /dev/null +++ b/src/main/java/org/ab/sentinel/DbEvents.java @@ -0,0 +1,32 @@ +package org.ab.sentinel; + +import org.ab.sentinel.db.api.DeleteByCondition; +import org.ab.sentinel.db.api.FetchMap; +import org.ab.sentinel.db.api.FetchOneByCondition; +import org.ab.sentinel.db.api.InsertAndReturn; +import org.ab.sentinel.db.api.UpdateById; +import org.jooq.Record; +import org.nanonative.nano.helper.event.model.Channel; + +import static org.nanonative.nano.helper.event.model.Channel.registerChannelId; + +import java.util.Map; + +public final class DbEvents { + private DbEvents() {} + + public static final Channel FETCH_ONE = + registerChannelId("DB_FETCH_ONE", FetchOneByCondition.class, Record.class); + + public static final Channel INSERT_RETURNING = + registerChannelId("DB_INSERT_RETURNING", InsertAndReturn.class, Record.class); + + public static final Channel UPDATE_BY_ID_RETURNING = + registerChannelId("DB_UPDATE_BY_ID_RETURNING", UpdateById.class, Record.class); + + public static final Channel DELETE_WHERE = + registerChannelId("DB_DELETE_WHERE", DeleteByCondition.class, Integer.class); + + public static final Channel FETCH_MAP = + registerChannelId("DB_FETCH_MAP", FetchMap.class, Map.class); +} diff --git a/src/main/java/org/ab/sentinel/controller/UserController.java b/src/main/java/org/ab/sentinel/controller/UserController.java index f213c5f..97017ff 100644 --- a/src/main/java/org/ab/sentinel/controller/UserController.java +++ b/src/main/java/org/ab/sentinel/controller/UserController.java @@ -66,7 +66,7 @@ public static void registerUser(Event event) { final String token = JwtHelper.createToken(Map.of("sub", user.getId(), "email", email), TOKEN_TTL); event.respond(jsonOk(event, Map.of("token", token))); - }, () -> event.respond(problem(event, 422, "Email already registered"))); + }, () -> event.error(new RuntimeException("ADD_USER failed"))); }); } } diff --git a/src/main/java/org/ab/sentinel/db/JooqDao.java b/src/main/java/org/ab/sentinel/db/JooqDao.java new file mode 100644 index 0000000..3197555 --- /dev/null +++ b/src/main/java/org/ab/sentinel/db/JooqDao.java @@ -0,0 +1,101 @@ +package org.ab.sentinel.db; + +import org.jooq.DSLContext; +import org.jooq.Table; +import org.jooq.TableField; +import org.jooq.Condition; +import org.jooq.UpdatableRecord; +import org.jooq.Record; +import org.jooq.exception.DataAccessException; +import org.jooq.impl.DSL; + +import java.util.Objects; +import java.util.Map; +import java.util.Optional; +import java.util.function.Function; + +public class JooqDao { + + private final DSLContext dsl; + + public JooqDao(DSLContext dsl) { + this.dsl = Objects.requireNonNull(dsl); + } + + /* ===================== READS (no tx required) ===================== */ + + public Optional getOne(Table table, Condition condition) { + return Optional.ofNullable(dsl.selectFrom(table).where(condition).fetchOne()); + } + + public boolean exists(Table table, Condition condition) { + return dsl.fetchExists(dsl.selectOne().from(table).where(condition)); + } + + public Map fetchMap(Table table, TableField keyField) { + return dsl.selectFrom(table).fetchMap(keyField); + } + + /* ===================== TRANSACTION ENTRYPOINT ===================== */ + + + public T writeTx(Function work) throws DataAccessException { + return dsl.transactionResult(cfg -> work.apply(new Tx(DSL.using(cfg)))); + } + + /* ===================== Tx based sql ===================== */ + + public > R insertReturning(Table table, Map, Object> values) { + return writeTx(tx -> tx.insertReturning(table, values)); + } + + public , ID> Optional updateByIdReturning( + Table table, TableField idField, ID id, Map, Object> values) { + return writeTx(tx -> tx.updateByIdReturning(table, idField, id, values)); + } + + public int deleteWhere(Table table, Condition condition) { + return writeTx(tx -> tx.deleteWhere(table, condition)); + } + + /* ===================== TX-SCOPED VIEW ===================== */ + + public static final class Tx { + private final DSLContext ctx; + + private Tx(DSLContext ctx) {this.ctx = ctx;} + + public > R insertReturning(Table table, Map, Object> values) { + R rec = ctx.newRecord(table); + setAll(rec, values); + rec.store(); + rec.refresh(); + return rec; + } + + public , ID> Optional updateByIdReturning( + Table table, TableField idField, ID id, Map, Object> values) { + R rec = ctx.selectFrom(table) + .where(idField.eq(id)) + .forUpdate() // lock row to avoid races + .fetchOne(); + if (null == rec) + return Optional.empty(); + setAll(rec, values); + rec.update(); + rec.refresh(); + return Optional.of(rec); + } + + public int deleteWhere(Table table, Condition condition) { + return ctx.deleteFrom(table).where(condition).execute(); + } + + private void setAll(UpdatableRecord rec, Map, Object> values) { + values.forEach((f, v) -> { + TableField tf = f; + rec.set(tf, v); + }); + } + } +} diff --git a/src/main/java/org/ab/sentinel/db/api/DeleteByCondition.java b/src/main/java/org/ab/sentinel/db/api/DeleteByCondition.java new file mode 100644 index 0000000..23ad504 --- /dev/null +++ b/src/main/java/org/ab/sentinel/db/api/DeleteByCondition.java @@ -0,0 +1,8 @@ +package org.ab.sentinel.db.api; + +import org.jooq.Condition; +import org.jooq.Table; + +import java.io.Serializable; + +public record DeleteByCondition(Table table, Condition condition) implements Serializable {} diff --git a/src/main/java/org/ab/sentinel/db/api/FetchMap.java b/src/main/java/org/ab/sentinel/db/api/FetchMap.java new file mode 100644 index 0000000..ca25348 --- /dev/null +++ b/src/main/java/org/ab/sentinel/db/api/FetchMap.java @@ -0,0 +1,8 @@ +package org.ab.sentinel.db.api; + +import org.jooq.Table; +import org.jooq.TableField; + +import java.io.Serializable; + +public record FetchMap(Table table, TableField keyField) implements Serializable {} diff --git a/src/main/java/org/ab/sentinel/db/api/FetchOneByCondition.java b/src/main/java/org/ab/sentinel/db/api/FetchOneByCondition.java new file mode 100644 index 0000000..db50e05 --- /dev/null +++ b/src/main/java/org/ab/sentinel/db/api/FetchOneByCondition.java @@ -0,0 +1,8 @@ +package org.ab.sentinel.db.api; + +import org.jooq.Condition; +import org.jooq.Table; + +import java.io.Serializable; + +public record FetchOneByCondition(Table table, Condition condition) implements Serializable {} diff --git a/src/main/java/org/ab/sentinel/db/api/InsertAndReturn.java b/src/main/java/org/ab/sentinel/db/api/InsertAndReturn.java new file mode 100644 index 0000000..b564cef --- /dev/null +++ b/src/main/java/org/ab/sentinel/db/api/InsertAndReturn.java @@ -0,0 +1,9 @@ +package org.ab.sentinel.db.api; + +import org.jooq.Table; +import org.jooq.TableField; + +import java.io.Serializable; +import java.util.Map; + +public record InsertAndReturn(Table table, Map, Object> values) implements Serializable {} diff --git a/src/main/java/org/ab/sentinel/db/api/UpdateById.java b/src/main/java/org/ab/sentinel/db/api/UpdateById.java new file mode 100644 index 0000000..cc229f6 --- /dev/null +++ b/src/main/java/org/ab/sentinel/db/api/UpdateById.java @@ -0,0 +1,10 @@ +package org.ab.sentinel.db.api; + +import org.jooq.Table; +import org.jooq.TableField; + +import java.io.Serializable; +import java.util.Map; + +public record UpdateById(Table table, TableField idField, Object id, + Map, Object> values) implements Serializable {} diff --git a/src/main/java/org/ab/sentinel/service/AppService.java b/src/main/java/org/ab/sentinel/service/AppService.java new file mode 100644 index 0000000..17cd01e --- /dev/null +++ b/src/main/java/org/ab/sentinel/service/AppService.java @@ -0,0 +1,137 @@ +package org.ab.sentinel.service; + +import berlin.yuna.typemap.model.TypeMapI; +import org.ab.sentinel.AppEvents; +import org.ab.sentinel.dto.AppDto; +import org.ab.sentinel.dto.UserDto; +import org.ab.sentinel.dto.integrations.AppIntegrationRequestDto; + +import static org.ab.sentinel.jooq.Tables.APPS; +import static org.ab.sentinel.jooq.tables.Users.USERS; +import static org.ab.sentinel.jooq.Tables.INTEGRATIONS; + +import org.ab.sentinel.jooq.tables.records.AppsRecord; +import org.ab.sentinel.jooq.tables.records.UsersRecord; +import org.ab.sentinel.jooq.tables.records.IntegrationsRecord; + +import org.ab.sentinel.DbEvents; +import org.ab.sentinel.db.api.FetchMap; +import org.ab.sentinel.db.api.FetchOneByCondition; +import org.ab.sentinel.db.api.InsertAndReturn; + +import org.jooq.TableField; +import org.nanonative.nano.core.model.Service; +import org.nanonative.nano.helper.event.model.Event; + +import java.nio.charset.StandardCharsets; +import java.time.ZoneOffset; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +public class AppService extends Service { + + @Override + public void start() {} + + @Override + public void stop() {} + + @Override + public Object onFailure(Event event) {return null;} + + @Override + public void onEvent(final Event event) { + + // =================== FETCH_APPS =================== + event.channel(AppEvents.FETCH_APPS).ifPresent(ev -> { + ev.context().newEvent(DbEvents.FETCH_MAP, () -> new FetchMap(APPS, APPS.NAME)).send().responseOpt() + .ifPresentOrElse(obj -> { + @SuppressWarnings("unchecked") + Map appMap = (Map) obj; + + Map out = appMap.entrySet().stream() + .collect(Collectors.toMap( + Map.Entry::getKey, + e -> toDto(e.getValue()), + (a, b) -> a, + LinkedHashMap::new + )); + ev.respond(out); + }, () -> ev.error(new RuntimeException("No Apps found"))); + }); + + // =================== ADD_USER =================== + event.channel(AppEvents.ADD_USER).ifPresent(ev -> { + UserDto user = ev.payload(); + + boolean exists = ev.context() + .newEvent(DbEvents.FETCH_ONE, () -> new FetchOneByCondition(USERS, USERS.EMAIL.eq(user.email()))) + .send() + .responseOpt() + .isPresent(); + + if (exists) { + ev.error(new RuntimeException("Email already registered")); + return; + } + + var values = new LinkedHashMap, Object>(); + values.put(USERS.EMAIL, user.email()); + values.put(USERS.NAME, user.name()); + values.put(USERS.PASSWORD_HASH, user.passwordHash()); + + ev.context() + .newEvent(DbEvents.INSERT_RETURNING, () -> new InsertAndReturn(USERS, values)) + .send() + .responseOpt() + .ifPresentOrElse(obj -> { + UsersRecord ur = (UsersRecord) obj; + ev.respond(ur); + }, () -> ev.error(new RuntimeException("User insert failed"))); + }); + + // =================== FETCH_USER =================== + event.channel(AppEvents.FETCH_USER).ifPresent(ev -> { + String email = ev.payload(); + + ev.context() + .newEvent(DbEvents.FETCH_ONE, () -> new FetchOneByCondition(USERS, USERS.EMAIL.eq(email))) + .send() + .responseOpt() + .ifPresentOrElse(obj -> { + UsersRecord user = (UsersRecord) obj; + ev.respond(user); + }, () -> ev.respond(null)); + }); + + // =================== APP_INT_REQ =================== + event.channel(AppEvents.APP_INT_REQ).ifPresent(ev -> { + AppIntegrationRequestDto req = ev.payload(); + + var values = new LinkedHashMap, Object>(); + values.put(INTEGRATIONS.APP_ID, req.appId()); + values.put(INTEGRATIONS.USER_ID, req.userId()); + values.put(INTEGRATIONS.SCOPES, req.scopes().split(",")); + values.put(INTEGRATIONS.ACCESS_TOKEN_ENC, req.accessToken().getBytes(StandardCharsets.UTF_8)); + values.put(INTEGRATIONS.EXPIRES_AT, req.expiresAt().atOffset(ZoneOffset.UTC)); + + ev.context() + .newEvent(DbEvents.INSERT_RETURNING, () -> new InsertAndReturn(INTEGRATIONS, values)) + .send() + .responseOpt() + .ifPresentOrElse(obj -> { + IntegrationsRecord rec = (IntegrationsRecord) obj; + ev.respond(rec); + }, () -> ev.error(new RuntimeException("Integration insert failed"))); + }); + } + + @Override + public void configure(final TypeMapI changes, final TypeMapI merged) {} + + private AppDto toDto(AppsRecord r) { + var meta = (r.getMetadata() == null) ? null : r.getMetadata().data(); + return new AppDto(r.getId(), r.getName(), r.getLogoUrl(), meta); + } +} diff --git a/src/main/java/org/ab/sentinel/service/PostgreSqlService.java b/src/main/java/org/ab/sentinel/service/PostgreSqlService.java index 41e7207..230656f 100644 --- a/src/main/java/org/ab/sentinel/service/PostgreSqlService.java +++ b/src/main/java/org/ab/sentinel/service/PostgreSqlService.java @@ -1,53 +1,48 @@ package org.ab.sentinel.service; import berlin.yuna.typemap.model.TypeMapI; -import org.ab.sentinel.AppEvents; - -import static org.ab.sentinel.jooq.Tables.APPS; -import static org.ab.sentinel.jooq.Tables.INTEGRATIONS; -import static org.ab.sentinel.jooq.tables.Users.USERS; -import static org.nanonative.nano.helper.config.ConfigRegister.registerConfig; - import org.ab.sentinel.db.DataSourceConfig; import org.ab.sentinel.db.DataSourceFactory; +import org.ab.sentinel.db.JooqDao; import org.ab.sentinel.db.JooqFactory; -import org.ab.sentinel.dto.AppDto; -import org.ab.sentinel.dto.UserDto; -import org.ab.sentinel.dto.integrations.AppIntegrationRequestDto; -import org.ab.sentinel.jooq.tables.records.AppsRecord; -import org.ab.sentinel.jooq.tables.records.IntegrationsRecord; -import org.ab.sentinel.jooq.tables.records.UsersRecord; +import org.ab.sentinel.DbEvents; +import org.ab.sentinel.db.api.DeleteByCondition; +import org.ab.sentinel.db.api.FetchMap; +import org.ab.sentinel.db.api.FetchOneByCondition; +import org.ab.sentinel.db.api.InsertAndReturn; +import org.ab.sentinel.db.api.UpdateById; import org.jooq.DSLContext; -import org.jooq.exception.DataAccessException; -import org.jooq.impl.DSL; +import org.jooq.TableField; +import org.jooq.Table; +import org.jooq.Record; import org.nanonative.nano.core.model.Service; import org.nanonative.nano.helper.event.model.Event; import javax.sql.DataSource; -import java.nio.charset.StandardCharsets; -import java.time.ZoneOffset; -import java.util.Map; -import java.util.LinkedHashMap; -import java.util.stream.Collectors; +import static org.nanonative.nano.helper.config.ConfigRegister.registerConfig; public final class PostgreSqlService extends Service { + // Config keys public static final String CONFIG_DB_USER = registerConfig("pg_db_user", "Database user"); public static final String CONFIG_DB_PASS = registerConfig("pg_db_pass", "Database password"); public static final String CONFIG_DB_NAME = registerConfig("pg_db_name", "Database name"); public static final String CONFIG_DB_HOST = registerConfig("pg_db_host", "Database host"); public static final String CONFIG_DB_PORT = registerConfig("pg_db_port", "Database port"); public static final String CONFIG_DB_OPTIONS = registerConfig("pg_db_opts", "Database options"); + private String dbHost; private Integer dbPort; private String dbName; private String dbUser; private String dbPass; private String dbOpts; + private DataSource ds; private DataSourceConfig dsConfig; private DSLContext dsl; + private JooqDao dao; @Override public void start() { @@ -56,70 +51,40 @@ public void start() { } @Override - public void stop() { - - } - - @Override - public Object onFailure(final Event event) { - return null; - } - - @Override + @SuppressWarnings("unchecked") public void onEvent(final Event event) { - event.channel(AppEvents.ADD_USER).ifPresent(this::saveUser); - event.channel(AppEvents.FETCH_USER).ifPresent(ev -> ev.respond(fetchUser(ev.payload()))); - event.channel(AppEvents.FETCH_APPS).ifPresent(ev -> ev.respond(getApps())); - event.channel(AppEvents.APP_INT_REQ).ifPresent(ev -> ev.respond(saveNewUserIntegration(ev.payload()))); - } - - private Map getApps() { - final Map apps = dsl.selectFrom(APPS).fetchMap(APPS.NAME); - return apps.entrySet().stream().collect(Collectors.toMap(Map.Entry::getKey, e -> { - var r = e.getValue(); - var meta = (r.getMetadata() == null) ? null : r.getMetadata().data(); - return new AppDto(r.getId(), r.getName(), r.getLogoUrl(), meta); - }, (a, b) -> a, // shouldn't hit since names are unique - LinkedHashMap::new)); + event.channel(DbEvents.FETCH_ONE).ifPresent(ev -> { + FetchOneByCondition p = ev.payload(); + ev.respond((Record) dao.getOne((Table) p.table(), p.condition()).orElse(null)); + }); + + event.channel(DbEvents.INSERT_RETURNING).ifPresent(ev -> { + InsertAndReturn p = ev.payload(); + ev.respond(dao.insertReturning((Table) p.table(), p.values())); + }); + + event.channel(DbEvents.UPDATE_BY_ID_RETURNING).ifPresent(ev -> { + UpdateById p = ev.payload(); + ev.respond((Record) dao.updateByIdReturning((Table) p.table(), (TableField) p.idField(), p.id(), p.values()) + .orElse(null)); + }); + + event.channel(DbEvents.DELETE_WHERE).ifPresent(ev -> { + DeleteByCondition p = ev.payload(); + ev.respond(dao.deleteWhere((Table) p.table(), p.condition())); + }); + + event.channel(DbEvents.FETCH_MAP).ifPresent(ev -> { + FetchMap payload = ev.payload(); + ev.respond(dao.fetchMap((Table) payload.table(), payload.keyField())); + }); } - private IntegrationsRecord saveNewUserIntegration(AppIntegrationRequestDto req) { - final IntegrationsRecord res = dsl.insertInto(INTEGRATIONS).set(INTEGRATIONS.APP_ID, req.appId()).set(INTEGRATIONS.USER_ID, req.userId()).set(INTEGRATIONS.SCOPES, req.scopes().split(",")).set(INTEGRATIONS.ACCESS_TOKEN_ENC, req.accessToken().getBytes(StandardCharsets.UTF_8)).set(INTEGRATIONS.EXPIRES_AT, req.expiresAt().atOffset(ZoneOffset.UTC)).returning(INTEGRATIONS.USER_ID, INTEGRATIONS.APP_ID).fetchOne(); - return res; - } - - private UsersRecord fetchUser(final String email) { - final UsersRecord user = dsl.selectFrom(USERS).where(USERS.EMAIL.eq(email)).fetchOne(); - return user; - } - - private void saveUser(final Event event) { - final UserDto user = event.payload(); - try { - dsl.transactionResult(configuration -> { - DSLContext ctx = DSL.using(configuration); - if (ctx.fetchExists(ctx.selectOne().from(USERS).where(USERS.EMAIL.eq(user.email())))) { - event.error(new RuntimeException("Email already registered")); - return null; - } else { - UsersRecord ur = ctx.insertInto(USERS).set(USERS.EMAIL, user.email()).set(USERS.NAME, user.name()).set(USERS.PASSWORD_HASH, user.passwordHash()).returning(USERS.ID).fetchOne(); - event.respond(ur); - return ur; - } - }); - } catch (DataAccessException dae) { - context.error(() -> "saveUser::jOOQ exception: {}", dae); - event.error(dae); - } - } @Override public void configure(final TypeMapI changes, final TypeMapI merged) { this.dbName = changes.asStringOpt(CONFIG_DB_NAME).orElse(merged.asString(CONFIG_DB_NAME)); this.dbUser = changes.asStringOpt(CONFIG_DB_USER).orElse(merged.asString(CONFIG_DB_USER)); - - // On the config change event, this merged has values from application.properties and not application-.properties - // even after doing NanoUtils.readProfiles() and using the context returned. this.dbPass = changes.asStringOpt(CONFIG_DB_PASS).orElse(merged.asString(CONFIG_DB_PASS)); this.dbPort = changes.asIntOpt(CONFIG_DB_PORT).orElse(merged.asInt(CONFIG_DB_PORT)); this.dbHost = changes.asStringOpt(CONFIG_DB_HOST).orElse(merged.asString(CONFIG_DB_HOST)); @@ -129,11 +94,21 @@ public void configure(final TypeMapI changes, final TypeMapI merged) { } } + @Override + public void stop() { + } + + @Override + public Object onFailure(final Event event) { + return null; + } + private void createOrUpdateDs(String host, Integer port, String name, String user, String pass, String options) { DataSourceConfig newConfig = new DataSourceConfig(host, port, name, user, pass, options); - if (null == this.dsConfig || !this.dsConfig.equals(newConfig)) { + if (this.dsConfig == null || !this.dsConfig.equals(newConfig)) { this.ds = DataSourceFactory.create(newConfig); this.dsl = JooqFactory.create(ds); + this.dao = new JooqDao(this.dsl); this.dsConfig = newConfig; } } diff --git a/src/test/java/org/ab/sentinel/controller/UserControllerTest.java b/src/test/java/org/ab/sentinel/controller/UserControllerTest.java index 0250637..6634bba 100644 --- a/src/test/java/org/ab/sentinel/controller/UserControllerTest.java +++ b/src/test/java/org/ab/sentinel/controller/UserControllerTest.java @@ -2,13 +2,12 @@ import org.ab.sentinel.PostgresContainer.SharedPostgresIT; import org.ab.sentinel.model.Postgres; +import org.ab.sentinel.service.AppService; import org.ab.sentinel.service.PostgreSqlService; import org.ab.sentinel.service.integrations.GithubIntegrationService; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; import org.nanonative.nano.core.Nano; -import org.nanonative.nano.core.model.Context; -import org.nanonative.nano.helper.NanoUtils; import org.nanonative.nano.services.http.HttpClient; import org.nanonative.nano.services.http.HttpServer; import org.nanonative.nano.services.http.model.HttpMethod; @@ -36,7 +35,7 @@ public static void initializeDB() { @Test void registerUser() { - final Nano nano = new Nano(Map.of("app_profiles", "dev"), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient()); + final Nano nano = new Nano(Map.of("app_profiles", "dev"), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient(), new AppService()); nano.context().newEvent(EVENT_CONFIG_CHANGE, () -> Map.of( CONFIG_DB_HOST, dbProp.dbHost(), @@ -49,7 +48,7 @@ void registerUser() { final HttpObject result = new HttpObject() .methodType(HttpMethod.POST) - .body(Map.of("email", "aa@berlin.iosk", "password", "abc","name","aj")) + .body(Map.of("email", "aa@berlin.iosk", "password", "abc", "name", "aj")) .path(serverUrl + nano.service(HttpServer.class).port() + "/auth/register") .send(nano.context(UserControllerTest.class)); @@ -60,7 +59,7 @@ void registerUser() { @Test void getApps() { - final Nano nano = new Nano(Map.of("app_profiles", "dev"), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient()); + final Nano nano = new Nano(Map.of("app_profiles", "dev"), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient(), new AppService()); nano.context(UserControllerTest.class).newEvent(EVENT_CONFIG_CHANGE, () -> Map.of( CONFIG_DB_HOST, dbProp.dbHost(), From fe8afff0ac2d3cd9391a15f07eec6445be4c3473 Mon Sep 17 00:00:00 2001 From: ab Date: Sat, 8 Nov 2025 08:48:51 +0100 Subject: [PATCH 3/3] profile fix --- .../ab/sentinel/controller/AppController.java | 4 ++-- .../sentinel/service/PostgreSqlService.java | 15 +++++------- src/main/resources/application.properties | 4 ---- .../controller/UserControllerTest.java | 23 +++---------------- 4 files changed, 11 insertions(+), 35 deletions(-) diff --git a/src/main/java/org/ab/sentinel/controller/AppController.java b/src/main/java/org/ab/sentinel/controller/AppController.java index 5abee01..99f149c 100644 --- a/src/main/java/org/ab/sentinel/controller/AppController.java +++ b/src/main/java/org/ab/sentinel/controller/AppController.java @@ -56,7 +56,7 @@ public static void integrationRequest(final Event event) switch (appId) { case 1 -> handleGithubIntegration(event, body, userId, appId); - default -> event.respond(problem(event, 404, "App not available")); + default -> event.error(new RuntimeException("App not available")); } }); } @@ -83,6 +83,6 @@ private static void handleGithubIntegration(final Event } else { event.respond(jsonOk(event, Map.of("integration", "success"))); } - }, () -> event.respond(problem(event, 502, "GitHub token validation unavailable"))); + }, () -> event.error(new RuntimeException("GitHub token validation unavailable"))); } } diff --git a/src/main/java/org/ab/sentinel/service/PostgreSqlService.java b/src/main/java/org/ab/sentinel/service/PostgreSqlService.java index 230656f..7863fc3 100644 --- a/src/main/java/org/ab/sentinel/service/PostgreSqlService.java +++ b/src/main/java/org/ab/sentinel/service/PostgreSqlService.java @@ -83,15 +83,12 @@ public void onEvent(final Event event) { @Override public void configure(final TypeMapI changes, final TypeMapI merged) { - this.dbName = changes.asStringOpt(CONFIG_DB_NAME).orElse(merged.asString(CONFIG_DB_NAME)); - this.dbUser = changes.asStringOpt(CONFIG_DB_USER).orElse(merged.asString(CONFIG_DB_USER)); - this.dbPass = changes.asStringOpt(CONFIG_DB_PASS).orElse(merged.asString(CONFIG_DB_PASS)); - this.dbPort = changes.asIntOpt(CONFIG_DB_PORT).orElse(merged.asInt(CONFIG_DB_PORT)); - this.dbHost = changes.asStringOpt(CONFIG_DB_HOST).orElse(merged.asString(CONFIG_DB_HOST)); - this.dbOpts = changes.asStringOpt(CONFIG_DB_OPTIONS).orElse(merged.asString(CONFIG_DB_OPTIONS)); - if (changes.containsKey(CONFIG_DB_PORT)) { - createOrUpdateDs(this.dbHost, changes.asInt(CONFIG_DB_PORT), this.dbName, this.dbUser, this.dbPass, this.dbOpts); - } + this.dbName = merged.asString(CONFIG_DB_NAME); + this.dbUser = merged.asString(CONFIG_DB_USER); + this.dbPass = merged.asString(CONFIG_DB_PASS); + this.dbPort = merged.asInt(CONFIG_DB_PORT); + this.dbHost = merged.asString(CONFIG_DB_HOST); + this.dbOpts = merged.asString(CONFIG_DB_OPTIONS); } @Override diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 073b337..a7e3a63 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,9 +1,5 @@ -app_service_http_port=8080 -app_log_level=DEBUG -app_log_formatter=console pg_db_host=localhost pg_db_port=5432 pg_db_name=nanodb pg_db_user=nanouser pg_db_pass=CHANGEME -# app_profiles=dev diff --git a/src/test/java/org/ab/sentinel/controller/UserControllerTest.java b/src/test/java/org/ab/sentinel/controller/UserControllerTest.java index 6634bba..4ed2f59 100644 --- a/src/test/java/org/ab/sentinel/controller/UserControllerTest.java +++ b/src/test/java/org/ab/sentinel/controller/UserControllerTest.java @@ -15,11 +15,7 @@ import java.util.Map; -import static org.ab.sentinel.service.PostgreSqlService.CONFIG_DB_HOST; -import static org.ab.sentinel.service.PostgreSqlService.CONFIG_DB_PASS; -import static org.ab.sentinel.service.PostgreSqlService.CONFIG_DB_PORT; import static org.assertj.core.api.Assertions.assertThat; -import static org.nanonative.nano.core.model.Context.EVENT_CONFIG_CHANGE; import static org.nanonative.nano.services.http.HttpServer.EVENT_HTTP_REQUEST; class UserControllerTest { @@ -35,14 +31,7 @@ public static void initializeDB() { @Test void registerUser() { - final Nano nano = new Nano(Map.of("app_profiles", "dev"), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient(), new AppService()); - - nano.context().newEvent(EVENT_CONFIG_CHANGE, () -> Map.of( - CONFIG_DB_HOST, dbProp.dbHost(), - CONFIG_DB_PORT, dbProp.dbPort(), - // TODO: Remove the below line as password is already mentioned in dev properties file and this is a work around - CONFIG_DB_PASS, dbProp.dbPass() - )).broadcast(true).send(); + final Nano nano = new Nano(Map.of("app_profiles", "dev", "pg_db_port", dbProp.dbPort()), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient(), new AppService()); nano.subscribeEvent(EVENT_HTTP_REQUEST, UserController::registerUser); @@ -59,15 +48,9 @@ void registerUser() { @Test void getApps() { - final Nano nano = new Nano(Map.of("app_profiles", "dev"), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient(), new AppService()); - - nano.context(UserControllerTest.class).newEvent(EVENT_CONFIG_CHANGE, () -> Map.of( - CONFIG_DB_HOST, dbProp.dbHost(), - CONFIG_DB_PORT, dbProp.dbPort() - )).send(); + final Nano nano = new Nano(Map.of("app_profiles", "dev", "pg_db_port", dbProp.dbPort()), new HttpServer(), new PostgreSqlService(), new GithubIntegrationService(), new HttpClient(), new AppService()); - nano.context(AppController.class) - .subscribeEvent(EVENT_HTTP_REQUEST, AppController::getApps); + nano.subscribeEvent(EVENT_HTTP_REQUEST, AppController::getApps); final HttpObject result = new HttpObject() .methodType(HttpMethod.GET)