@memberjunction/open-app-engine 5.45.1 → 5.47.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.d.ts +4 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/install/entity-teardown.d.ts +29 -0
- package/dist/install/entity-teardown.d.ts.map +1 -0
- package/dist/install/entity-teardown.js +118 -0
- package/dist/install/entity-teardown.js.map +1 -0
- package/dist/install/history-recorder.d.ts +2 -1
- package/dist/install/history-recorder.d.ts.map +1 -1
- package/dist/install/history-recorder.js +4 -0
- package/dist/install/history-recorder.js.map +1 -1
- package/dist/install/install-orchestrator.d.ts +7 -1
- package/dist/install/install-orchestrator.d.ts.map +1 -1
- package/dist/install/install-orchestrator.js +385 -201
- package/dist/install/install-orchestrator.js.map +1 -1
- package/dist/install/migration-application-ids.d.ts +2 -0
- package/dist/install/migration-application-ids.d.ts.map +1 -0
- package/dist/install/migration-application-ids.js +45 -0
- package/dist/install/migration-application-ids.js.map +1 -0
- package/dist/types/open-app-types.d.ts +5 -0
- package/dist/types/open-app-types.d.ts.map +1 -1
- package/package.json +5 -5
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { mkdirSync, readFileSync } from 'node:fs';
|
|
3
|
+
import { mkdirSync, readFileSync, rmSync } from 'node:fs';
|
|
4
4
|
import { createRequire } from 'node:module';
|
|
5
5
|
import { pathToFileURL } from 'node:url';
|
|
6
6
|
import { ParseAndValidateManifest } from '../manifest/manifest-loader.js';
|
|
@@ -9,13 +9,40 @@ import { ResolveDependencyGraph } from '../dependency/dependency-graph-builder.j
|
|
|
9
9
|
import { FetchManifestFromGitHub, DownloadMigrations, GetLatestVersion, ListGitHubReleases, ListGitHubTags, ValidateGitHubTag, ParseGitHubUrl } from '../github/github-client.js';
|
|
10
10
|
import semver from 'semver';
|
|
11
11
|
import { CreateAppSchema, DropAppSchema, SchemaExists, EscapeSqlString } from './schema-manager.js';
|
|
12
|
+
import { RunFkGraphTeardown, buildRootDoomedPredicate } from './entity-teardown.js';
|
|
13
|
+
import { extractApplicationIds } from './migration-application-ids.js';
|
|
12
14
|
import { RunAppMigrations } from './migration-runner.js';
|
|
13
15
|
import { AddAppPackages, RemoveAppPackages, RunPackageInstall, BumpPrefixedDependencies } from './package-manager.js';
|
|
14
16
|
import { AddServerDynamicPackages, AddClientDynamicPackages, RemoveServerDynamicPackages, ToggleServerDynamicPackages, AddEntityPackageMapping, RemoveEntityPackageMapping } from './config-manager.js';
|
|
15
17
|
import { AngularConfigManager } from './angular-config-manager.js';
|
|
16
18
|
import { Metadata, RunView } from '@memberjunction/core';
|
|
17
19
|
import { NormalizeUUID } from '@memberjunction/global';
|
|
18
|
-
import { RecordAppInstallation, RecordInstallHistoryEntry, RecordAppDependencies, ReplaceAppDependenciesAtomically, SetAppStatus, FindInstalledApp, CheckSchemaSharedByOtherApps, FindDependentApps, ListInstalledApps, UpdateAppRecord, } from './history-recorder.js';
|
|
20
|
+
import { RecordAppInstallation, RecordInstallHistoryEntry, RecordAppDependencies, DeleteAppDependencies, ReplaceAppDependenciesAtomically, SetAppStatus, FindInstalledApp, CheckSchemaSharedByOtherApps, FindDependentApps, ListInstalledApps, UpdateAppRecord, SetAppStep, } from './history-recorder.js';
|
|
21
|
+
const INSTALL_STEP_ORDER = ['RecordCreated', 'PackagesInstalled', 'ConfigUpdated', 'AngularExcludesUpdated', 'Finalized', 'HooksRun'];
|
|
22
|
+
const UPGRADE_STEP_ORDER = ['MigrationsApplied', 'PackagesInstalled', 'ConfigUpdated', 'AngularExcludesUpdated', 'RecordUpdated', 'HooksRun', 'DependenciesReplaced'];
|
|
23
|
+
const REMOVE_STEP_ORDER = ['DbCleanupDone', 'FilesRemoved'];
|
|
24
|
+
function IsStepDone(order, checkpoint, step) {
|
|
25
|
+
if (!checkpoint) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
const checkpointIndex = order.indexOf(checkpoint);
|
|
29
|
+
const stepIndex = order.indexOf(step);
|
|
30
|
+
return checkpointIndex >= 0 && checkpointIndex >= stepIndex;
|
|
31
|
+
}
|
|
32
|
+
function IsOwnCheckpoint(order, checkpoint) {
|
|
33
|
+
return !!checkpoint && order.includes(checkpoint);
|
|
34
|
+
}
|
|
35
|
+
const SETTLED_STATUSES = new Set(['Active', 'Disabled']);
|
|
36
|
+
function BuildMutexConflictResult(action, appName, version, startTime, currentStatus, displayVerb) {
|
|
37
|
+
const verb = displayVerb ?? action.toLowerCase();
|
|
38
|
+
const guidance = {
|
|
39
|
+
Installing: `finish it with 'mj app install <source>' (it will resume automatically)`,
|
|
40
|
+
Upgrading: `finish it with 'mj app upgrade ${appName}' (it will resume automatically)`,
|
|
41
|
+
Removing: `finish it with 'mj app remove ${appName}' (it will resume automatically)`,
|
|
42
|
+
Error: `resolve it — 'mj app install <source>' to retry a failed install, 'mj app upgrade ${appName}' to retry a failed upgrade, or 'mj app remove ${appName}' to remove it`,
|
|
43
|
+
};
|
|
44
|
+
return BuildFailureResult(action, appName, version, 'Schema', startTime, `Cannot ${verb} '${appName}' while it is '${currentStatus}' — another operation is still in progress or failed partway. ${guidance[currentStatus] ?? 'Resolve the in-progress operation first.'}`);
|
|
45
|
+
}
|
|
19
46
|
class OpenAppPhaseError extends Error {
|
|
20
47
|
constructor(Phase, message) {
|
|
21
48
|
super(message);
|
|
@@ -73,83 +100,120 @@ export async function InstallApp(options, context) {
|
|
|
73
100
|
}
|
|
74
101
|
const existingApp = await FindInstalledApp(context.ContextUser, manifest.name);
|
|
75
102
|
const isReinstall = existingApp != null && (existingApp.Status === 'Removed' || existingApp.Status === 'Error');
|
|
76
|
-
|
|
77
|
-
|
|
103
|
+
const isResume = existingApp != null && existingApp.Status === 'Installing' && existingApp.Version === manifest.version;
|
|
104
|
+
if (existingApp && !isReinstall && !isResume) {
|
|
105
|
+
const guidance = {
|
|
106
|
+
Active: `Use 'mj app upgrade ${manifest.name}' to update it.`,
|
|
107
|
+
Disabled: `Use 'mj app upgrade ${manifest.name}' to update it.`,
|
|
108
|
+
Upgrading: `An upgrade is already in progress — finish it with 'mj app upgrade ${manifest.name}' (it will resume automatically), then re-run this install if still needed.`,
|
|
109
|
+
Removing: `A removal is already in progress — finish it with 'mj app remove ${manifest.name}' (it will resume automatically), then re-run this install once it's fully removed.`,
|
|
110
|
+
};
|
|
111
|
+
return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, `App '${manifest.name}' is already installed with status '${existingApp.Status}'. ${guidance[existingApp.Status] ?? `Resolve the '${existingApp.Status}' state first.`}`);
|
|
112
|
+
}
|
|
113
|
+
const resumeCheckpoint = isResume ? (existingApp.LastCompletedStep ?? null) : null;
|
|
114
|
+
const phase1Complete = isResume && resumeCheckpoint != null;
|
|
115
|
+
if (isResume) {
|
|
116
|
+
Callbacks?.OnProgress?.('Resume', `Resuming install of ${manifest.name} v${manifest.version}${resumeCheckpoint ? ` from step '${resumeCheckpoint}'` : ''}...`);
|
|
78
117
|
}
|
|
79
|
-
Callbacks?.OnProgress?.('Install', `Installing ${manifest.name} v${manifest.version}...`);
|
|
80
118
|
let schemaCreated = false;
|
|
81
|
-
if (
|
|
82
|
-
|
|
83
|
-
if (!schemaResult.Success) {
|
|
84
|
-
return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, schemaResult.ErrorMessage ?? 'Schema creation failed');
|
|
85
|
-
}
|
|
86
|
-
schemaCreated = schemaResult.Created === true;
|
|
87
|
-
await PersistCanonicalSchemaName(manifest, context);
|
|
119
|
+
if (phase1Complete) {
|
|
120
|
+
createdAppId = existingApp.ID;
|
|
88
121
|
}
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
if (
|
|
122
|
+
else {
|
|
123
|
+
Callbacks?.OnProgress?.('Install', `Installing ${manifest.name} v${manifest.version}...`);
|
|
124
|
+
if (existingApp?.LastCompletedStep) {
|
|
125
|
+
await SetAppStep(context.ContextUser, existingApp.ID, null);
|
|
126
|
+
}
|
|
127
|
+
if (manifest.schema) {
|
|
128
|
+
const schemaResult = await HandleSchemaCreation(manifest, context, isReinstall || isResume, options.AllowDoubleUnderscoreSchema === true);
|
|
129
|
+
if (!schemaResult.Success) {
|
|
130
|
+
return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, schemaResult.ErrorMessage ?? 'Schema creation failed');
|
|
131
|
+
}
|
|
132
|
+
schemaCreated = schemaResult.Created === true;
|
|
133
|
+
await PersistCanonicalSchemaName(manifest, context);
|
|
134
|
+
}
|
|
135
|
+
if (manifest.migrations && manifest.schema) {
|
|
136
|
+
const migrationResult = await HandleMigrations(manifest, context, subpath);
|
|
137
|
+
if (!migrationResult.Success) {
|
|
138
|
+
await CompensateSchemaOnFailure(manifest, context, schemaCreated, options.AllowDoubleUnderscoreSchema === true, Callbacks);
|
|
139
|
+
return BuildFailureResult('Install', manifest.name, manifest.version, 'Migration', startTime, migrationResult.ErrorMessage ?? 'Migration failed');
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
Callbacks?.OnProgress?.('Record', 'Recording app installation...');
|
|
143
|
+
const recordResult = await RecordInstallationAtomically(context.ContextUser, manifest, Callbacks, undefined, subpath);
|
|
144
|
+
if (!recordResult.Success) {
|
|
92
145
|
await CompensateSchemaOnFailure(manifest, context, schemaCreated, options.AllowDoubleUnderscoreSchema === true, Callbacks);
|
|
93
|
-
return BuildFailureResult('Install', manifest.name, manifest.version, '
|
|
146
|
+
return BuildFailureResult('Install', manifest.name, manifest.version, 'Record', startTime, recordResult.ErrorMessage ?? 'Failed to record installation');
|
|
94
147
|
}
|
|
148
|
+
createdAppId = recordResult.AppId;
|
|
149
|
+
await SetAppStep(context.ContextUser, createdAppId, 'RecordCreated');
|
|
95
150
|
}
|
|
96
|
-
Callbacks?.OnProgress?.('Record', 'Recording app installation...');
|
|
97
|
-
const recordResult = await RecordInstallationAtomically(context.ContextUser, manifest, Callbacks, undefined, subpath);
|
|
98
|
-
if (!recordResult.Success) {
|
|
99
|
-
await CompensateSchemaOnFailure(manifest, context, schemaCreated, options.AllowDoubleUnderscoreSchema === true, Callbacks);
|
|
100
|
-
return BuildFailureResult('Install', manifest.name, manifest.version, 'Record', startTime, recordResult.ErrorMessage ?? 'Failed to record installation');
|
|
101
|
-
}
|
|
102
|
-
createdAppId = recordResult.AppId;
|
|
103
151
|
Callbacks?.OnProgress?.('Config', 'Updating configuration files...');
|
|
104
|
-
const pkgResult = await HandlePackageInstallation(manifest, context, effectivePackageVersion, effectiveVersionStrategy, options.Verbose);
|
|
105
152
|
let npmInstallWarning;
|
|
106
|
-
if (!
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
153
|
+
if (!IsStepDone(INSTALL_STEP_ORDER, resumeCheckpoint, 'PackagesInstalled')) {
|
|
154
|
+
const pkgResult = await HandlePackageInstallation(manifest, context, effectivePackageVersion, effectiveVersionStrategy, options.Verbose);
|
|
155
|
+
if (!pkgResult.Success) {
|
|
156
|
+
if (pkgResult.PackageJsonUpdated) {
|
|
157
|
+
npmInstallWarning = pkgResult.ErrorMessage;
|
|
158
|
+
Callbacks?.OnWarn?.('Packages', `npm install failed — package.json entries were added but dependencies were not resolved. Run 'npm install' manually after fixing npm auth.\n Detail: ${pkgResult.ErrorMessage}`);
|
|
159
|
+
}
|
|
160
|
+
else {
|
|
161
|
+
await SetAppStatus(context.ContextUser, createdAppId, 'Error');
|
|
162
|
+
await RecordFailureHistory(context.ContextUser, createdAppId, 'Install', manifest, 'Packages', pkgResult.ErrorMessage ?? 'Package installation failed', startTime);
|
|
163
|
+
return BuildFailureResult('Install', manifest.name, manifest.version, 'Packages', startTime, pkgResult.ErrorMessage ?? 'Package installation failed');
|
|
164
|
+
}
|
|
110
165
|
}
|
|
111
166
|
else {
|
|
167
|
+
await SetAppStep(context.ContextUser, createdAppId, 'PackagesInstalled');
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!IsStepDone(INSTALL_STEP_ORDER, resumeCheckpoint, 'ConfigUpdated')) {
|
|
171
|
+
const configResult = HandleServerConfig(manifest, context);
|
|
172
|
+
if (!configResult.Success) {
|
|
112
173
|
await SetAppStatus(context.ContextUser, createdAppId, 'Error');
|
|
113
|
-
await RecordFailureHistory(context.ContextUser, createdAppId, 'Install', manifest, '
|
|
114
|
-
return BuildFailureResult('Install', manifest.name, manifest.version, '
|
|
174
|
+
await RecordFailureHistory(context.ContextUser, createdAppId, 'Install', manifest, 'Config', configResult.ErrorMessage ?? 'Config update failed', startTime);
|
|
175
|
+
return BuildFailureResult('Install', manifest.name, manifest.version, 'Config', startTime, configResult.ErrorMessage ?? 'Config update failed');
|
|
115
176
|
}
|
|
177
|
+
await SetAppStep(context.ContextUser, createdAppId, 'ConfigUpdated');
|
|
116
178
|
}
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
await
|
|
120
|
-
await RecordFailureHistory(context.ContextUser, createdAppId, 'Install', manifest, 'Config', configResult.ErrorMessage ?? 'Config update failed', startTime);
|
|
121
|
-
return BuildFailureResult('Install', manifest.name, manifest.version, 'Config', startTime, configResult.ErrorMessage ?? 'Config update failed');
|
|
179
|
+
if (!IsStepDone(INSTALL_STEP_ORDER, resumeCheckpoint, 'AngularExcludesUpdated')) {
|
|
180
|
+
HandleAngularPrebundleExcludes(manifest, context);
|
|
181
|
+
await SetAppStep(context.ContextUser, createdAppId, 'AngularExcludesUpdated');
|
|
122
182
|
}
|
|
123
|
-
HandleAngularPrebundleExcludes(manifest, context);
|
|
124
183
|
Callbacks?.OnProgress?.('Record', 'Finalizing installation...');
|
|
125
184
|
const finalStatus = npmInstallWarning ? 'Disabled' : 'Active';
|
|
126
185
|
await SetAppStatus(context.ContextUser, createdAppId, finalStatus);
|
|
127
186
|
if (finalStatus !== 'Active') {
|
|
128
187
|
ToggleServerDynamicPackages(context.RepoRoot, manifest.name, false, context.ServerPackagePath);
|
|
129
188
|
}
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
189
|
+
await SetAppStep(context.ContextUser, createdAppId, 'Finalized');
|
|
190
|
+
if (!IsStepDone(INSTALL_STEP_ORDER, resumeCheckpoint, 'HooksRun')) {
|
|
191
|
+
if (manifest.hooks?.postInstall) {
|
|
192
|
+
Callbacks?.OnProgress?.('Hooks', 'Running postInstall hook...');
|
|
193
|
+
await ExecuteHook(manifest.hooks.postInstall, context.RepoRoot);
|
|
194
|
+
}
|
|
195
|
+
if (manifest.hooks?.postInstallModule) {
|
|
196
|
+
Callbacks?.OnProgress?.('Hooks', `Running postInstall module '${manifest.hooks.postInstallModule}'...`);
|
|
197
|
+
const appInfo = await FindInstalledApp(context.ContextUser, manifest.name);
|
|
198
|
+
await ExecuteHookModule(manifest.hooks.postInstallModule, {
|
|
199
|
+
App: appInfo,
|
|
200
|
+
RepoRoot: context.RepoRoot,
|
|
201
|
+
Provider: context.DatabaseProvider,
|
|
202
|
+
ContextUser: context.ContextUser,
|
|
203
|
+
Callbacks: context.Callbacks,
|
|
204
|
+
Manifest: manifest,
|
|
205
|
+
}, context.RepoRoot);
|
|
206
|
+
}
|
|
207
|
+
await SetAppStep(context.ContextUser, createdAppId, 'HooksRun');
|
|
145
208
|
}
|
|
209
|
+
await SetAppStep(context.ContextUser, createdAppId, null);
|
|
146
210
|
try {
|
|
147
211
|
await RecordInstallHistoryEntry(context.ContextUser, createdAppId, 'Install', manifest, {
|
|
148
212
|
Success: true,
|
|
149
213
|
DurationSeconds: GetDurationSeconds(startTime),
|
|
150
214
|
StartedAt: new Date(startTime),
|
|
151
215
|
EndedAt: new Date(),
|
|
152
|
-
Summary: 'Initial installation',
|
|
216
|
+
Summary: isResume ? 'Initial installation (resumed after a prior interrupted attempt)' : 'Initial installation',
|
|
153
217
|
});
|
|
154
218
|
}
|
|
155
219
|
catch (histErr) {
|
|
@@ -196,6 +260,7 @@ async function RecordInstallationAtomically(contextUser, manifest, callbacks, pr
|
|
|
196
260
|
try {
|
|
197
261
|
const appId = await RecordAppInstallation(contextUser, manifest, callbacks, tg, 'Installing', provider, subpath);
|
|
198
262
|
if (manifest.dependencies) {
|
|
263
|
+
await DeleteAppDependencies(contextUser, appId, tg);
|
|
199
264
|
await RecordAppDependencies(contextUser, appId, manifest.dependencies, tg);
|
|
200
265
|
}
|
|
201
266
|
callbacks?.OnProgress?.('Record', 'Committing installation records...');
|
|
@@ -236,6 +301,9 @@ export async function UpgradeApp(options, context) {
|
|
|
236
301
|
return BuildFailureResult('Upgrade', options.AppName, '', 'Schema', startTime, `App '${options.AppName}' is not installed`);
|
|
237
302
|
}
|
|
238
303
|
upgradeAppId = existingApp.ID;
|
|
304
|
+
if (existingApp.Status === 'Installing' || existingApp.Status === 'Removing') {
|
|
305
|
+
return BuildMutexConflictResult('Upgrade', options.AppName, existingApp.Version, startTime, existingApp.Status);
|
|
306
|
+
}
|
|
239
307
|
previousVersion = existingApp.Version;
|
|
240
308
|
const explicitUpgradeVersion = options.Version;
|
|
241
309
|
const targetVersion = explicitUpgradeVersion ?? (await GetLatestVersion(existingApp.RepositoryURL, context.GitHubOptions, existingApp.Subpath ?? undefined));
|
|
@@ -292,8 +360,19 @@ export async function UpgradeApp(options, context) {
|
|
|
292
360
|
}
|
|
293
361
|
}
|
|
294
362
|
}
|
|
363
|
+
const checkpointTargetsThisVersion = existingApp.LastCompletedStepTargetVersion === manifest.version;
|
|
364
|
+
const resumeCheckpoint = (existingApp.Status === 'Upgrading' || existingApp.Status === 'Error') && checkpointTargetsThisVersion ? (existingApp.LastCompletedStep ?? null) : null;
|
|
365
|
+
if (resumeCheckpoint) {
|
|
366
|
+
Callbacks?.OnProgress?.('Resume', `Resuming upgrade of ${options.AppName} to v${manifest.version} from step '${resumeCheckpoint}'...`);
|
|
367
|
+
}
|
|
368
|
+
else if ((existingApp.Status === 'Upgrading' || existingApp.Status === 'Error') && existingApp.LastCompletedStep && !checkpointTargetsThisVersion) {
|
|
369
|
+
Callbacks?.OnProgress?.('Resume', `A previous upgrade attempt targeting a different version (${existingApp.LastCompletedStepTargetVersion ?? 'unknown'}) was left in progress — starting the upgrade to v${manifest.version} from the top instead of resuming it.`);
|
|
370
|
+
}
|
|
371
|
+
if (!resumeCheckpoint && existingApp.LastCompletedStep) {
|
|
372
|
+
await SetAppStep(context.ContextUser, existingApp.ID, null);
|
|
373
|
+
}
|
|
295
374
|
await SetAppStatus(context.ContextUser, existingApp.ID, 'Upgrading');
|
|
296
|
-
if (manifest.migrations && manifest.schema) {
|
|
375
|
+
if (!IsStepDone(UPGRADE_STEP_ORDER, resumeCheckpoint, 'MigrationsApplied') && manifest.migrations && manifest.schema) {
|
|
297
376
|
const migrationResult = await HandleMigrations(manifest, context, subpath);
|
|
298
377
|
if (!migrationResult.Success) {
|
|
299
378
|
const detail = migrationResult.ErrorMessage ?? 'Migration failed';
|
|
@@ -305,55 +384,75 @@ export async function UpgradeApp(options, context) {
|
|
|
305
384
|
await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
|
|
306
385
|
return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Migration', startTime, recoverable);
|
|
307
386
|
}
|
|
387
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'MigrationsApplied', undefined, manifest.version);
|
|
308
388
|
}
|
|
309
|
-
const effectiveUpgradeVersion = explicitUpgradeVersion ? targetVersion.replace(/^v/, '') : manifest.version;
|
|
310
|
-
const effectiveUpgradeStrategy = explicitUpgradeVersion ? 'exact' : context.VersionStrategy;
|
|
311
|
-
const pkgResult = await HandlePackageInstallation(manifest, context, effectiveUpgradeVersion, effectiveUpgradeStrategy, options.Verbose);
|
|
312
389
|
let npmInstallWarning;
|
|
313
|
-
if (!
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
390
|
+
if (!IsStepDone(UPGRADE_STEP_ORDER, resumeCheckpoint, 'PackagesInstalled')) {
|
|
391
|
+
const effectiveUpgradeVersion = explicitUpgradeVersion ? targetVersion.replace(/^v/, '') : manifest.version;
|
|
392
|
+
const effectiveUpgradeStrategy = explicitUpgradeVersion ? 'exact' : context.VersionStrategy;
|
|
393
|
+
const pkgResult = await HandlePackageInstallation(manifest, context, effectiveUpgradeVersion, effectiveUpgradeStrategy, options.Verbose);
|
|
394
|
+
if (!pkgResult.Success) {
|
|
395
|
+
if (pkgResult.PackageJsonUpdated) {
|
|
396
|
+
npmInstallWarning = pkgResult.ErrorMessage;
|
|
397
|
+
Callbacks?.OnWarn?.('Packages', `npm install failed — package.json entries were updated but dependencies were not resolved. Run 'npm install' manually after fixing npm auth.\n Detail: ${pkgResult.ErrorMessage}`);
|
|
398
|
+
}
|
|
399
|
+
else {
|
|
400
|
+
await RecordFailureHistory(context.ContextUser, existingApp.ID, 'Upgrade', manifest, 'Packages', pkgResult.ErrorMessage ?? 'Package update failed', startTime, previousVersion);
|
|
401
|
+
await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
|
|
402
|
+
return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Packages', startTime, pkgResult.ErrorMessage ?? 'Package update failed');
|
|
403
|
+
}
|
|
317
404
|
}
|
|
318
405
|
else {
|
|
319
|
-
await
|
|
406
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'PackagesInstalled', undefined, manifest.version);
|
|
407
|
+
}
|
|
408
|
+
}
|
|
409
|
+
if (!IsStepDone(UPGRADE_STEP_ORDER, resumeCheckpoint, 'ConfigUpdated')) {
|
|
410
|
+
const configResult = HandleServerConfig(manifest, context);
|
|
411
|
+
if (!configResult.Success) {
|
|
412
|
+
await RecordFailureHistory(context.ContextUser, existingApp.ID, 'Upgrade', manifest, 'Config', configResult.ErrorMessage ?? 'Config update failed', startTime, previousVersion);
|
|
320
413
|
await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
|
|
321
|
-
return BuildFailureResult('Upgrade', options.AppName, targetVersion, '
|
|
414
|
+
return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Config', startTime, configResult.ErrorMessage ?? 'Config update failed');
|
|
322
415
|
}
|
|
416
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'ConfigUpdated', undefined, manifest.version);
|
|
323
417
|
}
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
await
|
|
327
|
-
await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
|
|
328
|
-
return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Config', startTime, configResult.ErrorMessage ?? 'Config update failed');
|
|
418
|
+
if (!IsStepDone(UPGRADE_STEP_ORDER, resumeCheckpoint, 'AngularExcludesUpdated')) {
|
|
419
|
+
HandleAngularPrebundleExcludes(manifest, context);
|
|
420
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'AngularExcludesUpdated', undefined, manifest.version);
|
|
329
421
|
}
|
|
330
|
-
HandleAngularPrebundleExcludes(manifest, context);
|
|
331
422
|
await UpdateAppRecord(context.ContextUser, existingApp.ID, {
|
|
332
423
|
Version: manifest.version,
|
|
333
424
|
ManifestJSON: JSON.stringify(manifest),
|
|
334
425
|
Status: 'Active',
|
|
335
426
|
});
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
427
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'RecordUpdated', undefined, manifest.version);
|
|
428
|
+
if (!IsStepDone(UPGRADE_STEP_ORDER, resumeCheckpoint, 'HooksRun')) {
|
|
429
|
+
if (manifest.hooks?.postUpgrade) {
|
|
430
|
+
Callbacks?.OnProgress?.('Hooks', 'Running postUpgrade hook...');
|
|
431
|
+
await ExecuteHook(manifest.hooks.postUpgrade, context.RepoRoot);
|
|
432
|
+
}
|
|
433
|
+
if (manifest.hooks?.postUpgradeModule) {
|
|
434
|
+
Callbacks?.OnProgress?.('Hooks', `Running postUpgrade module '${manifest.hooks.postUpgradeModule}'...`);
|
|
435
|
+
await ExecuteHookModule(manifest.hooks.postUpgradeModule, {
|
|
436
|
+
App: existingApp,
|
|
437
|
+
RepoRoot: context.RepoRoot,
|
|
438
|
+
Provider: context.DatabaseProvider,
|
|
439
|
+
ContextUser: context.ContextUser,
|
|
440
|
+
Callbacks: context.Callbacks,
|
|
441
|
+
Manifest: manifest,
|
|
442
|
+
}, context.RepoRoot);
|
|
443
|
+
}
|
|
444
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'HooksRun', undefined, manifest.version);
|
|
350
445
|
}
|
|
351
|
-
if (manifest.dependencies) {
|
|
446
|
+
if (!IsStepDone(UPGRADE_STEP_ORDER, resumeCheckpoint, 'DependenciesReplaced') && manifest.dependencies) {
|
|
352
447
|
const depsReplaced = await ReplaceAppDependenciesAtomically(context.ContextUser, existingApp.ID, manifest.dependencies);
|
|
353
448
|
if (!depsReplaced) {
|
|
354
449
|
Callbacks?.OnWarn?.('Record', 'App upgraded, but its dependency records could not be updated atomically — re-run the upgrade to refresh them.');
|
|
355
450
|
}
|
|
451
|
+
else {
|
|
452
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'DependenciesReplaced', undefined, manifest.version);
|
|
453
|
+
}
|
|
356
454
|
}
|
|
455
|
+
await SetAppStep(context.ContextUser, existingApp.ID, null, undefined, manifest.version);
|
|
357
456
|
try {
|
|
358
457
|
await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Upgrade', manifest, {
|
|
359
458
|
PreviousVersion: previousVersion,
|
|
@@ -409,12 +508,23 @@ export async function RemoveApp(options, context) {
|
|
|
409
508
|
return BuildFailureResult('Remove', options.AppName, '', 'Schema', startTime, `App '${options.AppName}' is not installed`);
|
|
410
509
|
}
|
|
411
510
|
removeAppId = existingApp.ID;
|
|
511
|
+
if (existingApp.Status === 'Installing' || existingApp.Status === 'Upgrading') {
|
|
512
|
+
return BuildMutexConflictResult('Remove', options.AppName, existingApp.Version, startTime, existingApp.Status);
|
|
513
|
+
}
|
|
412
514
|
if (!options.Force) {
|
|
413
515
|
const dependents = await FindDependentApps(context.ContextUser, options.AppName);
|
|
414
516
|
if (dependents.length > 0) {
|
|
415
517
|
return BuildFailureResult('Remove', options.AppName, existingApp.Version, 'Schema', startTime, `Cannot remove: the following apps depend on ${options.AppName}: ${dependents.join(', ')}. Use --force to override.`);
|
|
416
518
|
}
|
|
417
519
|
}
|
|
520
|
+
const rawCheckpoint = existingApp.Status === 'Removing' || existingApp.Status === 'Error' ? (existingApp.LastCompletedStep ?? null) : null;
|
|
521
|
+
const resumeCheckpoint = IsOwnCheckpoint(REMOVE_STEP_ORDER, rawCheckpoint) ? rawCheckpoint : null;
|
|
522
|
+
if (existingApp.LastCompletedStep && !resumeCheckpoint) {
|
|
523
|
+
await SetAppStep(context.ContextUser, existingApp.ID, null);
|
|
524
|
+
}
|
|
525
|
+
if (resumeCheckpoint) {
|
|
526
|
+
Callbacks?.OnProgress?.('Resume', `Resuming removal of ${options.AppName} from step '${resumeCheckpoint}'...`);
|
|
527
|
+
}
|
|
418
528
|
await SetAppStatus(context.ContextUser, existingApp.ID, 'Removing');
|
|
419
529
|
const manifest = JSON.parse(existingApp.ManifestJSON);
|
|
420
530
|
removeManifest = manifest;
|
|
@@ -433,90 +543,106 @@ export async function RemoveApp(options, context) {
|
|
|
433
543
|
Manifest: manifest,
|
|
434
544
|
}, context.RepoRoot);
|
|
435
545
|
}
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
let metadataResult = { Success: true };
|
|
447
|
-
if (existingApp.SchemaName && !schemaShared) {
|
|
448
|
-
Callbacks?.OnProgress?.('Metadata', `Removing entity metadata for schema '${existingApp.SchemaName}'...`);
|
|
449
|
-
metadataResult = await RemoveAppEntityMetadata(existingApp.SchemaName, context.ContextUser, Callbacks);
|
|
450
|
-
}
|
|
451
|
-
let teardownResult = { Success: true };
|
|
452
|
-
if (!options.KeepData) {
|
|
453
|
-
teardownResult = await HandleTeardown(manifest, context, existingApp.Subpath ?? undefined);
|
|
454
|
-
if (!teardownResult.Success) {
|
|
455
|
-
Callbacks?.OnError?.('Metadata', `Teardown failed: ${teardownResult.ErrorMessage}`);
|
|
546
|
+
if (!IsStepDone(REMOVE_STEP_ORDER, resumeCheckpoint, 'DbCleanupDone')) {
|
|
547
|
+
const shareCheck = existingApp.SchemaName
|
|
548
|
+
? await CheckSchemaSharedByOtherApps(context.ContextUser, existingApp.SchemaName, existingApp.ID)
|
|
549
|
+
: { Shared: false, CheckFailed: false };
|
|
550
|
+
const schemaShared = shareCheck.Shared;
|
|
551
|
+
const shareCheckError = shareCheck.CheckFailed
|
|
552
|
+
? `Could not determine whether schema '${existingApp.SchemaName}' is shared by other Open Apps (share-check query failed): ${shareCheck.ErrorMessage ?? 'unknown error'}`
|
|
553
|
+
: undefined;
|
|
554
|
+
if (schemaShared && !shareCheck.CheckFailed) {
|
|
555
|
+
Callbacks?.OnWarn?.('Schema', `Schema '${existingApp.SchemaName}' is still used by another installed Open App — skipping metadata + schema removal to protect co-tenant data.`);
|
|
456
556
|
}
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
Callbacks?.OnError?.('Schema', `Failed to drop schema: ${dropResult.ErrorMessage}`);
|
|
557
|
+
let metadataResult = { Success: true };
|
|
558
|
+
if (existingApp.SchemaName && !schemaShared) {
|
|
559
|
+
Callbacks?.OnProgress?.('Metadata', `Removing entity metadata for schema '${existingApp.SchemaName}'...`);
|
|
560
|
+
const declaredApplicationIds = await ExtractDeclaredApplicationIds(manifest, context, existingApp.Subpath ?? undefined);
|
|
561
|
+
metadataResult = await RemoveAppEntityMetadata(existingApp.SchemaName, context.ContextUser, Callbacks, context.DatabaseProvider, {
|
|
562
|
+
DatabaseProvider: context.DatabaseProvider,
|
|
563
|
+
MJCoreSchema: context.MJCoreSchema,
|
|
564
|
+
DeclaredApplicationIds: declaredApplicationIds,
|
|
565
|
+
});
|
|
467
566
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
].filter((e) => !!e);
|
|
475
|
-
if (removalErrors.length > 0) {
|
|
476
|
-
const combined = removalErrors.join('; ');
|
|
477
|
-
await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Remove', manifest, {
|
|
478
|
-
Success: false,
|
|
479
|
-
DurationSeconds: GetDurationSeconds(startTime),
|
|
480
|
-
StartedAt: new Date(startTime),
|
|
481
|
-
EndedAt: new Date(),
|
|
482
|
-
Summary: `Remove failed: ${combined}`,
|
|
483
|
-
});
|
|
484
|
-
await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
|
|
485
|
-
return BuildFailureResult('Remove', options.AppName, existingApp.Version, 'Schema', startTime, combined);
|
|
486
|
-
}
|
|
487
|
-
Callbacks?.OnProgress?.('Config', 'Removing config, client bootstrap, and package references...');
|
|
488
|
-
const otherApps = (await ListInstalledApps(context.ContextUser))
|
|
489
|
-
.filter(a => a.Name !== options.AppName && a.Status !== 'Removed');
|
|
490
|
-
const otherManifests = otherApps.flatMap(a => {
|
|
491
|
-
try {
|
|
492
|
-
return [JSON.parse(a.ManifestJSON)];
|
|
567
|
+
let teardownResult = { Success: true };
|
|
568
|
+
if (!options.KeepData) {
|
|
569
|
+
teardownResult = await HandleTeardown(manifest, context, existingApp.Subpath ?? undefined);
|
|
570
|
+
if (!teardownResult.Success) {
|
|
571
|
+
Callbacks?.OnError?.('Metadata', `Teardown failed: ${teardownResult.ErrorMessage}`);
|
|
572
|
+
}
|
|
493
573
|
}
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
574
|
+
let schemaDropError;
|
|
575
|
+
if (!options.KeepData && existingApp.SchemaName && !schemaShared) {
|
|
576
|
+
Callbacks?.OnProgress?.('Schema', `Dropping schema '${existingApp.SchemaName}'...`);
|
|
577
|
+
const dropResult = await DropAppSchema(existingApp.SchemaName, context.DatabaseProvider, {
|
|
578
|
+
allowDoubleUnderscore: options.AllowDoubleUnderscoreSchema === true,
|
|
579
|
+
});
|
|
580
|
+
if (!dropResult.Success) {
|
|
581
|
+
schemaDropError = dropResult.ErrorMessage;
|
|
582
|
+
Callbacks?.OnError?.('Schema', `Failed to drop schema: ${dropResult.ErrorMessage}`);
|
|
583
|
+
}
|
|
497
584
|
}
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
585
|
+
const removalErrors = [
|
|
586
|
+
shareCheckError,
|
|
587
|
+
teardownResult.Success ? undefined : teardownResult.ErrorMessage,
|
|
588
|
+
metadataResult.Success ? undefined : metadataResult.ErrorMessage,
|
|
589
|
+
schemaDropError,
|
|
590
|
+
].filter((e) => !!e);
|
|
591
|
+
if (removalErrors.length > 0) {
|
|
592
|
+
const combined = removalErrors.join('; ');
|
|
593
|
+
await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Remove', manifest, {
|
|
594
|
+
Success: false,
|
|
595
|
+
DurationSeconds: GetDurationSeconds(startTime),
|
|
596
|
+
StartedAt: new Date(startTime),
|
|
597
|
+
EndedAt: new Date(),
|
|
598
|
+
Summary: `Remove failed: ${combined}`,
|
|
599
|
+
});
|
|
600
|
+
await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
|
|
601
|
+
return BuildFailureResult('Remove', options.AppName, existingApp.Version, 'Schema', startTime, combined);
|
|
602
|
+
}
|
|
603
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'DbCleanupDone');
|
|
604
|
+
}
|
|
605
|
+
if (!IsStepDone(REMOVE_STEP_ORDER, resumeCheckpoint, 'FilesRemoved')) {
|
|
606
|
+
Callbacks?.OnProgress?.('Config', 'Removing config, client bootstrap, and package references...');
|
|
607
|
+
const otherApps = (await ListInstalledApps(context.ContextUser))
|
|
608
|
+
.filter(a => a.Name !== options.AppName && a.Status !== 'Removed');
|
|
609
|
+
const otherManifests = otherApps.flatMap(a => {
|
|
610
|
+
try {
|
|
611
|
+
return [JSON.parse(a.ManifestJSON)];
|
|
612
|
+
}
|
|
613
|
+
catch {
|
|
614
|
+
Callbacks?.OnWarn?.('Config', `Ignoring app '${a.Name}' when computing shared excludes — its ManifestJSON could not be parsed.`);
|
|
615
|
+
return [];
|
|
616
|
+
}
|
|
617
|
+
});
|
|
618
|
+
await Promise.all([
|
|
619
|
+
Promise.resolve(RemoveServerDynamicPackages(context.RepoRoot, options.AppName, context.ServerPackagePath)),
|
|
620
|
+
Promise.resolve(manifest.schema ? RemoveEntityPackageMapping(context.RepoRoot, manifest.schema.name, context.ServerPackagePath) : undefined),
|
|
621
|
+
Promise.resolve(HandleAngularPrebundleExcludeRemoval(manifest, otherManifests, context)),
|
|
622
|
+
Promise.resolve(RemoveAppPackages({
|
|
623
|
+
RepoRoot: context.RepoRoot,
|
|
624
|
+
ServerPackages: manifest.packages?.server ?? [],
|
|
625
|
+
ClientPackages: manifest.packages?.client ?? [],
|
|
626
|
+
SharedPackages: manifest.packages?.shared ?? [],
|
|
627
|
+
Version: existingApp.Version,
|
|
628
|
+
ServerPackagePath: context.ServerPackagePath,
|
|
629
|
+
ClientPackagePath: context.ClientPackagePath,
|
|
630
|
+
PackageManager: context.PackageManager,
|
|
631
|
+
AdditionalTargets: context.AdditionalTargets,
|
|
632
|
+
})),
|
|
633
|
+
]);
|
|
634
|
+
Callbacks?.OnProgress?.('Packages', 'Running package install...');
|
|
635
|
+
const installResult = RunPackageInstall(context.RepoRoot, options.Verbose, undefined, context.PackageManager);
|
|
636
|
+
if (!installResult.Success) {
|
|
637
|
+
Callbacks?.OnWarn?.('Packages', `Package install warning during removal: ${installResult.ErrorMessage}`);
|
|
638
|
+
}
|
|
639
|
+
await SetAppStep(context.ContextUser, existingApp.ID, 'FilesRemoved');
|
|
519
640
|
}
|
|
641
|
+
await UpdateAppRecord(context.ContextUser, existingApp.ID, {
|
|
642
|
+
Status: 'Removed',
|
|
643
|
+
LastCompletedStep: null,
|
|
644
|
+
LastCompletedStepTargetVersion: null,
|
|
645
|
+
});
|
|
520
646
|
try {
|
|
521
647
|
await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Remove', manifest, {
|
|
522
648
|
Success: true,
|
|
@@ -529,9 +655,6 @@ export async function RemoveApp(options, context) {
|
|
|
529
655
|
catch (histErr) {
|
|
530
656
|
Callbacks?.OnWarn?.('Record', `App removed, but the history audit entry could not be written: ${histErr instanceof Error ? histErr.message : String(histErr)}`);
|
|
531
657
|
}
|
|
532
|
-
await UpdateAppRecord(context.ContextUser, existingApp.ID, {
|
|
533
|
-
Status: 'Removed',
|
|
534
|
-
});
|
|
535
658
|
Callbacks?.OnSuccess?.('Remove', `Successfully removed ${options.AppName}`);
|
|
536
659
|
return {
|
|
537
660
|
Success: true,
|
|
@@ -565,6 +688,9 @@ export async function DisableApp(appName, context) {
|
|
|
565
688
|
if (!app) {
|
|
566
689
|
return BuildFailureResult('Install', appName, '', 'Config', startTime, `App '${appName}' is not installed`);
|
|
567
690
|
}
|
|
691
|
+
if (!SETTLED_STATUSES.has(app.Status)) {
|
|
692
|
+
return BuildMutexConflictResult('Install', appName, app.Version, startTime, app.Status, 'disable');
|
|
693
|
+
}
|
|
568
694
|
const toggle = ToggleServerDynamicPackages(context.RepoRoot, appName, false, context.ServerPackagePath);
|
|
569
695
|
if (!toggle.Success) {
|
|
570
696
|
return BuildFailureResult('Install', appName, app.Version, 'Config', startTime, toggle.ErrorMessage ?? 'Failed to update dynamicPackages.server in mj.config.cjs');
|
|
@@ -585,6 +711,9 @@ export async function EnableApp(appName, context) {
|
|
|
585
711
|
if (!app) {
|
|
586
712
|
return BuildFailureResult('Install', appName, '', 'Config', startTime, `App '${appName}' is not installed`);
|
|
587
713
|
}
|
|
714
|
+
if (!SETTLED_STATUSES.has(app.Status)) {
|
|
715
|
+
return BuildMutexConflictResult('Install', appName, app.Version, startTime, app.Status, 'enable');
|
|
716
|
+
}
|
|
588
717
|
const toggle = ToggleServerDynamicPackages(context.RepoRoot, appName, true, context.ServerPackagePath);
|
|
589
718
|
if (!toggle.Success) {
|
|
590
719
|
return BuildFailureResult('Install', appName, app.Version, 'Config', startTime, toggle.ErrorMessage ?? 'Failed to update dynamicPackages.server in mj.config.cjs');
|
|
@@ -607,6 +736,9 @@ async function ResolveDependencyChain(manifest, context) {
|
|
|
607
736
|
const installedApps = await ListInstalledApps(context.ContextUser);
|
|
608
737
|
const installedMap = {};
|
|
609
738
|
for (const app of installedApps) {
|
|
739
|
+
if (app.Status !== 'Active' && app.Status !== 'Disabled') {
|
|
740
|
+
continue;
|
|
741
|
+
}
|
|
610
742
|
installedMap[app.Name] = { Version: app.Version, Repository: app.RepositoryURL };
|
|
611
743
|
}
|
|
612
744
|
const dependencies = (manifest.dependencies ?? {});
|
|
@@ -922,63 +1054,88 @@ async function ExecuteHookModule(specifier, payload, repoRoot) {
|
|
|
922
1054
|
throw new OpenAppPhaseError('Hooks', err instanceof Error ? err.message : String(err));
|
|
923
1055
|
}
|
|
924
1056
|
}
|
|
925
|
-
export async function RemoveAppEntityMetadata(schemaName, contextUser, callbacks, provider) {
|
|
1057
|
+
export async function RemoveAppEntityMetadata(schemaName, contextUser, callbacks, provider, options) {
|
|
926
1058
|
try {
|
|
927
1059
|
const rv = new RunView();
|
|
928
1060
|
const escaped = EscapeSqlString(schemaName);
|
|
1061
|
+
const declaredAppIds = (options?.DeclaredApplicationIds ?? [])
|
|
1062
|
+
.map((id) => NormalizeUUID(id))
|
|
1063
|
+
.filter((id) => !!id);
|
|
1064
|
+
const useFkGraph = !!options?.DatabaseProvider?.Dialect;
|
|
929
1065
|
const md = (provider ?? new Metadata());
|
|
930
|
-
const tg = await md.CreateTransactionGroup();
|
|
931
|
-
const queueDeleteByFilterOrThrow = async (entityName, filter) => {
|
|
932
|
-
const r = await QueueDeleteEntitiesByFilter(rv, contextUser, entityName, filter, tg);
|
|
933
|
-
if (!r.Success) {
|
|
934
|
-
throw new Error(r.ErrorMessage ?? `Failed to queue delete of ${entityName} records`);
|
|
935
|
-
}
|
|
936
|
-
};
|
|
937
1066
|
const entityResult = await rv.RunView({
|
|
938
1067
|
EntityName: 'MJ: Entities',
|
|
939
|
-
ExtraFilter: `SchemaName = '${escaped}'`,
|
|
1068
|
+
ExtraFilter: `LOWER(SchemaName) = LOWER('${escaped}')`,
|
|
940
1069
|
ResultType: 'entity_object',
|
|
941
1070
|
}, contextUser);
|
|
942
1071
|
if (!entityResult.Success) {
|
|
943
1072
|
throw new Error(`Failed to query entities for schema '${schemaName}': ${entityResult.ErrorMessage}`);
|
|
944
1073
|
}
|
|
945
1074
|
if (entityResult.Results.length === 0) {
|
|
946
|
-
|
|
1075
|
+
const tg = await md.CreateTransactionGroup();
|
|
1076
|
+
const r = await QueueDeleteEntitiesByFilter(rv, contextUser, 'MJ: Schema Info', `LOWER(SchemaName) = LOWER('${escaped}')`, tg);
|
|
1077
|
+
if (!r.Success) {
|
|
1078
|
+
throw new Error(r.ErrorMessage ?? 'Failed to queue delete of MJ: Schema Info records');
|
|
1079
|
+
}
|
|
947
1080
|
if (!(await tg.Submit())) {
|
|
948
1081
|
throw new Error('Transaction failed: schema-info cleanup could not be committed');
|
|
949
1082
|
}
|
|
1083
|
+
await DeleteAppOwnedApplications(rv, declaredAppIds, contextUser, callbacks);
|
|
950
1084
|
callbacks?.OnSuccess?.('Metadata', `Entity metadata for schema '${schemaName}' removed`);
|
|
951
1085
|
return { Success: true };
|
|
952
1086
|
}
|
|
953
1087
|
const entityIds = entityResult.Results.map((e) => e.ID);
|
|
954
1088
|
const idList = entityIds.map((id) => `'${EscapeSqlString(id)}'`).join(',');
|
|
955
|
-
const fieldResult = await rv.RunView({
|
|
956
|
-
EntityName: 'MJ: Entity Fields',
|
|
957
|
-
ExtraFilter: `EntityID IN (${idList})`,
|
|
958
|
-
ResultType: 'entity_object',
|
|
959
|
-
}, contextUser);
|
|
960
|
-
if (!fieldResult.Success) {
|
|
961
|
-
throw new Error(`Failed to query entity fields for schema '${schemaName}': ${fieldResult.ErrorMessage}`);
|
|
962
|
-
}
|
|
963
|
-
const fieldIdList = fieldResult.Results.map((f) => `'${EscapeSqlString(f.ID)}'`).join(',');
|
|
964
1089
|
const ownedAppIds = await FindAppOwnedApplications(rv, contextUser, entityIds, idList);
|
|
965
|
-
if (
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
981
|
-
|
|
1090
|
+
if (useFkGraph) {
|
|
1091
|
+
const dbProvider = options.DatabaseProvider;
|
|
1092
|
+
const mjSchema = options?.MJCoreSchema ?? '__mj';
|
|
1093
|
+
const rootPredicate = buildRootDoomedPredicate(dbProvider.Dialect, schemaName);
|
|
1094
|
+
await RunFkGraphTeardown(dbProvider, mjSchema, rootPredicate, callbacks);
|
|
1095
|
+
}
|
|
1096
|
+
else {
|
|
1097
|
+
const tg = await md.CreateTransactionGroup();
|
|
1098
|
+
const queueDeleteByFilterOrThrow = async (entityName, filter) => {
|
|
1099
|
+
const r = await QueueDeleteEntitiesByFilter(rv, contextUser, entityName, filter, tg);
|
|
1100
|
+
if (!r.Success) {
|
|
1101
|
+
throw new Error(r.ErrorMessage ?? `Failed to queue delete of ${entityName} records`);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
const fieldResult = await rv.RunView({
|
|
1105
|
+
EntityName: 'MJ: Entity Fields',
|
|
1106
|
+
ExtraFilter: `EntityID IN (${idList})`,
|
|
1107
|
+
ResultType: 'entity_object',
|
|
1108
|
+
}, contextUser);
|
|
1109
|
+
if (!fieldResult.Success) {
|
|
1110
|
+
throw new Error(`Failed to query entity fields for schema '${schemaName}': ${fieldResult.ErrorMessage}`);
|
|
1111
|
+
}
|
|
1112
|
+
const fieldIdList = fieldResult.Results.map((f) => `'${EscapeSqlString(f.ID)}'`).join(',');
|
|
1113
|
+
if (fieldIdList.length > 0) {
|
|
1114
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Field Values', `EntityFieldID IN (${fieldIdList})`);
|
|
1115
|
+
}
|
|
1116
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Permissions', `EntityID IN (${idList})`);
|
|
1117
|
+
await queueDeleteByFilterOrThrow('MJ: Application Entities', `EntityID IN (${idList})`);
|
|
1118
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Settings', `EntityID IN (${idList})`);
|
|
1119
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Relationships', `EntityID IN (${idList}) OR RelatedEntityID IN (${idList})`);
|
|
1120
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Fields', `EntityID IN (${idList})`);
|
|
1121
|
+
for (const entity of entityResult.Results) {
|
|
1122
|
+
entity.TransactionGroup = tg;
|
|
1123
|
+
await entity.Delete();
|
|
1124
|
+
}
|
|
1125
|
+
await queueDeleteByFilterOrThrow('MJ: Schema Info', `SchemaName = '${escaped}'`);
|
|
1126
|
+
if (!(await tg.Submit())) {
|
|
1127
|
+
throw new Error('Transaction failed: entity metadata cleanup could not be committed atomically');
|
|
1128
|
+
}
|
|
1129
|
+
}
|
|
1130
|
+
const allAppIds = [];
|
|
1131
|
+
const appSeen = new Set();
|
|
1132
|
+
for (const id of ownedAppIds.map((x) => NormalizeUUID(x)).concat(declaredAppIds)) {
|
|
1133
|
+
if (id && !appSeen.has(id)) {
|
|
1134
|
+
appSeen.add(id);
|
|
1135
|
+
allAppIds.push(id);
|
|
1136
|
+
}
|
|
1137
|
+
}
|
|
1138
|
+
await DeleteAppOwnedApplications(rv, allAppIds, contextUser, callbacks);
|
|
982
1139
|
callbacks?.OnSuccess?.('Metadata', `Entity metadata for schema '${schemaName}' removed`);
|
|
983
1140
|
return { Success: true };
|
|
984
1141
|
}
|
|
@@ -988,6 +1145,33 @@ export async function RemoveAppEntityMetadata(schemaName, contextUser, callbacks
|
|
|
988
1145
|
return { Success: false, ErrorMessage: `Failed to remove entity metadata for schema '${schemaName}': ${message}` };
|
|
989
1146
|
}
|
|
990
1147
|
}
|
|
1148
|
+
async function ExtractDeclaredApplicationIds(manifest, context, subpath) {
|
|
1149
|
+
if (!manifest.migrations || !manifest.schema) {
|
|
1150
|
+
return [];
|
|
1151
|
+
}
|
|
1152
|
+
let tempDir;
|
|
1153
|
+
try {
|
|
1154
|
+
tempDir = join(tmpdir(), `mj-app-${manifest.name}-appids-${Date.now()}`);
|
|
1155
|
+
mkdirSync(tempDir, { recursive: true });
|
|
1156
|
+
const download = await DownloadAppMigrations(manifest, context, tempDir, subpath);
|
|
1157
|
+
if (!download.Success) {
|
|
1158
|
+
return [];
|
|
1159
|
+
}
|
|
1160
|
+
return await extractApplicationIds(tempDir);
|
|
1161
|
+
}
|
|
1162
|
+
catch {
|
|
1163
|
+
return [];
|
|
1164
|
+
}
|
|
1165
|
+
finally {
|
|
1166
|
+
if (tempDir) {
|
|
1167
|
+
try {
|
|
1168
|
+
rmSync(tempDir, { recursive: true, force: true });
|
|
1169
|
+
}
|
|
1170
|
+
catch {
|
|
1171
|
+
}
|
|
1172
|
+
}
|
|
1173
|
+
}
|
|
1174
|
+
}
|
|
991
1175
|
async function FindAppOwnedApplications(rv, contextUser, entityIds, idList) {
|
|
992
1176
|
const ours = new Set(entityIds.map((id) => NormalizeUUID(id)));
|
|
993
1177
|
const linked = await rv.RunView({ EntityName: 'MJ: Application Entities', ExtraFilter: `EntityID IN (${idList})`, Fields: ['ApplicationID'], ResultType: 'simple' }, contextUser);
|