oak-backend-base 4.1.29 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/AppLoader.d.ts +5 -1
- package/lib/AppLoader.js +49 -27
- package/lib/ClusterAppLoader.d.ts +1 -1
- package/lib/ClusterAppLoader.js +2 -0
- package/lib/DbStore.d.ts +1 -1
- package/lib/Synchronizer.d.ts +1 -1
- package/lib/Synchronizer.js +12 -27
- package/lib/cluster/DataSubscriber.d.ts +1 -1
- package/lib/index.d.ts +1 -0
- package/lib/index.js +1 -0
- package/lib/routines/update.d.ts +1 -1
- package/lib/types/Sync.d.ts +16 -16
- package/lib/upgrade.d.ts +50 -0
- package/lib/upgrade.js +262 -0
- package/lib/utils/dbPriority.d.ts +1 -1
- package/lib/utils/initializeData.d.ts +5 -0
- package/lib/utils/initializeData.js +61 -0
- package/package.json +7 -5
package/lib/AppLoader.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
2
2
|
import { AppLoader as GeneralAppLoader, Trigger, EntityDict, Watcher, OpRecord, FreeTimer, OperationResult, BaseTimer } from "oak-domain/lib/types";
|
|
3
|
-
import { BackendRuntimeContext } from 'oak-
|
|
3
|
+
import { BackendRuntimeContext } from 'oak-domain/lib/context/BackendRuntimeContext';
|
|
4
4
|
import { IncomingHttpHeaders, IncomingMessage } from 'http';
|
|
5
5
|
import { Namespace } from 'socket.io';
|
|
6
6
|
import DataSubscriber from './cluster/DataSubscriber';
|
|
@@ -8,6 +8,7 @@ import Synchronizer from './Synchronizer';
|
|
|
8
8
|
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
|
9
9
|
import { InternalErrorHandler } from './types';
|
|
10
10
|
import { AppDbStore } from './DbStore';
|
|
11
|
+
import { AppLoaderUpgradeOptions, AppLoaderUpgradeResult } from './upgrade';
|
|
11
12
|
export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends GeneralAppLoader<ED, Cxt> {
|
|
12
13
|
protected dbStore: AppDbStore<ED, Cxt>;
|
|
13
14
|
private aspectDict;
|
|
@@ -54,6 +55,7 @@ export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt exten
|
|
|
54
55
|
message?: string;
|
|
55
56
|
}>;
|
|
56
57
|
initialize(ifExists?: 'drop' | 'omit' | 'dropIfNotStatic'): Promise<void>;
|
|
58
|
+
upgrade(options?: AppLoaderUpgradeOptions): Promise<AppLoaderUpgradeResult>;
|
|
57
59
|
getStore(): AppDbStore<ED, Cxt>;
|
|
58
60
|
getEndpoints(prefix: string): [string, "post" | "get" | "put" | "delete", string, (params: Record<string, string>, headers: IncomingHttpHeaders, req: IncomingMessage, body?: any) => Promise<{
|
|
59
61
|
headers?: Record<string, string | string[]>;
|
|
@@ -87,7 +89,9 @@ export declare class AppLoader<ED extends EntityDict & BaseEntityDict, Cxt exten
|
|
|
87
89
|
*/
|
|
88
90
|
private selectForWBWatcher;
|
|
89
91
|
protected execWatcher(watcher: Watcher<ED, keyof ED, Cxt>): Promise<OperationResult<ED> | undefined>;
|
|
92
|
+
private lastCheckpointTs;
|
|
90
93
|
protected getCheckpointTs(): number;
|
|
94
|
+
protected setCheckpointTs(ts: number): void;
|
|
91
95
|
protected checkpoint(): Promise<number>;
|
|
92
96
|
startWatchers(): void;
|
|
93
97
|
protected execBaseTimer(timer: BaseTimer<ED, Cxt>, context: Cxt): Promise<OperationResult<ED>> | undefined;
|
package/lib/AppLoader.js
CHANGED
|
@@ -15,10 +15,11 @@ const dependencyBuilder_1 = require("oak-domain/lib/compiler/dependencyBuilder")
|
|
|
15
15
|
const DataSubscriber_1 = tslib_1.__importDefault(require("./cluster/DataSubscriber"));
|
|
16
16
|
const env_1 = require("./cluster/env");
|
|
17
17
|
const Synchronizer_1 = tslib_1.__importDefault(require("./Synchronizer"));
|
|
18
|
-
const i18n_1 = tslib_1.__importDefault(require("oak-domain/lib/data/i18n"));
|
|
19
18
|
const requirePrj_1 = tslib_1.__importDefault(require("./utils/requirePrj"));
|
|
20
19
|
const dbPriority_1 = require("./utils/dbPriority");
|
|
21
20
|
const DbStore_1 = require("./DbStore");
|
|
21
|
+
const upgrade_1 = require("./upgrade");
|
|
22
|
+
const initializeData_1 = require("./utils/initializeData");
|
|
22
23
|
class AppLoader extends types_1.AppLoader {
|
|
23
24
|
dbStore;
|
|
24
25
|
aspectDict;
|
|
@@ -241,13 +242,11 @@ class AppLoader extends types_1.AppLoader {
|
|
|
241
242
|
async initialize(ifExists) {
|
|
242
243
|
await this.dbStore.initialize({ ifExists });
|
|
243
244
|
const data = this.requireSth('lib/data/index');
|
|
244
|
-
// oak-domain中只有i18n
|
|
245
|
-
(0, assert_1.default)(data.i18n);
|
|
246
|
-
data.i18n.push(...i18n_1.default);
|
|
247
245
|
const context = this.contextBuilder(this.dbStore);
|
|
248
246
|
context.openRootMode();
|
|
249
|
-
|
|
250
|
-
|
|
247
|
+
const entities = (0, initializeData_1.getInitializationOrder)(data, this.dbStore.getSchema());
|
|
248
|
+
for (const entity of entities) {
|
|
249
|
+
const rows = data[entity];
|
|
251
250
|
if (rows.length > 0) {
|
|
252
251
|
await context.begin();
|
|
253
252
|
// 如果是static的对象,只要表中有数据就pass
|
|
@@ -267,23 +266,16 @@ class AppLoader extends types_1.AppLoader {
|
|
|
267
266
|
}
|
|
268
267
|
// 再插入所有的行
|
|
269
268
|
try {
|
|
270
|
-
const
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
});
|
|
281
|
-
if (rows2.length === 1000) {
|
|
282
|
-
await insertRows(idx + 1000);
|
|
283
|
-
}
|
|
284
|
-
}
|
|
285
|
-
};
|
|
286
|
-
await insertRows(0);
|
|
269
|
+
for (const rows2 of (0, initializeData_1.chunkRows)(rows)) {
|
|
270
|
+
await this.dbStore.operate(entity, {
|
|
271
|
+
data: rows2,
|
|
272
|
+
action: 'create',
|
|
273
|
+
}, context, {
|
|
274
|
+
dontCollect: true,
|
|
275
|
+
dontCreateOper: true,
|
|
276
|
+
blockTrigger: true,
|
|
277
|
+
});
|
|
278
|
+
}
|
|
287
279
|
await context.commit();
|
|
288
280
|
console.log(`data in ${entity} initialized, ${rows.length} rows inserted`);
|
|
289
281
|
}
|
|
@@ -296,6 +288,30 @@ class AppLoader extends types_1.AppLoader {
|
|
|
296
288
|
}
|
|
297
289
|
// await this.dbStore.disconnect(); // 不需要马上断开连接,在initialize后可能还会有操作,unmount时会断开
|
|
298
290
|
}
|
|
291
|
+
async upgrade(options = {}) {
|
|
292
|
+
const plan = await this.dbStore.makeUpgradePlan({
|
|
293
|
+
largeTableRowThreshold: options.largeTableRowThreshold,
|
|
294
|
+
});
|
|
295
|
+
let outputDir;
|
|
296
|
+
let files;
|
|
297
|
+
if (options.outputDir) {
|
|
298
|
+
outputDir = (0, path_1.resolve)(this.path, options.outputDir);
|
|
299
|
+
files = await (0, upgrade_1.writeUpgradeArtifacts)(plan, outputDir, this.dbStore.supportsTransactionalDdl());
|
|
300
|
+
}
|
|
301
|
+
const executed = await (0, upgrade_1.applyUpgradePlan)(this.dbStore, plan, options);
|
|
302
|
+
const remainingPlan = executed.total > 0
|
|
303
|
+
? await this.dbStore.makeUpgradePlan({
|
|
304
|
+
largeTableRowThreshold: options.largeTableRowThreshold,
|
|
305
|
+
})
|
|
306
|
+
: undefined;
|
|
307
|
+
return {
|
|
308
|
+
plan,
|
|
309
|
+
remainingPlan,
|
|
310
|
+
outputDir,
|
|
311
|
+
files,
|
|
312
|
+
executed,
|
|
313
|
+
};
|
|
314
|
+
}
|
|
299
315
|
getStore() {
|
|
300
316
|
return this.dbStore;
|
|
301
317
|
}
|
|
@@ -540,12 +556,18 @@ class AppLoader extends types_1.AppLoader {
|
|
|
540
556
|
// 这里只需要清理被执行的行,因为被跳过的行本来就不是这一次执行被占用的。
|
|
541
557
|
}
|
|
542
558
|
}
|
|
559
|
+
lastCheckpointTs = 0;
|
|
543
560
|
getCheckpointTs() {
|
|
544
|
-
|
|
545
|
-
return process.env.NODE_ENV === 'development' ? now - 30 * 1000 : now - 120 * 1000;
|
|
561
|
+
return this.lastCheckpointTs;
|
|
546
562
|
}
|
|
547
|
-
|
|
548
|
-
|
|
563
|
+
setCheckpointTs(ts) {
|
|
564
|
+
this.lastCheckpointTs = ts;
|
|
565
|
+
}
|
|
566
|
+
async checkpoint() {
|
|
567
|
+
const now = Date.now();
|
|
568
|
+
const count = await this.dbStore.checkpoint(this.getCheckpointTs());
|
|
569
|
+
this.setCheckpointTs(now);
|
|
570
|
+
return count;
|
|
549
571
|
}
|
|
550
572
|
startWatchers() {
|
|
551
573
|
const watchers = this.requireSth('lib/watchers/index');
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
2
2
|
import { EntityDict, OperationResult, Trigger, Watcher, FreeTimer, BaseTimer } from 'oak-domain/lib/types';
|
|
3
|
-
import { BackendRuntimeContext } from 'oak-
|
|
3
|
+
import { BackendRuntimeContext } from 'oak-domain/lib/context/BackendRuntimeContext';
|
|
4
4
|
import { AppLoader } from './AppLoader';
|
|
5
5
|
import { Namespace } from 'socket.io';
|
|
6
6
|
import { Socket } from 'socket.io-client';
|
package/lib/ClusterAppLoader.js
CHANGED
|
@@ -183,6 +183,7 @@ class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
|
183
183
|
async checkpoint() {
|
|
184
184
|
const { instanceCount, instanceId } = (0, env_1.getClusterInfo)();
|
|
185
185
|
let count = 0;
|
|
186
|
+
const now = Date.now();
|
|
186
187
|
for (const name in this.commitTriggers) {
|
|
187
188
|
if (this.commitTriggers[name]) {
|
|
188
189
|
count += await this.dbStore.independentCheckPoint(name, this.getCheckpointTs(), instanceCount, instanceId);
|
|
@@ -191,6 +192,7 @@ class ClusterAppLoader extends AppLoader_1.AppLoader {
|
|
|
191
192
|
count += await this.dbStore.independentCheckPoint(name, this.getCheckpointTs());
|
|
192
193
|
}
|
|
193
194
|
}
|
|
195
|
+
this.setCheckpointTs(now);
|
|
194
196
|
return count;
|
|
195
197
|
}
|
|
196
198
|
}
|
package/lib/DbStore.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { DbConfiguration } from 'oak-db/src/types/configuration';
|
|
2
2
|
import { EntityDict, StorageSchema, Trigger, Checker, SelectFreeEntities, UpdateFreeDict, AuthDeduceRelationMap, VolatileTrigger, OperateOption } from 'oak-domain/lib/types';
|
|
3
3
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
4
|
-
import { BackendRuntimeContext } from 'oak-
|
|
4
|
+
import { BackendRuntimeContext } from 'oak-domain/lib/context/BackendRuntimeContext';
|
|
5
5
|
import { DbTypeSymbol } from './utils/dbPriority';
|
|
6
6
|
import { CascadeStore } from 'oak-domain/lib/store/CascadeStore';
|
|
7
7
|
import { DbStore } from 'oak-db/lib/types/dbStore';
|
package/lib/Synchronizer.d.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { EntityDict, StorageSchema, EndpointItem, SyncConfig } from 'oak-domain/lib/types';
|
|
2
2
|
import { VolatileTrigger } from 'oak-domain/lib/types/Trigger';
|
|
3
3
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
4
|
-
import { BackendRuntimeContext } from 'oak-
|
|
4
|
+
import { BackendRuntimeContext } from 'oak-domain/lib/context/BackendRuntimeContext';
|
|
5
5
|
export type FetchFn = (url: string, options: {
|
|
6
6
|
method: string;
|
|
7
7
|
headers: Record<string, string>;
|
package/lib/Synchronizer.js
CHANGED
|
@@ -406,7 +406,7 @@ class Synchronizer {
|
|
|
406
406
|
makeCreateOperTrigger() {
|
|
407
407
|
const { config } = this;
|
|
408
408
|
const { remotes, self } = config;
|
|
409
|
-
const
|
|
409
|
+
const checkActionsMap = {};
|
|
410
410
|
// 根据remotes定义,建立从entity到需要同步的远端结点信息的Map
|
|
411
411
|
remotes.forEach((remote) => {
|
|
412
412
|
const { getPushInfo, pushEntities: pushEntityDefs, endpoint, pathToUser, relationName: rnRemote, onFailed, timeout } = remote;
|
|
@@ -416,34 +416,19 @@ class Synchronizer {
|
|
|
416
416
|
for (const def of pushEntityDefs) {
|
|
417
417
|
const { pathToRemoteEntity, pathToSelfEntity, relationName, recursive, entity, actions, onSynchronized } = def;
|
|
418
418
|
pushEntities.push(entity);
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
const tracker = entityActionMap.get(entity);
|
|
426
|
-
//如果当前定义是 wildcard(所有 action)
|
|
427
|
-
if (!actions || actions.length === 0) {
|
|
428
|
-
if (tracker.matchAllActions) {
|
|
429
|
-
throw new Error(`PushEntityDef 配置错误:entity「${entity}」的所有 action 被定义了多次`);
|
|
430
|
-
}
|
|
431
|
-
if (tracker.actions.size > 0) {
|
|
432
|
-
throw new Error(`PushEntityDef 配置错误:entity「${entity}」已定义特定 action「${Array.from(tracker.actions).join(', ')}」,不能再定义所有 action`);
|
|
433
|
-
}
|
|
434
|
-
tracker.matchAllActions = true;
|
|
419
|
+
(0, assert_1.default)(pathToRemoteEntity, 'pushEntityDef必须定义pathToRemoteEntity');
|
|
420
|
+
(0, assert_1.default)(pathToSelfEntity, 'pushEntityDef必须定义pathToSelfEntity');
|
|
421
|
+
(0, assert_1.default)(actions && actions.length > 0, 'pushEntityDef必须定义actions,并且长度必须大于0');
|
|
422
|
+
const checkKey = `${String(entity)}-${pathToRemoteEntity}-${pathToSelfEntity}`;
|
|
423
|
+
if (!checkActionsMap[checkKey]) {
|
|
424
|
+
checkActionsMap[checkKey] = new Set();
|
|
435
425
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
if (
|
|
439
|
-
throw new Error(`
|
|
440
|
-
}
|
|
441
|
-
for (const action of actions) {
|
|
442
|
-
if (tracker.actions.has(action)) {
|
|
443
|
-
throw new Error(`PushEntityDef 配置错误:entity「${entity}」的action「${action}」被定义了多次`);
|
|
444
|
-
}
|
|
445
|
-
tracker.actions.add(action);
|
|
426
|
+
const checkActions = checkActionsMap[checkKey];
|
|
427
|
+
for (const action of actions) {
|
|
428
|
+
if (checkActions.has(action)) {
|
|
429
|
+
throw new Error(`pushEntityDef中发现重复的entity/pathToRemoteEntity/pathToSelfEntity/action组合,重复的action是「${action}」,entity是「${String(entity)}」,pathToRemoteEntity是「${pathToRemoteEntity}」,pathToSelfEntity是「${pathToSelfEntity}」`);
|
|
446
430
|
}
|
|
431
|
+
checkActions.add(action);
|
|
447
432
|
}
|
|
448
433
|
const relationName2 = relationName || rnRemote;
|
|
449
434
|
const path2 = pathToUser ? `${pathToRemoteEntity}.${pathToUser}` : pathToRemoteEntity;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EntityDict, OpRecord } from 'oak-domain/lib/types';
|
|
2
2
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
3
|
-
import { BackendRuntimeContext } from 'oak-
|
|
3
|
+
import { BackendRuntimeContext } from 'oak-domain/lib/context/BackendRuntimeContext';
|
|
4
4
|
import { Namespace } from 'socket.io';
|
|
5
5
|
/**
|
|
6
6
|
* 集群行为备忘:
|
package/lib/index.d.ts
CHANGED
package/lib/index.js
CHANGED
|
@@ -7,3 +7,4 @@ Object.defineProperty(exports, "AppLoader", { enumerable: true, get: function ()
|
|
|
7
7
|
var ClusterAppLoader_1 = require("./ClusterAppLoader");
|
|
8
8
|
Object.defineProperty(exports, "ClusterAppLoader", { enumerable: true, get: function () { return ClusterAppLoader_1.ClusterAppLoader; } });
|
|
9
9
|
tslib_1.__exportStar(require("./cluster/env"), exports);
|
|
10
|
+
tslib_1.__exportStar(require("./upgrade"), exports);
|
package/lib/routines/update.d.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EntityDict as TypeEntityDict, StorageSchema } from 'oak-domain/lib/types';
|
|
2
2
|
import { AsyncContext } from 'oak-domain/lib/store/AsyncRowStore';
|
|
3
|
-
import BackendRuntimeContext from 'oak-
|
|
3
|
+
import BackendRuntimeContext from 'oak-domain/lib/context/BackendRuntimeContext';
|
|
4
4
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
5
5
|
type EntityDict = TypeEntityDict & BaseEntityDict;
|
|
6
6
|
/**
|
package/lib/types/Sync.d.ts
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
import { EntityDict } from 'oak-domain/lib/types';
|
|
2
|
-
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
3
|
-
import { BackendRuntimeContext } from 'oak-
|
|
4
|
-
import { RemotePushInfo, RemotePullInfo, SelfEncryptInfo, SyncRemoteConfigBase, SyncSelfConfigBase, SyncConfig } from 'oak-domain/lib/types/Sync';
|
|
5
|
-
interface SyncRemoteConfigWrapper<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends SyncRemoteConfigBase<ED, Cxt> {
|
|
6
|
-
getRemotePushInfo: (userId: string) => Promise<RemotePushInfo>;
|
|
7
|
-
getRemotePullInfo: (id: string) => Promise<RemotePullInfo>;
|
|
8
|
-
}
|
|
9
|
-
interface SyncSelfConfigWrapper<ED extends EntityDict & BaseEntityDict> extends SyncSelfConfigBase<ED> {
|
|
10
|
-
getSelfEncryptInfo: () => Promise<SelfEncryptInfo>;
|
|
11
|
-
}
|
|
12
|
-
export interface SyncConfigWrapper<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> {
|
|
13
|
-
self: SyncSelfConfigWrapper<ED>;
|
|
14
|
-
remotes: Array<SyncRemoteConfigWrapper<ED, Cxt>>;
|
|
15
|
-
}
|
|
16
|
-
export { RemotePushInfo, RemotePullInfo, SelfEncryptInfo, SyncConfig, };
|
|
1
|
+
import { EntityDict } from 'oak-domain/lib/types';
|
|
2
|
+
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
3
|
+
import { BackendRuntimeContext } from 'oak-domain/lib/context/BackendRuntimeContext';
|
|
4
|
+
import { RemotePushInfo, RemotePullInfo, SelfEncryptInfo, SyncRemoteConfigBase, SyncSelfConfigBase, SyncConfig } from 'oak-domain/lib/types/Sync';
|
|
5
|
+
interface SyncRemoteConfigWrapper<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> extends SyncRemoteConfigBase<ED, Cxt> {
|
|
6
|
+
getRemotePushInfo: (userId: string) => Promise<RemotePushInfo>;
|
|
7
|
+
getRemotePullInfo: (id: string) => Promise<RemotePullInfo>;
|
|
8
|
+
}
|
|
9
|
+
interface SyncSelfConfigWrapper<ED extends EntityDict & BaseEntityDict> extends SyncSelfConfigBase<ED> {
|
|
10
|
+
getSelfEncryptInfo: () => Promise<SelfEncryptInfo>;
|
|
11
|
+
}
|
|
12
|
+
export interface SyncConfigWrapper<ED extends EntityDict & BaseEntityDict, Cxt extends BackendRuntimeContext<ED>> {
|
|
13
|
+
self: SyncSelfConfigWrapper<ED>;
|
|
14
|
+
remotes: Array<SyncRemoteConfigWrapper<ED, Cxt>>;
|
|
15
|
+
}
|
|
16
|
+
export { RemotePushInfo, RemotePullInfo, SelfEncryptInfo, SyncConfig, };
|
package/lib/upgrade.d.ts
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { MigrationPlanningOptions, Plan } from 'oak-db';
|
|
2
|
+
export interface UpgradeExecutionOptions {
|
|
3
|
+
execute?: boolean;
|
|
4
|
+
}
|
|
5
|
+
export interface AppLoaderUpgradeOptions extends UpgradeExecutionOptions, MigrationPlanningOptions {
|
|
6
|
+
outputDir?: string;
|
|
7
|
+
}
|
|
8
|
+
export interface UpgradeOutputFiles {
|
|
9
|
+
migrationSql: string;
|
|
10
|
+
rollbackSql: string;
|
|
11
|
+
summary: string;
|
|
12
|
+
tableChanges: string;
|
|
13
|
+
warnings: string;
|
|
14
|
+
renameCandidates: string;
|
|
15
|
+
}
|
|
16
|
+
export interface UpgradeExecutionSummary {
|
|
17
|
+
prepareSql: number;
|
|
18
|
+
forwardSql: number;
|
|
19
|
+
onlineSql: number;
|
|
20
|
+
manualSql: number;
|
|
21
|
+
backwardSql: number;
|
|
22
|
+
total: number;
|
|
23
|
+
}
|
|
24
|
+
export interface AppLoaderUpgradeResult {
|
|
25
|
+
plan: Plan;
|
|
26
|
+
remainingPlan?: Plan;
|
|
27
|
+
outputDir?: string;
|
|
28
|
+
files?: UpgradeOutputFiles;
|
|
29
|
+
executed: UpgradeExecutionSummary;
|
|
30
|
+
}
|
|
31
|
+
type SqlExecutor = {
|
|
32
|
+
exec(sql: string, txnId?: string): Promise<void>;
|
|
33
|
+
begin(): Promise<string>;
|
|
34
|
+
commit(txnId: string): Promise<void>;
|
|
35
|
+
rollback(txnId: string): Promise<void>;
|
|
36
|
+
supportsTransactionalDdl(): boolean;
|
|
37
|
+
};
|
|
38
|
+
type SqlCategory = keyof Omit<UpgradeExecutionSummary, 'total'>;
|
|
39
|
+
type OrderedSqlStep = {
|
|
40
|
+
sql: string;
|
|
41
|
+
category: SqlCategory;
|
|
42
|
+
group: number;
|
|
43
|
+
table?: string;
|
|
44
|
+
order: number;
|
|
45
|
+
};
|
|
46
|
+
export declare function buildOrderedUpgradeSteps(plan: Plan): OrderedSqlStep[];
|
|
47
|
+
export declare function buildOrderedRollbackSteps(plan: Plan): OrderedSqlStep[];
|
|
48
|
+
export declare function writeUpgradeArtifacts(plan: Plan, outputDir: string, supportsTransactionalDdl?: boolean): Promise<UpgradeOutputFiles>;
|
|
49
|
+
export declare function applyUpgradePlan(executor: SqlExecutor, plan: Plan, options?: UpgradeExecutionOptions): Promise<UpgradeExecutionSummary>;
|
|
50
|
+
export {};
|
package/lib/upgrade.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.buildOrderedUpgradeSteps = buildOrderedUpgradeSteps;
|
|
4
|
+
exports.buildOrderedRollbackSteps = buildOrderedRollbackSteps;
|
|
5
|
+
exports.writeUpgradeArtifacts = writeUpgradeArtifacts;
|
|
6
|
+
exports.applyUpgradePlan = applyUpgradePlan;
|
|
7
|
+
const promises_1 = require("fs/promises");
|
|
8
|
+
const path_1 = require("path");
|
|
9
|
+
const categoryOrder = [
|
|
10
|
+
'prepareSql',
|
|
11
|
+
'forwardSql',
|
|
12
|
+
'onlineSql',
|
|
13
|
+
'manualSql',
|
|
14
|
+
'backwardSql',
|
|
15
|
+
];
|
|
16
|
+
const executableCategoryOrder = [
|
|
17
|
+
'prepareSql',
|
|
18
|
+
'forwardSql',
|
|
19
|
+
'onlineSql',
|
|
20
|
+
'manualSql',
|
|
21
|
+
];
|
|
22
|
+
function makeExecutionSummary() {
|
|
23
|
+
return {
|
|
24
|
+
prepareSql: 0,
|
|
25
|
+
forwardSql: 0,
|
|
26
|
+
onlineSql: 0,
|
|
27
|
+
manualSql: 0,
|
|
28
|
+
backwardSql: 0,
|
|
29
|
+
total: 0,
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
function extractTableName(sql) {
|
|
33
|
+
const patterns = [
|
|
34
|
+
/alter\s+table\s+[`"]([^`"]+)[`"]/i,
|
|
35
|
+
/create\s+table(?:\s+if\s+not\s+exists)?\s+[`"]([^`"]+)[`"]/i,
|
|
36
|
+
/drop\s+table(?:\s+if\s+exists)?\s+[`"]([^`"]+)[`"]/i,
|
|
37
|
+
/create\s+(?:unique\s+)?index(?:\s+concurrently)?(?:\s+if\s+not\s+exists)?\s+[`"][^`"]+[`"]\s+on\s+[`"]([^`"]+)[`"]/i,
|
|
38
|
+
];
|
|
39
|
+
for (const pattern of patterns) {
|
|
40
|
+
const match = sql.match(pattern);
|
|
41
|
+
if (match) {
|
|
42
|
+
return match[1];
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
function isCreateTableSql(sql) {
|
|
48
|
+
return /create\s+table/i.test(sql);
|
|
49
|
+
}
|
|
50
|
+
function isDropTableSql(sql) {
|
|
51
|
+
return /drop\s+table/i.test(sql);
|
|
52
|
+
}
|
|
53
|
+
function isForeignKeyDropSql(sql) {
|
|
54
|
+
return /drop\s+(?:foreign\s+key|constraint)/i.test(sql);
|
|
55
|
+
}
|
|
56
|
+
function isForeignKeyAddSql(sql) {
|
|
57
|
+
return /add\s+constraint\s+.*foreign\s+key|add\s+foreign\s+key/i.test(sql);
|
|
58
|
+
}
|
|
59
|
+
function isColumnSql(sql) {
|
|
60
|
+
return /add\s+column|modify\s+column|alter\s+column|rename\s+column/i.test(sql);
|
|
61
|
+
}
|
|
62
|
+
function isIndexSql(sql) {
|
|
63
|
+
return /rename\s+index|create\s+(?:unique\s+)?index|add\s+(?:unique\s+|fulltext\s+|spatial\s+)?index|drop\s+index|alter\s+index/i.test(sql);
|
|
64
|
+
}
|
|
65
|
+
function getCategoryPriority(category) {
|
|
66
|
+
switch (category) {
|
|
67
|
+
case 'prepareSql':
|
|
68
|
+
return 0;
|
|
69
|
+
case 'manualSql':
|
|
70
|
+
return 1;
|
|
71
|
+
case 'forwardSql':
|
|
72
|
+
return 2;
|
|
73
|
+
case 'onlineSql':
|
|
74
|
+
return 3;
|
|
75
|
+
case 'backwardSql':
|
|
76
|
+
return 4;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
function classifySqlGroup(sql, category) {
|
|
80
|
+
if (category === 'prepareSql') {
|
|
81
|
+
return 0;
|
|
82
|
+
}
|
|
83
|
+
if (category === 'manualSql') {
|
|
84
|
+
// manualSql 往往已经带着 planner 计算好的执行顺序,
|
|
85
|
+
// 例如 PostgreSQL enum 重建需要 rename/create/alter/drop 保持原样串行。
|
|
86
|
+
return 0;
|
|
87
|
+
}
|
|
88
|
+
if (category === 'backwardSql') {
|
|
89
|
+
if (isForeignKeyDropSql(sql)) {
|
|
90
|
+
return 60;
|
|
91
|
+
}
|
|
92
|
+
if (isIndexSql(sql)) {
|
|
93
|
+
return 70;
|
|
94
|
+
}
|
|
95
|
+
if (/drop\s+column/i.test(sql)) {
|
|
96
|
+
return 80;
|
|
97
|
+
}
|
|
98
|
+
if (isDropTableSql(sql)) {
|
|
99
|
+
return 90;
|
|
100
|
+
}
|
|
101
|
+
return 85;
|
|
102
|
+
}
|
|
103
|
+
if (isCreateTableSql(sql)) {
|
|
104
|
+
return 10;
|
|
105
|
+
}
|
|
106
|
+
if (isForeignKeyDropSql(sql)) {
|
|
107
|
+
return 15;
|
|
108
|
+
}
|
|
109
|
+
if (isColumnSql(sql)) {
|
|
110
|
+
return 20;
|
|
111
|
+
}
|
|
112
|
+
if (isIndexSql(sql) || category === 'onlineSql') {
|
|
113
|
+
return 30;
|
|
114
|
+
}
|
|
115
|
+
if (isForeignKeyAddSql(sql)) {
|
|
116
|
+
return 40;
|
|
117
|
+
}
|
|
118
|
+
return 50;
|
|
119
|
+
}
|
|
120
|
+
function buildOrderedSteps(plan, categories) {
|
|
121
|
+
let order = 0;
|
|
122
|
+
const steps = [];
|
|
123
|
+
for (const category of categories) {
|
|
124
|
+
for (const sql of plan[category]) {
|
|
125
|
+
steps.push({
|
|
126
|
+
sql,
|
|
127
|
+
category,
|
|
128
|
+
group: classifySqlGroup(sql, category),
|
|
129
|
+
table: extractTableName(sql),
|
|
130
|
+
order: order++,
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
return steps.sort((left, right) => getCategoryPriority(left.category) - getCategoryPriority(right.category)
|
|
135
|
+
|| left.group - right.group
|
|
136
|
+
|| left.order - right.order);
|
|
137
|
+
}
|
|
138
|
+
function buildOrderedUpgradeSteps(plan) {
|
|
139
|
+
return buildOrderedSteps(plan, executableCategoryOrder);
|
|
140
|
+
}
|
|
141
|
+
function buildOrderedRollbackSteps(plan) {
|
|
142
|
+
return buildOrderedSteps(plan, ['backwardSql']);
|
|
143
|
+
}
|
|
144
|
+
function toSqlFileContent(sqls) {
|
|
145
|
+
return sqls.length > 0 ? `${sqls.join('\n')}\n` : '';
|
|
146
|
+
}
|
|
147
|
+
function buildArtifactSqlStatements(steps, supportsTransactionalDdl = false) {
|
|
148
|
+
const sqls = [];
|
|
149
|
+
let openedTransaction = false;
|
|
150
|
+
const flushTransaction = () => {
|
|
151
|
+
if (!openedTransaction) {
|
|
152
|
+
return;
|
|
153
|
+
}
|
|
154
|
+
sqls.push('COMMIT;');
|
|
155
|
+
openedTransaction = false;
|
|
156
|
+
};
|
|
157
|
+
for (const step of steps) {
|
|
158
|
+
const transactional = supportsTransactionalDdl
|
|
159
|
+
&& step.category !== 'prepareSql'
|
|
160
|
+
&& step.category !== 'onlineSql';
|
|
161
|
+
if (transactional) {
|
|
162
|
+
if (!openedTransaction) {
|
|
163
|
+
sqls.push('BEGIN;');
|
|
164
|
+
openedTransaction = true;
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
else {
|
|
168
|
+
flushTransaction();
|
|
169
|
+
}
|
|
170
|
+
sqls.push(step.sql);
|
|
171
|
+
}
|
|
172
|
+
flushTransaction();
|
|
173
|
+
return sqls;
|
|
174
|
+
}
|
|
175
|
+
function getTableChanges(plan) {
|
|
176
|
+
return plan.tableChanges || [];
|
|
177
|
+
}
|
|
178
|
+
function buildTableChangeSummary(plan) {
|
|
179
|
+
const tableChanges = getTableChanges(plan);
|
|
180
|
+
return {
|
|
181
|
+
total: tableChanges.length,
|
|
182
|
+
create: tableChanges.filter((item) => item.lifecycle === 'create').length,
|
|
183
|
+
alter: tableChanges.filter((item) => item.lifecycle === 'alter').length,
|
|
184
|
+
drop: tableChanges.filter((item) => item.lifecycle === 'drop').length,
|
|
185
|
+
};
|
|
186
|
+
}
|
|
187
|
+
async function writeUpgradeArtifacts(plan, outputDir, supportsTransactionalDdl = false) {
|
|
188
|
+
await (0, promises_1.mkdir)(outputDir, {
|
|
189
|
+
recursive: true,
|
|
190
|
+
});
|
|
191
|
+
const files = {
|
|
192
|
+
migrationSql: (0, path_1.join)(outputDir, 'migration.sql'),
|
|
193
|
+
rollbackSql: (0, path_1.join)(outputDir, 'rollback.sql'),
|
|
194
|
+
summary: (0, path_1.join)(outputDir, 'summary.json'),
|
|
195
|
+
tableChanges: (0, path_1.join)(outputDir, 'table-changes.json'),
|
|
196
|
+
warnings: (0, path_1.join)(outputDir, 'warnings.json'),
|
|
197
|
+
renameCandidates: (0, path_1.join)(outputDir, 'rename-candidates.json'),
|
|
198
|
+
};
|
|
199
|
+
const orderedSql = buildArtifactSqlStatements(buildOrderedUpgradeSteps(plan), supportsTransactionalDdl);
|
|
200
|
+
const rollbackSql = buildArtifactSqlStatements(buildOrderedRollbackSteps(plan), supportsTransactionalDdl);
|
|
201
|
+
await Promise.all([
|
|
202
|
+
(0, promises_1.writeFile)(files.migrationSql, toSqlFileContent(orderedSql), 'utf8'),
|
|
203
|
+
(0, promises_1.writeFile)(files.rollbackSql, toSqlFileContent(rollbackSql), 'utf8'),
|
|
204
|
+
(0, promises_1.writeFile)(files.summary, `${JSON.stringify({
|
|
205
|
+
summary: plan.summary,
|
|
206
|
+
tables: buildTableChangeSummary(plan),
|
|
207
|
+
generatedAt: new Date().toISOString(),
|
|
208
|
+
}, null, 2)}\n`, 'utf8'),
|
|
209
|
+
(0, promises_1.writeFile)(files.tableChanges, `${JSON.stringify(getTableChanges(plan), null, 2)}\n`, 'utf8'),
|
|
210
|
+
(0, promises_1.writeFile)(files.warnings, `${JSON.stringify(plan.warnings, null, 2)}\n`, 'utf8'),
|
|
211
|
+
(0, promises_1.writeFile)(files.renameCandidates, `${JSON.stringify(plan.renameCandidates, null, 2)}\n`, 'utf8'),
|
|
212
|
+
]);
|
|
213
|
+
return files;
|
|
214
|
+
}
|
|
215
|
+
async function executeOrderedSqlSteps(executor, steps, summary) {
|
|
216
|
+
if (steps.length === 0) {
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
let txnId;
|
|
220
|
+
const supportsTransactionalDdl = executor.supportsTransactionalDdl();
|
|
221
|
+
const flushTransaction = async () => {
|
|
222
|
+
if (!txnId) {
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
await executor.commit(txnId);
|
|
226
|
+
txnId = undefined;
|
|
227
|
+
};
|
|
228
|
+
try {
|
|
229
|
+
for (const step of steps) {
|
|
230
|
+
const transactional = supportsTransactionalDdl
|
|
231
|
+
&& step.category !== 'prepareSql'
|
|
232
|
+
&& step.category !== 'onlineSql';
|
|
233
|
+
if (!transactional) {
|
|
234
|
+
await flushTransaction();
|
|
235
|
+
await executor.exec(step.sql);
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
if (!txnId) {
|
|
239
|
+
txnId = await executor.begin();
|
|
240
|
+
}
|
|
241
|
+
await executor.exec(step.sql, txnId);
|
|
242
|
+
}
|
|
243
|
+
summary[step.category] += 1;
|
|
244
|
+
summary.total += 1;
|
|
245
|
+
}
|
|
246
|
+
await flushTransaction();
|
|
247
|
+
}
|
|
248
|
+
catch (err) {
|
|
249
|
+
if (txnId) {
|
|
250
|
+
await executor.rollback(txnId);
|
|
251
|
+
}
|
|
252
|
+
throw err;
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
async function applyUpgradePlan(executor, plan, options = {}) {
|
|
256
|
+
const summary = makeExecutionSummary();
|
|
257
|
+
if (!options.execute) {
|
|
258
|
+
return summary;
|
|
259
|
+
}
|
|
260
|
+
await executeOrderedSqlSteps(executor, buildOrderedUpgradeSteps(plan), summary);
|
|
261
|
+
return summary;
|
|
262
|
+
}
|
|
@@ -2,7 +2,7 @@ import { MysqlStore, PostgreSQLStore } from "oak-db";
|
|
|
2
2
|
import { DbConfiguration } from "oak-db/src/types/configuration";
|
|
3
3
|
import { EntityDict, StorageSchema } from 'oak-domain/lib/types';
|
|
4
4
|
import { EntityDict as BaseEntityDict } from 'oak-domain/lib/base-app-domain';
|
|
5
|
-
import { BackendRuntimeContext } from 'oak-
|
|
5
|
+
import { BackendRuntimeContext } from 'oak-domain/lib/context/BackendRuntimeContext';
|
|
6
6
|
import { CascadeStore } from "oak-domain/lib/store/CascadeStore";
|
|
7
7
|
import { DbStore } from "oak-db/lib/types/dbStore";
|
|
8
8
|
/**
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
import { EntityDict, StorageSchema } from 'oak-domain/lib/types';
|
|
2
|
+
type SerializedData = Record<string, unknown[]>;
|
|
3
|
+
export declare function getInitializationOrder<ED extends EntityDict>(data: SerializedData, schema: StorageSchema<ED>): string[];
|
|
4
|
+
export declare function chunkRows<T>(rows: T[], size?: number): Generator<T[], void, unknown>;
|
|
5
|
+
export {};
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.getInitializationOrder = getInitializationOrder;
|
|
4
|
+
exports.chunkRows = chunkRows;
|
|
5
|
+
function collectEntityDependencies(entity, schema, entitySet) {
|
|
6
|
+
const desc = schema[entity];
|
|
7
|
+
if (!desc) {
|
|
8
|
+
return new Set();
|
|
9
|
+
}
|
|
10
|
+
const dependencies = new Set();
|
|
11
|
+
for (const attr of Object.values(desc.attributes || {})) {
|
|
12
|
+
if (attr.type !== 'ref' || !attr.ref) {
|
|
13
|
+
continue;
|
|
14
|
+
}
|
|
15
|
+
const refs = Array.isArray(attr.ref) ? attr.ref : [attr.ref];
|
|
16
|
+
for (const ref of refs) {
|
|
17
|
+
if (ref !== entity && entitySet.has(ref)) {
|
|
18
|
+
dependencies.add(ref);
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
return dependencies;
|
|
23
|
+
}
|
|
24
|
+
function getInitializationOrder(data, schema) {
|
|
25
|
+
const entities = Object.keys(data).filter((entity) => Array.isArray(data[entity]));
|
|
26
|
+
const entitySet = new Set(entities);
|
|
27
|
+
const dependencies = Object.fromEntries(entities.map((entity) => [
|
|
28
|
+
entity,
|
|
29
|
+
collectEntityDependencies(entity, schema, entitySet),
|
|
30
|
+
]));
|
|
31
|
+
const ordered = [];
|
|
32
|
+
const initialized = new Set();
|
|
33
|
+
const pending = new Set(entities);
|
|
34
|
+
while (pending.size > 0) {
|
|
35
|
+
let progressed = false;
|
|
36
|
+
for (const entity of entities) {
|
|
37
|
+
if (!pending.has(entity)) {
|
|
38
|
+
continue;
|
|
39
|
+
}
|
|
40
|
+
const deps = dependencies[entity];
|
|
41
|
+
if ([...deps].every((dep) => initialized.has(dep))) {
|
|
42
|
+
ordered.push(entity);
|
|
43
|
+
initialized.add(entity);
|
|
44
|
+
pending.delete(entity);
|
|
45
|
+
progressed = true;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
if (!progressed) {
|
|
49
|
+
const rest = entities.filter((entity) => pending.has(entity));
|
|
50
|
+
console.warn(`data initialization order has circular or unresolved dependencies, fallback to original order: ${rest.join(', ')}`);
|
|
51
|
+
ordered.push(...rest);
|
|
52
|
+
break;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
return ordered;
|
|
56
|
+
}
|
|
57
|
+
function* chunkRows(rows, size = 1000) {
|
|
58
|
+
for (let idx = 0; idx < rows.length; idx += size) {
|
|
59
|
+
yield rows.slice(idx, idx + size);
|
|
60
|
+
}
|
|
61
|
+
}
|
package/package.json
CHANGED
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "oak-backend-base",
|
|
3
|
-
"version": "
|
|
3
|
+
"version": "5.0.0",
|
|
4
4
|
"description": "oak-backend-base",
|
|
5
|
+
"oak": {
|
|
6
|
+
"package": true
|
|
7
|
+
},
|
|
5
8
|
"main": "lib/index",
|
|
6
9
|
"author": {
|
|
7
10
|
"name": "XuChang"
|
|
@@ -24,10 +27,9 @@
|
|
|
24
27
|
"mysql": "^2.18.1",
|
|
25
28
|
"mysql2": "^2.3.3",
|
|
26
29
|
"node-schedule": "^2.1.0",
|
|
27
|
-
"oak-common-aspect": "^
|
|
28
|
-
"oak-db": "^
|
|
29
|
-
"oak-domain": "^
|
|
30
|
-
"oak-frontend-base": "^5.3.46",
|
|
30
|
+
"oak-common-aspect": "^4.0.0",
|
|
31
|
+
"oak-db": "^4.0.1",
|
|
32
|
+
"oak-domain": "^6.0.0",
|
|
31
33
|
"socket.io": "^4.8.1",
|
|
32
34
|
"socket.io-client": "^4.7.2",
|
|
33
35
|
"uuid": "^8.3.2"
|