Files
Google-Sheets-Rate-Assistant/lib/config.js
T
LZStealth 00d0db8255
build-windows-exe / build (push) Successful in 26s
Add folder-based service account onboarding
2026-07-18 12:38:51 +01:00

313 lines
9.8 KiB
JavaScript

const fs = require('fs');
const path = require('path');
const CONFIG_FILE_NAME = 'config.json';
const DEFAULT_CONFIG_TEMPLATE = JSON.stringify({
apiKeys: [
{
key: 'YOUR_API_KEY_1',
rateLimitPerMinute: 50,
},
{
key: 'YOUR_API_KEY_2',
rateLimitPerMinute: 50,
},
],
serviceAccounts: [
{
path: 'credentials.json',
rateLimitPerMinute: 50,
},
{
path: 'credentials_2.json',
rateLimitPerMinute: 50,
},
],
documents: [
{
documentId: 'YOUR_SPREADSHEET_ID',
outputDir: 'output/your-document',
sheets: [
{
name: 'Sheet1',
outputFilename: 'sheet1.csv',
},
{
name: 'Sheet2',
outputFilename: 'sheet2.csv',
},
],
},
{
documentId: 'YOUR_SPREADSHEET_ID_2',
outputDir: 'output/your-document-2',
sheets: [
{
name: 'Sheet1',
outputFilename: 'sheet1.csv',
},
{
name: 'Sheet2',
outputFilename: 'sheet2.csv',
},
],
},
],
}, null, 2) + '\n';
function getDefaultConfigPath() {
const packagedApp = Boolean(process.pkg);
return path.resolve(packagedApp ? path.dirname(process.execPath) : process.cwd(), CONFIG_FILE_NAME);
}
function getConfigPathCandidates() {
const packagedApp = Boolean(process.pkg);
const externalDefault = getDefaultConfigPath();
return [
process.env.GSA_CONFIG,
externalDefault,
!packagedApp ? path.resolve(__dirname, '..', CONFIG_FILE_NAME) : null,
].filter(Boolean);
}
function createStarterConfig(configPath) {
const directory = path.dirname(configPath);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, {recursive: true});
}
fs.writeFileSync(configPath, DEFAULT_CONFIG_TEMPLATE, 'utf8');
}
function writeConfigFile(configPath, config) {
const directory = path.dirname(configPath);
if (!fs.existsSync(directory)) {
fs.mkdirSync(directory, {recursive: true});
}
fs.writeFileSync(configPath, JSON.stringify(config, null, 2) + '\n', 'utf8');
}
function getApiKeyValue(item) {
if (typeof item === 'string') {
return item.trim();
}
if (item && typeof item === 'object') {
return (item.key || item.apiKey || '').toString().trim();
}
return '';
}
function isPlaceholderApiKey(key) {
return key === 'YOUR_API_KEY_HERE';
}
function loadConfig() {
const configPath = getConfigPathCandidates().find(candidate => fs.existsSync(candidate));
if (!configPath) {
const starterPath = process.env.GSA_CONFIG || getDefaultConfigPath();
const error = new Error(`Missing config file at ${starterPath}`);
error.code = 'ERR_CONFIG_MISSING';
error.configPath = starterPath;
throw error;
}
const json = fs.readFileSync(configPath, 'utf8');
const config = JSON.parse(json);
Object.defineProperty(config, '__configPath', {
value: configPath,
enumerable: false,
configurable: true,
writable: true,
});
validateConfig(config);
return config;
}
function getConfigBaseDir(config) {
const configPath = config && (config.__configPath || config.configPath);
if (configPath) {
return path.dirname(configPath);
}
return process.cwd();
}
function resolveOutputPath(outputDir, filename) {
if (!path.isAbsolute(outputDir)) {
outputDir = path.resolve(process.cwd(), outputDir);
}
if (!fs.existsSync(outputDir)) {
fs.mkdirSync(outputDir, {recursive: true});
}
return path.join(outputDir, filename);
}
function validateConfig(config) {
if (!config || typeof config !== 'object') {
throw new Error('Missing or invalid configuration object');
}
if (config.apiKeys !== undefined && !Array.isArray(config.apiKeys)) {
throw new Error('`apiKeys` must be an array when present in config.json');
}
if (config.serviceAccounts !== undefined && !Array.isArray(config.serviceAccounts)) {
throw new Error('`serviceAccounts` must be an array when present in config.json');
}
if (config.documents !== undefined && !Array.isArray(config.documents)) {
throw new Error('`documents` must be an array when present in config.json');
}
if (!Array.isArray(config.documents) || config.documents.length === 0) {
throw new Error('`documents` must be a non-empty array in config.json');
}
const hasApiKeys = Array.isArray(config.apiKeys) && config.apiKeys.length > 0;
const hasSvcAccounts = Array.isArray(config.serviceAccounts) && config.serviceAccounts.length > 0;
// Do not allow both API keys and service accounts to be present simultaneously
if (hasApiKeys && hasSvcAccounts) {
throw new Error('Configuration must not include both `apiKeys` and `serviceAccounts`; choose one authentication method');
}
if (!hasApiKeys && !hasSvcAccounts) {
throw new Error('Configuration must include at least one `apiKeys` or `serviceAccounts`');
}
// Ensure API key values are unique when using apiKeys
if (hasApiKeys) {
const extracted = config.apiKeys.map(getApiKeyValue);
const nonEmpty = extracted.filter(Boolean);
const unique = new Set(nonEmpty);
if (unique.size !== nonEmpty.length) {
throw new Error('`apiKeys` contains duplicate `key` values; ensure all API keys are unique');
}
for (const item of config.apiKeys) {
const key = getApiKeyValue(item);
if (!key) {
throw new Error('Each entry in `apiKeys` must include a non-empty `key`');
}
if (isPlaceholderApiKey(key)) {
throw new Error('Each entry in `apiKeys` must include a real API key; replace `YOUR_API_KEY_HERE`');
}
if (typeof item !== 'string' && (!item || typeof item !== 'object')) {
throw new Error('`apiKeys` entries must be strings or objects');
}
if (item.rateLimitPerMinute !== undefined && (!Number.isFinite(item.rateLimitPerMinute) || item.rateLimitPerMinute <= 0)) {
throw new Error('`rateLimitPerMinute` in `apiKeys` must be a positive number when provided');
}
}
}
// Ensure service account credential paths are unique when using serviceAccounts
if (hasSvcAccounts) {
const baseDir = getConfigBaseDir(config);
for (const item of config.serviceAccounts) {
if (typeof item === 'string') {
if (!item.trim()) {
throw new Error('Each string entry in `serviceAccounts` must be non-empty');
}
continue;
}
if (!item || typeof item !== 'object') {
throw new Error('`serviceAccounts` entries must be strings or objects');
}
const p = (item.path || item.credentialsPath || item.file || '').toString().trim();
if (!p) {
throw new Error('Each entry in `serviceAccounts` must include `path` or `credentialsPath`');
}
if (item.rateLimitPerMinute !== undefined && (!Number.isFinite(item.rateLimitPerMinute) || item.rateLimitPerMinute <= 0)) {
throw new Error('`rateLimitPerMinute` in `serviceAccounts` must be a positive number when provided');
}
}
// Read and canonicalize the contents of each service account credential
const svcContents = config.serviceAccounts.map(item => {
let p = '';
if (typeof item === 'string') p = item.trim();
else if (item && typeof item === 'object') p = (item.path || item.file || item.credentialsPath || '').toString().trim();
if (!p) return '';
const full = path.isAbsolute(p) ? p : path.resolve(baseDir, p);
if (!fs.existsSync(full)) {
throw new Error(`Service account file not found: ${p}`);
}
const raw = fs.readFileSync(full, 'utf8').trim();
try {
const parsed = JSON.parse(raw);
const canonicalize = (obj) => {
if (obj === null || typeof obj !== 'object') return JSON.stringify(obj);
if (Array.isArray(obj)) return '[' + obj.map(canonicalize).join(',') + ']';
const keys = Object.keys(obj).sort();
return '{' + keys.map(k => JSON.stringify(k) + ':' + canonicalize(obj[k])).join(',') + '}';
};
return canonicalize(parsed);
} catch (e) {
// Non-JSON files: use raw contents
return raw;
}
}).filter(Boolean);
const uniqueSvc = new Set(svcContents);
if (uniqueSvc.size !== svcContents.length) {
throw new Error('`serviceAccounts` contains duplicate credential contents; ensure all service account credentials are unique');
}
}
// Basic validation of documents/sheets structure
for (const doc of config.documents) {
if (!doc || typeof doc !== 'object') {
throw new Error('Each document entry must be an object');
}
if (typeof doc.documentId !== 'string' || !doc.documentId.trim()) {
throw new Error('Each document must include a non-empty `documentId`');
}
if (doc.outputDir !== undefined && typeof doc.outputDir !== 'string') {
throw new Error(`Document ${doc.documentId} has an invalid outputDir; it must be a string`);
}
if (!Array.isArray(doc.sheets) || doc.sheets.length === 0) {
throw new Error(`Document ${doc.documentId} must include a non-empty 'sheets' array`);
}
for (const sheet of doc.sheets) {
if (typeof sheet === 'string') {
if (!sheet.trim()) {
throw new Error(`Document ${doc.documentId} contains an empty sheet name`);
}
continue;
}
if (!sheet || typeof sheet !== 'object') {
throw new Error(`Document ${doc.documentId} contains an invalid sheet entry`);
}
if (typeof sheet.name !== 'string' || !sheet.name.trim()) {
throw new Error(`Document ${doc.documentId} contains a sheet entry missing name`);
}
if (sheet.outputFilename !== undefined && typeof sheet.outputFilename !== 'string') {
throw new Error(`Document ${doc.documentId} sheet ${sheet.name} has an invalid outputFilename; it must be a string`);
}
}
}
}
module.exports = {loadConfig, resolveOutputPath, validateConfig, createStarterConfig, getDefaultConfigPath, writeConfigFile};