diff --git a/app/controller/material-center/ComponentLibrary.ts b/app/controller/material-center/ComponentLibrary.ts new file mode 100644 index 0000000..6e1e0ea --- /dev/null +++ b/app/controller/material-center/ComponentLibrary.ts @@ -0,0 +1,54 @@ +/** +* Copyright (c) 2023 - present TinyEngine Authors. +* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ +import { Controller } from 'egg'; +import { I_CreateComponentLibrary } from '../../lib/interface'; + +export default class ComponentController extends Controller { + + /** + * @summary 注册组件库 + * @router POST /api/componentLibrary/create + */ + async create(){ + const data: I_CreateComponentLibrary = this.ctx.request.body; + this.ctx.body = await this.service.materialCenter.componentLibrary.create(data); + } + + /** + * @summary 查询组件库 + * @router GET + */ + async find(){ + this.ctx.body = await this.service.materialCenter.componentLibrary.find(this.ctx.query); + } + + /** + * @summary 更新组件库 + * @router PUT + */ + async update(){ + const { id } = this.ctx.params; + const params = this.ctx.request.body; + this.ctx.body = await this.service.materialCenter.componentLibrary.update({id, ...params }); + } + + + /** + * @summary 删除组件库 + * @router DELETE + */ + async delete(){ + const { id } = this.ctx.params; + this.ctx.body = await this.service.materialCenter.componentLibrary.delete({ id }); + } + +} diff --git a/app/controller/material-center/UserComponents.ts b/app/controller/material-center/UserComponents.ts new file mode 100644 index 0000000..9950051 --- /dev/null +++ b/app/controller/material-center/UserComponents.ts @@ -0,0 +1,30 @@ +/** +* Copyright (c) 2023 - present TinyEngine Authors. +* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ +import { Controller } from 'egg'; + +export default class UserComponentController extends Controller { + + /** + * @summary 创建组件库 + * @router POST /component/bundle/create + */ + async bundleCreate(){ + const ctx = this.ctx; + try { + const fileStream = await ctx.getFileStream(); + this.ctx.body = await this.ctx.service.materialCenter.userComponents.bundleCreate(fileStream); + } catch (error) { + ctx.logger.error('[UserComponentController] bundleCreate error:', error); + }; + } + +} diff --git a/app/router/materialCenter/base.ts b/app/router/materialCenter/base.ts index 36b8348..c8b3830 100644 --- a/app/router/materialCenter/base.ts +++ b/app/router/materialCenter/base.ts @@ -62,4 +62,14 @@ export default (app: Application) => { subRouter.get('/tasks/status', controller.materialCenter.task.status); subRouter.get('/tasks/:id', controller.materialCenter.task.findById); + //组件库 + subRouter.post('/component-library/update/:id',controller.materialCenter.componentLibrary.update); + subRouter.post('/component-library/create',controller.materialCenter.componentLibrary.create); + subRouter.delete('/component-library/delete/:id',controller.materialCenter.componentLibrary.delete); + subRouter.get('/component-library/find',controller.materialCenter.componentLibrary.find); + + + // 拆分bundle.json + subRouter.post('/component/bundle/create',controller.materialCenter.userComponents.bundleCreate); + }; diff --git a/app/service/app-center/appSchema.ts b/app/service/app-center/appSchema.ts index c576957..835cbc2 100644 --- a/app/service/app-center/appSchema.ts +++ b/app/service/app-center/appSchema.ts @@ -22,7 +22,8 @@ class AppSchema extends SchemaService { ['dataSource', this.getSchemaDataSource], ['i18n', this.getSchemaI18n], ['componentsTree', this.getSchemaComponentsTree], - ['componentsMap', this.getSchemaComponentsMap] + ['componentsMap', this.getSchemaComponentsMap], + ['packages', this.getPackages] ]); // 获取schema数据 @@ -79,10 +80,28 @@ class AppSchema extends SchemaService { // 获取应用信息 private async setMeta(query?): Promise { const metaData: I_Response = await this.service.appCenter.apps.schemaMeta(this.appId, query); + const componentLibraryData = await this.service.materialCenter.componentLibrary.list(); this.meta = metaData.data; + this.meta.componentLibrary = componentLibraryData.data; return metaData; } + private getPackages() { + const {componentLibrary} = this.meta; + if(!Array.isArray(componentLibrary)) { + return this.ctx.helper.getResponseData([]); + } + const packages = componentLibrary.map((item) => ({ + name: item.name, + package: item.packageName, + version: item.version, + script: item.script, + css: item.css, + others: item.others + })); + return this.ctx.helper.getResponseData(packages); + } + // 获取元数据 private getSchemaMeta(): I_Response { const appData = this.meta.app; diff --git a/app/service/material-center/ComponentLibrary.ts b/app/service/material-center/ComponentLibrary.ts new file mode 100644 index 0000000..a8dc565 --- /dev/null +++ b/app/service/material-center/ComponentLibrary.ts @@ -0,0 +1,53 @@ +/** +* Copyright (c) 2023 - present TinyEngine Authors. +* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ +import { E_Method } from '../../lib/enum'; +import { I_CreateComponentLibrary } from '../../lib/interface'; +import * as qs from 'querystring'; +import DataService from '../dataService'; + +export default class ComponentLibrary extends DataService{ + + async create(param: I_CreateComponentLibrary) { + return this.query({ + url: 'component-library' , + method: E_Method.Post, + data: param + }); + } + + async find(param){ + const query = qs.stringify(param); + return this.query({ + url: `component-library?${query}` + }); + } + + async list() { + return this.query({ url: 'component-library' }); + } + + async update(param) { + const { id } = param; + return this.query({ + url: `component-library/${id}`, + method: E_Method.Put, + data: param + }); + } + + delete({ id }){ + return this.query({ + url: `component-library/${id}`, + method: E_Method.Delete + }); + } +} diff --git a/app/service/material-center/Material.ts b/app/service/material-center/Material.ts new file mode 100644 index 0000000..c061c09 --- /dev/null +++ b/app/service/material-center/Material.ts @@ -0,0 +1,37 @@ +/** +* Copyright (c) 2023 - present TinyEngine Authors. +* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ +import { E_Method } from '../../lib/enum'; +import DataService from '../dataService'; +import * as qs from 'querystring'; + +export default class Material extends DataService{ + + private base = 'materials'; + + async update(param) { + const {id} = param; + return this.query({ + url: `materials/${id}`, + method: E_Method.Put, + data: param + }); + + } + + async find(param) { + const query = typeof param === 'string' ? param : qs.stringify(param); + return this.fQuery({ + url: `${this.base}?${query}` + }); + } + +} diff --git a/app/service/material-center/MaterialHistories.ts b/app/service/material-center/MaterialHistories.ts new file mode 100644 index 0000000..16e6114 --- /dev/null +++ b/app/service/material-center/MaterialHistories.ts @@ -0,0 +1,45 @@ +/** +* Copyright (c) 2023 - present TinyEngine Authors. +* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ +import { E_Method } from '../../lib/enum'; +import DataService from '../dataService'; +import * as qs from 'querystring'; + +export default class MaterialHistories extends DataService{ + + private base = 'material-histories'; + + async create(param) { + return this.fQuery({ + url: 'material-histories', + method: E_Method.Post, + data: param + }); + } + + async update(param) { + const {id} = param; + return this.query({ + url: `material-histories/${id}`, + method: E_Method.Put, + data: param + }); + + } + + async find(param) { + const query = typeof param === 'string' ? param : qs.stringify(param); + return this.fQuery({ + url: `${this.base}?${query}` + }); + } + +} diff --git a/app/service/material-center/UserComponents.ts b/app/service/material-center/UserComponents.ts new file mode 100644 index 0000000..726ef36 --- /dev/null +++ b/app/service/material-center/UserComponents.ts @@ -0,0 +1,266 @@ +/** +* Copyright (c) 2023 - present TinyEngine Authors. +* Copyright (c) 2023 - present Huawei Cloud Computing Technologies Co., Ltd. +* +* Use of this source code is governed by an MIT-style license. +* +* THE OPEN SOURCE SOFTWARE IN THIS PRODUCT IS DISTRIBUTED IN THE HOPE THAT IT WILL BE USEFUL, +* BUT WITHOUT ANY WARRANTY, WITHOUT EVEN THE IMPLIED WARRANTY OF MERCHANTABILITY OR FITNESS FOR +* A PARTICULAR PURPOSE. SEE THE APPLICABLE LICENSES FOR MORE DETAILS. +* +*/ +import { E_AppErrorCode, E_MimeType, E_Method } from '../../lib/enum'; +import { pipeline } from 'stream'; +import * as path from 'path'; +import { promisify } from 'util'; +import fs from 'fs-extra'; +import { v4 as uuidv4 } from 'uuid'; +import { StatusCodes } from 'http-status-codes'; +import sendToWormhole from 'stream-wormhole'; +import { throwApiError } from '../../lib/ApiError'; +import DataService from '../dataService'; +import { I_Response } from '../../lib/interface'; +import * as qs from 'querystring'; + + +const pump = promisify(pipeline); + +export default class UserComponent extends DataService{ + + capitalize = (str) => `${str.charAt(0).toUpperCase()}${str.slice(1)}`; + toPascalCase = (str) => str.split('-').map(this.capitalize).join(''); + + private base = 'user-components'; + protected paramKeys = [ + 'docUrl', + 'devMode', + { + key: 'schema', + value: () => 'schema_fragment' + } + ]; + protected resKeys = [ + 'doc_url', + 'dev_mode', + { + key: 'schema_fragment', + value: () => 'schema' + } + ]; + + async bundleCreate (fileStream) { + const splitResult = await this.splitMaterials(fileStream); + const componentList = splitResult.components; + const packageList = splitResult.packages; + if(!packageList) { + return this.bulkComponentCreate(componentList); + } + await Promise.all(packageList.map(async (componentLibrary) => { + // 查询是否存在组件库 + const paramComponentLibrary = { + name: componentLibrary.name, + version: componentLibrary.version + } + const componentLibraryList = await this.service.materialCenter.componentLibrary.find(paramComponentLibrary); + componentLibrary.packageName = componentLibrary.package; + componentLibrary.framework = 'Vue'; + componentLibrary.isOfficial = true; + componentLibrary.isDefault = true; + if(componentLibraryList.data.length) { + // 修改组件库 + componentLibrary.id = componentLibraryList.data[0].id; + await this.service.materialCenter.componentLibrary.update(componentLibrary); + }else { + // 新增组件库 + await this.service.materialCenter.componentLibrary.create(componentLibrary); + } + })) + return await this.bulkComponentCreate(componentList); + } + + // 批量新增或者修改组件 + async bulkComponentCreate(componentList) { + let componentLibraryListResult: any = []; + const fileResult = { + insertNum: 0, + updateNum: 0 + } + + await Promise.all(componentList.map(async (component) => { + // 查询是否存在组件 + component.component = (typeof component.component === 'string') + ? component.component + : (Array.isArray(component.component) ? component.component.join(',') : ''); + const paramComponent = { + component: component.component, + version: component.version + }; + + const componentQueryList = await this.find(paramComponent); // 异步查询组件 + let packageName = ''; + if (component.npm && component.npm.package) { + packageName = component.npm.package; + } + + // 根据包名和版本去查询组件库是否存在 + const paramComponentLibrary = { + packageName: packageName, + version: component.version + }; + + const componentLibraryList = await this.service.materialCenter.componentLibrary.find(paramComponentLibrary); + let libraryId = componentLibraryList.data[0] ? componentLibraryList.data[0].id : null; + component.library = libraryId; + + if (!componentQueryList.data.length) { + fileResult.insertNum += 1; + // 新增组件 + component.id = null; + const componentObject: I_Response = await this.create(component); + componentLibraryListResult.push(componentObject); + // 修改物料和物料历史 + await this.service.materialCenter.materialHistories.update({ + id: 639, + components: componentObject.data.id + }) + await this.service.materialCenter.material.update({ + id: 1505, + user_components: componentObject.data.id + }) + + } else { + fileResult.updateNum += 1; + // 修改组件 + component.id = componentQueryList.data[0].id; + const componentObject: I_Response = await this.update(component); + componentLibraryListResult.push(componentObject); + } + + })); + + // 返回新增或者修改的条数 + return this.ctx.helper.getResponseData(fileResult); + } + + async create(param) { + return this.fQuery({ + url: this.base, + method: E_Method.Post, + data: param + }); + } + + async update(param) { + const {id} = param; + return this.fQuery({ + url: `${this.base}/${id}`, + method: E_Method.Put, + data: param + }); + + } + + async find(param) { + const query = typeof param === 'string' ? param : qs.stringify(param); + return this.fQuery({ + url: `${this.base}?${query}` + }); + } + + async splitMaterials(fileStream) { + let bundleDefault = { + data: { + framework: 'Vue', + materials: { + components: [], + blocks: [], + snippets: [], + packages: [] + } + } + } + const bundle = await this.parseJsonFileStream(fileStream) || bundleDefault; + const { components, snippets, packages } = bundle?.data.materials; + try { + // 预处理 snippets + const snippetsMap = {} + if(snippets != null && snippets.length != 0){ + snippets.forEach((snippetItem) => { + if (!Array.isArray(snippetItem?.children)) { + return + } + + snippetItem.children.forEach((item) => { + const key = item?.schema?.componentName || item.snippetName + if (!key) { + return + } + const realKey = this.toPascalCase(key) + if (!snippetsMap[realKey]) { + snippetsMap[realKey] = [] + } + snippetsMap[realKey].push({ + ...item, + category: snippetItem.group + }) + }) + }) + } + + // 处理组件 + components.forEach((comp) => { + const matchKey = Array.isArray(comp.component) + ? this.toPascalCase(comp.component[0]) + : this.toPascalCase(comp.component) + + const matchedSnippets = snippetsMap[matchKey] + + if (matchedSnippets?.length) { + comp.snippets = matchedSnippets + } + }) + + } catch (error) { + this.ctx.logger.error(`failed to split materials: ${error}.`) + } + return {components,packages}; + } + + async parseJsonFileStream(fileStream) { + const { filename, fieldname, encoding, mime } = fileStream; + this.logger.info(`parseJsonFileStream field: ${fieldname}, filename:${filename}, encoding: ${encoding}, mime: ${mime}`); + // 校验文件流合法性 + await this.validateFileStream(fileStream, E_AppErrorCode.CM308, [E_MimeType.Json]); + const jsonFileName = `${uuidv4()}_${filename.toLowerCase()}`; + const target = path.resolve(this.config.baseDir, '.tmp', jsonFileName); + try { + await fs.ensureDir(path.parse(target).dir); + const writeStream = fs.createWriteStream(target); + await pump(fileStream, writeStream); + return await fs.readJson(target); + } catch (err) { + await sendToWormhole(fileStream); + throwApiError((err as Error).message, StatusCodes.INTERNAL_SERVER_ERROR, E_AppErrorCode.CM309); + } finally { + await fs.remove(target); + } + } + + /** + * 校验文件流是否合法 + * @param { any } fileStream 文件流 + * @param { E_AppErrorCode } condition 报错码 + * @param { Array } mimeTypes 文件类型集合 + */ + async validateFileStream(fileStream, code: E_AppErrorCode, mimeTypes: Array) { + const { filename, mime } = fileStream; + const condition = filename && mimeTypes.includes(mime); + if (condition) { + return; + } + // 只要文件不合法就throw error, 无论是批量还是单个 + await sendToWormhole(fileStream); + throwApiError('', StatusCodes.BAD_REQUEST, code); + } + +}