Skip to content

Commit b52eead

Browse files
committed
Merge branch 'next' of github.com:devforth/adminforth into next
2 parents c89d98f + f8394d6 commit b52eead

File tree

11 files changed

+956
-28
lines changed

11 files changed

+956
-28
lines changed

adminforth/commands/createResource/generateResourceFile.js

Lines changed: 37 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,60 @@ export async function generateResourceFile({
1717
dataSource = "maindb",
1818
resourcesDir = "resources"
1919
}) {
20-
const fileName = `${table}.ts`;
21-
const filePath = path.resolve(process.cwd(), resourcesDir, fileName);
20+
const baseFileName = `${table}.ts`;
21+
const baseFilePath = path.resolve(process.cwd(), resourcesDir, baseFileName);
2222

23-
if (fsSync.existsSync(filePath)) {
24-
console.log(chalk.yellow(`⚠️ File already exists: ${filePath}`));
25-
return { alreadyExists: true, path: filePath };
23+
if (fsSync.existsSync(baseFilePath)) {
24+
const content = await fs.readFile(baseFilePath, "utf-8");
25+
const match = content.match(/dataSource:\s*["'](.+?)["']/);
26+
const existingDataSource = match?.[1];
27+
if (existingDataSource === dataSource) {
28+
console.log(chalk.yellow(`⚠️ File already exists with same dataSource: ${baseFilePath}`));
29+
return { alreadyExists: true, path: baseFilePath, fileName: baseFileName, resourceId: table };
30+
} else {
31+
const suffixedFileName = `${table}_${dataSource}.ts`;
32+
const suffixedFilePath = path.resolve(process.cwd(), resourcesDir, suffixedFileName);
33+
return await writeResourceFile(suffixedFilePath, suffixedFileName, {
34+
table,
35+
columns,
36+
dataSource,
37+
resourceId: `${table}_${dataSource}`,
38+
});
39+
}
2640
}
41+
42+
return await writeResourceFile(baseFilePath, baseFileName, {
43+
table,
44+
columns,
45+
dataSource,
46+
resourceId: table,
47+
});
48+
}
49+
50+
async function writeResourceFile(filePath, fileName, {
51+
table,
52+
columns,
53+
dataSource,
54+
resourceId,
55+
}) {
2756
const __filename = fileURLToPath(import.meta.url);
2857
const __dirname = path.dirname(__filename);
2958
const templatePath = path.resolve(__dirname, "templates/resource.ts.hbs");
3059
console.log(chalk.dim(`Using template: ${templatePath}`));
60+
3161
const context = {
3262
table,
3363
dataSource,
34-
resourceId: table,
64+
resourceId,
3565
label: table.charAt(0).toUpperCase() + table.slice(1),
3666
columns,
3767
};
3868

3969
const content = await renderHBSTemplate(templatePath, context);
40-
4170
await fs.mkdir(path.dirname(filePath), { recursive: true });
4271
await fs.writeFile(filePath, content, "utf-8");
4372

4473
console.log(chalk.green(`✅ Generated resource file: ${filePath}`));
4574

46-
return { alreadyExists: false, path: filePath };
75+
return { alreadyExists: false, path: filePath, fileName, resourceId };
4776
}

adminforth/commands/createResource/main.js

Lines changed: 25 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,19 +2,17 @@ import { callTsProxy, findAdminInstance } from "../callTsProxy.js";
22
import { toTitleCase } from '../utils.js';
33
import { generateResourceFile } from "./generateResourceFile.js";
44
import { injectResourceIntoIndex } from "./injectResourceIntoIndex.js";
5-
import { select } from "@inquirer/prompts";
5+
import { search, Separator } from "@inquirer/prompts";
66

77
export default async function createResource(args) {
8-
console.log("Bundling admin SPA...");
98
const instance = await findAdminInstance();
10-
console.log("🪲 Found admin instance:", instance.file);
11-
console.log("🪲 Found admin instance:", instance.file);
12-
console.log(JSON.stringify(instance));
139
const tables = await callTsProxy(`
1410
import { admin } from './${instance.file}.js';
1511
export async function exec() {
1612
await admin.discoverDatabases();
17-
return await admin.getAllTables();
13+
const allTables = await admin.getAllTables();
14+
setTimeout(process.exit);
15+
return allTables;
1816
}
1917
`);
2018

@@ -25,28 +23,41 @@ export default async function createResource(args) {
2523
}))
2624
);
2725

28-
const table = await select({
29-
message: "🗂 Choose a table to generate a resource for:",
30-
choices: tableChoices,
26+
const table = await search({
27+
message: '🔍 Choose a table to generate a resource for:',
28+
source: async (input = '') => {
29+
const term = input.toLowerCase();
30+
const choices = tableChoices
31+
.filter(c =>
32+
c.name.toLowerCase().includes(term)
33+
)
34+
.map(c => ({ name: c.name, value: c.value }));
35+
return [
36+
...choices,
37+
new Separator(),
38+
];
39+
},
3140
});
3241

3342
const columns = await callTsProxy(`
3443
import { admin } from './${instance.file}.js';
3544
export async function exec() {
3645
await admin.discoverDatabases();
37-
return await admin.getAllColumnsInTable("${table.table}");
46+
const columns = await admin.getAllColumnsInTable("${table.table}");
47+
setTimeout(process.exit);
48+
return columns;
3849
}
3950
`);
40-
console.log("🪲 Found columns:", columns);
4151

42-
generateResourceFile({
52+
const { resourceId } = await generateResourceFile({
4353
table: table.table,
4454
columns: columns[table.db],
4555
dataSource: table.db,
4656
});
57+
4758
injectResourceIntoIndex({
48-
table: table.table,
49-
resourceId: table.table,
59+
table: resourceId,
60+
resourceId: resourceId,
5061
label: toTitleCase(table.table),
5162
icon: "flowbite:user-solid",
5263
});

adminforth/dataConnectors/clickhouse.ts

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,36 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
3030
// }
3131
});
3232
}
33-
33+
async getAllTables(): Promise<Array<string>> {
34+
const res = await this.client.query({
35+
query: `
36+
SELECT name
37+
FROM system.tables
38+
WHERE database = '${this.dbName}'
39+
`,
40+
format: 'JSON',
41+
});
42+
const jsonResult = await res.json();
43+
return jsonResult.data.map((row: any) => row.name);
44+
}
45+
46+
async getAllColumnsInTable(tableName: string): Promise<string[]> {
47+
const res = await this.client.query({
48+
query: `
49+
SELECT name
50+
FROM system.columns
51+
WHERE database = '${this.dbName}' AND table = {table:String}
52+
`,
53+
format: 'JSON',
54+
query_params: {
55+
table: tableName,
56+
},
57+
});
58+
59+
const jsonResult = await res.json();
60+
return jsonResult.data.map((row: any) => row.name);
61+
}
62+
3463
async discoverFields(resource: AdminForthResource): Promise<{[key: string]: AdminForthResourceColumn}> {
3564
const tableName = resource.table;
3665

@@ -79,7 +108,7 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
79108
field._underlineType = baseType;
80109
field._baseTypeDebug = baseType;
81110
field.required = row.notnull == 1;
82-
field.primaryKey = row.pk == 1;
111+
field.primaryKey = row.is_in_primary_key == 1;
83112
field.default = row.dflt_value;
84113
fieldTypes[row.name] = field
85114
});

adminforth/dataConnectors/mongo.ts

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,42 @@ class MongoConnector extends AdminForthBaseConnector implements IAdminForthDataS
4646
[AdminForthSortDirections.asc]: 1,
4747
[AdminForthSortDirections.desc]: -1,
4848
};
49+
async getAllTables(): Promise<Array<string>>{
50+
const db = this.client.db();
51+
52+
const collections = await db.listCollections().toArray();
53+
54+
return collections.map(col => col.name);
55+
}
4956

