cabloy 5.1.206 → 5.1.207
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/.cabloy-version +1 -1
- package/CHANGELOG.md +14 -0
- package/package.json +1 -1
- package/repo-agent-governance/managed-assets.json +3 -3
- package/repo-agent-governance/policies/repository.md +1 -1
- package/repo-docs/backend/migration-and-changes.md +27 -8
- package/repo-docs/reference/repo-scripts.md +20 -1
- package/scripts/init.ts +15 -0
- package/vona/packages-cli/cli/package.json +1 -1
- package/vona/packages-cli/cli-set-api/package.json +1 -1
- package/vona/packages-cli/cli-set-api/src/lib/bean/cli.bin.dev.ts +17 -2
- package/vona/packages-cli/cli-set-api/test/binDevLifecycle.test.ts +100 -0
- package/vona/packages-vona/vona/package.json +1 -1
- package/vona/packages-vona/vona-core/package.json +1 -1
- package/vona/packages-vona/vona-core/src/lib/framework/cluster.ts +1 -1
- package/vona/packages-vona/vona-core/src/lib/framework/processMaster.test.ts +185 -0
- package/vona/packages-vona/vona-core/src/lib/framework/processMaster.ts +18 -7
- package/vona/packages-vona/vona-core/src/lib/framework/start.test.ts +127 -0
- package/vona/packages-vona/vona-mock/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-core/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-redis/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-redis/src/service/redis.ts +77 -42
- package/vona/src/suite-vendor/a-vona/modules/a-redis/test/redisCleanup.test.ts +378 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/service/startup.ts +56 -9
- package/vona/src/suite-vendor/a-vona/modules/a-startup/src/types/startup.ts +1 -0
- package/vona/src/suite-vendor/a-vona/modules/a-startup/test/devSeed.test.ts +104 -0
- package/vona/src/suite-vendor/a-vona/modules/a-version/package.json +1 -1
- package/vona/src/suite-vendor/a-vona/modules/a-version/src/service/database.ts +183 -51
- package/vona/src/suite-vendor/a-vona/modules/a-version/test/devDatabasePreparation.test.ts +221 -0
- package/vona/src/suite-vendor/a-vona/package.json +1 -1
|
@@ -28,17 +28,22 @@ export class ServiceStartup extends BeanBase {
|
|
|
28
28
|
}
|
|
29
29
|
// clear keys
|
|
30
30
|
await this._clearResources();
|
|
31
|
+
if (this.app.meta.isDev) {
|
|
32
|
+
await this.bean.executor.newCtx(
|
|
33
|
+
() => this.$scope.version.service.database.prepareDevDatabases(),
|
|
34
|
+
{ dbInfo: { level: 1 }, instanceName: null },
|
|
35
|
+
);
|
|
36
|
+
}
|
|
31
37
|
|
|
32
38
|
// run startups: not after
|
|
33
39
|
for (const startup of this._startups) {
|
|
34
40
|
const startupOptions = startup.beanOptions.options;
|
|
35
41
|
if (!startupOptions?.instance && startupOptions?.after !== true) {
|
|
36
|
-
// A previous dev startup may have left databaseInit's debounce key in Redis.
|
|
37
|
-
// Only the explicit reset must bypass that key to guarantee recreation.
|
|
38
42
|
const force =
|
|
39
43
|
this.app.meta.isDev &&
|
|
40
|
-
|
|
41
|
-
|
|
44
|
+
startup.name === 'a-version:databaseInit' &&
|
|
45
|
+
(process.env.VONA_DB_RESET === 'true' ||
|
|
46
|
+
this.$scope.version.service.database.createdInstanceNames.length > 0);
|
|
42
47
|
const run = () => this.runStartup(startup.name, null, force ? { force: true } : undefined);
|
|
43
48
|
if (this.app.meta.env.VONA_TEST_RUN_ID && startup.name === 'a-version:databaseInit') {
|
|
44
49
|
const defaultClient = this.app.config.database.defaultClient;
|
|
@@ -55,6 +60,25 @@ export class ServiceStartup extends BeanBase {
|
|
|
55
60
|
}
|
|
56
61
|
|
|
57
62
|
async appReady() {
|
|
63
|
+
const createdInstances = this.$scope.version.service.database.createdInstanceNames;
|
|
64
|
+
const preparingDev = this.app.meta.isDev && createdInstances.length > 0;
|
|
65
|
+
if (preparingDev) {
|
|
66
|
+
// Initialize and seed fresh instances before listeners or queue workers start.
|
|
67
|
+
const instanceNames = [
|
|
68
|
+
'',
|
|
69
|
+
...createdInstances.filter(name => name !== ''),
|
|
70
|
+
] as (keyof IInstanceRecord)[];
|
|
71
|
+
for (const instanceName of instanceNames) {
|
|
72
|
+
await this._appReadyInstanceStartup(instanceName, true, true);
|
|
73
|
+
}
|
|
74
|
+
for (const instanceName of instanceNames) {
|
|
75
|
+
await this.bean.executor.newCtx(() => this.runStartupInstanceAfter(instanceName), {
|
|
76
|
+
dbInfo: { level: 1 },
|
|
77
|
+
instanceName,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
58
82
|
// run startups: after
|
|
59
83
|
for (const startup of this._startups) {
|
|
60
84
|
const startupOptions = startup.beanOptions.options;
|
|
@@ -65,7 +89,7 @@ export class ServiceStartup extends BeanBase {
|
|
|
65
89
|
|
|
66
90
|
// version init : force: should be false
|
|
67
91
|
if (this.app.meta.isTest || this.app.meta.isDev) {
|
|
68
|
-
await this._appReadyInstanceStartup('', true);
|
|
92
|
+
if (!preparingDev) await this._appReadyInstanceStartup('', true);
|
|
69
93
|
} else {
|
|
70
94
|
// only start static instances (defined in config)
|
|
71
95
|
for (const key in this.app.config.instance.instances) {
|
|
@@ -78,8 +102,8 @@ export class ServiceStartup extends BeanBase {
|
|
|
78
102
|
}
|
|
79
103
|
}
|
|
80
104
|
|
|
81
|
-
//
|
|
82
|
-
if (this.app.meta.isTest
|
|
105
|
+
// Test seeding retains its existing lifecycle; dev seeding belongs to fresh instances.
|
|
106
|
+
if (this.app.meta.isTest) {
|
|
83
107
|
await this.seedDefaultInstance();
|
|
84
108
|
}
|
|
85
109
|
}
|
|
@@ -94,9 +118,17 @@ export class ServiceStartup extends BeanBase {
|
|
|
94
118
|
);
|
|
95
119
|
}
|
|
96
120
|
|
|
97
|
-
private async _appReadyInstanceStartup(
|
|
121
|
+
private async _appReadyInstanceStartup(
|
|
122
|
+
instanceName: keyof IInstanceRecord,
|
|
123
|
+
wait: boolean,
|
|
124
|
+
deferAfter?: boolean,
|
|
125
|
+
) {
|
|
98
126
|
const res = this.$scope.instance.service.instance.instanceStartupIsolate(instanceName, {
|
|
99
|
-
force:
|
|
127
|
+
force:
|
|
128
|
+
this.app.meta.isDev &&
|
|
129
|
+
(process.env.VONA_DB_RESET === 'true' ||
|
|
130
|
+
this.$scope.version.service.database.createdInstanceNames.includes(instanceName)),
|
|
131
|
+
deferAfter,
|
|
100
132
|
});
|
|
101
133
|
if (wait) {
|
|
102
134
|
await res;
|
|
@@ -182,6 +214,21 @@ export class ServiceStartup extends BeanBase {
|
|
|
182
214
|
await this.runStartup(startup.name, instanceName, options);
|
|
183
215
|
}
|
|
184
216
|
}
|
|
217
|
+
if (
|
|
218
|
+
this.app.meta.isDev &&
|
|
219
|
+
this.$scope.version.service.database.createdInstanceNames.includes(instanceName)
|
|
220
|
+
) {
|
|
221
|
+
await this.$scope.version.service.version.__instanceSeed(instanceName);
|
|
222
|
+
this.$scope.version.service.database.consumeCreatedInstance(instanceName);
|
|
223
|
+
}
|
|
224
|
+
if (options?.deferAfter) return;
|
|
225
|
+
await this.runStartupInstanceAfter(instanceName, options);
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
async runStartupInstanceAfter(
|
|
229
|
+
instanceName: keyof IInstanceRecord,
|
|
230
|
+
options?: IInstanceStartupOptions,
|
|
231
|
+
) {
|
|
185
232
|
// set flag
|
|
186
233
|
this.app.meta.appReadyInstances[instanceName] = true;
|
|
187
234
|
// run startups: after
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { ServiceStartup } from '../src/service/startup.ts';
|
|
5
|
+
|
|
6
|
+
function fixture(created: string[] = []) {
|
|
7
|
+
const calls: string[] = [];
|
|
8
|
+
const pending = new Set(created);
|
|
9
|
+
const app = {
|
|
10
|
+
meta: { isDev: true, isTest: false, appReadyInstances: {} as Record<string, boolean> },
|
|
11
|
+
config: { server: { listen: { disable: false } } },
|
|
12
|
+
};
|
|
13
|
+
const service = Object.create(ServiceStartup.prototype) as ServiceStartup;
|
|
14
|
+
const startups = [
|
|
15
|
+
{ name: 'a-version:instanceInit', beanOptions: { options: { instance: true } } },
|
|
16
|
+
{ name: 'a-runtime:runtime', beanOptions: { options: { instance: true, after: true } } },
|
|
17
|
+
{ name: 'a-queue:loadQueueWorkers', beanOptions: { options: { instance: true, after: true } } },
|
|
18
|
+
{ name: 'a-web:listen', beanOptions: { options: { after: true } } },
|
|
19
|
+
];
|
|
20
|
+
Object.defineProperties(service, {
|
|
21
|
+
app: { value: app },
|
|
22
|
+
_startups: { value: startups },
|
|
23
|
+
bean: { value: { executor: { newCtx: async (fn: () => Promise<unknown>) => fn() } } },
|
|
24
|
+
$scope: {
|
|
25
|
+
value: {
|
|
26
|
+
version: {
|
|
27
|
+
service: {
|
|
28
|
+
database: {
|
|
29
|
+
get createdInstanceNames() {
|
|
30
|
+
return [...pending];
|
|
31
|
+
},
|
|
32
|
+
consumeCreatedInstance: (name: string) => pending.delete(name),
|
|
33
|
+
},
|
|
34
|
+
version: {
|
|
35
|
+
__instanceSeed: async (name: string) => {
|
|
36
|
+
calls.push(`seed:${name}`);
|
|
37
|
+
},
|
|
38
|
+
},
|
|
39
|
+
},
|
|
40
|
+
},
|
|
41
|
+
instance: {
|
|
42
|
+
service: {
|
|
43
|
+
instance: {
|
|
44
|
+
instanceStartupIsolate: async (name: string, options: any) =>
|
|
45
|
+
service.runStartupInstance(name as never, options),
|
|
46
|
+
},
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
runStartup: {
|
|
52
|
+
value: async (name: string, instance: string) => {
|
|
53
|
+
calls.push(`${name}:${instance}`);
|
|
54
|
+
},
|
|
55
|
+
},
|
|
56
|
+
});
|
|
57
|
+
return { service, app, calls, pending };
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
describe('creation-driven dev seed', { concurrency: false }, () => {
|
|
61
|
+
it('seeds fresh instances before runtime, queues, and listening', async () => {
|
|
62
|
+
const { service, calls, pending, app } = fixture(['', 'isolateTest']);
|
|
63
|
+
await service.appReady();
|
|
64
|
+
assert.equal(pending.size, 0);
|
|
65
|
+
const lastSeed = calls.indexOf('seed:isolateTest');
|
|
66
|
+
assert.ok(lastSeed > calls.indexOf('seed:'));
|
|
67
|
+
for (const call of ['a-runtime:runtime:', 'a-queue:loadQueueWorkers:', 'a-web:listen:null']) {
|
|
68
|
+
assert.ok(calls.indexOf(call) > lastSeed, `${call} must follow all seeds`);
|
|
69
|
+
}
|
|
70
|
+
assert.equal(app.meta.appReadyInstances[''], true);
|
|
71
|
+
assert.equal(app.meta.appReadyInstances.isolateTest, true);
|
|
72
|
+
await service.runStartupInstance('' as never);
|
|
73
|
+
assert.equal(calls.filter(call => call === 'seed:').length, 1);
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it('does not seed existing default when only an isolated database is new', async () => {
|
|
77
|
+
const { service, calls } = fixture(['isolateTest']);
|
|
78
|
+
await service.appReady();
|
|
79
|
+
assert.ok(calls.includes('seed:isolateTest'));
|
|
80
|
+
assert.ok(!calls.includes('seed:'));
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('does not seed when all databases are reused', async () => {
|
|
84
|
+
const { service, calls } = fixture();
|
|
85
|
+
await service.appReady();
|
|
86
|
+
assert.ok(!calls.some(call => call.startsWith('seed:')));
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
it('fails before after-startups and readiness if seeding fails', async () => {
|
|
90
|
+
const { service, app, calls, pending } = fixture(['']);
|
|
91
|
+
(service as any).$scope.version.service.version.__instanceSeed = async () => {
|
|
92
|
+
throw new Error('seed failed');
|
|
93
|
+
};
|
|
94
|
+
await assert.rejects(service.appReady(), /seed failed/);
|
|
95
|
+
assert.ok(pending.has(''));
|
|
96
|
+
assert.deepEqual(app.meta.appReadyInstances, {});
|
|
97
|
+
assert.ok(
|
|
98
|
+
!calls.some(
|
|
99
|
+
call =>
|
|
100
|
+
call.includes('runtime') || call.includes('listen') || call.includes('loadQueueWorkers'),
|
|
101
|
+
),
|
|
102
|
+
);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { IInstanceRecord } from 'vona';
|
|
1
|
+
import type { IInstanceRecord, VonaApplication } from 'vona';
|
|
2
2
|
import type { ConfigInstanceBase } from 'vona-module-a-instance';
|
|
3
3
|
import type { IDatabaseClientRecord, ServiceDatabaseClient } from 'vona-module-a-orm';
|
|
4
4
|
|
|
@@ -20,6 +20,26 @@ import { createDatabaseTestName, getDatabaseTestPrefix } from '../lib/databaseTe
|
|
|
20
20
|
const __separator = '-';
|
|
21
21
|
const __timeFormat = `yyyyMMdd${__separator}HHmmss`;
|
|
22
22
|
|
|
23
|
+
interface IDevDatabaseTarget {
|
|
24
|
+
clientName: keyof IDatabaseClientRecord;
|
|
25
|
+
instanceName?: keyof IInstanceRecord;
|
|
26
|
+
configInstanceBase?: ConfigInstanceBase;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
interface IDevDatabasePlan extends IDevDatabaseTarget {
|
|
30
|
+
databasePrefix: string;
|
|
31
|
+
sqliteRoot?: string;
|
|
32
|
+
databases: string[];
|
|
33
|
+
create: boolean;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface IDevDatabasePreparation {
|
|
37
|
+
prepared: boolean;
|
|
38
|
+
createdInstances: Set<keyof IInstanceRecord>;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const devDatabasePreparations = new WeakMap<VonaApplication, IDevDatabasePreparation>();
|
|
42
|
+
|
|
23
43
|
export function assertDisposableDatabaseName(
|
|
24
44
|
databaseName: string,
|
|
25
45
|
databasePrefix: string,
|
|
@@ -49,6 +69,110 @@ export class ServiceDatabase extends BeanBase {
|
|
|
49
69
|
return getDatabaseTestPrefix(this.app.name, scope);
|
|
50
70
|
}
|
|
51
71
|
|
|
72
|
+
get createdInstanceNames(): (keyof IInstanceRecord)[] {
|
|
73
|
+
return [...(devDatabasePreparations.get(this.app)?.createdInstances ?? [])];
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
consumeCreatedInstance(instanceName: keyof IInstanceRecord) {
|
|
77
|
+
devDatabasePreparations.get(this.app)?.createdInstances.delete(instanceName);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public async prepareDevDatabases(reset = process.env.VONA_DB_RESET === 'true') {
|
|
81
|
+
if (!this.app.meta.isDev) return;
|
|
82
|
+
if (!this.configDatabase.testDatabase) {
|
|
83
|
+
if (reset) {
|
|
84
|
+
throw new Error('db:reset requires database.testDatabase=true in the dev configuration');
|
|
85
|
+
}
|
|
86
|
+
return;
|
|
87
|
+
}
|
|
88
|
+
if (devDatabasePreparations.get(this.app)?.prepared) return;
|
|
89
|
+
|
|
90
|
+
if (typeof this.configDatabase.defaultClient !== 'string') {
|
|
91
|
+
throw new TypeError('Disposable dev database preparation requires a static default client');
|
|
92
|
+
}
|
|
93
|
+
// Validate every target before clearing any application resources.
|
|
94
|
+
const plans: IDevDatabasePlan[] = [];
|
|
95
|
+
const clientNames = new Set<keyof IDatabaseClientRecord>();
|
|
96
|
+
for (const target of this.__databaseTargets()) {
|
|
97
|
+
if (target.configInstanceBase?.isolate && !target.configInstanceBase.id) {
|
|
98
|
+
throw new Error(`should specify id for isolate instance: ${target.instanceName}`);
|
|
99
|
+
}
|
|
100
|
+
const clientName = this.$scope.orm.service.database.prepareClientNameReal(target.clientName);
|
|
101
|
+
if (clientNames.has(clientName)) {
|
|
102
|
+
throw new Error(`Disposable database targets must use distinct clients: ${clientName}`);
|
|
103
|
+
}
|
|
104
|
+
clientNames.add(clientName);
|
|
105
|
+
plans.push(
|
|
106
|
+
await this.bean.database.switchDb(async () => {
|
|
107
|
+
const client = this.bean.database.current.client;
|
|
108
|
+
const databasePrefix = this.getDatabasePrefix(
|
|
109
|
+
target.instanceName,
|
|
110
|
+
target.configInstanceBase,
|
|
111
|
+
);
|
|
112
|
+
const sqliteRoot = this.__sqliteRoot(client);
|
|
113
|
+
const currentName = client.getDatabaseName();
|
|
114
|
+
const currentFilename = sqliteRoot ? path.basename(currentName, '.db') : currentName;
|
|
115
|
+
if (currentFilename.startsWith(databasePrefix)) {
|
|
116
|
+
assertDisposableDatabaseName(currentName, databasePrefix, sqliteRoot);
|
|
117
|
+
if (reset) {
|
|
118
|
+
throw new Error('db:reset requires a connection outside the disposable database');
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
const dbs = await this.__fetchDatabases(
|
|
122
|
+
client,
|
|
123
|
+
target.instanceName,
|
|
124
|
+
target.configInstanceBase,
|
|
125
|
+
);
|
|
126
|
+
for (const db of dbs) {
|
|
127
|
+
assertDisposableDatabaseName(db.name, databasePrefix, sqliteRoot);
|
|
128
|
+
}
|
|
129
|
+
return {
|
|
130
|
+
...target,
|
|
131
|
+
databasePrefix,
|
|
132
|
+
sqliteRoot,
|
|
133
|
+
databases: dbs.map(db => db.name),
|
|
134
|
+
create: reset,
|
|
135
|
+
};
|
|
136
|
+
}, target.clientName),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
if (!reset) {
|
|
141
|
+
const missing = plans.find(plan => plan.databases.length === 0);
|
|
142
|
+
if (missing) this.__missingDevDatabase(missing.instanceName);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (reset) {
|
|
146
|
+
await this.$scope.redis.service.redis.clearAllData();
|
|
147
|
+
await this.bean.worker.setAlive();
|
|
148
|
+
}
|
|
149
|
+
const preparation: IDevDatabasePreparation = {
|
|
150
|
+
prepared: false,
|
|
151
|
+
createdInstances: new Set(),
|
|
152
|
+
};
|
|
153
|
+
for (const plan of plans) {
|
|
154
|
+
await this.bean.database.switchDb(async () => {
|
|
155
|
+
const client = this.bean.database.current.client;
|
|
156
|
+
if (reset) {
|
|
157
|
+
for (const databaseName of plan.databases) {
|
|
158
|
+
assertDisposableDatabaseName(databaseName, plan.databasePrefix, plan.sqliteRoot);
|
|
159
|
+
await client.connection.schema.dropDatabase(databaseName);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
const databaseName = plan.create
|
|
163
|
+
? await this.__createDatabase(client, plan.instanceName, plan.configInstanceBase)
|
|
164
|
+
: plan.databases[0];
|
|
165
|
+
assertDisposableDatabaseName(databaseName, plan.databasePrefix, plan.sqliteRoot);
|
|
166
|
+
await client.changeConfigConnectionAndReloadWorker(databaseName);
|
|
167
|
+
if (plan.create) preparation.createdInstances.add(plan.instanceName ?? '');
|
|
168
|
+
this.$logger.silly(chalk.cyan(`dialect: ${client.db.dialectName}`));
|
|
169
|
+
this.$logger.silly(chalk.cyan(`database: ${databaseName}, pid: ${process.pid}`));
|
|
170
|
+
}, plan.clientName);
|
|
171
|
+
}
|
|
172
|
+
preparation.prepared = true;
|
|
173
|
+
devDatabasePreparations.set(this.app, preparation);
|
|
174
|
+
}
|
|
175
|
+
|
|
52
176
|
public async databaseInitStartup() {
|
|
53
177
|
// database
|
|
54
178
|
await this.__prepareDatabases(true);
|
|
@@ -97,22 +221,41 @@ export class ServiceDatabase extends BeanBase {
|
|
|
97
221
|
return await client.connection.schema.createDatabase(databaseName);
|
|
98
222
|
}
|
|
99
223
|
|
|
100
|
-
private
|
|
101
|
-
|
|
102
|
-
await this.prepareDatabase('default', versionStart);
|
|
103
|
-
// isolate
|
|
224
|
+
private __databaseTargets(): IDevDatabaseTarget[] {
|
|
225
|
+
const targets: IDevDatabaseTarget[] = [{ clientName: 'default' }];
|
|
104
226
|
for (const key in this.app.config.instance.instances) {
|
|
105
227
|
const instanceName = key as keyof IInstanceRecord;
|
|
106
228
|
const configInstanceBase = this.app.config.instance.instances[instanceName];
|
|
107
|
-
if (configInstanceBase === false) continue;
|
|
108
|
-
if (!configInstanceBase.
|
|
109
|
-
if (!configInstanceBase.isolateClient)
|
|
229
|
+
if (configInstanceBase === false || !configInstanceBase.isolate) continue;
|
|
230
|
+
if (!configInstanceBase.isolateClient) {
|
|
110
231
|
throw new Error(`should specify isolateClient for isolate instance: ${instanceName}`);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
232
|
+
}
|
|
233
|
+
targets.push({
|
|
234
|
+
clientName: configInstanceBase.isolateClient,
|
|
114
235
|
instanceName,
|
|
115
236
|
configInstanceBase,
|
|
237
|
+
});
|
|
238
|
+
}
|
|
239
|
+
return targets;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
private __sqliteRoot(client: ServiceDatabaseClient) {
|
|
243
|
+
return client.db.dialectName === 'better-sqlite3'
|
|
244
|
+
? resolveVonaLocalPaths(
|
|
245
|
+
this.app.projectPath,
|
|
246
|
+
this.app.meta.env.META_MODE,
|
|
247
|
+
this.app.meta.env.VONA_TEST_RUN_ID,
|
|
248
|
+
).sqliteRoot
|
|
249
|
+
: undefined;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
private async __prepareDatabases(versionStart: boolean) {
|
|
253
|
+
for (const target of this.__databaseTargets()) {
|
|
254
|
+
await this.prepareDatabase(
|
|
255
|
+
target.clientName,
|
|
256
|
+
versionStart,
|
|
257
|
+
target.instanceName,
|
|
258
|
+
target.configInstanceBase,
|
|
116
259
|
);
|
|
117
260
|
}
|
|
118
261
|
}
|
|
@@ -122,14 +265,36 @@ export class ServiceDatabase extends BeanBase {
|
|
|
122
265
|
instanceName?: keyof IInstanceRecord,
|
|
123
266
|
configInstanceBase?: ConfigInstanceBase,
|
|
124
267
|
) {
|
|
125
|
-
await this.__prepareDatabaseInner(
|
|
268
|
+
await this.__prepareDatabaseInner(instanceName, configInstanceBase);
|
|
126
269
|
if (versionStart) {
|
|
127
270
|
await this.scope.service.version.__start();
|
|
128
271
|
}
|
|
129
272
|
}
|
|
130
273
|
|
|
274
|
+
private __missingDevDatabase(instanceName?: keyof IInstanceRecord): never {
|
|
275
|
+
// eslint-disable-next-line
|
|
276
|
+
console.error(
|
|
277
|
+
[
|
|
278
|
+
'',
|
|
279
|
+
chalk.bgRed.white.bold(' DEVELOPMENT DATABASE MISSING '),
|
|
280
|
+
chalk.yellow(`Instance: ${instanceName || 'default'}`),
|
|
281
|
+
'',
|
|
282
|
+
chalk.bold.yellow('Run this command before starting the development server:'),
|
|
283
|
+
'',
|
|
284
|
+
chalk.bold.cyan(' npm run db:reset'),
|
|
285
|
+
'',
|
|
286
|
+
chalk.yellow('Warning: db:reset deletes managed development data.'),
|
|
287
|
+
chalk.yellow('Stop processes using the same database and Redis namespace first.'),
|
|
288
|
+
'',
|
|
289
|
+
].join('\n'),
|
|
290
|
+
);
|
|
291
|
+
throw new Error(
|
|
292
|
+
`Development database is missing for instance "${instanceName || 'default'}". ` +
|
|
293
|
+
'Run "npm run db:reset" before starting the development server.',
|
|
294
|
+
);
|
|
295
|
+
}
|
|
296
|
+
|
|
131
297
|
private async __prepareDatabaseInner(
|
|
132
|
-
versionStart: boolean,
|
|
133
298
|
instanceName?: keyof IInstanceRecord,
|
|
134
299
|
configInstanceBase?: ConfigInstanceBase,
|
|
135
300
|
) {
|
|
@@ -137,6 +302,10 @@ export class ServiceDatabase extends BeanBase {
|
|
|
137
302
|
// donothing
|
|
138
303
|
return;
|
|
139
304
|
}
|
|
305
|
+
if (this.app.meta.isDev) {
|
|
306
|
+
await this.prepareDevDatabases();
|
|
307
|
+
return;
|
|
308
|
+
}
|
|
140
309
|
const databasePrefix = this.getDatabasePrefix(instanceName, configInstanceBase);
|
|
141
310
|
if (this.app.meta.env.VONA_TEST_RUN_ID) {
|
|
142
311
|
const paths = resolveVonaLocalPaths(
|
|
@@ -149,16 +318,7 @@ export class ServiceDatabase extends BeanBase {
|
|
|
149
318
|
// client
|
|
150
319
|
const client = this.bean.database.current.client;
|
|
151
320
|
// get current database name
|
|
152
|
-
|
|
153
|
-
// dev/debug db
|
|
154
|
-
if (this.app.meta.isDev) {
|
|
155
|
-
const reset = versionStart && process.env.VONA_DB_RESET === 'true';
|
|
156
|
-
// Never drop a configured, non-disposable development database.
|
|
157
|
-
if (reset && !this.configDatabase.testDatabase) {
|
|
158
|
-
throw new Error('db:reset requires database.testDatabase=true in the dev configuration');
|
|
159
|
-
}
|
|
160
|
-
if (!this.configDatabase.testDatabase) return;
|
|
161
|
-
}
|
|
321
|
+
const databaseName = client.getDatabaseName();
|
|
162
322
|
const sqliteRoot =
|
|
163
323
|
client.db.dialectName === 'better-sqlite3'
|
|
164
324
|
? resolveVonaLocalPaths(
|
|
@@ -180,34 +340,6 @@ export class ServiceDatabase extends BeanBase {
|
|
|
180
340
|
`Isolated test database is outside its disposable namespace: ${databaseName}`,
|
|
181
341
|
);
|
|
182
342
|
}
|
|
183
|
-
// dev/debug db
|
|
184
|
-
if (this.app.meta.isDev) {
|
|
185
|
-
const reset = versionStart && process.env.VONA_DB_RESET === 'true';
|
|
186
|
-
// The database used to enumerate/drop databases must not itself be dropped.
|
|
187
|
-
if (isTestDatabase) {
|
|
188
|
-
if (reset)
|
|
189
|
-
throw new Error('db:reset requires a connection outside the disposable database');
|
|
190
|
-
return;
|
|
191
|
-
}
|
|
192
|
-
const dbs = await this.__fetchDatabases(client, instanceName, configInstanceBase);
|
|
193
|
-
for (const db of dbs) {
|
|
194
|
-
assertDisposableDatabaseName(db.name, databasePrefix, sqliteRoot);
|
|
195
|
-
}
|
|
196
|
-
if (reset) {
|
|
197
|
-
for (const db of dbs) {
|
|
198
|
-
await client.connection.schema.dropDatabase(db.name);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
databaseName =
|
|
202
|
-
reset || dbs.length === 0
|
|
203
|
-
? await this.__createDatabase(client, instanceName, configInstanceBase)
|
|
204
|
-
: dbs[0].name;
|
|
205
|
-
assertDisposableDatabaseName(databaseName, databasePrefix, sqliteRoot);
|
|
206
|
-
// set config and reload client
|
|
207
|
-
await client.changeConfigConnectionAndReloadWorker(databaseName);
|
|
208
|
-
this.$logger.silly(chalk.cyan(`dialect: ${client.db.dialectName}`));
|
|
209
|
-
this.$logger.silly(chalk.cyan(`database: ${databaseName}, pid: ${process.pid}`));
|
|
210
|
-
}
|
|
211
343
|
// test db
|
|
212
344
|
if (this.app.meta.isTest) {
|
|
213
345
|
// check
|