@snagset/server 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +83 -0
- package/dist/app.js +453 -0
- package/dist/app.js.map +1 -0
- package/dist/bin.js +951 -0
- package/dist/bin.js.map +1 -0
- package/dist/boot.js +321 -0
- package/dist/boot.js.map +1 -0
- package/dist/config.generated.js +116 -0
- package/dist/config.generated.js.map +1 -0
- package/dist/config.js +180 -0
- package/dist/config.js.map +1 -0
- package/dist/crypto.js +26 -0
- package/dist/crypto.js.map +1 -0
- package/dist/db/connect.js +49 -0
- package/dist/db/connect.js.map +1 -0
- package/dist/db/migrate.js +210 -0
- package/dist/db/migrate.js.map +1 -0
- package/dist/db/migrations/0001_init.sql +1060 -0
- package/dist/db/migrations/0002_maintenance_claim.sql +109 -0
- package/dist/db/migrations/0003_email_provider.sql +35 -0
- package/dist/db/migrations/0004_unsubscribe.sql +24 -0
- package/dist/db/rls.js +58 -0
- package/dist/db/rls.js.map +1 -0
- package/dist/jobs/index.js +171 -0
- package/dist/jobs/index.js.map +1 -0
- package/dist/jobs/reaper.js +182 -0
- package/dist/jobs/reaper.js.map +1 -0
- package/dist/keys.js +77 -0
- package/dist/keys.js.map +1 -0
- package/dist/mail/index.js +75 -0
- package/dist/mail/index.js.map +1 -0
- package/dist/mail/mailer.js +27 -0
- package/dist/mail/mailer.js.map +1 -0
- package/dist/mail/notify.js +405 -0
- package/dist/mail/notify.js.map +1 -0
- package/dist/mail/resend.js +95 -0
- package/dist/mail/resend.js.map +1 -0
- package/dist/mail/smtp.js +315 -0
- package/dist/mail/smtp.js.map +1 -0
- package/dist/mail/unsubscribe.js +134 -0
- package/dist/mail/unsubscribe.js.map +1 -0
- package/dist/projectCache.js +50 -0
- package/dist/projectCache.js.map +1 -0
- package/dist/projects.js +101 -0
- package/dist/projects.js.map +1 -0
- package/dist/routes/attachments.js +159 -0
- package/dist/routes/attachments.js.map +1 -0
- package/dist/routes/auth.js +55 -0
- package/dist/routes/auth.js.map +1 -0
- package/dist/routes/commentEdit.js +201 -0
- package/dist/routes/commentEdit.js.map +1 -0
- package/dist/routes/comments.js +313 -0
- package/dist/routes/comments.js.map +1 -0
- package/dist/routes/cors.js +174 -0
- package/dist/routes/cors.js.map +1 -0
- package/dist/routes/jobs.js +56 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/mcpAuth.js +187 -0
- package/dist/routes/mcpAuth.js.map +1 -0
- package/dist/routes/mcpToken.js +94 -0
- package/dist/routes/mcpToken.js.map +1 -0
- package/dist/routes/owner.js +288 -0
- package/dist/routes/owner.js.map +1 -0
- package/dist/routes/reviewLinks.js +169 -0
- package/dist/routes/reviewLinks.js.map +1 -0
- package/dist/routes/rounds.js +137 -0
- package/dist/routes/rounds.js.map +1 -0
- package/dist/routes/sessions.js +349 -0
- package/dist/routes/sessions.js.map +1 -0
- package/dist/routes/sites.js +192 -0
- package/dist/routes/sites.js.map +1 -0
- package/dist/routes/threads.js +1080 -0
- package/dist/routes/threads.js.map +1 -0
- package/dist/routes/widget.js +132 -0
- package/dist/routes/widget.js.map +1 -0
- package/dist/serverless.js +97 -0
- package/dist/serverless.js.map +1 -0
- package/dist/session.js +152 -0
- package/dist/session.js.map +1 -0
- package/dist/storage/bucket.js +116 -0
- package/dist/storage/bucket.js.map +1 -0
- package/dist/storage/settings.js +132 -0
- package/dist/storage/settings.js.map +1 -0
- package/dist/vendor/adapters/index.js +1 -0
- package/dist/vendor/adapters/outbound/fence.js +44 -0
- package/dist/vendor/adapters/outbound/sanitise.js +233 -0
- package/dist/vendor/types/index.js +4 -0
- package/dist/vendor/types/names.js +254 -0
- package/dist/vendor/widget/object.js +38 -0
- package/dist/vendor/widget/predicates.js +377 -0
- package/package.json +53 -0
- package/static/dashboard/assets/index-CaPfKhFP.css +1 -0
- package/static/dashboard/assets/index-DfTNjHCK.js +1 -0
- package/static/dashboard/index.html +14 -0
- package/static/widget/snag.anchor-ext.js +1 -0
- package/static/widget/snag.anchor-fuzzy.js +1 -0
- package/static/widget/snag.capture.js +4 -0
- package/static/widget/snag.crumbs.js +4 -0
- package/static/widget/snag.dock.js +5 -0
- package/static/widget/snag.js +5 -0
- package/static/widget/snag.panels.js +2 -0
- package/static/widget/snag.runtime.js +2 -0
- package/static/widget/snag.screenshot.js +1 -0
- package/static/widget/snag.text.js +2 -0
- package/vercel.json +6 -0
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { randomUUID, randomBytes, createHash } from "node:crypto";
|
|
3
|
+
import { ID_PREFIX } from "../vendor/types/names.js";
|
|
4
|
+
function fail(c, status, code, message) {
|
|
5
|
+
return c.json({ error: { code, message, requestId: c.get("requestId") } }, status);
|
|
6
|
+
}
|
|
7
|
+
const sha256 = (s) => createHash("sha256").update(s, "utf8").digest();
|
|
8
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
9
|
+
const rowId = (wire, prefix) => {
|
|
10
|
+
const bare = wire.startsWith(prefix) ? wire.slice(prefix.length) : wire;
|
|
11
|
+
return UUID.test(bare) ? bare.toLowerCase() : null;
|
|
12
|
+
};
|
|
13
|
+
/** §3.4's six. Anything else is refused rather than stored — an unknown
|
|
14
|
+
* permission in the column would be silently dropped at exchange, which reads
|
|
15
|
+
* to the operator as "I granted it and it did nothing". */
|
|
16
|
+
const KNOWN = [
|
|
17
|
+
"thread:create",
|
|
18
|
+
"comment:create",
|
|
19
|
+
"thread:resolve",
|
|
20
|
+
"thread:delete:own",
|
|
21
|
+
"thread:private",
|
|
22
|
+
"thread:assign",
|
|
23
|
+
];
|
|
24
|
+
const DEFAULT_PERMISSIONS = ["thread:create", "comment:create", "thread:resolve"];
|
|
25
|
+
/** Never `token`, never `tokenHash`. */
|
|
26
|
+
const shape = (r) => ({
|
|
27
|
+
id: ID_PREFIX.link + r.id,
|
|
28
|
+
...(r.round_id ? { roundId: ID_PREFIX.round + r.round_id } : {}),
|
|
29
|
+
name: r.name,
|
|
30
|
+
...(r.email ? { email: r.email } : {}),
|
|
31
|
+
environments: r.environments,
|
|
32
|
+
permissions: r.permissions,
|
|
33
|
+
allowNameOverride: r.allow_name_override,
|
|
34
|
+
maxUses: r.max_uses,
|
|
35
|
+
useCount: r.use_count,
|
|
36
|
+
createdAt: r.created_at.toISOString(),
|
|
37
|
+
expiresAt: r.expires_at.toISOString(),
|
|
38
|
+
revokedAt: r.revoked_at?.toISOString() ?? null,
|
|
39
|
+
lastUsedAt: r.last_used_at?.toISOString() ?? null,
|
|
40
|
+
});
|
|
41
|
+
export function reviewLinkRoutes(deps) {
|
|
42
|
+
const app = new Hono();
|
|
43
|
+
const { sql, projects } = deps;
|
|
44
|
+
const now = deps.now ?? (() => new Date());
|
|
45
|
+
const DAY = 86_400_000;
|
|
46
|
+
/**
|
|
47
|
+
* Owner-only, and owner OF THIS PROJECT (same pattern as `rounds.ts`).
|
|
48
|
+
*
|
|
49
|
+
* `requireOwner()` only proves SOME org's owner is signed in — nothing
|
|
50
|
+
* here compared `owner.orgId` to the project's before this fix, so an
|
|
51
|
+
* owner of org A could mint, list, or revoke org B's links by uid. 404
|
|
52
|
+
* rather than 403 on a mismatch: a project uid existing at all is not
|
|
53
|
+
* information another org gets to learn.
|
|
54
|
+
*/
|
|
55
|
+
const owned = async (c) => {
|
|
56
|
+
const uid = c.req.param("uid");
|
|
57
|
+
if (!uid)
|
|
58
|
+
return null;
|
|
59
|
+
const p = await projects.get(uid).catch(() => null);
|
|
60
|
+
if (!p)
|
|
61
|
+
return null;
|
|
62
|
+
const owner = c.get("owner");
|
|
63
|
+
return owner && owner.orgId === p.org_id ? p : null;
|
|
64
|
+
};
|
|
65
|
+
app.post("/api/v1/projects/:uid/review-links", async (c) => {
|
|
66
|
+
const project = await owned(c);
|
|
67
|
+
if (!project)
|
|
68
|
+
return fail(c, 404, "project_not_found", "No project with that uid.");
|
|
69
|
+
const body = (await c.req.json().catch(() => null));
|
|
70
|
+
const name = typeof body?.name === "string" ? body.name.trim().slice(0, 120) : "";
|
|
71
|
+
if (!name) {
|
|
72
|
+
return fail(c, 400, "validation_failed", "name is required — it labels the LINK (\"Client review — round 2\"), not a person.");
|
|
73
|
+
}
|
|
74
|
+
const permissions = Array.isArray(body?.permissions) ? body.permissions : DEFAULT_PERMISSIONS;
|
|
75
|
+
const unknown = permissions.filter((p) => !KNOWN.includes(p));
|
|
76
|
+
if (unknown.length) {
|
|
77
|
+
return fail(c, 422, "validation_failed", `Unknown permission(s): ${unknown.join(", ")}.`);
|
|
78
|
+
}
|
|
79
|
+
// Clamped ON WRITE, per §4.5.1's closing note: one default (14), one
|
|
80
|
+
// ceiling (90), and the clamp lives here rather than being trusted to a UI.
|
|
81
|
+
const ceiling = now().getTime() + deps.maxTtlDays * DAY;
|
|
82
|
+
const asked = typeof body?.expiresAt === "string" ? Date.parse(body.expiresAt) : NaN;
|
|
83
|
+
const expiresAt = new Date(Math.min(Number.isFinite(asked) ? asked : now().getTime() + deps.ttlDays * DAY, ceiling));
|
|
84
|
+
if (expiresAt <= now()) {
|
|
85
|
+
return fail(c, 400, "validation_failed", "expiresAt must be in the future.");
|
|
86
|
+
}
|
|
87
|
+
let roundId = null;
|
|
88
|
+
if (typeof body?.roundId === "string" && body.roundId) {
|
|
89
|
+
roundId = rowId(body.roundId, ID_PREFIX.round);
|
|
90
|
+
if (!roundId)
|
|
91
|
+
return fail(c, 400, "validation_failed", "roundId is not a valid id.");
|
|
92
|
+
const [round] = await sql `
|
|
93
|
+
SELECT id FROM rounds WHERE id = ${roundId} AND project_id = ${project.id} AND deleted_at IS NULL
|
|
94
|
+
`;
|
|
95
|
+
if (!round)
|
|
96
|
+
return fail(c, 404, "not_found", "That round is not in this project.");
|
|
97
|
+
}
|
|
98
|
+
// 32 bytes, unpadded base64url, 43 chars — foundations §7. Stored as
|
|
99
|
+
// sha256 only, which is why this response is the token's one appearance.
|
|
100
|
+
const token = randomBytes(32).toString("base64url");
|
|
101
|
+
const id = randomUUID();
|
|
102
|
+
const [row] = await sql `
|
|
103
|
+
INSERT INTO review_links
|
|
104
|
+
(id, org_id, project_id, round_id, token_hash, name, email, environments, permissions,
|
|
105
|
+
allow_name_override, max_uses, expires_at)
|
|
106
|
+
VALUES
|
|
107
|
+
(${id}, ${project.org_id}, ${project.id}, ${roundId}, ${sha256(token)}, ${name},
|
|
108
|
+
${typeof body?.email === "string" ? body.email : null},
|
|
109
|
+
${sql.array(Array.isArray(body?.environments) && body.environments.length ? body.environments : ["*"])},
|
|
110
|
+
${sql.array(permissions)},
|
|
111
|
+
${body?.allowNameOverride === false ? false : true},
|
|
112
|
+
${Number.isInteger(body?.maxUses) && body?.maxUses > 0 ? body.maxUses : null},
|
|
113
|
+
${expiresAt})
|
|
114
|
+
RETURNING id, round_id, name, email, environments, permissions, allow_name_override,
|
|
115
|
+
max_uses, use_count, created_at, expires_at, revoked_at, last_used_at
|
|
116
|
+
`;
|
|
117
|
+
c.header("Location", `/api/v1/review-links/${ID_PREFIX.link}${id}`);
|
|
118
|
+
return c.json({
|
|
119
|
+
...shape(row),
|
|
120
|
+
// The one and only time. The dashboard builds `<page>#snag_k=<token>` —
|
|
121
|
+
// the server does not know which page of the reviewed site to point at.
|
|
122
|
+
token,
|
|
123
|
+
fragment: `#snag_k=${token}`,
|
|
124
|
+
}, 201);
|
|
125
|
+
});
|
|
126
|
+
app.get("/api/v1/projects/:uid/review-links", async (c) => {
|
|
127
|
+
const project = await owned(c);
|
|
128
|
+
if (!project)
|
|
129
|
+
return fail(c, 404, "project_not_found", "No project with that uid.");
|
|
130
|
+
const rows = await sql `
|
|
131
|
+
SELECT id, round_id, name, email, environments, permissions, allow_name_override,
|
|
132
|
+
max_uses, use_count, created_at, expires_at, revoked_at, last_used_at
|
|
133
|
+
FROM review_links WHERE project_id = ${project.id} ORDER BY created_at DESC
|
|
134
|
+
`;
|
|
135
|
+
return c.json({ data: rows.map(shape), nextCursor: null });
|
|
136
|
+
});
|
|
137
|
+
app.delete("/api/v1/review-links/:id", async (c) => {
|
|
138
|
+
const id = rowId(c.req.param("id"), ID_PREFIX.link);
|
|
139
|
+
if (!id)
|
|
140
|
+
return fail(c, 404, "not_found", "No such review link.");
|
|
141
|
+
// This route is not project-scoped in the URL (a link id is looked up
|
|
142
|
+
// directly), so the org check has to happen here rather than via
|
|
143
|
+
// `owned()` — org A's owner must not be able to revoke org B's link by
|
|
144
|
+
// guessing/enumerating a `lnk_` id. `review_links.org_id` is denormalised
|
|
145
|
+
// onto the row for exactly this filter.
|
|
146
|
+
const owner = c.get("owner");
|
|
147
|
+
const [link] = await sql `
|
|
148
|
+
SELECT rl.id, rl.project_id, p.uid, rl.revoked_at
|
|
149
|
+
FROM review_links rl JOIN projects p ON p.id = rl.project_id
|
|
150
|
+
WHERE rl.id = ${id} AND rl.org_id = ${owner.orgId}
|
|
151
|
+
`;
|
|
152
|
+
if (!link)
|
|
153
|
+
return fail(c, 404, "not_found", "No such review link.");
|
|
154
|
+
// BOTH, in one transaction. `revoked_at` stops further exchanges; the epoch
|
|
155
|
+
// bump kills the sessions already issued. Doing only the first leaves every
|
|
156
|
+
// reviewer who already redeemed the link working for up to 12 hours.
|
|
157
|
+
await sql.begin(async (tx) => {
|
|
158
|
+
await tx `UPDATE review_links SET revoked_at = now() WHERE id = ${id} AND revoked_at IS NULL`;
|
|
159
|
+
await tx `UPDATE projects SET session_epoch = session_epoch + 1 WHERE id = ${link.project_id}`;
|
|
160
|
+
});
|
|
161
|
+
// So the operator who clicked sees it on THIS node without waiting out the
|
|
162
|
+
// 10 s TTL. Other nodes catch up on their own expiry, which is the bound
|
|
163
|
+
// §4.4 states.
|
|
164
|
+
projects.invalidate(link.uid);
|
|
165
|
+
return c.body(null, 204);
|
|
166
|
+
});
|
|
167
|
+
return app;
|
|
168
|
+
}
|
|
169
|
+
//# sourceMappingURL=reviewLinks.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reviewLinks.js","sourceRoot":"","sources":["../../src/routes/reviewLinks.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { ID_PREFIX } from "../vendor/types/names.js";
|
|
4
|
+
function fail(c, status, code, message) {
|
|
5
|
+
return c.json({ error: { code, message, requestId: c.get("requestId") } }, status);
|
|
6
|
+
}
|
|
7
|
+
const UUID = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
8
|
+
const rowId = (wire) => {
|
|
9
|
+
const bare = wire.startsWith(ID_PREFIX.round) ? wire.slice(ID_PREFIX.round.length) : wire;
|
|
10
|
+
return UUID.test(bare) ? bare.toLowerCase() : null;
|
|
11
|
+
};
|
|
12
|
+
const shape = (r) => ({
|
|
13
|
+
id: ID_PREFIX.round + r.id,
|
|
14
|
+
number: r.number,
|
|
15
|
+
name: r.name,
|
|
16
|
+
closed: !!r.closed_at,
|
|
17
|
+
closedAt: r.closed_at?.toISOString(),
|
|
18
|
+
createdAt: r.created_at.toISOString(),
|
|
19
|
+
threads: r.threads,
|
|
20
|
+
// D2's safeguard, server-computed. A closed round still reports this — the
|
|
21
|
+
// whole point is that closing a round must not make its unfinished work
|
|
22
|
+
// disappear from view.
|
|
23
|
+
unresolved: r.unresolved,
|
|
24
|
+
});
|
|
25
|
+
export function roundRoutes(deps) {
|
|
26
|
+
const app = new Hono();
|
|
27
|
+
const { sql, projects } = deps;
|
|
28
|
+
/** Owner-only, and owner OF THIS PROJECT. Returns the project or null. */
|
|
29
|
+
const owned = async (c) => {
|
|
30
|
+
const uid = c.req.param("uid");
|
|
31
|
+
if (!uid)
|
|
32
|
+
return null;
|
|
33
|
+
const p = await projects.get(uid).catch(() => null);
|
|
34
|
+
if (!p)
|
|
35
|
+
return null;
|
|
36
|
+
const owner = c.get("owner");
|
|
37
|
+
return owner && owner.orgId === p.org_id ? p : null;
|
|
38
|
+
};
|
|
39
|
+
/** Every round on the project, newest first, each with its counts. */
|
|
40
|
+
app.get("/api/v1/projects/:uid/rounds", async (c) => {
|
|
41
|
+
const p = await owned(c);
|
|
42
|
+
if (!p)
|
|
43
|
+
return fail(c, 404, "project_not_found", "No project with that uid.");
|
|
44
|
+
const rows = await sql `
|
|
45
|
+
SELECT r.id, r.number, r.name, r.closed_at, r.created_at,
|
|
46
|
+
count(t.id)::int AS threads,
|
|
47
|
+
count(t.id) FILTER (WHERE t.status <> 'resolved')::int AS unresolved
|
|
48
|
+
FROM rounds r
|
|
49
|
+
LEFT JOIN threads t ON t.round_id = r.id AND t.deleted_at IS NULL
|
|
50
|
+
WHERE r.project_id = ${p.id} AND r.deleted_at IS NULL
|
|
51
|
+
GROUP BY r.id
|
|
52
|
+
ORDER BY r.number DESC
|
|
53
|
+
`;
|
|
54
|
+
return c.json({ data: rows.map(shape) });
|
|
55
|
+
});
|
|
56
|
+
/**
|
|
57
|
+
* Open a new round. The previous open one is CLOSED in the same transaction,
|
|
58
|
+
* because "one active round at a time" is the model and two open rounds have
|
|
59
|
+
* no defined answer to "where does the next thread go".
|
|
60
|
+
*
|
|
61
|
+
* Numbering is allocated the same way thread keys are (§2.1): an UPDATE that
|
|
62
|
+
* row-locks the project, not a sequence, because gapless is the property and
|
|
63
|
+
* a sequence cannot promise it.
|
|
64
|
+
*/
|
|
65
|
+
app.post("/api/v1/projects/:uid/rounds", async (c) => {
|
|
66
|
+
const p = await owned(c);
|
|
67
|
+
if (!p)
|
|
68
|
+
return fail(c, 404, "project_not_found", "No project with that uid.");
|
|
69
|
+
const body = (await c.req.json().catch(() => null));
|
|
70
|
+
const name = typeof body?.name === "string" ? body.name.trim().slice(0, 80) : "";
|
|
71
|
+
const owner = c.get("owner");
|
|
72
|
+
const row = await sql.begin(async (tx) => {
|
|
73
|
+
await tx `SELECT set_config('app.org_id', ${p.org_id}, true)`;
|
|
74
|
+
const [next] = await tx `
|
|
75
|
+
SELECT COALESCE(max(number), 0) + 1 AS n FROM rounds WHERE project_id = ${p.id}
|
|
76
|
+
`;
|
|
77
|
+
const number = next.n;
|
|
78
|
+
const id = randomUUID();
|
|
79
|
+
const [created] = await tx `
|
|
80
|
+
WITH closed AS (
|
|
81
|
+
UPDATE rounds SET closed_at = now(), updated_at = now()
|
|
82
|
+
WHERE project_id = ${p.id}::uuid AND closed_at IS NULL AND deleted_at IS NULL
|
|
83
|
+
RETURNING id
|
|
84
|
+
),
|
|
85
|
+
made AS (
|
|
86
|
+
INSERT INTO rounds (id, org_id, project_id, number, name, created_by)
|
|
87
|
+
VALUES (${id}::uuid, ${p.org_id}::uuid, ${p.id}::uuid, ${number}::int,
|
|
88
|
+
${name || `Round ${number}`}::text, ${owner.userId}::uuid)
|
|
89
|
+
RETURNING id, number, name, closed_at, created_at
|
|
90
|
+
)
|
|
91
|
+
SELECT made.*, 0::int AS threads, 0::int AS unresolved FROM made
|
|
92
|
+
`;
|
|
93
|
+
return created;
|
|
94
|
+
});
|
|
95
|
+
return c.json(shape(row), 201);
|
|
96
|
+
});
|
|
97
|
+
/**
|
|
98
|
+
* Rename, close or reopen. Never renumber, and never move threads: `number`
|
|
99
|
+
* is display order minted once, and a thread's round is immutable (D2).
|
|
100
|
+
*/
|
|
101
|
+
app.patch("/api/v1/rounds/:id", async (c) => {
|
|
102
|
+
const owner = c.get("owner");
|
|
103
|
+
if (!owner)
|
|
104
|
+
return fail(c, 401, "unauthorized", "Sign in to the dashboard first.");
|
|
105
|
+
const id = rowId(c.req.param("id"));
|
|
106
|
+
if (!id)
|
|
107
|
+
return fail(c, 404, "not_found", "No such round.");
|
|
108
|
+
const body = (await c.req.json().catch(() => null));
|
|
109
|
+
const name = typeof body?.name === "string" ? body.name.trim().slice(0, 80) : null;
|
|
110
|
+
const closed = typeof body?.closed === "boolean" ? body.closed : null;
|
|
111
|
+
if (name === null && closed === null) {
|
|
112
|
+
return fail(c, 400, "validation_failed", "Send a name, or closed: true/false.");
|
|
113
|
+
}
|
|
114
|
+
if (name !== null && !name)
|
|
115
|
+
return fail(c, 400, "validation_failed", "A round needs a name.");
|
|
116
|
+
const [row] = await sql `
|
|
117
|
+
WITH upd AS (
|
|
118
|
+
UPDATE rounds SET
|
|
119
|
+
name = ${name ?? sql `name`},
|
|
120
|
+
closed_at = ${closed === null ? sql `closed_at` : closed ? sql `now()` : sql `NULL`},
|
|
121
|
+
updated_at = now()
|
|
122
|
+
WHERE id = ${id}::uuid AND org_id = ${owner.orgId}::uuid AND deleted_at IS NULL
|
|
123
|
+
RETURNING id, number, name, closed_at, created_at
|
|
124
|
+
)
|
|
125
|
+
SELECT upd.*,
|
|
126
|
+
(SELECT count(*)::int FROM threads t WHERE t.round_id = upd.id AND t.deleted_at IS NULL) AS threads,
|
|
127
|
+
(SELECT count(*)::int FROM threads t
|
|
128
|
+
WHERE t.round_id = upd.id AND t.deleted_at IS NULL AND t.status <> 'resolved') AS unresolved
|
|
129
|
+
FROM upd
|
|
130
|
+
`;
|
|
131
|
+
if (!row)
|
|
132
|
+
return fail(c, 404, "not_found", "No such round.");
|
|
133
|
+
return c.json(shape(row));
|
|
134
|
+
});
|
|
135
|
+
return app;
|
|
136
|
+
}
|
|
137
|
+
//# sourceMappingURL=rounds.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rounds.js","sourceRoot":"","sources":["../../src/routes/rounds.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,IAAI,EAAgB,MAAM,MAAM,CAAC;AAC1C,OAAO,EAAE,UAAU,EAAE,MAAM,aAAa,CAAC;AAEzC,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AAkCjD,SAAS,IAAI,CAAC,CAAM,EAAE,MAAc,EAAE,IAAY,EAAE,OAAe;IACjE,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,EAAE,IAAI,EAAE,OAAO,EAAE,SAAS,EAAE,CAAC,CAAC,GAAG,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,MAAa,CAAC,CAAC;AAC5F,CAAC;AAED,MAAM,IAAI,GAAG,iEAAiE,CAAC;AAC/E,MAAM,KAAK,GAAG,CAAC,IAAY,EAAiB,EAAE;IAC5C,MAAM,IAAI,GAAG,IAAI,CAAC,UAAU,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IAC1F,OAAO,IAAI,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AACrD,CAAC,CAAC;AAYF,MAAM,KAAK,GAAG,CAAC,CAAM,EAAE,EAAE,CAAC,CAAC;IACzB,EAAE,EAAE,SAAS,CAAC,KAAK,GAAG,CAAC,CAAC,EAAE;IAC1B,MAAM,EAAE,CAAC,CAAC,MAAM;IAChB,IAAI,EAAE,CAAC,CAAC,IAAI;IACZ,MAAM,EAAE,CAAC,CAAC,CAAC,CAAC,SAAS;IACrB,QAAQ,EAAE,CAAC,CAAC,SAAS,EAAE,WAAW,EAAE;IACpC,SAAS,EAAE,CAAC,CAAC,UAAU,CAAC,WAAW,EAAE;IACrC,OAAO,EAAE,CAAC,CAAC,OAAO;IAClB,2EAA2E;IAC3E,wEAAwE;IACxE,uBAAuB;IACvB,UAAU,EAAE,CAAC,CAAC,UAAU;CACzB,CAAC,CAAC;AAEH,MAAM,UAAU,WAAW,CAAC,IAAe;IACzC,MAAM,GAAG,GAAG,IAAI,IAAI,EAAO,CAAC;IAC5B,MAAM,EAAE,GAAG,EAAE,QAAQ,EAAE,GAAG,IAAI,CAAC;IAE/B,0EAA0E;IAC1E,MAAM,KAAK,GAAG,KAAK,EAAE,CAAM,EAAE,EAAE;QAC7B,MAAM,GAAG,GAAG,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;QAC/B,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC;QACtB,MAAM,CAAC,GAAG,MAAM,QAAQ,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAC;QACpD,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC;QACpB,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,OAAO,KAAK,IAAI,KAAK,CAAC,KAAK,KAAK,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;IACtD,CAAC,CAAC;IAEF,sEAAsE;IACtE,GAAG,CAAC,GAAG,CAAC,8BAA8B,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAClD,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,2BAA2B,CAAC,CAAC;QAC9E,MAAM,IAAI,GAAG,MAAM,GAAG,CAAO;;;;;;6BAMJ,CAAC,CAAC,EAAE;;;KAG5B,CAAC;QACF,OAAO,CAAC,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;IAC3C,CAAC,CAAC,CAAC;IAEH;;;;;;;;OAQG;IACH,GAAG,CAAC,IAAI,CAAC,8BAA8B,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QACnD,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,CAAC,CAAC,CAAC;QACzB,IAAI,CAAC,CAAC;YAAE,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,2BAA2B,CAAC,CAAC;QAC9E,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAA8B,CAAC;QACjF,MAAM,IAAI,GAAG,OAAO,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC;QACjF,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAE,CAAC;QAE9B,MAAM,GAAG,GAAG,MAAM,GAAG,CAAC,KAAK,CAAC,KAAK,EAAE,EAAE,EAAE,EAAE;YACvC,MAAM,EAAE,CAAA,mCAAmC,CAAC,CAAC,MAAM,SAAS,CAAC;YAC7D,MAAM,CAAC,IAAI,CAAC,GAAG,MAAM,EAAE,CAAiB;kFACoC,CAAC,CAAC,EAAE;OAC/E,CAAC;YACF,MAAM,MAAM,GAAG,IAAK,CAAC,CAAC,CAAC;YACvB,MAAM,EAAE,GAAG,UAAU,EAAE,CAAC;YACxB,MAAM,CAAC,OAAO,CAAC,GAAG,MAAM,EAAE,CAAO;;;+BAGR,CAAC,CAAC,EAAE;;;;;oBAKf,EAAE,WAAW,CAAC,CAAC,MAAM,WAAW,CAAC,CAAC,EAAE,WAAW,MAAM;oBACrD,IAAI,IAAI,SAAS,MAAM,EAAE,WAAW,KAAK,CAAC,MAAM;;;;OAI7D,CAAC;YACF,OAAO,OAAQ,CAAC;QAClB,CAAC,CAAC,CAAC;QACH,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,EAAE,GAAG,CAAC,CAAC;IACjC,CAAC,CAAC,CAAC;IAEH;;;OAGG;IACH,GAAG,CAAC,KAAK,CAAC,oBAAoB,EAAE,KAAK,EAAE,CAAC,EAAE,EAAE;QAC1C,MAAM,KAAK,GAAG,CAAC,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;QAC7B,IAAI,CAAC,KAAK;YAAE,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,cAAc,EAAE,iCAAiC,CAAC,CAAC;QACnF,MAAM,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC;QACpC,IAAI,CAAC,EAAE;YAAE,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC;QAC5D,MAAM,IAAI,GAAG,CAAC,MAAM,CAAC,CAAC,GAAG,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,CAAgD,CAAC;QACnG,MAAM,IAAI,GAAG,OAAO,IAAI,EAAE,IAAI,KAAK,QAAQ,CAAC,CAAC,CAAC,IAAI,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,KAAK,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC;QACnF,MAAM,MAAM,GAAG,OAAO,IAAI,EAAE,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC;QACtE,IAAI,IAAI,KAAK,IAAI,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;YACrC,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,qCAAqC,CAAC,CAAC;QAClF,CAAC;QACD,IAAI,IAAI,KAAK,IAAI,IAAI,CAAC,IAAI;YAAE,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,mBAAmB,EAAE,uBAAuB,CAAC,CAAC;QAE9F,MAAM,CAAC,GAAG,CAAC,GAAG,MAAM,GAAG,CAAO;;;mBAGf,IAAI,IAAI,GAAG,CAAA,MAAM;wBACZ,MAAM,KAAK,IAAI,CAAC,CAAC,CAAC,GAAG,CAAA,WAAW,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,GAAG,CAAA,OAAO,CAAC,CAAC,CAAC,GAAG,CAAA,MAAM;;qBAErE,EAAE,uBAAuB,KAAK,CAAC,KAAK;;;;;;;;KAQpD,CAAC;QACF,IAAI,CAAC,GAAG;YAAE,OAAO,IAAI,CAAC,CAAC,EAAE,GAAG,EAAE,WAAW,EAAE,gBAAgB,CAAC,CAAC;QAC7D,OAAO,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC;IAC5B,CAAC,CAAC,CAAC;IAEH,OAAO,GAAG,CAAC;AACb,CAAC"}
|