57+
async getAllColumnsInTable(collectionName: string): Promise<Array<string>> {
58+
59+
const sampleDocs = await this.client.db().collection(collectionName).find({}).sort({ _id: -1 }).limit(100).toArray();
60+
61+
const fieldSet = new Set<string>();
62+
63+
function flattenObject(obj: any, prefix = '') {
64+
Object.entries(obj).forEach(([key, value]) => {
65+
const fullKey = prefix ? `${prefix}.${key}` : key;
66+
if (fullKey.startsWith('_id.') && typeof value !== 'string' && typeof value !== 'number') {
67+
return;
68+
}
69+
if (value && typeof value === 'object' && !Array.isArray(value) && !(value instanceof Date)) {
70+
flattenObject(value, fullKey);
71+
} else {
72+
fieldSet.add(fullKey);
73+
}
74+
});
75+
}
76+
77+
for (const doc of sampleDocs) {
78+
flattenObject(doc);
79+
}
80+
81+
return Array.from(fieldSet);
82+
}
83+
84+
5085
async discoverFields(resource) {
5186
return resource.columns.filter((col) => !col.virtual).reduce((acc, col) => {
5287
if (!col.type) {

adminforth/dataConnectors/mysql.ts

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,29 @@ class MysqlConnector extends AdminForthBaseConnector implements IAdminForthDataS
3939
[AdminForthSortDirections.desc]: 'DESC',
4040
};
4141

42+
async getAllTables(): Promise<Array<string>> {
43+
const [rows] = await this.client.query(
44+
`
45+
SELECT table_name
46+
FROM information_schema.tables
47+
WHERE table_schema = DATABASE() AND table_type = 'BASE TABLE';
48+
`
49+
);
50+
return rows.map((row: any) => row.TABLE_NAME);
51+
}
52+
53+
async getAllColumnsInTable(tableName: string): Promise<Array<string>> {
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+
`,
60+
[tableName]
61+
);
62+
return rows.map((row: any) => row.COLUMN_NAME);
63+
}
64+
4265
async discoverFields(resource) {
4366
const [results] = await this.client.execute("SHOW COLUMNS FROM " + resource.table);
4467
const fieldTypes = {};

adminforth/dataConnectors/postgres.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,24 @@ class PostgresConnector extends AdminForthBaseConnector implements IAdminForthDa
4545
[AdminForthSortDirections.desc]: 'DESC',
4646
};
4747

48+
async getAllTables(): Promise<Array<string>> {
49+
const res = await this.client.query(`
50+
SELECT table_name
51+
FROM information_schema.tables
52+
WHERE table_schema = 'public' AND table_type = 'BASE TABLE';
53+
`);
54+
return res.rows.map(row => row.table_name);
55+
}
56+
57+
async getAllColumnsInTable(tableName: string): Promise<Array<string>> {
58+
const res = await this.client.query(`
59+
SELECT column_name
60+
FROM information_schema.columns
61+
WHERE table_name = $1 AND table_schema = 'public';
62+
`, [tableName]);
63+
return res.rows.map(row => row.column_name);
64+
}
65+
4866
async discoverFields(resource) {
4967

5068
const tableName = resource.table;

adminforth/documentation/docs/tutorial/03-Customization/13-standardPagesTuning.md

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -136,6 +136,22 @@ export default {
136136
]
137137
```
138138

139+
### Virtual scroll
140+
141+
Set `options.listVirtualScrollEnabled` to true to enable virtual scrolling in the table
142+
143+
```typescript title="./resources/apartments.ts"
144+
export default {
145+
resourceId: 'aparts',
146+
options: {
147+
...
148+
//diff-add
149+
listVirtualScrollEnabled: true,
150+
}
151+
}
152+
]
153+
```
154+
139155
### Custom row click action
140156

141157
By default, when you click on a record in the list view, the show view will be opened.

0 commit comments

Comments
 (0)