diff --git a/lib/base-app-domain/ModiEntity/Storage.js b/lib/base-app-domain/ModiEntity/Storage.js index 5cc06a0..5b23dcd 100644 --- a/lib/base-app-domain/ModiEntity/Storage.js +++ b/lib/base-app-domain/ModiEntity/Storage.js @@ -12,7 +12,8 @@ exports.desc = { type: "varchar", params: { length: 32 - } + }, + ref: ["user"] }, entityId: { type: "varchar", diff --git a/lib/base-app-domain/OperEntity/Storage.js b/lib/base-app-domain/OperEntity/Storage.js index 721a38e..3056ed0 100644 --- a/lib/base-app-domain/OperEntity/Storage.js +++ b/lib/base-app-domain/OperEntity/Storage.js @@ -12,7 +12,8 @@ exports.desc = { type: "varchar", params: { length: 32 - } + }, + ref: ["modi", "user"] }, entityId: { type: "varchar", diff --git a/lib/checkers/index.js b/lib/checkers/index.js index 3c7f105..2482056 100644 --- a/lib/checkers/index.js +++ b/lib/checkers/index.js @@ -7,6 +7,7 @@ var modi_1 = require("../store/modi"); function createDynamicCheckers(schema, authDict) { var checkers = []; checkers.push.apply(checkers, tslib_1.__spreadArray([], tslib_1.__read((0, modi_1.createModiRelatedCheckers)(schema)), false)); + // checkers.push(...createRemoveCheckers(schema)); if (authDict) { checkers.push.apply(checkers, tslib_1.__spreadArray([], tslib_1.__read((0, checker_1.createAuthCheckers)(schema, authDict)), false)); } diff --git a/lib/compiler/schemalBuilder.js b/lib/compiler/schemalBuilder.js index b493670..d33a45b 100644 --- a/lib/compiler/schemalBuilder.js +++ b/lib/compiler/schemalBuilder.js @@ -2871,6 +2871,14 @@ function constructAttributes(entity) { attrAssignments.push(factory.createPropertyAssignment(factory.createIdentifier("type"), factory.createStringLiteral("varchar")), factory.createPropertyAssignment(factory.createIdentifier("params"), factory.createObjectLiteralExpression([ factory.createPropertyAssignment(factory.createIdentifier("length"), factory.createNumericLiteral(typeArguments[0].literal.text)), ], true))); + // 如果是entity,在这里处理一下ref + if (ts.isIdentifier(name) && name.text === 'entity') { + var mtoRelations = ReversePointerRelations[entity]; + if (mtoRelations) { + var mtoEntities = mtoRelations.map(function (ele) { return (0, string_1.firstLetterLowerCase)(ele); }); + attrAssignments.push(factory.createPropertyAssignment(factory.createIdentifier("ref"), factory.createArrayLiteralExpression(mtoEntities.map(function (ele) { return factory.createStringLiteral(ele); }), false))); + } + } break; } case 'Text': diff --git a/lib/store/TriggerExecutor.js b/lib/store/TriggerExecutor.js index 0e51be6..2ad37ef 100644 --- a/lib/store/TriggerExecutor.js +++ b/lib/store/TriggerExecutor.js @@ -6,6 +6,7 @@ var assert_1 = tslib_1.__importDefault(require("assert")); var lodash_1 = require("../utils/lodash"); var filter_1 = require("../store/filter"); var Entity_1 = require("../types/Entity"); +var Trigger_1 = require("../types/Trigger"); var SyncRowStore_1 = require("./SyncRowStore"); var checker_1 = require("./checker"); /** @@ -33,10 +34,11 @@ var TriggerExecutor = /** @class */ (function () { var entity = checker.entity, action = checker.action, type = checker.type, conditionalFilter = checker.conditionalFilter; var triggerName = "".concat(String(entity)).concat(action, "\u6743\u9650\u68C0\u67E5-").concat(this.counter++); var _a = (0, checker_1.translateCheckerInAsyncContext)(checker), fn = _a.fn, when = _a.when; + var priority = type === 'data' ? Trigger_1.DATA_CHECKER_DEFAULT_PRIORITY : Trigger_1.CHECKER_DEFAULT_PRIORITY; // checker的默认优先级最低(前面的trigger可能会赋上一些相应的值) var trigger = { checkerType: type, name: triggerName, - priority: checker.priority || 20, + priority: checker.priority || priority, entity: entity, action: action, fn: fn, @@ -59,7 +61,10 @@ var TriggerExecutor = /** @class */ (function () { throw new Error("\u4E0D\u53EF\u6709\u540C\u540D\u7684\u89E6\u53D1\u5668\u300C".concat(trigger.name, "\u300D")); } if (typeof trigger.priority !== 'number') { - trigger.priority = 10; // 默认值 + trigger.priority = Trigger_1.TRIGGER_DEFAULT_PRIORITY; // 默认值 + } + else { + (0, assert_1.default)(trigger.priority <= Trigger_1.TRIGGER_MAX_PRIORITY && trigger.priority >= Trigger_1.TRIGGER_MIN_PRIORITY, "trigger\u300C".concat(trigger.name, "\u300D\u7684\u4F18\u5148\u7EA7\u5B9A\u4E49\u8D8A\u754C\uFF0C\u5E94\u8BE5\u5728").concat(Trigger_1.TRIGGER_MIN_PRIORITY, "\u5230").concat(Trigger_1.TRIGGER_MAX_PRIORITY, "\u4E4B\u95F4")); } if (trigger.filter) { (0, assert_1.default)(typeof trigger.action === 'string' && trigger.action !== 'create' diff --git a/lib/store/checker.d.ts b/lib/store/checker.d.ts index 533c708..7d95350 100644 --- a/lib/store/checker.d.ts +++ b/lib/store/checker.d.ts @@ -10,4 +10,17 @@ export declare function translateCheckerInSyncContext void; when: 'before' | 'after'; }; +/** + * 根据权限定义,创建出相应的checker + * @param schema + * @param authDict + * @returns + */ export declare function createAuthCheckers | SyncContext>(schema: StorageSchema, authDict: AuthDefDict): Checker[]; +/** + * 对对象的删除,检查其是否会产生其他行上的空指针,不允许这种情况的出现 + * @param schema + * @returns + * 如果有的对象允许删除,需要使用trigger来处理其相关联的外键对象,这些trigger写作before,则会在checker之前执行,仍然可以删除成功 + */ +export declare function createRemoveCheckers | SyncContext>(schema: StorageSchema): Checker[]; diff --git a/lib/store/checker.js b/lib/store/checker.js index 15a6927..c5ca2bd 100644 --- a/lib/store/checker.js +++ b/lib/store/checker.js @@ -1,6 +1,6 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); -exports.createAuthCheckers = exports.translateCheckerInSyncContext = exports.translateCheckerInAsyncContext = void 0; +exports.createRemoveCheckers = exports.createAuthCheckers = exports.translateCheckerInSyncContext = exports.translateCheckerInAsyncContext = void 0; var tslib_1 = require("tslib"); var assert_1 = tslib_1.__importDefault(require("assert")); var filter_1 = require("../store/filter"); @@ -392,6 +392,12 @@ function translateActionAuthFilterMaker(schema, relationItem, entity) { var filterMaker = translateCascadeRelationFilterMaker(schema, relationItem, entity); return function (userId) { return filterMaker(userId); }; } +/** + * 根据权限定义,创建出相应的checker + * @param schema + * @param authDict + * @returns + */ function createAuthCheckers(schema, authDict) { var checkers = []; var _loop_1 = function (entity) { @@ -496,3 +502,224 @@ function createAuthCheckers(schema, authDict) { return checkers; } exports.createAuthCheckers = createAuthCheckers; +/** + * 对对象的删除,检查其是否会产生其他行上的空指针,不允许这种情况的出现 + * @param schema + * @returns + * 如果有的对象允许删除,需要使用trigger来处理其相关联的外键对象,这些trigger写作before,则会在checker之前执行,仍然可以删除成功 + */ +function createRemoveCheckers(schema) { + var e_1, _a; + var checkers = []; + // 先建立所有的一对多的关系 + var OneToManyMatrix = {}; + var OneToManyOnEntityMatrix = {}; + var addToMto = function (e, f, attr) { + var _a; + if (OneToManyMatrix[f]) { + (_a = OneToManyMatrix[f]) === null || _a === void 0 ? void 0 : _a.push([e, attr]); + } + else { + OneToManyMatrix[f] = [[e, attr]]; + } + }; + var addToMtoEntity = function (e, fs) { + var e_2, _a; + var _b; + try { + for (var fs_1 = tslib_1.__values(fs), fs_1_1 = fs_1.next(); !fs_1_1.done; fs_1_1 = fs_1.next()) { + var f = fs_1_1.value; + if (!OneToManyOnEntityMatrix[f]) { + OneToManyOnEntityMatrix[f] = [e]; + } + else { + (_b = OneToManyOnEntityMatrix[f]) === null || _b === void 0 ? void 0 : _b.push(e); + } + } + } + catch (e_2_1) { e_2 = { error: e_2_1 }; } + finally { + try { + if (fs_1_1 && !fs_1_1.done && (_a = fs_1.return)) _a.call(fs_1); + } + finally { if (e_2) throw e_2.error; } + } + }; + for (var entity in schema) { + if (['operEntity'].includes(entity)) { + continue; // OperEntity会指向每一个对象,不必处理 + } + var attributes = schema[entity].attributes; + for (var attr in attributes) { + if (attributes[attr].type === 'ref') { + addToMto(entity, attributes[attr].ref, attr); + } + else if (attr === 'entity') { + if (attributes[attr].ref) { + addToMtoEntity(entity, attributes[attr].ref); + } + else if (process.env.NODE_ENV === 'development') { + console.warn("".concat(entity, "\u7684entity\u53CD\u6307\u6307\u9488\u627E\u4E0D\u5230\u6709\u6548\u7684\u5BF9\u8C61")); + } + } + } + } + // 当删除一时,要确认多上面没有指向一的数据 + var entities = (0, lodash_1.intersection)(Object.keys(OneToManyMatrix), Object.keys(OneToManyOnEntityMatrix)); + var _loop_3 = function (entity) { + checkers.push({ + entity: entity, + action: 'remove', + type: 'logical', + checker: function (operation, context, option) { + var e_3, _a, e_4, _b; + var promises = []; + if (OneToManyMatrix[entity]) { + var _loop_4 = function (otm) { + var _g, _h, _j, _k; + var _l = tslib_1.__read(otm, 2), e = _l[0], attr = _l[1]; + var proj = (_g = { + id: 1 + }, + _g[attr] = 1, + _g); + var filter = operation.filter && (_h = {}, + _h[attr.slice(0, attr.length - 2)] = operation.filter, + _h); + var result = context.select(e, { + data: proj, + filter: filter, + indexFrom: 0, + count: 1 + }, { dontCollect: true }); + if (result instanceof Promise) { + promises.push(result.then(function (_a) { + var _b, _c; + var _d = tslib_1.__read(_a, 1), row = _d[0]; + if (row) { + var record = { + a: 's', + d: (_b = {}, + _b[e] = (_c = {}, + _c[row.id] = row, + _c), + _b) + }; + throw new Exception_1.OakRowInconsistencyException(record, "\u60A8\u65E0\u6CD5\u5220\u9664\u5B58\u5728\u6709\u6548\u6570\u636E\u300C".concat(e, "\u300D\u5173\u8054\u7684\u884C")); + } + })); + } + else { + var _m = tslib_1.__read(result, 1), row = _m[0]; + if (row) { + var record = { + a: 's', + d: (_j = {}, + _j[e] = (_k = {}, + _k[row.id] = row, + _k), + _j) + }; + throw new Exception_1.OakRowInconsistencyException(record, "\u60A8\u65E0\u6CD5\u5220\u9664\u5B58\u5728\u6709\u6548\u6570\u636E\u300C".concat(e, "\u300D\u5173\u8054\u7684\u884C")); + } + } + }; + try { + for (var _c = (e_3 = void 0, tslib_1.__values(OneToManyMatrix[entity])), _d = _c.next(); !_d.done; _d = _c.next()) { + var otm = _d.value; + _loop_4(otm); + } + } + catch (e_3_1) { e_3 = { error: e_3_1 }; } + finally { + try { + if (_d && !_d.done && (_a = _c.return)) _a.call(_c); + } + finally { if (e_3) throw e_3.error; } + } + } + if (OneToManyOnEntityMatrix[entity]) { + var _loop_5 = function (otm) { + var _o, _p, _q; + var proj = { + id: 1, + entity: 1, + entityId: 1, + }; + var filter = operation.filter && (_o = {}, + _o[entity] = operation.filter, + _o); + var result = context.select(otm, { + data: proj, + filter: filter, + indexFrom: 0, + count: 1 + }, { dontCollect: true }); + if (result instanceof Promise) { + promises.push(result.then(function (_a) { + var _b, _c; + var _d = tslib_1.__read(_a, 1), row = _d[0]; + if (row) { + var record = { + a: 's', + d: (_b = {}, + _b[otm] = (_c = {}, + _c[row.id] = row, + _c), + _b) + }; + throw new Exception_1.OakRowInconsistencyException(record, "\u60A8\u65E0\u6CD5\u5220\u9664\u5B58\u5728\u6709\u6548\u6570\u636E\u300C".concat(otm, "\u300D\u5173\u8054\u7684\u884C")); + } + })); + } + else { + var _r = tslib_1.__read(result, 1), row = _r[0]; + if (row) { + var record = { + a: 's', + d: (_p = {}, + _p[otm] = (_q = {}, + _q[row.id] = row, + _q), + _p) + }; + throw new Exception_1.OakRowInconsistencyException(record, "\u60A8\u65E0\u6CD5\u5220\u9664\u5B58\u5728\u6709\u6548\u6570\u636E\u300C".concat(otm, "\u300D\u5173\u8054\u7684\u884C")); + } + } + }; + try { + for (var _e = (e_4 = void 0, tslib_1.__values(OneToManyOnEntityMatrix[entity])), _f = _e.next(); !_f.done; _f = _e.next()) { + var otm = _f.value; + _loop_5(otm); + } + } + catch (e_4_1) { e_4 = { error: e_4_1 }; } + finally { + try { + if (_f && !_f.done && (_b = _e.return)) _b.call(_e); + } + finally { if (e_4) throw e_4.error; } + } + } + if (promises.length > 0) { + return Promise.all(promises).then(function () { return undefined; }); + } + } + }); + }; + try { + for (var entities_1 = tslib_1.__values(entities), entities_1_1 = entities_1.next(); !entities_1_1.done; entities_1_1 = entities_1.next()) { + var entity = entities_1_1.value; + _loop_3(entity); + } + } + catch (e_1_1) { e_1 = { error: e_1_1 }; } + finally { + try { + if (entities_1_1 && !entities_1_1.done && (_a = entities_1.return)) _a.call(entities_1); + } + finally { if (e_1) throw e_1.error; } + } + return checkers; +} +exports.createRemoveCheckers = createRemoveCheckers; diff --git a/lib/types/Auth.d.ts b/lib/types/Auth.d.ts index cee2d7b..7c25dbe 100644 --- a/lib/types/Auth.d.ts +++ b/lib/types/Auth.d.ts @@ -12,7 +12,7 @@ export declare type DataChecker | Array>; - checker: (data: ED[T]['Create']['data'] | ED[T]['Update']['data'], context: Cxt) => void | Promise; + checker: (data: ED[T]['Create']['data'] | ED[T]['Update']['data'], context: Cxt) => any | Promise; conditionalFilter?: ED[T]['Update']['filter'] | ((operation: ED[T]['Operation'], context: Cxt, option: OperateOption) => ED[T]['Update']['filter'] | Promise); }; export declare type RowChecker | SyncContext> = { @@ -44,7 +44,7 @@ export declare type LogicalChecker; - checker: (operation: ED[T]['Operation'] | ED[T]['Selection'], context: Cxt, option: OperateOption | SelectOption) => void | Promise; + checker: (operation: ED[T]['Operation'] | ED[T]['Selection'], context: Cxt, option: OperateOption | SelectOption) => any | Promise; conditionalFilter?: ED[T]['Update']['filter'] | ((operation: ED[T]['Operation'], context: Cxt, option: OperateOption) => ED[T]['Update']['filter']); }; export declare type LogicalRelationChecker | SyncContext> = { @@ -53,7 +53,7 @@ export declare type LogicalRelationChecker; - checker: (operation: ED[T]['Operation'] | ED[T]['Selection'], context: Cxt, option: OperateOption | SelectOption) => void | Promise; + checker: (operation: ED[T]['Operation'] | ED[T]['Selection'], context: Cxt, option: OperateOption | SelectOption) => any | Promise; conditionalFilter?: ED[T]['Update']['filter'] | ((operation: ED[T]['Operation'], context: Cxt, option: OperateOption) => ED[T]['Update']['filter']); }; export declare type Checker | SyncContext> = DataChecker | RowChecker | RelationChecker | LogicalChecker | LogicalRelationChecker; diff --git a/lib/types/Storage.d.ts b/lib/types/Storage.d.ts index 341fabc..39883bf 100644 --- a/lib/types/Storage.d.ts +++ b/lib/types/Storage.d.ts @@ -20,7 +20,7 @@ export interface Index { export interface Attribute { type: DataType | Ref; params?: DataTypeParams; - ref?: string; + ref?: string | string[]; onRefDelete?: 'delete' | 'setNull' | 'ignore'; default?: string | number | boolean; notNull?: boolean; diff --git a/lib/types/Trigger.d.ts b/lib/types/Trigger.d.ts index 0015048..178e994 100644 --- a/lib/types/Trigger.d.ts +++ b/lib/types/Trigger.d.ts @@ -4,6 +4,14 @@ import { AsyncContext } from "../store/AsyncRowStore"; import { SyncContext } from "../store/SyncRowStore"; import { EntityDict, OperateOption } from "../types/Entity"; import { EntityShape } from "../types/Entity"; +/** + * 优先级越小,越早执行。定义在1~99之间 + */ +export declare const TRIGGER_DEFAULT_PRIORITY = 50; +export declare const TRIGGER_MIN_PRIORITY = 1; +export declare const TRIGGER_MAX_PRIORITY = 99; +export declare const DATA_CHECKER_DEFAULT_PRIORITY = 60; +export declare const CHECKER_DEFAULT_PRIORITY = 99; interface TriggerBase { checkerType?: CheckerType; entity: T; diff --git a/lib/types/Trigger.js b/lib/types/Trigger.js index 6cd1cd5..9d5bc2d 100644 --- a/lib/types/Trigger.js +++ b/lib/types/Trigger.js @@ -1,5 +1,14 @@ "use strict"; Object.defineProperty(exports, "__esModule", { value: true }); +exports.CHECKER_DEFAULT_PRIORITY = exports.DATA_CHECKER_DEFAULT_PRIORITY = exports.TRIGGER_MAX_PRIORITY = exports.TRIGGER_MIN_PRIORITY = exports.TRIGGER_DEFAULT_PRIORITY = void 0; +/** + * 优先级越小,越早执行。定义在1~99之间 + */ +exports.TRIGGER_DEFAULT_PRIORITY = 50; +exports.TRIGGER_MIN_PRIORITY = 1; +exports.TRIGGER_MAX_PRIORITY = 99; +exports.DATA_CHECKER_DEFAULT_PRIORITY = 60; +exports.CHECKER_DEFAULT_PRIORITY = 99; ; ; ; diff --git a/package.json b/package.json index f14cac2..2cd07fc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "oak-domain", - "version": "2.4.3", + "version": "2.4.4", "author": { "name": "XuChang" }, diff --git a/src/checkers/index.ts b/src/checkers/index.ts index 55f462d..936b230 100644 --- a/src/checkers/index.ts +++ b/src/checkers/index.ts @@ -1,6 +1,6 @@ import { EntityDict } from '../base-app-domain'; import { AsyncContext } from '../store/AsyncRowStore'; -import { createAuthCheckers } from '../store/checker'; +import { createAuthCheckers, createRemoveCheckers } from '../store/checker'; import { createModiRelatedCheckers } from '../store/modi'; import { SyncContext } from '../store/SyncRowStore'; import { StorageSchema, EntityDict as BaseEntityDict, Checker, AuthDef, AuthDefDict } from '../types'; @@ -8,6 +8,7 @@ import { StorageSchema, EntityDict as BaseEntityDict, Checker, AuthDef, AuthDefD export function createDynamicCheckers | SyncContext>(schema: StorageSchema, authDict?: AuthDefDict){ const checkers: Checker[] = []; checkers.push(...createModiRelatedCheckers(schema)); + // checkers.push(...createRemoveCheckers(schema)); if (authDict) { checkers.push(...createAuthCheckers(schema, authDict)); } diff --git a/src/compiler/schemalBuilder.ts b/src/compiler/schemalBuilder.ts index cc11e30..22c22ad 100644 --- a/src/compiler/schemalBuilder.ts +++ b/src/compiler/schemalBuilder.ts @@ -5222,6 +5222,26 @@ function constructAttributes(entity: string): ts.PropertyAssignment[] { ) ) ); + // 如果是entity,在这里处理一下ref + if (ts.isIdentifier(name) && name.text === 'entity') { + const mtoRelations = ReversePointerRelations[entity]; + if (mtoRelations) { + const mtoEntities = mtoRelations.map( + ele => firstLetterLowerCase(ele) + ); + attrAssignments.push( + factory.createPropertyAssignment( + factory.createIdentifier("ref"), + factory.createArrayLiteralExpression( + mtoEntities.map( + ele => factory.createStringLiteral(ele) + ), + false + ) + ) + ); + } + } break; } case 'Text': diff --git a/src/store/TriggerExecutor.ts b/src/store/TriggerExecutor.ts index a250a8f..3b7c726 100644 --- a/src/store/TriggerExecutor.ts +++ b/src/store/TriggerExecutor.ts @@ -5,7 +5,7 @@ import { EntityDict, OperateOption, SelectOption, TriggerDataAttribute, TriggerT import { EntityDict as BaseEntityDict } from '../base-app-domain'; import { Logger } from "../types/Logger"; import { Checker, CheckerType, LogicalChecker, RelationChecker } from '../types/Auth'; -import { Trigger, CreateTriggerCrossTxn, CreateTrigger, CreateTriggerInTxn, SelectTriggerAfter, UpdateTrigger } from "../types/Trigger"; +import { Trigger, CreateTriggerCrossTxn, CreateTrigger, CreateTriggerInTxn, SelectTriggerAfter, UpdateTrigger, TRIGGER_DEFAULT_PRIORITY, CHECKER_DEFAULT_PRIORITY, DATA_CHECKER_DEFAULT_PRIORITY, TRIGGER_MAX_PRIORITY, TRIGGER_MIN_PRIORITY } from "../types/Trigger"; import { AsyncContext } from './AsyncRowStore'; import { SyncContext } from './SyncRowStore'; import { translateCheckerInAsyncContext } from './checker'; @@ -50,10 +50,11 @@ export class TriggerExecutor { const { entity, action, type, conditionalFilter } = checker; const triggerName = `${String(entity)}${action}权限检查-${this.counter++}`; const { fn, when } = translateCheckerInAsyncContext(checker); + const priority = type === 'data' ? DATA_CHECKER_DEFAULT_PRIORITY : CHECKER_DEFAULT_PRIORITY; // checker的默认优先级最低(前面的trigger可能会赋上一些相应的值) const trigger = { checkerType: type, name: triggerName, - priority: checker.priority || 20, // checker的默认优先级稍高 + priority: checker.priority || priority, entity, action: action as 'update', fn, @@ -77,7 +78,10 @@ export class TriggerExecutor { throw new Error(`不可有同名的触发器「${trigger.name}」`); } if (typeof trigger.priority !== 'number') { - trigger.priority = 10; // 默认值 + trigger.priority = TRIGGER_DEFAULT_PRIORITY; // 默认值 + } + else { + assert(trigger.priority <= TRIGGER_MAX_PRIORITY && trigger.priority >= TRIGGER_MIN_PRIORITY, `trigger「${trigger.name}」的优先级定义越界,应该在${TRIGGER_MIN_PRIORITY}到${TRIGGER_MAX_PRIORITY}之间`); } if ((trigger as UpdateTrigger).filter) { assert(typeof trigger.action === 'string' && trigger.action !== 'create' diff --git a/src/store/checker.ts b/src/store/checker.ts index f32cf41..a5277f5 100644 --- a/src/store/checker.ts +++ b/src/store/checker.ts @@ -3,14 +3,14 @@ import { addFilterSegment, checkFilterContains, combineFilters } from "../store/ import { OakRowInconsistencyException, OakUserUnpermittedException } from '../types/Exception'; import { AuthDefDict, CascadeRelationItem, Checker, CreateTriggerInTxn, - EntityDict, OperateOption, SelectOption, StorageSchema, Trigger, UpdateTriggerInTxn, RelationHierarchy + EntityDict, OperateOption, SelectOption, StorageSchema, Trigger, UpdateTriggerInTxn, RelationHierarchy, SelectOpResult } from "../types"; import { EntityDict as BaseEntityDict } from '../base-app-domain'; import { AsyncContext } from "./AsyncRowStore"; import { getFullProjection } from './actionDef'; import { SyncContext } from './SyncRowStore'; import { firstLetterUpperCase } from '../utils/string'; -import { uniq, difference } from '../utils/lodash'; +import { intersection, uniq, difference } from '../utils/lodash'; import { judgeRelation } from './relation'; export function translateCheckerInAsyncContext< @@ -111,7 +111,7 @@ export function translateCheckerInAsyncContext< const filter2 = await relationFilter(operation, context, option); const { data } = operation as ED[keyof ED]['Create']; - const filter = data instanceof Array ? { + const filter = data instanceof Array ? { id: { $in: data.map( ele => ele.id, @@ -258,7 +258,7 @@ function translateCascadeRelationFilterMaker ({ - id: userId, + id: userId, }); } else if (schema[entity].relation) { @@ -371,7 +371,12 @@ function translateActionAuthFilterMaker( return (userId) => filterMaker(userId); } - +/** + * 根据权限定义,创建出相应的checker + * @param schema + * @param authDict + * @returns + */ export function createAuthCheckers | SyncContext>( schema: StorageSchema, authDict: AuthDefDict) { @@ -476,5 +481,181 @@ export function createAuthCheckers | SyncContext>(schema: StorageSchema) { + const checkers: Checker[] = []; + + // 先建立所有的一对多的关系 + const OneToManyMatrix: Partial>> = {}; + const OneToManyOnEntityMatrix: Partial>> = {}; + + const addToMto = (e: keyof ED, f: keyof ED, attr: string) => { + if (OneToManyMatrix[f]) { + OneToManyMatrix[f]?.push([e, attr]); + } + else { + OneToManyMatrix[f] = [[e, attr]]; + } + }; + + const addToMtoEntity = (e: keyof ED, fs: Array) => { + for (const f of fs) { + if (!OneToManyOnEntityMatrix[f]) { + OneToManyOnEntityMatrix[f] = [e]; + } + else { + OneToManyOnEntityMatrix[f]?.push(e); + } + } + }; + + for (const entity in schema) { + if (['operEntity'].includes(entity)) { + continue; // OperEntity会指向每一个对象,不必处理 + } + const { attributes } = schema[entity]; + for (const attr in attributes) { + if (attributes[attr].type === 'ref') { + addToMto(entity, attributes[attr].ref as keyof ED, attr); + } + else if (attr === 'entity') { + if (attributes[attr].ref) { + addToMtoEntity(entity, attributes[attr].ref as Array); + } + else if (process.env.NODE_ENV === 'development') { + console.warn(`${entity}的entity反指指针找不到有效的对象`); + } + } + } + } + + // 当删除一时,要确认多上面没有指向一的数据 + const entities = intersection(Object.keys(OneToManyMatrix), Object.keys(OneToManyOnEntityMatrix)); + for (const entity of entities) { + checkers.push({ + entity: entity as keyof ED, + action: 'remove', + type: 'logical', + checker: (operation, context, option) => { + const promises: Promise[] = []; + if (OneToManyMatrix[entity]) { + for (const otm of OneToManyMatrix[entity]!) { + const [e, attr] = otm; + const proj = { + id: 1, + [attr]: 1, + }; + const filter = operation.filter && { + [attr.slice(0, attr.length -2)]: operation.filter + } + const result = context.select(e, { + data: proj, + filter, + indexFrom: 0, + count: 1 + }, { dontCollect: true }); + if (result instanceof Promise) { + promises.push( + result.then( + ([row]) => { + if (row) { + const record = { + a: 's', + d: { + [e]: { + [row.id!]: row, + } + } + } as SelectOpResult; + throw new OakRowInconsistencyException(record, `您无法删除存在有效数据「${e as string}」关联的行`); + } + } + ) + ); + } + else { + const [row] = result; + if (row) { + const record = { + a: 's', + d: { + [e]: { + [row.id!]: row, + } + } + } as SelectOpResult; + throw new OakRowInconsistencyException(record, `您无法删除存在有效数据「${e as string}」关联的行`); + } + } + } + } + if (OneToManyOnEntityMatrix[entity]) { + for (const otm of OneToManyOnEntityMatrix[entity]!) { + const proj = { + id: 1, + entity: 1, + entityId: 1, + }; + const filter = operation.filter && { + [entity]: operation.filter + } + const result = context.select(otm, { + data: proj, + filter, + indexFrom: 0, + count: 1 + }, { dontCollect: true }); + if (result instanceof Promise) { + promises.push( + result.then( + ([row]) => { + if (row) { + const record = { + a: 's', + d: { + [otm]: { + [row.id!]: row, + } + } + } as SelectOpResult; + throw new OakRowInconsistencyException(record, `您无法删除存在有效数据「${otm as string}」关联的行`); + } + } + ) + ); + } + else { + const [row] = result; + if (row) { + const record = { + a: 's', + d: { + [otm]: { + [row.id!]: row, + } + } + } as SelectOpResult; + throw new OakRowInconsistencyException(record, `您无法删除存在有效数据「${otm as string}」关联的行`); + } + } + } + } + if (promises.length > 0) { + return Promise.all(promises).then( + () => undefined + ); + } + } + }) + } + return checkers; } \ No newline at end of file diff --git a/src/types/Auth.ts b/src/types/Auth.ts index c218609..db09f27 100644 --- a/src/types/Auth.ts +++ b/src/types/Auth.ts @@ -15,7 +15,7 @@ export type DataChecker | Array>; - checker: (data: ED[T]['Create']['data'] | ED[T]['Update']['data'], context: Cxt) => void | Promise; + checker: (data: ED[T]['Create']['data'] | ED[T]['Update']['data'], context: Cxt) => any | Promise; conditionalFilter?: ED[T]['Update']['filter'] | ( (operation: ED[T]['Operation'], context: Cxt, option: OperateOption) => ED[T]['Update']['filter'] | Promise ); @@ -62,7 +62,7 @@ export type LogicalChecker void | Promise; + ) => any | Promise; conditionalFilter?: ED[T]['Update']['filter'] | ((operation: ED[T]['Operation'], context: Cxt, option: OperateOption) => ED[T]['Update']['filter']); }; @@ -76,7 +76,7 @@ export type LogicalRelationChecker void | Promise; + ) => any | Promise; conditionalFilter?: ED[T]['Update']['filter'] | ((operation: ED[T]['Operation'], context: Cxt, option: OperateOption) => ED[T]['Update']['filter']); }; diff --git a/src/types/Storage.ts b/src/types/Storage.ts index 6b4f1ec..84190a3 100644 --- a/src/types/Storage.ts +++ b/src/types/Storage.ts @@ -27,7 +27,7 @@ export interface Index { export interface Attribute { type: DataType | Ref; params?: DataTypeParams; - ref?: string; + ref?: string | string[]; onRefDelete?: 'delete' | 'setNull' | 'ignore'; default?: string | number | boolean; notNull?: boolean; diff --git a/src/types/Trigger.ts b/src/types/Trigger.ts index 8cae331..75e91d8 100644 --- a/src/types/Trigger.ts +++ b/src/types/Trigger.ts @@ -5,6 +5,15 @@ import { SyncContext } from "../store/SyncRowStore"; import { EntityDict, OperateOption } from "../types/Entity"; import { EntityShape } from "../types/Entity"; +/** + * 优先级越小,越早执行。定义在1~99之间 + */ +export const TRIGGER_DEFAULT_PRIORITY = 50; +export const TRIGGER_MIN_PRIORITY = 1; +export const TRIGGER_MAX_PRIORITY = 99; +export const DATA_CHECKER_DEFAULT_PRIORITY = 60; +export const CHECKER_DEFAULT_PRIORITY = 99; + interface TriggerBase { checkerType?: CheckerType; entity: T;