// Paste into Extensions > Apps Script in any Google Sheet. No automatic trigger. function onOpen() { SpreadsheetApp.getUi().createMenu('Graph') .addItem('Set my access key', 'setGraphKey') .addItem('Add request headers', 'graphHeaders') .addItem('Object inventory', 'graphInventory') .addItem('Run selected request', 'runGraphRow').addToUi(); } function setGraphKey() { var ui = SpreadsheetApp.getUi(); var answer = ui.prompt('Graph access', 'Enter your existing terminal key. It is stored in your user properties, not the sheet.', ui.ButtonSet.OK_CANCEL); if (answer.getSelectedButton() === ui.Button.OK) { PropertiesService.getUserProperties().setProperty('MISC_GRAPH_KEY', answer.getResponseText().trim()); } } function graphHeaders() { var sheet = SpreadsheetApp.getActiveSheet(); var headers = ['method', 'path', 'body_json', 'if_match', 'http_status', 'response_json']; if (sheet.getRange(1, 1, 1, 6).getValues()[0].some(String)) throw new Error('Use an empty tab; existing headers were left unchanged.'); sheet.getRange(1, 1, 1, 6).setValues([headers]); } function runGraphRow() { var lock = LockService.getDocumentLock(); if (!lock.tryLock(1000)) throw new Error('Another request is running.'); try { var sheet = SpreadsheetApp.getActiveSheet(), row = sheet.getActiveCell().getRow(); if (row < 2) throw new Error('Select a request row below the headers.'); var expected = ['method', 'path', 'body_json', 'if_match', 'http_status', 'response_json']; if (JSON.stringify(sheet.getRange(1, 1, 1, 6).getValues()[0]) !== JSON.stringify(expected)) throw new Error('Use the six contract headers in A1:F1.'); var cells = sheet.getRange(row, 1, 1, 6).getValues()[0]; if (cells[4] !== '' || cells[5] !== '') throw new Error('This row already has a result. Review it before clearing E:F to rerun.'); var method = String(cells[0]).toUpperCase(), path = String(cells[1]); if (!['GET', 'POST', 'PATCH', 'PUT', 'DELETE'].includes(method)) throw new Error('Unsupported HTTP method.'); if (!/^\/(api\/(?:jarticles|library)(?:\/|\?|$)|graph\/(query(?:\/|\.|\?|$)|manifest\.json|contract\.json)|a\/)/.test(path) || path.includes('://') || path.includes('\\')) throw new Error('Use a relative graph path on miscsubjects.com.'); var key = PropertiesService.getUserProperties().getProperty('MISC_GRAPH_KEY'); if (!key && method !== 'GET') throw new Error('Use Graph > Set my access key first.'); var headers = key ? {'x-terminal-key': key} : {}; if (cells[3]) headers['if-match'] = String(cells[3]); var options = {method: method.toLowerCase(), headers: headers, muteHttpExceptions: true, followRedirects: false}; if (method !== 'GET') { options.contentType = 'application/json'; options.payload = JSON.stringify(cells[2] ? JSON.parse(String(cells[2])) : {}); } var response = UrlFetchApp.fetch('https://miscsubjects.com' + path, options); var code = response.getResponseCode(), text = response.getContentText(); // Keep every byte, including paged query cursors, when a response exceeds one cell. if (text.length > 45000) { var book = SpreadsheetApp.getActiveSpreadsheet(), output = book.insertSheet('Graph response ' + Date.now()); var chunks = []; for (var i = 0; i < text.length; i += 45000) chunks.push([i / 45000 + 1, text.slice(i, i + 45000)]); output.getRange(1, 1, 1, 2).setValues([['part', 'response_json_part']]); output.getRange(2, 1, chunks.length, 2).setNumberFormat('@').setValues(chunks); text = JSON.stringify({response_tab: output.getName(),parts: chunks.length,instructions: 'Concatenate column B in part order to reconstruct the complete JSON response.'}); } sheet.getRange(row, 5, 1, 2).setNumberFormat('@').setValues([[code, text]]); SpreadsheetApp.flush(); } finally { lock.releaseLock(); } } function graphInventory() { var key=PropertiesService.getUserProperties().getProperty('MISC_GRAPH_KEY'); if(!key) throw new Error('Use Graph > Set my access key first.'); var r=UrlFetchApp.fetch('https://miscsubjects.com/api/library/inventory?format=rows',{headers:{'x-terminal-key':key},muteHttpExceptions:true,followRedirects:false}); if(r.getResponseCode()!==200) throw new Error('Inventory returned HTTP '+r.getResponseCode()); var data=JSON.parse(r.getContentText()),book=SpreadsheetApp.getActiveSpreadsheet(),sheet=book.insertSheet('Object inventory '+Date.now()); var values=[data.columns].concat(data.rows.map(function(row){return data.columns.map(function(c){var v=row[c];return v==null?'':v;});})); sheet.getRange(1,1,values.length,data.columns.length).setNumberFormat('@').setValues(values); sheet.setFrozenRows(1);sheet.getRange(values.length+2,1).setValue(data.scope);SpreadsheetApp.flush(); }