@nycpickleball/cli 1.9.2 → 1.11.0
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/index.js +198 -4
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -642,9 +642,92 @@ function registerLeagueCommands(program2) {
|
|
|
642
642
|
// src/commands/player.ts
|
|
643
643
|
import "commander";
|
|
644
644
|
|
|
645
|
-
// src/
|
|
645
|
+
// src/database-parser.ts
|
|
646
646
|
import { readFileSync as readFileSync3 } from "fs";
|
|
647
647
|
import * as XLSX2 from "xlsx";
|
|
648
|
+
var HEADER_NAME_KEYS = ["name", "full name"];
|
|
649
|
+
var HEADER_EMAIL_KEYS = ["email"];
|
|
650
|
+
var HEADER_DUPR_ID_KEYS = ["dupr id"];
|
|
651
|
+
var HEADER_RATING_KEYS = ["dupr rating", "doubles dupr"];
|
|
652
|
+
var DUPR_ID_REGEX = /^[A-Z0-9]{6}$/;
|
|
653
|
+
function findHeader(rows, requiredKeys, maxScan = 6) {
|
|
654
|
+
for (let i = 0; i < Math.min(maxScan, rows.length); i++) {
|
|
655
|
+
const row = rows[i];
|
|
656
|
+
const cells = row.map(
|
|
657
|
+
(c) => typeof c === "string" ? c.trim().toLowerCase() : ""
|
|
658
|
+
);
|
|
659
|
+
const ok = requiredKeys.every(
|
|
660
|
+
(opts) => opts.some((k) => cells.includes(k))
|
|
661
|
+
);
|
|
662
|
+
if (ok) return { idx: i, header: cells };
|
|
663
|
+
}
|
|
664
|
+
return null;
|
|
665
|
+
}
|
|
666
|
+
function firstIndex(header, keys) {
|
|
667
|
+
for (const k of keys) {
|
|
668
|
+
const i = header.indexOf(k);
|
|
669
|
+
if (i >= 0) return i;
|
|
670
|
+
}
|
|
671
|
+
return -1;
|
|
672
|
+
}
|
|
673
|
+
function parsePlayerDatabase(path, sheetName = "Database") {
|
|
674
|
+
const buf = readFileSync3(path);
|
|
675
|
+
const wb = XLSX2.read(buf, { type: "buffer" });
|
|
676
|
+
const sheet = wb.Sheets[sheetName];
|
|
677
|
+
if (!sheet) {
|
|
678
|
+
throw new Error(
|
|
679
|
+
`Sheet "${sheetName}" not found. Available: ${wb.SheetNames.join(", ")}`
|
|
680
|
+
);
|
|
681
|
+
}
|
|
682
|
+
const rows = XLSX2.utils.sheet_to_json(sheet, {
|
|
683
|
+
header: 1,
|
|
684
|
+
defval: null,
|
|
685
|
+
blankrows: true
|
|
686
|
+
});
|
|
687
|
+
const headerInfo = findHeader(rows, [
|
|
688
|
+
HEADER_DUPR_ID_KEYS,
|
|
689
|
+
HEADER_EMAIL_KEYS,
|
|
690
|
+
HEADER_NAME_KEYS
|
|
691
|
+
]);
|
|
692
|
+
if (!headerInfo) {
|
|
693
|
+
throw new Error(
|
|
694
|
+
`Could not find header row with DUPR ID, Email, and Name columns in "${sheetName}".`
|
|
695
|
+
);
|
|
696
|
+
}
|
|
697
|
+
const idxId = firstIndex(headerInfo.header, HEADER_DUPR_ID_KEYS);
|
|
698
|
+
const idxName = firstIndex(headerInfo.header, HEADER_NAME_KEYS);
|
|
699
|
+
const idxEmail = firstIndex(headerInfo.header, HEADER_EMAIL_KEYS);
|
|
700
|
+
const idxRating = firstIndex(headerInfo.header, HEADER_RATING_KEYS);
|
|
701
|
+
const out = [];
|
|
702
|
+
for (let i = headerInfo.idx + 1; i < rows.length; i++) {
|
|
703
|
+
const row = rows[i];
|
|
704
|
+
if (!row) continue;
|
|
705
|
+
const rawEmail = row[idxEmail];
|
|
706
|
+
const rawName = row[idxName];
|
|
707
|
+
const rawId = row[idxId];
|
|
708
|
+
const email = typeof rawEmail === "string" ? rawEmail.trim().toLowerCase() : "";
|
|
709
|
+
const name = typeof rawName === "string" ? rawName.trim() : "";
|
|
710
|
+
const duprId = typeof rawId === "string" ? rawId.trim().toUpperCase() : "";
|
|
711
|
+
if (!email || !email.includes("@")) continue;
|
|
712
|
+
if (!duprId || !DUPR_ID_REGEX.test(duprId)) continue;
|
|
713
|
+
let duprRating;
|
|
714
|
+
if (idxRating >= 0) {
|
|
715
|
+
const v = row[idxRating];
|
|
716
|
+
if (typeof v === "number" && Number.isFinite(v)) duprRating = v;
|
|
717
|
+
}
|
|
718
|
+
out.push({
|
|
719
|
+
email,
|
|
720
|
+
name: name || email,
|
|
721
|
+
duprId,
|
|
722
|
+
duprRating
|
|
723
|
+
});
|
|
724
|
+
}
|
|
725
|
+
return out;
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
// src/questionnaire-parser.ts
|
|
729
|
+
import { readFileSync as readFileSync4 } from "fs";
|
|
730
|
+
import * as XLSX3 from "xlsx";
|
|
648
731
|
var HEADER_ALIASES = {
|
|
649
732
|
"email address": "email",
|
|
650
733
|
email: "email",
|
|
@@ -679,10 +762,10 @@ function parseSlackJoined(v) {
|
|
|
679
762
|
return void 0;
|
|
680
763
|
}
|
|
681
764
|
function parseQuestionnaire(path) {
|
|
682
|
-
const buf =
|
|
683
|
-
const wb =
|
|
765
|
+
const buf = readFileSync4(path);
|
|
766
|
+
const wb = XLSX3.read(buf, { type: "buffer" });
|
|
684
767
|
const sheet = wb.Sheets[wb.SheetNames[0]];
|
|
685
|
-
const rows =
|
|
768
|
+
const rows = XLSX3.utils.sheet_to_json(sheet, {
|
|
686
769
|
header: 1,
|
|
687
770
|
defval: null,
|
|
688
771
|
blankrows: true
|
|
@@ -728,6 +811,117 @@ function rootOpts2(cmd) {
|
|
|
728
811
|
}
|
|
729
812
|
function registerPlayerCommands(program2) {
|
|
730
813
|
const player = program2.command("player").description("Manage players");
|
|
814
|
+
player.command("enrich").description(
|
|
815
|
+
"Enrich existing players (matched by email) with DUPR ID + rating from a database xlsx"
|
|
816
|
+
).requiredOption("--file <path>", "Path to the database .xlsx").option(
|
|
817
|
+
"--sheet <name>",
|
|
818
|
+
"Sheet name to read (default: Database)",
|
|
819
|
+
"Database"
|
|
820
|
+
).option(
|
|
821
|
+
"--include-all",
|
|
822
|
+
"Also create new player records for emails not yet in the DB"
|
|
823
|
+
).option(
|
|
824
|
+
"--dry-run",
|
|
825
|
+
"Parse + match only; do not call the API"
|
|
826
|
+
).action(async function(options) {
|
|
827
|
+
const { client } = resolveConfig(rootOpts2(this));
|
|
828
|
+
let rows;
|
|
829
|
+
try {
|
|
830
|
+
rows = parsePlayerDatabase(options.file, options.sheet);
|
|
831
|
+
} catch (e) {
|
|
832
|
+
dieWith(
|
|
833
|
+
new Error(
|
|
834
|
+
`Failed to parse ${options.file}: ${e.message}`
|
|
835
|
+
)
|
|
836
|
+
);
|
|
837
|
+
}
|
|
838
|
+
if (rows.length === 0) {
|
|
839
|
+
dieWith(
|
|
840
|
+
new Error(
|
|
841
|
+
`Parsed 0 valid rows from sheet "${options.sheet}".`
|
|
842
|
+
)
|
|
843
|
+
);
|
|
844
|
+
}
|
|
845
|
+
const existing = await client.get("/api/players", { auth: true });
|
|
846
|
+
const knownEmails = new Set(
|
|
847
|
+
existing.data.map((p) => p.email.toLowerCase())
|
|
848
|
+
);
|
|
849
|
+
const target = options.includeAll ? rows : rows.filter((r) => knownEmails.has(r.email));
|
|
850
|
+
console.log(
|
|
851
|
+
`Parsed ${rows.length} rows; ${target.length} target row(s) (existing players: ${knownEmails.size})`
|
|
852
|
+
);
|
|
853
|
+
if (target.length === 0) {
|
|
854
|
+
console.log(
|
|
855
|
+
"Nothing to enrich. Use --include-all to also create new players."
|
|
856
|
+
);
|
|
857
|
+
return;
|
|
858
|
+
}
|
|
859
|
+
if (options.dryRun) {
|
|
860
|
+
printTable(
|
|
861
|
+
target.slice(0, 30).map((r) => ({
|
|
862
|
+
name: r.name,
|
|
863
|
+
email: r.email,
|
|
864
|
+
duprId: r.duprId,
|
|
865
|
+
duprRating: typeof r.duprRating === "number" ? r.duprRating.toFixed(2) : "\u2014",
|
|
866
|
+
action: knownEmails.has(r.email) ? "update" : "create"
|
|
867
|
+
})),
|
|
868
|
+
[
|
|
869
|
+
{ key: "name", label: "NAME" },
|
|
870
|
+
{ key: "email", label: "EMAIL" },
|
|
871
|
+
{ key: "duprId", label: "DUPR ID" },
|
|
872
|
+
{ key: "duprRating", label: "RATING" },
|
|
873
|
+
{ key: "action", label: "ACTION" }
|
|
874
|
+
]
|
|
875
|
+
);
|
|
876
|
+
if (target.length > 30) console.log(`\u2026and ${target.length - 30} more`);
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
const BATCH = 100;
|
|
880
|
+
let createdTotal = 0;
|
|
881
|
+
let updatedTotal = 0;
|
|
882
|
+
const errorsTotal = [];
|
|
883
|
+
for (let i = 0; i < target.length; i += BATCH) {
|
|
884
|
+
const slice = target.slice(i, i + BATCH);
|
|
885
|
+
try {
|
|
886
|
+
const res = await client.post("/api/players", {
|
|
887
|
+
players: slice.map((r) => ({
|
|
888
|
+
email: r.email,
|
|
889
|
+
name: r.name,
|
|
890
|
+
duprId: r.duprId,
|
|
891
|
+
duprRating: r.duprRating
|
|
892
|
+
}))
|
|
893
|
+
});
|
|
894
|
+
createdTotal += res.results.filter(
|
|
895
|
+
(r) => r.status === "created"
|
|
896
|
+
).length;
|
|
897
|
+
updatedTotal += res.results.filter(
|
|
898
|
+
(r) => r.status === "updated"
|
|
899
|
+
).length;
|
|
900
|
+
errorsTotal.push(
|
|
901
|
+
...res.errors.map((e) => ({
|
|
902
|
+
...e,
|
|
903
|
+
index: e.index + i
|
|
904
|
+
}))
|
|
905
|
+
);
|
|
906
|
+
console.log(
|
|
907
|
+
` batch ${i / BATCH + 1}/${Math.ceil(target.length / BATCH)}: +${res.results.length}`
|
|
908
|
+
);
|
|
909
|
+
} catch (e) {
|
|
910
|
+
dieWith(e);
|
|
911
|
+
}
|
|
912
|
+
}
|
|
913
|
+
console.log(
|
|
914
|
+
`
|
|
915
|
+
Done. created=${createdTotal} updated=${updatedTotal} errors=${errorsTotal.length}`
|
|
916
|
+
);
|
|
917
|
+
if (errorsTotal.length > 0) {
|
|
918
|
+
for (const err of errorsTotal.slice(0, 10)) {
|
|
919
|
+
console.log(` row ${err.index}: ${err.message}`);
|
|
920
|
+
}
|
|
921
|
+
if (errorsTotal.length > 10)
|
|
922
|
+
console.log(` \u2026and ${errorsTotal.length - 10} more`);
|
|
923
|
+
}
|
|
924
|
+
});
|
|
731
925
|
player.command("refresh-dupr").description(
|
|
732
926
|
"Trigger an immediate DUPR rating refresh for all players with a duprId"
|
|
733
927
|
).action(async function() {
|