diff --git a/.eslintignore b/.eslintignore new file mode 100644 index 00000000..10f5ec09 --- /dev/null +++ b/.eslintignore @@ -0,0 +1,7 @@ +app/proxy* +**/*.d.ts +node_modules/ +dist/ +coverage/ +mocks/ +.react_entries/ diff --git a/.eslintrc b/.eslintrc new file mode 100644 index 00000000..34ecb4a8 --- /dev/null +++ b/.eslintrc @@ -0,0 +1,6 @@ +{ + "extends": "eslint-config-egg/typescript", + "rules": { + + } +} diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml new file mode 100644 index 00000000..146da091 --- /dev/null +++ b/.github/workflows/nodejs.yml @@ -0,0 +1,52 @@ +# This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node +# For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions + +name: Node.js CI + +on: + push: + branches: [ master ] + pull_request: + branches: [ master ] + schedule: + - cron: '0 2 * * *' + +jobs: + build: + runs-on: ${{ matrix.os }} + + services: + mysql: + image: mysql:5.7 + env: + MYSQL_ALLOW_EMPTY_PASSWORD: true + MYSQL_DATABASE: cnpmjs_test + ports: + - 3306:3306 + options: --health-cmd="mysqladmin ping" --health-interval=10s --health-timeout=5s --health-retries=5 + + strategy: + fail-fast: false + matrix: + node-version: [14] + os: [ubuntu-latest] + + steps: + - name: Checkout Git Source + uses: actions/checkout@v2 + + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v1 + with: + node-version: ${{ matrix.node-version }} + + - name: Install Dependencies + run: npm i -g npminstall && npminstall + + - name: Continuous Integration + run: npm run ci + + - name: Code Coverage + uses: codecov/codecov-action@v1 + with: + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/DEVELOPER.md b/DEVELOPER.md new file mode 100644 index 00000000..46779172 --- /dev/null +++ b/DEVELOPER.md @@ -0,0 +1,41 @@ +# 如何共享 cnpmcore + +## 项目结构 + +``` +app +├── common +│ └── adapter +├── core +│ ├── entity +│ ├── events +│ ├── service +│ └── util +├── port +│ └── controller +├── repository +│ └── model +└── test + ├── control + │ └── response_time.test.js + └── controller + └── home.test.js +``` + +common: +- util:全局工具类 +- adapter:外部服务调用 + +core: +- entity:核心模型,实现业务行为 +- events:异步事件定义,以及消费,串联业务 +- service:核心业务 +- util:服务 core 内部,不对外暴露 + +repository: +- model:ORM 模型,数据定义 +- XXXRepository: 仓储接口,存储、查询过程 + +port: +- controller:HTTP controller + diff --git a/app/common/adapter/.gitkeep b/app/common/adapter/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/app/core/entity/Dist.ts b/app/core/entity/Dist.ts new file mode 100644 index 00000000..ba0090c1 --- /dev/null +++ b/app/core/entity/Dist.ts @@ -0,0 +1,32 @@ +import { Entity, EntityData } from './Entity'; +import { EasyData, EntityUtil } from '../util/EntityUtil'; + +export interface DistData extends EntityData { + distId: string; + name: string; + path: string; + size: number; + shasum: string; +} + +export class Dist extends Entity { + readonly distId: string; + readonly name: string; + readonly path: string; + readonly size: number; + readonly shasum: string; + + constructor(data: DistData) { + super(data); + this.distId = data.distId; + this.name = data.name; + this.path = data.path; + this.size = data.size; + this.shasum = data.shasum; + } + + static create(data: EasyData): Dist { + const newData = EntityUtil.defaultData(data, 'distId'); + return new Dist(newData); + } +} diff --git a/app/core/entity/Entity.ts b/app/core/entity/Entity.ts new file mode 100644 index 00000000..42915551 --- /dev/null +++ b/app/core/entity/Entity.ts @@ -0,0 +1,18 @@ +export interface EntityData { + id?: bigint; + gmtModified: Date; + gmtCreate: Date; +} + +export class Entity { + id?: bigint; + gmtModified: Date; + + readonly gmtCreate: Date; + + constructor(data: EntityData) { + this.id = data.id; + this.gmtCreate = data.gmtCreate; + this.gmtModified = data.gmtModified; + } +} diff --git a/app/core/entity/Package.ts b/app/core/entity/Package.ts new file mode 100644 index 00000000..35502b57 --- /dev/null +++ b/app/core/entity/Package.ts @@ -0,0 +1,70 @@ +import path from 'path'; +import { Entity, EntityData } from './Entity'; +import { EasyData, EntityUtil } from '../util/EntityUtil'; +import { Dist } from './Dist'; + +export interface PackageData extends EntityData { + scope?: string; + name: string; + packageId: string; + isPrivate: boolean; +} + +export enum DIST_NAMES { + MANIFEST = 'package.json', + README = 'readme.md', + TAR = '.tar.gz', +} + +interface FileInfo { + size: number; + shasum: string; +} + +export class Package extends Entity { + readonly scope?: string; + readonly name: string; + readonly packageId: string; + readonly isPrivate: boolean; + + constructor(data: PackageData) { + super(data); + this.scope = data.scope; + this.name = data.name; + this.packageId = data.packageId; + this.isPrivate = data.isPrivate; + } + + static create(data: EasyData): Package { + const newData = EntityUtil.defaultData(data, 'packageId'); + return new Package(newData); + } + + distDir(version: string) { + if (this.scope) { + return `/packages/${this.scope}/${this.name}/${version}/`; + } + return `/packages/${this.name}/${version}/`; + } + + private createDist(version: string, name: string, info: FileInfo) { + return Dist.create({ + name, + size: info.size, + shasum: info.shasum, + path: path.join(this.distDir(version), name), + }); + } + + createManifest(version: string, info: FileInfo) { + return this.createDist(version, DIST_NAMES.MANIFEST, info); + } + + createReadme(version: string, info: FileInfo) { + return this.createDist(version, DIST_NAMES.README, info); + } + + createTar(version: string, info: FileInfo) { + return this.createDist(version, `${this.name}-${version}${DIST_NAMES.TAR}`, info); + } +} diff --git a/app/core/entity/PackageVersion.ts b/app/core/entity/PackageVersion.ts new file mode 100644 index 00000000..5c50cb7e --- /dev/null +++ b/app/core/entity/PackageVersion.ts @@ -0,0 +1,39 @@ +import { Dist } from './Dist'; +import { Entity, EntityData } from './Entity'; +import { EasyData, EntityUtil } from '../util/EntityUtil'; + +export interface PackageVersionData extends EntityData { + packageId: string; + packageVersionId: string; + version: string; + manifestDist: Dist; + tarDist: Dist; + readmeDist: Dist; + publishTime: Date; +} + +export class PackageVersion extends Entity { + packageId: string; + packageVersionId: string; + version: string; + manifestDist: Dist; + tarDist: Dist; + readmeDist: Dist; + publishTime: Date; + + constructor(data: PackageVersionData) { + super(data); + this.packageId = data.packageId; + this.packageVersionId = data.packageVersionId; + this.version = data.version; + this.manifestDist = data.manifestDist; + this.tarDist = data.tarDist; + this.readmeDist = data.readmeDist; + this.publishTime = data.publishTime; + } + + static create(data: EasyData): PackageVersion { + const newData = EntityUtil.defaultData(data, 'packageVersionId'); + return new PackageVersion(newData); + } +} diff --git a/app/core/events/index.ts b/app/core/events/index.ts new file mode 100644 index 00000000..73285d60 --- /dev/null +++ b/app/core/events/index.ts @@ -0,0 +1,9 @@ +import '@eggjs/tegg'; + +export const PACKAGE_PUBLISHED = 'PACKAGE_PUBLISHED'; + +declare module '@eggjs/tegg' { + interface Events { + [PACKAGE_PUBLISHED]: (packageVersionId: string) => Promise; + } +} diff --git a/app/core/package.json b/app/core/package.json new file mode 100644 index 00000000..10e04fc7 --- /dev/null +++ b/app/core/package.json @@ -0,0 +1,6 @@ +{ + "name": "cnpmcore-core", + "eggModule": { + "name": "cnpmcoreCore" + } +} diff --git a/app/core/service/PackageManagerService.ts b/app/core/service/PackageManagerService.ts new file mode 100644 index 00000000..f21fe198 --- /dev/null +++ b/app/core/service/PackageManagerService.ts @@ -0,0 +1,53 @@ +import { AccessLevel, ContextProto, EventBus, Inject } from '@eggjs/tegg'; +import { PackageRepository } from '../../repository/PackageRepository'; +import { Package } from '../entity/Package'; +import { PackageVersion } from '../entity/PackageVersion'; +import { PACKAGE_PUBLISHED } from '../events'; + +export interface PublishPackageCmd { + // maintainer: Maintainer; + name: string; + version: string; + distTag: string; + packageJson: object; + dist: Buffer; +} + +@ContextProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class PackageManagerService { + @Inject() + private readonly eventBus: EventBus; + + @Inject() + private readonly packageRepository: PackageRepository; + + async publish(cmd: PublishPackageCmd) { + const pkg = Package.create({ + name: cmd.name, + isPrivate: true, + }); + await this.packageRepository.createPackage(pkg); + const pkgVersion = PackageVersion.create({ + packageId: pkg.packageId, + publishTime: new Date(), + manifestDist: pkg.createManifest(cmd.version, { + size: 0, + shasum: '', + }), + readmeDist: pkg.createReadme(cmd.version, { + size: 0, + shasum: '', + }), + tarDist: pkg.createTar(cmd.version, { + size: 0, + shasum: '', + }), + version: cmd.version, + }); + await this.packageRepository.createPackageVersion(pkgVersion); + this.eventBus.emit(PACKAGE_PUBLISHED, pkgVersion.packageVersionId); + return pkgVersion.packageVersionId; + } +} diff --git a/app/core/util/EntityUtil.ts b/app/core/util/EntityUtil.ts new file mode 100644 index 00000000..8f5aeb6d --- /dev/null +++ b/app/core/util/EntityUtil.ts @@ -0,0 +1,20 @@ +import { EntityData } from '../entity/Entity'; +import ObjectID from 'bson-objectid'; + +type PartialBy = Omit & Partial>; + +export type EasyData = PartialBy; + + +export class EntityUtil { + static defaultData(data: EasyData, id: Id): T { + Reflect.set(data, id, EntityUtil.createId()); + data.gmtCreate = data.gmtCreate || new Date(); + data.gmtModified = data.gmtModified || new Date(); + return data as T; + } + + static createId(): string { + return new ObjectID().toHexString(); + } +} diff --git a/app/port/controller/PackageController.ts b/app/port/controller/PackageController.ts new file mode 100644 index 00000000..8aa313b7 --- /dev/null +++ b/app/port/controller/PackageController.ts @@ -0,0 +1,16 @@ +import { HTTPController, HTTPMethod, HTTPMethodEnum, HTTPParam, Inject } from '@eggjs/tegg'; +import { PackageRepository } from '../../repository/PackageRepository'; + +@HTTPController() +export class PackageController { + @Inject() + private packageRepository: PackageRepository; + + @HTTPMethod({ + path: '/:name/:version', + method: HTTPMethodEnum.GET, + }) + async showVersion(@HTTPParam() name: string, @HTTPParam() version: string) { + return this.packageRepository.findPackageVersion(null, name, version); + } +} diff --git a/app/port/package.json b/app/port/package.json new file mode 100644 index 00000000..c6ebd0f2 --- /dev/null +++ b/app/port/package.json @@ -0,0 +1,6 @@ +{ + "name": "cnpmcore-port", + "eggModule": { + "name": "cnpmcorePort" + } +} diff --git a/app/repository/PackageRepository.ts b/app/repository/PackageRepository.ts new file mode 100644 index 00000000..716a6737 --- /dev/null +++ b/app/repository/PackageRepository.ts @@ -0,0 +1,64 @@ +import { AccessLevel, ContextProto } from '@eggjs/tegg'; +import { Package as PackageModel } from './model/Package'; +import { Package as PackageEntity } from '../core/entity/Package'; +import { ModelConvertor } from './util/ModelConvertor'; +import { PackageVersion as PackageVersionEntity } from '../core/entity/PackageVersion'; +import { PackageVersion as PackageVersionModel } from './model/PackageVersion'; +import { Dist as DistModel } from './model/Dist'; +import { Dist as DistEntity } from '../core/entity/Dist'; + +@ContextProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class PackageRepository { + async createPackage(pkgEntity: PackageEntity) { + const pkgModel = await ModelConvertor.convertEntityToModel(pkgEntity, PackageModel); + await pkgModel.save(); + } + + async createPackageVersion(pkgVersionEntity: PackageVersionEntity) { + await PackageVersionModel.transaction(async function(transaction) { + const [ + pkgVersionModel, + manifestDistModel, + tarDistModel, + readmeDistModel, + ] = await Promise.all([ + ModelConvertor.convertEntityToModel(pkgVersionEntity, PackageVersionModel, transaction), + ModelConvertor.convertEntityToModel(pkgVersionEntity.manifestDist, DistModel, transaction), + ModelConvertor.convertEntityToModel(pkgVersionEntity.tarDist, DistModel, transaction), + ModelConvertor.convertEntityToModel(pkgVersionEntity.readmeDist, DistModel, transaction), + ]); + pkgVersionEntity.id = pkgVersionModel.id; + pkgVersionEntity.tarDist.id = tarDistModel.id; + pkgVersionEntity.manifestDist.id = manifestDistModel.id; + pkgVersionEntity.readmeDist.id = readmeDistModel.id; + }); + } + + async findPackageVersion(scope: string | null, name: string, version: string): Promise { + const pkg = await PackageModel.findOne({ scope, name }) as PackageModel; + if (!pkg) return; + const pkgVersionModel = await PackageVersionModel.findOne({ + packageId: pkg.packageId, + version, + }) as PackageVersionModel; + if (!pkgVersionModel) return; + const [ + tarDistModel, + readmeDistModel, + manifestDistModel, + ] = await Promise.all([ + DistModel.findOne({ distId: pkgVersionModel.tarDistId }), + DistModel.findOne({ distId: pkgVersionModel.readmeDistId }), + DistModel.findOne({ distId: pkgVersionModel.manifestDistId }), + ]); + const data = { + tarDist: ModelConvertor.convertModelToEntity(tarDistModel!, DistEntity), + readmeDist: ModelConvertor.convertModelToEntity(readmeDistModel!, DistEntity), + manifestDist: ModelConvertor.convertModelToEntity(manifestDistModel!, DistEntity), + }; + const pkgVersionEntity = ModelConvertor.convertModelToEntity(pkgVersionModel, PackageVersionEntity, data); + return pkgVersionEntity; + } +} diff --git a/app/repository/model/Dist.ts b/app/repository/model/Dist.ts new file mode 100644 index 00000000..2a365e53 --- /dev/null +++ b/app/repository/model/Dist.ts @@ -0,0 +1,34 @@ +import { Attribute, Model } from '@eggjs/tegg-orm-decorator'; +// TODO leoric typing add DataTypes +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { DataTypes, Bone } from 'leoric'; + +@Model() +export class Dist extends Bone { + @Attribute(DataTypes.BIGINT, { + primary: true, + }) + id: bigint; + + @Attribute(DataTypes.DATE) + gmtCreate: Date; + + @Attribute(DataTypes.DATE) + gmtModified: Date; + + @Attribute(DataTypes.STRING(24)) + distId: string; + + @Attribute(DataTypes.STRING(100)) + name: string; + + @Attribute(DataTypes.STRING(512)) + path: string; + + @Attribute(DataTypes.INTEGER(10)) + size: number; + + @Attribute(DataTypes.STRING(512)) + shasum: string; +} diff --git a/app/repository/model/Package.ts b/app/repository/model/Package.ts new file mode 100644 index 00000000..40644d5b --- /dev/null +++ b/app/repository/model/Package.ts @@ -0,0 +1,31 @@ +import { Attribute, Model } from '@eggjs/tegg-orm-decorator'; +// TODO leoric typing add DataTypes +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { DataTypes, Bone } from 'leoric'; + +@Model() +export class Package extends Bone { + @Attribute(DataTypes.BIGINT, { + primary: true, + }) + id: bigint; + + @Attribute(DataTypes.DATE) + gmtCreate: Date; + + @Attribute(DataTypes.DATE) + gmtModified: Date; + + @Attribute(DataTypes.STRING(24)) + packageId: string; + + @Attribute(DataTypes.STRING(214)) + scope?: string; + + @Attribute(DataTypes.STRING(214)) + name: string; + + @Attribute(DataTypes.BOOLEAN) + isPrivate: boolean; +} diff --git a/app/repository/model/PackageDep.ts b/app/repository/model/PackageDep.ts new file mode 100644 index 00000000..44171bd7 --- /dev/null +++ b/app/repository/model/PackageDep.ts @@ -0,0 +1,34 @@ +import { Attribute, Model } from '@eggjs/tegg-orm-decorator'; +// TODO leoric typing add DataTypes +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { DataTypes, Bone } from 'leoric'; + +@Model() +export class PackageDep extends Bone { + @Attribute(DataTypes.BIGINT, { + primary: true, + }) + id: bigint; + + @Attribute(DataTypes.DATE) + gmtCreate: Date; + + @Attribute(DataTypes.DATE) + gmtModified: Date; + + @Attribute(DataTypes.STRING(24)) + packageVersionId: string; + + @Attribute(DataTypes.STRING(24)) + packageDepId: string; + + @Attribute(DataTypes.STRING(214)) + scope: string; + + @Attribute(DataTypes.STRING(214)) + name: string; + + @Attribute(DataTypes.STRING(100)) + spec: string; +} diff --git a/app/repository/model/PackageVersion.ts b/app/repository/model/PackageVersion.ts new file mode 100644 index 00000000..762215b6 --- /dev/null +++ b/app/repository/model/PackageVersion.ts @@ -0,0 +1,44 @@ +import { Attribute, Model } from '@eggjs/tegg-orm-decorator'; +// TODO leoric typing add DataTypes +// eslint-disable-next-line @typescript-eslint/ban-ts-comment +// @ts-ignore +import { DataTypes, Bone } from 'leoric'; +import { EntityProperty } from '../util/EntityProperty'; + +@Model() +export class PackageVersion extends Bone { + @Attribute(DataTypes.BIGINT, { + primary: true, + }) + id: bigint; + + @Attribute(DataTypes.DATE) + gmtCreate: Date; + + @Attribute(DataTypes.DATE) + gmtModified: Date; + + @Attribute(DataTypes.STRING(24)) + packageId: string; + + @Attribute(DataTypes.STRING(24)) + packageVersionId: string; + + @Attribute(DataTypes.STRING(30)) + version: string; + + @EntityProperty('manifestDist.distId') + @Attribute(DataTypes.STRING(24)) + manifestDistId: string; + + @EntityProperty('tarDist.distId') + @Attribute(DataTypes.STRING(24)) + tarDistId: string; + + @EntityProperty('readmeDist.distId') + @Attribute(DataTypes.STRING(24)) + readmeDistId: string; + + @Attribute(DataTypes.DATE) + publishTime: Date; +} diff --git a/app/repository/package.json b/app/repository/package.json new file mode 100644 index 00000000..db903524 --- /dev/null +++ b/app/repository/package.json @@ -0,0 +1,6 @@ +{ + "name": "cnpmcore-repository", + "eggModule": { + "name": "cnpmcoreRepository" + } +} diff --git a/app/repository/util/EntityProperty.ts b/app/repository/util/EntityProperty.ts new file mode 100644 index 00000000..2e208845 --- /dev/null +++ b/app/repository/util/EntityProperty.ts @@ -0,0 +1,13 @@ +import assert from 'assert'; +import { EggProtoImplClass } from '@eggjs/tegg'; +import { ModelConvertorUtil } from './ModelConvertorUtil'; + +export function EntityProperty(entityProperty: string) { + return function(target: any, modelProperty: PropertyKey) { + const clazz = target.constructor as EggProtoImplClass; + assert(typeof modelProperty === 'string', + `[model/${clazz.name}] expect method name be typeof string, but now is ${String(modelProperty)}`); + ModelConvertorUtil.addEntityPropertyName(entityProperty, clazz, modelProperty as string); + }; +} + diff --git a/app/repository/util/ModelConvertor.ts b/app/repository/util/ModelConvertor.ts new file mode 100644 index 00000000..9d3fc4b6 --- /dev/null +++ b/app/repository/util/ModelConvertor.ts @@ -0,0 +1,40 @@ +import { ModelMetadataUtil } from '@eggjs/tegg-orm-decorator'; +import { Bone } from 'leoric'; +import { ModelConvertorUtil } from './ModelConvertorUtil'; +import { EggProtoImplClass } from '@eggjs/tegg'; +import _ from 'lodash'; + +export class ModelConvertor { + static async convertEntityToModel(entity: object, ModelClazz: EggProtoImplClass, options?): Promise { + const metadata = ModelMetadataUtil.getControllerMetadata(ModelClazz); + if (!metadata) { + throw new Error(`Model ${ModelClazz.name} has no metadata`); + } + const attributes = {}; + for (const attributeMeta of metadata.attributes) { + const modelPropertyName = attributeMeta.propertyName; + const entityPropertyName = ModelConvertorUtil.getEntityPropertyName(ModelClazz, modelPropertyName); + const attributeValue = _.get(entity, entityPropertyName); + attributes[modelPropertyName] = attributeValue; + } + const model = await (ModelClazz as unknown as typeof Bone).create(attributes, options); + return model as T; + } + + static convertModelToEntity(bone: Bone, entityClazz: EggProtoImplClass, data?: object): T { + data = data || {}; + const ModelClazz = bone.constructor; + const metadata = ModelMetadataUtil.getControllerMetadata(ModelClazz); + if (!metadata) { + throw new Error(`Model ${ModelClazz.name} has no metadata`); + } + for (const attributeMeta of metadata.attributes) { + const modelPropertyName = attributeMeta.propertyName; + const entityPropertyName = ModelConvertorUtil.getEntityPropertyName(ModelClazz as EggProtoImplClass, modelPropertyName); + const attributeValue = bone[attributeMeta.propertyName]; + _.set(data, entityPropertyName, attributeValue); + } + const model = Reflect.construct(entityClazz, [ data ]); + return model; + } +} diff --git a/app/repository/util/ModelConvertorUtil.ts b/app/repository/util/ModelConvertorUtil.ts new file mode 100644 index 00000000..94983d9b --- /dev/null +++ b/app/repository/util/ModelConvertorUtil.ts @@ -0,0 +1,18 @@ +import { EggProtoImplClass, MetadataUtil } from '@eggjs/tegg'; + +const ENTITY_PROPERTY_MAP_ATTRIBUTE = Symbol.for('EggPrototype#model#entityPropertyMap'); + +export class ModelConvertorUtil { + static addEntityPropertyName(entityProperty: string, clazz: EggProtoImplClass, modelProperty: string) { + const propertyMap: Map = MetadataUtil.initOwnMapMetaData(ENTITY_PROPERTY_MAP_ATTRIBUTE, clazz, new Map()); + propertyMap.set(modelProperty, entityProperty); + } + + /** + * If has no entity property info, use modelProperty as default value + */ + static getEntityPropertyName(clazz: EggProtoImplClass, modelProperty: string): string { + const propertyMap: Map | undefined = MetadataUtil.getOwnMetaData(ENTITY_PROPERTY_MAP_ATTRIBUTE, clazz); + return propertyMap?.get(modelProperty) ?? modelProperty; + } +} diff --git a/config/config.default.ts b/config/config.default.ts index 208980f9..efe4b94e 100644 --- a/config/config.default.ts +++ b/config/config.default.ts @@ -6,5 +6,13 @@ export default (appInfo: EggAppConfig) => { // override config from framework / plugin config.keys = appInfo.name + '123456'; + config.orm = { + client: 'mysql', + database: 'cnpmcore', + host: 'localhost', + port: 3306, + user: 'root', + }; + return config; }; diff --git a/config/plugin.ts b/config/plugin.ts index f7493ab2..45dc9e47 100644 --- a/config/plugin.ts +++ b/config/plugin.ts @@ -13,6 +13,17 @@ const plugin: EggPlugin = { enable: true, package: '@eggjs/tegg-controller-plugin', }, + teggOrm: { + enable: true, + package: '@eggjs/tegg-orm-plugin', + }, + eventbusModule: { + enable: true, + package: '@eggjs/tegg-eventbus-plugin', + }, + view: { + enable: false, + }, }; export default plugin; diff --git a/package.json b/package.json index 49d5df64..deaf21e3 100644 --- a/package.json +++ b/package.json @@ -1,5 +1,5 @@ { - "name": "npmcore", + "name": "cnpmcore", "version": "0.0.2", "description": "npm core", "main": "index.js", @@ -25,6 +25,7 @@ "url": "git+https://github.com/cnpm/npmcore.git" }, "egg": { + "declarations": true, "typescript": true }, "keywords": [ @@ -42,6 +43,20 @@ }, "devDependencies": { "egg-bin": "^4.16.4", + "@eggjs/tegg-eventbus-plugin": "^0.1.5", + "@eggjs/tegg-orm-decorator": "^0.1.5", + "@eggjs/tegg-orm-plugin": "^0.1.5", + "@eggjs/tegg-plugin": "^0.1.1", + "@eggjs/tsconfig": "^1.0.0", + "bson-objectid": "^2.0.1", + "egg": "^2.29.4", + "eslint": "^7.32.0", + "eslint-config-egg": "^9.0.0", + "leoric": "^1.9.0", + "mysql": "^2.18.1", + "mysql2": "^2.3.0", + "@types/mocha": "^9.0.0", + "egg-mock": "^3.26.0", "typescript": "^4.3.5" }, "author": "killagu", diff --git a/sql/init.sql b/sql/init.sql new file mode 100644 index 00000000..28b48f5d --- /dev/null +++ b/sql/init.sql @@ -0,0 +1,56 @@ +CREATE TABLE IF NOT EXISTS `packages` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `gmt_create` datetime NOT NULL COMMENT 'create time', + `gmt_modified` datetime NOT NULL COMMENT 'modified time', + `package_id` varchar(24) NOT NULL COMMENT 'package id', + `is_private` tinyint NOT NULL DEFAULT 0 COMMENT 'private pkg or not, 1: true, other: false', + `name` varchar(214) NOT NULL COMMENT 'module name', + `scope` varchar(214) NULL COMMENT 'module name', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_package_id` (`package_id`), + UNIQUE KEY `uk_scope_name` (`scope`,`name`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='package info'; + +CREATE TABLE IF NOT EXISTS `package_versions` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `gmt_create` datetime NOT NULL COMMENT 'create time', + `gmt_modified` datetime NOT NULL COMMENT 'modified time', + `package_id` varchar(24) NOT NULL COMMENT 'package id', + `package_version_id` varchar(24) NOT NULL COMMENT 'package version id', + `version` varchar(30) NOT NULL COMMENT 'package version', + `manifest_dist_id` varchar(24) NOT NULL COMMENT 'manifest dist id', + `tar_dist_id` varchar(24) NOT NULL COMMENT 'tar dist id', + `readme_dist_id` varchar(24) NOT NULL COMMENT 'readme dist id', + `publish_time` datetime NOT NULL COMMENT 'publish time', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_package_version_id` (`package_version_id`), + UNIQUE KEY `uk_package_id_version` (`package_id`, `version`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='package version info'; + +CREATE TABLE IF NOT EXISTS `package_deps` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `gmt_create` datetime NOT NULL COMMENT 'create time', + `gmt_modified` datetime NOT NULL COMMENT 'modified time', + `package_version_id` varchar(24) NOT NULL COMMENT 'package version id', + `package_dep_id` varchar(24) NOT NULL COMMENT 'package dep id', + `scope` varchar(214) NOT NULL COMMENT 'package scope', + `name` varchar(214) NOT NULL COMMENT 'package name', + `spec` varchar(100) NOT NULL COMMENT 'package dep spec', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_package_dep_id` (`package_dep_id`), + UNIQUE KEY `uk_package_version_id_scope_name` (`package_version_id`, `scope`, `name`) +); + +CREATE TABLE IF NOT EXISTS `dists` ( + `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT 'primary key', + `gmt_create` datetime NOT NULL COMMENT 'create time', + `gmt_modified` datetime NOT NULL COMMENT 'modified time', + `dist_id` varchar(24) NOT NULL COMMENT 'dist id', + `name` varchar(100) NOT NULL COMMENT 'dist name', + `path` varchar(512) NOT NULL COMMENT 'access path', + `size` int(10) unsigned NOT NULL COMMENT 'file size', + `shasum` varchar(512) NOT NULL COMMENT 'dist shasum', + PRIMARY KEY (`id`), + UNIQUE KEY `uk_dist_id` (`dist_id`), + UNIQUE KEY `uk_path` (`path`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='dist info'; diff --git a/test/1.global.test.ts b/test/1.global.test.ts new file mode 100644 index 00000000..ea0a8bec --- /dev/null +++ b/test/1.global.test.ts @@ -0,0 +1,6 @@ +import { TestUtil } from './TestUtil'; + +before(async () => { + await TestUtil.createDatabase(); + console.log('create table done!!!'); +}); diff --git a/test/TestUtil.ts b/test/TestUtil.ts new file mode 100644 index 00000000..e9ca761f --- /dev/null +++ b/test/TestUtil.ts @@ -0,0 +1,44 @@ +import { promises as fs } from 'fs'; +import mysql from 'mysql'; +import path from 'path'; + +export class TestUtil { + static async getMySqlConfig(): Promise { + // TODO use env + return { + host: '127.0.0.1', + port: 3306, + password: '', + user: 'root', + multipleStatements: true, + }; + } + + static async getTableSqls(): Promise { + return await fs.readFile(path.join(__dirname, '../sql/init.sql'), 'utf8'); + } + + static async query(conn, sql) { + return new Promise((resolve, reject) => { + conn.query(sql, (err, res) => { + if (err) { + return reject(err); + } + return resolve(res); + }); + }); + } + + static async createDatabase() { + // TODO use leoric sync + const config = await this.getMySqlConfig(); + const connection = mysql.createConnection(config); + connection.connect(); + const sqls = await this.getTableSqls(); + await this.query(connection, 'DROP DATABASE IF EXISTS cnpmcore;'); + await this.query(connection, 'CREATE DATABASE IF NOT EXISTS cnpmcore;'); + await this.query(connection, 'USE cnpmcore;'); + await this.query(connection, sqls); + connection.destroy(); + } +} diff --git a/test/core/service/PackageManagerService.test.ts b/test/core/service/PackageManagerService.test.ts new file mode 100644 index 00000000..d6cda347 --- /dev/null +++ b/test/core/service/PackageManagerService.test.ts @@ -0,0 +1,44 @@ +import assert from 'assert'; +import { app } from 'egg-mock/bootstrap'; +import { Context } from 'egg'; +import { PackageManagerService } from '../../../app/core/service/PackageManagerService'; +import { PackageRepository } from '../../../app/repository/PackageRepository'; +import { Package } from '../../../app/repository/model/Package'; +import { PackageVersion } from '../../../app/repository/model/PackageVersion'; +import { Dist } from '../../../app/repository/model/Dist'; + +describe('test/core/service/PackageManagerService.test.ts', () => { + let ctx: Context; + let packageManagerService: PackageManagerService; + let packageRepository: PackageRepository; + + beforeEach(async () => { + ctx = await app.mockModuleContext(); + packageManagerService = await ctx.getEggObject(PackageManagerService); + packageRepository = await ctx.getEggObject(PackageRepository); + }); + + afterEach(async () => { + app.destroyModuleContext(ctx); + await Promise.all([ + Package.truncate(), + PackageVersion.truncate(), + Dist.truncate(), + ]); + }); + + describe('create package', () => { + it('should work', async () => { + await packageManagerService.publish({ + dist: Buffer.alloc(0), + distTag: '', + name: 'foo', + packageJson: {}, + version: '1.0.0', + }); + const pkgVersion = await packageRepository.findPackageVersion(null, 'foo', '1.0.0'); + assert(pkgVersion); + assert(pkgVersion.version === '1.0.0'); + }); + }); +}); diff --git a/test/port/controller/PackageController.test.ts b/test/port/controller/PackageController.test.ts new file mode 100644 index 00000000..c4efabd7 --- /dev/null +++ b/test/port/controller/PackageController.test.ts @@ -0,0 +1,47 @@ +import assert from 'assert'; +import { Context } from 'egg'; +import { app } from 'egg-mock/bootstrap'; +import { PackageManagerService } from '../../../app/core/service/PackageManagerService'; +import { Package } from '../../../app/repository/model/Package'; +import { PackageVersion } from '../../../app/repository/model/PackageVersion'; +import { Dist } from '../../../app/repository/model/Dist'; + +describe('test/controller/PackageController.test.ts', () => { + let ctx: Context; + let packageManagerService: PackageManagerService; + + beforeEach(async () => { + ctx = await app.mockModuleContext(); + packageManagerService = await ctx.getEggObject(PackageManagerService); + }); + + afterEach(async () => { + app.destroyModuleContext(ctx); + await Promise.all([ + Package.truncate(), + PackageVersion.truncate(), + Dist.truncate(), + ]); + }); + + describe('/:name/:test', () => { + beforeEach(async () => { + await packageManagerService.publish({ + dist: Buffer.alloc(0), + distTag: '', + name: 'foo', + packageJson: {}, + version: '1.0.0', + }); + }); + + it('should work', async () => { + await app.httpRequest() + .get('/foo/1.0.0') + .expect(200) + .expect(res => { + assert(res.body); + }); + }); + }); +}); diff --git a/tsconfig.json b/tsconfig.json index 376b9024..f2cc38d6 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,3 +1,11 @@ { - "extends": "@eggjs/tsconfig" + "extends": "@eggjs/tsconfig", + "compilerOptions": { + "declaration": false, + "resolveJsonModule": true, + "baseUrl": "." + }, + "exclude": [ + "node_modules" + ] } diff --git a/tsconfig.prod.json b/tsconfig.prod.json index 6820a12d..2cbaaac4 100644 --- a/tsconfig.prod.json +++ b/tsconfig.prod.json @@ -2,8 +2,14 @@ "extends": "@eggjs/tsconfig", "compilerOptions": { "outDir": "dist", + "declaration": false, + "resolveJsonModule": true, "baseUrl": "." }, + "exclude": [ + "node_modules", + "test" + ], "include": [ "app/**/*.ts", "app/**/*.json",