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