├── src ├── webform │ └── 备忘.txt ├── directives │ ├── post-entity-with-owner.ts │ ├── query-condition-is-null.ts │ ├── query-condition-is-not-null.ts │ ├── query-relation-on.ts │ ├── query-relation-where.ts │ ├── query-relation-take.ts │ ├── query-condition-like.ts │ ├── query-entity-skip.ts │ ├── query-field-file-url.ts │ ├── query-entity-take.ts │ ├── query-condition-or-like.ts │ ├── query-field-image-thumbnail.ts │ ├── query-condition-equal.ts │ ├── query-condition-not-equal.ts │ ├── query-condition-less-or-equal.ts │ ├── query-condition-greater-or-equal.ts │ ├── query-relation-count.ts │ ├── query-field-map-value.ts │ ├── query-relation-merge.ts │ ├── query-entity-select.ts │ ├── query-condition-between.ts │ ├── query-entity-orderby.ts │ ├── post-entity-ignore-emperty.ts │ ├── post-entity-bcrypt.ts │ ├── post-entity-send-mail.ts │ ├── query-relation-id.ts │ ├── query-relation-orderby.ts │ ├── post-remove-others.ts │ ├── query-field-image-url.ts │ ├── query-entity-paginate.ts │ ├── query-condition-in.ts │ ├── query-condition-not-in.ts │ ├── query-entity-where.ts │ ├── query-relation-select.ts │ ├── post-relation-cascade.ts │ ├── query-entity-tree.ts │ └── query-entity-fake-relation.ts ├── auth │ ├── constants.ts │ ├── local-auth.guard.ts │ ├── auth.service.spec.ts │ ├── auth.controller.ts │ ├── auth.module.ts │ ├── local.strategy.ts │ ├── jwt.strategy.ts │ └── auth.service.ts ├── util │ ├── create-id.ts │ ├── sleep.ts │ ├── cropt-js.ts │ ├── get-ext.ts │ ├── consts.ts │ └── DirectoryExportedDirectivesLoader.ts ├── rx-event │ ├── consts.ts │ ├── rx-event.module.ts │ ├── rx-event.ts │ └── rx-event.gateway.ts ├── entity-interface │ ├── MailStatus.ts │ ├── RxStorageType.ts │ ├── AddressItem.ts │ ├── MailPriority.ts │ ├── Label.ts │ ├── AliyunConfig.ts │ ├── RxConfig.ts │ ├── AbilityType.ts │ ├── AddressObject.ts │ ├── RxMediaType.ts │ ├── EmailAddress.ts │ ├── RxUserStatus.ts │ ├── RxEntityAuthSettings.ts │ ├── SendStatus.ts │ ├── MailTag.ts │ ├── Imap4Folder.ts │ ├── MailBoxType.ts │ ├── MailLabel.ts │ ├── Attachment.ts │ ├── RxPackage.ts │ ├── SmtpConfig.ts │ ├── RxAbility.ts │ ├── RxDepartment.ts │ ├── MailIdentifier.ts │ ├── MailReceiveConfig.ts │ ├── RxMediaFolder.ts │ ├── RxRole.ts │ ├── RxMedia.ts │ ├── MailConfig.ts │ ├── RxUser.ts │ └── Mail.ts ├── magic-meta │ ├── query │ │ ├── addon-relation-info.ts │ │ ├── query.relation-meta.ts │ │ ├── query-result.ts │ │ ├── query.root-meta.ts │ │ ├── parse-relations-from-where-sql.ts │ │ ├── parse-where-sql.ts │ │ └── query.entity-meta.ts │ ├── post │ │ ├── instance.meta.colletion.ts │ │ ├── relation.meta.colletion.ts │ │ └── instance.meta.ts │ ├── magic.service.ts │ ├── update │ │ └── update.meta.ts │ └── delete │ │ └── delete.meta.ts ├── schema │ ├── graph-meta-interface │ │ ├── package-status.ts │ │ ├── relation-type.ts │ │ ├── column-type.ts │ │ ├── entity-meta.ts │ │ ├── package-meta.ts │ │ ├── relation-meta.ts │ │ └── column-meta.ts │ ├── schema.module.ts │ ├── convert-default.ts │ ├── schema.controller.ts │ └── convert-type.ts ├── storage │ ├── aliyun │ │ ├── consts.ts │ │ ├── UrlCache.ts │ │ └── AliyunClient.ts │ ├── storage.module.ts │ ├── storage.client.ts │ ├── storage.controller.ts │ ├── disk │ │ └── DiskClient.ts │ └── storage.service.ts ├── app.service.ts ├── mailer │ ├── send │ │ ├── i-send-tasks-pool.ts │ │ ├── mail-on-queue.ts │ │ ├── i-send-job.ts │ │ ├── i-send-job-owner.ts │ │ ├── send-event.ts │ │ ├── mailer.send.service.ts │ │ ├── send-tasks-pool.ts │ │ └── send-task.ts │ ├── receive │ │ ├── i-receive-tasks-pool.ts │ │ ├── i-receive-job.ts │ │ ├── i-receive-job-owner.ts │ │ ├── receive-event.ts │ │ ├── mail-teller.ts │ │ ├── receive-tasks-pool.ts │ │ ├── mail-address-job.ts │ │ └── receive-task.ts │ ├── consts.ts │ ├── mailer.clients-pool.ts │ ├── mailer.module.ts │ ├── mailer.controller.ts │ ├── mailer.gateway.ts │ └── mailer.test-service.ts ├── install │ ├── install.data.ts │ ├── install.module.ts │ ├── install.controller.ts │ └── install.service.ts ├── rxbase │ ├── rxbase.module.ts │ └── rxbase.service.ts ├── package-manage │ ├── package-manage.module.ts │ └── package-manage.service.ts ├── app.controller.ts ├── typeorm │ ├── typeorm.module.ts │ └── typeorm.service.ts ├── directive │ ├── directive-type.ts │ ├── delete │ │ ├── delete.directive.class.ts │ │ └── delete.directive.ts │ ├── delete-directive.service.ts │ ├── directive.module.ts │ ├── post │ │ ├── post.directive.class.ts │ │ └── post.directive.ts │ ├── query │ │ ├── query.directive.class.ts │ │ ├── query.field-directive.class.ts │ │ ├── query.relation-directive-class.ts │ │ ├── query.condition-directive-class.ts │ │ ├── query.relation-directive.ts │ │ ├── query.condition-directive.ts │ │ ├── query.field-directive.ts │ │ └── query.directive.ts │ ├── directive.meta.ts │ ├── post-directive.service.ts │ ├── query-directive.service.ts │ └── directive.storage.ts ├── magic │ ├── base │ │ ├── getRelationModel.ts │ │ ├── parse-directives.ts │ │ ├── json-unit.ts │ │ └── tokens.ts │ ├── magic.module.ts │ ├── query │ │ ├── traverser │ │ │ ├── get-directives-or-where-statement.ts │ │ │ ├── get-directives-and-where-statement.ts │ │ │ ├── make-directives-query-builder.ts │ │ │ ├── make-effect-count-query-builder.ts │ │ │ ├── make-abilities-query-builder.ts │ │ │ ├── make-relations-builder.ts │ │ │ └── filter-result.ts │ │ └── magic.query.ts │ ├── upload │ │ ├── magic.upload.service.ts │ │ └── file-upload.utils.ts │ ├── update │ │ └── magic.update.parser.ts │ ├── delete │ │ └── magic.delete.parser.ts │ ├── magic.instance.service.ts │ └── ability.service.ts ├── app.controller.spec.ts ├── main.ts └── app.module.ts ├── RFC3501_cn.htm ├── .gitattributes ├── nest-cli.json ├── .prettierrc ├── tsconfig.build.json ├── test ├── jest-e2e.json └── app.e2e-spec.ts ├── ormconfig-example.json ├── tsconfig.json ├── .eslintrc.js ├── README.md └── package.json /src/webform/备忘.txt: -------------------------------------------------------------------------------- 1 | 该模块提供web表单收集功能 -------------------------------------------------------------------------------- /src/directives/post-entity-with-owner.ts: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /RFC3501_cn.htm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/codebdy/rx-models/HEAD/RFC3501_cn.htm -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | # Auto detect text files and perform LF normalization 2 | * text=auto 3 | -------------------------------------------------------------------------------- /nest-cli.json: -------------------------------------------------------------------------------- 1 | { 2 | "collection": "@nestjs/schematics", 3 | "sourceRoot": "src" 4 | } 5 | -------------------------------------------------------------------------------- /src/auth/constants.ts: -------------------------------------------------------------------------------- 1 | export const jwtConstants = { 2 | secret: 'secretKey', 3 | }; 4 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all", 4 | "endOfLine": "auto" 5 | } -------------------------------------------------------------------------------- /src/util/create-id.ts: -------------------------------------------------------------------------------- 1 | let seedId = 1; 2 | export const createId = () => { 3 | return seedId++; 4 | }; 5 | -------------------------------------------------------------------------------- /src/rx-event/consts.ts: -------------------------------------------------------------------------------- 1 | export const CHANNEL_RX_EVENT = '/rx-event'; 2 | export const RX_EVENT = 'RX-EVENT'; 3 | -------------------------------------------------------------------------------- /src/util/sleep.ts: -------------------------------------------------------------------------------- 1 | export function sleep(time) { 2 | return new Promise((resolve) => setTimeout(resolve, time)); 3 | } 4 | -------------------------------------------------------------------------------- /src/entity-interface/MailStatus.ts: -------------------------------------------------------------------------------- 1 | export enum MailStatus { 2 | SENDING = 'Sending', 3 | 4 | ERROR = 'Error', 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/entity-interface/RxStorageType.ts: -------------------------------------------------------------------------------- 1 | export enum RxStorageType { 2 | Disk = 'Disk', 3 | 4 | AliyunOSS = 'AliyunOSS', 5 | 6 | } 7 | -------------------------------------------------------------------------------- /src/magic-meta/query/addon-relation-info.ts: -------------------------------------------------------------------------------- 1 | export interface AddonRelationInfo { 2 | name: string; 3 | fields: string[]; 4 | } 5 | -------------------------------------------------------------------------------- /src/schema/graph-meta-interface/package-status.ts: -------------------------------------------------------------------------------- 1 | export enum PackageStatus { 2 | EDITING = 'EDITING', 3 | SYNCED = 'SYNCED', 4 | } 5 | -------------------------------------------------------------------------------- /src/storage/aliyun/consts.ts: -------------------------------------------------------------------------------- 1 | export const expaireTime = 36000; 2 | export const refetchPeriod = 3600; 3 | export const cacheSize = 10000; 4 | -------------------------------------------------------------------------------- /src/entity-interface/AddressItem.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface AddressItem { 3 | name?: string; 4 | address: string; 5 | group?: any[]; 6 | } 7 | -------------------------------------------------------------------------------- /src/entity-interface/MailPriority.ts: -------------------------------------------------------------------------------- 1 | export enum MailPriority { 2 | HIGH = 'high', 3 | 4 | NORMAL = 'normal', 5 | 6 | LOW = 'low', 7 | 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.json", 3 | "exclude": ["node_modules", "dist", "test", "**/*spec.ts", "storage", "public"] 4 | } 5 | -------------------------------------------------------------------------------- /src/entity-interface/Label.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface Label { 3 | id?: number; 4 | color?: string; 5 | text?: string; 6 | priority?: number; 7 | 8 | } 9 | -------------------------------------------------------------------------------- /src/entity-interface/AliyunConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface AliyunConfig { 3 | region?: string; 4 | accessKeyId?: string; 5 | accessKeySecret?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/entity-interface/RxConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | export const EntityRxConfig = 'RxConfig'; 3 | export interface RxConfig { 4 | id?: number; 5 | name: string; 6 | value: any; 7 | } 8 | -------------------------------------------------------------------------------- /src/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class AppService { 5 | getHello(): string { 6 | return 'Hello World!'; 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /src/entity-interface/AbilityType.ts: -------------------------------------------------------------------------------- 1 | export enum AbilityType { 2 | CREATE = 'create', 3 | 4 | READ = 'read', 5 | 6 | UPDATE = 'update', 7 | 8 | DELETE = 'delete', 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/entity-interface/AddressObject.ts: -------------------------------------------------------------------------------- 1 | import { AddressItem } from './AddressItem'; 2 | export interface AddressObject { 3 | value?: AddressItem[]; 4 | text?: string; 5 | html?: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/entity-interface/RxMediaType.ts: -------------------------------------------------------------------------------- 1 | export enum RxMediaType { 2 | DOCUMENT = 'document', 3 | 4 | IMAGE = 'image', 5 | 6 | VIDEO = 'video', 7 | 8 | AUDIO = 'audio', 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/entity-interface/EmailAddress.ts: -------------------------------------------------------------------------------- 1 | 2 | export const EntityEmailAddress = 'EmailAddress'; 3 | export interface EmailAddress { 4 | id?: number; 5 | address: string; 6 | isPrimary?: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/auth/local-auth.guard.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | 4 | @Injectable() 5 | export class LocalAuthGuard extends AuthGuard('local') {} 6 | -------------------------------------------------------------------------------- /src/mailer/send/i-send-tasks-pool.ts: -------------------------------------------------------------------------------- 1 | import { SendTask } from './send-task'; 2 | 3 | export interface ISendTasksPool { 4 | getTask(accountId: number): SendTask; 5 | removeTask(accountId: number): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/entity-interface/RxUserStatus.ts: -------------------------------------------------------------------------------- 1 | export enum RxUserStatus { 2 | /** 3 | * label: 正常 4 | */ 5 | NORMAL = 'Normal', 6 | 7 | /** 8 | * label: 禁用 9 | */ 10 | FORBIDDEN = 'Forbidden', 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/magic-meta/query/query.relation-meta.ts: -------------------------------------------------------------------------------- 1 | import { QueryEntityMeta } from './query.entity-meta'; 2 | 3 | export class QueryRelationMeta extends QueryEntityMeta { 4 | parentEntityMeta: QueryEntityMeta; 5 | name: string; 6 | } 7 | -------------------------------------------------------------------------------- /src/mailer/receive/i-receive-tasks-pool.ts: -------------------------------------------------------------------------------- 1 | import { ReceiveTask } from './receive-task'; 2 | 3 | export interface IReceiveTasksPool { 4 | getTask(accountId: number): ReceiveTask; 5 | removeTask(accountId: number): void; 6 | } 7 | -------------------------------------------------------------------------------- /src/entity-interface/RxEntityAuthSettings.ts: -------------------------------------------------------------------------------- 1 | 2 | export const EntityRxEntityAuthSettings = 'RxEntityAuthSettings'; 3 | export interface RxEntityAuthSettings { 4 | id?: number; 5 | entityUuid: string; 6 | expand?: boolean; 7 | } 8 | -------------------------------------------------------------------------------- /src/entity-interface/SendStatus.ts: -------------------------------------------------------------------------------- 1 | export enum SendStatus { 2 | SENDING = 'Sending', 3 | 4 | ERROR = 'Error', 5 | 6 | SUCCESS = 'Success', 7 | 8 | PART_SUCCESS = 'PartSuccess', 9 | 10 | WAITING = 'Waiting', 11 | 12 | } 13 | -------------------------------------------------------------------------------- /test/jest-e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "moduleFileExtensions": ["js", "json", "ts"], 3 | "rootDir": ".", 4 | "testEnvironment": "node", 5 | "testRegex": ".e2e-spec.ts$", 6 | "transform": { 7 | "^.+\\.(t|j)s$": "ts-jest" 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/entity-interface/MailTag.ts: -------------------------------------------------------------------------------- 1 | import { Mail } from './Mail'; 2 | 3 | export const EntityMailTag = 'MailTag'; 4 | export interface MailTag { 5 | id?: number; 6 | color?: string; 7 | name?: string; 8 | beMetMails?: Mail[]; 9 | } 10 | -------------------------------------------------------------------------------- /src/schema/graph-meta-interface/relation-type.ts: -------------------------------------------------------------------------------- 1 | export enum RelationType { 2 | INHERIT = 'inherit', 3 | ONE_TO_ONE = 'one-to-one', 4 | ONE_TO_MANY = 'one-to-many', 5 | MANY_TO_ONE = 'many-to-one', 6 | MANY_TO_MANY = 'many-to-many', 7 | } 8 | -------------------------------------------------------------------------------- /src/entity-interface/Imap4Folder.ts: -------------------------------------------------------------------------------- 1 | export enum Imap4Folder { 2 | SentBoxToSentBox = 'SentBoxToSentBox', 3 | 4 | InBoxToInBox = 'InBoxToInBox', 5 | 6 | SpamBoxToInBox = 'SpamBoxToInBox', 7 | 8 | SpamBoxToSpamBox = 'SpamBoxToSpamBox', 9 | 10 | } 11 | -------------------------------------------------------------------------------- /src/mailer/receive/i-receive-job.ts: -------------------------------------------------------------------------------- 1 | import { IReceiveJobOwner } from './i-receive-job-owner'; 2 | 3 | export interface IReceiveJob { 4 | jobOwner: IReceiveJobOwner; 5 | 6 | start(): void | Promise; 7 | abort(): void; 8 | //continue(): void; 9 | } 10 | -------------------------------------------------------------------------------- /src/mailer/send/mail-on-queue.ts: -------------------------------------------------------------------------------- 1 | import { SendStatus } from 'src/entity-interface/SendStatus'; 2 | 3 | export interface MailOnQueue { 4 | mailId: number; 5 | mailSubject: string; 6 | details?: string; 7 | status: SendStatus; 8 | canCancel?: boolean; 9 | } 10 | -------------------------------------------------------------------------------- /src/install/install.data.ts: -------------------------------------------------------------------------------- 1 | export interface InstallData { 2 | type: string; 3 | host: string; 4 | port: string; 5 | database: string; 6 | username: string; 7 | password: string; 8 | admin: string; 9 | adminPassword: string; 10 | withDemo: boolean; 11 | } 12 | -------------------------------------------------------------------------------- /src/magic-meta/query/query-result.ts: -------------------------------------------------------------------------------- 1 | export type QueryResult = { 2 | data: any | any[]; 3 | pagination?: { 4 | pageSize: number; 5 | pageIndex: number; 6 | totalCount: number; 7 | }; 8 | //没有分页,没有skip,没有take等影响记录数量指令时,总数量。 9 | totalCount?: number; 10 | }; 11 | -------------------------------------------------------------------------------- /src/rx-event/rx-event.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { RxEventGateway } from './rx-event.gateway'; 3 | 4 | @Global() 5 | @Module({ 6 | providers: [RxEventGateway], 7 | exports: [RxEventGateway], 8 | }) 9 | export class RxEventModule {} 10 | -------------------------------------------------------------------------------- /src/rxbase/rxbase.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { RxBaseService } from './rxbase.service'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [RxBaseService], 8 | exports: [RxBaseService], 9 | }) 10 | export class RxBaseModule {} 11 | -------------------------------------------------------------------------------- /src/magic-meta/query/query.root-meta.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN_GET_MANY } from 'src/magic/base/tokens'; 2 | import { QueryEntityMeta } from './query.entity-meta'; 3 | 4 | export class QueryRootMeta extends QueryEntityMeta { 5 | fetchString: 'getOne' | 'getMany' | 'count' = TOKEN_GET_MANY; 6 | } 7 | -------------------------------------------------------------------------------- /src/mailer/receive/i-receive-job-owner.ts: -------------------------------------------------------------------------------- 1 | import { MailerReceiveEvent } from './receive-event'; 2 | import { IReceiveJob } from './i-receive-job'; 3 | 4 | export interface IReceiveJobOwner { 5 | nextJob(): IReceiveJob; 6 | finishJob(): void; 7 | emit(event: MailerReceiveEvent): void; 8 | } 9 | -------------------------------------------------------------------------------- /src/package-manage/package-manage.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PackageManageService } from './package-manage.service'; 3 | 4 | @Module({ 5 | providers: [PackageManageService], 6 | exports: [PackageManageService], 7 | }) 8 | export class PackageManageModule {} 9 | -------------------------------------------------------------------------------- /src/rxbase/rxbase.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | 3 | @Injectable() 4 | export class RxBaseService { 5 | private host: string; 6 | 7 | setHost(host: string) { 8 | this.host = host; 9 | } 10 | 11 | getHost() { 12 | return this.host; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/mailer/send/i-send-job.ts: -------------------------------------------------------------------------------- 1 | import { ISendJobOwner } from './i-send-job-owner'; 2 | import { MailOnQueue } from './mail-on-queue'; 3 | 4 | export interface ISendJob { 5 | jobOwner: ISendJobOwner; 6 | 7 | start(): void | Promise; 8 | abort(): void; 9 | toQueueItem(): MailOnQueue; 10 | } 11 | -------------------------------------------------------------------------------- /src/entity-interface/MailBoxType.ts: -------------------------------------------------------------------------------- 1 | export enum MailBoxType { 2 | INBOX = 'Inbox', 3 | 4 | DRAFT = 'Draft', 5 | 6 | SENT = 'Sent', 7 | 8 | DELETEED = 'Deleted', 9 | 10 | JUNK = 'Junk', 11 | 12 | OUTBOX = 'Outbox', 13 | 14 | ERROR = 'Error', 15 | 16 | LOCAL_SENT = 'LocalSent', 17 | 18 | } 19 | -------------------------------------------------------------------------------- /src/entity-interface/MailLabel.ts: -------------------------------------------------------------------------------- 1 | import { Mail } from './Mail'; 2 | import { Label } from './Label'; 3 | import { RxUser } from './RxUser'; 4 | 5 | export const EntityMailLabel = 'MailLabel'; 6 | export interface MailLabel extends Label { 7 | id?: number; 8 | owner?: RxUser; 9 | attachesTo?: Mail[]; 10 | } 11 | -------------------------------------------------------------------------------- /src/entity-interface/Attachment.ts: -------------------------------------------------------------------------------- 1 | import { Mail } from './Mail'; 2 | 3 | export const EntityAttachment = 'Attachment'; 4 | export interface Attachment { 5 | id?: number; 6 | fileName?: string; 7 | mimeType?: string; 8 | path?: string; 9 | size?: number; 10 | bucket?: string; 11 | belongsTo?: Mail; 12 | } 13 | -------------------------------------------------------------------------------- /src/entity-interface/RxPackage.ts: -------------------------------------------------------------------------------- 1 | 2 | export const EntityRxPackage = 'RxPackage'; 3 | export interface RxPackage { 4 | id?: number; 5 | uuid: string; 6 | name: string; 7 | entities: any; 8 | diagrams?: any; 9 | relations?: any; 10 | status?: string; 11 | createdAt?: Date; 12 | updatedAt?: Date; 13 | } 14 | -------------------------------------------------------------------------------- /src/entity-interface/SmtpConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface SmtpConfig { 3 | address?: string; 4 | replyTo?: string; 5 | host?: string; 6 | port?: string; 7 | timeout?: number; 8 | useSSL?: boolean; 9 | requireTLS?: boolean; 10 | requiresAuth?: boolean; 11 | account?: string; 12 | password?: string; 13 | } 14 | -------------------------------------------------------------------------------- /src/entity-interface/RxAbility.ts: -------------------------------------------------------------------------------- 1 | import { RxRole } from './RxRole'; 2 | 3 | 4 | export const EntityRxAbility = 'RxAbility'; 5 | export interface RxAbility { 6 | id?: number; 7 | entityUuid: string; 8 | columnUuid?: string; 9 | can: boolean; 10 | expression?: string; 11 | abilityType: string; 12 | role?: RxRole; 13 | } 14 | -------------------------------------------------------------------------------- /src/mailer/send/i-send-job-owner.ts: -------------------------------------------------------------------------------- 1 | import { ISendJob } from './i-send-job'; 2 | import { MailerSendEvent } from './send-event'; 3 | 4 | export interface ISendJobOwner { 5 | nextJob(): ISendJob; 6 | finishJob(): void; 7 | emit(event: MailerSendEvent): void; 8 | onQueueChange(): void; 9 | onErrorJob(job: ISendJob): void; 10 | } 11 | -------------------------------------------------------------------------------- /src/app.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get } from '@nestjs/common'; 2 | import { AppService } from './app.service'; 3 | 4 | @Controller() 5 | export class AppController { 6 | constructor(private readonly appService: AppService) {} 7 | 8 | @Get() 9 | getHello(): string { 10 | return this.appService.getHello(); 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/magic-meta/post/instance.meta.colletion.ts: -------------------------------------------------------------------------------- 1 | import { PostDirective } from 'src/directive/post/post.directive'; 2 | import { InstanceMeta } from './instance.meta'; 3 | 4 | export class InstanceMetaCollection { 5 | instances: InstanceMeta[] = []; 6 | directives: PostDirective[] = []; 7 | entity: string; 8 | isSingle = false; 9 | } 10 | -------------------------------------------------------------------------------- /src/rx-event/rx-event.ts: -------------------------------------------------------------------------------- 1 | export enum RxEventType { 2 | InstancePost = 'InstancePost', 3 | InstanceUpdated = 'InstanceUpdated', 4 | InstanceDeleted = 'InstanceDeleted', 5 | } 6 | export interface RxEvent { 7 | eventType: RxEventType; 8 | entity: string; 9 | fields?: string[]; 10 | ownerId?: number; 11 | ids?: number[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/schema/schema.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { SchemaController } from './schema.controller'; 3 | import { SchemaService } from './schema.service'; 4 | 5 | @Module({ 6 | providers: [SchemaService], 7 | exports: [SchemaService], 8 | controllers: [SchemaController], 9 | }) 10 | export class SchemaModule {} 11 | -------------------------------------------------------------------------------- /src/entity-interface/RxDepartment.ts: -------------------------------------------------------------------------------- 1 | import { RxUser } from './RxUser'; 2 | import { RxRole } from './RxRole'; 3 | 4 | export const EntityRxDepartment = 'RxDepartment'; 5 | export interface RxDepartment { 6 | id?: number; 7 | name: string; 8 | desctiption?: string; 9 | uuid?: string; 10 | hasUsers?: RxUser[]; 11 | roles?: RxRole[]; 12 | } 13 | -------------------------------------------------------------------------------- /src/entity-interface/MailIdentifier.ts: -------------------------------------------------------------------------------- 1 | import { Mail } from './Mail'; 2 | import { MailBoxType } from './MailBoxType'; 3 | 4 | export const EntityMailIdentifier = 'MailIdentifier'; 5 | export interface MailIdentifier { 6 | id?: number; 7 | uidl: string; 8 | mailAddress: string; 9 | file: string; 10 | fromBox: MailBoxType; 11 | mail?: Mail; 12 | } 13 | -------------------------------------------------------------------------------- /src/mailer/send/send-event.ts: -------------------------------------------------------------------------------- 1 | import { MailOnQueue } from './mail-on-queue'; 2 | 3 | export enum MailerSendEventType { 4 | sentOneMail = 'sentOneMail', 5 | sendQueue = 'sendQueue', 6 | } 7 | 8 | export interface MailerSendEvent { 9 | type: MailerSendEventType; 10 | mailId?: number; 11 | mailSubject?: string; 12 | mailsQueue?: MailOnQueue[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/typeorm/typeorm.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { SchemaModule } from 'src/schema/schema.module'; 3 | import { TypeOrmService } from './typeorm.service'; 4 | 5 | @Global() 6 | @Module({ 7 | imports: [SchemaModule], 8 | providers: [TypeOrmService], 9 | exports: [TypeOrmService], 10 | }) 11 | export class TypeOrmModule {} 12 | -------------------------------------------------------------------------------- /ormconfig-example.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "mysql", 3 | "host": "localhost", 4 | "port": 3306, 5 | "username": "your_username", 6 | "password": "your_password", 7 | "database": "rxdrag", 8 | "synchronize": true, 9 | "migrations": ["dist/migration/*.js"], 10 | "cli": { 11 | "entitiesDir": "src/entity", 12 | "migrationsDir": "src/migration" 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/directive/directive-type.ts: -------------------------------------------------------------------------------- 1 | export enum DirectiveType { 2 | //结果过滤,可用于关联实体 3 | QUERY_ENTITY_DIRECTIVE = 1, 4 | QUERY_FIELD_DIRECTIVE, 5 | QUERY_RELATION_DIRECTIVE, 6 | //condition directive 既可以用于Entity级别,也可以用于relation级别 7 | QUERY_CONDITION_DIRECTIVE, 8 | POST_ENTITY_DIRECTIVE, 9 | POST_RELATION_DIRECTIVE, 10 | DELETE_DIRECTIVE, 11 | UPLOAD_DIRECTIVE, 12 | } 13 | -------------------------------------------------------------------------------- /src/storage/storage.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { StorageController } from './storage.controller'; 3 | import { StorageService } from './storage.service'; 4 | 5 | @Global() 6 | @Module({ 7 | providers: [StorageService], 8 | exports: [StorageService], 9 | controllers: [StorageController], 10 | }) 11 | export class StorageModule {} 12 | -------------------------------------------------------------------------------- /src/entity-interface/MailReceiveConfig.ts: -------------------------------------------------------------------------------- 1 | 2 | export interface MailReceiveConfig { 3 | name: string; 4 | host: string; 5 | port: string; 6 | timeout?: number; 7 | account?: string; 8 | password?: string; 9 | ssl?: boolean; 10 | spa?: boolean; 11 | starTtls?: boolean; 12 | removeAfterDays?: number; 13 | removeFromServer?: boolean; 14 | folders?: any[]; 15 | } 16 | -------------------------------------------------------------------------------- /src/schema/graph-meta-interface/column-type.ts: -------------------------------------------------------------------------------- 1 | export enum ColumnType { 2 | Number = 'Number', 3 | Boolean = 'Boolean', 4 | String = 'String', 5 | Text = 'Text', 6 | MediumText = 'MediumText', 7 | LongText = 'LongText', 8 | Enum = 'Enum', 9 | Date = 'Date', 10 | SimpleJson = 'simple-json', 11 | SimpleArray = 'simple-array', 12 | JsonArray = 'json-array', 13 | } 14 | -------------------------------------------------------------------------------- /src/entity-interface/RxMediaFolder.ts: -------------------------------------------------------------------------------- 1 | import { RxUser } from './RxUser'; 2 | import { RxMedia } from './RxMedia'; 3 | 4 | export const EntityRxMediaFolder = 'RxMediaFolder'; 5 | export interface RxMediaFolder { 6 | id?: number; 7 | name: string; 8 | order?: string; 9 | parent?: RxMediaFolder; 10 | user?: RxUser; 11 | medias?: RxMedia[]; 12 | children?: RxMediaFolder[]; 13 | } 14 | -------------------------------------------------------------------------------- /src/magic-meta/post/relation.meta.colletion.ts: -------------------------------------------------------------------------------- 1 | import { PostDirective } from 'src/directive/post/post.directive'; 2 | import { InstanceMeta } from './instance.meta'; 3 | 4 | export class RelationMetaCollection { 5 | entities: InstanceMeta[] = []; 6 | ids: number[] = []; 7 | directives: PostDirective[] = []; 8 | entity: string; 9 | relationName: string; 10 | isSingle = false; 11 | } 12 | -------------------------------------------------------------------------------- /src/magic-meta/magic.service.ts: -------------------------------------------------------------------------------- 1 | import { QueryResult } from 'src/magic-meta/query/query-result'; 2 | import { RxUser } from 'src/entity-interface/RxUser'; 3 | 4 | export interface MagicService { 5 | me: RxUser; 6 | 7 | query(json: any): Promise; 8 | 9 | post(json: any): Promise; 10 | 11 | delete(json: any): Promise; 12 | 13 | update(json: any): Promise; 14 | } 15 | -------------------------------------------------------------------------------- /src/magic/base/getRelationModel.ts: -------------------------------------------------------------------------------- 1 | import { getRepository } from 'typeorm'; 2 | 3 | export function getRelationModel(key: string, model: string) { 4 | const repository = getRepository(model); 5 | for (const relation of repository.metadata.relations) { 6 | if (relation.propertyName === key) { 7 | return relation.inverseEntityMetadata.name; 8 | } 9 | } 10 | 11 | return undefined; 12 | } 13 | -------------------------------------------------------------------------------- /src/schema/graph-meta-interface/entity-meta.ts: -------------------------------------------------------------------------------- 1 | import { ColumnMeta } from './column-meta'; 2 | 3 | export enum EntityType { 4 | NORMAL = 'Normal', 5 | ENUM = 'Enum', 6 | ABSTRACT = 'Abstract', 7 | INTERFACE = 'Interface', 8 | } 9 | 10 | export interface EntityMeta { 11 | uuid: string; 12 | name: string; 13 | tableName?: string; 14 | entityType?: EntityType; 15 | columns: ColumnMeta[]; 16 | eventable?: boolean; 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "module": "commonjs", 4 | "declaration": true, 5 | "removeComments": true, 6 | "emitDecoratorMetadata": true, 7 | "experimentalDecorators": true, 8 | "allowSyntheticDefaultImports": true, 9 | "target": "es2017", 10 | "sourceMap": true, 11 | "outDir": "./dist", 12 | "baseUrl": "./", 13 | "incremental": true, 14 | "resolveJsonModule": true 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/util/cropt-js.ts: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const CryptoJS = require('crypto-js'); 3 | 4 | export function encropt(message: string, croptKey: string) { 5 | return CryptoJS.AES.encrypt(message, croptKey).toString(); 6 | } 7 | 8 | export function decypt(ciphertext: string, croptKey: string) { 9 | const bytes = CryptoJS.AES.decrypt(ciphertext, croptKey); 10 | return bytes.toString(CryptoJS.enc.Utf8); 11 | } 12 | -------------------------------------------------------------------------------- /src/entity-interface/RxRole.ts: -------------------------------------------------------------------------------- 1 | import { RxUser } from './RxUser'; 2 | import { RxAbility } from './RxAbility'; 3 | import { RxDepartment } from './RxDepartment'; 4 | 5 | export const EntityRxRole = 'RxRole'; 6 | export interface RxRole { 7 | id?: number; 8 | name: string; 9 | description?: string; 10 | createdAt?: Date; 11 | updatedAt?: Date; 12 | users?: RxUser[]; 13 | abilities?: RxAbility[]; 14 | belongsToDepartment?: RxDepartment; 15 | } 16 | -------------------------------------------------------------------------------- /src/magic-meta/update/update.meta.ts: -------------------------------------------------------------------------------- 1 | import { RxAbility } from 'src/entity-interface/RxAbility'; 2 | import { EntityMeta } from 'src/schema/graph-meta-interface/entity-meta'; 3 | export class UpdateMeta { 4 | ids?: number[] = []; 5 | columns: any = {}; 6 | entityMeta: EntityMeta; 7 | expandFieldForAuth = false; 8 | abilities: RxAbility[] = []; 9 | whereSQL?: string; 10 | 11 | get entity() { 12 | return this.entityMeta.name; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/schema/graph-meta-interface/package-meta.ts: -------------------------------------------------------------------------------- 1 | import { EntityMeta } from './entity-meta'; 2 | import { RelationMeta } from './relation-meta'; 3 | 4 | export enum PackageStatus { 5 | EDITING = 'EDITING', 6 | SYNCED = 'SYNCED', 7 | } 8 | 9 | export interface PackageMeta { 10 | id?: number; 11 | uuid: string; 12 | name: string; 13 | entities?: EntityMeta[]; 14 | diagrams?: any[]; 15 | relations?: RelationMeta[]; 16 | status: PackageStatus; 17 | } 18 | -------------------------------------------------------------------------------- /src/install/install.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { PackageManageModule } from 'src/package-manage/package-manage.module'; 3 | import { InstallController } from './install.controller'; 4 | import { InstallService } from './install.service'; 5 | 6 | @Module({ 7 | imports: [PackageManageModule], 8 | providers: [InstallService], 9 | exports: [InstallService], 10 | controllers: [InstallController], 11 | }) 12 | export class InstallModule {} 13 | -------------------------------------------------------------------------------- /src/schema/convert-default.ts: -------------------------------------------------------------------------------- 1 | import { ColumnMeta } from './graph-meta-interface/column-meta'; 2 | import { ColumnType } from './graph-meta-interface/column-type'; 3 | 4 | export function convertDefault(column: ColumnMeta) { 5 | if (column.type === ColumnType.Boolean) { 6 | if (column.default === 'false') { 7 | return false; 8 | } 9 | 10 | if (column.default === 'true') { 11 | return true; 12 | } 13 | } 14 | 15 | return column.default; 16 | } 17 | -------------------------------------------------------------------------------- /src/mailer/consts.ts: -------------------------------------------------------------------------------- 1 | export const CHANNEL_MAILER = '/mailer'; 2 | export const EVENT_REGISTER_MAIL_CLIENT = 'registerMailClient'; 3 | export const EVENT_RECEIVEMAILS = 'receiveMails'; 4 | export const EVENT_MAIL_RECEIVING_EVENT = 'mailReceivingEvent'; 5 | export const EVENT_CANCEL_RECEIVE = 'cancelReceive'; 6 | //export const EVENT_CONTINUE_RECEIVE = 'continueReceive'; 7 | export const EVENT_MAIL_SENDING_EVENT = 'mailSendingEvent'; 8 | 9 | export const RX_MAIL_SIGN_ID = 'rx-mail-sign-id'; 10 | export const RX_MAIL_TO_ID = 'rx-mail-to-id'; 11 | -------------------------------------------------------------------------------- /src/auth/auth.service.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AuthService } from './auth.service'; 3 | 4 | describe('AuthService', () => { 5 | let service: AuthService; 6 | 7 | beforeEach(async () => { 8 | const module: TestingModule = await Test.createTestingModule({ 9 | providers: [AuthService], 10 | }).compile(); 11 | 12 | service = module.get(AuthService); 13 | }); 14 | 15 | it('should be defined', () => { 16 | expect(service).toBeDefined(); 17 | }); 18 | }); 19 | -------------------------------------------------------------------------------- /src/entity-interface/RxMedia.ts: -------------------------------------------------------------------------------- 1 | import { RxMediaFolder } from './RxMediaFolder'; 2 | import { RxUser } from './RxUser'; 3 | import { RxMediaType } from './RxMediaType'; 4 | 5 | export const EntityRxMedia = 'RxMedia'; 6 | export interface RxMedia { 7 | id?: number; 8 | name: string; 9 | mimetype?: string; 10 | fileName?: string; 11 | path?: string; 12 | size?: number; 13 | updatedAt?: Date; 14 | createdAt?: Date; 15 | mediaType?: RxMediaType; 16 | avatarOfUser?: RxUser; 17 | owner?: RxUser; 18 | folder?: RxMediaFolder; 19 | } 20 | -------------------------------------------------------------------------------- /src/rx-event/rx-event.gateway.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { WebSocketGateway, WebSocketServer } from '@nestjs/websockets'; 3 | import { Server } from 'socket.io'; 4 | import { CHANNEL_RX_EVENT, RX_EVENT } from './consts'; 5 | import { RxEvent } from './rx-event'; 6 | 7 | @Injectable() 8 | @WebSocketGateway({ namespace: CHANNEL_RX_EVENT }) 9 | export class RxEventGateway { 10 | @WebSocketServer() wsServer: Server; 11 | 12 | broadcastEvent(event: RxEvent) { 13 | this.wsServer.emit(RX_EVENT, event); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/entity-interface/MailConfig.ts: -------------------------------------------------------------------------------- 1 | import { RxUser } from './RxUser'; 2 | import { MailReceiveConfig } from './MailReceiveConfig'; 3 | import { SmtpConfig } from './SmtpConfig'; 4 | 5 | export const EntityMailConfig = 'MailConfig'; 6 | export interface MailConfig { 7 | id?: number; 8 | address: string; 9 | password?: string; 10 | pop3?: MailReceiveConfig; 11 | imap4?: MailReceiveConfig; 12 | smtp?: SmtpConfig; 13 | interval?: number; 14 | stop?: boolean; 15 | receiveInterval?: boolean; 16 | sendName?: string; 17 | belongsTo?: RxUser; 18 | } 19 | -------------------------------------------------------------------------------- /src/directive/delete/delete.directive.class.ts: -------------------------------------------------------------------------------- 1 | import { MagicService } from 'src/magic-meta/magic.service'; 2 | import { DirectiveType } from '../directive-type'; 3 | import { DirectiveMeta } from '../directive.meta'; 4 | import { DeleteDirective } from './delete.directive'; 5 | 6 | export interface DeleteDirectiveClass extends Function { 7 | description?: string; 8 | version?: string; 9 | 10 | directiveType: DirectiveType; 11 | directiveName: string; 12 | new ( 13 | directiveMeta: DirectiveMeta, 14 | magicService: MagicService, 15 | ): DeleteDirective; 16 | } 17 | -------------------------------------------------------------------------------- /src/magic/base/parse-directives.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveMeta } from 'src/directive/directive.meta'; 2 | 3 | function stringArrayToDirectives(strArray: string[]): DirectiveMeta[] { 4 | const directives = []; 5 | for (const directiveStr of strArray) { 6 | directives.push(new DirectiveMeta(directiveStr)); 7 | } 8 | return directives; 9 | } 10 | 11 | export function parseDirectives(str: string): [string?, DirectiveMeta[]?] { 12 | const strArray: string[] = str.split('@'); 13 | const key = strArray[0]?.trim(); 14 | return [key, stringArrayToDirectives(strArray.splice(1))]; 15 | } 16 | -------------------------------------------------------------------------------- /src/directives/query-condition-is-null.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | 4 | export class QueryConditionIsNullDirective extends QueryConditionDirective { 5 | static description = `Condition isNull directive.`; 6 | 7 | static version = '1.0'; 8 | 9 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 10 | 11 | static directiveName = 'isNull'; 12 | 13 | getAndWhereStatement(): [string, any] { 14 | return [`${this.field} IS NULL `, {}]; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/storage/storage.client.ts: -------------------------------------------------------------------------------- 1 | import { ImageSize } from 'src/util/consts'; 2 | 3 | export interface StorageClient { 4 | checkAndCreateBucket(bucket: string): Promise; 5 | 6 | putFileData(name: string, data: any, bucket: string): Promise; 7 | 8 | putFile( 9 | name: string, 10 | file: Express.Multer.File, 11 | bucket: string, 12 | ): Promise; 13 | 14 | resizeImage(path: string, bucket: string, size?: ImageSize): Promise; 15 | 16 | fileLocalPath(path: string, bucket: string): Promise; 17 | 18 | fileUrl(path: string, bucket: string): Promise; 19 | } 20 | -------------------------------------------------------------------------------- /src/magic-meta/post/instance.meta.ts: -------------------------------------------------------------------------------- 1 | import { RxAbility } from 'src/entity-interface/RxAbility'; 2 | import { EntityMeta } from 'src/schema/graph-meta-interface/entity-meta'; 3 | import { RelationMetaCollection } from './relation.meta.colletion'; 4 | 5 | export class InstanceMeta { 6 | meta: any = {}; 7 | relations: { [key: string]: RelationMetaCollection } = {}; 8 | savedRelations: { [key: string]: any } = {}; 9 | 10 | entityMeta: EntityMeta; 11 | //展开,对每个属性进行权限设置 12 | expandFieldForAuth = false; 13 | abilities: RxAbility[] = []; 14 | 15 | get entity() { 16 | return this.entityMeta?.name; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /src/util/get-ext.ts: -------------------------------------------------------------------------------- 1 | export function getExt(name: string) { 2 | if (!name) { 3 | return name; 4 | } 5 | const index = name.lastIndexOf('.'); 6 | return name 7 | .substr(index + 1) 8 | .replace('*', 'x') 9 | .replace('|', 'x') 10 | .replace(':', 'x') 11 | .replace('<', 'x') 12 | .replace('>', 'x') 13 | .replace('\\', 'x') 14 | .replace('/', 'x') 15 | .replace('?', 'x') 16 | .replace("'", 'x') 17 | .replace('`', 'x') 18 | .replace('@', 'x') 19 | .replace('#', 'x') 20 | .replace('%', 'x') 21 | .replace('^', 'x') 22 | .replace('&', 'x') 23 | .replace('$', 'x'); 24 | } 25 | -------------------------------------------------------------------------------- /src/directive/delete-directive.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { DirectiveStorage } from './directive.storage'; 3 | import { DeleteDirectiveClass } from './delete/delete.directive.class'; 4 | 5 | @Injectable() 6 | export class DeleteDirectiveService { 7 | constructor(private readonly directiveStorage: DirectiveStorage) {} 8 | 9 | findDirectiveOrFailed(name: string): DeleteDirectiveClass { 10 | const directiveClass = this.directiveStorage.deleteDirectiveClasses[name]; 11 | if (!directiveClass) { 12 | throw new Error(`No delete directive "${name}"`); 13 | } 14 | return directiveClass; 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /src/auth/auth.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Request, Post, UseGuards, Get } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { AuthService } from './auth.service'; 4 | import { LocalAuthGuard } from './local-auth.guard'; 5 | 6 | @Controller('auth') 7 | export class AuthController { 8 | constructor(private readonly authService: AuthService) {} 9 | 10 | @UseGuards(LocalAuthGuard) 11 | @Post('login') 12 | async login(@Request() req) { 13 | return this.authService.login(req.user); 14 | } 15 | 16 | @UseGuards(AuthGuard()) 17 | @Get('me') 18 | getProfile(@Request() req) { 19 | return req.user; 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/schema/schema.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, UseGuards, Request } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { RxUser } from 'src/entity-interface/RxUser'; 4 | import { SchemaService } from './schema.service'; 5 | 6 | @Controller() 7 | export class SchemaController { 8 | constructor(private readonly schemaService: SchemaService) {} 9 | 10 | @UseGuards(AuthGuard()) 11 | @Get('published-schema') 12 | getSchemas(@Request() req) { 13 | const user = req.user as RxUser; 14 | if (!user.isSupper && !user.isDemo) { 15 | return []; 16 | } 17 | return this.schemaService.getPackageSchemas(); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/directive/delete/delete.directive.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import { DeleteMeta } from 'src/magic-meta/delete/delete.meta'; 4 | import { MagicService } from 'src/magic-meta/magic.service'; 5 | import { DirectiveMeta } from '../directive.meta'; 6 | 7 | export class DeleteDirective { 8 | constructor( 9 | protected readonly directiveMeta: DirectiveMeta, 10 | protected readonly magicService: MagicService, 11 | ) {} 12 | 13 | async beforeDelete(deleteMeta: DeleteMeta) { 14 | return; 15 | } 16 | 17 | async afterDelete(deletedIds: number[], deleteMeta: DeleteMeta) {} 18 | } 19 | -------------------------------------------------------------------------------- /src/magic/magic.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | 3 | import { DirectiveModule } from 'src/directive/directive.module'; 4 | import { MailerModule } from 'src/mailer/mailer.module'; 5 | import { RxEventModule } from 'src/rx-event/rx-event.module'; 6 | import { SchemaModule } from 'src/schema/schema.module'; 7 | import { MagicController } from './magic.controller'; 8 | import { MagicUploadService } from './upload/magic.upload.service'; 9 | 10 | @Global() 11 | @Module({ 12 | imports: [SchemaModule, DirectiveModule, MailerModule, RxEventModule], 13 | providers: [MagicUploadService], 14 | controllers: [MagicController], 15 | }) 16 | export class MagicModule {} 17 | -------------------------------------------------------------------------------- /src/app.controller.spec.ts: -------------------------------------------------------------------------------- 1 | import { Test, TestingModule } from '@nestjs/testing'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | 5 | describe('AppController', () => { 6 | let app: TestingModule; 7 | 8 | beforeAll(async () => { 9 | app = await Test.createTestingModule({ 10 | controllers: [AppController], 11 | providers: [AppService], 12 | }).compile(); 13 | }); 14 | 15 | describe('getHello', () => { 16 | it('should return "Hello World!"', () => { 17 | const appController = app.get(AppController); 18 | expect(appController.getHello()).toBe('Hello World!'); 19 | }); 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /src/directive/directive.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { DirectiveStorage } from './directive.storage'; 3 | import { DeleteDirectiveService } from './delete-directive.service'; 4 | import { PostDirectiveService } from './post-directive.service'; 5 | import { QueryDirectiveService } from './query-directive.service'; 6 | 7 | @Module({ 8 | providers: [ 9 | DirectiveStorage, 10 | QueryDirectiveService, 11 | PostDirectiveService, 12 | DeleteDirectiveService, 13 | ], 14 | exports: [ 15 | DirectiveStorage, 16 | QueryDirectiveService, 17 | PostDirectiveService, 18 | DeleteDirectiveService, 19 | ], 20 | }) 21 | export class DirectiveModule {} 22 | -------------------------------------------------------------------------------- /src/directives/query-condition-is-not-null.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | 4 | export class QueryConditionIsNotNullDirective extends QueryConditionDirective { 5 | static description = `Condition isNotNull directive.`; 6 | 7 | static version = '1.0'; 8 | 9 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 10 | 11 | static directiveName = 'isNotNull'; 12 | 13 | getAndWhereStatement(): [string, any] { 14 | //const paramName = 'param' + createId(); 15 | //const paramValue = this.value; 16 | return [`${this.field} IS NOT NULL `, {}]; 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /test/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import * as request from 'supertest'; 2 | import { Test } from '@nestjs/testing'; 3 | import { AppModule } from './../src/app.module'; 4 | import { INestApplication } from '@nestjs/common'; 5 | 6 | describe('AppController (e2e)', () => { 7 | let app: INestApplication; 8 | 9 | beforeAll(async () => { 10 | const moduleFixture = await Test.createTestingModule({ 11 | imports: [AppModule], 12 | }).compile(); 13 | 14 | app = moduleFixture.createNestApplication(); 15 | await app.init(); 16 | }); 17 | 18 | it('/ (GET)', () => { 19 | return request(app.getHttpServer()) 20 | .get('/') 21 | .expect(200) 22 | .expect('Hello World!'); 23 | }); 24 | }); 25 | -------------------------------------------------------------------------------- /src/magic/query/traverser/get-directives-or-where-statement.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | 3 | export function getDirectivesOrWhereStatement( 4 | directives: QueryDirective[], 5 | ): [string, any] { 6 | const whereStringArray: string[] = []; 7 | let whereParams: any = {}; 8 | directives.forEach((directive) => { 9 | const [whereStr, param] = directive.getOrWhereStatement() || []; 10 | if (whereStr) { 11 | whereStringArray.push(whereStr); 12 | whereParams = { ...whereParams, ...param }; 13 | } 14 | }); 15 | if (whereStringArray.length > 0) { 16 | return [whereStringArray.join(' OR '), whereParams]; 17 | } 18 | return [undefined, undefined]; 19 | } 20 | -------------------------------------------------------------------------------- /src/magic/query/traverser/get-directives-and-where-statement.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | 3 | export function getDirectivesAndWhereStatement( 4 | directives: QueryDirective[], 5 | ): [string, any] { 6 | const whereStringArray: string[] = []; 7 | let whereParams: any = {}; 8 | directives.forEach((directive) => { 9 | const [whereStr, param] = directive.getAndWhereStatement() || []; 10 | if (whereStr) { 11 | whereStringArray.push(whereStr); 12 | whereParams = { ...whereParams, ...param }; 13 | } 14 | }); 15 | if (whereStringArray.length > 0) { 16 | return [whereStringArray.join(' AND '), whereParams]; 17 | } 18 | return [undefined, undefined]; 19 | } 20 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | parser: '@typescript-eslint/parser', 3 | parserOptions: { 4 | project: 'tsconfig.json', 5 | sourceType: 'module', 6 | }, 7 | plugins: ['@typescript-eslint/eslint-plugin'], 8 | extends: [ 9 | 'plugin:@typescript-eslint/recommended', 10 | 'plugin:prettier/recommended', 11 | ], 12 | root: true, 13 | env: { 14 | node: true, 15 | jest: true, 16 | }, 17 | ignorePatterns: ['.eslintrc.js'], 18 | rules: { 19 | '@typescript-eslint/interface-name-prefix': 'off', 20 | '@typescript-eslint/explicit-function-return-type': 'off', 21 | '@typescript-eslint/explicit-module-boundary-types': 'off', 22 | '@typescript-eslint/no-explicit-any': 'off', 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /src/directives/query-relation-on.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryRelationDirective } from 'src/directive/query/query.relation-directive'; 3 | //import { parseOnSql } from 'src/magic-meta/query/parse-on-sql'; 4 | 5 | export class QueryModelOnDirective extends QueryRelationDirective { 6 | static description = ` 7 | Relation on directive. 8 | 该指令目前不能正常使用。 9 | `; 10 | 11 | static version = '1.0'; 12 | 13 | static directiveType = DirectiveType.QUERY_RELATION_DIRECTIVE; 14 | 15 | static directiveName = 'on'; 16 | 17 | //getAndWhereStatement(): [string, any] | void { 18 | //const sql = parseOnSql(this.directiveMeta.value, this.relationMeta); 19 | //return [sql, {}]; 20 | //} 21 | } 22 | -------------------------------------------------------------------------------- /src/auth/auth.module.ts: -------------------------------------------------------------------------------- 1 | import { Global, Module } from '@nestjs/common'; 2 | import { AuthService } from './auth.service'; 3 | import { LocalStrategy } from './local.strategy'; 4 | import { PassportModule } from '@nestjs/passport'; 5 | import { JwtModule } from '@nestjs/jwt'; 6 | import { jwtConstants } from './constants'; 7 | import { JwtStrategy } from './jwt.strategy'; 8 | 9 | @Global() 10 | @Module({ 11 | imports: [ 12 | PassportModule.register({ defaultStrategy: 'jwt' }), 13 | JwtModule.register({ 14 | secret: jwtConstants.secret, 15 | signOptions: { expiresIn: '7 days' }, 16 | }), 17 | ], 18 | providers: [AuthService, LocalStrategy, JwtStrategy], 19 | exports: [AuthService, PassportModule], 20 | }) 21 | export class AuthModule {} 22 | -------------------------------------------------------------------------------- /src/directives/query-relation-where.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryRelationDirective } from 'src/directive/query/query.relation-directive'; 3 | //import { parseOnSql } from 'src/magic-meta/query/parse-on-sql'; 4 | 5 | export class QueryModelWhereDirective extends QueryRelationDirective { 6 | static description = ` 7 | Relation where directive. 8 | 本方法暂未实现 9 | `; 10 | 11 | static version = '1.0'; 12 | 13 | static directiveType = DirectiveType.QUERY_RELATION_DIRECTIVE; 14 | 15 | static directiveName = 'where'; 16 | 17 | //getAndWhereStatement(): [string, any] | void { 18 | //const sql = parseOnSql(this.directiveMeta.value, this.relationMeta); 19 | // return [sql, {}]; 20 | //} 21 | } 22 | -------------------------------------------------------------------------------- /src/directive/post/post.directive.class.ts: -------------------------------------------------------------------------------- 1 | import { MagicService } from 'src/magic-meta/magic.service'; 2 | import { MailerSendService } from 'src/mailer/send/mailer.send.service'; 3 | import { EntityManager } from 'typeorm'; 4 | import { DirectiveType } from '../directive-type'; 5 | import { DirectiveMeta } from '../directive.meta'; 6 | import { PostDirective } from './post.directive'; 7 | 8 | export interface PostDirectiveClass extends Function { 9 | description?: string; 10 | version?: string; 11 | 12 | directiveType: DirectiveType; 13 | directiveName: string; 14 | new ( 15 | entityManger: EntityManager, 16 | directiveMeta: DirectiveMeta, 17 | magicService: MagicService, 18 | mailerSendService: MailerSendService, 19 | ): PostDirective; 20 | } 21 | -------------------------------------------------------------------------------- /src/directive/query/query.directive.class.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveMeta } from '../directive.meta'; 2 | import { QueryDirective } from './query.directive'; 3 | import { DirectiveType } from '../directive-type'; 4 | import { MagicService } from 'src/magic-meta/magic.service'; 5 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 6 | import { SchemaService } from 'src/schema/schema.service'; 7 | 8 | export interface QueryDirectiveClass extends Function { 9 | description?: string; 10 | version?: string; 11 | 12 | directiveType: DirectiveType; 13 | directiveName: string; 14 | new ( 15 | directiveMeta: DirectiveMeta, 16 | rootMeta: QueryRootMeta, 17 | magicService: MagicService, 18 | schemaService: SchemaService, 19 | ): QueryDirective; 20 | } 21 | -------------------------------------------------------------------------------- /src/directives/query-relation-take.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryRelationDirective } from 'src/directive/query/query.relation-directive'; 3 | import { SelectQueryBuilder } from 'typeorm'; 4 | 5 | export class QueryRelationTakeDirective extends QueryRelationDirective { 6 | static description = `Magic query directive, set take(count) to QueryBuilder.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_RELATION_DIRECTIVE; 11 | 12 | static directiveName = 'take'; 13 | 14 | get count() { 15 | return this.directiveMeta.value; 16 | } 17 | 18 | makeQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 19 | qb.take(this.count); 20 | return qb; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/directives/query-condition-like.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionLikeDirective extends QueryConditionDirective { 6 | static description = `Condition like irective.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = 'like'; 13 | 14 | getAndWhereStatement(): [string, any] { 15 | const paramName = 'param' + createId(); 16 | return [ 17 | `${this.field} LIKE :${paramName} `, 18 | { 19 | [paramName]: this.value, 20 | }, 21 | ]; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/directives/query-entity-skip.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { SelectQueryBuilder } from 'typeorm'; 4 | 5 | export class QueryEntitySkipDirective extends QueryDirective { 6 | static description = ` 7 | Magic query directive, set skip(count) to QueryBuilder. 8 | `; 9 | static version = '1.0'; 10 | 11 | static directiveType = DirectiveType.QUERY_ENTITY_DIRECTIVE; 12 | 13 | static directiveName = 'skip'; 14 | 15 | isEffectResultCount = true; 16 | 17 | get count() { 18 | return this.directiveMeta.value; 19 | } 20 | 21 | addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 22 | qb.skip(this.count); 23 | return qb; 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/storage/storage.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Get, HttpException, UseGuards } from '@nestjs/common'; 2 | import { AuthGuard } from '@nestjs/passport'; 3 | import { StorageService } from './storage.service'; 4 | 5 | @Controller() 6 | export class StorageController { 7 | constructor(private service: StorageService) {} 8 | 9 | //客户端上传OSS用的TOKEN,本方法暂时没用 10 | @UseGuards(AuthGuard()) 11 | @Get('get-token-object') 12 | async getTokenObject() { 13 | try { 14 | //return await this.service.getTokenObject(); 15 | } catch (error: any) { 16 | console.error('get-token-object error:', error); 17 | throw new HttpException( 18 | { 19 | status: 500, 20 | error: error.message, 21 | }, 22 | 500, 23 | ); 24 | } 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/directive/query/query.field-directive.class.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveMeta } from '../directive.meta'; 2 | import { MagicService } from 'src/magic-meta/magic.service'; 3 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 4 | import { SchemaService } from 'src/schema/schema.service'; 5 | import { StorageService } from 'src/storage/storage.service'; 6 | import { QueryDirectiveClass } from './query.directive.class'; 7 | import { QueryFieldDirective } from './query.field-directive'; 8 | 9 | export interface QueryFieldDirectiveClass extends QueryDirectiveClass { 10 | new ( 11 | directiveMeta: DirectiveMeta, 12 | rootMeta: QueryRootMeta, 13 | magicService: MagicService, 14 | schemaService: SchemaService, 15 | storageService: StorageService, 16 | ): QueryFieldDirective; 17 | } 18 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { NestFactory } from '@nestjs/core'; 2 | import { NestExpressApplication } from '@nestjs/platform-express'; 3 | import { join } from 'path'; 4 | import { AppModule } from './app.module'; 5 | 6 | async function bootstrap() { 7 | const app = await NestFactory.create(AppModule); 8 | // eslint-disable-next-line @typescript-eslint/no-var-requires 9 | const bodyParser = require('body-parser'); 10 | app.use(bodyParser.json({ limit: '50mb' })); 11 | app.use( 12 | bodyParser.urlencoded({ 13 | limit: '50mb', 14 | extended: true, 15 | parameterLimit: 50000, 16 | }), 17 | ); 18 | app.useStaticAssets(join(__dirname, '..', 'public')); 19 | app.enableCors({ credentials: true, origin: true }); 20 | await app.listen(3001); 21 | } 22 | bootstrap(); 23 | -------------------------------------------------------------------------------- /src/directive/query/query.relation-directive-class.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveMeta } from '../directive.meta'; 2 | import { QueryDirectiveClass } from './query.directive.class'; 3 | import { QueryDirective } from './query.directive'; 4 | import { MagicService } from 'src/magic-meta/magic.service'; 5 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 6 | import { QueryRelationMeta } from 'src/magic-meta/query/query.relation-meta'; 7 | import { SchemaService } from 'src/schema/schema.service'; 8 | 9 | export interface QueryRelationDirectiveClass extends QueryDirectiveClass { 10 | new ( 11 | directiveMeta: DirectiveMeta, 12 | rootMeta: QueryRootMeta, 13 | relationMeta: QueryRelationMeta, 14 | magicService: MagicService, 15 | schemaService: SchemaService, 16 | ): QueryDirective; 17 | } 18 | -------------------------------------------------------------------------------- /src/directive/query/query.condition-directive-class.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirectiveClass } from './query.directive.class'; 2 | import { DirectiveMeta } from '../directive.meta'; 3 | import { QueryDirective } from './query.directive'; 4 | import { MagicService } from 'src/magic-meta/magic.service'; 5 | import { QueryEntityMeta } from 'src/magic-meta/query/query.entity-meta'; 6 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 7 | import { SchemaService } from 'src/schema/schema.service'; 8 | 9 | export interface QueryConditionDirectiveClass extends QueryDirectiveClass { 10 | new ( 11 | directiveMeta: DirectiveMeta, 12 | rootMeta: QueryRootMeta, 13 | ownerMeta: QueryEntityMeta, 14 | field: string, 15 | magicService: MagicService, 16 | schemaService: SchemaService, 17 | ): QueryDirective; 18 | } 19 | -------------------------------------------------------------------------------- /src/magic/base/json-unit.ts: -------------------------------------------------------------------------------- 1 | import { TOKEN_ENTITY } from './tokens'; 2 | import { parseDirectives } from './parse-directives'; 3 | import { DirectiveMeta } from 'src/directive/directive.meta'; 4 | 5 | export class JsonUnit { 6 | key = ''; 7 | directives: DirectiveMeta[] = []; 8 | value: any; 9 | constructor(keyStr: string, value: any) { 10 | const [key, directives] = parseDirectives(keyStr); 11 | this.key = key; 12 | this.directives = directives; 13 | this.value = value; 14 | } 15 | 16 | getDirective(directiveName: string) { 17 | for (const directive of this.directives) { 18 | if (directive.name === directiveName) { 19 | return directive; 20 | } 21 | } 22 | return undefined; 23 | } 24 | 25 | isModel() { 26 | return this.key.toLowerCase() === TOKEN_ENTITY; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/directives/query-field-file-url.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryFieldDirective } from 'src/directive/query/query.field-directive'; 3 | import { DEFAULT_FILE_PATH_FIELD } from 'src/util/consts'; 4 | 5 | export class QueryFieldFileUrlDirective extends QueryFieldDirective { 6 | static description = `获取文件的url,网址附加到url字段,格式url(path?), 参数默认为path`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_FIELD_DIRECTIVE; 11 | 12 | static directiveName = 'url'; 13 | 14 | async filterEntity(entity: any): Promise { 15 | const field = this.directiveMeta.value[0] || DEFAULT_FILE_PATH_FIELD; 16 | entity.url = await this.storageService.fileUrl( 17 | entity[field], 18 | entity['bucket'], 19 | ); 20 | return entity; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/directives/query-entity-take.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { SelectQueryBuilder } from 'typeorm'; 4 | 5 | export class QueryEntityTakeDirective extends QueryDirective { 6 | static description = ` 7 | Magic query directive, set take(count) to QueryBuilder. 8 | `; 9 | 10 | static version = '1.0'; 11 | 12 | static directiveType = DirectiveType.QUERY_ENTITY_DIRECTIVE; 13 | 14 | static directiveName = 'take'; 15 | 16 | isEffectResultCount = true; 17 | 18 | get count() { 19 | return this.directiveMeta.value; 20 | } 21 | 22 | addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 23 | qb.take(this.count); 24 | this.rootMeta.maxCount = this.count; 25 | return qb; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/directives/query-condition-or-like.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionOrLikeDirective extends QueryConditionDirective { 6 | static description = `Condition orLike irective. 本指令未仔细测试`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = 'orLike'; 13 | 14 | getOrWhereStatement(): [string, any] | void { 15 | const paramName = 'param' + createId(); 16 | console.log('getOrWhereStatement 被调用'); 17 | return [ 18 | `${this.field} LIKE :${paramName} `, 19 | { 20 | [paramName]: this.value, 21 | }, 22 | ]; 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/mailer/mailer.clients-pool.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { Socket } from 'socket.io'; 3 | 4 | export interface MailClient { 5 | accountId: number; 6 | socket: Socket; 7 | } 8 | 9 | @Injectable() 10 | export class MailerClientsPool { 11 | private pool = new Map(); 12 | 13 | addClient(id: string, client: MailClient) { 14 | this.pool.set(id, client); 15 | console.debug('socket client counts:', this.pool.size); 16 | } 17 | 18 | removeClient(id: string) { 19 | this.pool.delete(id); 20 | } 21 | 22 | has(id: string) { 23 | return this.pool.has(id); 24 | } 25 | 26 | getByAccountId(id: number) { 27 | const items = []; 28 | for (const item of this.pool) { 29 | if (item[1].accountId === id) { 30 | items.push(item[1]); 31 | } 32 | } 33 | 34 | return items; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/directives/query-field-image-thumbnail.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryFieldDirective } from 'src/directive/query/query.field-directive'; 3 | import { DEFAULT_FILE_PATH_FIELD, THUMBNAIL_SIZE } from 'src/util/consts'; 4 | 5 | export class QueryFieldImageThumbnailDirective extends QueryFieldDirective { 6 | static description = `获取RxMedia对象缩略图,网址附加到thumbnail字段,格式thumbnail(path?), 参数默认为path`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_FIELD_DIRECTIVE; 11 | 12 | static directiveName = 'thumbnail'; 13 | 14 | async filterEntity(entity: any): Promise { 15 | const field = this.directiveMeta.value[0] || DEFAULT_FILE_PATH_FIELD; 16 | entity.thumbnail = await this.storageService.resizeImage( 17 | entity[field], 18 | THUMBNAIL_SIZE, 19 | ); 20 | return entity; 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/directive/directive.meta.ts: -------------------------------------------------------------------------------- 1 | export class DirectiveMeta { 2 | name: string; 3 | value: any | any[]; 4 | 5 | /** 6 | * 7 | * @param directiveStr 命令字符串,可解析这种形式“directive(x,y...)” 8 | * @param value 如果提供了该值,则忽略解析出来的参数,用该值代替 9 | */ 10 | constructor(directiveStr: string, value?: any) { 11 | const nameReg = /[^(]*/i; 12 | this.name = nameReg.test(directiveStr) 13 | ? directiveStr.match(nameReg)[0].trim() 14 | : ''; 15 | const paramReg = /\([\s\S,]*\)/i; 16 | const paramStr = paramReg.test(directiveStr) 17 | ? directiveStr.match(paramReg)[0].replace('(', '').replace(')', '') 18 | : ''; 19 | const params = paramStr 20 | ? paramStr.split(',').map((token) => token.trim()) 21 | : undefined; 22 | this.value = params && params.length === 1 ? params[0] : params; 23 | if (value !== undefined) { 24 | this.value = value; 25 | } 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/magic/base/tokens.ts: -------------------------------------------------------------------------------- 1 | export const TOKEN_ENTITY = 'entity'; 2 | export const TOKEN_WHERE = 'where'; 3 | export const TOKEN_ON = 'on'; 4 | export const TOKEN_GET_MANY = 'getMany'; 5 | export const TOKEN_GET_ONE = 'getOne'; 6 | export const TOKEN_TAKE = 'take'; 7 | export const TOKEN_SKIP = 'skip'; 8 | export const TOKEN_PAGINATE = 'paginate'; 9 | export const TOKEN_SELECT = 'select'; 10 | export const TOKEN_ORDER_BY = 'orderBy'; 11 | export const TOKEN_LARGE_RELATION = 'LargeRelation'; 12 | export const TOKEN_COUNT = 'count'; 13 | export const TOKEN_CASCADE = 'cascade'; 14 | export const TOKEN_TREE = 'tree'; 15 | export const TOKEN_IDS = 'ids'; 16 | export const TOKEN_FAKE_RELATION = 'fakeRelation'; 17 | export const TOKEN_RELATION_NAME = 'relationName'; 18 | export const TOKEN_LINKE_FIELDS = 'linkFields'; 19 | 20 | export const TOKEN_UPDATE = 'update'; 21 | export const TOKEN_DELETE = 'delete'; 22 | 23 | export const TOKEN_SOFT = 'soft'; 24 | -------------------------------------------------------------------------------- /src/directive/query/query.relation-directive.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { MagicService } from 'src/magic-meta/magic.service'; 3 | import { QueryRelationMeta } from 'src/magic-meta/query/query.relation-meta'; 4 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 5 | import { SchemaService } from 'src/schema/schema.service'; 6 | import { DirectiveMeta } from '../directive.meta'; 7 | import { QueryDirective } from './query.directive'; 8 | 9 | export class QueryRelationDirective extends QueryDirective { 10 | constructor( 11 | protected readonly directiveMeta: DirectiveMeta, 12 | protected readonly rootMeta: QueryRootMeta, 13 | protected readonly relationMeta: QueryRelationMeta, 14 | protected readonly magicService: MagicService, 15 | protected readonly schemaService: SchemaService, 16 | ) { 17 | super(directiveMeta, rootMeta, magicService, schemaService); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/entity-interface/RxUser.ts: -------------------------------------------------------------------------------- 1 | import { RxRole } from './RxRole'; 2 | import { RxMedia } from './RxMedia'; 3 | import { RxMediaFolder } from './RxMediaFolder'; 4 | import { MailConfig } from './MailConfig'; 5 | import { Mail } from './Mail'; 6 | import { MailLabel } from './MailLabel'; 7 | import { RxDepartment } from './RxDepartment'; 8 | import { RxUserStatus } from './RxUserStatus'; 9 | 10 | export const EntityRxUser = 'RxUser'; 11 | export interface RxUser { 12 | id?: number; 13 | name: string; 14 | loginName: string; 15 | email?: string; 16 | password: string; 17 | isSupper?: boolean; 18 | isDemo?: boolean; 19 | status?: RxUserStatus; 20 | createdAt?: Date; 21 | updatedAt?: Date; 22 | belongsToDeparments?: RxDepartment[]; 23 | roles?: RxRole[]; 24 | avatar?: RxMedia; 25 | mediaFolders?: RxMediaFolder[]; 26 | medias?: RxMedia[]; 27 | mailConfigs?: MailConfig[]; 28 | mails?: Mail[]; 29 | mailLabels?: MailLabel[]; 30 | } 31 | -------------------------------------------------------------------------------- /src/magic/query/traverser/make-directives-query-builder.ts: -------------------------------------------------------------------------------- 1 | import { QueryEntityMeta } from 'src/magic-meta/query/query.entity-meta'; 2 | import { SelectQueryBuilder } from 'typeorm'; 3 | import { getDirectivesAndWhereStatement } from './get-directives-and-where-statement'; 4 | import { getDirectivesOrWhereStatement } from './get-directives-or-where-statement'; 5 | 6 | //构建qb 7 | export function makeDirectivesQueryBuilder( 8 | meta: QueryEntityMeta, 9 | qb: SelectQueryBuilder, 10 | ): SelectQueryBuilder { 11 | const directives = []; 12 | for (const directive of meta.directives) { 13 | directive.addToQueryBuilder(qb); 14 | directives.push(directive); 15 | } 16 | const [sqlAnd, paramsAnd] = getDirectivesAndWhereStatement(directives); 17 | sqlAnd && qb.andWhere(sqlAnd, paramsAnd); 18 | 19 | const [sqlOr, paramsOr] = getDirectivesOrWhereStatement(directives); 20 | sqlOr && qb.orWhere(sqlOr, paramsOr); 21 | return qb; 22 | } 23 | -------------------------------------------------------------------------------- /src/auth/local.strategy.ts: -------------------------------------------------------------------------------- 1 | import { Strategy } from 'passport-local'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { 4 | HttpException, 5 | Injectable, 6 | UnauthorizedException, 7 | } from '@nestjs/common'; 8 | import { AuthService } from './auth.service'; 9 | 10 | @Injectable() 11 | export class LocalStrategy extends PassportStrategy(Strategy) { 12 | constructor(private readonly authService: AuthService) { 13 | super(); 14 | } 15 | 16 | async validate(username: string, password: string): Promise { 17 | try { 18 | const user = await this.authService.validateUser(username, password); 19 | if (!user) { 20 | throw new UnauthorizedException(); 21 | } 22 | return user; 23 | } catch (error: any) { 24 | throw new HttpException( 25 | { 26 | status: 500, 27 | error: error.message, 28 | }, 29 | 500, 30 | ); 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/mailer/receive/receive-event.ts: -------------------------------------------------------------------------------- 1 | import { MailBoxType } from 'src/entity-interface/MailBoxType'; 2 | 3 | export enum MailerReceiveEventType { 4 | error = 'error', 5 | checkStorage = 'checkStorage', 6 | readLocalMailList = 'readLocalMailList', 7 | connect = 'connect', 8 | invalidState = 'invalidState', 9 | locked = 'locked', 10 | login = 'login', 11 | list = 'list', 12 | uidl = 'uidl', 13 | retr = 'retr', 14 | dele = 'dele', 15 | quit = 'quit', 16 | progress = 'progress', 17 | cancelling = 'cancelling', 18 | finished = 'finished', 19 | aborted = 'aborted', 20 | openMailBox = 'openMailBox', 21 | receivedOneMail = 'receivedOneMail', 22 | } 23 | 24 | export interface MailerReceiveEvent { 25 | type: MailerReceiveEventType; 26 | message?: string; 27 | total?: number; 28 | current?: number; 29 | size?: number; 30 | mailAddress?: string; 31 | name?: string; 32 | subject?: string; 33 | inMailbox?: MailBoxType; 34 | } 35 | -------------------------------------------------------------------------------- /src/directive/post-directive.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { DirectiveStorage } from './directive.storage'; 3 | import { PostDirectiveClass } from './post/post.directive.class'; 4 | 5 | @Injectable() 6 | export class PostDirectiveService { 7 | constructor(private readonly directiveStorage: DirectiveStorage) {} 8 | 9 | findEntityDirectiveOrFailed(name: string): PostDirectiveClass { 10 | const directiveClass = 11 | this.directiveStorage.postEntityDirectiveClasses[name]; 12 | if (!directiveClass) { 13 | throw new Error(`No entity directive "${name}"`); 14 | } 15 | return directiveClass; 16 | } 17 | 18 | findRelationDirectiveOrFailed(name: string): PostDirectiveClass { 19 | const directiveClass = 20 | this.directiveStorage.postRelationDirectiveClasses[name]; 21 | if (!directiveClass) { 22 | throw new Error(`No relation directive "${name}"`); 23 | } 24 | return directiveClass; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/magic/query/traverser/make-effect-count-query-builder.ts: -------------------------------------------------------------------------------- 1 | import { RxUser } from 'src/entity-interface/RxUser'; 2 | import { QueryEntityMeta } from 'src/magic-meta/query/query.entity-meta'; 3 | import { SelectQueryBuilder } from 'typeorm'; 4 | import { getDirectivesAndWhereStatement } from './get-directives-and-where-statement'; 5 | import { makeAbilitiesQueryBuilder } from './make-abilities-query-builder'; 6 | 7 | export function makeEffectCountQueryBuilder( 8 | meta: QueryEntityMeta, 9 | qb: SelectQueryBuilder, 10 | me: RxUser, 11 | ): SelectQueryBuilder { 12 | const directives = []; 13 | for (const directive of meta.directives) { 14 | if (directive.isEffectResultCount) { 15 | directive.addToQueryBuilder(qb); 16 | directives.push(directive); 17 | } 18 | } 19 | const [sql, params] = getDirectivesAndWhereStatement(directives); 20 | sql && qb.andWhere(sql, params); 21 | qb = makeAbilitiesQueryBuilder(meta, qb, me); 22 | return qb; 23 | } 24 | -------------------------------------------------------------------------------- /src/mailer/mailer.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { MailerClientsPool } from './mailer.clients-pool'; 3 | import { MailerController } from './mailer.controller'; 4 | import { MailerGateway } from './mailer.gateway'; 5 | import { MailerTestService } from './mailer.test-service'; 6 | import { MailerReceiveTasksPool } from './receive/receive-tasks-pool'; 7 | import { MailerSendService } from './send/mailer.send.service'; 8 | import { MailerSendTasksPool } from './send/send-tasks-pool'; 9 | 10 | @Module({ 11 | providers: [ 12 | MailerGateway, 13 | MailerClientsPool, 14 | MailerReceiveTasksPool, 15 | MailerSendTasksPool, 16 | MailerSendService, 17 | MailerTestService, 18 | ], 19 | exports: [ 20 | MailerGateway, 21 | MailerClientsPool, 22 | MailerReceiveTasksPool, 23 | MailerSendTasksPool, 24 | MailerSendService, 25 | MailerTestService, 26 | ], 27 | controllers: [MailerController], 28 | }) 29 | export class MailerModule {} 30 | -------------------------------------------------------------------------------- /src/directive/query/query.condition-directive.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveMeta } from '../directive.meta'; 2 | import { QueryDirective } from './query.directive'; 3 | import { MagicService } from 'src/magic-meta/magic.service'; 4 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 5 | import { QueryEntityMeta } from 'src/magic-meta/query/query.entity-meta'; 6 | import { SchemaService } from 'src/schema/schema.service'; 7 | 8 | export class QueryConditionDirective extends QueryDirective { 9 | constructor( 10 | protected readonly directiveMeta: DirectiveMeta, 11 | protected readonly rootMeta: QueryRootMeta, 12 | protected readonly ownerMeta: QueryEntityMeta, 13 | protected readonly field: string, 14 | protected readonly magicService: MagicService, 15 | protected readonly schemaService: SchemaService, 16 | ) { 17 | super(directiveMeta, rootMeta, magicService, schemaService); 18 | } 19 | 20 | get value() { 21 | return this.directiveMeta?.value; 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/directive/query/query.field-directive.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { MagicService } from 'src/magic-meta/magic.service'; 3 | import { QueryRelationMeta } from 'src/magic-meta/query/query.relation-meta'; 4 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 5 | import { SchemaService } from 'src/schema/schema.service'; 6 | import { StorageService } from 'src/storage/storage.service'; 7 | import { DirectiveMeta } from '../directive.meta'; 8 | import { QueryDirective } from './query.directive'; 9 | 10 | export class QueryFieldDirective extends QueryDirective { 11 | constructor( 12 | protected readonly directiveMeta: DirectiveMeta, 13 | protected readonly rootMeta: QueryRootMeta, 14 | protected readonly magicService: MagicService, 15 | protected readonly schemaService: SchemaService, 16 | protected readonly storageService: StorageService, 17 | ) { 18 | super(directiveMeta, rootMeta, magicService, schemaService); 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /src/directives/query-condition-equal.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionEqualDirective extends QueryConditionDirective { 6 | static description = `Condition equal directive.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = 'equal'; 13 | 14 | getAndWhereStatement(): [string, any] { 15 | const paramName = 'param' + createId(); 16 | let paramValue = this.value; 17 | if (this.value?.toString().startsWith('$me.')) { 18 | const [, columnStr] = (this.value as string).split('.'); 19 | paramValue = this.magicService.me[columnStr]; 20 | } 21 | return [ 22 | `${this.field} = :${paramName} `, 23 | { 24 | [paramName]: paramValue, 25 | }, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/directives/query-condition-not-equal.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionEqualDirective extends QueryConditionDirective { 6 | static description = `Condition not equal directive.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = '!='; 13 | 14 | getAndWhereStatement(): [string, any] { 15 | const paramName = 'param' + createId(); 16 | let paramValue = this.value; 17 | if (this.value?.toString().startsWith('$me.')) { 18 | const [, columnStr] = (this.value as string).split('.'); 19 | paramValue = this.magicService.me[columnStr]; 20 | } 21 | return [ 22 | `${this.field} != :${paramName} `, 23 | { 24 | [paramName]: paramValue, 25 | }, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/directives/query-condition-less-or-equal.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionLessOrEqualDirective extends QueryConditionDirective { 6 | static description = `Condition <= directive.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = '<='; 13 | 14 | getAndWhereStatement(): [string, any] { 15 | const paramName = 'param' + createId(); 16 | let paramValue = this.value; 17 | if (this.value?.toString().startsWith('$me.')) { 18 | const [, columnStr] = (this.value as string).split('.'); 19 | paramValue = this.magicService.me[columnStr]; 20 | } 21 | return [ 22 | `${this.field} <= :${paramName} `, 23 | { 24 | [paramName]: paramValue, 25 | }, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/directives/query-condition-greater-or-equal.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionGreaterOrEqualDirective extends QueryConditionDirective { 6 | static description = `Condition >= directive.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = '>='; 13 | 14 | getAndWhereStatement(): [string, any] { 15 | const paramName = 'param' + createId(); 16 | let paramValue = this.value; 17 | if (this.value?.toString().startsWith('$me.')) { 18 | const [, columnStr] = (this.value as string).split('.'); 19 | paramValue = this.magicService.me[columnStr]; 20 | } 21 | return [ 22 | `${this.field} >= :${paramName} `, 23 | { 24 | [paramName]: paramValue, 25 | }, 26 | ]; 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/schema/graph-meta-interface/relation-meta.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 关系类型 3 | */ 4 | export enum RelationType { 5 | INHERIT = 'inherit', 6 | ONE_TO_ONE = 'one-to-one', 7 | ONE_TO_MANY = 'one-to-many', 8 | MANY_TO_ONE = 'many-to-one', 9 | MANY_TO_MANY = 'many-to-many', 10 | } 11 | 12 | export enum CombinationType { 13 | ON_SOURCE = 'onSource', 14 | ON_TARGET = 'onTarget', 15 | } 16 | 17 | /** 18 | * 关系元数据 19 | */ 20 | export interface RelationMeta { 21 | /** 22 | * 唯一标识 23 | */ 24 | uuid: string; 25 | 26 | /** 27 | * 关系类型 28 | */ 29 | relationType: RelationType; 30 | 31 | /** 32 | * 关系的源实体标识 33 | */ 34 | sourceId: string; 35 | 36 | /** 37 | * 关系目标源实体标识 38 | */ 39 | targetId: string; 40 | 41 | /** 42 | * 源实体上的关系属性 43 | */ 44 | roleOnSource?: string; 45 | 46 | /** 47 | * 目标实体上的关系属性 48 | */ 49 | roleOnTarget?: string; 50 | 51 | /** 52 | * 拥有关系的实体ID,对应TypeORM的JoinTable或JoinColumn 53 | */ 54 | ownerId?: string; 55 | 56 | combination?: CombinationType; 57 | } 58 | -------------------------------------------------------------------------------- /src/util/consts.ts: -------------------------------------------------------------------------------- 1 | export const DB_CONFIG_FILE = 'dbconfig.json'; 2 | export const NOT_INSTALL_ERROR = 'Not installed yet, please install first'; 3 | export const SCHEMAS_DIR = 'schemas/'; 4 | export const SALT_OR_ROUNDS = 10; 5 | export const ALIYUN_CONFIG_KEY = 'aliyun'; 6 | export const BUCKET_MAILS = 'rxmodels-mails'; 7 | export const BUCKET_UPLOADS = 'rxmodels-uploads'; 8 | //export const FOLDER_INBOX = 'inbox'; 9 | //export const FOLDER_SENT = 'sent'; 10 | export const FOLEDER_ATTACHMENTS = 'attachments'; 11 | export const THUMBNAIL_SIZE = { 12 | width: 400, 13 | height: 400, 14 | }; 15 | export const DEFAULT_FILE_PATH_FIELD = 'path'; 16 | 17 | export interface ImageSize { 18 | width: number; 19 | height: number; 20 | } 21 | 22 | export const DISK_STORAGE_PATH = './storage/'; 23 | export const DISK_STORAGE_PUBLIC_PATH = './public/storage/'; 24 | export const DISK_STORAGE_PUBLIC_URL_BASE = '/storage/'; 25 | export const CONFIG_KEY_STORAGE = 'storage'; 26 | export const DEFAULT_TIME_OUT = 30; 27 | 28 | export const CRYPTO_KEY = 'LiRuXin'; 29 | -------------------------------------------------------------------------------- /src/schema/convert-type.ts: -------------------------------------------------------------------------------- 1 | import { ColumnType as MetaColumnType } from 'src/schema/graph-meta-interface/column-type'; 2 | import { ColumnType } from 'typeorm'; 3 | 4 | export function convertType(type: MetaColumnType): ColumnType { 5 | if (type === MetaColumnType.String || type === MetaColumnType.Enum) { 6 | return String; 7 | } 8 | 9 | if (type === MetaColumnType.Text) { 10 | return 'text'; 11 | } 12 | if (type === MetaColumnType.MediumText) { 13 | return 'mediumtext'; 14 | } 15 | if (type === MetaColumnType.LongText) { 16 | return 'longtext'; 17 | } 18 | 19 | if (type === MetaColumnType.Boolean) { 20 | return Boolean; 21 | } 22 | 23 | if (type === MetaColumnType.Number) { 24 | return Number; 25 | } 26 | 27 | if (type === MetaColumnType.Date) { 28 | return Date; 29 | } 30 | 31 | if (type === MetaColumnType.SimpleJson || type === MetaColumnType.JsonArray) { 32 | return 'simple-json'; 33 | } 34 | 35 | if (type === MetaColumnType.SimpleArray) { 36 | return 'simple-array'; 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/app.module.ts: -------------------------------------------------------------------------------- 1 | import { Module } from '@nestjs/common'; 2 | import { AppController } from './app.controller'; 3 | import { AppService } from './app.service'; 4 | import { AuthController } from './auth/auth.controller'; 5 | import { AuthModule } from './auth/auth.module'; 6 | import { MagicModule } from './magic/magic.module'; 7 | import { TypeOrmModule } from './typeorm/typeorm.module'; 8 | import { InstallModule } from './install/install.module'; 9 | import { PackageManageModule } from './package-manage/package-manage.module'; 10 | import { MailerModule } from './mailer/mailer.module'; 11 | import { StorageModule } from './storage/storage.module'; 12 | import { RxBaseModule } from './rxbase/rxbase.module'; 13 | 14 | @Module({ 15 | imports: [ 16 | PackageManageModule, 17 | InstallModule, 18 | TypeOrmModule, 19 | StorageModule, 20 | AuthModule, 21 | MagicModule, 22 | MailerModule, 23 | RxBaseModule, 24 | ], 25 | controllers: [AppController, AuthController], 26 | providers: [AppService], 27 | }) 28 | export class AppModule {} 29 | -------------------------------------------------------------------------------- /src/directives/query-relation-count.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryRelationDirective } from 'src/directive/query/query.relation-directive'; 3 | import { QueryResult } from 'src/magic-meta/query/query-result'; 4 | import { SelectQueryBuilder } from 'typeorm'; 5 | 6 | export class QueryRelationCountDirective extends QueryRelationDirective { 7 | static description = `Magic query directive, set relation count map to QueryBuilder.`; 8 | 9 | static version = '1.0'; 10 | 11 | static directiveType = DirectiveType.QUERY_RELATION_DIRECTIVE; 12 | 13 | static directiveName = 'count'; 14 | 15 | makeQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 16 | //queryBulider.loadRelationCountAndMap( 17 | // `${paramParser.modelUnit?.modelAlias}.relationCount`, 18 | // `${paramParser.modelUnit?.modelAlias}.roles`, 19 | //); 20 | return qb; 21 | } 22 | 23 | async filterResult(/*result: QueryResult*/): Promise { 24 | throw new Error('Method not implemented in QueryRelationCountDirective.'); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/magic/upload/magic.upload.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | //import * as sharp from 'sharp'; 3 | import { StorageService } from 'src/storage/storage.service'; 4 | import { BUCKET_UPLOADS } from 'src/util/consts'; 5 | 6 | @Injectable() 7 | export class MagicUploadService { 8 | constructor(private readonly storageService: StorageService) {} 9 | 10 | async saveFile(file: Express.Multer.File, fileName: string) { 11 | await this.storageService.checkAndCreateBucket(BUCKET_UPLOADS); 12 | await this.storageService.putFile(fileName, file, BUCKET_UPLOADS); 13 | } 14 | 15 | /*async saveThumbnail(file: Express.Multer.File) { 16 | const thumbnail = `/thumbnails/${file.filename}`; 17 | if (file.mimetype.match(/\/(jpg|jpeg|png|gif)$/)) { 18 | sharp(file.path) 19 | .resize(200, 200) 20 | .toFile('./public' + thumbnail, (err, info) => { 21 | console.debug('Resize Success', info); 22 | if (err) { 23 | console.error('Resize Error', err); 24 | } 25 | }); 26 | } 27 | 28 | return thumbnail; 29 | }*/ 30 | } 31 | -------------------------------------------------------------------------------- /src/mailer/send/mailer.send.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { EntityMail, Mail } from 'src/entity-interface/Mail'; 3 | import { StorageService } from 'src/storage/storage.service'; 4 | import { EntityManager } from 'typeorm'; 5 | import { MailerSendTasksPool } from './send-tasks-pool'; 6 | 7 | @Injectable() 8 | export class MailerSendService { 9 | constructor( 10 | private readonly tasksPool: MailerSendTasksPool, 11 | protected readonly storageService: StorageService, 12 | ) {} 13 | 14 | //注意,这是一个异步函数 15 | async sendMails(ids: number[], entityManger: EntityManager) { 16 | if (!ids?.length) { 17 | return; 18 | } 19 | 20 | const mails = await entityManger 21 | .getRepository(EntityMail) 22 | .createQueryBuilder('mail') 23 | .leftJoinAndSelect('mail.owner', 'owner') 24 | .leftJoinAndSelect('mail.attachments', 'attachments') 25 | .where('mail.id in (:...ids)', { ids: ids }) 26 | .getMany(); 27 | 28 | for (const mail of mails) { 29 | await this.tasksPool.createTask(mail, entityManger); 30 | } 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/directives/query-field-map-value.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryFieldDirective } from 'src/directive/query/query.field-directive'; 3 | 4 | export class QueryFieldMapValuelDirective extends QueryFieldDirective { 5 | static description = `映射字段值,@mapValue(field, oldValue, newValue)`; 6 | 7 | static version = '1.0'; 8 | 9 | static directiveType = DirectiveType.QUERY_FIELD_DIRECTIVE; 10 | 11 | static directiveName = 'mapValue'; 12 | 13 | async filterEntity(entity: any): Promise { 14 | if (this.directiveMeta.value.length < 1) { 15 | throw new Error('mapValue directive has too few params'); 16 | } 17 | 18 | const field = this.directiveMeta.value[0]; 19 | const mapObject = this.directiveMeta.value[1]; 20 | 21 | if (typeof mapObject !== 'object') { 22 | throw new Error('mapValue directive has not value map object'); 23 | } 24 | 25 | for (const oldValue in mapObject) { 26 | if (entity[field] === oldValue) { 27 | entity[field] = mapObject[oldValue]; 28 | } 29 | } 30 | 31 | return entity; 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/directives/query-relation-merge.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryRelationDirective } from 'src/directive/query/query.relation-directive'; 3 | 4 | export class QueryModelMergeDirective extends QueryRelationDirective { 5 | static description = ` 6 | 把指定的某个字段合并到父实体merge(fieldName, fieldNameOnParent) 7 | `; 8 | 9 | static version = '1.0'; 10 | 11 | static directiveType = DirectiveType.QUERY_RELATION_DIRECTIVE; 12 | 13 | static directiveName = 'merge'; 14 | 15 | get params() { 16 | return this.directiveMeta.value; 17 | } 18 | 19 | async filterEntity(entity: any, parentEntity: any): Promise { 20 | if (!this.params || this.params.length < 2) { 21 | throw new Error('Merge directive params too less'); 22 | } 23 | 24 | const fieldName = this.params[0]; 25 | const fieldNameOnParent = this.params[1]; 26 | 27 | if (entity && parentEntity) { 28 | parentEntity[fieldNameOnParent] = entity[fieldName]; 29 | } else { 30 | throw new Error('Instance or Instance entity is undefined'); 31 | } 32 | 33 | return entity; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/directives/query-entity-select.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { SelectQueryBuilder } from 'typeorm'; 4 | 5 | export class QueryEntitySelectDirective extends QueryDirective { 6 | static description = ` 7 | Magic query directive, select directive, to filter selected field. 8 | `; 9 | 10 | static version = '1.0'; 11 | 12 | static directiveType = DirectiveType.QUERY_ENTITY_DIRECTIVE; 13 | 14 | static directiveName = 'select'; 15 | 16 | addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 17 | if (!this.directiveMeta.value) { 18 | throw new Error('Select directive no params'); 19 | } 20 | qb.select( 21 | this.directiveMeta.value.map((field: string) => { 22 | if (!field?.trim || typeof field !== 'string') { 23 | throw new Error(`Select directive no param"${field}" is illegal`); 24 | } 25 | return this.rootMeta.alias + '.' + field; 26 | }), 27 | ); 28 | qb.addSelect([this.rootMeta.alias + '.id']); 29 | return qb; 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/directives/query-condition-between.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionBetweenDirective extends QueryConditionDirective { 6 | static description = `Condition between directive.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = 'between'; 13 | 14 | getAndWhereStatement(): [string, any] { 15 | const field = this.field; 16 | const value = this.value; 17 | if (!value || !value.length || value.length < 2) { 18 | throw new Error( 19 | `Field "${field}" value "${value}" can not be used to between directive`, 20 | ); 21 | } 22 | const paramName1 = 'param' + createId(); 23 | const paramName2 = 'param' + createId(); 24 | return [ 25 | `${field} BETWEEN (:${paramName1} AND :${paramName2}) `, 26 | { 27 | [paramName1]: value[0], 28 | [paramName2]: value[1], 29 | }, 30 | ]; 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/directives/query-entity-orderby.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { SelectQueryBuilder } from 'typeorm'; 4 | 5 | export class QueryEntityOrderByDirective extends QueryDirective { 6 | static description = ` 7 | Magic query directive, orderBy directive, to sort the result. 8 | `; 9 | 10 | static version = '1.0'; 11 | 12 | static directiveType = DirectiveType.QUERY_ENTITY_DIRECTIVE; 13 | 14 | static directiveName = 'orderBy'; 15 | 16 | addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 17 | const orderMap = this.getpMap(); 18 | if (orderMap) { 19 | qb.orderBy(orderMap); 20 | } 21 | return qb; 22 | } 23 | 24 | getpMap() { 25 | const orderMap = {} as any; 26 | if (!this.directiveMeta.value) { 27 | throw new Error('Not assign params to "select" directive'); 28 | } 29 | const orderBy = this.directiveMeta.value; 30 | for (const key in orderBy) { 31 | orderMap[`${this.rootMeta.alias}.${key}`] = orderBy[key]; 32 | } 33 | return orderMap; 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/directives/post-entity-ignore-emperty.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { PostDirective } from 'src/directive/post/post.directive'; 3 | import { InstanceMeta } from 'src/magic-meta/post/instance.meta'; 4 | 5 | export class PostIgnorEntityEmpertyDirective extends PostDirective { 6 | static description = `如果为空,则忽略该字段`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.POST_ENTITY_DIRECTIVE; 11 | 12 | static directiveName = 'ignoreEmperty'; 13 | 14 | private oldRelationIds: number[] = []; 15 | 16 | async beforeSaveInstance(instanceMeta: InstanceMeta) { 17 | if (Array.isArray(this.directiveMeta?.value)) { 18 | for (const param of this.directiveMeta?.value) { 19 | await this.processOneField(instanceMeta, param); 20 | } 21 | } else { 22 | await this.processOneField(instanceMeta, this.directiveMeta?.value); 23 | } 24 | return instanceMeta; 25 | } 26 | 27 | private async processOneField(instanceMeta: InstanceMeta, param: any) { 28 | const fieldValue = instanceMeta.meta[param]; 29 | if (!fieldValue) { 30 | delete instanceMeta.meta[param]; 31 | } 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/mailer/receive/mail-teller.ts: -------------------------------------------------------------------------------- 1 | import _ = require('lodash'); 2 | 3 | export class MailTeller { 4 | localMailList: string[] = []; 5 | newMailList: string[] = []; 6 | protected uidlData: string[]; 7 | sizeList: string[] = []; 8 | 9 | totalNew: number; 10 | 11 | currentMailIndex = 0; 12 | 13 | /** 14 | * 识别新邮件 15 | */ 16 | tellIt(uidlData: string[], startIndex = 1): void { 17 | this.uidlData = uidlData; 18 | this.newMailList = _.difference(this.uidlData, this.localMailList).splice( 19 | startIndex, 20 | ); 21 | this.totalNew = this.newMailList.length; 22 | this.currentMailIndex = 0; 23 | } 24 | 25 | getUidl(msg: string): string { 26 | return this.uidlData[msg]; 27 | } 28 | 29 | getMsgNumber(uidl: string): string { 30 | for (const msg in this.uidlData) { 31 | if (this.uidlData[msg] === uidl) { 32 | return msg; 33 | } 34 | } 35 | } 36 | 37 | nextMsgNumber(): string { 38 | if (this.newMailList.length > 0) { 39 | const uidl = this.newMailList.shift(); 40 | return this.getMsgNumber(uidl); 41 | } 42 | } 43 | 44 | cunrrentNumber(): number { 45 | return this.totalNew - this.newMailList.length; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/auth/jwt.strategy.ts: -------------------------------------------------------------------------------- 1 | import { ExtractJwt, Strategy } from 'passport-jwt'; 2 | import { PassportStrategy } from '@nestjs/passport'; 3 | import { HttpException, Injectable } from '@nestjs/common'; 4 | import { jwtConstants } from './constants'; 5 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 6 | 7 | @Injectable() 8 | export class JwtStrategy extends PassportStrategy(Strategy) { 9 | constructor(private readonly typeormSerivce: TypeOrmService) { 10 | super({ 11 | jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(), 12 | ignoreExpiration: false, 13 | secretOrKey: jwtConstants.secret, 14 | }); 15 | } 16 | 17 | async validate(payload: any) { 18 | try { 19 | //console.debug('JwtStrategy payload', payload); 20 | const userId = payload.sub; 21 | const user = await this.typeormSerivce 22 | .getRepository('RxUser') 23 | .createQueryBuilder('user') 24 | .leftJoinAndSelect('user.roles', 'RxRole') 25 | .where({ id: userId }) 26 | .getOne(); 27 | return user; 28 | } catch (error: any) { 29 | throw new HttpException( 30 | { 31 | status: 500, 32 | error: error.message, 33 | }, 34 | 500, 35 | ); 36 | } 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/directives/post-entity-bcrypt.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { PostDirective } from 'src/directive/post/post.directive'; 3 | import { InstanceMeta } from 'src/magic-meta/post/instance.meta'; 4 | import { SALT_OR_ROUNDS } from 'src/util/consts'; 5 | import * as bcrypt from 'bcrypt'; 6 | 7 | export class PostEntityBcryptDirective extends PostDirective { 8 | static description = `加密一个或几个字段`; 9 | 10 | static version = '1.0'; 11 | 12 | static directiveType = DirectiveType.POST_ENTITY_DIRECTIVE; 13 | 14 | static directiveName = 'bcrypt'; 15 | 16 | private oldRelationIds: number[] = []; 17 | 18 | async beforeSaveInstance(instanceMeta: InstanceMeta) { 19 | if (Array.isArray(this.directiveMeta?.value)) { 20 | for (const param of this.directiveMeta?.value) { 21 | await this.bcryptOneField(instanceMeta, param); 22 | } 23 | } else { 24 | await this.bcryptOneField(instanceMeta, this.directiveMeta?.value); 25 | } 26 | return instanceMeta; 27 | } 28 | 29 | private async bcryptOneField(instanceMeta: InstanceMeta, param: any) { 30 | const fieldValue = instanceMeta.meta[param]; 31 | if (fieldValue) { 32 | instanceMeta.meta[param] = await bcrypt.hash(fieldValue, SALT_OR_ROUNDS); 33 | } 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/directives/post-entity-send-mail.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { PostDirective } from 'src/directive/post/post.directive'; 3 | import { EntityMail } from 'src/entity-interface/Mail'; 4 | import { InstanceMetaCollection } from 'src/magic-meta/post/instance.meta.colletion'; 5 | 6 | export class PostEntitySendMailDirective extends PostDirective { 7 | static description = `发送邮件,格式sendMail(ids?:number[])`; 8 | 9 | static version = '1.0'; 10 | 11 | static directiveType = DirectiveType.POST_ENTITY_DIRECTIVE; 12 | 13 | static directiveName = 'sendMail'; 14 | 15 | async afterSaveEntityInstanceCollection( 16 | savedInstances: any[], 17 | instanceMetaCollection: InstanceMetaCollection, 18 | ) { 19 | let mailIds = Array.isArray(this.directiveMeta.value) 20 | ? this.directiveMeta.value 21 | : [this.directiveMeta.value]; 22 | 23 | if (!this.directiveMeta.value) { 24 | if (instanceMetaCollection.entity !== EntityMail) { 25 | throw new Error( 26 | 'Not privode mail id, and saved data is not a instance of Mail', 27 | ); 28 | } 29 | mailIds = savedInstances?.map((instance) => instance.id); 30 | } 31 | 32 | await this.mailerSendService.sendMails(mailIds, this.entityManger); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/magic-meta/delete/delete.meta.ts: -------------------------------------------------------------------------------- 1 | import { DeleteDirective } from 'src/directive/delete/delete.directive'; 2 | import { RxAbility } from 'src/entity-interface/RxAbility'; 3 | import { JsonUnit } from 'src/magic/base/json-unit'; 4 | import { TOKEN_SOFT } from 'src/magic/base/tokens'; 5 | 6 | export class DeleteMeta { 7 | private _jsonUnit: JsonUnit; 8 | public directives: DeleteDirective[] = []; 9 | abilities: RxAbility[] = []; 10 | 11 | constructor(jsonUnit: JsonUnit) { 12 | this._jsonUnit = jsonUnit; 13 | } 14 | 15 | get entity() { 16 | return this._jsonUnit.key; 17 | } 18 | 19 | get ids() { 20 | return Array.isArray(this._jsonUnit.value) 21 | ? this._jsonUnit.value 22 | : [this._jsonUnit.value]; 23 | } 24 | 25 | get isSoft() { 26 | return !!this._jsonUnit.getDirective(TOKEN_SOFT); 27 | } 28 | /*get cascades() { 29 | const cascadeCommand = this._jsonUnit.getCommand(TOKEN_CASCADE); 30 | return cascadeCommand ? cascadeCommand.value : undefined; 31 | } 32 | 33 | isCascade(relationName: string) { 34 | if (!this.cascades) { 35 | return false; 36 | } 37 | for (const relation of this.cascades) { 38 | if (relation === relationName) { 39 | return true; 40 | } 41 | } 42 | 43 | return false; 44 | }*/ 45 | } 46 | -------------------------------------------------------------------------------- /src/storage/aliyun/UrlCache.ts: -------------------------------------------------------------------------------- 1 | import { ImageSize } from 'src/util/consts'; 2 | import { cacheSize, expaireTime, refetchPeriod } from './consts'; 3 | 4 | export interface UrlInfo { 5 | time: Date; 6 | path: string; 7 | bucket: string; 8 | size?: ImageSize; 9 | url: string; 10 | } 11 | class UrlCache { 12 | private urls: UrlInfo[] = []; 13 | 14 | addUrl(urlInfo: UrlInfo) { 15 | this.urls.push(urlInfo); 16 | if (this.urls.length > cacheSize) { 17 | this.urls.shift(); 18 | } 19 | } 20 | 21 | getUrlInfo(path: string, bucket: string, size?: ImageSize) { 22 | const urlInfo = this.urls.find( 23 | (url) => 24 | url.path === path && 25 | url.bucket === bucket && 26 | (url.size === size || 27 | (url.size && 28 | url.size.height === size?.height && 29 | size.width === size?.width)), 30 | ); 31 | 32 | if (!urlInfo) { 33 | return urlInfo; 34 | } 35 | const now = new Date(); 36 | const age = (now.getTime() - urlInfo.time.getTime()) / 1000; 37 | //已过期 38 | if (expaireTime - age < refetchPeriod) { 39 | this.urls.slice(this.urls.indexOf(urlInfo), 1); 40 | return undefined; 41 | } 42 | 43 | return urlInfo; 44 | } 45 | } 46 | 47 | export const urlCache = new UrlCache(); 48 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | Nest Logo 3 |

