@memberjunction/open-app-engine 0.0.1 → 5.2.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.
Files changed (58) hide show
  1. package/dist/dependency/dependency-resolver.d.ts +23 -0
  2. package/dist/dependency/dependency-resolver.d.ts.map +1 -0
  3. package/dist/dependency/dependency-resolver.js +63 -0
  4. package/dist/dependency/dependency-resolver.js.map +1 -0
  5. package/dist/dependency/version-checker.d.ts +8 -0
  6. package/dist/dependency/version-checker.d.ts.map +1 -0
  7. package/dist/dependency/version-checker.js +65 -0
  8. package/dist/dependency/version-checker.js.map +1 -0
  9. package/dist/github/github-client.d.ts +29 -0
  10. package/dist/github/github-client.d.ts.map +1 -0
  11. package/dist/github/github-client.js +127 -0
  12. package/dist/github/github-client.js.map +1 -0
  13. package/dist/index.d.ts +25 -0
  14. package/dist/index.d.ts.map +1 -0
  15. package/dist/index.js +13 -0
  16. package/dist/index.js.map +1 -0
  17. package/dist/install/client-bootstrap-gen.d.ts +8 -0
  18. package/dist/install/client-bootstrap-gen.d.ts.map +1 -0
  19. package/dist/install/client-bootstrap-gen.js +35 -0
  20. package/dist/install/client-bootstrap-gen.js.map +1 -0
  21. package/dist/install/config-manager.d.ts +15 -0
  22. package/dist/install/config-manager.d.ts.map +1 -0
  23. package/dist/install/config-manager.js +114 -0
  24. package/dist/install/config-manager.js.map +1 -0
  25. package/dist/install/history-recorder.d.ts +25 -0
  26. package/dist/install/history-recorder.d.ts.map +1 -0
  27. package/dist/install/history-recorder.js +184 -0
  28. package/dist/install/history-recorder.js.map +1 -0
  29. package/dist/install/install-orchestrator.d.ts +20 -0
  30. package/dist/install/install-orchestrator.d.ts.map +1 -0
  31. package/dist/install/install-orchestrator.js +634 -0
  32. package/dist/install/install-orchestrator.js.map +1 -0
  33. package/dist/install/migration-runner.d.ts +23 -0
  34. package/dist/install/migration-runner.d.ts.map +1 -0
  35. package/dist/install/migration-runner.js +66 -0
  36. package/dist/install/migration-runner.js.map +1 -0
  37. package/dist/install/package-manager.d.ts +19 -0
  38. package/dist/install/package-manager.d.ts.map +1 -0
  39. package/dist/install/package-manager.js +89 -0
  40. package/dist/install/package-manager.js.map +1 -0
  41. package/dist/install/schema-manager.d.ts +11 -0
  42. package/dist/install/schema-manager.d.ts.map +1 -0
  43. package/dist/install/schema-manager.js +125 -0
  44. package/dist/install/schema-manager.js.map +1 -0
  45. package/dist/manifest/manifest-loader.d.ts +10 -0
  46. package/dist/manifest/manifest-loader.d.ts.map +1 -0
  47. package/dist/manifest/manifest-loader.js +30 -0
  48. package/dist/manifest/manifest-loader.js.map +1 -0
  49. package/dist/manifest/manifest-schema.d.ts +351 -0
  50. package/dist/manifest/manifest-schema.d.ts.map +1 -0
  51. package/dist/manifest/manifest-schema.js +80 -0
  52. package/dist/manifest/manifest-schema.js.map +1 -0
  53. package/dist/types/open-app-types.d.ts +66 -0
  54. package/dist/types/open-app-types.d.ts.map +1 -0
  55. package/dist/types/open-app-types.js +2 -0
  56. package/dist/types/open-app-types.js.map +1 -0
  57. package/package.json +34 -7
  58. package/README.md +0 -45
