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

Open
#4,002 1 comment 0 reactions 0 assignees View on GitHub
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

Open the contributing 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

Get new issues in your inbox

A short digest of beginner-friendly GitHub issues.