auth/invite_queries.ts

Invite database queries.

CRUD operations for the invite table — creating invites, finding unclaimed matches, claiming, and cleanup.

view source

Declarations
#

9 declarations

INVITE_COLUMNS
#

auth/invite_queries.ts view source

readonly ["id", "email", "username", "claimed_by", "claimed_at", "created_at", "created_by"] import {INVITE_COLUMNS} from '@fuzdev/fuz_app/auth/invite_queries.js';

The full invite column set, named explicitly so a row read fails loud on schema drift — invite rows ride the invite_create / invite_list RPC responses raw, so a SELECT * would silently carry a dropped or leftover column into the strict-validated wire shapes (see ACCOUNT_COLUMNS in auth/account_queries.ts for the outage class; the Rust twin names columns at every invite site). Keep in sync with Invite and the migration chain's end state.

query_create_invite
#

auth/invite_queries.ts view source

(deps: QueryDeps, input: CreateInviteInput): Promise<Invite> import {query_create_invite} from '@fuzdev/fuz_app/auth/invite_queries.js';

Create a new invite.

deps

query dependencies

input

the invite fields

returns

Promise<Invite>

the created invite

query_invite_claim_unscoped
#

auth/invite_queries.ts view source

(deps: QueryDeps, invite_id: string, account_id: string): Promise<boolean> import {query_invite_claim_unscoped} from '@fuzdev/fuz_app/auth/invite_queries.js';

Claim an invite by setting the claimed_by and claimed_at fields.

The _unscoped suffix is the safety signal — the SQL only checks the row state (claimed_at IS NULL), not whether the claiming account's email or username matches the invite. Callers must scope the lookup upstream via one of the _find_unclaimed_match* siblings (production uses _for_update to make find + claim atomic). Skipping the find step lets a caller claim any unclaimed invite by id.

Mirrors the query_session_revoke_by_hash_unscoped precedent — there is no scoped sibling because the scoping is provided by a separate find query, not by an alternate variant of this query.

deps

query dependencies

invite_id

the invite to claim

type string

account_id

the account claiming the invite

type string

returns

Promise<boolean>

true if the invite was claimed, false if already claimed or not found

query_invite_delete_unclaimed
#

auth/invite_queries.ts view source

(deps: QueryDeps, id: string): Promise<boolean> import {query_invite_delete_unclaimed} from '@fuzdev/fuz_app/auth/invite_queries.js';

Delete an unclaimed invite.

deps

query dependencies

id

the invite id

type string

returns

Promise<boolean>

true if deleted, false if not found or already claimed

query_invite_find_unclaimed_by_email
#

auth/invite_queries.ts view source

(deps: QueryDeps, email: string): Promise<Invite | undefined> import {query_invite_find_unclaimed_by_email} from '@fuzdev/fuz_app/auth/invite_queries.js';

Find an unclaimed invite by email (case-insensitive).

deps

email

type string

returns

Promise<Invite | undefined>

query_invite_find_unclaimed_by_username
#

auth/invite_queries.ts view source

(deps: QueryDeps, username: string): Promise<Invite | undefined> import {query_invite_find_unclaimed_by_username} from '@fuzdev/fuz_app/auth/invite_queries.js';

Find an unclaimed invite by username (case-insensitive).

deps

username

type string

returns

Promise<Invite | undefined>

query_invite_find_unclaimed_match_for_update
#

auth/invite_queries.ts view source

(deps: QueryDeps, email: string | null, username: string): Promise<Invite | undefined> import {query_invite_find_unclaimed_match_for_update} from '@fuzdev/fuz_app/auth/invite_queries.js';

Find an unclaimed invite matching email and/or username, taking a row-level write lock on the matched row.

Three scoping modes:

  • Email-only invite (email set, username NULL) → matches only if signup provides matching email.
  • Username-only invite (username set, email NULL) → matches only if signup provides matching username.
  • Both-field invite (both set) → requires BOTH email and username to match.

Must run inside the same transaction as query_invite_claim_unscoped: FOR UPDATE makes find + claim atomic, so a concurrent signup that matched the same invite blocks on the lock until this transaction commits/rolls back. After commit, the loser's find_for_update returns no row (the winner flipped claimed_at) and falls through to ERROR_NO_MATCHING_INVITE — no race window between find and claim.

deps

query dependencies — deps.db MUST be a transaction

email

email to match (or null if signup provides none)

type string | null

username

username to match

type string

returns

Promise<Invite | undefined>

the matching invite (locked), or undefined

query_invite_list_all
#

auth/invite_queries.ts view source

(deps: QueryDeps): Promise<Invite[]> import {query_invite_list_all} from '@fuzdev/fuz_app/auth/invite_queries.js';

List all invites, newest first.

deps

returns

Promise<Invite[]>

query_invite_list_all_with_usernames
#

auth/invite_queries.ts view source

(deps: QueryDeps): Promise<{ id: string & $brand<"Uuid">; email: string | null; username: string | null; claimed_by: (string & $brand<"Uuid">) | null; ... 4 more ...; claimed_by_username: string | null; }[]> import {query_invite_list_all_with_usernames} from '@fuzdev/fuz_app/auth/invite_queries.js';

List all invites with resolved creator/claimer usernames, newest first.

deps

query dependencies

returns

Promise<{ id: string & $brand<"Uuid">; email: string | null; username: string | null; claimed_by: (string & $brand<"Uuid">) | null; claimed_at: string | null; created_at: string; created_by: (string & $brand<...>) | null; created_by_username: string | null; claimed_by_username: string | null; }[]>

invites with created_by_username and claimed_by_username

Depends on
#

Imported by
#