4 | 5 | 6 |

rxModels 是一个低代码后端服务,基于业务模型生成后端,提供通用查询JSON接口

7 |

8 | 9 | # 本项目计划重构为GraphQL版,可能不再维护 10 | 11 | 演示地址:https://rxmodels-client.rxdrag.com/login 12 | 13 | ## 安装服务端 14 | ```console 15 | #不用下面第一条命令,直接在Github网站上Download一个zip格式的代码包,然后解压也很方便 16 | 17 | git clone https://github.com/rxdrag/rx-models.git 18 | 19 | cd rx-models 20 | 21 | npm install 22 | 23 | npm run start:dev 24 | ``` 25 | 在浏览器输入:http://localhost:3001/ ,看到熟悉的“Hello World!”,则说明已经成功运行了。 26 | 27 | 服务端使用了Sharp图形处理库来管理图片,这个库不设置代理,可能不容易安装成功,如果在`npm install`时没有成功,那么按照下面的命令,设置一下代理 28 | ```console 29 | npm config set sharp_binary_host "https://npm.taobao.org/mirrors/sharp" 30 | 31 | npm config set sharp_libvips_binary_host "https://npm.taobao.org/mirrors/sharp-libvips" 32 | ``` 33 | 设置完成以后再执行命令 34 | ```console 35 | npm install 36 | 37 | npm run start:dev 38 | ``` 39 | 40 | ## 文档 41 | 42 | [rxModels文档](https://rxdrag.com/docs/rx-models/install) 43 | 44 | ## Stay in touch 45 | 46 | - Author - 悠闲的水 47 | - Website - [https://rxdrag.com](https://rxdrag.com/) 48 | 49 | ## License 50 | 51 | rxModels is MIT licensed 52 | -------------------------------------------------------------------------------- /src/directives/query-relation-id.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryRelationDirective } from 'src/directive/query/query.relation-directive'; 3 | 4 | export class QueryModelTakeDirective extends QueryRelationDirective { 5 | static description = ` 6 | Magic query directive, id directive, to only get id field. 7 | `; 8 | 9 | static version = '1.0'; 10 | 11 | static directiveType = DirectiveType.QUERY_RELATION_DIRECTIVE; 12 | 13 | static directiveName = 'id'; 14 | 15 | get params() { 16 | return this.directiveMeta.value; 17 | } 18 | 19 | async filterEntity(entity: any): Promise { 20 | return { id: entity['id'] }; 21 | } 22 | 23 | /*addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 24 | if (!this.params || this.params.length === 0) { 25 | throw new Error('Select directive no params'); 26 | } 27 | 28 | console.log(this.params); 29 | qb.select( 30 | this.params.map((field: string) => { 31 | if (!field?.trim || typeof field !== 'string') { 32 | throw new Error(`Select directive no param"${field}" is illegal`); 33 | } 34 | return this.relationMeta.alias + '.' + field; 35 | }), 36 | ); 37 | qb.addSelect([this.relationMeta.alias + '.id']); 38 | return qb; 39 | }*/ 40 | } 41 | -------------------------------------------------------------------------------- /src/directives/query-relation-orderby.ts: -------------------------------------------------------------------------------- 1 | import { isString } from 'lodash'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { QueryRelationDirective } from 'src/directive/query/query.relation-directive'; 4 | import { SelectQueryBuilder } from 'typeorm'; 5 | 6 | export class QueryModelOrderByDirective extends QueryRelationDirective { 7 | static description = ` 8 | Magic query directive, relation orderBy directive, to sort the relation. 9 | `; 10 | 11 | static version = '1.0'; 12 | 13 | static directiveType = DirectiveType.QUERY_RELATION_DIRECTIVE; 14 | 15 | static directiveName = 'orderBy'; 16 | 17 | addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 18 | const orderMap = this.getpMap(); 19 | if (orderMap) { 20 | qb.orderBy(orderMap); 21 | } 22 | return qb; 23 | } 24 | 25 | getpMap() { 26 | const orderMap = {} as any; 27 | if (!this.directiveMeta.value) { 28 | throw new Error('Not assign params to "select" directive'); 29 | } 30 | const orderBy = this.directiveMeta.value as any; 31 | if (isString(orderBy)) { 32 | throw new Error('Orderby syntax error:' + orderBy); 33 | } 34 | for (const key in orderBy) { 35 | orderMap[`${this.relationMeta.alias}.${key}`] = orderBy[key]; 36 | } 37 | return orderMap; 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/mailer/send/send-tasks-pool.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { StorageService } from 'src/storage/storage.service'; 3 | import { MailerClientsPool } from '../mailer.clients-pool'; 4 | import { SendTask } from './send-task'; 5 | import { ISendTasksPool } from './i-send-tasks-pool'; 6 | import { Mail } from 'src/entity-interface/Mail'; 7 | import { EntityManager } from 'typeorm'; 8 | 9 | @Injectable() 10 | export class MailerSendTasksPool implements ISendTasksPool { 11 | private pool = new Map(); 12 | 13 | constructor( 14 | private readonly storageService: StorageService, 15 | private readonly clientsPool: MailerClientsPool, 16 | ) {} 17 | 18 | async createTask(mail: Mail, entityManger: EntityManager) { 19 | let task = this.pool.get(mail.owner.id); 20 | if (!task) { 21 | task = new SendTask( 22 | entityManger, 23 | this.storageService, 24 | this.clientsPool, 25 | this, 26 | mail.owner.id, 27 | [mail], 28 | ); 29 | this.pool.set(mail.owner.id, task); 30 | await task.start(); 31 | } else { 32 | task.addMail(mail); 33 | } 34 | } 35 | 36 | removeTask(accountId: number) { 37 | this.pool.delete(accountId); 38 | } 39 | 40 | getTask(accountId: number) { 41 | return this.pool.get(accountId); 42 | } 43 | } 44 | -------------------------------------------------------------------------------- /src/schema/graph-meta-interface/column-meta.ts: -------------------------------------------------------------------------------- 1 | import { ColumnType } from './column-type'; 2 | 3 | export interface ColumnMeta { 4 | uuid: string; 5 | name: string; 6 | type: ColumnType; 7 | primary?: boolean; 8 | generated?: true | 'uuid' | 'rowid' | 'increment'; 9 | createDate?: boolean; 10 | updateDate?: boolean; 11 | deleteDate?: boolean; 12 | version?: boolean; 13 | length?: string | number; 14 | width?: number; 15 | nullable?: boolean; 16 | readonly?: boolean; 17 | select?: boolean; 18 | /** 19 | * Specifies if column's value must be unique or not. 20 | */ 21 | unique?: boolean; 22 | 23 | index?: boolean; 24 | 25 | /** 26 | * Column comment. 27 | */ 28 | comment?: string; 29 | /** 30 | * Default database value. 31 | */ 32 | default?: any; 33 | /** 34 | * The precision for a decimal (exact numeric) column (applies only for decimal column), which is the maximum 35 | * number of digits that are stored for the values. 36 | */ 37 | precision?: number; 38 | /** 39 | * The scale for a decimal (exact numeric) column (applies only for decimal column), which represents the number 40 | * of digits to the right of the decimal point and must not be greater than precision. 41 | */ 42 | scale?: number; 43 | 44 | /** 45 | * Defines a column collation. 46 | */ 47 | //collation?: string; 48 | } 49 | -------------------------------------------------------------------------------- /src/directives/post-remove-others.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { PostDirective } from 'src/directive/post/post.directive'; 3 | import { InstanceMetaCollection } from 'src/magic-meta/post/instance.meta.colletion'; 4 | 5 | export class PostRemoveOthersDirective extends PostDirective { 6 | static description = `Remove records that not saved.此命令会删除POST之外的所有记录,请谨慎使用该命令`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.POST_ENTITY_DIRECTIVE; 11 | 12 | static directiveName = 'removeOthers'; 13 | 14 | //该命令权限管理通过MagicService实现 15 | async afterSaveEntityInstanceCollection( 16 | savedInstances: any[], 17 | instanceMetaCollection: InstanceMetaCollection, 18 | ) { 19 | const ids = savedInstances.map((instance) => instance.id); 20 | //目前万能接口不支持NOT IN运算符,变通实现一下 21 | const querMeta = { 22 | entity: instanceMetaCollection.entity, 23 | select: ['id'], 24 | }; 25 | const data = await this.magicService.query(querMeta); 26 | const allIds = data.data?.map((instance: { id: number }) => instance.id); 27 | const deleteIds = allIds.filter( 28 | (id: number) => !ids.find((id2) => id === id2), 29 | ); 30 | if (deleteIds.length > 0) { 31 | await this.magicService.delete({ 32 | [instanceMetaCollection.entity]: deleteIds, 33 | }); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/directives/query-field-image-url.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryFieldDirective } from 'src/directive/query/query.field-directive'; 3 | 4 | export class QueryFieldImageUrllDirective extends QueryFieldDirective { 5 | /** 6 | * urlField,默认src 7 | * size,默认不缩放,格式100x200 8 | * path, 默认path 9 | */ 10 | static description = `获取图片url,@imageUrl(urlField, size, path)`; 11 | 12 | static version = '1.0'; 13 | 14 | static directiveType = DirectiveType.QUERY_FIELD_DIRECTIVE; 15 | 16 | static directiveName = 'imageUrl'; 17 | 18 | async filterEntity(entity: any): Promise { 19 | let urlField = 'src'; 20 | let size = undefined; 21 | let field = 'path'; 22 | 23 | if (this.directiveMeta.value.length > 0) { 24 | urlField = this.directiveMeta.value[0]; 25 | } 26 | 27 | if (this.directiveMeta.value.length > 1) { 28 | let sizeStr: string = this.directiveMeta.value[1]; 29 | sizeStr = sizeStr.replace('X', 'x'); 30 | const [width, height] = sizeStr.split('x'); 31 | size = { 32 | width, 33 | height, 34 | }; 35 | } 36 | 37 | if (this.directiveMeta.value.length > 2) { 38 | field = this.directiveMeta.value[2]; 39 | } 40 | 41 | entity[urlField] = await this.storageService.resizeImage( 42 | entity[field], 43 | size, 44 | ); 45 | return entity; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/directives/query-entity-paginate.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { QueryResult } from 'src/magic-meta/query/query-result'; 4 | import { SelectQueryBuilder } from 'typeorm'; 5 | 6 | export class QueryEntityPaginateDirective extends QueryDirective { 7 | static description = ` 8 | Magic query directive, Paginate the results. 9 | `; 10 | 11 | static version = '1.0'; 12 | 13 | static directiveType = DirectiveType.QUERY_ENTITY_DIRECTIVE; 14 | 15 | static directiveName = 'paginate'; 16 | 17 | isEffectResultCount = true; 18 | 19 | get pageSize(): number { 20 | return parseInt(this.directiveMeta.value[0]); 21 | } 22 | 23 | get pageIndex() { 24 | return parseInt(this.directiveMeta.value[1]); 25 | } 26 | 27 | addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 28 | console.assert( 29 | this.directiveMeta.value?.length > 0, 30 | 'Too few pagination parmas', 31 | ); 32 | qb.skip(this.pageSize * this.pageIndex).take(this.pageSize); 33 | this.rootMeta.maxCount = this.pageSize; 34 | return qb; 35 | } 36 | 37 | async filterResult(result: QueryResult) { 38 | result.pagination = { 39 | pageSize: this.pageSize, 40 | pageIndex: this.pageIndex, 41 | totalCount: result.totalCount, 42 | }; 43 | return result; 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/auth/auth.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { JwtService } from '@nestjs/jwt'; 3 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 4 | import { NOT_INSTALL_ERROR } from 'src/util/consts'; 5 | import * as bcrypt from 'bcrypt'; 6 | import { RxUser } from 'src/entity-interface/RxUser'; 7 | 8 | @Injectable() 9 | export class AuthService { 10 | constructor( 11 | private readonly jwtService: JwtService, 12 | private readonly typeormSerivce: TypeOrmService, 13 | ) {} 14 | 15 | async validateUser(username: string, pass: string): Promise { 16 | if (!this.typeormSerivce.connection) { 17 | throw new Error(NOT_INSTALL_ERROR); 18 | } 19 | console.debug('AuthService*****'); 20 | const user = (await this.typeormSerivce.connection 21 | .getRepository('RxUser') 22 | .createQueryBuilder('user') 23 | .addSelect('user.password') 24 | .where({ loginName: username }) 25 | .getOne()) as RxUser; 26 | 27 | if (user && (await bcrypt.compare(pass, user.password))) { 28 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 29 | const { password, ...result } = user; 30 | return result; 31 | } 32 | return null; 33 | } 34 | 35 | async login(user: any) { 36 | const payload = { username: user.name, sub: user.id }; 37 | return { 38 | access_token: this.jwtService.sign(payload), 39 | }; 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/install/install.controller.ts: -------------------------------------------------------------------------------- 1 | import { Controller, Post, Get, Body, HttpException } from '@nestjs/common'; 2 | import { PackageManageService } from 'src/package-manage/package-manage.service'; 3 | import { sleep } from 'src/util/sleep'; 4 | import { InstallService } from './install.service'; 5 | 6 | @Controller() 7 | export class InstallController { 8 | constructor( 9 | private readonly installService: InstallService, 10 | private readonly packageManage: PackageManageService, 11 | ) {} 12 | 13 | @Post('install') 14 | async intstall(@Body() body) { 15 | try { 16 | return await this.installService.install(body); 17 | } catch (error: any) { 18 | console.error('Install error:', error); 19 | throw new HttpException( 20 | { 21 | status: 500, 22 | error: error.message, 23 | }, 24 | 500, 25 | ); 26 | } 27 | } 28 | 29 | @Get('is-installed') 30 | async isInstalled() { 31 | return await this.installService.isInstalled(); 32 | } 33 | 34 | @Post('publish-package') 35 | async publishPackage(@Body() body) { 36 | try { 37 | await sleep(1000); 38 | return await this.packageManage.publishPackages([body]); 39 | } catch (error: any) { 40 | console.error('Install error:', error); 41 | throw new HttpException( 42 | { 43 | status: 500, 44 | error: error.message, 45 | }, 46 | 500, 47 | ); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/directives/query-condition-in.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionBetweenDirective extends QueryConditionDirective { 6 | static description = `Condition in directive.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = 'in'; 13 | 14 | getAndWhereStatement(): [string, any] { 15 | const field = this.field; 16 | const value = this.value; 17 | if (!value) { 18 | throw new Error( 19 | `Field "${field}" value "${value}" can not be used to in directive`, 20 | ); 21 | } 22 | const paramName = 'param' + createId(); 23 | if (value.length === 0) { 24 | return [' false ', {}]; 25 | } 26 | 27 | if (value.length === 1) { 28 | let paramValue = this.value; 29 | if (this.value?.toString().startsWith('$me.')) { 30 | const [, columnStr] = (this.value as string).split('.'); 31 | paramValue = this.magicService.me[columnStr]; 32 | } 33 | return [ 34 | `${this.field} = :${paramName} `, 35 | { 36 | [paramName]: paramValue, 37 | }, 38 | ]; 39 | } 40 | 41 | return [ 42 | `${field} IN (:...${paramName}) `, 43 | { 44 | [paramName]: value, 45 | }, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/directives/query-condition-not-in.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryConditionDirective } from 'src/directive/query/query.condition-directive'; 3 | import { createId } from 'src/util/create-id'; 4 | 5 | export class QueryConditionBetweenDirective extends QueryConditionDirective { 6 | static description = `Condition not in directive.`; 7 | 8 | static version = '1.0'; 9 | 10 | static directiveType = DirectiveType.QUERY_CONDITION_DIRECTIVE; 11 | 12 | static directiveName = 'notIn'; 13 | 14 | getAndWhereStatement(): [string, any] { 15 | const field = this.field; 16 | const value = this.value; 17 | if (!value) { 18 | throw new Error( 19 | `Field "${field}" value "${value}" can not be used to in directive`, 20 | ); 21 | } 22 | const paramName = 'param' + createId(); 23 | if (value.length === 0) { 24 | return [' true ', {}]; 25 | } 26 | 27 | if (value.length === 1) { 28 | let paramValue = this.value; 29 | if (this.value?.toString().startsWith('$me.')) { 30 | const [, columnStr] = (this.value as string).split('.'); 31 | paramValue = this.magicService.me[columnStr]; 32 | } 33 | return [ 34 | `${this.field} != :${paramName} `, 35 | { 36 | [paramName]: paramValue, 37 | }, 38 | ]; 39 | } 40 | 41 | return [ 42 | `${field} NOT IN (:...${paramName}) `, 43 | { 44 | [paramName]: value, 45 | }, 46 | ]; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/mailer/receive/receive-tasks-pool.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Inject, Injectable } from '@nestjs/common'; 2 | import { MailConfig } from 'src/entity-interface/MailConfig'; 3 | import { StorageService } from 'src/storage/storage.service'; 4 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 5 | import { MailerClientsPool } from '../mailer.clients-pool'; 6 | import { ReceiveTask } from './receive-task'; 7 | import { IReceiveTasksPool } from './i-receive-tasks-pool'; 8 | 9 | @Injectable() 10 | export class MailerReceiveTasksPool implements IReceiveTasksPool { 11 | private pool = new Map(); 12 | 13 | constructor( 14 | @Inject(forwardRef(() => TypeOrmService)) 15 | private readonly typeOrmService: TypeOrmService, 16 | private readonly storageService: StorageService, 17 | private readonly clientsPool: MailerClientsPool, 18 | ) {} 19 | 20 | createTask(accountId: number, configs: MailConfig[]) { 21 | let task = this.pool.get(accountId); 22 | if (!task) { 23 | task = new ReceiveTask( 24 | this.typeOrmService, 25 | this.storageService, 26 | this.clientsPool, 27 | this, 28 | accountId, 29 | configs, 30 | ); 31 | this.pool.set(accountId, task); 32 | task.start(); 33 | } else { 34 | task.addConfigs(configs); 35 | } 36 | } 37 | 38 | removeTask(accountId: number) { 39 | this.pool.delete(accountId); 40 | } 41 | 42 | getTask(accountId: number) { 43 | return this.pool.get(accountId); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/magic-meta/query/parse-relations-from-where-sql.ts: -------------------------------------------------------------------------------- 1 | import { AddonRelationInfo } from './addon-relation-info'; 2 | 3 | // eslint-disable-next-line @typescript-eslint/no-var-requires 4 | const SqlWhereParser = require('sql-where-parser'); 5 | const OPERATOR_UNARY_MINUS = Symbol('-'); 6 | 7 | export function parseRelationsFromWhereSql(sql: string): AddonRelationInfo[] { 8 | if (!sql) { 9 | throw new Error( 10 | 'Not assign sql statement to where directive or expression', 11 | ); 12 | } 13 | 14 | const parser = new SqlWhereParser(); 15 | const relations: AddonRelationInfo[] = []; 16 | const evaluator = (operatorValue, operands) => { 17 | if (operatorValue === OPERATOR_UNARY_MINUS) { 18 | operatorValue = '-'; 19 | } 20 | if (operatorValue === ',') { 21 | return [].concat(operands[0], operands[1]); 22 | } 23 | 24 | if (operatorValue === 'NOT') { 25 | return 'NOT ' + operands[0]; 26 | } 27 | 28 | const arr = operands[0]?.split('.'); 29 | if (arr?.length > 1) { 30 | const fieldName = arr[arr.length - 1]; 31 | arr.splice(arr.length - 1, 1); 32 | const relationName = arr.join('.'); 33 | const relation = relations.find( 34 | (relation) => relation.name === relationName, 35 | ); 36 | if (relation) { 37 | relation.fields.push(fieldName); 38 | } else { 39 | relations.push({ name: relationName, fields: [fieldName] }); 40 | } 41 | } 42 | }; 43 | 44 | parser.parse(sql, evaluator); 45 | 46 | return relations; 47 | } 48 | -------------------------------------------------------------------------------- /src/entity-interface/Mail.ts: -------------------------------------------------------------------------------- 1 | import { Attachment } from './Attachment'; 2 | import { MailIdentifier } from './MailIdentifier'; 3 | import { RxUser } from './RxUser'; 4 | import { MailLabel } from './MailLabel'; 5 | import { MailPriority } from './MailPriority'; 6 | import { MailBoxType } from './MailBoxType'; 7 | import { SendStatus } from './SendStatus'; 8 | import { AddressItem } from './AddressItem'; 9 | 10 | export const EntityMail = 'Mail'; 11 | export interface Mail { 12 | id?: number; 13 | subject?: string; 14 | from?: AddressItem; 15 | to?: AddressItem[]; 16 | cc?: AddressItem[]; 17 | bcc?: AddressItem[]; 18 | date?: Date; 19 | messageId?: string; 20 | inReplyTo?: string; 21 | replyTo?: any; 22 | references?: any; 23 | html?: string; 24 | text?: string; 25 | priority?: MailPriority; 26 | inMailBox: MailBoxType; 27 | showAsOriginal?: boolean; 28 | fromAddress?: string; 29 | unRead?: boolean; 30 | answered?: boolean; 31 | deleted?: boolean; 32 | forwarded?: boolean; 33 | fromOldCustomer?: boolean; 34 | size?: number; 35 | inMailBoxBeforeDelete?: MailBoxType; 36 | scheduleSendDate?: Date; 37 | isSeparateSend?: boolean; 38 | sendStatus?: SendStatus; 39 | sendErrorMessage?: any[]; 40 | fromConfigId?: number; 41 | createdAt?: Date; 42 | updatedAt?: Date; 43 | isPlainText?: boolean; 44 | headers?: any; 45 | receivedAddress?: string; 46 | toAddress?: string; 47 | htmlAsText?: string; 48 | owner?: RxUser; 49 | labels?: MailLabel[]; 50 | attachments?: Attachment[]; 51 | identifier?: MailIdentifier; 52 | } 53 | -------------------------------------------------------------------------------- /src/magic/query/traverser/make-abilities-query-builder.ts: -------------------------------------------------------------------------------- 1 | import { RxAbility } from 'src/entity-interface/RxAbility'; 2 | import { RxUser } from 'src/entity-interface/RxUser'; 3 | import { parseWhereSql } from 'src/magic-meta/query/parse-where-sql'; 4 | import { QueryEntityMeta } from 'src/magic-meta/query/query.entity-meta'; 5 | import { SelectQueryBuilder } from 'typeorm'; 6 | 7 | export function makeAbilitiesQueryBuilder( 8 | meta: QueryEntityMeta, 9 | qb: SelectQueryBuilder, 10 | me: RxUser, 11 | ) { 12 | const abilities = meta.abilities; 13 | if (me.isDemo || me.isSupper) { 14 | return qb; 15 | } 16 | //如果没有权限设置 17 | if (abilities.length === 0) { 18 | qb.andWhere('false'); 19 | return qb; 20 | } 21 | const [whereStringArray, whereParams] = getEntityQueryAbilitySql( 22 | abilities, 23 | meta, 24 | me, 25 | ); 26 | 27 | if (whereStringArray.length > 0) { 28 | qb.andWhere(whereStringArray.join(' OR '), whereParams); 29 | } 30 | } 31 | 32 | export function getEntityQueryAbilitySql( 33 | abilities: RxAbility[], 34 | meta: QueryEntityMeta, 35 | me: RxUser, 36 | ): [string[], any] { 37 | const whereStringArray: string[] = []; 38 | let whereParams: any = {}; 39 | 40 | for (const ability of abilities) { 41 | //如果没有表达式,则说明具有所有读权限 42 | if (ability.can && !ability.expression) { 43 | return [[], {}]; 44 | } 45 | const [whereStr, params] = parseWhereSql(ability.expression, meta, me); 46 | if (whereStr) { 47 | whereStringArray.push(whereStr); 48 | whereParams = { ...whereParams, ...params }; 49 | } 50 | } 51 | 52 | return [whereStringArray, whereParams]; 53 | } 54 | -------------------------------------------------------------------------------- /src/directive/post/post.directive.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-empty-function */ 2 | /* eslint-disable @typescript-eslint/no-unused-vars */ 3 | import { MagicService } from 'src/magic-meta/magic.service'; 4 | import { InstanceMeta } from 'src/magic-meta/post/instance.meta'; 5 | import { InstanceMetaCollection } from 'src/magic-meta/post/instance.meta.colletion'; 6 | import { RelationMetaCollection } from 'src/magic-meta/post/relation.meta.colletion'; 7 | import { MailerSendService } from 'src/mailer/send/mailer.send.service'; 8 | import { SchemaService } from 'src/schema/schema.service'; 9 | import { EntityManager, EntitySchemaRelationOptions } from 'typeorm'; 10 | import { DirectiveMeta } from '../directive.meta'; 11 | 12 | export class PostDirective { 13 | constructor( 14 | protected readonly entityManger: EntityManager, 15 | protected readonly directiveMeta: DirectiveMeta, 16 | protected readonly magicService: MagicService, 17 | protected readonly mailerSendService: MailerSendService, 18 | ) {} 19 | 20 | async beforeSaveInstance(instanceMeta: InstanceMeta) { 21 | return instanceMeta; 22 | } 23 | 24 | async afterSaveInstance(savedInstance: any, entityName: string) {} 25 | 26 | async beforeUpdateRelationCollection( 27 | ownerInstanceMeta: InstanceMeta, 28 | relationMetaCollection: RelationMetaCollection, 29 | ) {} 30 | 31 | async afterSaveEntityInstanceCollection( 32 | savedInstances: any[], 33 | instanceMetaCollection: InstanceMetaCollection, 34 | ) {} 35 | 36 | async afterSaveOneRelationInstanceCollection( 37 | ownerInstanceMeta: InstanceMeta, 38 | savedInstances: any[], 39 | relationMetaCollection: RelationMetaCollection, 40 | ) {} 41 | } 42 | -------------------------------------------------------------------------------- /src/directives/query-entity-where.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { parseWhereSql } from 'src/magic-meta/query/parse-where-sql'; 4 | import { parseRelationsFromWhereSql } from 'src/magic-meta/query/parse-relations-from-where-sql'; 5 | import { createAddonRelation } from 'src/magic/query/magic.query.parser'; 6 | 7 | export class QueryEntityWhereDirective extends QueryDirective { 8 | static description = ` 9 | Where directive. 10 | `; 11 | 12 | static version = '1.0'; 13 | 14 | static directiveType = DirectiveType.QUERY_ENTITY_DIRECTIVE; 15 | 16 | static directiveName = 'where'; 17 | 18 | getAndWhereStatement(): [string, any] | void { 19 | const meta = this.rootMeta; 20 | //添加条件用到的关联 21 | const relationInfos = parseRelationsFromWhereSql(this.directiveMeta.value); 22 | //meta.addonRelationInfos.push(...relationInfos); 23 | for (const relationInfo of relationInfos) { 24 | createAddonRelation(relationInfo.name, meta, this.schemaService); 25 | } 26 | /*for (const relationInfo of relationInfos) { 27 | const relation = new QueryRelationMeta(); 28 | relation.entityMeta = this.schemaService.getRelationEntityMetaOrFailed( 29 | relationInfo.name, 30 | meta.entity, 31 | ); 32 | relation.name = relationInfo.name; 33 | relation.parentEntityMeta = meta; 34 | if (!meta.relations.find((rela) => rela.name === relation.name)) { 35 | meta.addAddOnRelation(relation); 36 | } 37 | }*/ 38 | 39 | return parseWhereSql( 40 | this.directiveMeta.value, 41 | this.rootMeta, 42 | this.magicService.me, 43 | ); 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/directives/query-relation-select.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { QueryRelationDirective } from 'src/directive/query/query.relation-directive'; 3 | 4 | export class QueryModelTakeDirective extends QueryRelationDirective { 5 | static description = ` 6 | Magic query directive, select directive, to filter selected field. 7 | `; 8 | 9 | static version = '1.0'; 10 | 11 | static directiveType = DirectiveType.QUERY_RELATION_DIRECTIVE; 12 | 13 | static directiveName = 'select'; 14 | 15 | get params() { 16 | return this.directiveMeta.value; 17 | } 18 | 19 | async filterEntity(entity: any): Promise { 20 | if (!this.params || this.params.length === 0) { 21 | throw new Error('Select directive no params'); 22 | } 23 | 24 | const newEntity = {} as any; 25 | 26 | for (const field of this.params) { 27 | if (!field?.trim || typeof field !== 'string') { 28 | throw new Error(`Select directive no param"${field}" is illegal`); 29 | } 30 | newEntity[field] = entity[field]; 31 | } 32 | 33 | return newEntity; 34 | } 35 | 36 | /*addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 37 | if (!this.params || this.params.length === 0) { 38 | throw new Error('Select directive no params'); 39 | } 40 | 41 | console.log(this.params); 42 | qb.select( 43 | this.params.map((field: string) => { 44 | if (!field?.trim || typeof field !== 'string') { 45 | throw new Error(`Select directive no param"${field}" is illegal`); 46 | } 47 | return this.relationMeta.alias + '.' + field; 48 | }), 49 | ); 50 | qb.addSelect([this.relationMeta.alias + '.id']); 51 | return qb; 52 | }*/ 53 | } 54 | -------------------------------------------------------------------------------- /src/directives/post-relation-cascade.ts: -------------------------------------------------------------------------------- 1 | import { DirectiveType } from 'src/directive/directive-type'; 2 | import { PostDirective } from 'src/directive/post/post.directive'; 3 | import { InstanceMeta } from 'src/magic-meta/post/instance.meta'; 4 | import { RelationMetaCollection } from 'src/magic-meta/post/relation.meta.colletion'; 5 | 6 | export class PostRelationCascadeDirective extends PostDirective { 7 | static description = `删除关联时,级联删除关联对象`; 8 | 9 | static version = '1.0'; 10 | 11 | static directiveType = DirectiveType.POST_RELATION_DIRECTIVE; 12 | 13 | static directiveName = 'cascade'; 14 | 15 | private oldRelationIds: number[] = []; 16 | 17 | async beforeUpdateRelationCollection( 18 | ownerInstanceMeta: InstanceMeta, 19 | relationMetaCollection: RelationMetaCollection, 20 | ) { 21 | const data = await this.magicService.query({ 22 | entity: ownerInstanceMeta.entity, 23 | id: ownerInstanceMeta.meta.id, 24 | [relationMetaCollection.relationName]: {}, 25 | '@getOne': true, 26 | }); 27 | if (data.data) { 28 | this.oldRelationIds = data.data[relationMetaCollection.relationName]?.map( 29 | (relation: { id: number }) => relation.id, 30 | ); 31 | } 32 | } 33 | 34 | //该命令的权限通过magicService完成 35 | async afterSaveOneRelationInstanceCollection( 36 | ownerInstanceMeta: InstanceMeta, 37 | savedInstances: any[], 38 | relationMetaCollection: RelationMetaCollection, 39 | ) { 40 | const deleteIds = this.oldRelationIds.filter( 41 | (id) => !savedInstances.find((instance) => instance.id === id), 42 | ); 43 | 44 | if (deleteIds.length > 0) { 45 | await this.magicService.delete({ 46 | [relationMetaCollection.entity]: deleteIds, 47 | }); 48 | } 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/magic/update/magic.update.parser.ts: -------------------------------------------------------------------------------- 1 | import { EntityMeta } from 'src/schema/graph-meta-interface/entity-meta'; 2 | import { SchemaService } from 'src/schema/schema.service'; 3 | import { UpdateMeta } from '../../magic-meta/update/update.meta'; 4 | import { AbilityService } from '../ability.service'; 5 | import { TOKEN_IDS, TOKEN_WHERE } from '../base/tokens'; 6 | export class MagicUpdateParser { 7 | constructor( 8 | private readonly schemaService: SchemaService, 9 | private readonly abilityService: AbilityService, 10 | ) {} 11 | 12 | async parse(json: any) { 13 | const metas: UpdateMeta[] = []; 14 | for (const keyStr in json) { 15 | const entityMeta = this.schemaService.getEntityMetaOrFailed( 16 | keyStr.trim(), 17 | ); 18 | const abilities = await this.abilityService.getEntityPostAbilities( 19 | entityMeta.uuid, 20 | ); 21 | const expand = await this.abilityService.isEntityExpand(entityMeta.uuid); 22 | const updateMeta = this.parseOneEntity(entityMeta, json[keyStr]); 23 | updateMeta.abilities = abilities; 24 | updateMeta.expandFieldForAuth = expand; 25 | metas.push(updateMeta); 26 | } 27 | return metas; 28 | } 29 | 30 | parseOneEntity(entityMeta: EntityMeta, json: any) { 31 | const updateMeta = new UpdateMeta(); 32 | 33 | updateMeta.entityMeta = entityMeta; 34 | for (const keyStr in json) { 35 | const value = json[keyStr]; 36 | if (keyStr.trim().toLowerCase() === TOKEN_IDS) { 37 | updateMeta.ids = value; 38 | } else if (keyStr.replace('@', '').trim() === TOKEN_WHERE) { 39 | updateMeta.whereSQL = value; 40 | } else { 41 | updateMeta.columns[keyStr] = value; 42 | } 43 | } 44 | 45 | return updateMeta; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/directive/query-directive.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { QueryDirectiveClass } from './query/query.directive.class'; 3 | import { QueryConditionDirectiveClass } from './query/query.condition-directive-class'; 4 | import { QueryRelationDirectiveClass } from './query/query.relation-directive-class'; 5 | import { DirectiveStorage } from './directive.storage'; 6 | import { QueryFieldDirectiveClass } from './query/query.field-directive.class'; 7 | 8 | @Injectable() 9 | export class QueryDirectiveService { 10 | constructor(private readonly directiveStorage: DirectiveStorage) {} 11 | 12 | findEntityDirectiveOrFailed(name: string): QueryDirectiveClass { 13 | const directiveClass = 14 | this.directiveStorage.queryEntityDirectiveClasses[name]; 15 | if (!directiveClass) { 16 | throw new Error(`No entity or field directive "${name}"`); 17 | } 18 | return directiveClass; 19 | } 20 | 21 | findRelationDirectiveOrFailed(name: string): QueryRelationDirectiveClass { 22 | const directiveClass = 23 | this.directiveStorage.queryRelationDirectiveClasses[name]; 24 | if (!directiveClass) { 25 | throw new Error(`No relation directive "${name}"`); 26 | } 27 | return directiveClass; 28 | } 29 | 30 | findConditionDirectiveOrFailed(name: string): QueryConditionDirectiveClass { 31 | const directiveClass = 32 | this.directiveStorage.queryConditionDirectiveClasses[name]; 33 | if (!directiveClass) { 34 | throw new Error(`No condition directive "${name}"`); 35 | } 36 | return directiveClass; 37 | } 38 | 39 | findFieldDirective(name: string): QueryFieldDirectiveClass { 40 | const directiveClass = 41 | this.directiveStorage.queryFieldDirectiveClasses[name]; 42 | return directiveClass; 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/package-manage/package-manage.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { EntityRxPackage, RxPackage } from 'src/entity-interface/RxPackage'; 3 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 4 | import { SCHEMAS_DIR } from 'src/util/consts'; 5 | import { PlatformTools } from 'typeorm/platform/PlatformTools'; 6 | @Injectable() 7 | export class PackageManageService { 8 | constructor(private readonly typeormSerivce: TypeOrmService) {} 9 | 10 | public async savePackage(aPackage: RxPackage) { 11 | const packageRepository = 12 | this.typeormSerivce.connection.getRepository(EntityRxPackage); 13 | let systemPackage = await packageRepository.findOne({ 14 | where: { uuid: aPackage.uuid }, 15 | }); 16 | 17 | if (!systemPackage) { 18 | systemPackage = packageRepository.create(); 19 | } 20 | 21 | systemPackage.uuid = aPackage.uuid; 22 | systemPackage.name = aPackage.name; 23 | systemPackage.status = aPackage.status; 24 | systemPackage.entities = aPackage.entities; 25 | systemPackage.diagrams = aPackage.diagrams; 26 | systemPackage.relations = aPackage.relations; 27 | 28 | await packageRepository.save(systemPackage); 29 | } 30 | 31 | public async publishPackages(packages: RxPackage[]) { 32 | if (!PlatformTools.fileExist(SCHEMAS_DIR)) { 33 | // eslint-disable-next-line @typescript-eslint/no-var-requires 34 | const fs = require('fs'); 35 | await fs.promises.mkdir(PlatformTools.pathResolve(SCHEMAS_DIR)); 36 | } 37 | 38 | for (const aPackage of packages) { 39 | await PlatformTools.writeFile( 40 | SCHEMAS_DIR + aPackage.uuid + '.json', 41 | JSON.stringify(aPackage, null, 2), 42 | ); 43 | } 44 | 45 | await this.typeormSerivce.restart(); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/magic/delete/magic.delete.parser.ts: -------------------------------------------------------------------------------- 1 | import { JsonUnit } from '../base/json-unit'; 2 | import { DeleteMeta } from '../../magic-meta/delete/delete.meta'; 3 | import { MagicService } from 'src/magic-meta/magic.service'; 4 | import { DeleteDirectiveService } from 'src/directive/delete-directive.service'; 5 | import { AbilityService } from '../ability.service'; 6 | import { SchemaService } from 'src/schema/schema.service'; 7 | import { TOKEN_SOFT } from '../base/tokens'; 8 | 9 | export class MagicDeleteParser { 10 | constructor( 11 | private readonly deleteDirectiveService: DeleteDirectiveService, 12 | private readonly magicService: MagicService, 13 | private readonly abilityService: AbilityService, 14 | public readonly schemaService: SchemaService, 15 | ) {} 16 | 17 | async parse(json: any) { 18 | const deleteMetas: DeleteMeta[] = []; 19 | for (const keyStr in json) { 20 | const value = json[keyStr]; 21 | const jsonUnit = new JsonUnit(keyStr, value); 22 | const deleteMeta = new DeleteMeta(jsonUnit); 23 | const entityMeta = this.schemaService.getEntityMetaOrFailed( 24 | deleteMeta.entity, 25 | ); 26 | const abilities = await this.abilityService.getEntityDeleteAbilities( 27 | entityMeta.uuid, 28 | ); 29 | deleteMeta.abilities = abilities; 30 | 31 | for (const directiveMeta of jsonUnit.directives) { 32 | if (directiveMeta.name !== TOKEN_SOFT) { 33 | const DirectiveClass = 34 | this.deleteDirectiveService.findDirectiveOrFailed( 35 | directiveMeta.name, 36 | ); 37 | deleteMeta.directives.push( 38 | new DirectiveClass(directiveMeta, this.magicService), 39 | ); 40 | } 41 | } 42 | 43 | deleteMetas.push(deleteMeta); 44 | } 45 | return deleteMetas; 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/magic/upload/file-upload.utils.ts: -------------------------------------------------------------------------------- 1 | import { HttpException, HttpStatus } from '@nestjs/common'; 2 | import { extname } from 'path'; 3 | import { RxMediaType } from 'src/entity-interface/RxMediaType'; 4 | import { v4 as uuid } from 'uuid'; 5 | 6 | export const fileFilter = (req: any, file: any, cb: any) => { 7 | if ( 8 | file.mimetype.match( 9 | /\/(jpg|jpeg|png|gif|pdf|AVI|nAVI|ASF|MOV|3GP|WMV|DivX|XviD|RM|RMV)$/, 10 | ) 11 | ) { 12 | // Allow storage of file 13 | cb(null, true); 14 | } else { 15 | // Reject file 16 | cb( 17 | new HttpException( 18 | `Unsupported file type ${extname(file.originalname)}`, 19 | HttpStatus.BAD_REQUEST, 20 | ), 21 | false, 22 | ); 23 | } 24 | }; 25 | 26 | export const fileName = (req: any, file: any, cb: any) => { 27 | cb(null, getFileName(file)); 28 | }; 29 | 30 | export const getFileName = (file: Express.Multer.File) => { 31 | return `${uuid()}${extname(file.originalname)}`; 32 | }; 33 | 34 | export const getFileType = (file: any): RxMediaType => { 35 | const ext = extname(file.originalname).replace('.', ''); 36 | 37 | if ( 38 | ext === 'doc' || 39 | ext === 'docx' || 40 | ext === 'xsl' || 41 | ext === 'xslx' || 42 | ext === 'pdf' 43 | ) { 44 | return RxMediaType.DOCUMENT; 45 | } 46 | 47 | if (ext === 'png' || ext === 'jpg' || ext === 'jpeg' || ext === 'gif') { 48 | return RxMediaType.IMAGE; 49 | } 50 | if ( 51 | ext === 'avi' || 52 | ext === 'mov' || 53 | ext === 'rmvb' || 54 | ext === 'rm' || 55 | ext === 'flv' || 56 | ext === 'mp4' || 57 | ext === '3gp' 58 | ) { 59 | return RxMediaType.VIDEO; 60 | } 61 | 62 | if ( 63 | ext === 'mpeg' || 64 | ext === 'mp3' || 65 | ext === 'mpeg-4' || 66 | ext === 'midi' || 67 | ext === 'wma' 68 | ) { 69 | return RxMediaType.AUDIO; 70 | } 71 | }; 72 | -------------------------------------------------------------------------------- /src/directives/query-entity-tree.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { QueryResult } from 'src/magic-meta/query/query-result'; 4 | import { TOKEN_GET_ONE } from 'src/magic/base/tokens'; 5 | import { SelectQueryBuilder } from 'typeorm'; 6 | 7 | export class QueryEntityTreeDirective extends QueryDirective { 8 | static description = `Magic query directive, make result to a tree struct.`; 9 | 10 | static version = '1.0'; 11 | 12 | static directiveType = DirectiveType.QUERY_ENTITY_DIRECTIVE; 13 | 14 | static directiveName = 'tree'; 15 | 16 | addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 17 | if (this.rootMeta.fetchString === TOKEN_GET_ONE) { 18 | throw Error('Tree directive can not use getOne directive'); 19 | } 20 | return qb.leftJoinAndSelect(`${this.rootMeta.alias}.parent`, 'parent'); 21 | } 22 | 23 | async filterResult(result: QueryResult) { 24 | result.data = this.do(result.data); 25 | return result; 26 | } 27 | 28 | private do(models: any[]) { 29 | const roots = []; 30 | const leftModels = []; 31 | for (const model of models) { 32 | if (!model.parent) { 33 | roots.push(model); 34 | } else { 35 | leftModels.push(model); 36 | } 37 | } 38 | 39 | for (const child of roots) { 40 | this.buildChildren(child, leftModels); 41 | } 42 | return roots; 43 | } 44 | 45 | private buildChildren(parentModel: any, models: any[]) { 46 | parentModel.children = []; 47 | const leftModels = []; 48 | for (const model of models) { 49 | if (model.parent?.id && model.parent.id === parentModel.id) { 50 | parentModel.children.push(model); 51 | delete model.parent; 52 | } else { 53 | leftModels.push(model); 54 | } 55 | } 56 | 57 | for (const child of parentModel.children) { 58 | this.buildChildren(child, leftModels); 59 | } 60 | } 61 | } 62 | -------------------------------------------------------------------------------- /src/directive/query/query.directive.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-unused-vars */ 2 | import { QueryResult } from 'src/magic-meta/query/query-result'; 3 | import { MagicService } from 'src/magic-meta/magic.service'; 4 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 5 | import { SchemaService } from 'src/schema/schema.service'; 6 | import { Connection, SelectQueryBuilder } from 'typeorm'; 7 | import { DirectiveMeta } from '../directive.meta'; 8 | 9 | export class QueryDirective { 10 | /** 11 | * 是否影响查询结果条数,如果是,分页时需要剔除该指令来求totalCount 12 | */ 13 | isEffectResultCount?: boolean; 14 | 15 | constructor( 16 | protected readonly directiveMeta: DirectiveMeta, 17 | protected readonly rootMeta: QueryRootMeta, 18 | protected readonly magicService: MagicService, 19 | protected readonly schemaService: SchemaService, 20 | ) {} 21 | 22 | /** 23 | * 构建QueryBuilder 24 | * @param qb 用于查询数据的QueryBuilder 25 | * @returns 修改后的QueryBuilder 26 | */ 27 | addToQueryBuilder(qb: SelectQueryBuilder): SelectQueryBuilder { 28 | return qb; 29 | } 30 | 31 | /** 32 | * 构建条件SQL,请不要包含where, AND条件 33 | * @returns 返回构建好的SQL语句跟参数 34 | */ 35 | getAndWhereStatement(): [string, any] | void { 36 | return; 37 | } 38 | 39 | /** 40 | * 构建条件SQL,请不要包含where, OR条件 41 | * @returns 42 | */ 43 | getOrWhereStatement(): [string, any] | void { 44 | return; 45 | } 46 | 47 | /** 48 | * 过滤查询结果 49 | * @param result 查询结果 50 | * @returns 过滤后的查询结果 51 | */ 52 | async filterResult(result: QueryResult): Promise { 53 | return result; 54 | } 55 | 56 | /** 57 | * 过滤实体 58 | * @param entity 传入实体 59 | * @returns 过滤后实体 60 | */ 61 | async filterEntity(entity: any, parentEntity?: any): Promise { 62 | return entity; 63 | } 64 | 65 | /** 66 | * 查询附带的修改操作,用于增加浏览次数,更改已读标志等功能 67 | * 该操作对查询结果有影响,所以在查询前调用 68 | * @param model 实体的类名 69 | * @param connection 数据库连接 70 | * @returns 71 | */ 72 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 73 | mutation(model: string, connection: Connection): void { 74 | return; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/mailer/mailer.controller.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Body, 3 | Controller, 4 | Get, 5 | HttpException, 6 | Post, 7 | UseGuards, 8 | } from '@nestjs/common'; 9 | import { AuthGuard } from '@nestjs/passport'; 10 | import { MailReceiveConfig } from 'src/entity-interface/MailReceiveConfig'; 11 | import { SmtpConfig } from 'src/entity-interface/SmtpConfig'; 12 | import { CRYPTO_KEY } from 'src/util/consts'; 13 | import { MailerTestService } from './mailer.test-service'; 14 | import { MailerSendService } from './send/mailer.send.service'; 15 | 16 | @Controller('mailer') 17 | export class MailerController { 18 | constructor( 19 | private readonly sendService: MailerSendService, 20 | private readonly testService: MailerTestService, 21 | ) {} 22 | 23 | /** 24 | * @returns 用户给邮件password字段加密的KEY 25 | */ 26 | @UseGuards(AuthGuard()) 27 | @Get('crypto-key') 28 | cryptoKey() { 29 | return { cryptoKey: CRYPTO_KEY }; 30 | } 31 | 32 | @UseGuards(AuthGuard()) 33 | @Post('test-pop3') 34 | async testPOP3(@Body() body: MailReceiveConfig) { 35 | try { 36 | return await this.testService.testPOP3(body); 37 | } catch (error: any) { 38 | console.error('testPOP3 error:', error); 39 | throw new HttpException( 40 | { 41 | status: 500, 42 | error: error.message, 43 | }, 44 | 500, 45 | ); 46 | } 47 | } 48 | 49 | @UseGuards(AuthGuard()) 50 | @Post('test-imap4') 51 | async testIMAP4(@Body() body: MailReceiveConfig) { 52 | try { 53 | return await this.testService.testIMAP4(body); 54 | } catch (error: any) { 55 | console.error('testIMAP4 error:', error); 56 | throw new HttpException( 57 | { 58 | status: 500, 59 | error: error.message, 60 | }, 61 | 500, 62 | ); 63 | } 64 | } 65 | 66 | @UseGuards(AuthGuard()) 67 | @Post('test-smtp') 68 | async testSMTP(@Body() body: SmtpConfig) { 69 | try { 70 | return await this.testService.testSMTP(body); 71 | } catch (error: any) { 72 | console.error('testSMTP error:', error); 73 | throw new HttpException( 74 | { 75 | status: 500, 76 | error: error.message, 77 | }, 78 | 500, 79 | ); 80 | } 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/magic/query/traverser/make-relations-builder.ts: -------------------------------------------------------------------------------- 1 | import { RxUser } from 'src/entity-interface/RxUser'; 2 | import { QueryRelationMeta } from 'src/magic-meta/query/query.relation-meta'; 3 | import { SelectQueryBuilder } from 'typeorm'; 4 | import { getEntityQueryAbilitySql } from './make-abilities-query-builder'; 5 | 6 | export function makeRelationsBuilder( 7 | relationMetas: QueryRelationMeta[], 8 | qb: SelectQueryBuilder, 9 | me: RxUser, 10 | ) { 11 | for (const relationMeta of relationMetas) { 12 | qb = makeOneRelationBuilder(relationMeta, qb, me); 13 | } 14 | 15 | return qb; 16 | } 17 | 18 | function makeOneRelationBuilder( 19 | relationMeta: QueryRelationMeta, 20 | qb: SelectQueryBuilder, 21 | me: RxUser, 22 | ) { 23 | const whereAndStringArray: string[] = []; 24 | const whereOrStringArray: string[] = []; 25 | let whereParams: any = {}; 26 | for (const directive of relationMeta.directives) { 27 | const [whereStrAnd, paramAnd] = directive.getAndWhereStatement() || []; 28 | if (whereStrAnd) { 29 | whereAndStringArray.push(whereStrAnd); 30 | whereParams = { ...whereParams, ...paramAnd }; 31 | } 32 | const [whereStrOr, paramOr] = directive.getOrWhereStatement() || []; 33 | if (whereStrOr) { 34 | whereOrStringArray.push(whereStrOr); 35 | whereParams = { ...whereParams, ...paramOr }; 36 | } 37 | //if (whereStrOr) { 38 | // qb.orWhere(whereStrOr, paramOr); 39 | //} 40 | directive.addToQueryBuilder(qb); 41 | } 42 | 43 | //本部分代码并未严格测试 44 | const orSQLString = 45 | whereOrStringArray.length > 0 46 | ? ' OR ' + whereOrStringArray.join(' OR ') 47 | : ''; 48 | 49 | const [whereArray, params] = getEntityQueryAbilitySql( 50 | relationMeta.abilities, 51 | relationMeta, 52 | me, 53 | ); 54 | 55 | if (whereArray.length > 0) { 56 | whereAndStringArray.push(whereArray.join(' OR ')); 57 | } 58 | 59 | qb.leftJoinAndSelect( 60 | `${relationMeta.parentEntityMeta.alias}.${relationMeta.name}`, 61 | relationMeta.alias, 62 | whereAndStringArray.join(' AND ') + orSQLString, 63 | { ...whereParams, ...params }, 64 | ); 65 | 66 | qb = makeRelationsBuilder( 67 | [...relationMeta.relations, ...relationMeta.addonRelations], 68 | qb, 69 | me, 70 | ); 71 | return qb; 72 | } 73 | -------------------------------------------------------------------------------- /src/typeorm/typeorm.service.ts: -------------------------------------------------------------------------------- 1 | import { 2 | Injectable, 3 | Logger, 4 | OnApplicationShutdown, 5 | OnModuleInit, 6 | } from '@nestjs/common'; 7 | import { SchemaService } from 'src/schema/schema.service'; 8 | import { DB_CONFIG_FILE } from 'src/util/consts'; 9 | import { 10 | Connection, 11 | createConnection, 12 | EntitySchema, 13 | Repository, 14 | } from 'typeorm'; 15 | import { PlatformTools } from 'typeorm/platform/PlatformTools'; 16 | 17 | const CONNECTION_WITH_SCHEMA_NAME = 'WithSchema'; 18 | 19 | @Injectable() 20 | export class TypeOrmService implements OnModuleInit, OnApplicationShutdown { 21 | private readonly _logger = new Logger('TypeOrmWithSchemaService'); 22 | private _connection?: Connection; 23 | private _connectionNumber = 1; 24 | 25 | constructor(private readonly schemaService: SchemaService) {} 26 | 27 | async createConnection() { 28 | if (!PlatformTools.fileExist(DB_CONFIG_FILE)) { 29 | return; 30 | //throw new Error(NOT_INSTALL_ERROR); 31 | } 32 | // eslint-disable-next-line @typescript-eslint/no-var-requires 33 | const dbConfig = require(PlatformTools.pathResolve(DB_CONFIG_FILE)); 34 | 35 | this._connection = await createConnection({ 36 | ...dbConfig, 37 | entities: this.schemaService.entitySchemas.map( 38 | (schema) => new EntitySchema(schema), 39 | ), 40 | name: CONNECTION_WITH_SCHEMA_NAME + this._connectionNumber, 41 | synchronize: true, 42 | }); 43 | } 44 | 45 | public get connection() { 46 | return this._connection; 47 | } 48 | 49 | public getRepository(name: string): Repository { 50 | return this.connection.getRepository(name); 51 | } 52 | 53 | //会关闭旧连接,并且以新名字创建一个新连接 54 | async restart() { 55 | this.closeConection(); 56 | this._connectionNumber++; 57 | //重新加载模式 58 | this.schemaService.reload(); 59 | await this.createConnection(); 60 | } 61 | 62 | async onModuleInit() { 63 | await this.createConnection(); 64 | console.debug('TypeOrmWithSchemaService initializated'); 65 | } 66 | 67 | async onApplicationShutdown() { 68 | this.closeConection(); 69 | } 70 | 71 | private async closeConection() { 72 | try { 73 | await this.connection?.close(); 74 | } catch (e) { 75 | this._logger.error(e?.message); 76 | } 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/mailer/mailer.gateway.ts: -------------------------------------------------------------------------------- 1 | import { 2 | SubscribeMessage, 3 | WebSocketGateway, 4 | WebSocketServer, 5 | } from '@nestjs/websockets'; 6 | import { Socket, Server } from 'socket.io'; 7 | import { MailConfig } from 'src/entity-interface/MailConfig'; 8 | //import { TypeOrmService } from 'src/typeorm/typeorm.service'; 9 | import { 10 | CHANNEL_MAILER, 11 | EVENT_CANCEL_RECEIVE, 12 | EVENT_RECEIVEMAILS, 13 | EVENT_REGISTER_MAIL_CLIENT, 14 | } from './consts'; 15 | import { MailerClientsPool } from './mailer.clients-pool'; 16 | import { MailerReceiveTasksPool } from './receive/receive-tasks-pool'; 17 | 18 | @WebSocketGateway({ namespace: CHANNEL_MAILER }) 19 | export class MailerGateway { 20 | @WebSocketServer() wss: Server; 21 | 22 | constructor( 23 | private readonly clientsPool: MailerClientsPool, 24 | private readonly tasksPool: MailerReceiveTasksPool, //private readonly typeOrmService: TypeOrmService, 25 | ) {} 26 | 27 | @SubscribeMessage(EVENT_REGISTER_MAIL_CLIENT) 28 | registerClient(client: Socket, message: { accountId: number }) { 29 | console.debug('Register client:', client.id, message.accountId); 30 | const mailClient = { 31 | accountId: message.accountId, 32 | socket: client, 33 | }; 34 | this.clientsPool.addClient(client.id, mailClient); 35 | client.on('disconnect', () => { 36 | console.debug('Client disconnect:', client.id, message.accountId); 37 | this.clientsPool.removeClient(client.id); 38 | }); 39 | this.tasksPool.getTask(message.accountId)?.emitStatusToClient(mailClient); 40 | } 41 | 42 | @SubscribeMessage(EVENT_RECEIVEMAILS) 43 | receiveMails( 44 | client: Socket, 45 | message: { accountId: number; configs: MailConfig[] }, 46 | ) { 47 | try { 48 | console.debug('receive emails', client.id); 49 | if (!this.clientsPool.has(client.id)) { 50 | this.registerClient(client, { accountId: message.accountId }); 51 | } 52 | this.tasksPool.createTask(message.accountId, message.configs); 53 | } catch (error) { 54 | console.error('捉到一个未知异常', error); 55 | } 56 | } 57 | 58 | @SubscribeMessage(EVENT_CANCEL_RECEIVE) 59 | cancelReceive(client: Socket, message: { accountId: number }) { 60 | this.tasksPool.getTask(message.accountId)?.abort(); 61 | } 62 | 63 | //@SubscribeMessage(EVENT_CONTINUE_RECEIVE) 64 | //retryReceive(client: Socket, message: { accountId: number }) { 65 | // this.tasksPool.getTask(message.accountId)?.continue(); 66 | //} 67 | } 68 | -------------------------------------------------------------------------------- /src/mailer/receive/mail-address-job.ts: -------------------------------------------------------------------------------- 1 | import { MailConfig } from 'src/entity-interface/MailConfig'; 2 | import { StorageService } from 'src/storage/storage.service'; 3 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 4 | import { Imap4Job } from './imap4-job'; 5 | import { IReceiveJob } from './i-receive-job'; 6 | import { Pop3Job } from './pop3-job'; 7 | import { IReceiveJobOwner } from './i-receive-job-owner'; 8 | import { MailerReceiveEvent } from './receive-event'; 9 | 10 | export class MailAddressJob implements IReceiveJob, IReceiveJobOwner { 11 | private jobs: IReceiveJob[] = []; 12 | private currentJob: IReceiveJob; 13 | constructor( 14 | private readonly typeOrmService: TypeOrmService, 15 | private readonly storageService: StorageService, 16 | private readonly config: MailConfig, 17 | public readonly jobOwner: IReceiveJobOwner, 18 | private readonly accountId: number, 19 | ) { 20 | if ( 21 | config.pop3?.account && 22 | config.pop3?.host && 23 | config.pop3.host && 24 | config.pop3.password && 25 | !config.stop 26 | ) { 27 | this.jobs.push( 28 | new Pop3Job( 29 | typeOrmService, 30 | storageService, 31 | config.address, 32 | config.pop3, 33 | this, 34 | this.accountId, 35 | ), 36 | ); 37 | } 38 | if ( 39 | config.imap4?.account && 40 | config.imap4?.host && 41 | config.imap4.host && 42 | config.imap4.password && 43 | !config.stop 44 | ) { 45 | this.jobs.push( 46 | new Imap4Job( 47 | typeOrmService, 48 | storageService, 49 | config.address, 50 | config.imap4, 51 | this, 52 | this.accountId, 53 | ), 54 | ); 55 | } 56 | } 57 | start() { 58 | this.nextJob()?.start(); 59 | } 60 | 61 | //continue(): void { 62 | // this.currentJob?.continue(); 63 | //} 64 | 65 | nextJob(): IReceiveJob { 66 | if (this.jobs.length === 0) { 67 | this.jobOwner.finishJob(); 68 | return; 69 | } 70 | this.currentJob = this.jobs.pop(); 71 | return this.currentJob; 72 | } 73 | 74 | finishJob(): void { 75 | this.nextJob()?.start(); 76 | } 77 | 78 | emit(event: MailerReceiveEvent): void { 79 | event.mailAddress = this.config.address; 80 | this.jobOwner.emit(event); 81 | } 82 | 83 | abort(): void { 84 | this.currentJob?.abort(); 85 | this.jobs = []; 86 | } 87 | } 88 | -------------------------------------------------------------------------------- /src/magic/query/traverser/filter-result.ts: -------------------------------------------------------------------------------- 1 | import { RxUser } from 'src/entity-interface/RxUser'; 2 | import { QueryResult } from 'src/magic-meta/query/query-result'; 3 | import { QueryEntityMeta } from 'src/magic-meta/query/query.entity-meta'; 4 | import { QueryRootMeta } from 'src/magic-meta/query/query.root-meta'; 5 | 6 | export async function filterResult( 7 | result: QueryResult, 8 | rootMeta: QueryRootMeta, 9 | me: RxUser, 10 | ) { 11 | if (Array.isArray(result.data)) { 12 | for (let i = 0; i < result.data.length; i++) { 13 | result.data[i] = await filterOneInstance(result.data[i], rootMeta, me); 14 | } 15 | } else { 16 | result.data = await filterOneInstance(result.data, rootMeta, me); 17 | } 18 | //进行directive过滤 19 | for (const directive of rootMeta.directives) { 20 | result = await directive.filterResult(result); 21 | } 22 | return result; 23 | } 24 | 25 | async function filterOneInstance( 26 | instance: any, 27 | meta: QueryEntityMeta, 28 | me: RxUser, 29 | parentIntance?: any, 30 | ) { 31 | if (!instance) { 32 | return instance; 33 | } 34 | //基于权限过滤 35 | if (meta.expandFieldForAuth && !(me.isDemo || me.isSupper)) { 36 | const fields = meta.getHasQueryAbilityFields(); 37 | for (const column of meta.entityMeta.columns) { 38 | const fieldName = column.name; 39 | if (!fields.find((field) => field === fieldName) && fieldName != 'id') { 40 | delete instance[fieldName]; 41 | } 42 | } 43 | } 44 | //删除无用的addon relation 45 | for (const addonRelation of meta.addonRelations) { 46 | if ( 47 | !meta.relations.find((relation) => relation.name === addonRelation.name) 48 | ) { 49 | delete instance[addonRelation.name]; 50 | } 51 | } 52 | //进行directive过滤 53 | for (const directive of meta.directives) { 54 | instance = await directive.filterEntity(instance, parentIntance); 55 | } 56 | //递归处理关联 57 | for (const relation of meta.relations) { 58 | const relationInstances = instance[relation.name]; 59 | if (Array.isArray(relationInstances)) { 60 | for (let i = 0; i < relationInstances.length; i++) { 61 | relationInstances[i] = await filterOneInstance( 62 | relationInstances[i], 63 | relation, 64 | me, 65 | instance, 66 | ); 67 | } 68 | } else { 69 | instance[relation.name] = await filterOneInstance( 70 | instance[relation.name], 71 | relation, 72 | me, 73 | instance, 74 | ); 75 | } 76 | } 77 | return instance; 78 | } 79 | -------------------------------------------------------------------------------- /src/directives/query-entity-fake-relation.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { DirectiveType } from 'src/directive/directive-type'; 3 | import { QueryResult } from 'src/magic-meta/query/query-result'; 4 | import { 5 | TOKEN_ENTITY, 6 | TOKEN_LINKE_FIELDS, 7 | TOKEN_RELATION_NAME, 8 | } from 'src/magic/base/tokens'; 9 | 10 | export class QueryEntityFakeRelationDirective extends QueryDirective { 11 | static description = ` 12 | Magic query directive, @fakeRelation. 13 | `; 14 | 15 | static version = '1.0'; 16 | 17 | static directiveType = DirectiveType.QUERY_ENTITY_DIRECTIVE; 18 | 19 | static directiveName = 'fakeRelation'; 20 | 21 | async filterResult(result: QueryResult) { 22 | const queryJSON = {} as any; 23 | let relationEntity = ''; 24 | let relationName = ''; 25 | let linkFields = []; 26 | const dirValue = this.directiveMeta?.value || ({} as any); 27 | for (const key in dirValue) { 28 | const keyStr = key.trim(); 29 | if (keyStr.replace('@', '').trim() === TOKEN_ENTITY) { 30 | relationEntity = dirValue[key]; 31 | queryJSON['entity'] = relationEntity; 32 | } else if (keyStr.startsWith('@')) { 33 | if (keyStr.replace('@', '').trim() === TOKEN_RELATION_NAME) { 34 | relationName = dirValue[key]; 35 | } else if (keyStr.replace('@', '').trim() === TOKEN_LINKE_FIELDS) { 36 | linkFields = dirValue[key]; 37 | } else { 38 | queryJSON[key] = dirValue[key]; 39 | } 40 | } else { 41 | queryJSON[key] = dirValue[key]; 42 | } 43 | } 44 | 45 | if (!relationEntity) { 46 | throw new Error('No entity on directive fakeRelation'); 47 | } 48 | if (!relationName) { 49 | throw new Error('No relationName on directive fakeRelation'); 50 | } 51 | if (!linkFields || !linkFields.length || linkFields.length < 2) { 52 | throw new Error('linkFields error on directive fakeRelation'); 53 | } 54 | 55 | const [rootField, relationField] = linkFields; 56 | 57 | const data = Array.isArray(result.data) ? result.data : [result.data]; 58 | 59 | const linkFieldValues = data 60 | .map((obj) => obj[rootField]) 61 | .filter((vl) => vl !== undefined); 62 | 63 | if (linkFieldValues?.length) { 64 | queryJSON[relationField + '@in'] = linkFieldValues; 65 | const relationResult = await this.magicService.query(queryJSON); 66 | for (const instance of data) { 67 | const linkValue = instance[rootField]; 68 | instance[relationName] = relationResult?.data?.find( 69 | (obj) => obj[relationField] === linkValue, 70 | ); 71 | } 72 | } 73 | 74 | return result; 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/magic/query/magic.query.ts: -------------------------------------------------------------------------------- 1 | import { MagicQueryParser } from './magic.query.parser'; 2 | import { QueryResult } from 'src/magic-meta/query/query-result'; 3 | import { TOKEN_COUNT, TOKEN_GET_MANY } from '../base/tokens'; 4 | import { MagicService } from 'src/magic-meta/magic.service'; 5 | import { AbilityService } from 'src/magic/ability.service'; 6 | import { QueryDirectiveService } from 'src/directive/query-directive.service'; 7 | import { SchemaService } from 'src/schema/schema.service'; 8 | import { EntityManager } from 'typeorm'; 9 | import { makeDirectivesQueryBuilder } from './traverser/make-directives-query-builder'; 10 | import { makeRelationsBuilder } from './traverser/make-relations-builder'; 11 | //import { makeEffectCountQueryBuilder } from './traverser/make-effect-count-query-builder'; 12 | import { filterResult } from './traverser/filter-result'; 13 | import { StorageService } from 'src/storage/storage.service'; 14 | 15 | export class MagicQuery { 16 | constructor( 17 | private readonly entityManager: EntityManager, 18 | private readonly abilityService: AbilityService, 19 | private readonly queryDirectiveService: QueryDirectiveService, 20 | private readonly schemaService: SchemaService, 21 | private readonly storageService: StorageService, 22 | private readonly magicService: MagicService, 23 | ) {} 24 | 25 | async query(json: any) { 26 | let totalCount = 0; 27 | const parser = new MagicQueryParser( 28 | this.queryDirectiveService, 29 | this.schemaService, 30 | this.magicService, 31 | this.abilityService, 32 | this.storageService, 33 | ); 34 | const meta = await parser.parse(json); 35 | const qb = this.entityManager 36 | .getRepository(meta.entity) 37 | .createQueryBuilder(meta.alias); 38 | 39 | makeDirectivesQueryBuilder(meta, qb); 40 | makeRelationsBuilder( 41 | [...meta.relations, ...meta.addonRelations], 42 | qb, 43 | this.magicService.me, 44 | ); 45 | totalCount = await qb.getCount(); 46 | if (meta.fetchString === TOKEN_COUNT) { 47 | return { totalCount, data: [] }; 48 | } 49 | //makeEffectCountQueryBuilder(meta, qb, this.magicService.me); 50 | if (meta.fetchString === TOKEN_GET_MANY) { 51 | //const count = await qb.getCount(); 52 | const LIMIT_COUNT = 1000; 53 | if ( 54 | (meta.maxCount === undefined && totalCount > LIMIT_COUNT) || 55 | meta.maxCount > LIMIT_COUNT 56 | ) { 57 | throw new Error( 58 | 'The result is too large, please use paginate directive', 59 | ); 60 | } 61 | } 62 | //console.debug('SQL:', qb.getSql()); 63 | const data = (await qb[meta.fetchString]()) as any; 64 | const result = 65 | meta.fetchString === TOKEN_GET_MANY 66 | ? ({ data, totalCount } as QueryResult) 67 | : ({ data } as QueryResult); 68 | return await filterResult(result, meta, this.magicService.me); 69 | } 70 | } 71 | -------------------------------------------------------------------------------- /src/magic-meta/query/parse-where-sql.ts: -------------------------------------------------------------------------------- 1 | import { RxUser } from 'src/entity-interface/RxUser'; 2 | import { createId } from 'src/util/create-id'; 3 | import { QueryEntityMeta } from './query.entity-meta'; 4 | 5 | // eslint-disable-next-line @typescript-eslint/no-var-requires 6 | const SqlWhereParser = require('sql-where-parser'); 7 | const OPERATOR_UNARY_MINUS = Symbol('-'); 8 | 9 | const converValue = (value: any) => { 10 | if (typeof value == 'string') { 11 | if (value.toLowerCase() === 'false') { 12 | return false; 13 | } 14 | if (value.toLowerCase() === 'true') { 15 | return true; 16 | } 17 | } 18 | return value; 19 | }; 20 | 21 | export function parseWhereSql( 22 | sql: string, 23 | ownerMeta: QueryEntityMeta, 24 | me: RxUser, 25 | ): [string, any] { 26 | if (!sql) { 27 | throw new Error( 28 | 'Not assign sql statement to where directive or expression', 29 | ); 30 | } 31 | 32 | const parser = new SqlWhereParser(); 33 | const params = {} as any; 34 | const evaluator = (operatorValueOrg, operandsOrg) => { 35 | let operatorValue = operatorValueOrg; 36 | const operands = operandsOrg; 37 | if (operatorValue === OPERATOR_UNARY_MINUS) { 38 | operatorValue = '-'; 39 | } 40 | if (operatorValue === ',') { 41 | return [].concat(operands[0], operands[1]); 42 | } 43 | 44 | if (operatorValue === 'NOT') { 45 | return { not: 'NOT ', value: operands[0] }; 46 | } 47 | 48 | const paramName = `param${createId()}`; 49 | 50 | switch (operatorValue) { 51 | case 'OR': 52 | return `(${operands.join(' OR ')})`; 53 | case 'AND': 54 | return `(${operands.join(' AND ')})`; 55 | case 'IS': 56 | if (operands[1]?.not) { 57 | operatorValue = 'IS NOT'; 58 | operands[1] = operands[1].value; 59 | } 60 | default: 61 | const arr = operands[0]?.split('.'); 62 | let modelAlias = ownerMeta.alias; 63 | if (arr && arr.length > 1) { 64 | const relationName = arr.pop(); 65 | const relation = ownerMeta.findRelationOrFailed(arr.join('.')); 66 | if (relation) { 67 | operands[0] = relationName; 68 | modelAlias = relation.alias; 69 | } 70 | } 71 | operands[0] = `${modelAlias}.${operands[0]}`; 72 | if (operands[1]?.toString()?.startsWith('$me.')) { 73 | const [, columnStr] = (operands[1] as string)?.split('.'); 74 | params[paramName] = me[columnStr]; 75 | } else { 76 | params[paramName] = converValue(operands[1]); 77 | } 78 | 79 | if (operatorValue === 'IN') { 80 | return `${operands[0]} ${operatorValue} (:...${paramName})`; 81 | } 82 | 83 | if (operatorValue === 'LIKE') { 84 | return `LOWER(${operands[0]}) ${operatorValue} LOWER(:${paramName})`; 85 | } 86 | return `${operands[0]} ${operatorValue} :${paramName}`; 87 | } 88 | }; 89 | 90 | const parsed = parser.parse(sql, evaluator); 91 | 92 | return [parsed, params]; 93 | } 94 | -------------------------------------------------------------------------------- /src/util/DirectoryExportedDirectivesLoader.ts: -------------------------------------------------------------------------------- 1 | import { PostDirectiveClass } from 'src/directive/post/post.directive.class'; 2 | import { QueryDirectiveClass } from 'src/directive/query/query.directive.class'; 3 | import { PlatformTools } from 'typeorm/platform/PlatformTools'; 4 | // eslint-disable-next-line @typescript-eslint/no-var-requires 5 | const glob = require('glob'); 6 | 7 | function requireUncached(module) { 8 | delete require.cache[require.resolve(module)]; 9 | return require(module); 10 | } 11 | 12 | /** 13 | * Loads all exported directives from the given directory. 14 | */ 15 | export function importDirectivesFromDirectories( 16 | directories: string[], 17 | formats = ['.js', '.cjs', '.ts'], 18 | // eslint-disable-next-line @typescript-eslint/ban-types 19 | ): QueryDirectiveClass[] | PostDirectiveClass[] { 20 | const logLevel = 'info'; 21 | const classesNotFoundMessage = 22 | 'No classes were found using the provided glob pattern: '; 23 | const classesFoundMessage = 'All classes found using provided glob pattern'; 24 | // eslint-disable-next-line @typescript-eslint/ban-types 25 | function loadFileClasses( 26 | exported: any, 27 | allLoaded: QueryDirectiveClass[] | PostDirectiveClass[], 28 | ) { 29 | if ( 30 | typeof exported === 'function' /*|| exported instanceof EntitySchema*/ 31 | ) { 32 | allLoaded.push(exported); 33 | } else if (Array.isArray(exported)) { 34 | exported.forEach((i: any) => loadFileClasses(i, allLoaded)); 35 | } else if (typeof exported === 'object' && exported !== null) { 36 | Object.keys(exported).forEach((key) => 37 | loadFileClasses(exported[key], allLoaded), 38 | ); 39 | } 40 | return allLoaded; 41 | } 42 | 43 | const allFiles = directories.reduce((allDirs, dir) => { 44 | return allDirs.concat(glob.sync(PlatformTools.pathNormalize(dir))); 45 | }, [] as string[]); 46 | 47 | if (directories.length > 0 && allFiles.length === 0) { 48 | console.debug(logLevel, `${classesNotFoundMessage} "${directories}"`); 49 | } else if (allFiles.length > 0) { 50 | console.debug( 51 | logLevel, 52 | `${classesFoundMessage} "${directories}" : "${allFiles}"`, 53 | ); 54 | } 55 | const dirs = allFiles 56 | .filter((file) => { 57 | const dtsExtension = file.substring(file.length - 5, file.length); 58 | return ( 59 | formats.indexOf(PlatformTools.pathExtname(file)) !== -1 && 60 | dtsExtension !== '.d.ts' 61 | ); 62 | }) 63 | .map((file) => requireUncached(PlatformTools.pathResolve(file))); 64 | 65 | return loadFileClasses(dirs, []); 66 | } 67 | 68 | export function importJsonsFromDirectories( 69 | directories: string[], 70 | format = '.json', 71 | ): any[] { 72 | const allFiles = directories.reduce((allDirs, dir) => { 73 | return allDirs.concat(glob.sync(PlatformTools.pathNormalize(dir))); 74 | }, [] as string[]); 75 | 76 | return allFiles 77 | .filter((file) => PlatformTools.pathExtname(file) === format) 78 | .map((file) => requireUncached(PlatformTools.pathResolve(file))); 79 | } 80 | -------------------------------------------------------------------------------- /src/magic/magic.instance.service.ts: -------------------------------------------------------------------------------- 1 | import { AbilityService } from 'src/magic/ability.service'; 2 | import { DeleteDirectiveService } from 'src/directive/delete-directive.service'; 3 | import { PostDirectiveService } from 'src/directive/post-directive.service'; 4 | import { QueryDirectiveService } from 'src/directive/query-directive.service'; 5 | import { QueryResult } from 'src/magic-meta/query/query-result'; 6 | import { MagicService } from 'src/magic-meta/magic.service'; 7 | import { SchemaService } from 'src/schema/schema.service'; 8 | import { EntityManager } from 'typeorm'; 9 | import { MagicDelete } from './delete/magic.delete'; 10 | import { MagicPost } from './post/magic.post'; 11 | import { MagicQuery } from './query/magic.query'; 12 | import { MagicUpdate } from './update/magic.update'; 13 | import { StorageService } from 'src/storage/storage.service'; 14 | import { MailerSendService } from 'src/mailer/send/mailer.send.service'; 15 | import { RxEventGateway } from 'src/rx-event/rx-event.gateway'; 16 | 17 | /** 18 | * 操作数据库通用类,所有数据库操作都应该通过该类进行,因为该类负责权限控制 19 | * 该类不需要注入,自己管理创建。 20 | * 该类的所有操作都在一个事务创建的EntityManager里 21 | */ 22 | export class MagicInstanceService implements MagicService { 23 | constructor( 24 | private readonly entityManager: EntityManager, 25 | public readonly abilityService: AbilityService, 26 | public readonly queryDirectiveService: QueryDirectiveService, 27 | public readonly postDirectiveService: PostDirectiveService, 28 | public readonly deleteDirectiveService: DeleteDirectiveService, 29 | public readonly schemaService: SchemaService, 30 | public readonly storageService: StorageService, 31 | protected readonly mailerSendService: MailerSendService, 32 | protected readonly rxEventGateway: RxEventGateway, 33 | ) {} 34 | 35 | get me() { 36 | return this.abilityService.me; 37 | } 38 | 39 | async query(json: any): Promise { 40 | return await new MagicQuery( 41 | this.entityManager, 42 | this.abilityService, 43 | this.queryDirectiveService, 44 | this.schemaService, 45 | this.storageService, 46 | this, 47 | ).query(json); 48 | } 49 | 50 | async post(json: any) { 51 | return await new MagicPost( 52 | this.entityManager, 53 | this.abilityService, 54 | this.postDirectiveService, 55 | this.schemaService, 56 | this, 57 | this.mailerSendService, 58 | this.rxEventGateway, 59 | ).post(json); 60 | } 61 | 62 | async delete(json: any) { 63 | return await new MagicDelete( 64 | this.entityManager, 65 | this.abilityService, 66 | this.deleteDirectiveService, 67 | this.schemaService, 68 | this, 69 | this.rxEventGateway, 70 | ).delete(json); 71 | } 72 | 73 | async update(json: any) { 74 | return await new MagicUpdate( 75 | this.entityManager, 76 | this.schemaService, 77 | this.abilityService, 78 | this, 79 | this.rxEventGateway, 80 | ).update(json); 81 | } 82 | 83 | /** 84 | * 拿到该变量,意味着已经脱离了权限控制,请一定不要进行数据库修改操作 85 | */ 86 | getEntityManager() { 87 | return this.entityManager; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/mailer/receive/receive-task.ts: -------------------------------------------------------------------------------- 1 | import { MailConfig } from 'src/entity-interface/MailConfig'; 2 | import { StorageService } from 'src/storage/storage.service'; 3 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 4 | import { EVENT_MAIL_RECEIVING_EVENT } from '../consts'; 5 | import { MailClient, MailerClientsPool } from '../mailer.clients-pool'; 6 | import { IReceiveTasksPool } from './i-receive-tasks-pool'; 7 | import { IReceiveJob } from './i-receive-job'; 8 | import { MailAddressJob } from './mail-address-job'; 9 | import { IReceiveJobOwner } from './i-receive-job-owner'; 10 | import { MailerReceiveEventType, MailerReceiveEvent } from './receive-event'; 11 | 12 | export class ReceiveTask implements IReceiveJobOwner { 13 | lastEvent?: MailerReceiveEvent; 14 | private currentJob: IReceiveJob; 15 | constructor( 16 | private readonly typeOrmService: TypeOrmService, 17 | private readonly storageService: StorageService, 18 | private readonly clientsPool: MailerClientsPool, 19 | private readonly tasksPool: IReceiveTasksPool, 20 | private readonly accountId: number, 21 | private configs: MailConfig[], 22 | ) {} 23 | 24 | nextJob() { 25 | if (this.configs.length > 0) { 26 | this.currentJob = new MailAddressJob( 27 | this.typeOrmService, 28 | this.storageService, 29 | this.configs.pop(), 30 | this, 31 | this.accountId, 32 | ); 33 | return this.currentJob; 34 | } else { 35 | //结束任务 36 | this.tasksPool.removeTask(this.accountId); 37 | this.lastEvent = { 38 | type: MailerReceiveEventType.finished, 39 | }; 40 | this.emitStatusEvent(); 41 | this.lastEvent = undefined; 42 | } 43 | } 44 | 45 | finishJob(): void { 46 | this.nextJob()?.start(); 47 | } 48 | 49 | addConfigs(configs: MailConfig[]) { 50 | for (const config of configs) { 51 | if (!this.configs.find((aConfig) => aConfig.address === config.address)) { 52 | this.configs.push(config); 53 | } 54 | } 55 | } 56 | 57 | start() { 58 | this.nextJob()?.start(); 59 | } 60 | 61 | emit(event: MailerReceiveEvent) { 62 | this.lastEvent = event; 63 | this.emitStatusEvent(); 64 | } 65 | 66 | emitStatusEvent() { 67 | const clients = this.clientsPool.getByAccountId(this.accountId); 68 | for (const client of clients) { 69 | if (client && client.socket.connected) { 70 | this.emitStatusToClient(client); 71 | } 72 | } 73 | } 74 | 75 | emitStatusToClient(client: MailClient) { 76 | if (this.lastEvent) { 77 | client.socket.emit(EVENT_MAIL_RECEIVING_EVENT, this.lastEvent); 78 | } 79 | } 80 | 81 | abort() { 82 | if (this.lastEvent?.type === MailerReceiveEventType.error) { 83 | this.lastEvent = { 84 | type: MailerReceiveEventType.aborted, 85 | }; 86 | this.tasksPool.removeTask(this.accountId); 87 | } else { 88 | this.lastEvent = { 89 | type: MailerReceiveEventType.cancelling, 90 | message: 'Cancelling...', 91 | }; 92 | } 93 | 94 | this.emitStatusEvent(); 95 | this.currentJob?.abort(); 96 | this.configs = []; 97 | } 98 | } 99 | -------------------------------------------------------------------------------- /src/magic-meta/query/query.entity-meta.ts: -------------------------------------------------------------------------------- 1 | import { QueryDirective } from 'src/directive/query/query.directive'; 2 | import { RxAbility } from 'src/entity-interface/RxAbility'; 3 | import { JsonUnit } from 'src/magic/base/json-unit'; 4 | import { EntityMeta } from 'src/schema/graph-meta-interface/entity-meta'; 5 | import { createId } from 'src/util/create-id'; 6 | //import { AddonRelationInfo } from './addon-relation-info'; 7 | import { QueryRelationMeta } from './query.relation-meta'; 8 | 9 | export class QueryEntityMeta { 10 | id: number; 11 | entityMeta: EntityMeta; 12 | relations: QueryRelationMeta[] = []; 13 | //权限或者where sql需要用到的关联 14 | addonRelations: QueryRelationMeta[] = []; 15 | directives: QueryDirective[] = []; 16 | //附加关联用到的字段,如果查询中不包含这些字段,需要在结果中滤除 17 | //addonRelationInfos: AddonRelationInfo[] = []; 18 | //展开,对每个属性进行设置 19 | expandFieldForAuth = false; 20 | //当前登录用户,Entity对应的Abiltity 21 | //根据该数据,可以衍生出方法,进行权限检查 22 | abilities: RxAbility[] = []; 23 | 24 | //关联条件,如user.id 25 | relationConditions: JsonUnit[] = []; 26 | 27 | maxCount?: number; 28 | 29 | constructor() { 30 | this.id = createId(); 31 | } 32 | 33 | get entity() { 34 | return this.entityMeta.name; 35 | } 36 | 37 | get alias() { 38 | return this.entityMeta.name?.toLowerCase() + this.id; 39 | } 40 | 41 | getHasQueryAbilityFields() { 42 | return this.abilities 43 | .filter((ability) => ability.columnUuid) 44 | .map((ability) => { 45 | return this.entityMeta.columns.find( 46 | (column) => column.uuid === ability.columnUuid, 47 | ).name; 48 | }); 49 | } 50 | 51 | addAddOnRelation(relationMeta: QueryRelationMeta) { 52 | const foundRelation = this.addonRelations.find( 53 | (relation) => relation.name === relationMeta.name, 54 | ); 55 | //避免重复添加 56 | if (!foundRelation) { 57 | this.addonRelations.push(relationMeta); 58 | } 59 | } 60 | 61 | pushDirective(directive: QueryDirective) { 62 | this.directives.push(directive); 63 | } 64 | 65 | findRelationOnce(relationName: string): QueryRelationMeta { 66 | for (const relationMeta of [...this.relations, ...this.addonRelations]) { 67 | if (relationMeta.name === relationName) { 68 | return relationMeta; 69 | } 70 | } 71 | } 72 | 73 | findRelation(relationString: string): QueryRelationMeta { 74 | const [relationName, ...leftStrArr] = relationString.split('.'); 75 | const leftString = leftStrArr.join('.'); 76 | const relation = this.findRelationOnce(relationName); 77 | if (relation) { 78 | if (leftString) { 79 | return relation.findRelation(leftString); 80 | } 81 | return relation; 82 | } 83 | } 84 | 85 | /** 86 | * 87 | * @param relationString 格式relationName.otherName.otherName 88 | * @returns 89 | */ 90 | findRelationOrFailed(relationString: string): QueryRelationMeta { 91 | const relation = this.findRelation(relationString); 92 | if (relation) { 93 | return relation; 94 | } 95 | throw new Error( 96 | `Please add relation ${relationString} of ${this.entity} to query meta`, 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/install/install.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, Logger } from '@nestjs/common'; 2 | import { RxUser } from 'src/entity-interface/RxUser'; 3 | import { PackageManageService } from 'src/package-manage/package-manage.service'; 4 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 5 | import { DB_CONFIG_FILE, SALT_OR_ROUNDS } from 'src/util/consts'; 6 | import { EntitySchema } from 'typeorm'; 7 | import { PlatformTools } from 'typeorm/platform/PlatformTools'; 8 | import * as bcrypt from 'bcrypt'; 9 | import { InstallData } from './install.data'; 10 | import * as packagesFromJson from './install.seed.json'; 11 | import { RxPackage } from 'src/entity-interface/RxPackage'; 12 | import { RxUserStatus } from 'src/entity-interface/RxUserStatus'; 13 | 14 | const packageSeed = packagesFromJson as RxPackage; 15 | 16 | export const CONNECTION_WITH_SCHEMA_NAME = 'withSchema'; 17 | 18 | @Injectable() 19 | export class InstallService { 20 | private readonly _logger = new Logger('TypeOrmWithSchemaService'); 21 | private _entitySchemas = new Map(); 22 | 23 | constructor( 24 | private readonly typeormSerivce: TypeOrmService, 25 | private readonly packageManage: PackageManageService, 26 | ) {} 27 | 28 | public async install(data: InstallData) { 29 | const dbConfigData = { 30 | type: data.type, 31 | host: data.host, 32 | port: data.port, 33 | database: data.database, 34 | username: data.username, 35 | password: data.password, 36 | }; 37 | 38 | //创建配置文件 39 | await PlatformTools.writeFile( 40 | DB_CONFIG_FILE, 41 | JSON.stringify(dbConfigData, null, 2), 42 | ); 43 | 44 | //发布系统包:生成用于构建Schema的文件并重启连接 45 | await this.packageManage.publishPackages([packageSeed]); 46 | 47 | //把系统包保存到数据库RxPackage表,保存后可以从前端读取并编辑 48 | await this.packageManage.savePackage(packageSeed); 49 | 50 | //创建超级管理员账号 51 | await this.createAccount({ 52 | name: 'Admin', 53 | loginName: data.admin, 54 | password: await bcrypt.hash(data.adminPassword, SALT_OR_ROUNDS), 55 | isSupper: true, 56 | status: RxUserStatus.NORMAL, 57 | }); 58 | 59 | //创建演示账号 60 | if (data.withDemo) { 61 | await this.createAccount({ 62 | name: 'Demo', 63 | loginName: 'demo', 64 | password: await bcrypt.hash('demo', SALT_OR_ROUNDS), 65 | isDemo: true, 66 | status: RxUserStatus.NORMAL, 67 | }); 68 | } 69 | 70 | return { 71 | success: true, 72 | }; 73 | } 74 | 75 | public async isInstalled() { 76 | return { installed: PlatformTools.fileExist(DB_CONFIG_FILE) }; 77 | } 78 | 79 | private async createAccount(user: RxUser) { 80 | if (!this.typeormSerivce.connection) { 81 | throw new Error('Install failed: null connection'); 82 | } 83 | const repository = this.typeormSerivce.getRepository('RxUser'); 84 | let userWillBeSave = (await repository 85 | .createQueryBuilder('user') 86 | .addSelect('user.password') 87 | .where({ loginName: user.name }) 88 | .getOne()) as RxUser; 89 | if (userWillBeSave) { 90 | for (const key in user) { 91 | userWillBeSave[key] = user[key]; 92 | } 93 | } else { 94 | userWillBeSave = user; 95 | } 96 | 97 | await repository.save(userWillBeSave); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "rx-models", 3 | "private": true, 4 | "version": "1.0.0", 5 | "description": "Models Server", 6 | "license": "MIT", 7 | "scripts": { 8 | "prebuild": "rimraf dist", 9 | "build": "nest build", 10 | "format": "prettier --write \"src/**/*.ts\" \"test/**/*.ts\"", 11 | "start": "nest start", 12 | "start:dev": "nest start --watch", 13 | "start:debug": "nest start --debug --watch", 14 | "start:prod": "node dist/main", 15 | "lint": "eslint \"{src,apps,libs,test}/**/*.ts\" --fix", 16 | "test": "jest", 17 | "test:watch": "jest --watch", 18 | "test:cov": "jest --coverage", 19 | "test:debug": "node --inspect-brk -r tsconfig-paths/register -r ts-node/register node_modules/.bin/jest --runInBand", 20 | "test:e2e": "jest --config ./test/jest-e2e.json", 21 | "typeorm": "ts-node -r tsconfig-paths/register ./node_modules/typeorm/cli.js" 22 | }, 23 | "dependencies": { 24 | "@nestjs/common": "^8.0.6", 25 | "@nestjs/core": "^8.0.6", 26 | "@nestjs/jwt": "^8.0.0", 27 | "@nestjs/passport": "^8.0.1", 28 | "@nestjs/platform-express": "^8.0.6", 29 | "@nestjs/platform-socket.io": "^8.0.6", 30 | "@nestjs/websockets": "^8.0.6", 31 | "ali-oss": "^6.16.0", 32 | "bcrypt": "^5.0.1", 33 | "crypto-js": "^4.1.1", 34 | "es6-symbol": "^3.1.3", 35 | "glob": "^7.1.7", 36 | "imap": "^0.8.19", 37 | "lodash": "^4.17.21", 38 | "mailparser": "^3.3.0", 39 | "mysql2": "^2.2.5", 40 | "nodemailer": "^6.6.5", 41 | "nodemailer-plugin-inline-base64": "^2.1.1", 42 | "passport": "^0.4.1", 43 | "passport-jwt": "^4.0.0", 44 | "passport-local": "^1.0.0", 45 | "reflect-metadata": "^0.1.13", 46 | "rimraf": "^3.0.2", 47 | "rxjs": "^7.3.0", 48 | "sharp": "^0.28.3", 49 | "sql-where-parser": "^2.2.1", 50 | "striptags": "^3.2.0", 51 | "tokenize-this": "^1.4.2", 52 | "typeorm": "^0.2.32", 53 | "uuid": "^8.3.2" 54 | }, 55 | "devDependencies": { 56 | "@nestjs/cli": "^7.6.0", 57 | "@nestjs/schematics": "^7.3.1", 58 | "@nestjs/testing": "^7.6.15", 59 | "@types/bcrypt": "^5.0.0", 60 | "@types/express": "^4.17.11", 61 | "@types/jest": "^26.0.22", 62 | "@types/lodash": "^4.14.172", 63 | "@types/multer": "^1.4.5", 64 | "@types/node": "^14.14.36", 65 | "@types/passport-jwt": "^3.0.5", 66 | "@types/passport-local": "^1.0.33", 67 | "@types/sharp": "^0.28.3", 68 | "@types/supertest": "^2.0.10", 69 | "@types/uuid": "^8.3.1", 70 | "@typescript-eslint/eslint-plugin": "^4.19.0", 71 | "@typescript-eslint/parser": "^4.19.0", 72 | "eslint": "^7.22.0", 73 | "eslint-config-prettier": "^8.1.0", 74 | "eslint-plugin-prettier": "^3.3.1", 75 | "jest": "^26.6.3", 76 | "prettier": "^2.2.1", 77 | "supertest": "^6.1.3", 78 | "ts-jest": "^26.5.4", 79 | "ts-loader": "^8.0.18", 80 | "ts-node": "^9.1.1", 81 | "tsconfig-paths": "^3.9.0", 82 | "typescript": "^4.2.3" 83 | }, 84 | "jest": { 85 | "moduleFileExtensions": [ 86 | "js", 87 | "json", 88 | "ts" 89 | ], 90 | "rootDir": "src", 91 | "testRegex": ".*\\.spec\\.ts$", 92 | "transform": { 93 | "^.+\\.(t|j)s$": "ts-jest" 94 | }, 95 | "collectCoverageFrom": [ 96 | "**/*.(t|j)s" 97 | ], 98 | "coverageDirectory": "../coverage", 99 | "testEnvironment": "node" 100 | } 101 | } 102 | -------------------------------------------------------------------------------- /src/mailer/send/send-task.ts: -------------------------------------------------------------------------------- 1 | import { Mail } from 'src/entity-interface/Mail'; 2 | import { SendStatus } from 'src/entity-interface/SendStatus'; 3 | import { StorageService } from 'src/storage/storage.service'; 4 | import { EntityManager } from 'typeorm'; 5 | import { EVENT_MAIL_SENDING_EVENT } from '../consts'; 6 | import { MailerClientsPool } from '../mailer.clients-pool'; 7 | import { ISendJob } from './i-send-job'; 8 | import { ISendJobOwner } from './i-send-job-owner'; 9 | import { ISendTasksPool } from './i-send-tasks-pool'; 10 | import { MailerSendEvent, MailerSendEventType } from './send-event'; 11 | import { SendJob } from './send-job'; 12 | 13 | /** 14 | * 一个Task对应一个账号 15 | */ 16 | export class SendTask implements ISendJobOwner { 17 | private currentJob: ISendJob; 18 | private aborted = false; 19 | private errorJobs: ISendJob[] = []; 20 | constructor( 21 | private readonly entityManger: EntityManager, 22 | private readonly storageService: StorageService, 23 | private readonly clientsPool: MailerClientsPool, 24 | private readonly tasksPool: ISendTasksPool, 25 | private readonly accountId: number, 26 | private readonly mails: Mail[], 27 | ) {} 28 | 29 | onErrorJob(job: ISendJob): void { 30 | this.errorJobs.push(job); 31 | } 32 | 33 | emit(event: MailerSendEvent): void { 34 | const clients = this.clientsPool.getByAccountId(this.accountId); 35 | for (const client of clients) { 36 | if (client && client.socket.connected) { 37 | client.socket.emit(EVENT_MAIL_SENDING_EVENT, event); 38 | } 39 | } 40 | } 41 | 42 | onQueueChange(): void { 43 | const queue = this.currentJob ? [this.currentJob.toQueueItem()] : []; 44 | for (const errorJob of this.errorJobs) { 45 | queue.push(errorJob.toQueueItem()); 46 | } 47 | 48 | for (const waitingMail of this.mails) { 49 | queue.push({ 50 | mailId: waitingMail.id, 51 | mailSubject: waitingMail.subject, 52 | status: SendStatus.WAITING, 53 | canCancel: true, 54 | }); 55 | } 56 | 57 | this.emit({ 58 | type: MailerSendEventType.sendQueue, 59 | mailsQueue: queue, 60 | }); 61 | } 62 | 63 | nextJob(): ISendJob | undefined { 64 | if (this.mails.length) { 65 | this.currentJob = new SendJob( 66 | this.entityManger, 67 | this.storageService, 68 | this, 69 | this.mails.pop(), 70 | ); 71 | return this.currentJob; 72 | } else { 73 | //结束任务 74 | this.tasksPool.removeTask(this.accountId); 75 | this.currentJob = undefined; 76 | } 77 | } 78 | 79 | finishJob(): void { 80 | this.nextJob()?.start(); 81 | } 82 | 83 | addMail(mail: Mail) { 84 | this.mails.push(mail); 85 | } 86 | 87 | async start() { 88 | await this.nextJob()?.start(); 89 | } 90 | 91 | abort() { 92 | /* if (this.lastEvent?.type === MailerEventType.error) { 93 | this.lastEvent = { 94 | type: MailerEventType.aborted, 95 | }; 96 | this.tasksPool.removeTask(this.accountId); 97 | } else { 98 | this.lastEvent = { 99 | type: MailerEventType.cancelling, 100 | message: 'Cancelling...', 101 | }; 102 | }*/ 103 | 104 | //this.emitStatusEvent(); 105 | this.currentJob?.abort(); 106 | this.mails.length = 0; 107 | this.currentJob = undefined; 108 | this.aborted = true; 109 | this.onQueueChange(); 110 | } 111 | } 112 | -------------------------------------------------------------------------------- /src/magic/ability.service.ts: -------------------------------------------------------------------------------- 1 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 2 | import { SchemaService } from 'src/schema/schema.service'; 3 | import { RxUser } from 'src/entity-interface/RxUser'; 4 | import { RxAbility } from 'src/entity-interface/RxAbility'; 5 | import { RxEntityAuthSettings } from 'src/entity-interface/RxEntityAuthSettings'; 6 | import { HttpException } from '@nestjs/common'; 7 | import { AbilityType } from 'src/entity-interface/AbilityType'; 8 | 9 | export class AbilityService { 10 | constructor( 11 | public readonly me: RxUser, 12 | private readonly typeormSerivce: TypeOrmService, 13 | private readonly schemaService: SchemaService, 14 | ) {} 15 | 16 | async isEntityExpand(entityUuid: string) { 17 | return ( 18 | await this.typeormSerivce 19 | .getRepository('RxEntityAuthSettings') 20 | .findOne({ entityUuid }) 21 | )?.expand; 22 | } 23 | 24 | async getEntityQueryAbilities(entityUuid: string) { 25 | const user = this.me; 26 | //console.debug('Read权限筛查用户:', user.name); 27 | this.loginCheck(user); 28 | if (user.isSupper || user.isDemo) { 29 | return []; 30 | } 31 | return await this.typeormSerivce 32 | .getRepository('RxAbility') 33 | .createQueryBuilder('rxability') 34 | .leftJoinAndSelect('rxability.role', 'role') 35 | .where( 36 | `rxability.entityUuid=:entityUuid 37 | and rxability.abilityType = '${AbilityType.READ}' 38 | and role.id IN (:...roleIds) 39 | `, 40 | { 41 | entityUuid, 42 | roleIds: user.roles?.map((role) => role.id) || [], 43 | }, 44 | ) 45 | .getMany(); 46 | } 47 | 48 | async getEntityPostAbilities(entityUuid: string) { 49 | const user = this.me; 50 | console.debug('Post权限筛查用户:', user.name); 51 | this.loginCheck(user); 52 | if (user.isSupper || user.isDemo) { 53 | return []; 54 | } 55 | return await this.typeormSerivce 56 | .getRepository('RxAbility') 57 | .createQueryBuilder('rxability') 58 | .leftJoinAndSelect('rxability.role', 'role') 59 | .where( 60 | `rxability.entityUuid=:entityUuid 61 | AND (rxability.abilityType = '${AbilityType.CREATE}' OR rxability.abilityType = '${AbilityType.UPDATE}') 62 | AND role.id IN (:...roleIds) 63 | `, 64 | { 65 | entityUuid, 66 | roleIds: user.roles?.map((role) => role.id) || [], 67 | }, 68 | ) 69 | .getMany(); 70 | } 71 | 72 | async getEntityDeleteAbilities(entityUuid: string) { 73 | const user = this.me; 74 | console.debug('Delete权限筛查用户:', user.name); 75 | this.loginCheck(user); 76 | if (user.isSupper || user.isDemo) { 77 | return []; 78 | } 79 | return await this.typeormSerivce 80 | .getRepository('RxAbility') 81 | .createQueryBuilder('rxability') 82 | .leftJoinAndSelect('rxability.role', 'role') 83 | .where( 84 | `rxability.entityUuid=:entityUuid 85 | AND rxability.abilityType = '${AbilityType.DELETE}' 86 | AND role.id IN (:...roleIds) 87 | `, 88 | { 89 | entityUuid, 90 | roleIds: user.roles?.map((role) => role.id) || [], 91 | }, 92 | ) 93 | .getMany(); 94 | } 95 | 96 | private loginCheck(user: RxUser) { 97 | if (!user) { 98 | throw new HttpException( 99 | { 100 | status: 401, 101 | error: 'Please login first!', 102 | }, 103 | 401, 104 | ); 105 | } 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/mailer/mailer.test-service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@nestjs/common'; 2 | import { MailReceiveConfig } from 'src/entity-interface/MailReceiveConfig'; 3 | import { SmtpConfig } from 'src/entity-interface/SmtpConfig'; 4 | import { CRYPTO_KEY, DEFAULT_TIME_OUT } from 'src/util/consts'; 5 | import { decypt } from 'src/util/cropt-js'; 6 | import { POP3Client } from './receive/poplib'; 7 | // eslint-disable-next-line @typescript-eslint/no-var-requires 8 | const nodemailer = require('nodemailer'); 9 | // eslint-disable-next-line @typescript-eslint/no-var-requires 10 | const Imap = require('imap'); 11 | 12 | export interface TestResult { 13 | status: boolean; 14 | message?: string; 15 | } 16 | 17 | @Injectable() 18 | export class MailerTestService { 19 | testPOP3(config: MailReceiveConfig): Promise { 20 | return new Promise((resolve) => { 21 | let connecting = true; 22 | setTimeout(() => { 23 | if (connecting) { 24 | console.debug('Connect time out'); 25 | resolve({ status: false, message: 'Connect time out' }); 26 | } 27 | }, (config.timeout || DEFAULT_TIME_OUT) * 1000); 28 | 29 | const client = new POP3Client(config.port, config.host, { 30 | tlserrs: false, 31 | enabletls: config.ssl, 32 | debug: false, 33 | }); 34 | 35 | client.on('connect', (data) => { 36 | connecting = false; 37 | console.debug('connect:', data); 38 | client.login(config.account, decypt(config.password, CRYPTO_KEY)); 39 | }); 40 | 41 | client.on('login', (status, rawdata) => { 42 | connecting = false; 43 | resolve({ status: status, message: rawdata }); 44 | }); 45 | 46 | client.on('error', (err) => { 47 | connecting = false; 48 | console.debug(err.toString() + ' errno:' + err.errno); 49 | resolve({ status: false, message: err.toString() }); 50 | }); 51 | }); 52 | } 53 | 54 | testSMTP(config: SmtpConfig): Promise { 55 | return new Promise((resolve) => { 56 | const option = { 57 | host: config.host, 58 | port: config.port, 59 | secure: config.useSSL, // true for 465, false for other ports 60 | //service: 'Hotmail', 61 | //secureConnection: false, // use SSL 62 | requiresAuth: config.requiresAuth, 63 | ignoreTLS: !config.requireTLS || false, 64 | requireTLS: config.requireTLS || false, 65 | connectionTimeout: config.timeout * 1000, //单位是毫秒 66 | auth: { 67 | user: config.account, 68 | pass: decypt(config.password, CRYPTO_KEY), 69 | }, 70 | }; 71 | 72 | const transporter = nodemailer.createTransport(option); 73 | 74 | transporter.verify(function (error) { 75 | if (error) { 76 | console.debug('Test Smtp Error', error); 77 | resolve({ status: false, message: error.toString() }); 78 | } else { 79 | resolve({ status: true }); 80 | } 81 | }); 82 | }); 83 | } 84 | 85 | testIMAP4(config: MailReceiveConfig): Promise { 86 | return new Promise((resolve) => { 87 | const client = new Imap({ 88 | user: config.account, 89 | password: decypt(config.password, CRYPTO_KEY), 90 | host: config.host, 91 | port: config.port, 92 | tls: config.ssl, 93 | connTimeout: config.timeout * 1000, 94 | //debug: console.error, 95 | }); 96 | client.connect(); 97 | client.once('ready', () => { 98 | resolve({ status: true }); 99 | client.destroy(); 100 | }); 101 | 102 | client.once('error', (error) => { 103 | console.debug('Test imap Error', error); 104 | resolve({ status: false, message: error.toString() }); 105 | client.destroy(); 106 | }); 107 | }); 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/storage/aliyun/AliyunClient.ts: -------------------------------------------------------------------------------- 1 | import { ImageSize } from 'src/util/consts'; 2 | import { StorageClient } from '../storage.client'; 3 | import { expaireTime } from './consts'; 4 | import { urlCache } from './UrlCache'; 5 | 6 | // eslint-disable-next-line @typescript-eslint/no-var-requires 7 | const OSS = require('ali-oss'); 8 | // eslint-disable-next-line @typescript-eslint/no-var-requires 9 | //const { STS } = require('ali-oss'); 10 | 11 | export class AliyunClient implements StorageClient { 12 | client: any; 13 | constructor(aliyunConfig: any) { 14 | this.client = new OSS(aliyunConfig); 15 | } 16 | async checkAndCreateBucket(bucket: string) { 17 | try { 18 | return await this.client.getBucketInfo(bucket); 19 | } catch (error) { 20 | // 指定的存储空间不存在。 21 | if (error.name === 'NoSuchBucketError' || error.code === 'NoSuchBucket') { 22 | return await this.createBucket(bucket); 23 | } else { 24 | throw error; 25 | } 26 | } 27 | } 28 | 29 | async createBucket(bucket: string) { 30 | const options = { 31 | storageClass: 'Standard', // 存储空间的默认存储类型为标准存储,即Standard。如果需要设置存储空间的存储类型为归档存储,请替换为Archive。 32 | acl: 'private', // 存储空间的默认读写权限为私有,即private。如果需要设置存储空间的读写权限为公共读,请替换为public-read。 33 | dataRedundancyType: 'LRS', // 存储空间的默认数据容灾类型为本地冗余存储,即LRS。如果需要设置数据容灾类型为同城冗余存储,请替换为ZRS。 34 | }; 35 | // 填写Bucket名称。 36 | return await this.client.putBucket(bucket, options); 37 | } 38 | 39 | async putFileData(name: string, data: any, bucket: string) { 40 | this.client.useBucket(bucket); 41 | return await this.client.put(name, Buffer.from(data)); 42 | } 43 | 44 | async putFile(name: string, file: Express.Multer.File, bucket: string) { 45 | this.client.useBucket(bucket); 46 | return await this.client.put(name, file.buffer); 47 | } 48 | 49 | async resizeImage(path: string, bucket: string, size?: ImageSize) { 50 | const urlInfo = urlCache.getUrlInfo(path, bucket, size); 51 | if (urlInfo) { 52 | return urlInfo.url; 53 | } 54 | this.client.useBucket(bucket); 55 | const url = await this.client.signatureUrl(path, { 56 | expires: expaireTime, 57 | method: 'GET', 58 | process: size 59 | ? `image/resize,w_${size.width},h_${size.height}` 60 | : undefined, 61 | }); 62 | urlCache.addUrl({ 63 | path: path, 64 | bucket: bucket, 65 | size: size, 66 | time: new Date(), 67 | url: url, 68 | }); 69 | return url; 70 | } 71 | 72 | async fileLocalPath(path: string, bucket: string) { 73 | const urlInfo = urlCache.getUrlInfo(path, bucket); 74 | if (urlInfo) { 75 | return urlInfo.url; 76 | } 77 | this.client.useBucket(bucket); 78 | const url = await this.client.signatureUrl(path, { 79 | expires: expaireTime, 80 | method: 'GET', 81 | process: undefined, 82 | }); 83 | urlCache.addUrl({ 84 | path: path, 85 | bucket: bucket, 86 | time: new Date(), 87 | url: url, 88 | }); 89 | return url; 90 | } 91 | 92 | async fileUrl(path: string, bucket: string) { 93 | return await this.fileLocalPath(path, bucket); 94 | } 95 | //客户端上传OSS用的TOKEN,本方法暂时没用 96 | /* async creatUploadsOperateToken() { 97 | await this.checkAndCreateBucket(FOLDER_UPLOADS); 98 | const client = new STS({ 99 | accessKeyId: aliyunConfig.accessKeyId, 100 | accessKeySecret: aliyunConfig.accessKeySecret, 101 | }); 102 | 103 | const result = await client.assumeRole( 104 | stsConfig.roleArn, 105 | stsConfig.policy, 106 | stsConfig.tokenExpireTime, 107 | ); 108 | return { 109 | AccessKeyId: result.credentials.AccessKeyId, 110 | AccessKeySecret: result.credentials.AccessKeySecret, 111 | SecurityToken: result.credentials.SecurityToken, 112 | Expiration: result.credentials.Expiration, 113 | }; 114 | }*/ 115 | } 116 | -------------------------------------------------------------------------------- /src/storage/disk/DiskClient.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DISK_STORAGE_PATH, 3 | DISK_STORAGE_PUBLIC_PATH, 4 | DISK_STORAGE_PUBLIC_URL_BASE, 5 | ImageSize, 6 | } from 'src/util/consts'; 7 | import { PlatformTools } from 'typeorm/platform/PlatformTools'; 8 | import { StorageClient } from '../storage.client'; 9 | import { dirname, parse, extname } from 'path'; 10 | import * as sharp from 'sharp'; 11 | // eslint-disable-next-line @typescript-eslint/no-var-requires 12 | const fs = require('fs'); 13 | 14 | export class DiskClient implements StorageClient { 15 | private host: string; 16 | 17 | setHost(host: string) { 18 | this.host = host; 19 | } 20 | async checkAndCreateBucket(bucket: string) { 21 | const folderName = DISK_STORAGE_PATH + bucket; 22 | await this.checkAndCreateDir(folderName); 23 | } 24 | 25 | private async checkAndCreateDir(dir: string) { 26 | try { 27 | if (!fs.existsSync(dir)) { 28 | await fs.mkdirSync(dir, { recursive: true }); 29 | } 30 | } catch (error) { 31 | throw error; 32 | } 33 | } 34 | 35 | async putFileData(name: string, data: any, bucket: string) { 36 | const fileName = DISK_STORAGE_PATH + bucket + '/' + name; 37 | await this.checkAndCreateDir(dirname(fileName)); 38 | await PlatformTools.writeFile(fileName, data); 39 | } 40 | 41 | async putFile(name: string, file: Express.Multer.File, bucket: string) { 42 | const fileName = DISK_STORAGE_PATH + bucket + '/' + name; 43 | await this.checkAndCreateDir(dirname(fileName)); 44 | await PlatformTools.writeFile(fileName, file.buffer); 45 | } 46 | 47 | async resizeImage(path: string, bucket: string, size?: ImageSize) { 48 | const fileName = DISK_STORAGE_PATH + bucket + '/' + path; 49 | if (size) { 50 | path = 51 | parse(path)?.name + `-${size.width}x${size.height}` + extname(path); 52 | } 53 | 54 | const nameWithBucket = bucket + '/' + path; 55 | const publicStoragePath = DISK_STORAGE_PUBLIC_PATH + nameWithBucket; 56 | const publicFileUrl = 57 | this.host + DISK_STORAGE_PUBLIC_URL_BASE + nameWithBucket; 58 | 59 | await this.checkAndCreateDir(dirname(publicStoragePath)); 60 | if (PlatformTools.fileExist(publicStoragePath)) { 61 | return publicFileUrl; 62 | } 63 | 64 | if (PlatformTools.fileExist(fileName)) { 65 | const extName = extname(fileName).replace('.', '').toLowerCase(); 66 | if ( 67 | extName === 'jpg' || 68 | extName === 'jpeg' || 69 | extName === 'png' || 70 | extName === 'gif' 71 | ) { 72 | const srp = sharp(fileName); 73 | if (size) { 74 | srp.resize(size.width, size.height); 75 | } 76 | srp.toFile(publicStoragePath, (err, info) => { 77 | console.debug('Resize Success', info); 78 | if (err) { 79 | console.error('Resize Error', err); 80 | } 81 | }); 82 | return publicFileUrl; 83 | } 84 | } 85 | return ''; 86 | } 87 | 88 | async fileLocalPath(path: string, bucket: string) { 89 | const nameWithBucket = bucket + '/' + path; 90 | 91 | return DISK_STORAGE_PATH + nameWithBucket; 92 | } 93 | 94 | async fileUrl(path: string, bucket: string) { 95 | const fileName = DISK_STORAGE_PATH + bucket + '/' + path; 96 | 97 | const nameWithBucket = bucket + '/' + path; 98 | const publicStoragePath = DISK_STORAGE_PUBLIC_PATH + nameWithBucket; 99 | const publicFileUrl = 100 | this.host + DISK_STORAGE_PUBLIC_URL_BASE + nameWithBucket; 101 | 102 | await this.checkAndCreateDir(dirname(publicStoragePath)); 103 | if (PlatformTools.fileExist(publicStoragePath)) { 104 | return publicFileUrl; 105 | } 106 | 107 | if (PlatformTools.fileExist(fileName)) { 108 | fs.writeFileSync(publicStoragePath, fs.readFileSync(fileName)); 109 | return publicFileUrl; 110 | } 111 | return ''; 112 | } 113 | } 114 | -------------------------------------------------------------------------------- /src/storage/storage.service.ts: -------------------------------------------------------------------------------- 1 | import { forwardRef, Inject, Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { AliyunConfig } from 'src/entity-interface/AliyunConfig'; 3 | import { EntityRxConfig, RxConfig } from 'src/entity-interface/RxConfig'; 4 | import { RxStorageType } from 'src/entity-interface/RxStorageType'; 5 | import { RxBaseService } from 'src/rxbase/rxbase.service'; 6 | import { TypeOrmService } from 'src/typeorm/typeorm.service'; 7 | import { CONFIG_KEY_STORAGE, BUCKET_UPLOADS, ImageSize } from 'src/util/consts'; 8 | import { AliyunClient } from './aliyun/AliyunClient'; 9 | import { DiskClient } from './disk/DiskClient'; 10 | import { StorageClient } from './storage.client'; 11 | 12 | type StorageConfig = { type: RxStorageType } & AliyunConfig; 13 | 14 | @Injectable() 15 | export class StorageService implements OnModuleInit { 16 | private inited = false; 17 | private storageClient: StorageClient; 18 | private storageType: RxStorageType = RxStorageType.Disk; 19 | constructor( 20 | @Inject(forwardRef(() => TypeOrmService)) 21 | private readonly typeOrmService: TypeOrmService, 22 | private readonly baseService: RxBaseService, 23 | ) { 24 | this.storageClient = new DiskClient(); 25 | } 26 | 27 | async onModuleInit() { 28 | if (!this.typeOrmService.connection) { 29 | this.inited = false; 30 | return; 31 | } 32 | this.inited = true; 33 | //await this.createConnection(); 34 | const repository = 35 | this.typeOrmService.connection.getRepository(EntityRxConfig); 36 | const rxConfig = await repository.findOne({ 37 | name: CONFIG_KEY_STORAGE, 38 | }); 39 | if (!rxConfig) { 40 | return; 41 | } 42 | const storageConfig = rxConfig.value as StorageConfig; 43 | const { type: storageType, ...aliyunConfig } = storageConfig || {}; 44 | this.storageType = storageType; 45 | if (storageType === RxStorageType.Disk) { 46 | return; 47 | } 48 | 49 | if (storageType === RxStorageType.AliyunOSS) { 50 | this.storageClient = new AliyunClient(aliyunConfig); 51 | } 52 | console.debug('StorageService initializated'); 53 | } 54 | 55 | async checkAndCreateBucket(bucket: string) { 56 | if (!this.inited) { 57 | await this.onModuleInit(); 58 | } 59 | return await this.storageClient.checkAndCreateBucket(bucket); 60 | } 61 | 62 | async putFileData(name: string, data: any, bucket: string) { 63 | if (!this.inited) { 64 | await this.onModuleInit(); 65 | } 66 | return await this.storageClient.putFileData(name, data, bucket); 67 | } 68 | 69 | async putFile(name: string, file: Express.Multer.File, bucket: string) { 70 | if (!this.inited) { 71 | await this.onModuleInit(); 72 | } 73 | return await this.storageClient.putFile(name, file, bucket); 74 | } 75 | 76 | /*async getTokenObject() { 77 | return await this.storageClient.creatUploadsOperateToken(); 78 | }*/ 79 | 80 | async resizeImage(path: string, size?: ImageSize) { 81 | if (!this.inited) { 82 | await this.onModuleInit(); 83 | } 84 | this.setHost(); 85 | return await this.storageClient.resizeImage(path, BUCKET_UPLOADS, size); 86 | } 87 | 88 | async fileLocalPath(path: string, bucket: string) { 89 | if (!this.inited) { 90 | await this.onModuleInit(); 91 | } 92 | this.setHost(); 93 | return await this.storageClient.fileLocalPath( 94 | path, 95 | bucket || BUCKET_UPLOADS, 96 | ); 97 | } 98 | 99 | async fileUrl(path: string, bucket?: string) { 100 | if (!this.inited) { 101 | await this.onModuleInit(); 102 | } 103 | this.setHost(); 104 | return await this.storageClient.fileUrl( 105 | path, 106 | bucket ? bucket : BUCKET_UPLOADS, 107 | ); 108 | } 109 | 110 | private setHost() { 111 | if (this.storageType === RxStorageType.Disk) { 112 | (this.storageClient as unknown as DiskClient).setHost( 113 | this.baseService.getHost(), 114 | ); 115 | } 116 | } 117 | } 118 | -------------------------------------------------------------------------------- /src/directive/directive.storage.ts: -------------------------------------------------------------------------------- 1 | import { Injectable, OnModuleInit } from '@nestjs/common'; 2 | import { importDirectivesFromDirectories } from 'src/util/DirectoryExportedDirectivesLoader'; 3 | import { DirectiveType } from './directive-type'; 4 | import { DeleteDirectiveClass } from './delete/delete.directive.class'; 5 | import { PostDirectiveClass } from './post/post.directive.class'; 6 | import { QueryDirectiveClass } from './query/query.directive.class'; 7 | import { QueryConditionDirectiveClass } from './query/query.condition-directive-class'; 8 | import { QueryRelationDirectiveClass } from './query/query.relation-directive-class'; 9 | import { QueryFieldDirectiveClass } from './query/query.field-directive.class'; 10 | 11 | @Injectable() 12 | export class DirectiveStorage implements OnModuleInit { 13 | queryEntityDirectiveClasses: { 14 | [key: string]: QueryDirectiveClass; 15 | } = {} as any; 16 | queryRelationDirectiveClasses: { 17 | [key: string]: QueryRelationDirectiveClass; 18 | } = {} as any; 19 | queryConditionDirectiveClasses: { 20 | [key: string]: QueryConditionDirectiveClass; 21 | } = {} as any; 22 | queryFieldDirectiveClasses: { 23 | [key: string]: QueryFieldDirectiveClass; 24 | } = {} as any; 25 | 26 | postEntityDirectiveClasses: { [key: string]: PostDirectiveClass } = {} as any; 27 | postRelationDirectiveClasses: { 28 | [key: string]: PostDirectiveClass; 29 | } = {} as any; 30 | 31 | deleteDirectiveClasses: { [key: string]: DeleteDirectiveClass } = {} as any; 32 | 33 | async onModuleInit() { 34 | await this.loadDirectiveClasses(); 35 | } 36 | 37 | async loadDirectiveClasses() { 38 | const directiveClasses: QueryDirectiveClass[] | PostDirectiveClass[] = 39 | importDirectivesFromDirectories([ 40 | 'dist/directives/*.js', 41 | 'directives/*.js', 42 | ]); 43 | directiveClasses.forEach((directiveClass) => { 44 | const directiveName = directiveClass.directiveName; 45 | if ( 46 | directiveClass.directiveType === DirectiveType.QUERY_ENTITY_DIRECTIVE 47 | ) { 48 | console.assert( 49 | !this.queryEntityDirectiveClasses[directiveName], 50 | `Query entity directive ${directiveName} duplicated!`, 51 | ); 52 | this.queryEntityDirectiveClasses[directiveName] = directiveClass; 53 | } 54 | 55 | if ( 56 | directiveClass.directiveType === DirectiveType.QUERY_RELATION_DIRECTIVE 57 | ) { 58 | console.assert( 59 | !this.queryRelationDirectiveClasses[directiveName], 60 | `Query relation directive ${directiveName} duplicated!`, 61 | ); 62 | this.queryRelationDirectiveClasses[directiveName] = directiveClass; 63 | } 64 | 65 | if ( 66 | directiveClass.directiveType === DirectiveType.QUERY_CONDITION_DIRECTIVE 67 | ) { 68 | console.assert( 69 | !this.queryConditionDirectiveClasses[directiveName], 70 | `Query condition directive ${directiveName} duplicated!`, 71 | ); 72 | this.queryConditionDirectiveClasses[directiveName] = directiveClass; 73 | } 74 | 75 | if ( 76 | directiveClass.directiveType === DirectiveType.QUERY_FIELD_DIRECTIVE 77 | ) { 78 | console.assert( 79 | !this.queryFieldDirectiveClasses[directiveName], 80 | `Query field directive ${directiveName} duplicated!`, 81 | ); 82 | this.queryFieldDirectiveClasses[directiveName] = directiveClass; 83 | } 84 | 85 | if ( 86 | directiveClass.directiveType === DirectiveType.POST_ENTITY_DIRECTIVE 87 | ) { 88 | console.assert( 89 | !this.postEntityDirectiveClasses[directiveName], 90 | `Post entity directive ${directiveName} duplicated!`, 91 | ); 92 | this.postEntityDirectiveClasses[directiveName] = directiveClass; 93 | } 94 | 95 | if ( 96 | directiveClass.directiveType === DirectiveType.POST_RELATION_DIRECTIVE 97 | ) { 98 | console.assert( 99 | !this.postRelationDirectiveClasses[directiveName], 100 | `Post relation directive ${directiveName} duplicated!`, 101 | ); 102 | this.postRelationDirectiveClasses[directiveName] = directiveClass; 103 | } 104 | if (directiveClass.directiveType === DirectiveType.DELETE_DIRECTIVE) { 105 | console.assert( 106 | !this.deleteDirectiveClasses[directiveName], 107 | `Delete directive ${directiveName} duplicated!`, 108 | ); 109 | this.deleteDirectiveClasses[directiveName] = directiveClass; 110 | } 111 | }); 112 | console.debug('Directives loaded'); 113 | } 114 | } 115 | --------------------------------------------------------------------------------