add a REST API to submit new game data via POST

This commit is contained in:
Jesse D. McDonald 2020-04-27 17:55:28 -05:00
parent 5542cb0ab5
commit 3653f3db99
1 changed files with 162 additions and 10 deletions

172
index.js
View File

@ -461,25 +461,177 @@ async function getMetaHandler(req, res, next) {
} }
} }
const updateTemplate = {
lightName(x) { return typeof x === 'string'; },
darkName(x) { return typeof x === 'string'; },
moves(x) { return Number.isInteger(x); },
status(x) { return typeof x === 'string'; },
timestamp(x) { return Number.isInteger(x); },
board(x) { return true; },
modified(x) { return Number.isInteger(x); },
};
function validateUpdate(body) {
try {
if (typeof body !== 'object' || 'modified' in body === false) {
console.log('invalid type or missing modified property');
return null;
}
for (const key in body) {
if (key in updateTemplate === false) {
console.log('extra key', key);
return null;
}
if (updateTemplate[key](body[key]) === false) {
console.log('invalid value', body[key], 'for key', key);
return null;
}
}
} catch(err) {
console.log('exception', err);
return null;
}
return body;
}
async function postGameHandler(req, res, next) {
res.set('Cache-Control', 'no-store');
try {
const time = +new Date();
const gameId = req.params.gameId;
const body = validateUpdate(req.body);
console.log('update for', gameId, 'at time', time, req.body, !body);
if (!gameId.match(/^[0-9a-f]{16}$/)) {
res.status(400).json({ message: 'malformed game ID' });
return;
}
if (!body) {
res.status(400).json({ message: 'invalid request' });
return;
}
const params = {
$gameId: gameId,
$lightName: '',
$darkName: '',
$moves: 0,
$status: '',
$timestamp: 0,
$board: '',
$modified: body.modified,
$time: time,
};
if (params.$modified >= params.$time) {
res.status(400).json({ message: 'invalid modification time' });
return;
}
let setClause = '';
let whereClause = '';
let hasBoard = false;
let hasMeta = false;
for (const key in body) {
if (key !== 'modified') {
if (key in updateTemplate === false) {
throw { message: 'internal error' };
}
if (key === 'board') {
params['$' + key] = JSON.stringify(body[key]);
hasBoard = true;
} else {
params['$' + key] = body[key];
hasMeta = true;
}
if (setClause !== '') {
setClause += ', ';
whereClause += ' OR ';
}
setClause += key + ' = $' + key;
whereClause += key + ' <> $' + key;
}
}
if (setClause === '') {
res.status(400).json({ message: 'empty update' });
return;
}
const db = await dbInit;
const insertSql = `
INSERT INTO games (gameId, lightName, darkName, moves, status, timestamp, board, added, modified)
VALUES ($gameId, $lightName, $darkName, $moves, $status, $timestamp, $board, $time, $time)
ON CONFLICT (gameId) DO UPDATE SET ${setClause}, modified = $time
WHERE (${whereClause}) AND modified = $modified
`;
const selectSql = `SELECT * FROM games WHERE gameId = $gameId`
let beginP, insertP, selectP, commitP;
db.serialize(() => {
/* Important: We need to start all these queries without waiting in between. */
/* Otherwise db.serialize() will not have the desired effect. */
beginP = db.execAsync(`BEGIN TRANSACTION`);
insertP = db.runAsync(insertSql, params);
selectP = db.getAsync(selectSql, { $gameId: gameId });
commitP = db.execAsync(`COMMIT TRANSACTION`);
});
/* Now wait for all the queries to finish. */
await Promise.all([beginP, insertP, commitP]);
const result = await selectP;
if (!result || result.modified !== params.$time) {
res.status(409).json({ message: 'update failed', modified: (result || {}).modified });
return;
}
signalGameUpdate(gameId);
res.json({ success: true, modified: result.modified });
(async () => {
if (hasBoard) {
gun.get(PacoSakoUUID + '/game/' + gameId).get('board').put(result.board);
}
if (hasMeta) {
const meta = {
lightName: result.lightName,
darkName: result.darkName,
moves: result.moves,
timestamp: result.timestamp,
status: (result.status === '') ? null : result.status,
};
const metaRef = gun.get(PacoSakoUUID + '/meta/' + gameId).put(meta);
if (meta.lightName !== '' || meta.darkName !== '' || meta.moves !== 0) {
gun.get(PacoSakoUUID + '/meta').get(gameId).put(metaRef);
} else {
gun.get(PacoSakoUUID + '/meta').get(gameId).put(null);
}
}
})().catch(console.error);
} catch (err) {
internalErrorJson(err, res);
}
}
app.get('/pacosako/api/games/poll/:afterTime', getGameListHandler); app.get('/pacosako/api/games/poll/:afterTime', getGameListHandler);
app.get('/pacosako/api/games', getGameListHandler); app.get('/pacosako/api/games', getGameListHandler);
app.get('/pacosako/api/game/:gameId/poll/:afterTime', getGameHandler); app.get('/pacosako/api/game/:gameId/poll/:afterTime', getGameHandler);
app.get('/pacosako/api/game/:gameId', getGameHandler); app.get('/pacosako/api/game/:gameId', getGameHandler);
app.post('/pacosako/api/game/:gameId', express.json(), postGameHandler);
app.get('/pacosako/api/meta/:gameId/poll/:afterTime', getMetaHandler); app.get('/pacosako/api/meta/:gameId/poll/:afterTime', getMetaHandler);
app.get('/pacosako/api/meta/:gameId', getMetaHandler); app.get('/pacosako/api/meta/:gameId', getMetaHandler);
/* TODO: Add APIs for posting updates. */
app.use('/pacosako/api/posttest', express.json());
app.post('/pacosako/api/posttest', async function(req, res, next) {
try {
res.json(req.body);
} catch (err) {
internalErrorJson(err, res);
}
});
app.use('/pacosako/api', express.static('public')); app.use('/pacosako/api', express.static('public'));
app.use('/gun', Gun.serve(__dirname)); app.use('/gun', Gun.serve(__dirname));