🏗️ PUT-1703: TeamStore — membership reads and writes (single writer) - #3710
Merged
Conversation
Contributor
Coverage Report
File Coverage
|
||||||||||||||||||||||||||||||||||||||||||||
This was referenced Sep 1, 2026
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 1, 2026 20:09
5629ae7 to
7d8050a
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 1, 2026 21:36
7d8050a to
8275ec2
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 1, 2026 22:04
8275ec2 to
7b1a1ba
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 1, 2026 22:50
7b1a1ba to
1e86d0e
Compare
This was referenced Sep 2, 2026
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 2, 2026 16:05
1e86d0e to
1ac35af
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 2, 2026 16:51
1ac35af to
ed582d0
Compare
This was referenced Sep 2, 2026
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 2, 2026 19:32
ed582d0 to
00f6d00
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 2, 2026 19:39
00f6d00 to
c24b424
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 2, 2026 21:37
c24b424 to
0cbd86b
Compare
Salazareo
approved these changes
Sep 3, 2026
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 3, 2026 13:46
0cbd86b to
5573c92
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 3, 2026 14:30
5573c92 to
21b8b8d
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 3, 2026 15:42
21b8b8d to
e97dbda
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 3, 2026 18:56
e97dbda to
eeb4d4c
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 3, 2026 19:07
eeb4d4c to
c5b7c99
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 3, 2026 19:17
c5b7c99 to
d82c8a7
Compare
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 3, 2026 19:20
d82c8a7 to
3e84e87
Compare
Collaborator
Author
Local validationEngine: mysql 8 · workspace with 4 members (1 owner + 3 provisioned seats) Full listing …before seats existed, and after provisioning, paginated below. Keyset pagination, page 1 ( {"items":[{"username":"tmowner", "org_owned":false,"created_at":"2026-09-03T23:25:43.000Z"},
{"username":"acmeseat1","org_owned":true, "created_at":"2026-09-03T23:27:28.000Z"}],
"cursor":"eyJpZCI6ODh9"}Page 2 ( {"items":[{"username":"acmeseat2","org_owned":true,"created_at":"2026-09-03T23:27:29.000Z"},
{"username":"acmeseat3","org_owned":true,"created_at":"2026-09-03T23:27:29.000Z"}]}Three properties worth calling out, all matching
|
Membership management for workspaces: addMember, removeMember, getMembership, isMember, listMembers and listTeamsForUser. The permission scan is untouched -- readUserGroupPerms already joins jct_user_group and resolves group grants; this is the management side. Resolves the ticket's "do not leave two writers" by splitting domains and enforcing the split in SQL rather than by convention. Every existing caller of GroupStore targets a seeded system group -- ADMIN_GROUP_UID, default_user_group, default_temp_group -- never a team, so the two stores were already disjoint in practice. GroupStore.addUsers/removeUsers now carry `AND kind IS NULL`, making a team uid a no-op there, which costs no extra query because it folds into the existing subquery and matches how addUsers already treats an unknown username. TeamStore's writes select group_id from a kind-filtered subquery, so neither store can reach the other's rows. org_owned is written here but never accepted from a request; TeamService sets it at provisioning and workspace creation only. listMembers is keyset-paginated on id per doc/pagination.md, using the shared cursor and limit helpers and fetching one row past the limit to decide whether a cursor is warranted. Passes 1/0 for org_owned rather than db.booleanValue, which yields a real boolean on postgres and is rejected by the smallint column there -- sqlite accepted it silently.
jfcastro92
force-pushed
the
juancastro/put-1703-22-teamstore-membership-reads-and-writes-single-writer
branch
from
September 3, 2026 20:02
3e84e87 to
8b4e259
Compare
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.
The management side of workspace membership.
readUserGroupPermsalready joinsjct_user_groupand resolves group grants for the permission scan — that path is untouched.The "single writer" decision
The ticket asked to decide whether
TeamStoresupersedesGroupStore.addUsers/removeUsers, and not to leave two writers.First, what the callers actually do. All five production call sites target a seeded system group:
AuthControllersignup ×2, save-account ×1default_user_group,default_temp_groupOIDCServicefirst logindefault_user_groupDefaultUserServicebootstrapADMIN_GROUP_UIDNever a team — those uids come from config and constants. So the two stores were already disjoint in practice, and the ticket's stated fear ("the one without the conflict clause will eventually be the one that is called") was defused by #3705 making
addUsersconflict-tolerant.So rather than delete
GroupStoreand churn the signup and OIDC paths inside a teams PR, the split is now enforced in SQL:TeamStorewrites selectgroup_idfrom akind = 'team'subquery.GroupStore.addUsers/removeUserscarryAND kind IS NULL.Neither store can reach the other's rows. That costs zero extra queries — it folds into subqueries both methods already had — and a team uid becomes a no-op, which is how
addUsersalready treats an unknown username (there's a pre-existing test for that). Two new tests on theGroupStoreside assert it cannot touch a team.⚠ If you'd rather have one literal writer and accept the blast radius, that's a
GroupStoredeletion plus five call-site rewrites through signup, OIDC and the self-hosted bootstrap. I judged that too invasive to smuggle into this PR, but say so and I'll do it separately.A cross-dialect bug worth seeing
addMemberfirst useddb.booleanValue(orgOwned). That looked right and passed on sqlite — 27/27 green.PostgresDatabaseClientoverridesbooleanValueto return a real boolean, because postgres has a realbooleantype. Butorg_ownedis an integer flag in all three dialects (tinyint(1)/INTEGER/smallint), so postgres rejected it outright:SQLite is loosely typed and stored
truewithout complaint. Now passing1/0explicitly, with a comment saying why so nobody "tidies" it back. This only surfaced because both engines were run.Other details
org_ownedis written here but never accepted from a request —TeamServicesets it at provisioning and workspace creation. It decides who pays, not who may read, and a test asserts the workspace owner's row carries0while a provisioned account carries1.listMembersis keyset-paginated onidperdoc/pagination.md, using the sharedencodeCursor/decodeCursor/normalizeLimithelpers and fetching one row past the limit to decide whether a cursor is warranted.Reads inherit the
kind = 'team' AND deleted_at IS NULLpredicate, so soft-deleting a workspace drops it out oflistTeamsForUserandisMemberwithout touching the membership rows.Verification
Both engines
Eleven tests added — membership round-trip,
org_owneddistinguishing the workspace owner, repeat-add being a no-op, removal reporting whether a row existed, workspace scoping, soft-delete dropping out of the user's list,addMemberrefusing a non-team group, and two on theGroupStoreside asserting the boundary.Two pagination tests: a 5-member workspace walked in pages of 2 terminates in exactly 3 pages with every member seen once, and
limit: 100_000is capped at 200 rather than trusted.Every write to `jct_user_group` in the codebase, after this change
Two stores, disjoint by predicate, one writer per domain.
Full suite and typecheck
Closes PUT-1703.