@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.
Files changed (2) hide show
  1. package/dist/index.js +198 -4
  2. 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/questionnaire-parser.ts
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 = readFileSync3(path);
683
- const wb = XLSX2.read(buf, { type: "buffer" });
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 = XLSX2.utils.sheet_to_json(sheet, {
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() {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@nycpickleball/cli",
3
- "version": "1.9.2",
3
+ "version": "1.11.0",
4
4
  "description": "CLI for managing NYC Pickleball leagues via the deployed API.",
5
5
  "license": "UNLICENSED",
6
6
  "publishConfig": {