BVB Source Codes

parse-server Show PostgresStorageAdapter.js Source code

Return Download parse-server: download PostgresStorageAdapter.js Source code - Download parse-server Source code - Type:.js
  1. import { createClient } from './PostgresClient';
  2. import Parse            from 'parse/node';
  3. import _                from 'lodash';
  4. import sql              from './sql';
  5.  
  6. const PostgresRelationDoesNotExistError = '42P01';
  7. const PostgresDuplicateRelationError = '42P07';
  8. const PostgresDuplicateColumnError = '42701';
  9. const PostgresDuplicateObjectError = '42710';
  10. const PostgresUniqueIndexViolationError = '23505';
  11. const PostgresTransactionAbortedError = '25P02';
  12. const logger = require('../../../logger');
  13.  
  14. const debug = function(){
  15.   let args = [...arguments];
  16.   args = ['PG: ' + arguments[0]].concat(args.slice(1, args.length));
  17.   const log = logger.getLogger();
  18.   log.debug.apply(log, args);
  19. }
  20.  
  21. const parseTypeToPostgresType = type => {
  22.   switch (type.type) {
  23.   case 'String': return 'text';
  24.   case 'Date': return 'timestamp with time zone';
  25.   case 'Object': return 'jsonb';
  26.   case 'File': return 'text';
  27.   case 'Boolean': return 'boolean';
  28.   case 'Pointer': return 'char(10)';
  29.   case 'Number': return 'double precision';
  30.   case 'GeoPoint': return 'point';
  31.   case 'Bytes': return 'jsonb';
  32.   case 'Array':
  33.     if (type.contents && type.contents.type === 'String') {
  34.       return 'text[]';
  35.     } else {
  36.       return 'jsonb';
  37.     }
  38.   default: throw `no type for ${JSON.stringify(type)} yet`;
  39.   }
  40. };
  41.  
  42. const ParseToPosgresComparator = {
  43.   '$gt': '>',
  44.   '$lt': '<',
  45.   '$gte': '>=',
  46.   '$lte': '<='
  47. }
  48.  
  49. const toPostgresValue = value => {
  50.   if (typeof value === 'object') {
  51.     if (value.__type === 'Date') {
  52.       return value.iso;
  53.     }
  54.     if (value.__type === 'File') {
  55.       return value.name;
  56.     }
  57.   }
  58.   return value;
  59. }
  60.  
  61. const transformValue = value => {
  62.   if (typeof value === 'object' &&
  63.         value.__type === 'Pointer') {
  64.     return value.objectId;
  65.   }
  66.   return value;
  67. }
  68.  
  69. // Duplicate from then mongo adapter...
  70. const emptyCLPS = Object.freeze({
  71.   find: {},
  72.   get: {},
  73.   create: {},
  74.   update: {},
  75.   delete: {},
  76.   addField: {},
  77. });
  78.  
  79. const defaultCLPS = Object.freeze({
  80.   find: {'*': true},
  81.   get: {'*': true},
  82.   create: {'*': true},
  83.   update: {'*': true},
  84.   delete: {'*': true},
  85.   addField: {'*': true},
  86. });
  87.  
  88. const toParseSchema = (schema) => {
  89.   if (schema.className === '_User') {
  90.     delete schema.fields._hashed_password;
  91.   }
  92.   if (schema.fields) {
  93.     delete schema.fields._wperm;
  94.     delete schema.fields._rperm;
  95.   }
  96.   let clps = defaultCLPS;
  97.   if (schema.classLevelPermissions) {
  98.     clps = {...emptyCLPS, ...schema.classLevelPermissions};
  99.   }
  100.   return {
  101.     className: schema.className,
  102.     fields: schema.fields,
  103.     classLevelPermissions: clps,
  104.   };
  105. }
  106.  
  107. const toPostgresSchema = (schema) => {
  108.   if (!schema) {
  109.     return schema;
  110.   }
  111.   schema.fields = schema.fields || {};
  112.   schema.fields._wperm = {type: 'Array', contents: {type: 'String'}}
  113.   schema.fields._rperm = {type: 'Array', contents: {type: 'String'}}
  114.   if (schema.className === '_User') {
  115.     schema.fields._hashed_password = {type: 'String'};
  116.     schema.fields._password_history = {type: 'Array'};
  117.   }
  118.   return schema;
  119. }
  120.  
  121. const handleDotFields = (object) => {
  122.   Object.keys(object).forEach(fieldName => {
  123.     if (fieldName.indexOf('.') > -1) {
  124.       const components = fieldName.split('.');
  125.       const first = components.shift();
  126.       object[first] = object[first] || {};
  127.       let currentObj = object[first];
  128.       let next;
  129.       let value = object[fieldName];
  130.       if (value && value.__op === 'Delete') {
  131.         value = undefined;
  132.       }
  133.       /* eslint-disable no-cond-assign */
  134.       while(next = components.shift()) {
  135.       /* eslint-enable no-cond-assign */
  136.         currentObj[next] = currentObj[next] || {};
  137.         if (components.length === 0) {
  138.           currentObj[next] = value;
  139.         }
  140.         currentObj = currentObj[next];
  141.       }
  142.       delete object[fieldName];
  143.     }
  144.   });
  145.   return object;
  146. }
  147.  
  148. const validateKeys = (object) => {
  149.   if (typeof object == 'object') {
  150.     for (const key in object) {
  151.       if (typeof object[key] == 'object') {
  152.         validateKeys(object[key]);
  153.       }
  154.  
  155.       if(key.includes('$') || key.includes('.')){
  156.         throw new Parse.Error(Parse.Error.INVALID_NESTED_KEY, "Nested keys should not contain the '$' or '.' characters");
  157.       }
  158.     }
  159.   }
  160. }
  161.  
  162. // Returns the list of join tables on a schema
  163. const joinTablesForSchema = (schema) => {
  164.   const list = [];
  165.   if (schema) {
  166.     Object.keys(schema.fields).forEach((field) => {
  167.       if (schema.fields[field].type === 'Relation') {
  168.         list.push(`_Join:${field}:${schema.className}`);
  169.       }
  170.     });
  171.   }
  172.   return list;
  173. }
  174.  
  175. const buildWhereClause = ({ schema, query, index }) => {
  176.   const patterns = [];
  177.   let values = [];
  178.   const sorts = [];
  179.  
  180.   schema = toPostgresSchema(schema);
  181.   for (const fieldName in query) {
  182.     const isArrayField = schema.fields
  183.           && schema.fields[fieldName]
  184.           && schema.fields[fieldName].type === 'Array';
  185.     const initialPatternsLength = patterns.length;
  186.     const fieldValue = query[fieldName];
  187.  
  188.     // nothingin the schema, it's gonna blow up
  189.     if (!schema.fields[fieldName]) {
  190.       // as it won't exist
  191.       if (fieldValue.$exists === false) {
  192.         continue;
  193.       }
  194.     }
  195.  
  196.     if (fieldName.indexOf('.') >= 0) {
  197.       const components = fieldName.split('.').map((cmpt, index) => {
  198.         if (index === 0) {
  199.           return `"${cmpt}"`;
  200.         }
  201.         return `'${cmpt}'`;
  202.       });
  203.       let name = components.slice(0, components.length - 1).join('->');
  204.       name += '->>' + components[components.length - 1];
  205.       patterns.push(`${name} = '${fieldValue}'`);
  206.     } else if (typeof fieldValue === 'string') {
  207.       patterns.push(`$${index}:name = $${index + 1}`);
  208.       values.push(fieldName, fieldValue);
  209.       index += 2;
  210.     } else if (typeof fieldValue === 'boolean') {
  211.       patterns.push(`$${index}:name = $${index + 1}`);
  212.       values.push(fieldName, fieldValue);
  213.       index += 2;
  214.     } else if (typeof fieldValue === 'number') {
  215.       patterns.push(`$${index}:name = $${index + 1}`);
  216.       values.push(fieldName, fieldValue);
  217.       index += 2;
  218.     } else if (fieldName === '$or' || fieldName === '$and') {
  219.       const clauses = [];
  220.       const clauseValues = [];
  221.       fieldValue.forEach((subQuery) =>  {
  222.         const clause = buildWhereClause({ schema, query: subQuery, index });
  223.         if (clause.pattern.length > 0) {
  224.           clauses.push(clause.pattern);
  225.           clauseValues.push(...clause.values);
  226.           index += clause.values.length;
  227.         }
  228.       });
  229.       const orOrAnd = fieldName === '$or' ? ' OR ' : ' AND ';
  230.       patterns.push(`(${clauses.join(orOrAnd)})`);
  231.       values.push(...clauseValues);
  232.     }
  233.  
  234.     if (fieldValue.$ne) {
  235.       if (isArrayField) {
  236.         fieldValue.$ne = JSON.stringify([fieldValue.$ne]);
  237.         patterns.push(`NOT array_contains($${index}:name, $${index + 1})`);
  238.       } else {
  239.         if (fieldValue.$ne === null) {
  240.           patterns.push(`$${index}:name <> $${index + 1}`);
  241.         } else {
  242.           // if not null, we need to manually exclude null
  243.           patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`);
  244.         }
  245.       }
  246.  
  247.       // TODO: support arrays
  248.       values.push(fieldName, fieldValue.$ne);
  249.       index += 2;
  250.     }
  251.  
  252.     if (fieldValue.$eq) {
  253.       patterns.push(`$${index}:name = $${index + 1}`);
  254.       values.push(fieldName, fieldValue.$eq);
  255.       index += 2;
  256.     }
  257.     const isInOrNin = Array.isArray(fieldValue.$in) || Array.isArray(fieldValue.$nin);
  258.     if (Array.isArray(fieldValue.$in) &&
  259.         isArrayField &&
  260.         schema.fields[fieldName].contents &&
  261.         schema.fields[fieldName].contents.type === 'String') {
  262.       const inPatterns = [];
  263.       let allowNull = false;
  264.       values.push(fieldName);
  265.       fieldValue.$in.forEach((listElem, listIndex) => {
  266.         if (listElem === null) {
  267.           allowNull = true;
  268.         } else {
  269.           values.push(listElem);
  270.           inPatterns.push(`$${index + 1 + listIndex - (allowNull ? 1 : 0)}`);
  271.         }
  272.       });
  273.       if (allowNull) {
  274.         patterns.push(`($${index}:name IS NULL OR $${index}:name && ARRAY[${inPatterns.join(',')}])`);
  275.       } else {
  276.         patterns.push(`$${index}:name && ARRAY[${inPatterns.join(',')}]`);
  277.       }
  278.       index = index + 1 + inPatterns.length;
  279.     } else if (isInOrNin) {
  280.       var createConstraint = (baseArray, notIn) => {
  281.         if (baseArray.length > 0) {
  282.           const not = notIn ? ' NOT ' : '';
  283.           if (isArrayField) {
  284.             patterns.push(`${not} array_contains($${index}:name, $${index + 1})`);
  285.             values.push(fieldName, JSON.stringify(baseArray));
  286.             index += 2;
  287.           } else {
  288.             const inPatterns = [];
  289.             values.push(fieldName);
  290.             baseArray.forEach((listElem, listIndex) => {
  291.               values.push(listElem);
  292.               inPatterns.push(`$${index + 1 + listIndex}`);
  293.             });
  294.             patterns.push(`$${index}:name ${not} IN (${inPatterns.join(',')})`);
  295.             index = index + 1 + inPatterns.length;
  296.           }
  297.         } else if (!notIn) {
  298.           values.push(fieldName);
  299.           patterns.push(`$${index}:name IS NULL`);
  300.           index = index + 1;
  301.         }
  302.       }
  303.       if (fieldValue.$in) {
  304.         createConstraint(_.flatMap(fieldValue.$in, elt => elt), false);
  305.       }
  306.       if (fieldValue.$nin) {
  307.         createConstraint(_.flatMap(fieldValue.$nin, elt => elt), true);
  308.       }
  309.     }
  310.  
  311.     if (Array.isArray(fieldValue.$all) && isArrayField) {
  312.       patterns.push(`array_contains_all($${index}:name, $${index + 1}::jsonb)`);
  313.       values.push(fieldName, JSON.stringify(fieldValue.$all));
  314.       index += 2;
  315.     }
  316.  
  317.     if (typeof fieldValue.$exists !== 'undefined') {
  318.       if (fieldValue.$exists) {
  319.         patterns.push(`$${index}:name IS NOT NULL`);
  320.       } else {
  321.         patterns.push(`$${index}:name IS NULL`);
  322.       }
  323.       values.push(fieldName);
  324.       index += 1;
  325.     }
  326.  
  327.     if (fieldValue.$nearSphere) {
  328.       const point = fieldValue.$nearSphere;
  329.       const distance = fieldValue.$maxDistance;
  330.       const distanceInKM = distance * 6371 * 1000;
  331.       patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) <= $${index + 3}`);
  332.       sorts.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index + 1}, $${index + 2})::geometry) ASC`)
  333.       values.push(fieldName, point.longitude, point.latitude, distanceInKM);
  334.       index += 4;
  335.     }
  336.  
  337.     if (fieldValue.$within && fieldValue.$within.$box) {
  338.       const box = fieldValue.$within.$box;
  339.       const left = box[0].longitude;
  340.       const bottom = box[0].latitude;
  341.       const right = box[1].longitude;
  342.       const top = box[1].latitude;
  343.  
  344.       patterns.push(`$${index}:name::point <@ $${index + 1}::box`);
  345.       values.push(fieldName, `((${left}, ${bottom}), (${right}, ${top}))`);
  346.       index += 2;
  347.     }
  348.  
  349.     if (fieldValue.$geoWithin && fieldValue.$geoWithin.$polygon) {
  350.       const polygon = fieldValue.$geoWithin.$polygon;
  351.       if (!(polygon instanceof Array)) {
  352.         throw new Parse.Error(
  353.           Parse.Error.INVALID_JSON,
  354.           'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
  355.         );
  356.       }
  357.       if (polygon.length < 3) {
  358.         throw new Parse.Error(
  359.           Parse.Error.INVALID_JSON,
  360.           'bad $geoWithin value; $polygon should contain at least 3 GeoPoints'
  361.         );
  362.       }
  363.       const points = polygon.map((point) => {
  364.         if (typeof point !== 'object' || point.__type !== 'GeoPoint') {
  365.           throw new Parse.Error(Parse.Error.INVALID_JSON, 'bad $geoWithin value');
  366.         } else {
  367.           Parse.GeoPoint._validate(point.latitude, point.longitude);
  368.         }
  369.         return `(${point.longitude}, ${point.latitude})`;
  370.       }).join(', ');
  371.  
  372.       patterns.push(`$${index}:name::point <@ $${index + 1}::polygon`);
  373.       values.push(fieldName, `(${points})`);
  374.       index += 2;
  375.     }
  376.  
  377.     if (fieldValue.$regex) {
  378.       let regex = fieldValue.$regex;
  379.       let operator = '~';
  380.       const opts = fieldValue.$options;
  381.       if (opts) {
  382.         if (opts.indexOf('i') >= 0) {
  383.           operator = '~*';
  384.         }
  385.         if (opts.indexOf('x') >= 0) {
  386.           regex = removeWhiteSpace(regex);
  387.         }
  388.       }
  389.  
  390.       regex = processRegexPattern(regex);
  391.  
  392.       patterns.push(`$${index}:name ${operator} '$${index + 1}:raw'`);
  393.       values.push(fieldName, regex);
  394.       index += 2;
  395.     }
  396.  
  397.     if (fieldValue.__type === 'Pointer') {
  398.       if (isArrayField) {
  399.         patterns.push(`array_contains($${index}:name, $${index + 1})`);
  400.         values.push(fieldName, JSON.stringify([fieldValue]));
  401.         index += 2;
  402.       } else {
  403.         patterns.push(`$${index}:name = $${index + 1}`);
  404.         values.push(fieldName, fieldValue.objectId);
  405.         index += 2;
  406.       }
  407.     }
  408.  
  409.     if (fieldValue.__type === 'Date') {
  410.       patterns.push(`$${index}:name = $${index + 1}`);
  411.       values.push(fieldName, fieldValue.iso);
  412.       index += 2;
  413.     }
  414.  
  415.     if (fieldValue.__type === 'GeoPoint') {
  416.       patterns.push('$' + index + ':name ~= POINT($' + (index + 1) + ', $' + (index + 2) + ')');
  417.       values.push(fieldName, fieldValue.longitude, fieldValue.latitude);
  418.       index += 3;
  419.     }
  420.  
  421.     Object.keys(ParseToPosgresComparator).forEach(cmp => {
  422.       if (fieldValue[cmp]) {
  423.         const pgComparator = ParseToPosgresComparator[cmp];
  424.         patterns.push(`$${index}:name ${pgComparator} $${index + 1}`);
  425.         values.push(fieldName, toPostgresValue(fieldValue[cmp]));
  426.         index += 2;
  427.       }
  428.     });
  429.  
  430.     if (initialPatternsLength === patterns.length) {
  431.       throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support this query type yet ${JSON.stringify(fieldValue)}`);
  432.    }
  433.  }
  434.  values = values.map(transformValue);
  435.  return { pattern: patterns.join(' AND '), values, sorts };
  436. }
  437.  
  438. export class PostgresStorageAdapter {
  439.  // Private
  440.  _collectionPrefix: string;
  441.  _client;
  442.  _pgp;
  443.  
  444.  constructor({
  445.    uri,
  446.    collectionPrefix = '',
  447.    databaseOptions
  448.  }) {
  449.    this._collectionPrefix = collectionPrefix;
  450.    const { client, pgp } = createClient(uri, databaseOptions);
  451.    this._client = client;
  452.    this._pgp = pgp;
  453.  }
  454.  
  455.  _ensureSchemaCollectionExists(conn) {
  456.    conn = conn || this._client;
  457.    return conn.none('CREATE TABLE IF NOT EXISTS "_SCHEMA" ( "className" varChar(120), "schema" jsonb, "isParseClass" bool, PRIMARY KEY ("className") )')
  458.    .catch(error => {
  459.      if (error.code === PostgresDuplicateRelationError
  460.          || error.code === PostgresUniqueIndexViolationError
  461.          || error.code === PostgresDuplicateObjectError) {
  462.        // Table already exists, must have been created by a different request. Ignore error.
  463.      } else {
  464.        throw error;
  465.      }
  466.    });
  467.  }
  468.  
  469.  classExists(name) {
  470.    return this._client.one(`SELECT EXISTS (SELECT 1 FROM   information_schema.tables WHERE table_name = $1)`, [name]).then((res) => {
  471.      return res.exists;
  472.    });
  473.  }
  474.  
  475.  setClassLevelPermissions(className, CLPs) {
  476.    return this._ensureSchemaCollectionExists().then(() => {
  477.      const values = [className, 'schema', 'classLevelPermissions', JSON.stringify(CLPs)]
  478.      return this._client.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values);
  479.    });
  480.  }
  481.  
  482.  createClass(className, schema) {
  483.    return this._client.tx(t => {
  484.      const q1 = this.createTable(className, schema, t);
  485.      const q2 = t.none('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($<className>, $<schema>, true)', { className, schema });
  486.  
  487.      return t.batch([q1, q2]);
  488.    })
  489.    .then(() => {
  490.      return toParseSchema(schema)
  491.    })
  492.    .catch((err) => {
  493.      if (Array.isArray(err.data) && err.data.length > 1 && err.data[0].result.code === PostgresTransactionAbortedError) {
  494.        err = err.data[1].result;
  495.      }
  496.  
  497.      if (err.code === PostgresUniqueIndexViolationError && err.detail.includes(className)) {
  498.        throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, `Class ${className} already exists.`)
  499.      }
  500.      throw err;
  501.    })
  502.  }
  503.  
  504.  // Just create a table, do not insert in schema
  505.  createTable(className, schema, conn) {
  506.    conn = conn || this._client;
  507.    debug('createTable', className, schema);
  508.    const valuesArray = [];
  509.    const patternsArray = [];
  510.    const fields = Object.assign({}, schema.fields);
  511.    if (className === '_User') {
  512.      fields._email_verify_token_expires_at = {type: 'Date'};
  513.      fields._email_verify_token = {type: 'String'};
  514.      fields._account_lockout_expires_at = {type: 'Date'};
  515.      fields._failed_login_count = {type: 'Number'};
  516.      fields._perishable_token = {type: 'String'};
  517.      fields._perishable_token_expires_at = {type: 'Date'};
  518.      fields._password_changed_at = {type: 'Date'};
  519.      fields._password_history = { type: 'Array'};
  520.    }
  521.    let index = 2;
  522.    const relations = [];
  523.    Object.keys(fields).forEach((fieldName) => {
  524.      const parseType = fields[fieldName];
  525.      // Skip when it's a relation
  526.       // We'll create the tables later
  527.       if (parseType.type === 'Relation') {
  528.         relations.push(fieldName)
  529.         return;
  530.       }
  531.       if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) {
  532.         parseType.contents = { type: 'String' };
  533.       }
  534.       valuesArray.push(fieldName);
  535.       valuesArray.push(parseTypeToPostgresType(parseType));
  536.       patternsArray.push(`$${index}:name $${index + 1}:raw`);
  537.       if (fieldName === 'objectId') {
  538.         patternsArray.push(`PRIMARY KEY ($${index}:name)`)
  539.       }
  540.       index = index + 2;
  541.     });
  542.     const qs = `CREATE TABLE IF NOT EXISTS $1:name (${patternsArray.join(',')})`;
  543.     const values = [className, ...valuesArray];
  544.     return this._ensureSchemaCollectionExists(conn)
  545.     .then(() => conn.none(qs, values))
  546.     .catch(error => {
  547.       if (error.code === PostgresDuplicateRelationError) {
  548.         // Table already exists, must have been created by a different request. Ignore error.
  549.       } else {
  550.         throw error;
  551.       }
  552.     }).then(() => {
  553.       // Create the relation tables
  554.       return Promise.all(relations.map((fieldName) => {
  555.         return conn.none('CREATE TABLE IF NOT EXISTS $<joinTable:name> ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', {joinTable: `_Join:${fieldName}:${className}`});
  556.       }));
  557.     });
  558.   }
  559.  
  560.   addFieldIfNotExists(className, fieldName, type) {
  561.     // TODO: Must be revised for invalid logic...
  562.     debug('addFieldIfNotExists', {className, fieldName, type});
  563.     return this._client.tx("addFieldIfNotExists", t=> {
  564.       let promise = Promise.resolve();
  565.       if (type.type !== 'Relation') {
  566.         promise = t.none('ALTER TABLE $<className:name> ADD COLUMN $<fieldName:name> $<postgresType:raw>', {
  567.           className,
  568.           fieldName,
  569.           postgresType: parseTypeToPostgresType(type)
  570.         })
  571.         .catch(error => {
  572.           if (error.code === PostgresRelationDoesNotExistError) {
  573.             return this.createClass(className, {fields: {[fieldName]: type}})
  574.           } else if (error.code === PostgresDuplicateColumnError) {
  575.             // Column already exists, created by other request. Carry on to
  576.             // See if it's the right type.
  577.           } else {
  578.             throw error;
  579.           }
  580.         })
  581.       } else {
  582.         promise = t.none('CREATE TABLE IF NOT EXISTS $<joinTable:name> ("relatedId" varChar(120), "owningId" varChar(120), PRIMARY KEY("relatedId", "owningId") )', {joinTable: `_Join:${fieldName}:${className}`})
  583.       }
  584.       return promise.then(() => {
  585.         return t.any('SELECT "schema" FROM "_SCHEMA" WHERE "className" = $<className> and ("schema"::json->\'fields\'->$<fieldName>) is not null', {className, fieldName});
  586.       }).then(result => {
  587.         if (result[0]) {
  588.           throw "Attempted to add a field that already exists";
  589.         } else {
  590.           const path = `{fields,${fieldName}}`;
  591.           return t.none(
  592.             'UPDATE "_SCHEMA" SET "schema"=jsonb_set("schema", $<path>, $<type>)  WHERE "className"=$<className>',
  593.             { path, type, className }
  594.           );
  595.         }
  596.       });
  597.     });
  598.   }
  599.  
  600.   // Drops a collection. Resolves with true if it was a Parse Schema (eg. _User, Custom, etc.)
  601.   // and resolves with false if it wasn't (eg. a join table). Rejects if deletion was impossible.
  602.   deleteClass(className) {
  603.     const operations = [
  604.       {query: `DROP TABLE IF EXISTS $1:name`, values: [className]},
  605.       {query: `DELETE FROM "_SCHEMA" WHERE "className" = $1`, values: [className]}
  606.     ];
  607.     return this._client.tx(t => t.none(this._pgp.helpers.concat(operations)))
  608.       .then(() => className.indexOf('_Join:') != 0); // resolves with false when _Join table
  609.   }
  610.  
  611.   // Delete all data known to this adapter. Used for testing.
  612.   deleteAllClasses() {
  613.     const now = new Date().getTime();
  614.     debug('deleteAllClasses');
  615.     return this._client.any('SELECT * FROM "_SCHEMA"')
  616.     .then(results => {
  617.       const joins = results.reduce((list, schema) => {
  618.         return list.concat(joinTablesForSchema(schema.schema));
  619.       }, []);
  620.       const classes = ['_SCHEMA','_PushStatus','_JobStatus','_Hooks','_GlobalConfig', ...results.map(result => result.className), ...joins];
  621.       return this._client.tx(t=>t.batch(classes.map(className=>t.none('DROP TABLE IF EXISTS $<className:name>', { className }))));
  622.     }, error => {
  623.       if (error.code === PostgresRelationDoesNotExistError) {
  624.         // No _SCHEMA collection. Don't delete anything.
  625.         return;
  626.       } else {
  627.         throw error;
  628.       }
  629.     }).then(() => {
  630.       debug(`deleteAllClasses done in ${new Date().getTime() - now}`);
  631.     });
  632.   }
  633.  
  634.   // Remove the column and all the data. For Relations, the _Join collection is handled
  635.   // specially, this function does not delete _Join columns. It should, however, indicate
  636.   // that the relation fields does not exist anymore. In mongo, this means removing it from
  637.   // the _SCHEMA collection.  There should be no actual data in the collection under the same name
  638.   // as the relation column, so it's fine to attempt to delete it. If the fields listed to be
  639.   // deleted do not exist, this function should return successfully anyways. Checking for
  640.   // attempts to delete non-existent fields is the responsibility of Parse Server.
  641.  
  642.   // This function is not obligated to delete fields atomically. It is given the field
  643.   // names in a list so that databases that are capable of deleting fields atomically
  644.   // may do so.
  645.  
  646.   // Returns a Promise.
  647.   deleteFields(className, schema, fieldNames) {
  648.     debug('deleteFields', className, fieldNames);
  649.     return Promise.resolve()
  650.     .then(() => {
  651.       fieldNames = fieldNames.reduce((list, fieldName) => {
  652.         const field = schema.fields[fieldName]
  653.         if (field.type !== 'Relation') {
  654.           list.push(fieldName);
  655.         }
  656.         delete schema.fields[fieldName];
  657.         return list;
  658.       }, []);
  659.  
  660.       const values = [className, ...fieldNames];
  661.       const columns = fieldNames.map((name, idx) => {
  662.         return `$${idx + 2}:name`;
  663.       }).join(', DROP COLUMN');
  664.  
  665.       const doBatch = (t) => {
  666.         const batch = [
  667.           t.none('UPDATE "_SCHEMA" SET "schema"=$<schema> WHERE "className"=$<className>', {schema, className})
  668.         ];
  669.         if (values.length > 1) {
  670.           batch.push(t.none(`ALTER TABLE $1:name DROP COLUMN ${columns}`, values));
  671.         }
  672.         return batch;
  673.       }
  674.       return this._client.tx((t) => {
  675.         return t.batch(doBatch(t));
  676.       });
  677.     });
  678.   }
  679.  
  680.   // Return a promise for all schemas known to this adapter, in Parse format. In case the
  681.   // schemas cannot be retrieved, returns a promise that rejects. Requirements for the
  682.   // rejection reason are TBD.
  683.   getAllClasses() {
  684.     return this._ensureSchemaCollectionExists()
  685.     .then(() => this._client.map('SELECT * FROM "_SCHEMA"', null, row => ({ className: row.className, ...row.schema })))
  686.     .then(res => res.map(toParseSchema))
  687.   }
  688.  
  689.   // Return a promise for the schema with the given name, in Parse format. If
  690.   // this adapter doesn't know about the schema, return a promise that rejects with
  691.   // undefined as the reason.
  692.   getClass(className) {
  693.     debug('getClass', className);
  694.     return this._client.any('SELECT * FROM "_SCHEMA" WHERE "className"=$<className>', { className })
  695.     .then(result => {
  696.       if (result.length === 1) {
  697.         return result[0].schema;
  698.       } else {
  699.         throw undefined;
  700.       }
  701.     }).then(toParseSchema);
  702.   }
  703.  
  704.   // TODO: remove the mongo format dependency in the return value
  705.   createObject(className, schema, object) {
  706.     debug('createObject', className, object);
  707.     let columnsArray = [];
  708.     const valuesArray = [];
  709.     schema = toPostgresSchema(schema);
  710.     const geoPoints = {};
  711.  
  712.     object = handleDotFields(object);
  713.  
  714.     validateKeys(object);
  715.  
  716.     Object.keys(object).forEach(fieldName => {
  717.       var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
  718.       if (authDataMatch) {
  719.         var provider = authDataMatch[1];
  720.         object['authData'] = object['authData'] || {};
  721.         object['authData'][provider] = object[fieldName];
  722.         delete object[fieldName];
  723.         fieldName = 'authData';
  724.       }
  725.  
  726.       columnsArray.push(fieldName);
  727.       if (!schema.fields[fieldName] && className === '_User') {
  728.         if (fieldName === '_email_verify_token' ||
  729.             fieldName === '_failed_login_count' ||
  730.             fieldName === '_perishable_token' ||
  731.             fieldName === '_password_history'){
  732.           valuesArray.push(object[fieldName]);
  733.         }
  734.  
  735.         if (fieldName === '_email_verify_token_expires_at') {
  736.           if (object[fieldName]) {
  737.             valuesArray.push(object[fieldName].iso);
  738.           } else {
  739.             valuesArray.push(null);
  740.           }
  741.         }
  742.  
  743.         if (fieldName === '_account_lockout_expires_at' ||
  744.             fieldName === '_perishable_token_expires_at' ||
  745.             fieldName === '_password_changed_at') {
  746.           if (object[fieldName]) {
  747.             valuesArray.push(object[fieldName].iso);
  748.           } else {
  749.             valuesArray.push(null);
  750.           }
  751.         }
  752.         return;
  753.       }
  754.       switch (schema.fields[fieldName].type) {
  755.       case 'Date':
  756.         if (object[fieldName]) {
  757.           valuesArray.push(object[fieldName].iso);
  758.         } else {
  759.           valuesArray.push(null);
  760.         }
  761.         break;
  762.       case 'Pointer':
  763.         valuesArray.push(object[fieldName].objectId);
  764.         break;
  765.       case 'Array':
  766.         if (['_rperm', '_wperm'].indexOf(fieldName) >= 0) {
  767.           valuesArray.push(object[fieldName]);
  768.         } else {
  769.           valuesArray.push(JSON.stringify(object[fieldName]));
  770.         }
  771.         break;
  772.       case 'Object':
  773.       case 'Bytes':
  774.       case 'String':
  775.       case 'Number':
  776.       case 'Boolean':
  777.         valuesArray.push(object[fieldName]);
  778.         break;
  779.       case 'File':
  780.         valuesArray.push(object[fieldName].name);
  781.         break;
  782.       case 'GeoPoint':
  783.           // pop the point and process later
  784.         geoPoints[fieldName] = object[fieldName];
  785.         columnsArray.pop();
  786.         break;
  787.       default:
  788.         throw `Type ${schema.fields[fieldName].type} not supported yet`;
  789.       }
  790.     });
  791.  
  792.     columnsArray = columnsArray.concat(Object.keys(geoPoints));
  793.     const initialValues = valuesArray.map((val, index) => {
  794.       let termination = '';
  795.       const fieldName = columnsArray[index];
  796.       if (['_rperm','_wperm'].indexOf(fieldName) >= 0) {
  797.         termination = '::text[]';
  798.       } else if (schema.fields[fieldName] && schema.fields[fieldName].type === 'Array') {
  799.         termination = '::jsonb';
  800.       }
  801.       return `$${index + 2 + columnsArray.length}${termination}`;
  802.     });
  803.     const geoPointsInjects = Object.keys(geoPoints).map((key) => {
  804.       const value = geoPoints[key];
  805.       valuesArray.push(value.longitude, value.latitude);
  806.       const l = valuesArray.length + columnsArray.length;
  807.       return `POINT($${l}, $${l + 1})`;
  808.     });
  809.  
  810.     const columnsPattern = columnsArray.map((col, index) => `$${index + 2}:name`).join(',');
  811.     const valuesPattern = initialValues.concat(geoPointsInjects).join(',')
  812.  
  813.     const qs = `INSERT INTO $1:name (${columnsPattern}) VALUES (${valuesPattern})`
  814.     const values = [className, ...columnsArray, ...valuesArray]
  815.     debug(qs, values);
  816.     return this._client.none(qs, values)
  817.     .then(() => ({ ops: [object] }))
  818.     .catch(error => {
  819.       if (error.code === PostgresUniqueIndexViolationError) {
  820.         throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
  821.       } else {
  822.         throw error;
  823.       }
  824.     })
  825.   }
  826.  
  827.   // Remove all objects that match the given Parse Query.
  828.   // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined.
  829.   // If there is some other error, reject with INTERNAL_SERVER_ERROR.
  830.   deleteObjectsByQuery(className, schema, query) {
  831.     debug('deleteObjectsByQuery', className, query);
  832.     const values = [className];
  833.     const index = 2;
  834.     const where = buildWhereClause({ schema, index, query })
  835.     values.push(...where.values);
  836.     if (Object.keys(query).length === 0) {
  837.       where.pattern = 'TRUE';
  838.     }
  839.     const qs = `WITH deleted AS (DELETE FROM $1:name WHERE ${where.pattern} RETURNING *) SELECT count(*) FROM deleted`;
  840.     debug(qs, values);
  841.     return this._client.one(qs, values , a => +a.count)
  842.     .then(count => {
  843.       if (count === 0) {
  844.         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.');
  845.       } else {
  846.         return count;
  847.       }
  848.     });
  849.   }
  850.   // Return value not currently well specified.
  851.   findOneAndUpdate(className, schema, query, update) {
  852.     debug('findOneAndUpdate', className, query, update);
  853.     return this.updateObjectsByQuery(className, schema, query, update).then((val) => val[0]);
  854.   }
  855.  
  856.   // Apply the update to all objects that match the given Parse Query.
  857.   updateObjectsByQuery(className, schema, query, update) {
  858.     debug('updateObjectsByQuery', className, query, update);
  859.     const updatePatterns = [];
  860.     const values = [className]
  861.     let index = 2;
  862.     schema = toPostgresSchema(schema);
  863.  
  864.     const originalUpdate = {...update};
  865.     update = handleDotFields(update);
  866.     // Resolve authData first,
  867.     // So we don't end up with multiple key updates
  868.     for (const fieldName in update) {
  869.       const authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
  870.       if (authDataMatch) {
  871.         var provider = authDataMatch[1];
  872.         const value = update[fieldName];
  873.         delete update[fieldName];
  874.         update['authData'] = update['authData'] || {};
  875.         update['authData'][provider] = value;
  876.       }
  877.     }
  878.  
  879.     for (const fieldName in update) {
  880.       const fieldValue = update[fieldName];
  881.       if (fieldValue === null) {
  882.         updatePatterns.push(`$${index}:name = NULL`);
  883.         values.push(fieldName);
  884.         index += 1;
  885.       } else if (fieldName == 'authData') {
  886.         // This recursively sets the json_object
  887.         // Only 1 level deep
  888.         const generate = (jsonb, key, value) => {
  889.           return `json_object_set_key(COALESCE(${jsonb}, '{}'::jsonb), ${key}, ${value})::jsonb`;
  890.         }
  891.         const lastKey = `$${index}:name`;
  892.         const fieldNameIndex = index;
  893.         index += 1;
  894.         values.push(fieldName);
  895.         const update = Object.keys(fieldValue).reduce((lastKey, key) => {
  896.           const str = generate(lastKey, `$${index}::text`, `$${index + 1}::jsonb`)
  897.           index += 2;
  898.           let value = fieldValue[key];
  899.           if (value) {
  900.             if (value.__op === 'Delete') {
  901.               value = null;
  902.             } else {
  903.               value = JSON.stringify(value)
  904.             }
  905.           }
  906.           values.push(key, value);
  907.           return str;
  908.         }, lastKey);
  909.         updatePatterns.push(`$${fieldNameIndex}:name = ${update}`);
  910.       } else if (fieldValue.__op === 'Increment') {
  911.         updatePatterns.push(`$${index}:name = COALESCE($${index}:name, 0) + $${index + 1}`);
  912.         values.push(fieldName, fieldValue.amount);
  913.         index += 2;
  914.       } else if (fieldValue.__op === 'Add') {
  915.         updatePatterns.push(`$${index}:name = array_add(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`);
  916.         values.push(fieldName, JSON.stringify(fieldValue.objects));
  917.         index += 2;
  918.       } else if (fieldValue.__op === 'Delete') {
  919.         updatePatterns.push(`$${index}:name = $${index + 1}`)
  920.         values.push(fieldName, null);
  921.         index += 2;
  922.       } else if (fieldValue.__op === 'Remove') {
  923.         updatePatterns.push(`$${index}:name = array_remove(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`)
  924.         values.push(fieldName, JSON.stringify(fieldValue.objects));
  925.         index += 2;
  926.       } else if (fieldValue.__op === 'AddUnique') {
  927.         updatePatterns.push(`$${index}:name = array_add_unique(COALESCE($${index}:name, '[]'::jsonb), $${index + 1}::jsonb)`);
  928.         values.push(fieldName, JSON.stringify(fieldValue.objects));
  929.         index += 2;
  930.       } else if (fieldName === 'updatedAt') { //TODO: stop special casing this. It should check for __type === 'Date' and use .iso
  931.         updatePatterns.push(`$${index}:name = $${index + 1}`)
  932.         values.push(fieldName, fieldValue);
  933.         index += 2;
  934.       } else if (typeof fieldValue === 'string') {
  935.         updatePatterns.push(`$${index}:name = $${index + 1}`);
  936.         values.push(fieldName, fieldValue);
  937.         index += 2;
  938.       } else if (typeof fieldValue === 'boolean') {
  939.         updatePatterns.push(`$${index}:name = $${index + 1}`);
  940.         values.push(fieldName, fieldValue);
  941.         index += 2;
  942.       } else if (fieldValue.__type === 'Pointer') {
  943.         updatePatterns.push(`$${index}:name = $${index + 1}`);
  944.         values.push(fieldName, fieldValue.objectId);
  945.         index += 2;
  946.       } else if (fieldValue.__type === 'Date') {
  947.         updatePatterns.push(`$${index}:name = $${index + 1}`);
  948.         values.push(fieldName, toPostgresValue(fieldValue));
  949.         index += 2;
  950.       } else if (fieldValue instanceof Date) {
  951.         updatePatterns.push(`$${index}:name = $${index + 1}`);
  952.         values.push(fieldName, fieldValue);
  953.         index += 2;
  954.       } else if (fieldValue.__type === 'File') {
  955.         updatePatterns.push(`$${index}:name = $${index + 1}`);
  956.         values.push(fieldName, toPostgresValue(fieldValue));
  957.         index += 2;
  958.       } else if (fieldValue.__type === 'GeoPoint') {
  959.         updatePatterns.push(`$${index}:name = POINT($${index + 1}, $${index + 2})`);
  960.         values.push(fieldName, fieldValue.longitude, fieldValue.latitude);
  961.         index += 3;
  962.       } else if (fieldValue.__type === 'Relation') {
  963.         // noop
  964.       } else if (typeof fieldValue === 'number') {
  965.         updatePatterns.push(`$${index}:name = $${index + 1}`);
  966.         values.push(fieldName, fieldValue);
  967.         index += 2;
  968.       } else if (typeof fieldValue === 'object'
  969.                     && schema.fields[fieldName]
  970.                     && schema.fields[fieldName].type === 'Object') {
  971.         // Gather keys to increment
  972.         const keysToIncrement = Object.keys(originalUpdate).filter(k => {
  973.           // choose top level fields that have a delete operation set
  974.           return originalUpdate[k].__op === 'Increment' && k.split('.').length === 2 && k.split(".")[0] === fieldName;
  975.         }).map(k => k.split('.')[1]);
  976.  
  977.         let incrementPatterns = '';
  978.         if (keysToIncrement.length > 0) {
  979.           incrementPatterns = ' || ' + keysToIncrement.map((c) => {
  980.             const amount = fieldValue[c].amount;
  981.             return `CONCAT('{"${c}":', COALESCE($${index}:name->>'${c}','0')::int + ${amount}, '}')::jsonb`;
  982.           }).join(' || ');
  983.           // Strip the keys
  984.           keysToIncrement.forEach((key) => {
  985.             delete fieldValue[key];
  986.           });
  987.         }
  988.  
  989.         const keysToDelete = Object.keys(originalUpdate).filter(k => {
  990.           // choose top level fields that have a delete operation set
  991.           return originalUpdate[k].__op === 'Delete' && k.split('.').length === 2 && k.split(".")[0] === fieldName;
  992.         }).map(k => k.split('.')[1]);
  993.  
  994.         const deletePatterns = keysToDelete.reduce((p, c, i) => {
  995.           return p + ` - '$${index + 1 + i}:value'`;
  996.         }, '');
  997.  
  998.         updatePatterns.push(`$${index}:name = ( COALESCE($${index}:name, '{}'::jsonb) ${deletePatterns} ${incrementPatterns} || $${index + 1 + keysToDelete.length}::jsonb )`);
  999.  
  1000.         values.push(fieldName, ...keysToDelete, JSON.stringify(fieldValue));
  1001.         index += 2 + keysToDelete.length;
  1002.       } else if (Array.isArray(fieldValue)
  1003.                     && schema.fields[fieldName]
  1004.                     && schema.fields[fieldName].type === 'Array') {
  1005.         const expectedType = parseTypeToPostgresType(schema.fields[fieldName]);
  1006.         if (expectedType === 'text[]') {
  1007.           updatePatterns.push(`$${index}:name = $${index + 1}::text[]`);
  1008.         } else {
  1009.           let type = 'text';
  1010.           for (const elt of fieldValue) {
  1011.             if (typeof elt == 'object') {
  1012.               type = 'json';
  1013.               break;
  1014.             }
  1015.           }
  1016.           updatePatterns.push(`$${index}:name = array_to_json($${index + 1}::${type}[])::jsonb`);
  1017.         }
  1018.         values.push(fieldName, fieldValue);
  1019.         index += 2;
  1020.       } else {
  1021.         debug('Not supported update', fieldName, fieldValue);
  1022.         return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support update ${JSON.stringify(fieldValue)} yet`));
  1023.      }
  1024.    }
  1025.  
  1026.    const where = buildWhereClause({ schema, index, query })
  1027.    values.push(...where.values);
  1028.  
  1029.    const qs = `UPDATE $1:name SET ${updatePatterns.join(',')} WHERE ${where.pattern} RETURNING *`;
  1030.    debug('update: ', qs, values);
  1031.    return this._client.any(qs, values);
  1032.  }
  1033.  
  1034.  // Hopefully, we can get rid of this. It's only used for config and hooks.
  1035.   upsertOneObject(className, schema, query, update) {
  1036.     debug('upsertOneObject', {className, query, update});
  1037.     const createValue = Object.assign({}, query, update);
  1038.     return this.createObject(className, schema, createValue).catch((err) => {
  1039.       // ignore duplicate value errors as it's upsert
  1040.       if (err.code === Parse.Error.DUPLICATE_VALUE) {
  1041.         return this.findOneAndUpdate(className, schema, query, update);
  1042.       }
  1043.       throw err;
  1044.     });
  1045.   }
  1046.  
  1047.   find(className, schema, query, { skip, limit, sort, keys }) {
  1048.     debug('find', className, query, {skip, limit, sort, keys });
  1049.     const hasLimit = limit !== undefined;
  1050.     const hasSkip = skip !== undefined;
  1051.     let values = [className];
  1052.     const where = buildWhereClause({ schema, query, index: 2 })
  1053.     values.push(...where.values);
  1054.  
  1055.     const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
  1056.     const limitPattern = hasLimit ? `LIMIT $${values.length + 1}` : '';
  1057.     if (hasLimit) {
  1058.       values.push(limit);
  1059.     }
  1060.     const skipPattern = hasSkip ? `OFFSET $${values.length + 1}` : '';
  1061.     if (hasSkip) {
  1062.       values.push(skip);
  1063.     }
  1064.  
  1065.     let sortPattern = '';
  1066.     if (sort) {
  1067.       const sorting = Object.keys(sort).map((key) => {
  1068.         // Using $idx pattern gives:  non-integer constant in ORDER BY
  1069.         if (sort[key] === 1) {
  1070.           return `"${key}" ASC`;
  1071.         }
  1072.         return `"${key}" DESC`;
  1073.       }).join(',');
  1074.       sortPattern = sort !== undefined && Object.keys(sort).length > 0 ? `ORDER BY ${sorting}` : '';
  1075.     }
  1076.     if (where.sorts && Object.keys(where.sorts).length > 0) {
  1077.       sortPattern = `ORDER BY ${where.sorts.join(',')}`;
  1078.     }
  1079.  
  1080.     let columns = '*';
  1081.     if (keys) {
  1082.       // Exclude empty keys
  1083.       keys = keys.filter((key) => {
  1084.         return key.length > 0;
  1085.       });
  1086.       columns = keys.map((key, index) => {
  1087.         return `$${index + values.length + 1}:name`;
  1088.       }).join(',');
  1089.       values = values.concat(keys);
  1090.     }
  1091.  
  1092.     const qs = `SELECT ${columns} FROM $1:name ${wherePattern} ${sortPattern} ${limitPattern} ${skipPattern}`;
  1093.     debug(qs, values);
  1094.     return this._client.any(qs, values)
  1095.     .catch((err) => {
  1096.       // Query on non existing table, don't crash
  1097.       if (err.code === PostgresRelationDoesNotExistError) {
  1098.         return [];
  1099.       }
  1100.       return Promise.reject(err);
  1101.     })
  1102.     .then(results => results.map(object => {
  1103.       Object.keys(schema.fields).forEach(fieldName => {
  1104.         if (schema.fields[fieldName].type === 'Pointer' && object[fieldName]) {
  1105.           object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass };
  1106.         }
  1107.         if (schema.fields[fieldName].type === 'Relation') {
  1108.           object[fieldName] = {
  1109.             __type: "Relation",
  1110.             className: schema.fields[fieldName].targetClass
  1111.           }
  1112.         }
  1113.         if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') {
  1114.           object[fieldName] = {
  1115.             __type: "GeoPoint",
  1116.             latitude: object[fieldName].y,
  1117.             longitude: object[fieldName].x
  1118.           }
  1119.         }
  1120.         if (object[fieldName] && schema.fields[fieldName].type === 'File') {
  1121.           object[fieldName] = {
  1122.             __type: 'File',
  1123.             name: object[fieldName]
  1124.           }
  1125.         }
  1126.       });
  1127.       //TODO: remove this reliance on the mongo format. DB adapter shouldn't know there is a difference between created at and any other date field.
  1128.       if (object.createdAt) {
  1129.         object.createdAt = object.createdAt.toISOString();
  1130.       }
  1131.       if (object.updatedAt) {
  1132.         object.updatedAt = object.updatedAt.toISOString();
  1133.       }
  1134.       if (object.expiresAt) {
  1135.         object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() };
  1136.       }
  1137.       if (object._email_verify_token_expires_at) {
  1138.         object._email_verify_token_expires_at = { __type: 'Date', iso: object._email_verify_token_expires_at.toISOString() };
  1139.       }
  1140.       if (object._account_lockout_expires_at) {
  1141.         object._account_lockout_expires_at = { __type: 'Date', iso: object._account_lockout_expires_at.toISOString() };
  1142.       }
  1143.       if (object._perishable_token_expires_at) {
  1144.         object._perishable_token_expires_at = { __type: 'Date', iso: object._perishable_token_expires_at.toISOString() };
  1145.       }
  1146.       if (object._password_changed_at) {
  1147.         object._password_changed_at = { __type: 'Date', iso: object._password_changed_at.toISOString() };
  1148.       }
  1149.  
  1150.       for (const fieldName in object) {
  1151.         if (object[fieldName] === null) {
  1152.           delete object[fieldName];
  1153.         }
  1154.         if (object[fieldName] instanceof Date) {
  1155.           object[fieldName] = { __type: 'Date', iso: object[fieldName].toISOString() };
  1156.         }
  1157.       }
  1158.  
  1159.       return object;
  1160.     }));
  1161.   }
  1162.  
  1163.   // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't
  1164.   // currently know which fields are nullable and which aren't, we ignore that criteria.
  1165.   // As such, we shouldn't expose this function to users of parse until we have an out-of-band
  1166.   // Way of determining if a field is nullable. Undefined doesn't count against uniqueness,
  1167.   // which is why we use sparse indexes.
  1168.   ensureUniqueness(className, schema, fieldNames) {
  1169.     // Use the same name for every ensureUniqueness attempt, because postgres
  1170.     // Will happily create the same index with multiple names.
  1171.     const constraintName = `unique_${fieldNames.sort().join('_')}`;
  1172.     const constraintPatterns = fieldNames.map((fieldName, index) => `$${index + 3}:name`);
  1173.     const qs = `ALTER TABLE $1:name ADD CONSTRAINT $2:name UNIQUE (${constraintPatterns.join(',')})`;
  1174.     return this._client.none(qs,[className, constraintName, ...fieldNames])
  1175.     .catch(error => {
  1176.       if (error.code === PostgresDuplicateRelationError && error.message.includes(constraintName)) {
  1177.         // Index already exists. Ignore error.
  1178.       } else if (error.code === PostgresUniqueIndexViolationError && error.message.includes(constraintName)) {
  1179.         // Cast the error into the proper parse error
  1180.         throw new Parse.Error(Parse.Error.DUPLICATE_VALUE, 'A duplicate value for a field with unique values was provided');
  1181.       } else {
  1182.         throw error;
  1183.       }
  1184.     });
  1185.   }
  1186.  
  1187.   // Executes a count.
  1188.   count(className, schema, query) {
  1189.     debug('count', className, query);
  1190.     const values = [className];
  1191.     const where = buildWhereClause({ schema, query, index: 2 });
  1192.     values.push(...where.values);
  1193.  
  1194.     const wherePattern = where.pattern.length > 0 ? `WHERE ${where.pattern}` : '';
  1195.     const qs = `SELECT count(*) FROM $1:name ${wherePattern}`;
  1196.     return this._client.one(qs, values, a => +a.count).catch((err) => {
  1197.       if (err.code === PostgresRelationDoesNotExistError) {
  1198.         return 0;
  1199.       }
  1200.       throw err;
  1201.     });
  1202.   }
  1203.  
  1204.   performInitialization({ VolatileClassesSchemas }) {
  1205.     debug('performInitialization');
  1206.     const promises = VolatileClassesSchemas.map((schema) => {
  1207.       return this.createTable(schema.className, schema).catch((err) => {
  1208.         if (err.code === PostgresDuplicateRelationError || err.code === Parse.Error.INVALID_CLASS_NAME) {
  1209.           return Promise.resolve();
  1210.         }
  1211.         throw err;
  1212.       });
  1213.     });
  1214.     return Promise.all(promises)
  1215.       .then(() => {
  1216.         return this._client.tx(t => {
  1217.           return t.batch([
  1218.             t.none(sql.misc.jsonObjectSetKeys),
  1219.             t.none(sql.array.add),
  1220.             t.none(sql.array.addUnique),
  1221.             t.none(sql.array.remove),
  1222.             t.none(sql.array.containsAll),
  1223.             t.none(sql.array.contains)
  1224.           ]);
  1225.         });
  1226.       })
  1227.       .then(data => {
  1228.         debug(`initializationDone in ${data.duration}`);
  1229.       })
  1230.       .catch(error => {
  1231.         /* eslint-disable no-console */
  1232.         console.error(error);
  1233.       });
  1234.   }
  1235. }
  1236.  
  1237. function removeWhiteSpace(regex) {
  1238.   if (!regex.endsWith('\n')){
  1239.     regex += '\n';
  1240.   }
  1241.  
  1242.   // remove non escaped comments
  1243.   return regex.replace(/([^\\])#.*\n/gmi, '$1')
  1244.     // remove lines starting with a comment
  1245.     .replace(/^#.*\n/gmi, '')
  1246.     // remove non escaped whitespace
  1247.     .replace(/([^\\])\s+/gmi, '$1')
  1248.     // remove whitespace at the beginning of a line
  1249.     .replace(/^\s+/, '')
  1250.     .trim();
  1251. }
  1252.  
  1253. function processRegexPattern(s) {
  1254.   if (s && s.startsWith('^')){
  1255.     // regex for startsWith
  1256.     return '^' + literalizeRegexPart(s.slice(1));
  1257.  
  1258.   } else if (s && s.endsWith('$')) {
  1259.     // regex for endsWith
  1260.     return literalizeRegexPart(s.slice(0, s.length - 1)) + '$';
  1261.   }
  1262.  
  1263.   // regex for contains
  1264.   return literalizeRegexPart(s);
  1265. }
  1266.  
  1267. function createLiteralRegex(remaining) {
  1268.   return remaining.split('').map(c => {
  1269.     if (c.match(/[0-9a-zA-Z]/) !== null) {
  1270.       // don't escape alphanumeric characters
  1271.       return c;
  1272.     }
  1273.     // escape everything else (single quotes with single quotes, everything else with a backslash)
  1274.     return c === `'` ? `''` : `\\${c}`;
  1275.  }).join('');
  1276. }
  1277.  
  1278. function literalizeRegexPart(s) {
  1279.  const matcher1 = /\\Q((?!\\E).*)\\E$/
  1280.  const result1 = s.match(matcher1);
  1281.  if(result1 && result1.length > 1 && result1.index > -1){
  1282.    // process regex that has a beginning and an end specified for the literal text
  1283.    const prefix = s.substr(0, result1.index);
  1284.    const remaining = result1[1];
  1285.  
  1286.    return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
  1287.  }
  1288.  
  1289.  // process regex that has a beginning specified for the literal text
  1290.  const matcher2 = /\\Q((?!\\E).*)$/
  1291.  const result2 = s.match(matcher2);
  1292.  if(result2 && result2.length > 1 && result2.index > -1){
  1293.    const prefix = s.substr(0, result2.index);
  1294.    const remaining = result2[1];
  1295.  
  1296.    return literalizeRegexPart(prefix) + createLiteralRegex(remaining);
  1297.  }
  1298.  
  1299.  // remove all instances of \Q and \E from the remaining text & escape single quotes
  1300.  return (
  1301.    s.replace(/([^\\])(\\E)/, '$1')
  1302.      .replace(/([^\\])(\\Q)/, '$1')
  1303.      .replace(/^\\E/, '')
  1304.      .replace(/^\\Q/, '')
  1305.      .replace(/([^'])'/, `$1''`)
  1306.      .replace(/^'([^'])/, `''$1`)
  1307.  );
  1308. }
  1309.  
  1310. export default PostgresStorageAdapter;
  1311. module.exports = PostgresStorageAdapter; // Required for tests
  1312.  
