Skip to content

feat: normalise PostgreSQL repo permissions into a repo_users join table - #1590

Open
dcoric wants to merge 22 commits into
finos:feat/postgresfrom
dcoric:feat/postgres-repo-users
Open

feat: normalise PostgreSQL repo permissions into a repo_users join table#1590
dcoric wants to merge 22 commits into
finos:feat/postgresfrom
dcoric:feat/postgres-repo-users

Conversation

@dcoric

@dcoric dcoric commented Jun 12, 2026

Copy link
Copy Markdown
Contributor

Description

Draft, stacked on #1532 (PostgreSQL sink) and #1581 (schema-migration runner). The diff below currently includes those changes. Once they merge, this branch will be rebased onto main so only the repo_users changes remain, and the draft flag will be removed.

Normalises PostgreSQL repo permissions out of the repos.users JSONB column into a dedicated repo_users(repo_id, username, role) join table, the follow-up to the v1 JSONB parity shipped in #1532.

  • Migration v3 (repo_users_table) creates repo_users with a composite primary key (repo_id, username, role), a CHECK (role IN ('canPush','canAuthorise')) constraint, a repo_id index, and ON DELETE CASCADE to repos. It backfills the table from the existing JSONB permissions in the same step.
  • Migration v4 (drop_repos_users_jsonb) drops the now-unused repos.users column.
  • The repo adapter (src/db/postgres/repo.ts) reads permissions via a single LEFT JOIN + array_agg(...) FILTER (...) (no N+1) and reconstructs the { canPush, canAuthorise } shape callers expect; writes go through idempotent INSERT ... ON CONFLICT DO NOTHING / DELETE.
  • Unit tests cover the read/aggregate, create, add/remove and delete-cascade paths. Integration tests exercise them against a real PostgreSQL, including an end-to-end test of the JSONB to repo_users backfill across single-user, multi-user and same-user-in-both-roles repos.

Depends on

This relies on #1558 / #1581 (the versioned schema-migration runner): the backfill is delivered as migration v3 and needs the runner to apply it. It should merge after #1532 and #1581.

