Original task (#91): The `app_permissions` join table had no unique
constraint, allowing the same `(app_id, permission_id)` pair to be
inserted repeatedly. The Admin Panel restriction had grown to 24
duplicate rows. This change prevents that recurring at the DB level
and verifies that the existing conflict-safe insert path keeps working.
Schema change:
- `lib/db/src/schema/apps.ts`: added a composite primary key on
`(app_id, permission_id)` for `appPermissionsTable`, matching the
pattern used by other join tables in this repo (rolePermissions,
userRoles, groupApps, etc.). This is enforced at the database level.
DB migration:
- Cleaned up the 23 duplicate rows still present in the DB before
applying the constraint (kept the earliest row per pair using
`ctid`), then ran `pnpm --filter @workspace/db run push` to sync the
schema. Verified the new primary key
`app_permissions_app_id_permission_id_pk` rejects duplicate inserts.
Graceful handling of duplicates:
- The seed script (`scripts/src/seed.ts`) already uses
`.onConflictDoNothing()` when inserting into `app_permissions`. With
the new primary key, that call is now properly idempotent (no longer
silently allowing duplicates).
- There is currently no HTTP route or admin UI that POSTs into
`app_permissions` (the task description listed `routes/apps.ts` as a
relevant file, but no such handler exists today). The constraint
itself is what prevents future regressions, and any future endpoint
should follow the seed's `.onConflictDoNothing()` pattern.
Tests:
- Added `artifacts/api-server/tests/app-permissions-unique.test.mjs`
with two cases that prove the new behavior:
1. A plain duplicate INSERT fails with SQLSTATE 23505 (unique
violation) and only one row remains.
2. `INSERT ... ON CONFLICT DO NOTHING` (the pattern Drizzle's
`.onConflictDoNothing()` emits) handles duplicates gracefully:
no error thrown, `rowCount` is 0, and exactly one row remains.
- All previously-passing api-server tests for apps/groups still pass
after the schema change.
Replit-Task-Id: 0589a4dc-5898-4c66-8feb-3cd48289fe89