UNPKG

backendless

Version:

Backendless JavaScript SDK for Node.js and the browser

522 lines (408 loc) 14.9 kB
import Utils from '../utils' import Expression from '../expression' import RTHandlers from './rt-handlers' import DataQueryBuilder from './data-query-builder' import LoadRelationsQueryBuilder from './load-relations-query-builder' import JSONUpdateBuilder from './json-update-builder' import geoConstructor, { GEO_CLASSES } from './geo/geo-constructor' import Geometry from './geo/geometry' function buildFindFirstLastQuery(queryBuilder, sortDir) { const query = (queryBuilder instanceof DataQueryBuilder) ? queryBuilder.toJSON() : (queryBuilder ? { ...queryBuilder } : {}) query.pageSize = 1 query.offset = 0 const { sortBy } = query if (!sortBy) { query.sortBy = [`created ${sortDir}`] } return DataQueryBuilder.toRequestBody(query) } export default class DataStore { constructor(model, dataService) { this.app = dataService.app this.classToTableMap = dataService.classToTableMap if (typeof model === 'string') { this.className = model this.model = this.classToTableMap[this.className] } else { this.className = Utils.getClassName(model) this.model = this.classToTableMap[this.className] || model } if (!this.className) { throw new Error('Class name should be specified') } } rt() { return this.rtHandlers = this.rtHandlers || new RTHandlers(this) } async save(object, isUpsert) { const url = isUpsert ? this.app.urls.dataTableUpsert(this.className) : this.app.urls.dataTable(this.className) return this.app.request .put({ url, data: convertToServerRecord(object), }) .then(result => this.parseResponse(result)) } async deepSave(object) { return this.app.request .put({ url : this.app.urls.dataTableDeepSave(this.className), data: convertToServerRecord(object), }) .then(result => this.parseResponse(result)) } async remove(object) { const objectId = object && object.objectId || object if (!objectId || (typeof objectId !== 'string' && typeof objectId !== 'number')) { throw new Error('Object Id must be provided and must be a string or number.') } return this.app.request.delete({ url: this.app.urls.dataTableObject(this.className, objectId), }) } async find(query) { return this.app.request .post({ url : this.app.urls.dataTableFind(this.className), data: DataQueryBuilder.toRequestBody(query), }) .then(result => this.parseResponse(result)) } async group(query) { return this.app.request .post({ url : this.app.urls.dataGrouping(this.className), data: DataQueryBuilder.toRequestBody(query) }) } async countInGroup(query) { if (!query.groupPath || typeof query.groupPath !== 'object') { throw new Error('Group Path must be provided and must be an object.') } return this.app.request .post({ url : `${this.app.urls.dataGrouping(this.className)}/count`, data: DataQueryBuilder.toRequestBody(query) }) } async findById(objectId, query) { let result if (objectId && typeof objectId === 'object' && !Array.isArray(objectId)) { // this is relevant for External Data Connectors where may be more that on primary key if (!Object.keys(objectId).length) { throw new Error('Provided object must have at least 1 primary keys.') } result = await this.app.request.get({ url : this.app.urls.dataTablePrimaryKey(this.className), query: objectId, }) } else { if (!objectId || (typeof objectId !== 'string' && typeof objectId !== 'number')) { throw new Error('Object Id must be provided and must be a string or number or an object of primary keys.') } if (query) { query.pageSize = null query.offset = null } result = await this.app.request .get({ url : this.app.urls.dataTableObject(this.className, objectId), queryString: DataQueryBuilder.toQueryString(query), }) } return this.parseResponse(result) } async findFirst(query) { return this.app.request .post({ url : this.app.urls.dataTableFind(this.className), data: buildFindFirstLastQuery(query, 'asc'), }) .then(result => this.parseResponse(result[0])) } async findLast(query) { return this.app.request .post({ url : this.app.urls.dataTableFind(this.className), data: buildFindFirstLastQuery(query, 'desc'), }) .then(result => this.parseResponse(result[0])) } async getObjectCount(condition) { let distinct = undefined let groupBy = undefined if (condition) { if (condition instanceof DataQueryBuilder) { distinct = condition.getDistinct() || undefined groupBy = condition.getGroupBy() || undefined condition = condition.getWhereClause() || undefined } else if (typeof condition !== 'string') { throw new Error('Condition must be a string or an instance of DataQueryBuilder.') } } return this.app.request.post({ url : this.app.urls.dataTableCount(this.className), data: { where: condition, distinct, groupBy }, }) } async loadRelations(parent, queryBuilder) { const parentObjectId = parent && parent.objectId || parent if (!parentObjectId || (typeof parentObjectId !== 'string' && typeof parentObjectId !== 'number')) { throw new Error('Parent Object Id must be provided and must be a string or number.') } const { relationName, relationModel, ...query } = queryBuilder instanceof LoadRelationsQueryBuilder ? queryBuilder.toJSON() : queryBuilder if (!relationName || typeof relationName !== 'string') { throw new Error('Relation Name must be provided and must be a string.') } return this.app.request .get({ url : this.app.urls.dataTableObjectRelation(this.className, parentObjectId, relationName), queryString: LoadRelationsQueryBuilder.toQueryString(query) }) .then(result => this.parseRelationsResponse(result, relationModel)) } async setRelation(parent, columnName, children) { return this.changeRelation(this.app.request.Methods.POST, parent, columnName, children) } async addRelation(parent, columnName, children) { return this.changeRelation(this.app.request.Methods.PUT, parent, columnName, children) } async deleteRelation(parent, columnName, children) { return this.changeRelation(this.app.request.Methods.DELETE, parent, columnName, children) } async bulkCreate(objects) { const errorMessage = 'Objects must be provided and must be an array of objects.' if (!objects || !Array.isArray(objects)) { throw new Error(errorMessage) } objects = objects.map(object => { if (!object || typeof object !== 'object' || Array.isArray(object)) { throw new Error(errorMessage) } return object }) return this.app.request.post({ url : this.app.urls.dataBulkTable(this.className), data: objects, }) } async bulkUpsert(objects) { const errorMessage = 'Objects must be provided and must be an array of objects.' if (!objects || !Array.isArray(objects) || !objects.length) { throw new Error(errorMessage) } objects = objects.map(object => { if (!object || typeof object !== 'object' || Array.isArray(object)) { throw new Error(errorMessage) } return object }) return this.app.request.put({ url : this.app.urls.dataBulkTableUpsert(this.className), data: objects, }) } async bulkUpdate(condition, changes) { if (!condition || typeof condition !== 'string') { throw new Error('Condition must be provided and must be a string.') } if (!changes || typeof changes !== 'object' || Array.isArray(changes)) { throw new Error('Changes must be provided and must be an object.') } return this.app.request.put({ url : this.app.urls.dataBulkTable(this.className), query: { where: condition }, data : changes, }) } async bulkDelete(condition) { if (!condition || (typeof condition !== 'string' && !Array.isArray(condition))) { throw new Error('Condition must be provided and must be a string or a list of objects.') } const queryData = {} if (typeof condition === 'string') { queryData.where = condition } else { const objectIds = condition.map(object => { const objectId = object && object.objectId || object if (!objectId || (typeof objectId !== 'string' && typeof objectId !== 'number')) { throw new Error( 'Can not transform "objects" to "whereClause". ' + 'Item must be a string or number or an object with property "objectId" as string.' ) } return `'${objectId}'` }) queryData.where = `objectId in (${objectIds.join(',')})` } return this.app.request.post({ url : this.app.urls.dataBulkTableDelete(this.className), data: queryData }) } /** * @private * */ parseRelationsResponse(result, RelationModel) { return convertToClientRecords(result, RelationModel, this) } /** * @private * */ parseResponse(result) { return convertToClientRecords(result, this.model, this) } /** * @private * */ changeRelation(method, parent, columnName, children) { const parentId = parent && parent.objectId || parent if (!parentId || (typeof parentId !== 'string' && typeof parentId !== 'number')) { throw new Error( 'Relation Parent must be provided and must be a string or number or an object with objectId property.' ) } if (!columnName || typeof columnName !== 'string') { throw new Error('Relation Column Name must be provided and must be a string.') } if (!children || (typeof children !== 'string' && !Array.isArray(children))) { throw new Error('Relation Children must be provided and must be a string or a list of objects.') } const condition = {} if (typeof children === 'string') { condition.whereClause = children } else { condition.childrenIds = children.map(child => { const childId = child && child.objectId || child if (!childId || (typeof childId !== 'string' && typeof childId !== 'number')) { throw new Error('Child Id must be provided and must be a string or number.') } return childId }) } const query = {} if (condition.whereClause) { query.whereClause = condition.whereClause } return this.app.request.send({ method, url : this.app.urls.dataTableObjectRelation(this.className, parentId, columnName), query, data: condition.childrenIds }) } } const convertToServerRecord = (() => { return sourceRecord => { const context = { instancesMap: new WeakMap() } return processTargetProps(context, sourceRecord, {}) } function processTargetProps(context, source, target) { for (const prop in source) { if (Array.isArray(source[prop])) { processTargetProps(context, source[prop], target[prop] = []) } else if ( source[prop] && typeof source[prop] === 'object' && !(source[prop] instanceof Geometry) && !(source[prop] instanceof JSONUpdateBuilder) && !(source[prop] instanceof Expression)) { if (source[prop] instanceof Date) { target[prop] = source[prop].getTime() } else if (context.instancesMap.has(source[prop])) { const iteratedTarget = context.instancesMap.get(source[prop]) if (!iteratedTarget.__subID) { iteratedTarget.__subID = Utils.uuid() } target[prop] = { __originSubID: iteratedTarget.__subID } } else { const iteratedTarget = target[prop] = {} context.instancesMap.set(source[prop], iteratedTarget) processTargetProps(context, source[prop], iteratedTarget) } } else { target[prop] = source[prop] } } return target } })() const convertToClientRecords = (() => { return (records, RootModel, dataStore) => { if (!records) { return records } const context = { RootModel, app : dataStore.app, classToTableMap: dataStore.classToTableMap, subIds : {}, postAssign : [], } const result = Array.isArray(records) ? records.map(record => sanitizeItem(context, record)) : sanitizeItem(context, records) assignPostRelations(context) return result } function createTargetRecord(context, source, target, prop) { const __subID = source[prop].__subID if (__subID && context.subIds[__subID]) { target[prop] = context.subIds[__subID] delete source[prop].__subID } else { const Model = context.classToTableMap[source[prop].___class] target[prop] = Model ? new Model() : {} if (__subID && !context.subIds[__subID]) { context.subIds[__subID] = target[prop] delete source[prop].__subID } processTargetProps(context, source[prop], target[prop]) } } function processTargetProp(context, source, target, prop) { if (Array.isArray(source[prop])) { processTargetProps(context, source[prop], target[prop] = []) } else if (source[prop] && typeof source[prop] === 'object') { if (GEO_CLASSES.includes(source[prop].___class)) { target[prop] = geoConstructor(source[prop]) } else if (source[prop].__originSubID) { context.postAssign.push([target, prop, source[prop].__originSubID]) } else { createTargetRecord(context, source, target, prop) } } else { target[prop] = source[prop] } } function processTargetProps(context, source, target) { for (const prop in source) { processTargetProp(context, source, target, prop) } } function sanitizeItem(context, sourceRecord) { const Model = context.RootModel || context.classToTableMap[sourceRecord.___class] const targetRecord = Model ? new Model() : {} if (sourceRecord.__subID) { if (context.subIds[sourceRecord.__subID]) { return context.subIds[sourceRecord.__subID] } context.subIds[sourceRecord.__subID] = targetRecord delete sourceRecord.__subID } processTargetProps(context, sourceRecord, targetRecord) return targetRecord } function assignPostRelations(context) { context.postAssign.forEach(([target, prop, __originSubID]) => { target[prop] = context.subIds[__originSubID] }) } })()