1 5
/*!
2 5
 * Copyright 2017 Google Inc. All Rights Reserved.
3 5
 *
4 5
 * Licensed under the Apache License, Version 2.0 (the "License");
5 5
 * you may not use this file except in compliance with the License.
6 5
 * You may obtain a copy of the License at
7 5
 *
8 5
 *      http://www.apache.org/licenses/LICENSE-2.0
9 5
 *
10 5
 * Unless required by applicable law or agreed to in writing, software
11 5
 * distributed under the License is distributed on an "AS IS" BASIS,
12 5
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 5
 * See the License for the specific language governing permissions and
14 5
 * limitations under the License.
15 5
 */
16 5
import {GrpcService} from './common-grpc/service';
17 5
import {PreciseDate} from '@google-cloud/precise-date';
18 5
import arrify = require('arrify');
19 5
import {Big} from 'big.js';
20 5
import * as is from 'is';
21 5
import {common as p} from 'protobufjs';
22 5
import {google as spannerClient} from '../protos/protos';
23 5

24 5
// eslint-disable-next-line @typescript-eslint/no-explicit-any
25 5
export type Value = any;
26 5

27 5
export interface Field {
28 5
  name: string;
29 5
  value: Value;
30 5
}
31 5

32 5
export interface Json {
33 5
  [field: string]: Value;
34 5
}
35 5

36 5
export interface JSONOptions {
37 5
  wrapNumbers?: boolean;
38 5
  wrapStructs?: boolean;
39 5
}
40 5

41 5
// https://github.com/Microsoft/TypeScript/issues/27920
42 5
type DateFields = [number, number, number];
43 5

44 5
/**
45 5
 * Date-like object used to represent Cloud Spanner Dates. DATE types represent
46 5
 * a logical calendar date, independent of time zone. DATE values do not
47 5
 * represent a specific 24-hour period. Rather, a given DATE value represents a
48 5
 * different 24-hour period when interpreted in a different time zone. Because
49 5
 * of this, all values passed to {@link Spanner.date} will be interpreted as
50 5
 * local time.
51 5
 *
52 5
 * To represent an absolute point in time, use {@link Spanner.timestamp}.
53 5
 *
54 5
 * @see Spanner.date
55 5
 * @see https://cloud.google.com/spanner/docs/data-types#date-type
56 5
 *
57 5
 * @class
58 5
 * @extends Date
59 5
 *
60 5
 * @param {string|number} [date] String representing the date or number
61 5
 *     representing the year. If year is a number between 0 and 99, then year is
62 5
 *     assumed to be 1900 + year.
63 5
 * @param {number} [month] Number representing the month (0 = January).
64 5
 * @param {number} [date] Number representing the date.
65 5
 *
66 5
 * @example
67 5
 * Spanner.date('3-3-1933');
68 5
 */
69 5
export class SpannerDate extends Date {
70 5
  constructor(dateString?: string);
71 5
  constructor(year: number, month: number, date: number);
72 5
  constructor(...dateFields: Array<string | number | undefined>) {
73 5
    const yearOrDateString = dateFields[0];
74 5

75 5
    // yearOrDateString could be 0 (number).
76 5
    if (yearOrDateString === null || yearOrDateString === undefined) {
77 5
      dateFields[0] = new Date().toDateString();
78 5
    }
79 5

80 5
    // JavaScript Date objects will interpret ISO date strings as Zulu time,
81 5
    // but by formatting it, we can infer local time.
82 5
    if (/^\d{4}-\d{1,2}-\d{1,2}/.test(yearOrDateString as string)) {
83 5
      const [year, month, date] = (yearOrDateString as string).split(/-|T/);
84 5
      dateFields = [`${month}-${date}-${year}`];
85 5
    }
86 5

87 5
    super(...(dateFields.slice(0, 3) as DateFields));
88 5
  }
89 5
  /**
90 5
   * Returns the date in ISO date format.
91 5
   * `YYYY-[M]M-[D]D`
92 5
   *
93 5
   * @returns {string}
94 5
   */
95 5
  toJSON(): string {
96 5
    const year = this.getFullYear();
97 5
    let month = (this.getMonth() + 1).toString();
98 5
    let date = this.getDate().toString();
99 5

100 5
    if (month.length === 1) {
101 5
      month = `0${month}`;
102 5
    }
103 5

104 5
    if (date.length === 1) {
105 5
      date = `0${date}`;
106 5
    }
107 5

108 5
    return `${year}-${month}-${date}`;
109 5
  }
110 5
}
111 5

