-
Notifications
You must be signed in to change notification settings - Fork 15
Refactor Find Domains into Layers #1680
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
+661
−461
Merged
Changes from all commits
Commits
Show all changes
11 commits
Select commit
Hold shift + click to select a range
95635fd
refactor findDomains into composable layers
shrugs c224477
fix: rename canonical registries CTE column to avoid ambiguity
shrugs c8b65ed
feat: changeset
shrugs f02c90c
add comment about registriescte select column name
shrugs 11d9564
Merge branch 'main' into feat/refactor-find-domains-availability
shrugs 689000c
fix: correctly reverse-traverse to establish v2domain's parentId
shrugs b702009
fix: rename headLabel to sortableLabel within the consumer context
shrugs 11cfd42
fix: refactor/reorganize some files for clarity
shrugs 077f320
Merge branch 'main' into feat/refactor-find-domains-availability
shrugs c6f2291
refactor: continue refactoring find-domains for clarity
shrugs 388c9bd
refactor: move some stuff from find-domains/types into locally-scoped…
shrugs 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
Some comments aren't visible on the classic Files Changed page.
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
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,21 @@ | ||
| --- | ||
| "ensapi": patch | ||
| --- | ||
|
|
||
| add `Account.domains` and enhance `Domain.subdomains` and `Registry.domains` with filtering and ordering | ||
|
|
||
| **`Account.domains`** (new) — paginated connection of domains owned by this account. | ||
| - `where: { name?: String, canonical?: Boolean }` — optional partial Interpreted Name filter and canonical filter (defaults to false) | ||
| - `order: { by: NAME | REGISTRATION_TIMESTAMP | REGISTRATION_EXPIRY, dir: ASC | DESC }` — ordering | ||
|
|
||
| **`Domain.subdomains`** (enhanced) — paginated connection of subdomains of this domain, now with filtering and ordering. | ||
| - `where: { name?: String }` — optional partial Interpreted Name filter | ||
| - `order: { by: NAME | REGISTRATION_TIMESTAMP | REGISTRATION_EXPIRY, dir: ASC | DESC }` — ordering | ||
|
|
||
| **`Registry.domains`** (enhanced) — paginated connection of domains in this registry, now with filtering and ordering. | ||
| - `where: { name?: String }` — optional partial Interpreted Name filter | ||
| - `order: { by: NAME | REGISTRATION_TIMESTAMP | REGISTRATION_EXPIRY, dir: ASC | DESC }` — ordering | ||
|
|
||
| **`Query.domains`** (updated) — `where.name` is now required. Added optional `where.canonical` filter (defaults to false). | ||
| - `where: { name: String!, canonical?: Boolean }` — required partial Interpreted Name, optional canonical filter | ||
| - `order: { by: NAME | REGISTRATION_TIMESTAMP | REGISTRATION_EXPIRY, dir: ASC | DESC }` — ordering | ||
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
123 changes: 123 additions & 0 deletions
123
apps/ensapi/src/graphql-api/lib/find-domains/find-domains-resolver-helpers.ts
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,123 @@ | ||
| import { asc, desc, type SQL, sql } from "drizzle-orm"; | ||
|
|
||
| import type { DomainCursor } from "@/graphql-api/lib/find-domains/domain-cursor"; | ||
| import type { DomainsWithOrderingMetadata } from "@/graphql-api/lib/find-domains/layers/with-ordering-metadata"; | ||
| import type { DomainsOrderBy } from "@/graphql-api/schema/domain"; | ||
| import type { OrderDirection } from "@/graphql-api/schema/order-direction"; | ||
|
|
||
| /** | ||
| * Get the order column for a given DomainsOrderBy value. | ||
| */ | ||
| function getOrderColumn( | ||
| domains: DomainsWithOrderingMetadata, | ||
| orderBy: typeof DomainsOrderBy.$inferType, | ||
| ) { | ||
| return { | ||
| NAME: domains.sortableLabel, | ||
| REGISTRATION_TIMESTAMP: domains.registrationTimestamp, | ||
| REGISTRATION_EXPIRY: domains.registrationExpiry, | ||
| }[orderBy]; | ||
| } | ||
|
|
||
| /** | ||
| * Build a cursor filter for keyset pagination on findDomains results. | ||
| * | ||
| * Uses tuple comparison for non-NULL cursor values, and explicit NULL handling | ||
| * for NULL cursor values (since PostgreSQL tuple comparison with NULL yields NULL/unknown). | ||
| * | ||
| * @param domains - The domains CTE | ||
| * @param cursor - The decoded DomainCursor | ||
| * @param queryOrderBy - The order field for the current query (must match cursor.by) | ||
| * @param queryOrderDir - The order direction for the current query (must match cursor.dir) | ||
| * @param direction - "after" for forward pagination, "before" for backward | ||
| * @param effectiveDesc - Whether the effective sort direction is descending | ||
| * @throws if cursor.by does not match queryOrderBy | ||
| * @throws if cursor.dir does not match queryOrderDir | ||
| * @returns SQL expression for the cursor filter | ||
| */ | ||
| export function cursorFilter( | ||
| domains: DomainsWithOrderingMetadata, | ||
| cursor: DomainCursor, | ||
| queryOrderBy: typeof DomainsOrderBy.$inferType, | ||
| queryOrderDir: typeof OrderDirection.$inferType, | ||
| direction: "after" | "before", | ||
| effectiveDesc: boolean, | ||
| ): SQL { | ||
| // Validate cursor was created with the same ordering as the current query | ||
| if (cursor.by !== queryOrderBy) { | ||
| throw new Error( | ||
| `Invalid cursor: cursor was created with orderBy=${cursor.by} but query uses orderBy=${queryOrderBy}`, | ||
| ); | ||
| } | ||
|
|
||
| if (cursor.dir !== queryOrderDir) { | ||
| throw new Error( | ||
| `Invalid cursor: cursor was created with orderDir=${cursor.dir} but query uses orderDir=${queryOrderDir}`, | ||
| ); | ||
| } | ||
|
|
||
| const orderColumn = getOrderColumn(domains, cursor.by); | ||
|
|
||
| // Determine comparison direction: | ||
| // - "after" with ASC = greater than cursor | ||
| // - "after" with DESC = less than cursor | ||
| // - "before" with ASC = less than cursor | ||
| // - "before" with DESC = greater than cursor | ||
| const useGreaterThan = (direction === "after") !== effectiveDesc; | ||
|
|
||
| // Handle NULL cursor values explicitly (PostgreSQL tuple comparison with NULL yields NULL/unknown) | ||
| // With NULLS LAST ordering: non-NULL values come before NULL values | ||
| if (cursor.value === null) { | ||
| if (direction === "after") { | ||
| // "after" a NULL = other NULLs with appropriate id comparison | ||
| return useGreaterThan | ||
| ? sql`(${orderColumn} IS NULL AND ${domains.id} > ${cursor.id})` | ||
| : sql`(${orderColumn} IS NULL AND ${domains.id} < ${cursor.id})`; | ||
| } else { | ||
| // "before" a NULL = all non-NULLs (they come before NULLs) + NULLs with appropriate id | ||
| return useGreaterThan | ||
| ? sql`(${orderColumn} IS NOT NULL OR (${orderColumn} IS NULL AND ${domains.id} > ${cursor.id}))` | ||
| : sql`(${orderColumn} IS NOT NULL OR (${orderColumn} IS NULL AND ${domains.id} < ${cursor.id}))`; | ||
| } | ||
| } | ||
|
|
||
| // Non-null cursor: use tuple comparison | ||
| // NOTE: Drizzle 0.41 doesn't support gt/lt with tuple arrays, so we use raw SQL | ||
| // NOTE: explicit cast required — Postgres can't infer parameter types in tuple comparisons | ||
| const op = useGreaterThan ? ">" : "<"; | ||
| const value = | ||
| cursor.by === "NAME" ? sql`${cursor.value}::text` : sql`${cursor.value}::numeric(78,0)`; | ||
| return sql`(${orderColumn}, ${domains.id}) ${sql.raw(op)} (${value}, ${cursor.id})`; | ||
| } | ||
|
|
||
| /** | ||
| * Compute the effective sort direction, combining user's orderDir with relay's inverted flag. | ||
| * XOR logic: inverted flips the sort for backward pagination. | ||
| */ | ||
| export function isEffectiveDesc( | ||
| orderDir: typeof OrderDirection.$inferType, | ||
| inverted: boolean, | ||
| ): boolean { | ||
| return (orderDir === "DESC") !== inverted; | ||
| } | ||
|
|
||
| export function orderFindDomains( | ||
| domains: DomainsWithOrderingMetadata, | ||
| orderBy: typeof DomainsOrderBy.$inferType, | ||
| orderDir: typeof OrderDirection.$inferType, | ||
| inverted: boolean, | ||
| ): SQL[] { | ||
| const effectiveDesc = isEffectiveDesc(orderDir, inverted); | ||
| const orderColumn = getOrderColumn(domains, orderBy); | ||
|
|
||
| // Always use NULLS LAST so unregistered domains (NULL registration fields) | ||
| // appear at the end regardless of sort direction | ||
| const primaryOrder = effectiveDesc | ||
| ? sql`${orderColumn} DESC NULLS LAST` | ||
| : sql`${orderColumn} ASC NULLS LAST`; | ||
|
|
||
| // Always include id as tiebreaker for stable ordering | ||
| const tiebreaker = effectiveDesc ? desc(domains.id) : asc(domains.id); | ||
|
|
||
| return [primaryOrder, tiebreaker]; | ||
| } |
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
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.