@olinfo/quizms-firebase 4.0.0-canary.20251130

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.
Files changed (94) hide show
  1. package/bin/bin.js +4 -0
  2. package/dist/cli.d.ts +1 -0
  3. package/dist/cli.js +36 -0
  4. package/dist/cli.js.map +1 -0
  5. package/dist/entry.d.ts +4 -0
  6. package/dist/entry.js +19 -0
  7. package/dist/entry.js.map +1 -0
  8. package/dist/functions.d.ts +14 -0
  9. package/dist/functions.js +104 -0
  10. package/dist/functions.js.map +1 -0
  11. package/dist/package.json.js +5 -0
  12. package/dist/package.json.js.map +1 -0
  13. package/dist/src/cli/definalize.js +23 -0
  14. package/dist/src/cli/definalize.js.map +1 -0
  15. package/dist/src/cli/export.js +54 -0
  16. package/dist/src/cli/export.js.map +1 -0
  17. package/dist/src/cli/files/firestore-indexes.json.js +10 -0
  18. package/dist/src/cli/files/firestore-indexes.json.js.map +1 -0
  19. package/dist/src/cli/files/firestore.rules.js +191 -0
  20. package/dist/src/cli/files/firestore.rules.js.map +1 -0
  21. package/dist/src/cli/files/storage.rules.js +35 -0
  22. package/dist/src/cli/files/storage.rules.js.map +1 -0
  23. package/dist/src/cli/import.js +140 -0
  24. package/dist/src/cli/import.js.map +1 -0
  25. package/dist/src/cli/init.js +140 -0
  26. package/dist/src/cli/init.js.map +1 -0
  27. package/dist/src/cli/update-scores.js +37 -0
  28. package/dist/src/cli/update-scores.js.map +1 -0
  29. package/dist/src/cli/utils/collection.js +55 -0
  30. package/dist/src/cli/utils/collection.js.map +1 -0
  31. package/dist/src/cli/utils/converters-admin.js +41 -0
  32. package/dist/src/cli/utils/converters-admin.js.map +1 -0
  33. package/dist/src/cli/utils/initialize.js +31 -0
  34. package/dist/src/cli/utils/initialize.js.map +1 -0
  35. package/dist/src/cli/utils/rest-api.js +23 -0
  36. package/dist/src/cli/utils/rest-api.js.map +1 -0
  37. package/dist/src/cli/utils/storage.js +41 -0
  38. package/dist/src/cli/utils/storage.js.map +1 -0
  39. package/dist/src/cli/utils/users.js +30 -0
  40. package/dist/src/cli/utils/users.js.map +1 -0
  41. package/dist/src/models/user.js +12 -0
  42. package/dist/src/models/user.js.map +1 -0
  43. package/dist/src/vite/entry.js +33 -0
  44. package/dist/src/vite/entry.js.map +1 -0
  45. package/dist/src/vite/preset.js +30 -0
  46. package/dist/src/vite/preset.js.map +1 -0
  47. package/dist/src/web/admin/announcements.js +85 -0
  48. package/dist/src/web/admin/announcements.js.map +1 -0
  49. package/dist/src/web/admin/contest-settings.js +16 -0
  50. package/dist/src/web/admin/contest-settings.js.map +1 -0
  51. package/dist/src/web/admin/context.js +11 -0
  52. package/dist/src/web/admin/context.js.map +1 -0
  53. package/dist/src/web/admin/dashboard.js +85 -0
  54. package/dist/src/web/admin/dashboard.js.map +1 -0
  55. package/dist/src/web/admin/export.js +59 -0
  56. package/dist/src/web/admin/export.js.map +1 -0
  57. package/dist/src/web/admin/layout.js +38 -0
  58. package/dist/src/web/admin/layout.js.map +1 -0
  59. package/dist/src/web/admin/provider.js +30 -0
  60. package/dist/src/web/admin/provider.js.map +1 -0
  61. package/dist/src/web/admin/school-table.js +79 -0
  62. package/dist/src/web/admin/school-table.js.map +1 -0
  63. package/dist/src/web/common/api.js +43 -0
  64. package/dist/src/web/common/api.js.map +1 -0
  65. package/dist/src/web/common/base-login.js +26 -0
  66. package/dist/src/web/common/base-login.js.map +1 -0
  67. package/dist/src/web/common/converters.js +57 -0
  68. package/dist/src/web/common/converters.js.map +1 -0
  69. package/dist/src/web/common/query.js +16 -0
  70. package/dist/src/web/common/query.js.map +1 -0
  71. package/dist/src/web/common/token-login.js +40 -0
  72. package/dist/src/web/common/token-login.js.map +1 -0
  73. package/dist/src/web/entry/admin.js +27 -0
  74. package/dist/src/web/entry/admin.js.map +1 -0
  75. package/dist/src/web/entry/student-statement.js +28 -0
  76. package/dist/src/web/entry/student-statement.js.map +1 -0
  77. package/dist/src/web/entry/student.js +126 -0
  78. package/dist/src/web/entry/student.js.map +1 -0
  79. package/dist/src/web/entry/teacher.js +88 -0
  80. package/dist/src/web/entry/teacher.js.map +1 -0
  81. package/dist/src/web/hooks/auth.js +26 -0
  82. package/dist/src/web/hooks/auth.js.map +1 -0
  83. package/dist/src/web/hooks/collection.js +68 -0
  84. package/dist/src/web/hooks/collection.js.map +1 -0
  85. package/dist/src/web/hooks/count.js +22 -0
  86. package/dist/src/web/hooks/count.js.map +1 -0
  87. package/dist/src/web/hooks/document.js +61 -0
  88. package/dist/src/web/hooks/document.js.map +1 -0
  89. package/dist/src/web/hooks/subscription.js +42 -0
  90. package/dist/src/web/hooks/subscription.js.map +1 -0
  91. package/dist/vite.d.ts +4 -0
  92. package/dist/vite.js +7 -0
  93. package/dist/vite.js.map +1 -0
  94. package/package.json +83 -0
