oak-backend-base 4.1.28 → 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,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
  /**
@@ -38,7 +38,7 @@ const getDbConfig = (path) => {
38
38
  // do nothing
39
39
  }
40
40
  }
41
- throw new Error(`没有找到数据库配置文件,请在configuration目录下添加任一配置文件:${Object.keys(exports.dbList).map(ele => `${ele}.json`).join('、')}`);
41
+ throw new Error(`没有找到数据库配置文件,请在configuration目录下添加任一配置文件:${Object.keys(exports.dbList).map(ele => `${ele}.json`).join('、')}, 当前环境: ${process.env.NODE_ENV}`);
42
42
  };
43
43
  exports.getDbConfig = getDbConfig;
44
44
  const getDbStoreClass = (config) => {
@@ -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.28",
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"
@@ -13,7 +16,10 @@
13
16
  "copy-files": "copyfiles -u 1 src/**/*.json lib/",
14
17
  "test": "ts-node test/test.ts",
15
18
  "test2": "ts-node test/testDbStore.ts",
16
- "build": "tsc && npm run copy-files"
19
+ "test:sync": "vitest run",
20
+ "test:sync:ui": "vitest --ui",
21
+ "build": "node ./scripts/build.js && npm run copy-files",
22
+ "make:test:domain": "ts-node scripts/makeTestDomain.ts"
17
23
  },
18
24
  "dependencies": {
19
25
  "@types/koa": "^2.15.0",
@@ -21,10 +27,9 @@
21
27
  "mysql": "^2.18.1",
22
28
  "mysql2": "^2.3.3",
23
29
  "node-schedule": "^2.1.0",
24
- "oak-common-aspect": "^3.0.5",
25
- "oak-db": "^3.3.13",
26
- "oak-domain": "^5.1.35",
27
- "oak-frontend-base": "^5.3.45",
30
+ "oak-common-aspect": "^4.0.0",
31
+ "oak-db": "^4.0.1",
32
+ "oak-domain": "^6.0.0",
28
33
  "socket.io": "^4.8.1",
29
34
  "socket.io-client": "^4.7.2",
30
35
  "uuid": "^8.3.2"
@@ -35,9 +40,10 @@
35
40
  "@types/node": "^20.6.0",
36
41
  "@types/node-schedule": "^2.1.0",
37
42
  "@types/uuid": "^8.3.4",
43
+ "@vitest/ui": "^4.0.18",
38
44
  "copyfiles": "^2.4.1",
39
- "ts-node": "^10.9.1",
40
45
  "tslib": "^2.4.0",
41
- "typescript": "^5.2.2"
46
+ "typescript": "^5.2.2",
47
+ "vitest": "^4.0.18"
42
48
  }
43
49
  }