Skip to content

Fix schema lookups and repository resolution to be resilient to mangling - #18

Merged
ame-axelor merged 3 commits into
axelor:devfrom
kac-axelor:mangling
May 20, 2026
Merged

ame-axelor merged 3 commits into
axelor:devfrom
kac-axelor:mangling

Conversation

@kac-axelor

@kac-axelor kac-axelor commented Mar 18, 2026 •

Copy link
Copy Markdown
Contributor

JavaScript bundlers (webpack, turbopack) mangle class and function names in production builds to reduce bundle size:

// Source
class AOSPartner extends AuditableModel { ... }

// After mangling
class e extends t { ... }

This is harmless for most code, but breaks any code that reads a class name at runtime and compares it against a string baked in at compile time. Goovee ORM has several such patterns.

TypeORM's EntityMetadata exposes two relevant properties:

  • metadata.name / metadata.targetName — the class constructor name ((EntityClass as Function).name). Gets mangled.
  • metadata.tableNameWithoutPrefix — the table name after applying the naming strategy, before any global entityPrefix. Never mangled and stable for schema lookups regardless of DataSource prefix configuration.

Similarly, RelationMetadata exposes .name (the class constructor name, mangled) and .target (the class constructor reference, unaffected by mangling).


Changes

1. src/client/parser/context.ts — isStringField schema lookup

isStringField is called during query parsing to decide whether to wrap a field in lower() / unaccent() for case-insensitive and accent-insensitive search. It looks up the entity's schema definition to check if a field is of type String.

The lookup was using metadata.targetName (mangled) to find the matching entry in schemaDefs (which stores original names like "AOSPartner"). This silently failed in production, causing isStringField to always return false and disabling normalization entirely.

The fix resolves the schema entry using the active TypeORM naming strategy — the same logic TypeORM uses to derive the table name from @Entity() decorator arguments — so the lookup is both mangling-resilient and correct regardless of which naming strategy is configured.

// Before
const schemaDef = this.schema.find((x) => x.name === repo.metadata.targetName);

// After
const ns = repo.metadata.connection.namingStrategy;
const schemaDef = this.schema.find(
  (x) => ns.tableName(x.name, x.table) === repo.metadata.tableNameWithoutPrefix,
);

2. src/client/repository/repository.ts — getProtectedFields schema lookup

getProtectedFields enforces that fields marked internal (never writable) or readonly (not updatable after creation) are rejected on create and update operations. It also looks up schemaDefs by entity name.

The lookup used meta.name (mangled), so the entity was never found. getProtectedFields would return an empty set, silently bypassing all protected field checks.

The fix passes the repo directly so getProtectedFields can use the naming strategy from the live connection, matching entries the same way TypeORM resolves them.

// Before
function getProtectedFields(client, entityName, mode) {
  const entity = schema.find((e) => e.name === entityName);
  ...
}
getProtectedFields(this.#client, meta.name, "create");
getProtectedFields(this.#client, meta.name, "update");

// After
function getProtectedFields(repo, client, mode) {
  const ns = repo.metadata.connection.namingStrategy;
  const entity = schema.find(
    (e) => ns.tableName(e.name, e.table) === repo.metadata.tableNameWithoutPrefix,
  );
  ...
}
getProtectedFields(repo, this.#client, "create");
getProtectedFields(repo, this.#client, "update");

3. src/client/repository/repository.ts and src/client/repository/query/query.ts — string-based getRepository calls

Three call sites were passing a string (the class constructor name) to TypeORM's getRepository. Under mangling, this works incidentally — rMeta.name and the internally registered metadata name are both the same mangled value — but it is fragile. If two entity classes are ever assigned the same mangled name (possible across async chunks), getRepository(string) silently returns the wrong repository.

The fix uses the class constructor reference instead, consistent with the already-correct pattern in join-handler.ts (getRepository(relation.type)).

// Before — string lookup, fragile under mangling
// repository.ts
const rRepo = repo.manager.getRepository(rMeta.name);
// query.ts
const rr = repo.manager.getRepository(relation.inverseEntityMetadata.name);

// After — class reference, safe under mangling
// repository.ts
const rRepo = repo.manager.getRepository(rMeta.target);
// query.ts
const rr = repo.manager.getRepository(relation.inverseEntityMetadata.target);

Notes

The portal project also adds --no-mangling to its next build command as a short-term mitigation while these fixes are released. That flag can be removed once this ORM version is consumed.

@kac-axelor kac-axelor changed the title Fix schema lookups and repository resolution to be resilient to name … Fix schema lookups and repository resolution to be resilient to mangling Mar 18, 2026
@kac-axelor
kac-axelor force-pushed the mangling branch 3 times, most recently from 8fd5440 to 35f3df8 Compare March 20, 2026 07:34
kac-axelor and others added 3 commits May 20, 2026 18:33
Reproduce the bugs PR axelor#18 fixes against a real DataSource and Postgres
container by mutating Function.name on entity classes before TypeORM
metadata is built (the same effect bundlers have in production).

Failing-on-broken-code (regression coverage):

- Top-level isStringField: lowerCase/unaccent normalization is silently
  disabled because the schemaDefs lookup misses when targetName is
  mangled. SELECTs return zero rows instead of normalized matches.

- Relation reference resolved by String field: `title: { select: { name
  "Jose" } }` throws "Referenced X not found" when the inverse entity
  is mangled, because the inner findOne hits the same isStringField bug
  and skips unaccent/lower wrapping.

Passing-as-documentation:

- ManyToOne reference, OneToMany collection-create, and relation-loader
  query under single-class mangling — these paths work incidentally
  because both sides read the mangled Function.name; the
  getRepository(rMeta.target) change is defensive.

- Name collision (two entity classes forced to the same mangled name) —
  still resolves to the correct repository, confirming TypeORM's lookup
  is more resilient than feared.
@ame-axelor
ame-axelor merged commit 7871064 into axelor:dev May 20, 2026
5 checks passed
ame-axelor pushed a commit that referenced this pull request May 20, 2026
…ing (#18)

* Fix schema lookups and repository resolution to be resilient to name mangling

* Fix schema lookups to use the active naming strategy for table name resolution

* Add e2e tests for mangling resilience

Reproduce the bugs PR #18 fixes against a real DataSource and Postgres
container by mutating Function.name on entity classes before TypeORM
metadata is built (the same effect bundlers have in production).

Failing-on-broken-code (regression coverage):

- Top-level isStringField: lowerCase/unaccent normalization is silently
  disabled because the schemaDefs lookup misses when targetName is
  mangled. SELECTs return zero rows instead of normalized matches.

- Relation reference resolved by String field: `title: { select: { name
  "Jose" } }` throws "Referenced X not found" when the inverse entity
  is mangled, because the inner findOne hits the same isStringField bug
  and skips unaccent/lower wrapping.

Passing-as-documentation:

- ManyToOne reference, OneToMany collection-create, and relation-loader
  query under single-class mangling — these paths work incidentally
  because both sides read the mangled Function.name; the
  getRepository(rMeta.target) change is defensive.

- Name collision (two entity classes forced to the same mangled name) —
  still resolves to the correct repository, confirming TypeORM's lookup
  is more resilient than feared.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants