// tslint:disable: variable-name import { Injectable, Inject, Logger } from '@nestjs/common'; import { NEST_GDATASTORE_OPTIONS } from './constants'; import { NestGdatastoreOptions, configService } from './interfaces'; import { Datastore, Query } from '@google-cloud/datastore'; import { Filter } from './interfaces/datastore.interface'; import { entity } from '@google-cloud/datastore/build/src/entity'; /** * Sample interface for NestGdatastoreService * * Customize this as needed to describe the NestGdatastoreService * */ interface INestGdatastoreService { datastore: Datastore; createEntity(entity: any, kind: string, keyName: string): void; readEntity( kind: string, filters: Filter[], latestEntity: { property: string }, ): Promise; replaceEntity(entity: any, kind: string, keyName: string | number): void; editEntity(id: string | number, kind: string, newValues: any[]): void; deleteEntity(kind: string, keyName: string | number): void; } @Injectable() export class NestGdatastoreService implements INestGdatastoreService, INestGdatastoreService { private readonly logger: Logger; constructor( @Inject('datastoreKeys') private config: NestGdatastoreOptions, ) { this.logger = new Logger('NestGdatastoreService'); this.logger.log(`Options: ${JSON.stringify(this.config)}`); } public datastore: Datastore = new Datastore({ projectId: this.config['PROJECT_ID'], keyFilename: this.config['DATASTOREKEY'], namespace: this.config['DATASTORE_NAMESPACE'], }); public async createEntity(entity: any, kind: string, keyName?: string) { let key = this.datastore.key([kind]); if (keyName) { key = this.datastore.key([kind, keyName]); } const insertionEntity = { key, data: entity }; const result = await this.datastore.save(insertionEntity); return result; } /** * @param {string} kind * @param {Filter[]} filters? NOTE: IF FILTERING ON KEY... ONLY USE NUMBER FOR ID * @param {{property:string}} latestEntity? */ public async readEntity( kind: string, filters?: Filter[], latestEntity?: { property: string }, orderBy?: { property: string, descending: boolean }, limit?: number ): Promise { let query = this.datastore.createQuery(kind); if (filters) { // tslint:disable-next-line:no-shadowed-variable filters.forEach(filter => { query = this.addFilter(query, filter, kind); }); } if (latestEntity) { query.order(latestEntity.property, { descending: true }); query.limit(1); }else{ if(orderBy){ query.order(orderBy.property, { descending: orderBy.descending }); } if(limit){ query.limit(limit); } } const result = await this.runSearchQuery(query); return result; } private sortObjects( objectList: T[], property: keyof T, limit?: number, ): T[] { let sortedList = objectList.sort((a: T, b: T) => { if (a[property] >= b[property]) { return -1; } return 1; }); if (limit && limit <= sortedList.length) { sortedList = sortedList.slice(0, limit); } return sortedList; } private addFilter(query: Query, specificFilter: Filter, kind: string) { if ( specificFilter.property === '__key__' && (typeof specificFilter.searchValue === 'string' || typeof specificFilter.searchValue === 'number') ) { specificFilter.searchValue = this.datastore.key([ kind, specificFilter.searchValue, ]); } return query.filter( specificFilter.property, specificFilter.comparator, specificFilter.searchValue, ); } private async runSearchQuery(query: Query): Promise { const [results] = await this.datastore.runQuery(query); return results.map((item: any) => { const key = this.datastore.KEY; const specificKey = item[key]; specificKey.id = specificKey.id ? parseInt(specificKey.id, 10) : undefined; return { ...item, meta: specificKey }; }); } public async massDeleteEntitiesInKind(kind: string) { const message = `Deleting all entities in testdb - ${kind} in 5 seconds, interreupt now or forever hold your peace`; await this.fiveSecondCounter(message, 5000); if (this.config['DATASTORE_NAMESPACE'] == 'datastore_test') { const entities = await this.readEntity(kind); const keyList = this.createKeyListFromEntities(kind, entities); await this.datastore.delete(keyList); } else { throw new Error( `Cannot delete ${kind}, Deleting entire kinds only allowed in test database!`, ); } } private createKeyListFromEntities( kind: string, entities: { meta: { id: number }; }[], ): Array { return entities.map(item => { return this.datastore.key([kind, item.meta.id]); }); } private async fiveSecondCounter(message: string, timer: number) { console.log(message); return new Promise(resolve => { let counter = timer; const interval = setInterval(() => { console.log(counter / 1000); counter = counter - 1000; if (counter < 0) { clearInterval(interval); console.log(`Deleting!`); resolve(); } }, 1000); }); } public async replaceEntity( entity: any, kind: string, keyName: string | number, ) { const transaction = this.datastore.transaction(); const key = this.datastore.key([kind, keyName]); try { await transaction.run(); transaction.save({ key, data: entity, }); const result = await transaction.commit(); return result; } catch (err) { transaction.rollback(); throw new Error(err); } } /** * @param {any} id id of the datastore entity * @param {string} kind Kind of the datastore entity * @param {any} newValues Top level key and nested values of the entity. * * eg. [{age:4}, geolocation:{lat:35,lng:65}] * * Given an id, a kind and a new set of values, can edit properties of an entity. * Capable of replacing top level keys with new values. * ie: {"geolocation":{"lat":3,"lng":3}, "age":3} * to replace lat, you will need to replace the whole geolocation object, and pass * in a new geolocation object as the third parameter. * * To replace age, you simply need to pass in {"age":5} as the third parameter. */ public async editEntity( id: string | number, kind: string, newValues: any[], ) { const oldEntity: T[] = await this.readEntity(kind, [ { property: '__key__', comparator: '=', searchValue: id, }, ]).catch(err => { throw new Error( `Failed to get entity of id: ${id}, kind: ${kind} because ${err}`, ); }); if (!oldEntity[0]) { throw new Error(`No Such entity with id ${id} in kind ${kind}`); } const parsedEntities = JSON.parse(JSON.stringify(oldEntity[0])); let newEntity = { ...parsedEntities }; newValues.forEach((newValue: { [k in keyof T]: any }) => { newEntity = { ...newEntity, ...newValue }; }); const transaction = this.datastore.transaction(); const key = this.datastore.key([kind, id]); try { await transaction.run(); transaction.save({ key, data: newEntity, }); await transaction.commit(); return newEntity; } catch (err) { transaction.rollback(); throw new Error(err); } } public async deleteEntity(kind: string, keyName: string | number) { const key = this.datastore.key([kind, keyName]); const result = await this.datastore.delete(key); return result; } }