Known follow-ups (non-blocking)

  • createRepo's optional create-time permission inserts are not wrapped in a single transaction. Impact is low because the public createRepo wrapper always passes empty permission arrays, so the loop is not exercised in the normal app flow; a follow-up can run it on one client inside BEGIN/COMMIT.
  • Postgres adopts the file backend's stricter semantics over mongo's in two edges: adding a user to a non-existent repo raises a foreign-key violation (mongo silently no-ops), and permission arrays are de-duplicated (mongo's $push can store duplicates). Both are functionally invisible to the .includes()-based consumers.

Related Issue

Resolves #1559

Checklist

General

Documentation

  • Documentation has been added/updated for any new features

Configuration

  • If configuration schema (config.schema.json) was modified: (not modified in this PR)

Tests

  • Tests have been added/updated for new functionality
  • Unit tests pass (npm test)
  • Linting and formatting pass (npm run lint and npm run format:check)
  • Type checks pass (npm run check-types)

dcoric added 8 commits June 10, 2026 14:30
…o-users

Resolutions: the user public_keys / optional email migration takes
version 2 (it ships first with the schema-migrations PR), so the
repo_users backfill moves to version 3 and the JSONB column drop to
version 4; version assertions and table lists updated accordingly.
repo.ts keeps the repo_users implementation and drops the remaining
issue reference in line with the review feedback on finos#1532.
The migration integration suite reset to an empty database before every
run, so migration v3's INSERT...SELECT backfill always executed over zero
rows and the JSONB-to-repo_users data migration was never observed. Add a
test that stages the pre-v3 schema (v1+v2 applied by hand, legacy `users`
JSONB column present), seeds repos with single-user, multi-user and
same-user-in-both-roles permissions, runs the remaining migrations, and
asserts every entry lands in repo_users and the legacy column is dropped.

Also correct an off-by-one comment: the legacy JSONB column is dropped by
migration v4, not v3.
@netlify

netlify Bot commented Jun 12, 2026

Copy link
Copy Markdown

Deploy Preview for endearing-brigadeiros-63f9d0 ready!

Name Link
🔨 Latest commit 1ea7ecc
🔍 Latest deploy log https://app.netlify.com/projects/endearing-brigadeiros-63f9d0/deploys/6a6730a8e108b10008852b0d
😎 Deploy Preview https://deploy-preview-1590.git-proxy.preview.finos.org
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@codecov

codecov Bot commented Jun 12, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 93.16547% with 38 lines in your changes missing coverage. Please review.
✅ Project coverage is 90.58%. Comparing base (b44f5ee) to head (1ea7ecc).
⚠️ Report is 190 commits behind head on feat/postgres.

Files with missing lines Patch % Lines
src/db/postgres/users.ts 92.98% 12 Missing ⚠️
src/service/index.ts 33.33% 8 Missing ⚠️
src/db/postgres/repo.ts 93.81% 4 Missing and 2 partials ⚠️
src/db/postgres/helper.ts 93.05% 5 Missing ⚠️
src/db/index.ts 42.85% 4 Missing ⚠️
src/db/postgres/pushes.ts 97.14% 3 Missing ⚠️
Additional details and impacted files
@@                Coverage Diff                @@
##           feat/postgres    #1590      +/-   ##
=================================================
+ Coverage          90.41%   90.58%   +0.16%     
=================================================
  Files                 87       93       +6     
  Lines               8661     9215     +554     
  Branches            1603     1711     +108     
=================================================
+ Hits                7831     8347     +516     
- Misses               799      835      +36     
- Partials              31       33       +2     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

…o-users

The schema migrations branch renamed the postgres DDL runner to
schemaMigrations.ts so it no longer collides with the Sink migration hooks
main expects in migrations.ts. The repo_users table and the drop of the
legacy JSONB column move with it, renumbered as versions 4 and 5 to sit
after the framework bookkeeping table.

Also implements updateRepo, which main added to the Sink interface. Since
permissions live in repo_users rather than a column on repos, a supplied
users object replaces that repo's rows instead of overwriting JSONB.
dcoric added 3 commits August 24, 2026 10:47
…o-users

Carries the repo dateCreated/lastModified parity work onto the
repo_users variant of the adapter: the join query returns the date
columns, createRepo stamps them, updateRepo maps them, and every
permission change bumps last_modified on repos with a separate update,
matching the mongo and fs backends.
@dcoric
dcoric marked this pull request as ready for review August 24, 2026 10:21
@dcoric
dcoric requested a review from a team as a code owner August 24, 2026 10:21

@jescalada jescalada left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few comments on this one 👍🏼

-- legacy repos.users column is dropped in a later migration once the adapter
-- reads and writes repo_users instead.
INSERT INTO repo_users (repo_id, username, role)
SELECT r._id, elem.username, 'canPush'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Wondering if it's okay to select the usernames directly rather than the lowercased version here and a few lines below? Notice how in addUserToRole and removeUserFromRole we're lowercasing them:

const addUserToRole = async (
  _id: string,
  user: string,
  role: 'canPush' | 'canAuthorise',
): Promise<void> => {
  await query(
    `INSERT INTO repo_users (repo_id, username, role)
     VALUES ($1, $2, $3)
     ON CONFLICT DO NOTHING`,
    [_id, user.toLowerCase(), role],
  );
  await query(`UPDATE repos SET last_modified = $2 WHERE _id = $1`, [
    _id,
    new Date().toISOString(),
  ]);
};

We might also want a few unit tests to cover this specifically so that previously added user entries are still retrievable

ON CONFLICT DO NOTHING;

INSERT INTO repo_users (repo_id, username, role)
SELECT r._id, elem.username, 'canAuthorise'

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same as above

Comment thread src/db/postgres/repo.ts
if (users !== undefined) {
await query(`DELETE FROM repo_users WHERE repo_id = $1`, [_id]);
for (const username of users.canPush ?? []) {
await addUserToRole(_id, username, 'canPush');

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we be handling errors in these? There are a few operations happening in updateRepo - we should do something to prevent invalid or half-filled entries (repos added successfully, but without the user roles assigned) in case there's some unexpected error (disconnection/network glitch, etc.)

Do we have anything to ensure rollbacks happen on errors?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

PostgreSQL sink: normalise repo permissions into a repo_users join table

2 participants