cabloy 5.1.205 → 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 +24 -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/scripts/runIsolatedTest.test.ts +21 -0
- package/scripts/runIsolatedTest.ts +14 -6
- 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/pnpm-lock.yaml +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
|
@@ -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
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
import assert from 'node:assert/strict';
|
|
2
|
+
import { describe, it } from 'node:test';
|
|
3
|
+
|
|
4
|
+
import { ServiceDatabase } from '../src/service/database.ts';
|
|
5
|
+
|
|
6
|
+
const databaseName = 'vona-test-preparation-s-20261014-120000';
|
|
7
|
+
const isolateName = 'vona-test-preparation-i-isolateTest-20261014-120000';
|
|
8
|
+
|
|
9
|
+
function fixture(existing: string[] = [], isolate?: string[]) {
|
|
10
|
+
const calls: string[] = [];
|
|
11
|
+
const app = {
|
|
12
|
+
name: 'preparation',
|
|
13
|
+
meta: { isDev: true, isTest: false, env: { META_MODE: 'dev' } },
|
|
14
|
+
config: {
|
|
15
|
+
database: { testDatabase: true, defaultClient: 'main', clients: { main: {}, isolated: {} } },
|
|
16
|
+
instance: {
|
|
17
|
+
instances: isolate
|
|
18
|
+
? { isolateTest: { id: 2, isolate: true, isolateClient: 'isolated' } }
|
|
19
|
+
: {},
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
};
|
|
23
|
+
const names = { main: existing, isolated: isolate ?? [] };
|
|
24
|
+
const clients = Object.fromEntries(
|
|
25
|
+
Object.entries(names).map(([key, databases]) => [
|
|
26
|
+
key,
|
|
27
|
+
{
|
|
28
|
+
db: { dialectName: 'pg' },
|
|
29
|
+
getDatabaseName: (): string => 'postgres',
|
|
30
|
+
connection: {
|
|
31
|
+
schema: {
|
|
32
|
+
fetchDatabases: async () => databases.map(name => ({ name })),
|
|
33
|
+
dropDatabase: async (name: string) => {
|
|
34
|
+
calls.push(`drop:${name}`);
|
|
35
|
+
databases.splice(databases.indexOf(name), 1);
|
|
36
|
+
},
|
|
37
|
+
createDatabase: async (name: string) => {
|
|
38
|
+
calls.push(`create:${key}`);
|
|
39
|
+
databases.push(name);
|
|
40
|
+
return name;
|
|
41
|
+
},
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
changeConfigConnectionAndReloadWorker: async (name: string) => calls.push(`reload:${name}`),
|
|
45
|
+
},
|
|
46
|
+
]),
|
|
47
|
+
);
|
|
48
|
+
const database = {
|
|
49
|
+
current: { client: clients.main },
|
|
50
|
+
switchDb: async (fn: () => Promise<unknown>, clientName: string) => {
|
|
51
|
+
const previous = database.current;
|
|
52
|
+
database.current = { client: clients[clientName === 'default' ? 'main' : clientName] };
|
|
53
|
+
try {
|
|
54
|
+
return await fn();
|
|
55
|
+
} finally {
|
|
56
|
+
database.current = previous;
|
|
57
|
+
}
|
|
58
|
+
},
|
|
59
|
+
};
|
|
60
|
+
const service = Object.create(ServiceDatabase.prototype) as ServiceDatabase;
|
|
61
|
+
Object.defineProperties(service, {
|
|
62
|
+
app: { value: app },
|
|
63
|
+
bean: { value: { database, worker: { setAlive: async () => calls.push('alive') } } },
|
|
64
|
+
$scope: {
|
|
65
|
+
value: {
|
|
66
|
+
orm: {
|
|
67
|
+
service: {
|
|
68
|
+
database: {
|
|
69
|
+
prepareClientNameReal: (name: string) => (name === 'default' ? 'main' : name),
|
|
70
|
+
},
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
redis: { service: { redis: { clearAllData: async () => calls.push('clear') } } },
|
|
74
|
+
version: { service: { version: { __start: async () => calls.push('migrate') } } },
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
scope: { value: { service: { version: { __start: async () => calls.push('migrate') } } } },
|
|
78
|
+
$logger: { value: { silly() {} } },
|
|
79
|
+
});
|
|
80
|
+
return { service, app, calls, clients };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
describe('dev database preparation', { concurrency: false }, () => {
|
|
84
|
+
it('clears all resources before reset creation and records only actual creations', async () => {
|
|
85
|
+
const { service, calls } = fixture();
|
|
86
|
+
await service.prepareDevDatabases(true);
|
|
87
|
+
assert.deepEqual(calls.slice(0, 3), ['clear', 'alive', 'create:main']);
|
|
88
|
+
assert.deepEqual(service.createdInstanceNames, ['']);
|
|
89
|
+
await service.prepareDevDatabases();
|
|
90
|
+
await service.databaseInitStartup();
|
|
91
|
+
assert.equal(calls.filter(call => call === 'clear').length, 1);
|
|
92
|
+
assert.equal(calls.filter(call => call === 'create:main').length, 1);
|
|
93
|
+
assert.equal(calls.at(-1), 'migrate');
|
|
94
|
+
service.consumeCreatedInstance('');
|
|
95
|
+
assert.deepEqual(service.createdInstanceNames, []);
|
|
96
|
+
});
|
|
97
|
+
|
|
98
|
+
it('reuses an existing database without clearing or requesting seeds', async () => {
|
|
99
|
+
const { service, calls } = fixture([databaseName]);
|
|
100
|
+
await service.prepareDevDatabases();
|
|
101
|
+
assert.deepEqual(calls, [`reload:${databaseName}`]);
|
|
102
|
+
assert.deepEqual(service.createdInstanceNames, []);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('rejects a missing isolate before reloading or changing the existing shared database', async () => {
|
|
106
|
+
const { service, calls } = fixture([databaseName], []);
|
|
107
|
+
await assert.rejects(service.prepareDevDatabases(false), /isolateTest.*npm run db:reset/);
|
|
108
|
+
assert.deepEqual(calls, []);
|
|
109
|
+
assert.deepEqual(service.createdInstanceNames, []);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('rejects a missing shared database without any preparation side effects', async () => {
|
|
113
|
+
const { service, calls } = fixture();
|
|
114
|
+
await assert.rejects(service.prepareDevDatabases(false), /default.*npm run db:reset/);
|
|
115
|
+
assert.deepEqual(calls, []);
|
|
116
|
+
assert.deepEqual(service.createdInstanceNames, []);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it('prints a prominent reset command and destructive-data warning before rejecting', async t => {
|
|
120
|
+
const { service, calls } = fixture();
|
|
121
|
+
const output: string[] = [];
|
|
122
|
+
t.mock.method(console, 'error', (message: string) => output.push(message));
|
|
123
|
+
await assert.rejects(service.prepareDevDatabases(false), /default.*npm run db:reset/);
|
|
124
|
+
assert.equal(output.length, 1);
|
|
125
|
+
assert.match(output[0], /DEVELOPMENT DATABASE MISSING/);
|
|
126
|
+
assert.match(output[0], /Instance: default/);
|
|
127
|
+
assert.match(output[0], /npm run db:reset/);
|
|
128
|
+
assert.match(output[0], /deletes managed development data/);
|
|
129
|
+
assert.match(output[0], /Stop processes using the same database and Redis namespace first/);
|
|
130
|
+
assert.deepEqual(calls, []);
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
it('rejects missing targets through legacy startup entrypoints before migration', async () => {
|
|
134
|
+
for (const versionStart of [false, true]) {
|
|
135
|
+
const { service, calls } = fixture();
|
|
136
|
+
await assert.rejects(
|
|
137
|
+
service.prepareDatabase('main' as never, versionStart),
|
|
138
|
+
/npm run db:reset/,
|
|
139
|
+
);
|
|
140
|
+
assert.deepEqual(calls, []);
|
|
141
|
+
}
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
it('preflights every target before any deletion', async () => {
|
|
145
|
+
const { service, calls } = fixture([databaseName], ['unowned-database']);
|
|
146
|
+
await assert.rejects(service.prepareDevDatabases());
|
|
147
|
+
assert.deepEqual(calls, []);
|
|
148
|
+
});
|
|
149
|
+
|
|
150
|
+
it('stops if full resource cleanup fails', async () => {
|
|
151
|
+
const { service, calls } = fixture();
|
|
152
|
+
(service as any).$scope.redis.service.redis.clearAllData = async () => {
|
|
153
|
+
throw new Error('cleanup failed');
|
|
154
|
+
};
|
|
155
|
+
await assert.rejects(service.prepareDevDatabases(true), /cleanup failed/);
|
|
156
|
+
assert.deepEqual(calls, []);
|
|
157
|
+
assert.deepEqual(service.createdInstanceNames, []);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('clears once and recreates all reset targets before migration', async () => {
|
|
161
|
+
const { service, calls } = fixture([databaseName], [isolateName]);
|
|
162
|
+
await service.prepareDevDatabases(true);
|
|
163
|
+
assert.equal(calls.filter(call => call === 'clear').length, 1);
|
|
164
|
+
assert.ok(calls.indexOf('clear') < calls.indexOf(`drop:${databaseName}`));
|
|
165
|
+
assert.ok(calls.includes(`drop:${isolateName}`));
|
|
166
|
+
assert.deepEqual(service.createdInstanceNames, ['', 'isolateTest']);
|
|
167
|
+
await service.databaseInitStartup();
|
|
168
|
+
assert.equal(calls.filter(call => call.startsWith('drop:')).length, 2);
|
|
169
|
+
assert.equal(calls.filter(call => call.startsWith('create:')).length, 2);
|
|
170
|
+
assert.equal(calls.filter(call => call === 'migrate').length, 2);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('rejects non-disposable reset without clearing anything', async () => {
|
|
174
|
+
const { service, app, calls } = fixture([databaseName]);
|
|
175
|
+
app.config.database.testDatabase = false;
|
|
176
|
+
await assert.rejects(service.prepareDevDatabases(true), /testDatabase=true/);
|
|
177
|
+
assert.deepEqual(calls, []);
|
|
178
|
+
await service.prepareDevDatabases(false);
|
|
179
|
+
assert.deepEqual(calls, []);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it('refuses to reset through a connection inside the target namespace', async () => {
|
|
183
|
+
const { service, clients, calls } = fixture([databaseName]);
|
|
184
|
+
clients.main.getDatabaseName = () => databaseName;
|
|
185
|
+
await assert.rejects(service.prepareDevDatabases(true), /connection outside/);
|
|
186
|
+
assert.deepEqual(calls, []);
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
it('checks actual existence even if the configured name belongs to the managed namespace', async () => {
|
|
190
|
+
const { service, clients, calls } = fixture();
|
|
191
|
+
clients.main.getDatabaseName = () => databaseName;
|
|
192
|
+
await assert.rejects(service.prepareDevDatabases(false), /npm run db:reset/);
|
|
193
|
+
assert.deepEqual(calls, []);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('propagates discovery failures without treating them as missing databases', async () => {
|
|
197
|
+
const { service, clients, calls } = fixture([databaseName]);
|
|
198
|
+
clients.main.connection.schema.fetchDatabases = async () => {
|
|
199
|
+
throw new Error('discovery failed');
|
|
200
|
+
};
|
|
201
|
+
await assert.rejects(service.prepareDevDatabases(false), /discovery failed/);
|
|
202
|
+
assert.deepEqual(calls, []);
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
it('propagates reset creation failure without migrating or requesting seeds', async () => {
|
|
206
|
+
const { service, clients, calls } = fixture([databaseName]);
|
|
207
|
+
clients.main.connection.schema.createDatabase = async () => {
|
|
208
|
+
throw new Error('creation failed');
|
|
209
|
+
};
|
|
210
|
+
await assert.rejects(service.prepareDevDatabases(true), /creation failed/);
|
|
211
|
+
assert.deepEqual(calls, ['clear', 'alive', `drop:${databaseName}`]);
|
|
212
|
+
assert.deepEqual(service.createdInstanceNames, []);
|
|
213
|
+
});
|
|
214
|
+
|
|
215
|
+
it('preserves both existing targets on reuse', async () => {
|
|
216
|
+
const { service, calls } = fixture([databaseName], [isolateName]);
|
|
217
|
+
await service.prepareDevDatabases();
|
|
218
|
+
assert.deepEqual(calls, [`reload:${databaseName}`, `reload:${isolateName}`]);
|
|
219
|
+
assert.deepEqual(service.createdInstanceNames, []);
|
|
220
|
+
});
|
|
221
|
+
});
|