UNPKG

smc-hub

Version:

CoCalc: Backend webserver component

1,296 lines (1,250 loc) 100 kB
// Generated by CoffeeScript 2.5.1 (function() { //######################################################################## // This file is part of CoCalc: Copyright © 2020 Sagemath, Inc. // License: AGPLv3 s.t. "Commons Clause" – see LICENSE.md for details //######################################################################## `User (and project) client queries COPYRIGHT : (c) 2017 SageMath, Inc. LICENSE : AGPLv3`; var EventEmitter, MAX_CHANGEFEEDS_PER_CLIENT, MAX_PATCH_FUTURE_MS, PROJECT_UPGRADES, SCHEMA, UserQueryQueue, _last_awaken_time, all_results, async, awaken_project, count_result, defaults, file_use_times, misc, one_result, pg_type, quote_field, required, underscore, boundMethodCheck = function(instance, Constructor) { if (!(instance instanceof Constructor)) { throw new Error('Bound instance method accessed before binding'); } }, indexOf = [].indexOf; MAX_CHANGEFEEDS_PER_CLIENT = 2000; // Reject all patches that have timestamp that is more than 3 minutes in the future. MAX_PATCH_FUTURE_MS = 1000 * 60 * 3; EventEmitter = require('events'); async = require('async'); underscore = require('underscore'); ({one_result, all_results, count_result, pg_type, quote_field} = require('./postgres-base')); ({UserQueryQueue} = require('./postgres-user-query-queue')); ({defaults} = misc = require('smc-util/misc')); required = defaults.required; ({PROJECT_UPGRADES, SCHEMA} = require('smc-util/schema')); ({file_use_times} = require('./postgres/file-use-times')); exports.extend_PostgreSQL = function(ext) { var PostgreSQL; return PostgreSQL = class PostgreSQL extends ext { constructor() { super(...arguments); // Cancel all queued up queries by the given client this.cancel_user_queries = this.cancel_user_queries.bind(this); this.user_query = this.user_query.bind(this); this._user_query = this._user_query.bind(this); /* TRACK CHANGEFEED COUNTS _inc and dec below are evidently broken, in that it's CRITICAL that they match up exactly, or users will be locked out until they just happen to switch to another hub with different tracking, which is silly. TODO: DISABLED FOR NOW! */ // Increment a count of the number of changefeeds by a given client so we can cap it. this._inc_changefeed_count = this._inc_changefeed_count.bind(this); // Corresonding decrement of count of the number of changefeeds by a given client. this._dec_changefeed_count = this._dec_changefeed_count.bind(this); // Handle user_query when opts.query is an array. opts below are as for user_query. this._user_query_array = this._user_query_array.bind(this); this.user_query_cancel_changefeed = this.user_query_cancel_changefeed.bind(this); this._query_is_cmp = this._query_is_cmp.bind(this); this._user_get_query_columns = this._user_get_query_columns.bind(this); this._require_is_admin = this._require_is_admin.bind(this); // Ensure that each project_id in project_ids is such that the account is in one of the given // groups for the project, or that the account is an admin. If not, cb(err). this._require_project_ids_in_groups = this._require_project_ids_in_groups.bind(this); this._query_parse_options = this._query_parse_options.bind(this); /* SET QUERIES */ this._parse_set_query_opts = this._parse_set_query_opts.bind(this); this._user_set_query_enforce_requirements = this._user_set_query_enforce_requirements.bind(this); this._user_set_query_where = this._user_set_query_where.bind(this); this._user_set_query_values = this._user_set_query_values.bind(this); this._user_set_query_hooks_prepare = this._user_set_query_hooks_prepare.bind(this); this._user_query_set_count = this._user_query_set_count.bind(this); this._user_query_set_delete = this._user_query_set_delete.bind(this); this._user_set_query_conflict = this._user_set_query_conflict.bind(this); this._user_query_set_upsert = this._user_query_set_upsert.bind(this); // Record is already in DB, so we update it: // this function handles a case that involves both // a jsonb_merge and an update. this._user_query_set_upsert_and_jsonb_merge = this._user_query_set_upsert_and_jsonb_merge.bind(this); this._user_set_query_main_query = this._user_set_query_main_query.bind(this); this.user_set_query = this.user_set_query.bind(this); // mod_fields counts the fields in query that might actually get modified // in the database when we do the query; e.g., account_id won't since it gets // filled in with the user's account_id, and project_write won't since it must // refer to an existing project. We use mod_field **only** to skip doing // no-op queries. It's just an optimization. this._mod_fields = this._mod_fields.bind(this); this._user_get_query_json_timestamps = this._user_get_query_json_timestamps.bind(this); // fill in the default values for obj using the client_query spec. this._user_get_query_set_defaults = this._user_get_query_set_defaults.bind(this); this._user_set_query_project_users = this._user_set_query_project_users.bind(this); this.project_action = this.project_action.bind(this); // This hook is called *before* the user commits a change to a project in the database // via a user set query. // TODO: Add a pre-check here as well that total upgrade isn't going to be exceeded. // This will avoid a possible subtle edge case if user is cheating and always somehow // crashes server...? this._user_set_query_project_change_before = this._user_set_query_project_change_before.bind(this); // This hook is called *after* the user commits a change to a project in the database // via a user set query. It could undo changes the user isn't allowed to make, which // might require doing various async calls, or take actions (e.g., setting quotas, // starting projects, etc.). this._user_set_query_project_change_after = this._user_set_query_project_change_after.bind(this); /* GET QUERIES */ // Make any functional substitutions defined by the schema. // This may mutate query in place. this._user_get_query_functional_subs = this._user_get_query_functional_subs.bind(this); this._parse_get_query_opts = this._parse_get_query_opts.bind(this); // _json_fields: map from field names to array of fields that should be parsed as timestamps // These keys of his map are also used by _user_query_set_upsert_and_jsonb_merge to determine // JSON deep merging for set queries. this._json_fields = this._json_fields.bind(this); this._user_get_query_where = this._user_get_query_where.bind(this); // Additional where object condition imposed by user's get query this._user_get_query_filter = this._user_get_query_filter.bind(this); this._user_get_query_options = this._user_get_query_options.bind(this); this._user_get_query_do_query = this._user_get_query_do_query.bind(this); this._user_get_query_query = this._user_get_query_query.bind(this); this._user_get_query_satisfied_by_obj = this._user_get_query_satisfied_by_obj.bind(this); this._user_get_query_changefeed = this._user_get_query_changefeed.bind(this); this.user_get_query = this.user_get_query.bind(this); /* Synchronized strings */ this._user_set_query_syncstring_change_after = this._user_set_query_syncstring_change_after.bind(this); // Verify that writing a patch is allowed. this._user_set_query_patches_check = this._user_set_query_patches_check.bind(this); // Verify that writing a patch is allowed. this._user_get_query_patches_check = this._user_get_query_patches_check.bind(this); // Verify that writing a patch is allowed. this._user_set_query_cursors_check = this._user_set_query_cursors_check.bind(this); // Verify that writing a patch is allowed. this._user_get_query_cursors_check = this._user_get_query_cursors_check.bind(this); this._syncstring_access_check = this._syncstring_access_check.bind(this); // Check permissions for querying for syncstrings in a project this._syncstrings_check = this._syncstrings_check.bind(this); // Other functions that are needed to implement various use queries, // e.g., for virtual queries like file_use_times. // ASYNC FUNCTION with no callback. this.file_use_times = this.file_use_times.bind(this); } cancel_user_queries(opts) { var ref; boundMethodCheck(this, PostgreSQL); opts = defaults(opts, { client_id: required }); return (ref = this._user_query_queue) != null ? ref.cancel_user_queries(opts) : void 0; } user_query(opts) { var j, len, o, ref, x; boundMethodCheck(this, PostgreSQL); opts = defaults(opts, { client_id: void 0, // if given, uses to control number of queries at once by one client. priority: void 0, // (NOT IMPLEMENTED) priority for this query (an integer [-10,...,19] like in UNIX) account_id: void 0, project_id: void 0, query: required, options: [], changes: void 0, cb: void 0 }); if (opts.account_id != null) { ref = opts.options; // Check for "sudo" by admin to query as a different user, which is done by specifying // options = [..., {account_id:'uuid'}, ...]. for (j = 0, len = ref.length; j < len; j++) { x = ref[j]; if (x.account_id != null) { // Check user is an admin, then change opts.account_id this.get_account({ columns: ['groups'], account_id: opts.account_id, cb: (err, r) => { var y; if (err) { return typeof opts.cb === "function" ? opts.cb(err) : void 0; } else if ((r['groups'] != null) && indexOf.call(r['groups'], 'admin') >= 0) { opts.account_id = x.account_id; opts.options = (function() { var l, len1, ref1, results; ref1 = opts.options; results = []; for (l = 0, len1 = ref1.length; l < len1; l++) { y = ref1[l]; if (y['account_id'] == null) { results.push(y); } } return results; })(); // now do query with new opts and options not including account_id sudo. return this.user_query(opts); } else { return typeof opts.cb === "function" ? opts.cb('user must be admin to sudo') : void 0; } } }); return; } } } if (opts.client_id == null) { // No client_id given, so do not use query queue. delete opts.priority; delete opts.client_id; this._user_query(opts); return; } if (this._user_query_queue == null) { o = { do_query: this._user_query, dbg: this._dbg('user_query_queue'), concurrent: this.concurrent }; if (this._user_query_queue == null) { this._user_query_queue = new UserQueryQueue(o); } } return this._user_query_queue.user_query(opts); } _user_query(opts) { var changes, dbg, err, id, is_set_query, j, len, multi, options, query, ref, subs, table, v, x; boundMethodCheck(this, PostgreSQL); opts = defaults(opts, { account_id: void 0, project_id: void 0, query: required, options: [], // used for initial query; **IGNORED** by changefeed!; // - Use [{set:true}] or [{set:false}] to force get or set query // - For a set query, use {delete:true} to delete instead of set. This is the only way // to delete a record, and won't work unless delete:true is set in the schema // for the table to explicitly allow deleting. changes: void 0, // id of change feed cb: void 0 // cb(err, result) # WARNING -- this *will* get called multiple times when changes is true! }); id = misc.uuid().slice(0, 6); dbg = this._dbg(`_user_query(id=${id})`); dbg(misc.to_json(opts.query)); if (misc.is_array(opts.query)) { dbg('array query instead'); this._user_query_array(opts); return; } subs = { '{account_id}': opts.account_id, '{project_id}': opts.project_id, '{now}': new Date() }; if (opts.changes != null) { changes = { id: opts.changes, cb: opts.cb }; } v = misc.keys(opts.query); if (v.length > 1) { dbg("FATAL no key"); if (typeof opts.cb === "function") { opts.cb('FATAL: must specify exactly one key in the query'); } return; } table = v[0]; query = opts.query[table]; if (misc.is_array(query)) { if (query.length > 1) { dbg("FATAL not implemented"); if (typeof opts.cb === "function") { opts.cb("FATAL: array of length > 1 not yet implemented"); } return; } multi = true; query = query[0]; } else { multi = false; } is_set_query = void 0; if (opts.options != null) { if (!misc.is_array(opts.options)) { dbg("FATAL options"); if (typeof opts.cb === "function") { opts.cb(`FATAL: options (=${misc.to_json(opts.options)}) must be an array`); } return; } ref = opts.options; for (j = 0, len = ref.length; j < len; j++) { x = ref[j]; if (x.set != null) { is_set_query = !!x.set; } } options = (function() { var l, len1, ref1, results; ref1 = opts.options; results = []; for (l = 0, len1 = ref1.length; l < len1; l++) { x = ref1[l]; if (x.set == null) { results.push(x); } } return results; })(); } else { options = []; } if (misc.is_object(query)) { query = misc.deep_copy(query); misc.obj_key_subs(query, subs); if (is_set_query == null) { is_set_query = !misc.has_null_leaf(query); } if (is_set_query) { dbg("do a set query"); if (changes) { dbg("FATAL: changefeed"); if (typeof opts.cb === "function") { opts.cb("FATAL: changefeeds only for read queries"); } return; } if ((opts.account_id == null) && (opts.project_id == null)) { dbg("FATAL: anon set"); if (typeof opts.cb === "function") { opts.cb("FATAL: no anonymous set queries"); } return; } dbg("user_set_query"); return this.user_set_query({ account_id: opts.account_id, project_id: opts.project_id, table: table, query: query, options: opts.options, cb: (err, x) => { dbg(`returned ${err}`); return typeof opts.cb === "function" ? opts.cb(err, { [`${table}`]: x }) : void 0; } }); } else { // do a get query if (changes && !multi) { dbg("FATAL: changefeed multi"); if (typeof opts.cb === "function") { opts.cb("FATAL: changefeeds only implemented for multi-document queries"); } return; } if (changes) { err = this._inc_changefeed_count(opts.account_id, opts.project_id, table, changes.id); if (err) { dbg(`err changefeed count -- ${err}`); if (typeof opts.cb === "function") { opts.cb(err); } return; } } dbg("user_get_query"); return this.user_get_query({ account_id: opts.account_id, project_id: opts.project_id, table: table, query: query, options: options, multi: multi, changes: changes, cb: (err, x) => { dbg(`returned ${err}`); if (err && changes) { // didn't actually make the changefeed, so don't count it. this._dec_changefeed_count(changes.id, table); } return typeof opts.cb === "function" ? opts.cb(err, !err ? { [`${table}`]: x } : void 0) : void 0; } }); } } else { dbg("FATAL - invalid table"); return typeof opts.cb === "function" ? opts.cb(`FATAL: invalid user_query of '${table}' -- query must be an object`) : void 0; } } _inc_changefeed_count(account_id, project_id, table, changefeed_id) { var client_name, cnt, ids; boundMethodCheck(this, PostgreSQL); return; client_name = `${account_id}-${project_id}`; cnt = this._user_get_changefeed_counts != null ? this._user_get_changefeed_counts : this._user_get_changefeed_counts = {}; ids = this._user_get_changefeed_id_to_user != null ? this._user_get_changefeed_id_to_user : this._user_get_changefeed_id_to_user = {}; if (cnt[client_name] == null) { cnt[client_name] = 1; } else if (cnt[client_name] >= MAX_CHANGEFEEDS_PER_CLIENT) { return `user may create at most ${MAX_CHANGEFEEDS_PER_CLIENT} changefeeds; please close files, refresh browser, restart project`; } else { // increment before successfully making get_query to prevent huge bursts causing trouble! cnt[client_name] += 1; } this._dbg(`_inc_changefeed_count(table='${table}')`)(`{${client_name}:${cnt[client_name]} ...}`); ids[changefeed_id] = client_name; return false; } _dec_changefeed_count(id, table) { var client_name, cnt, ref, t; boundMethodCheck(this, PostgreSQL); return; client_name = this._user_get_changefeed_id_to_user[id]; if (client_name != null) { if ((ref = this._user_get_changefeed_counts) != null) { ref[client_name] -= 1; } delete this._user_get_changefeed_id_to_user[id]; cnt = this._user_get_changefeed_counts; if (table != null) { t = `(table='${table}')`; } else { t = ""; } return this._dbg(`_dec_changefeed_count${t}`)(`counts={${client_name}:${cnt[client_name]} ...}`); } } _user_query_array(opts) { var f, result; boundMethodCheck(this, PostgreSQL); if (opts.changes && opts.query.length > 1) { if (typeof opts.cb === "function") { opts.cb("FATAL: changefeeds only implemented for single table"); } return; } result = []; f = (query, cb) => { return this.user_query({ account_id: opts.account_id, project_id: opts.project_id, query: query, options: opts.options, cb: (err, x) => { result.push(x); return cb(err); } }); }; return async.mapSeries(opts.query, f, (err) => { return opts.cb(err, result); }); } user_query_cancel_changefeed(opts) { var dbg, feed, ref; boundMethodCheck(this, PostgreSQL); opts = defaults(opts, { id: required, cb: void 0 // not really asynchronous }); dbg = this._dbg(`user_query_cancel_changefeed(id='${opts.id}')`); feed = (ref = this._changefeeds) != null ? ref[opts.id] : void 0; if (feed != null) { dbg("actually cancelling feed"); this._dec_changefeed_count(opts.id); delete this._changefeeds[opts.id]; feed.close(); } else { dbg("already cancelled before (no such feed)"); } return typeof opts.cb === "function" ? opts.cb() : void 0; } _query_is_cmp(obj) { var _, k; boundMethodCheck(this, PostgreSQL); if (!misc.is_object(obj)) { return false; } for (k in obj) { _ = obj[k]; if (indexOf.call(misc.operators, k) < 0) { return false; } return k; } return false; } _user_get_query_columns(query, remove_from_query) { var v; boundMethodCheck(this, PostgreSQL); v = misc.keys(query); if (remove_from_query != null) { // If remove_from_query is specified it should be an array of strings // and we do not includes these in what is returned. v = underscore.difference(v, remove_from_query); } return v; } _require_is_admin(account_id, cb) { boundMethodCheck(this, PostgreSQL); if (account_id == null) { cb("FATAL: user must be an admin"); return; } return this.is_admin({ account_id: account_id, cb: (err, is_admin) => { if (err) { return cb(err); } else if (!is_admin) { return cb("FATAL: user must be an admin"); } else { return cb(); } } }); } _require_project_ids_in_groups(account_id, project_ids, groups, cb) { var require_admin, s; boundMethodCheck(this, PostgreSQL); s = { [`${account_id}`]: true }; require_admin = false; return this._query({ query: `SELECT project_id, users#>'{${account_id}}' AS user FROM projects`, where: { "project_id = ANY($)": project_ids }, cache: true, cb: all_results((err, x) => { var j, known_project_ids, l, len, len1, p, project_id, ref, ref1; if (err) { return cb(err); } else { known_project_ids = {}; // we use this to ensure that each of the given project_ids exists. for (j = 0, len = x.length; j < len; j++) { p = x[j]; known_project_ids[p.project_id] = true; if (ref = (ref1 = p.user) != null ? ref1.group : void 0, indexOf.call(groups, ref) < 0) { require_admin = true; } } // If any of the project_ids don't exist, reject the query. for (l = 0, len1 = project_ids.length; l < len1; l++) { project_id = project_ids[l]; if (!known_project_ids[project_id]) { cb(`FATAL: unknown project_id '${misc.trunc(project_id, 100)}'`); return; } } if (require_admin) { return this._require_is_admin(account_id, cb); } else { return cb(); } } }) }); } _query_parse_options(options) { var j, len, name, r, value, x; boundMethodCheck(this, PostgreSQL); r = {}; for (j = 0, len = options.length; j < len; j++) { x = options[j]; for (name in x) { value = x[name]; switch (name) { case 'only_changes': r.only_changes = !!value; break; case 'limit': r.limit = value; break; case 'slice': r.slice = value; break; case 'order_by': if (value[0] === '-') { value = value.slice(1) + " DESC "; } r.order_by = value; break; case 'delete': null; break; // ignore delete here - is parsed elsewhere case 'heartbeat': this._dbg("_query_parse_options")("TODO/WARNING -- ignoring heartbeat option from old client"); break; default: r.err = `unknown option '${name}'`; } } } return r; } _parse_set_query_opts(opts) { var dbg, err, field, j, k, l, len, len1, len2, m, r, ref, ref1, ref10, ref11, ref12, ref2, ref3, ref4, ref5, ref6, ref7, ref8, ref9, s, v, val, x, y, z; boundMethodCheck(this, PostgreSQL); r = {}; if (opts.project_id != null) { dbg = r.dbg = this._dbg(`user_set_query(project_id='${opts.project_id}', table='${opts.table}')`); } else if (opts.account_id != null) { dbg = r.dbg = this._dbg(`user_set_query(account_id='${opts.account_id}', table='${opts.table}')`); } else { return { err: "FATAL: account_id or project_id must be specified" }; } if (SCHEMA[opts.table] == null) { return { err: `FATAL: table '${opts.table}' does not exist` }; } dbg(misc.to_json(opts.query)); if (opts.options) { dbg(`options=${misc.to_json(opts.options)}`); } r.query = misc.copy(opts.query); r.table = opts.table; r.db_table = (ref = SCHEMA[opts.table].virtual) != null ? ref : opts.table; r.account_id = opts.account_id; r.project_id = opts.project_id; s = SCHEMA[opts.table]; if (opts.account_id != null) { r.client_query = s != null ? s.user_query : void 0; } else { r.client_query = s != null ? s.project_query : void 0; } if (((ref1 = r.client_query) != null ? (ref2 = ref1.set) != null ? ref2.fields : void 0 : void 0) == null) { return { err: `FATAL: user set queries not allowed for table '${opts.table}'` }; } if (!this._mod_fields(opts.query, r.client_query)) { dbg("shortcut -- no fields will be modified, so nothing to do"); return; } ref3 = misc.keys(r.client_query.set.fields); for (j = 0, len = ref3.length; j < len; j++) { field = ref3[j]; if (r.client_query.set.fields[field] === void 0) { return { err: `FATAL: user set query not allowed for ${opts.table}.${field}` }; } val = r.client_query.set.fields[field]; if (typeof val === 'function') { try { r.query[field] = val(r.query, this); } catch (error) { err = error; return { err: `FATAL: error setting '${field}' -- ${err}` }; } } else { switch (val) { case 'account_id': if (r.account_id == null) { return { err: "FATAL: account_id must be specified" }; } r.query[field] = r.account_id; break; case 'project_id': if (r.project_id == null) { return { err: "FATAL: project_id must be specified" }; } r.query[field] = r.project_id; break; case 'time_id': r.query[field] = uuid.v1(); break; case 'project_write': if (r.query[field] == null) { return { err: `FATAL: must specify ${opts.table}.${field}` }; } r.require_project_ids_write_access = [r.query[field]]; break; case 'project_owner': if (r.query[field] == null) { return { err: `FATAL: must specify ${opts.table}.${field}` }; } r.require_project_ids_owner = [r.query[field]]; } } } if (r.client_query.set.admin) { r.require_admin = true; } r.primary_keys = this._primary_keys(r.db_table); r.json_fields = this._json_fields(r.db_table, r.query); ref4 = r.query; for (k in ref4) { v = ref4[k]; if (indexOf.call(r.primary_keys, k) >= 0) { continue; } if (((ref5 = r.client_query) != null ? (ref6 = ref5.set) != null ? (ref7 = ref6.fields) != null ? ref7[k] : void 0 : void 0 : void 0) !== void 0) { continue; } if (((ref8 = s.admin_query) != null ? (ref9 = ref8.set) != null ? (ref10 = ref9.fields) != null ? ref10[k] : void 0 : void 0 : void 0) !== void 0) { r.require_admin = true; continue; } return { err: `FATAL: changing ${r.table}.${k} not allowed` }; } // HOOKS which allow for running arbitrary code in response to // user set queries. In each case, new_val below is only the part // of the object that the user requested to change. // 0. CHECK: Runs before doing any further processing; has callback, so this // provides a generic way to quickly check whether or not this query is allowed // for things that can't be done declaratively. The check_hook can also // mutate the obj (the user query), e.g., to enforce limits on input size. r.check_hook = r.client_query.set.check_hook; // 1. BEFORE: If before_change is set, it is called with input // (database, old_val, new_val, account_id, cb) // before the actual change to the database is made. r.before_change_hook = r.client_query.set.before_change; // 2. INSTEAD OF: If instead_of_change is set, then instead_of_change_hook // is called with input // (database, old_val, new_val, account_id, cb) // *instead* of actually doing the update/insert to // the database. This makes it possible to run arbitrary // code whenever the user does a certain type of set query. // Obviously, if that code doesn't set the new_val in the // database, then new_val won't be the new val. r.instead_of_change_hook = r.client_query.set.instead_of_change; // 3. AFTER: If set, the on_change_hook is called with // (database, old_val, new_val, account_id, cb) // after everything the database has been modified. r.on_change_hook = r.client_query.set.on_change; // 4. instead of query r.instead_of_query = r.client_query.set.instead_of_query; //dbg("on_change_hook=#{on_change_hook?}, #{misc.to_json(misc.keys(client_query.set))}") // Set the query options -- order doesn't matter for set queries (unlike for get), so we // just merge the options into a single dictionary. // NOTE: As I write this, there is just one supported option: {delete:true}. r.options = {}; if (r.client_query.set.options != null) { ref11 = r.client_query.set.options; for (l = 0, len1 = ref11.length; l < len1; l++) { x = ref11[l]; for (y in x) { z = x[y]; r.options[y] = z; } } } if (opts.options != null) { ref12 = opts.options; for (m = 0, len2 = ref12.length; m < len2; m++) { x = ref12[m]; for (y in x) { z = x[y]; r.options[y] = z; } } } dbg(`options = ${misc.to_json(r.options)}`); if (r.options.delete && !r.client_query.set.delete) { return { // delete option is set, but deletes aren't explicitly allowed on this table. ERROR. err: `FATAL: delete from ${r.table} not allowed` }; } return r; } _user_set_query_enforce_requirements(r, cb) { boundMethodCheck(this, PostgreSQL); return async.parallel([ (cb) => { if (r.require_admin) { return this._require_is_admin(r.account_id, cb); } else { return cb(); } }, (cb) => { var err, j, len, ref, x; if (r.require_project_ids_write_access != null) { if (r.project_id != null) { err = void 0; ref = r.require_project_ids_write_access; for (j = 0, len = ref.length; j < len; j++) { x = ref[j]; if (x !== r.project_id) { err = "FATAL: can only query same project"; break; } } return cb(err); } else { return this._require_project_ids_in_groups(r.account_id, r.require_project_ids_write_access, ['owner', 'collaborator'], cb); } } else { return cb(); } }, (cb) => { if (r.require_project_ids_owner != null) { return this._require_project_ids_in_groups(r.account_id, r.require_project_ids_owner, ['owner'], cb); } else { return cb(); } } ], cb); } _user_set_query_where(r) { var j, len, primary_key, ref, type, value, where; boundMethodCheck(this, PostgreSQL); where = {}; ref = this._primary_keys(r.db_table); for (j = 0, len = ref.length; j < len; j++) { primary_key = ref[j]; type = pg_type(SCHEMA[r.db_table].fields[primary_key]); value = r.query[primary_key]; if (type === 'TIMESTAMP' && !misc.is_date(value)) { // Javascript is better at parsing its own dates than PostgreSQL value = new Date(value); } where[`${primary_key}=$::${type}`] = value; } return where; } _user_set_query_values(r) { var key, ref, ref1, s, type, value, values; boundMethodCheck(this, PostgreSQL); values = {}; s = SCHEMA[r.db_table]; ref = r.query; for (key in ref) { value = ref[key]; type = pg_type(s != null ? (ref1 = s.fields) != null ? ref1[key] : void 0 : void 0); if (type != null) { if (type === 'TIMESTAMP' && !misc.is_date(value)) { // (as above) Javascript is better at parsing its own dates than PostgreSQL value = new Date(value); } values[`${key}::${type}`] = value; } else { values[key] = value; } } return values; } _user_set_query_hooks_prepare(r, cb) { var j, len, primary_key, ref; boundMethodCheck(this, PostgreSQL); if ((r.on_change_hook != null) || (r.before_change_hook != null) || (r.instead_of_change_hook != null)) { ref = r.primary_keys; for (j = 0, len = ref.length; j < len; j++) { primary_key = ref[j]; if (r.query[primary_key] == null) { cb(`FATAL: query must specify (primary) key '${primary_key}'`); return; } } // get the old value before changing it // TODO: optimization -- can we restrict columns below? return this._query({ query: `SELECT * FROM ${r.db_table}`, where: this._user_set_query_where(r), cb: one_result((err, x) => { r.old_val = x; return cb(err); }) }); } else { return cb(); } } _user_query_set_count(r, cb) { boundMethodCheck(this, PostgreSQL); return this._query({ query: `SELECT COUNT(*) FROM ${r.db_table}`, where: this._user_set_query_where(r), cb: count_result(cb) }); } _user_query_set_delete(r, cb) { boundMethodCheck(this, PostgreSQL); return this._query({ query: `DELETE FROM ${r.db_table}`, where: this._user_set_query_where(r), cb: cb }); } _user_set_query_conflict(r) { boundMethodCheck(this, PostgreSQL); return r.primary_keys; } _user_query_set_upsert(r, cb) { boundMethodCheck(this, PostgreSQL); return this._query({ query: `INSERT INTO ${r.db_table}`, values: this._user_set_query_values(r), conflict: this._user_set_query_conflict(r), cb: cb }); } _user_query_set_upsert_and_jsonb_merge(r, cb) { var jsonb_merge, k, ref, set, v; boundMethodCheck(this, PostgreSQL); jsonb_merge = {}; for (k in r.json_fields) { v = r.query[k]; if (v != null) { jsonb_merge[k] = v; } } set = {}; ref = r.query; for (k in ref) { v = ref[k]; if ((v != null) && indexOf.call(r.primary_keys, k) < 0 && (jsonb_merge[k] == null)) { set[k] = v; } } return this._query({ query: `UPDATE ${r.db_table}`, jsonb_merge: jsonb_merge, set: set, where: this._user_set_query_where(r), cb: cb }); } _user_set_query_main_query(r, cb) { var cnt, j, len, primary_key, ref; boundMethodCheck(this, PostgreSQL); r.dbg("_user_set_query_main_query"); if (r.options.delete) { ref = r.primary_keys; for (j = 0, len = ref.length; j < len; j++) { primary_key = ref[j]; if (r.query[primary_key] == null) { cb("FATAL: delete query must set primary key"); return; } } r.dbg("delete based on primary key"); this._user_query_set_delete(r, cb); return; } if (r.instead_of_change_hook != null) { return r.instead_of_change_hook(this, r.old_val, r.query, r.account_id, cb); } else { if (misc.len(r.json_fields) === 0) { // easy case -- there are no jsonb merge fields; just do an upsert. this._user_query_set_upsert(r, cb); return; } // HARD CASE -- there are json_fields... so we are doing an insert // if the object isn't already in the database, and an update // if it is. This is ugly because I don't know how to do both // a JSON merge as an upsert. cnt = void 0; // will equal number of records having the primary key (so 0 or 1) return async.series([ (cb) => { return this._user_query_set_count(r, (err, n) => { cnt = n; return cb(err); }); }, (cb) => { if (cnt === 0) { // Just insert (do as upsert to avoid error in case of race) return this._user_query_set_upsert(r, cb); } else { // Do as an update -- record is definitely already in db since cnt > 0. // This would fail in the unlikely (but possible) case that somebody deletes // the record between the above count and when we do the UPDATE. // Using a transaction could avoid this. // Maybe such an error is reasonable and it's good to report it as such. return this._user_query_set_upsert_and_jsonb_merge(r, cb); } } ], cb); } } user_set_query(opts) { var r; boundMethodCheck(this, PostgreSQL); opts = defaults(opts, { account_id: void 0, project_id: void 0, table: required, query: required, options: void 0, // options=[{delete:true}] is the only supported nontrivial option here. cb: required // cb(err) }); if (this.is_standby) { opts.cb("set queries against standby not allowed"); return; } r = this._parse_set_query_opts(opts); //r.dbg("parsed query opts = #{misc.to_json(r)}") if (r == null) { // nothing to do opts.cb(); return; } if (r.err) { opts.cb(r.err); return; } return async.series([ (cb) => { return this._user_set_query_enforce_requirements(r, cb); }, (cb) => { if (r.check_hook != null) { return r.check_hook(this, r.query, r.account_id, r.project_id, cb); } else { return cb(); } }, (cb) => { return this._user_set_query_hooks_prepare(r, cb); }, (cb) => { if (r.before_change_hook != null) { return r.before_change_hook(this, r.old_val, r.query, r.account_id, cb); } else { return cb(); } }, (cb) => { var opts1; if (r.instead_of_query != null) { opts1 = misc.copy_without(opts, ['cb', 'changes', 'table']); return r.instead_of_query(this, opts1, cb); } else { return this._user_set_query_main_query(r, cb); } }, (cb) => { if (r.on_change_hook != null) { return r.on_change_hook(this, r.old_val, r.query, r.account_id, cb); } else { return cb(); } } ], (err) => { return opts.cb(err); }); } _mod_fields(query, client_query) { var field, j, len, ref, ref1; boundMethodCheck(this, PostgreSQL); ref = misc.keys(query); for (j = 0, len = ref.length; j < len; j++) { field = ref[j]; if ((ref1 = client_query.set.fields[field]) !== 'account_id' && ref1 !== 'project_write') { return true; } } return false; } _user_get_query_json_timestamps(obj, fields) { var k, results, v; boundMethodCheck(this, PostgreSQL); // obj is an object returned from the database via a query // Postgres JSONB doesn't support timestamps, so we convert // every json leaf node of obj that looks like JSON of a timestamp // to a Javascript Date. results = []; for (k in obj) { v = obj[k]; if (fields[k]) { results.push(obj[k] = misc.fix_json_dates(v, fields[k])); } else { results.push(void 0); } } return results; } _user_get_query_set_defaults(client_query, obj, fields) { var j, k, k0, len, ref, ref1, results, s, v, v0, x, y; boundMethodCheck(this, PostgreSQL); if (!misc.is_array(obj)) { obj = [obj]; } else if (obj.length === 0) { return; } s = (ref = client_query != null ? (ref1 = client_query.get) != null ? ref1.fields : void 0 : void 0) != null ? ref : {}; results = []; for (j = 0, len = fields.length; j < len; j++) { k = fields[j]; v = s[k]; if (v != null) { results.push((function() { var l, len1, results1; // k is a field for which a default value (=v) is provided in the schema results1 = []; for (l = 0, len1 = obj.length; l < len1; l++) { x = obj[l]; // For each obj pulled from the database that is defined... if (x != null) { // We check to see if the field k was set on that object. y = x[k]; if (y == null) { // It was NOT set, so we deep copy the default value for the field k. results1.push(x[k] = misc.deep_copy(v)); } else if (typeof v === 'object' && typeof y === 'object' && !misc.is_array(v)) { results1.push((function() { var results2; // y *is* defined and is an object, so we merge in the provided defaults. results2 = []; for (k0 in v) { v0 = v[k0]; if (y[k0] == null) { results2.push(y[k0] = v0); } else { results2.push(void 0); } } return results2; })()); } else { results1.push(void 0); } } else { results1.push(void 0); } } return results1; })()); } else { results.push(void 0); } } return results; } _user_set_query_project_users(obj, account_id) { var _, dbg, fingerprint, id, j, k, key, len, ref, ref1, ref2, ref3, ref4, upgrade_fields, users, v, x; boundMethodCheck(this, PostgreSQL); dbg = this._dbg("_user_set_query_project_users"); if (obj.users == null) { return; } //#dbg("disabled") //#return obj.users // - ensures all keys of users are valid uuid's (though not that they are valid users). // - and format is: // {group:'owner' or 'collaborator', hide:bool, upgrades:{a map}} // with valid upgrade fields. // nothing to do -- not changing users. upgrade_fields = PROJECT_UPGRADES.params; users = {}; ref = obj.users; // TODO: we obviously should check that a user is only changing the part // of this object involving themselves... or adding/removing collaborators. // That is not currently done below. TODO TODO TODO SECURITY. for (id in ref) { x = ref[id]; if (misc.is_valid_uuid_string(id)) { ref1 = misc.keys(x); for (j = 0, len = ref1.length; j < len; j++) { key = ref1[j]; if (key !== 'group' && key !== 'hide' && key !== 'upgrades' && key !== 'ssh_keys') { throw Error(`unknown field '${key}`); } } if ((x.group != null) && ((ref2 = x.group) !== 'owner' && ref2 !== 'collaborator')) { throw Error("invalid value for field 'group'"); } if ((x.hide != null) && typeof x.hide !== 'boolean') { throw Error("invalid type for field 'hide'"); } if (x.upgrades != null) { if (!misc.is_object(x.upgrades)) { throw Error("invalid type for field 'upgrades'"); } ref3 = x.upgrades; for (k in ref3) { _ = ref3[k]; if (!upgrade_fields[k]) { throw Error(`invalid upgrades field '${k}'`); } } } if (x.ssh_keys) { // do some checks. if (!misc.is_object(x.ssh_keys)) { throw Error("ssh_keys must be an object"); } ref4 = x.ssh_keys; for (fingerprint in ref4) { key = ref4[fingerprint]; if (!key) { // deleting continue; } if (!misc.is_object(key)) { throw Error("each key in ssh_keys must be an object"); } for (k in key) { v = key[k]; // the two dates are just numbers not actual timestamps... if (k !== 'title' && k !== 'value' && k !== 'creation_date' && k !== 'last_use_date') { throw Error(`invalid ssh_keys field '${k}'`); } } } } users[id] = x; } } return users; }