Skip to content

Commit d850180

Browse files
committed
feat: enhance getAllColumnsInTable method to include sampleValue and UUID detection
1 parent e5f49e8 commit d850180

File tree

10 files changed

+91
-32
lines changed

10 files changed

+91
-32
lines changed

adminforth/commands/createResource/templates/resource.ts.hbs

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { v1 as uuid } from "uuid";
21
import { AdminForthResourceInput, AdminForthDataTypes } from "adminforth";
32

43
export default {
@@ -11,7 +10,10 @@ export default {
1110
{
1211
name: "{{this.name}}"{{#if this.type}},
1312
type: AdminForthDataTypes.{{this.type}}{{/if}}{{#if this.isPrimaryKey}},
14-
primaryKey: true{{/if}}
13+
primaryKey: true{{/if}}{{#if this.isUUID}},
14+
components: {
15+
list: "@/renderers/CompactUUID.vue",
16+
}{{/if}},
1517
}{{#unless @last}},{{/unless}}
1618
{{/each}}
1719
],

adminforth/dataConnectors/baseConnector.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -357,7 +357,7 @@ export default class AdminForthBaseConnector implements IAdminForthDataSourceCon
357357
throw new Error('getAllTables() must be implemented in subclass');
358358
}
359359

360-
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; type?: string; isPrimaryKey?: boolean }>> {
360+
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; type?: string; isPrimaryKey?: boolean; sampleValue?: any; }>> {
361361
throw new Error('getAllColumnsInTable() must be implemented in subclass');
362362
}
363363

adminforth/dataConnectors/clickhouse.ts

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
4343
return jsonResult.data.map((row: any) => row.name);
4444
}
4545

46-
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; type?: string; isPrimaryKey?: boolean }>> {
46+
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; sampleValue?: any }>> {
4747
const res = await this.client.query({
4848
query: `
4949
SELECT name
@@ -57,7 +57,31 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
5757
});
5858

5959
const jsonResult = await res.json();
60-
return jsonResult.data.map((row: any) => ({ name: row.name }));
60+
const orderByField = ['updated_at', 'created_at', 'id'].find(f =>
61+
jsonResult.data.some((col: any) => col.name === f)
62+
);
63+
64+
let sampleRow = {};
65+
if (orderByField) {
66+
const sampleRes = await this.client.query({
67+
query: `SELECT * FROM ${this.dbName}.${tableName} ORDER BY ${orderByField} DESC LIMIT 1`,
68+
format: 'JSON',
69+
});
70+
const sampleJson = await sampleRes.json();
71+
sampleRow = sampleJson.data?.[0] ?? {};
72+
} else {
73+
const sampleRes = await this.client.query({
74+
query: `SELECT * FROM ${this.dbName}.${tableName} LIMIT 1`,
75+
format: 'JSON',
76+
});
77+
const sampleJson = await sampleRes.json();
78+
sampleRow = sampleJson.data?.[0] ?? {};
79+
}
80+
81+
return jsonResult.data.map((col: any) => ({
82+
name: col.name,
83+
sampleValue: sampleRow[col.name],
84+
}));
6185
}
6286

6387
async discoverFields(resource: AdminForthResource): Promise<{[key: string]: AdminForthResourceColumn}> {

adminforth/dataConnectors/mongo.ts

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,13 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
5454
return collections.map(col => col.name);
5555
}
5656

57-
async getAllColumnsInTable(collectionName: string): Promise<Array<{ name: string; type?: string; isPrimaryKey?: boolean }>> {
57+
async getAllColumnsInTable(collectionName: string): Promise<Array<{ name: string; type: string; isPrimaryKey?: boolean; sampleValue?: any; }>> {
5858

5959
const sampleDocs = await this.client.db().collection(collectionName).find({}).sort({ _id: -1 }).limit(100).toArray();
6060

6161
const fieldTypes = new Map<string, Set<string>>();
62-
62+
const sampleValues = new Map<string, any>();
63+
6364
function detectType(value: any): string {
6465
if (value === null || value === undefined) return 'string';
6566
if (typeof value === 'string') return 'string';
@@ -82,9 +83,10 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
8283
function flattenObject(obj: any, prefix = '') {
8384
Object.entries(obj).forEach(([key, value]) => {
8485
const fullKey = prefix ? `${prefix}.${key}` : key;
85-
86-
if (fullKey.startsWith('_id.') && typeof value !== 'string' && typeof value !== 'number') {
87-
return;
86+
87+
if (!fieldTypes.has(fullKey)) {
88+
fieldTypes.set(fullKey, new Set());
89+
sampleValues.set(fullKey, value);
8890
}
8991

9092
if (
@@ -101,7 +103,6 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
101103
!Array.isArray(value) &&
102104
!(value instanceof Date)
103105
) {
104-
// Вместо рекурсивного прохода — просто добавляем этот объект целиком как json
105106
addType(fullKey, 'json');
106107
} else {
107108
addType(fullKey, detectType(value));
@@ -134,6 +135,7 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
134135
name,
135136
type: typeMap[matched] ?? 'STRING',
136137
...(primaryKey ? { isPrimaryKey: true } : {}),
138+
sampleValue: sampleValues.get(name),
137139
};
138140
});
139141
}

adminforth/dataConnectors/mysql.ts

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -50,18 +50,24 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
5050
return rows.map((row: any) => row.TABLE_NAME);
5151
}
5252

53-
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; type?: string; isPrimaryKey?: boolean }>> {
54-
const [rows] = await this.client.query(
55-
`
56-
SELECT column_name
57-
FROM information_schema.columns
58-
WHERE table_name = ? AND table_schema = DATABASE();
59-
`,
53+
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; sampleValue?: any }>> {
54+
const [columns] = await this.client.query(
55+
`SELECT column_name FROM information_schema.columns WHERE table_name = ? AND table_schema = DATABASE()`,
6056
[tableName]
6157
);
6258

63-
return rows.map((row: any) => ({
64-
name: row.COLUMN_NAME,
59+
const columnNames = columns.map((c: any) => c.COLUMN_NAME);
60+
const orderByField = ['updated_at', 'created_at', 'id'].find(f => columnNames.includes(f));
61+
62+
let [rows] = orderByField
63+
? await this.client.query(`SELECT * FROM \`${tableName}\` ORDER BY \`${orderByField}\` DESC LIMIT 1`)
64+
: await this.client.query(`SELECT * FROM \`${tableName}\` LIMIT 1`);
65+
66+
const sampleRow = rows[0] || {};
67+
68+
return columns.map((col: any) => ({
69+
name: col.COLUMN_NAME,
70+
sampleValue: sampleRow[col.COLUMN_NAME],
6571
}));
6672
}
6773

adminforth/dataConnectors/postgres.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -54,14 +54,15 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
5454
return res.rows.map(row => row.table_name);
5555
}
5656

57-
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; type?: string; isPrimaryKey?: boolean }>> {
57+
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; sampleValue?: any }>> {
5858
const res = await this.client.query(`
5959
SELECT column_name
6060
FROM information_schema.columns
6161
WHERE table_name = $1 AND table_schema = 'public';
6262
`, [tableName]);
63-
64-
return res.rows.map(row => ({ name: row.column_name }));
63+
const sampleRowRes = await this.client.query(`SELECT * FROM ${tableName} ORDER BY ctid DESC LIMIT 1`);
64+
const sampleRow = sampleRowRes.rows[0] ?? {};
65+
return res.rows.map(row => ({ name: row.column_name, sampleValue: sampleRow[row.column_name] }));
6566
}
6667

6768
async discoverFields(resource) {

adminforth/dataConnectors/sqlite.ts

Lines changed: 16 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,12 +16,24 @@ class SQLiteConnector extends AdminForthBaseConnector implements IAdminForthData
1616
const rows = stmt.all();
1717
return rows.map((row) => row.name);
1818
}
19-
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string }>> {
19+
async getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; sampleValue?: any }>> {
2020
const stmt = this.client.prepare(`PRAGMA table_info(${tableName});`);
21-
const rows = stmt.all();
21+
const columns = stmt.all();
22+
23+
const orderByField = columns.find(c => ['created_at', 'id'].includes(c.name))?.name;
24+
25+
let sampleRow = {};
26+
if (orderByField) {
27+
const rowStmt = this.client.prepare(`SELECT * FROM ${tableName} ORDER BY ${orderByField} DESC LIMIT 1`);
28+
sampleRow = rowStmt.get() || {};
29+
} else {
30+
const rowStmt = this.client.prepare(`SELECT * FROM ${tableName} LIMIT 1`);
31+
sampleRow = rowStmt.get() || {};
32+
}
2233

23-
return rows.map(row => ({
24-
name: row.name || '',
34+
return columns.map(col => ({
35+
name: col.name || '',
36+
sampleValue: sampleRow[col.name],
2537
}));
2638
}
2739

adminforth/index.ts

Lines changed: 7 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import SQLiteConnector from './dataConnectors/sqlite.js';
66
import CodeInjector from './modules/codeInjector.js';
77
import ExpressServer from './servers/express.js';
88
// import FastifyServer from './servers/fastify.js';
9-
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp } from './modules/utils.js';
9+
import { ADMINFORTH_VERSION, listify, suggestIfTypo, RateLimiter, RAMLock, getClientIp, isProbablyUUIDColumn } from './modules/utils.js';
1010
import {
1111
type AdminForthConfig,
1212
type IAdminForth,
@@ -408,8 +408,8 @@ class AdminForth implements IAdminForth {
408408

409409
async getAllColumnsInTable(
410410
tableName: string
411-
): Promise<{ [dataSourceId: string]: Array<{ name: string; type?: string; isPrimaryKey?: boolean }> }> {
412-
const results: { [dataSourceId: string]: Array<{ name: string; type?: string; isPrimaryKey?: boolean }> } = {};
411+
): Promise<{ [dataSourceId: string]: Array<{ name: string; type?: string; isPrimaryKey?: boolean; isUUID?: boolean; }> }> {
412+
const results: { [dataSourceId: string]: Array<{ name: string; type?: string; isPrimaryKey?: boolean; isUUID?: boolean; }> } = {};
413413

414414
if (!this.config.databaseConnectors) {
415415
this.config.databaseConnectors = { ...this.connectorClasses };
@@ -420,7 +420,10 @@ class AdminForth implements IAdminForth {
420420
if (typeof connector.getAllColumnsInTable === 'function') {
421421
try {
422422
const columns = await connector.getAllColumnsInTable(tableName);
423-
results[dataSourceId] = columns;
423+
results[dataSourceId] = columns.map(column => ({
424+
...column,
425+
isUUID: isProbablyUUIDColumn(column),
426+
}));
424427
} catch (err) {
425428
console.error(`Error getting columns for table ${tableName} in dataSource ${dataSourceId}:`, err);
426429
results[dataSourceId] = [];

adminforth/modules/utils.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -487,4 +487,13 @@ export class RAMLock {
487487
}
488488
}
489489
}
490+
}
491+
492+
export function isProbablyUUIDColumn(column: { name: string; type?: string; sampleValue?: any }): boolean {
493+
if (column.type?.toLowerCase() === 'uuid') return true;
494+
if (typeof column.sampleValue === 'string') {
495+
return /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i.test(column.sampleValue);
496+
}
497+
498+
return false;
490499
}

adminforth/types/Back.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -143,7 +143,7 @@ export interface IAdminForthDataSourceConnector {
143143
/**
144144
* Function to get all columns in table.
145145
*/
146-
getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; type?: string; isPrimaryKey?: boolean }>>;
146+
getAllColumnsInTable(tableName: string): Promise<Array<{ name: string; type?: string; isPrimaryKey?: boolean; sampleValue?: any; }>>;
147147
/**
148148
* Optional.
149149
* You an redefine this function to define how one record should be fetched from database.

0 commit comments

Comments
 (0)