fix(workshop): enforce atomic revisions
This commit is contained in:
1 parent
213108dd2f
commit
558a7b65aa
4 files changed
+42
-11
No files matched your search
@@ -106,6 +106,18 @@ migrate(
|
||||
submissions.addIndex("idx_submissions_author", false, "author", "");
|
||||
submissions.addIndex("idx_submissions_created", false, "created", "");
|
||||
app.save(submissions);
|
||||
app
|
||||
.db()
|
||||
.newQuery(
|
||||
`CREATE TRIGGER submissions_revision_cas
|
||||
BEFORE UPDATE ON submissions
|
||||
FOR EACH ROW
|
||||
WHEN NEW.revision != OLD.revision + 1
|
||||
BEGIN
|
||||
SELECT RAISE(ABORT, 'submission revision must increment exactly once');
|
||||
END`,
|
||||
)
|
||||
.execute();
|
||||
|
||||
const votes = new Collection({
|
||||
type: "base",
|
||||
|
||||
@@ -24,6 +24,7 @@ const migration = await read("apps/workshop/pb_migrations/1784240000_workshop_wi
|
||||
for (const required of [
|
||||
"workshop_migration_state",
|
||||
"addedUserFields",
|
||||
"CREATE TRIGGER submissions_revision_cas",
|
||||
"state.rateLimits.enabled",
|
||||
"users.passwordAuth = state.users.passwordAuth",
|
||||
"users.oauth2 = state.users.oauth2",
|
||||
|
||||
@@ -67,7 +67,19 @@ Object.assign(users, original.rules);
|
||||
const settings = { rateLimits: structuredClone(original.rateLimits) };
|
||||
const collections = new Map([[users.id, users]]);
|
||||
const records = new Map();
|
||||
const rawQueries = [];
|
||||
const app = {
|
||||
db() {
|
||||
return {
|
||||
newQuery(sql) {
|
||||
return {
|
||||
execute() {
|
||||
rawQueries.push(sql);
|
||||
},
|
||||
};
|
||||
},
|
||||
};
|
||||
},
|
||||
findCollectionByNameOrId(name) {
|
||||
const collection = collections.get(name);
|
||||
if (!collection) throw new Error(`Missing collection ${name}`);
|
||||
@@ -113,6 +125,9 @@ migration.up(app);
|
||||
if (!collections.has("workshop_migration_state") || settings.rateLimits.enabled !== true) {
|
||||
throw new Error("Workshop migration did not capture state before applying changes");
|
||||
}
|
||||
if (!rawQueries.some((query) => query.includes("CREATE TRIGGER submissions_revision_cas"))) {
|
||||
throw new Error("Workshop migration did not install the atomic submission revision guard");
|
||||
}
|
||||
migration.down(app);
|
||||
|
||||
if (JSON.stringify(users.passwordAuth) !== JSON.stringify(original.passwordAuth))
|
||||
|
||||
@@ -178,17 +178,20 @@ if (!outdatedSubmission.outdated || outdatedSubmission.revision !== 3) {
|
||||
throw new Error("PocketBase must own submission revision increments");
|
||||
}
|
||||
|
||||
const concurrentUpdates = await Promise.all(
|
||||
["Concurrent first", "Concurrent second"].map((title) =>
|
||||
fetch(`${baseUrl}/api/collections/submissions/records/${submission.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json", ...authorSession.headers },
|
||||
body: JSON.stringify({ title, expectedRevision: 3 }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (concurrentUpdates.filter((response) => response.ok).length !== 1) {
|
||||
throw new Error("Exactly one update may consume an expected submission revision");
|
||||
for (let attempt = 0; attempt < 5; attempt += 1) {
|
||||
const expectedRevision = 3 + attempt;
|
||||
const concurrentUpdates = await Promise.all(
|
||||
["Concurrent first", "Concurrent second"].map((title) =>
|
||||
fetch(`${baseUrl}/api/collections/submissions/records/${submission.id}`, {
|
||||
method: "PATCH",
|
||||
headers: { "content-type": "application/json", ...authorSession.headers },
|
||||
body: JSON.stringify({ title: `${title} ${attempt}`, expectedRevision }),
|
||||
}),
|
||||
),
|
||||
);
|
||||
if (concurrentUpdates.filter((response) => response.ok).length !== 1) {
|
||||
throw new Error("Exactly one update may consume an expected submission revision");
|
||||
}
|
||||
}
|
||||
|
||||
await expectStatus(
|
||||
|
||||
Reference in new issue
Block a user