[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.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 java.util.List;
|
||||
import java.util.Locale;
|
||||
@@ -14,10 +17,16 @@ public class OrdsMetadataService {
|
||||
|
||||
private final OrdsMetadataMapper mapper;
|
||||
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.jdbcTemplate = jdbcTemplate;
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
}
|
||||
|
||||
public List<OrdsHandlerView> findHandlers() {
|
||||
@@ -71,4 +80,141 @@ public class OrdsMetadataService {
|
||||
END;
|
||||
""", 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.service.AppException;
|
||||
import com.cloudhandson.vpdbackoffice.service.OrdsMetadataService;
|
||||
import com.cloudhandson.vpdbackoffice.service.ProtectedObjectService;
|
||||
import org.springframework.dao.DataAccessException;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -14,9 +16,14 @@ import org.springframework.web.servlet.mvc.support.RedirectAttributes;
|
||||
public class ProtectedObjectController {
|
||||
|
||||
private final ProtectedObjectService protectedObjectService;
|
||||
private final OrdsMetadataService ordsMetadataService;
|
||||
|
||||
public ProtectedObjectController(ProtectedObjectService protectedObjectService) {
|
||||
public ProtectedObjectController(
|
||||
ProtectedObjectService protectedObjectService,
|
||||
OrdsMetadataService ordsMetadataService
|
||||
) {
|
||||
this.protectedObjectService = protectedObjectService;
|
||||
this.ordsMetadataService = ordsMetadataService;
|
||||
}
|
||||
|
||||
@GetMapping("/objects")
|
||||
@@ -60,6 +67,20 @@ public class ProtectedObjectController {
|
||||
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")
|
||||
public String disable(@RequestParam long objectId, RedirectAttributes redirectAttributes) {
|
||||
protectedObjectService.disableObject(objectId);
|
||||
|
||||
@@ -143,6 +143,17 @@ body {
|
||||
display: inline;
|
||||
}
|
||||
|
||||
.action-stack {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: .4rem;
|
||||
}
|
||||
|
||||
.action-stack .inline-form {
|
||||
display: inline-flex;
|
||||
}
|
||||
|
||||
.switch-field {
|
||||
align-items: center;
|
||||
display: flex;
|
||||
|
||||
@@ -61,7 +61,7 @@
|
||||
<th>Owner</th>
|
||||
<th>Object</th>
|
||||
<th>ORDS Path</th>
|
||||
<th></th>
|
||||
<th>Action</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
@@ -78,11 +78,18 @@
|
||||
</form>
|
||||
</td>
|
||||
<td>
|
||||
<form method="post" action="/objects/disable" 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-danger" type="submit">비활성화</button>
|
||||
</form>
|
||||
<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">
|
||||
<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-danger" type="submit">비활성화</button>
|
||||
</form>
|
||||
</div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr th:if="${#lists.isEmpty(objects)}">
|
||||
|
||||
Reference in New Issue
Block a user