# @guisao-llc/gambit-cascade

> Sequential cascading deletion for databases without foreign keys.

Latest version **0.2.0** (published 2026-09-23) · UNLICENSED license · 0 weekly downloads

## Install

```sh
npm install @guisao-llc/gambit-cascade
pnpm add @guisao-llc/gambit-cascade
yarn add @guisao-llc/gambit-cascade
bun add @guisao-llc/gambit-cascade
```

## Health

**Score 70/100 (B)** — status: active.

Positive: has types; esm support; no vulnerabilities; recently updated; high maintenance score; high quality score.

Warnings: low downloads; pre 1.0.

## Facts

| | |
|---|---|
| Version | 0.2.0 |
| Published | 2026-09-23 |
| First published | 2026-09-01 |
| Weekly downloads | 0 |
| License | UNLICENSED |
| TypeScript types | bundled |
| Module format | ESM + CommonJS |
| Node | >=20 <23 |
| Dependencies | 0 |
| Unpacked size | 8.5 KB |
| Known vulnerabilities | 0 |
| Install scripts | no |
| GitHub stars | 0 |
| Maintainers | carlosguisao |
| Keywords | mongodb, mongoose, cascade, delete, referential-integrity |

## Links

- npm: https://www.npmjs.com/package/@guisao-llc/gambit-cascade
- Repository: https://github.com/Guisao-LLC/Gambit
- Homepage: https://github.com/Guisao-LLC/Gambit/tree/main/packages/gambit-cascade
- Issues: https://github.com/Guisao-LLC/Gambit/issues
- npm.io page: https://npm.io/package/@guisao-llc/gambit-cascade

## Alternatives

