openvibers/openvibe.play Commits
A sandbox title is deleted by its app with everything in it (DELETE /titles/:title); a production title never is (#42)
Changes
@@@ -11,7 +11,8 @@ its boards. The rules of each game stay in the game; Play keeps the record and a- **Your own game, from the sandbox** (ADR-014 opt-in): any developer makes a project on openvibe.services and a sandboxapp with Play's three capabilities (in Network's default sandbox allowance). A sandbox app registers up to 3 titleswhose ids start with `sandbox-`, with guest players only (nothing adopted, no person's record touched), never listed,-- counted or indexed: readable at their id, marked `sandbox: true`. A production app (staff turn production on for the++ counted or indexed: readable at their id, marked `sandbox: true`, and deleted by their app (`DELETE /titles/:title`;++ a production title is never deleted). A production app (staff turn production on for theproject) registers the real name; it never takes a `sandbox-` id.- **Achievements**, including hidden ones whose name shows only once unlocked. A definition left out of an update isretired, never deleted: what players earned stays.@@@ -11,6 +11,7 @@* OpenVibe.Network's /@name and any site's showcase)* GET /me/achievements a person their own unlocks across titles* PUT /titles/:title play.title.manage register or update a title the caller manages++ * DELETE /titles/:title play.title.manage delete a sandbox title the caller manages (204)* POST /titles/:title/scores play.result.write post a round's scores to a board (idempotent by ref)* POST /titles/:title/unlocks play.result.write unlock achievements for a player (idempotent)* POST /titles/:title/players/:guest/adopt play.result.write a guest signed in: their rows become the person's@@@ -101,6 +102,10 @@ function createApi(ctx) {const out = await play.define(req.principal.requester, req.params.title, req.body || {}, { sandbox: req.principal.sandbox === true });return res.status(out.created ? 201 : 200).json(out);}));++ r.delete('/titles/:title', principal.requireCapability('play.title.manage'), titleOwner, limits.budget('play.define'), wrap(async (req, res) => {++ await play.removeSandbox(req.principal.requester, req.params.title);++ return res.status(204).end();++ }));r.post('/titles/:title/scores', principal.requireCapability('play.result.write'), titleOwner, limits.budget('play.write'), wrap(async (req, res) => {res.json(await play.recordScores(req.principal.requester, req.params.title, req.body || {}));}));@@@ -268,7 +268,7 @@ ${saves.length ? html`<section><h2>Your saves</h2><p class="pl-muted">What games<li>Add a sandbox app and ask for <code>play.title.manage</code>, <code>play.result.write</code> and <code>play.player.read</code>: a sandbox app gets them without waiting for anyone.</li><li>Get a token for audience <code>openvibe.play</code> (client credentials) and <code>PUT /titles/sandbox-your-game</code>.</li></ol>--<p>A sandbox app registers up to ${String(MAX_SANDBOX_TITLES)} titles, each id starting with <code>sandbox-</code> (anything else answers <code>400 play.sandbox_title</code>). Their players are guests only (<code>400 play.sandbox_guests_only</code> for a person), nothing is adopted, and they are never listed, counted or indexed: their pages and JSON answer at their id, marked <code>sandbox: true</code>, so you can check your boards. When your game is ready, staff turn production on for your project; its production app registers the real name and real players' progress counts on their profiles.</p>`,++<p>A sandbox app registers up to ${String(MAX_SANDBOX_TITLES)} titles, each id starting with <code>sandbox-</code>, and deletes one with everything in it by <code>DELETE /titles/:title</code> (<code>play.title.manage</code>; a production title is never deleted) (anything else answers <code>400 play.sandbox_title</code>). Their players are guests only (<code>400 play.sandbox_guests_only</code> for a person), nothing is adopted, and they are never listed, counted or indexed: their pages and JSON answer at their id, marked <code>sandbox: true</code>, so you can check your boards. When your game is ready, staff turn production on for your project; its production app registers the real name and real players' progress counts on their profiles.</p>`,}));r.get('/updates', (req, res) => page(req, res, {@@@ -5,6 +5,7 @@** define(owner, titleId, doc) register or update a title the caller manages; definitions missing from doc* are retired, never deleted (the unlocks and scores stay)++ * removeSandbox(owner, titleId) delete a sandbox title with everything in it (a production title stays)* recordScores(owner, titleId, post) { board, ref, entries: [{ player, name, score }], at? } → { recorded }* unlock(owner, titleId, post) { player, achievements: [id…], at? } → { unlocked: [newly unlocked ids] }* adopt(owner, titleId, guest, person) move a guest's unlocks and scores to the person who signed in@@@ -133,6 +134,14 @@ function createPlay({ s }) {});}++ /** A sandbox title's app deletes it with everything in it: its players are guests, so no one's record changes. A++ * production title is never deleted: what players earned stays on their profiles. */++ async function removeSandbox(owner, titleId) {++ const t = await ownTitle(owner, titleId);++ if (!isSandbox(t.id)) fail(409, 'play.title_kept', 'a production title is never deleted: what players earned stays (leave achievements and boards out of an update to retire them)');++ await db.exec('DELETE FROM play_titles WHERE id = $1 AND owner = $2', [t.id, owner]);++ }++async function recordScores(owner, titleId, post = {}) {await ownTitle(owner, titleId);const board = await db.maybe('SELECT * FROM play_boards WHERE title_id = $1 AND id = $2', [titleId, String(post.board || '')]);@@@ -342,7 +351,7 @@ function createPlay({ s }) {return (await db.exec('DELETE FROM play_saves WHERE title_id = $1 AND player = $2 AND slot = $3', [titleId, player(person), slotOf(slot)])) > 0;}-- return { define, recordScores, unlock, adopt, mergeSubject, board, title, titles, playerAchievements, ownTitle,++ return { define, removeSandbox, recordScores, unlock, adopt, mergeSubject, board, title, titles, playerAchievements, ownTitle,putSave, getSave, listSaves, deleteSave, mySaves, deleteMySave };}@@@ -64,6 +64,16 @@ const CAPS = ['play.title.manage', 'play.result.write', 'play.player.read'];assert.ok(!(await t.get('/sitemap.xml')).text.includes('sandbox-'), 'not in the sitemap');});++ await check('a sandbox title is deleted by its app with everything in it; a production title never', async () => {++ const del = (id, auth) => t.get(`/api/v1/titles/${id}`, { ...auth, method: 'DELETE' });++ assert.deepStrictEqual(await code(del('gem-run', production)), [409, 'play.title_kept']);++ assert.deepStrictEqual(await code(del('sandbox-gem-run', production)), [404, 'play.unknown_title'], 'another app is told it does not exist');++ assert.strictEqual((await del('sandbox-gem-run', sandbox)).status, 204);++ assert.strictEqual((await t.get('/api/v1/titles/sandbox-gem-run')).status, 404);++ assert.strictEqual((await t.get('/t/sandbox-gem-run/b/best')).status, 404, 'its boards went with it');++ assert.strictEqual((await put('sandbox-four', sandbox)).status, 201, 'the room it took is free again');++ });++await check('the docs and the home say how to start', async () => {const docs = await t.get('/docs');assert.match(docs.text, /<h2 id="sandbox">Your own game: start in the sandbox<\/h2>/);
@@ -11,7 +11,8 @@ its boards. The rules of each game stay in the game; Play keeps the record and a
@@ -11,7 +11,8 @@ its boards. The rules of each game stay in the game; Play keeps the record and a
- **Your own game, from the sandbox** (ADR-014 opt-in): any developer makes a project on openvibe.services and a sandbox
- **Your own game, from the sandbox** (ADR-014 opt-in): any developer makes a project on openvibe.services and a sandbox
app with Play's three capabilities (in Network's default sandbox allowance). A sandbox app registers up to 3 titles
app with Play's three capabilities (in Network's default sandbox allowance). A sandbox app registers up to 3 titles
whose ids start with `sandbox-`, with guest players only (nothing adopted, no person's record touched), never listed,
whose ids start with `sandbox-`, with guest players only (nothing adopted, no person's record touched), never listed,
- counted or indexed: readable at their id, marked `sandbox: true`. A production app (staff turn production on for the
+ counted or indexed: readable at their id, marked `sandbox: true`, and deleted by their app (`DELETE /titles/:title`;
+ a production title is never deleted). A production app (staff turn production on for the
project) registers the real name; it never takes a `sandbox-` id.
project) registers the real name; it never takes a `sandbox-` id.
- **Achievements**, including hidden ones whose name shows only once unlocked. A definition left out of an update is
- **Achievements**, including hidden ones whose name shows only once unlocked. A definition left out of an update is
retired, never deleted: what players earned stays.
retired, never deleted: what players earned stays.
@@ -11,6 +11,7 @@
@@ -11,6 +11,7 @@
* OpenVibe.Network's /@name and any site's showcase)
* OpenVibe.Network's /@name and any site's showcase)
* GET /me/achievements a person their own unlocks across titles
* GET /me/achievements a person their own unlocks across titles
* PUT /titles/:title play.title.manage register or update a title the caller manages
* PUT /titles/:title play.title.manage register or update a title the caller manages
+ * DELETE /titles/:title play.title.manage delete a sandbox title the caller manages (204)
* POST /titles/:title/scores play.result.write post a round's scores to a board (idempotent by ref)
* POST /titles/:title/scores play.result.write post a round's scores to a board (idempotent by ref)
* POST /titles/:title/unlocks play.result.write unlock achievements for a player (idempotent)
* POST /titles/:title/unlocks play.result.write unlock achievements for a player (idempotent)
* POST /titles/:title/players/:guest/adopt play.result.write a guest signed in: their rows become the person's
* POST /titles/:title/players/:guest/adopt play.result.write a guest signed in: their rows become the person's
@@ -101,6 +102,10 @@ function createApi(ctx) {
@@ -101,6 +102,10 @@ function createApi(ctx) {
const out = await play.define(req.principal.requester, req.params.title, req.body || {}, { sandbox: req.principal.sandbox === true });
const out = await play.define(req.principal.requester, req.params.title, req.body || {}, { sandbox: req.principal.sandbox === true });
return res.status(out.created ? 201 : 200).json(out);
return res.status(out.created ? 201 : 200).json(out);
}));
}));
+ r.delete('/titles/:title', principal.requireCapability('play.title.manage'), titleOwner, limits.budget('play.define'), wrap(async (req, res) => {
+ await play.removeSandbox(req.principal.requester, req.params.title);
+ return res.status(204).end();
+ }));
r.post('/titles/:title/scores', principal.requireCapability('play.result.write'), titleOwner, limits.budget('play.write'), wrap(async (req, res) => {
r.post('/titles/:title/scores', principal.requireCapability('play.result.write'), titleOwner, limits.budget('play.write'), wrap(async (req, res) => {
res.json(await play.recordScores(req.principal.requester, req.params.title, req.body || {}));
res.json(await play.recordScores(req.principal.requester, req.params.title, req.body || {}));
}));
}));
@@ -268,7 +268,7 @@ ${saves.length ? html`<section><h2>Your saves</h2><p class="pl-muted">What games
@@ -268,7 +268,7 @@ ${saves.length ? html`<section><h2>Your saves</h2><p class="pl-muted">What games
<li>Add a sandbox app and ask for <code>play.title.manage</code>, <code>play.result.write</code> and <code>play.player.read</code>: a sandbox app gets them without waiting for anyone.</li>
<li>Add a sandbox app and ask for <code>play.title.manage</code>, <code>play.result.write</code> and <code>play.player.read</code>: a sandbox app gets them without waiting for anyone.</li>
<li>Get a token for audience <code>openvibe.play</code> (client credentials) and <code>PUT /titles/sandbox-your-game</code>.</li>
<li>Get a token for audience <code>openvibe.play</code> (client credentials) and <code>PUT /titles/sandbox-your-game</code>.</li>
</ol>
</ol>
-<p>A sandbox app registers up to ${String(MAX_SANDBOX_TITLES)} titles, each id starting with <code>sandbox-</code> (anything else answers <code>400 play.sandbox_title</code>). Their players are guests only (<code>400 play.sandbox_guests_only</code> for a person), nothing is adopted, and they are never listed, counted or indexed: their pages and JSON answer at their id, marked <code>sandbox: true</code>, so you can check your boards. When your game is ready, staff turn production on for your project; its production app registers the real name and real players' progress counts on their profiles.</p>`,
+<p>A sandbox app registers up to ${String(MAX_SANDBOX_TITLES)} titles, each id starting with <code>sandbox-</code>, and deletes one with everything in it by <code>DELETE /titles/:title</code> (<code>play.title.manage</code>; a production title is never deleted) (anything else answers <code>400 play.sandbox_title</code>). Their players are guests only (<code>400 play.sandbox_guests_only</code> for a person), nothing is adopted, and they are never listed, counted or indexed: their pages and JSON answer at their id, marked <code>sandbox: true</code>, so you can check your boards. When your game is ready, staff turn production on for your project; its production app registers the real name and real players' progress counts on their profiles.</p>`,
}));
}));
r.get('/updates', (req, res) => page(req, res, {
r.get('/updates', (req, res) => page(req, res, {
@@ -5,6 +5,7 @@
@@ -5,6 +5,7 @@
*
*
* define(owner, titleId, doc) register or update a title the caller manages; definitions missing from doc
* define(owner, titleId, doc) register or update a title the caller manages; definitions missing from doc
* are retired, never deleted (the unlocks and scores stay)
* are retired, never deleted (the unlocks and scores stay)
+ * removeSandbox(owner, titleId) delete a sandbox title with everything in it (a production title stays)
* recordScores(owner, titleId, post) { board, ref, entries: [{ player, name, score }], at? } → { recorded }
* recordScores(owner, titleId, post) { board, ref, entries: [{ player, name, score }], at? } → { recorded }
* unlock(owner, titleId, post) { player, achievements: [id…], at? } → { unlocked: [newly unlocked ids] }
* unlock(owner, titleId, post) { player, achievements: [id…], at? } → { unlocked: [newly unlocked ids] }
* adopt(owner, titleId, guest, person) move a guest's unlocks and scores to the person who signed in
* adopt(owner, titleId, guest, person) move a guest's unlocks and scores to the person who signed in
@@ -133,6 +134,14 @@ function createPlay({ s }) {
@@ -133,6 +134,14 @@ function createPlay({ s }) {
});
});
}
}
+ /** A sandbox title's app deletes it with everything in it: its players are guests, so no one's record changes. A
+ * production title is never deleted: what players earned stays on their profiles. */
+ async function removeSandbox(owner, titleId) {
+ const t = await ownTitle(owner, titleId);
+ if (!isSandbox(t.id)) fail(409, 'play.title_kept', 'a production title is never deleted: what players earned stays (leave achievements and boards out of an update to retire them)');
+ await db.exec('DELETE FROM play_titles WHERE id = $1 AND owner = $2', [t.id, owner]);
+ }
+
async function recordScores(owner, titleId, post = {}) {
async function recordScores(owner, titleId, post = {}) {
await ownTitle(owner, titleId);
await ownTitle(owner, titleId);
const board = await db.maybe('SELECT * FROM play_boards WHERE title_id = $1 AND id = $2', [titleId, String(post.board || '')]);
const board = await db.maybe('SELECT * FROM play_boards WHERE title_id = $1 AND id = $2', [titleId, String(post.board || '')]);
@@ -342,7 +351,7 @@ function createPlay({ s }) {
@@ -342,7 +351,7 @@ function createPlay({ s }) {
return (await db.exec('DELETE FROM play_saves WHERE title_id = $1 AND player = $2 AND slot = $3', [titleId, player(person), slotOf(slot)])) > 0;
return (await db.exec('DELETE FROM play_saves WHERE title_id = $1 AND player = $2 AND slot = $3', [titleId, player(person), slotOf(slot)])) > 0;
}
}
- return { define, recordScores, unlock, adopt, mergeSubject, board, title, titles, playerAchievements, ownTitle,
+ return { define, removeSandbox, recordScores, unlock, adopt, mergeSubject, board, title, titles, playerAchievements, ownTitle,
putSave, getSave, listSaves, deleteSave, mySaves, deleteMySave };
putSave, getSave, listSaves, deleteSave, mySaves, deleteMySave };
}
}
@@ -64,6 +64,16 @@ const CAPS = ['play.title.manage', 'play.result.write', 'play.player.read'];
@@ -64,6 +64,16 @@ const CAPS = ['play.title.manage', 'play.result.write', 'play.player.read'];
assert.ok(!(await t.get('/sitemap.xml')).text.includes('sandbox-'), 'not in the sitemap');
assert.ok(!(await t.get('/sitemap.xml')).text.includes('sandbox-'), 'not in the sitemap');
});
});
+ await check('a sandbox title is deleted by its app with everything in it; a production title never', async () => {
+ const del = (id, auth) => t.get(`/api/v1/titles/${id}`, { ...auth, method: 'DELETE' });
+ assert.deepStrictEqual(await code(del('gem-run', production)), [409, 'play.title_kept']);
+ assert.deepStrictEqual(await code(del('sandbox-gem-run', production)), [404, 'play.unknown_title'], 'another app is told it does not exist');
+ assert.strictEqual((await del('sandbox-gem-run', sandbox)).status, 204);
+ assert.strictEqual((await t.get('/api/v1/titles/sandbox-gem-run')).status, 404);
+ assert.strictEqual((await t.get('/t/sandbox-gem-run/b/best')).status, 404, 'its boards went with it');
+ assert.strictEqual((await put('sandbox-four', sandbox)).status, 201, 'the room it took is free again');
+ });
+
await check('the docs and the home say how to start', async () => {
await check('the docs and the home say how to start', async () => {
const docs = await t.get('/docs');
const docs = await t.get('/docs');
assert.match(docs.text, /<h2 id="sandbox">Your own game: start in the sandbox<\/h2>/);
assert.match(docs.text, /<h2 id="sandbox">Your own game: start in the sandbox<\/h2>/);