@@ -0,0 +1,140 @@
1
+ import { existsSync as C } from "node:fs";
2
+ import { readFile as j } from "node:fs/promises";
3
+ import d from "node:path";
4
+ import { contestSchema as g, participationSchema as M, variantsConfigSchema as y, studentSchema as V, variantSchema as b } from "@olinfo/quizms/models";
5
+ import { Rng as P, validate as x } from "@olinfo/quizms/utils";
6
+ import { fatal as w, warning as z, success as D, load as p } from "@olinfo/quizms/utils-node";
7
+ import { deleteApp as O } from "firebase-admin/app";
8
+ import { groupBy as U, uniq as I } from "lodash-es";
9
+ import N from "picomatch";
10
+ import { glob as k } from "tinyglobby";
11
+ import u from "zod";
12
+ import { importCollection as h } from "./utils/collection.js";
13
+ import { contestConverter as q, participationConverter as A, studentConverter as F, variantConverter as Q } from "./utils/converters-admin.js";
14
+ import { initializeFirebase as B } from "./utils/initialize.js";
15
+ import { importStorage as E } from "./utils/storage.js";
16
+ import { importUsers as J } from "./utils/users.js";
17
+ async function ft(t) {
18
+ if (process.env.QUIZMS_MODE = "contest", C("data") || w("Cannot find data directory. Make sure you're inside a QuizMS project."), [
19
+ "admins",
20
+ "contests",
21
+ "schools",
22
+ "statements",
23
+ "students",
24
+ "teachers",
25
+ "variants"
26
+ ].every((m) => !t[m])) {
27
+ z("`Nothing to import. Use `--help` for usage.`");
28
+ return;
29
+ }
30
+ const { app: r, bucket: c, db: i } = await B();
31
+ t.admins, t.contests && await R(i, t), (t.schools || t.teachers) && await Z(i, t), t.students && await _(i, t), t.variants && await G(i, t), t.statements && await H(c, t), D("All done!"), await O(r);
32
+ }
33
+ async function R(t, s) {
34
+ const r = await p("contests", g);
35
+ await h(t, "contests", r, q, s);
36
+ }
37
+ async function Z(t, s) {
38
+ const r = M.omit({
39
+ schoolId: !0,
40
+ contestId: !0
41
+ }).extend({
42
+ contestIds: u.union([u.string(), u.array(u.string())]).default("*"),
43
+ password: u.string()
44
+ }), c = await p("schools", r), i = await p("contests", g);
45
+ let m;
46
+ if (s.teachers) {
47
+ const e = c.map((a) => ({
48
+ name: a.name,
49
+ username: a.id,
50
+ password: a.password
51
+ }));
52
+ await J(t, "teacher", e, s);
53
+ }
54
+ if (s.schools) {
55
+ const e = [];
56
+ for (const a of i)
57
+ for (const n of c) {
58
+ if (!N.isMatch(a.id, n.contestIds)) continue;
59
+ let f;
60
+ if (a.hasVariants)
61
+ if (n.pdfVariants)
62
+ f = n.pdfVariants.map((o) => `${a.id}-${o}`);
63
+ else {
64
+ m || (m = await p("variants", y));
65
+ const o = m.find(($) => $.id === a.id);
66
+ o || w(`Missing variants configuration for contest ${a.id}.`), f = new P(`${o.secret}-${o.id}-${n.id}-participation`).sample(o.pdfVariantIds, o.pdfPerSchool);
67
+ }
68
+ e.push({
69
+ id: `${n.id}-${a.id}`,
70
+ schoolId: n.id,
71
+ contestId: a.id,
72
+ name: n.name,
73
+ finalized: !1,
74
+ pdfVariants: f,
75
+ disabled: !1
76
+ });
77
+ }
78
+ await h(t, "participations", e, A, s);
79
+ }
80
+ }
81
+ async function _(t, s) {
82
+ const r = await p("students", V), c = U(r, "participationId");
83
+ await Promise.all(
84
+ Object.entries(c).map(
85
+ ([i, m]) => h(
86
+ t,
87
+ `participations/${i}/students`,
88
+ m,
89
+ F,
90
+ s
91
+ )
92
+ )
93
+ );
94
+ }
95
+ async function G(t, s) {
96
+ const r = await p("variants", y), c = await Promise.all(
97
+ r.flatMap((i) => I([...i.variantIds, ...i.pdfVariantIds]).map(async (e) => {
98
+ const a = d.join("variants", i.id, `${e}.json`);
99
+ let n;
100
+ try {
101
+ n = await j(a, "utf8");
102
+ } catch {
103
+ w(`Cannot find schema for variant ${e}. Use \`quizms variants\` to generate it.`);
104
+ }
105
+ try {
106
+ return x(b, JSON.parse(n));
107
+ } catch (f) {
108
+ w(`Invalid schema for variant ${e}: ${f}`);
109
+ }
110
+ }))
111
+ );
112
+ await h(t, "variants", c, Q, s);
113
+ }
114
+ async function H(t, s) {
115
+ const r = await p("contests", g), c = await p("variants", y), i = (/* @__PURE__ */ new Date()).toISOString().replace(/:/g, "-"), m = r.map(async (e) => {
116
+ const a = c.find((f) => f.id === e.id);
117
+ a || w(`Missing variants configuration for contest ${e.id}.`);
118
+ const n = I([...a.variantIds, ...a.pdfVariantIds]).map(
119
+ async (f) => {
120
+ const o = d.join("variants", a.id, f), v = d.join("statements", a.id, f);
121
+ return (await k("*", { cwd: o })).flatMap((l) => {
122
+ const S = d.extname(l);
123
+ return [
124
+ [
125
+ d.join(o, l),
126
+ d.join(v, `${d.basename(l, S)}-${i}${S}`)
127
+ ],
128
+ [d.join(o, l), d.join(v, l)]
129
+ ];
130
+ });
131
+ }
132
+ );
133
+ return (await Promise.all(n)).flat();
134
+ });
135
+ await E(t, "statements", (await Promise.all(m)).flat(), s);
136
+ }
137
+ export {
138
+ ft as default
139
+ };
140
+ //# sourceMappingURL=import.js.map
@@ -0,0 +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 { importCollection } from \"./utils/collection\";\nimport {\n contestConverter,\n participationConverter,\n studentConverter,\n variantConverter,\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};\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 ];\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.statements) {\n await importStatements(bucket, options);\n }\n\n success(\"All done!\");\n await deleteApp(app);\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}.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 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","importContests","importParticipations","importStudents","importVariants","importStatements","success","deleteApp","contests","load","contestSchema","importCollection","contestConverter","schoolSchema","participationSchema","z","schools","configs","teachers","school","importUsers","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","timestamp","statements","files","localDir","remoteDir","glob","file","ext","importStorage"],"mappings":"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"}
@@ -0,0 +1,140 @@
1
+ import { writeFile as d, mkdir as m, readFile as p } from "node:fs/promises";
2
+ import s from "node:path";
3
+ import { cwd as n } from "node:process";
4
+ import { select as b, confirm as g } from "@inquirer/prompts";
5
+ import { success as o, error as l, info as c } from "@olinfo/quizms/utils-node";
6
+ import { initializeApp as w, deleteApp as y } from "firebase-admin/app";
7
+ import h from "picocolors";
8
+ import v from "./files/firestore.rules.js";
9
+ import C from "./files/firestore-indexes.json.js";
10
+ import k from "./files/storage.rules.js";
11
+ import { initializeFirebase as F } from "./utils/initialize.js";
12
+ import { restApi as S, restProjectApi as f } from "./utils/rest-api.js";
13
+ async function G(e) {
14
+ let i = !0;
15
+ await x(), await R(e) && (i = !1);
16
+ const { app: r, bucket: t } = await F();
17
+ await j(r) && (i = !1), await A(t) && (i = !1), i ? o("Project initialized!") : l("The initialization was not completed due to some previous errors.");
18
+ }
19
+ async function x() {
20
+ const e = w(), i = await S(e, "firebase", "v1beta1", "");
21
+ await y(e);
22
+ const r = await b({
23
+ message: "Seleziona il progetto Firebase",
24
+ choices: i.results.map((t) => ({
25
+ value: t.projectId,
26
+ name: t.displayName
27
+ }))
28
+ });
29
+ await d(".firebaserc", JSON.stringify({ projects: { default: r } }, null, 2));
30
+ }
31
+ async function R(e) {
32
+ c("Copying files..."), await m("firebase", { recursive: !0 });
33
+ const i = "firebase/firestore.rules";
34
+ await a(i, v, e);
35
+ const r = "firebase/firestore-indexes.json";
36
+ await a(r, JSON.stringify(C, void 0, 2), e);
37
+ const t = "firebase/storage.rules";
38
+ await a(t, k, e);
39
+ const u = {
40
+ hosting: z(),
41
+ firestore: {
42
+ rules: s.relative(n(), i),
43
+ indexes: s.relative(n(), r)
44
+ },
45
+ storage: {
46
+ rules: s.relative(n(), t)
47
+ },
48
+ functions: [
49
+ {
50
+ codebase: "default",
51
+ disallowLegacyRuntimeConfig: !0,
52
+ ignore: ["node_modules", "*.log"],
53
+ source: "firebase/functions"
54
+ }
55
+ ]
56
+ };
57
+ return await a("firebase.json", JSON.stringify(u, void 0, 2), e), o("Files copied!"), 0;
58
+ }
59
+ function z() {
60
+ return {
61
+ public: ".quizms/hosting-build",
62
+ ignore: ["firebase.json", "**/.*", "**/node_modules/**"],
63
+ rewrites: [
64
+ {
65
+ source: "**",
66
+ destination: "/index.html"
67
+ }
68
+ ],
69
+ trailingSlash: !1,
70
+ headers: [
71
+ {
72
+ source: "/assets/**",
73
+ headers: [
74
+ {
75
+ key: "Cache-Control",
76
+ value: "public, max-age=31536000, immutable"
77
+ // 365 days
78
+ }
79
+ ]
80
+ },
81
+ {
82
+ source: "/blockly/**",
83
+ headers: [
84
+ {
85
+ key: "Cache-Control",
86
+ value: "public, max-age=86400"
87
+ // 1 day
88
+ }
89
+ ]
90
+ }
91
+ ],
92
+ predeploy: "npx quizms build"
93
+ };
94
+ }
95
+ async function j(e) {
96
+ c("Enabling Firestore backups...");
97
+ try {
98
+ return await f(e, "firestore", "v1", "/databases/(default)/backupSchedules", {
99
+ dailyRecurrence: {},
100
+ retention: "604800s"
101
+ }), await f(e, "firestore", "v1", "/databases/(default)/backupSchedules", {
102
+ weeklyRecurrence: {
103
+ day: "SUNDAY"
104
+ },
105
+ retention: "8467200s"
106
+ }), o("Backups enabled!"), 0;
107
+ } catch (i) {
108
+ return l(`Failed to enable Firestore backups: ${i}`), 1;
109
+ }
110
+ }
111
+ async function A(e) {
112
+ c("Enabling CORS for Firebase bucket...");
113
+ try {
114
+ return await e.setCorsConfiguration([
115
+ {
116
+ origin: ["*"],
117
+ method: ["GET"],
118
+ maxAgeSeconds: 3600
119
+ }
120
+ ]), o("CORS enabled!"), 0;
121
+ } catch (i) {
122
+ return l(`Failed to set CORS configuration: ${i}`), 1;
123
+ }
124
+ }
125
+ async function a(e, i, r) {
126
+ let t = r?.force ?? !1;
127
+ if (!t)
128
+ try {
129
+ if (await p(e, "utf8") === i) return;
130
+ } catch {
131
+ t = !0;
132
+ }
133
+ t || (t = await g({
134
+ message: `The file ${h.bold(s.basename(e))} already exists. Do you want to overwrite it?`
135
+ })), t && await d(e, i);
136
+ }
137
+ export {
138
+ G as default
139
+ };
140
+ //# sourceMappingURL=init.js.map
@@ -0,0 +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, deleteApp, initializeApp } from \"firebase-admin/app\";\nimport pc from \"picocolors\";\n\nimport firestoreRules from \"./files/firestore.rules?raw\";\nimport firestoreIndexes from \"./files/firestore-indexes.json\";\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\n if (await copyFiles(options)) 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 app = initializeApp();\n const data = await restApi(app, \"firebase\", \"v1beta1\", \"\");\n await deleteApp(app);\n\n const answer = await select({\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 copyFiles(options: InitOptions) {\n info(\"Copying files...\");\n\n await mkdir(\"firebase\", { 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 // See {@link https://github.com/firebase/firebase-tools/blob/09c2641e861f2e31798dfb4aba1a180e8fd08ea5/src/firebaseConfig.ts#L244 here}.\n const configs = {\n hosting: hostingConfigs(),\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 },\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() {\n return {\n public: \".quizms/hosting-build\",\n ignore: [\"firebase.json\", \"**/.*\", \"**/node_modules/**\"],\n rewrites: [\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\",\n };\n}\n\nasync function enableBackups(app: App) {\n info(\"Enabling Firestore backups...\");\n\n try {\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n dailyRecurrence: {},\n retention: \"604800s\",\n });\n await restProjectApi(app, \"firestore\", \"v1\", \"/databases/(default)/backupSchedules\", {\n weeklyRecurrence: {\n day: \"SUNDAY\",\n },\n retention: \"8467200s\",\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","copyFiles","app","bucket","initializeFirebase","enableBackups","enableCors","success","error","initializeApp","data","restApi","deleteApp","answer","select","project","writeFile","info","mkdir","firestoreRulesPath","initFile","firestoreRules","firestoreIndexesPath","firestoreIndexes","storageRulesPath","storageRules","configs","hostingConfigs","path","cwd","restProjectApi","err","fileName","content","write","readFile","confirm","pc"],"mappings":"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"}
@@ -0,0 +1,37 @@
1
+ import { pipeline as d } from "node:stream/promises";
2
+ import { calcScore as l } from "@olinfo/quizms/models";
3
+ import { info as v } from "@olinfo/quizms/utils-node";
4
+ import { SingleBar as w } from "cli-progress";
5
+ import { formatDistanceStrict as h } from "date-fns";
6
+ import { variantConverter as b, studentConverter as C } from "./utils/converters-admin.js";
7
+ import { initializeFirebase as S } from "./utils/initialize.js";
8
+ async function D() {
9
+ const { db: n } = await S();
10
+ v("Updating student scores.");
11
+ const m = await n.collection("variants").withConverter(b).get(), f = Object.fromEntries(m.docs.map((t) => [t.id, t.data()])), e = n.collectionGroup("students").withConverter(C), p = await e.count().get(), r = new w({
12
+ format: " {bar} {percentage}% | ETA: {eta_formatted} | {value}/{total}",
13
+ barCompleteChar: "█",
14
+ barIncompleteChar: "▂",
15
+ etaBuffer: 1e4,
16
+ formatTime: (t) => h(0, t * 1e3)
17
+ });
18
+ r.start(p.data().count, 0), await d(e.stream(), async function* (t) {
19
+ const o = [];
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
+ );
30
+ }
31
+ await Promise.all(o);
32
+ }), r.stop();
33
+ }
34
+ export {
35
+ D as default
36
+ };
37
+ //# sourceMappingURL=update-scores.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"update-scores.js","sources":["../../../src/cli/update-scores.ts"],"sourcesContent":["import { pipeline } from \"node:stream/promises\";\n\nimport { calcScore, type Student } from \"@olinfo/quizms/models\";\nimport { info } from \"@olinfo/quizms/utils-node\";\nimport { SingleBar } from \"cli-progress\";\nimport { formatDistanceStrict } from \"date-fns\";\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 const bar = new SingleBar({\n format: \" {bar} {percentage}% | ETA: {eta_formatted} | {value}/{total}\",\n barCompleteChar: \"\\u2588\",\n barIncompleteChar: \"\\u2582\",\n etaBuffer: 10000,\n formatTime: (t) => formatDistanceStrict(0, t * 1000),\n });\n\n bar.start(count.data().count, 0);\n await pipeline(ref.stream(), async function* (stream) {\n const promises: Promise<void>[] = [];\n for await (const data of stream) {\n const snapshot = data as unknown as QueryDocumentSnapshot<Student>;\n const ref = snapshot.ref;\n promises.push(\n db\n .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 .then(() => bar.increment()),\n );\n }\n await Promise.all(promises);\n });\n bar.stop();\n}\n"],"names":["updateScores","db","initializeFirebase","info","variantSnap","variantConverter","variants","doc","ref","studentConverter","count","bar","SingleBar","formatDistanceStrict","pipeline","stream","promises","data","t","student","score","calcScore"],"mappings":";;;;;;;AAWA,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,GAC1BG,IAAM,IAAIC,EAAU;AAAA,IACxB,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,IACnB,WAAW;AAAA,IACX,YAAY,CAAC,MAAMC,EAAqB,GAAG,IAAI,GAAI;AAAA,EAAA,CACpD;AAED,EAAAF,EAAI,MAAMD,EAAM,KAAA,EAAO,OAAO,CAAC,GAC/B,MAAMI,EAASN,EAAI,OAAA,GAAU,iBAAiBO,GAAQ;AACpD,UAAMC,IAA4B,CAAA;AAClC,qBAAiBC,KAAQF,GAAQ;AAE/B,YAAMP,IADWS,EACI;AACrB,MAAAD,EAAS;AAAA,QACPf,EACG,eAAe,OAAOiB,MAAM;AAE3B,gBAAMC,KADc,MAAMD,EAAE,IAAIV,CAAG,GACP,KAAA;AAC5B,cAAI,CAACW,GAAS,QAAS;AACvB,gBAAMC,IAAQC,EAAUF,GAASb,EAASa,EAAQ,OAAO,GAAG,MAAM,KAAK;AACvE,UAAIC,MAAUD,EAAQ,SACpBD,EAAE,OAAOV,GAAK,EAAE,OAAAY,EAAA,CAAO;AAAA,QAE3B,CAAC,EACA,KAAK,MAAMT,EAAI,WAAW;AAAA,MAAA;AAAA,IAEjC;AACA,UAAM,QAAQ,IAAIK,CAAQ;AAAA,EAC5B,CAAC,GACDL,EAAI,KAAA;AACN;"}
@@ -0,0 +1,55 @@
1
+ import { confirm as u } from "@inquirer/prompts";
2
+ import { fatal as m, info as w, success as b } from "@olinfo/quizms/utils-node";
3
+ import { SingleBar as $ } from "cli-progress";
4
+ import { upperFirst as x, partition as y } from "lodash-es";
5
+ import p from "picocolors";
6
+ async function E(o, t, g, h, r) {
7
+ if (r.delete) {
8
+ await u({
9
+ message: `You are about to delete all ${p.bold(
10
+ t
11
+ )}. Any previous data will be lost, this operation cannot be undone. Are you really sure?`,
12
+ default: !1
13
+ }) || m("Command aborted.");
14
+ const i = o.collection(t);
15
+ await o.recursiveDelete(i), w(`${x(t)} deleted!`);
16
+ }
17
+ const c = /* @__PURE__ */ new Set();
18
+ for (const e of await o.collection(t).listDocuments())
19
+ c.add(e.id);
20
+ const [s, l] = y(g, (e) => c.has(e.id));
21
+ s.length > 0 && !r.skipExisting && !r.force && m(
22
+ `Documents already exist in \`${t}\`. Use \`--force\` to overwrite or \`--skip-existing\` to ignore.`
23
+ );
24
+ const a = r.skipExisting ? l : [...s, ...l];
25
+ r.force && s.length > 0 && (await u({
26
+ message: `You are about to import the ${p.bold(
27
+ t
28
+ )}. ${s.length} document will be overwritten, the previous data will be lost. Are you really sure?`,
29
+ default: !1
30
+ }) || m("Command aborted."));
31
+ const f = new $({
32
+ format: " {bar} {percentage}% | {value}/{total}",
33
+ barCompleteChar: "█",
34
+ barIncompleteChar: "▂"
35
+ });
36
+ f.start(a.length, 0);
37
+ for (let e = 0; e < a.length; e += 400) {
38
+ f.update(e);
39
+ const i = o.batch();
40
+ for (const n of a.slice(e, e + 400)) {
41
+ const d = o.doc(`${t}/${n.id}`).withConverter(h);
42
+ r.force ? i.set(d, n) : i.create(d, n);
43
+ }
44
+ try {
45
+ await i.commit();
46
+ } catch (n) {
47
+ m(`Cannot import ${t}: ${n}`);
48
+ }
49
+ }
50
+ f.update(a.length), f.stop(), b(`${a.length} ${t} imported!`);
51
+ }
52
+ export {
53
+ E as importCollection
54
+ };
55
+ //# sourceMappingURL=collection.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"collection.js","sources":["../../../../src/cli/utils/collection.ts"],"sourcesContent":["import { confirm } from \"@inquirer/prompts\";\nimport { fatal, info, success } from \"@olinfo/quizms/utils-node\";\nimport { SingleBar } from \"cli-progress\";\nimport type { Firestore, FirestoreDataConverter } from \"firebase-admin/firestore\";\nimport { partition, upperFirst } from \"lodash-es\";\nimport pc from \"picocolors\";\n\ntype ImportOptions = {\n delete?: true;\n skipExisting?: true;\n force?: true;\n};\n\nexport async function importCollection<T extends { id: string }>(\n db: Firestore,\n collection: string,\n data: T[],\n converter: FirestoreDataConverter<T>,\n options: ImportOptions,\n) {\n if (options.delete) {\n const confirmed = await confirm({\n message: `You are about to delete all ${pc.bold(\n collection,\n )}. Any previous data will be lost, this operation cannot be undone. Are you really sure?`,\n default: false,\n });\n if (!confirmed) {\n fatal(\"Command aborted.\");\n }\n\n const ref = db.collection(collection);\n await db.recursiveDelete(ref);\n info(`${upperFirst(collection)} deleted!`);\n }\n\n const existingIds = new Set<string>();\n for (const doc of await db.collection(collection).listDocuments()) {\n existingIds.add(doc.id);\n }\n const [existing, nonExisting] = partition(data, (doc) => existingIds.has(doc.id));\n if (existing.length > 0 && !options.skipExisting && !options.force) {\n fatal(\n `Documents already exist in \\`${collection}\\`. Use \\`--force\\` to overwrite or \\`--skip-existing\\` to ignore.`,\n );\n }\n\n const docsToImport = options.skipExisting ? nonExisting : [...existing, ...nonExisting];\n\n if (options.force && existing.length > 0) {\n const confirmed = await confirm({\n message: `You are about to import the ${pc.bold(\n collection,\n )}. ${existing.length} document will be overwritten, the previous data will be lost. Are you really sure?`,\n default: false,\n });\n if (!confirmed) {\n fatal(\"Command aborted.\");\n }\n }\n\n const bar = new SingleBar({\n format: \" {bar} {percentage}% | {value}/{total}\",\n barCompleteChar: \"\\u2588\",\n barIncompleteChar: \"\\u2582\",\n });\n bar.start(docsToImport.length, 0);\n\n for (let i = 0; i < docsToImport.length; i += 400) {\n bar.update(i);\n const batch = db.batch();\n for (const record of docsToImport.slice(i, i + 400)) {\n const ref = db.doc(`${collection}/${record.id}`).withConverter(converter);\n if (options.force) {\n batch.set(ref, record);\n } else {\n batch.create(ref, record);\n }\n }\n try {\n await batch.commit();\n } catch (err) {\n fatal(`Cannot import ${collection}: ${err}`);\n }\n }\n\n bar.update(docsToImport.length);\n bar.stop();\n success(`${docsToImport.length} ${collection} imported!`);\n}\n"],"names":["importCollection","db","collection","data","converter","options","confirm","pc","fatal","ref","info","upperFirst","existingIds","doc","existing","nonExisting","partition","docsToImport","bar","SingleBar","i","batch","record","err","success"],"mappings":";;;;;AAaA,eAAsBA,EACpBC,GACAC,GACAC,GACAC,GACAC,GACA;AACA,MAAIA,EAAQ,QAAQ;AAOlB,IANkB,MAAMC,EAAQ;AAAA,MAC9B,SAAS,+BAA+BC,EAAG;AAAA,QACzCL;AAAA,MAAA,CACD;AAAA,MACD,SAAS;AAAA,IAAA,CACV,KAECM,EAAM,kBAAkB;AAG1B,UAAMC,IAAMR,EAAG,WAAWC,CAAU;AACpC,UAAMD,EAAG,gBAAgBQ,CAAG,GAC5BC,EAAK,GAAGC,EAAWT,CAAU,CAAC,WAAW;AAAA,EAC3C;AAEA,QAAMU,wBAAkB,IAAA;AACxB,aAAWC,KAAO,MAAMZ,EAAG,WAAWC,CAAU,EAAE;AAChD,IAAAU,EAAY,IAAIC,EAAI,EAAE;AAExB,QAAM,CAACC,GAAUC,CAAW,IAAIC,EAAUb,GAAM,CAACU,MAAQD,EAAY,IAAIC,EAAI,EAAE,CAAC;AAChF,EAAIC,EAAS,SAAS,KAAK,CAACT,EAAQ,gBAAgB,CAACA,EAAQ,SAC3DG;AAAA,IACE,gCAAgCN,CAAU;AAAA,EAAA;AAI9C,QAAMe,IAAeZ,EAAQ,eAAeU,IAAc,CAAC,GAAGD,GAAU,GAAGC,CAAW;AAEtF,EAAIV,EAAQ,SAASS,EAAS,SAAS,MACnB,MAAMR,EAAQ;AAAA,IAC9B,SAAS,+BAA+BC,EAAG;AAAA,MACzCL;AAAA,IAAA,CACD,KAAKY,EAAS,MAAM;AAAA,IACrB,SAAS;AAAA,EAAA,CACV,KAECN,EAAM,kBAAkB;AAI5B,QAAMU,IAAM,IAAIC,EAAU;AAAA,IACxB,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,EAAA,CACpB;AACD,EAAAD,EAAI,MAAMD,EAAa,QAAQ,CAAC;AAEhC,WAASG,IAAI,GAAGA,IAAIH,EAAa,QAAQG,KAAK,KAAK;AACjD,IAAAF,EAAI,OAAOE,CAAC;AACZ,UAAMC,IAAQpB,EAAG,MAAA;AACjB,eAAWqB,KAAUL,EAAa,MAAMG,GAAGA,IAAI,GAAG,GAAG;AACnD,YAAMX,IAAMR,EAAG,IAAI,GAAGC,CAAU,IAAIoB,EAAO,EAAE,EAAE,EAAE,cAAclB,CAAS;AACxE,MAAIC,EAAQ,QACVgB,EAAM,IAAIZ,GAAKa,CAAM,IAErBD,EAAM,OAAOZ,GAAKa,CAAM;AAAA,IAE5B;AACA,QAAI;AACF,YAAMD,EAAM,OAAA;AAAA,IACd,SAASE,GAAK;AACZ,MAAAf,EAAM,iBAAiBN,CAAU,KAAKqB,CAAG,EAAE;AAAA,IAC7C;AAAA,EACF;AAEA,EAAAL,EAAI,OAAOD,EAAa,MAAM,GAC9BC,EAAI,KAAA,GACJM,EAAQ,GAAGP,EAAa,MAAM,IAAIf,CAAU,YAAY;AAC1D;"}
@@ -0,0 +1,41 @@
1
+ import { contestSchema as a, participationSchema as p, studentSchema as u, variantSchema as d } from "@olinfo/quizms/models";
2
+ import { validate as c } from "@olinfo/quizms/utils";
3
+ import { Timestamp as e } from "firebase-admin/firestore";
4
+ import { cloneDeepWith as s, omit as m, isDate as v, isString as D, mapValues as Z } from "lodash-es";
5
+ import o, { ZodDate as l, ZodObject as S, ZodRecord as C, ZodArray as F, ZodOptional as b, ZodDefault as U, ZodUnion as j, ZodDiscriminatedUnion as y } from "zod";
6
+ import { userSchema as O } from "../../models/user.js";
7
+ function T(t) {
8
+ return s(m(t, "id"), (r) => {
9
+ if (v(r))
10
+ return e.fromDate(r);
11
+ if (D(r))
12
+ return r.trim();
13
+ if (r === void 0)
14
+ return null;
15
+ });
16
+ }
17
+ function n(t) {
18
+ return t instanceof l ? o.instanceof(e).transform((r) => r.toDate()).pipe(t) : t instanceof S ? o.object(Z(t.shape, (r) => n(r))) : t instanceof C ? o.record(n(t.element)) : t instanceof F ? n(t.element).array() : t instanceof b ? o.preprocess((r) => r ?? void 0, n(t.unwrap()).optional()) : t instanceof U ? n(t.removeDefault()).pipe(t) : t instanceof j ? o.union(t.options.map((r) => n(r))) : t instanceof y ? o.discriminatedUnion(
19
+ t.discriminator,
20
+ t.options.map((r) => n(r))
21
+ ) : t;
22
+ }
23
+ function g(t, r) {
24
+ const f = { ...r.data(), id: r.id };
25
+ return c(n(t), f);
26
+ }
27
+ function i(t) {
28
+ return {
29
+ toFirestore: (r) => T(r),
30
+ fromFirestore: (r) => g(t, r)
31
+ };
32
+ }
33
+ const W = i(a), k = i(p), q = i(u), B = i(O), E = i(d);
34
+ export {
35
+ W as contestConverter,
36
+ k as participationConverter,
37
+ q as studentConverter,
38
+ B as userConverter,
39
+ E as variantConverter
40
+ };
41
+ //# sourceMappingURL=converters-admin.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"converters-admin.js","sources":["../../../../src/cli/utils/converters-admin.ts"],"sourcesContent":["import {\n contestSchema,\n participationSchema,\n studentSchema,\n variantSchema,\n} from \"@olinfo/quizms/models\";\nimport { validate } from \"@olinfo/quizms/utils\";\nimport {\n type DocumentSnapshot,\n type FirestoreDataConverter,\n Timestamp,\n} from \"firebase-admin/firestore\";\nimport { cloneDeepWith, isDate, isString, mapValues, omit } from \"lodash-es\";\nimport z, {\n ZodArray,\n ZodDate,\n ZodDefault,\n ZodDiscriminatedUnion,\n ZodObject,\n ZodOptional,\n ZodRecord,\n type ZodType,\n type ZodTypeAny,\n ZodUnion,\n} from \"zod\";\n\nimport { userSchema } from \"~/models/user\";\n\nfunction convertToFirestore(data: Record<string, any>) {\n return cloneDeepWith(omit(data, \"id\"), (value) => {\n if (isDate(value)) {\n return Timestamp.fromDate(value);\n }\n if (isString(value)) {\n return value.trim();\n }\n if (value === undefined) {\n return null;\n }\n });\n}\n\nfunction toFirebaseSchema(schema: ZodTypeAny): ZodTypeAny {\n if (schema instanceof ZodDate) {\n return z\n .instanceof(Timestamp)\n .transform((ts) => ts.toDate())\n .pipe(schema);\n }\n if (schema instanceof ZodObject) {\n return z.object(mapValues(schema.shape, (field) => toFirebaseSchema(field)));\n }\n if (schema instanceof ZodRecord) {\n return z.record(toFirebaseSchema(schema.element));\n }\n if (schema instanceof ZodArray) {\n return toFirebaseSchema(schema.element).array();\n }\n if (schema instanceof ZodOptional) {\n return z.preprocess((val) => val ?? undefined, toFirebaseSchema(schema.unwrap()).optional());\n }\n if (schema instanceof ZodDefault) {\n return toFirebaseSchema(schema.removeDefault()).pipe(schema);\n }\n if (schema instanceof ZodUnion) {\n return z.union(schema.options.map((option: ZodTypeAny) => toFirebaseSchema(option)));\n }\n if (schema instanceof ZodDiscriminatedUnion) {\n return z.discriminatedUnion(\n schema.discriminator,\n schema.options.map((option: ZodTypeAny) => toFirebaseSchema(option)),\n );\n }\n return schema;\n}\n\nfunction parse<T>(schema: ZodType<T, any, any>, snapshot: DocumentSnapshot): T {\n const data = { ...snapshot.data(), id: snapshot.id };\n return validate(toFirebaseSchema(schema), data);\n}\n\nfunction converter<T extends object>(schema: ZodType<T, any, any>): FirestoreDataConverter<T> {\n return {\n toFirestore: (data) => convertToFirestore(data),\n fromFirestore: (snapshot) => parse(schema, snapshot),\n };\n}\n\nexport const contestConverter = converter(contestSchema);\nexport const participationConverter = converter(participationSchema);\nexport const studentConverter = converter(studentSchema);\nexport const userConverter = converter(userSchema);\nexport const variantConverter = converter(variantSchema);\n"],"names":["convertToFirestore","data","cloneDeepWith","omit","value","isDate","Timestamp","isString","toFirebaseSchema","schema","ZodDate","z","ts","ZodObject","mapValues","field","ZodRecord","ZodArray","ZodOptional","val","ZodDefault","ZodUnion","option","ZodDiscriminatedUnion","parse","snapshot","validate","converter","contestConverter","contestSchema","participationConverter","participationSchema","studentConverter","studentSchema","userConverter","userSchema","variantConverter","variantSchema"],"mappings":";;;;;;AA4BA,SAASA,EAAmBC,GAA2B;AACrD,SAAOC,EAAcC,EAAKF,GAAM,IAAI,GAAG,CAACG,MAAU;AAChD,QAAIC,EAAOD,CAAK;AACd,aAAOE,EAAU,SAASF,CAAK;AAEjC,QAAIG,EAASH,CAAK;AAChB,aAAOA,EAAM,KAAA;AAEf,QAAIA,MAAU;AACZ,aAAO;AAAA,EAEX,CAAC;AACH;AAEA,SAASI,EAAiBC,GAAgC;AACxD,SAAIA,aAAkBC,IACbC,EACJ,WAAWL,CAAS,EACpB,UAAU,CAACM,MAAOA,EAAG,OAAA,CAAQ,EAC7B,KAAKH,CAAM,IAEZA,aAAkBI,IACbF,EAAE,OAAOG,EAAUL,EAAO,OAAO,CAACM,MAAUP,EAAiBO,CAAK,CAAC,CAAC,IAEzEN,aAAkBO,IACbL,EAAE,OAAOH,EAAiBC,EAAO,OAAO,CAAC,IAE9CA,aAAkBQ,IACbT,EAAiBC,EAAO,OAAO,EAAE,MAAA,IAEtCA,aAAkBS,IACbP,EAAE,WAAW,CAACQ,MAAQA,KAAO,QAAWX,EAAiBC,EAAO,QAAQ,EAAE,SAAA,CAAU,IAEzFA,aAAkBW,IACbZ,EAAiBC,EAAO,cAAA,CAAe,EAAE,KAAKA,CAAM,IAEzDA,aAAkBY,IACbV,EAAE,MAAMF,EAAO,QAAQ,IAAI,CAACa,MAAuBd,EAAiBc,CAAM,CAAC,CAAC,IAEjFb,aAAkBc,IACbZ,EAAE;AAAA,IACPF,EAAO;AAAA,IACPA,EAAO,QAAQ,IAAI,CAACa,MAAuBd,EAAiBc,CAAM,CAAC;AAAA,EAAA,IAGhEb;AACT;AAEA,SAASe,EAASf,GAA8BgB,GAA+B;AAC7E,QAAMxB,IAAO,EAAE,GAAGwB,EAAS,QAAQ,IAAIA,EAAS,GAAA;AAChD,SAAOC,EAASlB,EAAiBC,CAAM,GAAGR,CAAI;AAChD;AAEA,SAAS0B,EAA4BlB,GAAyD;AAC5F,SAAO;AAAA,IACL,aAAa,CAACR,MAASD,EAAmBC,CAAI;AAAA,IAC9C,eAAe,CAACwB,MAAaD,EAAMf,GAAQgB,CAAQ;AAAA,EAAA;AAEvD;AAEO,MAAMG,IAAmBD,EAAUE,CAAa,GAC1CC,IAAyBH,EAAUI,CAAmB,GACtDC,IAAmBL,EAAUM,CAAa,GAC1CC,IAAgBP,EAAUQ,CAAU,GACpCC,IAAmBT,EAAUU,CAAa;"}
@@ -0,0 +1,31 @@
1
+ import { existsSync as o } from "node:fs";
2
+ import { readFile as s } from "node:fs/promises";
3
+ import { fatal as a } from "@olinfo/quizms/utils-node";
4
+ import { initializeApp as c } from "firebase-admin/app";
5
+ import { getFirestore as n } from "firebase-admin/firestore";
6
+ import { getStorage as f } from "firebase-admin/storage";
7
+ import { restProjectApi as p } from "./rest-api.js";
8
+ async function y() {
9
+ const t = await u(), e = c({ projectId: t }), i = n(e);
10
+ i.settings({ ignoreUndefinedProperties: !0 });
11
+ const r = await m(e);
12
+ return { app: e, db: i, bucket: r };
13
+ }
14
+ async function u() {
15
+ o(".firebaserc") || a("No project selected. Run `firebase use --add` first.");
16
+ const t = JSON.parse(await s(".firebaserc", "utf-8")), e = Object.values(t.projects)[0];
17
+ return e || a("Failed to get project ID from .firebaserc."), e;
18
+ }
19
+ async function m(t) {
20
+ let e;
21
+ try {
22
+ e = (await p(t, "firebasestorage", "v1beta", "/buckets")).buckets?.[0]?.name;
23
+ } catch (r) {
24
+ a(`Failed to get project information: ${r}`);
25
+ }
26
+ return e || a("Failed to get storage bucket name. Maybe sure you have initialized Firebase Storage."), f(t).bucket(e.replace(/.*\//, ""));
27
+ }
28
+ export {
29
+ y as initializeFirebase
30
+ };
31
+ //# sourceMappingURL=initialize.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"initialize.js","sources":["../../../../src/cli/utils/initialize.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\nimport { readFile } from \"node:fs/promises\";\n\nimport { fatal } from \"@olinfo/quizms/utils-node\";\nimport { type App, initializeApp } from \"firebase-admin/app\";\nimport { getFirestore } from \"firebase-admin/firestore\";\nimport { getStorage } from \"firebase-admin/storage\";\n\nimport { restProjectApi } from \"./rest-api\";\n\nexport async function initializeFirebase() {\n const projectId = await getProjectId();\n const app = initializeApp({ projectId });\n const db = getFirestore(app);\n db.settings({ ignoreUndefinedProperties: true });\n\n const bucket = await getFirebaseBucket(app);\n\n return { app, db, bucket };\n}\n\nasync function getProjectId() {\n if (!existsSync(\".firebaserc\")) {\n fatal(\"No project selected. Run `firebase use --add` first.\");\n }\n\n const data = JSON.parse(await readFile(\".firebaserc\", \"utf-8\"));\n const projectId = Object.values(data.projects)[0] as string;\n if (!projectId) {\n fatal(\"Failed to get project ID from .firebaserc.\");\n }\n\n return projectId;\n}\n\nasync function getFirebaseBucket(app: App) {\n let bucketName: string;\n try {\n const data = await restProjectApi(app, \"firebasestorage\", \"v1beta\", \"/buckets\");\n bucketName = data.buckets?.[0]?.name;\n } catch (err) {\n fatal(`Failed to get project information: ${err}`);\n }\n\n if (!bucketName) {\n fatal(\"Failed to get storage bucket name. Maybe sure you have initialized Firebase Storage.\");\n }\n\n const storage = getStorage(app);\n return storage.bucket(bucketName.replace(/.*\\//, \"\"));\n}\n"],"names":["initializeFirebase","projectId","getProjectId","app","initializeApp","db","getFirestore","bucket","getFirebaseBucket","existsSync","fatal","data","readFile","bucketName","restProjectApi","err","getStorage"],"mappings":";;;;;;;AAUA,eAAsBA,IAAqB;AACzC,QAAMC,IAAY,MAAMC,EAAA,GAClBC,IAAMC,EAAc,EAAE,WAAAH,GAAW,GACjCI,IAAKC,EAAaH,CAAG;AAC3B,EAAAE,EAAG,SAAS,EAAE,2BAA2B,GAAA,CAAM;AAE/C,QAAME,IAAS,MAAMC,EAAkBL,CAAG;AAE1C,SAAO,EAAE,KAAAA,GAAK,IAAAE,GAAI,QAAAE,EAAA;AACpB;AAEA,eAAeL,IAAe;AAC5B,EAAKO,EAAW,aAAa,KAC3BC,EAAM,sDAAsD;AAG9D,QAAMC,IAAO,KAAK,MAAM,MAAMC,EAAS,eAAe,OAAO,CAAC,GACxDX,IAAY,OAAO,OAAOU,EAAK,QAAQ,EAAE,CAAC;AAChD,SAAKV,KACHS,EAAM,4CAA4C,GAG7CT;AACT;AAEA,eAAeO,EAAkBL,GAAU;AACzC,MAAIU;AACJ,MAAI;AAEF,IAAAA,KADa,MAAMC,EAAeX,GAAK,mBAAmB,UAAU,UAAU,GAC5D,UAAU,CAAC,GAAG;AAAA,EAClC,SAASY,GAAK;AACZ,IAAAL,EAAM,sCAAsCK,CAAG,EAAE;AAAA,EACnD;AAEA,SAAKF,KACHH,EAAM,sFAAsF,GAG9EM,EAAWb,CAAG,EACf,OAAOU,EAAW,QAAQ,QAAQ,EAAE,CAAC;AACtD;"}
@@ -0,0 +1,23 @@
1
+ import { fatal as i } from "@olinfo/quizms/utils-node";
2
+ function g(t, r, o, s, e) {
3
+ return p(t, r, o, t.options.projectId + s, e);
4
+ }
5
+ async function p(t, r, o, s, e) {
6
+ const c = await t.options.credential?.getAccessToken();
7
+ c?.access_token || i("Failed to get access token from credential.");
8
+ const a = `https://${r}.googleapis.com/${o}/projects/${s}`, n = await (await fetch(a, {
9
+ method: e ? "POST" : "GET",
10
+ headers: {
11
+ Authorization: `Bearer ${c.access_token}`
12
+ },
13
+ body: e && JSON.stringify(e)
14
+ })).json();
15
+ if (n.error)
16
+ throw new Error(n.error.message);
17
+ return n;
18
+ }
19
+ export {
20
+ p as restApi,
21
+ g as restProjectApi
22
+ };
23
+ //# sourceMappingURL=rest-api.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"rest-api.js","sources":["../../../../src/cli/utils/rest-api.ts"],"sourcesContent":["import { fatal } from \"@olinfo/quizms/utils-node\";\nimport type { App } from \"firebase-admin/app\";\n\nexport function restProjectApi(\n app: App,\n service: string,\n version: `v${string}`,\n endpoint: string,\n body?: object,\n) {\n return restApi(app, service, version, app.options.projectId + endpoint, body);\n}\n\nexport async function restApi(\n app: App,\n service: string,\n version: `v${string}`,\n endpoint: string,\n body?: object,\n) {\n const credential = app.options.credential;\n const token = await credential?.getAccessToken();\n if (!token?.access_token) {\n fatal(\"Failed to get access token from credential.\");\n }\n\n const url = `https://${service}.googleapis.com/${version}/projects/${endpoint}`;\n const resp = await fetch(url, {\n method: body ? \"POST\" : \"GET\",\n headers: {\n Authorization: `Bearer ${token.access_token}`,\n },\n body: body && JSON.stringify(body),\n });\n\n const data = await resp.json();\n if (data.error) {\n throw new Error(data.error.message);\n }\n return data;\n}\n"],"names":["restProjectApi","app","service","version","endpoint","body","restApi","token","fatal","url","data"],"mappings":";AAGO,SAASA,EACdC,GACAC,GACAC,GACAC,GACAC,GACA;AACA,SAAOC,EAAQL,GAAKC,GAASC,GAASF,EAAI,QAAQ,YAAYG,GAAUC,CAAI;AAC9E;AAEA,eAAsBC,EACpBL,GACAC,GACAC,GACAC,GACAC,GACA;AAEA,QAAME,IAAQ,MADKN,EAAI,QAAQ,YACC,eAAA;AAChC,EAAKM,GAAO,gBACVC,EAAM,6CAA6C;AAGrD,QAAMC,IAAM,WAAWP,CAAO,mBAAmBC,CAAO,aAAaC,CAAQ,IASvEM,IAAO,OARA,MAAM,MAAMD,GAAK;AAAA,IAC5B,QAAQJ,IAAO,SAAS;AAAA,IACxB,SAAS;AAAA,MACP,eAAe,UAAUE,EAAM,YAAY;AAAA,IAAA;AAAA,IAE7C,MAAMF,KAAQ,KAAK,UAAUA,CAAI;AAAA,EAAA,CAClC,GAEuB,KAAA;AACxB,MAAIK,EAAK;AACP,UAAM,IAAI,MAAMA,EAAK,MAAM,OAAO;AAEpC,SAAOA;AACT;"}
@@ -0,0 +1,41 @@
1
+ import { existsSync as d } from "node:fs";
2
+ import { confirm as h } from "@inquirer/prompts";
3
+ import { fatal as m, success as u } from "@olinfo/quizms/utils-node";
4
+ import { SingleBar as w } from "cli-progress";
5
+ import { partition as x } from "lodash-es";
6
+ import b from "picocolors";
7
+ async function E(f, a, s, r) {
8
+ const t = new w({
9
+ format: " {bar} {percentage}% | {value}/{total}",
10
+ barCompleteChar: "█",
11
+ barIncompleteChar: "▂"
12
+ }), l = /* @__PURE__ */ new Set();
13
+ t.start(s.length, 0), await Promise.all(
14
+ s.map(async ([i, e]) => {
15
+ d(i) || m(
16
+ `Cannot find ${a}. Make sure to generate them first, use \`quizms --help\` for usage.`
17
+ );
18
+ const c = f.file(e), [g] = await c.exists().catch(() => [!1]);
19
+ g && l.add(e), t.increment();
20
+ })
21
+ ), t.stop();
22
+ const [o, p] = x(s, ([i, e]) => l.has(e));
23
+ o.length > 0 && !r.skipExisting && !r.force && m(
24
+ `Files already exist in \`${a}\`. Use \`--force\` to overwrite or \`--skip-existing\` to ignore.`
25
+ );
26
+ const n = r.skipExisting ? p : [...o, ...p];
27
+ r.force && o.length > 0 && (await h({
28
+ message: `You are about to import the ${b.bold(
29
+ a
30
+ )}. ${o.length} files will be overwritten, the previous data will be lost. Are you really sure?`,
31
+ default: !1
32
+ }) || m("Command aborted.")), t.start(n.length, 0), await Promise.all(
33
+ n.map(async ([i, e]) => {
34
+ await f.upload(i, { destination: e }), t.increment();
35
+ })
36
+ ), t.stop(), u(`${n.length} ${a} imported!`);
37
+ }
38
+ export {
39
+ E as importStorage
40
+ };
41
+ //# sourceMappingURL=storage.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"storage.js","sources":["../../../../src/cli/utils/storage.ts"],"sourcesContent":["import { existsSync } from \"node:fs\";\n\nimport type { Bucket } from \"@google-cloud/storage\";\nimport { confirm } from \"@inquirer/prompts\";\nimport { fatal, success } from \"@olinfo/quizms/utils-node\";\nimport { SingleBar } from \"cli-progress\";\nimport { partition } from \"lodash-es\";\nimport pc from \"picocolors\";\n\ntype ImportOptions = {\n skipExisting?: true;\n force?: true;\n};\n\nexport async function importStorage(\n bucket: Bucket,\n collection: string,\n files: [string, string][],\n options: ImportOptions,\n) {\n const bar = new SingleBar({\n format: \" {bar} {percentage}% | {value}/{total}\",\n barCompleteChar: \"\\u2588\",\n barIncompleteChar: \"\\u2582\",\n });\n\n const existingFiles = new Set<string>();\n\n bar.start(files.length, 0);\n await Promise.all(\n files.map(async ([local, remote]) => {\n if (!existsSync(local)) {\n fatal(\n `Cannot find ${collection}. Make sure to generate them first, use \\`quizms --help\\` for usage.`,\n );\n }\n\n const file = bucket.file(remote);\n const [exists] = await file.exists().catch(() => [false]);\n if (exists) {\n existingFiles.add(remote);\n }\n bar.increment();\n }),\n );\n bar.stop();\n\n const [existing, nonExisting] = partition(files, ([_, remote]) => existingFiles.has(remote));\n if (existing.length > 0 && !options.skipExisting && !options.force) {\n fatal(\n `Files already exist in \\`${collection}\\`. Use \\`--force\\` to overwrite or \\`--skip-existing\\` to ignore.`,\n );\n }\n\n const filesToImport = options.skipExisting ? nonExisting : [...existing, ...nonExisting];\n\n if (options.force && existing.length > 0) {\n const confirmed = await confirm({\n message: `You are about to import the ${pc.bold(\n collection,\n )}. ${existing.length} files will be overwritten, the previous data will be lost. Are you really sure?`,\n default: false,\n });\n if (!confirmed) {\n fatal(\"Command aborted.\");\n }\n }\n\n bar.start(filesToImport.length, 0);\n await Promise.all(\n filesToImport.map(async ([local, remote]) => {\n await bucket.upload(local, { destination: remote });\n bar.increment();\n }),\n );\n bar.stop();\n\n success(`${filesToImport.length} ${collection} imported!`);\n}\n"],"names":["importStorage","bucket","collection","files","options","bar","SingleBar","existingFiles","local","remote","existsSync","fatal","file","exists","existing","nonExisting","partition","_","filesToImport","confirm","pc","success"],"mappings":";;;;;;AAcA,eAAsBA,EACpBC,GACAC,GACAC,GACAC,GACA;AACA,QAAMC,IAAM,IAAIC,EAAU;AAAA,IACxB,QAAQ;AAAA,IACR,iBAAiB;AAAA,IACjB,mBAAmB;AAAA,EAAA,CACpB,GAEKC,wBAAoB,IAAA;AAE1B,EAAAF,EAAI,MAAMF,EAAM,QAAQ,CAAC,GACzB,MAAM,QAAQ;AAAA,IACZA,EAAM,IAAI,OAAO,CAACK,GAAOC,CAAM,MAAM;AACnC,MAAKC,EAAWF,CAAK,KACnBG;AAAA,QACE,eAAeT,CAAU;AAAA,MAAA;AAI7B,YAAMU,IAAOX,EAAO,KAAKQ,CAAM,GACzB,CAACI,CAAM,IAAI,MAAMD,EAAK,OAAA,EAAS,MAAM,MAAM,CAAC,EAAK,CAAC;AACxD,MAAIC,KACFN,EAAc,IAAIE,CAAM,GAE1BJ,EAAI,UAAA;AAAA,IACN,CAAC;AAAA,EAAA,GAEHA,EAAI,KAAA;AAEJ,QAAM,CAACS,GAAUC,CAAW,IAAIC,EAAUb,GAAO,CAAC,CAACc,GAAGR,CAAM,MAAMF,EAAc,IAAIE,CAAM,CAAC;AAC3F,EAAIK,EAAS,SAAS,KAAK,CAACV,EAAQ,gBAAgB,CAACA,EAAQ,SAC3DO;AAAA,IACE,4BAA4BT,CAAU;AAAA,EAAA;AAI1C,QAAMgB,IAAgBd,EAAQ,eAAeW,IAAc,CAAC,GAAGD,GAAU,GAAGC,CAAW;AAEvF,EAAIX,EAAQ,SAASU,EAAS,SAAS,MACnB,MAAMK,EAAQ;AAAA,IAC9B,SAAS,+BAA+BC,EAAG;AAAA,MACzClB;AAAA,IAAA,CACD,KAAKY,EAAS,MAAM;AAAA,IACrB,SAAS;AAAA,EAAA,CACV,KAECH,EAAM,kBAAkB,IAI5BN,EAAI,MAAMa,EAAc,QAAQ,CAAC,GACjC,MAAM,QAAQ;AAAA,IACZA,EAAc,IAAI,OAAO,CAACV,GAAOC,CAAM,MAAM;AAC3C,YAAMR,EAAO,OAAOO,GAAO,EAAE,aAAaC,GAAQ,GAClDJ,EAAI,UAAA;AAAA,IACN,CAAC;AAAA,EAAA,GAEHA,EAAI,KAAA,GAEJgB,EAAQ,GAAGH,EAAc,MAAM,IAAIhB,CAAU,YAAY;AAC3D;"}