diff --git a/.github/workflows/nodejs.yml b/.github/workflows/nodejs.yml index fe6a556c..d7a200a2 100644 --- a/.github/workflows/nodejs.yml +++ b/.github/workflows/nodejs.yml @@ -20,7 +20,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 14, 16, 18, 20 ] + node-version: [ 16, 18, 20 ] steps: - name: Checkout Git Source uses: actions/checkout@master @@ -51,7 +51,7 @@ jobs: strategy: fail-fast: false matrix: - node-version: [ 14, 16, 18, 20 ] + node-version: [ 16, 18, 20 ] steps: - name: Checkout Git Source uses: actions/checkout@master @@ -80,32 +80,33 @@ jobs: uses: codecov/codecov-action@v1 with: token: ${{ secrets.CODECOV_TOKEN }} - Runner-windows: - runs-on: windows-latest - - strategy: - fail-fast: false - matrix: - node-version: [ 14, 16, 18, 20 ] - steps: - - name: Checkout Git Source - uses: actions/checkout@master - - - name: Setup Node.js - uses: actions/setup-node@v1 - with: - node-version: ${{ matrix.node-version }} - - - name: Install Npm - run: npm i -g npm@8 - - - name: Install Dependencies - run: npm i - - - name: Continuous integration - run: npm run ci - - - name: Code Coverage - uses: codecov/codecov-action@v1 - with: - token: ${{ secrets.CODECOV_TOKEN }} +# GitHub mysql service not support windows +# Runner-windows: +# runs-on: windows-latest +# +# strategy: +# fail-fast: false +# matrix: +# node-version: [ 16, 18, 20 ] +# steps: +# - name: Checkout Git Source +# uses: actions/checkout@master +# +# - name: Setup Node.js +# uses: actions/setup-node@v1 +# with: +# node-version: ${{ matrix.node-version }} +# +# - name: Install Npm +# run: npm i -g npm@8 +# +# - name: Install Dependencies +# run: npm i +# +# - name: Continuous integration +# run: npm run ci +# +# - name: Code Coverage +# uses: codecov/codecov-action@v1 +# with: +# token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.gitignore b/.gitignore index 400766c0..53113035 100644 --- a/.gitignore +++ b/.gitignore @@ -42,3 +42,4 @@ plugin/tegg/test/fixtures/apps/**/*.js !standalone/standalone/test/fixtures/**/node_modules/**/*.js !plugin/tegg/test/fixtures/**/node_modules !plugin/config/test/fixtures/**/node_modules +.node diff --git a/core/core-decorator/src/model/EggMultiInstancePrototypeInfo.ts b/core/core-decorator/src/model/EggMultiInstancePrototypeInfo.ts index f329e3d0..7422bde3 100644 --- a/core/core-decorator/src/model/EggMultiInstancePrototypeInfo.ts +++ b/core/core-decorator/src/model/EggMultiInstancePrototypeInfo.ts @@ -9,6 +9,7 @@ export interface ObjectInfo { } export interface MultiInstancePrototypeGetObjectsContext { + moduleName: string; unitPath: string; } diff --git a/core/core-decorator/test/decorators.test.ts b/core/core-decorator/test/decorators.test.ts index 2168edfe..d9589c3f 100644 --- a/core/core-decorator/test/decorators.test.ts +++ b/core/core-decorator/test/decorators.test.ts @@ -118,6 +118,7 @@ describe('test/decorator.test.ts', () => { }; assert.deepStrictEqual(PrototypeUtil.getMultiInstanceProperty(FooLogger, { unitPath: 'foo', + moduleName: '', }), expectObjectProperty); }); }); diff --git a/core/dal-decorator/README.md b/core/dal-decorator/README.md new file mode 100644 index 00000000..67062a63 --- /dev/null +++ b/core/dal-decorator/README.md @@ -0,0 +1,5 @@ +# `@eggjs/dal-decorator` + +## Usage + +Please read [@eggjs/tegg-dal-plugin](../../plugin/dal-plugin) diff --git a/core/dal-decorator/index.ts b/core/dal-decorator/index.ts new file mode 100644 index 00000000..542f5a20 --- /dev/null +++ b/core/dal-decorator/index.ts @@ -0,0 +1,24 @@ +export * from './src/enum/CompressionType'; +export * from './src/enum/InsertMethod'; +export * from './src/enum/RowFormat'; +export * from './src/enum/IndexType'; +export * from './src/enum/ColumnType'; + +export * from './src/decorator/Index'; +export * from './src/decorator/Table'; +export * from './src/decorator/Column'; +export * from './src/decorator/DataSourceQualifier'; + +export * from './src/util/ColumnInfoUtil'; +export * from './src/util/IndexInfoUtil'; +export * from './src/util/TableInfoUtil'; + +export * from './src/model/ColumnModel'; +export * from './src/model/IndexModel'; +export * from './src/model/TableModel'; + +export * from './src/type/DateSource'; +export * from './src/type/Spatial'; +export * from './src/type/SqlMap'; +export * from './src/type/ColumnTsType'; +export * from './src/type/MySql'; diff --git a/core/dal-decorator/package.json b/core/dal-decorator/package.json new file mode 100644 index 00000000..bb98a8c2 --- /dev/null +++ b/core/dal-decorator/package.json @@ -0,0 +1,56 @@ +{ + "name": "@eggjs/dal-decorator", + "version": "3.32.0", + "description": "tegg dal decorator", + "keywords": [ + "egg", + "typescript", + "decorator", + "tegg", + "dal" + ], + "main": "dist/index.js", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts" + ], + "typings": "dist/index.d.ts", + "scripts": { + "test": "cross-env NODE_ENV=test NODE_OPTIONS='--no-deprecation' mocha", + "clean": "tsc -b --clean", + "tsc": "npm run clean && tsc -p ./tsconfig.json", + "tsc:pub": "npm run clean && tsc -p ./tsconfig.pub.json", + "prepublishOnly": "npm run tsc:pub" + }, + "author": "killagu ", + "license": "MIT", + "homepage": "https://github.com/eggjs/tegg", + "bugs": { + "url": "https://github.com/eggjs/tegg/issues" + }, + "repository": { + "type": "git", + "url": "git@github.com:eggjs/tegg.git", + "directory": "core/dal-decorator" + }, + "engines": { + "node": ">=14.0.0" + }, + "dependencies": { + "lodash.snakecase": "^4.1.1", + "pluralize": "^7.0.0", + "@eggjs/tegg-common-util": "^3.32.0", + "@eggjs/core-decorator": "^3.32.0" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.4", + "cross-env": "^7.0.3", + "mocha": "^10.2.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + } +} diff --git a/core/dal-decorator/src/decorator/Column.ts b/core/dal-decorator/src/decorator/Column.ts new file mode 100644 index 00000000..bfed3f63 --- /dev/null +++ b/core/dal-decorator/src/decorator/Column.ts @@ -0,0 +1,273 @@ +import assert from 'node:assert'; +import { ColumnType } from '../enum/ColumnType'; +import { EggProtoImplClass } from '@eggjs/core-decorator'; +import { ColumnInfoUtil } from '../util/ColumnInfoUtil'; +import { ColumnFormat } from '../enum/ColumnFormat'; + +export interface ColumnParams { + name?: string; + default?: string; + canNull?: boolean; + comment?: string; + visible?: boolean; + autoIncrement?: boolean; + uniqueKey?: boolean; + primaryKey?: boolean; + collate?: string; + columnFormat?: ColumnFormat; + engineAttribute?: string; + secondaryEngineAttribute?: string; +} + +export interface IColumnTypeParams { + type: ColumnType; +} + +export interface BitParams extends IColumnTypeParams { + type: ColumnType.BIT, + length?: number; +} + +export interface BoolParams extends IColumnTypeParams { + type: ColumnType.BOOL, +} + +interface BaseNumericParams extends IColumnTypeParams { + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +interface BaseFloatNumericParams extends IColumnTypeParams { + length?: number; + fractionalLength?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +export interface TinyIntParams extends BaseNumericParams { + type: ColumnType.TINYINT; +} + +export interface SmallIntParams extends BaseNumericParams { + type: ColumnType.SMALLINT; +} + +export interface MediumIntParams extends BaseNumericParams { + type: ColumnType.MEDIUMINT; +} + +export interface IntParams extends BaseNumericParams { + type: ColumnType.INT; +} + +export interface BigIntParams extends BaseNumericParams { + type: ColumnType.BIGINT; +} + +export interface DecimalParams extends BaseFloatNumericParams { + type: ColumnType.DECIMAL; +} + +export interface FloatParams extends BaseFloatNumericParams { + type: ColumnType.FLOAT; +} + +export interface DoubleParams extends BaseFloatNumericParams { + type: ColumnType.DOUBLE; +} + +export interface DateParams extends IColumnTypeParams { + type: ColumnType.DATE; +} + +export interface DateTimeParams extends IColumnTypeParams { + type: ColumnType.DATETIME; + precision?: number; +} + +export interface TimestampParams extends IColumnTypeParams { + type: ColumnType.TIMESTAMP; + precision?: number; +} + +export interface TimeParams extends IColumnTypeParams { + type: ColumnType.TIME; + precision?: number; +} + +export interface YearParams extends IColumnTypeParams { + type: ColumnType.YEAR; +} + +export interface CharParams extends IColumnTypeParams { + type: ColumnType.CHAR; + length?: number; + characterSet?: string; + collate?: string; +} + +export interface VarCharParams extends IColumnTypeParams { + type: ColumnType.VARCHAR; + length: number; + characterSet?: string; + collate?: string; +} + +export interface BinaryParams extends IColumnTypeParams { + type: ColumnType.BINARY; + length?: number; +} + +export interface VarBinaryParams extends IColumnTypeParams { + type: ColumnType.VARBINARY; + length: number; +} + +export interface TinyBlobParams extends IColumnTypeParams { + type: ColumnType.TINYBLOB; +} + +export interface TinyTextParams extends IColumnTypeParams { + type: ColumnType.TINYTEXT; + characterSet?: string; + collate?: string; +} + +export interface BlobParams extends IColumnTypeParams { + type: ColumnType.BLOB; + length?: number; +} + +export interface TextParams extends IColumnTypeParams { + type: ColumnType.TEXT; + length?: number; + characterSet?: string; + collate?: string; +} + +export interface MediumBlobParams extends IColumnTypeParams { + type: ColumnType.MEDIUMBLOB; +} + +export interface LongBlobParams extends IColumnTypeParams { + type: ColumnType.LONGBLOB; +} + +export interface MediumTextParams extends IColumnTypeParams { + type: ColumnType.MEDIUMTEXT; + characterSet?: string; + collate?: string; +} + +export interface LongTextParams extends IColumnTypeParams { + type: ColumnType.LONGTEXT; + characterSet?: string; + collate?: string; +} + +export interface EnumParams extends IColumnTypeParams { + type: ColumnType.ENUM; + enums: string[]; + characterSet?: string; + collate?: string; +} + +export interface SetParams extends IColumnTypeParams { + type: ColumnType.SET; + enums: string[]; + characterSet?: string; + collate?: string; +} + +export interface JsonParams extends IColumnTypeParams { + type: ColumnType.JSON; +} + +export interface BaseSpatialParams extends IColumnTypeParams { + SRID?: number; +} + +export interface GeometryParams extends BaseSpatialParams { + type: ColumnType.GEOMETRY; +} + +export interface PointParams extends BaseSpatialParams { + type: ColumnType.POINT; +} + +export interface LinestringParams extends BaseSpatialParams { + type: ColumnType.LINESTRING; +} + +export interface PolygonParams extends BaseSpatialParams { + type: ColumnType.POLYGON; +} + +export interface MultiPointParams extends BaseSpatialParams { + type: ColumnType.MULTIPOINT; +} + +export interface MultiLinestringParams extends BaseSpatialParams { + type: ColumnType.MULTILINESTRING; +} + +export interface MultiPolygonParams extends BaseSpatialParams { + type: ColumnType.MULTIPOLYGON; +} + +export interface GeometryCollectionParams extends BaseSpatialParams { + type: ColumnType.GEOMETRYCOLLECTION; +} + +export type ColumnTypeParams = BitParams +| BoolParams +| TinyIntParams +| SmallIntParams +| MediumIntParams +| IntParams +| BigIntParams +| DecimalParams +| FloatParams +| DoubleParams +| DateParams +| DateTimeParams +| TimestampParams +| TimeParams +| YearParams +| CharParams +| VarCharParams +| BinaryParams +| VarBinaryParams +| TinyBlobParams +| TinyTextParams +| BlobParams +| TextParams +| MediumBlobParams +| MediumTextParams +| LongBlobParams +| LongTextParams +| EnumParams +| SetParams +| JsonParams +| GeometryParams +| PointParams +| LinestringParams +| PolygonParams +| MultiPointParams +| MultiLinestringParams +| MultiPolygonParams +| GeometryCollectionParams; + +export function Column(type: ColumnTypeParams, params?: ColumnParams) { + return function(target: any, propertyKey: PropertyKey) { + assert(typeof propertyKey === 'string', + `[Column/${target.name}] expect column name be typeof string, but now is ${String(propertyKey)}`); + const tableClazz = target.constructor as EggProtoImplClass; + const columnName = propertyKey as string; + ColumnInfoUtil.addColumnType(tableClazz, columnName, type); + if (params) { + ColumnInfoUtil.addColumnInfo(tableClazz, columnName, params); + } + }; +} diff --git a/core/dal-decorator/src/decorator/DataSourceQualifier.ts b/core/dal-decorator/src/decorator/DataSourceQualifier.ts new file mode 100644 index 00000000..35213e72 --- /dev/null +++ b/core/dal-decorator/src/decorator/DataSourceQualifier.ts @@ -0,0 +1,11 @@ +import { QualifierUtil, EggProtoImplClass } from '@eggjs/tegg'; + +export const DataSourceQualifierAttribute = Symbol('Qualifier.DataSource'); +export const DataSourceInjectName = 'dataSource'; + +export function DataSourceQualifier(dataSourceName: string) { + return function(target: any, propertyKey: PropertyKey) { + QualifierUtil.addProperQualifier(target.constructor as EggProtoImplClass, + propertyKey, DataSourceQualifierAttribute, dataSourceName); + }; +} diff --git a/core/dal-decorator/src/decorator/Index.ts b/core/dal-decorator/src/decorator/Index.ts new file mode 100644 index 00000000..7df13ca5 --- /dev/null +++ b/core/dal-decorator/src/decorator/Index.ts @@ -0,0 +1,21 @@ +import { IndexType } from '../enum/IndexType'; +import { IndexStoreType } from '../enum/IndexStoreType'; +import { EggProtoImplClass } from '@eggjs/core-decorator'; +import { IndexInfoUtil } from '../util/IndexInfoUtil'; + +export interface IndexParams { + keys: string[]; + name?: string; + type?: IndexType, + storeType?: IndexStoreType; + comment?: string; + engineAttribute?: string; + secondaryEngineAttribute?: string; + parser?: string; +} + +export function Index(params: IndexParams) { + return function(constructor: EggProtoImplClass) { + IndexInfoUtil.addIndex(constructor, params); + }; +} diff --git a/core/dal-decorator/src/decorator/Table.ts b/core/dal-decorator/src/decorator/Table.ts new file mode 100644 index 00000000..ddd583ea --- /dev/null +++ b/core/dal-decorator/src/decorator/Table.ts @@ -0,0 +1,44 @@ +// Create Table https://dev.mysql.com/doc/refman/8.0/en/create-table.html + +import { AccessLevel, EggProtoImplClass, ObjectInitType, Prototype, PrototypeUtil } from '@eggjs/core-decorator'; +import { TableInfoUtil } from '../util/TableInfoUtil'; +import { InsertMethod } from '../enum/InsertMethod'; +import { StackUtil } from '@eggjs/tegg-common-util'; +import { CompressionType } from '../enum/CompressionType'; +import { RowFormat } from '../enum/RowFormat'; + +export interface TableParams { + name?: string; + dataSourceName?: string; + comment?: string; + autoExtendSize?: number; + autoIncrement?: number; + avgRowLength?: number; + characterSet?: string; + collate?: string; + compression?: CompressionType; + encryption?: boolean; + engine?: string; + engineAttribute?: string; + insertMethod?: InsertMethod; + keyBlockSize?: number; + maxRows?: number; + minRows?: number; + rowFormat?: RowFormat; + secondaryEngineAttribute?: string; +} + +export function Table(params?: TableParams) { + return function(constructor: EggProtoImplClass) { + TableInfoUtil.setIsTable(constructor); + if (params) { + TableInfoUtil.setTableParams(constructor, params); + } + const func = Prototype({ + accessLevel: AccessLevel.PUBLIC, + initType: ObjectInitType.ALWAYS_NEW, + }); + func(constructor); + PrototypeUtil.setFilePath(constructor, StackUtil.getCalleeFromStack(false, 5)); + }; +} diff --git a/core/dal-decorator/src/enum/ColumnFormat.ts b/core/dal-decorator/src/enum/ColumnFormat.ts new file mode 100644 index 00000000..61381760 --- /dev/null +++ b/core/dal-decorator/src/enum/ColumnFormat.ts @@ -0,0 +1,5 @@ +export enum ColumnFormat { + FIXED = 'FIXED', + DYNAMIC = 'DYNAMIC', + DEFAULT = 'DEFAULT', +} diff --git a/core/dal-decorator/src/enum/ColumnType.ts b/core/dal-decorator/src/enum/ColumnType.ts new file mode 100644 index 00000000..512987bc --- /dev/null +++ b/core/dal-decorator/src/enum/ColumnType.ts @@ -0,0 +1,46 @@ +// Data Types https://dev.mysql.com/doc/refman/8.0/en/data-types.html +export enum ColumnType { + // Numeric + BIT = 'BIT', + TINYINT = 'TINYINT', + BOOL = 'BOOL', + SMALLINT = 'SMALLINT', + MEDIUMINT = 'MEDIUMINT', + INT = 'INT', + BIGINT = 'BIGINT', + DECIMAL = 'DECIMAL', + FLOAT = 'FLOAT', + DOUBLE = 'DOUBLE', + // Date + DATE = 'DATE', + DATETIME = 'DATETIME', + TIMESTAMP = 'TIMESTAMP', + TIME = 'TIME', + YEAR = 'YEAR', + // String + CHAR = 'CHAR', + VARCHAR = 'VARCHAR', + BINARY = 'BINARY', + VARBINARY = 'VARBINARY', + TINYBLOB = 'TINYBLOB', + TINYTEXT = 'TINYTEXT', + BLOB = 'BLOB', + TEXT = 'TEXT', + MEDIUMBLOB = 'MEDIUMBLOB', + MEDIUMTEXT = 'MEDIUMTEXT', + LONGBLOB = 'LONGBLOB', + LONGTEXT = 'LONGTEXT', + ENUM = 'ENUM', + SET = 'SET', + // JSON + JSON = 'JSON', + // Spatial + GEOMETRY = 'GEOMETRY', + POINT = 'POINT', + LINESTRING = 'LINESTRING', + POLYGON = 'POLYGON', + MULTIPOINT = 'MULTIPOINT', + MULTILINESTRING = 'MULTILINESTRING', + MULTIPOLYGON = 'MULTIPOLYGON', + GEOMETRYCOLLECTION = 'GEOMETRYCOLLECTION', +} diff --git a/core/dal-decorator/src/enum/CompressionType.ts b/core/dal-decorator/src/enum/CompressionType.ts new file mode 100644 index 00000000..b1c56d5e --- /dev/null +++ b/core/dal-decorator/src/enum/CompressionType.ts @@ -0,0 +1,5 @@ +export enum CompressionType { + ZLIB = 'ZLIB', + LZ4 = 'LZ4', + NONE = 'NONE', +} diff --git a/core/dal-decorator/src/enum/IndexStoreType.ts b/core/dal-decorator/src/enum/IndexStoreType.ts new file mode 100644 index 00000000..95c565a7 --- /dev/null +++ b/core/dal-decorator/src/enum/IndexStoreType.ts @@ -0,0 +1,4 @@ +export enum IndexStoreType { + BTREE = 'BTREE', + HASH = 'HASH', +} diff --git a/core/dal-decorator/src/enum/IndexType.ts b/core/dal-decorator/src/enum/IndexType.ts new file mode 100644 index 00000000..63be2927 --- /dev/null +++ b/core/dal-decorator/src/enum/IndexType.ts @@ -0,0 +1,7 @@ +export enum IndexType { + PRIMARY = 'PRIMARY', + UNIQUE = 'UNIQUE', + INDEX = 'INDEX', + FULLTEXT = 'FULLTEXT', + SPATIAL = 'SPATIAL', +} diff --git a/core/dal-decorator/src/enum/InsertMethod.ts b/core/dal-decorator/src/enum/InsertMethod.ts new file mode 100644 index 00000000..abf9f5ff --- /dev/null +++ b/core/dal-decorator/src/enum/InsertMethod.ts @@ -0,0 +1,5 @@ +export enum InsertMethod { + NO = 'NO', + FIRST = 'FIRST', + LAST = 'LAST', +} diff --git a/core/dal-decorator/src/enum/RowFormat.ts b/core/dal-decorator/src/enum/RowFormat.ts new file mode 100644 index 00000000..05f54f0a --- /dev/null +++ b/core/dal-decorator/src/enum/RowFormat.ts @@ -0,0 +1,8 @@ +export enum RowFormat { + DEFAULT = 'DEFAULT', + DYNAMIC = 'DYNAMIC', + FIXED = 'FIXED', + COMPRESSED = 'COMPRESSED', + REDUNDANT = 'REDUNDANT', + COMPACT = 'COMPACT', +} diff --git a/core/dal-decorator/src/model/ColumnModel.ts b/core/dal-decorator/src/model/ColumnModel.ts new file mode 100644 index 00000000..ec82b7e1 --- /dev/null +++ b/core/dal-decorator/src/model/ColumnModel.ts @@ -0,0 +1,77 @@ +import { ColumnFormat } from '../enum/ColumnFormat'; +import { ColumnParams, ColumnTypeParams } from '../decorator/Column'; +import snakecase from 'lodash.snakecase'; + +export class ColumnModel { + columnName: string; + propertyName: string; + type: ColumnTypeParams; + canNull: boolean; + default?: string; + comment?: string; + visible?: boolean; + autoIncrement?: boolean; + uniqueKey?: boolean; + primaryKey?: boolean; + collate?: string; + columnFormat?: ColumnFormat; + engineAttribute?: string; + secondaryEngineAttribute?: string; + + constructor(params: { + columnName: string; + propertyName: string; + type: ColumnTypeParams; + canNull: boolean; + default?: string; + comment?: string; + visible?: boolean; + autoIncrement?: boolean; + uniqueKey?: boolean; + primaryKey?: boolean; + collate?: string; + columnFormat?: ColumnFormat; + engineAttribute?: string; + secondaryEngineAttribute?: string; + }) { + this.columnName = params.columnName; + this.propertyName = params.propertyName; + this.type = params.type; + this.canNull = params.canNull; + this.default = params.default; + this.comment = params.comment; + this.visible = params.visible; + this.autoIncrement = params.autoIncrement; + this.uniqueKey = params.uniqueKey; + this.primaryKey = params.primaryKey; + this.collate = params.collate; + this.columnFormat = params.columnFormat; + this.engineAttribute = params.engineAttribute; + this.secondaryEngineAttribute = params.secondaryEngineAttribute; + } + + static build(property: string, type: ColumnTypeParams, params?: ColumnParams) { + const columnName = params?.name ?? snakecase(property); + // TODO can null default should be false + let canNull = params?.canNull ?? true; + if (params?.primaryKey) { + canNull = false; + } + return new ColumnModel({ + columnName, + propertyName: property, + type, + canNull, + default: params?.default, + comment: params?.comment, + visible: params?.visible, + autoIncrement: params?.autoIncrement, + uniqueKey: params?.uniqueKey, + primaryKey: params?.primaryKey, + collate: params?.collate, + columnFormat: params?.columnFormat, + engineAttribute: params?.engineAttribute, + secondaryEngineAttribute: params?.secondaryEngineAttribute, + }); + } +} diff --git a/core/dal-decorator/src/model/IndexModel.ts b/core/dal-decorator/src/model/IndexModel.ts new file mode 100644 index 00000000..d72ac126 --- /dev/null +++ b/core/dal-decorator/src/model/IndexModel.ts @@ -0,0 +1,71 @@ +import { IndexType } from '../enum/IndexType'; +import { IndexStoreType } from '../enum/IndexStoreType'; +import { IndexParams } from '../decorator/Index'; +import { ColumnModel } from './ColumnModel'; + +export interface IndexKey { + columnName: string; + propertyName: string; +} + +export class IndexModel { + name: string; + keys: IndexKey[]; + type: IndexType; + + storeType?: IndexStoreType; + comment?: string; + engineAttribute?: string; + secondaryEngineAttribute?: string; + parser?: string; + + constructor(params: { + name: string; + keys: IndexKey[]; + type: IndexType; + storeType?: IndexStoreType; + comment?: string; + engineAttribute?: string; + secondaryEngineAttribute?: string; + parser?: string; + }) { + this.name = params.name; + this.keys = params.keys; + this.type = params.type; + this.storeType = params.storeType; + this.comment = params.comment; + this.engineAttribute = params.engineAttribute; + this.secondaryEngineAttribute = params.secondaryEngineAttribute; + this.parser = params.parser; + } + + static buildIndexName(keys: string[], type: IndexType) { + const prefix = type === IndexType.UNIQUE ? 'uk_' : 'idx_'; + return prefix + keys.join('_'); + } + + static build(params: IndexParams, columns: ColumnModel[]) { + const type = params.type ?? IndexType.INDEX; + const name = params.name ?? IndexModel.buildIndexName(params.keys, type); + const keys: Array = params.keys.map(t => { + const column = columns.find(c => c.propertyName === t); + if (!column) { + throw new Error(`Index "${name}" configuration error: has no property named "${t}"`); + } + return { + propertyName: column!.propertyName, + columnName: column!.columnName, + }; + }); + return new IndexModel({ + name, + keys, + type, + storeType: params.storeType, + comment: params.comment, + engineAttribute: params.engineAttribute, + secondaryEngineAttribute: params.secondaryEngineAttribute, + parser: params.parser, + }); + } +} diff --git a/core/dal-decorator/src/model/TableModel.ts b/core/dal-decorator/src/model/TableModel.ts new file mode 100644 index 00000000..7e5ed6df --- /dev/null +++ b/core/dal-decorator/src/model/TableModel.ts @@ -0,0 +1,148 @@ +import assert from 'node:assert'; +import { CompressionType } from '../enum/CompressionType'; +import { InsertMethod } from '../enum/InsertMethod'; +import { RowFormat } from '../enum/RowFormat'; +import { ColumnModel } from './ColumnModel'; +import { IndexModel } from './IndexModel'; +import { EggProtoImplClass } from '@eggjs/core-decorator'; +import { TableInfoUtil } from '../util/TableInfoUtil'; +import pluralize from 'pluralize'; +import snakecase from 'lodash.snakecase'; +import { ColumnInfoUtil } from '../util/ColumnInfoUtil'; +import { IndexInfoUtil } from '../util/IndexInfoUtil'; +import { IndexType } from '../enum/IndexType'; + +export class TableModel { + clazz: EggProtoImplClass; + name: string; + columns: Array; + indices: Array; + dataSourceName: string; + comment?: string; + autoExtendSize?: number; + autoIncrement?: number; + avgRowLength?: number; + characterSet?: string; + collate?: string; + compression?: CompressionType; + encryption?: boolean; + engine?: string; + engineAttribute?: string; + insertMethod?: InsertMethod; + keyBlockSize?: number; + maxRows?: number; + minRows?: number; + rowFormat?: RowFormat; + secondaryEngineAttribute?: string; + + constructor(params: { + clazz: EggProtoImplClass; + name: string; + dataSourceName: string; + columns: Array; + indices: Array; + comment?: string; + autoExtendSize?: number; + autoIncrement?: number; + avgRowLength?: number; + characterSet?: string; + collate?: string; + compression?: CompressionType; + encryption?: boolean; + engine?: string; + engineAttribute?: string; + insertMethod?: InsertMethod; + keyBlockSize?: number; + maxRows?: number; + minRows?: number; + rowFormat?: RowFormat; + secondaryEngineAttribute?: string; + }) { + this.clazz = params.clazz; + this.name = params.name; + this.dataSourceName = params.dataSourceName; + this.columns = params.columns; + this.indices = params.indices; + this.comment = params.comment; + this.autoExtendSize = params.autoExtendSize; + this.autoIncrement = params.autoIncrement; + this.avgRowLength = params.avgRowLength; + this.characterSet = params.characterSet; + this.collate = params.collate; + this.compression = params.compression; + this.encryption = params.encryption; + this.engine = params.engine; + this.engineAttribute = params.engineAttribute; + this.insertMethod = params.insertMethod; + this.keyBlockSize = params.keyBlockSize; + this.maxRows = params.maxRows; + this.minRows = params.minRows; + this.rowFormat = params.rowFormat; + this.secondaryEngineAttribute = params.secondaryEngineAttribute; + } + + getPrimary(): IndexModel | undefined { + const index = this.indices.find(t => t.type === IndexType.PRIMARY); + if (index) { + return index; + } + const primaryColumn = this.columns.filter(t => t.primaryKey === true); + return new IndexModel({ + name: 'PRIMARY', + type: IndexType.PRIMARY, + keys: primaryColumn.map(t => { + return { + columnName: t.columnName, + propertyName: t.propertyName, + }; + }), + }); + } + + static build(clazz: EggProtoImplClass): TableModel { + const params = TableInfoUtil.getTableParams(clazz as EggProtoImplClass); + const name = params?.name ?? snakecase(pluralize(clazz.name)); + const columnInfoMap = ColumnInfoUtil.getColumnInfoMap(clazz as EggProtoImplClass); + const columnTypeMap = ColumnInfoUtil.getColumnTypeMap(clazz as EggProtoImplClass); + // TODO set to default name + const dataSourceName = params?.dataSourceName ?? 'default'; + assert(TableInfoUtil.getIsTable(clazz as EggProtoImplClass), `${name} is not Table`); + assert(columnTypeMap, `${name} has no columns`); + const columns: Array = []; + const indices: Array = []; + for (const [ property, columnType ] of columnTypeMap?.entries()) { + const columnParam = columnInfoMap?.get(property); + columns.push(ColumnModel.build(property, columnType, columnParam)); + } + + const indexList = IndexInfoUtil.getIndexList(clazz as EggProtoImplClass); + for (const index of indexList) { + indices.push(IndexModel.build(index, columns)); + } + + return new TableModel({ + clazz, + name, + columns, + indices, + dataSourceName, + + comment: params?.comment, + autoExtendSize: params?.autoExtendSize, + autoIncrement: params?.autoIncrement, + avgRowLength: params?.avgRowLength, + characterSet: params?.characterSet, + collate: params?.collate, + compression: params?.compression, + encryption: params?.encryption, + engine: params?.engine, + engineAttribute: params?.engineAttribute, + insertMethod: params?.insertMethod, + keyBlockSize: params?.keyBlockSize, + maxRows: params?.maxRows, + minRows: params?.minRows, + rowFormat: params?.rowFormat, + secondaryEngineAttribute: params?.secondaryEngineAttribute, + }); + } +} diff --git a/core/dal-decorator/src/type/ColumnTsType.ts b/core/dal-decorator/src/type/ColumnTsType.ts new file mode 100644 index 00000000..0a60fe69 --- /dev/null +++ b/core/dal-decorator/src/type/ColumnTsType.ts @@ -0,0 +1,42 @@ +import { Geometry, GeometryCollection, Line, MultiLine, MultiPoint, MultiPolygon, Point, Polygon } from './Spatial'; + +export interface ColumnTsType { + BIT: Buffer, + TINYINT: number, + BOOL: 0 | 1, + SMALLINT: number, + MEDIUMINT: number, + INT: number, + BIGINT: string, + DECIMAL: string, + FLOAT: number, + DOUBLE: number, + DATE: Date, + DATETIME: Date, + TIMESTAMP: Date, + TIME: string, + YEAR: number, + CHAR: string, + VARCHAR: string, + BINARY: Buffer, + VARBINARY: Buffer, + TINYBLOB: Buffer, + TINYTEXT: string, + BLOB: Buffer, + TEXT: string, + MEDIUMBLOB: Buffer, + MEDIUMTEXT: string, + LONGBLOB: Buffer, + LONGTEXT: string, + ENUM: string, + SET: string, + JSON: object, + GEOMETRY: Geometry, + POINT: Point, + LINESTRING: Line, + POLYGON: Polygon, + MULTIPOINT: MultiPoint, + MULTILINESTRING: MultiLine, + MULTIPOLYGON: MultiPolygon, + GEOMETRYCOLLECTION: GeometryCollection, +} diff --git a/core/dal-decorator/src/type/DateSource.ts b/core/dal-decorator/src/type/DateSource.ts new file mode 100644 index 00000000..8a41a60f --- /dev/null +++ b/core/dal-decorator/src/type/DateSource.ts @@ -0,0 +1,15 @@ +export interface PaginateData { + total: number; + pageNum: number; + rows: Array; +} + + +export interface DataSource { + execute(sqlName: string, data?: any): Promise>; + executeScalar(sqlName: string, data?: any): Promise; + executeRaw(sqlName: string, data?: any): Promise>; + executeRawScalar(sqlName: string, data?: any): Promise; + paginate(sqlName: string, data: any, currentPage: number, perPageCount: number): Promise>; + count(sqlName: string, data?: any): Promise; +} diff --git a/core/dal-decorator/src/type/MySql.ts b/core/dal-decorator/src/type/MySql.ts new file mode 100644 index 00000000..f6bd1a96 --- /dev/null +++ b/core/dal-decorator/src/type/MySql.ts @@ -0,0 +1 @@ +export { InsertResult, UpdateResult, DeleteResult } from '@eggjs/rds/lib/types'; diff --git a/core/dal-decorator/src/type/Spatial.ts b/core/dal-decorator/src/type/Spatial.ts new file mode 100644 index 00000000..405fbc26 --- /dev/null +++ b/core/dal-decorator/src/type/Spatial.ts @@ -0,0 +1,51 @@ +import { ColumnType } from '../enum/ColumnType'; + +export interface Point { + x: number; + y: number; +} + +export type Line = Array; +export type Polygon = Array; +export type Geometry = Point | Line | Polygon; + +export type MultiPoint = Array; +export type MultiLine = Array; +export type MultiPolygon = Array; +export type GeometryCollection = Array; + +export class SpatialHelper { + static isPoint(t: Geometry) { + return typeof Reflect.get(t, 'x') === 'number' && typeof Reflect.get(t, 'y') === 'number'; + } + + static isLine(t: Geometry) { + return Array.isArray(t) && t[0] && SpatialHelper.isPoint(t[0]); + } + + static isPolygon(t: Geometry) { + return Array.isArray(t) && t[0] && SpatialHelper.isLine(t[0]); + } + + static getGeometryType(t: Geometry) { + if (SpatialHelper.isPoint(t)) { + return ColumnType.POINT; + } else if (SpatialHelper.isLine(t)) { + return ColumnType.LINESTRING; + } + return ColumnType.POLYGON; + + } + + static isMultiPoint(t: GeometryCollection) { + return Array.isArray(t) && t[0] && SpatialHelper.isPoint(t[0]); + } + + static isMultiLine(t: GeometryCollection) { + return Array.isArray(t) && t[0] && SpatialHelper.isLine(t[0]); + } + + static isMultiPolygon(t: GeometryCollection) { + return Array.isArray(t) && t[0] && SpatialHelper.isPolygon(t[0]); + } +} diff --git a/core/dal-decorator/src/type/SqlMap.ts b/core/dal-decorator/src/type/SqlMap.ts new file mode 100644 index 00000000..e9ffd12f --- /dev/null +++ b/core/dal-decorator/src/type/SqlMap.ts @@ -0,0 +1,23 @@ +export enum SqlType { + BLOCK = 'BLOCK', + INSERT = 'INSERT', + SELECT = 'SELECT', + UPDATE = 'UPDATE', + DELETE = 'DELETE', +} + +export interface BaseSqlMap { + type?: SqlType; +} + +export interface FullSqlMap extends BaseSqlMap { + type: SqlType.DELETE | SqlType.INSERT | SqlType.UPDATE | SqlType.SELECT; + sql: string; +} + +export interface BlockSqlMap extends BaseSqlMap { + type: SqlType.BLOCK; + content: string; +} + +export type SqlMap = FullSqlMap | BlockSqlMap; diff --git a/core/dal-decorator/src/util/ColumnInfoUtil.ts b/core/dal-decorator/src/util/ColumnInfoUtil.ts new file mode 100644 index 00000000..616b64bb --- /dev/null +++ b/core/dal-decorator/src/util/ColumnInfoUtil.ts @@ -0,0 +1,27 @@ +import { EggProtoImplClass, MetadataUtil } from '@eggjs/core-decorator'; +import { ColumnParams, ColumnTypeParams } from '../decorator/Column'; + +export const DAL_COLUMN_INFO_MAP = Symbol('EggPrototype#dalColumnInfoMap'); +export const DAL_COLUMN_TYPE_MAP = Symbol('EggPrototype#dalColumnTypeMap'); +export type ColumnInfoMap = Map; +export type ColumnTypeMap = Map; + +export class ColumnInfoUtil { + static addColumnInfo(clazz: EggProtoImplClass, property: string, column: ColumnInfoUtil) { + const columnInfoMap = MetadataUtil.initOwnMapMetaData(DAL_COLUMN_INFO_MAP, clazz, new Map()); + columnInfoMap.set(property, column); + } + + static addColumnType(clazz: EggProtoImplClass, property: string, type: ColumnTypeParams) { + const columnInfoMap = MetadataUtil.initOwnMapMetaData(DAL_COLUMN_TYPE_MAP, clazz, new Map()); + columnInfoMap.set(property, type); + } + + static getColumnInfoMap(clazz: EggProtoImplClass): ColumnInfoMap | undefined { + return MetadataUtil.getMetaData(DAL_COLUMN_INFO_MAP, clazz); + } + + static getColumnTypeMap(clazz: EggProtoImplClass): ColumnTypeMap | undefined { + return MetadataUtil.getMetaData(DAL_COLUMN_TYPE_MAP, clazz); + } +} diff --git a/core/dal-decorator/src/util/IndexInfoUtil.ts b/core/dal-decorator/src/util/IndexInfoUtil.ts new file mode 100644 index 00000000..736cd845 --- /dev/null +++ b/core/dal-decorator/src/util/IndexInfoUtil.ts @@ -0,0 +1,15 @@ +import { EggProtoImplClass, MetadataUtil } from '@eggjs/core-decorator'; +import { IndexParams } from '../decorator/Index'; + +export const DAL_INDEX_LIST = Symbol('EggPrototype#dalIndexList'); + +export class IndexInfoUtil { + static addIndex(clazz: EggProtoImplClass, index: IndexParams) { + const indexList: Array = MetadataUtil.initOwnArrayMetaData(DAL_INDEX_LIST, clazz, []); + indexList.push(index); + } + + static getIndexList(clazz: EggProtoImplClass): Array { + return MetadataUtil.getMetaData(DAL_INDEX_LIST, clazz) || []; + } +} diff --git a/core/dal-decorator/src/util/TableInfoUtil.ts b/core/dal-decorator/src/util/TableInfoUtil.ts new file mode 100644 index 00000000..36c29aaa --- /dev/null +++ b/core/dal-decorator/src/util/TableInfoUtil.ts @@ -0,0 +1,31 @@ +import { EggProtoImplClass, MetadataUtil } from '@eggjs/core-decorator'; +import { TableParams } from '../decorator/Table'; + +export const DAL_IS_TABLE = Symbol('EggPrototype#dalIsTable'); +export const DAL_TABLE_PARAMS = Symbol('EggPrototype#dalTableParams'); + +export const TABLE_CLAZZ_LIST: Array = []; + +export class TableInfoUtil { + static setIsTable(clazz: EggProtoImplClass) { + TABLE_CLAZZ_LIST.push(clazz); + MetadataUtil.defineMetaData(DAL_IS_TABLE, true, clazz); + } + + // TODO del + static getClazzList() { + return TABLE_CLAZZ_LIST; + } + + static getIsTable(clazz: EggProtoImplClass) { + return MetadataUtil.getMetaData(DAL_IS_TABLE, clazz) === true; + } + + static setTableParams(clazz: EggProtoImplClass, params: TableParams) { + MetadataUtil.defineMetaData(DAL_TABLE_PARAMS, params, clazz); + } + + static getTableParams(clazz: EggProtoImplClass): TableParams | undefined { + return MetadataUtil.getMetaData(DAL_TABLE_PARAMS, clazz); + } +} diff --git a/core/dal-decorator/test/fixtures/modules/dal/Foo.ts b/core/dal-decorator/test/fixtures/modules/dal/Foo.ts new file mode 100644 index 00000000..0db87f50 --- /dev/null +++ b/core/dal-decorator/test/fixtures/modules/dal/Foo.ts @@ -0,0 +1,23 @@ +import { Table, Index, Column, ColumnType, IndexType } from '../../../..'; + +@Table({ + comment: 'foo table', +}) +@Index({ + keys: [ 'name' ], + type: IndexType.UNIQUE, +}) +export class Foo { + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + }) + id: number; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }) + name: string; +} diff --git a/core/dal-decorator/test/fixtures/modules/dal/package.json b/core/dal-decorator/test/fixtures/modules/dal/package.json new file mode 100644 index 00000000..1769916d --- /dev/null +++ b/core/dal-decorator/test/fixtures/modules/dal/package.json @@ -0,0 +1,6 @@ +{ + "name": "dal", + "eggModule": { + "name": "dal" + } +} diff --git a/core/dal-decorator/test/index.test.ts b/core/dal-decorator/test/index.test.ts new file mode 100644 index 00000000..58feb335 --- /dev/null +++ b/core/dal-decorator/test/index.test.ts @@ -0,0 +1,58 @@ +import assert from 'node:assert'; +import { Foo } from './fixtures/modules/dal/Foo'; +import { ColumnInfoUtil, ColumnType, IndexInfoUtil, IndexType, TableInfoUtil } from '..'; +import { TableModel } from '../src/model/TableModel'; + +describe('test/dal/index.test.ts', () => { + it('decorator should work', () => { + const columnInfoMap = ColumnInfoUtil.getColumnInfoMap(Foo); + const columnTypeMap = ColumnInfoUtil.getColumnTypeMap(Foo); + + const indexList = IndexInfoUtil.getIndexList(Foo); + + const tableInfo = TableInfoUtil.getTableParams(Foo); + const isTable = TableInfoUtil.getIsTable(Foo); + + assert.deepStrictEqual(columnInfoMap, new Map([ + [ + 'id', + { + primaryKey: true, + }, + ], + ])); + assert.deepStrictEqual(columnTypeMap, new Map([ + [ + 'id', + { + type: ColumnType.INT, + }, + ], [ + 'name', + { + type: ColumnType.VARCHAR, + length: 100, + }, + ], + ])); + + assert.deepStrictEqual(indexList, [{ + keys: [ 'name' ], + type: IndexType.UNIQUE, + }]); + + assert.deepStrictEqual(tableInfo, { + comment: 'foo table', + }); + + assert.equal(isTable, true); + }); + + it('model should work', () => { + const table = TableModel.build(Foo); + assert(table); + assert(table.name === 'foos'); + assert.equal(table.columns.length, 2); + assert.equal(table.indices.length, 1); + }); +}); diff --git a/core/dal-decorator/tsconfig.json b/core/dal-decorator/tsconfig.json new file mode 100644 index 00000000..64b22405 --- /dev/null +++ b/core/dal-decorator/tsconfig.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "baseUrl": "./" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/core/dal-decorator/tsconfig.pub.json b/core/dal-decorator/tsconfig.pub.json new file mode 100644 index 00000000..64b22405 --- /dev/null +++ b/core/dal-decorator/tsconfig.pub.json @@ -0,0 +1,12 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "baseUrl": "./" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/core/dal-runtime/README.md b/core/dal-runtime/README.md new file mode 100644 index 00000000..a0b7fdf3 --- /dev/null +++ b/core/dal-runtime/README.md @@ -0,0 +1,5 @@ +# `@eggjs/dal-runtime` + +## Usage + +Please read [@eggjs/tegg-dal-plugin](../../plugin/dal-plugin) diff --git a/core/dal-runtime/index.ts b/core/dal-runtime/index.ts new file mode 100644 index 00000000..e7061137 --- /dev/null +++ b/core/dal-runtime/index.ts @@ -0,0 +1,8 @@ +export * from './src/SqlGenerator'; +export * from './src/CodeGenerator'; +export { TableSqlMap } from './src/TableSqlMap'; +export * from './src/SqlMapLoader'; + +export * from './src/DataSource'; +export * from './src/MySqlDataSource'; +export * from './src/TableModelInstanceBuilder'; diff --git a/core/dal-runtime/package.json b/core/dal-runtime/package.json new file mode 100644 index 00000000..13a2e947 --- /dev/null +++ b/core/dal-runtime/package.json @@ -0,0 +1,62 @@ +{ + "name": "@eggjs/dal-runtime", + "version": "3.32.0", + "description": "tegg dal decorator", + "keywords": [ + "egg", + "typescript", + "decorator", + "tegg", + "dal" + ], + "main": "dist/index.js", + "files": [ + "dist/**/*.js", + "dist/**/*.d.ts" + ], + "typings": "dist/index.d.ts", + "scripts": { + "test": "cross-env NODE_ENV=test NODE_OPTIONS='--no-deprecation' mocha", + "clean": "tsc -b --clean", + "tsc": "npm run clean && tsc -p ./tsconfig.json", + "tsc:pub": "npm run clean && tsc -p ./tsconfig.pub.json", + "prepublishOnly": "npm run tsc:pub" + }, + "author": "killagu ", + "license": "MIT", + "homepage": "https://github.com/eggjs/tegg", + "bugs": { + "url": "https://github.com/eggjs/tegg/issues" + }, + "repository": { + "type": "git", + "url": "git@github.com:eggjs/tegg.git", + "directory": "core/dal-decorator" + }, + "engines": { + "node": ">=14.0.0" + }, + "dependencies": { + "@eggjs/dal-decorator": "^3.32.0", + "@eggjs/rds": "^1.0.0", + "nunjucks": "^3.2.4", + "lodash": "^4.17.21", + "js-beautify": "^1.15.1", + "sdk-base": "^4.2.1", + "sqlstring": "^2.3.3" + }, + "publishConfig": { + "access": "public" + }, + "devDependencies": { + "@eggjs/tegg": "^3.32.0", + "@types/lodash": "^4.17.0", + "@types/nunjucks": "^3.2.6", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.4", + "cross-env": "^7.0.3", + "mocha": "^10.2.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + } +} diff --git a/core/dal-runtime/src/BaseSqlMap.ts b/core/dal-runtime/src/BaseSqlMap.ts new file mode 100644 index 00000000..b97f56a4 --- /dev/null +++ b/core/dal-runtime/src/BaseSqlMap.ts @@ -0,0 +1,337 @@ +import _ from 'lodash'; +import { ColumnType, IndexType, SqlType, SqlMap, TableModel } from '@eggjs/dal-decorator'; +import { type EggLogger } from 'egg'; +import { TemplateUtil } from './TemplateUtil'; + +export interface GenerateSqlMap { + name: string; + type: SqlType.DELETE | SqlType.UPDATE | SqlType.INSERT | SqlType.SELECT; + sql: string; +} + +export class BaseSqlMapGenerator { + private readonly tableModel: TableModel; + private readonly logger: EggLogger; + + constructor(tableModel: TableModel, logger: EggLogger) { + this.tableModel = tableModel; + this.logger = logger; + } + + generateAllColumns(countIf: boolean): string { + const str = this.tableModel.columns.map(t => `\`${t.columnName}\``) + .join(','); + return countIf ? `{% if $$count == true %}0{% else %}${str}{% endif %}` : str; + } + + generateFindByPrimary(): Array { + const result: Array = []; + const primary = this.tableModel.getPrimary(); + if (!primary) { + this.logger.warn(`表 \`${this.tableModel.name}\` 没有主键,无法生成主键查询语句。`); + return result; + } + + let sql = `SELECT ${this.generateAllColumns(true)} + FROM \`${this.tableModel.name}\` + WHERE `; + + sql += primary.keys.map(indexKey => `\`${indexKey.columnName}\` = {{$${indexKey.propertyName}}}`) + .join(' AND '); + if (primary.keys.length === 1) { + result.push({ + type: SqlType.SELECT, + name: `findBy${_.upperFirst(primary.keys[0].propertyName)}`, + sql, + }); + } + result.push({ + name: 'findByPrimary', + type: SqlType.SELECT, + sql, + }); + return result; + } + + // TODO index 的左匹配 + generateFindByIndexes() { + const sqlMaps: Array = []; + for (const index of this.tableModel.indices) { + if (index.type === IndexType.PRIMARY) continue; + + let sql = `SELECT ${this.generateAllColumns(true)} + FROM \`${this.tableModel.name}\` + WHERE `; + + sql += index.keys.map(indexKey => { + const s = `\`${indexKey.columnName}\` {{ "IS" if $${indexKey.propertyName} == null else "=" }} {{$${indexKey.propertyName}}}`; + return s; + }) + .join(' AND '); + + const tempName = _.upperFirst(_.camelCase(index.keys.length === 1 ? index.keys[0].propertyName : index.name)); + sqlMaps.push({ + name: `findBy${tempName}`, + type: SqlType.SELECT, + sql, + }); + sqlMaps.push({ + name: `findOneBy${tempName}`, + type: SqlType.SELECT, + sql: `${sql} LIMIT 0, 1`, + }); + } + return sqlMaps; + } + + generateInsert() { + let sql = `INSERT INTO \`${this.tableModel.name}\` `; + sql += '{% set ___first = true %}'; + + const keys: string[] = []; + const values: string[] = []; + for (const column of this.tableModel.columns) { + const { propertyName, columnName, type } = column; + if (column.propertyName !== 'gmtCreate' && column.propertyName !== 'gmtModified') { + // Add filter for Spatial Type + // - toPoint + // - toLine + // - toPolygon + // - toGeometry + // - toMultiPoint + // - toMultiLine + // - toMultiPolygon + // - toGeometryCollection + keys.push((` + {% if $${propertyName} !== undefined %} + {% if ___first %} + {% set ___first = false %} + {% else %} + , + {% endif %} + + \`${columnName}\` + {% endif %} + `).trim()); + + if (TemplateUtil.isSpatialType(column)) { + const filter = TemplateUtil.getSpatialFilter(column.type.type); + values.push((` + {% if $${propertyName} !== undefined %} + {% if ___first %} + {% set ___first = false %} + {% else %} + , + {% endif %} + + {{$${propertyName} | ${filter}}} + {% endif %} + `).trim()); + } else if (column.type.type === ColumnType.JSON) { + values.push((` + {% if $${propertyName} !== undefined %} + {% if ___first %} + {% set ___first = false %} + {% else %} + , + {% endif %} + + {{$${propertyName} | toJson}} + {% endif %} + `).trim()); + } else { + values.push((` + {% if $${propertyName} !== undefined %} + {% if ___first %} + {% set ___first = false %} + {% else %} + , + {% endif %} + + {{$${propertyName}}} + {% endif %} + `).trim()); + } + + + } else { + let now; + // Default value for gmtCreate/gmtModified + // int:UNIX_TEIMESTAMP + // bigint: ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000) + // datetime/timestamp Now() + if (type.type === ColumnType.INT) { + // 秒级时间戳 + now = 'UNIX_TIMESTAMP()'; + } else if (type.type === ColumnType.BIGINT) { + // 毫秒级时间戳 + now = 'ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000)'; + } else if (type.type === ColumnType.DATETIME || type.type === ColumnType.TIMESTAMP) { + now = type.precision ? `NOW(${type.precision})` : 'NOW()'; + } else { + this.logger.warn(`unknown type ${type.type} for ${propertyName}`); + } + keys.push((` + {% if ___first %} + {% set ___first = false %} + {% else %} + , + {% endif %} + + \`${columnName}\` + `).trim()); + + values.push((` + {% if ___first %} + {% set ___first = false %} + {% else %} + , + {% endif %} + + {{ $${propertyName} if $${propertyName} !== undefined else '${now}' }} + `).trim()); + } + } + + sql += `(${keys.join('')})`; + sql += '{% set ___first = true %}'; + sql += `VALUES(${values.join('')});`; + + return sql; + } + + generateUpdate() { + const primary = this.tableModel.getPrimary(); + if (!primary) { + this.logger.warn(`表 \`${this.tableModel.name}\` 没有主键,无法生成主键更新语句。`); + return; + } + + let sql = `UPDATE \`${this.tableModel.name}\` SET`; + sql += '{% set ___first = true %}'; + const kv: string[] = []; + for (const column of this.tableModel.columns) { + const { type, propertyName, columnName } = column; + let now; + if (type.type === ColumnType.INT) { + // 秒级时间戳 + now = 'UNIX_TIMESTAMP()'; + } else if (type.type === ColumnType.BIGINT) { + // 毫秒级时间戳 + now = 'ROUND(UNIX_TIMESTAMP(CURTIME(4)) * 1000)'; + } else if (type.type === ColumnType.TIMESTAMP || type.type === ColumnType.DATETIME) { + now = type.precision ? `NOW(${type.precision})` : 'NOW()'; + } + + // 若无更新时间字段,则自动更新该字段 + const temp = propertyName !== 'gmtModified' ? + + ` + {% if $${propertyName} !== undefined %} + {% if ___first %} + {% set ___first = false %} + {% else %} + , + {% endif %} + + \`${columnName}\` = {{$${propertyName}}} + {% endif %} + ` : + + ` + {% if ___first %} + {% set ___first = false %} + {% else %} + , + {% endif %} + + \`${columnName}\` = + {{ $${propertyName} if $${propertyName} !== undefined else '${now}' }} + `; + kv.push(temp); + } + + sql += kv.join(''); + sql += `WHERE ${primary.keys.map(indexKey => `\`${indexKey.columnName}\` = {{primary.${indexKey.propertyName}}}`) + .join(' AND ')}`; + + return sql; + } + + generateDelete() { + const primary = this.tableModel.getPrimary(); + if (!primary) { + this.logger.warn(`表 \`${this.tableModel.name}\` 没有主键,无法生成主键删除语句。`); + return; + } + + let sql = `DELETE + FROM \`${this.tableModel.name}\` + WHERE `; + + sql += primary.keys.map(indexKey => `\`${indexKey.columnName}\` = {{${indexKey.propertyName}}}`) + .join(' AND '); + + return sql; + } + + load(): Record { + const map: Record = {}; + + map.allColumns = { + type: SqlType.BLOCK, + content: this.generateAllColumns(false), + }; + + + const sqlMaps: Array = [ + /** + * 以主键进行索引 + * + * + `findByPrimary` + * + 若为单主键,则再加 `findBy键名` + */ + ...this.generateFindByPrimary(), + /** + * findBy 各索引 + * + * + 若为多列索引,则为 `findBy索引名` + * + 若为单列索引,则为 `findBy列名` + */ + ...this.generateFindByIndexes(), + /** + * 插入 + */ + { + name: 'insert', + type: SqlType.INSERT, + sql: this.generateInsert(), + } as GenerateSqlMap, + /** + * 主键更新 + */ + { + name: 'update', + type: SqlType.UPDATE, + sql: this.generateUpdate(), + } as GenerateSqlMap, + /** + * 主键删除 + */ + { + name: 'delete', + type: SqlType.DELETE, + sql: this.generateDelete(), + } as GenerateSqlMap, + ]; + for (const sqlMap of sqlMaps) { + map[sqlMap.name] = { + type: sqlMap.type, + sql: sqlMap.sql, + }; + } + + return map; + } +} + diff --git a/core/dal-runtime/src/CodeGenerator.ts b/core/dal-runtime/src/CodeGenerator.ts new file mode 100644 index 00000000..58c5139a --- /dev/null +++ b/core/dal-runtime/src/CodeGenerator.ts @@ -0,0 +1,136 @@ +import nunjucks, { type Environment } from 'nunjucks'; +import path from 'node:path'; +import _ from 'lodash'; +import { TemplateUtil } from './TemplateUtil'; +import { ColumnModel, TableModel } from '@eggjs/dal-decorator'; +import { PrototypeUtil } from '@eggjs/core-decorator'; +import fs from 'node:fs/promises'; +import { js_beautify } from 'js-beautify'; +import { SqlGenerator } from './SqlGenerator'; + +export interface CodeGeneratorOptions { + moduleDir: string; + moduleName: string; +} + +export enum Templates { + BASE_DAO = 'base_dao', + DAO = 'dao', + EXTENSION = 'extension', +} + +export class CodeGenerator { + private readonly moduleDir: string; + private readonly moduleName: string; + + constructor(options: CodeGeneratorOptions) { + this.moduleDir = options.moduleDir; + this.moduleName = options.moduleName; + this.createNunjucksEnv(); + } + + private njkEnv: Environment; + + createNunjucksEnv() { + this.njkEnv = nunjucks.configure(path.join(__dirname, './templates'), { + autoescape: false, + }); + this.njkEnv.addFilter('pascalCase', name => _.upperFirst(_.camelCase(name))); + this.njkEnv.addFilter('camelCase', name => _.camelCase(name)); + this.njkEnv.addFilter('dbTypeToTSType', TemplateUtil.dbTypeToTsType); + } + + genCode(tplName: Templates, filePath: string, tableModel: TableModel) { + let tableModelAbsolutePath = PrototypeUtil.getFilePath(tableModel.clazz)!; + tableModelAbsolutePath = tableModelAbsolutePath.substring(0, tableModelAbsolutePath.length - 3); + const data = { + table: tableModel, + file: filePath, + fileName: path.basename(filePath), + clazzName: tableModel.clazz.name, + moduleName: this.moduleName, + id: tableModel.columns.find(t => t.propertyName === 'id'), + primaryIndex: tableModel.getPrimary(), + tableModelPath: TemplateUtil.importPath(tableModelAbsolutePath, path.dirname(filePath)), + columnMap: tableModel.columns.reduce>((p, c) => { + p[c.propertyName] = c; + return p; + }, {}), + }; + return this.njkEnv.render(`${tplName}.njk`, data); + } + + async generate(tableModel: TableModel) { + const dalDir = path.join(this.moduleDir, 'dal'); + + // const tableName = tableModel.name; + // const clazzName = tableModel.clazz.name; + const clazzFileName = path.basename(PrototypeUtil.getFilePath(tableModel.clazz)!); + const baseFileName = path.basename(clazzFileName, '.ts'); + + // 要动的一些文件 + const paths = { + // e.g. app/dal/dao/base/example.ts + baseBizDAO: path.join(dalDir, `dao/base/Base${baseFileName}DAO.ts`), + // e.g. app/dal/dao/example.ts + bizDAO: path.join(dalDir, `dao/${baseFileName}DAO.ts`), + // e.g. app/dal/extension/example.ts + extension: path.join(dalDir, `extension/${baseFileName}Extension.ts`), + // e.g. app/dal/structure/example.json + structure: path.join(dalDir, `structure/${baseFileName}.json`), + // e.g. app/dal/structure/example.sql + structureSql: path.join(dalDir, `structure/${baseFileName}.sql`), + }; + + // 建立 structure 文件 + await fs.mkdir(path.dirname(paths.structure), { + recursive: true, + }); + await fs.writeFile(paths.structure, JSON.stringify(tableModel, null, 2), 'utf8'); + + const sqlGenerator = new SqlGenerator(); + const structureSql = sqlGenerator.generate(tableModel); + await fs.writeFile(paths.structureSql, structureSql, 'utf8'); + + + const codes = [{ + templates: Templates.BASE_DAO, + filePath: paths.baseBizDAO, + beautify: true, + }, { + templates: Templates.DAO, + filePath: paths.bizDAO, + beautify: true, + }, { + templates: Templates.EXTENSION, + filePath: paths.extension, + beautify: false, + }]; + for (const { templates, filePath, beautify } of codes) { + await fs.mkdir(path.dirname(filePath), { + recursive: true, + }); + const code = this.genCode(templates, filePath, tableModel); + let beautified: string; + if (beautify) { + beautified = js_beautify(code, { + brace_style: 'preserve-inline', + indent_size: 2, + jslint_happy: true, + preserve_newlines: false, + }); + } else { + beautified = code; + } + beautified = beautified + .replace(/( )*\/\/ empty-line( )*/g, '') + .replace(/Promise( )*<( )*(.+?)( )*>/g, 'Promise<$3>') + .replace(/Optional( )*<( )*(.+?)( )*>/g, 'Optional<$3>') + .replace(/Record( )*<( )*(.+?)( )*>/g, 'Record<$3>') + .replace(/Partial( )*<( )*(.+?)( )*>/g, 'Partial<$3>') + .replace(/DataSource( )*<( )*(.+?)( )*>/g, 'DataSource<$3>') + .replace(/ \? :/g, '?:'); + await fs.writeFile(filePath, beautified, 'utf8'); + } + } +} diff --git a/core/dal-runtime/src/DataSource.ts b/core/dal-runtime/src/DataSource.ts new file mode 100644 index 00000000..a440d4b0 --- /dev/null +++ b/core/dal-runtime/src/DataSource.ts @@ -0,0 +1,92 @@ +import { DataSource as IDataSource, PaginateData, SqlType, TableModel } from '@eggjs/dal-decorator'; +import { MysqlDataSource } from './MySqlDataSource'; +import { TableSqlMap } from './TableSqlMap'; +import { TableModelInstanceBuilder } from './TableModelInstanceBuilder'; + +export interface ExecuteSql { + sql: string; + template: string; + sqlType: SqlType; +} + +const PAGINATE_COUNT_WRAPPER = [ 'SELECT COUNT(0) as count FROM (', ') AS T' ]; + +export class DataSource implements IDataSource { + private readonly tableModel: TableModel; + private readonly mysqlDataSource: MysqlDataSource; + private readonly sqlMap: TableSqlMap; + + constructor(tableModel: TableModel, mysqlDataSource: MysqlDataSource, sqlMap: TableSqlMap) { + this.tableModel = tableModel; + this.mysqlDataSource = mysqlDataSource; + this.sqlMap = sqlMap; + } + + private generateSql(sqlName: string, data: object): ExecuteSql { + const sql = this.sqlMap.generate(sqlName, data, this.mysqlDataSource.timezone!); + const sqlType = this.sqlMap.getType(sqlName); + const template = this.sqlMap.getTemplateString(sqlName); + return { + sql, + sqlType, + template, + }; + } + + async count(sqlName: string, data?: any): Promise { + const newData = Object.assign({ $$count: true }, data); + const executeSql = this.generateSql(sqlName, newData); + return await this.#paginateCount(executeSql.sql); + } + + async execute(sqlName: string, data?: any): Promise> { + const executeSql = this.generateSql(sqlName, data); + const rows = await this.mysqlDataSource.query(executeSql.sql); + return rows.map(t => { + return TableModelInstanceBuilder.buildInstance(this.tableModel, t); + }); + } + + async executeRaw(sqlName: string, data?: any): Promise> { + const executeSql = this.generateSql(sqlName, data); + return await this.mysqlDataSource.query(executeSql.sql); + } + + async executeScalar(sqlName: string, data?: any): Promise { + const ret = await this.execute(sqlName, data); + if (!Array.isArray(ret)) return ret || null; + return ret[0] || null; + } + + async executeRawScalar(sqlName: string, data?: any): Promise { + const ret = await this.executeRaw(sqlName, data); + if (!Array.isArray(ret)) return (ret || null) as any; + return ret[0] || null; + } + + async paginate(sqlName: string, data: any, currentPage: number, perPageCount: number): Promise> { + const limit = `LIMIT ${(currentPage - 1) * perPageCount}, ${perPageCount}`; + const sql = this.generateSql(sqlName, data).sql + ' ' + limit; + const countSql = this.generateSql(sqlName, Object.assign({ $$count: true }, data)).sql; + + + const ret = await Promise.all([ + this.mysqlDataSource.query(sql), + this.#paginateCount(countSql), + ]); + + return { + total: ret[1], + pageNum: currentPage, + rows: ret[0].map(t => TableModelInstanceBuilder.buildInstance(this.tableModel, t)), + }; + } + + async #paginateCount(baseSQL: string): Promise { + const sql = `${PAGINATE_COUNT_WRAPPER[0]}${baseSQL}${PAGINATE_COUNT_WRAPPER[1]}`; + + const result = await this.mysqlDataSource.query(sql); + + return result[0].count; + } +} diff --git a/core/dal-runtime/src/MySqlDataSource.ts b/core/dal-runtime/src/MySqlDataSource.ts new file mode 100644 index 00000000..25b1738e --- /dev/null +++ b/core/dal-runtime/src/MySqlDataSource.ts @@ -0,0 +1,42 @@ +import { RDSClient } from '@eggjs/rds'; +// TODO fix export +import type { RDSClientOptions } from '@eggjs/rds/lib/types'; +import Base from 'sdk-base'; + +export interface DataSourceOptions extends RDSClientOptions { + name: string; + // default is select 1 + 1; + initSql?: string; +} + +const DEFAULT_OPTIONS: RDSClientOptions = { + supportBigNumbers: true, + bigNumberStrings: true, + trace: true, +}; + +export class MysqlDataSource extends Base { + private readonly client: RDSClient; + private readonly initSql: string; + readonly name: string; + readonly timezone?: string; + + constructor(options: DataSourceOptions) { + super({ initMethod: '_init' }); + const { name, initSql, ...mysqlOptions } = options; + this.client = new RDSClient(Object.assign({}, DEFAULT_OPTIONS, mysqlOptions)); + this.initSql = initSql ?? 'SELECT 1 + 1'; + this.name = name; + this.timezone = options.timezone; + } + + protected async _init() { + if (this.initSql) { + await this.client.query(this.initSql); + } + } + + async query(sql: string): Promise { + return this.client.query(sql); + } +} diff --git a/core/dal-runtime/src/NunjucksConverter.ts b/core/dal-runtime/src/NunjucksConverter.ts new file mode 100644 index 00000000..e4ee33c8 --- /dev/null +++ b/core/dal-runtime/src/NunjucksConverter.ts @@ -0,0 +1,112 @@ +export class NunjucksConverter { + /** + * 将变量 HTML 转义的逻辑改为 MySQL 防注入转义 + * + * eg: + * + * output += runtime.suppressValue(runtime.contextOrFrameLookup(context, frame, "allColumns") + * + * 转换为 + * + * output += runtime.escapeSQL.call(this, "allColumns", runtime.contextOrFrameLookup(context, frame, "allColumns") + * + * @param {String} code 转换前的代码 + * @return {String} 转换后的代码 + */ + static convertNormalVariableCode(code: string) { + return code.replace( + /\Woutput\W*?\+=\W*?runtime\.suppressValue\(runtime\.contextOrFrameLookup\((.+?),(.*?),\W*?"(.+?)"\)/g, + '\noutput += runtime.escapeSQL.call(this, "$3", runtime.contextOrFrameLookup($1, $2, "$3")'); + } + + /** + * 三目运算的 MySQL 防注入转义 + * + * eg: + * + * output += runtime.suppressValue((runtime.contextOrFrameLookup(context, frame, "$gmtCreate") !== \ + * runtime.contextOrFrameLookup(context, frame, "undefined")?runtime.contextOrFrameLookup(context,\ + * frame, "$gmtCreate"):"NOW()"), env.opts.autoescape); + * + * 转换为 + * + * output += runtime.suppressValue((runtime.contextOrFrameLookup(...) != ...) ? + * runtime.escapeSQL.call(this, "...", runtime.contextOrFrameLookup(...)) : + * ...) + * + * @param {String} code 转换前的代码 + * @return {String} 转换后的代码 + */ + static convertTernaryCode(code: string) { + // 先找到所有的 runtime.suppressValue((...?...:...), env...) + const ternaryBefore = code.match( + /\Woutput\W*?\+=\W*?runtime\.suppressValue\(\(.*\W*?\?\W*?.*?:.*\),\W*?env\.opts\.autoescape/g) || []; + + // 进行逐一处理 + const ternaryAfter = ternaryBefore.map(str => { + return str.replace( + /([\?:])runtime\.contextOrFrameLookup\((.+?),(.*?),\W*?"(.+?)"\)/g, + '$1runtime.escapeSQL.call(this, "$4", runtime.contextOrFrameLookup($2, $3, "$4"))', + ) + .replace( + /env.opts.autoescape$/g, + 'false', + ); + }); + + // 统一替换 + for (let i = 0; i < ternaryBefore.length; i++) { + code = code.replace(ternaryBefore[i], ternaryAfter[i]); + } + + return code; + } + + /** + * 对象的属性,如 `user.id` 防注入转义 + * + * eg: + * output += runtime.suppressValue(runtime.memberLookup(\ + * (runtime.contextOrFrameLookup(context, frame, "user")),"id"), env.opts.autoescape); + * + * 转换为 + * + * output += runtime.escapeSQL.call(this, "<...>", runtime.memberLookup(...), env.opts.autoescape); + * + * 由于 escapeSQL 中是根据 key 与预定义 block 匹配决定是否转义,而 memberLookup 的状态下总的 key 肯定不会匹配, + * 所以找一个绝对不会匹配的 "<...>" 传入。事实上它可以是任意一个不会被匹配的字符串,比如说 ">_<" 等。 + * + * @param {String} code 转换前的代码 + * @return {String} 转换后的代码 + */ + static convertNestedObjectCode(code: string) { + return code.replace( + /\Woutput\W*?\+=\W*?runtime\.suppressValue\(runtime\.memberLookup\((.+?)\), env\.opts\.autoescape\)/g, + '\noutput += runtime.escapeSQL.call(this, "<...>", runtime.memberLookup($1), env.opts.autoescape)'); + } + + /** + * For 中的 `t_xxx` 要被转义: + * + * eg: + * frame.set("...", t_...); + * ... + * output += runtime.suppressValue(t_.., env.opts.autoscape); + * + * 转换为 + * + * output += runtime.escapeSQL.call(this, "for.t_...", t_..., env.opts.autoescape); + * + * 由于 escapeSQL 中是根据 key 与预定义 block 匹配决定是否转义,而 memberLookup 的状态下总的 key 肯定不会匹配, + * 所以找一个绝对不会匹配的 "for.t_..." 传入。事实上它可以是任意一个不会被匹配的字符串,比如说 ">_<" 等。 + * + * @param {String} code 转换前的代码 + * @return {String} 转换后的代码 + */ + static convertValueInsideFor(code: string) { + return code.replace( + /\Woutput\W*?\+=\W*?runtime\.suppressValue\((t_\d+), env\.opts\.autoescape\)/g, + '\noutput += runtime.escapeSQL.call(this, "for.$1", $1, env.opts.autoescape)'); + } + +} diff --git a/core/dal-runtime/src/NunjucksUtil.ts b/core/dal-runtime/src/NunjucksUtil.ts new file mode 100644 index 00000000..78f29092 --- /dev/null +++ b/core/dal-runtime/src/NunjucksUtil.ts @@ -0,0 +1,90 @@ +import nunjucks, { Template, type Environment } from 'nunjucks'; +import sqlstring from 'sqlstring'; +import { NunjucksConverter } from './NunjucksConverter'; +import { SqlUtil } from './SqlUtil'; + +const compiler = (nunjucks as any).compiler; +const envs: Record = {}; + +const ROOT_RENDER_FUNC = Symbol('rootRenderFunc'); +const RUNTIME = Object.assign({}, nunjucks.runtime, { + escapeSQL: function escapeSQL(this: any, key, value) { + // 如果是预定义 block 则不转义 + if (this.env.globals[key]) return value; + return sqlstring.escape(value, true, this.env.timezone); + }, +}); + +function _replaceCodeWithSQLFeature(source) { + const funcs = [ + 'convertNormalVariableCode', // 普通变量 + 'convertTernaryCode', // 三目运算 + 'convertNestedObjectCode', // 对象中的变量,如 `user.id` + 'convertValueInsideFor', // for 中的值需要转义 + ]; + + return funcs.reduce((source, func) => NunjucksConverter[func](source), source); +} + +/** + * compile the string into function + * @see https://github.com/mozilla/nunjucks/blob/2fd547f/src/environment.js#L571-L592 + */ +function _compile(this: any) { + let source = compiler.compile( + this.tmplStr, + this.env.asyncFilters, + this.env.extensionsList, + this.path, + this.env.opts); + + /** + * 将一些 Nunjucks 的 HTML 转义的代码转换成 SQL 防注入的代码 + */ + source = _replaceCodeWithSQLFeature(source); + + // eslint-disable-next-line + const props = (new Function(source))(); + + this.blocks = this._getBlocks(props); + this[ROOT_RENDER_FUNC] = props.root; + this.rootRenderFunc = function(env, context, frame, _runtime, cb) { + /** + * 1. 将 runtime 遗弃,用新的 + * 2. 移除 SQL 语句中多余空白符 + */ + return this[ROOT_RENDER_FUNC](env, context, frame, RUNTIME, function(err, ret) { + // istanbul ignore if + if (err) return cb(err, ret); + return cb(err, SqlUtil.minify(ret || '')); + }); + }; + this.compiled = true; +} + +export class NunjucksUtils { + static createEnv(modelName: string) { + if (envs[modelName]) return envs[modelName]; + + const env = envs[modelName] = nunjucks.configure({ + autoescape: false, + }); + + return env; + } + + static compile(modelName: string, sqlName: string, sql: string) { + // istanbul ignore if + if (!envs[modelName]) { + throw new Error(`you should create an Environment for ${modelName} first.`); + } + + const template = new Template(sql, envs[modelName], `egg-dal:MySQL:${modelName}:${sqlName}`, false); + + // 做一些 hack,使得支持 MySQL 的一些 Escape + (template as any)._compile = _compile; + (template as any).compile(); + + return template; + } +} diff --git a/core/dal-runtime/src/SqlGenerator.ts b/core/dal-runtime/src/SqlGenerator.ts new file mode 100644 index 00000000..9be0a844 --- /dev/null +++ b/core/dal-runtime/src/SqlGenerator.ts @@ -0,0 +1,380 @@ +import { + BaseSpatialParams, + ColumnModel, + ColumnType, + ColumnTypeParams, + IndexModel, IndexType, + TableModel, +} from '@eggjs/dal-decorator'; + +// TODO diff 实现 +export class SqlGenerator { + private formatComment(comment: string) { + return comment.replace(/\n/g, '\\n'); + } + + private generateColumn(column: ColumnModel) { + const sqls: string[] = [ + ' ', + column.columnName, + this.generateColumnType(column.type), + ]; + if (column.canNull) { + sqls.push('NULL'); + } else { + sqls.push('NOT NULL'); + } + if (([ + ColumnType.POINT, + ColumnType.GEOMETRY, + ColumnType.POINT, + ColumnType.LINESTRING, + ColumnType.POLYGON, + ColumnType.MULTIPOINT, + ColumnType.MULTILINESTRING, + ColumnType.MULTIPOLYGON, + ColumnType.GEOMETRYCOLLECTION, + ] as ColumnType[]).includes(column.type.type)) { + const SRID = (column.type as BaseSpatialParams).SRID; + if (SRID) { + sqls.push(`SRID ${SRID}`); + } + } + if (typeof column.default !== 'undefined') { + sqls.push(`DEFAULT ${column.default}`); + } + if (column.autoIncrement) { + sqls.push('AUTO_INCREMENT'); + } + if (column.uniqueKey) { + sqls.push('UNIQUE KEY'); + } + if (column.primaryKey) { + sqls.push('PRIMARY KEY'); + } + if (column.comment) { + sqls.push(`COMMENT '${this.formatComment(column.comment)}'`); + } + if (column.collate) { + sqls.push(`COLLATE ${column.collate}`); + } + if (column.columnFormat) { + sqls.push(`COLUMN_FORMAT ${column.columnFormat}`); + } + if (column.engineAttribute) { + sqls.push(`ENGINE_ATTRIBUTE='${column.engineAttribute}'`); + } + if (column.secondaryEngineAttribute) { + sqls.push(`SECONDARY_ENGINE_ATTRIBUTE='${column.secondaryEngineAttribute}'`); + } + return sqls.join(' '); + } + + private generateColumnType(columnType: ColumnTypeParams) { + const sqls: string[] = []; + switch (columnType.type) { + case ColumnType.BOOL: { + sqls.push('BOOL'); + break; + } + case ColumnType.BIT: { + if (columnType.length) { + sqls.push(`BIT(${columnType.length})`); + } else { + sqls.push('BIT'); + } + break; + } + case ColumnType.TINYINT: + case ColumnType.SMALLINT: + case ColumnType.MEDIUMINT: + case ColumnType.INT: + case ColumnType.BIGINT: { + if (typeof columnType.length === 'number') { + sqls.push(`${columnType.type}(${columnType.length})`); + } else { + sqls.push(columnType.type); + } + if (columnType.unsigned) { + sqls.push('UNSIGNED'); + } + if (columnType.zeroFill) { + sqls.push('ZEROFILL'); + } + break; + } + case ColumnType.DECIMAL: + case ColumnType.FLOAT: + case ColumnType.DOUBLE: { + if (typeof columnType.length === 'number' && typeof columnType.fractionalLength === 'number') { + sqls.push(`${columnType.type}(${columnType.length},${columnType.fractionalLength})`); + } else if (typeof columnType.length === 'number') { + sqls.push(`${columnType.type}(${columnType.length})`); + } else { + sqls.push('TINYINT'); + } + if (columnType.unsigned) { + sqls.push('UNSIGNED'); + } + if (columnType.zeroFill) { + sqls.push('ZEROFILL'); + } + break; + } + case ColumnType.DATE: { + sqls.push('DATE'); + break; + } + case ColumnType.DATETIME: + case ColumnType.TIMESTAMP: + case ColumnType.TIME: { + if (columnType.precision) { + sqls.push(`${columnType.type}(${columnType.precision})`); + } else { + sqls.push(columnType.type); + } + break; + } + case ColumnType.YEAR: { + sqls.push('YEAR'); + break; + } + case ColumnType.CHAR: + case ColumnType.TEXT: { + if (columnType.length) { + sqls.push(`${columnType.type}(${columnType.length})`); + } else { + sqls.push(columnType.type); + } + if (columnType.characterSet) { + sqls.push(`CHARACTER SET ${columnType.characterSet}`); + } + if (columnType.collate) { + sqls.push(`COLLATE ${columnType.collate}`); + } + break; + } + case ColumnType.VARCHAR: { + sqls.push(`${columnType.type}(${columnType.length})`); + if (columnType.characterSet) { + sqls.push(`CHARACTER SET ${columnType.characterSet}`); + } + if (columnType.collate) { + sqls.push(`COLLATE ${columnType.collate}`); + } + break; + } + case ColumnType.BINARY: { + if (columnType.length) { + sqls.push(`${columnType.type}(${columnType.length})`); + } else { + sqls.push(columnType.type); + } + break; + } + case ColumnType.VARBINARY: { + sqls.push(`${columnType.type}(${columnType.length})`); + break; + } + case ColumnType.TINYBLOB: { + sqls.push('TINYBLOB'); + break; + } + case ColumnType.TINYTEXT: + case ColumnType.MEDIUMTEXT: + case ColumnType.LONGTEXT: { + sqls.push(columnType.type); + if (columnType.characterSet) { + sqls.push(`CHARACTER SET ${columnType.characterSet}`); + } + if (columnType.collate) { + sqls.push(`COLLATE ${columnType.collate}`); + } + break; + } + case ColumnType.BLOB: { + if (columnType.length) { + sqls.push(`${columnType.type}(${columnType.length})`); + } else { + sqls.push(columnType.type); + } + break; + } + case ColumnType.MEDIUMBLOB: { + sqls.push('MEDIUMBLOB'); + break; + } + case ColumnType.LONGBLOB: { + sqls.push('LONGBLOB'); + break; + } + case ColumnType.ENUM: { + const enumValue: string = columnType.enums.map(t => `'${t}'`).join(','); + sqls.push(`ENUM(${enumValue})`); + if (columnType.characterSet) { + sqls.push(`CHARACTER SET ${columnType.characterSet}`); + } + if (columnType.collate) { + sqls.push(`COLLATE ${columnType.collate}`); + } + break; + } + case ColumnType.SET: { + const enumValue: string = columnType.enums.map(t => `'${t}'`).join(','); + sqls.push(`SET(${enumValue})`); + if (columnType.characterSet) { + sqls.push(`CHARACTER SET ${columnType.characterSet}`); + } + if (columnType.collate) { + sqls.push(`COLLATE ${columnType.collate}`); + } + break; + } + case ColumnType.JSON: { + sqls.push('JSON'); + break; + } + case ColumnType.GEOMETRY: + case ColumnType.POINT: + case ColumnType.LINESTRING: + case ColumnType.POLYGON: + case ColumnType.MULTIPOINT: + case ColumnType.MULTILINESTRING: + case ColumnType.MULTIPOLYGON: + case ColumnType.GEOMETRYCOLLECTION: { + sqls.push(columnType.type); + break; + } + default: { + throw new Error(`unknown ColumnType ${columnType}`); + } + } + return sqls.join(' '); + } + + private generateIndex(indexModel: IndexModel) { + const indexSql: string[] = [ + ' ', + ]; + switch (indexModel.type) { + case IndexType.INDEX: { + indexSql.push('KEY'); + break; + } + case IndexType.UNIQUE: { + indexSql.push('UNIQUE KEY'); + break; + } + case IndexType.PRIMARY: { + indexSql.push('PRIMARY KEY'); + break; + } + case IndexType.FULLTEXT: { + indexSql.push('FULLTEXT KEY'); + break; + } + case IndexType.SPATIAL: { + indexSql.push('SPATIAL KEY'); + break; + } + default: { + throw new Error(`unknown IndexType ${indexModel.type}`); + } + } + indexSql.push(indexModel.name); + indexSql.push(`(${indexModel.keys.map(t => t.columnName).join(',')})`); + if (indexModel.storeType) { + indexSql.push(`USING ${indexModel.storeType}`); + } + if (indexModel.parser) { + indexSql.push(`WITH PARSER ${indexModel.parser}`); + } + if (indexModel.comment) { + indexSql.push(`COMMENT '${this.formatComment(indexModel.comment)}'`); + } + if (indexModel.engineAttribute) { + indexSql.push(`ENGINE_ATTRIBUTE='${indexModel.engineAttribute}'`); + } + if (indexModel.secondaryEngineAttribute) { + indexSql.push(`SECONDARY_ENGINE_ATTRIBUTE='${indexModel.secondaryEngineAttribute}'`); + } + return indexSql.join(' '); + } + + private generateTableOptions(tableModel: TableModel) { + const sqls: string[] = []; + if (tableModel.autoExtendSize) { + sqls.push(`AUTOEXTEND_SIZE=${tableModel.autoExtendSize}`); + } + if (tableModel.autoIncrement) { + sqls.push(`AUTO_INCREMENT=${tableModel.autoIncrement}`); + } + if (tableModel.avgRowLength) { + sqls.push(`AVG_ROW_LENGTH=${tableModel.avgRowLength}`); + } + if (tableModel.characterSet) { + sqls.push(`DEFAULT CHARACTER SET ${tableModel.characterSet}`); + } + if (tableModel.collate) { + sqls.push(`DEFAULT COLLATE ${tableModel.collate}`); + } + if (tableModel.comment) { + sqls.push(`COMMENT='${this.formatComment(tableModel.comment)}'`); + } + if (tableModel.compression) { + sqls.push(`COMPRESSION='${tableModel.compression}'`); + } + if (typeof tableModel.encryption !== 'undefined') { + sqls.push(`ENCRYPTION='${tableModel.encryption ? 'Y' : 'N'}'`); + } + if (typeof tableModel.engine !== 'undefined') { + sqls.push(`ENGINE=${tableModel.engine}`); + } + if (tableModel.engineAttribute) { + sqls.push(`ENGINE_ATTRIBUTE='${tableModel.engineAttribute}'`); + } + if (tableModel.secondaryEngineAttribute) { + sqls.push(`SECONDARY_ENGINE_ATTRIBUTE = '${tableModel.secondaryEngineAttribute}'`); + } + if (tableModel.insertMethod) { + sqls.push(`INSERT_METHOD=${tableModel.insertMethod}`); + } + if (tableModel.keyBlockSize) { + sqls.push(`KEY_BLOCK_SIZE=${tableModel.keyBlockSize}`); + } + if (tableModel.maxRows) { + sqls.push(`MAX_ROWS=${tableModel.maxRows}`); + } + if (tableModel.minRows) { + sqls.push(`MIN_ROWS=${tableModel.minRows}`); + } + if (tableModel.rowFormat) { + sqls.push(`ROW_FORMAT=${tableModel.rowFormat}`); + } + return sqls.join(', '); + } + + generate(tableModel: TableModel) { + const createSql: string[] = []; + createSql.push(`CREATE TABLE IF NOT EXISTS ${tableModel.name} (`); + + const columnSql: string[] = []; + for (const column of tableModel.columns) { + columnSql.push(this.generateColumn(column)); + } + + const indexSql: string[] = []; + for (const index of tableModel.indices) { + indexSql.push(this.generateIndex(index)); + } + if (indexSql.length) { + createSql.push(columnSql.join(',\n') + ','); + createSql.push(indexSql.join(',\n')); + } else { + createSql.push(columnSql.join(',\n')); + } + createSql.push(`) ${this.generateTableOptions(tableModel)};`); + + return createSql.join('\n'); + } +} diff --git a/core/dal-runtime/src/SqlMapLoader.ts b/core/dal-runtime/src/SqlMapLoader.ts new file mode 100644 index 00000000..1bbb800a --- /dev/null +++ b/core/dal-runtime/src/SqlMapLoader.ts @@ -0,0 +1,34 @@ +import path from 'node:path'; +import { TableModel, SqlMap } from '@eggjs/dal-decorator'; +import { type EggLogger } from 'egg'; +import { BaseSqlMapGenerator } from './BaseSqlMap'; +import { TableSqlMap } from './TableSqlMap'; + +export class SqlMapLoader { + private readonly logger: EggLogger; + private readonly tableModel: TableModel; + private readonly sqlMapPath: string; + + constructor(tableModel: TableModel, moduleDir: string, logger: EggLogger) { + this.sqlMapPath = path.join(moduleDir, 'dal/extension', `${tableModel.clazz.name}Extension.ts`); + this.logger = logger; + this.tableModel = tableModel; + } + + load(): TableSqlMap { + try { + // eslint-disable-next-line @typescript-eslint/no-var-requires + const customSqlMap: Record = require(this.sqlMapPath); + const baseSqlMapGenerator = new BaseSqlMapGenerator(this.tableModel, this.logger); + const baseSqlMap = baseSqlMapGenerator.load(); + const sqlMap = { + ...baseSqlMap, + ...customSqlMap, + }; + return new TableSqlMap(this.tableModel.clazz.name, sqlMap); + } catch (e) { + e.message = `load sql map ${this.sqlMapPath} failed: ${e.message}`; + throw e; + } + } +} diff --git a/core/dal-runtime/src/SqlUtil.ts b/core/dal-runtime/src/SqlUtil.ts new file mode 100644 index 00000000..42f0d3e5 --- /dev/null +++ b/core/dal-runtime/src/SqlUtil.ts @@ -0,0 +1,186 @@ +function isWhiteChar(ch) { + return ch === ' ' || ch === '\n' || ch === '\r' || ch === '\t'; +} + +const COMMENT_CHARS = '-#/'; +const MUL_CHAR_LEADING_COMMENT_FIRST_CHAR = { + MAY_BE_FIRST_COMMENT: '-', + MAY_BE_FIRST_BLOCK_COMMENT: '/', +}; +const MUL_CHAR_LEADING_COMMENT_VERIFIER = { + MAY_BE_FIRST_COMMENT: '-', + MAY_BE_FIRST_BLOCK_COMMENT: '*', +}; +const MUL_CHAR_LEADING_COMMENT_NEXT_STATE = { + MAY_BE_FIRST_COMMENT: 'IN_COMMENT_WAIT_HINT', + MAY_BE_FIRST_BLOCK_COMMENT: 'IN_BLOCK_COMMENT_WAIT_HINT', +}; + +export class SqlUtil { + static minify(sql: string) { + let ret = ''; + + let state = 'START'; + let tempNextState; + for (let i = 0; i < sql.length; i++) { + const ch = sql[i]; + switch (state) { + case 'MAY_BE_FIRST_COMMENT': + case 'MAY_BE_FIRST_BLOCK_COMMENT': + switch (ch) { + case '"': tempNextState = 'DOUBLE_QUOTE'; break; + case '\'': tempNextState = 'SINGLE_QUOTE'; break; + case MUL_CHAR_LEADING_COMMENT_VERIFIER[state]: + tempNextState = MUL_CHAR_LEADING_COMMENT_NEXT_STATE[state]; + break; + default: tempNextState = 'CONTENT'; break; + } + if (ch !== MUL_CHAR_LEADING_COMMENT_VERIFIER[state]) { + ret += `${MUL_CHAR_LEADING_COMMENT_FIRST_CHAR[state]}${ch}`; + } + state = tempNextState; + break; + + case 'IN_COMMENT_WAIT_HINT': + if (ch !== '+') { + state = 'IN_COMMENT'; + } else { + state = 'IN_COMMENT_HINT'; + ret += '--+'; + } + break; + + case 'IN_BLOCK_COMMENT_WAIT_HINT': + if (ch !== '+') { + state = 'IN_BLOCK_COMMENT'; + } else { + state = 'IN_BLOCK_COMMENT_HINT'; + ret += '/*+'; + } + break; + + case 'MAY_BE_LAST_BLOCK_COMMENT': + if (ch === '/') { + if (ret && !isWhiteChar(ret[ret.length - 1])) ret += ' '; + state = 'IN_SPACE'; + } else { + state = 'IN_BLOCK_COMMENT'; + } + break; + + case 'MAY_BE_LAST_BLOCK_COMMENT_HINT': + ret += ch; + if (ch === '/') { + state = 'IN_SPACE'; + if (isWhiteChar(sql[i + 1])) ret += sql[i + 1]; + } else { + state = 'IN_BLOCK_COMMENT_HINT'; + } + break; + + case 'IN_COMMENT': + if (ch === '\n' || ch === '\r') { + if (ret && !isWhiteChar(ret[ret.length - 1])) ret += ' '; + state = 'IN_SPACE'; + } + break; + + case 'IN_COMMENT_HINT': + ret += ch; + if (ch === '\n' || ch === '\r') { + state = 'IN_SPACE'; + } + break; + + case 'IN_BLOCK_COMMENT': + if (ch === '*') { + state = 'MAY_BE_LAST_BLOCK_COMMENT'; + } + break; + + case 'IN_BLOCK_COMMENT_HINT': + ret += ch; + if (ch === '*') { + state = 'MAY_BE_LAST_BLOCK_COMMENT_HINT'; + } + break; + + case 'START': + if (isWhiteChar(ch)) continue; + switch (ch) { + case '"': state = 'DOUBLE_QUOTE'; break; + case '\'': state = 'SINGLE_QUOTE'; break; + case '-': state = 'MAY_BE_FIRST_COMMENT'; break; + case '#': state = 'IN_COMMENT'; break; + case '/': state = 'MAY_BE_FIRST_BLOCK_COMMENT'; break; + default: state = 'CONTENT'; break; + } + if (!COMMENT_CHARS.includes(ch)) ret += ch; + break; + + case 'DOUBLE_QUOTE': + case 'SINGLE_QUOTE': + switch (ch) { + case '\\': state = `BACKSLASH_AFTER_${state}`; break; + case '\'': + if (state === 'SINGLE_QUOTE') { + state = 'QUOTE_DONE'; + } + break; + case '"': + if (state === 'DOUBLE_QUOTE') { + state = 'QUOTE_DONE'; + } + break; + default: break; + } + ret += ch; + break; + + case 'BACKSLASH_AFTER_SINGLE_QUOTE': + case 'BACKSLASH_AFTER_DOUBLE_QUOTE': + ret += ch; + state = state.substr(16); + break; + + case 'QUOTE_DONE': + case 'CONTENT': + switch (ch) { + case '\'': state = 'SINGLE_QUOTE'; break; + case '"': state = 'DOUBLE_QUOTE'; break; + case '-': state = 'MAY_BE_FIRST_COMMENT'; break; + case '#': state = 'IN_COMMENT'; break; + case '/': state = 'MAY_BE_FIRST_BLOCK_COMMENT'; break; + default: + if (isWhiteChar(ch)) { + state = 'IN_SPACE'; + ret += ' '; + continue; + } + state = 'CONTENT'; + } + if (!COMMENT_CHARS.includes(ch)) ret += ch; + break; + + case 'IN_SPACE': + switch (ch) { + case '\'': state = 'SINGLE_QUOTE'; break; + case '"': state = 'DOUBLE_QUOTE'; break; + case '-': state = 'MAY_BE_FIRST_COMMENT'; break; + case '#': state = 'IN_COMMENT'; break; + case '/': state = 'MAY_BE_FIRST_BLOCK_COMMENT'; break; + default: + if (isWhiteChar(ch)) continue; + state = 'CONTENT'; + } + if (!COMMENT_CHARS.includes(ch)) ret += ch; + break; + + default: + throw new Error('Unexpected state machine while minifying SQL.'); + } + } + + return ret.trim(); + } +} diff --git a/core/dal-runtime/src/TableModelInstanceBuilder.ts b/core/dal-runtime/src/TableModelInstanceBuilder.ts new file mode 100644 index 00000000..cb592504 --- /dev/null +++ b/core/dal-runtime/src/TableModelInstanceBuilder.ts @@ -0,0 +1,25 @@ +import { TableModel } from '@eggjs/dal-decorator'; + +export class TableModelInstanceBuilder { + constructor(tableModel: TableModel, row: Record) { + for (const [ key, value ] of Object.entries(row)) { + const column = tableModel.columns.find(t => t.columnName === key); + Reflect.set(this, column?.propertyName ?? key, value); + } + } + + static buildInstance(tableModel: TableModel, row: Record) { + return Reflect.construct(TableModelInstanceBuilder, [ tableModel, row ], tableModel.clazz); + } + + static buildRow(instance: T, tableModel: TableModel) { + const result: any = {}; + for (const column of tableModel.columns) { + const columnValue = Reflect.get(instance, column.propertyName); + if (typeof columnValue !== 'undefined') { + result[`$${column.propertyName}`] = columnValue; + } + } + return result; + } +} diff --git a/core/dal-runtime/src/TableSqlMap.ts b/core/dal-runtime/src/TableSqlMap.ts new file mode 100644 index 00000000..ff33c37e --- /dev/null +++ b/core/dal-runtime/src/TableSqlMap.ts @@ -0,0 +1,109 @@ +// const nunjucks = require('./NunjucksUtil'); +import { Template } from 'nunjucks'; +import { NunjucksUtils } from './NunjucksUtil'; +import { TemplateUtil } from './TemplateUtil'; +import { SqlMap, SqlType } from '@eggjs/dal-decorator'; + +export interface SqlGenerator { + type: SqlType; + template: Template, + raw: string, +} + +export class TableSqlMap { + readonly name: string; + private readonly map: Record; + private readonly blocks: Record; + private readonly sqlGenerator: Record; + + constructor(name: string, map: Record) { + this.name = name; + this.map = map; + + const env = NunjucksUtils.createEnv(name); + const extracted = this.#extract(this.map); + this.blocks = extracted.blocks; + this.sqlGenerator = extracted.sqlGenerator; + + for (const key in this.blocks) { + // istanbul ignore if + if (!this.blocks.hasOwnProperty(key)) continue; + env.addGlobal(key, this.blocks[key]); + } + + env.addFilter('toJson', TemplateUtil.toJson); + env.addFilter('toPoint', TemplateUtil.toPoint); + env.addFilter('toLine', TemplateUtil.toLine); + env.addFilter('toPolygon', TemplateUtil.toPolygon); + env.addFilter('toGeometry', TemplateUtil.toGeometry); + env.addFilter('toMultiPoint', TemplateUtil.toMultiPoint); + env.addFilter('toMultiLine', TemplateUtil.toMultiLine); + env.addFilter('toMultiPolygon', TemplateUtil.toMultiPolygon); + env.addFilter('toGeometryCollection', TemplateUtil.toGeometryCollection); + } + + #extract(map: Record) { + const ret = { + blocks: {}, + sqlGenerator: {}, + }; + + for (const key in map) { + // istanbul ignore if + if (!map.hasOwnProperty(key)) continue; + + const sqlMap = map[key]; + + switch (sqlMap.type) { + case SqlType.BLOCK: + ret.blocks[key] = sqlMap.content || ''; + break; + case SqlType.INSERT: + case SqlType.SELECT: + case SqlType.UPDATE: + case SqlType.DELETE: + default: + ret.sqlGenerator[key] = { + type: sqlMap.type, + template: NunjucksUtils.compile(this.name, key, sqlMap.sql || ''), + raw: sqlMap.sql, + }; + break; + } + } + + return ret; + } + + generate(name: string, data: object, timezone: string) { + const generator = this.sqlGenerator[name]; + // istanbul ignore if + if (!generator) { + throw new Error(`No sql map named '${name}' in '${name}'.`); + } + + const template = generator.template; + (template as any).env.timezone = timezone; + return template.render(data); + } + + getType(name: string): SqlType { + const generator = this.sqlGenerator[name]; + // istanbul ignore if + if (!generator) { + throw new Error(`No sql map named '${name}' in '${name}'.`); + } + + return generator.type; + } + + getTemplateString(name: string) { + const generator = this.sqlGenerator[name]; + // istanbul ignore if + if (!generator) { + throw new Error(`No sql map named '${name}' in '${name}'.`); + } + + return generator.raw; + } +} diff --git a/core/dal-runtime/src/TemplateUtil.ts b/core/dal-runtime/src/TemplateUtil.ts new file mode 100644 index 00000000..07ee7db0 --- /dev/null +++ b/core/dal-runtime/src/TemplateUtil.ts @@ -0,0 +1,101 @@ +import path from 'node:path'; +import { + ColumnModel, + ColumnType, + Geometry, GeometryCollection, + Line, + MultiLine, + MultiPoint, + MultiPolygon, + Point, + Polygon, SpatialHelper, +} from '@eggjs/dal-decorator'; + +export class TemplateUtil { + static isSpatialType(columnModel: ColumnModel): boolean { + switch (columnModel.type.type) { + case ColumnType.GEOMETRY: + case ColumnType.POINT: + case ColumnType.LINESTRING: + case ColumnType.POLYGON: + case ColumnType.MULTIPOINT: + case ColumnType.MULTILINESTRING: + case ColumnType.MULTIPOLYGON: + case ColumnType.GEOMETRYCOLLECTION: { + return true; + } + default: { + return false; + } + } + } + + static importPath(tableModelPath: string, currentPath: string) { + return path.relative(currentPath, tableModelPath); + } + + static dbTypeToTsType(columnType: ColumnType): string { + return `ColumnTsType['${columnType}']`; + } + + static toJson(value: any): string { + return JSON.stringify(JSON.stringify(value)); + } + + static toPoint(point: Point): string { + if (typeof point.x !== 'number' || typeof point.y !== 'number') { + throw new Error(`invalidate point ${JSON.stringify(point)}`); + } + return `Point(${point.x}, ${point.y})`; + } + static toLine(val: Line): string { + const points = val.map(t => TemplateUtil.toPoint(t)); + return `LINESTRING(${points.join(',')})`; + } + static toPolygon(val: Polygon): string { + const lines = val.map(t => TemplateUtil.toLine(t)); + return `POLYGON(${lines.join(',')})`; + } + static toGeometry(val: Geometry): string { + const type = SpatialHelper.getGeometryType(val); + const filterName = TemplateUtil.getSpatialFilter(type); + return TemplateUtil[filterName](val); + } + static toMultiPoint(val: MultiPoint): string { + const points = val.map(t => TemplateUtil.toPoint(t)); + return `MULTIPOINT(${points.join(',')})`; + } + static toMultiLine(val: MultiLine): string { + const lines = val.map(t => TemplateUtil.toLine(t)); + return `MULTILINESTRING(${lines.join(',')})`; + } + static toMultiPolygon(val: MultiPolygon): string { + const polygon = val.map(t => TemplateUtil.toPolygon(t)); + return `MULTIPOLYGON(${polygon.join(',')})`; + } + static toGeometryCollection(val: GeometryCollection): string { + const geometries = val.map(t => { + return TemplateUtil.toGeometry(t); + }); + return `GEOMETRYCOLLECTION(${geometries.join(',')})`; + } + + static spatialFilter = { + [ColumnType.POINT]: 'toPoint', + [ColumnType.LINESTRING]: 'toLine', + [ColumnType.POLYGON]: 'toPolygon', + [ColumnType.GEOMETRY]: 'toGeometry', + [ColumnType.MULTIPOINT]: 'toMultiPoint', + [ColumnType.MULTILINESTRING]: 'toMultiLine', + [ColumnType.MULTIPOLYGON]: 'toMultiPolygon', + [ColumnType.GEOMETRYCOLLECTION]: 'toGeometryCollection', + }; + + static getSpatialFilter(columnType: ColumnType) { + const filter = TemplateUtil.spatialFilter[columnType]; + if (!filter) { + throw new Error(`type ${columnType} is not spatial type`); + } + return filter; + } +} diff --git a/core/dal-runtime/src/templates/base_dao.njk b/core/dal-runtime/src/templates/base_dao.njk new file mode 100644 index 00000000..7c261187 --- /dev/null +++ b/core/dal-runtime/src/templates/base_dao.njk @@ -0,0 +1,150 @@ +{% macro newDataLogic(columns, old, new) %} +let tmp; +{% for column in table.columns %} +// empty-line +tmp = {{ old }}.{{ column.propertyName }}; +if (tmp !== undefined) { + {{ new }}.${{ column.propertyName }} = tmp; +} +{% endfor %} +{% endmacro %} + +{% macro findLogic(funcName, sqlName, idx, uniq) %} +public async {{ funcName }}( + {% for key in idx.keys %} + ${{ key.propertyName }}: {{columnMap[key.propertyName].type.type | dbTypeToTSType}}{% if loop.last !== true %},{% endif%} + {% endfor %} +): Promise<{{ clazzName }}{{ '| null' if uniq else '[]' }}> { + return this.dataSource.{{ 'executeScalar' if uniq else 'execute' }}('{{ sqlName }}', { + {% for key in idx.keys %} + ${{ key.propertyName }}, + {% endfor %} + }); +} +{% endmacro %} + +{% macro generatePrimaryType(primary) %} +{% if (primary.keys | length) === 1 %} +{{primary.keys[0].propertyName}}: {{ columnMap[primary.keys[0].propertyName].type.type | dbTypeToTSType}} +{% else %} +primary: { + {% for key in primary.keys %} + {{ key.propertyName }}: {{columnMap[key.propertyName].type.type | dbTypeToTSType}}{% if loop.last !== true %},{% endif%} + {% endfor %} +} +{% endif %} +{% endmacro %} + +{% macro generateUpdateValue(primary) %} +{% if (primary.keys | length) === 1 %} +const newData: Record = { + primary: { + {{primary.keys[0].propertyName}}, + }, +}; +{% else %} +const newData: Record = { + primary, +}; +{% endif %} +{% endmacro %} + +{% macro generateDeleteValue(primary) %} +{% if (primary.keys | length) === 1 %} +{ + {{primary.keys[0].propertyName}}, +} + +{% else %} +primary +{% endif %} +{% endmacro %} + +{% macro generateInsertType(primary) %} +Optional<{{clazzName}}, + {% for key in primary.keys %} + '{{ key.propertyName }}'{% if loop.last !== true %}|{% endif%} + {% endfor %} +> +{% endmacro %} + +import type { InsertResult, UpdateResult, DeleteResult } from '@eggjs/rds/lib/types'; +import { SingletonProto, AccessLevel, Inject } from '@eggjs/tegg'; +import { DataSource, DataSourceInjectName, DataSourceQualifier, ColumnTsType } from '@eggjs/tegg/dal'; +import { {{ clazzName }} } from '{{ tableModelPath }}'; +// empty-line +type Optional = Omit & Partial; +/** + * 自动生成的 {{ clazzName }}DAO 基类 + * @class Base{{ clazzName }}DAO + * @classdesc 该文件由 @eggjs/tegg 自动生成,请**不要**修改它! + */ +/* istanbul ignore next */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class Base{{ clazzName }}DAO { + static clazzModel = {{ clazzName }}; + + @Inject({ + name: DataSourceInjectName, + }) + @DataSourceQualifier('{{moduleName}}.{{ table.dataSourceName }}.{{ clazzName }}') + protected readonly dataSource: DataSource<{{clazzName}}>; + + // empty-line + {# insert: 插入 #} + public async insert(raw: {{generateInsertType(primaryIndex)}}): Promise { + const data: Record = {}; + + {{ newDataLogic(columns, 'raw', 'data') }} + + // empty-line + return this.dataSource.executeRawScalar('insert', data); + } + + // empty-line + {# update: 更新 #} + public async update({{generatePrimaryType(primaryIndex)}}, data: Partial<{{ ((clazzName)) }}>): Promise { + // empty-line + {{ generateUpdateValue(primaryIndex) }} + + {{ newDataLogic(columns, 'data', 'newData') }} + + // empty-line + return this.dataSource.executeRawScalar('update', newData); + } + + {% for funcName in [ 'delete', 'del' ] %} + // empty-line + {# delete: 删除 #} + public async {{ funcName }}({{generatePrimaryType(primaryIndex)}}): Promise { + return this.dataSource.executeRawScalar('delete', {{generateDeleteValue(primaryIndex)}}); + } + {% endfor %} + + {% for idx in table.indices %} + // empty-line + {# 某个索引 #} + {% set tmpName = ((idx.keys[0].propertyName if (idx.keys | length) === 1 else idx.name) | pascalCase) %} + {% set findName = 'findBy' + tmpName %} + {% set findOneName = 'findOneBy' + tmpName %} + {{ findLogic(findName, findName, idx, false) }} + // empty-line + {{ findLogic(findOneName, findOneName, idx, true) }} + {% endfor %} + + // empty-line + {# 某个索引 #} + {% if primaryIndex %} + {% set tmpName = ((primaryIndex.keys[0].propertyName if (primaryIndex.keys | length) === 1 else primaryIndex.name) | pascalCase) %} + {% set findName = 'findBy' + tmpName %} + {% set findOneName = 'findOneBy' + tmpName %} + {{ findLogic(findName, findName, primaryIndex, true) }} + {% if (primaryIndex.keys | length) === 1 %} + // empty-line + {{ findLogic('findByPrimary', findName, primaryIndex, true) }} + {% endif %} + {% endif %} +} +// empty-line diff --git a/core/dal-runtime/src/templates/dao.njk b/core/dal-runtime/src/templates/dao.njk new file mode 100644 index 00000000..c922fd5c --- /dev/null +++ b/core/dal-runtime/src/templates/dao.njk @@ -0,0 +1,17 @@ +import { SingletonProto, AccessLevel } from '@eggjs/tegg'; +import { Base{{ clazzName }}DAO } from './base/Base{{ clazzName }}DAO'; +// empty-line +/** + * {{ clazzName }}DAO 类 +{% if user.name %} * @author {{ user.name }} {% if user.email %}<{{ user.email }}>{% endif %} +{% endif %} * @class {{ clazzName }}DAO + * @classdesc 在此扩展关于 {{ clazzName }} 数据的一切操作 + * @extends Base{{ clazzName }}DAO + */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export default class {{ clazzName }}DAO extends Base{{ clazzName }}DAO { + // empty-line +} +// empty-line diff --git a/core/dal-runtime/src/templates/extension.njk b/core/dal-runtime/src/templates/extension.njk new file mode 100644 index 00000000..e1bf00a2 --- /dev/null +++ b/core/dal-runtime/src/templates/extension.njk @@ -0,0 +1,17 @@ +import { SqlMap } from '@eggjs/tegg/dal'; +// empty-line +/** + * Define Custom SQLs + * + * import { SqlMap, SqlType } from '@eggjs/tegg/dal'; + * + * export default { + * findByName: { + * type: SqlType.SELECT, + * sql: 'SELECT {{ allColumns }} from foo where name = {{ name }}' + * }, + * } + */ +export default { +// empty-line +} as Record; diff --git a/core/dal-runtime/test/CodeGenerator.test.ts b/core/dal-runtime/test/CodeGenerator.test.ts new file mode 100644 index 00000000..8358b782 --- /dev/null +++ b/core/dal-runtime/test/CodeGenerator.test.ts @@ -0,0 +1,21 @@ +import assert from 'node:assert'; +import path from 'node:path'; +import { Foo } from './fixtures/modules/generate_codes/Foo'; +import { MultiPrimaryKey } from './fixtures/modules/generate_codes/MultiPrimaryKey'; +import { TableModel } from '@eggjs/dal-decorator'; +import { CodeGenerator } from '../src/CodeGenerator'; + +describe('test/CodeGenerator.test.ts', () => { + it('BaseDao should work', async () => { + const generator = new CodeGenerator({ + moduleDir: path.join(__dirname, './fixtures/modules/generate_codes'), + moduleName: 'dal', + }); + const fooModel = TableModel.build(Foo); + await generator.generate(fooModel); + + const multiPrimaryKeyTableModel = TableModel.build(MultiPrimaryKey); + await generator.generate(multiPrimaryKeyTableModel); + assert(fooModel); + }); +}); diff --git a/core/dal-runtime/test/DAO.test.ts b/core/dal-runtime/test/DAO.test.ts new file mode 100644 index 00000000..6b54a858 --- /dev/null +++ b/core/dal-runtime/test/DAO.test.ts @@ -0,0 +1,145 @@ +import assert from 'node:assert'; +import { MysqlDataSource } from '../src/MySqlDataSource'; +import { SqlMapLoader } from '../src/SqlMapLoader'; +import { Foo } from './fixtures/modules/dal/Foo'; +import { TableModel } from '@eggjs/dal-decorator'; +import path from 'node:path'; +import { DataSource } from '../src/DataSource'; +import { SqlGenerator } from '../src/SqlGenerator'; +import FooDAO from './fixtures/modules/dal/dal/dao/FooDAO'; + +describe('test/DAO.test.ts', () => { + let dataSource: DataSource; + let tableModel: TableModel; + + before(async () => { + const mysql = new MysqlDataSource({ + name: 'foo', + host: '127.0.0.1', + user: 'root', + database: 'test', + timezone: '+08:00', + initSql: 'SET GLOBAL time_zone = \'+08:00\';', + }); + await mysql.ready(); + await mysql.query('DROP TABLE IF EXISTS egg_foo'); + + tableModel = TableModel.build(Foo); + + const sqlGenerator = new SqlGenerator(); + const createTableSql = sqlGenerator.generate(tableModel); + + await mysql.query(createTableSql); + + const sqlMapLoader = new SqlMapLoader(tableModel, path.join(__dirname, './fixtures/modules/dal'), console as any); + const sqlMap = sqlMapLoader.load(); + dataSource = new DataSource(tableModel, mysql, sqlMap); + }); + + it('execute should work', async () => { + const foo = new Foo(); + foo.name = 'name'; + foo.col1 = 'col1'; + foo.bitColumn = Buffer.from([ 0, 0 ]); + foo.boolColumn = 0; + foo.tinyIntColumn = 0; + foo.smallIntColumn = 1; + foo.mediumIntColumn = 3; + foo.intColumn = 3; + foo.bigIntColumn = '00099'; + foo.decimalColumn = '00002.33333'; + foo.floatColumn = 2.3; + foo.doubleColumn = 2.3; + foo.dateColumn = new Date('2024-03-16T16:00:00.000Z'); + foo.dateTimeColumn = new Date('2024-03-16T01:26:58.677Z'); + foo.timestampColumn = new Date('2024-03-16T01:26:58.677Z'); + foo.timeColumn = '838:59:50.123'; + foo.yearColumn = 2024; + foo.varCharColumn = 'var_char'; + foo.binaryColumn = Buffer.from('b'); + foo.varBinaryColumn = Buffer.from('var_binary'); + foo.tinyBlobColumn = Buffer.from('tiny_blob'); + foo.tinyTextColumn = 'text'; + foo.blobColumn = Buffer.from('blob'); + foo.textColumn = 'text'; + foo.mediumBlobColumn = Buffer.from('medium_blob'); + foo.longBlobColumn = Buffer.from('long_blob'); + foo.mediumTextColumn = 'medium_text'; + foo.longTextColumn = 'long_text'; + foo.enumColumn = 'A'; + foo.setColumn = 'B'; + foo.geometryColumn = { x: 10, y: 10 }; + foo.pointColumn = { x: 10, y: 10 }; + foo.lineStringColumn = [ + { x: 15, y: 15 }, + { x: 20, y: 20 }, + ]; + foo.polygonColumn = [ + [ + { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 }, + ], [ + { x: 5, y: 5 }, { x: 7, y: 5 }, { x: 7, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 5 }, + ], + ]; + foo.multipointColumn = [ + { x: 0, y: 0 }, { x: 20, y: 20 }, { x: 60, y: 60 }, + ]; + foo.multiLineStringColumn = [ + [ + { x: 10, y: 10 }, { x: 20, y: 20 }, + ], [ + { x: 15, y: 15 }, { x: 30, y: 15 }, + ], + ]; + foo.multiPolygonColumn = [ + [ + [ + { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 }, + ], + ], + [ + [ + { x: 5, y: 5 }, { x: 7, y: 5 }, { x: 7, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 5 }, + ], + ], + ]; + foo.geometryCollectionColumn = [ + { x: 10, y: 10 }, + { x: 30, y: 30 }, + [ + { x: 15, y: 15 }, { x: 20, y: 20 }, + ], + ]; + foo.jsonColumn = { + hello: 'json', + }; + + const fooDao = new FooDAO(); + (fooDao as any).dataSource = dataSource; + + const insertResult = await fooDao.insert(foo); + assert(insertResult); + foo.id = insertResult.insertId; + + const updateResult = await fooDao.update(foo.id, { + name: 'update_name_2', + }); + assert(updateResult); + assert.equal(updateResult.affectedRows, 1); + + foo.name = 'update_name_2'; + + const fooRow = await fooDao.findByPrimary(foo.id); + assert.deepStrictEqual(fooRow, foo); + + const fooRows = await fooDao.findByCol1(foo.col1); + assert.equal(fooRows.length, 1); + + assert.deepStrictEqual(fooRows[0], foo); + + await fooDao.delete(foo.id); + + const fooRow2 = await fooDao.findByPrimary(foo.id); + assert.equal(fooRow2, null); + }); +}); diff --git a/core/dal-runtime/test/DataSource.test.ts b/core/dal-runtime/test/DataSource.test.ts new file mode 100644 index 00000000..2a6ab357 --- /dev/null +++ b/core/dal-runtime/test/DataSource.test.ts @@ -0,0 +1,147 @@ +import assert from 'node:assert'; +import { MysqlDataSource } from '../src/MySqlDataSource'; +import { SqlMapLoader } from '../src/SqlMapLoader'; +import { Foo } from './fixtures/modules/dal/Foo'; +import { TableModel } from '@eggjs/dal-decorator'; +import path from 'node:path'; +import { DataSource } from '../src/DataSource'; +import { TableModelInstanceBuilder } from '../src/TableModelInstanceBuilder'; +import { DeleteResult, InsertResult, UpdateResult } from '@eggjs/rds/lib/types'; +import { SqlGenerator } from '../src/SqlGenerator'; + +describe('test/Datasource.test.ts', () => { + let dataSource: DataSource; + let tableModel: TableModel; + + before(async () => { + const mysql = new MysqlDataSource({ + name: 'foo', + host: '127.0.0.1', + user: 'root', + database: 'test', + timezone: '+08:00', + initSql: 'SET GLOBAL time_zone = \'+08:00\';', + }); + await mysql.ready(); + await mysql.query('DROP TABLE IF EXISTS egg_foo'); + + tableModel = TableModel.build(Foo); + + const sqlGenerator = new SqlGenerator(); + const createTableSql = sqlGenerator.generate(tableModel); + + await mysql.query(createTableSql); + + const sqlMapLoader = new SqlMapLoader(tableModel, path.join(__dirname, './fixtures/modules/dal'), console as any); + const sqlMap = sqlMapLoader.load(); + dataSource = new DataSource(tableModel, mysql, sqlMap); + }); + + it('execute should work', async () => { + const foo = new Foo(); + foo.name = 'name'; + foo.col1 = 'col1'; + foo.bitColumn = Buffer.from([ 0, 0 ]); + foo.boolColumn = 0; + foo.tinyIntColumn = 0; + foo.smallIntColumn = 1; + foo.mediumIntColumn = 3; + foo.intColumn = 3; + foo.bigIntColumn = '00099'; + foo.decimalColumn = '00002.33333'; + foo.floatColumn = 2.3; + foo.doubleColumn = 2.3; + foo.dateColumn = new Date('2024-03-16T16:00:00.000Z'); + foo.dateTimeColumn = new Date('2024-03-16T01:26:58.677Z'); + foo.timestampColumn = new Date('2024-03-16T01:26:58.677Z'); + foo.timeColumn = '838:59:50.123'; + foo.yearColumn = 2024; + foo.varCharColumn = 'var_char'; + foo.binaryColumn = Buffer.from('b'); + foo.varBinaryColumn = Buffer.from('var_binary'); + foo.tinyBlobColumn = Buffer.from('tiny_blob'); + foo.tinyTextColumn = 'text'; + foo.blobColumn = Buffer.from('blob'); + foo.textColumn = 'text'; + foo.mediumBlobColumn = Buffer.from('medium_blob'); + foo.longBlobColumn = Buffer.from('long_blob'); + foo.mediumTextColumn = 'medium_text'; + foo.longTextColumn = 'long_text'; + foo.enumColumn = 'A'; + foo.setColumn = 'B'; + foo.geometryColumn = { x: 10, y: 10 }; + foo.pointColumn = { x: 10, y: 10 }; + foo.lineStringColumn = [ + { x: 15, y: 15 }, + { x: 20, y: 20 }, + ]; + foo.polygonColumn = [ + [ + { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 }, + ], [ + { x: 5, y: 5 }, { x: 7, y: 5 }, { x: 7, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 5 }, + ], + ]; + foo.multipointColumn = [ + { x: 0, y: 0 }, { x: 20, y: 20 }, { x: 60, y: 60 }, + ]; + foo.multiLineStringColumn = [ + [ + { x: 10, y: 10 }, { x: 20, y: 20 }, + ], [ + { x: 15, y: 15 }, { x: 30, y: 15 }, + ], + ]; + foo.multiPolygonColumn = [ + [ + [ + { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 }, + ], + ], + [ + [ + { x: 5, y: 5 }, { x: 7, y: 5 }, { x: 7, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 5 }, + ], + ], + ]; + foo.geometryCollectionColumn = [ + { x: 10, y: 10 }, + { x: 30, y: 30 }, + [ + { x: 15, y: 15 }, { x: 20, y: 20 }, + ], + ]; + foo.jsonColumn = { + hello: 'json', + }; + const rowValue = TableModelInstanceBuilder.buildRow(foo, tableModel); + const insertResult: InsertResult = await dataSource.executeRawScalar('insert', rowValue); + assert(insertResult.insertId); + foo.id = insertResult.insertId; + + const updateResult: UpdateResult = await dataSource.executeRawScalar('update', { + primary: { + id: insertResult.insertId, + }, + $name: 'update_name', + }); + assert.equal(updateResult.affectedRows, 1); + foo.name = 'update_name'; + + const findRow = await dataSource.executeScalar('findByPrimary', { + $id: insertResult.insertId, + }); + assert(findRow); + assert.deepStrictEqual(findRow, foo); + + const deleteRow: DeleteResult = await dataSource.executeRawScalar('delete', { + id: insertResult.insertId, + }); + assert.equal(deleteRow.affectedRows, 1); + + const findRow2 = await dataSource.executeScalar('findByPrimary', { + $id: insertResult.insertId, + }); + assert.equal(findRow2, null); + }); +}); diff --git a/core/dal-runtime/test/SqlGenerator.test.ts b/core/dal-runtime/test/SqlGenerator.test.ts new file mode 100644 index 00000000..44e0bfa4 --- /dev/null +++ b/core/dal-runtime/test/SqlGenerator.test.ts @@ -0,0 +1,56 @@ +import assert from 'node:assert'; +import { Foo } from './fixtures/modules/dal/Foo'; +import { SqlGenerator } from '../src/SqlGenerator'; +import { TableModel } from '@eggjs/dal-decorator'; + +describe('test/SqlGenerator.test.ts', () => { + it('generator should work', () => { + const generator = new SqlGenerator(); + const fooModel = TableModel.build(Foo); + const sql = generator.generate(fooModel); + assert.equal(sql, 'CREATE TABLE IF NOT EXISTS egg_foo (\n' + + ' id INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT \'the primary key\',\n' + + ' name VARCHAR(100) NULL UNIQUE KEY,\n' + + ' col1 VARCHAR(100) NULL,\n' + + ' bit_column BIT(10) NULL,\n' + + ' bool_column BOOL NULL,\n' + + ' tiny_int_column TINYINT(5) UNSIGNED ZEROFILL NULL,\n' + + ' small_int_column SMALLINT(5) UNSIGNED ZEROFILL NULL,\n' + + ' medium_int_column MEDIUMINT(5) UNSIGNED ZEROFILL NULL,\n' + + ' int_column INT(5) UNSIGNED ZEROFILL NULL,\n' + + ' big_int_column BIGINT(5) UNSIGNED ZEROFILL NULL,\n' + + ' decimal_column DECIMAL(10,5) UNSIGNED ZEROFILL NULL,\n' + + ' float_column FLOAT(10,5) UNSIGNED ZEROFILL NULL,\n' + + ' double_column DOUBLE(10,5) UNSIGNED ZEROFILL NULL,\n' + + ' date_column DATE NULL,\n' + + ' date_time_column DATETIME(3) NULL,\n' + + ' timestamp_column TIMESTAMP(3) NULL,\n' + + ' time_column TIME(3) NULL,\n' + + ' year_column YEAR NULL,\n' + + ' var_char_column VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,\n' + + ' binary_column BINARY NULL,\n' + + ' var_binary_column VARBINARY(100) NULL,\n' + + ' tiny_blob_column TINYBLOB NULL,\n' + + ' tiny_text_column TINYTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,\n' + + ' blob_column BLOB(100) NULL,\n' + + ' text_column TEXT(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,\n' + + ' medium_blob_column MEDIUMBLOB NULL,\n' + + ' long_blob_column LONGBLOB NULL,\n' + + ' medium_text_column MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,\n' + + ' long_text_column LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,\n' + + ' enum_column ENUM(\'A\',\'B\') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,\n' + + ' set_column SET(\'A\',\'B\') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL,\n' + + ' geometry_column GEOMETRY NULL,\n' + + ' point_column POINT NULL,\n' + + ' line_string_column LINESTRING NULL,\n' + + ' polygon_column POLYGON NULL,\n' + + ' multipoint_column MULTIPOINT NULL,\n' + + ' multi_line_string_column MULTILINESTRING NULL,\n' + + ' multi_polygon_column MULTIPOLYGON NULL,\n' + + ' geometry_collection_column GEOMETRYCOLLECTION NULL,\n' + + ' json_column JSON NULL,\n' + + ' FULLTEXT KEY idx_col1 (col1) COMMENT \'index comment\\n\',\n' + + ' UNIQUE KEY uk_name_col1 (name,col1) USING BTREE COMMENT \'index comment\\n\'\n' + + ') DEFAULT CHARACTER SET utf8mb4, DEFAULT COLLATE utf8mb4_unicode_ci, COMMENT=\'foo table\';'); + }); +}); diff --git a/core/dal-runtime/test/fixtures/modules/dal/Foo.ts b/core/dal-runtime/test/fixtures/modules/dal/Foo.ts new file mode 100644 index 00000000..cbf4cad8 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/dal/Foo.ts @@ -0,0 +1,318 @@ +import { + Column, + ColumnType, + Geometry, + GeometryCollection, + Index, + IndexType, + Line, MultiLine, MultiPoint, MultiPolygon, Point, Polygon, + Table, +} from '@eggjs/dal-decorator'; +import { IndexStoreType } from '@eggjs/dal-decorator/src/enum/IndexStoreType'; + +@Table({ + name: 'egg_foo', + comment: 'foo table', + // autoExtendSize: 1024, + // autoIncrement: 100, + // avgRowLength: 1024, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + // compression: CompressionType.ZLIB, + // encryption: true, + // engine: 'NDB', + // engineAttribute: '{"key":"value"}', + // secondaryEngineAttribute: '{"key2":"value2"}', + // insertMethod: InsertMethod.FIRST, + // keyBlockSize: 1024, + // maxRows: 1000000, + // minRows: 100, + // rowFormat: RowFormat.COMPRESSED, +}) +@Index({ + keys: [ 'name', 'col1' ], + type: IndexType.UNIQUE, + storeType: IndexStoreType.BTREE, + comment: 'index comment\n', + // engineAttribute: '{"key":"value"}', + // secondaryEngineAttribute: '{"key2":"value2"}', +}) +@Index({ + keys: [ 'col1' ], + type: IndexType.FULLTEXT, + comment: 'index comment\n', + // engineAttribute: '{"key":"value"}', + // secondaryEngineAttribute: '{"key2":"value2"}', + // parser: 'foo', +}) +export class Foo { + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + autoIncrement: true, + comment: 'the primary key', + }) + id: number; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }, { + uniqueKey: true, + }) + name: string; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }, { + name: 'col1', + }) + col1: string; + + @Column({ + type: ColumnType.BIT, + length: 10, + }) + bitColumn: Buffer; + + @Column({ + type: ColumnType.BOOL, + }) + boolColumn: 0 | 1; + + @Column({ + type: ColumnType.TINYINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + tinyIntColumn: number; + + @Column({ + type: ColumnType.SMALLINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + smallIntColumn: number; + + @Column({ + type: ColumnType.MEDIUMINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + mediumIntColumn: number; + + @Column({ + type: ColumnType.INT, + length: 5, + unsigned: true, + zeroFill: true, + }) + intColumn: number; + + @Column({ + type: ColumnType.BIGINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + bigIntColumn: string; + + @Column({ + type: ColumnType.DECIMAL, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + decimalColumn: string; + + @Column({ + type: ColumnType.FLOAT, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + floatColumn: number; + + @Column({ + type: ColumnType.DOUBLE, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + doubleColumn: number; + + @Column({ + type: ColumnType.DATE, + }) + dateColumn: Date; + + @Column({ + type: ColumnType.DATETIME, + precision: 3, + }) + dateTimeColumn: Date; + + @Column({ + type: ColumnType.TIMESTAMP, + precision: 3, + }) + timestampColumn: Date; + + @Column({ + type: ColumnType.TIME, + precision: 3, + }) + timeColumn: string; + + @Column({ + type: ColumnType.YEAR, + }) + yearColumn: number; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + varCharColumn: string; + + @Column({ + type: ColumnType.BINARY, + }) + binaryColumn: Buffer; + + @Column({ + type: ColumnType.VARBINARY, + length: 100, + }) + varBinaryColumn: Buffer; + + @Column({ + type: ColumnType.TINYBLOB, + }) + tinyBlobColumn: Buffer; + + @Column({ + type: ColumnType.TINYTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + tinyTextColumn: string; + + @Column({ + type: ColumnType.BLOB, + length: 100, + }) + blobColumn: Buffer; + + @Column({ + type: ColumnType.TEXT, + length: 100, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + textColumn: string; + + @Column({ + type: ColumnType.MEDIUMBLOB, + }) + mediumBlobColumn: Buffer; + + @Column({ + type: ColumnType.LONGBLOB, + }) + longBlobColumn: Buffer; + + @Column({ + type: ColumnType.MEDIUMTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + mediumTextColumn: string; + + @Column({ + type: ColumnType.LONGTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + longTextColumn: string; + + @Column({ + type: ColumnType.ENUM, + enums: [ 'A', 'B' ], + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + enumColumn: string; + + @Column({ + type: ColumnType.SET, + enums: [ 'A', 'B' ], + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + setColumn: string; + + @Column({ + type: ColumnType.GEOMETRY, + // SRID: 4326, + }) + geometryColumn: Geometry; + + + @Column({ + type: ColumnType.POINT, + // SRID: 4326, + }) + pointColumn: Point; + + @Column({ + type: ColumnType.LINESTRING, + // SRID: 4326, + }) + lineStringColumn: Line; + + @Column({ + type: ColumnType.POLYGON, + // SRID: 4326, + }) + polygonColumn: Polygon; + + @Column({ + type: ColumnType.MULTIPOINT, + // SRID: 4326, + }) + multipointColumn: MultiPoint; + + @Column({ + type: ColumnType.MULTILINESTRING, + // SRID: 4326, + }) + multiLineStringColumn: MultiLine; + + @Column({ + type: ColumnType.MULTIPOLYGON, + // SRID: 4326, + }) + multiPolygonColumn: MultiPolygon; + + @Column({ + type: ColumnType.GEOMETRYCOLLECTION, + // SRID: 4326, + }) + geometryCollectionColumn: GeometryCollection; + + @Column({ + type: ColumnType.JSON, + }) + jsonColumn: object; +} diff --git a/core/dal-runtime/test/fixtures/modules/dal/dal/dao/FooDAO.ts b/core/dal-runtime/test/fixtures/modules/dal/dal/dao/FooDAO.ts new file mode 100644 index 00000000..5d4dfb9f --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/dal/dal/dao/FooDAO.ts @@ -0,0 +1,15 @@ +import { SingletonProto, AccessLevel } from '@eggjs/tegg'; +import { BaseFooDAO } from './base/BaseFooDAO'; + +/** + * FooDAO 类 + * @class FooDAO + * @classdesc 在此扩展关于 Foo 数据的一切操作 + * @extends BaseFooDAO + */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export default class FooDAO extends BaseFooDAO { + +} diff --git a/core/dal-runtime/test/fixtures/modules/dal/dal/dao/base/BaseFooDAO.ts b/core/dal-runtime/test/fixtures/modules/dal/dal/dao/base/BaseFooDAO.ts new file mode 100644 index 00000000..966cfe20 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/dal/dal/dao/base/BaseFooDAO.ts @@ -0,0 +1,492 @@ +import type { InsertResult, UpdateResult, DeleteResult } from '@eggjs/rds/lib/types'; +import { SingletonProto, AccessLevel, Inject, ModuleQualifier } from '@eggjs/tegg'; +import { DataSource, DataSourceInjectName, DataSourceQualifier } from '@eggjs/tegg/dal'; +import { Foo } from '../../../../generate_codes/Foo'; + +type Optional = Omit < T, K > & Partial ; +/** + * 自动生成的 FooDAO 基类 + * @class BaseFooDAO + * @classdesc 该文件由 @eggjs/tegg 自动生成,请**不要**修改它! + */ +/* istanbul ignore next */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class BaseFooDAO { + @Inject({ + name: DataSourceInjectName, + }) + @ModuleQualifier('dal') + @DataSourceQualifier('default.Foo') + protected readonly dataSource: DataSource ; + + public async insert(raw: Optional ): Promise { + const data: Record = {}; + let tmp; + + tmp = raw.id; + if (tmp !== undefined) { + data.$id = tmp; + } + + tmp = raw.name; + if (tmp !== undefined) { + data.$name = tmp; + } + + tmp = raw.col1; + if (tmp !== undefined) { + data.$col1 = tmp; + } + + tmp = raw.bitColumn; + if (tmp !== undefined) { + data.$bitColumn = tmp; + } + + tmp = raw.boolColumn; + if (tmp !== undefined) { + data.$boolColumn = tmp; + } + + tmp = raw.tinyIntColumn; + if (tmp !== undefined) { + data.$tinyIntColumn = tmp; + } + + tmp = raw.smallIntColumn; + if (tmp !== undefined) { + data.$smallIntColumn = tmp; + } + + tmp = raw.mediumIntColumn; + if (tmp !== undefined) { + data.$mediumIntColumn = tmp; + } + + tmp = raw.intColumn; + if (tmp !== undefined) { + data.$intColumn = tmp; + } + + tmp = raw.bigIntColumn; + if (tmp !== undefined) { + data.$bigIntColumn = tmp; + } + + tmp = raw.decimalColumn; + if (tmp !== undefined) { + data.$decimalColumn = tmp; + } + + tmp = raw.floatColumn; + if (tmp !== undefined) { + data.$floatColumn = tmp; + } + + tmp = raw.doubleColumn; + if (tmp !== undefined) { + data.$doubleColumn = tmp; + } + + tmp = raw.dateColumn; + if (tmp !== undefined) { + data.$dateColumn = tmp; + } + + tmp = raw.dateTimeColumn; + if (tmp !== undefined) { + data.$dateTimeColumn = tmp; + } + + tmp = raw.timestampColumn; + if (tmp !== undefined) { + data.$timestampColumn = tmp; + } + + tmp = raw.timeColumn; + if (tmp !== undefined) { + data.$timeColumn = tmp; + } + + tmp = raw.yearColumn; + if (tmp !== undefined) { + data.$yearColumn = tmp; + } + + tmp = raw.varCharColumn; + if (tmp !== undefined) { + data.$varCharColumn = tmp; + } + + tmp = raw.binaryColumn; + if (tmp !== undefined) { + data.$binaryColumn = tmp; + } + + tmp = raw.varBinaryColumn; + if (tmp !== undefined) { + data.$varBinaryColumn = tmp; + } + + tmp = raw.tinyBlobColumn; + if (tmp !== undefined) { + data.$tinyBlobColumn = tmp; + } + + tmp = raw.tinyTextColumn; + if (tmp !== undefined) { + data.$tinyTextColumn = tmp; + } + + tmp = raw.blobColumn; + if (tmp !== undefined) { + data.$blobColumn = tmp; + } + + tmp = raw.textColumn; + if (tmp !== undefined) { + data.$textColumn = tmp; + } + + tmp = raw.mediumBlobColumn; + if (tmp !== undefined) { + data.$mediumBlobColumn = tmp; + } + + tmp = raw.longBlobColumn; + if (tmp !== undefined) { + data.$longBlobColumn = tmp; + } + + tmp = raw.mediumTextColumn; + if (tmp !== undefined) { + data.$mediumTextColumn = tmp; + } + + tmp = raw.longTextColumn; + if (tmp !== undefined) { + data.$longTextColumn = tmp; + } + + tmp = raw.enumColumn; + if (tmp !== undefined) { + data.$enumColumn = tmp; + } + + tmp = raw.setColumn; + if (tmp !== undefined) { + data.$setColumn = tmp; + } + + tmp = raw.geometryColumn; + if (tmp !== undefined) { + data.$geometryColumn = tmp; + } + + tmp = raw.pointColumn; + if (tmp !== undefined) { + data.$pointColumn = tmp; + } + + tmp = raw.lineStringColumn; + if (tmp !== undefined) { + data.$lineStringColumn = tmp; + } + + tmp = raw.polygonColumn; + if (tmp !== undefined) { + data.$polygonColumn = tmp; + } + + tmp = raw.multipointColumn; + if (tmp !== undefined) { + data.$multipointColumn = tmp; + } + + tmp = raw.multiLineStringColumn; + if (tmp !== undefined) { + data.$multiLineStringColumn = tmp; + } + + tmp = raw.multiPolygonColumn; + if (tmp !== undefined) { + data.$multiPolygonColumn = tmp; + } + + tmp = raw.geometryCollectionColumn; + if (tmp !== undefined) { + data.$geometryCollectionColumn = tmp; + } + + tmp = raw.jsonColumn; + if (tmp !== undefined) { + data.$jsonColumn = tmp; + } + + return this.dataSource.executeRawScalar('insert', data); + } + + public async update(id: number, data: Partial ): Promise { + + const newData: Record = { + primary: { + id, + }, + }; + let tmp; + + tmp = data.id; + if (tmp !== undefined) { + newData.$id = tmp; + } + + tmp = data.name; + if (tmp !== undefined) { + newData.$name = tmp; + } + + tmp = data.col1; + if (tmp !== undefined) { + newData.$col1 = tmp; + } + + tmp = data.bitColumn; + if (tmp !== undefined) { + newData.$bitColumn = tmp; + } + + tmp = data.boolColumn; + if (tmp !== undefined) { + newData.$boolColumn = tmp; + } + + tmp = data.tinyIntColumn; + if (tmp !== undefined) { + newData.$tinyIntColumn = tmp; + } + + tmp = data.smallIntColumn; + if (tmp !== undefined) { + newData.$smallIntColumn = tmp; + } + + tmp = data.mediumIntColumn; + if (tmp !== undefined) { + newData.$mediumIntColumn = tmp; + } + + tmp = data.intColumn; + if (tmp !== undefined) { + newData.$intColumn = tmp; + } + + tmp = data.bigIntColumn; + if (tmp !== undefined) { + newData.$bigIntColumn = tmp; + } + + tmp = data.decimalColumn; + if (tmp !== undefined) { + newData.$decimalColumn = tmp; + } + + tmp = data.floatColumn; + if (tmp !== undefined) { + newData.$floatColumn = tmp; + } + + tmp = data.doubleColumn; + if (tmp !== undefined) { + newData.$doubleColumn = tmp; + } + + tmp = data.dateColumn; + if (tmp !== undefined) { + newData.$dateColumn = tmp; + } + + tmp = data.dateTimeColumn; + if (tmp !== undefined) { + newData.$dateTimeColumn = tmp; + } + + tmp = data.timestampColumn; + if (tmp !== undefined) { + newData.$timestampColumn = tmp; + } + + tmp = data.timeColumn; + if (tmp !== undefined) { + newData.$timeColumn = tmp; + } + + tmp = data.yearColumn; + if (tmp !== undefined) { + newData.$yearColumn = tmp; + } + + tmp = data.varCharColumn; + if (tmp !== undefined) { + newData.$varCharColumn = tmp; + } + + tmp = data.binaryColumn; + if (tmp !== undefined) { + newData.$binaryColumn = tmp; + } + + tmp = data.varBinaryColumn; + if (tmp !== undefined) { + newData.$varBinaryColumn = tmp; + } + + tmp = data.tinyBlobColumn; + if (tmp !== undefined) { + newData.$tinyBlobColumn = tmp; + } + + tmp = data.tinyTextColumn; + if (tmp !== undefined) { + newData.$tinyTextColumn = tmp; + } + + tmp = data.blobColumn; + if (tmp !== undefined) { + newData.$blobColumn = tmp; + } + + tmp = data.textColumn; + if (tmp !== undefined) { + newData.$textColumn = tmp; + } + + tmp = data.mediumBlobColumn; + if (tmp !== undefined) { + newData.$mediumBlobColumn = tmp; + } + + tmp = data.longBlobColumn; + if (tmp !== undefined) { + newData.$longBlobColumn = tmp; + } + + tmp = data.mediumTextColumn; + if (tmp !== undefined) { + newData.$mediumTextColumn = tmp; + } + + tmp = data.longTextColumn; + if (tmp !== undefined) { + newData.$longTextColumn = tmp; + } + + tmp = data.enumColumn; + if (tmp !== undefined) { + newData.$enumColumn = tmp; + } + + tmp = data.setColumn; + if (tmp !== undefined) { + newData.$setColumn = tmp; + } + + tmp = data.geometryColumn; + if (tmp !== undefined) { + newData.$geometryColumn = tmp; + } + + tmp = data.pointColumn; + if (tmp !== undefined) { + newData.$pointColumn = tmp; + } + + tmp = data.lineStringColumn; + if (tmp !== undefined) { + newData.$lineStringColumn = tmp; + } + + tmp = data.polygonColumn; + if (tmp !== undefined) { + newData.$polygonColumn = tmp; + } + + tmp = data.multipointColumn; + if (tmp !== undefined) { + newData.$multipointColumn = tmp; + } + + tmp = data.multiLineStringColumn; + if (tmp !== undefined) { + newData.$multiLineStringColumn = tmp; + } + + tmp = data.multiPolygonColumn; + if (tmp !== undefined) { + newData.$multiPolygonColumn = tmp; + } + + tmp = data.geometryCollectionColumn; + if (tmp !== undefined) { + newData.$geometryCollectionColumn = tmp; + } + + tmp = data.jsonColumn; + if (tmp !== undefined) { + newData.$jsonColumn = tmp; + } + + return this.dataSource.executeRawScalar('update', newData); + } + + public async delete(id: number): Promise { + return this.dataSource.executeRawScalar('delete', { + id, + }); + } + + public async del(id: number): Promise { + return this.dataSource.executeRawScalar('delete', { + id, + }); + } + + public async findByCol1($col1: string): Promise { + return this.dataSource.execute('findByCol1', { + $col1, + }); + } + + public async findOneByCol1($col1: string): Promise { + return this.dataSource.executeScalar('findOneByCol1', { + $col1, + }); + } + + public async findByUkNameCol1($name: string, $col1: string): Promise { + return this.dataSource.execute('findByUkNameCol1', { + $name, + $col1, + }); + } + + public async findOneByUkNameCol1($name: string, $col1: string): Promise { + return this.dataSource.executeScalar('findOneByUkNameCol1', { + $name, + $col1, + }); + } + + public async findById($id: number): Promise { + return this.dataSource.executeScalar('findById', { + $id, + }); + } + + public async findByPrimary($id: number): Promise { + return this.dataSource.executeScalar('findById', { + $id, + }); + } +} diff --git a/core/dal-runtime/test/fixtures/modules/dal/dal/extension/FooExtension.ts b/core/dal-runtime/test/fixtures/modules/dal/dal/extension/FooExtension.ts new file mode 100644 index 00000000..61f4f89e --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/dal/dal/extension/FooExtension.ts @@ -0,0 +1,5 @@ +import { SqlMap } from '@eggjs/dal-decorator'; + +export default { + +} as Record; diff --git a/core/dal-runtime/test/fixtures/modules/dal/dal/structure/Foo.json b/core/dal-runtime/test/fixtures/modules/dal/dal/structure/Foo.json new file mode 100644 index 00000000..1edd9fff --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/dal/dal/structure/Foo.json @@ -0,0 +1,421 @@ +{ + "name": "egg_foo", + "dataSourceName": "default", + "columns": [ + { + "columnName": "id", + "propertyName": "id", + "type": { + "type": "INT" + }, + "canNull": false, + "comment": "the primary key", + "autoIncrement": true, + "primaryKey": true + }, + { + "columnName": "name", + "propertyName": "name", + "type": { + "type": "VARCHAR", + "length": 100 + }, + "canNull": true, + "uniqueKey": true + }, + { + "columnName": "col1", + "propertyName": "col1", + "type": { + "type": "VARCHAR", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "bit_column", + "propertyName": "bitColumn", + "type": { + "type": "BIT", + "length": 10 + }, + "canNull": true + }, + { + "columnName": "bool_column", + "propertyName": "boolColumn", + "type": { + "type": "BOOL" + }, + "canNull": true + }, + { + "columnName": "tiny_int_column", + "propertyName": "tinyIntColumn", + "type": { + "type": "TINYINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "small_int_column", + "propertyName": "smallIntColumn", + "type": { + "type": "SMALLINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "medium_int_column", + "propertyName": "mediumIntColumn", + "type": { + "type": "MEDIUMINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "int_column", + "propertyName": "intColumn", + "type": { + "type": "INT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "big_int_column", + "propertyName": "bigIntColumn", + "type": { + "type": "BIGINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "decimal_column", + "propertyName": "decimalColumn", + "type": { + "type": "DECIMAL", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "float_column", + "propertyName": "floatColumn", + "type": { + "type": "FLOAT", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "double_column", + "propertyName": "doubleColumn", + "type": { + "type": "DOUBLE", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "date_column", + "propertyName": "dateColumn", + "type": { + "type": "DATE" + }, + "canNull": true + }, + { + "columnName": "date_time_column", + "propertyName": "dateTimeColumn", + "type": { + "type": "DATETIME", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "timestamp_column", + "propertyName": "timestampColumn", + "type": { + "type": "TIMESTAMP", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "time_column", + "propertyName": "timeColumn", + "type": { + "type": "TIME", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "year_column", + "propertyName": "yearColumn", + "type": { + "type": "YEAR" + }, + "canNull": true + }, + { + "columnName": "var_char_column", + "propertyName": "varCharColumn", + "type": { + "type": "VARCHAR", + "length": 100, + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "binary_column", + "propertyName": "binaryColumn", + "type": { + "type": "BINARY" + }, + "canNull": true + }, + { + "columnName": "var_binary_column", + "propertyName": "varBinaryColumn", + "type": { + "type": "VARBINARY", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "tiny_blob_column", + "propertyName": "tinyBlobColumn", + "type": { + "type": "TINYBLOB" + }, + "canNull": true + }, + { + "columnName": "tiny_text_column", + "propertyName": "tinyTextColumn", + "type": { + "type": "TINYTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "blob_column", + "propertyName": "blobColumn", + "type": { + "type": "BLOB", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "text_column", + "propertyName": "textColumn", + "type": { + "type": "TEXT", + "length": 100, + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "medium_blob_column", + "propertyName": "mediumBlobColumn", + "type": { + "type": "MEDIUMBLOB" + }, + "canNull": true + }, + { + "columnName": "long_blob_column", + "propertyName": "longBlobColumn", + "type": { + "type": "LONGBLOB" + }, + "canNull": true + }, + { + "columnName": "medium_text_column", + "propertyName": "mediumTextColumn", + "type": { + "type": "MEDIUMTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "long_text_column", + "propertyName": "longTextColumn", + "type": { + "type": "LONGTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "enum_column", + "propertyName": "enumColumn", + "type": { + "type": "ENUM", + "enums": [ + "A", + "B" + ], + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "set_column", + "propertyName": "setColumn", + "type": { + "type": "SET", + "enums": [ + "A", + "B" + ], + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "geometry_column", + "propertyName": "geometryColumn", + "type": { + "type": "GEOMETRY" + }, + "canNull": true + }, + { + "columnName": "point_column", + "propertyName": "pointColumn", + "type": { + "type": "POINT" + }, + "canNull": true + }, + { + "columnName": "line_string_column", + "propertyName": "lineStringColumn", + "type": { + "type": "LINESTRING" + }, + "canNull": true + }, + { + "columnName": "polygon_column", + "propertyName": "polygonColumn", + "type": { + "type": "POLYGON" + }, + "canNull": true + }, + { + "columnName": "multipoint_column", + "propertyName": "multipointColumn", + "type": { + "type": "MULTIPOINT" + }, + "canNull": true + }, + { + "columnName": "multi_line_string_column", + "propertyName": "multiLineStringColumn", + "type": { + "type": "MULTILINESTRING" + }, + "canNull": true + }, + { + "columnName": "multi_polygon_column", + "propertyName": "multiPolygonColumn", + "type": { + "type": "MULTIPOLYGON" + }, + "canNull": true + }, + { + "columnName": "geometry_collection_column", + "propertyName": "geometryCollectionColumn", + "type": { + "type": "GEOMETRYCOLLECTION" + }, + "canNull": true + }, + { + "columnName": "json_column", + "propertyName": "jsonColumn", + "type": { + "type": "JSON" + }, + "canNull": true + } + ], + "indices": [ + { + "name": "idx_col1", + "keys": [ + { + "propertyName": "col1", + "columnName": "col1" + } + ], + "type": "FULLTEXT", + "comment": "index comment\n" + }, + { + "name": "uk_name_col1", + "keys": [ + { + "propertyName": "name", + "columnName": "name" + }, + { + "propertyName": "col1", + "columnName": "col1" + } + ], + "type": "UNIQUE", + "storeType": "BTREE", + "comment": "index comment\n" + } + ], + "comment": "foo table", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" +} \ No newline at end of file diff --git a/core/dal-runtime/test/fixtures/modules/dal/dal/structure/Foo.sql b/core/dal-runtime/test/fixtures/modules/dal/dal/structure/Foo.sql new file mode 100644 index 00000000..104f7260 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/dal/dal/structure/Foo.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS egg_foo ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'the primary key', + name VARCHAR(100) NULL UNIQUE KEY, + col1 VARCHAR(100) NULL, + bit_column BIT(10) NULL, + bool_column BOOL NULL, + tiny_int_column TINYINT(5) UNSIGNED ZEROFILL NULL, + small_int_column SMALLINT(5) UNSIGNED ZEROFILL NULL, + medium_int_column MEDIUMINT(5) UNSIGNED ZEROFILL NULL, + int_column INT(5) UNSIGNED ZEROFILL NULL, + big_int_column BIGINT(5) UNSIGNED ZEROFILL NULL, + decimal_column DECIMAL(10,5) UNSIGNED ZEROFILL NULL, + float_column FLOAT(10,5) UNSIGNED ZEROFILL NULL, + double_column DOUBLE(10,5) UNSIGNED ZEROFILL NULL, + date_column DATE NULL, + date_time_column DATETIME(3) NULL, + timestamp_column TIMESTAMP(3) NULL, + time_column TIME(3) NULL, + year_column YEAR NULL, + var_char_column VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + binary_column BINARY NULL, + var_binary_column VARBINARY(100) NULL, + tiny_blob_column TINYBLOB NULL, + tiny_text_column TINYTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + blob_column BLOB(100) NULL, + text_column TEXT(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + medium_blob_column MEDIUMBLOB NULL, + long_blob_column LONGBLOB NULL, + medium_text_column MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + long_text_column LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + enum_column ENUM('A','B') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + set_column SET('A','B') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + geometry_column GEOMETRY NULL, + point_column POINT NULL, + line_string_column LINESTRING NULL, + polygon_column POLYGON NULL, + multipoint_column MULTIPOINT NULL, + multi_line_string_column MULTILINESTRING NULL, + multi_polygon_column MULTIPOLYGON NULL, + geometry_collection_column GEOMETRYCOLLECTION NULL, + json_column JSON NULL, + FULLTEXT KEY idx_col1 (col1) COMMENT 'index comment\n', + UNIQUE KEY uk_name_col1 (name,col1) USING BTREE COMMENT 'index comment\n' +) DEFAULT CHARACTER SET utf8mb4, DEFAULT COLLATE utf8mb4_unicode_ci, COMMENT='foo table'; \ No newline at end of file diff --git a/core/dal-runtime/test/fixtures/modules/dal/package.json b/core/dal-runtime/test/fixtures/modules/dal/package.json new file mode 100644 index 00000000..1769916d --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/dal/package.json @@ -0,0 +1,6 @@ +{ + "name": "dal", + "eggModule": { + "name": "dal" + } +} diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/Foo.ts b/core/dal-runtime/test/fixtures/modules/generate_codes/Foo.ts new file mode 100644 index 00000000..cbf4cad8 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/Foo.ts @@ -0,0 +1,318 @@ +import { + Column, + ColumnType, + Geometry, + GeometryCollection, + Index, + IndexType, + Line, MultiLine, MultiPoint, MultiPolygon, Point, Polygon, + Table, +} from '@eggjs/dal-decorator'; +import { IndexStoreType } from '@eggjs/dal-decorator/src/enum/IndexStoreType'; + +@Table({ + name: 'egg_foo', + comment: 'foo table', + // autoExtendSize: 1024, + // autoIncrement: 100, + // avgRowLength: 1024, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + // compression: CompressionType.ZLIB, + // encryption: true, + // engine: 'NDB', + // engineAttribute: '{"key":"value"}', + // secondaryEngineAttribute: '{"key2":"value2"}', + // insertMethod: InsertMethod.FIRST, + // keyBlockSize: 1024, + // maxRows: 1000000, + // minRows: 100, + // rowFormat: RowFormat.COMPRESSED, +}) +@Index({ + keys: [ 'name', 'col1' ], + type: IndexType.UNIQUE, + storeType: IndexStoreType.BTREE, + comment: 'index comment\n', + // engineAttribute: '{"key":"value"}', + // secondaryEngineAttribute: '{"key2":"value2"}', +}) +@Index({ + keys: [ 'col1' ], + type: IndexType.FULLTEXT, + comment: 'index comment\n', + // engineAttribute: '{"key":"value"}', + // secondaryEngineAttribute: '{"key2":"value2"}', + // parser: 'foo', +}) +export class Foo { + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + autoIncrement: true, + comment: 'the primary key', + }) + id: number; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }, { + uniqueKey: true, + }) + name: string; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }, { + name: 'col1', + }) + col1: string; + + @Column({ + type: ColumnType.BIT, + length: 10, + }) + bitColumn: Buffer; + + @Column({ + type: ColumnType.BOOL, + }) + boolColumn: 0 | 1; + + @Column({ + type: ColumnType.TINYINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + tinyIntColumn: number; + + @Column({ + type: ColumnType.SMALLINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + smallIntColumn: number; + + @Column({ + type: ColumnType.MEDIUMINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + mediumIntColumn: number; + + @Column({ + type: ColumnType.INT, + length: 5, + unsigned: true, + zeroFill: true, + }) + intColumn: number; + + @Column({ + type: ColumnType.BIGINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + bigIntColumn: string; + + @Column({ + type: ColumnType.DECIMAL, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + decimalColumn: string; + + @Column({ + type: ColumnType.FLOAT, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + floatColumn: number; + + @Column({ + type: ColumnType.DOUBLE, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + doubleColumn: number; + + @Column({ + type: ColumnType.DATE, + }) + dateColumn: Date; + + @Column({ + type: ColumnType.DATETIME, + precision: 3, + }) + dateTimeColumn: Date; + + @Column({ + type: ColumnType.TIMESTAMP, + precision: 3, + }) + timestampColumn: Date; + + @Column({ + type: ColumnType.TIME, + precision: 3, + }) + timeColumn: string; + + @Column({ + type: ColumnType.YEAR, + }) + yearColumn: number; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + varCharColumn: string; + + @Column({ + type: ColumnType.BINARY, + }) + binaryColumn: Buffer; + + @Column({ + type: ColumnType.VARBINARY, + length: 100, + }) + varBinaryColumn: Buffer; + + @Column({ + type: ColumnType.TINYBLOB, + }) + tinyBlobColumn: Buffer; + + @Column({ + type: ColumnType.TINYTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + tinyTextColumn: string; + + @Column({ + type: ColumnType.BLOB, + length: 100, + }) + blobColumn: Buffer; + + @Column({ + type: ColumnType.TEXT, + length: 100, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + textColumn: string; + + @Column({ + type: ColumnType.MEDIUMBLOB, + }) + mediumBlobColumn: Buffer; + + @Column({ + type: ColumnType.LONGBLOB, + }) + longBlobColumn: Buffer; + + @Column({ + type: ColumnType.MEDIUMTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + mediumTextColumn: string; + + @Column({ + type: ColumnType.LONGTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + longTextColumn: string; + + @Column({ + type: ColumnType.ENUM, + enums: [ 'A', 'B' ], + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + enumColumn: string; + + @Column({ + type: ColumnType.SET, + enums: [ 'A', 'B' ], + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + setColumn: string; + + @Column({ + type: ColumnType.GEOMETRY, + // SRID: 4326, + }) + geometryColumn: Geometry; + + + @Column({ + type: ColumnType.POINT, + // SRID: 4326, + }) + pointColumn: Point; + + @Column({ + type: ColumnType.LINESTRING, + // SRID: 4326, + }) + lineStringColumn: Line; + + @Column({ + type: ColumnType.POLYGON, + // SRID: 4326, + }) + polygonColumn: Polygon; + + @Column({ + type: ColumnType.MULTIPOINT, + // SRID: 4326, + }) + multipointColumn: MultiPoint; + + @Column({ + type: ColumnType.MULTILINESTRING, + // SRID: 4326, + }) + multiLineStringColumn: MultiLine; + + @Column({ + type: ColumnType.MULTIPOLYGON, + // SRID: 4326, + }) + multiPolygonColumn: MultiPolygon; + + @Column({ + type: ColumnType.GEOMETRYCOLLECTION, + // SRID: 4326, + }) + geometryCollectionColumn: GeometryCollection; + + @Column({ + type: ColumnType.JSON, + }) + jsonColumn: object; +} diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/MultiPrimaryKey.ts b/core/dal-runtime/test/fixtures/modules/generate_codes/MultiPrimaryKey.ts new file mode 100644 index 00000000..14af1a15 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/MultiPrimaryKey.ts @@ -0,0 +1,38 @@ +import { + Column, + ColumnType, + Table, +} from '@eggjs/dal-decorator'; + +@Table({ + name: 'multi_primary_key_table', + comment: 'multi primary key table', +}) +export class MultiPrimaryKey { + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + autoIncrement: true, + comment: 'the primary key', + }) + id1: number; + + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + autoIncrement: true, + comment: 'the primary key', + }) + id2: number; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }, { + uniqueKey: true, + }) + name: string; + +} diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/FooDAO.ts b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/FooDAO.ts new file mode 100644 index 00000000..5d4dfb9f --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/FooDAO.ts @@ -0,0 +1,15 @@ +import { SingletonProto, AccessLevel } from '@eggjs/tegg'; +import { BaseFooDAO } from './base/BaseFooDAO'; + +/** + * FooDAO 类 + * @class FooDAO + * @classdesc 在此扩展关于 Foo 数据的一切操作 + * @extends BaseFooDAO + */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export default class FooDAO extends BaseFooDAO { + +} diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/MultiPrimaryKeyDAO.ts b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/MultiPrimaryKeyDAO.ts new file mode 100644 index 00000000..c6649cf4 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/MultiPrimaryKeyDAO.ts @@ -0,0 +1,15 @@ +import { SingletonProto, AccessLevel } from '@eggjs/tegg'; +import { BaseMultiPrimaryKeyDAO } from './base/BaseMultiPrimaryKeyDAO'; + +/** + * MultiPrimaryKeyDAO 类 + * @class MultiPrimaryKeyDAO + * @classdesc 在此扩展关于 MultiPrimaryKey 数据的一切操作 + * @extends BaseMultiPrimaryKeyDAO + */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export default class MultiPrimaryKeyDAO extends BaseMultiPrimaryKeyDAO { + +} diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/base/BaseFooDAO.ts b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/base/BaseFooDAO.ts new file mode 100644 index 00000000..37e63098 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/base/BaseFooDAO.ts @@ -0,0 +1,492 @@ +import type { InsertResult, UpdateResult, DeleteResult } from '@eggjs/rds/lib/types'; +import { SingletonProto, AccessLevel, Inject } from '@eggjs/tegg'; +import { DataSource, DataSourceInjectName, DataSourceQualifier, ColumnTsType } from '@eggjs/tegg/dal'; +import { Foo } from '../../../Foo'; + +type Optional = Omit < T, K > & Partial ; +/** + * 自动生成的 FooDAO 基类 + * @class BaseFooDAO + * @classdesc 该文件由 @eggjs/tegg 自动生成,请**不要**修改它! + */ +/* istanbul ignore next */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class BaseFooDAO { + static clazzModel = Foo; + @Inject({ + name: DataSourceInjectName, + }) + @DataSourceQualifier('dal.default.Foo') + protected readonly dataSource: DataSource ; + + public async insert(raw: Optional ): Promise { + const data: Record = {}; + let tmp; + + tmp = raw.id; + if (tmp !== undefined) { + data.$id = tmp; + } + + tmp = raw.name; + if (tmp !== undefined) { + data.$name = tmp; + } + + tmp = raw.col1; + if (tmp !== undefined) { + data.$col1 = tmp; + } + + tmp = raw.bitColumn; + if (tmp !== undefined) { + data.$bitColumn = tmp; + } + + tmp = raw.boolColumn; + if (tmp !== undefined) { + data.$boolColumn = tmp; + } + + tmp = raw.tinyIntColumn; + if (tmp !== undefined) { + data.$tinyIntColumn = tmp; + } + + tmp = raw.smallIntColumn; + if (tmp !== undefined) { + data.$smallIntColumn = tmp; + } + + tmp = raw.mediumIntColumn; + if (tmp !== undefined) { + data.$mediumIntColumn = tmp; + } + + tmp = raw.intColumn; + if (tmp !== undefined) { + data.$intColumn = tmp; + } + + tmp = raw.bigIntColumn; + if (tmp !== undefined) { + data.$bigIntColumn = tmp; + } + + tmp = raw.decimalColumn; + if (tmp !== undefined) { + data.$decimalColumn = tmp; + } + + tmp = raw.floatColumn; + if (tmp !== undefined) { + data.$floatColumn = tmp; + } + + tmp = raw.doubleColumn; + if (tmp !== undefined) { + data.$doubleColumn = tmp; + } + + tmp = raw.dateColumn; + if (tmp !== undefined) { + data.$dateColumn = tmp; + } + + tmp = raw.dateTimeColumn; + if (tmp !== undefined) { + data.$dateTimeColumn = tmp; + } + + tmp = raw.timestampColumn; + if (tmp !== undefined) { + data.$timestampColumn = tmp; + } + + tmp = raw.timeColumn; + if (tmp !== undefined) { + data.$timeColumn = tmp; + } + + tmp = raw.yearColumn; + if (tmp !== undefined) { + data.$yearColumn = tmp; + } + + tmp = raw.varCharColumn; + if (tmp !== undefined) { + data.$varCharColumn = tmp; + } + + tmp = raw.binaryColumn; + if (tmp !== undefined) { + data.$binaryColumn = tmp; + } + + tmp = raw.varBinaryColumn; + if (tmp !== undefined) { + data.$varBinaryColumn = tmp; + } + + tmp = raw.tinyBlobColumn; + if (tmp !== undefined) { + data.$tinyBlobColumn = tmp; + } + + tmp = raw.tinyTextColumn; + if (tmp !== undefined) { + data.$tinyTextColumn = tmp; + } + + tmp = raw.blobColumn; + if (tmp !== undefined) { + data.$blobColumn = tmp; + } + + tmp = raw.textColumn; + if (tmp !== undefined) { + data.$textColumn = tmp; + } + + tmp = raw.mediumBlobColumn; + if (tmp !== undefined) { + data.$mediumBlobColumn = tmp; + } + + tmp = raw.longBlobColumn; + if (tmp !== undefined) { + data.$longBlobColumn = tmp; + } + + tmp = raw.mediumTextColumn; + if (tmp !== undefined) { + data.$mediumTextColumn = tmp; + } + + tmp = raw.longTextColumn; + if (tmp !== undefined) { + data.$longTextColumn = tmp; + } + + tmp = raw.enumColumn; + if (tmp !== undefined) { + data.$enumColumn = tmp; + } + + tmp = raw.setColumn; + if (tmp !== undefined) { + data.$setColumn = tmp; + } + + tmp = raw.geometryColumn; + if (tmp !== undefined) { + data.$geometryColumn = tmp; + } + + tmp = raw.pointColumn; + if (tmp !== undefined) { + data.$pointColumn = tmp; + } + + tmp = raw.lineStringColumn; + if (tmp !== undefined) { + data.$lineStringColumn = tmp; + } + + tmp = raw.polygonColumn; + if (tmp !== undefined) { + data.$polygonColumn = tmp; + } + + tmp = raw.multipointColumn; + if (tmp !== undefined) { + data.$multipointColumn = tmp; + } + + tmp = raw.multiLineStringColumn; + if (tmp !== undefined) { + data.$multiLineStringColumn = tmp; + } + + tmp = raw.multiPolygonColumn; + if (tmp !== undefined) { + data.$multiPolygonColumn = tmp; + } + + tmp = raw.geometryCollectionColumn; + if (tmp !== undefined) { + data.$geometryCollectionColumn = tmp; + } + + tmp = raw.jsonColumn; + if (tmp !== undefined) { + data.$jsonColumn = tmp; + } + + return this.dataSource.executeRawScalar('insert', data); + } + + public async update(id: ColumnTsType['INT'], data: Partial ): Promise { + + const newData: Record = { + primary: { + id, + }, + }; + let tmp; + + tmp = data.id; + if (tmp !== undefined) { + newData.$id = tmp; + } + + tmp = data.name; + if (tmp !== undefined) { + newData.$name = tmp; + } + + tmp = data.col1; + if (tmp !== undefined) { + newData.$col1 = tmp; + } + + tmp = data.bitColumn; + if (tmp !== undefined) { + newData.$bitColumn = tmp; + } + + tmp = data.boolColumn; + if (tmp !== undefined) { + newData.$boolColumn = tmp; + } + + tmp = data.tinyIntColumn; + if (tmp !== undefined) { + newData.$tinyIntColumn = tmp; + } + + tmp = data.smallIntColumn; + if (tmp !== undefined) { + newData.$smallIntColumn = tmp; + } + + tmp = data.mediumIntColumn; + if (tmp !== undefined) { + newData.$mediumIntColumn = tmp; + } + + tmp = data.intColumn; + if (tmp !== undefined) { + newData.$intColumn = tmp; + } + + tmp = data.bigIntColumn; + if (tmp !== undefined) { + newData.$bigIntColumn = tmp; + } + + tmp = data.decimalColumn; + if (tmp !== undefined) { + newData.$decimalColumn = tmp; + } + + tmp = data.floatColumn; + if (tmp !== undefined) { + newData.$floatColumn = tmp; + } + + tmp = data.doubleColumn; + if (tmp !== undefined) { + newData.$doubleColumn = tmp; + } + + tmp = data.dateColumn; + if (tmp !== undefined) { + newData.$dateColumn = tmp; + } + + tmp = data.dateTimeColumn; + if (tmp !== undefined) { + newData.$dateTimeColumn = tmp; + } + + tmp = data.timestampColumn; + if (tmp !== undefined) { + newData.$timestampColumn = tmp; + } + + tmp = data.timeColumn; + if (tmp !== undefined) { + newData.$timeColumn = tmp; + } + + tmp = data.yearColumn; + if (tmp !== undefined) { + newData.$yearColumn = tmp; + } + + tmp = data.varCharColumn; + if (tmp !== undefined) { + newData.$varCharColumn = tmp; + } + + tmp = data.binaryColumn; + if (tmp !== undefined) { + newData.$binaryColumn = tmp; + } + + tmp = data.varBinaryColumn; + if (tmp !== undefined) { + newData.$varBinaryColumn = tmp; + } + + tmp = data.tinyBlobColumn; + if (tmp !== undefined) { + newData.$tinyBlobColumn = tmp; + } + + tmp = data.tinyTextColumn; + if (tmp !== undefined) { + newData.$tinyTextColumn = tmp; + } + + tmp = data.blobColumn; + if (tmp !== undefined) { + newData.$blobColumn = tmp; + } + + tmp = data.textColumn; + if (tmp !== undefined) { + newData.$textColumn = tmp; + } + + tmp = data.mediumBlobColumn; + if (tmp !== undefined) { + newData.$mediumBlobColumn = tmp; + } + + tmp = data.longBlobColumn; + if (tmp !== undefined) { + newData.$longBlobColumn = tmp; + } + + tmp = data.mediumTextColumn; + if (tmp !== undefined) { + newData.$mediumTextColumn = tmp; + } + + tmp = data.longTextColumn; + if (tmp !== undefined) { + newData.$longTextColumn = tmp; + } + + tmp = data.enumColumn; + if (tmp !== undefined) { + newData.$enumColumn = tmp; + } + + tmp = data.setColumn; + if (tmp !== undefined) { + newData.$setColumn = tmp; + } + + tmp = data.geometryColumn; + if (tmp !== undefined) { + newData.$geometryColumn = tmp; + } + + tmp = data.pointColumn; + if (tmp !== undefined) { + newData.$pointColumn = tmp; + } + + tmp = data.lineStringColumn; + if (tmp !== undefined) { + newData.$lineStringColumn = tmp; + } + + tmp = data.polygonColumn; + if (tmp !== undefined) { + newData.$polygonColumn = tmp; + } + + tmp = data.multipointColumn; + if (tmp !== undefined) { + newData.$multipointColumn = tmp; + } + + tmp = data.multiLineStringColumn; + if (tmp !== undefined) { + newData.$multiLineStringColumn = tmp; + } + + tmp = data.multiPolygonColumn; + if (tmp !== undefined) { + newData.$multiPolygonColumn = tmp; + } + + tmp = data.geometryCollectionColumn; + if (tmp !== undefined) { + newData.$geometryCollectionColumn = tmp; + } + + tmp = data.jsonColumn; + if (tmp !== undefined) { + newData.$jsonColumn = tmp; + } + + return this.dataSource.executeRawScalar('update', newData); + } + + public async delete(id: ColumnTsType['INT']): Promise { + return this.dataSource.executeRawScalar('delete', { + id, + }); + } + + public async del(id: ColumnTsType['INT']): Promise { + return this.dataSource.executeRawScalar('delete', { + id, + }); + } + + public async findByCol1($col1: ColumnTsType['VARCHAR']): Promise { + return this.dataSource.execute('findByCol1', { + $col1, + }); + } + + public async findOneByCol1($col1: ColumnTsType['VARCHAR']): Promise { + return this.dataSource.executeScalar('findOneByCol1', { + $col1, + }); + } + + public async findByUkNameCol1($name: ColumnTsType['VARCHAR'], $col1: ColumnTsType['VARCHAR']): Promise { + return this.dataSource.execute('findByUkNameCol1', { + $name, + $col1, + }); + } + + public async findOneByUkNameCol1($name: ColumnTsType['VARCHAR'], $col1: ColumnTsType['VARCHAR']): Promise { + return this.dataSource.executeScalar('findOneByUkNameCol1', { + $name, + $col1, + }); + } + + public async findById($id: ColumnTsType['INT']): Promise { + return this.dataSource.executeScalar('findById', { + $id, + }); + } + + public async findByPrimary($id: ColumnTsType['INT']): Promise { + return this.dataSource.executeScalar('findById', { + $id, + }); + } +} diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/base/BaseMultiPrimaryKeyDAO.ts b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/base/BaseMultiPrimaryKeyDAO.ts new file mode 100644 index 00000000..355c3bfe --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/dao/base/BaseMultiPrimaryKeyDAO.ts @@ -0,0 +1,94 @@ +import type { InsertResult, UpdateResult, DeleteResult } from '@eggjs/rds/lib/types'; +import { SingletonProto, AccessLevel, Inject } from '@eggjs/tegg'; +import { DataSource, DataSourceInjectName, DataSourceQualifier, ColumnTsType } from '@eggjs/tegg/dal'; +import { MultiPrimaryKey } from '../../../MultiPrimaryKey'; + +type Optional = Omit < T, K > & Partial ; +/** + * 自动生成的 MultiPrimaryKeyDAO 基类 + * @class BaseMultiPrimaryKeyDAO + * @classdesc 该文件由 @eggjs/tegg 自动生成,请**不要**修改它! + */ +/* istanbul ignore next */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class BaseMultiPrimaryKeyDAO { + static clazzModel = MultiPrimaryKey; + @Inject({ + name: DataSourceInjectName, + }) + @DataSourceQualifier('dal.default.MultiPrimaryKey') + protected readonly dataSource: DataSource ; + + public async insert(raw: Optional ): Promise { + const data: Record = {}; + let tmp; + + tmp = raw.id1; + if (tmp !== undefined) { + data.$id1 = tmp; + } + + tmp = raw.id2; + if (tmp !== undefined) { + data.$id2 = tmp; + } + + tmp = raw.name; + if (tmp !== undefined) { + data.$name = tmp; + } + + return this.dataSource.executeRawScalar('insert', data); + } + + public async update(primary: { + id1: ColumnTsType['INT'], + id2: ColumnTsType['INT'] + }, data: Partial ): Promise { + + const newData: Record = { + primary, + }; + let tmp; + + tmp = data.id1; + if (tmp !== undefined) { + newData.$id1 = tmp; + } + + tmp = data.id2; + if (tmp !== undefined) { + newData.$id2 = tmp; + } + + tmp = data.name; + if (tmp !== undefined) { + newData.$name = tmp; + } + + return this.dataSource.executeRawScalar('update', newData); + } + + public async delete(primary: { + id1: ColumnTsType['INT'], + id2: ColumnTsType['INT'] + }): Promise { + return this.dataSource.executeRawScalar('delete', primary); + } + + public async del(primary: { + id1: ColumnTsType['INT'], + id2: ColumnTsType['INT'] + }): Promise { + return this.dataSource.executeRawScalar('delete', primary); + } + + public async findByPrimary($id1: ColumnTsType['INT'], $id2: ColumnTsType['INT']): Promise { + return this.dataSource.executeScalar('findByPrimary', { + $id1, + $id2, + }); + } +} diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/extension/FooExtension.ts b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/extension/FooExtension.ts new file mode 100644 index 00000000..24c2e288 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/extension/FooExtension.ts @@ -0,0 +1,17 @@ +import { SqlMap } from '@eggjs/tegg/dal'; + +/** + * Define Custom SQLs + * + * import { SqlMap, SqlType } from '@eggjs/tegg/dal'; + * + * export default { + * findByName: { + * type: SqlType.SELECT, + * sql: 'SELECT from foo where name = ' + * }, + * } + */ +export default { + +} as Record; diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/extension/MultiPrimaryKeyExtension.ts b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/extension/MultiPrimaryKeyExtension.ts new file mode 100644 index 00000000..24c2e288 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/extension/MultiPrimaryKeyExtension.ts @@ -0,0 +1,17 @@ +import { SqlMap } from '@eggjs/tegg/dal'; + +/** + * Define Custom SQLs + * + * import { SqlMap, SqlType } from '@eggjs/tegg/dal'; + * + * export default { + * findByName: { + * type: SqlType.SELECT, + * sql: 'SELECT from foo where name = ' + * }, + * } + */ +export default { + +} as Record; diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/Foo.json b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/Foo.json new file mode 100644 index 00000000..1edd9fff --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/Foo.json @@ -0,0 +1,421 @@ +{ + "name": "egg_foo", + "dataSourceName": "default", + "columns": [ + { + "columnName": "id", + "propertyName": "id", + "type": { + "type": "INT" + }, + "canNull": false, + "comment": "the primary key", + "autoIncrement": true, + "primaryKey": true + }, + { + "columnName": "name", + "propertyName": "name", + "type": { + "type": "VARCHAR", + "length": 100 + }, + "canNull": true, + "uniqueKey": true + }, + { + "columnName": "col1", + "propertyName": "col1", + "type": { + "type": "VARCHAR", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "bit_column", + "propertyName": "bitColumn", + "type": { + "type": "BIT", + "length": 10 + }, + "canNull": true + }, + { + "columnName": "bool_column", + "propertyName": "boolColumn", + "type": { + "type": "BOOL" + }, + "canNull": true + }, + { + "columnName": "tiny_int_column", + "propertyName": "tinyIntColumn", + "type": { + "type": "TINYINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "small_int_column", + "propertyName": "smallIntColumn", + "type": { + "type": "SMALLINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "medium_int_column", + "propertyName": "mediumIntColumn", + "type": { + "type": "MEDIUMINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "int_column", + "propertyName": "intColumn", + "type": { + "type": "INT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "big_int_column", + "propertyName": "bigIntColumn", + "type": { + "type": "BIGINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "decimal_column", + "propertyName": "decimalColumn", + "type": { + "type": "DECIMAL", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "float_column", + "propertyName": "floatColumn", + "type": { + "type": "FLOAT", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "double_column", + "propertyName": "doubleColumn", + "type": { + "type": "DOUBLE", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "date_column", + "propertyName": "dateColumn", + "type": { + "type": "DATE" + }, + "canNull": true + }, + { + "columnName": "date_time_column", + "propertyName": "dateTimeColumn", + "type": { + "type": "DATETIME", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "timestamp_column", + "propertyName": "timestampColumn", + "type": { + "type": "TIMESTAMP", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "time_column", + "propertyName": "timeColumn", + "type": { + "type": "TIME", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "year_column", + "propertyName": "yearColumn", + "type": { + "type": "YEAR" + }, + "canNull": true + }, + { + "columnName": "var_char_column", + "propertyName": "varCharColumn", + "type": { + "type": "VARCHAR", + "length": 100, + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "binary_column", + "propertyName": "binaryColumn", + "type": { + "type": "BINARY" + }, + "canNull": true + }, + { + "columnName": "var_binary_column", + "propertyName": "varBinaryColumn", + "type": { + "type": "VARBINARY", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "tiny_blob_column", + "propertyName": "tinyBlobColumn", + "type": { + "type": "TINYBLOB" + }, + "canNull": true + }, + { + "columnName": "tiny_text_column", + "propertyName": "tinyTextColumn", + "type": { + "type": "TINYTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "blob_column", + "propertyName": "blobColumn", + "type": { + "type": "BLOB", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "text_column", + "propertyName": "textColumn", + "type": { + "type": "TEXT", + "length": 100, + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "medium_blob_column", + "propertyName": "mediumBlobColumn", + "type": { + "type": "MEDIUMBLOB" + }, + "canNull": true + }, + { + "columnName": "long_blob_column", + "propertyName": "longBlobColumn", + "type": { + "type": "LONGBLOB" + }, + "canNull": true + }, + { + "columnName": "medium_text_column", + "propertyName": "mediumTextColumn", + "type": { + "type": "MEDIUMTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "long_text_column", + "propertyName": "longTextColumn", + "type": { + "type": "LONGTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "enum_column", + "propertyName": "enumColumn", + "type": { + "type": "ENUM", + "enums": [ + "A", + "B" + ], + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "set_column", + "propertyName": "setColumn", + "type": { + "type": "SET", + "enums": [ + "A", + "B" + ], + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "geometry_column", + "propertyName": "geometryColumn", + "type": { + "type": "GEOMETRY" + }, + "canNull": true + }, + { + "columnName": "point_column", + "propertyName": "pointColumn", + "type": { + "type": "POINT" + }, + "canNull": true + }, + { + "columnName": "line_string_column", + "propertyName": "lineStringColumn", + "type": { + "type": "LINESTRING" + }, + "canNull": true + }, + { + "columnName": "polygon_column", + "propertyName": "polygonColumn", + "type": { + "type": "POLYGON" + }, + "canNull": true + }, + { + "columnName": "multipoint_column", + "propertyName": "multipointColumn", + "type": { + "type": "MULTIPOINT" + }, + "canNull": true + }, + { + "columnName": "multi_line_string_column", + "propertyName": "multiLineStringColumn", + "type": { + "type": "MULTILINESTRING" + }, + "canNull": true + }, + { + "columnName": "multi_polygon_column", + "propertyName": "multiPolygonColumn", + "type": { + "type": "MULTIPOLYGON" + }, + "canNull": true + }, + { + "columnName": "geometry_collection_column", + "propertyName": "geometryCollectionColumn", + "type": { + "type": "GEOMETRYCOLLECTION" + }, + "canNull": true + }, + { + "columnName": "json_column", + "propertyName": "jsonColumn", + "type": { + "type": "JSON" + }, + "canNull": true + } + ], + "indices": [ + { + "name": "idx_col1", + "keys": [ + { + "propertyName": "col1", + "columnName": "col1" + } + ], + "type": "FULLTEXT", + "comment": "index comment\n" + }, + { + "name": "uk_name_col1", + "keys": [ + { + "propertyName": "name", + "columnName": "name" + }, + { + "propertyName": "col1", + "columnName": "col1" + } + ], + "type": "UNIQUE", + "storeType": "BTREE", + "comment": "index comment\n" + } + ], + "comment": "foo table", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" +} \ No newline at end of file diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/Foo.sql b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/Foo.sql new file mode 100644 index 00000000..104f7260 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/Foo.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS egg_foo ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'the primary key', + name VARCHAR(100) NULL UNIQUE KEY, + col1 VARCHAR(100) NULL, + bit_column BIT(10) NULL, + bool_column BOOL NULL, + tiny_int_column TINYINT(5) UNSIGNED ZEROFILL NULL, + small_int_column SMALLINT(5) UNSIGNED ZEROFILL NULL, + medium_int_column MEDIUMINT(5) UNSIGNED ZEROFILL NULL, + int_column INT(5) UNSIGNED ZEROFILL NULL, + big_int_column BIGINT(5) UNSIGNED ZEROFILL NULL, + decimal_column DECIMAL(10,5) UNSIGNED ZEROFILL NULL, + float_column FLOAT(10,5) UNSIGNED ZEROFILL NULL, + double_column DOUBLE(10,5) UNSIGNED ZEROFILL NULL, + date_column DATE NULL, + date_time_column DATETIME(3) NULL, + timestamp_column TIMESTAMP(3) NULL, + time_column TIME(3) NULL, + year_column YEAR NULL, + var_char_column VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + binary_column BINARY NULL, + var_binary_column VARBINARY(100) NULL, + tiny_blob_column TINYBLOB NULL, + tiny_text_column TINYTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + blob_column BLOB(100) NULL, + text_column TEXT(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + medium_blob_column MEDIUMBLOB NULL, + long_blob_column LONGBLOB NULL, + medium_text_column MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + long_text_column LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + enum_column ENUM('A','B') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + set_column SET('A','B') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + geometry_column GEOMETRY NULL, + point_column POINT NULL, + line_string_column LINESTRING NULL, + polygon_column POLYGON NULL, + multipoint_column MULTIPOINT NULL, + multi_line_string_column MULTILINESTRING NULL, + multi_polygon_column MULTIPOLYGON NULL, + geometry_collection_column GEOMETRYCOLLECTION NULL, + json_column JSON NULL, + FULLTEXT KEY idx_col1 (col1) COMMENT 'index comment\n', + UNIQUE KEY uk_name_col1 (name,col1) USING BTREE COMMENT 'index comment\n' +) DEFAULT CHARACTER SET utf8mb4, DEFAULT COLLATE utf8mb4_unicode_ci, COMMENT='foo table'; \ No newline at end of file diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/MultiPrimaryKey.json b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/MultiPrimaryKey.json new file mode 100644 index 00000000..8d4e19e7 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/MultiPrimaryKey.json @@ -0,0 +1,40 @@ +{ + "name": "multi_primary_key_table", + "dataSourceName": "default", + "columns": [ + { + "columnName": "id_1", + "propertyName": "id1", + "type": { + "type": "INT" + }, + "canNull": false, + "comment": "the primary key", + "autoIncrement": true, + "primaryKey": true + }, + { + "columnName": "id_2", + "propertyName": "id2", + "type": { + "type": "INT" + }, + "canNull": false, + "comment": "the primary key", + "autoIncrement": true, + "primaryKey": true + }, + { + "columnName": "name", + "propertyName": "name", + "type": { + "type": "VARCHAR", + "length": 100 + }, + "canNull": true, + "uniqueKey": true + } + ], + "indices": [], + "comment": "multi primary key table" +} \ No newline at end of file diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/MultiPrimaryKey.sql b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/MultiPrimaryKey.sql new file mode 100644 index 00000000..4cbfedf3 --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/dal/structure/MultiPrimaryKey.sql @@ -0,0 +1,5 @@ +CREATE TABLE IF NOT EXISTS multi_primary_key_table ( + id_1 INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'the primary key', + id_2 INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'the primary key', + name VARCHAR(100) NULL UNIQUE KEY +) COMMENT='multi primary key table'; \ No newline at end of file diff --git a/core/dal-runtime/test/fixtures/modules/generate_codes/package.json b/core/dal-runtime/test/fixtures/modules/generate_codes/package.json new file mode 100644 index 00000000..1769916d --- /dev/null +++ b/core/dal-runtime/test/fixtures/modules/generate_codes/package.json @@ -0,0 +1,6 @@ +{ + "name": "dal", + "eggModule": { + "name": "dal" + } +} diff --git a/core/dal-runtime/tsconfig.json b/core/dal-runtime/tsconfig.json new file mode 100644 index 00000000..00f8bd8e --- /dev/null +++ b/core/dal-runtime/tsconfig.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "baseUrl": "./", + "target": "ES2020" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/core/dal-runtime/tsconfig.pub.json b/core/dal-runtime/tsconfig.pub.json new file mode 100644 index 00000000..00f8bd8e --- /dev/null +++ b/core/dal-runtime/tsconfig.pub.json @@ -0,0 +1,13 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "dist", + "baseUrl": "./", + "target": "ES2020" + }, + "exclude": [ + "dist", + "node_modules", + "test" + ] +} diff --git a/core/metadata/src/factory/EggPrototypeCreatorFactory.ts b/core/metadata/src/factory/EggPrototypeCreatorFactory.ts index a3efb42f..add7e751 100644 --- a/core/metadata/src/factory/EggPrototypeCreatorFactory.ts +++ b/core/metadata/src/factory/EggPrototypeCreatorFactory.ts @@ -20,6 +20,7 @@ export class EggPrototypeCreatorFactory { if (PrototypeUtil.isEggMultiInstancePrototype(clazz)) { const multiInstanceProtoInfo = PrototypeUtil.getMultiInstanceProperty(clazz, { unitPath: loadUnit.unitPath, + moduleName: loadUnit.name, })!; for (const obj of multiInstanceProtoInfo.objects) { properties.push({ diff --git a/core/metadata/src/impl/ModuleLoadUnit.ts b/core/metadata/src/impl/ModuleLoadUnit.ts index 5f020cfd..371522f6 100644 --- a/core/metadata/src/impl/ModuleLoadUnit.ts +++ b/core/metadata/src/impl/ModuleLoadUnit.ts @@ -32,13 +32,14 @@ class ProtoNode implements GraphNodeObj { readonly qualifiers: QualifierInfo[]; readonly initType: ObjectInitTypeLike; - constructor(clazz: EggProtoImplClass, objName: EggPrototypeName, unitPath: string) { + constructor(clazz: EggProtoImplClass, objName: EggPrototypeName, unitPath: string, moduleName: string) { this.name = objName; this.id = '' + (id++); this.clazz = clazz; this.qualifiers = QualifierUtil.getProtoQualifiers(clazz); this.initType = PrototypeUtil.getInitType(clazz, { unitPath, + moduleName, })!; } @@ -65,11 +66,13 @@ export class ModuleGraph { private graph: Graph; clazzList: EggProtoImplClass[]; readonly unitPath: string; + readonly name: string; - constructor(clazzList: EggProtoImplClass[], unitPath: string) { + constructor(clazzList: EggProtoImplClass[], unitPath: string, name: string) { this.clazzList = clazzList; this.graph = new Graph(); this.unitPath = unitPath; + this.name = name; this.build(); } @@ -103,9 +106,10 @@ export class ModuleGraph { for (const clazz of this.clazzList) { const objNames = PrototypeUtil.getObjNames(clazz, { unitPath: this.unitPath, + moduleName: this.name, }); for (const objName of objNames) { - protoGraphNodes.push(new GraphNode(new ProtoNode(clazz, objName, this.unitPath))); + protoGraphNodes.push(new GraphNode(new ProtoNode(clazz, objName, this.unitPath, this.name))); } } for (const node of protoGraphNodes) { @@ -163,6 +167,7 @@ export class ModuleLoadUnit implements LoadUnit { attribute: InitTypeQualifierAttribute, value: PrototypeUtil.getInitType(clazz, { unitPath: this.unitPath, + moduleName: this.name, })!, }, { attribute: LoadUnitNameQualifierAttribute, @@ -177,7 +182,7 @@ export class ModuleLoadUnit implements LoadUnit { async init() { const clazzList = this.loadClazz(); - const protoGraph = new ModuleGraph(clazzList, this.unitPath); + const protoGraph = new ModuleGraph(clazzList, this.unitPath, this.name); protoGraph.sort(); this.clazzList = protoGraph.clazzList; for (const clazz of this.clazzList) { diff --git a/core/metadata/src/model/AppGraph.ts b/core/metadata/src/model/AppGraph.ts index 55403012..ad1f5f05 100644 --- a/core/metadata/src/model/AppGraph.ts +++ b/core/metadata/src/model/AppGraph.ts @@ -58,6 +58,7 @@ export class ClazzMap { for (const instanceNode of graph.nodes.values()) { const property = PrototypeUtil.getMultiInstanceProperty(clazz, { unitPath: instanceNode.val.moduleConfig.path, + moduleName: instanceNode.val.moduleConfig.name, }); assert(property, `multi instance property not found for ${clazz.name}`); for (const info of property.objects) { @@ -66,6 +67,7 @@ export class ClazzMap { name: info.name, accessLevel: PrototypeUtil.getAccessLevel(clazz, { unitPath: instanceNode.val.moduleConfig.path, + moduleName: instanceNode.val.moduleConfig.name, }) as AccessLevel, qualifiers: [ ...qualifiers, @@ -84,6 +86,7 @@ export class ClazzMap { name: property.name, accessLevel: PrototypeUtil.getAccessLevel(clazz, { unitPath: ownerNode.val.moduleConfig.path, + moduleName: ownerNode.val.moduleConfig.name, }) as AccessLevel, qualifiers, ownerModule: ownerNode, @@ -185,6 +188,7 @@ export class ModuleNode implements GraphNodeObj { attribute: InitTypeQualifierAttribute, value: PrototypeUtil.getInitType(clazz, { unitPath: this.moduleConfig.path, + moduleName: this.moduleConfig.name, })!, }, { attribute: LoadUnitNameQualifierAttribute, diff --git a/core/metadata/test/ModuleGraph.test.ts b/core/metadata/test/ModuleGraph.test.ts index 8085c2df..3f4050ba 100644 --- a/core/metadata/test/ModuleGraph.test.ts +++ b/core/metadata/test/ModuleGraph.test.ts @@ -7,7 +7,7 @@ describe('test/ModuleGraph.test.ts', () => { const modulePath = path.join(__dirname, './fixtures/modules/extends-module'); const loader = new TestLoader(modulePath); const clazzList = loader.load(); - const graph = new ModuleGraph(clazzList, modulePath); + const graph = new ModuleGraph(clazzList, modulePath, 'foo'); graph.sort(); }); }); diff --git a/core/tegg/dal.ts b/core/tegg/dal.ts new file mode 100644 index 00000000..3d437c14 --- /dev/null +++ b/core/tegg/dal.ts @@ -0,0 +1 @@ +export * from '@eggjs/dal-decorator'; diff --git a/core/tegg/package.json b/core/tegg/package.json index c778dd90..f5510da9 100644 --- a/core/tegg/package.json +++ b/core/tegg/package.json @@ -38,6 +38,7 @@ "@eggjs/aop-decorator": "^3.32.0", "@eggjs/controller-decorator": "^3.32.0", "@eggjs/core-decorator": "^3.32.0", + "@eggjs/dal-decorator": "^3.32.0", "@eggjs/eventbus-decorator": "^3.32.0", "@eggjs/standalone-decorator": "^3.32.0", "@eggjs/tegg-background-task": "^3.32.0", diff --git a/plugin/dal/README.md b/plugin/dal/README.md new file mode 100644 index 00000000..9f83b7df --- /dev/null +++ b/plugin/dal/README.md @@ -0,0 +1,623 @@ +# @eggjs/tegg-dal-plugin + +@eggjs/tegg-dal-plugin 支持使用注解的方式来开发 egg 中的 dal。 + +## Install + +```shell +# tegg 注解 +npm i --save @eggjs/tegg +# tegg 插件 +npm i --save @eggjs/tegg-plugin +# tegg dal 插件 +npm i --save @eggjs/tegg-dal-plugin +``` + +## Prepare +```json +// tsconfig.json +{ + "compilerOptions": { + // 注解特性需要显示打开 + "experimentalDecorators": true + } +} +``` + +## Config + +```js +// config/plugin.js +exports.tegg = { + package: '@eggjs/tegg-plugin', + enable: true, +}; + +exports.teggDal = { + package: '@eggjs/tegg-dal-plugin', + enable: true, +}; +``` + +## Usage + +### module.yml +通过 module.yml 来配置 module 中的 mysql 数据源。 + +```yaml +dataSource: + # 数据源名称,可以在 @Table 注解中指定 + # 如果 module 中只有一个 dataSource,@Table 会默认使用这个数据源 + foo: + connectionLimit: 100 + database: 'test' + host: '127.0.0.1' + user: root + port: 3306 +``` + +### Table +`TableModel` 定义一个表结构,包括表配置、列、索引。 + +```ts +import { Table, Index, Column, ColumnType, IndexType } from '@eggjs/tegg/dal'; + +// 定义了一个表 +@Table({ + comment: 'foo table', +}) +// 定义了一个唯一索引,列是 name +@Index({ + keys: [ 'name' ], + type: IndexType.UNIQUE, +}) +export class Foo { + // 定义了主键,类型是 int + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + }) + id: number; + + // 定义了 name 列,类型是 varchar + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }) + name: string; +} +``` + +详细参数定义如下,具体参数值可以参考 https://dev.mysql.com/doc/refman/8.0/en/create-table.html + +建表参数,使用方式为 `@Table(parmas?: TableParams)` +```ts +export interface TableParams { + // 数据库表名 + name?: string; + // 数据源名称,如果 module 只有一个 dataSource 则默认使用这个 + dataSourceName?: string; + comment?: string; + autoExtendSize?: number; + autoIncrement?: number; + avgRowLength?: number; + characterSet?: string; + collate?: string; + compression?: CompressionType; + encryption?: boolean; + engine?: string; + engineAttribute?: string; + insertMethod?: InsertMethod; + keyBlockSize?: number; + maxRows?: number; + minRows?: number; + rowFormat?: RowFormat; + secondaryEngineAttribute?: string; +} +``` + +建索引参数,使用方式为 `@Index(parmas?: IndexParams)` +```ts +export interface IndexParams { + // 索引的列 + keys: string[]; + // 索引名称,如果未指定会用 列名拼接 + // 如 [column1, column2 ] + // 普通索引为 idx_column1_column2 + // 唯一索引为 uk_column1_column2 + name?: string; + type?: IndexType, + storeType?: IndexStoreType; + comment?: string; + engineAttribute?: string; + secondaryEngineAttribute?: string; + parser?: string; +} +``` + +建列参数,使用方式为 `@Column(type: ColumnTypeParams, parmas?: ColumnParams)` +```ts +export interface ColumnParams { + // 列名,默认转换规则 userName 至 user_name + name?: string; + // 默认值 + default?: string; + // 是否可控,默认为 false + canNull?: boolean; + comment?: string; + visible?: boolean; + autoIncrement?: boolean; + uniqueKey?: boolean; + primaryKey?: boolean; + collate?: string; + columnFormat?: ColumnFormat; + engineAttribute?: string; + secondaryEngineAttribute?: string; +} +``` + +支持的类型 +```ts +export enum ColumnType { + // Numeric + BIT = 'BIT', + TINYINT = 'TINYINT', + BOOL = 'BOOL', + SMALLINT = 'SMALLINT', + MEDIUMINT = 'MEDIUMINT', + INT = 'INT', + BIGINT = 'BIGINT', + DECIMAL = 'DECIMAL', + FLOAT = 'FLOAT', + DOUBLE = 'DOUBLE', + // Date + DATE = 'DATE', + DATETIME = 'DATETIME', + TIMESTAMP = 'TIMESTAMP', + TIME = 'TIME', + YEAR = 'YEAR', + // String + CHAR = 'CHAR', + VARCHAR = 'VARCHAR', + BINARY = 'BINARY', + VARBINARY = 'VARBINARY', + TINYBLOB = 'TINYBLOB', + TINYTEXT = 'TINYTEXT', + BLOB = 'BLOB', + TEXT = 'TEXT', + MEDIUMBLOB = 'MEDIUMBLOB', + MEDIUMTEXT = 'MEDIUMTEXT', + LONGBLOB = 'LONGBLOB', + LONGTEXT = 'LONGTEXT', + ENUM = 'ENUM', + SET = 'SET', + // JSON + JSON = 'JSON', + // Spatial + GEOMETRY = 'GEOMETRY', + POINT = 'POINT', + LINESTRING = 'LINESTRING', + POLYGON = 'POLYGON', + MULTIPOINT = 'MULTIPOINT', + MULTILINESTRING = 'MULTILINESTRING', + MULTIPOLYGON = 'MULTIPOLYGON', + GEOMETRYCOLLECTION = 'GEOMETRYCOLLECTION', +} +``` + +支持的类型参数,详细可参考 https://dev.mysql.com/doc/refman/8.0/en/data-types.html + +如果 mysql 类型和 ts 类型对应关系不确定可直接使用 `ColumnTsType` 类型,如 +```ts +import { Table, Index, Column, ColumnType, IndexType, ColumnTsType } from '@eggjs/tegg/dal'; + +// 定义了一个表 +@Table({ + comment: 'foo table', +}) +// 定义了一个唯一索引,列是 name +@Index({ + keys: [ 'name' ], + type: IndexType.UNIQUE, +}) +export class Foo { + // 定义了主键,类型是 int + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + }) + id: ColumnTsType['INT']; + + // 定义了 name 列,类型是 varchar + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }) + name: ColumnTsType['VARCHAR']; +} +``` + +```ts +// Bit 类型,对应 js 中的 Buffer +export interface BitParams { + type: ColumnType.BIT, + // Bit 长度 + length?: number; +} + +// Bool 类型,注意在 js 中需要使用 0 或者 1 +export interface BoolParams { + type: ColumnType.BOOL, +} + +// TinyInt 类型,对应 js 中的 number +export interface TinyIntParams { + type: ColumnType.TINYINT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// SmallInt 类型,对应 js 中的 number +export interface SmallIntParams { + type: ColumnType.SMALLINT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// MediumInt 类型,对应 js 中的 number +export interface MediumIntParams { + type: ColumnType.MEDIUMINT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// MediumInt 类型,对应 js 中的 number +export interface IntParams { + type: ColumnType.INT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// BigInt 类型,对应 js 中的 string +export interface BigIntParams { + type: ColumnType.BIGINT; + length?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// Decimal 类型,对应 js 中的 string +export interface DecimalParams { + type: ColumnType.DECIMAL; + length?: number; + fractionalLength?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// Float 类型,对应 js 中的 number +export interface FloatParams { + type: ColumnType.FLOAT; + length?: number; + fractionalLength?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// Double 类型,对应 js 中的 number +export interface DoubleParams { + type: ColumnType.DOUBLE; + length?: number; + fractionalLength?: number; + unsigned?: boolean; + zeroFill?: boolean; +} + +// Date 类型,对应 js 中的 Date +export interface DateParams { + type: ColumnType.DATE; +} + +// DateTime 类型,对应 js 中的 Date +export interface DateTimeParams { + type: ColumnType.DATETIME; + precision?: number; +} + +// Timestamp 类型,对应 js 中的 Date +export interface TimestampParams { + type: ColumnType.TIMESTAMP; + precision?: number; +} + +// Times 类型,对应 js 中的 string +export interface TimeParams { + type: ColumnType.TIME; + precision?: number; +} + +// Year 类型,对应 js 中的 number +export interface YearParams { + type: ColumnType.YEAR; +} + +// Char 类型,对应 js 中的 string +export interface CharParams { + type: ColumnType.CHAR; + length?: number; + characterSet?: string; + collate?: string; +} + +// VarChar 类型,对应 js 中的 string +export interface VarCharParams { + type: ColumnType.VARCHAR; + length: number; + characterSet?: string; + collate?: string; +} + +// Binary 类型,对应 js 中的 Buffer +export interface BinaryParams { + type: ColumnType.BINARY; + length?: number; +} + +// VarBinary 类型,对应 js 中的 Buffer +export interface VarBinaryParams { + type: ColumnType.VARBINARY; + length: number; +} + +// TinyBlob 类型,对应 js 中的 Buffer +export interface TinyBlobParams { + type: ColumnType.TINYBLOB; +} + +// TinyText 类型,对应 js 中的 string +export interface TinyTextParams { + type: ColumnType.TINYTEXT; + characterSet?: string; + collate?: string; +} + +// Blob 类型,对应 js 中的 Buffer +export interface BlobParams { + type: ColumnType.BLOB; + length?: number; +} + +// Text 类型,对应 js 中的 string +export interface TextParams { + type: ColumnType.TEXT; + length?: number; + characterSet?: string; + collate?: string; +} + +// MediumBlob 类型,对应 js 中的 Buffer +export interface MediumBlobParams { + type: ColumnType.MEDIUMBLOB; +} + +// LongBlob 类型,对应 js 中的 Buffer +export interface LongBlobParams { + type: ColumnType.LONGBLOB; +} + +// MediumText 类型,对应 js 中的 string +export interface MediumTextParams { + type: ColumnType.MEDIUMTEXT; + characterSet?: string; + collate?: string; +} + +// LongText 类型,对应 js 中的 string +export interface LongTextParams { + type: ColumnType.LONGTEXT; + characterSet?: string; + collate?: string; +} + +// Enum 类型,对应 js 中的 string +export interface EnumParams { + type: ColumnType.ENUM; + enums: string[]; + characterSet?: string; + collate?: string; +} + +// Set 类型,对应 js 中的 string +export interface SetParams { + type: ColumnType.SET; + enums: string[]; + characterSet?: string; + collate?: string; +} + +// Json 类型,对应 js 中的 Object +export interface JsonParams { + type: ColumnType.JSON; +} + +// Gemotry 类型,对应 Point, Line, Polygon +export interface GeometryParams { + type: ColumnType.GEOMETRY; + SRID?: number; +} + +export interface PointParams { + type: ColumnType.POINT; + SRID?: number; +} + +export interface LinestringParams { + type: ColumnType.LINESTRING; + SRID?: number; +} + +export interface PolygonParams { + type: ColumnType.POLYGON; + SRID?: number; +} + +export interface MultiPointParams { + type: ColumnType.MULTIPOINT; + SRID?: number; +} + +export interface MultiLinestringParams { + type: ColumnType.MULTILINESTRING; + SRID?: number; +} + +export interface MultiPolygonParams { + type: ColumnType.MULTIPOLYGON; + SRID?: number; +} + +// GeometryCollection 对应 Array +export interface GeometryCollectionParams { + type: ColumnType.GEOMETRYCOLLECTION; + SRID?: number; +} +``` + +### 目录结构 +运行 `egg-bin dal gen` 即可生成 `dal` 相关目录,包括 dao、extension、structure + +```plain +dal +├── dao +│ ├── FooDAO.ts +│ └── base +│ └── BaseFooDAO.ts +├── extension +│ └── FooExtension.ts +└── structure + ├── Foo.json + └── Foo.sql +``` + +- dao: 表访问类,生成的 BaseDAO 请勿修改,其中包含了根据表结构生成的基础访问方法,如 insert/update/delete 以及根据索引信息生成的 find 方法 +- extension: 扩展文件,如果需要自定义 sql,需要在 extension 文件中定义 +- structure: 建表语句以及表结构 + +### DAO + +注入 DAO 即可实现对表的访问 + +```ts +import { SingletonProto, Inject } from '@eggjs/tegg'; + +@SingletonProto() +export class FooRepository { + @Inject() + private readonly fooDAO: FooDAO; + + async create(foo: Foo) { + await this.fooDAO.insert(foo); + } +} +``` + +#### 自定义 Sql +1. 在 extension 中定义自定义 Sql + +```ts +// dal/extension/FooExtension.ts +import { SqlMap, SqlType } from '@eggjs/tegg/dal'; + +export default { + findByName: { + type: SqlType.SELECT, + sql: 'SELECT {{ allColumns }} FROM egg_foo WHERE name = {{ name }}', + }, +} as Record; +``` + +2. 在 dao 中定义自定义方法 +```ts +import { SingletonProto, AccessLevel } from '@eggjs/tegg'; +import { BaseFooDAO } from './base/BaseFooDAO'; +import { Foo } from '../../Foo'; + +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export default class FooDAO extends BaseFooDAO { + async findByName(name: string): Promise { + return this.dataSource.execute('findByName', { + name, + }); + } +} +``` + +支持的自定义 filter +``` +- toPoint +- toLine +- toPolygon +- toGeometry +- toMultiPoint +- toMultiLine +- toMultiPolygon +- toGeometryCollection +``` + +支持自定义 block 来简化 sql, 如内置的 allColumns +```ts +export default { + findByName: { + type: SqlType.BLOCK, + sql: 'id, name', + }, +} as Record; +``` + +### DataSource +DataSource 仅能在 DAO 中使用,可以将 mysql 返回的数据反序列化为类。支持的方法有 + +```ts +export interface DataSource { + // 将返回的行都转换为 T + execute(sqlName: string, data?: any): Promise>; + // 将返回的行都转换为 T, 仅返回第一条 + executeScalar(sqlName: string, data?: any): Promise; + // 直接返回 mysql 数据 + executeRaw(sqlName: string, data?: any): Promise>; + // 直接返回 mysql 数据, 仅返回第一条 + executeRawScalar(sqlName: string, data?: any): Promise; + // 返回分页数据 + paginate(sqlName: string, data: any, currentPage: number, perPageCount: number): Promise; + // 返回行数 + count(sqlName: string, data?: any): Promise; +} +``` + +### 时区问题 + +注意连接配置中的时区必须和数据库的时区完全一致,否则可能出现时间错误的问题。 + +```yaml +dataSource: + foo: + connectionLimit: 100 + database: 'test' + host: '127.0.0.1' + user: root + port: 3306 + timezone: '+08:00' +``` + +可以通过以下 SQL 来查看数据库时区 +```sql +SELECT @@GLOBAL.time_zone; +``` diff --git a/plugin/dal/app.ts b/plugin/dal/app.ts new file mode 100644 index 00000000..be0c50ef --- /dev/null +++ b/plugin/dal/app.ts @@ -0,0 +1,35 @@ +import { Application } from 'egg'; +import { DalTableEggPrototypeHook } from './lib/DalTableEggPrototypeHook'; +import { MysqlDataSourceManager } from './lib/MysqlDataSourceManager'; +import { SqlMapManager } from './lib/SqlMapManager'; +import { TableModelManager } from './lib/TableModelManager'; +import { DalModuleLoadUnitHook } from './lib/DalModuleLoadUnitHook'; + +export default class ControllerAppBootHook { + private readonly app: Application; + private readonly dalTableEggPrototypeHook: DalTableEggPrototypeHook; + private dalModuleLoadUnitHook: DalModuleLoadUnitHook; + + constructor(app: Application) { + this.app = app; + this.dalTableEggPrototypeHook = new DalTableEggPrototypeHook(this.app.logger); + } + + configWillLoad() { + this.dalModuleLoadUnitHook = new DalModuleLoadUnitHook(this.app.moduleConfigs); + this.app.eggPrototypeLifecycleUtil.registerLifecycle(this.dalTableEggPrototypeHook); + this.app.loadUnitLifecycleUtil.registerLifecycle(this.dalModuleLoadUnitHook); + } + + async beforeClose() { + if (this.dalTableEggPrototypeHook) { + await this.app.eggPrototypeLifecycleUtil.deleteLifecycle(this.dalTableEggPrototypeHook); + } + if (this.dalModuleLoadUnitHook) { + await this.app.loadUnitLifecycleUtil.deleteLifecycle(this.dalModuleLoadUnitHook); + } + MysqlDataSourceManager.instance.clear(); + SqlMapManager.instance.clear(); + TableModelManager.instance.clear(); + } +} diff --git a/plugin/dal/lib/DalModuleLoadUnitHook.ts b/plugin/dal/lib/DalModuleLoadUnitHook.ts new file mode 100644 index 00000000..f0ceec7e --- /dev/null +++ b/plugin/dal/lib/DalModuleLoadUnitHook.ts @@ -0,0 +1,31 @@ +import { MysqlDataSourceManager } from './MysqlDataSourceManager'; +import { LifecycleHook } from '@eggjs/tegg-lifecycle'; +import { ModuleConfigHolder } from '@eggjs/tegg-common-util'; +import { DataSourceOptions } from '@eggjs/dal-runtime'; +import { LoadUnit, LoadUnitLifecycleContext } from '@eggjs/tegg/helper'; + +export class DalModuleLoadUnitHook implements LifecycleHook { + private readonly moduleConfigs: Record; + + constructor(moduleConfigs: Record) { + this.moduleConfigs = moduleConfigs; + } + + async preCreate(_: LoadUnitLifecycleContext, loadUnit: LoadUnit): Promise { + const moduleConfigHolder = this.moduleConfigs[loadUnit.name]; + if (!moduleConfigHolder) return; + const dataSourceConfig: Record | undefined = (moduleConfigHolder.config as any).dataSource; + if (!dataSourceConfig) return; + await Promise.all(Object.entries(dataSourceConfig).map(async ([ name, config ]) => { + try { + await MysqlDataSourceManager.instance.createDataSource(loadUnit.name, name, { + ...config, + name, + }); + } catch (e) { + e.message = `create module ${loadUnit.name} datasource ${name} failed: ` + e.message; + throw e; + } + })); + } +} diff --git a/plugin/dal/lib/DalTableEggPrototypeHook.ts b/plugin/dal/lib/DalTableEggPrototypeHook.ts new file mode 100644 index 00000000..96577974 --- /dev/null +++ b/plugin/dal/lib/DalTableEggPrototypeHook.ts @@ -0,0 +1,24 @@ +import { LifecycleHook } from '@eggjs/tegg-lifecycle'; +import { EggPrototype, EggPrototypeLifecycleContext } from '@eggjs/tegg-metadata'; +import { TableInfoUtil, TableModel } from '@eggjs/dal-decorator'; +import { EggLogger } from 'egg'; +import { SqlMapLoader } from '@eggjs/dal-runtime'; +import { TableModelManager } from './TableModelManager'; +import { SqlMapManager } from './SqlMapManager'; + +export class DalTableEggPrototypeHook implements LifecycleHook { + private readonly logger: EggLogger; + + constructor(logger: EggLogger) { + this.logger = logger; + } + + async preCreate(ctx: EggPrototypeLifecycleContext): Promise { + if (!TableInfoUtil.getIsTable(ctx.clazz)) return; + const tableModel = TableModel.build(ctx.clazz); + TableModelManager.instance.set(ctx.loadUnit.name, tableModel); + const loader = new SqlMapLoader(tableModel, ctx.loadUnit.unitPath, this.logger); + const sqlMap = loader.load(); + SqlMapManager.instance.set(ctx.loadUnit.name, sqlMap); + } +} diff --git a/plugin/dal/lib/DataSource.ts b/plugin/dal/lib/DataSource.ts new file mode 100644 index 00000000..76fc7137 --- /dev/null +++ b/plugin/dal/lib/DataSource.ts @@ -0,0 +1,112 @@ +import assert from 'node:assert'; +import path from 'node:path'; +import fs from 'node:fs'; +import { + AccessLevel, + MultiInstanceProto, + MultiInstancePrototypeGetObjectsContext, + ObjectInfo, + ObjectInitType, + LifecycleInit, +} from '@eggjs/tegg'; +import { ModuleConfigUtil } from '@eggjs/tegg-common-util'; +import { + DataSourceInjectName, + DataSourceQualifierAttribute, + TableInfoUtil, + DataSource as IDataSource, TableModel, PaginateData, +} from '@eggjs/tegg/dal'; +import { DataSource } from '@eggjs/dal-runtime'; +import { EggObject, EggObjectLifeCycleContext } from '@eggjs/tegg-runtime'; +import { TableModelManager } from './TableModelManager'; +import { MysqlDataSourceManager } from './MysqlDataSourceManager'; +import { SqlMapManager } from './SqlMapManager'; + +@MultiInstanceProto({ + accessLevel: AccessLevel.PUBLIC, + initType: ObjectInitType.SINGLETON, + getObjects(ctx: MultiInstancePrototypeGetObjectsContext) { + const config = ModuleConfigUtil.loadModuleConfigSync(ctx.unitPath) as any | undefined; + const dataSources = Object.keys(config?.dataSource || {}); + const result: ObjectInfo[] = []; + const daoDir = path.join(ctx.unitPath, 'dal/dao'); + let dirents: string[]; + try { + dirents = fs.readdirSync(daoDir); + } catch { + return []; + } + + const daos = dirents.filter(t => t.endsWith('DAO.ts')); + // eslint-disable-next-line @typescript-eslint/no-var-requires + const daoClazzList = daos.map(t => { + // eslint-disable-next-line @typescript-eslint/no-var-requires + return require(path.join(daoDir, t)).default; + }); + const tableClazzList = daoClazzList.map(t => { + // eslint-disable-next-line no-proto + return Object.getPrototypeOf(t).clazzModel; + }); + const dataSourceLength = dataSources.length; + + for (const dataSource of dataSources) { + const moduleClazzList = tableClazzList.filter(clazz => { + const tableParams = TableInfoUtil.getTableParams(clazz); + const dataSourceName = tableParams?.dataSourceName ?? 'default'; + return dataSourceLength === 1 || dataSourceName === dataSource; + }); + for (const clazz of moduleClazzList) { + result.push({ + name: DataSourceInjectName, + qualifiers: [{ + attribute: DataSourceQualifierAttribute, + value: `${ctx.moduleName}.${dataSource}.${clazz.name}`, + }], + }); + } + } + return result; + }, +}) +export class DataSourceDelegate implements IDataSource { + private dataSource: DataSource; + + @LifecycleInit() + async init(_: EggObjectLifeCycleContext, obj: EggObject) { + const dataSourceQualifierValue = obj.proto.getQualifier(DataSourceQualifierAttribute); + // eslint-disable-next-line @typescript-eslint/no-unused-vars + const [ moduleName, dataSource, clazzName ] = (dataSourceQualifierValue as string).split('.'); + const tableModel = TableModelManager.instance.get(moduleName, clazzName); + assert(tableModel, `not found table ${dataSourceQualifierValue}`); + const mysqlDataSource = MysqlDataSourceManager.instance.get(moduleName, dataSource); + assert(mysqlDataSource, `not found dataSource ${dataSource} in module ${moduleName}`); + const sqlMap = SqlMapManager.instance.get(moduleName, clazzName); + assert(sqlMap, `not found SqlMap ${clazzName} in module ${moduleName}`); + + this.dataSource = new DataSource(tableModel as TableModel, mysqlDataSource, sqlMap); + } + + async execute(sqlName: string, data?: any): Promise { + return this.dataSource.execute(sqlName, data); + } + + async executeScalar(sqlName: string, data?: any): Promise { + return this.dataSource.executeScalar(sqlName, data); + } + + async executeRaw(sqlName: string, data?: any): Promise { + return this.dataSource.executeRaw(sqlName, data); + } + + async executeRawScalar(sqlName: string, data?: any): Promise { + return this.dataSource.executeRawScalar(sqlName, data); + } + + async paginate(sqlName: string, data: any, currentPage: number, perPageCount: number): Promise> { + return this.dataSource.paginate(sqlName, data, currentPage, perPageCount); + } + + async count(sqlName: string, data?: any): Promise { + return this.dataSource.count(sqlName, data); + } +} diff --git a/plugin/dal/lib/MysqlDataSourceManager.ts b/plugin/dal/lib/MysqlDataSourceManager.ts new file mode 100644 index 00000000..9880ac99 --- /dev/null +++ b/plugin/dal/lib/MysqlDataSourceManager.ts @@ -0,0 +1,61 @@ +import { DataSourceOptions, MysqlDataSource } from '@eggjs/dal-runtime'; +import crypto from 'node:crypto'; + +export class MysqlDataSourceManager { + static instance = new MysqlDataSourceManager(); + + private readonly dataSourceIndices: Map>; + private readonly dataSources: Map; + + constructor() { + this.dataSourceIndices = new Map(); + this.dataSources = new Map(); + } + + get(moduleName: string, dataSourceName: string): MysqlDataSource | undefined { + const dataSourceIndex = this.dataSourceIndices.get(moduleName) + ?.get(dataSourceName); + if (dataSourceIndex) { + return this.dataSources.get(dataSourceIndex); + } + } + + async createDataSource(moduleName: string, dataSourceName: string, config: DataSourceOptions) { + const dataSourceConfig = { + ...config, + name: dataSourceName, + }; + const index = MysqlDataSourceManager.createDataSourceKey(dataSourceConfig); + let dataSource = this.dataSources.get(index); + if (!dataSource) { + dataSource = new MysqlDataSource(dataSourceConfig); + this.dataSources.set(index, dataSource); + } + let moduledataSourceIndices = this.dataSourceIndices.get(moduleName); + if (!moduledataSourceIndices) { + moduledataSourceIndices = new Map(); + this.dataSourceIndices.set(moduleName, moduledataSourceIndices); + } + moduledataSourceIndices.set(dataSourceName, index); + + await dataSource.ready(); + } + + clear() { + this.dataSourceIndices.clear(); + } + + static createDataSourceKey(dataSourceOptions: DataSourceOptions): string { + const hash = crypto.createHash('md5'); + const keys = Object.keys(dataSourceOptions) + .sort(); + for (const key of keys) { + const value = dataSourceOptions[key]; + if (value) { + hash.update(key); + hash.update(String(value)); + } + } + return hash.digest('hex'); + } +} diff --git a/plugin/dal/lib/SqlMapManager.ts b/plugin/dal/lib/SqlMapManager.ts new file mode 100644 index 00000000..8b177f80 --- /dev/null +++ b/plugin/dal/lib/SqlMapManager.ts @@ -0,0 +1,28 @@ +import { TableSqlMap } from '@eggjs/dal-runtime'; + +export class SqlMapManager { + static instance = new SqlMapManager(); + + private sqlMaps: Map>; + + constructor() { + this.sqlMaps = new Map(); + } + + get(moduleName: string, clazzName: string): TableSqlMap | undefined { + return this.sqlMaps.get(moduleName)?.get(clazzName); + } + + set(moduleName: string, sqlMap: TableSqlMap) { + let tables = this.sqlMaps.get(moduleName); + if (!tables) { + tables = new Map(); + this.sqlMaps.set(moduleName, tables); + } + tables.set(sqlMap.name, sqlMap); + } + + clear() { + this.sqlMaps.clear(); + } +} diff --git a/plugin/dal/lib/TableModelManager.ts b/plugin/dal/lib/TableModelManager.ts new file mode 100644 index 00000000..c7fa0afe --- /dev/null +++ b/plugin/dal/lib/TableModelManager.ts @@ -0,0 +1,28 @@ +import { TableModel } from '@eggjs/dal-decorator'; + +export class TableModelManager { + static instance = new TableModelManager(); + + private tableModels: Map>; + + constructor() { + this.tableModels = new Map(); + } + + get(moduleName: string, clazzName: string): TableModel | undefined { + return this.tableModels.get(moduleName)?.get(clazzName); + } + + set(moduleName: string, tableModel: TableModel) { + let tables = this.tableModels.get(moduleName); + if (!tables) { + tables = new Map(); + this.tableModels.set(moduleName, tables); + } + tables.set(tableModel.clazz.name, tableModel); + } + + clear() { + this.tableModels.clear(); + } +} diff --git a/plugin/dal/package.json b/plugin/dal/package.json new file mode 100644 index 00000000..78598c7b --- /dev/null +++ b/plugin/dal/package.json @@ -0,0 +1,72 @@ +{ + "name": "@eggjs/tegg-dal-plugin", + "eggPlugin": { + "name": "teggDal", + "strict": false, + "dependencies": [ + "tegg" + ] + }, + "eggModule": { + "name": "teggDal" + }, + "version": "3.32.0", + "description": "dal plugin for egg", + "keywords": [ + "egg", + "plugin", + "typescript", + "module", + "tegg", + "dal" + ], + "files": [ + "app.js", + "app.d.ts", + "lib/**/*.js", + "lib/**/*.d.ts", + "app/**/*.js", + "app/**/*.d.ts", + "typings/*.d.ts" + ], + "types": "typings/index.d.ts", + "scripts": { + "test": "cross-env NODE_ENV=test NODE_OPTIONS='--no-deprecation' mocha", + "clean": "tsc -b --clean", + "tsc": "npm run clean && tsc -p ./tsconfig.json", + "tsc:pub": "npm run clean && tsc -p ./tsconfig.pub.json", + "prepublishOnly": "npm run tsc:pub" + }, + "homepage": "https://github.com/eggjs/tegg", + "bugs": { + "url": "https://github.com/eggjs/tegg/issues" + }, + "repository": { + "type": "git", + "url": "git@github.com:eggjs/tegg.git", + "directory": "plugin/controller" + }, + "engines": { + "node": ">=14.0.0" + }, + "dependencies": { + "@eggjs/tegg": "^3.32.0", + "@eggjs/dal-runtime": "^3.32.0" + }, + "devDependencies": { + "@eggjs/tegg-plugin": "^3.32.0", + "@types/mocha": "^10.0.1", + "@types/node": "^20.2.4", + "cross-env": "^7.0.3", + "egg": "^3.9.1", + "egg-mock": "^5.5.0", + "egg-tracer": "^2.0.0", + "globby": "^11.1.0", + "mocha": "^10.2.0", + "ts-node": "^10.9.1", + "typescript": "^5.0.4" + }, + "publishConfig": { + "access": "public" + } +} diff --git a/plugin/dal/test/dal.test.ts b/plugin/dal/test/dal.test.ts new file mode 100644 index 00000000..e838e053 --- /dev/null +++ b/plugin/dal/test/dal.test.ts @@ -0,0 +1,119 @@ +import assert from 'assert'; +import path from 'path'; +import mm, { MockApplication } from 'egg-mock'; +import FooDAO from './fixtures/apps/dal-app/modules/dal/dal/dao/FooDAO'; +import { Foo } from './fixtures/apps/dal-app/modules/dal/Foo'; + +describe('test/dal.test.ts', () => { + let app: MockApplication; + + afterEach(async () => { + mm.restore(); + }); + + before(async () => { + mm(process.env, 'EGG_TYPESCRIPT', true); + mm(process, 'cwd', () => { + return path.join(__dirname, '../'); + }); + app = mm.app({ + baseDir: path.join(__dirname, './fixtures/apps/dal-app'), + framework: require.resolve('egg'), + }); + await app.ready(); + }); + + after(() => { + return app.close(); + }); + + it('should work', async () => { + await app.mockModuleContextScope(async ctx => { + const fooDAO = await ctx.getEggObject(FooDAO); + const foo = new Foo(); + foo.name = 'name'; + foo.col1 = 'col1'; + foo.bitColumn = Buffer.from([ 0, 0 ]); + foo.boolColumn = 0; + foo.tinyIntColumn = 0; + foo.smallIntColumn = 1; + foo.mediumIntColumn = 3; + foo.intColumn = 3; + foo.bigIntColumn = '00099'; + foo.decimalColumn = '00002.33333'; + foo.floatColumn = 2.3; + foo.doubleColumn = 2.3; + foo.dateColumn = new Date('2020-03-15T16:00:00.000Z'); + foo.dateTimeColumn = new Date('2024-03-16T01:26:58.677Z'); + foo.timestampColumn = new Date('2024-03-16T01:26:58.677Z'); + foo.timeColumn = '838:59:50.123'; + foo.yearColumn = 2024; + foo.varCharColumn = 'var_char'; + foo.binaryColumn = Buffer.from('b'); + foo.varBinaryColumn = Buffer.from('var_binary'); + foo.tinyBlobColumn = Buffer.from('tiny_blob'); + foo.tinyTextColumn = 'text'; + foo.blobColumn = Buffer.from('blob'); + foo.textColumn = 'text'; + foo.mediumBlobColumn = Buffer.from('medium_blob'); + foo.longBlobColumn = Buffer.from('long_blob'); + foo.mediumTextColumn = 'medium_text'; + foo.longTextColumn = 'long_text'; + foo.enumColumn = 'A'; + foo.setColumn = 'B'; + foo.geometryColumn = { x: 10, y: 10 }; + foo.pointColumn = { x: 10, y: 10 }; + foo.lineStringColumn = [ + { x: 15, y: 15 }, + { x: 20, y: 20 }, + ]; + foo.polygonColumn = [ + [ + { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 }, + ], [ + { x: 5, y: 5 }, { x: 7, y: 5 }, { x: 7, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 5 }, + ], + ]; + foo.multipointColumn = [ + { x: 0, y: 0 }, { x: 20, y: 20 }, { x: 60, y: 60 }, + ]; + foo.multiLineStringColumn = [ + [ + { x: 10, y: 10 }, { x: 20, y: 20 }, + ], [ + { x: 15, y: 15 }, { x: 30, y: 15 }, + ], + ]; + foo.multiPolygonColumn = [ + [ + [ + { x: 0, y: 0 }, { x: 10, y: 0 }, { x: 10, y: 10 }, { x: 0, y: 10 }, { x: 0, y: 0 }, + ], + ], + [ + [ + { x: 5, y: 5 }, { x: 7, y: 5 }, { x: 7, y: 7 }, { x: 5, y: 7 }, { x: 5, y: 5 }, + ], + ], + ]; + foo.geometryCollectionColumn = [ + { x: 10, y: 10 }, + { x: 30, y: 30 }, + [ + { x: 15, y: 15 }, { x: 20, y: 20 }, + ], + ]; + foo.jsonColumn = { + hello: 'json', + }; + const insertResult = await fooDAO.insert(foo); + foo.id = insertResult.insertId; + + const findFoo = await fooDAO.findByPrimary(foo.id); + assert(findFoo); + + const deleteResult = await fooDAO.delete(foo.id); + assert.equal(deleteResult.affectedRows, 1); + }); + }); +}); diff --git a/plugin/dal/test/fixtures/apps/dal-app/config/config.default.js b/plugin/dal/test/fixtures/apps/dal-app/config/config.default.js new file mode 100644 index 00000000..586c9e0c --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/config/config.default.js @@ -0,0 +1,13 @@ +'use strict'; + +module.exports = function() { + const config = { + keys: 'test key', + security: { + csrf: { + ignoreJSON: false, + } + }, + }; + return config; +}; diff --git a/plugin/dal/test/fixtures/apps/dal-app/config/module.json b/plugin/dal/test/fixtures/apps/dal-app/config/module.json new file mode 100644 index 00000000..aa221111 --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/config/module.json @@ -0,0 +1,7 @@ +[ + { + "path": "../modules/dal" + }, { + "package": "../../../../" + } +] diff --git a/plugin/dal/test/fixtures/apps/dal-app/config/plugin.js b/plugin/dal/test/fixtures/apps/dal-app/config/plugin.js new file mode 100644 index 00000000..828aebbf --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/config/plugin.js @@ -0,0 +1,16 @@ +'use strict'; + +exports.tracer = { + package: 'egg-tracer', + enable: true, +}; + +exports.tegg = { + package: '@eggjs/tegg-plugin', + enable: true, +}; + +exports.teggConfig = { + package: '@eggjs/tegg-config', + enable: true, +}; diff --git a/plugin/dal/test/fixtures/apps/dal-app/modules/dal/Foo.ts b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/Foo.ts new file mode 100644 index 00000000..44f02610 --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/Foo.ts @@ -0,0 +1,292 @@ +import { + Column, + ColumnType, + Geometry, + GeometryCollection, + Index, + IndexType, + Line, MultiLine, MultiPoint, MultiPolygon, Point, Polygon, + Table, +} from '@eggjs/dal-decorator'; +import { IndexStoreType } from '@eggjs/dal-decorator/src/enum/IndexStoreType'; + +@Table({ + name: 'egg_foo', + comment: 'foo table', + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', +}) +@Index({ + keys: [ 'name', 'col1' ], + type: IndexType.UNIQUE, + storeType: IndexStoreType.BTREE, + comment: 'index comment\n', +}) +@Index({ + keys: [ 'col1' ], + type: IndexType.FULLTEXT, + comment: 'index comment\n', +}) +export class Foo { + @Column({ + type: ColumnType.INT, + }, { + primaryKey: true, + autoIncrement: true, + comment: 'the primary key', + }) + id: number; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }, { + uniqueKey: true, + }) + name: string; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + }, { + name: 'col1', + }) + col1: string; + + @Column({ + type: ColumnType.BIT, + length: 10, + }) + bitColumn: Buffer; + + @Column({ + type: ColumnType.BOOL, + }) + boolColumn: 0 | 1; + + @Column({ + type: ColumnType.TINYINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + tinyIntColumn: number; + + @Column({ + type: ColumnType.SMALLINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + smallIntColumn: number; + + @Column({ + type: ColumnType.MEDIUMINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + mediumIntColumn: number; + + @Column({ + type: ColumnType.INT, + length: 5, + unsigned: true, + zeroFill: true, + }) + intColumn: number; + + @Column({ + type: ColumnType.BIGINT, + length: 5, + unsigned: true, + zeroFill: true, + }) + bigIntColumn: string; + + @Column({ + type: ColumnType.DECIMAL, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + decimalColumn: string; + + @Column({ + type: ColumnType.FLOAT, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + floatColumn: number; + + @Column({ + type: ColumnType.DOUBLE, + length: 10, + fractionalLength: 5, + unsigned: true, + zeroFill: true, + }) + doubleColumn: number; + + @Column({ + type: ColumnType.DATE, + }) + dateColumn: Date; + + @Column({ + type: ColumnType.DATETIME, + precision: 3, + }) + dateTimeColumn: Date; + + @Column({ + type: ColumnType.TIMESTAMP, + precision: 3, + }) + timestampColumn: Date; + + @Column({ + type: ColumnType.TIME, + precision: 3, + }) + timeColumn: string; + + @Column({ + type: ColumnType.YEAR, + }) + yearColumn: number; + + @Column({ + type: ColumnType.VARCHAR, + length: 100, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + varCharColumn: string; + + @Column({ + type: ColumnType.BINARY, + }) + binaryColumn: Buffer; + + @Column({ + type: ColumnType.VARBINARY, + length: 100, + }) + varBinaryColumn: Buffer; + + @Column({ + type: ColumnType.TINYBLOB, + }) + tinyBlobColumn: Buffer; + + @Column({ + type: ColumnType.TINYTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + tinyTextColumn: string; + + @Column({ + type: ColumnType.BLOB, + length: 100, + }) + blobColumn: Buffer; + + @Column({ + type: ColumnType.TEXT, + length: 100, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + textColumn: string; + + @Column({ + type: ColumnType.MEDIUMBLOB, + }) + mediumBlobColumn: Buffer; + + @Column({ + type: ColumnType.LONGBLOB, + }) + longBlobColumn: Buffer; + + @Column({ + type: ColumnType.MEDIUMTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + mediumTextColumn: string; + + @Column({ + type: ColumnType.LONGTEXT, + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + longTextColumn: string; + + @Column({ + type: ColumnType.ENUM, + enums: [ 'A', 'B' ], + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + enumColumn: string; + + @Column({ + type: ColumnType.SET, + enums: [ 'A', 'B' ], + characterSet: 'utf8mb4', + collate: 'utf8mb4_unicode_ci', + }) + setColumn: string; + + @Column({ + type: ColumnType.GEOMETRY, + }) + geometryColumn: Geometry; + + + @Column({ + type: ColumnType.POINT, + }) + pointColumn: Point; + + @Column({ + type: ColumnType.LINESTRING, + }) + lineStringColumn: Line; + + @Column({ + type: ColumnType.POLYGON, + }) + polygonColumn: Polygon; + + @Column({ + type: ColumnType.MULTIPOINT, + }) + multipointColumn: MultiPoint; + + @Column({ + type: ColumnType.MULTILINESTRING, + }) + multiLineStringColumn: MultiLine; + + @Column({ + type: ColumnType.MULTIPOLYGON, + }) + multiPolygonColumn: MultiPolygon; + + @Column({ + type: ColumnType.GEOMETRYCOLLECTION, + }) + geometryCollectionColumn: GeometryCollection; + + @Column({ + type: ColumnType.JSON, + }) + jsonColumn: object; +} diff --git a/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/dao/FooDAO.ts b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/dao/FooDAO.ts new file mode 100644 index 00000000..1095d886 --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/dao/FooDAO.ts @@ -0,0 +1,20 @@ +import { SingletonProto, AccessLevel } from '@eggjs/tegg'; +import { BaseFooDAO } from './base/BaseFooDAO'; +import { Foo } from '../../Foo'; + +/** + * FooDAO 类 + * @class FooDAO + * @classdesc 在此扩展关于 Foo 数据的一切操作 + * @augments BaseFooDAO + */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export default class FooDAO extends BaseFooDAO { + async findByName(name: string): Promise { + return this.dataSource.execute('findByName', { + name, + }); + } +} diff --git a/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/dao/base/BaseFooDAO.ts b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/dao/base/BaseFooDAO.ts new file mode 100644 index 00000000..db0e23f8 --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/dao/base/BaseFooDAO.ts @@ -0,0 +1,494 @@ +import type { InsertResult, UpdateResult, DeleteResult } from '@eggjs/rds/lib/types'; +import { SingletonProto, AccessLevel, Inject } from '@eggjs/tegg'; +import { DataSource, DataSourceInjectName, DataSourceQualifier } from '@eggjs/tegg/dal'; +import { Foo } from '../../../Foo'; + +type Optional = Omit & Partial; +/** + * 自动生成的 FooDAO 基类 + * @class BaseFooDAO + * @classdesc 该文件由 @eggjs/tegg 自动生成,请**不要**修改它! + */ + +/* istanbul ignore next */ +@SingletonProto({ + accessLevel: AccessLevel.PUBLIC, +}) +export class BaseFooDAO { + static clazzModel = Foo; + + @Inject({ + name: DataSourceInjectName, + }) + @DataSourceQualifier('dal.foo.Foo') + protected readonly dataSource: DataSource; + + public async insert(raw: Optional): Promise { + const data: Record = {}; + let tmp; + + tmp = raw.id; + if (tmp !== undefined) { + data.$id = tmp; + } + + tmp = raw.name; + if (tmp !== undefined) { + data.$name = tmp; + } + + tmp = raw.col1; + if (tmp !== undefined) { + data.$col1 = tmp; + } + + tmp = raw.bitColumn; + if (tmp !== undefined) { + data.$bitColumn = tmp; + } + + tmp = raw.boolColumn; + if (tmp !== undefined) { + data.$boolColumn = tmp; + } + + tmp = raw.tinyIntColumn; + if (tmp !== undefined) { + data.$tinyIntColumn = tmp; + } + + tmp = raw.smallIntColumn; + if (tmp !== undefined) { + data.$smallIntColumn = tmp; + } + + tmp = raw.mediumIntColumn; + if (tmp !== undefined) { + data.$mediumIntColumn = tmp; + } + + tmp = raw.intColumn; + if (tmp !== undefined) { + data.$intColumn = tmp; + } + + tmp = raw.bigIntColumn; + if (tmp !== undefined) { + data.$bigIntColumn = tmp; + } + + tmp = raw.decimalColumn; + if (tmp !== undefined) { + data.$decimalColumn = tmp; + } + + tmp = raw.floatColumn; + if (tmp !== undefined) { + data.$floatColumn = tmp; + } + + tmp = raw.doubleColumn; + if (tmp !== undefined) { + data.$doubleColumn = tmp; + } + + tmp = raw.dateColumn; + if (tmp !== undefined) { + data.$dateColumn = tmp; + } + + tmp = raw.dateTimeColumn; + if (tmp !== undefined) { + data.$dateTimeColumn = tmp; + } + + tmp = raw.timestampColumn; + if (tmp !== undefined) { + data.$timestampColumn = tmp; + } + + tmp = raw.timeColumn; + if (tmp !== undefined) { + data.$timeColumn = tmp; + } + + tmp = raw.yearColumn; + if (tmp !== undefined) { + data.$yearColumn = tmp; + } + + tmp = raw.varCharColumn; + if (tmp !== undefined) { + data.$varCharColumn = tmp; + } + + tmp = raw.binaryColumn; + if (tmp !== undefined) { + data.$binaryColumn = tmp; + } + + tmp = raw.varBinaryColumn; + if (tmp !== undefined) { + data.$varBinaryColumn = tmp; + } + + tmp = raw.tinyBlobColumn; + if (tmp !== undefined) { + data.$tinyBlobColumn = tmp; + } + + tmp = raw.tinyTextColumn; + if (tmp !== undefined) { + data.$tinyTextColumn = tmp; + } + + tmp = raw.blobColumn; + if (tmp !== undefined) { + data.$blobColumn = tmp; + } + + tmp = raw.textColumn; + if (tmp !== undefined) { + data.$textColumn = tmp; + } + + tmp = raw.mediumBlobColumn; + if (tmp !== undefined) { + data.$mediumBlobColumn = tmp; + } + + tmp = raw.longBlobColumn; + if (tmp !== undefined) { + data.$longBlobColumn = tmp; + } + + tmp = raw.mediumTextColumn; + if (tmp !== undefined) { + data.$mediumTextColumn = tmp; + } + + tmp = raw.longTextColumn; + if (tmp !== undefined) { + data.$longTextColumn = tmp; + } + + tmp = raw.enumColumn; + if (tmp !== undefined) { + data.$enumColumn = tmp; + } + + tmp = raw.setColumn; + if (tmp !== undefined) { + data.$setColumn = tmp; + } + + tmp = raw.geometryColumn; + if (tmp !== undefined) { + data.$geometryColumn = tmp; + } + + tmp = raw.pointColumn; + if (tmp !== undefined) { + data.$pointColumn = tmp; + } + + tmp = raw.lineStringColumn; + if (tmp !== undefined) { + data.$lineStringColumn = tmp; + } + + tmp = raw.polygonColumn; + if (tmp !== undefined) { + data.$polygonColumn = tmp; + } + + tmp = raw.multipointColumn; + if (tmp !== undefined) { + data.$multipointColumn = tmp; + } + + tmp = raw.multiLineStringColumn; + if (tmp !== undefined) { + data.$multiLineStringColumn = tmp; + } + + tmp = raw.multiPolygonColumn; + if (tmp !== undefined) { + data.$multiPolygonColumn = tmp; + } + + tmp = raw.geometryCollectionColumn; + if (tmp !== undefined) { + data.$geometryCollectionColumn = tmp; + } + + tmp = raw.jsonColumn; + if (tmp !== undefined) { + data.$jsonColumn = tmp; + } + + return this.dataSource.executeRawScalar('insert', data); + } + + public async update(id: number, data: Partial): Promise { + + const newData: Record = { + primary: { + id, + }, + }; + let tmp; + + tmp = data.id; + if (tmp !== undefined) { + newData.$id = tmp; + } + + tmp = data.name; + if (tmp !== undefined) { + newData.$name = tmp; + } + + tmp = data.col1; + if (tmp !== undefined) { + newData.$col1 = tmp; + } + + tmp = data.bitColumn; + if (tmp !== undefined) { + newData.$bitColumn = tmp; + } + + tmp = data.boolColumn; + if (tmp !== undefined) { + newData.$boolColumn = tmp; + } + + tmp = data.tinyIntColumn; + if (tmp !== undefined) { + newData.$tinyIntColumn = tmp; + } + + tmp = data.smallIntColumn; + if (tmp !== undefined) { + newData.$smallIntColumn = tmp; + } + + tmp = data.mediumIntColumn; + if (tmp !== undefined) { + newData.$mediumIntColumn = tmp; + } + + tmp = data.intColumn; + if (tmp !== undefined) { + newData.$intColumn = tmp; + } + + tmp = data.bigIntColumn; + if (tmp !== undefined) { + newData.$bigIntColumn = tmp; + } + + tmp = data.decimalColumn; + if (tmp !== undefined) { + newData.$decimalColumn = tmp; + } + + tmp = data.floatColumn; + if (tmp !== undefined) { + newData.$floatColumn = tmp; + } + + tmp = data.doubleColumn; + if (tmp !== undefined) { + newData.$doubleColumn = tmp; + } + + tmp = data.dateColumn; + if (tmp !== undefined) { + newData.$dateColumn = tmp; + } + + tmp = data.dateTimeColumn; + if (tmp !== undefined) { + newData.$dateTimeColumn = tmp; + } + + tmp = data.timestampColumn; + if (tmp !== undefined) { + newData.$timestampColumn = tmp; + } + + tmp = data.timeColumn; + if (tmp !== undefined) { + newData.$timeColumn = tmp; + } + + tmp = data.yearColumn; + if (tmp !== undefined) { + newData.$yearColumn = tmp; + } + + tmp = data.varCharColumn; + if (tmp !== undefined) { + newData.$varCharColumn = tmp; + } + + tmp = data.binaryColumn; + if (tmp !== undefined) { + newData.$binaryColumn = tmp; + } + + tmp = data.varBinaryColumn; + if (tmp !== undefined) { + newData.$varBinaryColumn = tmp; + } + + tmp = data.tinyBlobColumn; + if (tmp !== undefined) { + newData.$tinyBlobColumn = tmp; + } + + tmp = data.tinyTextColumn; + if (tmp !== undefined) { + newData.$tinyTextColumn = tmp; + } + + tmp = data.blobColumn; + if (tmp !== undefined) { + newData.$blobColumn = tmp; + } + + tmp = data.textColumn; + if (tmp !== undefined) { + newData.$textColumn = tmp; + } + + tmp = data.mediumBlobColumn; + if (tmp !== undefined) { + newData.$mediumBlobColumn = tmp; + } + + tmp = data.longBlobColumn; + if (tmp !== undefined) { + newData.$longBlobColumn = tmp; + } + + tmp = data.mediumTextColumn; + if (tmp !== undefined) { + newData.$mediumTextColumn = tmp; + } + + tmp = data.longTextColumn; + if (tmp !== undefined) { + newData.$longTextColumn = tmp; + } + + tmp = data.enumColumn; + if (tmp !== undefined) { + newData.$enumColumn = tmp; + } + + tmp = data.setColumn; + if (tmp !== undefined) { + newData.$setColumn = tmp; + } + + tmp = data.geometryColumn; + if (tmp !== undefined) { + newData.$geometryColumn = tmp; + } + + tmp = data.pointColumn; + if (tmp !== undefined) { + newData.$pointColumn = tmp; + } + + tmp = data.lineStringColumn; + if (tmp !== undefined) { + newData.$lineStringColumn = tmp; + } + + tmp = data.polygonColumn; + if (tmp !== undefined) { + newData.$polygonColumn = tmp; + } + + tmp = data.multipointColumn; + if (tmp !== undefined) { + newData.$multipointColumn = tmp; + } + + tmp = data.multiLineStringColumn; + if (tmp !== undefined) { + newData.$multiLineStringColumn = tmp; + } + + tmp = data.multiPolygonColumn; + if (tmp !== undefined) { + newData.$multiPolygonColumn = tmp; + } + + tmp = data.geometryCollectionColumn; + if (tmp !== undefined) { + newData.$geometryCollectionColumn = tmp; + } + + tmp = data.jsonColumn; + if (tmp !== undefined) { + newData.$jsonColumn = tmp; + } + + return this.dataSource.executeRawScalar('update', newData); + } + + public async delete(id: number): Promise { + return this.dataSource.executeRawScalar('delete', { + id, + }); + } + + public async del(id: number): Promise { + return this.dataSource.executeRawScalar('delete', { + id, + }); + } + + public async findByCol1($col1: string): Promise { + return this.dataSource.execute('findByCol1', { + $col1, + }); + } + + public async findOneByCol1($col1: string): Promise { + return this.dataSource.executeScalar('findOneByCol1', { + $col1, + }); + } + + public async findByUkNameCol1($name: string, $col1: string): Promise { + return this.dataSource.execute('findByUkNameCol1', { + $name, + $col1, + }); + } + + public async findOneByUkNameCol1($name: string, $col1: string): Promise { + return this.dataSource.executeScalar('findOneByUkNameCol1', { + $name, + $col1, + }); + } + + public async findById($id: number): Promise { + return this.dataSource.executeScalar('findById', { + $id, + }); + } + + public async findByPrimary($id: number): Promise { + return this.dataSource.executeScalar('findById', { + $id, + }); + } +} diff --git a/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/extension/FooExtension.ts b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/extension/FooExtension.ts new file mode 100644 index 00000000..a919cb15 --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/extension/FooExtension.ts @@ -0,0 +1,20 @@ +import { SqlMap } from '@eggjs/tegg/dal'; + +/** + * Define Custom SQLs + * + * import { SqlMap, SqlType } from '@eggjs/tegg/dal'; + * + * export default { + * findByName: { + * type: SqlType.SELECT, + * sql: 'SELECT {{ allColumns }} from foo where name = {{ name }}' + * }, + * } + */ +export default { + findByName: { + type: 'SELECT', + sql: 'SELECT {{ allColumns }} FROM egg_foo WHERE name = {{ name }}', + }, +} as Record; diff --git a/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/structure/Foo.json b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/structure/Foo.json new file mode 100644 index 00000000..1edd9fff --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/structure/Foo.json @@ -0,0 +1,421 @@ +{ + "name": "egg_foo", + "dataSourceName": "default", + "columns": [ + { + "columnName": "id", + "propertyName": "id", + "type": { + "type": "INT" + }, + "canNull": false, + "comment": "the primary key", + "autoIncrement": true, + "primaryKey": true + }, + { + "columnName": "name", + "propertyName": "name", + "type": { + "type": "VARCHAR", + "length": 100 + }, + "canNull": true, + "uniqueKey": true + }, + { + "columnName": "col1", + "propertyName": "col1", + "type": { + "type": "VARCHAR", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "bit_column", + "propertyName": "bitColumn", + "type": { + "type": "BIT", + "length": 10 + }, + "canNull": true + }, + { + "columnName": "bool_column", + "propertyName": "boolColumn", + "type": { + "type": "BOOL" + }, + "canNull": true + }, + { + "columnName": "tiny_int_column", + "propertyName": "tinyIntColumn", + "type": { + "type": "TINYINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "small_int_column", + "propertyName": "smallIntColumn", + "type": { + "type": "SMALLINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "medium_int_column", + "propertyName": "mediumIntColumn", + "type": { + "type": "MEDIUMINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "int_column", + "propertyName": "intColumn", + "type": { + "type": "INT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "big_int_column", + "propertyName": "bigIntColumn", + "type": { + "type": "BIGINT", + "length": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "decimal_column", + "propertyName": "decimalColumn", + "type": { + "type": "DECIMAL", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "float_column", + "propertyName": "floatColumn", + "type": { + "type": "FLOAT", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "double_column", + "propertyName": "doubleColumn", + "type": { + "type": "DOUBLE", + "length": 10, + "fractionalLength": 5, + "unsigned": true, + "zeroFill": true + }, + "canNull": true + }, + { + "columnName": "date_column", + "propertyName": "dateColumn", + "type": { + "type": "DATE" + }, + "canNull": true + }, + { + "columnName": "date_time_column", + "propertyName": "dateTimeColumn", + "type": { + "type": "DATETIME", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "timestamp_column", + "propertyName": "timestampColumn", + "type": { + "type": "TIMESTAMP", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "time_column", + "propertyName": "timeColumn", + "type": { + "type": "TIME", + "precision": 3 + }, + "canNull": true + }, + { + "columnName": "year_column", + "propertyName": "yearColumn", + "type": { + "type": "YEAR" + }, + "canNull": true + }, + { + "columnName": "var_char_column", + "propertyName": "varCharColumn", + "type": { + "type": "VARCHAR", + "length": 100, + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "binary_column", + "propertyName": "binaryColumn", + "type": { + "type": "BINARY" + }, + "canNull": true + }, + { + "columnName": "var_binary_column", + "propertyName": "varBinaryColumn", + "type": { + "type": "VARBINARY", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "tiny_blob_column", + "propertyName": "tinyBlobColumn", + "type": { + "type": "TINYBLOB" + }, + "canNull": true + }, + { + "columnName": "tiny_text_column", + "propertyName": "tinyTextColumn", + "type": { + "type": "TINYTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "blob_column", + "propertyName": "blobColumn", + "type": { + "type": "BLOB", + "length": 100 + }, + "canNull": true + }, + { + "columnName": "text_column", + "propertyName": "textColumn", + "type": { + "type": "TEXT", + "length": 100, + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "medium_blob_column", + "propertyName": "mediumBlobColumn", + "type": { + "type": "MEDIUMBLOB" + }, + "canNull": true + }, + { + "columnName": "long_blob_column", + "propertyName": "longBlobColumn", + "type": { + "type": "LONGBLOB" + }, + "canNull": true + }, + { + "columnName": "medium_text_column", + "propertyName": "mediumTextColumn", + "type": { + "type": "MEDIUMTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "long_text_column", + "propertyName": "longTextColumn", + "type": { + "type": "LONGTEXT", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "enum_column", + "propertyName": "enumColumn", + "type": { + "type": "ENUM", + "enums": [ + "A", + "B" + ], + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "set_column", + "propertyName": "setColumn", + "type": { + "type": "SET", + "enums": [ + "A", + "B" + ], + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" + }, + "canNull": true + }, + { + "columnName": "geometry_column", + "propertyName": "geometryColumn", + "type": { + "type": "GEOMETRY" + }, + "canNull": true + }, + { + "columnName": "point_column", + "propertyName": "pointColumn", + "type": { + "type": "POINT" + }, + "canNull": true + }, + { + "columnName": "line_string_column", + "propertyName": "lineStringColumn", + "type": { + "type": "LINESTRING" + }, + "canNull": true + }, + { + "columnName": "polygon_column", + "propertyName": "polygonColumn", + "type": { + "type": "POLYGON" + }, + "canNull": true + }, + { + "columnName": "multipoint_column", + "propertyName": "multipointColumn", + "type": { + "type": "MULTIPOINT" + }, + "canNull": true + }, + { + "columnName": "multi_line_string_column", + "propertyName": "multiLineStringColumn", + "type": { + "type": "MULTILINESTRING" + }, + "canNull": true + }, + { + "columnName": "multi_polygon_column", + "propertyName": "multiPolygonColumn", + "type": { + "type": "MULTIPOLYGON" + }, + "canNull": true + }, + { + "columnName": "geometry_collection_column", + "propertyName": "geometryCollectionColumn", + "type": { + "type": "GEOMETRYCOLLECTION" + }, + "canNull": true + }, + { + "columnName": "json_column", + "propertyName": "jsonColumn", + "type": { + "type": "JSON" + }, + "canNull": true + } + ], + "indices": [ + { + "name": "idx_col1", + "keys": [ + { + "propertyName": "col1", + "columnName": "col1" + } + ], + "type": "FULLTEXT", + "comment": "index comment\n" + }, + { + "name": "uk_name_col1", + "keys": [ + { + "propertyName": "name", + "columnName": "name" + }, + { + "propertyName": "col1", + "columnName": "col1" + } + ], + "type": "UNIQUE", + "storeType": "BTREE", + "comment": "index comment\n" + } + ], + "comment": "foo table", + "characterSet": "utf8mb4", + "collate": "utf8mb4_unicode_ci" +} \ No newline at end of file diff --git a/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/structure/Foo.sql b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/structure/Foo.sql new file mode 100644 index 00000000..104f7260 --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/dal/structure/Foo.sql @@ -0,0 +1,44 @@ +CREATE TABLE IF NOT EXISTS egg_foo ( + id INT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT 'the primary key', + name VARCHAR(100) NULL UNIQUE KEY, + col1 VARCHAR(100) NULL, + bit_column BIT(10) NULL, + bool_column BOOL NULL, + tiny_int_column TINYINT(5) UNSIGNED ZEROFILL NULL, + small_int_column SMALLINT(5) UNSIGNED ZEROFILL NULL, + medium_int_column MEDIUMINT(5) UNSIGNED ZEROFILL NULL, + int_column INT(5) UNSIGNED ZEROFILL NULL, + big_int_column BIGINT(5) UNSIGNED ZEROFILL NULL, + decimal_column DECIMAL(10,5) UNSIGNED ZEROFILL NULL, + float_column FLOAT(10,5) UNSIGNED ZEROFILL NULL, + double_column DOUBLE(10,5) UNSIGNED ZEROFILL NULL, + date_column DATE NULL, + date_time_column DATETIME(3) NULL, + timestamp_column TIMESTAMP(3) NULL, + time_column TIME(3) NULL, + year_column YEAR NULL, + var_char_column VARCHAR(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + binary_column BINARY NULL, + var_binary_column VARBINARY(100) NULL, + tiny_blob_column TINYBLOB NULL, + tiny_text_column TINYTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + blob_column BLOB(100) NULL, + text_column TEXT(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + medium_blob_column MEDIUMBLOB NULL, + long_blob_column LONGBLOB NULL, + medium_text_column MEDIUMTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + long_text_column LONGTEXT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + enum_column ENUM('A','B') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + set_column SET('A','B') CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL, + geometry_column GEOMETRY NULL, + point_column POINT NULL, + line_string_column LINESTRING NULL, + polygon_column POLYGON NULL, + multipoint_column MULTIPOINT NULL, + multi_line_string_column MULTILINESTRING NULL, + multi_polygon_column MULTIPOLYGON NULL, + geometry_collection_column GEOMETRYCOLLECTION NULL, + json_column JSON NULL, + FULLTEXT KEY idx_col1 (col1) COMMENT 'index comment\n', + UNIQUE KEY uk_name_col1 (name,col1) USING BTREE COMMENT 'index comment\n' +) DEFAULT CHARACTER SET utf8mb4, DEFAULT COLLATE utf8mb4_unicode_ci, COMMENT='foo table'; \ No newline at end of file diff --git a/plugin/dal/test/fixtures/apps/dal-app/modules/dal/module.yml b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/module.yml new file mode 100644 index 00000000..191f15f1 --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/module.yml @@ -0,0 +1,8 @@ +dataSource: + foo: + connectionLimit: 100 + database: 'test' + host: '127.0.0.1' + user: root + port: 3306 + timezone: '+08:00' diff --git a/plugin/dal/test/fixtures/apps/dal-app/modules/dal/package.json b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/package.json new file mode 100644 index 00000000..1769916d --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/modules/dal/package.json @@ -0,0 +1,6 @@ +{ + "name": "dal", + "eggModule": { + "name": "dal" + } +} diff --git a/plugin/dal/test/fixtures/apps/dal-app/package.json b/plugin/dal/test/fixtures/apps/dal-app/package.json new file mode 100644 index 00000000..8e7ccb8d --- /dev/null +++ b/plugin/dal/test/fixtures/apps/dal-app/package.json @@ -0,0 +1,3 @@ +{ + "name": "dal-app" +} diff --git a/plugin/dal/tsconfig.json b/plugin/dal/tsconfig.json new file mode 100644 index 00000000..74935f4b --- /dev/null +++ b/plugin/dal/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./" + }, + "exclude": [ + "node_modules" + ] +} diff --git a/plugin/dal/tsconfig.pub.json b/plugin/dal/tsconfig.pub.json new file mode 100644 index 00000000..8205bd19 --- /dev/null +++ b/plugin/dal/tsconfig.pub.json @@ -0,0 +1,10 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "baseUrl": "./" + }, + "exclude": [ + "node_modules", + "test" + ] +} diff --git a/plugin/dal/typings/index.d.ts b/plugin/dal/typings/index.d.ts new file mode 100644 index 00000000..45f758d4 --- /dev/null +++ b/plugin/dal/typings/index.d.ts @@ -0,0 +1,3 @@ +import 'egg'; +import '@eggjs/tegg-plugin'; +import '@eggjs/tegg-config'; diff --git a/plugin/tegg/lib/AppLoadUnit.ts b/plugin/tegg/lib/AppLoadUnit.ts index f724b5be..f45af14a 100644 --- a/plugin/tegg/lib/AppLoadUnit.ts +++ b/plugin/tegg/lib/AppLoadUnit.ts @@ -42,6 +42,7 @@ export class AppLoadUnit implements LoadUnit { attribute: InitTypeQualifierAttribute, value: PrototypeUtil.getInitType(clazz, { unitPath: this.unitPath, + moduleName: this.name, })!, }, { attribute: LoadUnitNameQualifierAttribute,