Replace the entity slugs on organizations, workspaces, resource pools, and plan ladders with nullable `key` columns and add keys to products, prices, and entitlement sets. Rename `providers.slug` to `provider` and add partial unique indexes for system and org role names. Assign invoice numbers per billing account from a gapless transactional counter; Stripe's number moves to the invoice mapping as an external reference. Seeds, fixtures, and the operator lookup address rows by key, and the returning-login resync no longer blanks a display name when the IdP sends no `name` claim.
121 lines
5.5 KiB
SQL
121 lines
5.5 KiB
SQL
-- name: CreateOrganization :one
|
|
-- Identity is org_id, presentation is name, and organization names are
|
|
-- deliberately not unique. `key` is the optional declarative address
|
|
-- (entity-keys §1 to §4): a caller outside the database -- a seed, a
|
|
-- configuration loader, an API client -- supplies it to name this row
|
|
-- literally. UI-created organizations leave it NULL, and a personal
|
|
-- organization never gets one: a key is never derived from a person's login
|
|
-- or display name (§4).
|
|
INSERT INTO core.organizations (name, org_type, owner_person_id, key)
|
|
VALUES ($1, $2, $3, sqlc.narg(key))
|
|
RETURNING *;
|
|
|
|
-- name: GetOrganizationByKey :one
|
|
-- Root-scoped key resolver (entity-keys §5): the query that makes `key` a
|
|
-- whole feature rather than a seed-only column. uq_organizations_key
|
|
-- guarantees at most one row matches; a NULL key never matches because the
|
|
-- parameter is compared with `=`.
|
|
SELECT * FROM core.organizations
|
|
WHERE key = $1;
|
|
|
|
-- name: GetOrganizationByID :one
|
|
SELECT * FROM core.organizations
|
|
WHERE org_id = $1;
|
|
|
|
-- name: GetOrganizationsByOwner :many
|
|
SELECT * FROM core.organizations
|
|
WHERE owner_person_id = $1
|
|
ORDER BY name;
|
|
|
|
-- name: ListOrganizations :many
|
|
SELECT * FROM core.organizations
|
|
ORDER BY name;
|
|
|
|
-- name: ListOrganizationsPage :many
|
|
-- Organizations list operator surface (operator-list-scale UX-4): the
|
|
-- paged, searched, org-type-filtered successor to ListOrganizationsWithOwner
|
|
-- -- each row still carries its owner's display name so no organization can
|
|
-- read as unowned (ux-honest-surfaces UX-16). owner_person_id is NOT NULL
|
|
-- (every organization has an owner person, the System tenant included --
|
|
-- 00001_init.sql), so the LEFT JOIN is defensive rather than expected to
|
|
-- ever drop the owner's name.
|
|
--
|
|
-- sqlc.narg(q): NULL matches every row; set, a case-insensitive substring
|
|
-- match against the organization's name, or its owner's display
|
|
-- name or primary email (operator-list-scale: "carries a scoped
|
|
-- server-side search" -- "organizations: name and owner name or
|
|
-- email").
|
|
-- sqlc.narg(org_type): NULL matches every org type; set, an exact facet
|
|
-- match (operator-list-scale: "Status filters exist ... organizations: org
|
|
-- type").
|
|
-- count(*) OVER() carries the true total for the filtered set alongside
|
|
-- the page's rows (design D2), so the stated total never reflects only the
|
|
-- page.
|
|
SELECT o.org_id, o.name, o.org_type, o.owner_person_id, o.status, o.created_at, o.updated_at,
|
|
p.display_name AS owner_display_name,
|
|
count(*) OVER() AS total_count
|
|
FROM core.organizations o
|
|
LEFT JOIN core.persons p ON p.person_id = o.owner_person_id
|
|
WHERE (sqlc.narg(q)::text IS NULL
|
|
OR o.name ILIKE '%' || sqlc.narg(q)::text || '%'
|
|
OR p.display_name ILIKE '%' || sqlc.narg(q)::text || '%'
|
|
OR p.primary_email ILIKE '%' || sqlc.narg(q)::text || '%')
|
|
AND (sqlc.narg(org_type)::text IS NULL OR o.org_type = sqlc.narg(org_type)::text)
|
|
ORDER BY o.name
|
|
LIMIT sqlc.arg(page_limit)::int OFFSET sqlc.arg(page_offset)::int;
|
|
|
|
-- name: SearchOrganizationsByName :many
|
|
-- Landing lookup's organization branch (entity-keys). Runs only
|
|
-- when no person matches the lookup term; a dedicated query rather than
|
|
-- ListOrganizationsPage because that query's owner-email term would blur
|
|
-- the person/organization split the lookup depends on. Active
|
|
-- organizations only, name substring match, bounded by LIMIT so a short
|
|
-- term cannot return the whole table.
|
|
SELECT o.org_id, o.name, o.org_type,
|
|
p.display_name AS owner_display_name
|
|
FROM core.organizations o
|
|
LEFT JOIN core.persons p ON p.person_id = o.owner_person_id
|
|
WHERE o.status = 'active'
|
|
AND o.name ILIKE '%' || sqlc.arg(name)::text || '%'
|
|
ORDER BY o.name
|
|
LIMIT sqlc.arg(row_limit)::int;
|
|
|
|
-- name: ListOrganizationsByType :many
|
|
SELECT * FROM core.organizations
|
|
WHERE org_type = $1
|
|
ORDER BY name;
|
|
|
|
-- name: CountTeamOrganizations :one
|
|
-- Operator overview headline. Personal orgs exist one-per-member by
|
|
-- structural convention (design/organization/architecture.md §2), so an
|
|
-- organizations count that includes them tracks the people count and reads
|
|
-- as an error. Negative-space predicate instead of org_type = 'team':
|
|
-- everything active that is neither the auto-created personal type nor a
|
|
-- reserved type counts, so team, enterprise, and deployment-defined types
|
|
-- need no code change here and the reserved system org never appears.
|
|
SELECT COUNT(*) FROM core.organizations o
|
|
JOIN core.org_types t ON t.org_type = o.org_type
|
|
WHERE o.status = 'active' AND o.org_type <> 'personal' AND NOT t.is_reserved;
|
|
|
|
-- name: EnsureSystemOrganization :exec
|
|
-- Idempotently insert the singleton System organization. The partial unique
|
|
-- index uq_organizations_one_system enforces "exactly one System tenant", and
|
|
-- the conflict target repeats the index predicate so Postgres can infer it.
|
|
-- Read back with GetSystemOrganization.
|
|
--
|
|
-- `system` is one of the two keys the system writes for itself (entity-keys
|
|
-- §4): a fixed constant for a row the console creates by design, not a value
|
|
-- derived from anybody's data. It is written here rather than by the Go
|
|
-- caller so the only path that creates the System tenant is also the only
|
|
-- path that names it.
|
|
INSERT INTO core.organizations (name, org_type, owner_person_id, key)
|
|
VALUES ($1, $2, $3, 'system')
|
|
ON CONFLICT (org_type) WHERE org_type = 'system' DO NOTHING;
|
|
|
|
-- name: GetSystemOrganization :one
|
|
-- The singleton System organization, resolved by its natural key. Paired
|
|
-- with EnsureSystemOrganization; uq_organizations_one_system guarantees at
|
|
-- most one row matches.
|
|
SELECT * FROM core.organizations
|
|
WHERE org_type = 'system';
|