112 5
/**
113 5
 * Using an abstract class to simplify checking for wrapped numbers.
114 5
 *
115 5
 * @private
116 5
 */
117 5
abstract class WrappedNumber {
118 5
  value!: string | number;
119 5
  abstract valueOf(): number;
120 5
}
121 5

122 5
/**
123 5
 * @typedef Float
124 5
 * @see Spanner.float
125 5
 */
126 5
export class Float extends WrappedNumber {
127 5
  value: number;
128 5
  constructor(value: number) {
129 5
    super();
130 5
    this.value = value;
131 5
  }
132 5
  valueOf(): number {
133 5
    return Number(this.value);
134 5
  }
135 5
}
136 5

137 5
/**
138 5
 * @typedef Int
139 5
 * @see Spanner.int
140 5
 */
141 5
export class Int extends WrappedNumber {
142 5
  value: string;
143 5
  constructor(value: string) {
144 5
    super();
145 5
    this.value = value.toString();
146 5
  }
147 5
  valueOf(): number {
148 5
    const num = Number(this.value);
149 5
    if (num > Number.MAX_SAFE_INTEGER) {
150 5
      throw new Error(`Integer ${this.value} is out of bounds.`);
151 5
    }
152 5
    return num;
153 5
  }
154 5
}
155 5

156 5
/**
157 5
 * @typedef Struct
158 5
 * @see Spanner.struct
159 5
 */
160 5
export class Struct extends Array<Field> {
161 5
  /**
162 5
   * Converts struct into a pojo (plain old JavaScript object).
163 5
   *
164 5
   * @param {JSONOptions} [options] JSON options.
165 5
   * @returns {object}
166 5
   */
167 5
  toJSON(options?: JSONOptions): Json {
168 5
    return codec.convertFieldsToJson(this, options);
169 5
  }
170 5
  /**
171 5
   * Converts an array of fields to a struct.
172 5
   *
173 5
   * @private
174 5
   *
175 5
   * @param {object[]} fields List of struct fields.
176 5
   * @return {Struct}
177 5
   */
178 5
  static fromArray(fields: Field[]): Struct {
179 5
    return new Struct(...fields);
180 5
  }
181 5
  /**
182 5
   * Converts a JSON object to a struct.
183 5
   *
184 5
   * @private
185 5
   *
186 5
   * @param {object} json Struct JSON.
187 5
   * @return {Struct}
188 5
   */
189 5
  static fromJSON(json: Json): Struct {
190 5
    const fields = Object.keys(json || {}).map(name => {
191 5
      const value = json[name];
192 5
      return {name, value};
193 5
    });
194 5
    return Struct.fromArray(fields);
195 5
  }
196 5
}
197 5

198 5
/**
199 5
 * @typedef Numeric
200 5
 * @see Spanner.numeric
201 5
 */
202 5
export class Numeric {
203 5
  value: string;
204 5
  constructor(value: string) {
205 5
    this.value = value;
206 5
  }
207 5
  valueOf(): Big {
208 5
    return new Big(this.value);
209 5
  }
210 5
  toJSON(): string {
211 5
    return this.valueOf().toJSON();
212 5
  }
213 5
}
214 5

215 5
/**
216 5
 * @typedef JSONOptions
217 5
 * @property {boolean} [wrapNumbers=false] Indicates if the numbers should be
218 5
 *     wrapped in Int/Float wrappers.
219 5
 * @property {boolean} [wrapStructs=false] Indicates if the structs should be
220 5
 *     wrapped in Struct wrapper.
221 5
 */
222 5
/**
223 5
 * Wherever a row or struct object is returned, it is assigned a "toJSON"
224 5
 * function. This function will generate the JSON for that row.
225 5
 *
226 5
 * @private
227 5
 *
228 5
 * @param {array} row The row to generate JSON for.
229 5
 * @param {JSONOptions} [options] JSON options.
230 5
 * @returns {object}
231 5
 */
232 5
function convertFieldsToJson(fields: Field[], options?: JSONOptions): Json {
233 5
  const json: Json = {};
234 5

235 5
  const defaultOptions = {wrapNumbers: false, wrapStructs: false};
236 5

237 5
  options = Object.assign(defaultOptions, options);
238 5

239 5
  for (const {name, value} of fields) {
240 5
    if (!name) {
241 5
      continue;
242 5
    }
243 5

244 5
    try {
245 5
      json[name] = convertValueToJson(value, options);
246 5
    } catch (e) {
247 5
      e.message = [
248 5
        `Serializing column "${name}" encountered an error: ${e.message}`,
249 5
        'Call row.toJSON({ wrapNumbers: true }) to receive a custom type.',
250 5
      ].join(' ');
251 5
      throw e;
252 5
    }
253 5
  }
254 5

255 5
  return json;
256 5
}
257 5

258 5
/**
259 5
 * Attempts to convert a wrapped or nested value into a native JavaScript type.
260 5
 *
261 5
 * @private
262 5
 *
263 5
 * @param {*} value The value to convert.
264 5
 * @param {JSONOptions} options JSON options.
265 5
 * @return {*}
266 5
 */
267 5
function convertValueToJson(value: Value, options: JSONOptions): Value {
268 5
  if (!options.wrapNumbers && value instanceof WrappedNumber) {
269 5
    return value.valueOf();
270 5
  }
271 5

272 5
  if (value instanceof Struct) {
273 5
    if (!options.wrapStructs) {
274 5
      return value.toJSON(options);
275 5
    }
276 5

277 5
    return value.map(({name, value}) => {
278 5
      value = convertValueToJson(value, options);
279 5
      return {name, value};
280 5
    });
281 5
  }
282 5

283 5
  if (Array.isArray(value)) {
284 5
    return value.map(child => convertValueToJson(child, options));
285 5
  }
286 5

287 5
  return value;
288 5
}
289 5

290 5
/**
291 5
 * Re-decode after the generic gRPC decoding step.
292 5
 *
293 5
 * @private
294 5
 *
295 5
 * @param {*} value Value to decode
296 5
 * @param {object[]} type Value type object.
297 5
 * @returns {*}
298 5
 */
299 5
function decode(value: Value, type: spannerClient.spanner.v1.Type): Value {
300 5
  if (is.null(value)) {
301 5
    return null;
302 5
  }
303 5

304 5
  let decoded = value;
305 5
  let fields;
306 5

307 5
  switch (type.code) {
308 5
    case spannerClient.spanner.v1.TypeCode.BYTES:
309 5
    case 'BYTES':
310 5
      decoded = Buffer.from(decoded, 'base64');
311 5
      break;
312 5
    case spannerClient.spanner.v1.TypeCode.FLOAT64:
313 5
    case 'FLOAT64':
314 5
      decoded = new Float(decoded);
315 5
      break;
316 5
    case spannerClient.spanner.v1.TypeCode.INT64:
317 5
    case 'INT64':
318 5
      decoded = new Int(decoded);
319 5
      break;
320 5
    case spannerClient.spanner.v1.TypeCode.NUMERIC:
321 5
    case 'NUMERIC':
322 5
      decoded = new Numeric(decoded);
323 5
      break;
324 5
    case spannerClient.spanner.v1.TypeCode.TIMESTAMP:
325 5
    case 'TIMESTAMP':
326 5
      decoded = new PreciseDate(decoded);
327 5
      break;
328 5
    case spannerClient.spanner.v1.TypeCode.DATE:
329 5
    case 'DATE':
330 5
      decoded = new SpannerDate(decoded);
331 5
      break;
332 5
    case spannerClient.spanner.v1.TypeCode.ARRAY:
333 5
    case 'ARRAY':
334 5
      decoded = decoded.map(value => {
335 5
        return decode(
336 5
          value,
337 5
          type.arrayElementType! as spannerClient.spanner.v1.Type
338 5
        );
339 5
      });
340 5
      break;
341 5
    case spannerClient.spanner.v1.TypeCode.STRUCT:
342 5
    case 'STRUCT':
343 5
      fields = type.structType!.fields!.map(({name, type}, index) => {
344 5
        const value = decode(
345 5
          decoded[name!] || decoded[index],
346 5
          type as spannerClient.spanner.v1.Type
347 5
        );
348 5
        return {name, value};
349 5
      });
350 5
      decoded = Struct.fromArray(fields as Field[]);
351 5
      break;
352 5
    default:
353 5
      break;
354 5
  }
355 5

356 5
  return decoded;
357 5
}
358 5

359 5
/**
360 5
 * Encode a value in the format the API expects.
361 5
 *
362 5
 * @private
363 5
 *
364 5
 * @param {*} value The value to be encoded.
365 5
 * @returns {object} google.protobuf.Value
366 5
 */
367 5
function encode(value: Value): p.IValue {
368 5
  return GrpcService.encodeValue_(encodeValue(value));
369 5
}
370 5

371 5
/**
372 5
 * Formats values into expected format of google.protobuf.Value. The actual
373 5
 * conversion to a google.protobuf.Value object happens via
374 5
 * `Service.encodeValue_`
375 5
 *
376 5
 * @private
377 5
 *
378 5
 * @param {*} value The value to be encoded.
379 5
 * @returns {*}
380 5
 */
381 5
function encodeValue(value: Value): Value {
382 5
  if (is.number(value) && !is.decimal(value)) {
383 5
    return value.toString();
384 5
  }
385 5

386 5
  if (is.date(value)) {
387 5
    return value.toJSON();
388 5
  }
389 5

390 5
  if (value instanceof WrappedNumber) {
391 5
    return value.value;
392 5
  }
393 5

394 5
  if (value instanceof Numeric) {
395 5
    return value.value;
396 5
  }
397 5

398 5
  if (Buffer.isBuffer(value)) {
399 5
    return value.toString('base64');
400 5
  }
401 5

402 5
  if (value instanceof Struct) {
403 5
    return Array.from(value).map(field => encodeValue(field.value));
404 5
  }
405 5

406 5
  if (is.array(value)) {
407 5
    return value.map(encodeValue);
408 5
  }
409 5

410 5
  return value;
411 5
}
412 5

413 5
/**
414 5
 * Just a map with friendlier names for the types.
415 5
 *
416 5
 * @private
417 5
 * @enum {string}
418 5
 */
419 5
const TypeCode: {
420 5
  [name: string]: keyof typeof spannerClient.spanner.v1.TypeCode;
421 5
} = {
422 5
  unspecified: 'TYPE_CODE_UNSPECIFIED',
423 5
  bool: 'BOOL',
424 5
  int64: 'INT64',
425 5
  float64: 'FLOAT64',
426 5
  numeric: 'NUMERIC',
427 5
  timestamp: 'TIMESTAMP',
428 5
  date: 'DATE',
429 5
  string: 'STRING',
430 5
  bytes: 'BYTES',
431 5
  array: 'ARRAY',
432 5
  struct: 'STRUCT',
433 5
};
434 5

435 5
/**
436 5
 * Conveniece Type object that simplifies specifying the data type, the array
437 5
 * child type and/or struct fields.
438 5
 *
439 5
 * @private
440 5
 */
441 5
export interface Type {
442 5
  type: string;
443 5
  fields?: FieldType[];
444 5
  child?: Type;
445 5
}
446 5

447 5
interface FieldType extends Type {
448 5
  name: string;
449 5
}
450 5

451 5
/**
452 5
 * @typedef {ParamType} StructField
453 5
 * @property {string} name The name of the field.
454 5
 */
455 5
/**
456 5
 * @typedef {object} ParamType
457 5
 * @property {string} type The param type. Must be one of the following:
458 5
 *     - float64
459 5
 *     - int64
460 5
 *     - numeric
461 5
 *     - bool
462 5
 *     - string
463 5
 *     - bytes
464 5
 *     - timestamp
465 5
 *     - date
466 5
 *     - struct
467 5
 *     - array
468 5
 * @property {StructField[]} [fields] **For struct types only**. Type
469 5
 *     definitions for the individual fields.
470 5
 * @property {string|ParamType} [child] **For array types only**. The array
471 5
 *     element type.
472 5
 */
473 5
/**
474 5
 * Get the corresponding Spanner data type for the provided value.
475 5
 *
476 5
 * @private
477 5
 *
478 5
 * @param {*} value - The value.
479 5
 * @returns {object}
480 5
 *
481 5
 * @example
482 5
 * codec.getType(NaN);
483 5
 * // {type: 'float64'}
484 5
 */
485 5
function getType(value: Value): Type {
486 5
  const isSpecialNumber =
487 5
    is.infinite(value) || (is.number(value) && isNaN(value));
488 5

489 5
  if (is.decimal(value) || isSpecialNumber || value instanceof Float) {
490 5
    return {type: 'float64'};
491 5
  }
492 5

493 5
  if (is.number(value) || value instanceof Int) {
494 5
    return {type: 'int64'};
495 5
  }
496 5

497 5
  if (value instanceof Numeric) {
498 5
    return {type: 'numeric'};
499 5
  }
500 5

501 5
  if (is.boolean(value)) {
502 5
    return {type: 'bool'};
503 5
  }
504 5

505 5
  if (is.string(value)) {
506 5
    return {type: 'string'};
507 5
  }
508 5

509 5
  if (Buffer.isBuffer(value)) {
510 5
    return {type: 'bytes'};
511 5
  }
512 5

513 5
  if (value instanceof SpannerDate) {
514 5
    return {type: 'date'};
515 5
  }
516 5

517 5
  if (is.date(value)) {
518 5
    return {type: 'timestamp'};
519 5
  }
520 5

521 5
  if (value instanceof Struct) {
522 5
    return {
523 5
      type: 'struct',
524 5
      fields: Array.from(value).map(({name, value}) => {
525 5
        return Object.assign({name}, getType(value));
526 5
      }),
527 5
    };
528 5
  }
529 5

530 5
  if (is.array(value)) {
531 5
    let child;
532 5

533 5
    for (let i = 0; i < value.length; i++) {
534 5
      child = value[i];
535 5

536 5
      if (!is.null(child)) {
537 5
        break;
538 5
      }
539 5
    }
540 5

541 5
    return {
542 5
      type: 'array',
543 5
      child: getType(child),
544 5
    };
545 5
  }
546 5

547 5
  return {type: 'unspecified'};
548 5
}
549 5