downloadPostgresStorageAdapter.js Source code - Download parse-server Source code
Related Source Codes/Software:
react-boilerplate - 2017-06-07
webtorrent - Streaming torrent client for the web ... 2017-06-06
machine-learning-for-software-engineers - A complete daily plan for studying to become a mac... 2017-06-06
upterm - A terminal emulator for the 21st century. 2017-06-06
lottie-android - Render After Effects animations natively on Androi... 2017-06-07
AsyncDisplayKit - Smooth asynchronous user interfaces for iOS apps. ... 2017-06-07
ionicons - The premium icon font for Ionic ... 2017-06-07
storybook - 2017-06-07
prettier - Prettier is an opinionated JavaScript formatter. ... 2017-06-08
CRYENGINE - CRYENGINE is a powerful real-time game development... 2017-06-11
postal - 2017-06-11
reactide - Reactide is the first dedicated IDE for React web ... 2017-06-11
rkt - rkt is a pod-native container engine for Linux. It... 2017-06-11
uWebSockets - Tiny WebSockets https://for... 2017-06-11
realworld - TodoMVC for the RealWorld - Exemplary fullstack Me... 2017-06-11
goreplay - GoReplay is an open-source tool for capturing and ... 2017-06-10
pyenv - Simple Python version management 2017-06-10
redux-saga - An alternative side effect model for Redux apps ... 2017-06-10
angular-starter - 2017-06-10

 Back to top