require('dotenv').config({ path: __dirname + '/.env' }); const NOCODB_URL = process.env.NOCODB_URL || 'http://localhost:8090'; const NOCODB_TOKEN = process.env.NOCODB_TOKEN; const NOCODB_BASE_ID = process.env.NOCODB_BASE_ID; class NocoDBError extends Error { constructor(message, status, details) { super(message); this.name = 'NocoDBError'; this.status = status; this.details = details; } } // Cache: table name → table ID const tableIdCache = {}; async function resolveTableId(tableName) { if (tableIdCache[tableName]) return tableIdCache[tableName]; const res = await fetch(`${NOCODB_URL}/api/v2/meta/bases/${NOCODB_BASE_ID}/tables`, { headers: { 'xc-token': NOCODB_TOKEN }, }); if (!res.ok) throw new NocoDBError('Failed to fetch tables', res.status); const data = await res.json(); for (const t of data.list || []) { tableIdCache[t.title] = t.id; } if (!tableIdCache[tableName]) { throw new NocoDBError(`Table "${tableName}" not found in base ${NOCODB_BASE_ID}`, 404); } return tableIdCache[tableName]; } function buildWhere(conditions) { if (!conditions || conditions.length === 0) return ''; return conditions .map(c => `(${c.field},${c.op},${c.value})`) .join('~and'); } async function request(method, url, body) { const opts = { method, headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json', }, }; if (body !== undefined) opts.body = JSON.stringify(body); const res = await fetch(url, opts); if (!res.ok) { let details; try { details = await res.json(); } catch {} throw new NocoDBError( `NocoDB ${method} ${url} failed: ${res.status}`, res.status, details ); } // DELETE returns empty or {msg} const text = await res.text(); return text ? JSON.parse(text) : {}; } // ─── Link Resolution ───────────────────────────────────────── // Cache: "Table.Field" → { colId, tableId } const linkColCache = {}; async function getLinkColId(table, linkField) { const key = `${table}.${linkField}`; if (linkColCache[key]) return linkColCache[key]; const tableId = await resolveTableId(table); const res = await fetch(`${NOCODB_URL}/api/v2/meta/tables/${tableId}`, { headers: { 'xc-token': NOCODB_TOKEN }, }); if (!res.ok) throw new NocoDBError('Failed to fetch table metadata', res.status); const meta = await res.json(); for (const c of meta.columns || []) { if (c.uidt === 'Links' || c.uidt === 'LinkToAnotherRecord') { linkColCache[`${table}.${c.title}`] = { colId: c.id, tableId }; } } return linkColCache[key] || null; } async function fetchLinkedRecords(table, recordId, linkField) { const info = await getLinkColId(table, linkField); if (!info) return []; try { const data = await request('GET', `${NOCODB_URL}/api/v2/tables/${info.tableId}/links/${info.colId}/records/${recordId}`); return data.list || []; } catch { return []; } } async function resolveLinks(table, records, linkFields) { if (!records || !linkFields || linkFields.length === 0) return; const arr = Array.isArray(records) ? records : [records]; const promises = []; for (const record of arr) { for (const field of linkFields) { const val = record[field]; if (typeof val === 'number' && val > 0) { promises.push( fetchLinkedRecords(table, record.Id, field) .then(linked => { record[field] = linked; }) ); } else if (typeof val === 'number') { record[field] = []; } } } await Promise.all(promises); } const nocodb = { /** * List records with optional filtering, sorting, pagination. * Pass `links: ['Field1','Field2']` to resolve linked records. */ async list(table, { where, sort, fields, limit, offset, links } = {}) { const tableId = await resolveTableId(table); const params = new URLSearchParams(); if (where) params.set('where', typeof where === 'string' ? where : buildWhere(where)); if (sort) params.set('sort', sort); if (fields) params.set('fields', Array.isArray(fields) ? fields.join(',') : fields); if (limit) params.set('limit', String(limit)); if (offset) params.set('offset', String(offset)); const qs = params.toString(); const data = await request('GET', `${NOCODB_URL}/api/v2/tables/${tableId}/records${qs ? '?' + qs : ''}`); const records = data.list || []; if (links && links.length > 0) { await resolveLinks(table, records, links); } return records; }, /** * Get a single record by row ID. * Pass `{ links: ['Field1'] }` as third arg to resolve linked records. */ async get(table, rowId, { links } = {}) { const tableId = await resolveTableId(table); const record = await request('GET', `${NOCODB_URL}/api/v2/tables/${tableId}/records/${rowId}`); if (links && links.length > 0) { await resolveLinks(table, [record], links); } return record; }, /** * Create a single record, returns the created record */ async create(table, data) { const tableId = await resolveTableId(table); return request('POST', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, data); }, /** * Update a single record by row ID */ async update(table, rowId, data) { const tableId = await resolveTableId(table); return request('PATCH', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, { Id: rowId, ...data }); }, /** * Delete a single record by row ID */ async delete(table, rowId) { const tableId = await resolveTableId(table); return request('DELETE', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, { Id: rowId }); }, /** * Bulk create records */ async bulkCreate(table, records) { const tableId = await resolveTableId(table); return request('POST', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, records); }, /** * Bulk update records (each must include Id) */ async bulkUpdate(table, records) { const tableId = await resolveTableId(table); return request('PATCH', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, records); }, /** * Bulk delete records (each must include Id) */ async bulkDelete(table, records) { const tableId = await resolveTableId(table); return request('DELETE', `${NOCODB_URL}/api/v2/tables/${tableId}/records`, records); }, // Expose helpers buildWhere, resolveTableId, getLinkColId, NocoDBError, clearCache() { Object.keys(tableIdCache).forEach(k => delete tableIdCache[k]); }, // Config getters get url() { return NOCODB_URL; }, get token() { return NOCODB_TOKEN; }, get baseId() { return NOCODB_BASE_ID; }, }; module.exports = nocodb;