@@ -0,0 +1,634 @@
1
+ import { tmpdir } from 'node:os';
2
+ import { join } from 'node:path';
3
+ import { mkdirSync } from 'node:fs';
4
+ import { ParseAndValidateManifest } from '../manifest/manifest-loader.js';
5
+ import { CheckMJVersionCompatibility, IsValidUpgrade } from '../dependency/version-checker.js';
6
+ import { ResolveDependencies } from '../dependency/dependency-resolver.js';
7
+ import { FetchManifestFromGitHub, DownloadMigrations, GetLatestVersion } from '../github/github-client.js';
8
+ import { CreateAppSchema, DropAppSchema, SchemaExists, EscapeSqlString } from './schema-manager.js';
9
+ import { RunAppMigrations } from './migration-runner.js';
10
+ import { AddAppPackages, RemoveAppPackages, RunNpmInstall } from './package-manager.js';
11
+ import { AddServerDynamicPackages, RemoveServerDynamicPackages, ToggleServerDynamicPackages } from './config-manager.js';
12
+ import { RegenerateClientBootstrap } from './client-bootstrap-gen.js';
13
+ import { Metadata, RunView } from '@memberjunction/core';
14
+ import { RecordAppInstallation, RecordInstallHistoryEntry, RecordAppDependencies, DeleteAppDependencies, SetAppStatus, FindInstalledApp, FindDependentApps, ListInstalledApps, UpdateAppRecord, } from './history-recorder.js';
15
+ export async function InstallApp(options, context) {
16
+ const startTime = Date.now();
17
+ const { Callbacks } = context;
18
+ let createdAppId;
19
+ let manifest;
20
+ try {
21
+ Callbacks?.OnProgress?.('Fetch', `Fetching manifest from ${options.Source}...`);
22
+ const fetchResult = await FetchManifestFromGitHub(options.Source, options.Version, context.GitHubOptions);
23
+ if (!fetchResult.Success || !fetchResult.ManifestJSON) {
24
+ return BuildFailureResult('Install', options.Source, '', 'Schema', startTime, fetchResult.ErrorMessage ?? 'Failed to fetch manifest');
25
+ }
26
+ Callbacks?.OnProgress?.('Validate', 'Validating manifest...');
27
+ const parseResult = ParseAndValidateManifest(fetchResult.ManifestJSON);
28
+ if (!parseResult.Success || !parseResult.Manifest) {
29
+ return BuildFailureResult('Install', options.Source, '', 'Schema', startTime, `Invalid manifest: ${parseResult.Errors?.join(', ')}`);
30
+ }
31
+ manifest = parseResult.Manifest;
32
+ Callbacks?.OnProgress?.('Validate', 'Checking MJ version compatibility and resolving dependencies...');
33
+ const [compatResult, depResult] = await Promise.all([
34
+ Promise.resolve(CheckMJVersionCompatibility(context.MJVersion, manifest.mjVersionRange)),
35
+ ResolveDependencyChain(manifest, context),
36
+ ]);
37
+ if (!compatResult.Compatible) {
38
+ return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, compatResult.Message ?? 'Incompatible MJ version');
39
+ }
40
+ if (!depResult.Success) {
41
+ return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, depResult.ErrorMessage ?? 'Dependency resolution failed');
42
+ }
43
+ if (depResult.DepsToInstall && depResult.DepsToInstall.length > 0) {
44
+ const depsResult = await InstallDependencies(depResult.DepsToInstall, context);
45
+ if (!depsResult.Success) {
46
+ return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, depsResult.ErrorMessage ?? 'Dependency installation failed');
47
+ }
48
+ }
49
+ const existingApp = await FindInstalledApp(context.ContextUser, manifest.name);
50
+ const isReinstall = existingApp != null && existingApp.Status === 'Removed';
51
+ if (existingApp && !isReinstall) {
52
+ 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.`);
53
+ }
54
+ Callbacks?.OnProgress?.('Install', `Installing ${manifest.name} v${manifest.version}...`);
55
+ let schemaCreated = false;
56
+ if (manifest.schema) {
57
+ const schemaResult = await HandleSchemaCreation(manifest, context, isReinstall);
58
+ if (!schemaResult.Success) {
59
+ return BuildFailureResult('Install', manifest.name, manifest.version, 'Schema', startTime, schemaResult.ErrorMessage ?? 'Schema creation failed');
60
+ }
61
+ schemaCreated = !isReinstall;
62
+ }
63
+ if (manifest.migrations && manifest.schema) {
64
+ const migrationResult = await HandleMigrations(manifest, context);
65
+ if (!migrationResult.Success) {
66
+ await CompensateSchemaOnFailure(manifest, context, schemaCreated, Callbacks);
67
+ return BuildFailureResult('Install', manifest.name, manifest.version, 'Migration', startTime, migrationResult.ErrorMessage ?? 'Migration failed');
68
+ }
69
+ }
70
+ Callbacks?.OnProgress?.('Record', 'Recording app installation...');
71
+ const recordResult = await RecordInstallationAtomically(context.ContextUser, manifest, Callbacks);
72
+ if (!recordResult.Success) {
73
+ await CompensateSchemaOnFailure(manifest, context, schemaCreated, Callbacks);
74
+ return BuildFailureResult('Install', manifest.name, manifest.version, 'Record', startTime, recordResult.ErrorMessage ?? 'Failed to record installation');
75
+ }
76
+ createdAppId = recordResult.AppId;
77
+ Callbacks?.OnProgress?.('Config', 'Updating configuration files...');
78
+ const pkgResult = await HandlePackageInstallation(manifest, context);
79
+ if (!pkgResult.Success) {
80
+ await SetAppStatus(context.ContextUser, createdAppId, 'Error');
81
+ await RecordFailureHistory(context.ContextUser, createdAppId, 'Install', manifest, 'Packages', pkgResult.ErrorMessage ?? 'Package installation failed', startTime);
82
+ return BuildFailureResult('Install', manifest.name, manifest.version, 'Packages', startTime, pkgResult.ErrorMessage ?? 'Package installation failed');
83
+ }
84
+ const configResult = HandleServerConfig(manifest, context);
85
+ if (!configResult.Success) {
86
+ await SetAppStatus(context.ContextUser, createdAppId, 'Error');
87
+ await RecordFailureHistory(context.ContextUser, createdAppId, 'Install', manifest, 'Config', configResult.ErrorMessage ?? 'Config update failed', startTime);
88
+ return BuildFailureResult('Install', manifest.name, manifest.version, 'Config', startTime, configResult.ErrorMessage ?? 'Config update failed');
89
+ }
90
+ await HandleClientBootstrapRegeneration(context);
91
+ if (manifest.hooks?.postInstall) {
92
+ Callbacks?.OnProgress?.('Hooks', 'Running postInstall hook...');
93
+ await ExecuteHook(manifest.hooks.postInstall, context.RepoRoot);
94
+ }
95
+ Callbacks?.OnProgress?.('Record', 'Finalizing installation...');
96
+ await SetAppStatus(context.ContextUser, createdAppId, 'Active');
97
+ await RecordInstallHistoryEntry(context.ContextUser, createdAppId, 'Install', manifest, {
98
+ Success: true,
99
+ DurationSeconds: GetDurationSeconds(startTime),
100
+ StartedAt: new Date(startTime),
101
+ EndedAt: new Date(),
102
+ Summary: 'Initial installation',
103
+ });
104
+ Callbacks?.OnSuccess?.('Install', `Successfully installed ${manifest.name} v${manifest.version}`);
105
+ return {
106
+ Success: true,
107
+ Action: 'Install',
108
+ AppName: manifest.name,
109
+ Version: manifest.version,
110
+ DurationSeconds: GetDurationSeconds(startTime),
111
+ Summary: 'App installed successfully. Restart MJAPI and rebuild MJExplorer to activate.',
112
+ };
113
+ }
114
+ catch (error) {
115
+ const message = error instanceof Error ? error.message : String(error);
116
+ if (createdAppId && manifest) {
117
+ try {
118
+ await RecordFailureHistory(context.ContextUser, createdAppId, 'Install', manifest, 'Schema', message, startTime);
119
+ await SetAppStatus(context.ContextUser, createdAppId, 'Error');
120
+ }
121
+ catch {
122
+ }
123
+ }
124
+ Callbacks?.OnError?.('Install', message);
125
+ return BuildFailureResult('Install', options.Source, '', 'Schema', startTime, message);
126
+ }
127
+ }
128
+ async function RecordInstallationAtomically(contextUser, manifest, callbacks) {
129
+ const md = new Metadata();
130
+ const tg = await md.CreateTransactionGroup();
131
+ try {
132
+ const appId = await RecordAppInstallation(contextUser, manifest, callbacks, tg, 'Installing');
133
+ if (manifest.dependencies) {
134
+ await RecordAppDependencies(contextUser, appId, manifest.dependencies, tg);
135
+ }
136
+ callbacks?.OnProgress?.('Record', 'Committing installation records...');
137
+ const success = await tg.Submit();
138
+ if (!success) {
139
+ return { Success: false, ErrorMessage: 'Transaction failed: one or more records could not be saved' };
140
+ }
141
+ return { Success: true, AppId: appId };
142
+ }
143
+ catch (error) {
144
+ const message = error instanceof Error ? error.message : String(error);
145
+ return { Success: false, ErrorMessage: `Transaction failed: ${message}` };
146
+ }
147
+ }
148
+ async function CompensateSchemaOnFailure(manifest, context, schemaWasCreated, callbacks) {
149
+ if (!schemaWasCreated || !manifest.schema) {
150
+ return;
151
+ }
152
+ try {
153
+ callbacks?.OnProgress?.('Rollback', `Rolling back: dropping schema '${manifest.schema.name}'...`);
154
+ await DropAppSchema(manifest.schema.name, context.DatabaseProvider);
155
+ callbacks?.OnProgress?.('Rollback', `Schema '${manifest.schema.name}' dropped successfully`);
156
+ }
157
+ catch (rollbackError) {
158
+ const msg = rollbackError instanceof Error ? rollbackError.message : String(rollbackError);
159
+ callbacks?.OnError?.('Rollback', `Failed to drop schema '${manifest.schema.name}' during rollback: ${msg}`);
160
+ }
161
+ }
162
+ export async function UpgradeApp(options, context) {
163
+ const startTime = Date.now();
164
+ const { Callbacks } = context;
165
+ let upgradeAppId;
166
+ let manifest;
167
+ let previousVersion = '';
168
+ try {
169
+ const existingApp = await FindInstalledApp(context.ContextUser, options.AppName);
170
+ if (!existingApp) {
171
+ return BuildFailureResult('Upgrade', options.AppName, '', 'Schema', startTime, `App '${options.AppName}' is not installed`);
172
+ }
173
+ upgradeAppId = existingApp.ID;
174
+ previousVersion = existingApp.Version;
175
+ const targetVersion = options.Version ?? (await GetLatestVersion(existingApp.RepositoryURL, context.GitHubOptions));
176
+ if (!targetVersion) {
177
+ return BuildFailureResult('Upgrade', options.AppName, '', 'Schema', startTime, 'Could not determine target version');
178
+ }
179
+ const upgradeCheck = IsValidUpgrade(previousVersion, targetVersion);
180
+ if (!upgradeCheck.Compatible) {
181
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Schema', startTime, upgradeCheck.Message ?? 'Invalid upgrade version');
182
+ }
183
+ Callbacks?.OnProgress?.('Fetch', `Fetching manifest for ${options.AppName} v${targetVersion}...`);
184
+ const fetchResult = await FetchManifestFromGitHub(existingApp.RepositoryURL, targetVersion, context.GitHubOptions);
185
+ if (!fetchResult.Success || !fetchResult.ManifestJSON) {
186
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Schema', startTime, fetchResult.ErrorMessage ?? 'Failed to fetch manifest');
187
+ }
188
+ const parseResult = ParseAndValidateManifest(fetchResult.ManifestJSON);
189
+ if (!parseResult.Success || !parseResult.Manifest) {
190
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Schema', startTime, `Invalid manifest: ${parseResult.Errors?.join(', ')}`);
191
+ }
192
+ manifest = parseResult.Manifest;
193
+ const compatResult = CheckMJVersionCompatibility(context.MJVersion, manifest.mjVersionRange);
194
+ if (!compatResult.Compatible) {
195
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Schema', startTime, compatResult.Message ?? 'Incompatible MJ version');
196
+ }
197
+ if (manifest.dependencies && Object.keys(manifest.dependencies).length > 0) {
198
+ const depResult = await ResolveDependencyChain(manifest, context);
199
+ if (!depResult.Success) {
200
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Schema', startTime, depResult.ErrorMessage ?? 'Dependency check failed for upgrade');
201
+ }
202
+ if (depResult.DepsToInstall && depResult.DepsToInstall.length > 0) {
203
+ const installResult = await InstallDependencies(depResult.DepsToInstall, context);
204
+ if (!installResult.Success) {
205
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Schema', startTime, installResult.ErrorMessage ?? 'Failed to install new dependencies for upgrade');
206
+ }
207
+ }
208
+ }
209
+ await SetAppStatus(context.ContextUser, existingApp.ID, 'Upgrading');
210
+ if (manifest.migrations && manifest.schema) {
211
+ const migrationResult = await HandleMigrations(manifest, context);
212
+ if (!migrationResult.Success) {
213
+ await RecordFailureHistory(context.ContextUser, existingApp.ID, 'Upgrade', manifest, 'Migration', migrationResult.ErrorMessage ?? 'Migration failed', startTime, previousVersion);
214
+ await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
215
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Migration', startTime, migrationResult.ErrorMessage ?? 'Migration failed');
216
+ }
217
+ }
218
+ const pkgResult = await HandlePackageInstallation(manifest, context);
219
+ if (!pkgResult.Success) {
220
+ await RecordFailureHistory(context.ContextUser, existingApp.ID, 'Upgrade', manifest, 'Packages', pkgResult.ErrorMessage ?? 'Package update failed', startTime, previousVersion);
221
+ await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
222
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Packages', startTime, pkgResult.ErrorMessage ?? 'Package update failed');
223
+ }
224
+ const configResult = HandleServerConfig(manifest, context);
225
+ if (!configResult.Success) {
226
+ await RecordFailureHistory(context.ContextUser, existingApp.ID, 'Upgrade', manifest, 'Config', configResult.ErrorMessage ?? 'Config update failed', startTime, previousVersion);
227
+ await SetAppStatus(context.ContextUser, existingApp.ID, 'Error');
228
+ return BuildFailureResult('Upgrade', options.AppName, targetVersion, 'Config', startTime, configResult.ErrorMessage ?? 'Config update failed');
229
+ }
230
+ await HandleClientBootstrapRegeneration(context);
231
+ if (manifest.hooks?.postUpgrade) {
232
+ Callbacks?.OnProgress?.('Hooks', 'Running postUpgrade hook...');
233
+ await ExecuteHook(manifest.hooks.postUpgrade, context.RepoRoot);
234
+ }
235
+ await UpdateAppRecord(context.ContextUser, existingApp.ID, {
236
+ Version: manifest.version,
237
+ ManifestJSON: JSON.stringify(manifest),
238
+ Status: 'Active',
239
+ });
240
+ if (manifest.dependencies) {
241
+ await DeleteAppDependencies(context.ContextUser, existingApp.ID);
242
+ await RecordAppDependencies(context.ContextUser, existingApp.ID, manifest.dependencies);
243
+ }
244
+ await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Upgrade', manifest, {
245
+ PreviousVersion: previousVersion,
246
+ Success: true,
247
+ DurationSeconds: GetDurationSeconds(startTime),
248
+ StartedAt: new Date(startTime),
249
+ EndedAt: new Date(),
250
+ Summary: `Upgraded from ${previousVersion} to ${manifest.version}`,
251
+ });
252
+ Callbacks?.OnSuccess?.('Upgrade', `Successfully upgraded ${options.AppName} to v${manifest.version}`);
253
+ return {
254
+ Success: true,
255
+ Action: 'Upgrade',
256
+ AppName: options.AppName,
257
+ Version: manifest.version,
258
+ DurationSeconds: GetDurationSeconds(startTime),
259
+ Summary: `Upgraded from ${previousVersion} to ${manifest.version}. Restart MJAPI and rebuild MJExplorer.`,
260
+ };
261
+ }
262
+ catch (error) {
263
+ const message = error instanceof Error ? error.message : String(error);
264
+ if (upgradeAppId) {
265
+ try {
266
+ if (manifest) {
267
+ await RecordFailureHistory(context.ContextUser, upgradeAppId, 'Upgrade', manifest, 'Schema', message, startTime, previousVersion);
268
+ }
269
+ await SetAppStatus(context.ContextUser, upgradeAppId, 'Error');
270
+ }
271
+ catch {
272
+ }
273
+ }
274
+ Callbacks?.OnError?.('Upgrade', message);
275
+ return BuildFailureResult('Upgrade', options.AppName, '', 'Schema', startTime, message);
276
+ }
277
+ }
278
+ export async function RemoveApp(options, context) {
279
+ const startTime = Date.now();
280
+ const { Callbacks } = context;
281
+ let removeAppId;
282
+ let removeManifest;
283
+ try {
284
+ const existingApp = await FindInstalledApp(context.ContextUser, options.AppName);
285
+ if (!existingApp) {
286
+ return BuildFailureResult('Remove', options.AppName, '', 'Schema', startTime, `App '${options.AppName}' is not installed`);
287
+ }
288
+ removeAppId = existingApp.ID;
289
+ if (!options.Force) {
290
+ const dependents = await FindDependentApps(context.ContextUser, options.AppName);
291
+ if (dependents.length > 0) {
292
+ return BuildFailureResult('Remove', options.AppName, existingApp.Version, 'Schema', startTime, `Cannot remove: the following apps depend on ${options.AppName}: ${dependents.join(', ')}. Use --force to override.`);
293
+ }
294
+ }
295
+ await SetAppStatus(context.ContextUser, existingApp.ID, 'Removing');
296
+ removeManifest = JSON.parse(existingApp.ManifestJSON);
297
+ const manifest = removeManifest;
298
+ if (manifest.hooks?.preRemove) {
299
+ Callbacks?.OnProgress?.('Hooks', 'Running preRemove hook...');
300
+ await ExecuteHook(manifest.hooks.preRemove, context.RepoRoot);
301
+ }
302
+ Callbacks?.OnProgress?.('Config', 'Removing config, client bootstrap, and package references...');
303
+ await Promise.all([
304
+ Promise.resolve(RemoveServerDynamicPackages(context.RepoRoot, options.AppName)),
305
+ HandleClientBootstrapRegeneration(context),
306
+ Promise.resolve(RemoveAppPackages({
307
+ RepoRoot: context.RepoRoot,
308
+ ServerPackages: manifest.packages?.server ?? [],
309
+ ClientPackages: manifest.packages?.client ?? [],
310
+ SharedPackages: manifest.packages?.shared ?? [],
311
+ Version: existingApp.Version,
312
+ })),
313
+ ]);
314
+ Callbacks?.OnProgress?.('Packages', 'Running npm install...');
315
+ const npmResult = RunNpmInstall(context.RepoRoot, options.Verbose);
316
+ if (!npmResult.Success) {
317
+ Callbacks?.OnWarn?.('Packages', `npm install warning during removal: ${npmResult.ErrorMessage}`);
318
+ }
319
+ if (existingApp.SchemaName) {
320
+ Callbacks?.OnProgress?.('Metadata', `Removing entity metadata for schema '${existingApp.SchemaName}'...`);
321
+ await RemoveAppEntityMetadata(existingApp.SchemaName, context.ContextUser, Callbacks);
322
+ }
323
+ if (!options.KeepData && existingApp.SchemaName) {
324
+ Callbacks?.OnProgress?.('Schema', `Dropping schema '${existingApp.SchemaName}'...`);
325
+ const dropResult = await DropAppSchema(existingApp.SchemaName, context.DatabaseProvider);
326
+ if (!dropResult.Success) {
327
+ Callbacks?.OnWarn?.('Schema', `Failed to drop schema: ${dropResult.ErrorMessage}`);
328
+ }
329
+ }
330
+ await RecordInstallHistoryEntry(context.ContextUser, existingApp.ID, 'Remove', manifest, {
331
+ Success: true,
332
+ DurationSeconds: GetDurationSeconds(startTime),
333
+ StartedAt: new Date(startTime),
334
+ EndedAt: new Date(),
335
+ Summary: options.KeepData ? 'Removed (data kept)' : 'Removed (data dropped)',
336
+ });
337
+ await UpdateAppRecord(context.ContextUser, existingApp.ID, {
338
+ Status: 'Removed',
339
+ });
340
+ Callbacks?.OnSuccess?.('Remove', `Successfully removed ${options.AppName}`);
341
+ return {
342
+ Success: true,
343
+ Action: 'Remove',
344
+ AppName: options.AppName,
345
+ Version: existingApp.Version,
346
+ DurationSeconds: GetDurationSeconds(startTime),
347
+ Summary: options.KeepData ? 'App removed (database schema preserved)' : 'App removed',
348
+ };
349
+ }
350
+ catch (error) {
351
+ const message = error instanceof Error ? error.message : String(error);
352
+ if (removeAppId) {
353
+ try {
354
+ if (removeManifest) {
355
+ await RecordFailureHistory(context.ContextUser, removeAppId, 'Remove', removeManifest, 'Schema', message, startTime);
356
+ }
357
+ await SetAppStatus(context.ContextUser, removeAppId, 'Error');
358
+ }
359
+ catch {
360
+ }
361
+ }
362
+ Callbacks?.OnError?.('Remove', message);
363
+ return BuildFailureResult('Remove', options.AppName, '', 'Schema', startTime, message);
364
+ }
365
+ }
366
+ export async function DisableApp(appName, context) {
367
+ const startTime = Date.now();
368
+ const app = await FindInstalledApp(context.ContextUser, appName);
369
+ if (!app) {
370
+ return BuildFailureResult('Install', appName, '', 'Config', startTime, `App '${appName}' is not installed`);
371
+ }
372
+ ToggleServerDynamicPackages(context.RepoRoot, appName, false);
373
+ await HandleClientBootstrapRegeneration(context);
374
+ await SetAppStatus(context.ContextUser, app.ID, 'Disabled');
375
+ return {
376
+ Success: true,
377
+ Action: 'Install',
378
+ AppName: appName,
379
+ Version: app.Version,
380
+ DurationSeconds: GetDurationSeconds(startTime),
381
+ Summary: 'App disabled. Restart MJAPI and rebuild MJExplorer.',
382
+ };
383
+ }
384
+ export async function EnableApp(appName, context) {
385
+ const startTime = Date.now();
386
+ const app = await FindInstalledApp(context.ContextUser, appName);
387
+ if (!app) {
388
+ return BuildFailureResult('Install', appName, '', 'Config', startTime, `App '${appName}' is not installed`);
389
+ }
390
+ ToggleServerDynamicPackages(context.RepoRoot, appName, true);
391
+ await HandleClientBootstrapRegeneration(context);
392
+ await SetAppStatus(context.ContextUser, app.ID, 'Active');
393
+ return {
394
+ Success: true,
395
+ Action: 'Install',
396
+ AppName: appName,
397
+ Version: app.Version,
398
+ DurationSeconds: GetDurationSeconds(startTime),
399
+ Summary: 'App enabled. Restart MJAPI and rebuild MJExplorer.',
400
+ };
401
+ }
402
+ async function ResolveDependencyChain(manifest, context) {
403
+ if (!manifest.dependencies || Object.keys(manifest.dependencies).length === 0) {
404
+ return { Success: true };
405
+ }
406
+ context.Callbacks?.OnProgress?.('Dependencies', 'Resolving dependencies...');
407
+ const installedApps = await ListInstalledApps(context.ContextUser);
408
+ const installedMap = {};
409
+ for (const app of installedApps) {
410
+ installedMap[app.Name] = { Version: app.Version, Repository: app.RepositoryURL };
411
+ }
412
+ const rootNode = {
413
+ AppName: manifest.name,
414
+ Repository: manifest.repository,
415
+ Dependencies: manifest.dependencies,
416
+ };
417
+ const result = ResolveDependencies(rootNode, installedMap);
418
+ if (!result.Success) {
419
+ return { Success: false, ErrorMessage: result.ErrorMessage };
420
+ }
421
+ const depsToInstall = (result.InstallOrder ?? [])
422
+ .filter((d) => !d.AlreadyInstalled)
423
+ .map((d) => ({ AppName: d.AppName, Repository: d.Repository, VersionRange: d.VersionRange }));
424
+ return { Success: true, DepsToInstall: depsToInstall };
425
+ }
426
+ async function InstallDependencies(deps, context) {
427
+ for (const dep of deps) {
428
+ if (!dep.Repository) {
429
+ return {
430
+ Success: false,
431
+ ErrorMessage: `Dependency '${dep.AppName}' is not installed and no repository URL was provided in the manifest. Use the object form: { "version": "${dep.VersionRange}", "repository": "https://github.com/..." }`,
432
+ };
433
+ }
434
+ context.Callbacks?.OnProgress?.('Dependencies', `Installing dependency from ${dep.Repository}...`);
435
+ const result = await InstallApp({ Source: dep.Repository }, context);
436
+ if (!result.Success) {
437
+ return { Success: false, ErrorMessage: `Failed to install dependency: ${result.ErrorMessage}` };
438
+ }
439
+ }
440
+ return { Success: true };
441
+ }
442
+ async function HandleSchemaCreation(manifest, context, isReinstall = false) {
443
+ if (!manifest.schema) {
444
+ return { Success: true };
445
+ }
446
+ context.Callbacks?.OnProgress?.('Schema', `Checking schema '${manifest.schema.name}'...`);
447
+ const exists = await SchemaExists(manifest.schema.name, context.DatabaseProvider);
448
+ if (exists) {
449
+ if (isReinstall) {
450
+ context.Callbacks?.OnProgress?.('Schema', `Reusing existing schema '${manifest.schema.name}'`);
451
+ return { Success: true };
452
+ }
453
+ return { Success: false, ErrorMessage: `Schema '${manifest.schema.name}' already exists` };
454
+ }
455
+ if (manifest.schema.createIfNotExists !== false) {
456
+ context.Callbacks?.OnProgress?.('Schema', `Creating schema '${manifest.schema.name}'...`);
457
+ const result = await CreateAppSchema(manifest.schema.name, context.DatabaseProvider);
458
+ return { Success: result.Success, ErrorMessage: result.ErrorMessage };
459
+ }
460
+ return { Success: false, ErrorMessage: `Schema '${manifest.schema.name}' does not exist and createIfNotExists is false` };
461
+ }
462
+ async function HandleMigrations(manifest, context) {
463
+ if (!manifest.schema || !manifest.migrations) {
464
+ return { Success: true };
465
+ }
466
+ context.Callbacks?.OnProgress?.('Migration', 'Downloading migration files...');
467
+ const tempDir = join(tmpdir(), `mj-app-${manifest.name}-${Date.now()}`);
468
+ mkdirSync(tempDir, { recursive: true });
469
+ const downloadResult = await DownloadMigrations(manifest.repository, manifest.version, manifest.migrations.directory, tempDir, context.GitHubOptions);
470
+ if (!downloadResult.Success) {
471
+ return { Success: false, ErrorMessage: downloadResult.ErrorMessage };
472
+ }
473
+ context.Callbacks?.OnProgress?.('Migration', `Running ${downloadResult.Files?.length ?? 0} migration(s)...`);
474
+ const migrationResult = await RunAppMigrations({
475
+ MigrationsDir: tempDir,
476
+ SchemaName: manifest.schema.name,
477
+ DatabaseConfig: context.DatabaseConfig,
478
+ });
479
+ return { Success: migrationResult.Success, ErrorMessage: migrationResult.ErrorMessage };
480
+ }
481
+ async function HandlePackageInstallation(manifest, context) {
482
+ if (!manifest.packages) {
483
+ return { Success: true };
484
+ }
485
+ context.Callbacks?.OnProgress?.('Packages', 'Adding npm packages...');
486
+ const addResult = AddAppPackages({
487
+ RepoRoot: context.RepoRoot,
488
+ ServerPackages: manifest.packages.server ?? [],
489
+ ClientPackages: manifest.packages.client ?? [],
490
+ SharedPackages: manifest.packages.shared ?? [],
491
+ Version: manifest.version,
492
+ });
493
+ if (!addResult.Success) {
494
+ return { Success: false, ErrorMessage: addResult.ErrorMessage };
495
+ }
496
+ context.Callbacks?.OnProgress?.('Packages', 'Running npm install...');
497
+ const installResult = RunNpmInstall(context.RepoRoot, undefined, manifest.packages.registry);
498
+ return { Success: installResult.Success, ErrorMessage: installResult.ErrorMessage };
499
+ }
500
+ function HandleServerConfig(manifest, context) {
501
+ context.Callbacks?.OnProgress?.('Config', 'Updating server config...');
502
+ const result = AddServerDynamicPackages(context.RepoRoot, manifest);
503
+ return { Success: result.Success, ErrorMessage: result.ErrorMessage };
504
+ }
505
+ async function HandleClientBootstrapRegeneration(context) {
506
+ context.Callbacks?.OnProgress?.('Config', 'Regenerating client bootstrap...');
507
+ const apps = await ListInstalledApps(context.ContextUser);
508
+ const appDeps = new Map();
509
+ for (const app of apps) {
510
+ const manifest = JSON.parse(app.ManifestJSON);
511
+ const depNames = manifest.dependencies ? Object.keys(manifest.dependencies) : [];
512
+ appDeps.set(app.Name, depNames);
513
+ }
514
+ const sortedNames = TopologicalSortApps(appDeps);
515
+ const entries = [];
516
+ const appsByName = new Map(apps.map((a) => [a.Name, a]));
517
+ for (const name of sortedNames) {
518
+ const app = appsByName.get(name);
519
+ if (!app)
520
+ continue;
521
+ const manifest = JSON.parse(app.ManifestJSON);
522
+ const clientPkgs = [...(manifest.packages?.client ?? []), ...(manifest.packages?.shared ?? [])];
523
+ for (const pkg of clientPkgs) {
524
+ entries.push({
525
+ AppName: app.Name,
526
+ Version: app.Version,
527
+ PackageName: pkg.name,
528
+ Enabled: app.Status === 'Active',
529
+ });
530
+ }
531
+ }
532
+ RegenerateClientBootstrap(context.RepoRoot, entries);
533
+ }
534
+ function TopologicalSortApps(appDeps) {
535
+ const visited = new Set();
536
+ const result = [];
537
+ function Visit(name) {
538
+ if (visited.has(name))
539
+ return;
540
+ visited.add(name);
541
+ const deps = appDeps.get(name) ?? [];
542
+ for (const dep of deps) {
543
+ if (appDeps.has(dep)) {
544
+ Visit(dep);
545
+ }
546
+ }
547
+ result.push(name);
548
+ }
549
+ for (const name of appDeps.keys()) {
550
+ Visit(name);
551
+ }
552
+ return result;
553
+ }
554
+ async function ExecuteHook(command, cwd) {
555
+ const { execSync } = await import('node:child_process');
556
+ execSync(command, { cwd, encoding: 'utf-8', timeout: 120000, stdio: 'inherit' });
557
+ }
558
+ async function RemoveAppEntityMetadata(schemaName, contextUser, callbacks) {
559
+ try {
560
+ const rv = new RunView();
561
+ const escaped = EscapeSqlString(schemaName);
562
+ const entityResult = await rv.RunView({
563
+ EntityName: 'MJ: Entities',
564
+ ExtraFilter: `SchemaName = '${escaped}'`,
565
+ ResultType: 'entity_object',
566
+ }, contextUser);
567
+ if (!entityResult.Success || entityResult.Results.length === 0) {
568
+ await DeleteEntitiesByFilter(rv, contextUser, 'MJ: Schema Info', `SchemaName = '${escaped}'`);
569
+ callbacks?.OnSuccess?.('Metadata', `Entity metadata for schema '${schemaName}' removed`);
570
+ return;
571
+ }
572
+ const entityIds = entityResult.Results.map((e) => String(e.Get('ID')));
573
+ const idList = entityIds.map((id) => `'${EscapeSqlString(id)}'`).join(',');
574
+ const entityIdFilter = `EntityID IN (${idList})`;
575
+ await Promise.all([
576
+ DeleteEntitiesByFilter(rv, contextUser, 'MJ: Entity Permissions', entityIdFilter),
577
+ DeleteEntitiesByFilter(rv, contextUser, 'MJ: Application Entities', entityIdFilter),
578
+ ]);
579
+ await DeleteEntitiesByFilter(rv, contextUser, 'MJ: Entity Relationships', `EntityID IN (${idList}) OR RelatedEntityID IN (${idList})`);
580
+ await DeleteEntitiesByFilter(rv, contextUser, 'MJ: Entity Fields', `EntityID IN (${idList})`);
581
+ for (const entity of entityResult.Results) {
582
+ await entity.Delete();
583
+ }
584
+ await DeleteEntitiesByFilter(rv, contextUser, 'MJ: Schema Info', `SchemaName = '${escaped}'`);
585
+ callbacks?.OnSuccess?.('Metadata', `Entity metadata for schema '${schemaName}' removed`);
586
+ }
587
+ catch (error) {
588
+ const message = error instanceof Error ? error.message : String(error);
589
+ callbacks?.OnWarn?.('Metadata', `Failed to remove entity metadata: ${message}`);
590
+ }
591
+ }
592
+ async function DeleteEntitiesByFilter(rv, contextUser, entityName, filter) {
593
+ const result = await rv.RunView({
594
+ EntityName: entityName,
595
+ ExtraFilter: filter,
596
+ ResultType: 'entity_object',
597
+ }, contextUser);
598
+ if (result.Success) {
599
+ for (const record of result.Results) {
600
+ await record.Delete();
601
+ }
602
+ }
603
+ }
604
+ function GetDurationSeconds(startTime) {
605
+ return Math.round((Date.now() - startTime) / 1000);
606
+ }
607
+ function BuildFailureResult(action, appName, version, errorPhase, startTime, errorMessage) {
608
+ return {
609
+ Success: false,
610
+ Action: action,
611
+ AppName: appName,
612
+ Version: version,
613
+ ErrorMessage: errorMessage,
614
+ ErrorPhase: errorPhase,
615
+ DurationSeconds: GetDurationSeconds(startTime),
616
+ };
617
+ }
618
+ async function RecordFailureHistory(contextUser, appId, action, manifest, errorPhase, errorMessage, startTime, previousVersion) {
619
+ try {
620
+ await RecordInstallHistoryEntry(contextUser, appId, action, manifest, {
621
+ PreviousVersion: previousVersion,
622
+ Success: false,
623
+ ErrorPhase: errorPhase,
624
+ ErrorMessage: errorMessage,
625
+ DurationSeconds: GetDurationSeconds(startTime),
626
+ StartedAt: new Date(startTime),
627
+ EndedAt: new Date(),
628
+ Summary: `Failed during ${errorPhase} phase: ${errorMessage}`,
629
+ });
630
+ }
631
+ catch {
632
+ }
633
+ }
634
+ //# sourceMappingURL=install-orchestrator.js.map