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.
@@ -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-frontend-base/lib/context/BackendRuntimeContext';
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
- for (const entity in data) {
250
- let rows = data[entity];
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 insertRows = async (idx) => {
271
- const rows2 = rows.slice(idx, 1000);
272
- if (rows2.length > 0) {
273
- await this.dbStore.operate(entity, {
274
- data: rows,
275
- action: 'create',
276
- }, context, {
277
- dontCollect: true,
278
- dontCreateOper: true,
279
- blockTrigger: true,
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
- const now = Date.now();
545
- return process.env.NODE_ENV === 'development' ? now - 30 * 1000 : now - 120 * 1000;
561
+ return this.lastCheckpointTs;
546
562
  }
547
- checkpoint() {
548
- return this.dbStore.checkpoint(this.getCheckpointTs());
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-frontend-base/lib/context/BackendRuntimeContext';
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';
@@ -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-frontend-base/lib/context/BackendRuntimeContext';
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';
@@ -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-frontend-base/lib/context/BackendRuntimeContext';
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>;
@@ -406,7 +406,7 @@ class Synchronizer {
406
406
  makeCreateOperTrigger() {
407
407
  const { config } = this;
408
408
  const { remotes, self } = config;
409
- const entityActionMap = new Map();
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
- if (!entityActionMap.has(entity)) {
420
- entityActionMap.set(entity, {
421
- actions: new Set(),
422
- matchAllActions: false,
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
- else {
437
- // 如果当前定义是特定 action
438
- if (tracker.matchAllActions) {
439
- throw new Error(`PushEntityDef 配置错误:entity「${entity}」已定义所有 action,不能再定义特定 action「${actions.join(', ')}」`);
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-frontend-base/lib/context/BackendRuntimeContext';
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
@@ -1,3 +1,4 @@
1
1
  export { AppLoader } from './AppLoader';
2
2
  export { ClusterAppLoader } from './ClusterAppLoader';
3
3
  export * from './cluster/env';
4
+ export * from './upgrade';
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);
@@ -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-frontend-base/lib/context/BackendRuntimeContext';
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
  /**
@@ -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-frontend-base/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, };
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, };
@@ -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-frontend-base/lib/context/BackendRuntimeContext';
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": "4.1.29",
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": "^3.0.6",
28
- "oak-db": "^3.3.14",
29
- "oak-domain": "^5.1.36",
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"