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', getGameListHandler);
|
||||
|
||||
app.get('/pacosako/api/game/:gameId/poll/:afterTime', 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', 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('/gun', Gun.serve(__dirname));
|
||||
|
|
|
|||
Loading…
Reference in New Issue