UNPKG

@xuda.io/xuda-dbs-plugin-xuda

Version:

Xuda Database Socket for Xuda's proprietary structure powered by CouchDB

1,031 lines (897 loc) 29.4 kB
import { createRequire } from 'module'; import _ from 'lodash'; import crypto from 'node:crypto'; const require = createRequire(import.meta.url); const check_unique = async function (e, docP, table_obj, db, app_id_reference) { var len = docP.udfIndex.length; if (!len) { return { code: -11, data: 'table must have primary index' }; } var found_unique = undefined; var ret = { code: 1, data: 'ok' }; for await (var val of docP.udfIndex) { if (!table_obj.tableIndexes) { ret = { code: -14, data: 'table definition missing for: ' + val.fileId, }; break; } const table_index = find_item_by_key_root(table_obj.tableIndexes, 'id', val.indexId); if (!table_index) { ret = { code: -13, data: 'keys definition missing for Table: ' + val.fileId, }; break; } if (!table_index.data.unique) { continue; } if (!table_index?.data?.keys || _.isEmpty(table_index.data.keys)) { ret = { code: -14, data: 'keys definition missing for index: ' + val.indexId, }; break; } var keysArr = table_index.data.keys; var keyValue = []; for await (const [keySegment, valSegment] of Object.entries(keysArr)) { let _fieldType = find_item_by_key(table_obj.tableFields, 'field_id', valSegment).props.fieldType; let _val = await get_cast_val('check_unique', valSegment, _fieldType, val.keyValue[Number(keySegment)]); keyValue.push(_val); } var key = '["' + val.fileId + '","' + val.indexId + '",[' + keyValue.join(',') + ']]'; try { e.view = 'db_index'; const ret = await query_db(e, db, app_id_reference, table_obj); const json = ret.data; if (json.code > 0) { var data = json.data; if (data?.rows?.length) { for await (var val of data.rows) { if (val.id !== docP._id) { found_unique = true; } } if (found_unique) { throw 'error checking unique:' + key; } } } return { code: 1, data: 'ok' }; } catch (msg) { console.error(msg); ret = { code: -22, data: msg }; break; } } return ret; }; const get_index_json = async function (docInP, table_obj) { try { var keysArr = []; var index_json = []; var key_val = []; if (docInP.udfData && table_obj) { if (table_obj.tableIndexes) { for await (var valIndex of table_obj.tableIndexes) { keysArr = valIndex.data.keys; key_val = []; for await (var valSegment of keysArr) { if (docInP.udfData.data[valSegment]) { key_val.push(docInP.udfData.data[valSegment]); continue; } const field_obj = find_item_by_key(table_obj.tableFields, 'field_id', valSegment); if (!field_obj) { throw 'field not found in key: ' + valSegment; } let _val = await get_cast_val('get_index_json', field_obj.id, field_obj.props.fieldType, null); key_val.push(_val); } index_json.push({ fileId: docInP.udfData.udffileid, indexId: valIndex.id, keySegments: keysArr, keyValue: key_val, }); } } } return { code: 1, data: index_json }; } catch (msg) { return { code: -1, data: msg }; } }; const find_item_by_key_root = function (arr, key, val) { return _.find(arr, function (e) { return e[key] === val; }); }; const find_item_by_key = function (arr, key, val) { return _.find(arr, function (e) { return e.data[key] === val; }); }; const get_cast_val = async function (source, attributeP, typeP, valP) { const module = require('@xuda.io/xu_cast'); return module.cast( typeP, valP, function (res) { var msg = `error converting ${attributeP} from ${valP} to ${typeP}`; console.error(source, msg); }, function (res) { var msg = `type mismatch auto conversion made to ${attributeP} from value ${valP} to ${typeP}`; console.warn(source, msg); }, ); }; const get_opt = function (e, table_obj) { var limit = 99999; var skip = 0; if (e.limit) { limit = Number(e.limit); } if (e.skip) { skip = JSON.parse(e.skip); } var fields = []; if (e.fields) { for (const [key, val] of Object.entries(typeof e.fields === 'string' ? e.fields.split(',') : e.fields)) { fields.push(val); } } else { for (const [key, val] of Object.entries(table_obj.tableFields)) { fields.push(val.data.field_id); } } var data = {}; var from = e.filter_from ? JSON.parse(e.filter_from) : {}; var to = e.filter_to ? JSON.parse(e.filter_to) : {}; var sort = []; for (const [key, val] of Object.entries(from)) { var field_name = key; sort.push(field_name); if (val === to[key]) { data[field_name] = val; } else { data[field_name] = { $gte: val }; } } for (const [key, val] of Object.entries(to)) { var field_name = key; if (val !== from[key]) { data[field_name]['$lte'] = val; } } var selector = {}; if (from && !_.isEmpty(from)) { selector = data; } var opt = { selector, limit, skip, fields, }; if (sort.length) { opt.sort = sort; } let _sortModel = e?.sortModel || []; if (typeof _sortModel === 'string') { _sortModel = JSON.parse(e.sortModel); } if (_sortModel.length) { opt.sort = _sortModel; if (opt.sort) { for (const [key, val] of Object.entries(opt.sort)) { var field_name = val.field_id || val.colId; opt.sort[key] = { ['udfData.data.' + field_name]: val.sort_dir || val.sort, }; } } } if (e.total_fields_info) { fields = []; for (const [key, val] of Object.entries(JSON.parse(e.total_fields_info))) { var field_name = val.field_id; fields.push(field_name); } opt.fields = fields; } for (const [key, val] of Object.entries(opt.fields)) { opt.fields[key] = 'udfData.data.' + val; } const _sort_model = typeof e.sortModel === 'string' ? JSON.parse(e.sortModel) : e.sortModel; if (!e?.sortModel || !_sort_model.length) { if (opt.sort) { for (const [key, val] of Object.entries(opt.sort)) { opt.sort[key] = { ['udfData.data.' + val]: e.sortOrder === 'des' ? 'desc' : 'asc', }; } } } var selector_new = {}; function replaceKeysInQuery(query) { const keys_to_replace = table_obj.tableFields.map((e) => { return e?.data?.field_id; }); function recursiveReplace(obj) { if (typeof obj === 'object' && obj !== null) { let newObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { let newKey = keys_to_replace.includes(key) ? `udfData.data.${key}` : key; newObj[newKey] = recursiveReplace(obj[key]); } } return newObj; } else { return obj; } } return recursiveReplace(query); } function replaceRegexOptions(query) { function recursiveReplace(obj) { if (typeof obj === 'object' && obj !== null) { let newObj = Array.isArray(obj) ? [] : {}; for (let key in obj) { if (obj.hasOwnProperty(key)) { if (key === '$regex') { newObj[key] = obj.$options ? `(?${obj.$options})${obj[key]}` : obj[key]; return newObj; } newObj[key] = recursiveReplace(obj[key]); } } return newObj; } else { return obj; } } return recursiveReplace(query); } if (e.dataSourceFilterModelType === 'query' && e.filterModelMongo) { selector_new['$and'] = [replaceRegexOptions(replaceKeysInQuery(typeof e.filterModelMongo === 'string' ? JSON.parse(e.filterModelMongo) : e.filterModelMongo))]; } const _filterModelUserMongo = typeof e.filterModelUserMongo === 'string' ? JSON.parse(e.filterModelUserMongo) : e.filterModelUserMongo; if (!_.isEmpty(_filterModelUserMongo)) { if (!selector_new['$and']) { selector_new['$and'] = []; } selector_new['$and'].push(replaceRegexOptions(replaceKeysInQuery(_filterModelUserMongo))); } if (e.dataSourceFilterModelType === 'index') { for (const [key, val] of Object.entries(opt.selector)) { selector_new['udfData.data.' + key] = val; } } opt.selector = selector_new; opt.selector['udfData.udffileid'] = e.table_id; opt.selector['docType'] = 'database'; opt.selector.stat = e.archived ? 4 : 3; opt.fields.push('_id'); opt.fields.push('_rev'); if (e.selector) { opt.selector = { ...e.selector, ...opt.selector }; } return opt; }; const query_db = async function (e, db, app_id_reference, table_obj) { var key = undefined; var opt = get_opt(e, table_obj); const runtime_get_mango_data = async function () { const done = async function (body) { const raw_data = async function () { var rows = []; for (var val of body.docs) { var data = {}; if (e.fields) { for (const [key2, val2] of Object.entries(typeof e.fields === 'string' ? e.fields.split(',') : e.fields)) { if (val.udfData) { data[val2] = val['udfData']['data'][val2]; } else { data[val2] = null; } } } else { for (const [key2, val2] of Object.entries(table_obj.tableFields)) { if (val.udfData) { data[val2.data.field_id] = val['udfData']['data'][val2.data.field_id]; } else { data[val2.data.field_id] = null; } } } rows.push({ id: val._id, value: data, }); } return { code: 1, data: { rows: rows, total_rows: rows.length, opt } }; }; const count_data = async function () { var rows = []; var keys_obj = {}; const table_index = find_item_by_key_root(table_obj.tableIndexes, 'id', e.indexId); if (e.indexId && table_index) { var index_keys = table_index.data.keys; var index_id = e.indexId; for (var val of body.docs) { var data = {}; const _tableFieldsObj = find_item_by_key(table_obj.tableFields, 'field_id', e.field_id); for (const [key2, val2] of Object.entries(table_obj.tableFields)) { data[val2.data.field_id] = val['udfData']['data'][val2.data.field_id]; } var key_arr = []; for (const [key_idx, key_name] of Object.entries(index_keys)) { key_arr.push(data[key_name]); } keys_obj[key_arr]++; } for (const [key_arr, key_count] of Object.entries(keys_obj)) { rows.push({ key: [e.key, index_id, key_arr], value: body.docs.length, }); } } else { rows.push({ key: '', value: body.docs.length }); } return { code: 1, data: { rows: rows, total_rows: rows.length, opt } }; }; const totals = async function () { const median = (arr) => { const mid = Math.floor(arr.length / 2), nums = [...arr].sort((a, b) => a - b); return arr.length % 2 !== 0 ? nums[mid] : (nums[mid - 1] + nums[mid]) / 2; }; var total_fields_info = JSON.parse(e.total_fields_info); var totals_prop = {}; var totals_obj = {}; var totals_counts = {}; var totals_arr = {}; var totals_sums = {}; var totals_group_obj = {}; for (const [key, val] of Object.entries(total_fields_info)) { totals_counts[val.field_id] = 0; totals_prop[val.field_id] = val; totals_arr[val.field_id] = []; totals_sums[val.field_id] = 0; totals_group_obj[val.field_id] = {}; } for (var row_data of body.docs) { for (const [key, val] of Object.entries(row_data.udfData.data)) { var field_id = key; var value = val; totals_counts[field_id]++; switch (totals_prop[field_id].sum_type) { case 'sum': if (typeof totals_obj[field_id] === 'undefined') { totals_obj[field_id] = value; } else { totals_obj[field_id] += value; } break; case 'average': totals_sums[field_id] += value; totals_obj[field_id] = totals_sums[field_id] / totals_counts[field_id]; break; case 'median': if (!totals_arr[field_id].includes(value)) { totals_arr[field_id].push(value); } totals_obj[field_id] = median(totals_arr[field_id]); break; case 'min': if (typeof totals_obj[field_id] === 'undefined' || value < totals_obj[field_id]) { totals_obj[field_id] = value; } break; case 'max': if (typeof totals_obj[field_id] === 'undefined' || value > totals_obj[field_id]) { totals_obj[field_id] = value; } break; case 'count': if (typeof totals_obj[field_id] === 'undefined') { totals_obj[field_id] = 1; } else { totals_obj[field_id]++; } break; case 'distinct': if (!totals_arr[field_id].includes(value)) { totals_arr[field_id].push(value); } totals_obj[field_id] = totals_arr[field_id].length; break; case 'group': if (!totals_obj[field_id]) { totals_obj[field_id] = {}; } if (typeof totals_obj[field_id][value] === 'undefined') { totals_obj[field_id][value] = 1; } else { totals_obj[field_id][value]++; } break; default: totals_obj[field_id] = null; } } } return { code: 1, data: totals_obj }; }; if (e.count) { return count_data(); } if (e.total_fields_info) { return totals(); } return raw_data(); }; const get_mango_index_fields = function (selector) { const fields = new Set(); const add_selector_fields = function (obj) { if (!obj || typeof obj !== 'object' || Array.isArray(obj)) { return; } for (const [key, val] of Object.entries(obj)) { if (['$and', '$or', '$nor'].includes(key)) { if (Array.isArray(val)) { val.forEach(add_selector_fields); } continue; } if (key.startsWith('$')) { continue; } fields.add(key); } }; add_selector_fields(selector); return [...fields]; }; const create_mango_index_for_selector = async function () { const index = get_mango_index_fields(opt.selector); if (!index.length) { return; } const index_suffix = index.join('_').replace(/[^a-zA-Z0-9_]+/g, '_').slice(0, 80) || 'selector'; const mango_index_obj = { index: { fields: index, }, name: `index_${e.table_id}_${index_suffix}`, ddoc: `mango_index_table_${e.table_id}_${index_suffix}`, }; try { await db.createIndex(mango_index_obj); } catch (err) { // The read already succeeded; index creation is only an optimization. } }; // A COUNT must span the WHOLE result set, not the current page. `opt` carries // the page limit/skip, so counting body.docs.length under them could never // exceed one page. Ask for ids only (unless count_data's index-grouping // branch needs the field data) and cap the scan so a pathological table // cannot stall the request. const COUNT_SCAN_CAP = 100000; const find_opt = e.count ? Object.assign({}, opt, { limit: COUNT_SCAN_CAP, skip: 0, fields: e.indexId ? opt.fields : ['_id'], }) : opt; try { try { const doc = await db.find(find_opt); if (doc?.warning?.includes('No matching index found')) { await create_mango_index_for_selector(); } return await done(doc); } catch (err) { if (err.error === 'no_usable_index') { const index_name = `index_${e.table_id}_${new Date().valueOf().toString()}`; const _selector = _.cloneDeep(opt.selector); delete _selector['$and']; const _sort = _.cloneDeep(opt.sort); const mango_index_obj = { index: { fields: Object.keys( _.reduce( _sort, (ret, val) => { ret[Object.keys(val)[0]] = val; return ret; }, _selector, ), ), }, name: index_name, ddoc: `mango_index_table_${e.table_id}`, }; try { const result = await db.createIndex(mango_index_obj); const doc = await db.find(find_opt); return await done(doc); } catch (err) { return { code: -1, data: err.message }; } } else { return { code: -1, data: err.message, opt }; } } } catch (err) { return { code: -1, data: err.message }; } }; const runtime_get_query_data = async function () { var limit = 99999999; var skip = 0; if (e.limit) { limit = e.limitP; } if (e.skipP) { skip = e.skipP; } var view_opt = undefined; if (key) { view_opt = { key, include_docs: !e.ids && view !== 'db_index_reduce', limit, skip, }; } else { if (e.reduce) { view_opt = { startkey: e.startkey ? JSON.parse(e.startkey) : '', endkey: e.endkey ? JSON.parse(e.endkey) : '', include_docs: false, reduce: true, group_level: 3, descending: e.desc, }; } else { view_opt = { startkey: e.startkey ? JSON.parse(e.startkey) : '', endkey: e.endkey ? JSON.parse(e.endkey) : '', include_docs: !e.ids, limit, skip, descending: e.desc, }; } } try { const body = await db.view(`xuda`, view, view_opt); if (e.reduce) { let rows = []; const table_index = find_item_by_key_root(table_obj.tableIndexes, 'id', e.indexId); for await (var val of body.rows) { const arr = val.key[2]; var obj = {}; for await (const [arr_key, arr_val] of Object.entries(arr)) { let field_id = table_index.data.keys[Number(arr_key)]; const _tableFieldsObj = find_item_by_key(table_obj.tableFields, 'field_id', field_id); let field_value = await get_cast_val('dbs_update', e.field_id, _tableFieldsObj.props.fieldType, arr_val); obj[field_id] = field_value; } obj['REDUCE_VALUE'] = val.value; rows.push({ id: crypto.randomUUID(), value: obj }); } return { code: 1, data: { rows, opt } }; } var rows = []; for await (var val of body.rows) { if (e.ids) { rows.push({ id: val.id, value: val.value._id }); continue; } rows.push({ id: val.id, value: val.doc.udfData, }); } return { code: 1, data: { rows: rows, total_rows: rows.length, opt } }; } catch (err) { return { code: -1, data: err.message }; } }; const count_tables = async function () { var opt = { reduce: true, group_level: 1, }; if (e.table_id) { opt.key = e.table_id; } const body = await db.view(`xuda`, 'db_table_counts', opt); return { code: 1, data: body }; }; var view; if (e.reduce && JSON.parse(e.reduce)) { view = 'db_index_reduce'; return await runtime_get_query_data(); } if (e.view === 'db_index') { key = e.key; view = e.view; return await runtime_get_query_data(); } // A COUNT must answer "how many rows match THIS query", not "how many rows are // in the table". count_tables() reads the db_table_counts reduce view keyed by // table_id alone, so it ignores the selector completely — and `filter_from` is // only ever set for dataSourceFilterModelType==='index', so every MONGO-filtered // count landed here and came back with the whole-table total. Live symptom: a // filtered question list returned its correct rows while rows_found stayed 837, // so the pager kept offering 168 pages over an empty/short result set. // Keep the cheap view for a genuinely UNFILTERED count; anything carrying a // selector is counted through Mango against that same selector. const _has_filter_value = function (v) { if (v === undefined || v === null || v === '') return false; try { const o = typeof v === 'string' ? JSON.parse(v) : v; return !_.isEmpty(o); } catch (err) { return true; // present but unparseable — assume it filters; never over-count } }; const _count_is_filtered = !!e.filter_from || _has_filter_value(e.filterModelMongo) || _has_filter_value(e.filterModelUserMongo) || _has_filter_value(e.filterModelNative); if (e.count && !_count_is_filtered) { return await count_tables(); } return await runtime_get_mango_data(); }; export const create = async (params, setup_doc, resolve, reject) => { const e = params.e; const db = params.db; const app_id_reference = params.app_id_reference; const table_obj = params.table_obj; const create_data_obj = async function (table_data) { var doc = {}; for await (const val of table_obj.tableFields) { doc[val.data.field_id] = await get_cast_val('dbs_create', val.data.field_id, val.props.fieldType, table_data[val.data.field_id]); } return doc; }; const get_uuid = function (table_id) { return 'dbs-' + table_id + '_' + crypto.randomUUID(); }; const batch = async function () { var docs = []; try { for await (var val of e.table_data) { var doc = { _id: get_uuid(e.table_id), docType: 'database', user: e.uid, app_id: e.app_id, stat: 3, udfData: { udffileid: e.table_id, data: await create_data_obj(val), error: {}, }, ts: Date.now(), date: Date.now(), }; const index_ret = await get_index_json(doc, table_obj); if (index_ret < 0) throw index_ret.data; doc.udfIndex = index_ret.data; const ret = await check_unique(e, doc, table_obj, db, app_id_reference); if (ret.code < 0) { throw ret; } docs.push(doc); } try { const body = await db.bulk({ docs: docs }); return resolve(body); } catch (error) { return reject(error); } } catch (msg) { return reject(msg); } }; const single = async function () { var doc = { _id: get_uuid(e.table_id), docType: 'database', user: e.uid, app_id: e.app_id, stat: 3, udfData: { udffileid: e.table_id, data: await create_data_obj(e.table_data), error: {}, }, ts: Date.now(), date: Date.now(), }; const index_ret = await get_index_json(doc, table_obj); if (index_ret.code < 0) { return index_ret; } doc.udfIndex = index_ret.data; const ret = await check_unique(e, doc, table_obj, db, app_id_reference); if (ret.code < 0) { return reject(ret.data); } try { const body = await db.insert(doc); return resolve(body); } catch (error) { return reject(error); } }; if (Array.isArray(e.table_data)) { return await batch(); } return await single(); }; export const read = async (params, setup_doc, resolve, reject) => { const e = params.e; const db = params.db; const app_id_reference = params.app_id_reference; const table_obj = params.table_obj; const ret = await query_db(e, db, app_id_reference, table_obj); if (ret.code < 0) { return reject(ret.data); } return resolve(ret.data); }; export const update = async (params, setup_doc, resolve, reject) => { const e = params.e; const db = params.db; const app_id_reference = params.app_id_reference; const table_obj = params.table_obj; try { if (e.row_id === 'newRecord') { return create(params, setup_doc, resolve, reject); } var doc = await db.get(e.row_id, {}); var error = undefined; if (!e.field_id && !e.table_data) { error = 'Invalid field_id or table_data object to save'; return reject(error); } if (e.field_id) { let _tableFieldsObj = find_item_by_key(table_obj.tableFields, 'field_id', e.field_id); if (!_tableFieldsObj || _.isEmpty(_tableFieldsObj)) { return reject('Error: ' + e.field_id + ' - field not found'); } doc.udfData.data[e.field_id] = await get_cast_val('dbs_update', e.field_id, _tableFieldsObj.props.fieldType, e.field_value); } if (e.table_data) { for await (const [key, val] of Object.entries(e.table_data)) { let _tableFieldsObj = find_item_by_key(table_obj.tableFields, 'field_id', key); if (!_tableFieldsObj || _.isEmpty(_tableFieldsObj)) { reject('Error: ' + key + ' - field not found'); break; } doc.udfData.data[key] = await get_cast_val('dbs_update', key, _tableFieldsObj.props.fieldType, val); } } if (error) { return reject(error); } doc.ts = Date.now(); const index_ret = await get_index_json(doc, table_obj); if (index_ret.code < 0) { return index_ret; } doc.udfIndex = index_ret.data; const ret = await check_unique(e, doc, table_obj, db, app_id_reference); if (ret.code < 0) { return ret; } try { const body = await db.insert(doc); return resolve(body); } catch (error) { return reject(error); } } catch (error) { return reject(error); } }; export const deleteFunc = async (params, setup_doc, resolve, reject) => { const e = params.e; const db = params.db; const app_id_reference = params.app_id_reference; const table_obj = params.table_obj; const data = await db.fetch({ keys: e.ids }); var docs_to_delete = []; for await (var val of data.rows) { var doc = val.doc; if (doc && doc._id) { if (e.permanent) { docs_to_delete.push({ _id: doc._id, _rev: doc._rev, _deleted: true, }); } else { doc.stat = 4; doc.ts = Date.now(); docs_to_delete.push(doc); } } } try { await db.bulk({ docs: docs_to_delete }); resolve(docs_to_delete); } catch (error) { return { code: -110, data: error }; } }; export { deleteFunc as delete }; // Renaming export to avoid reserved keyword conflict export const restore = async (params, setup_doc, resolve, reject) => { const e = params.e; const db = params.db; const app_id_reference = params.app_id_reference; const table_obj = params.table_obj; var docs_to_restore = []; const data = await db.fetch({ keys: e.ids }); for await (var val of data.rows) { var doc = val.doc; if (doc && doc._id) { doc.stat = 3; doc.ts = Date.now(); docs_to_restore.push(doc); } } try { await db.bulk({ docs: docs_to_restore }); resolve(docs_to_restore); } catch (error) { return reject(error); } }; export const check_unique_export = async (params, setup_doc, resolve, reject) => { const e = params.e; const db = params.db; const app_id_reference = params.app_id_reference; const table_obj = params.table_obj; const doc = params.e.doc; try { const ret = await check_unique(e, doc, table_obj, db, app_id_reference); if (ret.code < 0) { throw ret; } resolve(ret); } catch (error) { return reject(error); } }; export { check_unique_export as check_unique }; export const get_connection = async (params, setup_doc, resolve, reject) => { try { if (!setup_doc.db_connection_string) throw 'db_connection_string missing'; const nano = require('nano')(setup_doc.db_connection_string); if (!nano) { return reject('connection error'); } await nano.get('_design/xuda'); resolve(nano); } catch (err) { return reject(err.message || err); } };