mirror of
https://github.com/fluxscape/fluxscape.git
synced 2026-01-11 23:02:55 +01:00
Adding support for some more options when fetching a record.
91f9aca25b/src/Routers/ClassesRouter.js (L60-L68)
658 lines
20 KiB
JavaScript
658 lines
20 KiB
JavaScript
const NoodlRuntime = require('../../noodl-runtime');
|
|
const Model = require('../model');
|
|
const Collection = require('../collection');
|
|
const CloudFile = require('./cloudfile');
|
|
const EventEmitter = require('../events');
|
|
|
|
const _protectedFields = {
|
|
_common: ['_createdAt', '_updatedAt', 'objectId'],
|
|
_User: ['_email_verify_token']
|
|
};
|
|
|
|
function _removeProtectedFields(data, className) {
|
|
const _data = Object.assign({}, data);
|
|
_protectedFields._common.forEach((f) => delete _data[f]);
|
|
if (className && _protectedFields[className]) _protectedFields[className].forEach((f) => delete _data[f]);
|
|
|
|
return _data;
|
|
}
|
|
|
|
class CloudStore {
|
|
constructor(modelScope) {
|
|
this._initCloudServices();
|
|
|
|
this.events = new EventEmitter();
|
|
this.events.setMaxListeners(10000);
|
|
this.modelScope = modelScope;
|
|
|
|
this._fromJSON = (item, collectionName) => CloudStore._fromJSON(item, collectionName, modelScope);
|
|
this._deserializeJSON = (data, type) => CloudStore._deserializeJSON(data, type, modelScope);
|
|
this._serializeObject = (data, collectionName) => CloudStore._serializeObject(data, collectionName, modelScope);
|
|
}
|
|
|
|
_initCloudServices() {
|
|
_collections = undefined; // clear collection cache, so it's refetched
|
|
|
|
const cloudServices = NoodlRuntime.instance.getMetaData('cloudservices');
|
|
if (cloudServices) {
|
|
this.appId = cloudServices.appId;
|
|
this.endpoint = cloudServices.endpoint;
|
|
}
|
|
|
|
const dbVersionMajor = NoodlRuntime.instance.getMetaData('dbVersionMajor');
|
|
this.dbVersionMajor = dbVersionMajor;
|
|
}
|
|
|
|
on() {
|
|
this.events.on.apply(this.events, arguments);
|
|
}
|
|
|
|
off() {
|
|
this.events.off.apply(this.events, arguments);
|
|
}
|
|
|
|
_makeRequest(path, options) {
|
|
if (typeof _noodl_cloud_runtime_version === 'undefined') {
|
|
// Running in browser
|
|
var xhr = new XMLHttpRequest();
|
|
|
|
xhr.onreadystatechange = function () {
|
|
if (xhr.readyState === 4) {
|
|
var json;
|
|
try {
|
|
// In SSR, we dont have xhr.response
|
|
json = JSON.parse(xhr.response || xhr.responseText);
|
|
} catch (e) {}
|
|
|
|
if (xhr.status === 200 || xhr.status === 201) {
|
|
options.success(json);
|
|
} else options.error(json || { error: xhr.responseText, status: xhr.status });
|
|
}
|
|
};
|
|
|
|
xhr.open(options.method || 'GET', this.endpoint + path, true);
|
|
|
|
xhr.setRequestHeader('X-Parse-Application-Id', this.appId);
|
|
if (typeof _noodl_cloudservices !== 'undefined') {
|
|
xhr.setRequestHeader('X-Parse-Master-Key', _noodl_cloudservices.masterKey);
|
|
}
|
|
|
|
// Check for current users
|
|
var _cu = localStorage['Parse/' + this.appId + '/currentUser'];
|
|
if (_cu !== undefined) {
|
|
try {
|
|
const currentUser = JSON.parse(_cu);
|
|
xhr.setRequestHeader('X-Parse-Session-Token', currentUser.sessionToken);
|
|
} catch (e) {
|
|
// Failed to extract session token
|
|
}
|
|
}
|
|
|
|
if (options.onUploadProgress) {
|
|
xhr.upload.onprogress = (pe) => options.onUploadProgress(pe);
|
|
}
|
|
|
|
if (options.content instanceof File) {
|
|
xhr.send(options.content);
|
|
} else {
|
|
xhr.setRequestHeader('Content-Type', 'application/json');
|
|
xhr.send(JSON.stringify(options.content));
|
|
}
|
|
} else {
|
|
// Running in cloud runtime
|
|
const endpoint = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.endpoint : this.endpoint;
|
|
const appId = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.appId : this.appId;
|
|
const masterKey = typeof _noodl_cloudservices !== 'undefined' ? _noodl_cloudservices.masterKey : undefined;
|
|
|
|
fetch(endpoint + path, {
|
|
method: options.method || 'GET',
|
|
headers: {
|
|
'X-Parse-Application-Id': appId,
|
|
'X-Parse-Master-Key': masterKey,
|
|
'Content-Type': 'application/json'
|
|
},
|
|
body: JSON.stringify(options.content)
|
|
})
|
|
.then((r) => {
|
|
if (r.status === 200 || r.status === 201) {
|
|
if (options.method === 'DELETE') {
|
|
options.success(undefined);
|
|
} else {
|
|
r.json()
|
|
.then((json) => options.success(json))
|
|
.catch((e) =>
|
|
options.error({
|
|
error: 'CloudStore: Failed to get json result.'
|
|
})
|
|
);
|
|
}
|
|
} else {
|
|
if (options.method === 'DELETE') {
|
|
options.error({ error: 'Failed to delete.' });
|
|
} else {
|
|
r.json()
|
|
.then((json) => options.error(json))
|
|
.catch((e) => options.error({ error: 'Failed to fetch.' }));
|
|
}
|
|
}
|
|
})
|
|
.catch((e) => {
|
|
options.error({ error: e.message });
|
|
});
|
|
}
|
|
}
|
|
|
|
query(options) {
|
|
this._makeRequest('/classes/' + options.collection, {
|
|
method: 'POST',
|
|
content: {
|
|
_method: 'GET',
|
|
where: options.where,
|
|
limit: options.limit,
|
|
skip: options.skip,
|
|
include: Array.isArray(options.include) ? options.include.join(',') : options.include,
|
|
keys: Array.isArray(options.select) ? options.select.join(',') : options.select,
|
|
order: Array.isArray(options.sort) ? options.sort.join(',') : options.sort,
|
|
count: options.count
|
|
},
|
|
success: function (response) {
|
|
options.success(response.results, response.count);
|
|
},
|
|
error: function () {
|
|
options.error();
|
|
}
|
|
});
|
|
}
|
|
|
|
aggregate(options) {
|
|
const args = [];
|
|
|
|
if (!options.group) {
|
|
options.error('You need to provide group option.');
|
|
return;
|
|
}
|
|
|
|
if (options.limit) args.push('limit=' + options.limit);
|
|
if (options.skip) args.push('skip=' + options.skip);
|
|
|
|
const grouping = {};
|
|
|
|
Object.keys(options.group).forEach((k) => {
|
|
const _g = {};
|
|
const group = options.group[k];
|
|
if (group['avg'] !== undefined) _g['$avg'] = '$' + group['avg'];
|
|
else if (group['sum'] !== undefined) _g['$sum'] = '$' + group['sum'];
|
|
else if (group['max'] !== undefined) _g['$max'] = '$' + group['max'];
|
|
else if (group['min'] !== undefined) _g['$min'] = '$' + group['min'];
|
|
else if (group['distinct'] !== undefined) _g['$addToSet'] = '$' + group['distinct'];
|
|
|
|
grouping[k] = _g;
|
|
});
|
|
|
|
// I don't know which version the API was changed, lets just say above 4 for now.
|
|
if (this.dbVersionMajor && this.dbVersionMajor > 4) {
|
|
grouping._id = null;
|
|
|
|
if (options.where) args.push('$match=' + encodeURIComponent(JSON.stringify(options.where)));
|
|
|
|
args.push('$group=' + JSON.stringify(grouping));
|
|
} else {
|
|
grouping.objectId = null;
|
|
|
|
if (options.where) args.push('match=' + encodeURIComponent(JSON.stringify(options.where)));
|
|
|
|
args.push('group=' + JSON.stringify(grouping));
|
|
}
|
|
|
|
this._makeRequest('/aggregate/' + options.collection + (args.length > 0 ? '?' + args.join('&') : ''), {
|
|
success: function (response) {
|
|
const res = {};
|
|
|
|
if (!response.results || response.results.length !== 1) {
|
|
options.success({}); // No result
|
|
return;
|
|
}
|
|
|
|
Object.keys(options.group).forEach((k) => {
|
|
res[k] = response.results[0][k];
|
|
});
|
|
|
|
options.success(res);
|
|
},
|
|
error: function () {
|
|
options.error();
|
|
}
|
|
});
|
|
}
|
|
|
|
count(options) {
|
|
const args = [];
|
|
|
|
if (options.where) args.push('where=' + encodeURIComponent(JSON.stringify(options.where)));
|
|
args.push('limit=0');
|
|
args.push('count=1');
|
|
|
|
this._makeRequest('/classes/' + options.collection + (args.length > 0 ? '?' + args.join('&') : ''), {
|
|
success: function (response) {
|
|
options.success(response.count);
|
|
},
|
|
error: function () {
|
|
options.error();
|
|
}
|
|
});
|
|
}
|
|
|
|
distinct(options) {
|
|
const args = [];
|
|
|
|
if (options.where) args.push('where=' + encodeURIComponent(JSON.stringify(options.where)));
|
|
args.push('distinct=' + options.property);
|
|
|
|
this._makeRequest('/aggregate/' + options.collection + (args.length > 0 ? '?' + args.join('&') : ''), {
|
|
success: function (response) {
|
|
options.success(response.results);
|
|
},
|
|
error: function () {
|
|
options.error();
|
|
}
|
|
});
|
|
}
|
|
|
|
/**
|
|
*
|
|
* @param {{
|
|
* objectId: string;
|
|
* collection: string;
|
|
* keys?: string[] | string;
|
|
* include?: string[] | string;
|
|
* excludeKeys?: string[] | string;
|
|
* success: (data: unknown) => void;
|
|
* error: (error: unknown) => void;
|
|
* }} options
|
|
*/
|
|
fetch(options) {
|
|
const args = [];
|
|
|
|
if (options.include) {
|
|
args.push('include=' + (Array.isArray(options.include) ? options.include.join(',') : options.include));
|
|
}
|
|
|
|
if (options.keys) {
|
|
args.push('keys=' + (Array.isArray(options.keys) ? options.keys.join(',') : options.keys));
|
|
}
|
|
|
|
if (options.excludeKeys) {
|
|
args.push(
|
|
'excludeKeys=' + (Array.isArray(options.excludeKeys) ? options.excludeKeys.join(',') : options.excludeKeys)
|
|
);
|
|
}
|
|
|
|
this._makeRequest(
|
|
'/classes/' + options.collection + '/' + options.objectId + (args.length > 0 ? '?' + args.join('&') : ''),
|
|
{
|
|
method: 'GET',
|
|
success: (response) => {
|
|
options.success(response);
|
|
this.events.emit('fetch', {
|
|
type: 'fetch',
|
|
objectId: options.objectId,
|
|
object: response,
|
|
collection: options.collection
|
|
});
|
|
},
|
|
error: function (res) {
|
|
options.error(res.error);
|
|
}
|
|
}
|
|
);
|
|
}
|
|
|
|
create(options) {
|
|
this._makeRequest('/classes/' + options.collection, {
|
|
method: 'POST',
|
|
content: Object.assign(
|
|
_removeProtectedFields(_serializeObject(options.data, options.collection), options.collection),
|
|
{ ACL: options.acl }
|
|
),
|
|
success: (response) => {
|
|
const _obj = Object.assign({}, options.data, response);
|
|
options.success(_obj);
|
|
this.events.emit('create', {
|
|
type: 'create',
|
|
objectId: options.objectId,
|
|
object: _obj,
|
|
collection: options.collection
|
|
});
|
|
},
|
|
error: function (res) {
|
|
options.error(res.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
increment(options) {
|
|
const data = {};
|
|
|
|
for (let key in options.properties) {
|
|
data[key] = { __op: 'Increment', amount: options.properties[key] };
|
|
}
|
|
|
|
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
|
method: 'PUT',
|
|
content: data,
|
|
success: (response) => {
|
|
options.success(response);
|
|
},
|
|
error: function (res) {
|
|
options.error(res.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
save(options) {
|
|
const _data = Object.assign({}, options.data);
|
|
delete _data.createdAt;
|
|
delete _data.updatedAt;
|
|
|
|
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
|
method: 'PUT',
|
|
content: Object.assign(_removeProtectedFields(_serializeObject(_data, options.collection), options.collection), {
|
|
ACL: options.acl
|
|
}),
|
|
success: (response) => {
|
|
options.success(response);
|
|
this.events.emit('save', {
|
|
type: 'save',
|
|
objectId: options.objectId,
|
|
object: Object.assign({}, options.data, response),
|
|
collection: options.collection
|
|
});
|
|
},
|
|
error: function (res) {
|
|
options.error(res.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
delete(options) {
|
|
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
|
method: 'DELETE',
|
|
success: () => {
|
|
options.success();
|
|
this.events.emit('delete', {
|
|
type: 'delete',
|
|
objectId: options.objectId,
|
|
collection: options.collection
|
|
});
|
|
},
|
|
error: function (res) {
|
|
options.error(res.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
addRelation(options) {
|
|
const _content = {};
|
|
_content[options.key] = {
|
|
__op: 'AddRelation',
|
|
objects: [
|
|
{
|
|
__type: 'Pointer',
|
|
objectId: options.targetObjectId,
|
|
className: options.targetClass
|
|
}
|
|
]
|
|
};
|
|
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
|
method: 'PUT',
|
|
content: _content,
|
|
success: function (response) {
|
|
options.success(response);
|
|
},
|
|
error: function (res) {
|
|
options.error(res.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
removeRelation(options) {
|
|
const _content = {};
|
|
_content[options.key] = {
|
|
__op: 'RemoveRelation',
|
|
objects: [
|
|
{
|
|
__type: 'Pointer',
|
|
objectId: options.targetObjectId,
|
|
className: options.targetClass
|
|
}
|
|
]
|
|
};
|
|
this._makeRequest('/classes/' + options.collection + '/' + options.objectId, {
|
|
method: 'PUT',
|
|
content: _content,
|
|
success: function (response) {
|
|
options.success(response);
|
|
},
|
|
error: function (res) {
|
|
options.error(res.error);
|
|
}
|
|
});
|
|
}
|
|
|
|
uploadFile(options) {
|
|
this._makeRequest('/files/' + options.file.name, {
|
|
method: 'POST',
|
|
content: options.file,
|
|
contentType: options.file.type,
|
|
success: (response) => options.success(Object.assign({}, options.data, response)),
|
|
error: (err) => options.error(err),
|
|
onUploadProgress: options.onUploadProgress
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Users holding the master key are allowed to delete files
|
|
*
|
|
* @param {{
|
|
* file: {
|
|
* name: string;
|
|
* }
|
|
* success: (data: unknown) => void;
|
|
* error: (error: unknown) => void;
|
|
* }} options
|
|
*/
|
|
deleteFile(options) {
|
|
this._makeRequest('/files/' + options.file.name, {
|
|
method: 'DELETE',
|
|
success: (response) => options.success(Object.assign({}, options.data, response)),
|
|
error: (err) => options.error(err)
|
|
});
|
|
}
|
|
}
|
|
|
|
function _isArrayOfObjects(a) {
|
|
if (!Array.isArray(a)) return false;
|
|
for (var i = 0; i < a.length; i++) if (typeof a[i] !== 'object' || a[i] === null) return false;
|
|
|
|
return true;
|
|
}
|
|
|
|
function _toJSON(obj) {
|
|
if (obj instanceof Model) {
|
|
var res = {};
|
|
for (var key in obj.data) {
|
|
res[key] = _toJSON(obj.data[key]);
|
|
}
|
|
return res;
|
|
} else if (obj instanceof Collection) {
|
|
var res = [];
|
|
obj.items.forEach((m) => {
|
|
res.push(_toJSON(m));
|
|
});
|
|
return res;
|
|
}
|
|
return obj;
|
|
}
|
|
|
|
function _serializeObject(data, collectionName, modelScope) {
|
|
if (CloudStore._collections[collectionName]) var schema = CloudStore._collections[collectionName].schema;
|
|
|
|
for (var key in data) {
|
|
var _type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined;
|
|
|
|
if (data[key] === undefined || data[key] === null) {
|
|
// Keep null and undefined as is
|
|
} else if (_type === 'Pointer' && typeof data[key] === 'string') {
|
|
// This is a string pointer to an object
|
|
data[key] = {
|
|
__type: 'Pointer',
|
|
className: schema.properties[key].targetClass,
|
|
objectId: data[key]
|
|
};
|
|
} else if (_type === 'Pointer' && typeof data[key] === 'object' && (modelScope || Model).instanceOf(data[key])) {
|
|
// This is an embedded object that should be stored as pointer
|
|
data[key] = {
|
|
__type: 'Pointer',
|
|
className: schema.properties[key].targetClass,
|
|
objectId: data[key].getId()
|
|
};
|
|
} else if (_type === 'Date' && (typeof data[key] === 'string' || data[key] instanceof Date)) {
|
|
data[key] = {
|
|
__type: 'Date',
|
|
iso: data[key] instanceof Date ? data[key].toISOString() : data[key]
|
|
};
|
|
} else if (_type === 'File' && data[key] instanceof CloudFile) {
|
|
const cloudFile = data[key];
|
|
data[key] = {
|
|
__type: 'File',
|
|
url: cloudFile.getUrl(),
|
|
name: cloudFile.getName()
|
|
};
|
|
} else if (_type === 'Array' && typeof data[key] === 'string' && Collection.exists(data[key])) {
|
|
data[key] = _toJSON(Collection.get(data[key]));
|
|
} else if (_type === 'Object' && typeof data[key] === 'string' && (modelScope || Model).exists(data[key])) {
|
|
data[key] = _toJSON((modelScope || Model).get(data[key]));
|
|
} else if (_type === 'GeoPoint' && typeof data[key] === 'object') {
|
|
data[key] = {
|
|
__type: 'GeoPoint',
|
|
latitude: Number(data[key].latitude),
|
|
longitude: Number(data[key].longitude)
|
|
};
|
|
} else data[key] = _toJSON(data[key]);
|
|
}
|
|
|
|
return data;
|
|
}
|
|
|
|
function _deserializeJSON(data, type, modelScope) {
|
|
if (data === undefined) return;
|
|
if (data === null) return null;
|
|
|
|
if (type === 'Relation' && data.__type === 'Relation') {
|
|
return undefined; // Ignore relation fields
|
|
} else if (type === 'Pointer' && data.__type === 'Pointer') {
|
|
// This is a pointer type, resolve into id
|
|
return data.objectId;
|
|
} else if (type === 'Date' && data.__type === 'Date') {
|
|
return new Date(data.iso);
|
|
} else if (type === 'Date' && typeof data === 'string') {
|
|
return new Date(data);
|
|
} else if (type === 'File' && data.__type === 'File') {
|
|
return new CloudFile(data);
|
|
} else if (type === 'GeoPoint' && data.__type === 'GeoPoint') {
|
|
return {
|
|
latitude: data.latitude,
|
|
longitude: data.longitude
|
|
};
|
|
} else if (_isArrayOfObjects(data)) {
|
|
var a = [];
|
|
for (var i = 0; i < data.length; i++) {
|
|
a.push(_deserializeJSON(data[i], undefined, modelScope));
|
|
}
|
|
var c = Collection.get();
|
|
c.set(a);
|
|
return c;
|
|
} else if (Array.isArray(data)) return data;
|
|
// This is an array with mixed data, just return it
|
|
else if (data && data.__type === 'Object' && data.className !== undefined && data.objectId !== undefined) {
|
|
const _data = Object.assign({}, data);
|
|
delete _data.className;
|
|
delete _data.__type;
|
|
return _fromJSON(_data, data.className, modelScope);
|
|
} else if (typeof data === 'object' && data !== null) {
|
|
var m = (modelScope || Model).get();
|
|
for (var key in data) {
|
|
m.set(key, _deserializeJSON(data[key], undefined, modelScope));
|
|
}
|
|
return m;
|
|
} else return data;
|
|
}
|
|
|
|
function _fromJSON(item, collectionName, modelScope) {
|
|
const modelStore = modelScope || Model;
|
|
|
|
const model = modelStore.get(item.objectId);
|
|
model._class = collectionName;
|
|
|
|
let schema = undefined;
|
|
if (collectionName !== undefined && CloudStore._collections[collectionName] !== undefined) {
|
|
schema = CloudStore._collections[collectionName].schema;
|
|
}
|
|
|
|
for (const key in item) {
|
|
if (key === 'objectId' || key === 'ACL') {
|
|
continue;
|
|
}
|
|
|
|
const _type = schema && schema.properties && schema.properties[key] ? schema.properties[key].type : undefined;
|
|
model.set(key, _deserializeJSON(item[key], _type, modelScope));
|
|
}
|
|
|
|
return model;
|
|
}
|
|
|
|
CloudStore._fromJSON = _fromJSON;
|
|
CloudStore._deserializeJSON = _deserializeJSON;
|
|
CloudStore._serializeObject = _serializeObject;
|
|
|
|
CloudStore.forScope = (modelScope) => {
|
|
if (modelScope === undefined) return CloudStore.instance;
|
|
if (modelScope._cloudStore) return modelScope._cloudStore;
|
|
|
|
modelScope._cloudStore = new CloudStore(modelScope);
|
|
return modelScope._cloudStore;
|
|
};
|
|
|
|
var _instance;
|
|
Object.defineProperty(CloudStore, 'instance', {
|
|
get: function () {
|
|
if (_instance === undefined) _instance = new CloudStore();
|
|
return _instance;
|
|
}
|
|
});
|
|
|
|
var _collections;
|
|
Object.defineProperty(CloudStore, '_collections', {
|
|
get: function () {
|
|
if (_collections === undefined) {
|
|
_collections = {};
|
|
const dbCollections = NoodlRuntime.instance.getMetaData('dbCollections') || [];
|
|
dbCollections.forEach((c) => {
|
|
_collections[c.name] = c;
|
|
});
|
|
|
|
const systemCollections = NoodlRuntime.instance.getMetaData('systemCollections') || [];
|
|
systemCollections.forEach((c) => {
|
|
_collections[c.name] = c;
|
|
});
|
|
}
|
|
return _collections;
|
|
}
|
|
});
|
|
|
|
CloudStore.invalidateCollections = () => {
|
|
_collections = undefined;
|
|
};
|
|
|
|
module.exports = CloudStore;
|