add a REST API to submit new game data via POST
This commit is contained in:
parent
5542cb0ab5
commit
3653f3db99
172
index.js
172
index.js
|
|
@ -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));
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue