entity.js

"use strict";
// Copyright 2014 Google LLC
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//      http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
Object.defineProperty(exports, "__esModule", { value: true });
const arrify = require("arrify");
const extend = require("extend");
const is = require("is");
const Protobuf = require("protobufjs");
const path = require("path");
// tslint:disable-next-line no-namespace
var entity;
(function (entity_1) {
    class InvalidKeyError extends Error {
        constructor(opts) {
            const errorMessages = {
                MISSING_KIND: 'A key should contain at least a kind.',
                MISSING_ANCESTOR_ID: 'Ancestor keys require an id or name.',
            };
            super(errorMessages[opts.code]);
            this.name = 'InvalidKey';
        }
    }
    entity_1.InvalidKeyError = InvalidKeyError;
    /**
     * A symbol to access the Key object from an entity object.
     *
     * @type {symbol}
     * @private
     */
    entity_1.KEY_SYMBOL = Symbol('KEY');
    /**
     * Build a Datastore Double object. For long doubles, a string can be
     * provided.
     *
     * @class
     * @param {number} value The double value.
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const aDouble = datastore.double(7.3);
     */
    class Double {
        constructor(value) {
            /**
             * @name Double#type
             * @type {string}
             */
            this.type = 'DatastoreDouble';
            /**
             * @name Double#value
             * @type {number}
             */
            this.value = value;
        }
    }
    entity_1.Double = Double;
    /**
     * Check if something is a Datastore Double object.
     *
     * @private
     * @param {*} value
     * @returns {boolean}
     */
    function isDsDouble(value) {
        return value instanceof entity.Double;
    }
    entity_1.isDsDouble = isDsDouble;
    /**
     * Check if a value is a Datastore Double object converted from JSON.
     *
     * @private
     * @param {*} value
     * @returns {boolean}
     */
    function isDsDoubleLike(value) {
        const maybeDsDouble = value;
        return (isDsDouble(maybeDsDouble) ||
            (is.object(maybeDsDouble) &&
                is.number(maybeDsDouble.value) &&
                maybeDsDouble.type === 'DatastoreDouble'));
    }
    entity_1.isDsDoubleLike = isDsDoubleLike;
    /**
     * Build a Datastore Int object. For long integers, a string can be provided.
     *
     * @class
     * @param {number|string} value The integer value.
     * @param {object} [typeCastOptions] Configuration to convert
     *     values of `integerValue` type to a custom value. Must provide an
     *     `integerTypeCastFunction` to handle `integerValue` conversion.
     * @param {function} typeCastOptions.integerTypeCastFunction A custom user
     *     provided function to convert `integerValue`.
     * @param {sting|string[]} [typeCastOptions.properties] `Entity` property
     *     names to be converted using `integerTypeCastFunction`.
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const anInt = datastore.int(7);
     */
    class Int extends Number {
        constructor(value, typeCastOptions) {
            super(typeof value === 'object' ? value.integerValue : value);
            this._entityPropertyName =
                typeof value === 'object' ? value.propertyName : undefined;
            this.value =
                typeof value === 'object'
                    ? value.integerValue.toString()
                    : value.toString();
            /**
             * @name Int#type
             * @type {string}
             */
            this.type = 'DatastoreInt';
            /**
             * @name Int#value
             * @type {string}
             */
            if (typeCastOptions) {
                this.typeCastFunction = typeCastOptions.integerTypeCastFunction;
                if (typeof typeCastOptions.integerTypeCastFunction !== 'function') {
                    throw new Error(`integerTypeCastFunction is not a function or was not provided.`);
                }
                this.typeCastProperties = typeCastOptions.properties
                    ? arrify(typeCastOptions.properties)
                    : undefined;
            }
        }
        // tslint:disable-next-line no-any
        valueOf() {
            let shouldCustomCast = this.typeCastFunction ? true : false;
            if (this.typeCastProperties &&
                !this.typeCastProperties.includes(this._entityPropertyName)) {
                shouldCustomCast = false;
            }
            if (shouldCustomCast) {
                try {
                    return this.typeCastFunction(this.value);
                }
                catch (error) {
                    error.message = `integerTypeCastFunction threw an error:\n\n  - ${error.message}`;
                    throw error;
                }
            }
            else {
                return decodeIntegerValue({
                    integerValue: this.value,
                    propertyName: this._entityPropertyName,
                });
            }
        }
        toJSON() {
            return { type: this.type, value: this.value };
        }
    }
    entity_1.Int = Int;
    /**
     * Check if something is a Datastore Int object.
     *
     * @private
     * @param {*} value
     * @returns {boolean}
     */
    function isDsInt(value) {
        return value instanceof entity.Int;
    }
    entity_1.isDsInt = isDsInt;
    /**
     * Check if a value is a Datastore Int object converted from JSON.
     *
     * @private
     * @param {*} value
     * @returns {boolean}
     */
    function isDsIntLike(value) {
        const maybeDsInt = value;
        return (isDsInt(maybeDsInt) ||
            (is.object(maybeDsInt) &&
                is.string(maybeDsInt.value) &&
                maybeDsInt.type === 'DatastoreInt'));
    }
    entity_1.isDsIntLike = isDsIntLike;
    /**
     * Build a Datastore Geo Point object.
     *
     * @class
     * @param {object} coordinates Coordinate value.
     * @param {number} coordinates.latitude Latitudinal value.
     * @param {number} coordinates.longitude Longitudinal value.
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const coordinates = {
     *   latitude: 40.6894,
     *   longitude: -74.0447
     * };
     *
     * const geoPoint = datastore.geoPoint(coordinates);
     */
    class GeoPoint {
        constructor(coordinates) {
            /**
             * Coordinate value.
             *
             * @name GeoPoint#coordinates
             * @type {object}
             * @property {number} latitude Latitudinal value.
             * @property {number} longitude Longitudinal value.
             */
            this.value = coordinates;
        }
    }
    entity_1.GeoPoint = GeoPoint;
    /**
     * Check if something is a Datastore Geo Point object.
     *
     * @private
     * @param {*} value
     * @returns {boolean}
     */
    function isDsGeoPoint(value) {
        return value instanceof entity.GeoPoint;
    }
    entity_1.isDsGeoPoint = isDsGeoPoint;
    /**
     * Build a Datastore Key object.
     *
     * @class
     * @param {object} options Configuration object.
     * @param {array} options.path Key path.
     * @param {string} [options.namespace] Optional namespace.
     *
     * @example
     * //-
     * // Create an incomplete key with a kind value of `Company`.
     * //-
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const key = datastore.key('Company');
     *
     * @example
     * //-
     * // Create a complete key with a kind value of `Company` and id`123`.
     * //-
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const key = datastore.key(['Company', 123]);
     *
     * @example
     * //-
     * // If the ID integer is outside the bounds of a JavaScript Number
     * // object, create an Int.
     * //-
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const key = datastore.key([
     *   'Company',
     *   datastore.int('100000000000001234')
     * ]);
     *
     * @example
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * // Create a complete key with a kind value of `Company` and name `Google`.
     * // Note: `id` is used for numeric identifiers and `name` is used otherwise.
     * const key = datastore.key(['Company', 'Google']);
     *
     * @example
     * //-
     * // Create a complete key from a provided namespace and path.
     * //-
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const key = datastore.key({
     *   namespace: 'My-NS',
     *   path: ['Company', 123]
     * });
     *
     * @example <caption>Serialize the key for later re-use.</caption>
     * const {Datastore} = require('@google-cloud/datastore');
     * const datastore = new Datastore();
     * const key = datastore.key({
     *   namespace: 'My-NS',
     *   path: ['Company', 123]
     * });
     * // Later...
     * const key = datastore.key(key.serialized);
     */
    class Key {
        constructor(options) {
            /**
             * @name Key#namespace
             * @type {string}
             */
            this.namespace = options.namespace;
            options.path = [].slice.call(options.path);
            if (options.path.length % 2 === 0) {
                const identifier = options.path.pop();
                if (is.number(identifier) ||
                    isDsInt(identifier) ||
                    isDsIntLike(identifier)) {
                    this.id = (identifier.value || identifier);
                }
                else if (is.string(identifier)) {
                    this.name = identifier;
                }
            }
            this.kind = options.path.pop();
            if (options.path.length > 0) {
                this.parent = new Key(options);
            }
            // `path` is computed on demand to consider any changes that may have been
            // made to the key.
            /**
             * @name Key#path
             * @type {array}
             */
            Object.defineProperty(this, 'path', {
                enumerable: true,
                get() {
                    return arrify(this.parent && this.parent.path).concat([
                        this.kind,
                        this.name || this.id,
                    ]);
                },
            });
        }
        /**
         * Access the `serialized` property for a library-compatible way to re-use a
         * key.
         *
         * @returns {object}
         *
         * @example
         * const key = datastore.key({
         *   namespace: 'My-NS',
         *   path: ['Company', 123]
         * });
         *
         * // Later...
         * const key = datastore.key(key.serialized);
         */
        get serialized() {
            const serializedKey = {
                namespace: this.namespace,
                path: [this.kind, this.name || new Int(this.id)],
            };
            if (this.parent) {
                serializedKey.path = this.parent.serialized.path.concat(serializedKey.path);
            }
            return serializedKey;
        }
    }
    entity_1.Key = Key;
    /**
     * Check if something is a Datastore Key object.
     *
     * @private
     * @param {*} value
     * @returns {boolean}
     */
    function isDsKey(value) {
        return value instanceof entity.Key;
    }
    entity_1.isDsKey = isDsKey;
    /**
     * Convert a protobuf `integerValue`.
     *
     * @private
     * @param {object} value The `integerValue` to convert.
     */
    function decodeIntegerValue(value) {
        const num = Number(value.integerValue);
        if (!Number.isSafeInteger(num)) {
            throw new Error('We attempted to return all of the numeric values, but ' +
                (value.propertyName ? value.propertyName + ' ' : '') +
                'value ' +
                value.integerValue +
                " is out of bounds of 'Number.MAX_SAFE_INTEGER'.\n" +
                "To prevent this error, please consider passing 'options.wrapNumbers=true' or\n" +
                "'options.wrapNumbers' as\n" +
                '{\n' +
                '  integerTypeCastFunction: provide <your_custom_function>\n' +
                '  properties: optionally specify property name(s) to be cutom casted' +
                '}\n');
        }
        return num;
    }
    /**
     * @typedef {object} IntegerTypeCastOptions Configuration to convert
     *     values of `integerValue` type to a custom value. Must provide an
     *     `integerTypeCastFunction` to handle `integerValue` conversion.
     * @property {function} integerTypeCastFunction A custom user
     *     provided function to convert `integerValue`.
     * @property {string | string[]} [properties] `Entity` property
     *     names to be converted using `integerTypeCastFunction`.
     */
    /**
     * Convert a protobuf Value message to its native value.
     *
     * @private
     * @param {object} valueProto The protobuf Value message to convert.
     * @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in
     *     {@link Datastore#Int} objects.
     *     If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
     *     If an `object`, this will return a value returned by
     *     `wrapNumbers.integerTypeCastFunction`.
     *     Please see {@link IntegerTypeCastOptions} for options descriptions.
     * @returns {*}
     *
     * @example
     * decodeValueProto({
     *   booleanValue: false
     * });
     * // false
     *
     * decodeValueProto({
     *   stringValue: 'Hi'
     * });
     * // 'Hi'
     *
     * decodeValueProto({
     *   blobValue: Buffer.from('68656c6c6f')
     * });
     * // <Buffer 68 65 6c 6c 6f>
     */
    function decodeValueProto(valueProto, wrapNumbers) {
        const valueType = valueProto.valueType;
        const value = valueProto[valueType];
        switch (valueType) {
            case 'arrayValue': {
                // tslint:disable-next-line no-any
                return value.values.map((val) => entity.decodeValueProto(val, wrapNumbers));
            }
            case 'blobValue': {
                return Buffer.from(value, 'base64');
            }
            case 'nullValue': {
                return null;
            }
            case 'doubleValue': {
                return Number(value);
            }
            case 'integerValue': {
                return wrapNumbers
                    ? typeof wrapNumbers === 'object'
                        ? new entity.Int(valueProto, wrapNumbers).valueOf()
                        : new entity.Int(valueProto, undefined)
                    : decodeIntegerValue(valueProto);
            }
            case 'entityValue': {
                return entity.entityFromEntityProto(value, wrapNumbers);
            }
            case 'keyValue': {
                return entity.keyFromKeyProto(value);
            }
            case 'timestampValue': {
                const milliseconds = Number(value.nanos) / 1e6;
                return new Date(Number(value.seconds) * 1000 + milliseconds);
            }
            default: {
                return value;
            }
        }
    }
    entity_1.decodeValueProto = decodeValueProto;
    /**
     * Convert any native value to a protobuf Value message object.
     *
     * @private
     * @param {*} value Native value.
     * @returns {object}
     *
     * @example
     * encodeValue('Hi');
     * // {
     * //   stringValue: 'Hi'
     * // }
     */
    // tslint:disable-next-line no-any
    function encodeValue(value) {
        const valueProto = {};
        if (is.boolean(value)) {
            valueProto.booleanValue = value;
            return valueProto;
        }
        if (is.null(value)) {
            valueProto.nullValue = 0;
            return valueProto;
        }
        if (typeof value === 'number') {
            if (value % 1 === 0) {
                value = new entity.Int(value);
            }
            else {
                value = new entity.Double(value);
            }
        }
        if (isDsInt(value)) {
            valueProto.integerValue = value.value;
            return valueProto;
        }
        if (isDsDouble(value)) {
            valueProto.doubleValue = value.value;
            return valueProto;
        }
        if (isDsGeoPoint(value)) {
            valueProto.geoPointValue = value.value;
            return valueProto;
        }
        if (value instanceof Date) {
            const seconds = value.getTime() / 1000;
            valueProto.timestampValue = {
                seconds: Math.floor(seconds),
                nanos: value.getMilliseconds() * 1e6,
            };
            return valueProto;
        }
        if (is.string(value)) {
            valueProto.stringValue = value;
            return valueProto;
        }
        if (value instanceof Buffer) {
            valueProto.blobValue = value;
            return valueProto;
        }
        if (Array.isArray(value)) {
            valueProto.arrayValue = {
                values: value.map(entity.encodeValue),
            };
            return valueProto;
        }
        if (isDsKey(value)) {
            valueProto.keyValue = entity.keyToKeyProto(value);
            return valueProto;
        }
        if (is.object(value)) {
            if (!is.empty(value)) {
                value = extend(true, {}, value);
                for (const prop in value) {
                    if (value.hasOwnProperty(prop)) {
                        value[prop] = entity.encodeValue(value[prop]);
                    }
                }
            }
            valueProto.entityValue = {
                properties: value,
            };
            return valueProto;
        }
        throw new Error('Unsupported field value, ' + value + ', was provided.');
    }
    entity_1.encodeValue = encodeValue;
    /**
     * Convert any entity protocol to a plain object.
     *
     * @todo Use registered metadata if provided.
     *
     * @private
     * @param {object} entityProto The protocol entity object to convert.
     * @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in
     *     {@link Datastore#Int} objects.
     *     If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
     *     If an `object`, this will return a value returned by
     *     `wrapNumbers.integerTypeCastFunction`.
     *     Please see {@link IntegerTypeCastOptions} for options descriptions.
     * @returns {object}
     *
     * @example
     * entityFromEntityProto({
     *   properties: {
     *     map: {
     *       name: {
     *         value: {
     *           valueType: 'stringValue',
     *           stringValue: 'Stephen'
     *         }
     *       }
     *     }
     *   }
     * });
     * // {
     * //   name: 'Stephen'
     * // }
     */
    // tslint:disable-next-line no-any
    function entityFromEntityProto(entityProto, wrapNumbers) {
        // tslint:disable-next-line no-any
        const entityObject = {};
        const properties = entityProto.properties || {};
        // tslint:disable-next-line forin
        for (const property in properties) {
            const value = properties[property];
            entityObject[property] = entity.decodeValueProto(value, wrapNumbers);
        }
        return entityObject;
    }
    entity_1.entityFromEntityProto = entityFromEntityProto;
    /**
     * Convert an entity object to an entity protocol object.
     *
     * @private
     * @param {object} entityObject The entity object to convert.
     * @returns {object}
     *
     * @example
     * entityToEntityProto({
     *   excludeFromIndexes: [
     *     'name'
     *   ],
     *   data: {
     *     name: 'Burcu',
     *     legit: true
     *   }
     * });
     * // {
     * //   key: null,
     * //   properties: {
     * //     name: {
     * //       stringValue: 'Burcu'
     * //       excludeFromIndexes: true
     * //     },
     * //     legit: {
     * //       booleanValue: true
     * //     }
     * //   }
     * // }
     */
    function entityToEntityProto(entityObject) {
        const properties = entityObject.data;
        const excludeFromIndexes = entityObject.excludeFromIndexes;
        const entityProto = {
            key: null,
            properties: Object.keys(properties).reduce((encoded, key) => {
                encoded[key] = entity.encodeValue(properties[key]);
                return encoded;
            }, 
            // tslint:disable-next-line no-any
            {}),
        };
        if (excludeFromIndexes && excludeFromIndexes.length > 0) {
            excludeFromIndexes.forEach((excludePath) => {
                excludePathFromEntity(entityProto, excludePath);
            });
        }
        return entityProto;
        function excludePathFromEntity(entity, path) {
            const arrayIndex = path.indexOf('[]');
            const entityIndex = path.indexOf('.');
            const wildcardIndex = path.indexOf('.*');
            const hasArrayPath = arrayIndex > -1;
            const hasEntityPath = entityIndex > -1;
            const hasWildCard = wildcardIndex > -1;
            if (!hasArrayPath && !hasEntityPath) {
                // This is the path end node. Traversal ends here in either case.
                if (entity.properties) {
                    if (entity.properties[path] &&
                        // array properties should be excluded with [] syntax:
                        !entity.properties[path].arrayValue) {
                        // This is the property to exclude!
                        entity.properties[path].excludeFromIndexes = true;
                    }
                }
                else if (!path) {
                    // This is a primitive or entity root that should be excluded.
                    entity.excludeFromIndexes = true;
                }
                return;
            }
            let delimiterIndex;
            if (hasArrayPath && hasEntityPath) {
                delimiterIndex = Math.min(arrayIndex, entityIndex);
            }
            else {
                delimiterIndex = Math.max(arrayIndex, entityIndex);
            }
            const firstPathPartIsArray = delimiterIndex === arrayIndex;
            const firstPathPartIsEntity = delimiterIndex === entityIndex;
            const delimiter = firstPathPartIsArray ? '[]' : '.';
            const splitPath = path.split(delimiter);
            const firstPathPart = splitPath.shift();
            const remainderPath = splitPath.join(delimiter).replace(/^(\.|\[\])/, '');
            if (!(entity.properties && entity.properties[firstPathPart]) &&
                !hasWildCard) {
                // Either a primitive or an entity for which this path doesn't apply.
                return;
            }
            if (firstPathPartIsArray &&
                // check also if the property in question is actually an array value.
                entity.properties[firstPathPart].arrayValue &&
                // check if wildcard is not applied
                !hasWildCard) {
                const array = entity.properties[firstPathPart].arrayValue;
                // tslint:disable-next-line no-any
                array.values.forEach((value) => {
                    if (remainderPath === '') {
                        // We want to exclude *this* array property, which is
                        // equivalent with excluding all its values
                        // (including entity values at their roots):
                        excludePathFromEntity(value, remainderPath // === ''
                        );
                    }
                    else {
                        // Path traversal continues at value.entityValue,
                        // if it is an entity, or must end at value.
                        excludePathFromEntity(value.entityValue || value, remainderPath // !== ''
                        );
                    }
                });
            }
            else if (firstPathPartIsArray && hasWildCard && remainderPath === '*') {
                const array = entity.properties[firstPathPart].arrayValue;
                // tslint:disable-next-line no-any
                array.values.forEach((value) => {
                    if (value.entityValue) {
                        excludePathFromEntity(value.entityValue, '.*');
                    }
                    else {
                        excludePathFromEntity(value, '');
                    }
                });
            }
            else if (firstPathPartIsEntity) {
                if (firstPathPart === '') {
                    Object.keys(entity.properties).forEach(path => {
                        const newPath = entity.properties[path].arrayValue
                            ? path + '[].*'
                            : path + '.*';
                        excludePathFromEntity(entity, newPath);
                    });
                }
                else {
                    if (hasWildCard && remainderPath === '*') {
                        const parentEntity = entity.properties[firstPathPart].entityValue;
                        if (parentEntity) {
                            Object.keys(parentEntity.properties).forEach(path => {
                                const newPath = parentEntity.properties[path].arrayValue
                                    ? path + '[].*'
                                    : path + '.*';
                                excludePathFromEntity(parentEntity, newPath);
                            });
                        }
                        else {
                            excludePathFromEntity(entity, firstPathPart);
                        }
                    }
                    else {
                        const parentEntity = entity.properties[firstPathPart].entityValue;
                        excludePathFromEntity(parentEntity, remainderPath);
                    }
                }
            }
        }
    }
    entity_1.entityToEntityProto = entityToEntityProto;
    /**
     * Convert an API response array to a qualified Key and data object.
     *
     * @private
     * @param {object[]} results The response array.
     * @param {object} results.entity An entity object.
     * @param {object} results.entity.key The entity's key.
     * @param {boolean | IntegerTypeCastOptions} [wrapNumbers=false] Wrap values of integerValue type in
     *     {@link Datastore#Int} objects.
     *     If a `boolean`, this will wrap values in {@link Datastore#Int} objects.
     *     If an `object`, this will return a value returned by
     *     `wrapNumbers.integerTypeCastFunction`.
     *     Please see {@link IntegerTypeCastOptions} for options descriptions.
     *
     * @example
     * request_('runQuery', {}, (err, response) => {
     *   const entityObjects = formatArray(response.batch.entityResults);
     *   // {
     *   //   key: {},
     *   //   data: {
     *   //     fieldName: 'value'
     *   //   }
     *   // }
     *   //
     * });
     */
    function formatArray(results, wrapNumbers) {
        return results.map(result => {
            const ent = entity.entityFromEntityProto(result.entity, wrapNumbers);
            ent[entity.KEY_SYMBOL] = entity.keyFromKeyProto(result.entity.key);
            return ent;
        });
    }
    entity_1.formatArray = formatArray;
    /**
     * Find the properties which value size is large than 1500 bytes,
     * with excludeLargeProperties enabled, automatically exclude properties from indexing.
     * This will allow storing string values larger than 1500 bytes
     *
     * @param entities Datastore key object(s).
     * @param path namespace of provided entity properties
     * @param properties properties which value size is large than 1500 bytes
     */
    function findLargeProperties_(entities, path, properties = []) {
        const MAX_DATASTORE_VALUE_LENGTH = 1500;
        if (Array.isArray(entities)) {
            for (const entry of entities) {
                if (entry.name && entry.value) {
                    if (is.string(entry.value) &&
                        Buffer.from(entry.value).length > MAX_DATASTORE_VALUE_LENGTH) {
                        entry.excludeFromIndexes = true;
                    }
                    else {
                        continue;
                    }
                }
                findLargeProperties_(entry, path.concat('[]'), properties);
            }
        }
        else if (is.object(entities)) {
            const keys = Object.keys(entities);
            for (const key of keys) {
                findLargeProperties_(entities[key], path.concat(`${path ? '.' : ''}${key}`), properties);
            }
        }
        else if (is.string(entities) &&
            Buffer.from(entities).length > MAX_DATASTORE_VALUE_LENGTH) {
            if (properties.indexOf(path) < 0) {
                properties.push(path);
            }
        }
        return properties;
    }
    entity_1.findLargeProperties_ = findLargeProperties_;
    /**
     * Check if a key is complete.
     *
     * @private
     * @param {Key} key The Key object.
     * @returns {boolean}
     *
     * @example
     * isKeyComplete(new Key(['Company', 'Google'])); // true
     * isKeyComplete(new Key('Company')); // false
     */
    function isKeyComplete(key) {
        const lastPathElement = entity.keyToKeyProto(key).path.pop();
        return !!(lastPathElement.id || lastPathElement.name);
    }
    entity_1.isKeyComplete = isKeyComplete;
    /**
     * Convert a key protocol object to a Key object.
     *
     * @private
     * @param {object} keyProto The key protocol object to convert.
     * @returns {Key}
     *
     * @example
     * const key = keyFromKeyProto({
     *   partitionId: {
     *     projectId: 'project-id',
     *     namespaceId: ''
     *   },
     *   path: [
     *     {
     *       kind: 'Kind',
     *       id: '4790047639339008'
     *     }
     *   ]
     * });
     */
    function keyFromKeyProto(keyProto) {
        // tslint:disable-next-line no-any
        const keyOptions = {
            path: [],
        };
        if (keyProto.partitionId && keyProto.partitionId.namespaceId) {
            keyOptions.namespace = keyProto.partitionId.namespaceId;
        }
        keyProto.path.forEach((path, index) => {
            keyOptions.path.push(path.kind);
            let id = path[path.idType];
            if (path.idType === 'id') {
                id = new entity.Int(id);
            }
            if (is.defined(id)) {
                keyOptions.path.push(id);
            }
            else if (index < keyProto.path.length - 1) {
                throw new InvalidKeyError({
                    code: 'MISSING_ANCESTOR_ID',
                });
            }
        });
        return new entity.Key(keyOptions);
    }
    entity_1.keyFromKeyProto = keyFromKeyProto;
    /**
     * Convert a Key object to a key protocol object.
     *
     * @private
     * @param {Key} key The Key object to convert.
     * @returns {object}
     *
     * @example
     * const keyProto = keyToKeyProto(new Key(['Company', 1]));
     * // {
     * //   path: [
     * //     {
     * //       kind: 'Company',
     * //       id: 1
     * //     }
     * //   ]
     * // }
     */
    function keyToKeyProto(key) {
        if (is.undefined(key.kind)) {
            throw new InvalidKeyError({
                code: 'MISSING_KIND',
            });
        }
        // tslint:disable-next-line no-any
        const keyProto = {
            path: [],
        };
        if (key.namespace) {
            keyProto.partitionId = {
                namespaceId: key.namespace,
            };
        }
        let numKeysWalked = 0;
        // Reverse-iterate over the Key objects.
        do {
            if (numKeysWalked > 0 && is.undefined(key.id) && is.undefined(key.name)) {
                // This isn't just an incomplete key. An ancestor key is incomplete.
                throw new InvalidKeyError({
                    code: 'MISSING_ANCESTOR_ID',
                });
            }
            // tslint:disable-next-line no-any
            const pathElement = {
                kind: key.kind,
            };
            if (is.defined(key.id)) {
                pathElement.id = key.id;
            }
            if (is.defined(key.name)) {
                pathElement.name = key.name;
            }
            keyProto.path.unshift(pathElement);
            // tslint:disable-next-line no-conditional-assignment
        } while ((key = key.parent) && ++numKeysWalked);
        return keyProto;
    }
    entity_1.keyToKeyProto = keyToKeyProto;
    /**
     * Convert a query object to a query protocol object.
     *
     * @private
     * @param {object} q The query object to convert.
     * @returns {object}
     *
     * @example
     * queryToQueryProto({
     *   namespace: '',
     *   kinds: [
     *     'Kind'
     *   ],
     *   filters: [],
     *   orders: [],
     *   groupByVal: [],
     *   selectVal: [],
     *   startVal: null,
     *   endVal: null,
     *   limitVal: -1,
     *   offsetVal: -1
     * });
     * // {
     * //   projection: [],
     * //   kinds: [
     * //     {
     * //       name: 'Kind'
     * //     }
     * //   ],
     * //   order: [],
     * //   groupBy: []
     * // }
     */
    function queryToQueryProto(query) {
        const OP_TO_OPERATOR = {
            '=': 'EQUAL',
            '>': 'GREATER_THAN',
            '>=': 'GREATER_THAN_OR_EQUAL',
            '<': 'LESS_THAN',
            '<=': 'LESS_THAN_OR_EQUAL',
            HAS_ANCESTOR: 'HAS_ANCESTOR',
        };
        const SIGN_TO_ORDER = {
            '-': 'DESCENDING',
            '+': 'ASCENDING',
        };
        const queryProto = {
            distinctOn: query.groupByVal.map(groupBy => {
                return {
                    name: groupBy,
                };
            }),
            kind: query.kinds.map(kind => {
                return {
                    name: kind,
                };
            }),
            order: query.orders.map(order => {
                return {
                    property: {
                        name: order.name,
                    },
                    direction: SIGN_TO_ORDER[order.sign],
                };
            }),
            projection: query.selectVal.map(select => {
                return {
                    property: {
                        name: select,
                    },
                };
            }),
        };
        if (query.endVal) {
            queryProto.endCursor = query.endVal;
        }
        if (query.limitVal > 0) {
            queryProto.limit = {
                value: query.limitVal,
            };
        }
        if (query.offsetVal > 0) {
            queryProto.offset = query.offsetVal;
        }
        if (query.startVal) {
            queryProto.startCursor = query.startVal;
        }
        if (query.filters.length > 0) {
            const filters = query.filters.map(filter => {
                // tslint:disable-next-line no-any
                let value = {};
                if (filter.name === '__key__') {
                    value.keyValue = entity.keyToKeyProto(filter.val);
                }
                else {
                    value = entity.encodeValue(filter.val);
                }
                return {
                    propertyFilter: {
                        property: {
                            name: filter.name,
                        },
                        op: OP_TO_OPERATOR[filter.op],
                        value,
                    },
                };
            });
            queryProto.filter = {
                compositeFilter: {
                    filters,
                    op: 'AND',
                },
            };
        }
        return queryProto;
    }
    entity_1.queryToQueryProto = queryToQueryProto;
    /**
     * URL safe key encoding and decoding helper utility.
     *
     *  This is intended to work with the "legacy" representation of a
     * datastore "Key" used within Google App Engine (a so-called "Reference").
     *
     * @private
     * @class
     */
    class URLSafeKey {
        constructor() {
            this.protos = this.loadProtos_();
        }
        /**
         *  Load AppEngine protobuf file.
         *
         *  @private
         */
        loadProtos_() {
            const root = new Protobuf.Root();
            const loadedRoot = root.loadSync(path.join(__dirname, '..', 'protos', 'app_engine_key.proto'));
            loadedRoot.resolveAll();
            return loadedRoot.nested;
        }
        /**
         * Convert key to url safe base64 encoded string.
         *
         * @private
         * @param {string} projectId Project Id.
         * @param {entity.Key} key Entity key object.
         * @param {string} locationPrefix Optional .
         *  The location prefix of an App Engine project ID.
         *  Often this value is 's~', but may also be 'e~', or other location prefixes
         *  currently unknown.
         * @returns {string} base64 endocded urlsafe key.
         */
        legacyEncode(projectId, key, locationPrefix) {
            const elements = [];
            let currentKey = key;
            do {
                // tslint:disable-next-line no-any
                const element = {
                    type: currentKey.kind,
                };
                if (is.defined(currentKey.id)) {
                    element.id = currentKey.id;
                }
                if (is.defined(currentKey.name)) {
                    element.name = currentKey.name;
                }
                elements.unshift(element);
                currentKey = currentKey.parent;
            } while (currentKey);
            if (locationPrefix) {
                projectId = `${locationPrefix}${projectId}`;
            }
            const reference = {
                app: projectId,
                namespace: key.namespace,
                path: { element: elements },
            };
            const buffer = this.protos.Reference.encode(reference).finish();
            return this.convertToBase64_(buffer);
        }
        /**
         * Helper to convert URL safe key string to entity key object
         *
         * This is intended to work with the "legacy" representation of a
         * datastore "Key" used within Google App Engine (a so-called "Reference").
         *
         * @private
         * @param {entity.Key} key Entity key object.
         * @param {string} locationPrefix Optional .
         *  The location prefix of an App Engine project ID.
         *  Often this value is 's~', but may also be 'e~', or other location prefixes
         *  currently unknown.
         * @returns {string} Created urlsafe key.
         */
        legacyDecode(key) {
            const buffer = this.convertToBuffer_(key);
            const message = this.protos.Reference.decode(buffer);
            const reference = this.protos.Reference.toObject(message, {
                longs: String,
            });
            const pathElements = [];
            reference.path.element.forEach((element) => {
                pathElements.push(element.type);
                if (is.defined(element.name)) {
                    pathElements.push(element.name);
                }
                if (is.defined(element.id)) {
                    pathElements.push(new entity.Int(element.id));
                }
            });
            const keyOptions = {
                path: pathElements,
            };
            if (!is.empty(reference.namespace)) {
                keyOptions.namespace = reference.namespace;
            }
            return new entity.Key(keyOptions);
        }
        /**
         * Convert buffer to base64 encoding.
         *
         * @private
         * @param {Buffer} buffer
         * @returns {string} Base64 encoded string.
         */
        convertToBase64_(buffer) {
            return buffer
                .toString('base64')
                .replace(/\+/g, '-')
                .replace(/\//g, '_')
                .replace(/=+$/, '');
        }
        /**
         * Rebuild base64 from encoded url safe string and convert to buffer.
         *
         * @private
         * @param {string} val Encoded url safe string.
         * @returns {string} Base64 encoded string.
         */
        convertToBuffer_(val) {
            val = val.replace(/-/g, '+').replace(/_/g, '/');
            while (val.length % 4) {
                val += '=';
            }
            return Buffer.from(val, 'base64');
        }
    }
    entity_1.URLSafeKey = URLSafeKey;
})(entity = exports.entity || (exports.entity = {}));
//# sourceMappingURL=entity.js.map