Repository navigation
feat(scim-sync): read the shared PostgreSQL through Hyperdrive #9715
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Changes from 4 commits
Commits
Show all changes
7 commits
Select commit
Hold shift + click to select a range
9d1d175
feat(scim-sync): read the shared PostgreSQL through Hyperdrive
zfralish e832af0
fix(scim-sync): bound connections, and stop guessing between ambiguou…
zfralish 4f8eb7d
refactor(scim-sync): drop the group queries
zfralish 7f06c3c
docs(scim-sync): record that a directory group maps to a sub-organiza…
zfralish ccf0341
fix(scim-sync): separate all sub-organizations from directory groups
zfralish 7356b15
docs(scim-sync): correct what an organization group is
zfralish 611e8e6
Merge branch 'main' into scim-db
zfralish File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
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
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
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
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
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,78 @@ | ||
| import { organization_membership_removals, organization_memberships } from '@kilocode/db/schema'; | ||
| import { and, eq } from 'drizzle-orm'; | ||
|
|
||
| import type { WorkerDb } from './pg'; | ||
|
|
||
| export type MembershipRow = typeof organization_memberships.$inferSelect; | ||
| export type MembershipRemovalRow = typeof organization_membership_removals.$inferSelect; | ||
|
|
||
| /** | ||
| * `organization_memberships` declares no foreign keys on either | ||
| * `organization_id` or `kilo_user_id`, so a row here does not prove the | ||
| * organization or the user exists, and neither is cleaned up when the other | ||
| * side goes away. Callers that need the referenced row must look it up. | ||
| * | ||
| * `(organization_id, kilo_user_id)` is unique, so this returns at most one row. | ||
| */ | ||
| export async function findMembership( | ||
| db: WorkerDb, | ||
| organizationId: string, | ||
| kiloUserId: string | ||
| ): Promise<MembershipRow | null> { | ||
| const rows = await db | ||
| .select() | ||
| .from(organization_memberships) | ||
| .where( | ||
| and( | ||
| eq(organization_memberships.organization_id, organizationId), | ||
| eq(organization_memberships.kilo_user_id, kiloUserId) | ||
| ) | ||
| ) | ||
| .limit(1); | ||
| return rows.at(0) ?? null; | ||
| } | ||
|
|
||
| export async function listOrganizationMemberships( | ||
| db: WorkerDb, | ||
| organizationId: string | ||
| ): Promise<MembershipRow[]> { | ||
| return db | ||
| .select() | ||
| .from(organization_memberships) | ||
| .where(eq(organization_memberships.organization_id, organizationId)); | ||
| } | ||
|
|
||
| export async function listUserMemberships( | ||
| db: WorkerDb, | ||
| kiloUserId: string | ||
| ): Promise<MembershipRow[]> { | ||
| return db | ||
| .select() | ||
| .from(organization_memberships) | ||
| .where(eq(organization_memberships.kilo_user_id, kiloUserId)); | ||
| } | ||
|
|
||
| /** | ||
| * Removal tombstone. A removed membership is hard-deleted and a row is written | ||
| * here instead, so the absence of a membership plus the presence of a tombstone | ||
| * is what distinguishes "deliberately removed" from "never joined". A tombstone | ||
| * is not cleared when the user rejoins, so its presence does not imply the | ||
| * membership is currently absent — check both. | ||
| */ | ||
| export async function findMembershipRemoval( | ||
| db: WorkerDb, | ||
| organizationId: string, | ||
| kiloUserId: string | ||
| ): Promise<MembershipRemovalRow | null> { | ||
| const rows = await db | ||
| .select() | ||
| .from(organization_membership_removals) | ||
| .where( | ||
| and( | ||
| eq(organization_membership_removals.organization_id, organizationId), | ||
| eq(organization_membership_removals.kilo_user_id, kiloUserId) | ||
| ) | ||
| ) | ||
| .limit(1); | ||
| return rows.at(0) ?? null; | ||
| } |
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,115 @@ | ||
| import { organizations } from '@kilocode/db/schema'; | ||
| import { and, count, eq, isNull } from 'drizzle-orm'; | ||
|
|
||
| import type { WorkerDb } from './pg'; | ||
|
|
||
| /** | ||
| * The WorkOS directory hierarchy maps onto the organization hierarchy: | ||
| * | ||
| * | WorkOS | Kilo | Binding | | ||
| * |---|---|---| | ||
| * | directory | parent organization | `directory_id` | | ||
| * | directory group | **sub-organization** | `directory_group_id`, `directory_idp_id` | | ||
| * | group membership | membership on that sub-organization | — | | ||
| * | ||
| * The binding columns live on every `organizations` row, parent or child, which | ||
| * is what makes a sub-organization the thing a directory group resolves to. | ||
| * Kilo's `organization_groups` table is unrelated: it holds capability policy | ||
| * sets and is not populated by directory sync. | ||
| */ | ||
|
|
||
| export type OrganizationRow = typeof organizations.$inferSelect; | ||
|
|
||
| /** | ||
| * Every query here filters `deleted_at IS NULL`. | ||
| * | ||
| * That is not cosmetic. The unique indexes on `directory_group_id` and | ||
| * `external_org_id` are partial, scoped to live rows, so a soft-deleted | ||
| * organization can hold the same binding as a live one. Dropping the filter | ||
| * makes those lookups non-deterministic. | ||
| */ | ||
| const isLive = isNull(organizations.deleted_at); | ||
|
|
||
| export async function findOrganizationById( | ||
| db: WorkerDb, | ||
| organizationId: string | ||
| ): Promise<OrganizationRow | null> { | ||
| const rows = await db | ||
| .select() | ||
| .from(organizations) | ||
| .where(and(eq(organizations.id, organizationId), isLive)) | ||
| .limit(1); | ||
| return rows.at(0) ?? null; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve the sub-organization a directory group is bound to. This is the | ||
| * central lookup for a `dsync.group.*` event. | ||
| */ | ||
| export async function findOrganizationByDirectoryGroupId( | ||
| db: WorkerDb, | ||
| directoryGroupId: string | ||
| ): Promise<OrganizationRow | null> { | ||
| const rows = await db | ||
| .select() | ||
| .from(organizations) | ||
| .where(and(eq(organizations.directory_group_id, directoryGroupId), isLive)) | ||
| .limit(1); | ||
| return rows.at(0) ?? null; | ||
| } | ||
|
|
||
| /** | ||
| * Resolve by identity-provider group ID, which survives a directory | ||
| * disconnect/reconnect that mints a new `directory_group_id`. | ||
| * | ||
| * Returns every match rather than one: this index is deliberately non-unique, | ||
| * because a reconnect can leave two live sub-organizations briefly sharing an | ||
| * IdP group while the new `directory_group_id` is adopted. | ||
| */ | ||
| export async function findOrganizationsByDirectoryIdpId( | ||
| db: WorkerDb, | ||
| directoryIdpId: string | ||
| ): Promise<OrganizationRow[]> { | ||
| return db | ||
| .select() | ||
| .from(organizations) | ||
| .where(and(eq(organizations.directory_idp_id, directoryIdpId), isLive)); | ||
| } | ||
|
|
||
| /** The organization's identifier in the customer's own system. */ | ||
| export async function findOrganizationByExternalOrgId( | ||
| db: WorkerDb, | ||
| externalOrgId: string | ||
| ): Promise<OrganizationRow | null> { | ||
| const rows = await db | ||
| .select() | ||
| .from(organizations) | ||
| .where(and(eq(organizations.external_org_id, externalOrgId), isLive)) | ||
| .limit(1); | ||
| return rows.at(0) ?? null; | ||
| } | ||
|
|
||
| /** | ||
| * The sub-organizations under a parent — which is also the set of directory | ||
| * groups bound to that directory, since a directory group maps to a | ||
| * sub-organization. | ||
| * | ||
| * The hierarchy is one level deep (a child cannot itself be a parent), so this | ||
| * is the whole subtree rather than just the first layer. Depth is enforced in | ||
| * application code, not by the schema. | ||
| */ | ||
| export async function findChildOrganizations( | ||
| db: WorkerDb, | ||
| parentOrganizationId: string | ||
| ): Promise<OrganizationRow[]> { | ||
| return db | ||
| .select() | ||
| .from(organizations) | ||
| .where(and(eq(organizations.parent_organization_id, parentOrganizationId), isLive)); | ||
| } | ||
|
|
||
| /** Reachability probe. Counts in SQL rather than materializing every row. */ | ||
| export async function countOrganizations(db: WorkerDb): Promise<number> { | ||
| const rows = await db.select({ total: count() }).from(organizations).where(isLive); | ||
| return rows.at(0)?.total ?? 0; | ||
| } | ||
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,31 @@ | ||
| import { getWorkerDb, type WorkerDb } from '@kilocode/db/client'; | ||
|
|
||
| /** | ||
| * Bounds both the statement and the connection. | ||
| * | ||
| * `statement_timeout` is sent in the PostgreSQL startup packet, so it cannot | ||
| * fire until a connection exists. A database or Hyperdrive that refuses or | ||
| * blackholes new connections would otherwise wait on `pg.Pool`'s default, which | ||
| * has no client-side connect timeout — the exact outage `/status` exists to | ||
| * report. Same reasoning as `services/ai-attribution/src/util/auth.ts`. | ||
| */ | ||
| const TIMEOUT_MS = 10_000; | ||
|
|
||
| /** | ||
| * Return a Drizzle client for the shared PostgreSQL reachable through the | ||
| * `HYPERDRIVE` binding. | ||
| * | ||
| * `getWorkerDb` opens a fresh `max: 1` pool per call — Hyperdrive owns the real | ||
| * pooling — so callers must invoke this per operation and never cache the | ||
| * result in module scope. A cached client outlives the request or Durable | ||
| * Object context that created it, and its I/O then fails in whatever context | ||
| * picks it up next. | ||
| */ | ||
| export function getPgDb(env: Pick<Env, 'HYPERDRIVE'>): WorkerDb { | ||
| return getWorkerDb(env.HYPERDRIVE.connectionString, { | ||
| connectionTimeoutMillis: TIMEOUT_MS, | ||
| statement_timeout: TIMEOUT_MS, | ||
| }); | ||
| } | ||
|
|
||
| export type { WorkerDb }; |
Oops, something went wrong.
Oops, something went wrong.
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.
Uh oh!
There was an error while loading. Please reload this page.