256 lines
7.2 KiB
JavaScript
256 lines
7.2 KiB
JavaScript
import fs from 'fs/promises'
|
|
import path from 'path'
|
|
|
|
export class DocTypeInterfaceGenerator {
|
|
constructor(appsPath, appDoctypeMap, outputPath) {
|
|
this.appsPath = appsPath
|
|
this.appDoctypeMap = appDoctypeMap
|
|
this.outputPath = outputPath
|
|
this.processedDoctypes = new Set()
|
|
this.existingInterfaces = {}
|
|
this.updatedInterfaces = 0
|
|
this.jsonFileCache = new Map()
|
|
this.summary = {
|
|
processed: 0,
|
|
updated: 0,
|
|
skipped: 0,
|
|
notFound: 0,
|
|
details: [],
|
|
}
|
|
}
|
|
|
|
async generate() {
|
|
await this.loadExistingInterfaces()
|
|
|
|
const promises = []
|
|
for (const appName of Object.keys(this.appDoctypeMap)) {
|
|
for (const pagetypeName of this.appDoctypeMap[appName]) {
|
|
promises.push(this.processDoctype(appName, pagetypeName))
|
|
}
|
|
}
|
|
await Promise.all(promises)
|
|
|
|
this.printSummary()
|
|
|
|
if (this.updatedInterfaces > 0) {
|
|
const baseInterfaces = this.generateBaseInterfaces()
|
|
const interfacesString = [
|
|
baseInterfaces,
|
|
...Object.values(this.existingInterfaces),
|
|
].join('\n')
|
|
|
|
await fs.mkdir(path.dirname(this.outputPath), { recursive: true })
|
|
await fs.writeFile(this.outputPath, interfacesString)
|
|
}
|
|
}
|
|
|
|
printSummary() {
|
|
console.log('\nJingrow Type Generation Summary:')
|
|
console.log(`- Total processed: ${this.summary.processed} doctypes`)
|
|
console.log(`- Updated: ${this.summary.updated} interfaces`)
|
|
console.log(`- Skipped: ${this.summary.skipped} (no changes)`)
|
|
if (this.summary.notFound > 0) {
|
|
console.log(`- Not found: ${this.summary.notFound}`)
|
|
}
|
|
|
|
if (this.updatedInterfaces > 0) {
|
|
console.log(
|
|
`\nOutput file updated with ${this.updatedInterfaces} interface${this.updatedInterfaces === 1 ? '' : 's'}.`,
|
|
)
|
|
} else {
|
|
console.log('\nNo new schema changes.')
|
|
}
|
|
}
|
|
|
|
async loadExistingInterfaces() {
|
|
try {
|
|
const outputContent = await fs.readFile(this.outputPath, 'utf8')
|
|
const interfaceMatches = outputContent.match(
|
|
/\/\/ Last updated: [^\n]+\nexport interface\s+\w+\s+extends\s+\w+\s+{[^}]+}\n/g,
|
|
)
|
|
if (interfaceMatches) {
|
|
interfaceMatches.forEach((interfaceStr) => {
|
|
const match = interfaceStr.match(/export interface\s+(\w+)\s+extends/)
|
|
if (match) {
|
|
const interfaceName = match[1]
|
|
this.existingInterfaces[interfaceName] = interfaceStr
|
|
}
|
|
})
|
|
}
|
|
} catch (err) {
|
|
if (err.code !== 'ENOENT') {
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
async processDoctype(appName, pagetypeName) {
|
|
if (this.processedDoctypes.has(pagetypeName)) {
|
|
return
|
|
}
|
|
this.processedDoctypes.add(pagetypeName)
|
|
this.summary.processed++
|
|
|
|
const jsonFilePath = await this.findJsonFile(appName, pagetypeName)
|
|
if (!jsonFilePath) {
|
|
this.summary.notFound++
|
|
this.summary.details.push(`${pagetypeName}: not found`)
|
|
return
|
|
}
|
|
const jsonData = JSON.parse(await fs.readFile(jsonFilePath, 'utf8'))
|
|
const lastModified = jsonData.modified
|
|
|
|
const interfaceName = jsonData.name.replace(/\s+/g, '')
|
|
const existingInterface = this.existingInterfaces[interfaceName]
|
|
if (
|
|
existingInterface &&
|
|
existingInterface.includes(`// Last updated: ${lastModified}`)
|
|
) {
|
|
this.summary.skipped++
|
|
this.summary.details.push(`${pagetypeName}: skipped (no changes)`)
|
|
return
|
|
}
|
|
|
|
const fields = jsonData.fields
|
|
|
|
const typeMapping = {
|
|
Data: 'string',
|
|
'Text Editor': 'string',
|
|
Link: 'string',
|
|
Table: 'any[]',
|
|
'Table MultiSelect': 'any[]',
|
|
Percent: 'number',
|
|
Int: 'number',
|
|
Float: 'number',
|
|
Datetime: 'string', // "YYYY-MM-DD HH:MM:SS"
|
|
Date: 'string', // "YYYY-MM-DD"
|
|
Check: '0 | 1',
|
|
'Attach Image': 'string',
|
|
'Dynamic Link': 'string',
|
|
'Small Text': 'string',
|
|
Color: 'string',
|
|
Text: 'string',
|
|
Autocomplete: 'string',
|
|
Password: 'string',
|
|
Code: 'string',
|
|
'Read Only': 'string',
|
|
}
|
|
|
|
let interfaceString = `// Last updated: ${lastModified}\nexport interface ${interfaceName} extends ${jsonData.istable ? 'ChildDocType' : 'PageType'} {\n`
|
|
|
|
for (const field of fields) {
|
|
if (
|
|
[
|
|
'Section Break',
|
|
'Column Break',
|
|
'Tab Break',
|
|
'HTML',
|
|
'Button',
|
|
].includes(field.fieldtype)
|
|
) {
|
|
continue
|
|
}
|
|
let tsType = typeMapping[field.fieldtype] || 'any'
|
|
if (field.fieldtype === 'Select' && field.options) {
|
|
const options = field.options
|
|
.split('\n')
|
|
.map((option) => `'${option}'`)
|
|
.join(' | ')
|
|
tsType = options
|
|
} else if (
|
|
['Table', 'Table MultiSelect'].includes(field.fieldtype) &&
|
|
field.options
|
|
) {
|
|
const relatedDoctype = field.options
|
|
tsType = `${relatedDoctype.replace(/\s+/g, '')}[]`
|
|
await this.processDoctype(
|
|
appName,
|
|
relatedDoctype.toLowerCase().replace(/ /g, '_'),
|
|
)
|
|
}
|
|
let description = `/** ${field.label}: ${field.fieldtype}`
|
|
if (
|
|
['Table', 'Table MultiSelect', 'Link', 'Dynamic Link'].includes(
|
|
field.fieldtype,
|
|
) &&
|
|
field.options
|
|
) {
|
|
description += ` (${field.options})`
|
|
}
|
|
description += ' */'
|
|
let optional =
|
|
field.reqd ||
|
|
['Check', 'Table', 'Table MultiSelect'].includes(field.fieldtype)
|
|
? ''
|
|
: '?'
|
|
interfaceString += ` ${description}\n ${field.fieldname}${optional}: ${tsType};\n`
|
|
}
|
|
|
|
interfaceString += `}\n`
|
|
this.updatedInterfaces++
|
|
this.existingInterfaces[interfaceName] = interfaceString
|
|
this.summary.updated++
|
|
this.summary.details.push(`${pagetypeName}: updated`)
|
|
}
|
|
|
|
async findJsonFile(appName, pagetypeName) {
|
|
const cacheKey = `${appName}/${pagetypeName}`
|
|
if (this.jsonFileCache.has(cacheKey)) {
|
|
return this.jsonFileCache.get(cacheKey)
|
|
}
|
|
|
|
const targetPattern = path.join(
|
|
'pagetype',
|
|
pagetypeName,
|
|
`${pagetypeName}.json`,
|
|
)
|
|
let foundPath = null
|
|
|
|
const searchDirectory = async (directory) => {
|
|
const files = await fs.readdir(directory)
|
|
for (const file of files) {
|
|
const fullPath = path.join(directory, file)
|
|
try {
|
|
const stat = await fs.stat(fullPath)
|
|
if (stat.isDirectory()) {
|
|
await searchDirectory(fullPath)
|
|
} else if (fullPath.endsWith(targetPattern)) {
|
|
foundPath = fullPath
|
|
return
|
|
}
|
|
} catch (error) {
|
|
// Skip files/directories that can't be accessed (e.g., broken symlinks, permission denied)
|
|
if (error.code === 'ENOENT' || error.code === 'EACCES') {
|
|
continue
|
|
}
|
|
// Re-throw other unexpected errors
|
|
throw error
|
|
}
|
|
}
|
|
}
|
|
|
|
await searchDirectory(path.join(this.appsPath, appName))
|
|
|
|
this.jsonFileCache.set(cacheKey, foundPath)
|
|
return foundPath
|
|
}
|
|
|
|
generateBaseInterfaces() {
|
|
return `interface PageType {
|
|
name: string;
|
|
creation: string;
|
|
modified: string;
|
|
owner: string;
|
|
modified_by: string;
|
|
}
|
|
|
|
interface ChildDocType extends PageType {
|
|
parent?: string;
|
|
parentfield?: string;
|
|
parenttype?: string;
|
|
idx?: number;
|
|
}
|
|
`
|
|
}
|
|
}
|