@sqb/connect
Version:
Multi-dialect database connection framework written with TypeScript
187 lines (186 loc) • 7.44 kB
JavaScript
import { DataType } from '@sqb/builder';
const isValueProperty = (prop) => prop.fieldAlias && !prop.converter;
const isObjectProperty = (prop) => prop.converter && !prop.findCommand;
const isNestedProperty = (prop) => prop.converter && prop.findCommand;
export class RowConverter {
resultType;
parent;
_properties = {};
_propertyKeys;
constructor(resultType, parent) {
this.resultType = resultType;
this.parent = parent;
}
addValueProperty(args) {
this._propertyKeys = undefined;
const item = {
fieldAlias: args.fieldAlias,
dataType: args.dataType,
parse: args.parse,
};
this._properties[args.name] = item;
return item;
}
addObjectProperty(args) {
this._checkCircularDep(args.type);
this._propertyKeys = undefined;
const converter = new RowConverter(args.type, this);
const item = {
converter,
type: args.type,
};
this._properties[args.name] = item;
return item;
}
addNestedProperty(args) {
this._checkCircularDep(args.type);
const converter = new RowConverter(args.type, this);
const item = {
converter,
type: args.type,
parentField: args.parentField,
keyField: args.keyField,
findCommand: args.findCommand,
sort: args.sort,
paramValues: [],
};
this._properties[args.name] = item;
return item;
}
get keys() {
if (!this._propertyKeys)
this._propertyKeys = Object.keys(this._properties);
return this._propertyKeys;
}
async transform(connection, fields, rows, onTransform) {
const rowLen = rows.length;
const result = [];
for (let rowIdx = 0; rowIdx < rowLen; rowIdx++) {
const row = rows[rowIdx];
const o = (await this._rowToObject(connection, fields, row)) || {};
if (onTransform)
onTransform(fields, row, o);
result.push(o);
}
if (!this.keys)
return result;
await this._iterateForNested(this, connection, fields, rows, result);
// Return only non empty objects
return result.filter(x => !!x);
}
_rowToObject(executor, fields, row) {
// Cache keys for better performance
const fieldKeys = this.keys;
const fieldsLen = fieldKeys.length;
let result;
for (let elIdx = 0; elIdx < fieldsLen; elIdx++) {
const elKey = fieldKeys[elIdx];
const prop = this._properties[elKey];
if (isValueProperty(prop)) {
const field = fields.get(prop.fieldAlias);
if (field) {
let v = row[field.index];
if (typeof prop.parse === 'function')
v = prop.parse(v, elKey);
if (v != null) {
result = result || {};
if (prop.dataType === DataType.JSON && typeof v === 'string')
v = JSON.parse(v);
result[elKey] = v;
}
}
}
else if (isNestedProperty(prop)) {
// One2Many Eager field
// Keep a list of key field/value pairs to fetch rows for eager relation
const _params = prop.paramValues;
const f = fields.get(prop.parentField);
const v = f && row[f.index];
if (v != null && !_params.includes(v))
_params.push(v);
}
else if (isObjectProperty(prop)) {
const v = prop.converter._rowToObject(executor, fields, row);
if (v != null) {
result = result || {};
result[elKey] = v;
}
}
}
if (result)
Object.setPrototypeOf(result, this.resultType.prototype);
return result;
}
async _iterateForNested(node, connection, fields, rows, result) {
// Fetch one-2-many related rows and merge with result rows
const keys = node.keys;
const propertyLen = keys.length;
const promises = [];
for (let propIdx = 0; propIdx < propertyLen; propIdx++) {
const propKey = keys[propIdx];
const prop = node._properties[propKey];
if (isNestedProperty(prop)) {
if (!(prop.paramValues && prop.paramValues.length))
continue;
const resultType = this.resultType;
const promise = (async function (p) {
const findCommand = p.findCommand;
let fld;
const map = new Map();
const r = await findCommand.execute({
connection,
limit: findCommand.maxEagerFetch + 1,
params: { [p.parentField]: p.paramValues },
onTransformRow: (_fields, row, obj) => {
fld = fld || _fields.get('' + p.keyField);
const keyValue = fld && row[fld.index];
if (keyValue != null) {
const keyValues = Array.isArray(keyValue)
? keyValue
: [keyValue];
keyValues.forEach(k => {
let arr = map.get(k);
if (!arr) {
arr = [];
map.set(k, arr);
}
arr.push(obj);
});
}
},
});
if (r.length > findCommand.maxEagerFetch) {
throw new Error(`Number of returning rows for "${propKey}" exceeds maxEagerFetch limit`);
}
for (let i = 0; i < result.length; i++) {
const row = rows[i];
let obj = result[i];
if (!obj) {
obj = {};
Object.setPrototypeOf(result, resultType.prototype);
}
const f = fields.get('' + p.parentField);
const keyValue = f && row[f.index];
if (keyValue != null) {
const arr = map.get(keyValue);
if (arr) {
obj[propKey] = arr;
}
}
}
})(prop);
promises.push(promise);
}
else if (isObjectProperty(prop)) {
promises.push(this._iterateForNested(prop.converter, connection, fields, rows, result));
}
}
await Promise.all(promises);
}
_checkCircularDep(t) {
if (this.resultType === t)
throw new Error('Circular fields requested');
if (this.parent)
this.parent._checkCircularDep(t);
}
}