diff --git a/packages/database/src/index.ts b/packages/database/src/index.ts index 956d1bc44..a07d1542c 100644 --- a/packages/database/src/index.ts +++ b/packages/database/src/index.ts @@ -4,6 +4,6 @@ export * from './utils'; export { Database as default } from './database'; export * from './relation-repository/belongs-to-many-repository'; export * from './relation-repository/belongs-to-repository'; -export { Model } from 'sequelize'; +export { Model, ModelCtor } from 'sequelize'; export * from './fields'; export * from './update-associations'; \ No newline at end of file diff --git a/packages/database/src/repository.ts b/packages/database/src/repository.ts index 284039a92..3868a46ba 100644 --- a/packages/database/src/repository.ts +++ b/packages/database/src/repository.ts @@ -56,9 +56,7 @@ export type WhiteList = string[]; export type BlackList = string[]; export type AssociationKeysToBeUpdate = string[]; -export type Values = { - [key: string]: string | number | Values | Array; -}; +export type Values = any; export interface CountOptions extends Omit, TransactionAble { fields?: Fields; diff --git a/packages/plugin-notifications/.npmignore b/packages/plugin-notifications/.npmignore new file mode 100644 index 000000000..461574b2f --- /dev/null +++ b/packages/plugin-notifications/.npmignore @@ -0,0 +1,7 @@ +node_modules +*.log +docs +__tests__ +tsconfig.json +src +.fatherrc.ts \ No newline at end of file diff --git a/packages/plugin-notifications/package.json b/packages/plugin-notifications/package.json new file mode 100644 index 000000000..8410f8a5d --- /dev/null +++ b/packages/plugin-notifications/package.json @@ -0,0 +1,19 @@ +{ + "name": "@nocobase/plugin-notifications", + "version": "0.6.0-alpha.0", + "main": "lib/index.js", + "license": "MIT", + "scripts": { + "build": "rimraf -rf lib esm dist && npm run build:cjs && npm run build:esm", + "build:cjs": "tsc --project tsconfig.build.json", + "build:esm": "tsc --project tsconfig.build.json --module es2015 --outDir esm" + }, + "dependencies": { + "nodemailer": "^6.6.1" + }, + "devDependencies": { + "@types/nodemailer": "6.4.4", + "nodemailer-mock": "^1.5.11" + }, + "gitHead": "f0b335ac30f29f25c95d7d137655fa64d8d67f1e" +} diff --git a/packages/plugin-notifications/src/__tests__/notifications.test.ts b/packages/plugin-notifications/src/__tests__/notifications.test.ts new file mode 100644 index 000000000..c15e32668 --- /dev/null +++ b/packages/plugin-notifications/src/__tests__/notifications.test.ts @@ -0,0 +1,54 @@ +import Database from '@nocobase/database'; +import { Notification, NotificationService } from '../models'; +import nodemailerMock from 'nodemailer-mock'; +import { mockServer } from '@nocobase/test'; +import _ from 'lodash'; +import plugin from '../server'; + +jest.setTimeout(300000); + +describe('notifications', () => { + let db: Database; + + beforeEach(async () => { + const app = mockServer(); + app.plugin(plugin); + await app.load(); + db = app.db; + await db.sync(); + NotificationService.createTransport = nodemailerMock.createTransport; + }); + + afterEach(() => db.close()); + + it('create', async () => { + const Notification = db.getCollection('notifications'); + const notification = await Notification.repository.create({ + values: { + subject: 'Subject', + body: 'hell world', + receiver_options: { + data: 'to@nocobase.com', + fromTable: 'users', + filter: {}, + dataField: 'email', + }, + service: { + type: 'email', + title: '阿里云邮件推送', + options: { + host: 'smtpdm.aliyun.com', + port: 465, + secure: true, + auth: { + user: 'from@nocobase.com', + pass: 'pass', + }, + from: 'NocoBase', + }, + }, + }, + }) as Notification; + await notification.send(); + }); +}); diff --git a/packages/plugin-notifications/src/collections/notification_logs.ts b/packages/plugin-notifications/src/collections/notification_logs.ts new file mode 100644 index 000000000..0eb276b91 --- /dev/null +++ b/packages/plugin-notifications/src/collections/notification_logs.ts @@ -0,0 +1,25 @@ +import { CollectionOptions } from '@nocobase/database'; +import { NotificationLog } from '../models'; + +export default { + name: 'notification_logs', + model: NotificationLog, + title: '通知日志', + fields: [ + { + title: '接收人', + type: 'json', + name: 'receiver', + }, + { + title: '状态', + type: 'string', + name: 'state', + }, + { + title: '详情', + type: 'json', + name: 'response', + }, + ] +} as CollectionOptions; diff --git a/packages/plugin-notifications/src/collections/notification_services.ts b/packages/plugin-notifications/src/collections/notification_services.ts new file mode 100644 index 000000000..99e734f8b --- /dev/null +++ b/packages/plugin-notifications/src/collections/notification_services.ts @@ -0,0 +1,25 @@ +import { CollectionOptions } from '@nocobase/database'; +import { NotificationService } from '../models'; + +export default { + name: 'notification_services', + model: NotificationService, + title: '通知服务', + fields: [ + { + title: '类型', + type: 'string', + name: 'type', + }, + { + title: '服务名称', + type: 'string', + name: 'title', + }, + { + title: '配置信息', + type: 'json', + name: 'options', + }, + ] +} as CollectionOptions; diff --git a/packages/plugin-notifications/src/collections/notifications.ts b/packages/plugin-notifications/src/collections/notifications.ts new file mode 100644 index 000000000..c9ab34eb5 --- /dev/null +++ b/packages/plugin-notifications/src/collections/notifications.ts @@ -0,0 +1,42 @@ +import { CollectionOptions } from '@nocobase/database'; +import { Notification } from '../models'; + +export default { + name: 'notifications', + model: Notification, + title: '通知', + fields: [ + { + type: 'uid', + name: 'name', + prefix: 'n_', + }, + { + title: '主题', + type: 'string', + name: 'subject', + }, + { + title: '内容', + type: 'text', + name: 'body', + }, + { + title: '接收人配置', + type: 'json', + name: 'receiver_options', + }, + { + title: '发送服务', + type: 'belongsTo', + name: 'service', + target: 'notification_services', + }, + { + title: '日志', + type: 'hasMany', + name: 'logs', + target: 'notification_logs', + }, + ] +} as CollectionOptions; diff --git a/packages/plugin-notifications/src/models/Notification.ts b/packages/plugin-notifications/src/models/Notification.ts new file mode 100644 index 000000000..273f336b8 --- /dev/null +++ b/packages/plugin-notifications/src/models/Notification.ts @@ -0,0 +1,74 @@ +import Database, { Model } from '@nocobase/database'; +import { NotificationService } from './NotificationService'; +import _ from 'lodash'; + +export class Notification extends Model { + + [key: string]: any; + + get db(): Database { + return this.constructor['database']; + } + + async getReceiversByOptions(): Promise { + const { data, fromTable, filter, dataField } = this.receiver_options; + let receivers = []; + if (data) { + receivers = Array.isArray(data) ? data : [data]; + } else if (fromTable) { + const collection = this.db.getCollection(fromTable); + const rows = await collection.repository.find({ + filter, + }); + receivers = rows.map(row => row[dataField]); + } + return receivers; + } + + async send(options: any = {}) { + if (!this.service) { + this.service = await this.getService(); + } + const receivers = await this.getReceiversByOptions(); + let { to } = options; + if (to) { + to = Array.isArray(to) ? to : [to]; + receivers.push(...to); + } + console.log(receivers) + for (const receiver of receivers) { + try { + const response = await (this.service as NotificationService).send({ + to: receiver, + subject: this.getSubject(), + html: this.getBody(options), + }); + await this.createLog({ + receiver, + state: 'success', + response, + }); + await new Promise((resolve) => { + setTimeout(resolve, 100); + }); + } catch (error) { + console.error(error); + await this.createLog({ + receiver, + state: 'fail', + response: {}, + }); + } + } + } + + getSubject() { + return this.subject; + } + + getBody(data) { + const compiled = _.template(this.body) + const body = compiled(data); + return body; + } +} diff --git a/packages/plugin-notifications/src/models/NotificationLog.ts b/packages/plugin-notifications/src/models/NotificationLog.ts new file mode 100644 index 000000000..0587cfa0e --- /dev/null +++ b/packages/plugin-notifications/src/models/NotificationLog.ts @@ -0,0 +1,5 @@ +import { Model } from '@nocobase/database'; + +export class NotificationLog extends Model { + +} diff --git a/packages/plugin-notifications/src/models/NotificationService.ts b/packages/plugin-notifications/src/models/NotificationService.ts new file mode 100644 index 000000000..d491119fb --- /dev/null +++ b/packages/plugin-notifications/src/models/NotificationService.ts @@ -0,0 +1,26 @@ +import { Model } from '@nocobase/database'; +import nodemailer from 'nodemailer'; + +export class NotificationService extends Model { + [key: string]: any; + + static createTransport = nodemailer.createTransport; + + private _transporter = null; + + get transporter() { + if (this._transporter) { + return this._transporter; + } + return this._transporter = NotificationService.createTransport(this.options); + } + + async send(options) { + const { from } = this.options; + const mailOptions = { + from, + ...options, + }; + return this.transporter.sendMail(mailOptions); + } +} diff --git a/packages/plugin-notifications/src/models/index.ts b/packages/plugin-notifications/src/models/index.ts new file mode 100644 index 000000000..190994a9d --- /dev/null +++ b/packages/plugin-notifications/src/models/index.ts @@ -0,0 +1,3 @@ +export * from './Notification'; +export * from './NotificationLog'; +export * from './NotificationService'; diff --git a/packages/plugin-notifications/src/server.ts b/packages/plugin-notifications/src/server.ts new file mode 100644 index 000000000..76b1c1327 --- /dev/null +++ b/packages/plugin-notifications/src/server.ts @@ -0,0 +1,11 @@ +import path from 'path'; +import { PluginOptions } from '@nocobase/server'; + +export default { + name: 'notifications', + async load() { + await this.app.db.import({ + directory: path.resolve(__dirname, 'collections'), + }); + } +} as PluginOptions diff --git a/packages/plugin-notifications/tsconfig.build.json b/packages/plugin-notifications/tsconfig.build.json new file mode 100644 index 000000000..8d3db734b --- /dev/null +++ b/packages/plugin-notifications/tsconfig.build.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.build.json", + "compilerOptions": { + "outDir": "./lib", + "declaration": true + }, + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["./src/__tests__/*", "./esm/*", "./lib/*"] +} \ No newline at end of file diff --git a/packages/plugin-notifications/tsconfig.json b/packages/plugin-notifications/tsconfig.json new file mode 100644 index 000000000..e10a5305a --- /dev/null +++ b/packages/plugin-notifications/tsconfig.json @@ -0,0 +1,5 @@ +{ + "extends": "../../tsconfig.json", + "include": ["./src/**/*.ts", "./src/**/*.tsx"], + "exclude": ["./esm/*", "./lib/*"] +} \ No newline at end of file diff --git a/yarn.lock b/yarn.lock index 6d0a78fa5..90e9e2f2f 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2437,6 +2437,13 @@ resolved "https://registry.npmjs.org/@types/node/-/node-14.17.34.tgz#fe4b38b3f07617c0fa31ae923fca9249641038f0" integrity sha512-USUftMYpmuMzeWobskoPfzDi+vkpe0dvcOBRNOscFrGxVp4jomnRxWuVohgqBow2xyIPC0S3gjxV/5079jhmDg== +"@types/nodemailer@6.4.4": + version "6.4.4" + resolved "https://registry.npmjs.org/@types/nodemailer/-/nodemailer-6.4.4.tgz#c265f7e7a51df587597b3a49a023acaf0c741f4b" + integrity sha512-Ksw4t7iliXeYGvIQcSIgWQ5BLuC/mljIEbjf615svhZL10PE9t+ei8O9gDaD3FPCasUJn9KTLwz2JFJyiiyuqw== + dependencies: + "@types/node" "*" + "@types/normalize-package-data@^2.4.0": version "2.4.1" resolved "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.1.tgz#d3357479a0fdfdd5907fe67e17e0a85c906e1301" @@ -9584,6 +9591,19 @@ node-releases@^2.0.1: resolved "https://registry.npmjs.org/node-releases/-/node-releases-2.0.1.tgz#3d1d395f204f1f2f29a54358b9fb678765ad2fc5" integrity sha512-CqyzN6z7Q6aMeF/ktcMVTzhAHCEpf8SOarwpzpf8pNBY2k5/oM34UHldUwp8VKI7uxct2HxSRdJjBaZeESzcxA== +nodemailer-mock@^1.5.11: + version "1.5.11" + resolved "https://registry.npmjs.org/nodemailer-mock/-/nodemailer-mock-1.5.11.tgz#1bb6b9af44e9007380191d32e33555ea136a819f" + integrity sha512-RbqkppKkptTgSzry3q8u/YD8pxulln/hYQ30O5brFRki8gEKv7KoAYGwfErZM2VsI7nGNBYRo5IOLnMW3NXBcw== + dependencies: + debug "^4.3.2" + nodemailer "^6.x" + +nodemailer@^6.6.1, nodemailer@^6.x: + version "6.7.2" + resolved "https://registry.npmjs.org/nodemailer/-/nodemailer-6.7.2.tgz#44b2ad5f7ed71b7067f7a21c4fedabaec62b85e0" + integrity sha512-Dz7zVwlef4k5R71fdmxwR8Q39fiboGbu3xgswkzGwczUfjp873rVxt1O46+Fh0j1ORnAC6L9+heI8uUpO6DT7Q== + "nopt@2 || 3": version "3.0.6" resolved "https://registry.npmjs.org/nopt/-/nopt-3.0.6.tgz#c6465dbf08abcd4db359317f79ac68a646b28ff9"