Skip to content

Commit 0b9e623

Browse files
committed
feat: enhance resource file generation and column synchronization logic
1 parent 3b5399a commit 0b9e623

5 files changed

Lines changed: 240 additions & 10 deletions

File tree

adminforth/commands/callTsProxy.js

Lines changed: 31 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,27 @@ import dotenv from "dotenv";
88
const currentFilePath = import.meta.url;
99
const currentFileFolder = path.dirname(currentFilePath).replace("file:", "");
1010

11+
function getLocalBinPath(currentDirectory) {
12+
return path.join(currentDirectory, "node_modules", ".bin");
13+
}
14+
15+
function getLocalBinExecutable(currentDirectory, command) {
16+
const extension = process.platform === "win32" ? ".cmd" : "";
17+
const executablePath = path.join(getLocalBinPath(currentDirectory), `${command}${extension}`);
18+
return fs.existsSync(executablePath) ? executablePath : command;
19+
}
20+
21+
function getEnvWithLocalBin(currentDirectory) {
22+
const pathKey = process.platform === "win32" ? "Path" : "PATH";
23+
const localBinPath = getLocalBinPath(currentDirectory);
24+
const currentPath = process.env[pathKey] || "";
25+
26+
return {
27+
...process.env,
28+
[pathKey]: [localBinPath, currentPath].filter(Boolean).join(path.delimiter),
29+
};
30+
}
31+
1132
export function callTsProxy(tsCode, silent=false) {
1233

1334
const currentDirectory = process.cwd();
@@ -22,8 +43,8 @@ export function callTsProxy(tsCode, silent=false) {
2243

2344
process.env.HEAVY_DEBUG && console.log("🌐 Calling tsproxy with code:", path.join(currentFileFolder, "proxy.ts"));
2445
return new Promise((resolve, reject) => {
25-
const child = spawn("tsx", [path.join(currentFileFolder, "proxy.ts")], {
26-
env: process.env,
46+
const child = spawn(getLocalBinExecutable(currentDirectory, "tsx"), [path.join(currentFileFolder, "proxy.ts")], {
47+
env: getEnvWithLocalBin(currentDirectory),
2748
});
2849
let stderr = "";
2950
let stdoutLogs = [];
@@ -36,8 +57,16 @@ export function callTsProxy(tsCode, silent=false) {
3657
stderr += data;
3758
});
3859

60+
child.on("error", (error) => {
61+
reject(error);
62+
});
63+
3964
child.on("close", (code) => {
4065
const tsProxyResult = stdoutLogs.find(log => log.includes('>>>>>>>'));
66+
if (!tsProxyResult) {
67+
reject(new Error(`Invalid JSON from tsproxy. stdout: ${stdoutLogs.join("")}, stderr: ${stderr}`));
68+
return;
69+
}
4170
const preparedStdout = tsProxyResult.slice(tsProxyResult.indexOf('>>>>>>>') + 7, tsProxyResult.lastIndexOf('<<<<<<<'));
4271
const preparedStdoutLogs = stdoutLogs.filter(log => !log.includes('>>>>>>>'));
4372
if (code === 0) {

adminforth/commands/createResource/generateResourceFile.js

Lines changed: 198 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,32 @@ import path from "path";
44
import chalk from "chalk";
55
import Handlebars from "handlebars";
66
import { fileURLToPath } from 'url';
7+
import { parse } from "@babel/parser";
8+
import * as recast from "recast";
9+
import { namedTypes as n, builders as b } from "ast-types";
10+
11+
const DATA_SOURCE_RE = /dataSource:\s*["'](.+?)["']/;
12+
const ADMINFORTH_DATA_TYPE_KEYS = {
13+
string: "STRING",
14+
integer: "INTEGER",
15+
float: "FLOAT",
16+
decimal: "DECIMAL",
17+
boolean: "BOOLEAN",
18+
date: "DATE",
19+
datetime: "DATETIME",
20+
time: "TIME",
21+
text: "TEXT",
22+
json: "JSON",
23+
};
24+
25+
const parser = {
26+
parse(source) {
27+
return parse(source, {
28+
sourceType: "module",
29+
plugins: ["typescript"],
30+
});
31+
},
32+
};
733

834
export async function renderHBSTemplate(templatePath, data){
935
const templateContent = await fs.readFile(templatePath, "utf-8");
@@ -22,14 +48,35 @@ export async function generateResourceFile({
2248

2349
if (fsSync.existsSync(baseFilePath)) {
2450
const content = await fs.readFile(baseFilePath, "utf-8");
25-
const match = content.match(/dataSource:\s*["'](.+?)["']/);
51+
const match = content.match(DATA_SOURCE_RE);
2652
const existingDataSource = match?.[1];
2753
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 };
54+
const syncedColumnsCount = await syncResourceColumns(baseFilePath, content, columns);
55+
return {
56+
alreadyExists: true,
57+
path: baseFilePath,
58+
fileName: baseFileName,
59+
resourceId: table,
60+
syncedColumnsCount,
61+
};
3062
} else {
3163
const suffixedFileName = `${table}_${dataSource}.ts`;
3264
const suffixedFilePath = path.resolve(process.cwd(), resourcesDir, suffixedFileName);
65+
if (fsSync.existsSync(suffixedFilePath)) {
66+
const suffixedContent = await fs.readFile(suffixedFilePath, "utf-8");
67+
const suffixedMatch = suffixedContent.match(DATA_SOURCE_RE);
68+
const suffixedDataSource = suffixedMatch?.[1];
69+
if (suffixedDataSource === dataSource) {
70+
const syncedColumnsCount = await syncResourceColumns(suffixedFilePath, suffixedContent, columns);
71+
return {
72+
alreadyExists: true,
73+
path: suffixedFilePath,
74+
fileName: suffixedFileName,
75+
resourceId: `${table}_${dataSource}`,
76+
syncedColumnsCount,
77+
};
78+
}
79+
}
3380
return await writeResourceFile(suffixedFilePath, suffixedFileName, {
3481
table,
3582
columns,
@@ -63,7 +110,7 @@ async function writeResourceFile(filePath, fileName, {
63110
dataSource,
64111
resourceId,
65112
label: table.charAt(0).toUpperCase() + table.slice(1),
66-
columns,
113+
columns: columns.map(normalizeColumnForTemplate),
67114
};
68115

69116
const content = await renderHBSTemplate(templatePath, context);
@@ -74,3 +121,150 @@ async function writeResourceFile(filePath, fileName, {
74121

75122
return { alreadyExists: false, path: filePath, fileName, resourceId };
76123
}
124+
125+
async function syncResourceColumns(filePath, content, discoveredColumns) {
126+
const ast = recast.parse(content, { parser });
127+
const columnsArray = findResourceColumnsArray(ast);
128+
129+
if (!columnsArray) {
130+
throw new Error(`Could not find resource columns array in ${filePath}`);
131+
}
132+
133+
const existingColumnNames = new Set(
134+
columnsArray.elements
135+
.filter((element) => n.ObjectExpression.check(element))
136+
.map((element) => getObjectPropertyValue(element, "name"))
137+
.filter(Boolean)
138+
);
139+
140+
const columnsToImport = discoveredColumns.filter((column) => !existingColumnNames.has(column.name));
141+
142+
if (!columnsToImport.length) {
143+
console.log(chalk.green(`✅ Resource is already in sync: ${filePath}`));
144+
return 0;
145+
}
146+
147+
console.log(chalk.cyan(`ℹ️ Going to import ${formatColumnsCount(columnsToImport.length)}: ${columnsToImport.map((column) => column.name).join(", ")}`));
148+
149+
columnsArray.elements.push(...columnsToImport.map(createColumnAstNode));
150+
151+
const newContent = recast.print(ast, {
152+
tabWidth: 2,
153+
useTabs: false,
154+
trailingComma: true,
155+
wrapColumn: 1,
156+
}).code;
157+
158+
await fs.writeFile(filePath, newContent, "utf-8");
159+
console.log(chalk.green(`✅ Imported ${formatColumnsCount(columnsToImport.length)} into resource file: ${filePath}`));
160+
161+
return columnsToImport.length;
162+
}
163+
164+
function findResourceColumnsArray(ast) {
165+
let columnsArray = null;
166+
167+
recast.visit(ast, {
168+
visitObjectExpression(path) {
169+
const properties = path.node.properties;
170+
const columnsProp = findObjectProperty(properties, "columns");
171+
const hasResourceShape = findObjectProperty(properties, "dataSource") && findObjectProperty(properties, "table");
172+
173+
if (hasResourceShape && columnsProp && n.ArrayExpression.check(columnsProp.value)) {
174+
columnsArray = columnsProp.value;
175+
return false;
176+
}
177+
178+
this.traverse(path);
179+
},
180+
});
181+
182+
return columnsArray;
183+
}
184+
185+
function findObjectProperty(properties, name) {
186+
return properties.find((property) => (
187+
n.ObjectProperty.check(property) &&
188+
getPropertyKeyName(property) === name
189+
));
190+
}
191+
192+
function getPropertyKeyName(property) {
193+
if (n.Identifier.check(property.key)) {
194+
return property.key.name;
195+
}
196+
if (n.StringLiteral.check(property.key) || n.Literal.check(property.key)) {
197+
return property.key.value;
198+
}
199+
return null;
200+
}
201+
202+
function getObjectPropertyValue(objectExpression, name) {
203+
const property = findObjectProperty(objectExpression.properties, name);
204+
if (!property) {
205+
return null;
206+
}
207+
if (n.StringLiteral.check(property.value) || n.Literal.check(property.value)) {
208+
return property.value.value;
209+
}
210+
return null;
211+
}
212+
213+
function createColumnAstNode(column) {
214+
const properties = [
215+
b.objectProperty(b.identifier("name"), b.stringLiteral(column.name)),
216+
];
217+
218+
if (column.type) {
219+
properties.push(
220+
b.objectProperty(
221+
b.identifier("type"),
222+
b.memberExpression(b.identifier("AdminForthDataTypes"), b.identifier(getAdminForthDataTypeKey(column.type)))
223+
)
224+
);
225+
}
226+
227+
if (column.isPrimaryKey) {
228+
properties.push(b.objectProperty(b.identifier("primaryKey"), b.booleanLiteral(true)));
229+
}
230+
231+
if (column.isUUID) {
232+
properties.push(
233+
b.objectProperty(
234+
b.identifier("components"),
235+
b.objectExpression([
236+
b.objectProperty(b.identifier("list"), b.stringLiteral("@/renderers/CompactUUID.vue")),
237+
])
238+
)
239+
);
240+
}
241+
242+
properties.push(
243+
b.objectProperty(
244+
b.identifier("showIn"),
245+
b.objectExpression([
246+
b.objectProperty(b.identifier("all"), b.booleanLiteral(true)),
247+
])
248+
)
249+
);
250+
251+
return b.objectExpression(properties);
252+
}
253+
254+
function formatColumnsCount(count) {
255+
return `${count} column${count === 1 ? "" : "s"}`;
256+
}
257+
258+
function normalizeColumnForTemplate(column) {
259+
if (!column.type) {
260+
return column;
261+
}
262+
return {
263+
...column,
264+
type: getAdminForthDataTypeKey(column.type),
265+
};
266+
}
267+
268+
function getAdminForthDataTypeKey(type) {
269+
return ADMINFORTH_DATA_TYPE_KEYS[type] || type;
270+
}

adminforth/commands/createResource/main.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ export default async function createResource(args) {
4343
import { admin } from './${instance.file}.js';
4444
export async function exec() {
4545
await admin.discoverDatabases();
46-
const columns = await admin.getAllColumnsInTable("${table.table}");
46+
const columns = await admin.getAllColumnsInTable("${table.table}", "${table.db}");
4747
setTimeout(process.exit);
4848
return columns;
4949
}
@@ -55,7 +55,7 @@ export default async function createResource(args) {
5555
dataSource: table.db,
5656
});
5757

58-
injectResourceIntoIndex({
58+
await injectResourceIntoIndex({
5959
table: resourceId,
6060
resourceId: resourceId,
6161
label: toTitleCase(table.table),

adminforth/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -521,7 +521,8 @@ class AdminForth implements IAdminForth {
521521
}
522522

523523
async getAllColumnsInTable(
524-
tableName: string
524+
tableName: string,
525+
dataSourceId?: string
525526
): Promise<{ [dataSourceId: string]: Array<{ name: string; type?: string; isPrimaryKey?: boolean; isUUID?: boolean; }> }> {
526527
const results: { [dataSourceId: string]: Array<{ name: string; type?: string; isPrimaryKey?: boolean; isUUID?: boolean; }> } = {};
527528

@@ -530,7 +531,9 @@ class AdminForth implements IAdminForth {
530531
}
531532

532533
await Promise.all(
533-
Object.entries(this.connectors).map(async ([dataSourceId, connector]) => {
534+
Object.entries(this.connectors)
535+
.filter(([connectorDataSourceId]) => !dataSourceId || connectorDataSourceId === dataSourceId)
536+
.map(async ([dataSourceId, connector]) => {
534537
if (typeof connector.getAllColumnsInTable === 'function') {
535538
try {
536539
const columns = await connector.getAllColumnsInTable(tableName);

adminforth/modules/codeInjector.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,10 @@ class CodeInjector implements ICodeInjector {
147147

148148

149149
async doesUserHasPnpmLockFile(dir: string): Promise<boolean> {
150+
if (!dir) {
151+
return false;
152+
}
153+
150154
const usersPackagePath = path.join(dir, 'package.json');
151155
let packageContent: { dependencies: any, devDependencies: any } = null;
152156
try {

0 commit comments

Comments
 (0)