|  | 1 | +import { join } from 'path'; | 
|  | 2 | +import { pathExists, ensureFile, writeFile } from 'fs-extra'; | 
|  | 3 | +import { NodeItemConfigType } from '/@/views/rule/designer/types/node'; | 
|  | 4 | +import { camelCase, upperFirst, snakeCase } from 'lodash-es'; | 
|  | 5 | +import { execSync } from 'child_process'; | 
|  | 6 | + | 
|  | 7 | +type GroupNodeType = { [key: string]: NodeItemConfigType[] }; | 
|  | 8 | + | 
|  | 9 | +const RULE_CHAIN_FILE_PATH = join(process.cwd(), '/src/views/rule/designer'); | 
|  | 10 | + | 
|  | 11 | +const list: NodeItemConfigType[] = [ | 
|  | 12 | +  { | 
|  | 13 | +    type: 'FLOW', | 
|  | 14 | +    scope: 'TENANT', | 
|  | 15 | +    name: 'acknowledge', | 
|  | 16 | +    clazz: 'org.thingsboard.rule.engine.flow.TbAckNode', | 
|  | 17 | +    configurationDescriptor: { | 
|  | 18 | +      nodeDefinition: { | 
|  | 19 | +        details: | 
|  | 20 | +          "After acknowledgement, the message is pushed to related rule nodes. Useful if you don't care what happens to this message next.", | 
|  | 21 | +        description: 'Acknowledges the incoming message', | 
|  | 22 | +        inEnabled: true, | 
|  | 23 | +        outEnabled: true, | 
|  | 24 | +        relationTypes: ['Success', 'Failure'], | 
|  | 25 | +        customRelations: false, | 
|  | 26 | +        ruleChainNode: false, | 
|  | 27 | +        defaultConfiguration: { | 
|  | 28 | +          version: 0, | 
|  | 29 | +        }, | 
|  | 30 | +        uiResources: ['static/rulenode/rulenode-core-config.js'], | 
|  | 31 | +        configDirective: 'tbNodeEmptyConfig', | 
|  | 32 | +        icon: '', | 
|  | 33 | +        iconUrl: '', | 
|  | 34 | +        docUrl: '', | 
|  | 35 | +      }, | 
|  | 36 | +    }, | 
|  | 37 | +    actions: null, | 
|  | 38 | +  }, | 
|  | 39 | +  { | 
|  | 40 | +    createdTime: 1668743016418, | 
|  | 41 | +    type: 'FILTER', | 
|  | 42 | +    scope: 'TENANT', | 
|  | 43 | +    name: 'check alarm status', | 
|  | 44 | +    clazz: 'org.thingsboard.rule.engine.filter.TbCheckAlarmStatusNode', | 
|  | 45 | +    configurationDescriptor: { | 
|  | 46 | +      nodeDefinition: { | 
|  | 47 | +        details: | 
|  | 48 | +          'If the alarm status matches the specified one - msg is success if does not match - msg is failure.', | 
|  | 49 | +        description: 'Checks alarm status.', | 
|  | 50 | +        inEnabled: true, | 
|  | 51 | +        outEnabled: true, | 
|  | 52 | +        relationTypes: ['True', 'False', 'Failure'], | 
|  | 53 | +        customRelations: false, | 
|  | 54 | +        ruleChainNode: false, | 
|  | 55 | +        defaultConfiguration: { | 
|  | 56 | +          alarmStatusList: ['ACTIVE_ACK', 'ACTIVE_UNACK'], | 
|  | 57 | +        }, | 
|  | 58 | +        uiResources: ['static/rulenode/rulenode-core-config.js'], | 
|  | 59 | +        configDirective: 'tbFilterNodeCheckAlarmStatusConfig', | 
|  | 60 | +        icon: '', | 
|  | 61 | +        iconUrl: '', | 
|  | 62 | +        docUrl: '', | 
|  | 63 | +      }, | 
|  | 64 | +    }, | 
|  | 65 | +    actions: null, | 
|  | 66 | +  }, | 
|  | 67 | +]; | 
|  | 68 | + | 
|  | 69 | +const getCategoryConfigName = (name: string) => { | 
|  | 70 | +  return `${upperFirst(camelCase(name))}CategoryConfig`; | 
|  | 71 | +}; | 
|  | 72 | + | 
|  | 73 | +const getNodeConfigName = (name: string) => { | 
|  | 74 | +  return `${upperFirst(camelCase(name))}Config`; | 
|  | 75 | +}; | 
|  | 76 | + | 
|  | 77 | +const getCagegoryEnumName = (name: string) => { | 
|  | 78 | +  return `${upperFirst(name.toLowerCase())}CategoryComponentEnum`; | 
|  | 79 | +}; | 
|  | 80 | + | 
|  | 81 | +const getEnumKeyName = (name: string) => { | 
|  | 82 | +  return snakeCase(name).toUpperCase(); | 
|  | 83 | +}; | 
|  | 84 | + | 
|  | 85 | +const createFile = async (fileName: string, fileContent?: string, replace = false) => { | 
|  | 86 | +  const path = join(RULE_CHAIN_FILE_PATH, './packages', fileName); | 
|  | 87 | + | 
|  | 88 | +  const flag = await pathExists(path); | 
|  | 89 | + | 
|  | 90 | +  if (flag && !replace) return false; | 
|  | 91 | + | 
|  | 92 | +  await ensureFile(path); | 
|  | 93 | + | 
|  | 94 | +  fileContent && (await writeFile(path, fileContent, { encoding: 'utf-8' })); | 
|  | 95 | +}; | 
|  | 96 | + | 
|  | 97 | +const groupByType = () => { | 
|  | 98 | +  const group: { [key: string]: NodeItemConfigType[] } = {}; | 
|  | 99 | + | 
|  | 100 | +  list.forEach((item) => { | 
|  | 101 | +    if (!group[item.type]) group[item.type] = []; | 
|  | 102 | +    group[item.type].push(item); | 
|  | 103 | +  }); | 
|  | 104 | + | 
|  | 105 | +  return group; | 
|  | 106 | +}; | 
|  | 107 | + | 
|  | 108 | +const generateCategoryEnumFile = async (data: GroupNodeType) => { | 
|  | 109 | +  const defaultContent = ` | 
|  | 110 | +export enum EntryCategoryComponentEnum { | 
|  | 111 | +  INPUT = 'Input', | 
|  | 112 | +} | 
|  | 113 | +  `; | 
|  | 114 | + | 
|  | 115 | +  const fileContent = Object.keys(data).reduce((prev, next) => { | 
|  | 116 | +    const enumName = getCagegoryEnumName(next); | 
|  | 117 | + | 
|  | 118 | +    const enumKeys = data[next].map((item) => getEnumKeyName(item.name)); | 
|  | 119 | + | 
|  | 120 | +    const content = `export enum ${enumName} { | 
|  | 121 | +        ${enumKeys.map((name) => `${name} = '${upperFirst(camelCase(name))}'`)} | 
|  | 122 | +    }`; | 
|  | 123 | + | 
|  | 124 | +    return `${prev} \n ${content}`; | 
|  | 125 | +  }, defaultContent); | 
|  | 126 | + | 
|  | 127 | +  createFile('../enum/category.ts', fileContent, true); | 
|  | 128 | +  return fileContent; | 
|  | 129 | +}; | 
|  | 130 | + | 
|  | 131 | +const generateRuleNodeEnumFile = async (data: GroupNodeType) => { | 
|  | 132 | +  const categoryKeys = Object.keys(data).map((type) => type.toUpperCase()); | 
|  | 133 | +  const filePath = join(RULE_CHAIN_FILE_PATH, './packages/index.type.ts'); | 
|  | 134 | +  const fileContent = ` | 
|  | 135 | +export enum RuleNodeTypeEnum { | 
|  | 136 | +  ${categoryKeys.map((item) => `${item} = '${item}'`).join(',\n')} | 
|  | 137 | +} | 
|  | 138 | +  `; | 
|  | 139 | + | 
|  | 140 | +  await writeFile(filePath, fileContent, { | 
|  | 141 | +    encoding: 'utf-8', | 
|  | 142 | +  }); | 
|  | 143 | + | 
|  | 144 | +  return fileContent; | 
|  | 145 | +}; | 
|  | 146 | + | 
|  | 147 | +const generateCategoryIndexFile = async (type: string, data: NodeItemConfigType[]) => { | 
|  | 148 | +  const getComponentsName = data.map((temp) => `${upperFirst(camelCase(temp.name))}`); | 
|  | 149 | +  const importContent = getComponentsName.map( | 
|  | 150 | +    (name) => `import { ${name}Config } from './${name}';\n` | 
|  | 151 | +  ); | 
|  | 152 | + | 
|  | 153 | +  const components = getComponentsName.map((item) => `${item}Config`); | 
|  | 154 | + | 
|  | 155 | +  const content = `import type { CategoryConfigType, NodeItemConfigType } from '../../types/node'; | 
|  | 156 | +import { RuleNodeTypeEnum } from '../index.type'; | 
|  | 157 | +${importContent.join('')} | 
|  | 158 | + | 
|  | 159 | +export const ${getCategoryConfigName(type)}: CategoryConfigType = { | 
|  | 160 | +  category: RuleNodeTypeEnum.${type.toUpperCase()}, | 
|  | 161 | +  backgroundColor: '#ede550', | 
|  | 162 | +  title: '筛选器', | 
|  | 163 | +  icon: 'tabler:circuit-ground', | 
|  | 164 | +  description: '使用配置条件筛选传入消息', | 
|  | 165 | +}; | 
|  | 166 | + | 
|  | 167 | +export const ${upperFirst(type.toLowerCase())}Components: NodeItemConfigType[] = [${components}]; | 
|  | 168 | +`; | 
|  | 169 | + | 
|  | 170 | +  createFile(`./${upperFirst(type.toLowerCase())}/index.ts`, content); | 
|  | 171 | + | 
|  | 172 | +  return content; | 
|  | 173 | +}; | 
|  | 174 | + | 
|  | 175 | +const generateNodeIndexFile = async (type: string, data: NodeItemConfigType) => { | 
|  | 176 | +  const categoryEnumName = getCagegoryEnumName(type); | 
|  | 177 | + | 
|  | 178 | +  const nodeConfigName = getNodeConfigName(data.name); | 
|  | 179 | + | 
|  | 180 | +  const content = ` | 
|  | 181 | +  import { ${categoryEnumName} } from '../../../enum/category'; | 
|  | 182 | +  import { useCreateNodeKey } from '../../../hook/useCreateNodeKey'; | 
|  | 183 | +  import type { NodeItemConfigType } from '../../../types/node'; | 
|  | 184 | +  import { RuleNodeTypeEnum } from '../../index.type'; | 
|  | 185 | + | 
|  | 186 | +  const keys = useCreateNodeKey(${categoryEnumName}.${getEnumKeyName(data.name)}); | 
|  | 187 | + | 
|  | 188 | +  export interface ${upperFirst(camelCase(data.name))}DataType { | 
|  | 189 | + | 
|  | 190 | +  } | 
|  | 191 | + | 
|  | 192 | +  export const ${nodeConfigName}: NodeItemConfigType = { | 
|  | 193 | +    ...keys, | 
|  | 194 | +    clazz: '${data.clazz}', | 
|  | 195 | +    categoryType: RuleNodeTypeEnum.${type.toUpperCase()}, | 
|  | 196 | +    name: '${data.name}', | 
|  | 197 | +    backgroundColor: '#ede550', | 
|  | 198 | +    configurationDescriptor: ${JSON.stringify(data.configurationDescriptor, null, 2)} | 
|  | 199 | +  }; | 
|  | 200 | +  `; | 
|  | 201 | + | 
|  | 202 | +  createFile( | 
|  | 203 | +    `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/index.ts`, | 
|  | 204 | +    content | 
|  | 205 | +  ); | 
|  | 206 | + | 
|  | 207 | +  return content; | 
|  | 208 | +}; | 
|  | 209 | + | 
|  | 210 | +const generateNodeVueTemplateFile = async (type: string, data: NodeItemConfigType) => { | 
|  | 211 | +  const content = ` | 
|  | 212 | +  <script lang="ts" setup> | 
|  | 213 | +    import type { CreateModalDefineExposeType } from '../../../types'; | 
|  | 214 | +    import { BasicForm, useForm } from '/@/components/Form'; | 
|  | 215 | +    import { formSchemas } from './create.config'; | 
|  | 216 | +    import { NodeData } from '../../../types/node'; | 
|  | 217 | + | 
|  | 218 | +    defineProps<{ | 
|  | 219 | +      config: NodeData; | 
|  | 220 | +    }>(); | 
|  | 221 | + | 
|  | 222 | +    const [register, { validate, getFieldsValue, setFieldsValue, resetFields }] = useForm({ | 
|  | 223 | +      schemas: formSchemas, | 
|  | 224 | +      showActionButtonGroup: false, | 
|  | 225 | +    }); | 
|  | 226 | + | 
|  | 227 | +    const getValue: CreateModalDefineExposeType['getFieldsValue'] = async () => { | 
|  | 228 | +      await validate(); | 
|  | 229 | +      const value = getFieldsValue() || {}; | 
|  | 230 | +      return value; | 
|  | 231 | +    }; | 
|  | 232 | + | 
|  | 233 | +    const setValue: CreateModalDefineExposeType['setFieldsValue'] = (value) => { | 
|  | 234 | +      resetFields(); | 
|  | 235 | +      setFieldsValue(value); | 
|  | 236 | +    }; | 
|  | 237 | + | 
|  | 238 | +    defineExpose({ | 
|  | 239 | +      setFieldsValue: setValue, | 
|  | 240 | +      getFieldsValue: getValue, | 
|  | 241 | +    } as CreateModalDefineExposeType); | 
|  | 242 | +  </script> | 
|  | 243 | + | 
|  | 244 | +  <template> | 
|  | 245 | +    <BasicForm @register="register" /> | 
|  | 246 | +  </template> | 
|  | 247 | +  `; | 
|  | 248 | + | 
|  | 249 | +  createFile( | 
|  | 250 | +    `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/create.vue`, | 
|  | 251 | +    content | 
|  | 252 | +  ); | 
|  | 253 | +  return content; | 
|  | 254 | +}; | 
|  | 255 | + | 
|  | 256 | +const generateNodeVueConfigFile = async (type: string, data: NodeItemConfigType) => { | 
|  | 257 | +  const content = ` | 
|  | 258 | +  import { NodeBindDataFieldEnum, NodeBindDataFieldNameEnum } from '../../../enum/node'; | 
|  | 259 | +  import { FormSchema } from '/@/components/Form'; | 
|  | 260 | + | 
|  | 261 | +  export const formSchemas: FormSchema[] = [ | 
|  | 262 | +  ]; | 
|  | 263 | +  `; | 
|  | 264 | + | 
|  | 265 | +  createFile( | 
|  | 266 | +    `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/create.config.ts`, | 
|  | 267 | +    content | 
|  | 268 | +  ); | 
|  | 269 | +  return content; | 
|  | 270 | +}; | 
|  | 271 | + | 
|  | 272 | +const generateNodeConfigFile = async (type: string, data: NodeItemConfigType) => { | 
|  | 273 | +  const nodeConfigName = getNodeConfigName(data.name); | 
|  | 274 | +  const categoryConfigName = getCategoryConfigName(type); | 
|  | 275 | +  const content = ` | 
|  | 276 | +  import { cloneDeep } from 'lodash-es'; | 
|  | 277 | +  import { PublicNodeItemClass } from '../../../types/node'; | 
|  | 278 | +  import type { | 
|  | 279 | +    CategoryConfigType, | 
|  | 280 | +    CreateComponentType, | 
|  | 281 | +    NodeItemConfigType, | 
|  | 282 | +  } from '../../../types/node'; | 
|  | 283 | +  import { ${categoryConfigName} } from '..'; | 
|  | 284 | +  import { ${nodeConfigName} } from '.'; | 
|  | 285 | + | 
|  | 286 | +  export class Config extends PublicNodeItemClass implements CreateComponentType { | 
|  | 287 | +    public config: NodeItemConfigType = cloneDeep(${nodeConfigName}); | 
|  | 288 | + | 
|  | 289 | +    public categoryConfig: CategoryConfigType = cloneDeep(${categoryConfigName}); | 
|  | 290 | + | 
|  | 291 | +    constructor() { | 
|  | 292 | +      super(); | 
|  | 293 | +    } | 
|  | 294 | +  } | 
|  | 295 | +  `; | 
|  | 296 | + | 
|  | 297 | +  createFile( | 
|  | 298 | +    `./${upperFirst(type.toLowerCase())}/${upperFirst(camelCase(data.name))}/config.ts`, | 
|  | 299 | +    content | 
|  | 300 | +  ); | 
|  | 301 | +  return content; | 
|  | 302 | +}; | 
|  | 303 | + | 
|  | 304 | +const bootstrap = async () => { | 
|  | 305 | +  const groupData = groupByType(); | 
|  | 306 | +  await generateRuleNodeEnumFile(groupData); | 
|  | 307 | +  await generateCategoryEnumFile(groupData); | 
|  | 308 | + | 
|  | 309 | +  for (const type of Object.keys(groupData)) { | 
|  | 310 | +    const item = groupData[type]; | 
|  | 311 | +    await generateCategoryIndexFile(type, item); | 
|  | 312 | +    for (const temp of item) { | 
|  | 313 | +      await generateNodeConfigFile(type, temp); | 
|  | 314 | +      await generateNodeIndexFile(type, temp); | 
|  | 315 | +      await generateNodeVueConfigFile(type, temp); | 
|  | 316 | +      await generateNodeVueTemplateFile(type, temp); | 
|  | 317 | +    } | 
|  | 318 | +  } | 
|  | 319 | +  execSync(`npx eslint --fix ${RULE_CHAIN_FILE_PATH}/*.{vue,ts}`); | 
|  | 320 | +}; | 
|  | 321 | + | 
|  | 322 | +bootstrap(); | 
... | ... |  |