@memberjunction/open-app-engine 5.41.0 → 5.43.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/dependency/dependency-graph-builder.d.ts +1 -1
- package/dist/dependency/dependency-graph-builder.d.ts.map +1 -1
- package/dist/dependency/dependency-graph-builder.js +5 -3
- package/dist/dependency/dependency-graph-builder.js.map +1 -1
- package/dist/dependency/dependency-resolver.d.ts +1 -0
- package/dist/dependency/dependency-resolver.d.ts.map +1 -1
- package/dist/dependency/dependency-resolver.js.map +1 -1
- package/dist/github/github-client.d.ts +10 -5
- package/dist/github/github-client.d.ts.map +1 -1
- package/dist/github/github-client.js +110 -94
- package/dist/github/github-client.js.map +1 -1
- package/dist/index.d.ts +2 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +2 -3
- package/dist/index.js.map +1 -1
- package/dist/install/config-manager.d.ts +2 -1
- package/dist/install/config-manager.d.ts.map +1 -1
- package/dist/install/config-manager.js +175 -28
- package/dist/install/config-manager.js.map +1 -1
- package/dist/install/history-recorder.d.ts +14 -3
- package/dist/install/history-recorder.d.ts.map +1 -1
- package/dist/install/history-recorder.js +82 -48
- package/dist/install/history-recorder.js.map +1 -1
- package/dist/install/install-orchestrator.d.ts +9 -2
- package/dist/install/install-orchestrator.d.ts.map +1 -1
- package/dist/install/install-orchestrator.js +348 -146
- package/dist/install/install-orchestrator.js.map +1 -1
- package/dist/install/migration-runner.d.ts +27 -0
- package/dist/install/migration-runner.d.ts.map +1 -1
- package/dist/install/migration-runner.js +29 -8
- package/dist/install/migration-runner.js.map +1 -1
- package/dist/install/package-manager.d.ts.map +1 -1
- package/dist/install/package-manager.js +51 -20
- package/dist/install/package-manager.js.map +1 -1
- package/dist/install/schema-manager.d.ts.map +1 -1
- package/dist/install/schema-manager.js +17 -12
- package/dist/install/schema-manager.js.map +1 -1
- package/dist/manifest/manifest-schema.d.ts +53 -6
- package/dist/manifest/manifest-schema.d.ts.map +1 -1
- package/dist/manifest/manifest-schema.js +20 -7
- package/dist/manifest/manifest-schema.js.map +1 -1
- package/dist/types/open-app-types.d.ts +3 -0
- package/dist/types/open-app-types.d.ts.map +1 -1
- package/package.json +6 -3
- package/dist/install/client-bootstrap-gen.d.ts +0 -8
- package/dist/install/client-bootstrap-gen.d.ts.map +0 -1
- package/dist/install/client-bootstrap-gen.js +0 -38
- package/dist/install/client-bootstrap-gen.js.map +0 -1
|
@@ -1,34 +1,36 @@
|
|
|
1
1
|
import { tmpdir } from 'node:os';
|
|
2
2
|
import { join } from 'node:path';
|
|
3
|
-
import { mkdirSync } from 'node:fs';
|
|
3
|
+
import { mkdirSync, readFileSync } from 'node:fs';
|
|
4
4
|
import { ParseAndValidateManifest } from '../manifest/manifest-loader.js';
|
|
5
5
|
import { CheckMJVersionCompatibility, IsValidUpgrade } from '../dependency/version-checker.js';
|
|
6
6
|
import { ResolveDependencyGraph } from '../dependency/dependency-graph-builder.js';
|
|
7
|
-
import { FetchManifestFromGitHub, DownloadMigrations, GetLatestVersion, ValidateGitHubTag } from '../github/github-client.js';
|
|
7
|
+
import { FetchManifestFromGitHub, DownloadMigrations, GetLatestVersion, ListGitHubReleases, ListGitHubTags, ValidateGitHubTag, ParseGitHubUrl } from '../github/github-client.js';
|
|
8
|
+
import semver from 'semver';
|
|
8
9
|
import { CreateAppSchema, DropAppSchema, SchemaExists, EscapeSqlString } from './schema-manager.js';
|
|
9
10
|
import { RunAppMigrations } from './migration-runner.js';
|
|
10
11
|
import { AddAppPackages, RemoveAppPackages, RunPackageInstall, BumpPrefixedDependencies } from './package-manager.js';
|
|
11
|
-
import { AddServerDynamicPackages, RemoveServerDynamicPackages, ToggleServerDynamicPackages, AddEntityPackageMapping, RemoveEntityPackageMapping } from './config-manager.js';
|
|
12
|
+
import { AddServerDynamicPackages, AddClientDynamicPackages, RemoveServerDynamicPackages, ToggleServerDynamicPackages, AddEntityPackageMapping, RemoveEntityPackageMapping } from './config-manager.js';
|
|
12
13
|
import { AngularConfigManager } from './angular-config-manager.js';
|
|
13
|
-
import { RegenerateClientBootstrap } from './client-bootstrap-gen.js';
|
|
14
14
|
import { Metadata, RunView } from '@memberjunction/core';
|
|
15
|
-
import {
|
|
15
|
+
import { NormalizeUUID } from '@memberjunction/global';
|
|
16
|
+
import { RecordAppInstallation, RecordInstallHistoryEntry, RecordAppDependencies, ReplaceAppDependenciesAtomically, SetAppStatus, FindInstalledApp, CheckSchemaSharedByOtherApps, FindDependentApps, ListInstalledApps, UpdateAppRecord, } from './history-recorder.js';
|
|
16
17
|
export async function InstallApp(options, context) {
|
|
17
18
|
const startTime = Date.now();
|
|
18
19
|
const { Callbacks } = context;
|
|
19
20
|
let createdAppId;
|
|
20
21
|
let manifest;
|
|
22
|
+
const subpath = options.Subpath ?? ParseGitHubUrl(options.Source)?.Subpath;
|
|
21
23
|
try {
|
|
22
24
|
const explicitVersion = options.Version;
|
|
23
25
|
if (explicitVersion) {
|
|
24
26
|
Callbacks?.OnProgress?.('Fetch', `Validating version tag v${explicitVersion.replace(/^v/, '')} exists...`);
|
|
25
|
-
const tagResult = await ValidateGitHubTag(options.Source, explicitVersion, context.GitHubOptions);
|
|
27
|
+
const tagResult = await ValidateGitHubTag(options.Source, explicitVersion, context.GitHubOptions, subpath);
|
|
26
28
|
if (!tagResult.Exists) {
|
|
27
29
|
return BuildFailureResult('Install', options.Source, '', 'Schema', startTime, tagResult.ErrorMessage ?? `Version ${explicitVersion} not found`);
|
|
28
30
|
}
|
|
29
31
|
}
|
|
30
32
|
Callbacks?.OnProgress?.('Fetch', `Fetching manifest from ${options.Source}...`);
|
|
31
|
-
const fetchResult = await FetchManifestFromGitHub(options.Source, explicitVersion, context.GitHubOptions);
|
|
33
|
+
const fetchResult = await FetchManifestFromGitHub(options.Source, explicitVersion, context.GitHubOptions, subpath);
|
|
32
34
|
if (!fetchResult.Success || !fetchResult.ManifestJSON) {
|
|
33
35
|
return BuildFailureResult('Install', options.Source, '', 'Schema', startTime, fetchResult.ErrorMessage ?? 'Failed to fetch manifest');
|
|
34
36
|
}
|
|
@@ -61,7 +63,7 @@ export async function InstallApp(options, context) {
|
|
|
61
63
|
}
|
|
62
64
|
}
|
|
63
65
|
const existingApp = await FindInstalledApp(context.ContextUser, manifest.name);
|
|
64
|
-
const isReinstall = existingApp != null && existingApp.Status === 'Removed';
|
|
66
|
+
const isReinstall = existingApp != null && (existingApp.Status === 'Removed' || existingApp.Status === 'Error');
|
|
65
67
|
if (existingApp && !isReinstall) {
|
|
66
68
|
return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, `App '${manifest.name}' is already installed with status '${existingApp.Status}'. Use 'mj app upgrade' to update it.`);
|
|
67
69
|
}
|
|
@@ -72,24 +74,24 @@ export async function InstallApp(options, context) {
|
|
|
72
74
|
if (!schemaResult.Success) {
|
|
73
75
|
return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, schemaResult.ErrorMessage ?? 'Schema creation failed');
|
|
74
76
|
}
|
|
75
|
-
schemaCreated =
|
|
77
|
+
schemaCreated = schemaResult.Created === true;
|
|
76
78
|
}
|
|
77
79
|
if (manifest.migrations && manifest.schema) {
|
|
78
|
-
const migrationResult = await HandleMigrations(manifest, context);
|
|
80
|
+
const migrationResult = await HandleMigrations(manifest, context, subpath);
|
|
79
81
|
if (!migrationResult.Success) {
|
|
80
82
|
await CompensateSchemaOnFailure(manifest, context, schemaCreated, options.AllowDoubleUnderscoreSchema === true, Callbacks);
|
|
81
83
|
return BuildFailureResult('Install', manifest.name, manifest.version, 'Migration', startTime, migrationResult.ErrorMessage ?? 'Migration failed');
|
|
82
84
|
}
|
|
83
85
|
}
|
|
84
86
|
Callbacks?.OnProgress?.('Record', 'Recording app installation...');
|
|
85
|
-
const recordResult = await RecordInstallationAtomically(context.ContextUser, manifest, Callbacks);
|
|
87
|
+
const recordResult = await RecordInstallationAtomically(context.ContextUser, manifest, Callbacks, undefined, subpath);
|
|
86
88
|
if (!recordResult.Success) {
|
|
87
89
|
await CompensateSchemaOnFailure(manifest, context, schemaCreated, options.AllowDoubleUnderscoreSchema === true, Callbacks);
|
|
88
90
|
return BuildFailureResult('Install', manifest.name, manifest.version, 'Record', startTime, recordResult.ErrorMessage ?? 'Failed to record installation');
|
|
89
91
|
}
|
|
90
92
|
createdAppId = recordResult.AppId;
|
|
91
93
|
Callbacks?.OnProgress?.('Config', 'Updating configuration files...');
|
|
92
|
-
const pkgResult = await HandlePackageInstallation(manifest, context, effectivePackageVersion, effectiveVersionStrategy);
|
|
94
|
+
const pkgResult = await HandlePackageInstallation(manifest, context, effectivePackageVersion, effectiveVersionStrategy, options.Verbose);
|
|
93
95
|
let npmInstallWarning;
|
|
94
96
|
if (!pkgResult.Success) {
|
|
95
97
|
if (pkgResult.PackageJsonUpdated) {
|
|
@@ -110,24 +112,38 @@ export async function InstallApp(options, context) {
|
|
|
110
112
|
}
|
|
111
113
|
HandleAngularPrebundleExcludes(manifest, context);
|
|
112
114
|
Callbacks?.OnProgress?.('Record', 'Finalizing installation...');
|
|
113
|
-
|
|
114
|
-
await
|
|
115
|
+
const finalStatus = npmInstallWarning ? 'Disabled' : 'Active';
|
|
116
|
+
await SetAppStatus(context.ContextUser, createdAppId, finalStatus);
|
|
117
|
+
if (finalStatus !== 'Active') {
|
|
118
|
+
ToggleServerDynamicPackages(context.RepoRoot, manifest.name, false);
|
|
119
|
+
}
|
|
115
120
|
if (manifest.hooks?.postInstall) {
|
|
116
121
|
Callbacks?.OnProgress?.('Hooks', 'Running postInstall hook...');
|
|
117
122
|
await ExecuteHook(manifest.hooks.postInstall, context.RepoRoot);
|
|
118
123
|
}
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
124
|
+
try {
|
|
125
|
+
await RecordInstallHistoryEntry(context.ContextUser, createdAppId, 'Install', manifest, {
|
|
126
|
+
Success: true,
|
|
127
|
+
DurationSeconds: GetDurationSeconds(startTime),
|
|
128
|
+
StartedAt: new Date(startTime),
|
|
129
|
+
EndedAt: new Date(),
|
|
130
|
+
Summary: 'Initial installation',
|
|
131
|
+
});
|
|
132
|
+
}
|
|
133
|
+
catch (histErr) {
|
|
134
|
+
Callbacks?.OnWarn?.('Record', `App installed, but the history audit entry could not be written: ${histErr instanceof Error ? histErr.message : String(histErr)}`);
|
|
135
|
+
}
|
|
126
136
|
Callbacks?.OnSuccess?.('Install', `Successfully installed ${manifest.name} v${manifest.version}`);
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
137
|
+
let summary;
|
|
138
|
+
if (npmInstallWarning) {
|
|
139
|
+
summary = `App installed but left DISABLED — npm install failed, so its packages are not resolved. package.json and config were updated; log in to npm ('npm login') or fix your .npmrc, run 'npm install', then 'mj app enable ${manifest.name}'.`;
|
|
140
|
+
}
|
|
141
|
+
else if (manifest.schema) {
|
|
142
|
+
summary = `App installed. Its schema and tables exist, but entity metadata is generated by CodeGen — run 'mj codegen', then restart MJAPI and rebuild MJExplorer to activate.`;
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
summary = 'App installed successfully. Restart MJAPI and rebuild MJExplorer to activate.';
|
|
146
|
+
}
|
|
131
147
|
return {
|
|
132
148
|
Success: true,
|
|
133
149
|
Action: 'Install',
|
|
@@ -151,11 +167,11 @@ export async function InstallApp(options, context) {
|
|
|
151
167
|
return BuildFailureResult('Install', options.Source, '', 'Schema', startTime, message);
|
|
152
168
|
}
|
|
153
169
|
}
|
|
154
|
-
async function RecordInstallationAtomically(contextUser, manifest, callbacks, provider) {
|
|
170
|
+
async function RecordInstallationAtomically(contextUser, manifest, callbacks, provider, subpath) {
|
|
155
171
|
const md = (provider ?? new Metadata());
|
|
156
172
|
const tg = await md.CreateTransactionGroup();
|
|
157
173
|
try {
|
|
158
|
-
const appId = await RecordAppInstallation(contextUser, manifest, callbacks, tg, 'Installing');
|
|
174
|
+
const appId = await RecordAppInstallation(contextUser, manifest, callbacks, tg, 'Installing', provider, subpath);
|
|
159
175
|
if (manifest.dependencies) {
|
|
160
176
|
await RecordAppDependencies(contextUser, appId, manifest.dependencies, tg);
|
|
161
177
|
}
|
|
@@ -199,12 +215,12 @@ export async function UpgradeApp(options, context) {
|
|
|
199
215
|
upgradeAppId = existingApp.ID;
|
|
200
216
|
previousVersion = existingApp.Version;
|
|
201
217
|
const explicitUpgradeVersion = options.Version;
|
|
202
|
-
const targetVersion = explicitUpgradeVersion ?? (await GetLatestVersion(existingApp.RepositoryURL, context.GitHubOptions));
|
|
218
|
+
const targetVersion = explicitUpgradeVersion ?? (await GetLatestVersion(existingApp.RepositoryURL, context.GitHubOptions, existingApp.Subpath ?? undefined));
|
|
203
219
|
if (!targetVersion) {
|
|
204
220
|
return BuildFailureResult('Upgrade', options.AppName, '', 'Schema', startTime, 'Could not determine target version');
|
|
205
221
|
}
|
|
206
222
|
if (explicitUpgradeVersion) {
|
|
207
|
-
const tagResult = await ValidateGitHubTag(existingApp.RepositoryURL, targetVersion, context.GitHubOptions);
|
|
223
|
+
const tagResult = await ValidateGitHubTag(existingApp.RepositoryURL, targetVersion, context.GitHubOptions, existingApp.Subpath ?? undefined);
|
|
208
224
|
if (!tagResult.Exists) {
|
|
209
225
|
return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Schema', startTime, tagResult.ErrorMessage ?? `Version ${targetVersion} not found`);
|
|
210
226
|
}
|
|
@@ -223,8 +239,9 @@ export async function UpgradeApp(options, context) {
|
|
|
223
239
|
Summary: upgradeCheck.Message ?? `Already at version ${targetVersion}`,
|
|
224
240
|
};
|
|
225
241
|
}
|
|
242
|
+
const subpath = existingApp.Subpath ?? undefined;
|
|
226
243
|
Callbacks?.OnProgress?.('Fetch', `Fetching manifest for ${options.AppName} v${targetVersion}...`);
|
|
227
|
-
const fetchResult = await FetchManifestFromGitHub(existingApp.RepositoryURL, targetVersion, context.GitHubOptions);
|
|
244
|
+
const fetchResult = await FetchManifestFromGitHub(existingApp.RepositoryURL, targetVersion, context.GitHubOptions, subpath);
|
|
228
245
|
if (!fetchResult.Success || !fetchResult.ManifestJSON) {
|
|
229
246
|
return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Schema', startTime, fetchResult.ErrorMessage ?? 'Failed to fetch manifest');
|
|
230
247
|
}
|
|
@@ -254,16 +271,21 @@ export async function UpgradeApp(options, context) {
|
|
|
254
271
|
}
|
|
255
272
|
await SetAppStatus(context.ContextUser, existingApp.ID, 'Upgrading');
|
|
256
273
|
if (manifest.migrations && manifest.schema) {
|
|
257
|
-
const migrationResult = await HandleMigrations(manifest, context);
|
|
274
|
+
const migrationResult = await HandleMigrations(manifest, context, subpath);
|
|
258
275
|
if (!migrationResult.Success) {
|
|
259
|
-
|
|
276
|
+
const detail = migrationResult.ErrorMessage ?? 'Migration failed';
|
|
277
|
+
const recoverable = `${detail}\n\nThe schema may be partially upgraded (migrations are forward-only; there is no ` +
|
|
278
|
+
`automatic rollback). Skyway tracks applied migrations, so once the cause is fixed, re-run ` +
|
|
279
|
+
`'mj app upgrade ${options.AppName}' to resume from the last successful migration. If the schema ` +
|
|
280
|
+
`is left inconsistent, restore it from a backup taken before the upgrade.`;
|
|
281
|
+
await RecordFailureHistory(context.ContextUser, existingApp.ID, 'Upgrade', manifest, 'Migration', recoverable, startTime, previousVersion);
|
|
260
282
|
await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
|
|
261
|
-
return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Migration', startTime,
|
|
283
|
+
return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Migration', startTime, recoverable);
|
|
262
284
|
}
|
|
263
285
|
}
|
|
264
286
|
const effectiveUpgradeVersion = explicitUpgradeVersion ? targetVersion.replace(/^v/, '') : manifest.version;
|
|
265
287
|
const effectiveUpgradeStrategy = explicitUpgradeVersion ? 'exact' : context.VersionStrategy;
|
|
266
|
-
const pkgResult = await HandlePackageInstallation(manifest, context, effectiveUpgradeVersion, effectiveUpgradeStrategy);
|
|
288
|
+
const pkgResult = await HandlePackageInstallation(manifest, context, effectiveUpgradeVersion, effectiveUpgradeStrategy, options.Verbose);
|
|
267
289
|
let npmInstallWarning;
|
|
268
290
|
if (!pkgResult.Success) {
|
|
269
291
|
if (pkgResult.PackageJsonUpdated) {
|
|
@@ -288,23 +310,29 @@ export async function UpgradeApp(options, context) {
|
|
|
288
310
|
ManifestJSON: JSON.stringify(manifest),
|
|
289
311
|
Status: 'Active',
|
|
290
312
|
});
|
|
291
|
-
await HandleClientBootstrapRegeneration(context);
|
|
292
313
|
if (manifest.hooks?.postUpgrade) {
|
|
293
314
|
Callbacks?.OnProgress?.('Hooks', 'Running postUpgrade hook...');
|
|
294
315
|
await ExecuteHook(manifest.hooks.postUpgrade, context.RepoRoot);
|
|
295
316
|
}
|
|
296
317
|
if (manifest.dependencies) {
|
|
297
|
-
await
|
|
298
|
-
|
|
318
|
+
const depsReplaced = await ReplaceAppDependenciesAtomically(context.ContextUser, existingApp.ID, manifest.dependencies);
|
|
319
|
+
if (!depsReplaced) {
|
|
320
|
+
Callbacks?.OnWarn?.('Record', 'App upgraded, but its dependency records could not be updated atomically — re-run the upgrade to refresh them.');
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
try {
|
|
324
|
+
await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Upgrade', manifest, {
|
|
325
|
+
PreviousVersion: previousVersion,
|
|
326
|
+
Success: true,
|
|
327
|
+
DurationSeconds: GetDurationSeconds(startTime),
|
|
328
|
+
StartedAt: new Date(startTime),
|
|
329
|
+
EndedAt: new Date(),
|
|
330
|
+
Summary: `Upgraded from ${previousVersion} to ${manifest.version}`,
|
|
331
|
+
});
|
|
332
|
+
}
|
|
333
|
+
catch (histErr) {
|
|
334
|
+
Callbacks?.OnWarn?.('Record', `App upgraded, but the history audit entry could not be written: ${histErr instanceof Error ? histErr.message : String(histErr)}`);
|
|
299
335
|
}
|
|
300
|
-
await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Upgrade', manifest, {
|
|
301
|
-
PreviousVersion: previousVersion,
|
|
302
|
-
Success: true,
|
|
303
|
-
DurationSeconds: GetDurationSeconds(startTime),
|
|
304
|
-
StartedAt: new Date(startTime),
|
|
305
|
-
EndedAt: new Date(),
|
|
306
|
-
Summary: `Upgraded from ${previousVersion} to ${manifest.version}`,
|
|
307
|
-
});
|
|
308
336
|
Callbacks?.OnSuccess?.('Upgrade', `Successfully upgraded ${options.AppName} to v${manifest.version}`);
|
|
309
337
|
const baseSummary = `Upgraded from ${previousVersion} to ${manifest.version}. Restart MJAPI and rebuild MJExplorer.`;
|
|
310
338
|
const summary = npmInstallWarning
|
|
@@ -359,15 +387,73 @@ export async function RemoveApp(options, context) {
|
|
|
359
387
|
Callbacks?.OnProgress?.('Hooks', 'Running preRemove hook...');
|
|
360
388
|
await ExecuteHook(manifest.hooks.preRemove, context.RepoRoot);
|
|
361
389
|
}
|
|
390
|
+
const shareCheck = existingApp.SchemaName
|
|
391
|
+
? await CheckSchemaSharedByOtherApps(context.ContextUser, existingApp.SchemaName, existingApp.ID)
|
|
392
|
+
: { Shared: false, CheckFailed: false };
|
|
393
|
+
const schemaShared = shareCheck.Shared;
|
|
394
|
+
const shareCheckError = shareCheck.CheckFailed
|
|
395
|
+
? `Could not determine whether schema '${existingApp.SchemaName}' is shared by other Open Apps (share-check query failed): ${shareCheck.ErrorMessage ?? 'unknown error'}`
|
|
396
|
+
: undefined;
|
|
397
|
+
if (schemaShared && !shareCheck.CheckFailed) {
|
|
398
|
+
Callbacks?.OnWarn?.('Schema', `Schema '${existingApp.SchemaName}' is still used by another installed Open App — skipping metadata + schema removal to protect co-tenant data.`);
|
|
399
|
+
}
|
|
400
|
+
let metadataResult = { Success: true };
|
|
401
|
+
if (existingApp.SchemaName && !schemaShared) {
|
|
402
|
+
Callbacks?.OnProgress?.('Metadata', `Removing entity metadata for schema '${existingApp.SchemaName}'...`);
|
|
403
|
+
metadataResult = await RemoveAppEntityMetadata(existingApp.SchemaName, context.ContextUser, Callbacks);
|
|
404
|
+
}
|
|
405
|
+
let teardownResult = { Success: true };
|
|
406
|
+
if (!options.KeepData) {
|
|
407
|
+
teardownResult = await HandleTeardown(manifest, context, existingApp.Subpath ?? undefined);
|
|
408
|
+
if (!teardownResult.Success) {
|
|
409
|
+
Callbacks?.OnError?.('Metadata', `Teardown failed: ${teardownResult.ErrorMessage}`);
|
|
410
|
+
}
|
|
411
|
+
}
|
|
412
|
+
let schemaDropError;
|
|
413
|
+
if (!options.KeepData && existingApp.SchemaName && !schemaShared) {
|
|
414
|
+
Callbacks?.OnProgress?.('Schema', `Dropping schema '${existingApp.SchemaName}'...`);
|
|
415
|
+
const dropResult = await DropAppSchema(existingApp.SchemaName, context.DatabaseProvider, {
|
|
416
|
+
allowDoubleUnderscore: options.AllowDoubleUnderscoreSchema === true,
|
|
417
|
+
});
|
|
418
|
+
if (!dropResult.Success) {
|
|
419
|
+
schemaDropError = dropResult.ErrorMessage;
|
|
420
|
+
Callbacks?.OnError?.('Schema', `Failed to drop schema: ${dropResult.ErrorMessage}`);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
const removalErrors = [
|
|
424
|
+
shareCheckError,
|
|
425
|
+
teardownResult.Success ? undefined : teardownResult.ErrorMessage,
|
|
426
|
+
metadataResult.Success ? undefined : metadataResult.ErrorMessage,
|
|
427
|
+
schemaDropError,
|
|
428
|
+
].filter((e) => !!e);
|
|
429
|
+
if (removalErrors.length > 0) {
|
|
430
|
+
const combined = removalErrors.join('; ');
|
|
431
|
+
await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Remove', manifest, {
|
|
432
|
+
Success: false,
|
|
433
|
+
DurationSeconds: GetDurationSeconds(startTime),
|
|
434
|
+
StartedAt: new Date(startTime),
|
|
435
|
+
EndedAt: new Date(),
|
|
436
|
+
Summary: `Remove failed: ${combined}`,
|
|
437
|
+
});
|
|
438
|
+
await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
|
|
439
|
+
return BuildFailureResult('Remove', options.AppName, existingApp.Version, 'Schema', startTime, combined);
|
|
440
|
+
}
|
|
362
441
|
Callbacks?.OnProgress?.('Config', 'Removing config, client bootstrap, and package references...');
|
|
363
442
|
const otherApps = (await ListInstalledApps(context.ContextUser))
|
|
364
443
|
.filter(a => a.Name !== options.AppName && a.Status !== 'Removed');
|
|
365
|
-
const otherManifests = otherApps.
|
|
444
|
+
const otherManifests = otherApps.flatMap(a => {
|
|
445
|
+
try {
|
|
446
|
+
return [JSON.parse(a.ManifestJSON)];
|
|
447
|
+
}
|
|
448
|
+
catch {
|
|
449
|
+
Callbacks?.OnWarn?.('Config', `Ignoring app '${a.Name}' when computing shared excludes — its ManifestJSON could not be parsed.`);
|
|
450
|
+
return [];
|
|
451
|
+
}
|
|
452
|
+
});
|
|
366
453
|
await Promise.all([
|
|
367
454
|
Promise.resolve(RemoveServerDynamicPackages(context.RepoRoot, options.AppName)),
|
|
368
455
|
Promise.resolve(manifest.schema ? RemoveEntityPackageMapping(context.RepoRoot, manifest.schema.name) : undefined),
|
|
369
456
|
Promise.resolve(HandleAngularPrebundleExcludeRemoval(manifest, otherManifests, context)),
|
|
370
|
-
HandleClientBootstrapRegeneration(context),
|
|
371
457
|
Promise.resolve(RemoveAppPackages({
|
|
372
458
|
RepoRoot: context.RepoRoot,
|
|
373
459
|
ServerPackages: manifest.packages?.server ?? [],
|
|
@@ -385,26 +471,18 @@ export async function RemoveApp(options, context) {
|
|
|
385
471
|
if (!installResult.Success) {
|
|
386
472
|
Callbacks?.OnWarn?.('Packages', `Package install warning during removal: ${installResult.ErrorMessage}`);
|
|
387
473
|
}
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
allowDoubleUnderscore: options.AllowDoubleUnderscoreSchema === true,
|
|
474
|
+
try {
|
|
475
|
+
await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Remove', manifest, {
|
|
476
|
+
Success: true,
|
|
477
|
+
DurationSeconds: GetDurationSeconds(startTime),
|
|
478
|
+
StartedAt: new Date(startTime),
|
|
479
|
+
EndedAt: new Date(),
|
|
480
|
+
Summary: options.KeepData ? 'Removed (data kept)' : 'Removed (data dropped)',
|
|
396
481
|
});
|
|
397
|
-
if (!dropResult.Success) {
|
|
398
|
-
Callbacks?.OnWarn?.('Schema', `Failed to drop schema: ${dropResult.ErrorMessage}`);
|
|
399
|
-
}
|
|
400
482
|
}
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
StartedAt: new Date(startTime),
|
|
405
|
-
EndedAt: new Date(),
|
|
406
|
-
Summary: options.KeepData ? 'Removed (data kept)' : 'Removed (data dropped)',
|
|
407
|
-
});
|
|
483
|
+
catch (histErr) {
|
|
484
|
+
Callbacks?.OnWarn?.('Record', `App removed, but the history audit entry could not be written: ${histErr instanceof Error ? histErr.message : String(histErr)}`);
|
|
485
|
+
}
|
|
408
486
|
await UpdateAppRecord(context.ContextUser, existingApp.ID, {
|
|
409
487
|
Status: 'Removed',
|
|
410
488
|
});
|
|
@@ -440,9 +518,11 @@ export async function DisableApp(appName, context) {
|
|
|
440
518
|
if (!app) {
|
|
441
519
|
return BuildFailureResult('Install', appName, '', 'Config', startTime, `App '${appName}' is not installed`);
|
|
442
520
|
}
|
|
443
|
-
ToggleServerDynamicPackages(context.RepoRoot, appName, false);
|
|
521
|
+
const toggle = ToggleServerDynamicPackages(context.RepoRoot, appName, false);
|
|
522
|
+
if (!toggle.Success) {
|
|
523
|
+
return BuildFailureResult('Install', appName, app.Version, 'Config', startTime, toggle.ErrorMessage ?? 'Failed to update dynamicPackages.server in mj.config.cjs');
|
|
524
|
+
}
|
|
444
525
|
await SetAppStatus(context.ContextUser, app.ID, 'Disabled');
|
|
445
|
-
await HandleClientBootstrapRegeneration(context);
|
|
446
526
|
return {
|
|
447
527
|
Success: true,
|
|
448
528
|
Action: 'Install',
|
|
@@ -458,9 +538,11 @@ export async function EnableApp(appName, context) {
|
|
|
458
538
|
if (!app) {
|
|
459
539
|
return BuildFailureResult('Install', appName, '', 'Config', startTime, `App '${appName}' is not installed`);
|
|
460
540
|
}
|
|
461
|
-
ToggleServerDynamicPackages(context.RepoRoot, appName, true);
|
|
541
|
+
const toggle = ToggleServerDynamicPackages(context.RepoRoot, appName, true);
|
|
542
|
+
if (!toggle.Success) {
|
|
543
|
+
return BuildFailureResult('Install', appName, app.Version, 'Config', startTime, toggle.ErrorMessage ?? 'Failed to update dynamicPackages.server in mj.config.cjs');
|
|
544
|
+
}
|
|
462
545
|
await SetAppStatus(context.ContextUser, app.ID, 'Active');
|
|
463
|
-
await HandleClientBootstrapRegeneration(context);
|
|
464
546
|
return {
|
|
465
547
|
Success: true,
|
|
466
548
|
Action: 'Install',
|
|
@@ -492,12 +574,12 @@ async function ResolveDependencyChain(manifest, context) {
|
|
|
492
574
|
}
|
|
493
575
|
const depsToInstall = (result.InstallOrder ?? [])
|
|
494
576
|
.filter((d) => !d.AlreadyInstalled)
|
|
495
|
-
.map((d) => ({ AppName: d.AppName, Repository: d.Repository, VersionRange: d.VersionRange }));
|
|
577
|
+
.map((d) => ({ AppName: d.AppName, Repository: d.Repository, VersionRange: d.VersionRange, Subpath: d.Subpath }));
|
|
496
578
|
return { Success: true, DepsToInstall: depsToInstall };
|
|
497
579
|
}
|
|
498
580
|
function BuildManifestFetcher(context) {
|
|
499
|
-
return async (repoUrl) => {
|
|
500
|
-
const fetched = await FetchManifestFromGitHub(repoUrl, undefined, context.GitHubOptions);
|
|
581
|
+
return async (repoUrl, subpath) => {
|
|
582
|
+
const fetched = await FetchManifestFromGitHub(repoUrl, undefined, context.GitHubOptions, subpath);
|
|
501
583
|
if (!fetched.Success || !fetched.ManifestJSON) {
|
|
502
584
|
return { Success: false, ErrorMessage: fetched.ErrorMessage ?? 'Failed to fetch manifest' };
|
|
503
585
|
}
|
|
@@ -524,8 +606,14 @@ async function InstallDependencies(deps, context, inherited) {
|
|
|
524
606
|
};
|
|
525
607
|
}
|
|
526
608
|
context.Callbacks?.OnProgress?.('Dependencies', `Installing dependency from ${dep.Repository}...`);
|
|
609
|
+
const resolved = await ResolveDependencyVersion(dep.Repository, dep.VersionRange, context.GitHubOptions);
|
|
610
|
+
if (resolved.ErrorMessage) {
|
|
611
|
+
return { Success: false, ErrorMessage: `Cannot install dependency '${dep.AppName}': ${resolved.ErrorMessage}` };
|
|
612
|
+
}
|
|
527
613
|
const result = await InstallApp({
|
|
528
614
|
Source: dep.Repository,
|
|
615
|
+
Subpath: dep.Subpath,
|
|
616
|
+
Version: resolved.Version,
|
|
529
617
|
_skipDependencyResolution: true,
|
|
530
618
|
AllowDoubleUnderscoreSchema: inherited.AllowDoubleUnderscoreSchema,
|
|
531
619
|
Verbose: inherited.Verbose,
|
|
@@ -536,6 +624,35 @@ async function InstallDependencies(deps, context, inherited) {
|
|
|
536
624
|
}
|
|
537
625
|
return { Success: true };
|
|
538
626
|
}
|
|
627
|
+
export async function ResolveDependencyVersion(repoUrl, versionRange, options) {
|
|
628
|
+
const range = (versionRange ?? '').trim();
|
|
629
|
+
if (range === '' || range === '*' || range.toLowerCase() === 'latest') {
|
|
630
|
+
return { Version: undefined };
|
|
631
|
+
}
|
|
632
|
+
if (semver.valid(range)) {
|
|
633
|
+
return { Version: range.replace(/^v/, '') };
|
|
634
|
+
}
|
|
635
|
+
if (!semver.validRange(range)) {
|
|
636
|
+
return { ErrorMessage: `version '${versionRange}' is not a valid semver version or range` };
|
|
637
|
+
}
|
|
638
|
+
const [releases, tags] = await Promise.all([
|
|
639
|
+
ListGitHubReleases(repoUrl, options),
|
|
640
|
+
ListGitHubTags(repoUrl, options),
|
|
641
|
+
]);
|
|
642
|
+
const candidates = [
|
|
643
|
+
...releases.filter((r) => !r.Draft).map((r) => r.TagName.replace(/^v/, '')),
|
|
644
|
+
...tags.map((t) => t.replace(/^v/, '')),
|
|
645
|
+
].filter((v) => semver.valid(v) != null);
|
|
646
|
+
if (candidates.length === 0) {
|
|
647
|
+
return { ErrorMessage: `no published versions found at ${repoUrl} to satisfy '${versionRange}'` };
|
|
648
|
+
}
|
|
649
|
+
const best = semver.maxSatisfying(candidates, range);
|
|
650
|
+
if (!best) {
|
|
651
|
+
const latest = candidates.sort(semver.rcompare)[0];
|
|
652
|
+
return { ErrorMessage: `no published version at ${repoUrl} satisfies '${versionRange}' (latest available: ${latest})` };
|
|
653
|
+
}
|
|
654
|
+
return { Version: best };
|
|
655
|
+
}
|
|
539
656
|
async function HandleSchemaCreation(manifest, context, isReinstall = false, allowDoubleUnderscore = false) {
|
|
540
657
|
if (!manifest.schema) {
|
|
541
658
|
return { Success: true };
|
|
@@ -545,25 +662,26 @@ async function HandleSchemaCreation(manifest, context, isReinstall = false, allo
|
|
|
545
662
|
if (exists) {
|
|
546
663
|
if (isReinstall || manifest.schema.createIfNotExists !== false) {
|
|
547
664
|
context.Callbacks?.OnProgress?.('Schema', `Reusing existing schema '${manifest.schema.name}'`);
|
|
548
|
-
return { Success: true };
|
|
665
|
+
return { Success: true, Created: false };
|
|
549
666
|
}
|
|
550
667
|
return { Success: false, ErrorMessage: `Schema '${manifest.schema.name}' already exists` };
|
|
551
668
|
}
|
|
552
669
|
if (manifest.schema.createIfNotExists !== false) {
|
|
553
670
|
context.Callbacks?.OnProgress?.('Schema', `Creating schema '${manifest.schema.name}'...`);
|
|
554
671
|
const result = await CreateAppSchema(manifest.schema.name, context.DatabaseProvider, { allowDoubleUnderscore });
|
|
555
|
-
return { Success: result.Success, ErrorMessage: result.ErrorMessage };
|
|
672
|
+
return { Success: result.Success, ErrorMessage: result.ErrorMessage, Created: result.Success };
|
|
556
673
|
}
|
|
557
674
|
return { Success: false, ErrorMessage: `Schema '${manifest.schema.name}' does not exist and createIfNotExists is false` };
|
|
558
675
|
}
|
|
559
|
-
async function HandleMigrations(manifest, context) {
|
|
676
|
+
async function HandleMigrations(manifest, context, subpath) {
|
|
560
677
|
if (!manifest.schema || !manifest.migrations) {
|
|
561
678
|
return { Success: true };
|
|
562
679
|
}
|
|
563
680
|
context.Callbacks?.OnProgress?.('Migration', 'Downloading migration files...');
|
|
564
681
|
const tempDir = join(tmpdir(), `mj-app-${manifest.name}-${Date.now()}`);
|
|
565
682
|
mkdirSync(tempDir, { recursive: true });
|
|
566
|
-
const
|
|
683
|
+
const platform = context.DatabaseProvider.Dialect.PlatformKey;
|
|
684
|
+
const downloadResult = await DownloadAppMigrations(manifest, context, tempDir, subpath);
|
|
567
685
|
if (!downloadResult.Success) {
|
|
568
686
|
return { Success: false, ErrorMessage: downloadResult.ErrorMessage };
|
|
569
687
|
}
|
|
@@ -574,10 +692,62 @@ async function HandleMigrations(manifest, context) {
|
|
|
574
692
|
DatabaseConfig: context.DatabaseConfig,
|
|
575
693
|
MJCoreSchema: context.MJCoreSchema,
|
|
576
694
|
ExtraPlaceholders: context.MigrationPlaceholders,
|
|
695
|
+
Platform: platform,
|
|
577
696
|
});
|
|
578
697
|
return { Success: migrationResult.Success, ErrorMessage: migrationResult.ErrorMessage };
|
|
579
698
|
}
|
|
580
|
-
async function
|
|
699
|
+
async function HandleTeardown(manifest, context, subpath) {
|
|
700
|
+
const teardownDir = manifest.migrations?.teardownDirectory;
|
|
701
|
+
if (!teardownDir) {
|
|
702
|
+
return { Success: true };
|
|
703
|
+
}
|
|
704
|
+
const platform = context.DatabaseProvider.Dialect.PlatformKey;
|
|
705
|
+
const dir = platform === 'postgresql' ? `${teardownDir}-pg` : teardownDir;
|
|
706
|
+
const tempDir = join(tmpdir(), `mj-app-${manifest.name}-teardown-${Date.now()}`);
|
|
707
|
+
mkdirSync(tempDir, { recursive: true });
|
|
708
|
+
context.Callbacks?.OnProgress?.('Metadata', 'Downloading teardown scripts...');
|
|
709
|
+
const download = await DownloadMigrations(manifest.repository, manifest.version, dir, tempDir, context.GitHubOptions, subpath);
|
|
710
|
+
if (!download.Success) {
|
|
711
|
+
return { Success: false, ErrorMessage: `Failed to download teardown scripts: ${download.ErrorMessage}` };
|
|
712
|
+
}
|
|
713
|
+
const files = (download.Files ?? []).filter((f) => f.endsWith('.sql')).sort();
|
|
714
|
+
if (files.length === 0) {
|
|
715
|
+
context.Callbacks?.OnWarn?.('Metadata', `No teardown scripts in '${dir}' — this app's rows in the shared core schema will NOT be retired on remove.`);
|
|
716
|
+
return { Success: true };
|
|
717
|
+
}
|
|
718
|
+
const mjSchema = context.MJCoreSchema ?? '__mj';
|
|
719
|
+
context.Callbacks?.OnProgress?.('Metadata', `Running ${files.length} teardown script(s) against '${mjSchema}'...`);
|
|
720
|
+
await context.DatabaseProvider.BeginTransaction();
|
|
721
|
+
try {
|
|
722
|
+
for (const file of files) {
|
|
723
|
+
const sql = readFileSync(join(tempDir, file), 'utf-8').split('${mjSchema}').join(mjSchema);
|
|
724
|
+
if (sql.trim()) {
|
|
725
|
+
await context.DatabaseProvider.ExecuteSQL(sql);
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
await context.DatabaseProvider.CommitTransaction();
|
|
729
|
+
}
|
|
730
|
+
catch (error) {
|
|
731
|
+
await context.DatabaseProvider.RollbackTransaction();
|
|
732
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
733
|
+
return { Success: false, ErrorMessage: `Teardown failed for '${manifest.name}' (rolled back): ${message}` };
|
|
734
|
+
}
|
|
735
|
+
context.Callbacks?.OnSuccess?.('Metadata', `Retired this app's rows from '${mjSchema}' (${files.length} teardown script(s)).`);
|
|
736
|
+
return { Success: true };
|
|
737
|
+
}
|
|
738
|
+
async function DownloadAppMigrations(manifest, context, tempDir, subpath) {
|
|
739
|
+
const baseDir = manifest.migrations.directory;
|
|
740
|
+
const isPG = context.DatabaseProvider.Dialect.PlatformKey === 'postgresql';
|
|
741
|
+
if (isPG) {
|
|
742
|
+
const pgDir = `${baseDir.replace(/\/+$/, '')}-pg`;
|
|
743
|
+
const pgResult = await DownloadMigrations(manifest.repository, manifest.version, pgDir, tempDir, context.GitHubOptions, subpath);
|
|
744
|
+
if (pgResult.Success && (pgResult.Files?.length ?? 0) > 0) {
|
|
745
|
+
return pgResult;
|
|
746
|
+
}
|
|
747
|
+
}
|
|
748
|
+
return DownloadMigrations(manifest.repository, manifest.version, baseDir, tempDir, context.GitHubOptions, subpath);
|
|
749
|
+
}
|
|
750
|
+
async function HandlePackageInstallation(manifest, context, packageVersion, versionStrategy, verbose) {
|
|
581
751
|
if (!manifest.packages) {
|
|
582
752
|
return { Success: true };
|
|
583
753
|
}
|
|
@@ -606,7 +776,7 @@ async function HandlePackageInstallation(manifest, context, packageVersion, vers
|
|
|
606
776
|
}
|
|
607
777
|
}
|
|
608
778
|
context.Callbacks?.OnProgress?.('Packages', 'Running package install...');
|
|
609
|
-
const installResult = RunPackageInstall(context.RepoRoot,
|
|
779
|
+
const installResult = RunPackageInstall(context.RepoRoot, verbose, manifest.packages.registry, context.PackageManager);
|
|
610
780
|
if (!installResult.Success) {
|
|
611
781
|
return { Success: false, PackageJsonUpdated: true, ErrorMessage: installResult.ErrorMessage };
|
|
612
782
|
}
|
|
@@ -618,6 +788,10 @@ function HandleServerConfig(manifest, context) {
|
|
|
618
788
|
if (!dynamicResult.Success) {
|
|
619
789
|
return { Success: false, ErrorMessage: dynamicResult.ErrorMessage };
|
|
620
790
|
}
|
|
791
|
+
const clientResult = AddClientDynamicPackages(context.RepoRoot, manifest);
|
|
792
|
+
if (!clientResult.Success) {
|
|
793
|
+
return { Success: false, ErrorMessage: clientResult.ErrorMessage };
|
|
794
|
+
}
|
|
621
795
|
const entityResult = AddEntityPackageMapping(context.RepoRoot, manifest);
|
|
622
796
|
if (!entityResult.Success) {
|
|
623
797
|
return { Success: false, ErrorMessage: entityResult.ErrorMessage };
|
|
@@ -646,104 +820,132 @@ function HandleAngularPrebundleExcludeRemoval(manifest, otherManifests, context)
|
|
|
646
820
|
manager.RemovePrebundleExcludes(manifest, otherManifests);
|
|
647
821
|
manager.Save();
|
|
648
822
|
}
|
|
649
|
-
async function HandleClientBootstrapRegeneration(context) {
|
|
650
|
-
context.Callbacks?.OnProgress?.('Config', 'Regenerating client bootstrap...');
|
|
651
|
-
const apps = await ListInstalledApps(context.ContextUser);
|
|
652
|
-
const appDeps = new Map();
|
|
653
|
-
for (const app of apps) {
|
|
654
|
-
const manifest = JSON.parse(app.ManifestJSON);
|
|
655
|
-
const depNames = manifest.dependencies ? Object.keys(manifest.dependencies) : [];
|
|
656
|
-
appDeps.set(app.Name, depNames);
|
|
657
|
-
}
|
|
658
|
-
const sortedNames = TopologicalSortApps(appDeps);
|
|
659
|
-
const entries = [];
|
|
660
|
-
const appsByName = new Map(apps.map((a) => [a.Name, a]));
|
|
661
|
-
for (const name of sortedNames) {
|
|
662
|
-
const app = appsByName.get(name);
|
|
663
|
-
if (!app)
|
|
664
|
-
continue;
|
|
665
|
-
const manifest = JSON.parse(app.ManifestJSON);
|
|
666
|
-
const clientPkgs = [...(manifest.packages?.client ?? []), ...(manifest.packages?.shared ?? [])];
|
|
667
|
-
for (const pkg of clientPkgs) {
|
|
668
|
-
entries.push({
|
|
669
|
-
AppName: app.Name,
|
|
670
|
-
Version: app.Version,
|
|
671
|
-
PackageName: pkg.name,
|
|
672
|
-
Enabled: app.Status === 'Active',
|
|
673
|
-
});
|
|
674
|
-
}
|
|
675
|
-
}
|
|
676
|
-
RegenerateClientBootstrap(context.RepoRoot, entries, context.ClientPackagePath, context.ClientBootstrapSubpath);
|
|
677
|
-
}
|
|
678
|
-
function TopologicalSortApps(appDeps) {
|
|
679
|
-
const visited = new Set();
|
|
680
|
-
const result = [];
|
|
681
|
-
function Visit(name) {
|
|
682
|
-
if (visited.has(name))
|
|
683
|
-
return;
|
|
684
|
-
visited.add(name);
|
|
685
|
-
const deps = appDeps.get(name) ?? [];
|
|
686
|
-
for (const dep of deps) {
|
|
687
|
-
if (appDeps.has(dep)) {
|
|
688
|
-
Visit(dep);
|
|
689
|
-
}
|
|
690
|
-
}
|
|
691
|
-
result.push(name);
|
|
692
|
-
}
|
|
693
|
-
for (const name of appDeps.keys()) {
|
|
694
|
-
Visit(name);
|
|
695
|
-
}
|
|
696
|
-
return result;
|
|
697
|
-
}
|
|
698
823
|
async function ExecuteHook(command, cwd) {
|
|
699
824
|
const { execSync } = await import('node:child_process');
|
|
700
825
|
execSync(command, { cwd, encoding: 'utf-8', timeout: 120000, stdio: 'inherit' });
|
|
701
826
|
}
|
|
702
|
-
async function RemoveAppEntityMetadata(schemaName, contextUser, callbacks) {
|
|
827
|
+
export async function RemoveAppEntityMetadata(schemaName, contextUser, callbacks, provider) {
|
|
703
828
|
try {
|
|
704
829
|
const rv = new RunView();
|
|
705
830
|
const escaped = EscapeSqlString(schemaName);
|
|
831
|
+
const md = (provider ?? new Metadata());
|
|
832
|
+
const tg = await md.CreateTransactionGroup();
|
|
833
|
+
const queueDeleteByFilterOrThrow = async (entityName, filter) => {
|
|
834
|
+
const r = await QueueDeleteEntitiesByFilter(rv, contextUser, entityName, filter, tg);
|
|
835
|
+
if (!r.Success) {
|
|
836
|
+
throw new Error(r.ErrorMessage ?? `Failed to queue delete of ${entityName} records`);
|
|
837
|
+
}
|
|
838
|
+
};
|
|
706
839
|
const entityResult = await rv.RunView({
|
|
707
840
|
EntityName: 'MJ: Entities',
|
|
708
841
|
ExtraFilter: `SchemaName = '${escaped}'`,
|
|
709
842
|
ResultType: 'entity_object',
|
|
710
843
|
}, contextUser);
|
|
711
|
-
if (!entityResult.Success
|
|
712
|
-
|
|
844
|
+
if (!entityResult.Success) {
|
|
845
|
+
throw new Error(`Failed to query entities for schema '${schemaName}': ${entityResult.ErrorMessage}`);
|
|
846
|
+
}
|
|
847
|
+
if (entityResult.Results.length === 0) {
|
|
848
|
+
await queueDeleteByFilterOrThrow('MJ: Schema Info', `SchemaName = '${escaped}'`);
|
|
849
|
+
if (!(await tg.Submit())) {
|
|
850
|
+
throw new Error('Transaction failed: schema-info cleanup could not be committed');
|
|
851
|
+
}
|
|
713
852
|
callbacks?.OnSuccess?.('Metadata', `Entity metadata for schema '${schemaName}' removed`);
|
|
714
|
-
return;
|
|
853
|
+
return { Success: true };
|
|
715
854
|
}
|
|
716
|
-
const entityIds = entityResult.Results.map((e) =>
|
|
855
|
+
const entityIds = entityResult.Results.map((e) => e.ID);
|
|
717
856
|
const idList = entityIds.map((id) => `'${EscapeSqlString(id)}'`).join(',');
|
|
718
|
-
const
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
857
|
+
const fieldResult = await rv.RunView({
|
|
858
|
+
EntityName: 'MJ: Entity Fields',
|
|
859
|
+
ExtraFilter: `EntityID IN (${idList})`,
|
|
860
|
+
ResultType: 'entity_object',
|
|
861
|
+
}, contextUser);
|
|
862
|
+
if (!fieldResult.Success) {
|
|
863
|
+
throw new Error(`Failed to query entity fields for schema '${schemaName}': ${fieldResult.ErrorMessage}`);
|
|
864
|
+
}
|
|
865
|
+
const fieldIdList = fieldResult.Results.map((f) => `'${EscapeSqlString(f.ID)}'`).join(',');
|
|
866
|
+
const ownedAppIds = await FindAppOwnedApplications(rv, contextUser, entityIds, idList);
|
|
867
|
+
if (fieldIdList.length > 0) {
|
|
868
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Field Values', `EntityFieldID IN (${fieldIdList})`);
|
|
869
|
+
}
|
|
870
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Permissions', `EntityID IN (${idList})`);
|
|
871
|
+
await queueDeleteByFilterOrThrow('MJ: Application Entities', `EntityID IN (${idList})`);
|
|
872
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Settings', `EntityID IN (${idList})`);
|
|
873
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Relationships', `EntityID IN (${idList}) OR RelatedEntityID IN (${idList})`);
|
|
874
|
+
await queueDeleteByFilterOrThrow('MJ: Entity Fields', `EntityID IN (${idList})`);
|
|
725
875
|
for (const entity of entityResult.Results) {
|
|
876
|
+
entity.TransactionGroup = tg;
|
|
726
877
|
await entity.Delete();
|
|
727
878
|
}
|
|
728
|
-
await
|
|
879
|
+
await queueDeleteByFilterOrThrow('MJ: Schema Info', `SchemaName = '${escaped}'`);
|
|
880
|
+
if (!(await tg.Submit())) {
|
|
881
|
+
throw new Error('Transaction failed: entity metadata cleanup could not be committed atomically');
|
|
882
|
+
}
|
|
883
|
+
await DeleteAppOwnedApplications(rv, ownedAppIds, contextUser, callbacks);
|
|
729
884
|
callbacks?.OnSuccess?.('Metadata', `Entity metadata for schema '${schemaName}' removed`);
|
|
885
|
+
return { Success: true };
|
|
730
886
|
}
|
|
731
887
|
catch (error) {
|
|
732
888
|
const message = error instanceof Error ? error.message : String(error);
|
|
733
|
-
callbacks?.
|
|
889
|
+
callbacks?.OnError?.('Metadata', `Failed to remove entity metadata: ${message}`);
|
|
890
|
+
return { Success: false, ErrorMessage: `Failed to remove entity metadata for schema '${schemaName}': ${message}` };
|
|
734
891
|
}
|
|
735
892
|
}
|
|
736
|
-
async function
|
|
893
|
+
async function FindAppOwnedApplications(rv, contextUser, entityIds, idList) {
|
|
894
|
+
const ours = new Set(entityIds.map((id) => NormalizeUUID(id)));
|
|
895
|
+
const linked = await rv.RunView({ EntityName: 'MJ: Application Entities', ExtraFilter: `EntityID IN (${idList})`, Fields: ['ApplicationID'], ResultType: 'simple' }, contextUser);
|
|
896
|
+
if (!linked.Success) {
|
|
897
|
+
return [];
|
|
898
|
+
}
|
|
899
|
+
const candidateIds = [...new Set(linked.Results.map((r) => r.ApplicationID))];
|
|
900
|
+
if (candidateIds.length === 0) {
|
|
901
|
+
return [];
|
|
902
|
+
}
|
|
903
|
+
const candList = candidateIds.map((id) => `'${EscapeSqlString(id)}'`).join(',');
|
|
904
|
+
const allLinks = await rv.RunView({ EntityName: 'MJ: Application Entities', ExtraFilter: `ApplicationID IN (${candList})`, Fields: ['ApplicationID', 'EntityID'], ResultType: 'simple' }, contextUser);
|
|
905
|
+
if (!allLinks.Success) {
|
|
906
|
+
return [];
|
|
907
|
+
}
|
|
908
|
+
const allOursByApp = new Map();
|
|
909
|
+
for (const link of allLinks.Results) {
|
|
910
|
+
const wasAllOurs = allOursByApp.get(link.ApplicationID) ?? true;
|
|
911
|
+
allOursByApp.set(link.ApplicationID, wasAllOurs && ours.has(NormalizeUUID(link.EntityID)));
|
|
912
|
+
}
|
|
913
|
+
return [...allOursByApp.entries()].filter(([, allOurs]) => allOurs).map(([appId]) => appId);
|
|
914
|
+
}
|
|
915
|
+
async function DeleteAppOwnedApplications(rv, applicationIds, contextUser, callbacks) {
|
|
916
|
+
if (applicationIds.length === 0) {
|
|
917
|
+
return;
|
|
918
|
+
}
|
|
919
|
+
const idList = applicationIds.map((id) => `'${EscapeSqlString(id)}'`).join(',');
|
|
920
|
+
const appsResult = await rv.RunView({ EntityName: 'MJ: Applications', ExtraFilter: `ID IN (${idList})`, ResultType: 'entity_object' }, contextUser);
|
|
921
|
+
if (!appsResult.Success) {
|
|
922
|
+
return;
|
|
923
|
+
}
|
|
924
|
+
for (const app of appsResult.Results) {
|
|
925
|
+
try {
|
|
926
|
+
if (!(await app.Delete())) {
|
|
927
|
+
callbacks?.OnWarn?.('Metadata', `Left Application '${app.Name}' — it still has dependents; a reinstall will reuse it (${app.LatestResult?.CompleteMessage ?? 'has FK references'}).`);
|
|
928
|
+
}
|
|
929
|
+
}
|
|
930
|
+
catch (error) {
|
|
931
|
+
callbacks?.OnWarn?.('Metadata', `Could not remove app-owned Application '${app.Name}': ${error instanceof Error ? error.message : String(error)}`);
|
|
932
|
+
}
|
|
933
|
+
}
|
|
934
|
+
}
|
|
935
|
+
async function QueueDeleteEntitiesByFilter(rv, contextUser, entityName, filter, transactionGroup) {
|
|
737
936
|
const result = await rv.RunView({
|
|
738
937
|
EntityName: entityName,
|
|
739
938
|
ExtraFilter: filter,
|
|
740
939
|
ResultType: 'entity_object',
|
|
741
940
|
}, contextUser);
|
|
742
|
-
if (result.Success) {
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
|
|
941
|
+
if (!result.Success) {
|
|
942
|
+
return { Success: false, ErrorMessage: `Failed to query ${entityName}: ${result.ErrorMessage}` };
|
|
943
|
+
}
|
|
944
|
+
for (const record of result.Results) {
|
|
945
|
+
record.TransactionGroup = transactionGroup;
|
|
946
|
+
await record.Delete();
|
|
746
947
|
}
|
|
948
|
+
return { Success: true };
|
|
747
949
|
}
|
|
748
950
|
function GetDurationSeconds(startTime) {
|
|
749
951
|
return Math.round((Date.now() - startTime) / 1000);
|