Skip to content

Commit 441eab6

Browse files
authored
Merge pull request #230 from devforth/next
Next
2 parents e9aa8c5 + 4f5c364 commit 441eab6

38 files changed

+1482
-141
lines changed

adminforth/commands/bundle.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ async function bundle() {
1616
`);
1717

1818
} catch (e) {
19-
console.log(`Running file ${file} failed`, e);
19+
console.log(`Running budndle failed`, e);
2020
}
2121
}
2222

adminforth/commands/createApp/templates/Dockerfile.hbs

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,5 +3,5 @@ WORKDIR /code/
33
ADD package.json package-lock.json /code/
44
RUN npm ci
55
ADD . /code/
6-
RUN --mount=type=cache,target=/tmp npx adminforth bundle
6+
RUN npx adminforth bundle
77
CMD ["sh", "-c", "npm run migrate:prod && npm run prod"]

adminforth/commands/createCustomComponent/configUpdater.js

Lines changed: 60 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -47,9 +47,27 @@ async function findResourceFilePath(resourceId) {
4747

4848
if (n.TSAsExpression.check(declaration) && n.ObjectExpression.check(declaration.expression)) {
4949
objectExpressionNode = declaration.expression;
50-
}
51-
else if (n.ObjectExpression.check(declaration)) {
50+
} else if (n.ObjectExpression.check(declaration)) {
5251
objectExpressionNode = declaration;
52+
} else if (n.Identifier.check(declaration)) {
53+
const varName = declaration.name;
54+
55+
recast.visit(ast, {
56+
visitVariableDeclaration(path) {
57+
for (const decl of path.node.declarations) {
58+
if (
59+
n.VariableDeclarator.check(decl) &&
60+
n.Identifier.check(decl.id) &&
61+
decl.id.name === varName &&
62+
n.ObjectExpression.check(decl.init)
63+
) {
64+
objectExpressionNode = decl.init;
65+
return false;
66+
}
67+
}
68+
this.traverse(path);
69+
}
70+
});
5371
}
5472

5573
if (objectExpressionNode) {
@@ -118,6 +136,25 @@ export async function updateResourceConfig(resourceId, columnName, fieldType, co
118136
objectExpressionNode = declaration.expression;
119137
} else if (n.ObjectExpression.check(declaration)) {
120138
objectExpressionNode = declaration;
139+
} else if (n.Identifier.check(declaration)) {
140+
const varName = declaration.name;
141+
142+
recast.visit(ast, {
143+
visitVariableDeclaration(path) {
144+
for (const decl of path.node.declarations) {
145+
if (
146+
n.VariableDeclarator.check(decl) &&
147+
n.Identifier.check(decl.id) &&
148+
decl.id.name === varName &&
149+
n.ObjectExpression.check(decl.init)
150+
) {
151+
objectExpressionNode = decl.init;
152+
return false;
153+
}
154+
}
155+
this.traverse(path);
156+
}
157+
});
121158
}
122159

123160
if (!objectExpressionNode) {
@@ -446,9 +483,28 @@ export async function updateCrudInjectionConfig(resourceId, crudType, injectionP
446483
let objectExpressionNode = null;
447484

448485
if (n.TSAsExpression.check(declaration) && n.ObjectExpression.check(declaration.expression)) {
449-
objectExpressionNode = declaration.expression;
486+
objectExpressionNode = declaration.expression;
450487
} else if (n.ObjectExpression.check(declaration)) {
451-
objectExpressionNode = declaration;
488+
objectExpressionNode = declaration;
489+
} else if (n.Identifier.check(declaration)) {
490+
const varName = declaration.name;
491+
492+
recast.visit(ast, {
493+
visitVariableDeclaration(path) {
494+
for (const decl of path.node.declarations) {
495+
if (
496+
n.VariableDeclarator.check(decl) &&
497+
n.Identifier.check(decl.id) &&
498+
decl.id.name === varName &&
499+
n.ObjectExpression.check(decl.init)
500+
) {
501+
objectExpressionNode = decl.init;
502+
return false;
503+
}
504+
}
505+
this.traverse(path);
506+
}
507+
});
452508
}
453509

454510
if (!objectExpressionNode) {

adminforth/commands/createCustomComponent/main.js

Lines changed: 40 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -58,13 +58,26 @@ async function handleFieldComponentCreation(config, resources) {
5858

5959
console.log(chalk.grey(`Selected ❯ 🔤 Custom fields ❯ ${fieldType}`));
6060

61-
const resourceId = await select({
62-
message: 'Select resource for which you want to change component:',
63-
choices: [
64-
...resources.map(r => ({ name: `${r.label} ${chalk.grey(`${r.resourceId}`)}`, value: r.resourceId })),
65-
new Separator(),
66-
{ name: '🔙 BACK', value: '__BACK__' },
67-
]
61+
const resourceId = await search({
62+
message: 'Select resource for which you want to change component:',
63+
source: async (input) => {
64+
const searchTerm = input ? input.toLowerCase() : '';
65+
66+
const filtered = resources.filter(r => {
67+
const label = r.label || '';
68+
const id = r.resourceId || '';
69+
return label.toLowerCase().includes(searchTerm) || id.toLowerCase().includes(searchTerm);
70+
});
71+
72+
return [
73+
...filtered.map(r => ({
74+
name: `${r.label} ${chalk.grey(`${r.resourceId}`)}`,
75+
value: r.resourceId,
76+
})),
77+
new Separator(),
78+
{ name: '🔙 BACK', value: '__BACK__' },
79+
];
80+
}
6881
});
6982
if (resourceId === '__BACK__') return handleFieldComponentCreation(config, resources); // Pass config back
7083

@@ -144,14 +157,28 @@ async function handleCrudPageInjectionCreation(config, resources) {
144157

145158
console.log(chalk.grey(`Selected ❯ 📄 CRUD Page Injection ❯ ${crudType}`));
146159

147-
const resourceId = await select({
160+
const resourceId = await search({
148161
message: 'Select resource for which you want to inject the component:',
149-
choices: [
150-
...resources.map(r => ({ name: `${r.label} ${chalk.grey(`${r.resourceId}`)}`, value: r.resourceId })),
151-
new Separator(),
152-
{ name: '🔙 BACK', value: '__BACK__' },
153-
],
162+
source: async (input) => {
163+
const searchTerm = input ? input.toLowerCase() : '';
164+
165+
const filtered = resources.filter(r => {
166+
const label = r.label || '';
167+
const id = r.resourceId || '';
168+
return label.toLowerCase().includes(searchTerm) || id.toLowerCase().includes(searchTerm);
169+
});
170+
171+
return [
172+
...filtered.map(r => ({
173+
name: `${r.label} ${chalk.grey(`${r.resourceId}`)}`,
174+
value: r.resourceId,
175+
})),
176+
new Separator(),
177+
{ name: '🔙 BACK', value: '__BACK__' },
178+
];
179+
}
154180
});
181+
155182
if (resourceId === '__BACK__') return handleCrudPageInjectionCreation(config, resources);
156183

157184
const selectedResource = resources.find(r => r.resourceId === resourceId);

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: 35 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
});
@@ -178,6 +207,10 @@ class ClickhouseConnector extends AdminForthBaseConnector implements IAdminForth
178207
const column = resource.dataSourceColumns.find((col) => col.name == field);
179208
let placeholder = `{f$?:${column._underlineType}}`;
180209
let operator = this.OperatorsMap[filter.operator];
210+
if ((filter.operator == AdminForthFilterOperators.LIKE || filter.operator == AdminForthFilterOperators.ILIKE) && column._underlineType == 'UUID') {
211+
placeholder = '{f$?:String}';
212+
field = `toString(${field})`;
213+
}
181214
if (filter.operator == AdminForthFilterOperators.IN || filter.operator == AdminForthFilterOperators.NIN) {
182215
placeholder = `(${filter.value.map((_, j) => `{p$?:${column._underlineType}}`).join(', ')})`;
183216
} else if (filter.operator == AdminForthFilterOperators.EQ && filter.value === null) {

0 commit comments

Comments
 (0)