217 lines
6.7 KiB
JavaScript
217 lines
6.7 KiB
JavaScript
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;
|