/*********************** * PROPFIG (edit these) ***********************/ const PROPFIG = { spreadsheetId: "17II0V8EsTL-9nHq2HgqhHcay-t-hhOaY5wNEzXHEsAM", sheetId: 249063374, // the tab's sheetId (not gid string; usually same number) courseCol: 6, // Column C = 3 headerRow: 1, startDataRow: 2, northpassBaseUrl: "https://api.northpass.com", cacheTtlSeconds: 6 * 60 * 60 // 6 hours }; /** * One-time setup: * Run setNorthpassApiKey_() to store your API key securely in Script Properties. */ function setNorthpassApiKey() { const apiKey = "N5AQIwjIeeYDBSU9vTST37Nvg"; PropertiesService.getScriptProperties().setProperty("NORTHPASS_API_KEY", apiKey); } /** * Main entrypoint: reads course names from col C and writes core_competencies * into the first empty column (based on header row). */ function writeCoreCompetenciesFromCourses() { const ss = SpreadsheetApp.openById(PROPFIG.spreadsheetId); const sheet = getSheetById_(ss, PROPFIG.sheetId); const apiKey = PropertiesService.getScriptProperties().getProperty("NORTHPASS_API_KEY"); if (!apiKey) throw new Error("Missing NORTHPASS_API_KEY. Run setNorthpassApiKey_() first."); // Determine output column: first empty cell in header row; if none, append. const lastCol = Math.max(sheet.getLastColumn(), 1); const headerValues = sheet.getRange(PROPFIG.headerRow, 1, 1, lastCol).getValues()[0]; let outCol = headerValues.findIndex(v => String(v || "").trim() === "") + 1; if (outCol === 0) outCol = lastCol + 1; // Write a header label (optional but useful) sheet.getRange(PROPFIG.headerRow, outCol).setValue("core_competencies"); // Read course column values const lastRow = sheet.getLastRow(); if (lastRow < PROPFIG.startDataRow) return; const numRows = lastRow - PROPFIG.startDataRow + 1; const courseValues = sheet.getRange(PROPFIG.startDataRow, PROPFIG.courseCol, numRows, 1).getValues(); // Prep caches: in-run memo + script cache const runMemo = {}; // { courseNameLower: { id, coreCompetenciesString } } const cache = CacheService.getScriptCache(); // Build output column values aligned to rows const output = new Array(numRows).fill([""]); for (let i = 0; i < numRows; i++) { const rawCourse = String(courseValues[i][0] || "").trim(); if (!rawCourse) continue; const key = rawCourse.toLowerCase(); // 1) in-run memo if (runMemo[key]) { output[i] = [runMemo[key].coreCompetenciesString]; continue; } // 2) CacheService (persists across runs for TTL) const cached = cache.get(cacheKey_(key)); if (cached) { const parsed = JSON.parse(cached); runMemo[key] = parsed; output[i] = [parsed.coreCompetenciesString]; continue; } // 3) Fetch from Northpass const courseId = fetchCourseIdByExactName_(apiKey, rawCourse); const coreCompetencies = fetchCoreCompetenciesByCourseId_(apiKey, courseId); const coreStr = normalizeToSingleString_(coreCompetencies); const record = { id: courseId, coreCompetenciesString: coreStr }; runMemo[key] = record; cache.put(cacheKey_(key), JSON.stringify(record), PROPFIG.cacheTtlSeconds); output[i] = [coreStr]; } // Write results in one batch sheet.getRange(PROPFIG.startDataRow, outCol, numRows, 1).setValues(output); } /*********************** * Northpass API helpers ***********************/ /** * Gets course id by exact name using: * /v2/courses?filter[name][eq]= * If no match is returned, falls back to: * /v2/courses?filter[name]= * * Filtering is documented by Northpass. :contentReference[oaicite:3]{index=3} */ function fetchCourseIdByExactName_(apiKey, courseName) { const encoded = encodeURIComponent(courseName); const urlsToTry = [ `${PROPFIG.northpassBaseUrl}/v2/courses?filter[name][eq]=${encoded}`, `${PROPFIG.northpassBaseUrl}/v2/courses?filter[name]=${encoded}` // fallback ]; for (const url of urlsToTry) { const json = northpassGetJson_(apiKey, url); // Typical Northpass JSON: { data: [ { id, type, attributes... }, ... ] } :contentReference[oaicite:4]{index=4} const data = json && json.data; if (Array.isArray(data) && data.length > 0 && data[0].id) { return data[0].id; } } throw new Error(`No course found for name "${courseName}" (tried eq + fallback).`); } /** * Fetches course properties: * /v2/properties/courses/{id} * Returns: data.attributes.customized_properties.core_competencies :contentReference[oaicite:5]{index=5} */ function fetchCoreCompetenciesByCourseId_(apiKey, courseId) { const url = `${PROPFIG.northpassBaseUrl}/v2/properties/courses/${encodeURIComponent(courseId)}`; const json = northpassGetJson_(apiKey, url); const core = json && json.data && json.data.attributes && json.data.attributes.customized_properties && json.data.attributes.customized_properties.core_competencies; // Could be undefined/null/string/array/object depending on how it's stored return core ?? ""; } /** * Makes an authenticated GET request to Northpass using X-Api-Key header. * Northpass API key auth is documented. :contentReference[oaicite:6]{index=6} */ function northpassGetJson_(apiKey, url) { const resp = UrlFetchApp.fetch(url, { method: "get", muteHttpExceptions: true, headers: { "Accept": "application/json", "X-Api-Key": apiKey } }); const code = resp.getResponseCode(); const text = resp.getContentText(); if (code < 200 || code >= 300) { throw new Error(`Northpass API error ${code} for ${url}\n${text}`); } return JSON.parse(text); } /*********************** * Sheet helpers ***********************/ function getSheetById_(spreadsheet, sheetId) { const sheets = spreadsheet.getSheets(); for (const s of sheets) { if (s.getSheetId() === sheetId) return s; } throw new Error(`No sheet found with sheetId=${sheetId}`); } /*********************** * Formatting + caching ***********************/ function normalizeToSingleString_(value) { if (value === null || value === undefined) return ""; if (Array.isArray(value)) return value.map(v => String(v).trim()).filter(Boolean).join("; "); if (typeof value === "object") return JSON.stringify(value); return String(value); } function cacheKey_(courseNameLower) { return `np_course_${courseNameLower}`; }