Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions backend/src/db/sightings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -41,8 +41,8 @@ const SELECT = `
const ORDER_BY = {
DATE_DESC: 's.SIGHTING_DATETIME DESC, s.UFO_SIGHTING_ID DESC',
DATE_ASC: 's.SIGHTING_DATETIME ASC, s.UFO_SIGHTING_ID ASC',
CITY_ASC: 'l.CITY ASC, s.UFO_SIGHTING_ID ASC',
SHAPE_ASC: 'sh.NAME ASC, s.UFO_SIGHTING_ID ASC',
CITY_ASC: "NULLIF(l.CITY, '') IS NULL, l.CITY ASC, s.UFO_SIGHTING_ID ASC",
SHAPE_ASC: "NULLIF(sh.NAME, '') IS NULL, sh.NAME ASC, s.UFO_SIGHTING_ID ASC",
}

// MySQL gives "2005-01-01 00:30:00"; the API uses ISO 8601 without timezone.
Expand Down
51 changes: 49 additions & 2 deletions backend/src/schema.db.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,12 @@ import { resolvers, typeDefs } from './schema.ts'
// npx tsx --env-file=.env --test src/schema.db.test.ts
const hasDatabase = Boolean(process.env.DB_NAME)

type Node = { id: string; datetime: string | null; shape: string | null }
type Node = {
id: string
datetime: string | null
city: string | null
shape: string | null
}
type Page = {
totalCount: number
pageInfo: { hasNextPage: boolean; endCursor: string | null }
Expand All @@ -29,7 +34,7 @@ async function sightings(variables: Record<string, unknown>): Promise<Page> {
first: $first, after: $after) {
totalCount
pageInfo { hasNextPage endCursor }
edges { node { id datetime shape } }
edges { node { id datetime city shape } }
}
}`,
variables,
Expand Down Expand Up @@ -98,6 +103,48 @@ describe('sightings against the real database', { skip: !hasDatabase }, () => {
})
}

for (const [sort, field] of [
['CITY_ASC', 'city'],
['SHAPE_ASC', 'shape'],
] as const) {
it(`puts missing values last and returns every sighting once (${sort})`, async () => {
const seen = new Set<string>()
let missingSeen = false
let after: string | null = null
let page: Page

do {
page = await sightings({ sort, first: 100, after })
assert.ok(page.edges.length > 0, 'the dataset should contain sightings')
if (after === null) {
assert.ok(page.edges[0].node[field], 'real names should come first')
}

for (const { node } of page.edges) {
assert.ok(!seen.has(node.id), `duplicate sighting ${node.id}`)
seen.add(node.id)
if (node[field] === null || node[field] === '') {
missingSeen = true
} else {
assert.equal(
missingSeen,
false,
'a real name followed a missing value',
)
}
}

after = page.pageInfo.endCursor
} while (page.pageInfo.hasNextPage)

assert.equal(
seen.size,
page.totalCount,
'every sighting should be returned',
)
})
}

it('sorts by date across page boundaries', async () => {
const dates: string[] = []
let after: string | null = null
Expand Down
11 changes: 6 additions & 5 deletions backend/src/schema.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -88,18 +88,19 @@ describe('sightings search', () => {

describe('sightings sort', () => {
const orders = {
DATE_DESC: 's.SIGHTING_DATETIME DESC',
DATE_ASC: 's.SIGHTING_DATETIME ASC',
CITY_ASC: 'l.CITY ASC',
SHAPE_ASC: 'sh.NAME ASC',
DATE_DESC: 's.SIGHTING_DATETIME DESC, s.UFO_SIGHTING_ID DESC',
DATE_ASC: 's.SIGHTING_DATETIME ASC, s.UFO_SIGHTING_ID ASC',
CITY_ASC: "NULLIF(l.CITY, '') IS NULL, l.CITY ASC, s.UFO_SIGHTING_ID ASC",
SHAPE_ASC:
"NULLIF(sh.NAME, '') IS NULL, sh.NAME ASC, s.UFO_SIGHTING_ID ASC",
}

for (const [sort, clause] of Object.entries(orders)) {
it(`orders by ${sort} in the database`, async () => {
fakeDatabase([])
await run(SIGHTINGS, { sort })

assert.ok(listCall().sql.includes(`ORDER BY ${clause}`))
assert.ok(listCall().sql.includes(`ORDER BY ${clause} LIMIT ? OFFSET ?`))
})
}

Expand Down