725 lines
24 KiB
PHP
725 lines
24 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\DataSource;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Libraries\AuditLogger;
|
|
use App\Libraries\ConnectionFactory;
|
|
use App\Models\DataSourceModel;
|
|
use PDO;
|
|
use PDOException;
|
|
use Throwable;
|
|
|
|
class DataSourceController extends BaseController
|
|
{
|
|
public function index()
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$dataSources = (new DataSourceModel())->forWorkspace($workspaceId);
|
|
|
|
return view('datasource/index', [
|
|
'title' => 'Data Sources | Chart-Board',
|
|
'dataSources' => $dataSources,
|
|
]);
|
|
}
|
|
|
|
public function create()
|
|
{
|
|
return view('datasource/create', [
|
|
'title' => 'Create Data Source | Chart-Board',
|
|
]);
|
|
}
|
|
|
|
public function show(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$model = new DataSourceModel();
|
|
$dataSource = $model->where('workspace_id', $workspaceId)->find($id);
|
|
|
|
if (! $dataSource) {
|
|
return redirect()->to('/datasource')->with('error', 'Data source not found.');
|
|
}
|
|
|
|
$proof = $this->buildConnectionProof($dataSource);
|
|
|
|
return view('datasource/show', [
|
|
'title' => 'Data Source Details | Chart-Board',
|
|
'dataSource' => $dataSource,
|
|
'proof' => $proof,
|
|
]);
|
|
}
|
|
|
|
public function proof(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$model = new DataSourceModel();
|
|
$dataSource = $model->where('workspace_id', $workspaceId)->find($id);
|
|
|
|
if (! $dataSource) {
|
|
return $this->response->setJSON([
|
|
'success' => false,
|
|
'message' => 'Data source not found.',
|
|
])->setStatusCode(404);
|
|
}
|
|
|
|
$proof = $this->buildConnectionProof($dataSource);
|
|
$html = view('datasource/_proof_content', [
|
|
'dataSource' => $dataSource,
|
|
'proof' => $proof,
|
|
]);
|
|
|
|
return $this->response->setJSON([
|
|
'success' => true,
|
|
'message' => 'Proof refreshed.',
|
|
'proof' => $proof,
|
|
'html' => $html,
|
|
]);
|
|
}
|
|
|
|
public function store()
|
|
{
|
|
$rules = $this->baseRules();
|
|
if (! $this->validate($rules)) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$payload = $this->buildPayload($workspaceId);
|
|
$payload['created_by'] = (int) $this->session->get('user_id');
|
|
$payload['status'] = 'untested';
|
|
$payload['error_message'] = null;
|
|
$payload['last_tested_at'] = null;
|
|
|
|
$dsModel = new DataSourceModel();
|
|
$newId = (int) $dsModel->insert($payload, true);
|
|
|
|
if ($newId > 0) {
|
|
AuditLogger::log(
|
|
'datasource.created',
|
|
'data_source',
|
|
$newId,
|
|
null,
|
|
$this->snapshotDataSource($payload),
|
|
$workspaceId,
|
|
(int) $this->session->get('user_id')
|
|
);
|
|
}
|
|
|
|
return redirect()->to('/datasource')->with('success', 'Data source created.');
|
|
}
|
|
|
|
public function edit(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$dataSource = (new DataSourceModel())
|
|
->where('workspace_id', $workspaceId)
|
|
->find($id);
|
|
|
|
if (! $dataSource) {
|
|
return redirect()->to('/datasource')->with('error', 'Data source not found.');
|
|
}
|
|
|
|
return view('datasource/edit', [
|
|
'title' => 'Edit Data Source | Chart-Board',
|
|
'dataSource' => $dataSource,
|
|
]);
|
|
}
|
|
|
|
public function update(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$model = new DataSourceModel();
|
|
$dataSource = $model->where('workspace_id', $workspaceId)->find($id);
|
|
|
|
if (! $dataSource) {
|
|
return redirect()->to('/datasource')->with('error', 'Data source not found.');
|
|
}
|
|
|
|
$rules = $this->baseRules(false);
|
|
if (! $this->validate($rules)) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
|
|
$payload = $this->buildPayload($workspaceId);
|
|
|
|
// Keep previously encrypted secrets when user leaves masked value unchanged.
|
|
if (trim((string) $this->request->getPost('password')) === '') {
|
|
unset($payload['password']);
|
|
}
|
|
if (trim((string) $this->request->getPost('api_auth_value')) === '') {
|
|
unset($payload['api_auth_value']);
|
|
}
|
|
|
|
$payload['status'] = 'untested';
|
|
$payload['error_message'] = null;
|
|
$payload['last_tested_at'] = null;
|
|
|
|
$oldSnap = $this->snapshotDataSource($dataSource);
|
|
$model->update($id, $payload);
|
|
$newRow = $model->find($id) ?? [];
|
|
AuditLogger::log(
|
|
'datasource.updated',
|
|
'data_source',
|
|
$id,
|
|
$oldSnap,
|
|
$this->snapshotDataSource($newRow),
|
|
$workspaceId,
|
|
(int) $this->session->get('user_id')
|
|
);
|
|
|
|
return redirect()->to('/datasource')->with('success', 'Data source updated.');
|
|
}
|
|
|
|
public function delete(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$model = new DataSourceModel();
|
|
$dataSource = $model->where('workspace_id', $workspaceId)->find($id);
|
|
|
|
if (! $dataSource) {
|
|
return redirect()->to('/datasource')->with('error', 'Data source not found.');
|
|
}
|
|
|
|
AuditLogger::log(
|
|
'datasource.deleted',
|
|
'data_source',
|
|
$id,
|
|
$this->snapshotDataSource($dataSource),
|
|
null,
|
|
$workspaceId,
|
|
(int) $this->session->get('user_id')
|
|
);
|
|
|
|
$model->delete($id);
|
|
return redirect()->to('/datasource')->with('success', 'Data source deleted.');
|
|
}
|
|
|
|
public function test()
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$id = (int) $this->request->getPost('id');
|
|
$model = new DataSourceModel();
|
|
$dataSource = $id > 0
|
|
? $model->where('workspace_id', $workspaceId)->find($id)
|
|
: null;
|
|
|
|
$payload = $dataSource ?: $this->buildPayload($workspaceId);
|
|
$result = ['success' => false, 'message' => 'Connection test failed.'];
|
|
|
|
try {
|
|
$connector = (new ConnectionFactory())->make((string) $payload['type']);
|
|
$result = $connector->test($payload);
|
|
} catch (Throwable $e) {
|
|
$result = ['success' => false, 'message' => $e->getMessage()];
|
|
}
|
|
|
|
if ($dataSource) {
|
|
$model->update($id, [
|
|
'status' => $result['success'] ? 'connected' : 'failed',
|
|
'error_message' => $result['success'] ? null : $result['message'],
|
|
'last_tested_at' => date('Y-m-d H:i:s'),
|
|
]);
|
|
}
|
|
|
|
if ($this->request->isAJAX()) {
|
|
return $this->response->setJSON($result);
|
|
}
|
|
|
|
return redirect()->back()->with(
|
|
$result['success'] ? 'success' : 'error',
|
|
$result['message']
|
|
);
|
|
}
|
|
|
|
public function schema(int $id)
|
|
{
|
|
$workspaceId = (int) $this->session->get('active_workspace_id');
|
|
$model = new DataSourceModel();
|
|
$dataSource = $model->where('workspace_id', $workspaceId)->find($id);
|
|
|
|
if (! $dataSource) {
|
|
return $this->response->setJSON([
|
|
'success' => false,
|
|
'message' => 'Data source not found.',
|
|
])->setStatusCode(404);
|
|
}
|
|
|
|
$cacheKey = 'ds_schema_' . $id . '_' . md5((string) ($dataSource['updated_at'] ?? ''));
|
|
$cached = cache()->get($cacheKey);
|
|
if (is_array($cached)) {
|
|
return $this->response->setJSON([
|
|
'success' => true,
|
|
'message' => 'Schema loaded from cache.',
|
|
'data' => $cached,
|
|
]);
|
|
}
|
|
|
|
try {
|
|
$schemaData = $this->loadSchema((string) $dataSource['type'], $dataSource);
|
|
cache()->save($cacheKey, $schemaData, 300);
|
|
|
|
return $this->response->setJSON([
|
|
'success' => true,
|
|
'message' => 'Schema loaded successfully.',
|
|
'data' => $schemaData,
|
|
]);
|
|
} catch (Throwable $e) {
|
|
return $this->response->setJSON([
|
|
'success' => false,
|
|
'message' => $e->getMessage(),
|
|
])->setStatusCode(422);
|
|
}
|
|
}
|
|
|
|
private function baseRules(bool $requirePassword = true): array
|
|
{
|
|
$passwordRule = $requirePassword ? 'permit_empty|max_length[1000]' : 'permit_empty|max_length[1000]';
|
|
|
|
return [
|
|
'name' => 'required|min_length[3]|max_length[150]',
|
|
'type' => 'required|in_list[mysql,postgresql,mongodb,rest_api,csv]',
|
|
'host' => 'permit_empty|max_length[255]',
|
|
'port' => 'permit_empty|integer|greater_than_equal_to[1]|less_than_equal_to[65535]',
|
|
'database_name' => 'permit_empty|max_length[150]',
|
|
'username' => 'permit_empty|max_length[150]',
|
|
'password' => $passwordRule,
|
|
'connection_uri' => 'permit_empty|max_length[5000]',
|
|
'api_base_url' => 'permit_empty|max_length[500]|valid_url_strict',
|
|
'api_method' => 'permit_empty|in_list[GET,POST]',
|
|
'api_auth_type' => 'permit_empty|in_list[none,bearer,basic,api_key]',
|
|
'api_auth_value' => 'permit_empty|max_length[5000]',
|
|
'csv_file_path' => 'permit_empty|max_length[500]',
|
|
'csv_delimiter' => 'permit_empty|max_length[1]',
|
|
];
|
|
}
|
|
|
|
private function buildPayload(int $workspaceId): array
|
|
{
|
|
$headersRaw = trim((string) $this->request->getPost('api_headers'));
|
|
|
|
$data = [
|
|
'workspace_id' => $workspaceId,
|
|
'name' => strip_tags((string) $this->request->getPost('name')),
|
|
'type' => (string) $this->request->getPost('type'),
|
|
'host' => trim((string) $this->request->getPost('host')) ?: null,
|
|
'port' => $this->request->getPost('port') !== null && $this->request->getPost('port') !== '' ? (int) $this->request->getPost('port') : null,
|
|
'database_name' => trim((string) $this->request->getPost('database_name')) ?: null,
|
|
'username' => trim((string) $this->request->getPost('username')) ?: null,
|
|
'password' => (string) $this->request->getPost('password'),
|
|
'connection_uri' => trim((string) $this->request->getPost('connection_uri')) ?: null,
|
|
'ssl_enabled' => $this->request->getPost('ssl_enabled') ? 1 : 0,
|
|
'ssl_ca' => trim((string) $this->request->getPost('ssl_ca')) ?: null,
|
|
'api_base_url' => trim((string) $this->request->getPost('api_base_url')) ?: null,
|
|
'api_method' => (string) ($this->request->getPost('api_method') ?: 'GET'),
|
|
'api_auth_type' => (string) ($this->request->getPost('api_auth_type') ?: 'none'),
|
|
'api_auth_value' => (string) $this->request->getPost('api_auth_value'),
|
|
'api_headers' => $headersRaw !== '' ? json_encode($this->sanitizeHeadersJson($headersRaw)) : null,
|
|
'csv_file_path' => trim((string) $this->request->getPost('csv_file_path')) ?: null,
|
|
'csv_delimiter' => trim((string) $this->request->getPost('csv_delimiter')) ?: ',',
|
|
];
|
|
|
|
return $this->normalizeForType($data);
|
|
}
|
|
|
|
private function normalizeForType(array $data): array
|
|
{
|
|
$type = (string) ($data['type'] ?? '');
|
|
if (in_array($type, ['mysql', 'postgresql'], true)) {
|
|
$data['api_base_url'] = null;
|
|
$data['api_auth_type'] = 'none';
|
|
$data['api_auth_value'] = null;
|
|
$data['api_headers'] = null;
|
|
$data['csv_file_path'] = null;
|
|
$data['csv_delimiter'] = ',';
|
|
return $data;
|
|
}
|
|
|
|
if ($type === 'mongodb') {
|
|
$data['database_name'] = null;
|
|
$data['api_base_url'] = null;
|
|
$data['api_auth_type'] = 'none';
|
|
$data['api_auth_value'] = null;
|
|
$data['api_headers'] = null;
|
|
$data['csv_file_path'] = null;
|
|
$data['csv_delimiter'] = ',';
|
|
return $data;
|
|
}
|
|
|
|
if ($type === 'rest_api') {
|
|
$data['host'] = null;
|
|
$data['port'] = null;
|
|
$data['database_name'] = null;
|
|
$data['username'] = null;
|
|
$data['password'] = null;
|
|
$data['connection_uri'] = null;
|
|
$data['ssl_enabled'] = 0;
|
|
$data['ssl_ca'] = null;
|
|
$data['csv_file_path'] = null;
|
|
$data['csv_delimiter'] = ',';
|
|
return $data;
|
|
}
|
|
|
|
if ($type === 'csv') {
|
|
$data['host'] = null;
|
|
$data['port'] = null;
|
|
$data['database_name'] = null;
|
|
$data['username'] = null;
|
|
$data['password'] = null;
|
|
$data['connection_uri'] = null;
|
|
$data['ssl_enabled'] = 0;
|
|
$data['ssl_ca'] = null;
|
|
$data['api_base_url'] = null;
|
|
$data['api_auth_type'] = 'none';
|
|
$data['api_auth_value'] = null;
|
|
$data['api_headers'] = null;
|
|
}
|
|
|
|
return $data;
|
|
}
|
|
|
|
private function sanitizeHeadersJson(string $json): array
|
|
{
|
|
$decoded = json_decode($json, true);
|
|
if (! is_array($decoded)) {
|
|
return [];
|
|
}
|
|
|
|
$clean = [];
|
|
foreach ($decoded as $key => $value) {
|
|
$cleanKey = strip_tags((string) $key);
|
|
if ($cleanKey === '') {
|
|
continue;
|
|
}
|
|
|
|
$clean[$cleanKey] = strip_tags((string) $value);
|
|
}
|
|
|
|
return $clean;
|
|
}
|
|
|
|
private function buildConnectionProof(array $dataSource): array
|
|
{
|
|
$proof = [
|
|
'success' => false,
|
|
'message' => 'Connection proof not available.',
|
|
'meta' => [],
|
|
'data' => [],
|
|
];
|
|
|
|
try {
|
|
$connector = (new ConnectionFactory())->make((string) $dataSource['type']);
|
|
$test = $connector->test($dataSource);
|
|
$proof['success'] = (bool) ($test['success'] ?? false);
|
|
$proof['message'] = (string) ($test['message'] ?? $proof['message']);
|
|
} catch (Throwable $e) {
|
|
$proof['message'] = $e->getMessage();
|
|
return $proof;
|
|
}
|
|
|
|
if (! $proof['success']) {
|
|
return $proof;
|
|
}
|
|
|
|
$type = (string) $dataSource['type'];
|
|
|
|
if (in_array($type, ['mysql', 'postgresql'], true)) {
|
|
try {
|
|
$schema = $this->loadSchema($type, $dataSource);
|
|
$tables = $schema['tables'] ?? [];
|
|
$proof['meta']['table_count'] = count($tables);
|
|
$proof['data']['tables'] = $tables;
|
|
} catch (Throwable $e) {
|
|
$proof['message'] = 'Connected, but unable to load schema: ' . $e->getMessage();
|
|
}
|
|
|
|
return $proof;
|
|
}
|
|
|
|
if ($type === 'rest_api') {
|
|
try {
|
|
$preview = $this->fetchApiPreview($dataSource);
|
|
$proof['meta']['http_code'] = $preview['http_code'];
|
|
$proof['data']['api_preview'] = $preview['data'];
|
|
$proof['data']['api_raw'] = $preview['raw'];
|
|
} catch (Throwable $e) {
|
|
$proof['message'] = 'Connected, but unable to fetch API preview: ' . $e->getMessage();
|
|
}
|
|
|
|
return $proof;
|
|
}
|
|
|
|
if ($type === 'csv') {
|
|
try {
|
|
$csv = $this->loadCsvPreview($dataSource);
|
|
$proof['meta']['row_count_preview'] = count($csv['rows']);
|
|
$proof['data']['csv_headers'] = $csv['headers'];
|
|
$proof['data']['csv_rows'] = $csv['rows'];
|
|
} catch (Throwable $e) {
|
|
$proof['message'] = 'Connected, but unable to read CSV preview: ' . $e->getMessage();
|
|
}
|
|
}
|
|
|
|
return $proof;
|
|
}
|
|
|
|
private function loadSchema(string $type, array $config): array
|
|
{
|
|
if ($type === 'mysql') {
|
|
return $this->loadMySqlSchema($config);
|
|
}
|
|
|
|
if ($type === 'postgresql') {
|
|
return $this->loadPostgreSqlSchema($config);
|
|
}
|
|
|
|
throw new \RuntimeException('Schema browsing is currently supported only for MySQL and PostgreSQL sources.');
|
|
}
|
|
|
|
private function loadMySqlSchema(array $config): array
|
|
{
|
|
$dsn = sprintf(
|
|
'mysql:host=%s;port=%d;dbname=%s;charset=utf8mb4',
|
|
$config['host'] ?? '',
|
|
(int) ($config['port'] ?? 3306),
|
|
$config['database_name'] ?? ''
|
|
);
|
|
|
|
try {
|
|
$pdo = new PDO(
|
|
$dsn,
|
|
(string) ($config['username'] ?? ''),
|
|
(string) ($config['password'] ?? ''),
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_TIMEOUT => 10,
|
|
]
|
|
);
|
|
} catch (PDOException $e) {
|
|
throw new \RuntimeException('MySQL schema connection failed: ' . $e->getMessage());
|
|
}
|
|
|
|
$tablesStmt = $pdo->query('SHOW TABLES');
|
|
$tableRows = $tablesStmt ? $tablesStmt->fetchAll(PDO::FETCH_NUM) : [];
|
|
$tables = [];
|
|
|
|
foreach ($tableRows as $tableRow) {
|
|
$tableName = (string) ($tableRow[0] ?? '');
|
|
if ($tableName === '') {
|
|
continue;
|
|
}
|
|
|
|
$columnsStmt = $pdo->prepare('SHOW COLUMNS FROM `' . str_replace('`', '``', $tableName) . '`');
|
|
$columnsStmt->execute();
|
|
$columnsRows = $columnsStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$columns = [];
|
|
foreach ($columnsRows as $column) {
|
|
$columns[] = [
|
|
'name' => (string) ($column['Field'] ?? ''),
|
|
'type' => (string) ($column['Type'] ?? ''),
|
|
'nullable' => (string) ($column['Null'] ?? '') === 'YES',
|
|
];
|
|
}
|
|
|
|
$tables[] = [
|
|
'name' => $tableName,
|
|
'columns' => $columns,
|
|
];
|
|
}
|
|
|
|
return ['tables' => $tables];
|
|
}
|
|
|
|
private function loadPostgreSqlSchema(array $config): array
|
|
{
|
|
$dsn = sprintf(
|
|
'pgsql:host=%s;port=%d;dbname=%s',
|
|
$config['host'] ?? '',
|
|
(int) ($config['port'] ?? 5432),
|
|
$config['database_name'] ?? ''
|
|
);
|
|
|
|
try {
|
|
$pdo = new PDO(
|
|
$dsn,
|
|
(string) ($config['username'] ?? ''),
|
|
(string) ($config['password'] ?? ''),
|
|
[
|
|
PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION,
|
|
PDO::ATTR_TIMEOUT => 10,
|
|
]
|
|
);
|
|
} catch (PDOException $e) {
|
|
throw new \RuntimeException('PostgreSQL schema connection failed: ' . $e->getMessage());
|
|
}
|
|
|
|
$tableStmt = $pdo->query(
|
|
"SELECT table_name
|
|
FROM information_schema.tables
|
|
WHERE table_schema = 'public'
|
|
ORDER BY table_name"
|
|
);
|
|
$tableRows = $tableStmt ? $tableStmt->fetchAll(PDO::FETCH_ASSOC) : [];
|
|
$tables = [];
|
|
|
|
$columnStmt = $pdo->prepare(
|
|
"SELECT column_name, data_type, is_nullable
|
|
FROM information_schema.columns
|
|
WHERE table_schema = 'public' AND table_name = :table
|
|
ORDER BY ordinal_position"
|
|
);
|
|
|
|
foreach ($tableRows as $row) {
|
|
$tableName = (string) ($row['table_name'] ?? '');
|
|
if ($tableName === '') {
|
|
continue;
|
|
}
|
|
|
|
$columnStmt->execute(['table' => $tableName]);
|
|
$columnsRows = $columnStmt->fetchAll(PDO::FETCH_ASSOC);
|
|
$columns = [];
|
|
foreach ($columnsRows as $column) {
|
|
$columns[] = [
|
|
'name' => (string) ($column['column_name'] ?? ''),
|
|
'type' => (string) ($column['data_type'] ?? ''),
|
|
'nullable' => (string) ($column['is_nullable'] ?? '') === 'YES',
|
|
];
|
|
}
|
|
|
|
$tables[] = [
|
|
'name' => $tableName,
|
|
'columns' => $columns,
|
|
];
|
|
}
|
|
|
|
return ['tables' => $tables];
|
|
}
|
|
|
|
private function fetchApiPreview(array $config): array
|
|
{
|
|
$url = (string) ($config['api_base_url'] ?? '');
|
|
if ($url === '' || ! filter_var($url, FILTER_VALIDATE_URL)) {
|
|
throw new \RuntimeException('A valid API base URL is required.');
|
|
}
|
|
|
|
$headers = ['Accept: application/json'];
|
|
$authType = (string) ($config['api_auth_type'] ?? 'none');
|
|
$authValue = (string) ($config['api_auth_value'] ?? '');
|
|
|
|
if ($authType === 'bearer' && $authValue !== '') {
|
|
$headers[] = 'Authorization: Bearer ' . $authValue;
|
|
} elseif ($authType === 'api_key' && $authValue !== '') {
|
|
$headers[] = 'X-API-KEY: ' . $authValue;
|
|
} elseif ($authType === 'basic' && $authValue !== '') {
|
|
$headers[] = 'Authorization: Basic ' . base64_encode($authValue);
|
|
}
|
|
|
|
$ch = curl_init($url);
|
|
curl_setopt_array($ch, [
|
|
CURLOPT_RETURNTRANSFER => true,
|
|
CURLOPT_CUSTOMREQUEST => (string) ($config['api_method'] ?? 'GET'),
|
|
CURLOPT_TIMEOUT => 10,
|
|
CURLOPT_HTTPHEADER => $headers,
|
|
]);
|
|
$raw = (string) curl_exec($ch);
|
|
$httpCode = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE);
|
|
$error = curl_error($ch);
|
|
curl_close($ch);
|
|
|
|
if ($error !== '') {
|
|
throw new \RuntimeException($error);
|
|
}
|
|
|
|
$decoded = json_decode($raw, true);
|
|
$preview = is_array($decoded) ? $decoded : null;
|
|
if (is_array($preview)) {
|
|
// Keep preview concise for UI readability.
|
|
$preview = $this->limitDepth($preview, 2, 20);
|
|
}
|
|
|
|
return [
|
|
'http_code' => $httpCode,
|
|
'data' => $preview,
|
|
'raw' => mb_substr($raw, 0, 2000),
|
|
];
|
|
}
|
|
|
|
private function loadCsvPreview(array $config): array
|
|
{
|
|
$path = (string) ($config['csv_file_path'] ?? '');
|
|
if ($path === '' || ! is_file($path)) {
|
|
throw new \RuntimeException('CSV file not found.');
|
|
}
|
|
|
|
$delimiter = (string) ($config['csv_delimiter'] ?? ',');
|
|
$handle = fopen($path, 'r');
|
|
if ($handle === false) {
|
|
throw new \RuntimeException('Unable to open CSV file.');
|
|
}
|
|
|
|
$headers = fgetcsv($handle, 0, $delimiter);
|
|
if ($headers === false) {
|
|
fclose($handle);
|
|
throw new \RuntimeException('CSV appears empty.');
|
|
}
|
|
|
|
$rows = [];
|
|
$limit = 20;
|
|
while (($row = fgetcsv($handle, 0, $delimiter)) !== false && count($rows) < $limit) {
|
|
$rowAssoc = [];
|
|
foreach ($headers as $i => $header) {
|
|
$rowAssoc[(string) $header] = (string) ($row[$i] ?? '');
|
|
}
|
|
$rows[] = $rowAssoc;
|
|
}
|
|
fclose($handle);
|
|
|
|
return [
|
|
'headers' => array_map(static fn($h) => (string) $h, $headers),
|
|
'rows' => $rows,
|
|
];
|
|
}
|
|
|
|
/**
|
|
* @param array<string, mixed> $row
|
|
* @return array<string, mixed>
|
|
*/
|
|
private function snapshotDataSource(array $row): array
|
|
{
|
|
$keys = ['id', 'name', 'type', 'host', 'port', 'database_name', 'username', 'api_base_url', 'status'];
|
|
|
|
$out = [];
|
|
foreach ($keys as $k) {
|
|
if (array_key_exists($k, $row)) {
|
|
$out[$k] = $row[$k];
|
|
}
|
|
}
|
|
|
|
return $out;
|
|
}
|
|
|
|
private function limitDepth(array $data, int $depth, int $limit): array
|
|
{
|
|
if ($depth <= 0) {
|
|
return [];
|
|
}
|
|
|
|
$count = 0;
|
|
$result = [];
|
|
foreach ($data as $key => $value) {
|
|
if ($count >= $limit) {
|
|
break;
|
|
}
|
|
$count++;
|
|
|
|
if (is_array($value)) {
|
|
$result[$key] = $this->limitDepth($value, $depth - 1, $limit);
|
|
continue;
|
|
}
|
|
|
|
$result[$key] = $value;
|
|
}
|
|
|
|
return $result;
|
|
}
|
|
}
|