MemberJunction / MemberJunction/MJ
Open App manifest cannot declare migration placeholders its own migrations require — the burden falls on every host, undiscoverably
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 323
Description
An Open App whose migrations reference a schema other than its own and `__mj` has no way to say so. `mj-app.json` has no field for it, so the placeholder must be configured by **every host that installs the app**, in the host's own `mj.config.cjs`. Nothing in the manifest, the CLI output, or the install flow tells a host operator this is required. The first signal is a raw SQL syntax error partway through the Migration phase.
## Where
**The manifest has no field.** `packages/OpenApp/Engine/src/manifest/manifest-schema.ts:104`
```ts
const migrationsSchema = z.object({
directory: z.string().optional().default('migrations'),
engine: z.enum(['flyway', 'skyway']).optional().default('skyway'),
/** OPTIONAL teardown directory ... */
teardownDirectory: z.string().optional(),
});
```
**Only two placeholders are supplied automatically.** `packages/OpenApp/Engine/src/install/migration-runner.ts:282`
```ts
Placeholders: {
'flyway:defaultSchema': canonicalSchema,
mjSchema: mjCoreSchema ?? '__mj',
...(extraPlaceholders ?? {}),
},
```
**And `extraPlaceholders` comes from the host, never the app.** `packages/OpenApp/Engine/src/install/install-orchestrator.ts:1609` passes `context.MigrationPlaceholders`, which `packages/MJCLI/src/utils/open-app-context.ts:164` reads as `config.openApps?.migrationPlaceholders` — the host repo's config.
## Why it fails silently until it fails loudly
Skyway substitutes **only known** placeholders and leaves unknown `${…}` patterns untouched — a deliberate choice so it does not mangle JS template literals or JSON templates embedded in SQL. Correct in general, but it means an unresolved placeholder is not reported as an unresolved placeholder. The literal text `${commonSchema}.Person` is handed to the database, which rejects it as a syntax error naming a token the operator has never seen and cannot search for meaningfully.
By then the installer has already created the schema, so `CompensateSchemaOnFailure` drops it and the operator is back to zero with an error that does not say what to configure.
## The concrete case
`bizapps-caliber` declares an entity that IS-A extends `bizapps-common`'s `Person` on a shared primary key, so one of its migrations carries:
```sql
CONSTRAINT FK_Applicant_Person FOREIGN KEY (ID) REFERENCES ${commonSchema}.Person(ID),
```
This resolves correctly during development, because `mj migrate` derives placeholders from `SQLOutput.schemaPlaceholders` in the app's *own* config (`packages/MJCLI/src/config.ts`, the `schemaPlaceholders` branch). The two commands read placeholders from **two different places**, so an app can be fully green in its own repo and fail on the first foreign host — which is exactly what happened here, and the divergence is itself worth a look.
The app cannot work around it:
- The manifest has no field to declare it (above).
- Hardcoding the sibling schema name into the migration removes the indirection the placeholder mechanism exists to provide, and it is not always known at authoring time.
- The migration is already merged, and merged migrations are immutable — editing one breaks the Flyway/Skyway checksum on every database that has run it.
So the only remedy is a per-host config edit, discoverable only by reading `open-app-engine` source. Any app with a cross-schema reference hits this, and IS-A over a sibling Open App's entity is a pattern MJ actively supports — `validateISARelationships` handles it, and CodeGen emits the child base view as an `INNER JOIN` onto the parent's base table.
## Suggested fix
Let the manifest declare what its migrations need, and have the engine merge app-declared defaults under host overrides:
```jsonc
"migrations": {
"directory": "migrations",
"engine": "skyway",
"placeholders": { "commonSchema": "__mj_BizAppsCommon" }
}
```
Merge order `app defaults → host overrides` keeps the existing host escape hatch intact for the case it was built for (a host that relocated a schema) while making the common case work with no host action at all.
Two smaller changes worth considering independently, each of which would have turned this from a dead end into a one-line fix:
1. **Fail loudly on an unresolved placeholder in Open App migrations.** Skyway's pass-through default is right for general SQL, but the install path knows it is running app migrations. A pre-flight scan for `${…}` tokens that are neither built-in nor supplied, erroring with the token name and where to set it, costs one pass over the SQL and replaces a database syntax error with an actionable message.
2. **Reconcile the two placeholder sources.** `mj migrate` reads `SQLOutput.schemaPlaceholders`; `mj app install` reads `openApps.migrationPlaceholders`. An app author has no reason to expect that the command they use daily and the command their consumers use resolve placeholders from different config keys.
## Verified on
- Published `@memberjunction/open-app-engine@5.51.0` and `@memberjunction/cli@5.51.0`
- Source at `d872831228` on the 6.0.0 line — unchanged; line numbers above are from that commit
- Skyway pass-through behaviour: `@memberjunction/skyway-core`, `executor/placeholder.js` (`SubstitutePlaceholders` returns the full match for unknown names)
## Related
- MemberJunction/bizapps-caliber#44 — the host install this blocks; the workaround is documented as a required host edit in that repo's `docs/host-install-runbook.md`
Contributor guide
Assessment
This issue has not been assessed yet.