- [angular-pipes](https://npm.io/package/angular-pipes.md) — 5.6K weekly downloads
- [@ng-web-apis/midi](https://npm.io/package/@ng-web-apis/midi.md) — 2.6K weekly downloads
- [happn-3](https://npm.io/package/happn-3.md) — 1.6K weekly downloads
- [@opensip-cli/lang-go](https://npm.io/package/@opensip-cli/lang-go.md) — 1.2K weekly downloads
- [mongoose-typescript](https://npm.io/package/mongoose-typescript.md) — 85 weekly downloads

## Recent versions

- 0.2.0 (latest) — 2026-09-23
- 0.1.0 — 2026-09-01

## README

# Gambit

The parts of an application that are not the application.

Every app in this family needs the same things before it can start being itself:
someone can log in, someone holds a role, mail gets delivered, a record has a
history, deleting a thing deletes what hung off it. Gambit is those parts, kept
in one place and versioned, so a new app starts with them already solved.

Two apps consume it today — a driving-school platform and a clinic platform.
Neither one's vocabulary appears anywhere in here, and that is enforced rather
than trusted (see **The rule**, below).

## Layout

```
packages/
  gambit-auth/        HTTP errors, JWT, request authentication
  gambit-cascade/     deleting a thing deletes what hung off it
  gambit-rbac/        roles, permissions, the authorize middleware, the cache
  gambit-account/     what an account IS — fields, password and avatar rules
  gambit-person/      who holds one — the person record, and enrolling them
  gambit-settings/    app settings with typed defaults
  gambit-testing/     the RBAC route grid every consuming app runs
  gambit-ui/          React panels built on the rules above
  create-gambit-app/  the generator that starts a new app with all of it
```

They landed in dependency order, and the graph is shallow: most depend on
nothing, `rbac` depends on `auth` and `cascade`, `person` and `ui` depend on
`account`. `gambit-auth` went first because everything else needs it.

`account` and `person` are the pair worth explaining, since the names do not
separate themselves. An account is a **credential**; a person is **who holds
it**. An app has accounts with no person behind them — a service account, a
seed administrator — and people who do not have an account yet.

Still staged for extraction and unpackaged: `email`, `events`, `change-log`,
`data`, `ai`, `diagnostics`.

## Install

```bash
npm install @guisao-llc/gambit-auth
```

That is the whole of it. Public packages on the public registry: no `.npmrc`,
no token, no registry configuration, nothing to add to a deploy environment.

That is a deliberate choice rather than an accident. The first plan put these in
GitHub Packages, which would have meant a `read:packages` token in every
consuming app's build environment — and that cost lands at deploy time, in a
clean container, as an install failure whose error message never mentions
tokens. There is nothing in here worth protecting: it is authentication,
authorization and mail plumbing. The value being kept private lives in the apps
that consume it, and stays there.

## Publishing

```bash
npm publish --workspace @guisao-llc/gambit-auth
```

`prepublishOnly` builds, runs the tests, and then runs `scripts/prepublish-check.mjs`
against the files that will actually ship. That last gate exists because npm
restricts unpublishing after 72 hours, so a leaked secret is public forever. It
refuses to publish on:

- an email address (`example.com` excepted — that is what docs are for)
- a private key block, AWS key id, GitHub or Slack token
- an assigned secret/password/token literal
- a connection string carrying credentials
- **app vocabulary** — a package naming a specific application is not a leak,
  but it is a design failure, and shipping it publicly makes that claim to
  strangers

It scans `dist/`, not `src/`, because those are different sets: `files`
controls what ships, and a comment stripped from source can still sit in a
stale build.

## Working on it

```bash
npm install        # once, at the root — npm workspaces
npm run build      # both module formats, across all packages
npm test           # each package's own suite
```

Tests run against `dist/`, not `src/`, deliberately: they assert what a consumer
actually receives after install — the compiled entrypoint, the exports
`package.json` points at, the runtime behavior. A test against source can pass
while the published artifact is broken.

## Module format

Every package ships **both** formats from one source tree:

```
dist/           CommonJS + the .d.ts files, shared by both halves
dist/esm/       the same code as ES modules
```

`exports` picks between them — `require` gets `dist/`, `import` gets `dist/esm/`
— and `main`/`module`/`types` stay as they were, so a resolver too old to read
`exports` still finds the CommonJS build. The type declarations are emitted once,
by the CommonJS pass, because two identical copies could only ever disagree.

This is not housekeeping. CommonJS-only is what took both apps down once:
installed from the registry a CJS package lands in `node_modules` and Vite
applies interop, but as a `file:` link it is treated as source, the interop is
skipped, and Rollup reports every named export as missing. `gambit-ui` is
browser code, and a browser package that cannot be tree-shaken makes every
consumer pay for the parts it does not import.

Two rules keep the dual build working:

- **Relative imports are written with a `.js` extension** — `./password-policy.js`,
  not `./password-policy`. TypeScript resolves it back to the `.ts` file and
  emits the specifier verbatim. Node's ESM loader requires the extension and
  `require` tolerates it, so it is the only spelling that satisfies both.
- **`dist/esm/package.json` is generated** (`scripts/mark-esm.mjs`) and contains
  `{"type": "module"}`. Without it Node reads those files under the root
  package's default — CommonJS — and throws on the first `import`.

Anything server-only sits behind its own subpath rather than the root, so the
root stays importable from a browser: `@guisao-llc/gambit-account/mongoose`,
`@guisao-llc/gambit-person/mongoose`. That split exists because re-exporting a
Mongoose schema from the root once dragged Node's `events` into a client bundle
and killed both apps with `Class extends value undefined` — a stack trace naming
neither Mongoose nor the package.

## The rule

**A Gambit package may import another Gambit package, a node builtin, or an npm
dependency. Nothing else.**

That sounds obvious and is easy to break by accident. In the app this was
extracted from, the guard originally checked two *denylists* — don't import from
`models/`, don't import from `config/` — and a middleware sat for months
importing the app's own JWT module, because it was neither of those. It would
have surfaced only when the foundation package failed to extract.

The check is an allowlist now, derived from the list of packages itself, so
there is no second table to keep in sync.

### A clean import graph is not proof of portability

Learned the expensive way. A generic CRUD handler passed every static measure —
named no app concept, imported no app model, 109 lines of pure express and
mongoose. Repointing its JWT import from the app's module to the generic one
turned four authorization tests red, because the app's test harness *mocked*
that module and reaching around the mock made the handler really verify a fake
token.

So: **anything that needs to know who the caller is takes that knowledge as
config.** `createAuthenticate` takes a `verifyToken`. So does the authorization
middleware, and so does the CRUD layer. Three seams, one shape — and the host
app's mocks keep working, which is the part that catches this class of bug.

Run the consuming app's full suite after moving any import. Compare test
**counts**, not just pass/fail: a suite that fails to compile contributes zero
tests and hides in the totals rather than failing.

## Versioning

Packages are versioned independently and pinned by consumers. The point of the
whole exercise is that the core can change on its own branch without either app
moving until it chooses to.

Two traps, both of which have already cost something:

- **A caret on a `0.x` version admits PATCH ONLY.** `^0.1.0` will never install
  `0.2.0`. An app pinned that way does not fall behind loudly — it simply stays
  where it is, and the gap only shows up when someone compares two apps by hand.
- **`publish-all` skips a package whose version already exists**, which is
  correct (a published version is immutable) but means *forgetting a version
  bump looks exactly like a successful publish*. Bump in the same commit as the
  change.

---
_Source: https://npm.io/package/@guisao-llc/gambit-cascade · Machine-readable twin of the npm.io package page. Health data is recomputed on every publish._
