feat(member-base-nestjs-module): let applications name the casbin policy table - #214
Merged
Conversation
…icy table Casbin policies went into typeorm-adapter's own `casbin_rule` and nowhere else, because `newAdapter` was called with only its first argument and the second one — where `customCasbinRuleEntity` lives — was never exposed. Two applications sharing a database therefore shared the table. Since the usual startup routine is "load the authoritative rules, clear the table, write them back", each one's boot briefly empties the table the other is enforcing against. `casbinRuleEntity` gives each of them its own. Building the adapter config is a standalone function rather than an inline ternary because the provider around it cannot be unit tested: it dynamic-imports typeorm-adapter and then opens a real connection. Same split, and the same reason, as load-typeorm-adapter.ts. Left unset the config stays `undefined`, so `newAdapter` is called exactly as before and existing deployments keep `casbin_rule`. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Deploying rytass-utils-storybook with
|
| Latest commit: |
4eaff8e
|
| Status: | ✅ Deploy successful! |
| Preview URL: | https://8051c1eb.rytass-utils-storybook.pages.dev |
| Branch Preview URL: | https://feat-casbin-rule-entity.rytass-utils-storybook.pages.dev |
…ly controls
The section said the entity is "read for its table name only — the columns
must remain those of CasbinRule". Both halves were wrong. typeorm-adapter
builds every policy row from the class (savePolicyLine does `new
(this.getCasbinRuleConstructor())()`), resolves the repository through it and,
where it opens the connection itself, creates the table from it. Extra columns
are explicitly supported upstream; @CreateDateColumn / @UpdateDateColumn are
that README's own example.
Which is also why the option is inert on the other branch. `newAdapter` builds
`entities: [getCasbinRuleType(...)]` only when handed connection options; given
`{ connection: dataSource }` it takes that DataSource's entity list as it finds
it while still resolving the repository through the custom class. An
application reusing its main DataSource — the natural choice in exactly the
two-apps-one-database setup this option is for — gets EntityMetadataNotFoundError
out of loadPolicy() at boot, naming TypeORM rather than the option that caused
it. Documented rather than fixed: registering entities on someone else's
DataSource is not this module's call.
llms.txt gains the section it was missing entirely, and the same caveat lands
on the option's JSDoc so it shows up at the call site.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TYPZe3xb4ZNVzHGhLeJE5U
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Why
Casbin policies went into typeorm-adapter's own
casbin_ruleand nowhere else.newAdapteraccepts a second argument carryingcustomCasbinRuleEntity, but the module only ever passed the first one, so the table name was not reachable throughMemberBaseModule.forRoot.Two applications pointed at the same database therefore shared one policy table. Since the usual startup routine is "load the authoritative rules, clear the table, write them back", each one's boot briefly empties the table the other is enforcing against. Converging at the end is not the same as being isolated in between.
What
A new
casbinRuleEntityoption. Subclass typeorm-adapter'sCasbinRule, name the table, hand it over:Two decisions worth flagging for review
The config is
undefinedwhen the option is unset, not{}.newAdapteris then called exactly as it was before this option existed, so no existing deployment changes behaviour or leavescasbin_rule. The spec covers all three unset shapes (no options,{}, adapter configured but no entity).Building that config is a standalone function rather than an inline ternary. The
CASBIN_ENFORCERprovider around it cannot be unit tested — it dynamic-imports typeorm-adapter (whichjest.mockcannot intercept under ESM) and then opens a real connection, andoption-providers.tsis excluded from coverage for that reason. Extracting it is what gives the new option test coverage at all. This is the same split, for the same reason, asload-typeorm-adapter.ts.TypeORMAdapterConfigis restated locally because typeorm-adapter declares it but does not re-export it from its entry point; the alternative was a deep import oftypeorm-adapter/lib/adapter.Scope
This separates the cache, not the permissions. If both applications rebuild their policies from the same upstream tables, both still end up with identical contents, and a permission missing upstream stays missing in both. Setting the option on an application that already has policies starts it from an empty table — it does not migrate rows. Both caveats are stated in the README section.
Verification
ldapts, an optional peer dependency that is not installed in this checkout;tscreports the same single unrelated error on the clean tree.🤖 Generated with Claude Code