MemberJunction / MemberJunction/MJ
Browser resolves the CodeGen-generated entity class instead of the app's custom subclass, so client-side Validate() overrides and helper methods silently never run
- Dominant language
- TSQL
- Stars
- 29
- Forks
- 6
- Avg merge
- 2d 1h
- Merged PRs (30d)
- 323
Description
## Summary
In the browser, MJ's ClassFactory resolves the **CodeGen-generated** entity class rather than the app's **custom subclass** registered for the same entity key — so every method and `Validate()` override on the custom subclass is silently absent client-side. The generated class renders and saves normally, so nothing appears wrong; the app's client-side rules simply never run.
Confirmed still failing **after restarting both MJExplorer and MJAPI**, so this is not a stale dev bundle.
## Evidence
`bizapps-contracts` has a shared subclass with a method that exists nowhere else:
```ts
// packages/Entities/src/ContractEntity.ts
@RegisterClass(BaseEntity, 'MJ_BizApps_Contracts: Contracts')
export class ContractEntity extends mjBizAppsContractsContractEntity {
public Supersede(successor: ContractEntity): void { ... }
public override Validate(): ValidationResult { ... } // app rules live here
}
```
A form panel guarded on it and reported the failure to the user:
```ts
if (typeof this.Record.Supersede !== 'function') { /* shown to the user */ }
```
That branch fires in the browser. Since `Supersede` exists only on `ContractEntity`, the resolved class must be the generated `mjBizAppsContractsContractEntity`.
The same operation **works on the server**. A harness that boots the provider and imports both packages resolves `ContractEntityServer` and persists correctly:
```
predecessor class: ContractEntityServer
Save() -> true
✔ PERSISTED
```
## What I checked, and why I think it should work
- The built `dist/ContractEntity.js` **does** contain `Supersede`.
- The dist barrel evaluates `generated/entity_subclasses.js` **first**, then `ContractEntity.js`. With equal priority the **last** registration wins, so the custom subclass should win.
- No deep imports bypass the barrel — the generated form components import `@mj-biz-apps/contracts-entities` (the barrel), not the generated file directly.
- ES modules evaluate once, so a later re-import should not re-register the generated class.
- **This is MJ's own convention, not a deviation.** MJ core does exactly the same thing: `MJEntityEntityExtended`, `MJEntityFieldEntityExtended`, `MJQueryEntityExtended`, `MJTemplateEntityExtended`, `MJListDetailEntityExtended` all use `@RegisterClass(BaseEntity, 'MJ: …')` with **no priority** and rely on barrel order.
## Stated uncertainty
I have **not** isolated the mechanism, and I want to be explicit rather than send someone chasing my guess. Candidates:
1. **Bundler-driven evaluation order.** Angular's build may hoist or reorder the barrel's re-exports so the generated module evaluates *after* the custom one, flipping the tie. If so, relying on barrel order is unsafe in a bundled browser context even though it holds in Node — which would make this a platform-level concern, since MJ core relies on the same mechanism.
2. **Two ClassFactory instances / registry split.** If the browser bundle ends up with more than one copy of `@memberjunction/global`, registrations land in a different factory than the one the resolver reads. Note this instance carries a known workspace-resolution stopgap (`preferWorkspacePackages`), so a duplicated copy is plausible here specifically and may be local rather than general.
3. **Something app-side I have not found** — a second registration, or a registration that never fires because a module is tree-shaken in the production build path.
A quick way to discriminate: log `Metadata.Provider` resolution for the entity plus the ClassFactory's registered candidates and their priorities at runtime in the browser, and check whether more than one `@memberjunction/global` is present in the bundle.
## Why it matters even though a workaround exists
The failure mode is silence. An app author writes `Validate()` overrides and helper methods on a custom subclass, the form works, saves succeed — and none of the app's client-side rules are in effect. In this app that would mean a reserved-pattern guard, a duplicate-child guard and a flag-consistency guard all quietly not running in the browser while passing on the server.
The affected work is being moved to a Remotable Operation, which is the right architecture for a cross-record write anyway and sidesteps this entirely. Filing because the underlying resolution question is broader than the one feature.
## Environment
MJ `6.1.0-edge.2/3`, `bizapps-contracts` on `build/backend-requirements`, Angular 21, pnpm parent-workspace topology, MJExplorer dev server. Reproduced after full restarts of Explorer and MJAPI.
Contributor guide
Research direction
Start with packages/Entities/src/ContractEntity.ts and the generated/entity_subclasses.js barrel, then inspect Metadata.Provider and ClassFactory registrations in the browser bundle. Compare registered candidates and priorities, and check whether multiple @memberjunction/global copies are present. Done when the browser resolves ContractEntity and its Supersede and Validate methods are available.
Written by the indexing model from the issue text.
Assessment
- Tech stack
- angular, typescript
- Domain
- frontend
- Issue type
- Bug
- Difficulty
- 4/5
- Estimated time
- 3-5 days
- Activity status
- Active
- Clarity
- Mostly clear
- Newbie friendliness
- 43/100