From f3d1363d0dc7481a96738909eb69218595920f43 Mon Sep 17 00:00:00 2001 From: Marcin Lewandowski Date: Mon, 15 Mar 2021 09:55:28 +0100 Subject: [PATCH] RDBC-424 Investigate performance of RavenDb node js client --- src/Documents/Changes/DatabaseChanges.ts | 20 +++--- src/Documents/Commands/FacetQueryCommand.ts | 17 ++--- src/Documents/Commands/GetConflictsCommand.ts | 19 ++++-- src/Documents/Commands/QueryCommand.ts | 43 +++++++++--- .../Operations/Counters/CounterDetail.ts | 14 ++-- .../TimeSeries/GetTimeSeriesOperation.ts | 31 ++++++--- .../GetTimeSeriesStatisticsOperation.ts | 25 +++++-- .../Queries/Facets/AggregationQueryBase.ts | 6 +- .../InMemoryDocumentSessionOperations.ts | 8 +-- .../Session/Operations/BatchOperation.ts | 4 +- .../Session/Operations/QueryOperation.ts | 68 ++++++++++++------- .../Session/TimeSeries/TimeSeriesEntry.ts | 6 +- .../TimeSeries/TypedTimeSeriesEntry.ts | 2 +- src/Http/RavenCommand.ts | 8 +-- src/Http/RavenCommandResponsePipeline.ts | 8 +-- .../Json/Streams/TransformJsonKeysProfiles.ts | 9 ++- src/Mapping/ObjectMapper.ts | 2 +- .../GetCertificateMetadataOperation.ts | 17 +++-- .../Operations/GetDatabaseRecordOperation.ts | 18 +++-- src/Types/index.ts | 14 ++++ src/Utility/ObjectUtil.ts | 33 +++++++++ src/Utility/StreamUtil.ts | 8 ++- test/Ported/HttpsTest.ts | 2 + test/Ported/Issues/RavenDB_9587.ts | 6 ++ .../TimeSeries/TimeSeriesTypedSession.ts | 2 +- 25 files changed, 268 insertions(+), 122 deletions(-) diff --git a/src/Documents/Changes/DatabaseChanges.ts b/src/Documents/Changes/DatabaseChanges.ts index 34c86c007..441cddd7e 100644 --- a/src/Documents/Changes/DatabaseChanges.ts +++ b/src/Documents/Changes/DatabaseChanges.ts @@ -20,11 +20,10 @@ import CurrentIndexAndNode from "../../Http/CurrentIndexAndNode"; import { RequestExecutor } from "../../Http/RequestExecutor"; import { DocumentConventions } from "../Conventions/DocumentConventions"; import { ServerNode } from "../../Http/ServerNode"; -import { ObjectTypeDescriptor } from "../../Types"; +import { ObjectTypeDescriptor, ServerResponse } from "../../Types"; import { UpdateTopologyParameters } from "../../Http/UpdateTopologyParameters"; import { TypeUtil } from "../../Utility/TypeUtil"; import { TimeSeriesChange } from "./TimeSeriesChange"; -import { QueryResult } from "../Queries/QueryResult"; export class DatabaseChanges implements IDatabaseChanges { @@ -436,13 +435,16 @@ export class DatabaseChanges implements IDatabaseChanges { const value = message.Value; let transformedValue = ObjectUtil.transformObjectKeys(value, { defaultTransform: "camel" }); if (type === "TimeSeriesChange") { - transformedValue = this._conventions.objectMapper - .fromObjectLiteral(transformedValue, { - nestedTypes: { - from: "date", - to: "date" - } - }); + const dateUtil = this._conventions.dateUtil; + + const timeSeriesValue = transformedValue as ServerResponse; + + const overrides: Partial = { + from: dateUtil.parse(timeSeriesValue.from), + to: dateUtil.parse(timeSeriesValue.to) + }; + + transformedValue = Object.assign(transformedValue, overrides); } this._notifySubscribers(type, transformedValue, Array.from(this._counters.values())); break; diff --git a/src/Documents/Commands/FacetQueryCommand.ts b/src/Documents/Commands/FacetQueryCommand.ts index 4f9aeacf6..f00b135d3 100644 --- a/src/Documents/Commands/FacetQueryCommand.ts +++ b/src/Documents/Commands/FacetQueryCommand.ts @@ -3,6 +3,7 @@ import { DocumentConventions } from "../Conventions/DocumentConventions"; import * as stream from "readable-stream"; import { QueryCommand } from "./QueryCommand"; import { RavenCommandResponsePipeline } from "../../Http/RavenCommandResponsePipeline"; +import { ServerResponse } from "../../Types"; export class FacetQueryCommand extends QueryCommand { @@ -25,18 +26,18 @@ export class FacetQueryCommand extends QueryCommand { fromCache: boolean, bodyCallback?: (body: string) => void): Promise { - const rawResult = await RavenCommandResponsePipeline.create() + const rawResult = await RavenCommandResponsePipeline.create>() .collectBody(bodyCallback) .parseJsonAsync() .jsonKeysTransform("FacetQuery") .process(bodyStream); - const queryResult = conventions.objectMapper.fromObjectLiteral(rawResult, { - typeName: QueryResult.name, - nestedTypes: { - indexTimestamp: "date", - lastQueryTime: "date" - } - }, new Map([[QueryResult.name, QueryResult]])); + + const overrides: Partial = { + indexTimestamp: conventions.dateUtil.parse(rawResult.indexTimestamp), + lastQueryTime: conventions.dateUtil.parse(rawResult.lastQueryTime) + }; + + const queryResult = Object.assign(new QueryResult(), rawResult, overrides) as QueryResult; if (fromCache) { queryResult.durationInMs = -1; diff --git a/src/Documents/Commands/GetConflictsCommand.ts b/src/Documents/Commands/GetConflictsCommand.ts index 59cb45130..3b1e696d8 100644 --- a/src/Documents/Commands/GetConflictsCommand.ts +++ b/src/Documents/Commands/GetConflictsCommand.ts @@ -4,6 +4,7 @@ import { GetConflictsResult } from "./GetConflictsResult"; import { ServerNode } from "../../Http/ServerNode"; import * as stream from "readable-stream"; import { DocumentConventions } from "../Conventions/DocumentConventions"; +import { ServerResponse } from "../../Types"; export class GetConflictsCommand extends RavenCommand { @@ -35,12 +36,18 @@ export class GetConflictsCommand extends RavenCommand { } let body: string = null; - const results = await this._defaultPipeline(_ => body = _).process(bodyStream); - this.result = this._conventions.objectMapper.fromObjectLiteral(results, { - nestedTypes: { - "results[].lastModified": "date" - } - }); + const payload = await this._defaultPipeline>(_ => body = _).process(bodyStream); + const dateUtil = this._conventions.dateUtil; + + const { results, ...otherProps } = payload; + + this.result = { + ...otherProps, + results: results.map(r => ({ + ...r, + lastModified: dateUtil.parse(r.lastModified) + })) + }; return body; } diff --git a/src/Documents/Commands/QueryCommand.ts b/src/Documents/Commands/QueryCommand.ts index ed6f03082..3fcdc3edd 100644 --- a/src/Documents/Commands/QueryCommand.ts +++ b/src/Documents/Commands/QueryCommand.ts @@ -9,6 +9,9 @@ import { JsonSerializer } from "../../Mapping/Json/Serializer"; import * as stream from "readable-stream"; import { RavenCommandResponsePipeline } from "../../Http/RavenCommandResponsePipeline"; import { StringBuilder } from "../../Utility/StringBuilder"; +import { ServerResponse } from "../../Types"; +import { QueryTimings } from "../Queries/Timings/QueryTimings"; +import { StringUtil } from "../../Utility/StringUtil"; export interface QueryCommandOptions { metadataOnly?: boolean; @@ -102,19 +105,13 @@ export class QueryCommand extends RavenCommand { fromCache: boolean, bodyCallback?: (body: string) => void): Promise { - const rawResult = await RavenCommandResponsePipeline.create() + const rawResult = await RavenCommandResponsePipeline.create>() .collectBody(bodyCallback) .parseJsonAsync() .jsonKeysTransform("DocumentQuery", conventions) .process(bodyStream); - const queryResult = conventions.objectMapper - .fromObjectLiteral(rawResult, { - typeName: QueryResult.name, - nestedTypes: { - indexTimestamp: "date", - lastQueryTime: "date" - } - }, new Map([[QueryResult.name, QueryResult]])); + + const queryResult = QueryCommand._mapToLocalObject(rawResult, conventions); if (fromCache) { queryResult.durationInMs = -1; @@ -127,4 +124,32 @@ export class QueryCommand extends RavenCommand { return queryResult; } + + private static _mapToLocalObject(json: ServerResponse, conventions: DocumentConventions): QueryResult { + const { indexTimestamp, lastQueryTime, timings, ...otherProps } = json; + + const overrides: Partial = { + indexTimestamp: conventions.dateUtil.parse(indexTimestamp), + lastQueryTime: conventions.dateUtil.parse(lastQueryTime), + timings: QueryCommand._mapTimingsToLocalObject(timings) + }; + + return Object.assign(new QueryResult(), otherProps, overrides); + } + + private static _mapTimingsToLocalObject(timings: ServerResponse) { + if (!timings) { + return undefined; + } + + const mapped = new QueryTimings(); + mapped.durationInMs = timings.durationInMs; + mapped.timings = timings.timings ? {} : undefined; + if (timings.timings) { + Object.keys(timings.timings).forEach(time => { + mapped.timings[StringUtil.uncapitalize(time)] = QueryCommand._mapTimingsToLocalObject(timings.timings[time]); + }); + } + return mapped; + } } diff --git a/src/Documents/Operations/Counters/CounterDetail.ts b/src/Documents/Operations/Counters/CounterDetail.ts index 63c66e520..875fa6825 100644 --- a/src/Documents/Operations/Counters/CounterDetail.ts +++ b/src/Documents/Operations/Counters/CounterDetail.ts @@ -1,8 +1,8 @@ -export class CounterDetail { - public documentId: string; - public counterName: string; - public totalValue: number; - public etag: number; - public counterValues: { [key: string]: number }; - public changeVector: string; +export interface CounterDetail { + documentId: string; + counterName: string; + totalValue: number; + etag?: number; + counterValues?: { [key: string]: number }; + changeVector?: string; } diff --git a/src/Documents/Operations/TimeSeries/GetTimeSeriesOperation.ts b/src/Documents/Operations/TimeSeries/GetTimeSeriesOperation.ts index 4b30439ea..8651e647f 100644 --- a/src/Documents/Operations/TimeSeries/GetTimeSeriesOperation.ts +++ b/src/Documents/Operations/TimeSeries/GetTimeSeriesOperation.ts @@ -13,6 +13,7 @@ import { DocumentConventions } from "../../Conventions/DocumentConventions"; import { RavenCommand } from "../../../Http/RavenCommand"; import { ServerNode } from "../../../Http/ServerNode"; import { StringBuilder } from "../../../Utility/StringBuilder"; +import { ServerResponse } from "../../../Types"; export class GetTimeSeriesOperation implements IOperation { private readonly _docId: string; @@ -125,7 +126,7 @@ class GetTimeSeriesCommand extends RavenCommand { } let body: string = null; - const results = await this._defaultPipeline(_ => body = _) + const results = await this._defaultPipeline>(_ => body = _) .process(bodyStream); this.result = reviveTimeSeriesRangeResult(results, this._conventions); @@ -138,13 +139,25 @@ class GetTimeSeriesCommand extends RavenCommand { } } -export function reviveTimeSeriesRangeResult(result: TimeSeriesRangeResult, conventions: DocumentConventions) { - return Object.assign(new TimeSeriesRangeResult(), conventions.objectMapper.fromObjectLiteral(result, { - nestedTypes: { - "from": "date", - "to": "date", - "entries": "TimeSeriesEntry", - "entries[].timestamp": "date" +export function reviveTimeSeriesRangeResult(result: ServerResponse, conventions: DocumentConventions): TimeSeriesRangeResult { + const { entries, from, to, ...restProps } = result; + + const entryMapper = (rawEntry: ServerResponse) => { + const result = new TimeSeriesEntry(); + + const entryOverrides: Partial = { + timestamp: conventions.dateUtil.parse(rawEntry.timestamp) } - }, new Map([[TimeSeriesEntry.name, TimeSeriesEntry]]))); + + return Object.assign(result, rawEntry, entryOverrides) as TimeSeriesEntry; + } + + const overrides: Partial = { + ...restProps, + to: conventions.dateUtil.parse(result.to), + from: conventions.dateUtil.parse(result.from), + entries: entries.map(entryMapper), + } + + return Object.assign(new TimeSeriesRangeResult(), overrides); } \ No newline at end of file diff --git a/src/Documents/Operations/TimeSeries/GetTimeSeriesStatisticsOperation.ts b/src/Documents/Operations/TimeSeries/GetTimeSeriesStatisticsOperation.ts index 1c161bce2..e66b53e74 100644 --- a/src/Documents/Operations/TimeSeries/GetTimeSeriesStatisticsOperation.ts +++ b/src/Documents/Operations/TimeSeries/GetTimeSeriesStatisticsOperation.ts @@ -7,6 +7,7 @@ import * as stream from "readable-stream"; import { RavenCommand } from "../../../Http/RavenCommand"; import { ServerNode } from "../../../Http/ServerNode"; import { DocumentConventions } from "../../Conventions/DocumentConventions"; +import { ServerResponse } from "../../../Types"; export class GetTimeSeriesStatisticsOperation implements IOperation { private readonly _documentId: string; @@ -54,13 +55,23 @@ class GetTimeSeriesStatisticsCommand extends RavenCommand async setResponseAsync(bodyStream: stream.Stream, fromCache: boolean): Promise { let body: string = null; - const results = await this._defaultPipeline(_ => body = _).process(bodyStream); - this.result = this._conventions.objectMapper.fromObjectLiteral(results, { - nestedTypes: { - "timeSeries[].startDate": "date", - "timeSeries[].endDate": "date" - } - }); + const results = await this._defaultPipeline>(_ => body = _).process(bodyStream); + + const { timeSeries, ...restProps } = results; + + const dateUtil = this._conventions.dateUtil; + + this.result = { + ...restProps, + timeSeries: timeSeries.map(t => { + const { startDate, endDate } = t; + return { + ...t, + startDate: dateUtil.parse(startDate), + endDate: dateUtil.parse(endDate) + } + }) + } return body; } diff --git a/src/Documents/Queries/Facets/AggregationQueryBase.ts b/src/Documents/Queries/Facets/AggregationQueryBase.ts index 0368e5afd..64a42ca8b 100644 --- a/src/Documents/Queries/Facets/AggregationQueryBase.ts +++ b/src/Documents/Queries/Facets/AggregationQueryBase.ts @@ -15,9 +15,6 @@ export interface FacetResultObject { [key: string]: FacetResult; } -const FACET_RESULT_TYPE_INFO = { typeName: FacetResult.name }; -const FACET_RESULT_TYPES_MAP = new Map([[FacetResult.name, FacetResult]]); - export abstract class AggregationQueryBase { private readonly _session: InMemoryDocumentSessionOperations; @@ -60,8 +57,7 @@ export abstract class AggregationQueryBase { const results: FacetResultObject = {}; const mapper = new TypesAwareObjectMapper(); for (const result of queryResult.results) { - const facetResult = mapper.fromObjectLiteral( - result, FACET_RESULT_TYPE_INFO, FACET_RESULT_TYPES_MAP); + const facetResult = Object.assign(new FacetResult(), result); results[facetResult.name] = facetResult; } diff --git a/src/Documents/Session/InMemoryDocumentSessionOperations.ts b/src/Documents/Session/InMemoryDocumentSessionOperations.ts index d0f4ea886..89473d4e0 100644 --- a/src/Documents/Session/InMemoryDocumentSessionOperations.ts +++ b/src/Documents/Session/InMemoryDocumentSessionOperations.ts @@ -2,7 +2,7 @@ import { EntityToJson } from "./EntityToJson"; import { IDisposable } from "../../Types/Contracts"; import { SessionInfo, ConcurrencyCheckMode, StoreOptions } from "./IDocumentSession"; import { IMetadataDictionary } from "./IMetadataDictionary"; -import { ObjectTypeDescriptor, ClassConstructor } from "../../Types"; +import { ObjectTypeDescriptor, ClassConstructor, ServerResponse } from "../../Types"; import { SessionEventsEmitter, SessionBeforeStoreEventArgs, @@ -835,7 +835,7 @@ export abstract class InMemoryDocumentSessionOperations } } - public registerTimeSeries(resultTimeSeries: Record>) { + public registerTimeSeries(resultTimeSeries: Record[]>>) { if (this.noTracking || !resultTimeSeries) { return; } @@ -1110,7 +1110,7 @@ export abstract class InMemoryDocumentSessionOperations ranges.splice(fromRangeIndex + 1, toRangeIndex - fromRangeIndex); } - private static _parseTimeSeriesRangeResult(json: TimeSeriesRangeResult, + private static _parseTimeSeriesRangeResult(json: ServerResponse, id: string, databaseName: string, conventions: DocumentConventions): TimeSeriesRangeResult { @@ -1706,7 +1706,7 @@ export abstract class InMemoryDocumentSessionOperations dirty = true; } - documentInfo.metadata[prop] = ObjectUtil.clone(documentInfo.metadataInstance[prop]); + documentInfo.metadata[prop] = ObjectUtil.deepJsonClone(documentInfo.metadataInstance[prop]); } } diff --git a/src/Documents/Session/Operations/BatchOperation.ts b/src/Documents/Session/Operations/BatchOperation.ts index 760185106..f51f92565 100644 --- a/src/Documents/Session/Operations/BatchOperation.ts +++ b/src/Documents/Session/Operations/BatchOperation.ts @@ -169,11 +169,11 @@ export class BatchOperation { private _applyMetadataModifications(id: string, documentInfo: DocumentInfo): void { documentInfo.metadataInstance = null; - documentInfo.metadata = ObjectUtil.clone(documentInfo.metadata); + documentInfo.metadata = ObjectUtil.deepLiteralClone(documentInfo.metadata); documentInfo.metadata["@change-vector"] = documentInfo.changeVector; - const documentCopy = ObjectUtil.clone(documentInfo.document); + const documentCopy = ObjectUtil.deepLiteralClone(documentInfo.document); documentCopy[CONSTANTS.Documents.Metadata.KEY] = documentInfo.metadata; documentInfo.document = documentCopy; diff --git a/src/Documents/Session/Operations/QueryOperation.ts b/src/Documents/Session/Operations/QueryOperation.ts index 51fee0462..7c1426814 100644 --- a/src/Documents/Session/Operations/QueryOperation.ts +++ b/src/Documents/Session/Operations/QueryOperation.ts @@ -19,8 +19,8 @@ import { TimeSeriesRawResult } from "../../Queries/TimeSeries/TimeSeriesRawResul import { TimeSeriesRangeAggregation } from "../../Queries/TimeSeries/TimeSeriesRangeAggregation"; import { ObjectUtil } from "../../../Utility/ObjectUtil"; import { TimeSeriesEntry } from "../TimeSeries/TimeSeriesEntry"; -import { TypesAwareObjectMapper } from "../../../Mapping/ObjectMapper"; import { StringBuilder } from "../../../Utility/StringBuilder"; +import { DocumentConventions } from "../../Conventions/DocumentConventions"; const log = getLogger({ module: "QueryOperation" }); @@ -258,9 +258,9 @@ export class QueryOperation { const mapper = conventions.objectMapper; if (result instanceof TimeSeriesAggregationResult) { - Object.assign(result, QueryOperation._reviveTimeSeriesAggregationResult(raw, mapper)); + Object.assign(result, QueryOperation._reviveTimeSeriesAggregationResult(raw, conventions)); } else if (result instanceof TimeSeriesRawResult) { - Object.assign(result, QueryOperation._reviveTimeSeriesRawResult(raw, mapper)); + Object.assign(result, QueryOperation._reviveTimeSeriesRawResult(raw, conventions)); } else { if (fieldsToFetch && fieldsToFetch.projections) { const keys = conventions.entityFieldNameConvention @@ -291,9 +291,9 @@ export class QueryOperation { if (value) { const newValue = QueryOperation._detectTimeSeriesResultType(value); if (newValue instanceof TimeSeriesAggregationResult) { - Object.assign(newValue, QueryOperation._reviveTimeSeriesAggregationResult(value, mapper)); + Object.assign(newValue, QueryOperation._reviveTimeSeriesAggregationResult(value, conventions)); } else if (newValue instanceof TimeSeriesRawResult) { - Object.assign(newValue, QueryOperation._reviveTimeSeriesRawResult(value, mapper)); + Object.assign(newValue, QueryOperation._reviveTimeSeriesRawResult(value, conventions)); } result[timeSeriesField] = newValue; @@ -315,27 +315,47 @@ export class QueryOperation { return new TimeSeriesRawResult(); } - private static _reviveTimeSeriesAggregationResult(raw: object, mapper: TypesAwareObjectMapper) { - const rawLower = ObjectUtil.transformObjectKeys(raw, { defaultTransform: "camel" }); - return mapper.fromObjectLiteral(rawLower, { - typeName: "object", - nestedTypes: { - "results[]": "TimeSeriesRangeAggregation", - "results[].from": "date", - "results[].to": "date" - } - }, new Map([[TimeSeriesRangeAggregation.name, TimeSeriesRangeAggregation]])); + private static _reviveTimeSeriesAggregationResult(raw: object, conventions: DocumentConventions) { + const rawLower = ObjectUtil.transformObjectKeys(raw, { defaultTransform: "camel" }) as any; + + const { results, ...otherProps } = rawLower; + + const mappedResults: TimeSeriesRangeAggregation[] = results.map(r => { + const { from, to, ...otherRangeProps } = r; + + const overrides: Partial = { + from: conventions.dateUtil.parse(from), + to: conventions.dateUtil.parse(to) + }; + + return Object.assign(new TimeSeriesRangeAggregation(), otherRangeProps, overrides); + }); + + return { + ...otherProps, + results: mappedResults + } } - private static _reviveTimeSeriesRawResult(raw: object, mapper: TypesAwareObjectMapper) { - const rawLower = ObjectUtil.transformObjectKeys(raw, { defaultTransform: "camel" }); - return mapper.fromObjectLiteral(rawLower, { - typeName: "object", - nestedTypes: { - "results[]": "TimeSeriesEntry", - "results[].timestamp": "date" - } - }, new Map([[TimeSeriesEntry.name, TimeSeriesEntry]])); + private static _reviveTimeSeriesRawResult(raw: object, conventions: DocumentConventions) { + const rawLower = ObjectUtil.transformObjectKeys(raw, { defaultTransform: "camel" }) as any; + + const { results, ...otherProps } = rawLower; + + const mappedResults: TimeSeriesRangeAggregation[] = results.map(r => { + const { timestamp, ...otherRangeProps } = r; + + const overrides: Partial = { + timestamp: conventions.dateUtil.parse(timestamp), + }; + + return Object.assign(new TimeSeriesEntry(), otherRangeProps, overrides); + }); + + return { + ...otherProps, + results: mappedResults + } } public get noTracking() { diff --git a/src/Documents/Session/TimeSeries/TimeSeriesEntry.ts b/src/Documents/Session/TimeSeries/TimeSeriesEntry.ts index 1158a2007..3ab003b63 100644 --- a/src/Documents/Session/TimeSeries/TimeSeriesEntry.ts +++ b/src/Documents/Session/TimeSeries/TimeSeriesEntry.ts @@ -7,7 +7,7 @@ export class TimeSeriesEntry { public timestamp: Date; public tag: string; public values: number[]; - public rollup: boolean; + public isRollup: boolean; public get value(): number { if (this.values.length === 1) { @@ -28,11 +28,11 @@ export class TimeSeriesEntry { public asTypedEntry(clazz: ClassConstructor) { const entry = new TypedTimeSeriesEntry(); - entry.rollup = this.rollup; + entry.isRollup = this.isRollup; entry.tag = this.tag; entry.timestamp = this.timestamp; entry.values = this.values; - entry.value = TimeSeriesValuesHelper.setFields(clazz, this.values, this.rollup); + entry.value = TimeSeriesValuesHelper.setFields(clazz, this.values, this.isRollup); return entry; } } \ No newline at end of file diff --git a/src/Documents/Session/TimeSeries/TypedTimeSeriesEntry.ts b/src/Documents/Session/TimeSeries/TypedTimeSeriesEntry.ts index 0166ddb64..4d486e48d 100644 --- a/src/Documents/Session/TimeSeries/TypedTimeSeriesEntry.ts +++ b/src/Documents/Session/TimeSeries/TypedTimeSeriesEntry.ts @@ -3,6 +3,6 @@ export class TypedTimeSeriesEntry { public timestamp: Date; public tag: string; public values: number[]; - public rollup: boolean; + public isRollup: boolean; public value: T; } diff --git a/src/Http/RavenCommand.ts b/src/Http/RavenCommand.ts index fe21b8716..76827ed4d 100644 --- a/src/Http/RavenCommand.ts +++ b/src/Http/RavenCommand.ts @@ -13,7 +13,7 @@ import { JsonSerializer } from "../Mapping/Json/Serializer"; import { RavenCommandResponsePipeline } from "./RavenCommandResponsePipeline"; import { DocumentConventions } from "../Documents/Conventions/DocumentConventions"; import * as http from "http"; -import { ObjectTypeDescriptor } from "../Types"; +import { ObjectTypeDescriptor, ServerResponse } from "../Types"; const log = getLogger({ module: "RavenCommand" }); @@ -94,9 +94,9 @@ export abstract class RavenCommand { await this.setResponseAsync(readable, true); } - protected _defaultPipeline( - bodyCallback?: (body: string) => void): RavenCommandResponsePipeline { - return this._pipeline() + protected _defaultPipeline( + bodyCallback?: (body: string) => void): RavenCommandResponsePipeline { + return this._pipeline() .parseJsonSync() .collectBody(bodyCallback) .objectKeysTransform("camel"); diff --git a/src/Http/RavenCommandResponsePipeline.ts b/src/Http/RavenCommandResponsePipeline.ts index 851f1ec4f..9af9766c0 100644 --- a/src/Http/RavenCommandResponsePipeline.ts +++ b/src/Http/RavenCommandResponsePipeline.ts @@ -172,19 +172,19 @@ export class RavenCommandResponsePipeline extends EventEmitter { streams.push(...opts.jsonAsync.filters); } } else if (opts.jsonSync) { - let json = ""; + const jsonChunks = []; const parseJsonSyncTransform = new stream.Transform({ readableObjectMode: true, transform(chunk, enc, callback) { - json += chunk.toString("utf8"); + jsonChunks.push(chunk.toString("utf8")); callback(); }, flush(callback) { try { - callback(null, JSON.parse(json)); + callback(null, JSON.parse(jsonChunks.join(""))); } catch (err) { callback( - getError("InvalidOperationException", `Error parsing response: '${json}'.`, err)); + getError("InvalidOperationException", `Error parsing response: '${jsonChunks.join("")}'.`, err)); } } }); diff --git a/src/Mapping/Json/Streams/TransformJsonKeysProfiles.ts b/src/Mapping/Json/Streams/TransformJsonKeysProfiles.ts index 054104419..8c17f6f98 100644 --- a/src/Mapping/Json/Streams/TransformJsonKeysProfiles.ts +++ b/src/Mapping/Json/Streams/TransformJsonKeysProfiles.ts @@ -1,7 +1,6 @@ import { CasingConvention } from "../../../Utility/ObjectUtil"; import { DocumentConventions } from "../../../Documents/Conventions/DocumentConventions"; import { throwError } from "../../../Exceptions"; -import { CONSTANTS } from "../../../Constants"; export type TransformJsonKeysProfile = "CommandResponsePayload" @@ -24,7 +23,7 @@ function getSimpleKeysTransform(convention: CasingConvention) { } export function getTransformJsonKeysProfile( - profile: TransformJsonKeysProfile, conventions?: DocumentConventions) { + profile: TransformJsonKeysProfile, conventions?: DocumentConventions): { getCurrentTransform: (key: any, stack: any) => CasingConvention } { if (profile === "CommandResponsePayload") { return getSimpleKeysTransform("camel"); } @@ -116,7 +115,7 @@ export function getTransformJsonKeysProfile( throwError("NotSupportedException", `Invalid profile name ${profile}`); } -function facetQueryGetTransform(key, stack) { +function facetQueryGetTransform(key, stack): CasingConvention { const len = stack.length; @@ -382,7 +381,7 @@ function buildEntityKeysTransformForDocumentLoad(entityCasingConvention) { }; } -function buildEntityKeysTransformForDocumentQuery(entityCasingConvention) { +function buildEntityKeysTransformForDocumentQuery(entityCasingConvention: CasingConvention) { return function entityKeysTransform(key, stack) { const len = stack.length; @@ -473,7 +472,7 @@ function buildEntityKeysTransformForDocumentQuery(entityCasingConvention) { }; } -function handleMetadataJsonKeys(key: string, stack: string[], stackLength: number, metadataKeyLevel: number) { +function handleMetadataJsonKeys(key: string, stack: string[], stackLength: number, metadataKeyLevel: number): CasingConvention { if (stackLength === metadataKeyLevel) { return null; // don't touch @metadata key } diff --git a/src/Mapping/ObjectMapper.ts b/src/Mapping/ObjectMapper.ts index 584a113eb..b30162838 100644 --- a/src/Mapping/ObjectMapper.ts +++ b/src/Mapping/ObjectMapper.ts @@ -56,7 +56,7 @@ export class TypesAwareObjectMapper implements ITypesAwareObjectMapper { public fromObjectLiteral( rawResult: object, typeInfo?: TypeInfo, knownTypes?: Map): TResult { - rawResult = ObjectUtil.clone(rawResult); + rawResult = ObjectUtil.deepLiteralClone(rawResult); const typeName = typeInfo ? typeInfo.typeName : null; const nestedTypes = typeInfo ? typeInfo.nestedTypes : null; const types = knownTypes || this._conventions.knownEntityTypesByName; diff --git a/src/ServerWide/Operations/Certificates/GetCertificateMetadataOperation.ts b/src/ServerWide/Operations/Certificates/GetCertificateMetadataOperation.ts index 5a8a04a25..f729682b0 100644 --- a/src/ServerWide/Operations/Certificates/GetCertificateMetadataOperation.ts +++ b/src/ServerWide/Operations/Certificates/GetCertificateMetadataOperation.ts @@ -6,6 +6,7 @@ import { IServerOperation, OperationResultType } from "../../../Documents/Operat import { DocumentConventions } from "../../../Documents/Conventions/DocumentConventions"; import { RavenCommand } from "../../../Http/RavenCommand"; import { ServerNode } from "../../../Http/ServerNode"; +import { ServerResponse } from "../../../Types"; export class GetCertificateMetadataOperation implements IServerOperation { private readonly _thumbprint: string; @@ -59,12 +60,18 @@ class GetCertificateMetadataCommand extends RavenCommand { } let body: string = null; - const results = await this._defaultPipeline(_ => body = _).process(bodyStream); - const resultsMapped = this._conventions.objectMapper.fromObjectLiteral<{ results: CertificateMetadata[] }>(results, { - nestedTypes: { - "results[].notAfter": "date" + const response = await this._defaultPipeline>(_ => body = _).process(bodyStream); + + const dateUtil = this._conventions.dateUtil; + + const resultsMapped: CertificateMetadata[] = response.results.map(cert => { + const { notAfter } = cert; + + return { + ...cert, + notAfter: dateUtil.parse(notAfter) } - }).results; + }) if (resultsMapped.length !== 1) { this._throwInvalidResponse(); diff --git a/src/ServerWide/Operations/GetDatabaseRecordOperation.ts b/src/ServerWide/Operations/GetDatabaseRecordOperation.ts index c7616cb51..07450e6c3 100644 --- a/src/ServerWide/Operations/GetDatabaseRecordOperation.ts +++ b/src/ServerWide/Operations/GetDatabaseRecordOperation.ts @@ -1,11 +1,12 @@ import { HttpRequestParameters } from "../../Primitives/Http"; import * as stream from "readable-stream"; import { IServerOperation, OperationResultType } from "../../Documents/Operations/OperationAbstractions"; -import { DatabaseRecordWithEtag } from ".."; +import { DatabaseRecordWithEtag, IndexHistoryEntry } from ".."; import { DocumentConventions } from "../../Documents/Conventions/DocumentConventions"; import { RavenCommand } from "../../Http/RavenCommand"; import { ServerNode } from "../../Http/ServerNode"; import { TimeSeriesConfiguration } from "../../Documents/Operations/TimeSeries/TimeSeriesConfiguration"; +import { ServerResponse } from "../../Types"; export class GetDatabaseRecordOperation implements IServerOperation { private readonly _database: string; @@ -63,16 +64,21 @@ export class GetDatabaseRecordCommand extends RavenCommand this._conventions.objectMapper.fromObjectLiteral(item, { - nestedTypes: { - createdAt: "date" - } - })); + history[indexName] = indexHistory.map(item => { + const { createdAt, ...otherHistoryProps } = item as unknown as ServerResponse; + + return { + ...otherHistoryProps, + createdAt: dateUtil.parse(createdAt) + } as IndexHistoryEntry; + }); } } diff --git a/src/Types/index.ts b/src/Types/index.ts index 9d2c87cea..c7e8f04ba 100644 --- a/src/Types/index.ts +++ b/src/Types/index.ts @@ -73,3 +73,17 @@ export abstract class PropsBasedObjectLiteralDescriptor } export type Field = keyof T & string | string; + +export type ServerResponse = T extends Date|string ? T : { + [K in keyof T]: T[K] extends Date + ? string + : ServerResponse; +} + +export type ServerCasing = T extends string | number ? T : { + [K in keyof T & string as `${Capitalize}`]: T[K] extends Array + ? R extends string ? R[] : ServerCasing[] + : T[K] extends object + ? ServerCasing + : T[K]; +} diff --git a/src/Utility/ObjectUtil.ts b/src/Utility/ObjectUtil.ts index 95ffd874e..52adb0c50 100644 --- a/src/Utility/ObjectUtil.ts +++ b/src/Utility/ObjectUtil.ts @@ -1,4 +1,5 @@ import * as changeCase from "change-case"; +import { TypeUtil } from "./TypeUtil"; function iden(x, locale) { return x; @@ -8,10 +9,42 @@ export class ObjectUtil { // WARNING: some methods are assigned below dynamically + /** + * @deprecated Use deepJsonClone or deepLiteralClone for better performance + * @param o Object to clone + */ public static clone(o) { return JSON.parse(JSON.stringify(o)); } + public static deepJsonClone(o) { + return JSON.parse(JSON.stringify(o)); + } + + public static deepLiteralClone(item) { + if (!item) { + return item; + } + + let result; + + if (Array.isArray(item)) { + result = []; + for (let index = 0; index < item.length; index++) { + result[index] = ObjectUtil.deepLiteralClone(item[index]); + } + } else if (TypeUtil.isObject(item)) { + result = {}; + for (const prop in item) { + result[prop] = ObjectUtil.deepLiteralClone(item[prop]); + } + } else { + result = item; + } + + return result; + } + public static mapToLiteral(input: Map): { [key: string]: TValue }; public static mapToLiteral( input: Map, diff --git a/src/Utility/StreamUtil.ts b/src/Utility/StreamUtil.ts index d4d904db6..19ca10eec 100644 --- a/src/Utility/StreamUtil.ts +++ b/src/Utility/StreamUtil.ts @@ -28,8 +28,12 @@ export async function readToBuffer(stream: stream.Stream): Promise { return Buffer.concat(chunks); } -export function readToEnd(readable: stream.Readable): Promise { - return reduceStreamToPromise(readable, (result, chunk) => result + chunk, ""); +export async function readToEnd(readable: stream.Readable | stream.Stream): Promise { + const chunks = []; + readable.on("data", chunk => chunks.push(chunk)); + + await finishedAsync(readable); + return chunks.join(""); } export function bufferToReadable(b: Buffer) { diff --git a/test/Ported/HttpsTest.ts b/test/Ported/HttpsTest.ts index dfc63f241..dfd501e60 100644 --- a/test/Ported/HttpsTest.ts +++ b/test/Ported/HttpsTest.ts @@ -166,6 +166,8 @@ describe("HttpsTest", function () { .isNotNull(); assertThat(certificateMetadata.securityClearance) .isEqualTo("ValidUser"); + assertThat(certificateMetadata.notAfter instanceof Date) + .isTrue(); const certificatesMetadata = await store.maintenance.server.send( new GetCertificatesMetadataOperation(certificateMetadata.name)); diff --git a/test/Ported/Issues/RavenDB_9587.ts b/test/Ported/Issues/RavenDB_9587.ts index 807d219a4..4c2149294 100644 --- a/test/Ported/Issues/RavenDB_9587.ts +++ b/test/Ported/Issues/RavenDB_9587.ts @@ -38,6 +38,12 @@ describe("RavenDB-9587", function () { .isGreaterThan(0); assertThat(timings.timings) .isNotNull(); + assertThat(timings instanceof QueryTimings) + .isTrue(); + Object.keys(timings.timings).forEach(key => { + assertThat(timings.timings[key] instanceof QueryTimings) + .isTrue(); + }); } }); diff --git a/test/Ported/TimeSeries/TimeSeriesTypedSession.ts b/test/Ported/TimeSeries/TimeSeriesTypedSession.ts index 2a8be6bfe..1b8516492 100644 --- a/test/Ported/TimeSeries/TimeSeriesTypedSession.ts +++ b/test/Ported/TimeSeries/TimeSeriesTypedSession.ts @@ -523,7 +523,7 @@ import { TypedTimeSeriesRollupEntry } from "../../../src/Documents/Session/TimeS .isGreaterThan(0); for (const res of result.results) { - if (res.rollup) { + if (res.isRollup) { assertThat(res.values.length) .isGreaterThan(0); assertThat(res.value.low)