@devdogsuga/backstage 0.1.5 → 0.1.6
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 +79 -16
- package/dist/{catalog-BUOwCd2Q.js → catalog-Dke7bQVz.js} +124 -6
- package/dist/{cli-B2S6gw9o.js → cli-BhZ4je2D.js} +9 -7
- package/dist/cli-Do2nWD7W.js +136 -0
- package/dist/{client-B7HhuTaA.js → client-B_MY9kVI.js} +2 -1
- package/dist/client-DwDGjalU.js +2 -0
- package/dist/commands-5Ou8xJw5.js +225 -0
- package/dist/commands-BQ2NIqub.js +436 -0
- package/dist/commands-CLSxKz-l.js +352 -0
- package/dist/commands-DzALVX5g.js +12 -0
- package/dist/{commands-Bk7IbhPD.js → commands-Sx1fDNNn.js} +2 -1
- package/dist/{commands-BMQ1OZIr.js → commands-zPSnOGzH.js} +3 -2
- package/dist/document-DumoZXoO.js +287 -0
- package/dist/dry-run-3IxPmCtW.js +58 -0
- package/dist/launch.js +3 -3
- package/dist/meetings-DfU867eV.js +118 -0
- package/dist/{options-BTjOf5KP.js → options-DCaqm8BI.js} +1 -57
- package/dist/queries-DeEwfe2c.js +170 -0
- package/dist/read-B-aJJewb.js +141 -0
- package/dist/{vault-C8LYJFOQ.js → vault-CsBC-1Mq.js} +1 -287
- package/package.json +6 -6
- package/dist/client-DzXQHd58.js +0 -2
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
import { T as cliName } from "./telemetry-Bjoz29Hl.js";
|
|
2
|
+
//#region ../cli-core/src/dry-run.ts
|
|
3
|
+
const DRY_RUN_FLAG = "--dry-run";
|
|
4
|
+
/** Commands that forward everything after their name to another tool. */
|
|
5
|
+
const FORWARDING = /* @__PURE__ */ new Set([
|
|
6
|
+
"supabase",
|
|
7
|
+
"wrangler",
|
|
8
|
+
"drizzle-kit",
|
|
9
|
+
"psql",
|
|
10
|
+
"bw",
|
|
11
|
+
"run"
|
|
12
|
+
]);
|
|
13
|
+
let active = false;
|
|
14
|
+
function setDryRun(on) {
|
|
15
|
+
active = on;
|
|
16
|
+
}
|
|
17
|
+
function isDryRun() {
|
|
18
|
+
return active;
|
|
19
|
+
}
|
|
20
|
+
/**
|
|
21
|
+
* Decides whether `argv` asks for a dry run and returns the argv to dispatch.
|
|
22
|
+
*
|
|
23
|
+
* For a forwarding command a leading `--dry-run` is consumed (the tool never
|
|
24
|
+
* sees it) and a later one is the tool's. For any other command the argv is
|
|
25
|
+
* returned as it came, because several commands read `--dry-run` themselves.
|
|
26
|
+
*/
|
|
27
|
+
function resolveDryRun(argv) {
|
|
28
|
+
const firstPositional = argv.findIndex((arg) => !arg.startsWith("-"));
|
|
29
|
+
const lead = firstPositional === -1 ? argv.length : firstPositional;
|
|
30
|
+
const command = argv[firstPositional];
|
|
31
|
+
if (command !== void 0 && FORWARDING.has(command)) {
|
|
32
|
+
const rest = argv.filter((arg, i) => i >= lead || arg !== "--dry-run");
|
|
33
|
+
return {
|
|
34
|
+
dryRun: rest.length !== argv.length,
|
|
35
|
+
rest
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
return {
|
|
39
|
+
dryRun: argv.includes(DRY_RUN_FLAG),
|
|
40
|
+
rest: [...argv]
|
|
41
|
+
};
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* How a command treats `--dry-run`, from the nearest node on its path that
|
|
45
|
+
* says. `undefined` means it did not say, and the dispatcher stops it.
|
|
46
|
+
*/
|
|
47
|
+
function dryRunKind(catalog, path) {
|
|
48
|
+
for (let end = path.length; end > 0; end -= 1) {
|
|
49
|
+
const kind = catalog.findCommand(path.slice(0, end))?.dryRun;
|
|
50
|
+
if (kind !== void 0) return kind;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
/** The command line a stopped command would have run. */
|
|
54
|
+
function wouldRunLine(argv) {
|
|
55
|
+
return `Would run: ${cliName()} ${argv.join(" ")}`;
|
|
56
|
+
}
|
|
57
|
+
//#endregion
|
|
58
|
+
export { wouldRunLine as a, setDryRun as i, isDryRun as n, resolveDryRun as r, dryRunKind as t };
|
package/dist/launch.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { C as discoverRepoRoot, E as setCliName, S as RepoNotFoundError, T as cliName, _ as installFailureLog, d as hasYes, g as stripTierFlag, h as stripNoEnvFlag, m as setNoEnv, n as captureDevtoolsError, o as initDevtoolsTelemetry, p as isNonInteractive, s as lastSentryEventId } from "./telemetry-Bjoz29Hl.js";
|
|
2
|
-
import { c as setMenuEnvHook, d as formatCommand, f as helpPath, n as bareGroupStartPath, t as catalog } from "./catalog-
|
|
2
|
+
import { c as setMenuEnvHook, d as formatCommand, f as helpPath, n as bareGroupStartPath, t as catalog } from "./catalog-Dke7bQVz.js";
|
|
3
3
|
import { i as loadEnvSession, r as loadEnvLoad } from "./peers-B0KDkI6a.js";
|
|
4
4
|
import { t as nonEmpty } from "./connection-D3XsOE3S.js";
|
|
5
|
-
import {
|
|
5
|
+
import { i as setDryRun, n as isDryRun, r as resolveDryRun } from "./dry-run-3IxPmCtW.js";
|
|
6
6
|
import { o as unwrap, r as errorMessage } from "./ui-CdKo8mLw.js";
|
|
7
7
|
import { confirm } from "@clack/prompts";
|
|
8
8
|
//#region ../cli-core/src/pipes.ts
|
|
@@ -322,7 +322,7 @@ async function launchWith(argv, options) {
|
|
|
322
322
|
*/
|
|
323
323
|
async function launch(argv) {
|
|
324
324
|
await launchWith(argv, { dispatch: async (args) => {
|
|
325
|
-
const { main } = await import("./cli-
|
|
325
|
+
const { main } = await import("./cli-BhZ4je2D.js");
|
|
326
326
|
await main(args);
|
|
327
327
|
} });
|
|
328
328
|
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
import { t as UsageError } from "./ui-CdKo8mLw.js";
|
|
2
|
+
import "./cli-Do2nWD7W.js";
|
|
3
|
+
//#region src/production/time.ts
|
|
4
|
+
/**
|
|
5
|
+
* Dates as officers type them. Meetings happen in Athens, so a bare
|
|
6
|
+
* `2026-09-30` means that Eastern day, never UTC's: UTC's date rolls at 20:00
|
|
7
|
+
* Eastern, and the platform's old `to=` filter cut off the evening's meeting
|
|
8
|
+
* on exactly the day it named.
|
|
9
|
+
*/
|
|
10
|
+
const EVENT_TZ = "America/New_York";
|
|
11
|
+
const DAY = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
|
+
function isDay(text) {
|
|
13
|
+
return DAY.test(text);
|
|
14
|
+
}
|
|
15
|
+
/** Minutes `zone` is ahead of UTC at `at` (negative west of Greenwich). */
|
|
16
|
+
function offsetMinutes(at, zone) {
|
|
17
|
+
const parts = new Intl.DateTimeFormat("en-US", {
|
|
18
|
+
timeZone: zone,
|
|
19
|
+
hourCycle: "h23",
|
|
20
|
+
year: "numeric",
|
|
21
|
+
month: "2-digit",
|
|
22
|
+
day: "2-digit",
|
|
23
|
+
hour: "2-digit",
|
|
24
|
+
minute: "2-digit",
|
|
25
|
+
second: "2-digit"
|
|
26
|
+
}).formatToParts(at);
|
|
27
|
+
const get = (type) => Number(parts.find((p) => p.type === type)?.value);
|
|
28
|
+
const asUtc = Date.UTC(get("year"), get("month") - 1, get("day"), get("hour"), get("minute"), get("second"));
|
|
29
|
+
return Math.round((asUtc - at.getTime()) / 6e4);
|
|
30
|
+
}
|
|
31
|
+
/** Midnight at the start of `day` (YYYY-MM-DD) in {@link EVENT_TZ}. */
|
|
32
|
+
function startOfDay(day, zone = EVENT_TZ) {
|
|
33
|
+
const utcMidnight = /* @__PURE__ */ new Date(`${day}T00:00:00Z`);
|
|
34
|
+
const probe = new Date(utcMidnight.getTime() + 216e5);
|
|
35
|
+
return /* @__PURE__ */ new Date(utcMidnight.getTime() - offsetMinutes(probe, zone) * 6e4);
|
|
36
|
+
}
|
|
37
|
+
/** Midnight at the start of the day after `day`. */
|
|
38
|
+
function endOfDay(day, zone = EVENT_TZ) {
|
|
39
|
+
const next = /* @__PURE__ */ new Date(`${day}T00:00:00Z`);
|
|
40
|
+
next.setUTCDate(next.getUTCDate() + 1);
|
|
41
|
+
return startOfDay(next.toISOString().slice(0, 10), zone);
|
|
42
|
+
}
|
|
43
|
+
/** The Eastern calendar date of `at`, as YYYY-MM-DD. */
|
|
44
|
+
function dayOf(at, zone = EVENT_TZ) {
|
|
45
|
+
return new Intl.DateTimeFormat("en-CA", {
|
|
46
|
+
timeZone: zone,
|
|
47
|
+
year: "numeric",
|
|
48
|
+
month: "2-digit",
|
|
49
|
+
day: "2-digit"
|
|
50
|
+
}).format(at);
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* A `--from`/`--to` value: a day (the whole Eastern day, `to` inclusive) or
|
|
54
|
+
* a full ISO timestamp (exact). Returns the bound to compare with: `from` is
|
|
55
|
+
* inclusive, `to` exclusive.
|
|
56
|
+
*/
|
|
57
|
+
function parseBound(flag, text) {
|
|
58
|
+
if (!text) return void 0;
|
|
59
|
+
if (isDay(text)) return flag === "--from" ? startOfDay(text) : endOfDay(text);
|
|
60
|
+
const at = new Date(text);
|
|
61
|
+
if (/T\d{2}:\d{2}/.test(text) && !Number.isNaN(at.getTime())) return at;
|
|
62
|
+
throw new UsageError(`${flag} takes a day (2026-09-30) or a timestamp (2026-09-30T18:00:00-04:00), not "${text}".`);
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
//#region src/production/meetings.ts
|
|
66
|
+
/**
|
|
67
|
+
* Naming a meeting on the command line: `--meeting <day|slug|config id|uuid>`.
|
|
68
|
+
*
|
|
69
|
+
* Slugs are the meeting's Eastern date (`2026-09-09`), with `-2` for a second
|
|
70
|
+
* meeting that day, so a day is how officers will usually name one. A day with
|
|
71
|
+
* two meetings is ambiguous and the error lists both slugs rather than picking.
|
|
72
|
+
*/
|
|
73
|
+
/** Live meetings an argument could mean: by id, slug or config id, or on that Eastern day. */
|
|
74
|
+
const MEETING_QUERY = `
|
|
75
|
+
select
|
|
76
|
+
m."id"::text as "id",
|
|
77
|
+
m."slug",
|
|
78
|
+
m."configId",
|
|
79
|
+
coalesce(m."nameOverride", m."kind", 'Meeting') as "title",
|
|
80
|
+
m."startsAt",
|
|
81
|
+
m."endsAt",
|
|
82
|
+
m."cancelledAt",
|
|
83
|
+
m."countsForCredit"
|
|
84
|
+
from "platform"."meetings" m
|
|
85
|
+
where m."deletedAt" is null
|
|
86
|
+
and (
|
|
87
|
+
m."id"::text = $1 or m."slug" = $1 or m."configId" = $1
|
|
88
|
+
or ($2::boolean and (m."startsAt" at time zone '${EVENT_TZ}')::date = $1::date)
|
|
89
|
+
)
|
|
90
|
+
order by m."startsAt"
|
|
91
|
+
`;
|
|
92
|
+
function findMeetingsWith(sql) {
|
|
93
|
+
return async (named) => {
|
|
94
|
+
return (await sql.unsafe(MEETING_QUERY, [named, isDay(named)])).map((r) => ({
|
|
95
|
+
id: String(r.id),
|
|
96
|
+
slug: String(r.slug),
|
|
97
|
+
configId: r.configId ?? null,
|
|
98
|
+
title: String(r.title),
|
|
99
|
+
startsAt: r.startsAt,
|
|
100
|
+
endsAt: r.endsAt,
|
|
101
|
+
cancelledAt: r.cancelledAt ?? null,
|
|
102
|
+
countsForCredit: r.countsForCredit === true
|
|
103
|
+
}));
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
/** The one meeting `named` means, or a usage error saying why there isn't one. */
|
|
107
|
+
function pickMeeting(named, found) {
|
|
108
|
+
const exact = found.filter((m) => m.id === named || m.slug === named || m.configId === named);
|
|
109
|
+
const candidates = exact.length > 0 ? exact : found;
|
|
110
|
+
if (candidates.length === 1) return candidates[0];
|
|
111
|
+
if (candidates.length === 0) throw new UsageError(`No meeting matches "${named}". Name it by day (2026-09-09), slug, config id or id.`);
|
|
112
|
+
throw new UsageError(`"${named}" could be ${candidates.map((m) => `${m.slug} (${m.title})`).join(" or ")}. Pass the slug.`);
|
|
113
|
+
}
|
|
114
|
+
function describeMeeting(m) {
|
|
115
|
+
return `${m.title}, ${dayOf(m.startsAt)} (${m.slug})`;
|
|
116
|
+
}
|
|
117
|
+
//#endregion
|
|
118
|
+
export { parseBound as i, findMeetingsWith as n, pickMeeting as r, describeMeeting as t };
|
|
@@ -1,59 +1,3 @@
|
|
|
1
|
-
import { T as cliName } from "./telemetry-Bjoz29Hl.js";
|
|
2
|
-
//#region ../cli-core/src/dry-run.ts
|
|
3
|
-
const DRY_RUN_FLAG = "--dry-run";
|
|
4
|
-
/** Commands that forward everything after their name to another tool. */
|
|
5
|
-
const FORWARDING = /* @__PURE__ */ new Set([
|
|
6
|
-
"supabase",
|
|
7
|
-
"wrangler",
|
|
8
|
-
"drizzle-kit",
|
|
9
|
-
"psql",
|
|
10
|
-
"bw",
|
|
11
|
-
"run"
|
|
12
|
-
]);
|
|
13
|
-
let active = false;
|
|
14
|
-
function setDryRun(on) {
|
|
15
|
-
active = on;
|
|
16
|
-
}
|
|
17
|
-
function isDryRun() {
|
|
18
|
-
return active;
|
|
19
|
-
}
|
|
20
|
-
/**
|
|
21
|
-
* Decides whether `argv` asks for a dry run and returns the argv to dispatch.
|
|
22
|
-
*
|
|
23
|
-
* For a forwarding command a leading `--dry-run` is consumed (the tool never
|
|
24
|
-
* sees it) and a later one is the tool's. For any other command the argv is
|
|
25
|
-
* returned as it came, because several commands read `--dry-run` themselves.
|
|
26
|
-
*/
|
|
27
|
-
function resolveDryRun(argv) {
|
|
28
|
-
const firstPositional = argv.findIndex((arg) => !arg.startsWith("-"));
|
|
29
|
-
const lead = firstPositional === -1 ? argv.length : firstPositional;
|
|
30
|
-
const command = argv[firstPositional];
|
|
31
|
-
if (command !== void 0 && FORWARDING.has(command)) {
|
|
32
|
-
const rest = argv.filter((arg, i) => i >= lead || arg !== "--dry-run");
|
|
33
|
-
return {
|
|
34
|
-
dryRun: rest.length !== argv.length,
|
|
35
|
-
rest
|
|
36
|
-
};
|
|
37
|
-
}
|
|
38
|
-
return {
|
|
39
|
-
dryRun: argv.includes(DRY_RUN_FLAG),
|
|
40
|
-
rest: [...argv]
|
|
41
|
-
};
|
|
42
|
-
}
|
|
43
|
-
/**
|
|
44
|
-
* How a command treats `--dry-run`, from the nearest node on its path that
|
|
45
|
-
* says. `undefined` means it did not say, and the dispatcher stops it.
|
|
46
|
-
*/
|
|
47
|
-
function dryRunKind(catalog, path) {
|
|
48
|
-
for (let end = path.length; end > 0; end -= 1) {
|
|
49
|
-
const kind = catalog.findCommand(path.slice(0, end))?.dryRun;
|
|
50
|
-
if (kind !== void 0) return kind;
|
|
51
|
-
}
|
|
52
|
-
}
|
|
53
|
-
/** The command line a stopped command would have run. */
|
|
54
|
-
function wouldRunLine(argv) {
|
|
55
|
-
return `Would run: ${cliName()} ${argv.join(" ")}`;
|
|
56
|
-
}
|
|
57
1
|
const QR_FLAGS = {
|
|
58
2
|
text: {
|
|
59
3
|
name: "text",
|
|
@@ -180,4 +124,4 @@ function qrParseOptions() {
|
|
|
180
124
|
return Object.fromEntries([...Object.values(QR_FLAGS), ...Object.values(QR_EXTRA_FLAGS)].map((flag) => [flag.name, { type: "string" }]));
|
|
181
125
|
}
|
|
182
126
|
//#endregion
|
|
183
|
-
export {
|
|
127
|
+
export { qrParseOptions as i, QR_FLAGS as n, qrCatalogOptions as r, QR_EXTRA_FLAGS as t };
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
//#region src/export/queries.ts
|
|
2
|
+
/**
|
|
3
|
+
* The three exports, moved from the platform's `server/export/` with their
|
|
4
|
+
* columns unchanged: anything downstream that read `/export/stars` reads
|
|
5
|
+
* `backstage export stars` the same way.
|
|
6
|
+
*
|
|
7
|
+
* Each query selects exactly its columns, aliased to the header names, in
|
|
8
|
+
* header order. Parameters: `$1` from (inclusive), `$2` to (exclusive), `$3`
|
|
9
|
+
* a meeting id; each null when not given.
|
|
10
|
+
*/
|
|
11
|
+
const EXPORT_KINDS = [
|
|
12
|
+
"stars",
|
|
13
|
+
"attendance",
|
|
14
|
+
"reflections"
|
|
15
|
+
];
|
|
16
|
+
/** The member columns every export leads with. */
|
|
17
|
+
const MEMBER = (userId) => `
|
|
18
|
+
${userId} as "user_id",
|
|
19
|
+
p."preferredName" as "preferred_name",
|
|
20
|
+
u."email" as "email",
|
|
21
|
+
(
|
|
22
|
+
select i."identity_data" ->> 'user_name'
|
|
23
|
+
from "auth"."identities" i
|
|
24
|
+
where i."user_id" = ${userId} and i."provider" = 'github'
|
|
25
|
+
limit 1
|
|
26
|
+
) as "github_login"`;
|
|
27
|
+
const MEMBER_COLUMNS = [
|
|
28
|
+
"user_id",
|
|
29
|
+
"preferred_name",
|
|
30
|
+
"email",
|
|
31
|
+
"github_login"
|
|
32
|
+
];
|
|
33
|
+
const MEMBER_JOINS = (userId) => `
|
|
34
|
+
left join "platform"."profile" p on p."userId" = ${userId}
|
|
35
|
+
left join "auth"."users" u on u."id" = ${userId}`;
|
|
36
|
+
const EXPORTS = {
|
|
37
|
+
/**
|
|
38
|
+
* One row per earned star: a meeting attended or a competition entered.
|
|
39
|
+
* Attendance is a meeting fact and participation a competition fact;
|
|
40
|
+
* neither is attributed to a workshop.
|
|
41
|
+
*/
|
|
42
|
+
stars: {
|
|
43
|
+
summary: "One row per earned meeting or competition star.",
|
|
44
|
+
columns: [
|
|
45
|
+
...MEMBER_COLUMNS,
|
|
46
|
+
"activity_type",
|
|
47
|
+
"activity_id",
|
|
48
|
+
"activity_starts_at",
|
|
49
|
+
"earned_at",
|
|
50
|
+
"meeting_id",
|
|
51
|
+
"competition_id",
|
|
52
|
+
"meeting_star",
|
|
53
|
+
"competition_star",
|
|
54
|
+
"won"
|
|
55
|
+
],
|
|
56
|
+
sql: `
|
|
57
|
+
select ${MEMBER("ms.\"userId\"")},
|
|
58
|
+
ms."activityType" as "activity_type",
|
|
59
|
+
ms."activityId" as "activity_id",
|
|
60
|
+
ms."startsAt" as "activity_starts_at",
|
|
61
|
+
ms."earnedAt" as "earned_at",
|
|
62
|
+
ms."meetingId" as "meeting_id",
|
|
63
|
+
ms."competitionId" as "competition_id",
|
|
64
|
+
ms."activityType" = 'meeting' as "meeting_star",
|
|
65
|
+
ms."activityType" = 'competition' as "competition_star",
|
|
66
|
+
ms."won" as "won"
|
|
67
|
+
from "platform"."memberStars" ms
|
|
68
|
+
${MEMBER_JOINS("ms.\"userId\"")}
|
|
69
|
+
where ($1::timestamptz is null or ms."startsAt" >= $1::timestamptz)
|
|
70
|
+
and ($2::timestamptz is null or ms."startsAt" < $2::timestamptz)
|
|
71
|
+
and $3::uuid is null
|
|
72
|
+
order by ms."startsAt", ms."userId", ms."activityType", ms."activityId"`
|
|
73
|
+
},
|
|
74
|
+
/**
|
|
75
|
+
* One row per check-in, the raw ledger under the stars: an officer
|
|
76
|
+
* reconciling a night sees every scan (and every imported row, method
|
|
77
|
+
* `import`) rather than a derived boolean.
|
|
78
|
+
*/
|
|
79
|
+
attendance: {
|
|
80
|
+
summary: "One row per check-in: who, which meeting, when, and how.",
|
|
81
|
+
columns: [
|
|
82
|
+
...MEMBER_COLUMNS,
|
|
83
|
+
"meeting_config_id",
|
|
84
|
+
"meeting_title",
|
|
85
|
+
"meeting_starts_at",
|
|
86
|
+
"checked_in_at",
|
|
87
|
+
"check_in_method",
|
|
88
|
+
"counts_for_credit"
|
|
89
|
+
],
|
|
90
|
+
sql: `
|
|
91
|
+
select ${MEMBER("a.\"userId\"")},
|
|
92
|
+
m."configId" as "meeting_config_id",
|
|
93
|
+
coalesce(m."nameOverride", m."kind", 'Meeting') as "meeting_title",
|
|
94
|
+
m."startsAt" as "meeting_starts_at",
|
|
95
|
+
a."recordedAt" as "checked_in_at",
|
|
96
|
+
a."method"::text as "check_in_method",
|
|
97
|
+
m."countsForCredit" as "counts_for_credit"
|
|
98
|
+
from "platform"."attendance" a
|
|
99
|
+
join "platform"."meetings" m on m."id" = a."meetingId"
|
|
100
|
+
${MEMBER_JOINS("a.\"userId\"")}
|
|
101
|
+
where ($1::timestamptz is null or m."startsAt" >= $1::timestamptz)
|
|
102
|
+
and ($2::timestamptz is null or m."startsAt" < $2::timestamptz)
|
|
103
|
+
and ($3::uuid is null or a."meetingId" = $3::uuid)
|
|
104
|
+
order by m."startsAt", a."userId", a."id"`
|
|
105
|
+
},
|
|
106
|
+
/**
|
|
107
|
+
* One row per reflection, carrying the member's current text. The revision
|
|
108
|
+
* history stays in `reflectionRevisions`; this is "what did people write",
|
|
109
|
+
* not "who edited what".
|
|
110
|
+
*/
|
|
111
|
+
reflections: {
|
|
112
|
+
summary: "One row per reflection, with the member's current text.",
|
|
113
|
+
columns: [
|
|
114
|
+
...MEMBER_COLUMNS,
|
|
115
|
+
"activity_type",
|
|
116
|
+
"activity_id",
|
|
117
|
+
"activity_title",
|
|
118
|
+
"content",
|
|
119
|
+
"submitted_at",
|
|
120
|
+
"created_at",
|
|
121
|
+
"updated_at",
|
|
122
|
+
"revision_count"
|
|
123
|
+
],
|
|
124
|
+
sql: `
|
|
125
|
+
select ${MEMBER("r.\"userId\"")},
|
|
126
|
+
case when r."meetingId" is not null then 'meeting' else 'competition' end as "activity_type",
|
|
127
|
+
coalesce(r."meetingId", r."competitionId") as "activity_id",
|
|
128
|
+
case
|
|
129
|
+
when r."meetingId" is not null then (
|
|
130
|
+
select coalesce(m."nameOverride", m."kind", 'Meeting')
|
|
131
|
+
from "platform"."meetings" m where m."id" = r."meetingId"
|
|
132
|
+
)
|
|
133
|
+
else (select c."title" from "platform"."competitions" c where c."id" = r."competitionId")
|
|
134
|
+
end as "activity_title",
|
|
135
|
+
r."content" as "content",
|
|
136
|
+
r."submittedAt" as "submitted_at",
|
|
137
|
+
r."createdAt" as "created_at",
|
|
138
|
+
r."updatedAt" as "updated_at",
|
|
139
|
+
(
|
|
140
|
+
select count(*)::int from "platform"."reflectionRevisions" rr
|
|
141
|
+
where rr."reflectionId" = r."id"
|
|
142
|
+
) as "revision_count"
|
|
143
|
+
from "platform"."reflections" r
|
|
144
|
+
${MEMBER_JOINS("r.\"userId\"")}
|
|
145
|
+
where ($1::timestamptz is null or r."createdAt" >= $1::timestamptz)
|
|
146
|
+
and ($2::timestamptz is null or r."createdAt" < $2::timestamptz)
|
|
147
|
+
and $3::uuid is null
|
|
148
|
+
order by r."createdAt", r."userId", r."id"`
|
|
149
|
+
}
|
|
150
|
+
};
|
|
151
|
+
function exportParams(filters) {
|
|
152
|
+
return [
|
|
153
|
+
filters.from?.toISOString() ?? null,
|
|
154
|
+
filters.to?.toISOString() ?? null,
|
|
155
|
+
filters.meetingId ?? null
|
|
156
|
+
];
|
|
157
|
+
}
|
|
158
|
+
/**
|
|
159
|
+
* The filters as the audit row records them, in the platform's shape: an
|
|
160
|
+
* unfiltered export is an explicit `{}`, distinguishable from a slice.
|
|
161
|
+
*/
|
|
162
|
+
function auditFilters(filters) {
|
|
163
|
+
const out = {};
|
|
164
|
+
if (filters.from) out.from = filters.from.toISOString();
|
|
165
|
+
if (filters.to) out.to = filters.to.toISOString();
|
|
166
|
+
if (filters.meetingId) out.meetingId = filters.meetingId;
|
|
167
|
+
return out;
|
|
168
|
+
}
|
|
169
|
+
//#endregion
|
|
170
|
+
export { exportParams as i, EXPORT_KINDS as n, auditFilters as r, EXPORTS as t };
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { r as errorMessage } from "./ui-CdKo8mLw.js";
|
|
2
|
+
import { d as withConnection, s as ProductionError } from "./cli-Do2nWD7W.js";
|
|
3
|
+
//#region src/production/accounts.ts
|
|
4
|
+
/**
|
|
5
|
+
* Production accounts: reading them, finding one by email, and creating the
|
|
6
|
+
* ones a member list names but nobody has made yet.
|
|
7
|
+
*
|
|
8
|
+
* An email finds its account by the profile's `ugaEmail` first, then by the
|
|
9
|
+
* sign-in address. Officers often sign in with a personal address and have
|
|
10
|
+
* their MyID one recorded as `ugaEmail`; matching only the sign-in address
|
|
11
|
+
* would give them a second, empty account.
|
|
12
|
+
*
|
|
13
|
+
* Accounts are created through the Auth admin API, confirmed and with no
|
|
14
|
+
* invite email, so the member's first sign-in with that address lands on an
|
|
15
|
+
* account that already has their history. Nothing makes a profile when an
|
|
16
|
+
* account is created; each caller inserts its own, with what it knows.
|
|
17
|
+
*/
|
|
18
|
+
const ACCOUNTS_QUERY = `
|
|
19
|
+
select
|
|
20
|
+
u."id" as "userId",
|
|
21
|
+
lower(u."email") as "authEmail",
|
|
22
|
+
lower(p."ugaEmail") as "ugaEmail",
|
|
23
|
+
p."userId" is not null as "hasProfile",
|
|
24
|
+
p."preferredName" as "preferredName",
|
|
25
|
+
p."involvementFirstName" as "involvementFirstName",
|
|
26
|
+
p."involvementLastName" as "involvementLastName"
|
|
27
|
+
from "auth"."users" u
|
|
28
|
+
left join "platform"."profile" p on p."userId" = u."id"
|
|
29
|
+
order by u."created_at"
|
|
30
|
+
`;
|
|
31
|
+
/** {@link ACCOUNTS_QUERY} on an open connection. */
|
|
32
|
+
async function accountsWith(sql) {
|
|
33
|
+
return (await sql.unsafe(ACCOUNTS_QUERY)).map((row) => ({
|
|
34
|
+
userId: String(row.userId),
|
|
35
|
+
authEmail: row.authEmail ?? null,
|
|
36
|
+
ugaEmail: row.ugaEmail ?? null,
|
|
37
|
+
hasProfile: row.hasProfile === true,
|
|
38
|
+
preferredName: row.preferredName ?? null,
|
|
39
|
+
involvementFirstName: row.involvementFirstName ?? null,
|
|
40
|
+
involvementLastName: row.involvementLastName ?? null
|
|
41
|
+
}));
|
|
42
|
+
}
|
|
43
|
+
async function readAccounts(url) {
|
|
44
|
+
return withConnection(url, "Could not read production's accounts", accountsWith);
|
|
45
|
+
}
|
|
46
|
+
/** Finds an account by a lowercased email: `ugaEmail` first, then sign-in. */
|
|
47
|
+
function accountFinder(accounts) {
|
|
48
|
+
const byUga = /* @__PURE__ */ new Map();
|
|
49
|
+
const byAuth = /* @__PURE__ */ new Map();
|
|
50
|
+
for (const a of accounts) {
|
|
51
|
+
if (a.ugaEmail) byUga.set(a.ugaEmail, [...byUga.get(a.ugaEmail) ?? [], a]);
|
|
52
|
+
if (a.authEmail && !byAuth.has(a.authEmail)) byAuth.set(a.authEmail, a);
|
|
53
|
+
}
|
|
54
|
+
return (email) => {
|
|
55
|
+
const viaUga = byUga.get(email);
|
|
56
|
+
return viaUga?.find((a) => a.authEmail === email) ?? viaUga?.[0] ?? byAuth.get(email);
|
|
57
|
+
};
|
|
58
|
+
}
|
|
59
|
+
function createAccountFor(apiUrl, secretKey) {
|
|
60
|
+
let client = null;
|
|
61
|
+
return async (email) => {
|
|
62
|
+
client ??= import("@supabase/supabase-js").then(({ createClient }) => createClient(apiUrl, secretKey, { auth: {
|
|
63
|
+
persistSession: false,
|
|
64
|
+
autoRefreshToken: false,
|
|
65
|
+
detectSessionInUrl: false
|
|
66
|
+
} }));
|
|
67
|
+
const { data, error } = await (await client).auth.admin.createUser({
|
|
68
|
+
email,
|
|
69
|
+
email_confirm: true
|
|
70
|
+
});
|
|
71
|
+
if (error || !data.user) throw new ProductionError(error?.message ?? "no user returned");
|
|
72
|
+
return data.user.id;
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
/** How many accounts are created at once. */
|
|
76
|
+
const CREATE_CONCURRENCY = 4;
|
|
77
|
+
/**
|
|
78
|
+
* Creates an account for each email, a few at a time. A failure is recorded
|
|
79
|
+
* and the rest carry on: one bad address must not cost everyone else.
|
|
80
|
+
*/
|
|
81
|
+
async function createAccounts(emails, create) {
|
|
82
|
+
const created = /* @__PURE__ */ new Map();
|
|
83
|
+
const failed = [];
|
|
84
|
+
const queue = [...emails];
|
|
85
|
+
const worker = async () => {
|
|
86
|
+
for (let email = queue.shift(); email; email = queue.shift()) try {
|
|
87
|
+
created.set(email, await create(email));
|
|
88
|
+
} catch (err) {
|
|
89
|
+
failed.push({
|
|
90
|
+
email,
|
|
91
|
+
reason: errorMessage(err)
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
};
|
|
95
|
+
await Promise.all(Array.from({ length: CREATE_CONCURRENCY }, () => worker()));
|
|
96
|
+
return {
|
|
97
|
+
created,
|
|
98
|
+
failed
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
//#endregion
|
|
102
|
+
//#region src/csv/read.ts
|
|
103
|
+
/**
|
|
104
|
+
* Reading CSV the way spreadsheets write it: RFC 4180 quoting, CRLF or LF, and
|
|
105
|
+
* the byte-order mark Excel puts in front of UTF-8.
|
|
106
|
+
*/
|
|
107
|
+
/** RFC 4180 rows: quoted fields may hold commas, quotes (`""`) and newlines. */
|
|
108
|
+
function parseCsv(text) {
|
|
109
|
+
const rows = [];
|
|
110
|
+
let row = [];
|
|
111
|
+
let field = "";
|
|
112
|
+
let quoted = false;
|
|
113
|
+
const src = text.replace(/^/, "");
|
|
114
|
+
for (let i = 0; i < src.length; i += 1) {
|
|
115
|
+
const ch = src[i];
|
|
116
|
+
if (quoted) {
|
|
117
|
+
if (ch === "\"" && src[i + 1] === "\"") {
|
|
118
|
+
field += "\"";
|
|
119
|
+
i += 1;
|
|
120
|
+
} else if (ch === "\"") quoted = false;
|
|
121
|
+
else field += ch;
|
|
122
|
+
} else if (ch === "\"") quoted = true;
|
|
123
|
+
else if (ch === ",") {
|
|
124
|
+
row.push(field);
|
|
125
|
+
field = "";
|
|
126
|
+
} else if (ch === "\n" || ch === "\r") {
|
|
127
|
+
if (ch === "\r" && src[i + 1] === "\n") i += 1;
|
|
128
|
+
row.push(field);
|
|
129
|
+
rows.push(row);
|
|
130
|
+
row = [];
|
|
131
|
+
field = "";
|
|
132
|
+
} else field += ch;
|
|
133
|
+
}
|
|
134
|
+
if (field !== "" || row.length > 0) {
|
|
135
|
+
row.push(field);
|
|
136
|
+
rows.push(row);
|
|
137
|
+
}
|
|
138
|
+
return rows;
|
|
139
|
+
}
|
|
140
|
+
//#endregion
|
|
141
|
+
export { createAccounts as a, createAccountFor as i, accountFinder as n, readAccounts as o, accountsWith as r, parseCsv as t };
|