550 5
/**
551 5
 * Converts a value to google.protobuf.ListValue
552 5
 *
553 5
 * @private
554 5
 *
555 5
 * @param {*} value The value to convert.
556 5
 * @returns {object}
557 5
 */
558 5
function convertToListValue<T>(value: T): p.IListValue {
559 5
  const values = (arrify(value) as T[]).map(codec.encode);
560 5
  return {values};
561 5
}
562 5

563 5
/**
564 5
 * Converts milliseconds to google.protobuf.Timestamp
565 5
 *
566 5
 * @private
567 5
 *
568 5
 * @param {number} ms The milliseconds to convert.
569 5
 * @returns {object}
570 5
 */
571 5
function convertMsToProtoTimestamp(
572 5
  ms: number
573 5
): spannerClient.protobuf.ITimestamp {
574 5
  const rawSeconds = ms / 1000;
575 5
  const seconds = Math.floor(rawSeconds);
576 5
  const nanos = Math.round((rawSeconds - seconds) * 1e9);
577 5
  return {seconds, nanos};
578 5
}
579 5

580 5
/**
581 5
 * Converts google.protobuf.Timestamp to Date object.
582 5
 *
583 5
 * @private
584 5
 *
585 5
 * @param {object} timestamp The protobuf timestamp.
586 5
 * @returns {Date}
587 5
 */
588 5
function convertProtoTimestampToDate({
589 5
  nanos = 0,
590 5
  seconds = 0,
591 5
}: p.ITimestamp): Date {
592 5
  const ms = Math.floor(nanos) / 1e6;
593 5
  const s = Math.floor(seconds as number);
594 5
  return new Date(s * 1000 + ms);
595 5
}
596 5

597 5
/**
598 5
 * Encodes paramTypes into correct structure.
599 5
 *
600 5
 * @private
601 5
 *
602 5
 * @param {object|string} [config='unspecified'] Type config.
603 5
 * @return {object}
604 5
 */
605 5
function createTypeObject(
606 5
  friendlyType?: string | Type
607 5
): spannerClient.spanner.v1.Type {
608 5
  if (!friendlyType) {
609 5
    friendlyType = 'unspecified';
610 5
  }
611 5

612 5
  if (is.string(friendlyType)) {
613 5
    friendlyType = {type: friendlyType} as Type;
614 5
  }
615 5

616 5
  const config: Type = friendlyType as Type;
617 5
  const code: keyof typeof spannerClient.spanner.v1.TypeCode =
618 5
    TypeCode[config.type] || TypeCode.unspecified;
619 5
  const type: spannerClient.spanner.v1.Type = {
620 5
    code,
621 5
  } as spannerClient.spanner.v1.Type;
622 5

623 5
  if (code === 'ARRAY') {
624 5
    type.arrayElementType = codec.createTypeObject(config.child);
625 5
  }
626 5

627 5
  if (code === 'STRUCT') {
628 5
    type.structType = {
629 5
      fields: arrify(config.fields!).map(field => {
630 5
        return {name: field.name, type: codec.createTypeObject(field)};
631 5
      }),
632 5
    };
633 5
  }
634 5

635 5
  return type;
636 5
}
637 5

638 5
export const codec = {
639 5
  convertToListValue,
640 5
  convertMsToProtoTimestamp,
641 5
  convertProtoTimestampToDate,
642 5
  createTypeObject,
643 5
  SpannerDate,
644 5
  Float,
645 5
  Int,
646 5
  Numeric,
647 5
  convertFieldsToJson,
648 5
  decode,
649 5
  encode,
650 5
  getType,
651 5
  Struct,
652 5
};

Read our documentation on viewing source code .

Loading