Merge pull request #1 from Coffey-Labs/drop-the-no-game-shim
Stop guessing which board a request means
This commit is contained in:
@@ -79,14 +79,17 @@ accounts has ever been able to offer.
|
|||||||
Every entry in a post is validated before any of them is written: a party that
|
Every entry in a post is validated before any of them is written: a party that
|
||||||
half-posts is worse than one that does not post at all.
|
half-posts is worse than one that does not post at all.
|
||||||
|
|
||||||
### The missing-game shim
|
### Every request names its game
|
||||||
|
|
||||||
A request with no `game` is treated as `lemonade`. That is a compatibility
|
There is no default. A request that does not say which board it wants is
|
||||||
shim, not a default worth keeping — the deployed lemonade bundle posts no game
|
refused with the list of boards there are.
|
||||||
at all, because when it was built there was only one board, and copies of it
|
|
||||||
are sitting in people's browsers. It can go once that bundle has been rebuilt
|
There was briefly a shim that read a missing `game` as `lemonade`, because the
|
||||||
and redeployed, and not before, or every score set from a cached page lands
|
deployed lemonade bundle predated this service and posted no game at all. It
|
||||||
nowhere.
|
was removed once that bundle had been rebuilt and was what the site served —
|
||||||
|
verified by reading the asset the public URL actually points at, not the one
|
||||||
|
the origin holds, because there is a CDN in between and the two are not the
|
||||||
|
same claim.
|
||||||
|
|
||||||
## The migration
|
## The migration
|
||||||
|
|
||||||
|
|||||||
+16
-12
@@ -113,10 +113,10 @@ function readBody(req: IncomingMessage): Promise<string> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Rows go back in the game's own field names, flattened, exactly as its
|
* Rows go back in the game's own field names, flattened, exactly as its client
|
||||||
* client already expects them. That is not politeness -- the lemonade stand's
|
* expects them. That is the bargain that makes a shared service invisible: a
|
||||||
* bundle is sitting in people's browsers right now expecting `assets` and
|
* game asks for its board and gets `assets` and `days`, or `bagged` and
|
||||||
* `days`, and it will keep expecting them until it is rebuilt.
|
* `arrows`, and never has to know it is sharing a table with anybody.
|
||||||
*/
|
*/
|
||||||
const shape = (rows: Row[]) =>
|
const shape = (rows: Row[]) =>
|
||||||
rows.map((r) => ({ id: r.id, name: r.name, at: r.at, ...r.fields }))
|
rows.map((r) => ({ id: r.id, name: r.name, at: r.at, ...r.fields }))
|
||||||
@@ -124,14 +124,18 @@ const shape = (rows: Row[]) =>
|
|||||||
/**
|
/**
|
||||||
* Which game a request is about.
|
* Which game a request is about.
|
||||||
*
|
*
|
||||||
* A missing game means lemonade, and that is a compatibility shim rather than
|
* There was a shim here that read a missing game as lemonade, because the
|
||||||
* a default worth keeping: the deployed lemonade client posts no game at all,
|
* deployed lemonade bundle predated this service and posted no game at all.
|
||||||
* because when it was built there was only one board. It can go once that
|
* That bundle has been rebuilt and is what the site serves now -- both games
|
||||||
* bundle has been rebuilt and redeployed -- and not before, or every score set
|
* name themselves on every call -- so the shim has been removed and a request
|
||||||
* from a cached page lands nowhere.
|
* that does not say which board it wants is refused rather than guessed at.
|
||||||
|
*
|
||||||
|
* Guessing was the right thing while there was something to guess for. It is
|
||||||
|
* the wrong thing now: silently filing an unlabelled score under whichever
|
||||||
|
* game happened to be first is the sort of default that is invisible until it
|
||||||
|
* is wrong, and the caller always knows which game it is.
|
||||||
*/
|
*/
|
||||||
function gameFor(explicit: unknown): Game | undefined {
|
function gameFor(explicit: unknown): Game | undefined {
|
||||||
if (explicit === undefined || explicit === null || explicit === '') return lookup('lemonade')
|
|
||||||
return lookup(explicit)
|
return lookup(explicit)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -153,7 +157,7 @@ const server = createServer(async (req, res) => {
|
|||||||
|
|
||||||
if (path === '/api/scores' && req.method === 'GET') {
|
if (path === '/api/scores' && req.method === 'GET') {
|
||||||
const game = gameFor(url.searchParams.get('game') ?? undefined)
|
const game = gameFor(url.searchParams.get('game') ?? undefined)
|
||||||
if (!game) return send(res, 404, { error: 'unknown game' })
|
if (!game) return send(res, 404, { error: `unknown game; try one of: ${known().join(', ')}` })
|
||||||
return send(res, 200, { game: game.id, scores: shape(store.board(game, BOARD_LIMIT)) })
|
return send(res, 200, { game: game.id, scores: shape(store.board(game, BOARD_LIMIT)) })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,7 +174,7 @@ const server = createServer(async (req, res) => {
|
|||||||
|
|
||||||
const body = (parsed ?? {}) as { game?: unknown; entries?: unknown }
|
const body = (parsed ?? {}) as { game?: unknown; entries?: unknown }
|
||||||
const game = gameFor(body.game)
|
const game = gameFor(body.game)
|
||||||
if (!game) return send(res, 404, { error: 'unknown game' })
|
if (!game) return send(res, 404, { error: `unknown game; try one of: ${known().join(', ')}` })
|
||||||
|
|
||||||
const list = Array.isArray(parsed) ? parsed : body.entries
|
const list = Array.isArray(parsed) ? parsed : body.entries
|
||||||
if (!Array.isArray(list)) return send(res, 400, { error: 'expected an array of entries' })
|
if (!Array.isArray(list)) return send(res, 400, { error: 'expected an array of entries' })
|
||||||
|
|||||||
@@ -15,6 +15,21 @@ describe('the registry', () => {
|
|||||||
expect(lookup('doom')).toBeUndefined()
|
expect(lookup('doom')).toBeUndefined()
|
||||||
expect(lookup(42)).toBeUndefined()
|
expect(lookup(42)).toBeUndefined()
|
||||||
})
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* There is no default game, and there must not be one.
|
||||||
|
*
|
||||||
|
* A shim used to read a missing game as lemonade, for bundles that predated
|
||||||
|
* this service. Now that nothing posts unlabelled, guessing would be worse
|
||||||
|
* than refusing: it would file somebody's score under whichever game
|
||||||
|
* happened to be first, silently, and nobody would find out until the board
|
||||||
|
* looked wrong. These are the shapes a missing game arrives in.
|
||||||
|
*/
|
||||||
|
it('refuses to guess when nothing says which board', () => {
|
||||||
|
for (const missing of [undefined, null, '', 0, false, {}, []]) {
|
||||||
|
expect(lookup(missing)).toBeUndefined()
|
||||||
|
}
|
||||||
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|||||||
Reference in New Issue
Block a user