No description
  • TypeScript 55.9%
  • Shell 31.7%
  • JavaScript 12.4%
Find a file
LordAndradus d1fb568cbb Infra: deploy.sh targets official/ (the compose per-plugin ro-mount) - the shared ./plugins path is shadowed for this plugin
Owner call: the steamstore repo's deploy.sh is the origin tool and stays untouched; README deploy section aligned
2026-07-04 03:32:33 -05:00
.gitattributes Initial commit: mapping-report GameVault backend plugin 2026-07-03 22:45:23 -05:00
.gitignore Initial commit: mapping-report GameVault backend plugin 2026-07-03 22:45:23 -05:00
deploy.sh Infra: deploy.sh targets official/ (the compose per-plugin ro-mount) - the shared ./plugins path is shadowed for this plugin 2026-07-04 03:32:33 -05:00
gamevault.d.ts Initial commit: mapping-report GameVault backend plugin 2026-07-03 22:45:23 -05:00
mapping-report.controller.ts Bugfix: sentinel-aware unmapped query - a steam-unfiltered 'unmapped-<id>' no-match sentinel row is NOT a mapping 2026-07-04 03:30:50 -05:00
mapping-report.plugin.module.ts Initial commit: mapping-report GameVault backend plugin 2026-07-03 22:45:23 -05:00
package-lock.json Initial commit: mapping-report GameVault backend plugin 2026-07-03 22:45:23 -05:00
package.json Initial commit: mapping-report GameVault backend plugin 2026-07-03 22:45:23 -05:00
postbuild.js Initial commit: mapping-report GameVault backend plugin 2026-07-03 22:45:23 -05:00
README.md Infra: deploy.sh targets official/ (the compose per-plugin ro-mount) - the shared ./plugins path is shadowed for this plugin 2026-07-04 03:32:33 -05:00
tsconfig.json Initial commit: mapping-report GameVault backend plugin 2026-07-03 22:45:23 -05:00

Mapping Report — GameVault / LudoNexus Backend Plugin

A minimal, routes-only, read-only NestJS plugin for the GameVault backend (built against v16.3.0). It adds a single endpoint that returns the IDs of games which have zero REAL provider-metadata mappings — i.e. games that no metadata provider (IGDB, Steam Unfiltered, …) has successfully matched. "Real" is load-bearing: the Steam Unfiltered provider ships a no-match SENTINEL junction row (provider_data_id = "unmapped-<gameId>", priority -1) instead of throwing, so a sentinel-only game has a junction row yet is not mapped and must appear in this report. (recovered-<appid> rows are the delisted-game recovery family — those are real mappings and do NOT appear.)

It exists to back the LudoNexus client's "Mapped Games" filter — the provider-backed upgrade tracked as backlog item 999.14. The client can call this endpoint to know exactly which library items are still orphaned and surface them for review, instead of guessing client-side.

What this plugin is NOT. It registers no metadata provider, patches no global prototypes, calls no external APIs, ships no configuration, and has no self-updater. It contributes exactly one guarded GET route and nothing else. Zero third-party runtime dependencies — every module it imports (@nestjs/common, @nestjs/typeorm, typeorm, @nestjs/swagger) is provided by the backend at runtime, so dist/ drops straight into /plugins with no bundled node_modules.

Endpoint contract

GET /api/plugins/mapping/unmapped

(The backend adds the global /api prefix; the controller itself declares plugins/mapping + unmapped.)

Auth: requires the EDITOR role (@MinimumRole(Role.EDITOR)). The backend's auth guards are registered globally as APP_GUARDs, so a valid bearer token or API key plus the EDITOR role is all that's required — the plugin adds no guard of its own and never skips them.

Response (200 OK), snake_case to match the backend API convention:

{
  "unmapped_game_ids": [12, 87, 340],   // number[] — ids of unmapped games
  "count": 3,                            // number   — unmapped_game_ids.length
  "generated_at": "2026-07-03T12:34:56.789Z" // string — ISO-8601 timestamp
}

The query

SELECT g.id
FROM gamevault_game g
WHERE g.deleted_at IS NULL
  AND NOT EXISTS (
    SELECT 1
    FROM gamevault_game_provider_metadata_game_metadata j
    JOIN game_metadata m ON m.id = j.game_metadata_id
    WHERE j.gamevault_game_id = g.id
      AND m.deleted_at IS NULL
      AND (m.provider_data_id IS NULL
           OR m.provider_data_id NOT LIKE 'unmapped-%')
  );

