@olinfo/quizms-firebase 4.0.1 → 4.1.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/dist/functions.d.ts +2 -11
- package/dist/functions.js +2 -2
- package/dist/package.json.js +1 -1
- package/dist/src/cli/files/functions-index.js +4 -4
- package/dist/src/cli/files/functions-index.js.map +1 -1
- package/dist/src/cli/import.js +85 -96
- package/dist/src/cli/import.js.map +1 -1
- package/dist/src/cli/init.js +8 -8
- package/dist/src/cli/init.js.map +1 -1
- package/dist/src/cli/update-scores.js +21 -32
- package/dist/src/cli/update-scores.js.map +1 -1
- package/dist/src/cli/utils/collection.js +20 -18
- package/dist/src/cli/utils/collection.js.map +1 -1
- package/dist/src/cli/utils/converters-admin.js +31 -32
- package/dist/src/cli/utils/converters-admin.js.map +1 -1
- package/dist/src/cli/utils/region-finder.js +6 -5
- package/dist/src/cli/utils/region-finder.js.map +1 -1
- package/dist/src/cli/utils/storage.js +26 -35
- package/dist/src/cli/utils/storage.js.map +1 -1
- package/dist/src/cli/utils/users.js +13 -12
- package/dist/src/cli/utils/users.js.map +1 -1
- package/dist/src/functions/api/admin.js +25 -10
- package/dist/src/functions/api/admin.js.map +1 -1
- package/dist/src/functions/api/index.js +26 -37
- package/dist/src/functions/api/index.js.map +1 -1
- package/dist/src/functions/api/queries.js +24 -25
- package/dist/src/functions/api/queries.js.map +1 -1
- package/dist/src/functions/api/student.js +62 -72
- package/dist/src/functions/api/student.js.map +1 -1
- package/dist/src/functions/api/teacher.js +104 -111
- package/dist/src/functions/api/teacher.js.map +1 -1
- package/dist/src/functions/api/trpc.js +18 -0
- package/dist/src/functions/api/trpc.js.map +1 -0
- package/dist/src/functions/submissions.js +1 -1
- package/dist/src/functions/submissions.js.map +1 -1
- package/dist/src/models/user.js +2 -2
- package/dist/src/models/user.js.map +1 -1
- package/dist/src/models/website.js +1 -1
- package/dist/src/models/website.js.map +1 -1
- package/dist/src/vite/preset.js +3 -4
- package/dist/src/vite/preset.js.map +1 -1
- package/dist/src/web/admin/announcements.js +9 -10
- package/dist/src/web/admin/announcements.js.map +1 -1
- package/dist/src/web/admin/school-table.js +7 -8
- package/dist/src/web/admin/school-table.js.map +1 -1
- package/dist/src/web/common/api.js +32 -35
- package/dist/src/web/common/api.js.map +1 -1
- package/dist/src/web/common/converters.js +35 -36
- package/dist/src/web/common/converters.js.map +1 -1
- package/dist/src/web/common/token-login.js +11 -11
- package/dist/src/web/common/token-login.js.map +1 -1
- package/dist/src/web/entry/admin.js +5 -7
- package/dist/src/web/entry/admin.js.map +1 -1
- package/dist/src/web/entry/student-restoring.js +18 -20
- package/dist/src/web/entry/student-restoring.js.map +1 -1
- package/dist/src/web/entry/student.js +70 -76
- package/dist/src/web/entry/student.js.map +1 -1
- package/dist/src/web/entry/teacher.js +68 -68
- package/dist/src/web/entry/teacher.js.map +1 -1
- package/dist/src/web/hooks/collection.js +29 -30
- package/dist/src/web/hooks/collection.js.map +1 -1
- package/dist/src/web/hooks/document.js +35 -36
- package/dist/src/web/hooks/document.js.map +1 -1
- package/dist/src/web/hooks/subscription.js +26 -22
- package/dist/src/web/hooks/subscription.js.map +1 -1
- package/package.json +29 -25
- package/dist/src/functions/api/schema.js +0 -42
- package/dist/src/functions/api/schema.js.map +0 -1
package/dist/functions.d.ts
CHANGED
|
@@ -1,19 +1,10 @@
|
|
|
1
|
-
import { CallableRequest } from 'firebase-functions/https';
|
|
2
1
|
import { Change } from 'firebase-functions/firestore';
|
|
3
2
|
import { DocumentSnapshot } from 'firebase-admin/firestore';
|
|
3
|
+
import { Express as Express_2 } from 'express-serve-static-core';
|
|
4
4
|
import { FirestoreAuthEvent } from 'firebase-functions/firestore';
|
|
5
5
|
import { Request as Request_2 } from 'firebase-functions/tasks';
|
|
6
6
|
|
|
7
|
-
export declare
|
|
8
|
-
success: boolean;
|
|
9
|
-
errorCode: string;
|
|
10
|
-
error: string;
|
|
11
|
-
}>;
|
|
12
|
-
|
|
13
|
-
declare type EndpointSuccess<Res> = {
|
|
14
|
-
success: true;
|
|
15
|
-
data: Res;
|
|
16
|
-
};
|
|
7
|
+
export declare const apiHandler: Express_2;
|
|
17
8
|
|
|
18
9
|
export declare function studentUpdatedHandler(event: FirestoreAuthEvent<Change<DocumentSnapshot> | undefined>): Promise<void>;
|
|
19
10
|
|
package/dist/functions.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { default as a } from "./src/functions/api/index.js";
|
|
2
2
|
import { studentUpdatedHandler as t } from "./src/functions/submissions.js";
|
|
3
3
|
import { updateScoresHandler as p } from "./src/functions/update-scores.js";
|
|
4
4
|
export {
|
|
5
|
-
|
|
5
|
+
a as apiHandler,
|
|
6
6
|
t as studentUpdatedHandler,
|
|
7
7
|
p as updateScoresHandler
|
|
8
8
|
};
|
package/dist/package.json.js
CHANGED
|
@@ -1,16 +1,16 @@
|
|
|
1
|
-
const
|
|
1
|
+
const t = `import {
|
|
2
2
|
apiHandler,
|
|
3
3
|
studentUpdatedHandler,
|
|
4
4
|
updateScoresHandler,
|
|
5
5
|
} from "@olinfo/quizms-firebase/functions";
|
|
6
6
|
import { setGlobalOptions } from "firebase-functions";
|
|
7
7
|
import { onDocumentWrittenWithAuthContext } from "firebase-functions/firestore";
|
|
8
|
-
import {
|
|
8
|
+
import { onRequest } from "firebase-functions/https";
|
|
9
9
|
import { onTaskDispatched } from "firebase-functions/tasks";
|
|
10
10
|
|
|
11
11
|
setGlobalOptions({ region: "QUIZMS_REGION" });
|
|
12
12
|
|
|
13
|
-
export const api =
|
|
13
|
+
export const api = onRequest({ cors: ["http://localhost:1234"] }, apiHandler);
|
|
14
14
|
|
|
15
15
|
export const submissions = onDocumentWrittenWithAuthContext(
|
|
16
16
|
"participations/{participationId}/students/{studentId}",
|
|
@@ -20,6 +20,6 @@ export const submissions = onDocumentWrittenWithAuthContext(
|
|
|
20
20
|
export const updateScores = onTaskDispatched({ timeoutSeconds: 540 }, updateScoresHandler);
|
|
21
21
|
`;
|
|
22
22
|
export {
|
|
23
|
-
|
|
23
|
+
t as default
|
|
24
24
|
};
|
|
25
25
|
//# sourceMappingURL=functions-index.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"functions-index.js","sources":["../../../../src/cli/files/functions-index.js?raw"],"sourcesContent":["export default \"import {\\n apiHandler,\\n studentUpdatedHandler,\\n updateScoresHandler,\\n} from \\\"@olinfo/quizms-firebase/functions\\\";\\nimport { setGlobalOptions } from \\\"firebase-functions\\\";\\nimport { onDocumentWrittenWithAuthContext } from \\\"firebase-functions/firestore\\\";\\nimport {
|
|
1
|
+
{"version":3,"file":"functions-index.js","sources":["../../../../src/cli/files/functions-index.js?raw"],"sourcesContent":["export default \"import {\\n apiHandler,\\n studentUpdatedHandler,\\n updateScoresHandler,\\n} from \\\"@olinfo/quizms-firebase/functions\\\";\\nimport { setGlobalOptions } from \\\"firebase-functions\\\";\\nimport { onDocumentWrittenWithAuthContext } from \\\"firebase-functions/firestore\\\";\\nimport { onRequest } from \\\"firebase-functions/https\\\";\\nimport { onTaskDispatched } from \\\"firebase-functions/tasks\\\";\\n\\nsetGlobalOptions({ region: \\\"QUIZMS_REGION\\\" });\\n\\nexport const api = onRequest({ cors: [\\\"http://localhost:1234\\\"] }, apiHandler);\\n\\nexport const submissions = onDocumentWrittenWithAuthContext(\\n \\\"participations/{participationId}/students/{studentId}\\\",\\n studentUpdatedHandler,\\n);\\n\\nexport const updateScores = onTaskDispatched({ timeoutSeconds: 540 }, updateScoresHandler);\\n\""],"names":["functionsIndex"],"mappings":"AAAA,MAAAA,IAAe;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;"}
|
package/dist/src/cli/import.js
CHANGED
|
@@ -1,23 +1,23 @@
|
|
|
1
|
-
import { existsSync as
|
|
2
|
-
import { readFile as
|
|
3
|
-
import
|
|
4
|
-
import { contestSchema as
|
|
5
|
-
import { Rng as
|
|
6
|
-
import { fatal as u, warning as
|
|
7
|
-
import { deleteApp as
|
|
8
|
-
import { groupBy as
|
|
9
|
-
import
|
|
10
|
-
import { glob as
|
|
11
|
-
import
|
|
12
|
-
import { userSchema as
|
|
13
|
-
import { websiteSchema as
|
|
14
|
-
import { importCollection as
|
|
15
|
-
import { contestConverter as
|
|
16
|
-
import { initializeFirebase as
|
|
17
|
-
import { importStorage as
|
|
18
|
-
import { importUsers as
|
|
19
|
-
async function
|
|
20
|
-
if (process.env.QUIZMS_MODE = "contest",
|
|
1
|
+
import { existsSync as I } from "node:fs";
|
|
2
|
+
import { readFile as b } from "node:fs/promises";
|
|
3
|
+
import m from "node:path";
|
|
4
|
+
import { contestSchema as $, participationSchema as g, studentSchema as C, variantsConfigSchema as j, variantSchema as V } from "@olinfo/quizms/models";
|
|
5
|
+
import { Rng as M, validate as P } from "@olinfo/quizms/utils";
|
|
6
|
+
import { fatal as u, warning as x, success as z, load as f, loadContests as v } from "@olinfo/quizms/utils-node";
|
|
7
|
+
import { deleteApp as D } from "firebase-admin/app";
|
|
8
|
+
import { groupBy as O, uniq as y } from "lodash-es";
|
|
9
|
+
import U from "picomatch";
|
|
10
|
+
import { glob as A } from "tinyglobby";
|
|
11
|
+
import l from "zod";
|
|
12
|
+
import { userSchema as N } from "../models/user.js";
|
|
13
|
+
import { websiteSchema as k } from "../models/website.js";
|
|
14
|
+
import { importCollection as w } from "./utils/collection.js";
|
|
15
|
+
import { contestConverter as q, participationConverter as F, studentConverter as Q, variantConverter as B, websiteConverter as E } from "./utils/converters-admin.js";
|
|
16
|
+
import { initializeFirebase as J } from "./utils/initialize.js";
|
|
17
|
+
import { importStorage as R } from "./utils/storage.js";
|
|
18
|
+
import { importUsers as S } from "./utils/users.js";
|
|
19
|
+
async function ht(t) {
|
|
20
|
+
if (process.env.QUIZMS_MODE = "contest", I("data") || u("Cannot find data directory. Make sure you're inside a QuizMS project."), [
|
|
21
21
|
"admins",
|
|
22
22
|
"contests",
|
|
23
23
|
"schools",
|
|
@@ -26,126 +26,115 @@ async function vt(t) {
|
|
|
26
26
|
"teachers",
|
|
27
27
|
"variants",
|
|
28
28
|
"websites"
|
|
29
|
-
].every((
|
|
30
|
-
|
|
29
|
+
].every((n) => !t[n])) {
|
|
30
|
+
x("`Nothing to import. Use `--help` for usage.`");
|
|
31
31
|
return;
|
|
32
32
|
}
|
|
33
|
-
const { app:
|
|
34
|
-
t.admins && await
|
|
33
|
+
const { app: e, bucket: o, db: i } = await J();
|
|
34
|
+
t.admins && await W(i, t), t.contests && await Z(i, t), (t.schools || t.teachers) && await _(i, t), t.students && await G(i, t), t.variants && await H(i, t), t.websites && await K(i, t), t.statements && await L(o, t), z("All done!"), await D(e);
|
|
35
35
|
}
|
|
36
|
-
async function
|
|
37
|
-
const
|
|
38
|
-
await
|
|
36
|
+
async function W(t, s) {
|
|
37
|
+
const e = await f("admins", N.omit({ role: !0 }));
|
|
38
|
+
await S(t, "admin", e, s);
|
|
39
39
|
}
|
|
40
|
-
async function
|
|
41
|
-
const
|
|
42
|
-
await
|
|
40
|
+
async function Z(t, s) {
|
|
41
|
+
const e = await f("contests", $);
|
|
42
|
+
await w(t, "contests", e, q, s);
|
|
43
43
|
}
|
|
44
|
-
async function
|
|
45
|
-
const
|
|
44
|
+
async function _(t, s) {
|
|
45
|
+
const e = g.omit({
|
|
46
46
|
schoolId: !0,
|
|
47
47
|
contestId: !0
|
|
48
48
|
}).extend({
|
|
49
|
-
contestIds:
|
|
50
|
-
password:
|
|
51
|
-
}),
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
const r = c.map((a) => ({
|
|
49
|
+
contestIds: l.union([l.string(), l.array(l.string())]).default("*"),
|
|
50
|
+
password: l.string()
|
|
51
|
+
}), o = await f("schools", e), i = await v();
|
|
52
|
+
if (s.teachers) {
|
|
53
|
+
const n = o.map((a) => ({
|
|
55
54
|
name: a.name,
|
|
56
55
|
username: a.id,
|
|
57
56
|
password: a.password
|
|
58
57
|
}));
|
|
59
|
-
await
|
|
58
|
+
await S(t, "teacher", n, s);
|
|
60
59
|
}
|
|
61
|
-
if (
|
|
62
|
-
const
|
|
63
|
-
for (const a of
|
|
64
|
-
for (const
|
|
65
|
-
if (!
|
|
66
|
-
let
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
else {
|
|
71
|
-
m || (m = await d("variants", y));
|
|
72
|
-
const o = m.find((S) => S.id === a.id);
|
|
73
|
-
o || u(`Missing variants configuration for contest ${a.id}.`), f = new x(`${o.secret}-${o.id}-${e.id}-participation`).sample(o.pdfVariantIds, o.pdfPerSchool);
|
|
74
|
-
}
|
|
75
|
-
r.push({
|
|
76
|
-
id: `${e.id}-${a.id}`,
|
|
77
|
-
schoolId: e.id,
|
|
60
|
+
if (s.schools) {
|
|
61
|
+
const n = [];
|
|
62
|
+
for (const a of i)
|
|
63
|
+
for (const r of o) {
|
|
64
|
+
if (!U.isMatch(a.id, r.contestIds)) continue;
|
|
65
|
+
let c;
|
|
66
|
+
a.hasVariants && (r.pdfVariants ? c = r.pdfVariants.map((d) => `${a.id}-${d}`) : c = new M(`${a.secret}-${a.id}-${r.id}-participation`).sample(a.pdfVariantIds, a.pdfPerSchool)), n.push({
|
|
67
|
+
id: `${r.id}-${a.id}`,
|
|
68
|
+
schoolId: r.id,
|
|
78
69
|
contestId: a.id,
|
|
79
|
-
name:
|
|
70
|
+
name: r.name,
|
|
80
71
|
finalized: !1,
|
|
81
|
-
pdfVariants:
|
|
72
|
+
pdfVariants: c,
|
|
82
73
|
disabled: !1
|
|
83
74
|
});
|
|
84
75
|
}
|
|
85
|
-
await
|
|
76
|
+
await w(t, "participations", n, F, s);
|
|
86
77
|
}
|
|
87
78
|
}
|
|
88
|
-
async function
|
|
89
|
-
const
|
|
79
|
+
async function G(t, s) {
|
|
80
|
+
const e = await f("students", C), o = O(e, "participationId");
|
|
90
81
|
await Promise.all(
|
|
91
|
-
Object.entries(
|
|
92
|
-
([
|
|
82
|
+
Object.entries(o).map(
|
|
83
|
+
([i, n]) => w(
|
|
93
84
|
t,
|
|
94
|
-
`participations/${
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
85
|
+
`participations/${i}/students`,
|
|
86
|
+
n,
|
|
87
|
+
Q,
|
|
88
|
+
s
|
|
98
89
|
)
|
|
99
90
|
)
|
|
100
91
|
);
|
|
101
92
|
}
|
|
102
|
-
async function
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
const
|
|
106
|
-
let
|
|
93
|
+
async function H(t, s) {
|
|
94
|
+
const e = await f("variants", j), o = await Promise.all(
|
|
95
|
+
e.flatMap((i) => y([...i.variantIds, ...i.pdfVariantIds]).map(async (a) => {
|
|
96
|
+
const r = m.join("variants", i.id, a, "answers.json");
|
|
97
|
+
let c;
|
|
107
98
|
try {
|
|
108
|
-
|
|
99
|
+
c = await b(r, "utf8");
|
|
109
100
|
} catch {
|
|
110
|
-
u(`Cannot find schema for variant ${
|
|
101
|
+
u(`Cannot find schema for variant ${a}. Use \`quizms variants\` to generate it.`);
|
|
111
102
|
}
|
|
112
103
|
try {
|
|
113
|
-
return
|
|
114
|
-
} catch (
|
|
115
|
-
u(`Invalid schema for variant ${
|
|
104
|
+
return P(V, JSON.parse(c));
|
|
105
|
+
} catch (d) {
|
|
106
|
+
u(`Invalid schema for variant ${a}: ${d}`);
|
|
116
107
|
}
|
|
117
108
|
}))
|
|
118
109
|
);
|
|
119
|
-
await
|
|
110
|
+
await w(t, "variants", o, B, s);
|
|
120
111
|
}
|
|
121
|
-
async function
|
|
122
|
-
const
|
|
123
|
-
await
|
|
112
|
+
async function K(t, s) {
|
|
113
|
+
const e = await f("websites", k);
|
|
114
|
+
await w(t, "websites", e, E, s);
|
|
124
115
|
}
|
|
125
|
-
async function
|
|
126
|
-
const
|
|
127
|
-
const a =
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
return (await k("*", { cwd: o })).flatMap((l) => {
|
|
133
|
-
const $ = p.extname(l);
|
|
116
|
+
async function L(t, s) {
|
|
117
|
+
const e = await v(), o = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-"), i = e.map(async (n) => {
|
|
118
|
+
const a = y([...n.variantIds, ...n.pdfVariantIds]).map(
|
|
119
|
+
async (r) => {
|
|
120
|
+
const c = m.join("variants", n.id, r), d = m.join("statements", n.id, r);
|
|
121
|
+
return (await A("*", { cwd: c })).flatMap((p) => {
|
|
122
|
+
const h = m.extname(p);
|
|
134
123
|
return [
|
|
135
124
|
[
|
|
136
|
-
|
|
137
|
-
|
|
125
|
+
m.join(c, p),
|
|
126
|
+
m.join(d, `${m.basename(p, h)}-${o}${h}`)
|
|
138
127
|
],
|
|
139
|
-
[
|
|
128
|
+
[m.join(c, p), m.join(d, p)]
|
|
140
129
|
];
|
|
141
130
|
});
|
|
142
131
|
}
|
|
143
132
|
);
|
|
144
|
-
return (await Promise.all(
|
|
133
|
+
return (await Promise.all(a)).flat();
|
|
145
134
|
});
|
|
146
|
-
await
|
|
135
|
+
await R(t, "statements", (await Promise.all(i)).flat(), s);
|
|
147
136
|
}
|
|
148
137
|
export {
|
|
149
|
-
|
|
138
|
+
ht as default
|
|
150
139
|
};
|
|
151
140
|
//# sourceMappingURL=import.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"import.js","sources":["../../../src/cli/import.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport {\n contestSchema,\n type Participation,\n participationSchema,\n studentSchema,\n type VariantsConfig,\n variantSchema,\n variantsConfigSchema,\n} from \"@olinfo/quizms/models\";\nimport { Rng, validate } from \"@olinfo/quizms/utils\";\nimport { fatal, load, success, warning } from \"@olinfo/quizms/utils-node\";\nimport { deleteApp } from \"firebase-admin/app\";\nimport type { Firestore } from \"firebase-admin/firestore\";\nimport { groupBy, uniq } from \"lodash-es\";\nimport picomatch from \"picomatch\";\nimport { glob } from \"tinyglobby\";\nimport z from \"zod\";\n\nimport { userSchema } from \"~/models/user\";\nimport { websiteSchema } from \"~/models/website\";\n\nimport { importCollection } from \"./utils/collection\";\nimport {\n contestConverter,\n participationConverter,\n studentConverter,\n variantConverter,\n websiteConverter,\n} from \"./utils/converters-admin\";\nimport { initializeFirebase } from \"./utils/initialize\";\nimport { importStorage } from \"./utils/storage\";\nimport { importUsers } from \"./utils/users\";\n\ntype ImportOptions = {\n config: string;\n skipExisting?: true;\n delete?: true;\n force?: true;\n\n admins?: true;\n contests?: true;\n schools?: true;\n statements?: true;\n students?: true;\n teachers?: true;\n variants?: true;\n websites?: true;\n};\n\nexport default async function importData(options: ImportOptions) {\n process.env.QUIZMS_MODE = \"contest\";\n\n if (!existsSync(\"data\")) {\n fatal(\"Cannot find data directory. Make sure you're inside a QuizMS project.\");\n }\n\n const collections: (keyof ImportOptions)[] = [\n \"admins\",\n \"contests\",\n \"schools\",\n \"statements\",\n \"students\",\n \"teachers\",\n \"variants\",\n \"websites\",\n ];\n if (collections.every((key) => !options[key])) {\n warning(\"`Nothing to import. Use `--help` for usage.`\");\n return;\n }\n\n const { app, bucket, db } = await initializeFirebase();\n\n if (options.admins) {\n await importAdmins(db, options);\n }\n if (options.contests) {\n await importContests(db, options);\n }\n if (options.schools || options.teachers) {\n await importParticipations(db, options);\n }\n if (options.students) {\n await importStudents(db, options);\n }\n if (options.variants) {\n await importVariants(db, options);\n }\n if (options.websites) {\n await importWebsites(db, options);\n }\n if (options.statements) {\n await importStatements(bucket, options);\n }\n\n success(\"All done!\");\n await deleteApp(app);\n}\n\nasync function importAdmins(db: Firestore, options: ImportOptions) {\n const users = await load(\"admins\", userSchema.omit({ role: true }));\n await importUsers(db, \"admin\", users, options);\n}\n\nasync function importContests(db: Firestore, options: ImportOptions) {\n const contests = await load(\"contests\", contestSchema);\n await importCollection(db, \"contests\", contests, contestConverter, options);\n}\n\nasync function importParticipations(db: Firestore, options: ImportOptions) {\n const schoolSchema = participationSchema\n .omit({\n schoolId: true,\n contestId: true,\n })\n .extend({\n contestIds: z.union([z.string(), z.array(z.string())]).default(\"*\"),\n password: z.string(),\n });\n const schools = await load(\"schools\", schoolSchema);\n const contests = await load(\"contests\", contestSchema);\n let configs: VariantsConfig[] | undefined;\n\n if (options.teachers) {\n const teachers = schools.map((school) => ({\n name: school.name,\n username: school.id,\n password: school.password,\n }));\n await importUsers(db, \"teacher\", teachers, options);\n }\n\n if (options.schools) {\n const participations: Participation[] = [];\n\n for (const contest of contests) {\n for (const school of schools) {\n if (!picomatch.isMatch(contest.id, school.contestIds)) continue;\n\n let pdfVariants: string[] | undefined;\n if (contest.hasVariants) {\n if (school.pdfVariants) {\n pdfVariants = school.pdfVariants.map((id) => `${contest.id}-${id}`);\n } else {\n if (!configs) {\n configs = await load(\"variants\", variantsConfigSchema);\n }\n const config = configs.find((c) => c.id === contest.id);\n if (!config) {\n fatal(`Missing variants configuration for contest ${contest.id}.`);\n }\n\n const rng = new Rng(`${config.secret}-${config.id}-${school.id}-participation`);\n pdfVariants = rng.sample(config.pdfVariantIds, config.pdfPerSchool);\n }\n }\n\n participations.push({\n id: `${school.id}-${contest.id}`,\n schoolId: school.id,\n contestId: contest.id,\n name: school.name,\n finalized: false,\n pdfVariants,\n disabled: false,\n });\n }\n }\n\n await importCollection(db, \"participations\", participations, participationConverter, options);\n }\n}\n\nasync function importStudents(db: Firestore, options: ImportOptions) {\n const students = await load(\"students\", studentSchema);\n const participations = groupBy(students, \"participationId\");\n await Promise.all(\n Object.entries(participations).map(([participationId, students]) =>\n importCollection(\n db,\n `participations/${participationId}/students`,\n students,\n studentConverter,\n options,\n ),\n ),\n );\n}\n\nasync function importVariants(db: Firestore, options: ImportOptions) {\n const variantsConfig = await load(\"variants\", variantsConfigSchema);\n const variants = await Promise.all(\n variantsConfig.flatMap((config) => {\n const ids = uniq([...config.variantIds, ...config.pdfVariantIds]);\n return ids.map(async (id) => {\n const fileName = path.join(\"variants\", config.id, id, \"answers.json\");\n let schema: string;\n try {\n schema = await readFile(fileName, \"utf8\");\n } catch {\n fatal(`Cannot find schema for variant ${id}. Use \\`quizms variants\\` to generate it.`);\n }\n try {\n return validate(variantSchema, JSON.parse(schema));\n } catch (err) {\n fatal(`Invalid schema for variant ${id}: ${err}`);\n }\n });\n }),\n );\n await importCollection(db, \"variants\", variants, variantConverter, options);\n}\n\nasync function importWebsites(db: Firestore, options: ImportOptions) {\n const websites = await load(\"websites\", websiteSchema);\n await importCollection(db, \"websites\", websites, websiteConverter, options);\n}\n\nasync function importStatements(bucket: Bucket, options: ImportOptions) {\n const contests = await load(\"contests\", contestSchema);\n const variantsConfig = await load(\"variants\", variantsConfigSchema);\n\n const timestamp = new Date().toISOString().replace(/:/g, \"-\");\n\n const statements = contests.map(async (contest) => {\n const config = variantsConfig.find((c) => c.id === contest.id);\n if (!config) {\n fatal(`Missing variants configuration for contest ${contest.id}.`);\n }\n\n const files = uniq([...config.variantIds, ...config.pdfVariantIds]).map(\n async (id): Promise<[string, string][]> => {\n const localDir = path.join(\"variants\", config.id, id);\n const remoteDir = path.join(\"statements\", config.id, id);\n const files = await glob(\"*\", { cwd: localDir });\n\n return files.flatMap((file) => {\n const ext = path.extname(file);\n return [\n [\n path.join(localDir, file),\n path.join(remoteDir, `${path.basename(file, ext)}-${timestamp}${ext}`),\n ],\n [path.join(localDir, file), path.join(remoteDir, file)],\n ];\n });\n },\n );\n return (await Promise.all(files)).flat();\n });\n await importStorage(bucket, \"statements\", (await Promise.all(statements)).flat(), options);\n}\n"],"names":["importData","options","existsSync","fatal","key","warning","app","bucket","db","initializeFirebase","importAdmins","importContests","importParticipations","importStudents","importVariants","importWebsites","importStatements","success","deleteApp","users","load","userSchema","importUsers","contests","contestSchema","importCollection","contestConverter","schoolSchema","participationSchema","z","schools","configs","teachers","school","participations","contest","picomatch","pdfVariants","id","variantsConfigSchema","config","c","Rng","participationConverter","students","studentSchema","groupBy","participationId","studentConverter","variantsConfig","variants","uniq","fileName","path","schema","readFile","validate","variantSchema","err","variantConverter","websites","websiteSchema","websiteConverter","timestamp","statements","files","localDir","remoteDir","glob","file","ext","importStorage"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"import.js","sources":["../../../src/cli/import.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\nimport path from \"node:path\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport {\n contestSchema,\n type Participation,\n participationSchema,\n studentSchema,\n variantSchema,\n variantsConfigSchema,\n} from \"@olinfo/quizms/models\";\nimport { Rng, validate } from \"@olinfo/quizms/utils\";\nimport { fatal, load, loadContests, success, warning } from \"@olinfo/quizms/utils-node\";\nimport { deleteApp } from \"firebase-admin/app\";\nimport type { Firestore } from \"firebase-admin/firestore\";\nimport { groupBy, uniq } from \"lodash-es\";\nimport picomatch from \"picomatch\";\nimport { glob } from \"tinyglobby\";\nimport z from \"zod\";\n\nimport { userSchema } from \"~/models/user\";\nimport { websiteSchema } from \"~/models/website\";\n\nimport { importCollection } from \"./utils/collection\";\nimport {\n contestConverter,\n participationConverter,\n studentConverter,\n variantConverter,\n websiteConverter,\n} from \"./utils/converters-admin\";\nimport { initializeFirebase } from \"./utils/initialize\";\nimport { importStorage } from \"./utils/storage\";\nimport { importUsers } from \"./utils/users\";\n\ntype ImportOptions = {\n config: string;\n skipExisting?: true;\n delete?: true;\n force?: true;\n\n admins?: true;\n contests?: true;\n schools?: true;\n statements?: true;\n students?: true;\n teachers?: true;\n variants?: true;\n websites?: true;\n};\n\nexport default async function importData(options: ImportOptions) {\n process.env.QUIZMS_MODE = \"contest\";\n\n if (!existsSync(\"data\")) {\n fatal(\"Cannot find data directory. Make sure you're inside a QuizMS project.\");\n }\n\n const collections: (keyof ImportOptions)[] = [\n \"admins\",\n \"contests\",\n \"schools\",\n \"statements\",\n \"students\",\n \"teachers\",\n \"variants\",\n \"websites\",\n ];\n if (collections.every((key) => !options[key])) {\n warning(\"`Nothing to import. Use `--help` for usage.`\");\n return;\n }\n\n const { app, bucket, db } = await initializeFirebase();\n\n if (options.admins) {\n await importAdmins(db, options);\n }\n if (options.contests) {\n await importContests(db, options);\n }\n if (options.schools || options.teachers) {\n await importParticipations(db, options);\n }\n if (options.students) {\n await importStudents(db, options);\n }\n if (options.variants) {\n await importVariants(db, options);\n }\n if (options.websites) {\n await importWebsites(db, options);\n }\n if (options.statements) {\n await importStatements(bucket, options);\n }\n\n success(\"All done!\");\n await deleteApp(app);\n}\n\nasync function importAdmins(db: Firestore, options: ImportOptions) {\n const users = await load(\"admins\", userSchema.omit({ role: true }));\n await importUsers(db, \"admin\", users, options);\n}\n\nasync function importContests(db: Firestore, options: ImportOptions) {\n const contests = await load(\"contests\", contestSchema);\n await importCollection(db, \"contests\", contests, contestConverter, options);\n}\n\nasync function importParticipations(db: Firestore, options: ImportOptions) {\n const schoolSchema = participationSchema\n .omit({\n schoolId: true,\n contestId: true,\n })\n .extend({\n contestIds: z.union([z.string(), z.array(z.string())]).default(\"*\"),\n password: z.string(),\n });\n const schools = await load(\"schools\", schoolSchema);\n const contests = await loadContests();\n\n if (options.teachers) {\n const teachers = schools.map((school) => ({\n name: school.name,\n username: school.id,\n password: school.password,\n }));\n await importUsers(db, \"teacher\", teachers, options);\n }\n\n if (options.schools) {\n const participations: Participation[] = [];\n\n for (const contest of contests) {\n for (const school of schools) {\n if (!picomatch.isMatch(contest.id, school.contestIds)) continue;\n\n let pdfVariants: string[] | undefined;\n if (contest.hasVariants) {\n if (school.pdfVariants) {\n pdfVariants = school.pdfVariants.map((id) => `${contest.id}-${id}`);\n } else {\n const rng = new Rng(`${contest.secret}-${contest.id}-${school.id}-participation`);\n pdfVariants = rng.sample(contest.pdfVariantIds, contest.pdfPerSchool);\n }\n }\n\n participations.push({\n id: `${school.id}-${contest.id}`,\n schoolId: school.id,\n contestId: contest.id,\n name: school.name,\n finalized: false,\n pdfVariants,\n disabled: false,\n });\n }\n }\n\n await importCollection(db, \"participations\", participations, participationConverter, options);\n }\n}\n\nasync function importStudents(db: Firestore, options: ImportOptions) {\n const students = await load(\"students\", studentSchema);\n const participations = groupBy(students, \"participationId\");\n await Promise.all(\n Object.entries(participations).map(([participationId, students]) =>\n importCollection(\n db,\n `participations/${participationId}/students`,\n students,\n studentConverter,\n options,\n ),\n ),\n );\n}\n\nasync function importVariants(db: Firestore, options: ImportOptions) {\n const variantsConfig = await load(\"variants\", variantsConfigSchema);\n const variants = await Promise.all(\n variantsConfig.flatMap((config) => {\n const ids = uniq([...config.variantIds, ...config.pdfVariantIds]);\n return ids.map(async (id) => {\n const fileName = path.join(\"variants\", config.id, id, \"answers.json\");\n let schema: string;\n try {\n schema = await readFile(fileName, \"utf8\");\n } catch {\n fatal(`Cannot find schema for variant ${id}. Use \\`quizms variants\\` to generate it.`);\n }\n try {\n return validate(variantSchema, JSON.parse(schema));\n } catch (err) {\n fatal(`Invalid schema for variant ${id}: ${err}`);\n }\n });\n }),\n );\n await importCollection(db, \"variants\", variants, variantConverter, options);\n}\n\nasync function importWebsites(db: Firestore, options: ImportOptions) {\n const websites = await load(\"websites\", websiteSchema);\n await importCollection(db, \"websites\", websites, websiteConverter, options);\n}\n\nasync function importStatements(bucket: Bucket, options: ImportOptions) {\n const contests = await loadContests();\n\n const timestamp = new Date().toISOString().replace(/:/g, \"-\");\n\n const statements = contests.map(async (config) => {\n const files = uniq([...config.variantIds, ...config.pdfVariantIds]).map(\n async (id): Promise<[string, string][]> => {\n const localDir = path.join(\"variants\", config.id, id);\n const remoteDir = path.join(\"statements\", config.id, id);\n const files = await glob(\"*\", { cwd: localDir });\n\n return files.flatMap((file) => {\n const ext = path.extname(file);\n return [\n [\n path.join(localDir, file),\n path.join(remoteDir, `${path.basename(file, ext)}-${timestamp}${ext}`),\n ],\n [path.join(localDir, file), path.join(remoteDir, file)],\n ];\n });\n },\n );\n return (await Promise.all(files)).flat();\n });\n await importStorage(bucket, \"statements\", (await Promise.all(statements)).flat(), options);\n}\n"],"names":["importData","options","existsSync","fatal","key","warning","app","bucket","db","initializeFirebase","importAdmins","importContests","importParticipations","importStudents","importVariants","importWebsites","importStatements","success","deleteApp","users","load","userSchema","importUsers","contests","contestSchema","importCollection","contestConverter","schoolSchema","participationSchema","z","schools","loadContests","teachers","school","participations","contest","picomatch","pdfVariants","id","Rng","participationConverter","students","studentSchema","groupBy","participationId","studentConverter","variantsConfig","variantsConfigSchema","variants","config","uniq","fileName","path","schema","readFile","validate","variantSchema","err","variantConverter","websites","websiteSchema","websiteConverter","timestamp","statements","files","localDir","remoteDir","glob","file","ext","importStorage"],"mappings":"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"}
|
package/dist/src/cli/init.js
CHANGED
|
@@ -1,10 +1,10 @@
|
|
|
1
1
|
import { writeFile as d, mkdir as p, readFile as g } from "node:fs/promises";
|
|
2
2
|
import n from "node:path";
|
|
3
3
|
import { cwd as l } from "node:process";
|
|
4
|
-
import {
|
|
4
|
+
import { styleText as y } from "node:util";
|
|
5
|
+
import { select as m, confirm as w } from "@inquirer/prompts";
|
|
5
6
|
import { success as c, error as f, info as o } from "@olinfo/quizms/utils-node";
|
|
6
|
-
import { applicationDefault as
|
|
7
|
-
import h from "picocolors";
|
|
7
|
+
import { applicationDefault as h } from "firebase-admin/app";
|
|
8
8
|
import { getGcpRegions as k } from "./utils/region-finder.js";
|
|
9
9
|
import v from "./files/firestore.rules.js";
|
|
10
10
|
import S from "./files/firestore-indexes.json.js";
|
|
@@ -22,7 +22,7 @@ async function Y(e) {
|
|
|
22
22
|
await $(a) && (i = !1), await D(s) && (i = !1), i ? c("Project initialized!") : f("The initialization was not completed due to some previous errors.");
|
|
23
23
|
}
|
|
24
24
|
async function I() {
|
|
25
|
-
const e = await j(
|
|
25
|
+
const e = await j(h(), "firebase", "v1beta1", ""), i = await m({
|
|
26
26
|
message: "Seleziona il progetto Firebase",
|
|
27
27
|
choices: e.results.map((t) => ({
|
|
28
28
|
value: t.projectId,
|
|
@@ -131,10 +131,10 @@ async function $(e) {
|
|
|
131
131
|
"v1",
|
|
132
132
|
"/databases/(default)/backupSchedules"
|
|
133
133
|
);
|
|
134
|
-
return i.backupSchedules
|
|
134
|
+
return i.backupSchedules?.some((s) => s.dailyRecurrence) ? o("Daily backup schedule already exists.") : await u(e, "firestore", "v1", "/databases/(default)/backupSchedules", {
|
|
135
135
|
dailyRecurrence: {},
|
|
136
136
|
retention: "604800s"
|
|
137
|
-
}), i.backupSchedules
|
|
137
|
+
}), i.backupSchedules?.some((s) => s.weeklyRecurrence) ? o("Weekly backup schedule already exists.") : await u(e, "firestore", "v1", "/databases/(default)/backupSchedules", {
|
|
138
138
|
weeklyRecurrence: {
|
|
139
139
|
day: "SUNDAY"
|
|
140
140
|
},
|
|
@@ -166,8 +166,8 @@ async function r(e, i, t) {
|
|
|
166
166
|
} catch {
|
|
167
167
|
a = !0;
|
|
168
168
|
}
|
|
169
|
-
a || (a = await
|
|
170
|
-
message: `The file ${
|
|
169
|
+
a || (a = await w({
|
|
170
|
+
message: `The file ${y("bold", n.basename(e))} already exists. Do you want to overwrite it?`
|
|
171
171
|
})), a && await d(e, i);
|
|
172
172
|
}
|
|
173
173
|
export {
|
package/dist/src/cli/init.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"init.js","sources":["../../../src/cli/init.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { cwd } from \"node:process\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport { confirm, select } from \"@inquirer/prompts\";\nimport { error, info, success } from \"@olinfo/quizms/utils-node\";\nimport { type App, applicationDefault } from \"firebase-admin/app\";\nimport pc from \"picocolors\";\n\nimport { getGcpRegions } from \"~/cli/utils/region-finder\";\n\nimport firestoreRules from \"./files/firestore.rules?raw\";\nimport firestoreIndexes from \"./files/firestore-indexes.json\";\nimport functionsIndex from \"./files/functions-index.js?raw\";\nimport functionsPackage from \"./files/functions-package\";\nimport storageRules from \"./files/storage.rules?raw\";\nimport { initializeFirebase } from \"./utils/initialize\";\nimport { restApi, restProjectApi } from \"./utils/rest-api\";\n\ntype InitOptions = {\n force?: boolean;\n};\n\nexport default async function init(options: InitOptions) {\n let initialized = true;\n\n await selectProject();\n const region = await getRegion();\n\n if (await copyFiles(options, region)) initialized = false;\n\n const { app, bucket } = await initializeFirebase();\n if (await enableBackups(app)) initialized = false;\n if (await enableCors(bucket)) initialized = false;\n\n if (initialized) {\n success(\"Project initialized!\");\n } else {\n error(\"The initialization was not completed due to some previous errors.\");\n }\n}\n\nasync function selectProject() {\n const data = await restApi(applicationDefault(), \"firebase\", \"v1beta1\", \"\");\n\n const answer = await select<string>({\n message: \"Seleziona il progetto Firebase\",\n choices: data.results.map((project: any) => ({\n value: project.projectId,\n name: project.displayName,\n })),\n });\n await writeFile(\".firebaserc\", JSON.stringify({ projects: { default: answer } }, null, 2));\n}\n\nasync function getRegion() {\n const regions = await getGcpRegions();\n return select<string>({\n message: \"Seleziona la regione\",\n choices: regions.map((region) => ({\n value: region.region,\n name: `${region.region} (${region.name})`,\n })),\n });\n}\n\nasync function copyFiles(options: InitOptions, region: string) {\n info(\"Copying files...\");\n\n await mkdir(\"firebase/functions\", { recursive: true });\n\n const firestoreRulesPath = \"firebase/firestore.rules\";\n await initFile(firestoreRulesPath, firestoreRules, options);\n\n const firestoreIndexesPath = \"firebase/firestore-indexes.json\";\n await initFile(firestoreIndexesPath, JSON.stringify(firestoreIndexes, undefined, 2), options);\n\n const storageRulesPath = \"firebase/storage.rules\";\n await initFile(storageRulesPath, storageRules, options);\n\n await initFile(\"firebase/functions/.env\", `QUIZMS_REGION=${region}`, options);\n await initFile(\n \"firebase/functions/index.js\",\n functionsIndex.replace(\"QUIZMS_REGION\", region),\n options,\n );\n await initFile(\n \"firebase/functions/package.json\",\n JSON.stringify(functionsPackage, undefined, 2),\n options,\n );\n\n const configs = {\n $schema: \"./node_modules/firebase-tools/schema/firebase-config.json\",\n hosting: hostingConfigs(region),\n firestore: {\n rules: path.relative(cwd(), firestoreRulesPath),\n indexes: path.relative(cwd(), firestoreIndexesPath),\n },\n storage: {\n rules: path.relative(cwd(), storageRulesPath),\n },\n functions: [\n {\n codebase: \"default\",\n disallowLegacyRuntimeConfig: true,\n ignore: [\"node_modules\", \"*.log\"],\n source: \"firebase/functions\",\n runtime: \"nodejs24\",\n },\n ],\n };\n await initFile(\"firebase.json\", JSON.stringify(configs, undefined, 2), options);\n\n success(\"Files copied!\");\n return 0;\n}\n\nfunction hostingConfigs(region: string) {\n return {\n public: \".quizms/hosting-build\",\n ignore: [\"firebase.json\", \"**/.*\", \"**/node_modules/**\"],\n rewrites: [\n {\n source: \"/api\",\n function: {\n functionId: \"api\",\n region,\n },\n },\n {\n source: \"**\",\n destination: \"/index.html\",\n },\n ],\n trailingSlash: false,\n headers: [\n {\n source: \"/assets/**\",\n headers: [\n {\n key: \"Cache-Control\",\n value: \"public, max-age=31536000, immutable\", // 365 days\n },\n ],\n },\n {\n source: \"/blockly/**\",\n headers: [\n {\n key: \"Cache-Control\",\n value: \"public, max-age=86400\", // 1 day\n },\n ],\n },\n ],\n predeploy: \"npx quizms build firebase\",\n };\n}\n\nasync function enableBackups(app: App) {\n info(\"Enabling Firestore backups...\");\n\n try {\n const existingsBackups = await restProjectApi(\n app,\n \"firestore\",\n \"v1\",\n \"/databases/(default)/backupSchedules\",\n );\n const hasDaily = existingsBackups.backupSchedules.some((s: any) => s.dailyRecurrence);\n if (hasDaily) {\n info(\"Daily backup schedule already exists.\");\n } else {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n dailyRecurrence: {},\n retention: \"604800s\",\n });\n }\n\n const hasWeekly = existingsBackups.backupSchedules.some((s: any) => s.weeklyRecurrence);\n if (hasWeekly) {\n info(\"Weekly backup schedule already exists.\");\n } else {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n weeklyRecurrence: {\n day: \"SUNDAY\",\n },\n retention: \"8467200s\",\n });\n }\n\n success(\"Backups enabled!\");\n return 0;\n } catch (err) {\n error(`Failed to enable Firestore backups: ${err}`);\n return 1;\n }\n}\n\nasync function enableCors(bucket: Bucket) {\n info(\"Enabling CORS for Firebase bucket...\");\n\n try {\n await bucket.setCorsConfiguration([\n {\n origin: [\"*\"],\n method: [\"GET\"],\n maxAgeSeconds: 3600,\n },\n ]);\n success(\"CORS enabled!\");\n return 0;\n } catch (err) {\n error(`Failed to set CORS configuration: ${err}`);\n return 1;\n }\n}\n\nasync function initFile(fileName: string, content: string, options?: InitOptions) {\n let write = options?.force ?? false;\n if (!write) {\n try {\n const prevContent = await readFile(fileName, \"utf8\");\n if (prevContent === content) return;\n } catch {\n write = true;\n }\n }\n\n if (!write) {\n write = await confirm({\n message: `The file ${pc.bold(path.basename(fileName))} already exists. Do you want to overwrite it?`,\n });\n }\n\n if (write) {\n await writeFile(fileName, content);\n }\n}\n"],"names":["init","options","initialized","selectProject","region","getRegion","copyFiles","app","bucket","initializeFirebase","enableBackups","enableCors","success","error","data","restApi","applicationDefault","answer","select","project","writeFile","regions","getGcpRegions","info","mkdir","firestoreRulesPath","initFile","firestoreRules","firestoreIndexesPath","firestoreIndexes","storageRulesPath","storageRules","functionsIndex","functionsPackage","configs","hostingConfigs","path","cwd","existingsBackups","restProjectApi","err","fileName","content","write","readFile","confirm","pc"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"init.js","sources":["../../../src/cli/init.ts"],"sourcesContent":["import { mkdir, readFile, writeFile } from \"node:fs/promises\";\nimport path from \"node:path\";\nimport { cwd } from \"node:process\";\nimport { styleText } from \"node:util\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport { confirm, select } from \"@inquirer/prompts\";\nimport { error, info, success } from \"@olinfo/quizms/utils-node\";\nimport { type App, applicationDefault } from \"firebase-admin/app\";\n\nimport { getGcpRegions } from \"~/cli/utils/region-finder\";\n\nimport firestoreRules from \"./files/firestore.rules?raw\";\nimport firestoreIndexes from \"./files/firestore-indexes.json\";\nimport functionsIndex from \"./files/functions-index.js?raw\";\nimport functionsPackage from \"./files/functions-package\";\nimport storageRules from \"./files/storage.rules?raw\";\nimport { initializeFirebase } from \"./utils/initialize\";\nimport { restApi, restProjectApi } from \"./utils/rest-api\";\n\ntype InitOptions = {\n force?: boolean;\n};\n\nexport default async function init(options: InitOptions) {\n let initialized = true;\n\n await selectProject();\n const region = await getRegion();\n\n if (await copyFiles(options, region)) initialized = false;\n\n const { app, bucket } = await initializeFirebase();\n if (await enableBackups(app)) initialized = false;\n if (await enableCors(bucket)) initialized = false;\n\n if (initialized) {\n success(\"Project initialized!\");\n } else {\n error(\"The initialization was not completed due to some previous errors.\");\n }\n}\n\nasync function selectProject() {\n const data = await restApi(applicationDefault(), \"firebase\", \"v1beta1\", \"\");\n\n const answer = await select<string>({\n message: \"Seleziona il progetto Firebase\",\n choices: data.results.map((project: any) => ({\n value: project.projectId,\n name: project.displayName,\n })),\n });\n await writeFile(\".firebaserc\", JSON.stringify({ projects: { default: answer } }, null, 2));\n}\n\nasync function getRegion() {\n const regions = await getGcpRegions();\n return select<string>({\n message: \"Seleziona la regione\",\n choices: regions.map((region) => ({\n value: region.region,\n name: `${region.region} (${region.name})`,\n })),\n });\n}\n\nasync function copyFiles(options: InitOptions, region: string) {\n info(\"Copying files...\");\n\n await mkdir(\"firebase/functions\", { recursive: true });\n\n const firestoreRulesPath = \"firebase/firestore.rules\";\n await initFile(firestoreRulesPath, firestoreRules, options);\n\n const firestoreIndexesPath = \"firebase/firestore-indexes.json\";\n await initFile(firestoreIndexesPath, JSON.stringify(firestoreIndexes, undefined, 2), options);\n\n const storageRulesPath = \"firebase/storage.rules\";\n await initFile(storageRulesPath, storageRules, options);\n\n await initFile(\"firebase/functions/.env\", `QUIZMS_REGION=${region}`, options);\n await initFile(\n \"firebase/functions/index.js\",\n functionsIndex.replace(\"QUIZMS_REGION\", region),\n options,\n );\n await initFile(\n \"firebase/functions/package.json\",\n JSON.stringify(functionsPackage, undefined, 2),\n options,\n );\n\n const configs = {\n $schema: \"./node_modules/firebase-tools/schema/firebase-config.json\",\n hosting: hostingConfigs(region),\n firestore: {\n rules: path.relative(cwd(), firestoreRulesPath),\n indexes: path.relative(cwd(), firestoreIndexesPath),\n },\n storage: {\n rules: path.relative(cwd(), storageRulesPath),\n },\n functions: [\n {\n codebase: \"default\",\n disallowLegacyRuntimeConfig: true,\n ignore: [\"node_modules\", \"*.log\"],\n source: \"firebase/functions\",\n runtime: \"nodejs24\",\n },\n ],\n };\n await initFile(\"firebase.json\", JSON.stringify(configs, undefined, 2), options);\n\n success(\"Files copied!\");\n return 0;\n}\n\nfunction hostingConfigs(region: string) {\n return {\n public: \".quizms/hosting-build\",\n ignore: [\"firebase.json\", \"**/.*\", \"**/node_modules/**\"],\n rewrites: [\n {\n source: \"/api\",\n function: {\n functionId: \"api\",\n region,\n },\n },\n {\n source: \"**\",\n destination: \"/index.html\",\n },\n ],\n trailingSlash: false,\n headers: [\n {\n source: \"/assets/**\",\n headers: [\n {\n key: \"Cache-Control\",\n value: \"public, max-age=31536000, immutable\", // 365 days\n },\n ],\n },\n {\n source: \"/blockly/**\",\n headers: [\n {\n key: \"Cache-Control\",\n value: \"public, max-age=86400\", // 1 day\n },\n ],\n },\n ],\n predeploy: \"npx quizms build firebase\",\n };\n}\n\nasync function enableBackups(app: App) {\n info(\"Enabling Firestore backups...\");\n\n try {\n const existingsBackups = await restProjectApi(\n app,\n \"firestore\",\n \"v1\",\n \"/databases/(default)/backupSchedules\",\n );\n const hasDaily = existingsBackups.backupSchedules?.some((s: any) => s.dailyRecurrence);\n if (hasDaily) {\n info(\"Daily backup schedule already exists.\");\n } else {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n dailyRecurrence: {},\n retention: \"604800s\",\n });\n }\n\n const hasWeekly = existingsBackups.backupSchedules?.some((s: any) => s.weeklyRecurrence);\n if (hasWeekly) {\n info(\"Weekly backup schedule already exists.\");\n } else {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n weeklyRecurrence: {\n day: \"SUNDAY\",\n },\n retention: \"8467200s\",\n });\n }\n\n success(\"Backups enabled!\");\n return 0;\n } catch (err) {\n error(`Failed to enable Firestore backups: ${err}`);\n return 1;\n }\n}\n\nasync function enableCors(bucket: Bucket) {\n info(\"Enabling CORS for Firebase bucket...\");\n\n try {\n await bucket.setCorsConfiguration([\n {\n origin: [\"*\"],\n method: [\"GET\"],\n maxAgeSeconds: 3600,\n },\n ]);\n success(\"CORS enabled!\");\n return 0;\n } catch (err) {\n error(`Failed to set CORS configuration: ${err}`);\n return 1;\n }\n}\n\nasync function initFile(fileName: string, content: string, options?: InitOptions) {\n let write = options?.force ?? false;\n if (!write) {\n try {\n const prevContent = await readFile(fileName, \"utf8\");\n if (prevContent === content) return;\n } catch {\n write = true;\n }\n }\n\n if (!write) {\n write = await confirm({\n message: `The file ${styleText(\"bold\", path.basename(fileName))} already exists. Do you want to overwrite it?`,\n });\n }\n\n if (write) {\n await writeFile(fileName, content);\n }\n}\n"],"names":["init","options","initialized","selectProject","region","getRegion","copyFiles","app","bucket","initializeFirebase","enableBackups","enableCors","success","error","data","restApi","applicationDefault","answer","select","project","writeFile","regions","getGcpRegions","info","mkdir","firestoreRulesPath","initFile","firestoreRules","firestoreIndexesPath","firestoreIndexes","storageRulesPath","storageRules","functionsIndex","functionsPackage","configs","hostingConfigs","path","cwd","existingsBackups","restProjectApi","err","fileName","content","write","readFile","confirm","styleText"],"mappings":";;;;;;;;;;;;;;;AAwBA,eAA8BA,EAAKC,GAAsB;AACvD,MAAIC,IAAc;AAElB,QAAMC,EAAA;AACN,QAAMC,IAAS,MAAMC,EAAA;AAErB,EAAI,MAAMC,EAAUL,GAASG,CAAM,MAAGF,IAAc;AAEpD,QAAM,EAAE,KAAAK,GAAK,QAAAC,EAAA,IAAW,MAAMC,EAAA;AAC9B,EAAI,MAAMC,EAAcH,CAAG,MAAGL,IAAc,KACxC,MAAMS,EAAWH,CAAM,MAAGN,IAAc,KAExCA,IACFU,EAAQ,sBAAsB,IAE9BC,EAAM,mEAAmE;AAE7E;AAEA,eAAeV,IAAgB;AAC7B,QAAMW,IAAO,MAAMC,EAAQC,KAAsB,YAAY,WAAW,EAAE,GAEpEC,IAAS,MAAMC,EAAe;AAAA,IAClC,SAAS;AAAA,IACT,SAASJ,EAAK,QAAQ,IAAI,CAACK,OAAkB;AAAA,MAC3C,OAAOA,EAAQ;AAAA,MACf,MAAMA,EAAQ;AAAA,IAAA,EACd;AAAA,EAAA,CACH;AACD,QAAMC,EAAU,eAAe,KAAK,UAAU,EAAE,UAAU,EAAE,SAASH,EAAA,EAAO,GAAK,MAAM,CAAC,CAAC;AAC3F;AAEA,eAAeZ,IAAY;AACzB,QAAMgB,IAAU,MAAMC,EAAA;AACtB,SAAOJ,EAAe;AAAA,IACpB,SAAS;AAAA,IACT,SAASG,EAAQ,IAAI,CAACjB,OAAY;AAAA,MAChC,OAAOA,EAAO;AAAA,MACd,MAAM,GAAGA,EAAO,MAAM,KAAKA,EAAO,IAAI;AAAA,IAAA,EACtC;AAAA,EAAA,CACH;AACH;AAEA,eAAeE,EAAUL,GAAsBG,GAAgB;AAC7D,EAAAmB,EAAK,kBAAkB,GAEvB,MAAMC,EAAM,sBAAsB,EAAE,WAAW,IAAM;AAErD,QAAMC,IAAqB;AAC3B,QAAMC,EAASD,GAAoBE,GAAgB1B,CAAO;AAE1D,QAAM2B,IAAuB;AAC7B,QAAMF,EAASE,GAAsB,KAAK,UAAUC,GAAkB,QAAW,CAAC,GAAG5B,CAAO;AAE5F,QAAM6B,IAAmB;AACzB,QAAMJ,EAASI,GAAkBC,GAAc9B,CAAO,GAEtD,MAAMyB,EAAS,2BAA2B,iBAAiBtB,CAAM,IAAIH,CAAO,GAC5E,MAAMyB;AAAA,IACJ;AAAA,IACAM,EAAe,QAAQ,iBAAiB5B,CAAM;AAAA,IAC9CH;AAAA,EAAA,GAEF,MAAMyB;AAAA,IACJ;AAAA,IACA,KAAK,UAAUO,GAAkB,QAAW,CAAC;AAAA,IAC7ChC;AAAA,EAAA;AAGF,QAAMiC,IAAU;AAAA,IACd,SAAS;AAAA,IACT,SAASC,EAAe/B,CAAM;AAAA,IAC9B,WAAW;AAAA,MACT,OAAOgC,EAAK,SAASC,EAAA,GAAOZ,CAAkB;AAAA,MAC9C,SAASW,EAAK,SAASC,EAAA,GAAOT,CAAoB;AAAA,IAAA;AAAA,IAEpD,SAAS;AAAA,MACP,OAAOQ,EAAK,SAASC,EAAA,GAAOP,CAAgB;AAAA,IAAA;AAAA,IAE9C,WAAW;AAAA,MACT;AAAA,QACE,UAAU;AAAA,QACV,6BAA6B;AAAA,QAC7B,QAAQ,CAAC,gBAAgB,OAAO;AAAA,QAChC,QAAQ;AAAA,QACR,SAAS;AAAA,MAAA;AAAA,IACX;AAAA,EACF;AAEF,eAAMJ,EAAS,iBAAiB,KAAK,UAAUQ,GAAS,QAAW,CAAC,GAAGjC,CAAO,GAE9EW,EAAQ,eAAe,GAChB;AACT;AAEA,SAASuB,EAAe/B,GAAgB;AACtC,SAAO;AAAA,IACL,QAAQ;AAAA,IACR,QAAQ,CAAC,iBAAiB,SAAS,oBAAoB;AAAA,IACvD,UAAU;AAAA,MACR;AAAA,QACE,QAAQ;AAAA,QACR,UAAU;AAAA,UACR,YAAY;AAAA,UACZ,QAAAA;AAAA,QAAA;AAAA,MACF;AAAA,MAEF;AAAA,QACE,QAAQ;AAAA,QACR,aAAa;AAAA,MAAA;AAAA,IACf;AAAA,IAEF,eAAe;AAAA,IACf,SAAS;AAAA,MACP;AAAA,QACE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP;AAAA,YACE,KAAK;AAAA,YACL,OAAO;AAAA;AAAA,UAAA;AAAA,QACT;AAAA,MACF;AAAA,MAEF;AAAA,QACE,QAAQ;AAAA,QACR,SAAS;AAAA,UACP;AAAA,YACE,KAAK;AAAA,YACL,OAAO;AAAA;AAAA,UAAA;AAAA,QACT;AAAA,MACF;AAAA,IACF;AAAA,IAEF,WAAW;AAAA,EAAA;AAEf;AAEA,eAAeM,EAAcH,GAAU;AACrC,EAAAgB,EAAK,+BAA+B;AAEpC,MAAI;AACF,UAAMe,IAAmB,MAAMC;AAAA,MAC7BhC;AAAA,MACA;AAAA,MACA;AAAA,MACA;AAAA,IAAA;AAGF,WADiB+B,EAAiB,iBAAiB,KAAK,CAAC,MAAW,EAAE,eAAe,IAEnFf,EAAK,uCAAuC,IAE5C,MAAMgB,EAAehC,GAAK,aAAa,MAAM,wCAAwC;AAAA,MACnF,iBAAiB,CAAA;AAAA,MACjB,WAAW;AAAA,IAAA,CACZ,GAGe+B,EAAiB,iBAAiB,KAAK,CAAC,MAAW,EAAE,gBAAgB,IAErFf,EAAK,wCAAwC,IAE7C,MAAMgB,EAAehC,GAAK,aAAa,MAAM,wCAAwC;AAAA,MACnF,kBAAkB;AAAA,QAChB,KAAK;AAAA,MAAA;AAAA,MAEP,WAAW;AAAA,IAAA,CACZ,GAGHK,EAAQ,kBAAkB,GACnB;AAAA,EACT,SAAS4B,GAAK;AACZ,WAAA3B,EAAM,uCAAuC2B,CAAG,EAAE,GAC3C;AAAA,EACT;AACF;AAEA,eAAe7B,EAAWH,GAAgB;AACxC,EAAAe,EAAK,sCAAsC;AAE3C,MAAI;AACF,iBAAMf,EAAO,qBAAqB;AAAA,MAChC;AAAA,QACE,QAAQ,CAAC,GAAG;AAAA,QACZ,QAAQ,CAAC,KAAK;AAAA,QACd,eAAe;AAAA,MAAA;AAAA,IACjB,CACD,GACDI,EAAQ,eAAe,GAChB;AAAA,EACT,SAAS4B,GAAK;AACZ,WAAA3B,EAAM,qCAAqC2B,CAAG,EAAE,GACzC;AAAA,EACT;AACF;AAEA,eAAed,EAASe,GAAkBC,GAAiBzC,GAAuB;AAChF,MAAI0C,IAAQ1C,GAAS,SAAS;AAC9B,MAAI,CAAC0C;AACH,QAAI;AAEF,UADoB,MAAMC,EAASH,GAAU,MAAM,MAC/BC,EAAS;AAAA,IAC/B,QAAQ;AACN,MAAAC,IAAQ;AAAA,IACV;AAGF,EAAKA,MACHA,IAAQ,MAAME,EAAQ;AAAA,IACpB,SAAS,YAAYC,EAAU,QAAQV,EAAK,SAASK,CAAQ,CAAC,CAAC;AAAA,EAAA,CAChE,IAGCE,KACF,MAAMvB,EAAUqB,GAAUC,CAAO;AAErC;"}
|
|
@@ -1,37 +1,26 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
for await (const u of t) {
|
|
21
|
-
const i = u.ref;
|
|
22
|
-
o.push(
|
|
23
|
-
n.runTransaction(async (s) => {
|
|
24
|
-
const a = (await s.get(i)).data();
|
|
25
|
-
if (!a?.variant) return;
|
|
26
|
-
const c = l(a, f[a.variant]?.schema) ?? null;
|
|
27
|
-
c !== a.score && s.update(i, { score: c });
|
|
28
|
-
}).then(() => r.increment())
|
|
29
|
-
);
|
|
1
|
+
import { calcScore as d } from "@olinfo/quizms/models";
|
|
2
|
+
import { info as f, withProgress as p } from "@olinfo/quizms/utils-node";
|
|
3
|
+
import { variantConverter as m, studentConverter as v } from "./utils/converters-admin.js";
|
|
4
|
+
import { initializeFirebase as l } from "./utils/initialize.js";
|
|
5
|
+
async function y() {
|
|
6
|
+
const { db: n } = await l();
|
|
7
|
+
f("Updating student scores.");
|
|
8
|
+
const s = await n.collection("variants").withConverter(m).get(), c = Object.fromEntries(s.docs.map((t) => [t.id, t.data()])), r = n.collectionGroup("students").withConverter(v), u = await r.count().get();
|
|
9
|
+
await p(
|
|
10
|
+
r.stream(),
|
|
11
|
+
u.data().count,
|
|
12
|
+
async (t) => {
|
|
13
|
+
const e = t.ref;
|
|
14
|
+
await n.runTransaction(async (o) => {
|
|
15
|
+
const a = (await o.get(e)).data();
|
|
16
|
+
if (!a?.variant) return;
|
|
17
|
+
const i = d(a, c[a.variant]?.schema) ?? null;
|
|
18
|
+
i !== a.score && o.update(e, { score: i });
|
|
19
|
+
});
|
|
30
20
|
}
|
|
31
|
-
|
|
32
|
-
}), r.stop();
|
|
21
|
+
);
|
|
33
22
|
}
|
|
34
23
|
export {
|
|
35
|
-
|
|
24
|
+
y as default
|
|
36
25
|
};
|
|
37
26
|
//# sourceMappingURL=update-scores.js.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"update-scores.js","sources":["../../../src/cli/update-scores.ts"],"sourcesContent":["import {
|
|
1
|
+
{"version":3,"file":"update-scores.js","sources":["../../../src/cli/update-scores.ts"],"sourcesContent":["import { calcScore, type Student } from \"@olinfo/quizms/models\";\nimport { info, withProgress } from \"@olinfo/quizms/utils-node\";\nimport type { QueryDocumentSnapshot } from \"firebase-admin/firestore\";\n\nimport { studentConverter, variantConverter } from \"./utils/converters-admin\";\nimport { initializeFirebase } from \"./utils/initialize\";\n\nexport default async function updateScores() {\n const { db } = await initializeFirebase();\n\n info(\"Updating student scores.\");\n\n const variantRef = db.collection(\"variants\").withConverter(variantConverter);\n const variantSnap = await variantRef.get();\n const variants = Object.fromEntries(variantSnap.docs.map((doc) => [doc.id, doc.data()]));\n\n const ref = db.collectionGroup(\"students\").withConverter(studentConverter);\n\n const count = await ref.count().get();\n await withProgress(\n ref.stream() as AsyncIterable<QueryDocumentSnapshot<Student>>,\n count.data().count,\n async (snapshot) => {\n const ref = snapshot.ref;\n await db.runTransaction(async (t) => {\n const studentSnap = await t.get(ref);\n const student = studentSnap.data();\n if (!student?.variant) return;\n const score = calcScore(student, variants[student.variant]?.schema) ?? null;\n if (score !== student.score) {\n t.update(ref, { score });\n }\n });\n },\n );\n}\n"],"names":["updateScores","db","initializeFirebase","info","variantSnap","variantConverter","variants","doc","ref","studentConverter","count","withProgress","snapshot","t","student","score","calcScore"],"mappings":";;;;AAOA,eAA8BA,IAAe;AAC3C,QAAM,EAAE,IAAAC,MAAO,MAAMC,EAAA;AAErB,EAAAC,EAAK,0BAA0B;AAG/B,QAAMC,IAAc,MADDH,EAAG,WAAW,UAAU,EAAE,cAAcI,CAAgB,EACtC,IAAA,GAC/BC,IAAW,OAAO,YAAYF,EAAY,KAAK,IAAI,CAACG,MAAQ,CAACA,EAAI,IAAIA,EAAI,KAAA,CAAM,CAAC,CAAC,GAEjFC,IAAMP,EAAG,gBAAgB,UAAU,EAAE,cAAcQ,CAAgB,GAEnEC,IAAQ,MAAMF,EAAI,MAAA,EAAQ,IAAA;AAChC,QAAMG;AAAA,IACJH,EAAI,OAAA;AAAA,IACJE,EAAM,OAAO;AAAA,IACb,OAAOE,MAAa;AAClB,YAAMJ,IAAMI,EAAS;AACrB,YAAMX,EAAG,eAAe,OAAOY,MAAM;AAEnC,cAAMC,KADc,MAAMD,EAAE,IAAIL,CAAG,GACP,KAAA;AAC5B,YAAI,CAACM,GAAS,QAAS;AACvB,cAAMC,IAAQC,EAAUF,GAASR,EAASQ,EAAQ,OAAO,GAAG,MAAM,KAAK;AACvE,QAAIC,MAAUD,EAAQ,SACpBD,EAAE,OAAOL,GAAK,EAAE,OAAAO,EAAA,CAAO;AAAA,MAE3B,CAAC;AAAA,IACH;AAAA,EAAA;AAEJ;"}
|