[Developer] #424 generate object query ORDS handlers
This commit is contained in:
@@ -0,0 +1,9 @@
|
|||||||
|
package com.cloudhandson.vpdbackoffice.domain.ords;
|
||||||
|
|
||||||
|
public record OrdsObjectHandlerResult(
|
||||||
|
long objectId,
|
||||||
|
String ordsPath,
|
||||||
|
String moduleName,
|
||||||
|
String template
|
||||||
|
) {
|
||||||
|
}
|
||||||
@@ -2,6 +2,9 @@ package com.cloudhandson.vpdbackoffice.service;
|
|||||||
|
|
||||||
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerUpdateCommand;
|
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerUpdateCommand;
|
||||||
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerView;
|
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsHandlerView;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.ords.OrdsObjectHandlerResult;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedColumn;
|
||||||
|
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObject;
|
||||||
import com.cloudhandson.vpdbackoffice.mapper.OrdsMetadataMapper;
|
import com.cloudhandson.vpdbackoffice.mapper.OrdsMetadataMapper;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Locale;
|
import java.util.Locale;
|
||||||
@@ -14,10 +17,16 @@ public class OrdsMetadataService {
|
|||||||
|
|
||||||
private final OrdsMetadataMapper mapper;
|
private final OrdsMetadataMapper mapper;
|
||||||
private final JdbcTemplate jdbcTemplate;
|
private final JdbcTemplate jdbcTemplate;
|
||||||
|
private final ProtectedObjectService protectedObjectService;
|
||||||
|
|
||||||
public OrdsMetadataService(OrdsMetadataMapper mapper, JdbcTemplate jdbcTemplate) {
|
public OrdsMetadataService(
|
||||||
|
OrdsMetadataMapper mapper,
|
||||||
|
JdbcTemplate jdbcTemplate,
|
||||||
|
ProtectedObjectService protectedObjectService
|
||||||
|
) {
|
||||||
this.mapper = mapper;
|
this.mapper = mapper;
|
||||||
this.jdbcTemplate = jdbcTemplate;
|
this.jdbcTemplate = jdbcTemplate;
|
||||||
|
this.protectedObjectService = protectedObjectService;
|
||||||
}
|
}
|
||||||
|
|
||||||
public List<OrdsHandlerView> findHandlers() {
|
public List<OrdsHandlerView> findHandlers() {
|
||||||
@@ -71,4 +80,141 @@ public class OrdsMetadataService {
|
|||||||
END;
|
END;
|
||||||
""", handler.moduleName(), handler.template(), handler.method(), command.source());
|
""", handler.moduleName(), handler.template(), handler.method(), command.source());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Transactional
|
||||||
|
public OrdsObjectHandlerResult createObjectQueryHandler(long objectId) {
|
||||||
|
ProtectedObject object = protectedObjectService.assertEnabled(objectId);
|
||||||
|
List<String> columns = protectedObjectService.findColumns(objectId).stream()
|
||||||
|
.map(ProtectedColumn::columnName)
|
||||||
|
.toList();
|
||||||
|
if (columns.isEmpty()) {
|
||||||
|
throw new AppException("보호 객체 컬럼이 없어 ORDS 조회 Handler를 만들 수 없습니다.");
|
||||||
|
}
|
||||||
|
requireIdentifier(object.owner(), "owner");
|
||||||
|
requireIdentifier(object.objectName(), "objectName");
|
||||||
|
columns.forEach(column -> requireIdentifier(column, "column"));
|
||||||
|
|
||||||
|
String currentUser = jdbcTemplate.queryForObject("SELECT USER FROM dual", String.class);
|
||||||
|
if (currentUser == null || !currentUser.equalsIgnoreCase("CB_ORDS")) {
|
||||||
|
throw new AppException("ORDS 조회 Handler 생성은 ORDS parsing schema(CB_ORDS)로 DB에 연결했을 때만 가능합니다. 현재 연결 사용자: "
|
||||||
|
+ currentUser);
|
||||||
|
}
|
||||||
|
|
||||||
|
String moduleName = "cb.object.query";
|
||||||
|
String basePath = "cb-object-query/";
|
||||||
|
String template = object.owner().toLowerCase(Locale.ROOT) + "/" + object.objectName().toLowerCase(Locale.ROOT);
|
||||||
|
String ordsPath = "cb-ords/" + basePath + template;
|
||||||
|
String source = objectQuerySource(object, columns);
|
||||||
|
|
||||||
|
jdbcTemplate.update("""
|
||||||
|
BEGIN
|
||||||
|
ORDS.DEFINE_MODULE(
|
||||||
|
p_module_name => ?,
|
||||||
|
p_base_path => ?,
|
||||||
|
p_items_per_page => 25,
|
||||||
|
p_status => 'PUBLISHED'
|
||||||
|
);
|
||||||
|
ORDS.DEFINE_TEMPLATE(
|
||||||
|
p_module_name => ?,
|
||||||
|
p_pattern => ?
|
||||||
|
);
|
||||||
|
ORDS.DEFINE_HANDLER(
|
||||||
|
p_module_name => ?,
|
||||||
|
p_pattern => ?,
|
||||||
|
p_method => 'POST',
|
||||||
|
p_source_type => ORDS.source_type_plsql,
|
||||||
|
p_source => ?,
|
||||||
|
p_items_per_page => 0
|
||||||
|
);
|
||||||
|
ORDS.DEFINE_PARAMETER(
|
||||||
|
p_module_name => ?,
|
||||||
|
p_pattern => ?,
|
||||||
|
p_method => 'POST',
|
||||||
|
p_name => 'Authorization',
|
||||||
|
p_bind_variable_name => 'auth_header',
|
||||||
|
p_source_type => 'HEADER',
|
||||||
|
p_param_type => 'STRING',
|
||||||
|
p_access_method => 'IN'
|
||||||
|
);
|
||||||
|
ORDS.DEFINE_PARAMETER(
|
||||||
|
p_module_name => ?,
|
||||||
|
p_pattern => ?,
|
||||||
|
p_method => 'POST',
|
||||||
|
p_name => 'limit',
|
||||||
|
p_bind_variable_name => 'row_limit',
|
||||||
|
p_source_type => 'QUERY',
|
||||||
|
p_param_type => 'INT',
|
||||||
|
p_access_method => 'IN'
|
||||||
|
);
|
||||||
|
COMMIT;
|
||||||
|
END;
|
||||||
|
""",
|
||||||
|
moduleName, basePath,
|
||||||
|
moduleName, template,
|
||||||
|
moduleName, template, source,
|
||||||
|
moduleName, template,
|
||||||
|
moduleName, template);
|
||||||
|
protectedObjectService.updateOrdsPath(object.objectId(), ordsPath);
|
||||||
|
return new OrdsObjectHandlerResult(object.objectId(), ordsPath, moduleName, template);
|
||||||
|
}
|
||||||
|
|
||||||
|
private String objectQuerySource(ProtectedObject object, List<String> columns) {
|
||||||
|
String jsonEntries = columns.stream()
|
||||||
|
.map(column -> "'" + column.toLowerCase(Locale.ROOT) + "' VALUE " + column)
|
||||||
|
.reduce((left, right) -> left + ",\n " + right)
|
||||||
|
.orElseThrow();
|
||||||
|
String selectColumns = String.join(", ", columns);
|
||||||
|
return """
|
||||||
|
DECLARE
|
||||||
|
v_auth VARCHAR2(4000);
|
||||||
|
v_bearer_key VARCHAR2(4000);
|
||||||
|
v_limit NUMBER := LEAST(GREATEST(NVL(:row_limit, 50), 1), 500);
|
||||||
|
v_rows_json CLOB;
|
||||||
|
BEGIN
|
||||||
|
v_auth := TRIM(:auth_header);
|
||||||
|
IF v_auth IS NULL OR LOWER(SUBSTR(v_auth, 1, 7)) != 'bearer ' THEN
|
||||||
|
RAISE_APPLICATION_ERROR(-20101, 'Authorization header must be Bearer <key>');
|
||||||
|
END IF;
|
||||||
|
v_bearer_key := TRIM(SUBSTR(v_auth, 8));
|
||||||
|
|
||||||
|
admin.cb_agent_ctx_pkg.clear_user;
|
||||||
|
admin.cb_agent_ctx_pkg.set_user_by_bearer(v_bearer_key);
|
||||||
|
|
||||||
|
SELECT JSON_ARRAYAGG(
|
||||||
|
JSON_OBJECT(
|
||||||
|
%s
|
||||||
|
RETURNING CLOB
|
||||||
|
)
|
||||||
|
RETURNING CLOB
|
||||||
|
)
|
||||||
|
INTO v_rows_json
|
||||||
|
FROM (
|
||||||
|
SELECT %s
|
||||||
|
FROM %s.%s
|
||||||
|
WHERE ROWNUM <= v_limit
|
||||||
|
);
|
||||||
|
|
||||||
|
IF v_rows_json IS NULL THEN
|
||||||
|
v_rows_json := '[]';
|
||||||
|
END IF;
|
||||||
|
|
||||||
|
:status_code := 200;
|
||||||
|
OWA_UTIL.MIME_HEADER('application/json', TRUE);
|
||||||
|
HTP.P(JSON_OBJECT('rows' VALUE v_rows_json FORMAT JSON RETURNING CLOB));
|
||||||
|
admin.cb_agent_ctx_pkg.clear_user;
|
||||||
|
EXCEPTION
|
||||||
|
WHEN OTHERS THEN
|
||||||
|
admin.cb_agent_ctx_pkg.clear_user;
|
||||||
|
:status_code := 403;
|
||||||
|
OWA_UTIL.MIME_HEADER('application/json', TRUE);
|
||||||
|
HTP.P(JSON_OBJECT('error' VALUE SQLERRM RETURNING CLOB));
|
||||||
|
END;
|
||||||
|
""".formatted(jsonEntries, selectColumns, object.owner(), object.objectName());
|
||||||
|
}
|
||||||
|
|
||||||
|
private void requireIdentifier(String value, String label) {
|
||||||
|
if (value == null || !value.matches("[A-Z][A-Z0-9_$#]*")) {
|
||||||
|
throw new AppException("ORDS Handler 생성에 사용할 수 없는 " + label + " 식별자입니다: " + value);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,7 +2,9 @@ package com.cloudhandson.vpdbackoffice.web;
|
|||||||
|
|
||||||
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
|
import com.cloudhandson.vpdbackoffice.domain.protectedobject.ProtectedObjectCreateCommand;
|
||||||
import com.cloudhandson.vpdbackoffice.service.AppException;
|
import com.cloudhandson.vpdbackoffice.service.AppException;
|
||||||
|
import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService;
|
||||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||||
|
import org.springframework.dao.DataAccessException;
|
||||||
import org.springframework.stereotype.Controller;
|
import org.springframework.stereotype.Controller;
|
||||||
import org.springframework.ui.Model;
|
import org.springframework.ui.Model;
|
||||||
import org.springframework.web.bind.annotation.GetMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
@@ -14,9 +16,14 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
|||||||
public class ProtectedObjectController {
|
public class ProtectedObjectController {
|
||||||
|
|
||||||
private final ProtectedObjectService protectedObjectService;
|
private final ProtectedObjectService protectedObjectService;
|
||||||
|
private final OrdsMetadataService ordsMetadataService;
|
||||||
|
|
||||||
public ProtectedObjectController(ProtectedObjectService protectedObjectService) {
|
public ProtectedObjectController(
|
||||||
|
ProtectedObjectService protectedObjectService,
|
||||||
|
OrdsMetadataService ordsMetadataService
|
||||||
|
) {
|
||||||
this.protectedObjectService = protectedObjectService;
|
this.protectedObjectService = protectedObjectService;
|
||||||
|
this.ordsMetadataService = ordsMetadataService;
|
||||||
}
|
}
|
||||||
|
|
||||||
@GetMapping("/objects")
|
@GetMapping("/objects")
|
||||||
@@ -60,6 +67,20 @@ public class ProtectedObjectController {
|
|||||||
return "redirect:/objects";
|
return "redirect:/objects";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@PostMapping("/objects/ords-handler")
|
||||||
|
public String createOrdsHandler(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
|
||||||
|
try {
|
||||||
|
var result = ordsMetadataService.createObjectQueryHandler(objectId);
|
||||||
|
redirectAttributes.addFlashAttribute("message", "ORDS 조회 Handler를 생성했습니다: " + result.ordsPath());
|
||||||
|
} catch (AppException exception) {
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage", exception.getMessage());
|
||||||
|
} catch (DataAccessException exception) {
|
||||||
|
RuntimeErrorMessage error = RuntimeErrorMessages.dataAccess(exception);
|
||||||
|
redirectAttributes.addFlashAttribute("errorMessage", error.message());
|
||||||
|
}
|
||||||
|
return "redirect:/objects";
|
||||||
|
}
|
||||||
|
|
||||||
@PostMapping("/objects/disable")
|
@PostMapping("/objects/disable")
|
||||||
public String disable(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
|
public String disable(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
|
||||||
protectedObjectService.disableObject(objectId);
|
protectedObjectService.disableObject(objectId);
|
||||||
|
|||||||
@@ -143,6 +143,17 @@ body {
|
|||||||
display: inline;
|
display: inline;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.action-stack {
|
||||||
|
align-items: center;
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: .4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-stack .inline-form {
|
||||||
|
display: inline-flex;
|
||||||
|
}
|
||||||
|
|
||||||
.switch-field {
|
.switch-field {
|
||||||
align-items: center;
|
align-items: center;
|
||||||
display: flex;
|
display: flex;
|
||||||
|
|||||||
@@ -61,7 +61,7 @@
|
|||||||
<th>Owner</th>
|
<th>Owner</th>
|
||||||
<th>Object</th>
|
<th>Object</th>
|
||||||
<th>ORDS Path</th>
|
<th>ORDS Path</th>
|
||||||
<th></th>
|
<th>Action</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
@@ -78,11 +78,18 @@
|
|||||||
</form>
|
</form>
|
||||||
</td>
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
<div class="action-stack">
|
||||||
|
<form method="post" action="/objects/ords-handler" class="inline-form">
|
||||||
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
|
<input type="hidden" name="objectId" th:value="${object.objectId()}">
|
||||||
|
<button class="btn btn-sm btn-outline-primary" type="submit">조회 Handler 생성</button>
|
||||||
|
</form>
|
||||||
<form method="post" action="/objects/disable" class="inline-form">
|
<form method="post" action="/objects/disable" class="inline-form">
|
||||||
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
<input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}">
|
||||||
<input type="hidden" name="objectId" th:value="${object.objectId()}">
|
<input type="hidden" name="objectId" th:value="${object.objectId()}">
|
||||||
<button class="btn btn-sm btn-outline-danger" type="submit">비활성화</button>
|
<button class="btn btn-sm btn-outline-danger" type="submit">비활성화</button>
|
||||||
</form>
|
</form>
|
||||||
|
</div>
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr th:if="${#lists.isEmpty(objects)}">
|
<tr th:if="${#lists.isEmpty(objects)}">
|
||||||
|
|||||||
Reference in New Issue
Block a user