Issued via repository.createQueryBuilder(...).getRawMany(). A game is unmapped when it has no junction row to a live, non-sentinel metadata row. Bare junction-existence is NOT enough — that was the original query, and it classified sentinel-only games as mapped, blanking the client's Unmapped work queue (falsified live against the dev mirror, 2026-07-04). The IS NULL arm keeps a (malformed) null-provider_data_id row counting as mapped — SQL's NULL NOT LIKE is NULL, which would silently flip such rows — matching the client's Game.HasRealProviderMapping rule exactly: only the sentinel prefix changes classification. Both soft-delete filters are explicit and required. Postgres returns integer columns as strings, so ids are coerced with Number().

Acceptance check (this plugin has no test harness — this is the executable pin; run it against a mirror DB whenever the query changes): on the dev mirror's spicy DB (24 games, two of them sentinel-only) the query returns exactly those two ids [9, 19]; on the game DB (10 games, all real appids) it returns [].

Build

npm install
npm run build      # tsc -> dist/, then postbuild.js rewrites the src/... requires

npm run build runs tsc then postbuild.js. tsc type-checks against the ambient src/... stubs in gamevault.d.ts; postbuild.js rewrites the emitted require("src/...") specifiers to the relative paths that resolve at runtime inside the backend:

compile-time specifier rewritten to resolves at runtime to
src/globals ../../globals dist/src/globals
src/modules/games/... ../games/... dist/src/modules/games/...
src/modules/users/... ../users/... dist/src/modules/users/...
src/decorators/... ../../decorators/... dist/src/decorators/...

The compiled entrypoint is dist/mapping-report.plugin.module.js — its .plugin.module.js suffix is what the backend's plugin loader globs for.

Deploy

Deploy to the WSL/docker mirror stack FIRST and verify boot before touching production. The backend plugin loader is all-or-nothing: it import()s every *.plugin.module.js under /plugins in one Promise.all, and one plugin file that fails to load throws the whole batch — which would take down every plugin, including the Steam Unfiltered metadata provider. A broken drop here silently disables Steam metadata on the whole server. Prove it boots on the mirror first.

On the mirror stack this plugin is bind-mounted per-plugin from:

$HOME/GameVault/official/ludonexus-plugin-mapping-report/   <- dist/* contents here

(the compose override mounts that dir read-only at /plugins/ludonexus-plugin-mapping-report, shadowing the shared ./plugins dir for this plugin — see the gotcha below). In-container it still sits one directory deep under /plugins, which the rewritten ../ / ../../globals requires need.

deploy.sh automates the mirror deploy (build → staged-swap into official/docker compose up -d --force-recreate --no-deps game spicy). Run it manually from WSL:

./deploy.sh            # build + hot-redeploy into the shared ./plugins, reload backends
./deploy.sh --fresh    # additionally wipe the disposable 'spicy' database first

Or deploy by hand: copy the contents of dist/ into .../plugins/ludonexus-plugin-mapping-report/ and restart the backend containers so each re-copies /plugins on boot.

⚠️ Mount-shadow gotcha (cost a debugging round on 2026-07-04): if the compose file bind-mounts a per-plugin override — e.g. ~/GameVault/official/ludonexus-plugin-mapping-report → /plugins/ludonexus-plugin-mapping-report (ro) — that mount shadows the shared ./plugins dir for this plugin, and deploy.sh's target is silently ignored: the backend keeps running the old build while the shared dir holds the new one. After ANY deploy, verify the code the container actually runs, not the host dir: docker exec <backend> grep -c 'NOT LIKE' /app/dist/src/modules/ludonexus-plugin-mapping-report/mapping-report.controller.js (must be ≥1 on the sentinel-aware build). deploy.sh targets official/ since 2026-07-04 for exactly this reason; any copy left under the shared ./plugins/ludonexus-plugin-mapping-report/ is shadowed dead weight — safe to delete, and worth deleting so a future mount change can't resurrect a stale build.

Client consumer

LudoNexus app — the "Mapped Games" filter provider-backed upgrade (backlog 999.14). The client hits GET /api/plugins/mapping/unmapped to get the authoritative set of unmapped game ids and drives the filter / review UI from it, rather than inferring mapping state client-side.