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