diff --git a/.github/workflows/RavenClient.yml b/.github/workflows/RavenClient.yml index e969cf956..37cc7c90e 100644 --- a/.github/workflows/RavenClient.yml +++ b/.github/workflows/RavenClient.yml @@ -24,7 +24,7 @@ jobs: strategy: matrix: - node-version: [12.x, 14.x, 16.x] + node-version: [12.x, 14.x, 16.x, 18.x] serverVersion: ["5.2", "5.3"] fail-fast: false @@ -71,4 +71,4 @@ jobs: run: node -e "require('./dist').DocumentStore" - name: Check imports - run: npm run check-imports \ No newline at end of file + run: npm run check-imports diff --git a/package-lock.json b/package-lock.json index efdf97598..2d56dfb09 100644 --- a/package-lock.json +++ b/package-lock.json @@ -397,9 +397,9 @@ } }, "@sinonjs/fake-timers": { - "version": "9.1.1", - "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.1.tgz", - "integrity": "sha512-Wp5vwlZ0lOqpSYGKqr53INws9HLkt6JDc/pDZcPf7bchQnrXJMXPns8CXx0hFikMSGSWfvtvvpb2gtMVfkWagA==", + "version": "9.1.2", + "resolved": "https://registry.npmjs.org/@sinonjs/fake-timers/-/fake-timers-9.1.2.tgz", + "integrity": "sha512-BPS4ynJW/o92PUR4wgriz2Ud5gpST5vz6GQfMixEDK0Z8ZCUv2M7SkBLykH56T++Xs+8ln9zTGbOvNGIe02/jw==", "dev": true, "requires": { "@sinonjs/commons": "^1.7.0" @@ -481,9 +481,9 @@ "dev": true }, "@types/mocha": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.0.tgz", - "integrity": "sha512-QCWHkbMv4Y5U9oW10Uxbr45qMMSzl4OzijsozynUAgx3kEHUdXB00udx2dWDQ7f2TU2a2uuiFaRZjCe3unPpeg==", + "version": "9.1.1", + "resolved": "https://registry.npmjs.org/@types/mocha/-/mocha-9.1.1.tgz", + "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, "@types/node": { @@ -587,9 +587,9 @@ "integrity": "sha512-9UjMCHK5GPgQRoNbqdLIAvAy0EInuiqbW0PBMtVP6B5B2HQJlvoJHM+KodPZMEjOa5VkSc+5LH7xy+cUzQdmHw==" }, "@types/ws": { - "version": "7.4.0", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.0.tgz", - "integrity": "sha512-Y29uQ3Uy+58bZrFLhX36hcI3Np37nqWE7ky5tjiDoy1GDZnIwVxS0CgF+s+1bXMzjKBFy+fqaRfb708iNzdinw==", + "version": "7.4.7", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-7.4.7.tgz", + "integrity": "sha512-JQbbmxZTZehdc2iszGKs5oC3NFnjeay7mtAWrdt7qNtAVK0g19muApzAy4bm9byz79xa2ZnO/BOBC2R8RC5Lww==", "dev": true, "requires": { "@types/node": "*" @@ -1369,17 +1369,28 @@ } }, "glob": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", - "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "dev": true, "requires": { "fs.realpath": "^1.0.0", "inflight": "^1.0.4", "inherits": "2", - "minimatch": "^3.0.4", + "minimatch": "^3.1.1", "once": "^1.3.0", "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } } }, "glob-parent": { @@ -2038,6 +2049,31 @@ } } }, + "glob": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.0.tgz", + "integrity": "sha512-lmLf6gtyrPq8tTjSmrO94wBeQbFR3HbLHbuyD69wuyQkImp2hWqMGB47OX65FBkPffO641IP9jWa1z4ivqG26Q==", + "dev": true, + "requires": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.0.4", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "dependencies": { + "minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "requires": { + "brace-expansion": "^1.1.7" + } + } + } + }, "minimatch": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-4.2.1.tgz", @@ -2056,9 +2092,9 @@ } }, "moment": { - "version": "2.29.2", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.2.tgz", - "integrity": "sha512-UgzG4rvxYpN15jgCmVJwac49h9ly9NurikMWGPdVxm8GZD6XjkKPxDTjQQ43gtGgnV3X0cAyWDdP2Wexoquifg==" + "version": "2.29.3", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.3.tgz", + "integrity": "sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==" }, "ms": { "version": "2.1.2", @@ -2845,13 +2881,13 @@ "dev": true }, "sinon": { - "version": "13.0.1", - "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.1.tgz", - "integrity": "sha512-8yx2wIvkBjIq/MGY1D9h1LMraYW+z1X0mb648KZnKSdvLasvDu7maa0dFaNYdTDczFgbjNw2tOmWdTk9saVfwQ==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/sinon/-/sinon-13.0.2.tgz", + "integrity": "sha512-KvOrztAVqzSJWMDoxM4vM+GPys1df2VBoXm+YciyB/OLMamfS3VXh3oGh5WtrAGSzrgczNWFFY22oKb7Fi5eeA==", "dev": true, "requires": { "@sinonjs/commons": "^1.8.3", - "@sinonjs/fake-timers": "^9.0.0", + "@sinonjs/fake-timers": "^9.1.2", "@sinonjs/samsam": "^6.1.1", "diff": "^5.0.0", "nise": "^5.1.1", diff --git a/package.json b/package.json index 5dd00cb2b..40a687e2a 100644 --- a/package.json +++ b/package.json @@ -56,22 +56,22 @@ "devDependencies": { "@types/bluebird": "^3.5.36", "@types/md5": "^2.3.2", - "@types/mocha": "^9.1.0", + "@types/mocha": "^9.1.1", "@types/pluralize": "0.0.29", "@types/rimraf": "^3.0.2", "@types/sinon": "^10.0.11", "@types/unzipper": "^0.10.5", "@types/util.promisify": "^1.0.4", - "@types/ws": "^7.4.0", + "@types/ws": "^7.4.7", "cross-os": "^1.3.0", - "glob": "^7.2.0", + "glob": "^7.2.3", "http-proxy-agent": "^5.0.0", "lodash.orderby": "^4.6.0", "mocha": "^9.2.2", "nyc": "^15.1.0", "open": "^8.4.0", "rimraf": "^3.0.2", - "sinon": "^13.0.1", + "sinon": "^13.0.2", "source-map-support": "^0.5.21", "ts-node": "^10.7.0", "tslint": "^6.1.3", @@ -92,7 +92,7 @@ "change-case": "^3.1.0", "deprecate": "^1.1.1", "md5": "^2.3.0", - "moment": "^2.29.2", + "moment": "^2.29.3", "node-fetch": "^2.6.7", "object.entries": "^1.1.5", "object.values": "^1.1.5", diff --git a/src/Documents/BulkInsertOperation.ts b/src/Documents/BulkInsertOperation.ts index fbb3d4f1b..435742e1d 100644 --- a/src/Documents/BulkInsertOperation.ts +++ b/src/Documents/BulkInsertOperation.ts @@ -28,6 +28,7 @@ import { TimeSeriesOperations } from "./TimeSeries/TimeSeriesOperations"; import { TimeSeriesValuesHelper } from "./Session/TimeSeries/TimeSeriesValuesHelper"; export class BulkInsertOperation { + private _options: BulkInsertOptions; private readonly _generateEntityIdOnTheClient: GenerateEntityIdOnTheClient; private readonly _requestExecutor: RequestExecutor; @@ -45,6 +46,7 @@ export class BulkInsertOperation { private readonly _timeSeriesBatchSize: number; private _concurrentCheck: number = 0; + private _isInitialWrite: boolean = true; private _bulkInsertAborted: Promise; private _abortReject: Function; @@ -53,12 +55,15 @@ export class BulkInsertOperation { private _requestBodyStream: stream.PassThrough; private _pipelineFinished: Promise; - public constructor(database: string, store: IDocumentStore) { + public constructor(database: string, store: IDocumentStore, options?: BulkInsertOptions) { this._conventions = store.conventions; if (StringUtil.isNullOrEmpty(database)) { this._throwNoDatabase(); } this._requestExecutor = store.getRequestExecutor(database); + this._useCompression = options ? options.useCompression : false; + + this._options = options ?? {}; this._timeSeriesBatchSize = this._conventions.bulkInsert.timeSeriesBatchSize; @@ -310,7 +315,7 @@ export class BulkInsertOperation { this._requestBodyStream = new stream.PassThrough(); const bulkCommand = - new BulkInsertCommand(this._operationId, this._requestBodyStream, this._nodeTag); + new BulkInsertCommand(this._operationId, this._requestBodyStream, this._nodeTag, this._options.skipOverwriteIfUnchanged); bulkCommand.useCompression = this._useCompression; const bulkCommandPromise = this._requestExecutor.execute(bulkCommand); @@ -787,19 +792,25 @@ export class BulkInsertCommand extends RavenCommand { } private readonly _stream: stream.Readable; + private _skipOverwriteIfUnchanged: boolean; private readonly _id: number; public useCompression: boolean; - public constructor(id: number, stream: stream.Readable, nodeTag: string) { + public constructor(id: number, stream: stream.Readable, nodeTag: string, skipOverwriteIfUnchanged: boolean) { super(); this._stream = stream; this._id = id; this._selectedNodeTag = nodeTag; + this._skipOverwriteIfUnchanged = skipOverwriteIfUnchanged; } public createRequest(node: ServerNode): HttpRequestParameters { - const uri = node.url + "/databases/" + node.database + "/bulk_insert?id=" + this._id; + const uri = node.url + + "/databases/" + node.database + + "/bulk_insert?id=" + this._id + + "&skipOverwriteIfUnchanged=" + (this._skipOverwriteIfUnchanged ? "true" : "false"); + const headers = this._headers().typeAppJson().build(); // TODO: useCompression ? new GzipCompressingEntity(_stream) : _stream); return { @@ -815,3 +826,8 @@ export class BulkInsertCommand extends RavenCommand { } } + +export interface BulkInsertOptions { + useCompression?: boolean; + skipOverwriteIfUnchanged?: boolean; +} diff --git a/src/Documents/Commands/Batches/ClusterWideBatchCommand.ts b/src/Documents/Commands/Batches/ClusterWideBatchCommand.ts index 8a967abd9..9a19629c6 100644 --- a/src/Documents/Commands/Batches/ClusterWideBatchCommand.ts +++ b/src/Documents/Commands/Batches/ClusterWideBatchCommand.ts @@ -27,7 +27,7 @@ export class ClusterWideBatchCommand extends SingleNodeBatchCommand implements I let options = super._appendOptions(); if (TypeUtil.isNullOrUndefined(this._disableAtomicDocumentWrites)) { - return ; + return ""; } options @@ -35,4 +35,4 @@ export class ClusterWideBatchCommand extends SingleNodeBatchCommand implements I return options; } -} \ No newline at end of file +} diff --git a/src/Documents/Commands/ConditionalGetDocumentsCommand.ts b/src/Documents/Commands/ConditionalGetDocumentsCommand.ts index 37c673af1..e644c654e 100644 --- a/src/Documents/Commands/ConditionalGetDocumentsCommand.ts +++ b/src/Documents/Commands/ConditionalGetDocumentsCommand.ts @@ -8,6 +8,7 @@ import { DocumentConventions } from "../Conventions/DocumentConventions"; import { readToEnd, stringToReadable } from "../../Utility/StreamUtil"; import { RavenCommandResponsePipeline } from "../../Http/RavenCommandResponsePipeline"; import { ObjectUtil } from "../../Utility/ObjectUtil"; +import { CONSTANTS, HEADERS } from "../../Constants"; export class ConditionalGetDocumentsCommand extends RavenCommand { @@ -31,7 +32,7 @@ export class ConditionalGetDocumentsCommand extends RavenCommand { private _includeAllCounters: boolean; private _timeSeriesIncludes: AbstractTimeSeriesRange[]; + private _revisionsIncludeByChangeVector: string[]; + private _revisionsIncludeByDateTime: Date; private _compareExchangeValueIncludes: string[]; private readonly _metadataOnly: boolean; @@ -114,6 +119,8 @@ export class GetDocumentsCommand extends RavenCommand { this._metadataOnly = opts.metadataOnly; this._timeSeriesIncludes = opts.timeSeriesIncludes; this._compareExchangeValueIncludes = opts.compareExchangeValueIncludes; + this._revisionsIncludeByDateTime = opts.revisionIncludeByDateTimeBefore; + this._revisionsIncludeByChangeVector = opts.revisionsIncludesByChangeVector; } else if (opts.hasOwnProperty("start") && opts.hasOwnProperty("pageSize")) { opts = opts as GetDocumentsStartingWithOptions; this._start = opts.start; @@ -224,6 +231,16 @@ export class GetDocumentsCommand extends RavenCommand { } } + if (this._revisionsIncludeByChangeVector) { + for (const changeVector of this._revisionsIncludeByChangeVector) { + query += "&revisions=" + this._urlEncode(changeVector); + } + } + + if (this._revisionsIncludeByDateTime) { + query += "&revisionsBefore=" + DateUtil.utc.stringify(this._revisionsIncludeByDateTime); + } + if (this._compareExchangeValueIncludes) { for (const compareExchangeValue of this._compareExchangeValueIncludes) { query += "&cmpxchg=" + this._urlEncode(compareExchangeValue); @@ -336,6 +353,7 @@ export class GetDocumentsCommand extends RavenCommand { compareExchangeValueIncludes: ObjectUtil.mapCompareExchangeToLocalObject(json.CompareExchangeValueIncludes), timeSeriesIncludes: ObjectUtil.mapTimeSeriesIncludesToLocalObject(json.TimeSeriesIncludes), counterIncludes: ObjectUtil.mapCounterIncludesToLocalObject(json.CounterIncludes), + revisionIncludes: json.RevisionIncludes, nextPageStart: json.NextPageStart }; } diff --git a/src/Documents/Commands/GetRevisionsCommand.ts b/src/Documents/Commands/GetRevisionsCommand.ts index 990b32e36..a1ea8c7ba 100644 --- a/src/Documents/Commands/GetRevisionsCommand.ts +++ b/src/Documents/Commands/GetRevisionsCommand.ts @@ -59,6 +59,18 @@ export class GetRevisionsCommand extends RavenCommand { this._conventions = conventions; } + public get id(): string { + return this._id; + } + + public get before(): Date { + return this._before; + } + + public get changeVector(): string { + return this._changeVector; + } + public get changeVectors() { return this._changeVectors; } diff --git a/src/Documents/Commands/HeadAttachmentCommand.ts b/src/Documents/Commands/HeadAttachmentCommand.ts index ede7558ed..584b9a17e 100644 --- a/src/Documents/Commands/HeadAttachmentCommand.ts +++ b/src/Documents/Commands/HeadAttachmentCommand.ts @@ -7,6 +7,7 @@ import { ServerNode } from "../../Http/ServerNode"; import { StatusCodes } from "./../../Http/StatusCode"; import * as stream from "readable-stream"; import { getRequiredEtagHeader } from "../../Utility/HttpUtil"; +import { HEADERS } from "../../Constants"; export class HeadAttachmentCommand extends RavenCommand { @@ -41,12 +42,14 @@ export class HeadAttachmentCommand extends RavenCommand { + "/attachments?id=" + encodeURIComponent(this._documentId) + "&name=" + encodeURIComponent(this._name); - const req = { + const req: HttpRequestParameters = { method: "HEAD", uri }; - this._addChangeVectorIfNotNull(this._changeVector, req); + if (this._changeVector) { + req.headers[HEADERS.IF_NONE_MATCH] = `"${this._changeVector}"`; + } return req; } diff --git a/src/Documents/Commands/HeadDocumentCommand.ts b/src/Documents/Commands/HeadDocumentCommand.ts index 7440d8492..bba147634 100644 --- a/src/Documents/Commands/HeadDocumentCommand.ts +++ b/src/Documents/Commands/HeadDocumentCommand.ts @@ -6,6 +6,7 @@ import { HttpCache } from "../../Http/HttpCache"; import { getRequiredEtagHeader } from "../../Utility/HttpUtil"; import { ServerNode } from "../../Http/ServerNode"; import * as stream from "readable-stream"; +import { HEADERS } from "../../Constants"; export class HeadDocumentCommand extends RavenCommand { @@ -34,7 +35,7 @@ export class HeadDocumentCommand extends RavenCommand { const headers = this._headers() .typeAppJson(); if (this._changeVector) { - headers.with("If-None-Match", this._changeVector); + headers.with(HEADERS.IF_NONE_MATCH, this._changeVector); } return { diff --git a/src/Documents/Commands/MultiGet/MultiGetCommand.ts b/src/Documents/Commands/MultiGet/MultiGetCommand.ts index 6f4a7b4bb..2fd0b1cc5 100644 --- a/src/Documents/Commands/MultiGet/MultiGetCommand.ts +++ b/src/Documents/Commands/MultiGet/MultiGetCommand.ts @@ -171,7 +171,7 @@ export class MultiGetCommand extends RavenCommand implements IDis for (let i = 0; i < responses.length; i++) { const res = responses[i]; const command = this._commands[i]; - this._maybeSetCache(res, command); + this._maybeSetCache(res, command, i); if (this._cached && res.statusCode === StatusCodes.NotModified) { const clonedResponse = new GetResponse(); @@ -191,8 +191,12 @@ export class MultiGetCommand extends RavenCommand implements IDis } } - private _maybeSetCache(getResponse: GetResponse, command: GetRequest): void { + private _maybeSetCache(getResponse: GetResponse, command: GetRequest, cachedIndex: number): void { if (getResponse.statusCode === StatusCodes.NotModified) { + // if not modified - update age + if (this._cached) { + this._cached.values[cachedIndex][0].notModified(); + } return; } @@ -220,11 +224,24 @@ export class MultiGetCommand extends RavenCommand implements IDis } public closeCache() { + //If _cached is not null - it means that the client approached with this multitask request to node and the request failed. + //and now client tries to send it to another node. if (this._cached) { this._cached.dispose(); - } - this._cached = null; + this._cached = null; + + // The client sends the commands. + // Some of which could be saved in cache with a response + // that includes the change vector that received from the old fallen node. + // The client can't use those responses because their URLs are different + // (include the IP and port of the old node), because of that the client + // needs to get those docs again from the new node. + + for (const command of this._commands) { + delete command.headers[HEADERS.IF_NONE_MATCH]; + } + } } } @@ -245,4 +262,4 @@ class Cached implements IDisposable { this.values = null; } -} \ No newline at end of file +} diff --git a/src/Documents/Commands/QueryCommand.ts b/src/Documents/Commands/QueryCommand.ts index cc20e1867..fe4d3cec3 100644 --- a/src/Documents/Commands/QueryCommand.ts +++ b/src/Documents/Commands/QueryCommand.ts @@ -184,6 +184,7 @@ export class QueryCommand extends RavenCommand { includedCounterNames: json.IncludedCounterNames, timeSeriesIncludes: ObjectUtil.mapTimeSeriesIncludesToLocalObject(json.TimeSeriesIncludes), compareExchangeValueIncludes: ObjectUtil.mapCompareExchangeToLocalObject(json.CompareExchangeValueIncludes), + revisionIncludes: json.RevisionIncludes, timeSeriesFields: json.TimeSeriesFields, timings: QueryCommand._mapTimingsToLocalObject(json.Timings) } diff --git a/src/Documents/DocumentStore.ts b/src/Documents/DocumentStore.ts index 8ae653c1c..09b982e8d 100644 --- a/src/Documents/DocumentStore.ts +++ b/src/Documents/DocumentStore.ts @@ -12,13 +12,14 @@ import { IDocumentSession } from "./Session/IDocumentSession"; import { SessionOptions } from "./Session/SessionOptions"; import { DocumentSession } from "./Session/DocumentSession"; import { IAuthOptions } from "../Auth/AuthOptions"; -import { BulkInsertOperation } from "./BulkInsertOperation"; +import { BulkInsertOperation, BulkInsertOptions } from "./BulkInsertOperation"; import { IDatabaseChanges } from "./Changes/IDatabaseChanges"; import { DatabaseChanges } from "./Changes/DatabaseChanges"; import { DatabaseSmuggler } from "./Smuggler/DatabaseSmuggler"; import { DatabaseChangesOptions } from "./Changes/DatabaseChangesOptions"; import { IDisposable } from "../Types/Contracts"; import { MultiDatabaseHiLoIdGenerator } from "./Identity/MultiDatabaseHiLoIdGenerator"; +import { TypeUtil } from "../Utility/TypeUtil"; const log = getLogger({ module: "DocumentStore" }); @@ -84,6 +85,10 @@ export class DocumentStore extends DocumentStoreBase { this._identifier = identifier; } + public get hiLoIdGenerator() { + return this._multiDbHiLo; + } + /** * Disposes the document store */ @@ -420,9 +425,14 @@ export class DocumentStore extends DocumentStoreBase { public bulkInsert(): BulkInsertOperation; public bulkInsert(database: string): BulkInsertOperation; - public bulkInsert(database?: string): BulkInsertOperation { + public bulkInsert(options: BulkInsertOptions): BulkInsertOperation; + public bulkInsert(database: string, options: BulkInsertOptions): BulkInsertOperation; + public bulkInsert(databaseOrOptions?: string | BulkInsertOptions, optionalOptions?: BulkInsertOptions): BulkInsertOperation { this.assertInitialized(); - return new BulkInsertOperation(this.getEffectiveDatabase(database), this); + const database = TypeUtil.isString(databaseOrOptions) ? this.getEffectiveDatabase(databaseOrOptions) : this.getEffectiveDatabase(null); + const options: BulkInsertOptions = TypeUtil.isString(databaseOrOptions) ? optionalOptions : databaseOrOptions; + + return new BulkInsertOperation(database, this, options); } } diff --git a/src/Documents/DocumentStoreBase.ts b/src/Documents/DocumentStoreBase.ts index e7ed2762a..c2bb38ef1 100644 --- a/src/Documents/DocumentStoreBase.ts +++ b/src/Documents/DocumentStoreBase.ts @@ -25,7 +25,7 @@ import { DocumentConventions } from "./Conventions/DocumentConventions"; import { RequestExecutor } from "../Http/RequestExecutor"; import { IndexCreation } from "../Documents/Indexes/IndexCreation"; import { PutIndexesOperation } from "./Operations/Indexes/PutIndexesOperation"; -import { BulkInsertOperation } from "./BulkInsertOperation"; +import { BulkInsertOperation, BulkInsertOptions } from "./BulkInsertOperation"; import { IDatabaseChanges } from "./Changes/IDatabaseChanges"; import { DocumentSubscriptions } from "./Subscriptions/DocumentSubscriptions"; import { DocumentStore } from "./DocumentStore"; @@ -37,6 +37,7 @@ import { IDisposable } from "../Types/Contracts"; import { TimeSeriesOperations } from "./TimeSeries/TimeSeriesOperations"; import { IAbstractIndexCreationTask } from "./Indexes/IAbstractIndexCreationTask"; import { StringUtil } from "../Utility/StringUtil"; +import { IHiLoIdGenerator } from "./Identity/IHiLoIdGenerator"; export abstract class DocumentStoreBase extends EventEmitter @@ -70,6 +71,8 @@ export abstract class DocumentStoreBase public abstract identifier: string; + public abstract hiLoIdGenerator: IHiLoIdGenerator; + public abstract initialize(): IDocumentStore; public abstract openSession(): IDocumentSession; @@ -153,7 +156,10 @@ export abstract class DocumentStoreBase private _authOptions: IAuthOptions; - public abstract bulkInsert(database?: string): BulkInsertOperation; + public abstract bulkInsert(): BulkInsertOperation; + public abstract bulkInsert(database: string): BulkInsertOperation; + public abstract bulkInsert(database: string, options: BulkInsertOptions): BulkInsertOperation; + public abstract bulkInsert(options: BulkInsertOptions): BulkInsertOperation; private readonly _subscriptions: DocumentSubscriptions; diff --git a/src/Documents/IDocumentStore.ts b/src/Documents/IDocumentStore.ts index f56bde6a4..b69954186 100644 --- a/src/Documents/IDocumentStore.ts +++ b/src/Documents/IDocumentStore.ts @@ -10,7 +10,7 @@ import { BeforeConversionToEntityEventArgs, AfterConversionToEntityEventArgs, FailedRequestEventArgs, - TopologyUpdatedEventArgs + TopologyUpdatedEventArgs, BeforeRequestEventArgs, SucceedRequestEventArgs } from "./Session/SessionEvents"; import { IDisposable } from "../Types/Contracts"; import { MaintenanceOperationExecutor } from "./Operations/MaintenanceOperationExecutor"; @@ -18,13 +18,14 @@ import { OperationExecutor } from "./Operations/OperationExecutor"; import { RequestExecutor } from "../Http/RequestExecutor"; import { DocumentConventions } from "./Conventions/DocumentConventions"; import { InMemoryDocumentSessionOperations } from "./Session/InMemoryDocumentSessionOperations"; -import { BulkInsertOperation } from "./BulkInsertOperation"; +import { BulkInsertOperation, BulkInsertOptions } from "./BulkInsertOperation"; import { IDatabaseChanges } from "./Changes/IDatabaseChanges"; import { DocumentSubscriptions } from "./Subscriptions/DocumentSubscriptions"; import { SessionOptions } from "./Session/SessionOptions"; import { DatabaseSmuggler } from "./Smuggler/DatabaseSmuggler"; import { IAbstractIndexCreationTask } from "./Indexes/IAbstractIndexCreationTask"; import { TimeSeriesOperations } from "./TimeSeries/TimeSeriesOperations"; +import { IHiLoIdGenerator } from "./Identity/IHiLoIdGenerator"; export interface SessionEventsProxy { addSessionListener(eventName: "failedRequest", eventHandler: (eventArgs: FailedRequestEventArgs) => void): this; @@ -106,6 +107,10 @@ export interface SessionDisposingEventArgs { export interface DocumentStoreEventEmitter { + on(eventName: "beforeRequest", eventHandler: (args: BeforeRequestEventArgs) => void): this; + + on(eventName: "succeedRequest", eventHandler: (args: SucceedRequestEventArgs) => void): this; + on(eventName: "failedRequest", eventHandler: (args: FailedRequestEventArgs) => void): this; on(eventName: "sessionCreated", eventHandler: (args: SessionCreatedEventArgs) => void): this; @@ -116,6 +121,10 @@ export interface DocumentStoreEventEmitter { on(eventName: "executorsDisposed", eventHandler: (callback: () => void) => void): this; + once(eventName: "beforeRequest", eventHandler: (args: BeforeRequestEventArgs) => void): this; + + once(eventName: "succeedRequest", eventHandler: (args: SucceedRequestEventArgs) => void): this; + once(eventName: "failedRequest", eventHandler: (args: FailedRequestEventArgs) => void): this; once(eventName: "sessionCreated", eventHandler: (args: SessionCreatedEventArgs) => void): this; @@ -126,6 +135,10 @@ export interface DocumentStoreEventEmitter { once(eventName: "executorsDisposed", eventHandler: (callback: () => void) => void): this; + removeListener(eventName: "beforeRequest", eventHandler: (args: BeforeRequestEventArgs) => void): this; + + removeListener(eventName: "succeedRequest", eventHandler: (args: SucceedRequestEventArgs) => void): this; + removeListener(eventName: "failedRequest", eventHandler: (args: FailedRequestEventArgs) => void): this; removeListener(eventName: "sessionCreated", eventHandler: (args: SessionCreatedEventArgs) => void): void; @@ -227,6 +240,8 @@ export interface IDocumentStore extends IDisposable, */ authOptions: IStoreAuthOptions; + hiLoIdGenerator: IHiLoIdGenerator; + timeSeries: TimeSeriesOperations; /** @@ -239,7 +254,10 @@ export interface IDocumentStore extends IDisposable, */ urls: string[]; - bulkInsert(database?: string): BulkInsertOperation; + bulkInsert(): BulkInsertOperation; + bulkInsert(database: string): BulkInsertOperation; + bulkInsert(database: string, options: BulkInsertOptions): BulkInsertOperation; + bulkInsert(options: BulkInsertOptions): BulkInsertOperation; subscriptions: DocumentSubscriptions; @@ -263,6 +281,12 @@ export interface IDocumentStore extends IDisposable, addSessionListener( eventName: "topologyUpdated", eventHandler: (args: TopologyUpdatedEventArgs) => void): this; + addSessionListener( + eventName: "succeedRequest", eventHandler: (args: SucceedRequestEventArgs) => void): this; + + addSessionListener( + eventName: "beforeRequest", eventHandler: (args: BeforeRequestEventArgs) => void): this; + addSessionListener( eventName: "failedRequest", eventHandler: (args: FailedRequestEventArgs) => void): this; diff --git a/src/Documents/Identity/HiloIdGenerator.ts b/src/Documents/Identity/HiloIdGenerator.ts index 3a9c8d7ab..cb22a7ab2 100644 --- a/src/Documents/Identity/HiloIdGenerator.ts +++ b/src/Documents/Identity/HiloIdGenerator.ts @@ -8,6 +8,7 @@ import { HiloReturnCommand } from "./Commands/HiloReturnCommand"; import { NextHiloCommand, HiLoResult } from "./Commands/NextHiloCommand"; import { HiloRangeValue } from "./HiloRangeValue"; import { DocumentConventions } from "../Conventions/DocumentConventions"; +import { Lazy } from "../Lazy"; export class HiloIdGenerator { private _store: IDocumentStore; @@ -20,7 +21,8 @@ export class HiloIdGenerator { private _prefix?: string = null; private _lastBatchSize: number = 0; private _serverTag: string = null; - private _generatorLock = semaphore(); + + private _nextRangeTask: Lazy; constructor(tag: string, store: IDocumentStore, dbName: string, identityPartsSeparator: string) { this._lastRangeAt = DateUtil.zeroDate(); @@ -44,36 +46,44 @@ export class HiloIdGenerator { public async nextId(): Promise { while (true) { + const current = this._nextRangeTask; + // local range is not exhausted yet const range = this._range; - let id = range.increment(); + const id = range.increment(); if (id <= range.maxId) { return id; } - let acquiredSemContext: SemaphoreAcquisitionContext; try { - //local range is exhausted , need to get a new range - acquiredSemContext = acquireSemaphore(this._generatorLock, { - contextName: `${this.constructor.name}_${this._tag}` - }); - - await acquiredSemContext.promise; - - const maybeNewRange = this._range; - if (maybeNewRange !== range) { - id = maybeNewRange.increment(); - if (id <= maybeNewRange.maxId) { - return id; - } + // let's try to call the existing task for next range + await current.getValue(); + if (range !== this._range) { + continue; } + } catch (e) { + // previous task was faulted, we will try to replace it + } - await this._getNextRange(); - } finally { - if (acquiredSemContext) { - acquiredSemContext.dispose(); - } + // local range is exhausted , need to get a new range + const maybeNextTask = new Lazy(() => this._getNextRange()); + let changed = false; + if (this._nextRangeTask === current) { + changed = true; + this._nextRangeTask = maybeNextTask; + } + + if (changed) { + await maybeNextTask.getValue(); + continue; + } + + try { + // failed to replace, let's wait on the previous task + await this._nextRangeTask.getValue(); + } catch (e) { + // previous task was faulted, we will try again } } } diff --git a/src/Documents/Identity/IHiLoIdGenerator.ts b/src/Documents/Identity/IHiLoIdGenerator.ts new file mode 100644 index 000000000..b6e6f76f1 --- /dev/null +++ b/src/Documents/Identity/IHiLoIdGenerator.ts @@ -0,0 +1,8 @@ +import { ObjectTypeDescriptor } from "../../Types"; + + +export interface IHiLoIdGenerator { + generateNextIdFor(database: string, collectionName: string): Promise; + generateNextIdFor(database: string, documentType: ObjectTypeDescriptor): Promise; + generateNextIdFor(database: string, entity: Object): Promise; +} diff --git a/src/Documents/Identity/MultiDatabaseHiLoIdGenerator.ts b/src/Documents/Identity/MultiDatabaseHiLoIdGenerator.ts index c70f5f94a..3d2954acf 100644 --- a/src/Documents/Identity/MultiDatabaseHiLoIdGenerator.ts +++ b/src/Documents/Identity/MultiDatabaseHiLoIdGenerator.ts @@ -2,8 +2,11 @@ import { MultiTypeHiLoIdGenerator } from "./MultiTypeHiLoIdGenerator"; import { DocumentStore } from "../DocumentStore"; import { IRavenObject } from "../../Types/IRavenObject"; import { DocumentStoreBase } from "../DocumentStoreBase"; +import { IHiLoIdGenerator } from "./IHiLoIdGenerator"; +import { TypeUtil } from "../../Utility/TypeUtil"; +import { ObjectTypeDescriptor } from "../../Types"; -export class MultiDatabaseHiLoIdGenerator { +export class MultiDatabaseHiLoIdGenerator implements IHiLoIdGenerator { protected readonly _store: DocumentStore; @@ -31,4 +34,31 @@ export class MultiDatabaseHiLoIdGenerator { await generator.returnUnusedRange(); } } + + public generateNextIdFor(database: string, collectionName: string): Promise; + public generateNextIdFor(database: string, documentType: ObjectTypeDescriptor): Promise; + public generateNextIdFor(database: string, entity: Object): Promise; + public generateNextIdFor(database: string, target: string | ObjectTypeDescriptor | Object): Promise { + if (TypeUtil.isString(target)) { + return this._generateNextIdFor(database, target); + } + + if (TypeUtil.isObjectTypeDescriptor(target)) { + const collectionName = this._store.conventions.getCollectionNameForType(target); + return this._generateNextIdFor(database, collectionName); + } + + const collectionName = this._store.conventions.getCollectionNameForEntity(target); + return this._generateNextIdFor(database, collectionName); + } + + private async _generateNextIdFor(database: string, collectionName: string): Promise { + database = this._store.getEffectiveDatabase(database); + + if (!(database in this._generators)) { + this._generators[database] = new MultiTypeHiLoIdGenerator(this._store, database); + } + + return this._generators[database].generateNextIdFor(collectionName); + } } diff --git a/src/Documents/Identity/MultiTypeHiLoIdGenerator.ts b/src/Documents/Identity/MultiTypeHiLoIdGenerator.ts index 39378fc8e..706b2a557 100644 --- a/src/Documents/Identity/MultiTypeHiLoIdGenerator.ts +++ b/src/Documents/Identity/MultiTypeHiLoIdGenerator.ts @@ -91,6 +91,31 @@ export class MultiTypeHiLoIdGenerator { } } + public async generateNextIdFor(collectionName: string): Promise { + let value = this._idGeneratorsByTag[collectionName]; + if (value) { + return value.nextId(); + } + + const acquiredSem = acquireSemaphore(this._sem); + try { + await acquiredSem.promise; + + value = this._idGeneratorsByTag[collectionName]; + if (value) { + return value.nextId(); + } + + value = this._createGeneratorFor(collectionName); + this._idGeneratorsByTag[collectionName] = value; + + } finally { + acquiredSem.dispose(); + } + + return value.nextId(); + } + protected _createGeneratorFor(tag: string): HiloIdGenerator { return new HiloIdGenerator(tag, this._store, this._dbName, this._identityPartsSeparator); } diff --git a/src/Documents/Indexes/AutoIndexDefinition.ts b/src/Documents/Indexes/AutoIndexDefinition.ts index 5b6b46943..f18fb123a 100644 --- a/src/Documents/Indexes/AutoIndexDefinition.ts +++ b/src/Documents/Indexes/AutoIndexDefinition.ts @@ -1,13 +1,11 @@ import { IndexPriority, IndexState, IndexType } from "./Enums"; import { AutoIndexFieldOptions } from "./AutoIndexFieldOptions"; +import { IndexDefinitionBase } from "./IndexDefinitionBase"; -export interface AutoIndexDefinition { +export interface AutoIndexDefinition extends IndexDefinitionBase { type: IndexType; - name: string; - priority: IndexPriority; - state: IndexState; collection: string; mapFields: Record; groupByFields: Record; -} \ No newline at end of file +} diff --git a/src/Documents/Indexes/IndexDefinition.ts b/src/Documents/Indexes/IndexDefinition.ts index 27e50c667..ea8886414 100644 --- a/src/Documents/Indexes/IndexDefinition.ts +++ b/src/Documents/Indexes/IndexDefinition.ts @@ -7,16 +7,13 @@ import { AbstractIndexDefinitionBuilder } from "./AbstractIndexDefinitionBuilder import { IndexSourceType } from "./IndexSourceType"; import { AdditionalAssembly } from "./AdditionalAssembly"; import { IndexDeploymentMode } from "./IndexDeploymentMode"; +import { IndexDefinitionBase } from "./IndexDefinitionBase"; export interface IndexConfiguration { [key: string]: string; } -export class IndexDefinition { - - public name: string; - public priority: IndexPriority; - public state: IndexState; +export class IndexDefinition extends IndexDefinitionBase { /** * Index lock mode: diff --git a/src/Documents/Indexes/IndexDefinitionBase.ts b/src/Documents/Indexes/IndexDefinitionBase.ts new file mode 100644 index 000000000..65856d8f8 --- /dev/null +++ b/src/Documents/Indexes/IndexDefinitionBase.ts @@ -0,0 +1,9 @@ +import { IndexPriority, IndexState } from "./Enums"; + + +export abstract class IndexDefinitionBase { + public name: string; + public priority: IndexPriority; + public state: IndexState; + +} diff --git a/src/Documents/Indexes/RollingIndex.ts b/src/Documents/Indexes/RollingIndex.ts index 5bdc67881..339cb16c5 100644 --- a/src/Documents/Indexes/RollingIndex.ts +++ b/src/Documents/Indexes/RollingIndex.ts @@ -2,4 +2,5 @@ import { RollingIndexDeployment } from "./RollingIndexDeployment"; export interface RollingIndex { activeDeployments: Record; -} \ No newline at end of file + raftCommandIndex: number; +} diff --git a/src/Documents/Operations/Backups/S3Settings.ts b/src/Documents/Operations/Backups/S3Settings.ts index 5a910dadb..e6cbfb63f 100644 --- a/src/Documents/Operations/Backups/S3Settings.ts +++ b/src/Documents/Operations/Backups/S3Settings.ts @@ -3,4 +3,5 @@ import { AmazonSettings } from "./AmazonSettings"; export interface S3Settings extends AmazonSettings { bucketName: string; customServerUrl: string; -} \ No newline at end of file + forcePathStyle: boolean; +} diff --git a/src/Documents/Operations/Replication/GetReplicationHubAccessOperation.ts b/src/Documents/Operations/Replication/GetReplicationHubAccessOperation.ts index 29bbce59a..a1574f644 100644 --- a/src/Documents/Operations/Replication/GetReplicationHubAccessOperation.ts +++ b/src/Documents/Operations/Replication/GetReplicationHubAccessOperation.ts @@ -53,7 +53,7 @@ class GetReplicationHubAccessCommand extends RavenCommand { queryResult.resultEtag = this.resultEtag; queryResult.nodeTag = this.nodeTag; queryResult.counterIncludes = this.counterIncludes; + queryResult.revisionIncludes = this.revisionIncludes; queryResult.includedCounterNames = this.includedCounterNames; queryResult.timeSeriesIncludes = this.timeSeriesIncludes; queryResult.compareExchangeValueIncludes = this.compareExchangeValueIncludes; diff --git a/src/Documents/Queries/QueryResultBase.ts b/src/Documents/Queries/QueryResultBase.ts index 265b932cf..2d3537e5e 100644 --- a/src/Documents/Queries/QueryResultBase.ts +++ b/src/Documents/Queries/QueryResultBase.ts @@ -20,6 +20,8 @@ export abstract class QueryResultBase { public counterIncludes: object; + public revisionIncludes: any[]; + public includedCounterNames: { [key: string]: string[] }; public timeSeriesIncludes: any; diff --git a/src/Documents/Session/AbstractDocumentQuery.ts b/src/Documents/Session/AbstractDocumentQuery.ts index b3d1cccc3..8acea2442 100644 --- a/src/Documents/Session/AbstractDocumentQuery.ts +++ b/src/Documents/Session/AbstractDocumentQuery.ts @@ -87,6 +87,7 @@ import { StringBuilder } from "../../Utility/StringBuilder"; import { ProjectionBehavior } from "../Queries/ProjectionBehavior"; import { AbstractTimeSeriesRange } from "../Operations/TimeSeries/AbstractTimeSeriesRange"; import { IAbstractDocumentQueryImpl } from "./IAbstractDocumentQueryImpl"; +import { RevisionIncludesToken } from "./Tokens/RevisionIncludesToken"; /** * A query against a Raven index @@ -568,12 +569,12 @@ export abstract class AbstractDocumentQuery) { + if (!this._revisionsIncludesTokens) { + this._revisionsIncludesTokens = []; + } + + for (const changeVector of revisionsToIncludeByChangeVector) { + this._revisionsIncludesTokens.push(RevisionIncludesToken.createForPath(changeVector)); + } + } + public get parameterPrefix() { return this._parameterPrefix; } diff --git a/src/Documents/Session/DocumentQuery.ts b/src/Documents/Session/DocumentQuery.ts index c5f3b19be..067f4eb09 100644 --- a/src/Documents/Session/DocumentQuery.ts +++ b/src/Documents/Session/DocumentQuery.ts @@ -618,6 +618,7 @@ export class DocumentQuery query._documentIncludes = new Set(this._documentIncludes); query._counterIncludesTokens = this._counterIncludesTokens; query._timeSeriesIncludesTokens = this._timeSeriesIncludesTokens; + query._revisionsIncludesTokens = this._revisionsIncludesTokens; query._compareExchangeValueIncludesTokens = this._compareExchangeValueIncludesTokens; query._rootTypes = new Set([this._clazz]); diff --git a/src/Documents/Session/DocumentSession.ts b/src/Documents/Session/DocumentSession.ts index 66a129361..f69b532b1 100644 --- a/src/Documents/Session/DocumentSession.ts +++ b/src/Documents/Session/DocumentSession.ts @@ -184,6 +184,9 @@ export class DocumentSession extends InMemoryDocumentSessionOperations internalOpts.compareExchangeValueIncludes = [ ...builder.compareExchangeValuesToInclude ]; } + internalOpts.revisionIncludesByChangeVector = builder.revisionsToIncludeByChangeVector ? Array.from(builder.revisionsToIncludeByChangeVector) : null; + internalOpts.revisionsToIncludeByDateTime = builder.revisionsToIncludeByDateTime; + internalOpts.includeAllCounters = builder.isAllCounters; } else { internalOpts.includes = options.includes as string[]; @@ -379,6 +382,8 @@ export class DocumentSession extends InMemoryDocumentSessionOperations loadOperation.withCounters(opts.counterIncludes); } + loadOperation.withRevisions(opts.revisionIncludesByChangeVector); + loadOperation.withRevisions(opts.revisionsToIncludeByDateTime); loadOperation.withTimeSeries(opts.timeSeriesIncludes); loadOperation.withCompareExchange(opts.compareExchangeValueIncludes); diff --git a/src/Documents/Session/DocumentSessionRevisions.ts b/src/Documents/Session/DocumentSessionRevisions.ts index c76885eb1..a034e10b7 100644 --- a/src/Documents/Session/DocumentSessionRevisions.ts +++ b/src/Documents/Session/DocumentSessionRevisions.ts @@ -38,6 +38,12 @@ export class DocumentSessionRevisions extends DocumentSessionRevisionsBase imple const operation = new GetRevisionOperation(this._session, id, options.start, options.pageSize); const command = operation.createRequest(); + if (!command) { + return operation.getRevisionsFor(options.documentType); + } + if (this._sessionInfo) { + this._sessionInfo.incrementRequestCount(); + } await this._requestExecutor.execute(command, this._sessionInfo); operation.result = command.result; return operation.getRevisionsFor(options.documentType); @@ -52,12 +58,19 @@ export class DocumentSessionRevisions extends DocumentSessionRevisionsBase imple } as SessionRevisionsMetadataOptions, options || {}); const operation = new GetRevisionOperation(this._session, id, options.start, options.pageSize, true); const command = operation.createRequest(); + if (!command) { + return operation.getRevisionsMetadataFor(); + } + if (this._sessionInfo) { + this._sessionInfo.incrementRequestCount(); + } await this._requestExecutor.execute(command, this._sessionInfo); operation.result = command.result; return operation.getRevisionsMetadataFor(); } public async get(id: string, date: Date): Promise; + public async get(id: string, date: Date, documentType: DocumentType): Promise; public async get(changeVector: string): Promise; public async get(changeVector: string, documentType: DocumentType): Promise; @@ -67,7 +80,8 @@ export class DocumentSessionRevisions extends DocumentSessionRevisionsBase imple : Promise>; public async get( changeVectorOrVectorsOrId: string | string[], - documentTypeOrDate?: DocumentType | Date) + documentTypeOrDate?: DocumentType | Date, + documentTypeForDateOverload?: DocumentType) : Promise | TEntity> { const documentType = TypeUtil.isDocumentType(documentTypeOrDate) @@ -76,7 +90,7 @@ export class DocumentSessionRevisions extends DocumentSessionRevisionsBase imple if (TypeUtil.isDate(documentTypeOrDate)) { return this._getByIdAndDate( - changeVectorOrVectorsOrId as string, documentTypeOrDate); + changeVectorOrVectorsOrId as string, documentTypeOrDate, documentTypeForDateOverload); } else { return this._get(changeVectorOrVectorsOrId, documentType); } @@ -86,6 +100,12 @@ export class DocumentSessionRevisions extends DocumentSessionRevisionsBase imple id: string, date: Date, clazz?: DocumentType) { const operation = new GetRevisionOperation(this._session, id, date); const command = operation.createRequest(); + if (!command) { + return operation.getRevision(clazz); + } + if (this._sessionInfo) { + this._sessionInfo.incrementRequestCount(); + } await this._requestExecutor.execute(command, this._sessionInfo); operation.result = command.result; return operation.getRevision(clazz); @@ -97,6 +117,14 @@ export class DocumentSessionRevisions extends DocumentSessionRevisionsBase imple const operation = new GetRevisionOperation(this._session, changeVectorOrVectors as any); const command = operation.createRequest(); + if (!command) { + return TypeUtil.isArray(changeVectorOrVectors) + ? operation.getRevisions(documentType) + : operation.getRevision(documentType); + } + if (this._sessionInfo) { + this._sessionInfo.incrementRequestCount(); + } await this._requestExecutor.execute(command, this._sessionInfo); operation.result = command.result; return TypeUtil.isArray(changeVectorOrVectors) @@ -107,6 +135,9 @@ export class DocumentSessionRevisions extends DocumentSessionRevisionsBase imple public async getCountFor(id: string): Promise { const operation = new GetRevisionsCountOperation(id); const command = operation.createRequest(); + if (this._sessionInfo) { + this._sessionInfo.incrementRequestCount(); + } await this._requestExecutor.execute(command, this._sessionInfo); return command.result; } diff --git a/src/Documents/Session/IDocumentSession.ts b/src/Documents/Session/IDocumentSession.ts index f55d97205..062388171 100644 --- a/src/Documents/Session/IDocumentSession.ts +++ b/src/Documents/Session/IDocumentSession.ts @@ -55,6 +55,10 @@ export class SessionInfo { this.noCaching = options.noCaching; } + public incrementRequestCount(): void { + this._session.incrementRequestCount(); + } + public setContext(sessionKey: string) { if (StringUtil.isNullOrWhitespace(sessionKey)) { throwError("InvalidArgumentException", "Session key cannot be null or whitespace."); @@ -319,6 +323,8 @@ export interface SessionLoadInternalParameters { includeAllCounters?: boolean; timeSeriesIncludes?: AbstractTimeSeriesRange[]; compareExchangeValueIncludes?: string[]; + revisionIncludesByChangeVector?: string[]; + revisionsToIncludeByDateTime?: Date; } export interface IDocumentSessionImpl extends IDocumentSession { diff --git a/src/Documents/Session/IRevisionsSessionOperations.ts b/src/Documents/Session/IRevisionsSessionOperations.ts index 5b4d9470c..6ce30ce57 100644 --- a/src/Documents/Session/IRevisionsSessionOperations.ts +++ b/src/Documents/Session/IRevisionsSessionOperations.ts @@ -40,6 +40,11 @@ export interface IRevisionsSessionOperations { */ get(id: string, date: Date): Promise; + /** + * Returns a document revision by date. + */ + get(id: string, date: Date, documentType: DocumentType): Promise; + /** * Returns a document revision by change vector. */ diff --git a/src/Documents/Session/InMemoryDocumentSessionOperations.ts b/src/Documents/Session/InMemoryDocumentSessionOperations.ts index eff007f07..94a80a3e5 100644 --- a/src/Documents/Session/InMemoryDocumentSessionOperations.ts +++ b/src/Documents/Session/InMemoryDocumentSessionOperations.ts @@ -143,6 +143,16 @@ export abstract class InMemoryDocumentSessionOperations public includedDocumentsById: Map = CaseInsensitiveKeysMap.create(); + /** + * Translate between an CV and its associated entity + */ + public includeRevisionsByChangeVector: Map = CaseInsensitiveKeysMap.create(); + + /** + * Translate between an ID and its associated entity + */ + public includeRevisionsIdByDateTimeBefore: Map> = CaseInsensitiveKeysMap.create(); + public documentsByEntity: DocumentsByEntityHolder = new DocumentsByEntityHolder(); public deletedEntities: DeletedEntitiesHolder = new DeletedEntitiesHolder(); @@ -425,15 +435,30 @@ export abstract class InMemoryDocumentSessionOperations } } - public checkIfIdAlreadyIncluded(ids: string[], includes: { [key: string]: ObjectTypeDescriptor }): boolean; - public checkIfIdAlreadyIncluded(ids: string[], includes: string[]): boolean; - public checkIfIdAlreadyIncluded( - ids: string[], includes: string[] | { [key: string]: ObjectTypeDescriptor }): boolean { + public checkIfAllChangeVectorsAreAlreadyIncluded(changeVectors: string[]): boolean { + if (!this.includeRevisionsByChangeVector) { + return false; + } + + for (const cv of changeVectors) { + if (!this.includeRevisionsByChangeVector.has(cv)) { + return false; + } + } + + return true; + } - if (!Array.isArray(includes) && typeof includes === "object") { - return this.checkIfIdAlreadyIncluded(ids, Object.keys(includes)); + public checkIfRevisionByDateTimeBeforeAlreadyIncluded(id: string, dateTime: Date): boolean { + if (!this.includeRevisionsIdByDateTimeBefore) { + return false; } + const dictionaryDateTimeToDocument = this.includeRevisionsIdByDateTimeBefore.get(id); + return dictionaryDateTimeToDocument && dictionaryDateTimeToDocument.has(dateTime.getTime()); + } + + public checkIfIdAlreadyIncluded(ids: string[], includes: string[]): boolean { for (const id of ids) { if (this._knownMissingIds.has(id)) { continue; @@ -625,6 +650,49 @@ export abstract class InMemoryDocumentSessionOperations } } + public registerRevisionIncludes(revisionIncludes: any[]) { + if (this.noTracking) { + return; + } + + if (!revisionIncludes) { + return; + } + + if (!this.includeRevisionsByChangeVector) { + this.includeRevisionsByChangeVector = CaseInsensitiveKeysMap.create(); + } + + if (!this.includeRevisionsIdByDateTimeBefore) { + this.includeRevisionsIdByDateTimeBefore = CaseInsensitiveKeysMap.create(); + } + + for (const obj of revisionIncludes) { + if (!obj) { + continue; + } + + const json = obj; + const id = json.Id; + const changeVector = json.ChangeVector; + const beforeAsText = json.Before; + const dateTime = beforeAsText ? DateUtil.utc.parse(beforeAsText) : null; + const revision = json.Revision; + + this.includeRevisionsByChangeVector.set(changeVector, DocumentInfo.getNewDocumentInfo(revision)); + + if (dateTime && !StringUtil.isNullOrWhitespace(id)) { + const map = new Map(); + + this.includeRevisionsIdByDateTimeBefore.set(id, map); + + const documentInfo = new DocumentInfo(); + documentInfo.document = revision; + map.set(dateTime.getTime(), documentInfo); + } + } + } + public registerMissingIncludes(results: object[], includes: object, includePaths: string[]): void { if (this.noTracking) { return; @@ -1577,7 +1645,7 @@ export abstract class InMemoryDocumentSessionOperations const beforeDeleteEventArgs = new SessionBeforeDeleteEventArgs(this, documentInfo.id, documentInfo.entity); this.emit("beforeDelete", beforeDeleteEventArgs); - result.sessionCommands.push(new DeleteCommandData(documentInfo.id, changeVector)); + result.sessionCommands.push(new DeleteCommandData(documentInfo.id, changeVector, documentInfo.changeVector)); } if (!changes) { @@ -1890,7 +1958,7 @@ export abstract class InMemoryDocumentSessionOperations } } - return !!this.deletedEntities.size; + return !!this.deletedEntities.size || this.deferredCommands.length > 0; } /** diff --git a/src/Documents/Session/Loaders/IGenericIncludeBuilder.ts b/src/Documents/Session/Loaders/IGenericIncludeBuilder.ts index f9bdac11a..53c10acbb 100644 --- a/src/Documents/Session/Loaders/IGenericIncludeBuilder.ts +++ b/src/Documents/Session/Loaders/IGenericIncludeBuilder.ts @@ -2,11 +2,13 @@ import { IDocumentIncludeBuilder } from "./IDocumentIncludeBuilder"; import { ICounterIncludeBuilder } from "./ICounterIncludeBuilder"; import { ICompareExchangeValueIncludeBuilder } from "./ICompareExchangeValueIncludeBuilder"; import { IGenericTimeSeriesIncludeBuilder } from "./IGenericTimeSeriesIncludeBuilder"; +import { IGenericRevisionIncludeBuilder } from "./IGenericRevisionIncludeBuilder"; export interface IGenericIncludeBuilder extends IDocumentIncludeBuilder, ICounterIncludeBuilder, IGenericTimeSeriesIncludeBuilder, - ICompareExchangeValueIncludeBuilder { + ICompareExchangeValueIncludeBuilder, + IGenericRevisionIncludeBuilder { -} \ No newline at end of file +} diff --git a/src/Documents/Session/Loaders/IGenericRevisionIncludeBuilder.ts b/src/Documents/Session/Loaders/IGenericRevisionIncludeBuilder.ts new file mode 100644 index 000000000..35a361b42 --- /dev/null +++ b/src/Documents/Session/Loaders/IGenericRevisionIncludeBuilder.ts @@ -0,0 +1,5 @@ + +export interface IGenericRevisionIncludeBuilder { + includeRevisions(path: string): TBuilder; + includeRevisions(before: Date): TBuilder; +} diff --git a/src/Documents/Session/Loaders/IncludeBuilder.ts b/src/Documents/Session/Loaders/IncludeBuilder.ts index a3d17a56a..a52378da3 100644 --- a/src/Documents/Session/Loaders/IncludeBuilder.ts +++ b/src/Documents/Session/Loaders/IncludeBuilder.ts @@ -90,4 +90,16 @@ export class IncludeBuilder extends IncludeBuilderBase implements IIncludeBuilde return this; } -} \ No newline at end of file + includeRevisions(path: string): IIncludeBuilder; + includeRevisions(before: Date): IIncludeBuilder; + includeRevisions(pathOrDate: string | Date): IIncludeBuilder { + if (TypeUtil.isString(pathOrDate)) { + this._withAlias(); + this._includeRevisionsByChangeVectors(pathOrDate); + return this; + } else { + this._includeRevisionsBefore(pathOrDate); + return this; + } + } +} diff --git a/src/Documents/Session/Loaders/IncludeBuilderBase.ts b/src/Documents/Session/Loaders/IncludeBuilderBase.ts index 717626cb9..e7e08ab44 100644 --- a/src/Documents/Session/Loaders/IncludeBuilderBase.ts +++ b/src/Documents/Session/Loaders/IncludeBuilderBase.ts @@ -22,6 +22,8 @@ export class IncludeBuilderBase { public countersToIncludeBySourcePath: CountersByDocId; public timeSeriesToIncludeBySourceAlias: Map; public compareExchangeValuesToInclude: Set; + public revisionsToIncludeByChangeVector: Set; + public revisionsToIncludeByDateTime: Date; public includeTimeSeriesTags: boolean; public includeTimeSeriesDocument: boolean; @@ -82,6 +84,22 @@ export class IncludeBuilderBase { this.documentsToInclude.add(path); } + protected _includeRevisionsBefore(revisionsToIncludeByDateTime: Date): void { + this.revisionsToIncludeByDateTime = revisionsToIncludeByDateTime; + } + + protected _includeRevisionsByChangeVectors(path: string): void { + if (StringUtil.isNullOrWhitespace(path)) { + throwError("InvalidArgumentException", "Path cannot be null or whitespace"); + } + + if (!this.revisionsToIncludeByChangeVector) { + this.revisionsToIncludeByChangeVector = new Set(); + } + + this.revisionsToIncludeByChangeVector.add(path); + } + protected _includeCounter(path: string, name: string): void { if (!name) { throwError("InvalidArgumentException", "Name cannot be empty."); diff --git a/src/Documents/Session/Loaders/QueryIncludeBuilder.ts b/src/Documents/Session/Loaders/QueryIncludeBuilder.ts index 9f183f2ce..3d96497bf 100644 --- a/src/Documents/Session/Loaders/QueryIncludeBuilder.ts +++ b/src/Documents/Session/Loaders/QueryIncludeBuilder.ts @@ -99,4 +99,17 @@ export class QueryIncludeBuilder extends IncludeBuilderBase implements IQueryInc return this; } + + public includeRevisions(path: string): IQueryIncludeBuilder; + public includeRevisions(before: Date): IQueryIncludeBuilder; + public includeRevisions(pathOrDate: Date | string): IQueryIncludeBuilder { + if (TypeUtil.isString(pathOrDate)) { + this._withAlias(); + this._includeRevisionsByChangeVectors(pathOrDate); + } else { + this._includeRevisionsBefore(pathOrDate); + } + + return this; + } } diff --git a/src/Documents/Session/Operations/GetRevisionOperation.ts b/src/Documents/Session/Operations/GetRevisionOperation.ts index bdb2398ba..a7fd864bc 100644 --- a/src/Documents/Session/Operations/GetRevisionOperation.ts +++ b/src/Documents/Session/Operations/GetRevisionOperation.ts @@ -46,7 +46,18 @@ export class GetRevisionOperation { } public createRequest() { - this._session.incrementRequestCount(); + if (this._command.changeVectors) { + return this._session.checkIfAllChangeVectorsAreAlreadyIncluded(this._command.changeVectors) ? null : this._command; + } + + if (this._command.changeVector) { + return this._session.checkIfAllChangeVectorsAreAlreadyIncluded([this.command.changeVector]) ? null : this._command; + } + + if (this.command.before) { + return this._session.checkIfRevisionByDateTimeBeforeAlreadyIncluded(this.command.id, this.command.before) ? null : this._command; + } + return this._command; } @@ -110,6 +121,36 @@ export class GetRevisionOperation { public getRevision(documentType: DocumentType): TEntity | null { if (!this._result) { + + let revision: DocumentInfo; + + if (this._command.changeVectors) { + for (const changeVector of this._command.changeVectors) { + revision = this._session.includeRevisionsByChangeVector.get(changeVector); + if (revision) { + return this._getRevision(documentType, revision.document); + } + } + } + + if (this.command.changeVector && this._session.includeRevisionsByChangeVector) { + revision = this._session.includeRevisionsByChangeVector.get(this._command.changeVector); + if (revision) { + return this._getRevision(documentType, revision.document); + } + } + + if (this._command.before && this._session.includeRevisionsIdByDateTimeBefore) { + const dictionaryDateTimeToDocument = this._session.includeRevisionsIdByDateTimeBefore.get(this._command.id); + + if (dictionaryDateTimeToDocument) { + revision = dictionaryDateTimeToDocument.get(this._command.before.getTime()); + if (revision) { + return this._getRevision(documentType, revision.document); + } + } + } + return null; } @@ -121,6 +162,17 @@ export class GetRevisionOperation { documentType: DocumentType): RevisionsCollectionObject { const results = {} as RevisionsCollectionObject; + if (!this._result) { + for (const changeVector of this._command.changeVectors) { + const revision = this._session.includeRevisionsByChangeVector.get(changeVector); + if (revision) { + results[changeVector] = this._getRevision(documentType, revision.document); + } + } + + return results; + } + for (let i = 0; i < this._command.changeVectors.length; i++) { const changeVector = this._command.changeVectors[i]; if (!changeVector) { diff --git a/src/Documents/Session/Operations/Lazy/LazyConditionalLoadOperation.ts b/src/Documents/Session/Operations/Lazy/LazyConditionalLoadOperation.ts index 03849576a..464a0ec1d 100644 --- a/src/Documents/Session/Operations/Lazy/LazyConditionalLoadOperation.ts +++ b/src/Documents/Session/Operations/Lazy/LazyConditionalLoadOperation.ts @@ -31,7 +31,7 @@ export class LazyConditionalLoadOperation implements ILazyOper request.url = "/docs"; request.method = "GET"; request.query = "?id=" + encodeURIComponent(this._id); - request.headers["If-None-Match"] = `"${this._changeVector}"`; + request.headers[HEADERS.IF_NONE_MATCH] = `"${this._changeVector}"`; return request; } @@ -92,4 +92,4 @@ export class LazyConditionalLoadOperation implements ILazyOper this._result = null; this._session.registerMissing(this._id); } -} \ No newline at end of file +} diff --git a/src/Documents/Session/Operations/LoadOperation.ts b/src/Documents/Session/Operations/LoadOperation.ts index 32515e35d..2dfafebb7 100644 --- a/src/Documents/Session/Operations/LoadOperation.ts +++ b/src/Documents/Session/Operations/LoadOperation.ts @@ -22,6 +22,8 @@ export class LoadOperation { private _ids: string[]; private _includes: string[]; private _countersToInclude: string[]; + private _revisionsToIncludeByChangeVector: string[]; + private _revisionsToIncludeByDateTimeBefore: Date; private _compareExchangeValuesToInclude: string[]; private _includeAllCounters: boolean; private _timeSeriesToInclude: AbstractTimeSeriesRange[]; @@ -49,7 +51,9 @@ export class LoadOperation { metadataOnly: false, conventions: this._session.conventions, timeSeriesIncludes: this._timeSeriesToInclude, - compareExchangeValueIncludes: this._compareExchangeValuesToInclude + compareExchangeValueIncludes: this._compareExchangeValuesToInclude, + revisionsIncludesByChangeVector: this._revisionsToIncludeByChangeVector, + revisionIncludeByDateTimeBefore: this._revisionsToIncludeByDateTimeBefore }; if (this._includeAllCounters) { @@ -86,6 +90,20 @@ export class LoadOperation { return this; } + public withRevisions(revisionsByChangeVector: string[]): LoadOperation; + public withRevisions(revisionByDateTimeBefore: Date): LoadOperation; + public withRevisions(revisions: string[] | Date): LoadOperation { + if (TypeUtil.isArray(revisions)) { + this._revisionsToIncludeByChangeVector = revisions; + } + + if (TypeUtil.isDate(revisions)) { + this._revisionsToIncludeByDateTimeBefore = revisions; + } + + return this; + } + public withAllCounters() { this._includeAllCounters = true; return this; @@ -216,6 +234,10 @@ export class LoadOperation { this._session.registerTimeSeries(result.timeSeriesIncludes); } + if (this._revisionsToIncludeByChangeVector || this._revisionsToIncludeByDateTimeBefore) { + this._session.registerRevisionIncludes(result.revisionIncludes); + } + if (this._compareExchangeValuesToInclude) { this._session.clusterSession.registerCompareExchangeValues(result.compareExchangeValueIncludes); } diff --git a/src/Documents/Session/Operations/QueryOperation.ts b/src/Documents/Session/Operations/QueryOperation.ts index 4508909b1..700ae34c5 100644 --- a/src/Documents/Session/Operations/QueryOperation.ts +++ b/src/Documents/Session/Operations/QueryOperation.ts @@ -192,6 +192,10 @@ export class QueryOperation { if (queryResult.compareExchangeValueIncludes) { this._session.clusterSession.registerCompareExchangeValues(queryResult.compareExchangeValueIncludes); } + + if (queryResult.revisionIncludes) { + this._session.registerRevisionIncludes(queryResult.revisionIncludes); + } } } diff --git a/src/Documents/Session/SessionEvents.ts b/src/Documents/Session/SessionEvents.ts index 87bd161b5..ea870082c 100644 --- a/src/Documents/Session/SessionEvents.ts +++ b/src/Documents/Session/SessionEvents.ts @@ -202,11 +202,15 @@ export class FailedRequestEventArgs { public database: string; public url: string; public error: Error; + public request: HttpRequestParameters; + public response: HttpResponse; - public constructor(database: string, url: string, error: Error) { + public constructor(database: string, url: string, error: Error, request: HttpRequestParameters, response: HttpResponse) { this.database = database; this.url = url; this.error = error; + this.request = request; + this.response = response; } } @@ -246,4 +250,4 @@ export class SucceedRequestEventArgs { this.request = request; this.attemptNumber = attemptNumber; } -} \ No newline at end of file +} diff --git a/src/Documents/Session/Tokens/FacetToken.ts b/src/Documents/Session/Tokens/FacetToken.ts index 1ee85d1da..bb809bdee 100644 --- a/src/Documents/Session/Tokens/FacetToken.ts +++ b/src/Documents/Session/Tokens/FacetToken.ts @@ -248,7 +248,7 @@ export class FacetAggregationToken extends QueryToken { } writer.append(" as "); - this._writeField(writer, this._fieldDisplayName); + QueryToken.writeField(writer, this._fieldDisplayName); } public static max(fieldName: string): FacetAggregationToken diff --git a/src/Documents/Session/Tokens/FieldsToFetchToken.ts b/src/Documents/Session/Tokens/FieldsToFetchToken.ts index f0e17dc6c..179bec64c 100644 --- a/src/Documents/Session/Tokens/FieldsToFetchToken.ts +++ b/src/Documents/Session/Tokens/FieldsToFetchToken.ts @@ -45,7 +45,7 @@ export class FieldsToFetchToken extends QueryToken { if (!fieldToFetch) { writer.append("null"); } else { - this._writeField(writer, fieldToFetch); + QueryToken.writeField(writer, fieldToFetch); } if (this.customFunction) { diff --git a/src/Documents/Session/Tokens/GroupByKeyToken.ts b/src/Documents/Session/Tokens/GroupByKeyToken.ts index fe3a58703..7c113e319 100644 --- a/src/Documents/Session/Tokens/GroupByKeyToken.ts +++ b/src/Documents/Session/Tokens/GroupByKeyToken.ts @@ -16,7 +16,7 @@ export class GroupByKeyToken extends QueryToken { } public writeTo(writer): void { - this._writeField(writer, this._fieldName || "key()"); + QueryToken.writeField(writer, this._fieldName || "key()"); if (!this._projectedName || this._projectedName === this._fieldName) { return; diff --git a/src/Documents/Session/Tokens/GroupByToken.ts b/src/Documents/Session/Tokens/GroupByToken.ts index 6fa40232f..3c7804dca 100644 --- a/src/Documents/Session/Tokens/GroupByToken.ts +++ b/src/Documents/Session/Tokens/GroupByToken.ts @@ -22,7 +22,7 @@ export class GroupByToken extends QueryToken { if (this._method !== "None") { writer.append("Array("); } - this._writeField(writer, this._fieldName); + QueryToken.writeField(writer, this._fieldName); if (this._method !== "None") { writer.append(")"); } diff --git a/src/Documents/Session/Tokens/HighlightingToken.ts b/src/Documents/Session/Tokens/HighlightingToken.ts index decb716a5..6bb3ad0f4 100644 --- a/src/Documents/Session/Tokens/HighlightingToken.ts +++ b/src/Documents/Session/Tokens/HighlightingToken.ts @@ -26,7 +26,7 @@ export class HighlightingToken extends QueryToken { public writeTo(writer: StringBuilder): void { writer.append("highlight("); - this._writeField(writer, this._fieldName); + QueryToken.writeField(writer, this._fieldName); writer .append(",") .append(this._fragmentLength) diff --git a/src/Documents/Session/Tokens/OrderByToken.ts b/src/Documents/Session/Tokens/OrderByToken.ts index 131f90142..79f9e2bcc 100644 --- a/src/Documents/Session/Tokens/OrderByToken.ts +++ b/src/Documents/Session/Tokens/OrderByToken.ts @@ -103,7 +103,7 @@ export class OrderByToken extends QueryToken { writer .append("custom(") } - this._writeField(writer, this._fieldName); + QueryToken.writeField(writer, this._fieldName); if (this._sorterName) { writer diff --git a/src/Documents/Session/Tokens/QueryToken.ts b/src/Documents/Session/Tokens/QueryToken.ts index 35c8ad3ca..558eff9ae 100644 --- a/src/Documents/Session/Tokens/QueryToken.ts +++ b/src/Documents/Session/Tokens/QueryToken.ts @@ -4,8 +4,8 @@ export abstract class QueryToken { public abstract writeTo(writer: StringBuilder); - protected _writeField(writer: StringBuilder, field: string) { - const keyWord = QueryToken.RQL_KEYWORDS.has(field); + public static writeField(writer: StringBuilder, field: string) { + const keyWord = QueryToken.isKeyword(field); if (keyWord) { writer.append("'"); } @@ -16,6 +16,10 @@ export abstract class QueryToken { } } + public static isKeyword(field: string): boolean { + return QueryToken.RQL_KEYWORDS.has(field); + } + private static RQL_KEYWORDS: Set = new Set([ "as", "select", diff --git a/src/Documents/Session/Tokens/RevisionIncludesToken.ts b/src/Documents/Session/Tokens/RevisionIncludesToken.ts new file mode 100644 index 000000000..882520437 --- /dev/null +++ b/src/Documents/Session/Tokens/RevisionIncludesToken.ts @@ -0,0 +1,40 @@ +import { QueryToken } from "./QueryToken"; +import { DateUtil } from "../../../Utility/DateUtil"; +import { StringBuilder } from "../../../Utility/StringBuilder"; +import { StringUtil } from "../../../Utility/StringUtil"; + + +export class RevisionIncludesToken extends QueryToken { + private readonly _dateTime: string; + private readonly _path: string; + + private constructor(args: { date?: string, path?: string}) { + super(); + + this._dateTime = args.date; + this._path = args.path; + } + + public static createForDate(dateTime: Date) { + return new RevisionIncludesToken({ + date: DateUtil.default.stringify(dateTime), + }); + } + + public static createForPath(path: string) { + return new RevisionIncludesToken({ + path + }); + } + + writeTo(writer: StringBuilder) { + writer.append("revisions('"); + if (this._dateTime) { + writer.append(this._dateTime); + } else if (!StringUtil.isNullOrWhitespace(this._path)) { + writer.append(this._path); + } + + writer.append("')"); + } +} diff --git a/src/Documents/Session/Tokens/WhereToken.ts b/src/Documents/Session/Tokens/WhereToken.ts index e0ea1c53a..84c7f6e8b 100644 --- a/src/Documents/Session/Tokens/WhereToken.ts +++ b/src/Documents/Session/Tokens/WhereToken.ts @@ -161,15 +161,18 @@ export class WhereToken extends QueryToken { } public writeTo(writer): void { - if (this.options.boost) { + // tslint:disable-next-line:triple-equals + if (this.options.boost != null) { writer.append("boost("); } - if (this.options.fuzzy) { + // tslint:disable-next-line:triple-equals + if (this.options.fuzzy != null) { writer.append("fuzzy("); } - if (this.options.proximity) { + // tslint:disable-next-line:triple-equals + if (this.options.proximity != null) { writer.append("proximity("); } @@ -216,21 +219,24 @@ export class WhereToken extends QueryToken { writer.append(")"); } - if (this.options.proximity) { + // tslint:disable-next-line:triple-equals + if (this.options.proximity != null) { writer .append(", ") .append(this.options.proximity) .append(")"); } - if (this.options.fuzzy) { + // tslint:disable-next-line:triple-equals + if (this.options.fuzzy != null) { writer .append(", ") .append(this.options.fuzzy) .append(")"); } - if (this.options.boost) { + // tslint:disable-next-line:triple-equals + if (this.options.boost != null) { writer .append(", ") .append(this.options.boost) @@ -239,7 +245,7 @@ export class WhereToken extends QueryToken { } private _writeInnerWhere(writer): void { - this._writeField(writer, this.fieldName); + QueryToken.writeField(writer, this.fieldName); switch (this.whereOperator) { case "Equals": diff --git a/src/Documents/Subscriptions/DocumentSubscriptions.ts b/src/Documents/Subscriptions/DocumentSubscriptions.ts index bdc10682a..2d69badc4 100644 --- a/src/Documents/Subscriptions/DocumentSubscriptions.ts +++ b/src/Documents/Subscriptions/DocumentSubscriptions.ts @@ -23,6 +23,7 @@ import { SubscriptionUpdateOptions } from "./SubscriptionUpdateOptions"; import { UpdateSubscriptionCommand } from "../Commands/UpdateSubscriptionCommand"; import { CounterIncludesToken } from "../Session/Tokens/CounterIncludesToken"; import { TimeSeriesIncludesToken } from "../Session/Tokens/TimeSeriesIncludesToken"; +import { QueryToken } from "../Session/Tokens/QueryToken"; export class DocumentSubscriptions implements IDisposable { private readonly _store: DocumentStore; @@ -152,7 +153,7 @@ export class DocumentSubscriptions implements IDisposable { .append("'"); } else { queryBuilder - .append(include); + .append(QueryToken.isKeyword(include) ? "'" + include + "'" : include); } numberOfIncludesAdded++; diff --git a/src/Documents/Subscriptions/SubscriptionBatch.ts b/src/Documents/Subscriptions/SubscriptionBatch.ts index a278d87f3..3e1a9b52d 100644 --- a/src/Documents/Subscriptions/SubscriptionBatch.ts +++ b/src/Documents/Subscriptions/SubscriptionBatch.ts @@ -59,6 +59,10 @@ export class SubscriptionBatch { return this._items ? this._items.length : 0; } + public getNumberOfIncludes() { + return this._includes ? this._includes.length : 0; + } + public constructor(documentType: DocumentType, revisions: boolean, requestExecutor: RequestExecutor, store: IDocumentStore, dbName: string) { this._documentType = documentType; diff --git a/src/Documents/Subscriptions/SubscriptionConnectionServerMessage.ts b/src/Documents/Subscriptions/SubscriptionConnectionServerMessage.ts index 3b10204f5..15b0285f6 100644 --- a/src/Documents/Subscriptions/SubscriptionConnectionServerMessage.ts +++ b/src/Documents/Subscriptions/SubscriptionConnectionServerMessage.ts @@ -15,6 +15,7 @@ export interface SubscriptionRedirectData { currentTag: string; redirectedTag: string; reasons: Record; + registerConnectionDurationInTicks: number; } export type MessageType = "None" diff --git a/src/Documents/Subscriptions/SubscriptionCreationOptions.ts b/src/Documents/Subscriptions/SubscriptionCreationOptions.ts index 030fd74c3..d6641700b 100644 --- a/src/Documents/Subscriptions/SubscriptionCreationOptions.ts +++ b/src/Documents/Subscriptions/SubscriptionCreationOptions.ts @@ -7,5 +7,6 @@ export interface SubscriptionCreationOptions { includes?: (builder: ISubscriptionIncludeBuilder) => void; changeVector?: string; mentorNode?: string; + disabled?: boolean; documentType?: DocumentType; } diff --git a/src/Documents/Subscriptions/SubscriptionWorker.ts b/src/Documents/Subscriptions/SubscriptionWorker.ts index d64ca8d62..bf86a2bcd 100644 --- a/src/Documents/Subscriptions/SubscriptionWorker.ts +++ b/src/Documents/Subscriptions/SubscriptionWorker.ts @@ -145,24 +145,16 @@ export class SubscriptionWorker implements IDisposable { } } - const [socket, chosenUrl] = await TcpUtils.connectWithPriority(tcpInfo, command.result.certificate, this._store.authOptions); + const result = await TcpUtils.connectSecuredTcpSocket( + tcpInfo, + command.result.certificate, + this._store.authOptions, + "Subscription", + (chosenUrl, tcpInfo, socket) => this._negotiateProtocolVersionForSubscription(chosenUrl, tcpInfo, socket)); - this._tcpClient = socket; + this._tcpClient = result.socket; - this._ensureParser(); - - const databaseName = this._store.getEffectiveDatabase(this._dbName); - - const parameters = { - database: databaseName, - operation: "Subscription", - version: SUBSCRIPTION_TCP_VERSION, - readResponseAndGetVersionCallback: url => this._readServerResponseAndGetVersion(url), - destinationNodeTag: this.currentNodeTag, - destinationUrl: chosenUrl - } as TcpNegotiateParameters; - - this._supportedFeatures = await TcpNegotiation.negotiateProtocolVersion(this._tcpClient, parameters); + this._supportedFeatures = result.supportedFeatures; if (this._supportedFeatures.protocolVersion <= 0) { throwError("InvalidOperationException", @@ -191,6 +183,23 @@ export class SubscriptionWorker implements IDisposable { return this._tcpClient; } + private async _negotiateProtocolVersionForSubscription(chosenUrl: string, tcpInfo: TcpConnectionInfo, socket: Socket): Promise { + const databaseName = this._store.getEffectiveDatabase(this._dbName); + + const parameters = { + database: databaseName, + operation: "Subscription", + version: SUBSCRIPTION_TCP_VERSION, + readResponseAndGetVersionCallback: url => this._readServerResponseAndGetVersion(url, socket), + destinationNodeTag: this.currentNodeTag, + destinationUrl: chosenUrl, + destinationServerId: tcpInfo.serverId + } as TcpNegotiateParameters; + + return TcpNegotiation.negotiateProtocolVersion(socket, parameters); + } + + private async _legacyTryGetTcpInfo(requestExecutor: RequestExecutor, node?: ServerNode) { const tcpCommand = new GetTcpInfoCommand("Subscription/" + this._dbName, this._dbName); try { @@ -229,19 +238,19 @@ export class SubscriptionWorker implements IDisposable { }); } - private _ensureParser() { + private _ensureParser(socket: Socket) { const keysTransformProfile = getTransformJsonKeysProfile( this._revisions ? "SubscriptionRevisionsResponsePayload" : "SubscriptionResponsePayload", this._store.conventions); this._parser = stream.pipeline([ - this._tcpClient, + socket, new Parser({ jsonStreaming: true, streamValues: false }), new TransformKeysJsonStream(keysTransformProfile), new StreamValues() ], err => { - if (err && !this._tcpClient.destroyed) { + if (err && !socket.destroyed) { this._emitter.emit("error", err); } }) as stream.Transform; @@ -250,7 +259,8 @@ export class SubscriptionWorker implements IDisposable { } // noinspection JSUnusedLocalSymbols - private async _readServerResponseAndGetVersion(url: string): Promise { + private async _readServerResponseAndGetVersion(url: string, socket: Socket): Promise { + this._ensureParser(socket); const x: any = await this._readNextObject(); switch (x.status) { case "Ok": @@ -268,6 +278,9 @@ export class SubscriptionWorker implements IDisposable { await this._sendDropMessage(x.value); throwError("InvalidOperationException", "Can't connect to database " + this._dbName + " because: " + x.message); + break; + case "InvalidNetworkTopology": + throwError("InvalidNetworkTopologyException", "Failed to connect to url " + url + " because " + x.message); } return x.version; @@ -300,8 +313,12 @@ export class SubscriptionWorker implements IDisposable { } if (connectionStatus.type !== "ConnectionStatus") { - throwError("InvalidOperationException", - "Server returned illegal type message when expecting connection status, was: " + connectionStatus.type); + let message = "Server returned illegal type message when expecting connection status, was:" + connectionStatus.type; + + if (connectionStatus.type === "Error") { + message += ". Exception: " + connectionStatus.exception; + } + throwError("InvalidOperationException", message); } // noinspection FallThroughInSwitchStatementJS @@ -329,6 +346,16 @@ export class SubscriptionWorker implements IDisposable { "Subscription with id '" + this._options.subscriptionName + "' cannot be opened, because it does not exist. " + connectionStatus.exception); case "Redirect": + if (this._options.strategy === "WaitForFree") { + if (connectionStatus.data) { + const registerConnectionDurationInTicks = connectionStatus.data["RegisterConnectionDurationInTicks"]; + if (registerConnectionDurationInTicks / 10_000 >= this._options.maxErroneousPeriod) { + // this worker connection Waited For Free for more than MaxErroneousPeriod + this._lastConnectionFailure = null; + } + } + } + const data = connectionStatus.data; const appropriateNode = data.redirectedTag; const currentNode = data.currentTag; diff --git a/src/Exceptions/index.ts b/src/Exceptions/index.ts index 416897328..dff6c1ee0 100644 --- a/src/Exceptions/index.ts +++ b/src/Exceptions/index.ts @@ -53,6 +53,8 @@ export type RavenErrorType = "RavenException" | "DocumentDoesNotExistsException" | "NonUniqueObjectException" | "ConcurrencyException" + | "ClusterTransactionConcurrencyException" + | "InvalidNetworkTopologyException" | "ArgumentNullException" | "ArgumentOutOfRangeException" | "DatabaseDoesNotExistException" @@ -81,6 +83,7 @@ export type RavenErrorType = "RavenException" | "SubscriptionDoesNotExistException" | "SubscriptionConnectionDownException" | "SubscriptionInvalidStateException" + | "SubscriptionMessageTypeException" | "SubscriptionException" | "SubscriberErrorException" | "SubscriptionInUseException" @@ -211,6 +214,10 @@ export class ExceptionDispatcher { return getError("DocumentConflictException", schema.message, null, { json }); } + if (schema.type.includes("ClusterTransactionConcurrencyException")) { + return getError("ClusterTransactionConcurrencyException", schema.message, null, { json }); + } + return getError("ConcurrencyException", schema.message); } diff --git a/src/Http/ClusterRequestExecutor.ts b/src/Http/ClusterRequestExecutor.ts index daa2c8bed..cee60a0b3 100644 --- a/src/Http/ClusterRequestExecutor.ts +++ b/src/Http/ClusterRequestExecutor.ts @@ -118,13 +118,7 @@ export class ClusterRequestExecutor extends RequestExecutor { }) .then(() => { const results = command.result; - const members = results.topology.members; - const nodes = Object.keys(members) - .reduce((reduceResult, clusterTag) => { - const url = members[clusterTag]; - const serverNode = new ServerNode({ clusterTag, url }); - return [...reduceResult, serverNode]; - }, []); + const nodes = ServerNode.createFrom(results.topology); const newTopology = new Topology(results.etag, nodes); if (!this._nodeSelector) { diff --git a/src/Http/RequestExecutor.ts b/src/Http/RequestExecutor.ts index 77f2ce116..8622f6cda 100644 --- a/src/Http/RequestExecutor.ts +++ b/src/Http/RequestExecutor.ts @@ -291,8 +291,8 @@ export class RequestExecutor implements IDisposable { this._emitter.off(event, handler); } - private _onFailedRequestInvoke(url: string, e: Error) { - const args = new FailedRequestEventArgs(this._databaseName, url, e); + private _onFailedRequestInvoke(url: string, e: Error, req?: HttpRequestParameters, response?: HttpResponse) { + const args = new FailedRequestEventArgs(this._databaseName, url, e, req, response); this._emitter.emit("failedRequest", args); } @@ -960,22 +960,20 @@ export class RequestExecutor implements IDisposable { && response.headers && response.headers.get(HEADERS.REFRESH_CLIENT_CONFIGURATION); - if (refreshTopology || refreshClientConfiguration) { - const serverNode = new ServerNode({ - database: this._databaseName, - url: chosenNode.url - }); + const tasks: Promise[] = []; - const updateParameters = new UpdateTopologyParameters(serverNode); + if (refreshTopology) { + const updateParameters = new UpdateTopologyParameters(chosenNode); updateParameters.timeoutInMs = 0; updateParameters.debugTag = "refresh-topology-header"; + tasks.push(this.updateTopology(updateParameters)); + } - const topologyTask: Promise = refreshTopology ? this.updateTopology(updateParameters) : null; - const clientConfiguration: Promise = refreshClientConfiguration ? this._updateClientConfiguration(serverNode) : null; - - await topologyTask; - await clientConfiguration; + if (refreshClientConfiguration) { + tasks.push(this._updateClientConfiguration(chosenNode)); } + + await Promise.all(tasks); } private async _sendRequestToServer(chosenNode: ServerNode, @@ -1094,7 +1092,7 @@ export class RequestExecutor implements IDisposable { private _setRequestHeaders(sessionInfo: SessionInfo, cachedChangeVector: string, req: HttpRequestParameters) { if (cachedChangeVector) { - req.headers["If-None-Match"] = `"${cachedChangeVector}"`; + req.headers[HEADERS.IF_NONE_MATCH] = `"${cachedChangeVector}"`; } if (!this._disableClientConfigurationUpdates) { @@ -1135,7 +1133,7 @@ export class RequestExecutor implements IDisposable { } private static _tryGetServerVersion(response: HttpResponse) { - return response.headers[HEADERS.SERVER_VERSION]; + return response.headers.get(HEADERS.SERVER_VERSION); } private _throwFailedToContactAllNodes( @@ -1304,7 +1302,12 @@ export class RequestExecutor implements IDisposable { const raftRequestString = "raft-request-id=" + raftCommand.getRaftUniqueRequestId(); - builder = new URL(builder.search ? builder.toString() + "&" + raftRequestString : builder.toString() + "?" + raftRequestString); + let joinCharacter = builder.search ? "&" : "?"; + if (!builder.search && req.uri.endsWith("?")) { + joinCharacter = ""; + } + + builder = new URL(builder.toString() + joinCharacter + raftRequestString); } if (this._shouldBroadcast(command)) { @@ -1495,7 +1498,7 @@ export class RequestExecutor implements IDisposable { return false; } - this._onFailedRequestInvoke(url, error); + this._onFailedRequestInvoke(url, error, req, response); await this._executeOnSpecificNode(command, sessionInfo, { chosenNode: indexAndNodeAndEtag.currentNode, diff --git a/src/Http/ServerNode.ts b/src/Http/ServerNode.ts index a7bb84f95..d8480f902 100644 --- a/src/Http/ServerNode.ts +++ b/src/Http/ServerNode.ts @@ -1,5 +1,6 @@ import { IRavenObject } from "../Types/IRavenObject"; import { UriUtility } from "../Http/UriUtility"; +import { ClusterTopology } from "./ClusterTopology"; export type ServerNodeRole = "None" | "Promotable" | "Member" | "Rehab"; @@ -56,6 +57,34 @@ export class ServerNode { this._lastServerVersionCheck = 0; } + public static createFrom(topology: ClusterTopology): ServerNode[] { + const nodes: ServerNode[] = []; + + if (!topology) { + return nodes; + } + + Object.keys(topology.members).forEach(node => { + const member = topology.members[node]; + + nodes.push(new ServerNode({ + url: member, + clusterTag: node + })); + }); + + Object.keys(topology.watchers).forEach(node => { + const watcher = topology.watchers[node]; + + nodes.push(new ServerNode({ + url: watcher, + clusterTag: node + })); + }); + + return nodes; + } + public get lastServerVersion() { return this._lastServerVersion; } diff --git a/src/Mapping/MetadataAsDictionary.ts b/src/Mapping/MetadataAsDictionary.ts index 10b9d4e57..d7660eb36 100644 --- a/src/Mapping/MetadataAsDictionary.ts +++ b/src/Mapping/MetadataAsDictionary.ts @@ -43,7 +43,8 @@ class MetadataInternal { } private _metadataConvertValue(key, val) { - if (!val) { + // tslint:disable-next-line:triple-equals + if (val == null) { return null; } diff --git a/src/ServerWide/Commands/GetTcpInfoCommand.ts b/src/ServerWide/Commands/GetTcpInfoCommand.ts index 9d0879109..bbcce77fc 100644 --- a/src/ServerWide/Commands/GetTcpInfoCommand.ts +++ b/src/ServerWide/Commands/GetTcpInfoCommand.ts @@ -8,6 +8,7 @@ export class TcpConnectionInfo { public certificate: string; public urls: string[]; public nodeTag: string; + public serverId: string; } export class GetTcpInfoCommand extends RavenCommand { diff --git a/src/ServerWide/Operations/Configuration/DatabaseSettings.ts b/src/ServerWide/Operations/Configuration/DatabaseSettings.ts new file mode 100644 index 000000000..1b563b81c --- /dev/null +++ b/src/ServerWide/Operations/Configuration/DatabaseSettings.ts @@ -0,0 +1,5 @@ + + +export interface DatabaseSettings { + settings: Record; +} diff --git a/src/ServerWide/Operations/Configuration/GetDatabaseSettingsOperation.ts b/src/ServerWide/Operations/Configuration/GetDatabaseSettingsOperation.ts new file mode 100644 index 000000000..89164d693 --- /dev/null +++ b/src/ServerWide/Operations/Configuration/GetDatabaseSettingsOperation.ts @@ -0,0 +1,73 @@ +import { IMaintenanceOperation, OperationResultType } from "../../../Documents/Operations/OperationAbstractions"; +import { DatabaseSettings } from "./DatabaseSettings"; +import { throwError } from "../../../Exceptions"; +import { RavenCommand } from "../../../Http/RavenCommand"; +import { ServerNode } from "../../../Http/ServerNode"; +import { HttpRequestParameters } from "../../../Primitives/Http"; +import stream from "readable-stream"; +import { DocumentConventions } from "../../../Documents/Conventions/DocumentConventions"; + +export class GetDatabaseSettingsOperation implements IMaintenanceOperation { + + private readonly _databaseName: string; + + public get resultType(): OperationResultType { + return "CommandResult"; + } + + public constructor(databaseName: string) { + if (!databaseName) { + throwError("InvalidArgumentException", "DatabaseName cannot be null"); + } + this._databaseName = databaseName; + } + + getCommand(conventions: DocumentConventions): RavenCommand { + return new GetDatabaseSettingsCommand(this._databaseName); + } +} + + +class GetDatabaseSettingsCommand extends RavenCommand { + private readonly _databaseName: string; + + public constructor(databaseName: string) { + super(); + if (!databaseName) { + throwError("InvalidArgumentException", "DatabaseName cannot be null"); + } + + this._databaseName = databaseName; + } + + get isReadRequest(): boolean { + return false; + } + + createRequest(node: ServerNode): HttpRequestParameters { + const uri = node.url + "/databases/" + this._databaseName + "/admin/record"; + + return { + method: "GET", + uri + } + } + + async setResponseAsync(bodyStream: stream.Stream, fromCache: boolean): Promise { + if (!bodyStream) { + this._throwInvalidResponse(); + } + + let body; + + const result = await this._pipeline() + .parseJsonSync() + .collectBody(_ => body = _) + .process(bodyStream); + + this.result = { + settings: result.Settings + } + return body; + } +} diff --git a/src/ServerWide/Operations/Configuration/PutDatabaseSettingsOperation.ts b/src/ServerWide/Operations/Configuration/PutDatabaseSettingsOperation.ts new file mode 100644 index 000000000..b392a1cfa --- /dev/null +++ b/src/ServerWide/Operations/Configuration/PutDatabaseSettingsOperation.ts @@ -0,0 +1,77 @@ +import { IMaintenanceOperation, OperationResultType } from "../../../Documents/Operations/OperationAbstractions"; +import { throwError } from "../../../Exceptions"; +import { RavenCommand } from "../../../Http/RavenCommand"; +import { DocumentConventions } from "../../../Documents/Conventions/DocumentConventions"; +import { IRaftCommand } from "../../../Http/IRaftCommand"; +import { RaftIdGenerator } from "../../../Utility/RaftIdGenerator"; +import { ServerNode } from "../../../Http/ServerNode"; +import { HttpRequestParameters } from "../../../Primitives/Http"; + +export class PutDatabaseSettingsOperation implements IMaintenanceOperation { + private readonly _databaseName: string; + private readonly _configurationSettings: Record; + + public constructor(databaseName: string, configurationSettings: Record) { + if (!databaseName) { + throwError("InvalidArgumentException", "DatabaseName cannot be null"); + } + + this._databaseName = databaseName; + + if (!configurationSettings) { + throwError("InvalidArgumentException", "ConfigurationSettings cannot be null"); + } + + this._configurationSettings = configurationSettings; + } + + public get resultType(): OperationResultType { + return "CommandResult"; + } + + getCommand(conventions: DocumentConventions): RavenCommand { + return new PutDatabaseConfigurationSettingsCommand(this._configurationSettings, this._databaseName); + } +} + +class PutDatabaseConfigurationSettingsCommand extends RavenCommand implements IRaftCommand { + private readonly _configurationSettings: Record; + private readonly _databaseName: string; + + public constructor(configurationSettings: Record, databaseName: string) { + super(); + + if (!databaseName) { + throwError("InvalidArgumentException", "DatabaseName cannot be null"); + } + + this._databaseName = databaseName; + + if (!configurationSettings) { + throwError("InvalidArgumentException", "ConfigurationSettings cannot be null"); + } + + this._configurationSettings = configurationSettings; + } + + get isReadRequest(): boolean { + return false; + } + + getRaftUniqueRequestId(): string { + return RaftIdGenerator.newId(); + } + + createRequest(node: ServerNode): HttpRequestParameters { + const uri = node.url + "/databases/" + this._databaseName + "/admin/configuration/settings"; + + const body = this._serializer.serialize(this._configurationSettings); + + return { + uri, + method: "PUT", + headers: this._headers().typeAppJson().build(), + body + } + } +} diff --git a/src/ServerWide/Operations/index.ts b/src/ServerWide/Operations/index.ts index 0680f6604..4e8b3fd4e 100644 --- a/src/ServerWide/Operations/index.ts +++ b/src/ServerWide/Operations/index.ts @@ -29,6 +29,7 @@ export interface DatabaseTopology { databaseTopologyIdBase64: string; clusterTransactionIdBase64: string; priorityOrder: string[]; + nodesModifiedAt: string; } export function getAllNodesFromTopology(topology: DatabaseTopology) { diff --git a/src/ServerWide/Tcp/TcpConnectionHeaderMessage.ts b/src/ServerWide/Tcp/TcpConnectionHeaderMessage.ts index 16e048614..c0b187b46 100644 --- a/src/ServerWide/Tcp/TcpConnectionHeaderMessage.ts +++ b/src/ServerWide/Tcp/TcpConnectionHeaderMessage.ts @@ -4,6 +4,7 @@ import { DetailedReplicationHubAccess } from "../../Documents/Operations/Replica export interface TcpConnectionHeaderMessage { databaseName: string; sourceNodeTag: string; + serverId: string; operation: OperationTypes; operationVersion: number; info: string; diff --git a/src/ServerWide/Tcp/TcpConnectionStatus.ts b/src/ServerWide/Tcp/TcpConnectionStatus.ts index 37b2df5d1..e4f5c77b5 100644 --- a/src/ServerWide/Tcp/TcpConnectionStatus.ts +++ b/src/ServerWide/Tcp/TcpConnectionStatus.ts @@ -1,2 +1,2 @@ -export type TcpConnectionStatus = "Ok" | "AuthorizationFailed" | "TcpVersionMismatch"; +export type TcpConnectionStatus = "Ok" | "AuthorizationFailed" | "TcpVersionMismatch" | "InvalidNetworkTopology"; diff --git a/src/ServerWide/Tcp/TcpNegotiateParameters.ts b/src/ServerWide/Tcp/TcpNegotiateParameters.ts index 23367fea7..2578ddc25 100644 --- a/src/ServerWide/Tcp/TcpNegotiateParameters.ts +++ b/src/ServerWide/Tcp/TcpNegotiateParameters.ts @@ -1,12 +1,14 @@ import { AuthorizationInfo, OperationTypes } from "./TcpConnectionHeaderMessage"; +import { Socket } from "net"; export interface TcpNegotiateParameters { operation: OperationTypes; - authorizeInfo: AuthorizationInfo; + authorizeInfo?: AuthorizationInfo; version: number; database: string; sourceNodeTag?: string; destinationNodeTag: string; destinationUrl: string; - readResponseAndGetVersionCallback: (url: string) => Promise; + destinationServerId: string; + readResponseAndGetVersionCallback: (url: string, socket: Socket) => Promise; } diff --git a/src/ServerWide/Tcp/TcpNegotiation.ts b/src/ServerWide/Tcp/TcpNegotiation.ts index b0d1e08ae..1fbedee1d 100644 --- a/src/ServerWide/Tcp/TcpNegotiation.ts +++ b/src/ServerWide/Tcp/TcpNegotiation.ts @@ -22,7 +22,7 @@ export class TcpNegotiation { let currentRef: number = parameters.version; while (true) { await this._sendTcpVersionInfo(socket, parameters, currentRef); - const version = await parameters.readResponseAndGetVersionCallback(parameters.destinationUrl); + const version = await parameters.readResponseAndGetVersionCallback(parameters.destinationUrl, socket); log.info("Read response from " + (parameters.sourceNodeTag || parameters.destinationUrl) + " for " + parameters.operation + ", received version is '" + version + "'"); diff --git a/src/Utility/HttpUtil.ts b/src/Utility/HttpUtil.ts index 17533993d..8ce77c24b 100644 --- a/src/Utility/HttpUtil.ts +++ b/src/Utility/HttpUtil.ts @@ -28,7 +28,9 @@ export function getEtagHeader(responseOrHeaders: HttpResponse | IncomingHttpHead etagHeaders = null; } - return Array.isArray(etagHeaders) ? etagHeaders[0] : (etagHeaders || null); + const singleHeader = Array.isArray(etagHeaders) ? etagHeaders[0] : (etagHeaders || null); + + return singleHeader ? etagHeaderToChangeVector(singleHeader) : null; } export function etagHeaderToChangeVector(responseHeader: string) { diff --git a/src/Utility/TcpUtils.ts b/src/Utility/TcpUtils.ts index 1ffe39b59..a85281107 100644 --- a/src/Utility/TcpUtils.ts +++ b/src/Utility/TcpUtils.ts @@ -5,8 +5,9 @@ import { IAuthOptions } from "../Auth/AuthOptions"; import * as tls from "tls"; import { Certificate } from "../Auth/Certificate"; import { PeerCertificate } from "tls"; -import { getError } from "../Exceptions"; +import { getError, throwError } from "../Exceptions"; import { TcpConnectionInfo } from "../ServerWide/Commands/GetTcpInfoCommand"; +import { OperationTypes, SupportedFeatures } from "../ServerWide/Tcp/TcpConnectionHeaderMessage"; export class TcpUtils { public static async connect( @@ -63,13 +64,14 @@ export class TcpUtils { } } - public static async connectWithPriority(info: TcpConnectionInfo, serverCertificate: string, - clientCertificate: IAuthOptions): Promise<[Socket, string]> { + public static async connectSecuredTcpSocket(info: TcpConnectionInfo, serverCertificate: string, + clientCertificate: IAuthOptions, operationType: OperationTypes, negotiationCallback: NegotiationCallback): Promise { if (info.urls) { for (const url of info.urls) { try { const socket = await this.connect(url, serverCertificate, clientCertificate); - return [socket, url]; + const supportedFeatures = await this._invokeNegotiation(info, operationType, negotiationCallback, url, socket); + return new ConnectSecuredTcpSocketResult(url, socket, supportedFeatures); } catch { // ignored } @@ -77,6 +79,31 @@ export class TcpUtils { } const socket = await this.connect(info.url, serverCertificate, clientCertificate); - return [socket, info.url]; + const supportedFeatures = await this._invokeNegotiation(info, operationType, negotiationCallback, info.url, socket); + return new ConnectSecuredTcpSocketResult(info.url, socket, supportedFeatures); + } + + private static _invokeNegotiation(info: TcpConnectionInfo, operationType: OperationTypes, negotiationCallback: NegotiationCallback, url: string, socket: Socket) { + switch (operationType) { + case "Subscription": + return negotiationCallback(url, info, socket); + default: + throwError("NotSupportedException", "Operation type '" + operationType + "' not supported"); + } + } +} + +type NegotiationCallback = (url: string, info: TcpConnectionInfo, socket: Socket) => Promise; + +export class ConnectSecuredTcpSocketResult { + url: string; + socket: Socket; + supportedFeatures: SupportedFeatures; + + + constructor(url: string, socket: Socket, supportedFeatures: SupportedFeatures) { + this.url = url; + this.socket = socket; + this.supportedFeatures = supportedFeatures; } } diff --git a/src/Utility/TypeUtil.ts b/src/Utility/TypeUtil.ts index 18c9c7081..7615b67d9 100644 --- a/src/Utility/TypeUtil.ts +++ b/src/Utility/TypeUtil.ts @@ -66,7 +66,7 @@ export class TypeUtil { && (!!value.prototype && !!value.prototype.constructor.name); } - public static isObjectTypeDescriptor(value: any): boolean { + public static isObjectTypeDescriptor(value: any): value is ObjectTypeDescriptor { return !!value && typeof value !== "string" && (this.isClass(value) || this.isObjectLiteralTypeDescriptor(value)); diff --git a/src/index.ts b/src/index.ts index 510855c7b..7c77726b1 100644 --- a/src/index.ts +++ b/src/index.ts @@ -64,6 +64,9 @@ export * from "./ServerWide/Operations/Configuration/GetServerWideClientConfigur export * from "./ServerWide/Operations/Configuration/GetServerWideBackupConfigurationsOperation"; export * from "./ServerWide/Operations/Configuration/PutServerWideBackupConfigurationOperation"; export * from "./ServerWide/Operations/Configuration/ServerWideBackupConfiguration"; +export * from "./ServerWide/Operations/Configuration/DatabaseSettings"; +export * from "./ServerWide/Operations/Configuration/GetDatabaseSettingsOperation"; +export * from "./ServerWide/Operations/Configuration/PutDatabaseSettingsOperation"; export { GetDatabaseTopologyCommand } from "./ServerWide/Commands/GetDatabaseTopologyCommand"; @@ -352,6 +355,7 @@ export * from "./Documents/Indexes/IndexStats"; export * from "./Documents/Indexes/IndexSourceType"; export * from "./Documents/Indexes"; export * from "./Documents/Indexes/StronglyTyped"; +export * from "./Documents/Indexes/IndexDefinitionBase"; export * from "./Documents/Indexes/Analysis/AnalyzerDefinition"; export * from "./Documents/Indexes/AbstractCsharpIndexCreationTask"; export * from "./Documents/Indexes/AbstractCsharpMultiMapIndexCreationTask"; @@ -450,17 +454,18 @@ export * from "./Documents/Session/ISessionDocumentTimeSeries"; export * from "./Documents/Session/ISessionDocumentTypedAppendTimeSeriesBase"; export * from "./Documents/Session/ISessionDocumentTypedTimeSeries"; export * from "./Documents/Session/JavaScriptMap"; -export * from "./Documents/Session/IMetadataDictionary"; -export * from "./Documents/Session/DocumentResultStream"; -export * from "./Documents/Session/SessionDocumentRollupTypedTimeSeries"; -export * from "./Documents/Session/SessionDocumentTimeSeries"; -export * from "./Documents/Session/SessionDocumentTypedTimeSeries"; -export * from "./Documents/Session/SessionTimeSeriesBase"; +export * from "./Documents/Session/IMetadataDictionary"; +export * from "./Documents/Session/DocumentResultStream"; +export * from "./Documents/Session/SessionDocumentRollupTypedTimeSeries"; +export * from "./Documents/Session/SessionDocumentTimeSeries"; +export * from "./Documents/Session/SessionDocumentTypedTimeSeries"; +export * from "./Documents/Session/SessionTimeSeriesBase"; export * from "./Documents/Session/Loaders/ICounterIncludeBuilder"; export * from "./Documents/Session/Loaders/IAbstractTimeSeriesIncludeBuilder"; export * from "./Documents/Session/Loaders/ICompareExchangeValueIncludeBuilder"; export * from "./Documents/Session/Loaders/IDocumentIncludeBuilder"; export * from "./Documents/Session/Loaders/IGenericIncludeBuilder"; +export * from "./Documents/Session/Loaders/IGenericRevisionIncludeBuilder"; export * from "./Documents/Session/Loaders/IGenericTimeSeriesIncludeBuilder"; export * from "./Documents/Session/Loaders/ISubscriptionIncludeBuilder"; export * from "./Documents/Session/Loaders/ISubscriptionTimeSeriesIncludeBuilder"; diff --git a/test/Documents/Commands/PutDocumentCommandTests.ts b/test/Documents/Commands/PutDocumentCommandTests.ts index 8b47a69b9..8810b7fa2 100644 --- a/test/Documents/Commands/PutDocumentCommandTests.ts +++ b/test/Documents/Commands/PutDocumentCommandTests.ts @@ -5,6 +5,7 @@ import { IDocumentStore, PutDocumentCommand, } from "../../../src"; +import { assertThat } from "../../Utils/AssertExtensions"; describe("PutDocumentCommand", function () { @@ -43,4 +44,37 @@ describe("PutDocumentCommand", function () { assert.strictEqual(loadedUser.age, user.age); assert.strictEqual(loadedUser.constructor, User); }); + + + it("canPutDocumentUsingCommandWithSurrogatePairs", async () => { + const nameWithEmojis = "Marcin \uD83D\uDE21\uD83D\uDE21\uD83E\uDD2C\uD83D\uDE00😡😡🤬😀"; + + const user = new User(); + user.name = nameWithEmojis; + user.age = 31; + + const node = store.conventions.objectMapper.toObjectLiteral(user); + + const command = new PutDocumentCommand("users/2", null, node); + await store.getRequestExecutor().execute(command); + + const result = command.result; + + + assertThat(result.id) + .isEqualTo("users/2"); + assertThat(result.changeVector) + .isNotNull(); + + const session = store.openSession(); + try { + const loadedUser = await session.load("users/2", User); + + assertThat(loadedUser.name) + .isEqualTo(nameWithEmojis); + } finally { + session.dispose(); + } + }); + }); diff --git a/test/Ported/DatabaseSettingsOperationTest.ts b/test/Ported/DatabaseSettingsOperationTest.ts new file mode 100644 index 000000000..0b09711bf --- /dev/null +++ b/test/Ported/DatabaseSettingsOperationTest.ts @@ -0,0 +1,72 @@ +import { IDocumentStore, ToggleDatabasesStateOperation } from "../../src"; +import { disposeTestDocumentStore, testContext } from "../Utils/TestUtil"; +import { + PutDatabaseSettingsOperation +} from "../../src/ServerWide/Operations/Configuration/PutDatabaseSettingsOperation"; +import { + GetDatabaseSettingsOperation +} from "../../src/ServerWide/Operations/Configuration/GetDatabaseSettingsOperation"; +import { assertThat } from "../Utils/AssertExtensions"; +import { DatabaseSettings } from "../../src/ServerWide/Operations/Configuration/DatabaseSettings"; + + +describe("DatabaseSettingsOperationTest", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("checkIfConfigurationSettingsIsEmpty", async () => { + await checkIfOurValuesGotSaved(store, {}); + }); + + it("changeSingleSettingKeyOnServer", async () => { + const settings = { + "Storage.PrefetchResetThresholdInGb": "10" + } + await putConfigurationSettings(store, settings); + await checkIfOurValuesGotSaved(store, settings); + }); + + it("changeMultipleSettingsKeysOnServer", async () => { + const settings = { + "Storage.PrefetchResetThresholdInGb": "10", + "Storage.TimeToSyncAfterFlushInSec": "35", + "Tombstones.CleanupIntervalInMin": "10" + }; + + await putConfigurationSettings(store, settings); + await checkIfOurValuesGotSaved(store, settings); + }) + + async function putConfigurationSettings(store: IDocumentStore, settings: Record) { + await store.maintenance.send(new PutDatabaseSettingsOperation(store.database, settings)); + await store.maintenance.server.send(new ToggleDatabasesStateOperation(store.database, true)); + await store.maintenance.server.send(new ToggleDatabasesStateOperation(store.database, false)); + } + + async function getConfigurationSettings(store: IDocumentStore): Promise { + const settings = await store.maintenance.send(new GetDatabaseSettingsOperation(store.database)); + assertThat(settings) + .isNotNull(); + return settings; + } + + async function checkIfOurValuesGotSaved(store: IDocumentStore, data: Record) { + const settings = await getConfigurationSettings(store); + + Object.keys(data).forEach(key => { + const configurationValue = settings.settings[key]; + assertThat(configurationValue) + .isNotNull(); + assertThat(configurationValue) + .isEqualTo(data[key]); + }) + } + +}); diff --git a/test/Ported/Documents/CanQueryAndIncludeRevisions.ts b/test/Ported/Documents/CanQueryAndIncludeRevisions.ts new file mode 100644 index 000000000..d5101165f --- /dev/null +++ b/test/Ported/Documents/CanQueryAndIncludeRevisions.ts @@ -0,0 +1,458 @@ +import { AbstractJavaScriptIndexCreationTask, IDocumentStore } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { assertThat } from "../../Utils/AssertExtensions"; +import { delay } from "bluebird"; + +describe("CanQueryAndIncludeRevisionsTest", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("query_IncludeAllQueryFunctionality", async () => { + const cvList: string[] = []; + + const id = "users/rhino"; + + await testContext.setupRevisions(store, false, 5); + + { + const session = store.openSession(); + const user = new User(); + user.name = "Omer"; + await session.store(user, id); + await session.saveChanges(); + } + + let changeVector: string; + + const beforeDateTime = new Date(); + + { + const session = store.openSession(); + let metadatas = await session.advanced.revisions.getMetadataFor(id); + assertThat(metadatas) + .hasSize(1); + + changeVector = metadatas[0]["@change-vector"]; + + session.advanced.patch(id, "firstRevision", changeVector); + + await session.saveChanges(); + + cvList.push(changeVector); + + metadatas = await session.advanced.revisions.getMetadataFor(id); + + changeVector = metadatas[0]["@change-vector"]; + + cvList.push(changeVector); + + await session.advanced.patch(id, "secondRevision", changeVector); + + await session.saveChanges(); + + metadatas = await session.advanced.revisions.getMetadataFor(id); + + changeVector = metadatas[0]["@change-vector"]; + + cvList.push(changeVector); + + await session.advanced.patch(id, "changeVectors", cvList); + await session.saveChanges(); + } + + { + const session = store.openSession(); + const query = await session.query(User) + .include(builder => { + builder.includeRevisions("changeVectors") + .includeRevisions("firstRevision") + .includeRevisions("secondRevision") + }) + .waitForNonStaleResults(); + + await query.all(); + + const revision1 = await session.advanced.revisions.get(cvList[0], User); + const revision2 = await session.advanced.revisions.get(cvList[1], User); + const revision3 = await session.advanced.revisions.get(cvList[2], User); + + assertThat(revision1) + .isNotNull(); + assertThat(revision2) + .isNotNull(); + assertThat(revision3) + .isNotNull(); + + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + }); + + it("load_IncludeBuilder_IncludeRevisionByChangeVector", async () => { + const id = "users/rhino"; + + await testContext.setupRevisions(store, false, 5); + + let changeVector; + + { + const session = store.openSession(); + const user = new User(); + user.name = "Omer"; + await session.store(user, id); + await session.saveChanges(); + + const metadatas = await session.advanced.revisions.getMetadataFor(id); + assertThat(metadatas) + .hasSize(1); + + changeVector = metadatas[0]["@change-vector"]; + + await session.advanced.patch(id, "changeVector", changeVector); + await session.saveChanges(); + } + + { + const session = store.openSession(); + const query = await session.load(id, { + documentType: User, + includes: builder => builder.includeRevisions("changeVector") + }); + + const revision = await session.advanced.revisions.get(changeVector, User); + assertThat(revision) + .isNotNull(); + + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + }); + + it("load_IncludeBuilder_IncludeRevisionByChangeVectors", async () => { + const cvList: string[] = []; + + const id = "users/rhino"; + + await testContext.setupRevisions(store, false, 5); + + { + const session = store.openSession(); + const user = new User(); + user.name = "Omer"; + await session.store(user, id); + await session.saveChanges(); + } + + let changeVector: string; + + { + const session = store.openSession(); + let metadatas = await session.advanced.revisions.getMetadataFor(id); + assertThat(metadatas) + .hasSize(1); + + changeVector = metadatas[0]["@change-vector"]; + await session.saveChanges(); + cvList.push(changeVector); + + metadatas = await session.advanced.revisions.getMetadataFor(id); + changeVector = metadatas[0]["@change-vector"]; + cvList.push(changeVector); + + await session.saveChanges(); + metadatas = await session.advanced.revisions.getMetadataFor(id); + changeVector = metadatas[0]["@change-vector"]; + + cvList.push(changeVector); + await session.advanced.patch(id, "changeVectors", cvList); + await session.saveChanges(); + } + + { + const session = store.openSession(); + const query = await session.load(id, { + documentType: User, + includes: builder => builder.includeRevisions("changeVectors") + }); + + const revision1 = await session.advanced.revisions.get(cvList[0], User); + const revision2 = await session.advanced.revisions.get(cvList[1], User); + const revision3 = await session.advanced.revisions.get(cvList[2], User); + + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + }); + + it("load_IncludeBuilder_IncludeRevisionsByProperty_ChangeVectorAndChangeVectors", async () => { + const cvList: string[] = []; + + const id = "users/rhino"; + + await testContext.setupRevisions(store, false, 5); + + { + const session = store.openSession(); + const user = new User(); + user.name = "Omer"; + await session.store(user, id); + await session.saveChanges(); + } + + let changeVector: string; + + { + const session = store.openSession(); + let metadatas = await session.advanced.revisions.getMetadataFor(id); + assertThat(metadatas) + .hasSize(1); + + changeVector = metadatas[0]["@change-vector"]; + session.advanced.patch(id, "firstRevision", changeVector); + await session.saveChanges(); + cvList.push(changeVector); + + metadatas = await session.advanced.revisions.getMetadataFor(id); + changeVector = metadatas[0]["@change-vector"]; + cvList.push(changeVector); + session.advanced.patch(id, "secondRevision", changeVector); + await session.saveChanges(); + + metadatas = await session.advanced.revisions.getMetadataFor(id); + changeVector = metadatas[0]["@change-vector"]; + cvList.push(changeVector); + session.advanced.patch(id, "changeVectors", cvList); + await session.saveChanges(); + } + + { + const session = store.openSession(); + await session.load(id, { + documentType: User, + includes: builder => builder.includeRevisions("changeVectors").includeRevisions("firstRevision").includeRevisions("secondRevision") + }); + + const revision1 = await session.advanced.revisions.get(cvList[0], User); + const revision2 = await session.advanced.revisions.get(cvList[1], User); + const revision3 = await session.advanced.revisions.get(cvList[2], User); + + const revisions = await session.advanced.revisions.get(cvList, User); + + assertThat(revision1) + .isNotNull(); + assertThat(revision2) + .isNotNull(); + assertThat(revision3) + .isNotNull(); + assertThat(revisions) + .isNotNull(); + + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + }); + + it("load_IncludeBuilder_IncludeRevisionByDateTime_VerifyUtc", async () => { + let changeVector: string; + + const id = "users/rhino"; + + await testContext.setupRevisions(store, false, 5); + + { + const session = store.openSession(); + const user = new User(); + user.name = "Omer"; + await session.store(user, id); + await session.saveChanges(); + + const metadatas = await session.advanced.revisions.getMetadataFor(id); + assertThat(metadatas) + .hasSize(1); + + changeVector = metadatas[0]["@change-vector"]; + await session.advanced.patch(id, "changeVector", changeVector); + await session.advanced.patch(id, "changeVectors", [changeVector]); + } + + const dateTime = new Date(); + + await delay(2); + + { + const session = store.openSession(); + + const query = await session.load(id, { + documentType: User, + includes: builder => builder + .includeRevisions(dateTime) + .includeRevisions("changeVector") + .includeRevisions("changeVectors") + }); + + const revision = await session.advanced.revisions.get(id, dateTime, User); + const revision2 = await session.advanced.revisions.get(changeVector, User); + + assertThat(query) + .isNotNull(); + assertThat(revision) + .isNotNull(); + assertThat(revision2) + .isNotNull(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + }); + + it("query_IncludeBuilder_IncludeRevisionBefore", async () => { + let changeVector: string; + + const id = "users/rhino"; + + await testContext.setupRevisions(store, true, 5); + + { + const session = store.openSession(); + const user = new User(); + user.name = "Omer"; + await session.store(user, id); + await session.saveChanges(); + + const metadatas = await session.advanced.revisions.getMetadataFor(id); + assertThat(metadatas) + .hasSize(1); + + changeVector = metadatas[0]["@change-vector"]; + } + + const beforeDateTime = new Date(); + + await delay(2); + + { + const session = store.openSession(); + const query = session.query(User) + .waitForNonStaleResults() + .include(builder => builder.includeRevisions(beforeDateTime)); + + const users = await query.all(); + + const revision = await session.advanced.revisions.get(changeVector, User); + assertThat(users) + .isNotNull(); + assertThat(revision) + .isNotNull(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + }); + + it("query_RawQueryChangeVectorInsidePropertyWithIndex", async () => { + const id = "users/rhino"; + + await testContext.setupRevisions(store, true, 5); + + { + const session = store.openSession(); + const user = new User(); + user.name = "Omer"; + await session.store(user, id); + await session.saveChanges(); + } + + let changeVector: string; + + { + const session = store.openSession(); + const metadatas = await session.advanced.revisions.getMetadataFor(id); + assertThat(metadatas) + .hasSize(1); + + changeVector = metadatas[0]["@change-vector"]; + + await session.advanced.patch(id, "changeVector", changeVector); + await session.saveChanges(); + } + + { + const session = store.openSession(); + const query = await session.advanced + .rawQuery("from Users where name = 'Omer' include revisions($p0)", User) + .addParameter("p0", "changeVector") + .waitForNonStaleResults() + .all(); + + const revision = await session.advanced.revisions.get(changeVector, User); + assertThat(revision) + .isNotNull(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + }); + + it("query_RawQueryGetRevisionBeforeDateTime", async () => { + const id = "user/rhino"; + + await testContext.setupRevisions(store, true, 5); + + let changeVector: string; + + { + const session = store.openSession(); + const user = new User(); + user.name = "Omer"; + await session.store(user, id); + await session.saveChanges(); + + const metadatas = await session.advanced.revisions.getMetadataFor(id); + assertThat(metadatas) + .hasSize(1) + + changeVector = metadatas[0]["@change-vector"]; + } + + { + const session = store.openSession(); + const getRevisionsBefore = new Date(); + const query = await session.advanced.rawQuery("from Users include revisions($p0)", User) + .addParameter("p0", getRevisionsBefore) + .waitForNonStaleResults() + .all(); + + const revision = await session.advanced.revisions.get(changeVector, User); + + assertThat(query) + .isNotNull(); + assertThat(revision) + .isNotNull(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + }) +}); + +class NameIndex extends AbstractJavaScriptIndexCreationTask { + public constructor() { + super(); + + this.map(User, u => ({ + name: u.name + })); + } +} + +class User { + name: string; + changeVector: string; + firstRevision: string; + secondRevision: string; + thirdRevision: string; + changeVectors: string[]; +} diff --git a/test/Ported/Graph/BasicGraphQueriesTest.ts b/test/Ported/Graph/BasicGraphQueriesTest.ts index bec28b92e..2bc459e8f 100644 --- a/test/Ported/Graph/BasicGraphQueriesTest.ts +++ b/test/Ported/Graph/BasicGraphQueriesTest.ts @@ -17,7 +17,7 @@ describe("BasicGraphQueriesTest", function () { await disposeTestDocumentStore(store)); it("query_with_no_matches_and_select_should_return_empty_result", async () => { - await testContext.createDogDataWithoutEdges(store); + await testContext.samples.createDogDataWithoutEdges(store); { const session = store.openSession(); @@ -34,7 +34,7 @@ describe("BasicGraphQueriesTest", function () { }); it("query_with_no_matches_and_without_select_should_return_empty_result", async () => { - await testContext.createDogDataWithoutEdges(store); + await testContext.samples.createDogDataWithoutEdges(store); { const session = store.openSession(); @@ -46,7 +46,7 @@ describe("BasicGraphQueriesTest", function () { }); it("empty_vertex_node_should_work", async () => { - await testContext.createMoviesData(store); + await testContext.samples.createMoviesData(store); { const session = store.openSession(); @@ -58,7 +58,7 @@ describe("BasicGraphQueriesTest", function () { }); it("can_flatten_result_for_single_vertex_in_row", async () => { - await testContext.createMoviesData(store); + await testContext.samples.createMoviesData(store); { const session = store.openSession(); @@ -75,7 +75,7 @@ describe("BasicGraphQueriesTest", function () { }); it("mutliple_results_in_row_wont_flatten_results", async () => { - await testContext.createMoviesData(store); + await testContext.samples.createMoviesData(store); { const session = store.openSession(); @@ -92,7 +92,7 @@ describe("BasicGraphQueriesTest", function () { }); it("can_query_without_collection_identifier", async () => { - await testContext.createMoviesData(store); + await testContext.samples.createMoviesData(store); { const session = store.openSession(); @@ -113,7 +113,7 @@ describe("BasicGraphQueriesTest", function () { }); it("can_use_explicit_with_clause", async () => { - await testContext.createMoviesData(store); + await testContext.samples.createMoviesData(store); { const session = store.openSession(); @@ -131,7 +131,7 @@ describe("BasicGraphQueriesTest", function () { }); it("can_filter_vertices_with_explicit_with_clause", async () => { - await testContext.createMoviesData(store); + await testContext.samples.createMoviesData(store); { const session = store.openSession(); @@ -150,7 +150,7 @@ describe("BasicGraphQueriesTest", function () { }); it("findReferences", async () => { - await testContext.createSimpleData(store); + await testContext.samples.createSimpleData(store); { const session = store.openSession(); @@ -207,7 +207,7 @@ async function query(documentType: DocumentType, mutate?.(store); if (parameters.waitForIndexing) { - await testContext.waitForIndexing(store); + await testContext.indexes.waitForIndexing(store); } { @@ -222,4 +222,4 @@ async function query(documentType: DocumentType, } finally { store.dispose(); } -} \ No newline at end of file +} diff --git a/test/Ported/HiLoTest.ts b/test/Ported/HiLoTest.ts index 685576e21..733efa306 100644 --- a/test/Ported/HiLoTest.ts +++ b/test/Ported/HiLoTest.ts @@ -3,11 +3,11 @@ import * as assert from "assert"; import { testContext, disposeTestDocumentStore } from "../Utils/TestUtil"; import { - IDocumentStore, HiloIdGenerator, DocumentStore, MultiDatabaseHiLoIdGenerator, } from "../../src"; import { ArrayUtil } from "../../src/Utility/ArrayUtil"; +import { assertThat } from "../Utils/AssertExtensions"; describe("HiLo", function () { @@ -20,7 +20,7 @@ describe("HiLo", function () { public productName: string; } - let store: IDocumentStore; + let store: DocumentStore; beforeEach(async function () { store = await testContext.getDocumentStore(); @@ -245,4 +245,86 @@ describe("HiLo", function () { assert.strictEqual(idNumbers[i - 1], i); } }); + + it("generate_HiLo_Ids", async () => { + const multiDbHiLo = new MultiDatabaseHiLoIdGenerator(store); + + const usersIds = new Map(); + const productsIds = new Map(); + + const count = 10; + const tasks: Promise[] = []; + + for (let i = 0; i < count; i++) { + tasks.push(new Promise(async resolve => { + let id = await multiDbHiLo.generateNextIdFor(null, "Users"); + + assertThat(usersIds.has(id)) + .isFalse(); + usersIds.set(id, true); + + id = await multiDbHiLo.generateNextIdFor(null, "Products"); + assertThat(productsIds.has(id)) + .isFalse(); + productsIds.set(id, true); + + resolve(); + })); + } + + await Promise.all(tasks); + + assertThat(usersIds) + .hasSize(count); + assertThat(productsIds) + .hasSize(count); + + tasks.length = 0; + + const task = async () => { + let id = await multiDbHiLo.generateNextIdFor(null, "Users"); + + assertThat(usersIds.has(id)) + .isFalse(); + usersIds.set(id, true); + + id = await store.hiLoIdGenerator.generateNextIdFor(null, "Products"); + assertThat(productsIds.has(id)) + .isFalse(); + productsIds.set(id, true); + + id = await store.hiLoIdGenerator.generateNextIdFor(null, User); + assertThat(usersIds.has(id)) + .isFalse(); + usersIds.set(id, true); + + id = await store.hiLoIdGenerator.generateNextIdFor(null, new Product()); + assertThat(productsIds.has(id)) + .isFalse(); + productsIds.set(id, true); + + id = await store.hiLoIdGenerator.generateNextIdFor(null, new User()); + assertThat(usersIds.has(id)) + .isFalse(); + usersIds.set(id, true); + + id = await store.hiLoIdGenerator.generateNextIdFor(null, Product); + assertThat(productsIds.has(id)) + .isFalse(); + productsIds.set(id, true); + } + + + for (let i = 0; i < count; i++) { + tasks.push(task()); + } + + await Promise.all(tasks); + + assertThat(usersIds) + .hasSize(count * 4); + assertThat(productsIds) + .hasSize(count * 4); + }); + }); diff --git a/test/Ported/HttpsTest.ts b/test/Ported/HttpsTest.ts index d46dd02a7..9c6477208 100644 --- a/test/Ported/HttpsTest.ts +++ b/test/Ported/HttpsTest.ts @@ -274,11 +274,14 @@ async function extractCertificate(certificateRawData: CertificateRawData) { const entryText = await readToEnd(entry); const lines = entryText.split(/\r?\n/); cert = lines.slice(1, lines.length - 2).join("\r\n"); + break; } else { entry.autodrain(); } } + stream.destroy(); + return cert; } diff --git a/test/Ported/Issues/RDBC_501.ts b/test/Ported/Issues/RDBC_501.ts new file mode 100644 index 000000000..e0450f4f4 --- /dev/null +++ b/test/Ported/Issues/RDBC_501.ts @@ -0,0 +1,112 @@ +import { IDocumentStore, TimeSeriesAggregationResult, TimeSeriesValue } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import moment = require("moment"); +import { assertThat } from "../../Utils/AssertExtensions"; + +describe("RDBC_501Test", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("shouldProperlyMapTypedEntries", async () => { + const baseLine = moment().utc().startOf("day"); + + { + const session = store.openSession(); + const symbol = new MarkerSymbol(); + await session.store(symbol, "markerSymbols/1-A"); + + const price1 = new SymbolPrice(); + price1.low = 1; + price1.high = 10; + price1.open = 4; + price1.close = 7; + + const price2 = new SymbolPrice(); + price2.low = 21; + price2.high = 210; + price2.open = 24; + price2.close = 27; + + const price3 = new SymbolPrice(); + price3.low = 321; + price3.high = 310; + price3.open = 34; + price3.close = 37; + + const tsf = session.timeSeriesFor(symbol, "history", SymbolPrice); + + tsf.append(baseLine.clone().add(1, "hours").toDate(), price1); + tsf.append(baseLine.clone().add(2, "hours").toDate(), price2); + tsf.append(baseLine.clone().add(2, "days").toDate(), price3); + + await session.saveChanges(); + } + + { + const session = store.openSession(); + const aggregatedHistoryQueryResult = await session.query(MarkerSymbol) + .selectTimeSeries(b => b.raw("from history\n" + + " group by '1 days'\n" + + " select first(), last(), min(), max()"), TimeSeriesAggregationResult) + .first(); + + assertThat(aggregatedHistoryQueryResult.results) + .hasSize(2); + + const typed = aggregatedHistoryQueryResult.asTypedEntry(SymbolPrice); + + assertThat(typed.results) + .hasSize(2); + + const firstResult = typed.results[0]; + assertThat(firstResult.min.open) + .isEqualTo(4); + assertThat(firstResult.min.close) + .isEqualTo(7); + assertThat(firstResult.min.low) + .isEqualTo(1); + assertThat(firstResult.min.high) + .isEqualTo(10); + + assertThat(firstResult.first.open) + .isEqualTo(4); + assertThat(firstResult.first.close) + .isEqualTo(7); + assertThat(firstResult.first.low) + .isEqualTo(1); + assertThat(firstResult.first.high) + .isEqualTo(10); + + const secondResult = typed.results[1]; + + assertThat(secondResult.min.open) + .isEqualTo(34); + assertThat(secondResult.min.close) + .isEqualTo(37); + assertThat(secondResult.min.low ) + .isEqualTo(321); + assertThat(secondResult.min.high) + .isEqualTo(310); + } + }); +}); + +class SymbolPrice { + public static readonly TIME_SERIES_VALUES: TimeSeriesValue = ["open", "close", "high", "low"]; + + public open: number; + public close: number; + public high: number; + public low: number; +} + +class MarkerSymbol { + id: string; +} diff --git a/test/Ported/Issues/RDBC_538.ts b/test/Ported/Issues/RDBC_538.ts new file mode 100644 index 000000000..4910e53b6 --- /dev/null +++ b/test/Ported/Issues/RDBC_538.ts @@ -0,0 +1,90 @@ +import { DatabaseRecord, DocumentStore } from "../../../src"; +import { ClusterTestContext, RavenTestContext } from "../../Utils/TestUtil"; +import { User } from "../../Assets/Entities"; +import { assertThat } from "../../Utils/AssertExtensions"; + +(RavenTestContext.isPullRequest ? describe.skip : describe)("RDBC_538Test", function () { + + let testContext: ClusterTestContext; + + beforeEach(async function () { + testContext = new ClusterTestContext(); + }); + + afterEach(async () => testContext.dispose()); + + it("canHandleSubscriptionRedirect", async () => { + const cluster = await testContext.createRaftCluster(3); + try { + const leader = cluster.getInitialLeader(); + + const databaseName = testContext.getDatabaseName(); + + // create database on single node + + const databaseRecord: DatabaseRecord = { + databaseName + }; + + await cluster.createDatabase(databaseRecord, 3, cluster.getInitialLeader().url); + + let id: string; + + { + // save document + const store = new DocumentStore(leader.url, databaseName); + try { + store.initialize(); + + { + const session = store.openSession(); + const user1 = new User(); + user1.age = 31; + await session.store(user1, "users/1"); + await session.saveChanges(); + } + + id = await store.subscriptions.create(User); + } finally { + store.dispose(); + } + } + + + // now open store on leader + { + const store = new DocumentStore(leader.url, databaseName); + try { + store.initialize(); + + const subscription = store.subscriptions.getSubscriptionWorker({ + documentType: User, + subscriptionName: id + }); + + let key: string; + + await new Promise((resolve, reject) => { + subscription.on("error", reject); + subscription.on("batch", (batch, callback) => { + key = batch.items[0].id; + callback(); + resolve(); + }) + }) + + assertThat(key) + .isNotNull() + .isEqualTo("users/1"); + + await store.subscriptions.delete(id); + + } finally { + store.dispose(); + } + } + } finally { + cluster.dispose(); + } + }); +}); diff --git a/test/Ported/Issues/RavenDB_14939.ts b/test/Ported/Issues/RavenDB_14939.ts index 16d763c6d..9f197fb26 100644 --- a/test/Ported/Issues/RavenDB_14939.ts +++ b/test/Ported/Issues/RavenDB_14939.ts @@ -46,7 +46,7 @@ describe("RavenDB_14939Test", function () { await store.maintenance.send(new ResetIndexOperation(new MyIndex(analyzerName).getIndexName())); - const errors = await testContext.waitForIndexingErrors(store, 10_000); + const errors = await testContext.indexes.waitForIndexingErrors(store, 10_000); assertThat(errors) .hasSize(1); assertThat(errors[0].errors) @@ -112,4 +112,4 @@ class MyIndex extends AbstractJavaScriptIndexCreationTask { this.index("name", "Search"); this.analyze("name", analyzerName); } -} \ No newline at end of file +} diff --git a/test/Ported/Issues/RavenDB_16328_Analyzers.ts b/test/Ported/Issues/RavenDB_16328_Analyzers.ts index 83b996642..e704a6dbf 100644 --- a/test/Ported/Issues/RavenDB_16328_Analyzers.ts +++ b/test/Ported/Issues/RavenDB_16328_Analyzers.ts @@ -46,7 +46,7 @@ describe("RavenDB_16328_AnalyzersTest", function () { await store.maintenance.send(new ResetIndexOperation(new MyIndex(analyzerName).getIndexName())); - const errors = await testContext.waitForIndexingErrors(store, 10_000); + const errors = await testContext.indexes.waitForIndexingErrors(store, 10_000); assertThat(errors) .hasSize(1); assertThat(errors[0].errors) diff --git a/test/Ported/Issues/RavenDB_16614.ts b/test/Ported/Issues/RavenDB_16614.ts new file mode 100644 index 000000000..7eaa12e67 --- /dev/null +++ b/test/Ported/Issues/RavenDB_16614.ts @@ -0,0 +1,59 @@ + +import { DocumentStore, SessionOptions } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { User } from "../../Assets/Entities"; +import { assertThat, assertThrows } from "../../Utils/AssertExtensions"; + +describe("RavenDB_16614Test", function () { + + let store: DocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("modificationInAnotherTransactionWillFailWithDelete", async () => { + const sessionOptions: SessionOptions = { + transactionMode: "ClusterWide" + }; + + { + const session = store.openSession(sessionOptions); + const user1 = new User(); + user1.name = "arava"; + + const user2 = new User(); + user2.name = "phoebe"; + + await session.store(user1, "users/arava"); + await session.store(user2, "users/phoebe"); + await session.saveChanges(); + session.dispose(); + } + + { + const session = store.openSession(sessionOptions); + + const user = await session.load("users/arava", User); + await session.delete(user); + const user2 = await session.load("users/phoebe", User); + user2.name = "Phoebe Eini"; + + { + const conflictedSession = store.openSession(sessionOptions); + const conflictedArava = await conflictedSession.load("users/arava", User); + conflictedArava.name = "Arava!"; + await conflictedSession.saveChanges(); + conflictedSession.dispose(); + } + + await assertThrows(async () => await session.saveChanges(), e => { + assertThat(e.name) + .isEqualTo("ClusterTransactionConcurrencyException"); + }); + } + }); +}); diff --git a/test/Ported/Issues/RavenDB_16929.ts b/test/Ported/Issues/RavenDB_16929.ts new file mode 100644 index 000000000..d441e5bab --- /dev/null +++ b/test/Ported/Issues/RavenDB_16929.ts @@ -0,0 +1,62 @@ +import { IDocumentStore } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { assertThat } from "../../Utils/AssertExtensions"; + +describe("RavenDB_16929Test", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("documentWithStringWithNullCharacterAtEndShouldNotHaveChangeOnLoad", async () => { + { + const session = store.openSession(); + const doc = new TestDoc(); + doc.id = "doc/1"; + doc.descriptionChar = "a"; + doc.description = "TestString\0"; + await session.store(doc); + await session.saveChanges(); + } + + { + const session = store.openSession(); + const doc = await session.load("doc/1", TestDoc); + const t = doc.description; + assertThat(session.advanced.hasChanges()) + .isFalse(); + assertThat(session.advanced.hasChanged(doc)) + .isFalse(); + } + }); + + it("documentWithEmptyCharShouldNotHaveChangeOnLoad", async () => { + { + const session = store.openSession(); + const doc = new TestDoc(); + doc.id = "doc/1"; + await session.store(doc); + await session.saveChanges(); + } + + { + const session = store.openSession(); + const doc = await session.load("doc/1", TestDoc); + assertThat(session.advanced.hasChanges()) + .isFalse(); + assertThat(session.advanced.hasChanged(doc)) + .isFalse(); + } + }) +}); + +class TestDoc { + descriptionChar: string; + id: string; + description: string; +} diff --git a/test/Ported/Issues/RavenDB_16975.ts b/test/Ported/Issues/RavenDB_16975.ts new file mode 100644 index 000000000..235591094 --- /dev/null +++ b/test/Ported/Issues/RavenDB_16975.ts @@ -0,0 +1,59 @@ +import { IDocumentStore, SubscriptionCreationOptions } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { assertThat } from "../../Utils/AssertExtensions"; +import { User } from "../../Assets/Entities"; + + +describe("RavenDB_16975Test", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("should_Not_Send_Include_Message", async () => { + + { + const session = store.openSession(); + const person = new User(); + person.name = "Arava"; + await session.store(person, "users/1"); + await session.saveChanges(); + } + + const creationOptions: SubscriptionCreationOptions = { + query: "from Users" + }; + + const id = await store.subscriptions.create(creationOptions); + + const sub = store.subscriptions.getSubscriptionWorker(id); + try { + await new Promise((resolve, reject) => { + sub.on("error", reject); + sub.on("batch", async (batch, callback) => { + + assertThat(batch.items) + .isNotEmpty(); + + { + const s = batch.openSession(); + assertThat(batch.getNumberOfIncludes()) + .isZero(); + assertThat(s.advanced.numberOfRequests) + .isZero(); + } + + resolve(); + callback(); + }); + }); + } finally { + sub.dispose(); + } + }); +}); diff --git a/test/Ported/Issues/RavenDB_16985.ts b/test/Ported/Issues/RavenDB_16985.ts new file mode 100644 index 000000000..cb4077cf8 --- /dev/null +++ b/test/Ported/Issues/RavenDB_16985.ts @@ -0,0 +1,30 @@ +import { IDocumentStore } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { User } from "../../Assets/Entities"; +import { assertThat } from "../../Utils/AssertExtensions"; + +describe("RavenDB_16985Test", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("checkIfHasChangesIsTrueAfterAddingAttachment", async () => { + + const session = store.openSession(); + const user = new User(); + await session.store(user); + await session.saveChanges(); + + session.advanced.attachments.store(user, "my-test.txt", Buffer.from([1])); + + const hasChanges = session.advanced.hasChanges(); + assertThat(hasChanges) + .isTrue(); + }); +}); diff --git a/test/Ported/Issues/RavenDB_17012.ts b/test/Ported/Issues/RavenDB_17012.ts new file mode 100644 index 000000000..8502ad848 --- /dev/null +++ b/test/Ported/Issues/RavenDB_17012.ts @@ -0,0 +1,111 @@ +import { GetStatisticsOperation, IDocumentStore } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { User } from "../../Assets/Entities"; +import { BulkInsertOptions } from "../../../src/Documents/BulkInsertOperation"; +import { assertThat } from "../../Utils/AssertExtensions"; + + +describe("RavenDB_17012Test", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("can_SkipOverwriteIfUnchanged", async () => { + const docsCount = 500; + + const docs: User[] = []; + + { + const bulkInsert = store.bulkInsert(); + try { + for (let i = 0; i < docsCount; i++) { + const user = new User(); + docs.push(user); + await bulkInsert.store(user, i + "") + } + } finally { + await bulkInsert.finish(); + } + } + + let stats = await store.maintenance.send(new GetStatisticsOperation()); + const lastETag = stats.lastDocEtag; + + const options: BulkInsertOptions = { + skipOverwriteIfUnchanged: true + }; + + { + const bulk = store.bulkInsert(options); + + try { + for (let i = 0; i < docsCount; i++) { + const user = new User(); + docs.push(user); + await bulk.store(user, i + ""); + } + } finally { + await bulk.finish(); + } + } + + stats = await store.maintenance.send(new GetStatisticsOperation()); + + assertThat(stats.lastDocEtag) + .isEqualTo(lastETag); + }); + + it("can_SkipOverwriteIfUnchanged_SomeDocuments", async () => { + const docsCount = 500; + + const docs: User[] = []; + + { + const bulkInsert = store.bulkInsert(); + try { + for (let i = 0; i < docsCount; i++) { + const user = new User(); + user.age = i; + docs.push(user); + await bulkInsert.store(user, i + "") + } + } finally { + await bulkInsert.finish(); + } + } + + let stats = await store.maintenance.send(new GetStatisticsOperation()); + const lastETag = stats.lastDocEtag; + + const options: BulkInsertOptions = { + skipOverwriteIfUnchanged: true + }; + + { + const bulkInsert = store.bulkInsert(options); + try { + for (let i = 0; i < docsCount; i++) { + const doc = docs[i]; + if (i % 2 === 0) { + doc.age = 2 * (i+1); + } + + await bulkInsert.store(doc, i + ""); + } + } finally { + await bulkInsert.finish(); + } + } + + stats = await store.maintenance.send(new GetStatisticsOperation()); + assertThat(stats.lastDocEtag) + .isEqualTo(lastETag + docsCount / 2); + }); +}); + diff --git a/test/Ported/Issues/RavenDB_17041.ts b/test/Ported/Issues/RavenDB_17041.ts new file mode 100644 index 000000000..a85e6bb18 --- /dev/null +++ b/test/Ported/Issues/RavenDB_17041.ts @@ -0,0 +1,98 @@ +import { AbstractJavaScriptIndexCreationTask, IDocumentStore } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { assertThat } from "../../Utils/AssertExtensions"; + +describe("RavenDB_17041Test", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("can_Include_Secondary_Level_With_Alias", async () => { + const userIndex = new UserIndex(); + await userIndex.execute(store); + + { + const session = store.openSession(); + const role1 = new RoleData(); + role1.name = "admin"; + role1.role = "role/1"; + + const role2 = new RoleData(); + role2.name = "developer"; + role2.role = "role/2"; + + const roles = [role1, role2]; + + const user = new User(); + user.firstName = "Rhinos"; + user.lastName = "Hiber"; + user.roles = roles; + + await session.store(role1, role1.role); + await session.store(role2, role2.role); + await session.store(user); + await session.saveChanges(); + } + + await testContext.waitForIndexing(store); + + { + const session = store.openSession(); + const query = "from index 'UserIndex' as u " + + "select { firstName : u.firstName, " + + "lastName : u.lastName, " + + "roles : u.roles.map(function(r){return {role:r.role};}) } " + + "include 'u.roles[].role'"; + + const users = await session.advanced.rawQuery(query, User) + .all(); + + assertThat(users) + .hasSize(1); + + let loaded = await session.load("role/1", RoleData); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + assertThat(loaded.role) + .isEqualTo("role/1"); + + loaded = await session.load("role/2", RoleData); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + assertThat(loaded.role) + .isEqualTo("role/2"); + } + }); +}); + + +class UserIndex extends AbstractJavaScriptIndexCreationTask { + + constructor() { + super(); + + this.map("Users", u => ({ + firstName: u.firstName, + lastName: u.lastName, + roles: u.roles + })); + } +} + + +class RoleData { + name: string; + role: string; +} + +class User { + firstName: string; + lastName: string; + roles: RoleData[]; +} diff --git a/test/Ported/Issues/RavenDB_17420.ts b/test/Ported/Issues/RavenDB_17420.ts new file mode 100644 index 000000000..8a3b6ada2 --- /dev/null +++ b/test/Ported/Issues/RavenDB_17420.ts @@ -0,0 +1,41 @@ +import { IDocumentStore } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { assertThat } from "../../Utils/AssertExtensions"; + +describe("RavenDB_17420Test", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("can_use_boost_on_in_query", async () => { + { + const session = store.openSession(); + const item = new Item(); + item.name = "ET"; + await session.store(item); + await session.saveChanges(); + } + + { + const session = store.openSession(); + const first = await session.advanced.documentQuery(Item) + .whereIn("name", ["ET", "Alien"]) + .boost(0) + .first(); + + assertThat(session.advanced.getMetadataFor(first)["@index-score"]) + .isZero(); + } + }); +}); + + +class Item { + name: string; +} diff --git a/test/Ported/Issues/RavenDB_17551.ts b/test/Ported/Issues/RavenDB_17551.ts new file mode 100644 index 000000000..aa8ab6181 --- /dev/null +++ b/test/Ported/Issues/RavenDB_17551.ts @@ -0,0 +1,43 @@ +import { IDocumentStore } from "../../../src"; +import { disposeTestDocumentStore, testContext } from "../../Utils/TestUtil"; +import { User } from "../../Assets/Entities"; +import { assertThat } from "../../Utils/AssertExtensions"; + +describe("RavenDB_17551Test", function () { + + let store: IDocumentStore; + + beforeEach(async function () { + store = await testContext.getDocumentStore(); + }); + + afterEach(async () => + await disposeTestDocumentStore(store)); + + it("canUseOffsetWithCollectionQuery", async () => { + { + const session = store.openSession(); + for (let i = 0; i < 5; i++) { + const user = new User(); + user.name = "i = " + i; + await session.store(user); + } + + await session.saveChanges(); + } + + { + const session = store.openSession(); + assertThat(await session.query(User) + .take(3) + .skip(2) + .all()) + .hasSize(3); + assertThat(await session.query(User) + .take(3) + .skip(3) + .all()) + .hasSize(2); + } + }); +}); diff --git a/test/Ported/Issues/RavenDB_18364.ts b/test/Ported/Issues/RavenDB_18364.ts new file mode 100644 index 000000000..1e368c1b1 --- /dev/null +++ b/test/Ported/Issues/RavenDB_18364.ts @@ -0,0 +1,84 @@ +import { DocumentStore, IDocumentStore } from "../../../src"; +import { ClusterTestContext, RavenTestContext } from "../../Utils/TestUtil"; +import { URL } from "url"; +import { assertThat } from "../../Utils/AssertExtensions"; + +(RavenTestContext.isPullRequest ? describe.skip : describe)("RavenDB_18364", function () { + + let testContext: ClusterTestContext; + + beforeEach(async function () { + testContext = new ClusterTestContext(); + }); + + afterEach(async () => testContext.dispose()); + + it("lazilyLoad_WhenCachedResultAndFailover_ShouldNotReturnReturnNull", async () => { + const cluster = await testContext.createRaftCluster(2); + + try { + const databaseName = testContext.getDatabaseName(); + + await cluster.createDatabase(databaseName, 2, cluster.getInitialLeader().url); + + let store: IDocumentStore; + try { + store = new DocumentStore(cluster.getInitialLeader().url, databaseName); + store.initialize(); + + const id = "testObject/0"; + + { + const session = store.openSession(); + const o = new TestObj(); + o.largeContent = "abcd"; + await session.store(o, id); + session.advanced.waitForReplicationAfterSaveChanges({ + replicas: 1 + }); + await session.saveChanges(); + } + + let firstNodeUrl: string; + + { + const session = store.openSession(); + session.advanced.requestExecutor.on("succeedRequest", event => { + firstNodeUrl = "http://" + new URL(event.url).host; + }); + + await session.load(id, TestObj); + } + + const firstServer = cluster.nodes.find(x => x.url === firstNodeUrl); + await cluster.disposeServer(firstServer.nodeTag); + + { + const session = store.openSession(); + const lazilyLoaded0 = session.advanced.lazily.load(id, TestObj); + const loaded0 = await lazilyLoaded0.getValue(); + assertThat(loaded0) + .isNotNull(); + } + + { + const session = store.openSession(); + const lazilyLoaded0 = session.advanced.lazily.load(id, TestObj); + const loaded0 = await lazilyLoaded0.getValue(); + assertThat(loaded0) + .isNotNull(); + } + } finally { + store.dispose(); + } + } finally { + cluster.dispose(); + } + }); +}); + + +class TestObj { + id: string; + largeContent: string; +} diff --git a/test/Ported/Issues/RavenDB_6292.ts b/test/Ported/Issues/RavenDB_6292.ts index cd265bc35..7f6c42160 100644 --- a/test/Ported/Issues/RavenDB_6292.ts +++ b/test/Ported/Issues/RavenDB_6292.ts @@ -1,4 +1,3 @@ -import * as BluebirdPromise from "bluebird"; import * as assert from "assert"; import { RavenTestContext, testContext, disposeTestDocumentStore } from "../../Utils/TestUtil"; @@ -11,8 +10,6 @@ import { ReplicationTestContext } from "../../Utils/ReplicationTestContext"; import { Address, User } from "../../Assets/Entities"; import { QueryCommand } from "../../../src/Documents/Commands/QueryCommand"; import { tryGetConflict } from "../../../src/Mapping/Json"; -import { Stopwatch } from "../../../src/Utility/Stopwatch"; -import { throwError } from "../../../src/Exceptions"; (RavenTestContext.isPullRequest ? describe.skip : describe)( `${RavenTestContext.isPullRequest ? "[Skipped on PR] " : ""}` + @@ -79,7 +76,7 @@ import { throwError } from "../../../src/Exceptions"; } await replication.setupReplication(source, destination); - await waitForConflict(destination, "addresses/1"); + await testContext.replication.waitForConflict(destination, "addresses/1"); { const session = destination.openSession(); @@ -121,26 +118,6 @@ import { throwError } from "../../../src/Exceptions"; source.dispose(); } - async function waitForConflict(docStore: IDocumentStore, id: string) { - const sw = Stopwatch.createStarted(); - while (sw.elapsed < 10000) { - try { - const session = docStore.openSession(); - await session.load(id); - - await BluebirdPromise.delay(10); - } catch (e) { - if (e.name === "DocumentConflictException") { - return; - } - - throw e; - } - } - - throwError("InvalidOperationException", - "Waited for conflict on '" + id + "' but it did not happen"); - } }); }); }); diff --git a/test/Ported/Issues/RavenDB_6967.ts b/test/Ported/Issues/RavenDB_6967.ts index 0382fcb80..5c526dd36 100644 --- a/test/Ported/Issues/RavenDB_6967.ts +++ b/test/Ported/Issues/RavenDB_6967.ts @@ -73,9 +73,9 @@ describe("RavenDB_6967", function () { await session.saveChanges(); } - await testContext.waitForIndexingErrors(store, 60_000, "Index1"); - await testContext.waitForIndexingErrors(store, 60_000, "Index2"); - await testContext.waitForIndexingErrors(store, 60_000, "Index3"); + await testContext.indexes.waitForIndexingErrors(store, 60_000, "Index1"); + await testContext.indexes.waitForIndexingErrors(store, 60_000, "Index2"); + await testContext.indexes.waitForIndexingErrors(store, 60_000, "Index3"); await store.maintenance.send(new StopIndexingOperation()); diff --git a/test/Ported/RevisionsTest.ts b/test/Ported/RevisionsTest.ts index 0a5d124eb..0a825daab 100644 --- a/test/Ported/RevisionsTest.ts +++ b/test/Ported/RevisionsTest.ts @@ -405,6 +405,14 @@ describe("RevisionsTest", function () { await session.saveChanges(); } + { + const session = store.openSession(); + const revision = session.advanced.lazily.load("users/1", User); + const doc = await revision.getValue(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(1); + } + for (let i = 0; i < 10; i++) { const session = store.openSession(); const user = await session.load(id, Company); @@ -522,6 +530,20 @@ describe("RevisionsTest", function () { assertThat(revisionsLazilyValue.name) .isEqualTo(revisions.name); } + + { + const session = store.openSession(); + const revisions = await session.advanced.revisions.get(cv, User); + const revisionsLazy = session.advanced.revisions.lazily.get(cv, User); + const revisionsLazilyValue = await revisionsLazy.getValue(); + + assertThat(session.advanced.numberOfRequests) + .isEqualTo(2); + assertThat(revisionsLazilyValue.id) + .isEqualTo(revisions.id); + assertThat(revisionsLazilyValue.name) + .isEqualTo(revisions.name); + } }); it("canGetAllRevisionsForDocument_UsingStoreOperation", async function () { diff --git a/test/Ported/Server/DatabasesTest.ts b/test/Ported/Server/DatabasesTest.ts index 27bdfc37b..85ef49cf7 100644 --- a/test/Ported/Server/DatabasesTest.ts +++ b/test/Ported/Server/DatabasesTest.ts @@ -62,7 +62,7 @@ describe("DatabasesTest", function () { }); it("canGetInfoAutoIndexInfo", async () => { - await testContext.createMoviesData(store); + await testContext.samples.createMoviesData(store); { const session = store.openSession(); @@ -114,4 +114,4 @@ describe("DatabasesTest", function () { assertThat(fieldOptions.isNameQuoted) .isFalse(); }); -}); \ No newline at end of file +}); diff --git a/test/Ported/Subscriptions/SubscriptionsBasicTest.ts b/test/Ported/Subscriptions/SubscriptionsBasicTest.ts index 863fe7911..a92f89c1d 100644 --- a/test/Ported/Subscriptions/SubscriptionsBasicTest.ts +++ b/test/Ported/Subscriptions/SubscriptionsBasicTest.ts @@ -7,7 +7,7 @@ import DocumentStore, { SubscriptionWorkerOptions, SubscriptionBatch, SubscriptionCreationOptions, - SubscriptionWorker, ToggleOngoingTaskStateOperation, SubscriptionUpdateOptions + SubscriptionWorker, ToggleOngoingTaskStateOperation, SubscriptionUpdateOptions, Lazy } from "../../../src"; import { AsyncQueue } from "../../Utils/AsyncQueue"; import * as semaphore from "semaphore"; @@ -18,6 +18,7 @@ import { delay } from "bluebird"; import { GetOngoingTaskInfoOperation } from "../../../src/Documents/Operations/GetOngoingTaskInfoOperation"; import { OngoingTaskSubscription } from "../../../src/Documents/Operations/OngoingTasks/OngoingTask"; import { assertThat, assertThrows } from "../../Utils/AssertExtensions"; +import { TimeValue } from "../../../src/Primitives/TimeValue"; describe("SubscriptionsBasicTest", function () { const _reasonableWaitTime = 60 * 1000; @@ -1013,314 +1014,346 @@ describe("SubscriptionsBasicTest", function () { .isEqualTo(state.subscriptionId); }); + it("canCreateSubscriptionWithIncludeTimeSeries_LastRangeByTime", async () => { + const now = testContext.utcToday(); + + const subscriptionCreationOptions: SubscriptionCreationOptions = { + includes: builder => builder.includeTimeSeries("stockPrice", "Last", TimeValue.ofMonths(1)), + documentType: Company + }; + + const name = await store.subscriptions.create(subscriptionCreationOptions); + + const worker = store.subscriptions.getSubscriptionWorker({ + subscriptionName: name, + documentType: Company + }); + + const mre = semaphore(1); + mre.take(TypeUtil.NOOP); + + try { + worker.on("batch", async (batch, callback) => { + const session = batch.openSession(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + const company = await session.load("companies/1", Company); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + const timeSeries = session.timeSeriesFor(company, "stockPrice"); + const timeSeriesEntries = await timeSeries.get(now.clone().add(-7, "days").toDate(), null); + + assertThat(timeSeriesEntries) + .hasSize(1); + assertThat(timeSeriesEntries[0].timestamp) + .isEqualTo(now.toDate()); + assertThat(timeSeriesEntries[0].value) + .isEqualTo(10); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + mre.leave(); + callback(); + }); + + { + const session = store.openSession(); + const company = new Company(); + company.id = "companies/1"; + company.name = "HR"; + + await session.store(company); + + session.timeSeriesFor(company, "stockPrice") + .append(now.toDate(), 10); + await session.saveChanges(); + } + + await acquireSemaphore(mre, { timeout: _reasonableWaitTime }); + + } finally { + worker.dispose(); + } + }); + + it("canCreateSubscriptionWithIncludeTimeSeries_LastRangeByCount", async () => { + const now = testContext.utcToday(); + + const subscriptionCreationOptions: SubscriptionCreationOptions = { + includes: builder => builder.includeTimeSeries("stockPrice", "Last", 32), + documentType: Company + }; + + const name = await store.subscriptions.create(subscriptionCreationOptions); + + const worker = store.subscriptions.getSubscriptionWorker({ + subscriptionName: name, + documentType: Company + }); + + const mre = semaphore(1); + mre.take(TypeUtil.NOOP); + + try { + worker.on("batch", async (batch, callback) => { + const session = batch.openSession(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + const company = await session.load("companies/1", Company); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + const timeSeries = session.timeSeriesFor(company, "stockPrice"); + const timeSeriesEntries = await timeSeries.get( + now.clone().add(-7, "days").toDate(), + null + ); + + assertThat(timeSeriesEntries) + .hasSize(1); + assertThat(timeSeriesEntries[0].timestamp) + .isEqualTo(now.clone().add(-7, "days").toDate()); + assertThat(timeSeriesEntries[0].value) + .isEqualTo(10); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + mre.leave(); + callback(); + }); + + { + const session = store.openSession(); + const company = new Company(); + company.id = "companies/1"; + company.name = "HR"; + + await session.store(company); + + session.timeSeriesFor(company, "stockPrice") + .append(now.toDate(), 10); + await session.saveChanges(); + } + + await acquireSemaphore(mre, { timeout: _reasonableWaitTime }); + + } finally { + worker.dispose(); + } + }); + + it("canCreateSubscriptionWithIncludeTimeSeries_Array_LastRange", async () => { + const now = testContext.utcToday(); + + const subscriptionCreationOptions: SubscriptionCreationOptions = { + includes: builder => builder.includeTimeSeries(["stockPrice", "stockPrice2"], "Last", TimeValue.ofDays(7)), + documentType: Company + }; + + const name = await store.subscriptions.create(subscriptionCreationOptions); + + const worker = store.subscriptions.getSubscriptionWorker({ + subscriptionName: name, + documentType: Company + }); + + const mre = semaphore(1); + mre.take(TypeUtil.NOOP); + + try { + worker.on("batch", async (batch, callback) => { + const session = batch.openSession(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + const company = await session.load("companies/1", Company); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + let timeSeries = session.timeSeriesFor(company, "stockPrice"); + let timeSeriesEntries = await timeSeries.get( + now.clone().add(-7, "days").toDate(), + null + ); + + assertThat(timeSeriesEntries) + .hasSize(1); + assertThat(timeSeriesEntries[0].timestamp) + .isEqualTo(now.clone().add(-7, "days").toDate()); + assertThat(timeSeriesEntries[0].value) + .isEqualTo(10); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + timeSeries = session.timeSeriesFor(company, "stockPrice2"); + timeSeriesEntries = await timeSeries.get( + now.clone().add(-5, "days").toDate(), + null + ); + + assertThat(timeSeriesEntries) + .hasSize(1); + assertThat(timeSeriesEntries[0].timestamp) + .isEqualTo(now.clone().add(-5, "days").toDate()); + assertThat(timeSeriesEntries[0].value) + .isEqualTo(100); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + mre.leave(); + callback(); + }); + + { + const session = store.openSession(); + const company = new Company(); + company.id = "companies/1"; + company.name = "HR"; + + await session.store(company); + + session.timeSeriesFor(company, "stockPrice") + .append(now.clone().add(-7, "days").toDate(), 10); + session.timeSeriesFor(company, "stockPrice2") + .append(now.clone().add(-5, "days").toDate(), 10); + await session.saveChanges(); + } + + await acquireSemaphore(mre, { timeout: _reasonableWaitTime }); + + } finally { + worker.dispose(); + } + }); + + it("canCreateSubscriptionWithIncludeTimeSeries_All_LastRange", async () => { + const now = testContext.utcToday(); + + const subscriptionCreationOptions: SubscriptionCreationOptions = { + includes: builder => builder.includeAllTimeSeries("Last", TimeValue.ofDays(7)), + documentType: Company + }; + + const name = await store.subscriptions.create(subscriptionCreationOptions); + + const worker = store.subscriptions.getSubscriptionWorker({ + subscriptionName: name, + documentType: Company + }); + + const mre = semaphore(1); + mre.take(TypeUtil.NOOP); + + try { + worker.on("batch", async (batch, callback) => { + const session = batch.openSession(); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + const company = await session.load("companies/1", Company); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + let timeSeries = session.timeSeriesFor(company, "stockPrice"); + let timeSeriesEntries = await timeSeries.get( + now.clone().add(-7, "days").toDate(), + null + ); + + assertThat(timeSeriesEntries) + .hasSize(1); + assertThat(timeSeriesEntries[0].timestamp) + .isEqualTo(now.clone().add(-7, "days").toDate()); + assertThat(timeSeriesEntries[0].value) + .isEqualTo(10); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + timeSeries = session.timeSeriesFor(company, "stockPrice2"); + timeSeriesEntries = await timeSeries.get( + now.clone().add(-5, "days").toDate(), + null + ); + + assertThat(timeSeriesEntries) + .hasSize(1); + assertThat(timeSeriesEntries[0].timestamp) + .isEqualTo(now.clone().add(-5, "days").toDate()); + assertThat(timeSeriesEntries[0].value) + .isEqualTo(100); + assertThat(session.advanced.numberOfRequests) + .isEqualTo(0); + + mre.leave(); + callback(); + }); + + { + const session = store.openSession(); + const company = new Company(); + company.id = "companies/1"; + company.name = "HR"; + + await session.store(company); + + session.timeSeriesFor(company, "stockPrice") + .append(now.clone().add(-7, "days").toDate(), 10); + session.timeSeriesFor(company, "stockPrice2") + .append(now.clone().add(-5, "days").toDate(), 10); + await session.saveChanges(); + } + + await acquireSemaphore(mre, { timeout: _reasonableWaitTime }); + + } finally { + worker.dispose(); + } + }); + + it("canUseEmoji", async () => { + let user1: User; + + { + const session = store.openSession(); + user1 = new User(); + user1.name = "user_\uD83D\uDE21\uD83D\uDE21\uD83E\uDD2C\uD83D\uDE00😡😡🤬😀"; + await session.store(user1, "users/1"); + await session.saveChanges(); + } + + const creationOptions: SubscriptionCreationOptions = { + name: "name_\uD83D\uDE21\uD83D\uDE21\uD83E\uDD2C\uD83D\uDE00😡😡🤬😀", + documentType: User + }; + + const id = await store.subscriptions.create(creationOptions); + + const subscription = store.subscriptions.getSubscriptionWorker({ + documentType: User, + subscriptionName: id + }); + + const keys = new AsyncQueue(); + + try { + subscription.on("batch", (batch, callback) => { + batch.items.forEach(x => keys.push(x.result.name)); + callback(); + }); + + const key = await keys.poll(_reasonableWaitTime); + assertThat(key) + .isNotNull() + .isEqualTo(user1.name); + } finally { + subscription.dispose(); + } + }); }); -/* TODO - -+ -+ @Test -+ public void disposeSubscriptionWorkerShouldNotThrow() throws Exception { -+ Semaphore mre = new Semaphore(0); -+ Semaphore mre2 = new Semaphore(0); -+ -+ try (IDocumentStore store = getDocumentStore()) { -+ store.getRequestExecutor().addOnBeforeRequestListener((sender, handler) -> { -+ if (handler.getUrl().contains("info/remote-task/tcp?database=")) { -+ mre.release(); -+ try { -+ assertThat(mre2.tryAcquire(_reasonableWaitTime, TimeUnit.SECONDS)) -+ .isTrue(); -+ } catch (InterruptedException e) { -+ throw new RuntimeException(e); -+ } -+ } -+ }); -+ -+ String id = store.subscriptions().create(Company.class, new SubscriptionCreationOptions()); -+ SubscriptionWorkerOptions workerOptions = new SubscriptionWorkerOptions(id); -+ workerOptions.setIgnoreSubscriberErrors(true); -+ workerOptions.setStrategy(SubscriptionOpeningStrategy.TAKE_OVER); -+ SubscriptionWorker worker = store.subscriptions().getSubscriptionWorker(Company.class, workerOptions, store.getDatabase()); -+ -+ CompletableFuture t = worker.run(x -> { -+ }); -+ -+ assertThat(mre.tryAcquire(_reasonableWaitTime, TimeUnit.SECONDS)) -+ .isTrue(); -+ worker.close(false); -+ mre2.release(); -+ -+ Thread.sleep(5000); -+ waitForValue(() -> t.isDone(), true, Duration.ofSeconds(5)); -+ assertThat(t.isCompletedExceptionally()) -+ .isFalse(); -+ } -+ } -+ -+ @Test -+ public void canCreateSubscriptionWithIncludeTimeSeries_LastRangeByTime() throws Exception { -+ Date now = RavenTestHelper.utcToday(); -+ -+ try (IDocumentStore store = getDocumentStore()) { -+ SubscriptionCreationOptions subscriptionCreationOptions = new SubscriptionCreationOptions(); -+ subscriptionCreationOptions.setIncludes(b -> b.includeTimeSeries("stockPrice", TimeSeriesRangeType.LAST, TimeValue.ofMonths(1))); -+ -+ String name = store.subscriptions() -+ .create(Company.class, subscriptionCreationOptions); -+ -+ try (SubscriptionWorker worker = store.subscriptions().getSubscriptionWorker(Company.class, name)) { -+ Semaphore mre = new Semaphore(0); -+ -+ worker.run(batch -> { -+ try (IDocumentSession session = batch.openSession()) { -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ Company company = session.load(Company.class, "companies/1"); -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ ISessionDocumentTimeSeries timeSeries = session.timeSeriesFor(company, "stockPrice"); -+ TimeSeriesEntry[] timeSeriesEntries = timeSeries.get(DateUtils.addDays(now, -7), null); -+ -+ assertThat(timeSeriesEntries) -+ .hasSize(1); -+ assertThat(timeSeriesEntries[0].getTimestamp()) -+ .isEqualTo(now); -+ assertThat(timeSeriesEntries[0].getValue()) -+ .isEqualTo(10); -+ -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isEqualTo(0); -+ } -+ -+ mre.release(); -+ }); -+ -+ try (IDocumentSession session = store.openSession()) { -+ Company company = new Company(); -+ company.setId("companies/1"); -+ company.setName("HR"); -+ -+ session.store(company); -+ -+ session.timeSeriesFor(company, "stockPrice") -+ .append(now, 10); -+ -+ session.saveChanges(); -+ } -+ -+ assertThat(mre.tryAcquire(30, TimeUnit.SECONDS)) -+ .isTrue(); -+ } -+ } -+ } -+ -+ @Test -+ public void canCreateSubscriptionWithIncludeTimeSeries_LastRangeByCount() throws Exception { -+ Date now = RavenTestHelper.utcToday(); -+ -+ try (IDocumentStore store = getDocumentStore()) { -+ SubscriptionCreationOptions creationOptions = new SubscriptionCreationOptions(); -+ creationOptions.setIncludes(b -> b.includeTimeSeries("stockPrice", TimeSeriesRangeType.LAST, 32)); -+ String name = store.subscriptions() -+ .create(Company.class, creationOptions); -+ -+ Semaphore mre = new Semaphore(0); -+ -+ try (SubscriptionWorker worker = store.subscriptions().getSubscriptionWorker(Company.class, name)) { -+ worker.run(batch -> { -+ try (IDocumentSession session = batch.openSession()) { -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ Company company = session.load(Company.class, "companies/1"); -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ ISessionDocumentTimeSeries timeSeries = session.timeSeriesFor(company, "stockPrice"); -+ TimeSeriesEntry[] timeSeriesEntries = timeSeries.get(DateUtils.addDays(now, -7), null); -+ -+ assertThat(timeSeriesEntries) -+ .hasSize(1); -+ assertThat(timeSeriesEntries[0].getTimestamp()) -+ .isEqualTo(DateUtils.addDays(now, -7)); -+ assertThat(timeSeriesEntries[0].getValue()) -+ .isEqualTo(10); -+ -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ } -+ -+ mre.release(); -+ }); -+ -+ try (IDocumentSession session = store.openSession()) { -+ Company company = new Company(); -+ company.setId("companies/1"); -+ company.setName("HR"); -+ -+ session.store(company); -+ -+ session.timeSeriesFor(company, "stockPrice") -+ .append(DateUtils.addDays(now, -7), 10); -+ session.saveChanges(); -+ } -+ -+ assertThat(mre.tryAcquire(30, TimeUnit.SECONDS)) -+ .isTrue(); -+ } -+ } -+ } -+ -+ @Test -+ public void canCreateSubscriptionWithIncludeTimeSeries_Array_LastRange() throws Exception { -+ Date now = RavenTestHelper.utcToday(); -+ -+ try (IDocumentStore store = getDocumentStore()) { -+ SubscriptionCreationOptions creationOptions = new SubscriptionCreationOptions(); -+ creationOptions.setIncludes(builder -+ -> builder.includeTimeSeries( -+ new String[] { "stockPrice", "stockPrice2" }, TimeSeriesRangeType.LAST, TimeValue.ofDays(7))); -+ -+ String name = store.subscriptions().create(Company.class, creationOptions); -+ -+ Semaphore mre = new Semaphore(0); -+ -+ try (SubscriptionWorker worker = store.subscriptions().getSubscriptionWorker(Company.class, name)) { -+ worker.run(batch -> { -+ try (IDocumentSession session = batch.openSession()) { -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ Company company = session.load(Company.class, "companies/1"); -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ ISessionDocumentTimeSeries timeSeries = session.timeSeriesFor(company, "stockPrice"); -+ TimeSeriesEntry[] timeSeriesEntries = timeSeries.get(DateUtils.addDays(now, -7), null); -+ -+ assertThat(timeSeriesEntries) -+ .hasSize(1); -+ assertThat(timeSeriesEntries[0].getTimestamp()) -+ .isEqualTo(DateUtils.addDays(now, -7)); -+ assertThat(timeSeriesEntries[0].getValue()) -+ .isEqualTo(10); -+ -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isEqualTo(0); -+ -+ timeSeries = session.timeSeriesFor(company, "stockPrice2"); -+ timeSeriesEntries = timeSeries.get(DateUtils.addDays(now, -5), null); -+ -+ assertThat(timeSeriesEntries) -+ .hasSize(1); -+ assertThat(timeSeriesEntries[0].getTimestamp()) -+ .isEqualTo(DateUtils.addDays(now, -5)); -+ assertThat(timeSeriesEntries[0].getValue()) -+ .isEqualTo(100); -+ -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isEqualTo(0); -+ } -+ -+ mre.release(); -+ }); -+ -+ try (IDocumentSession session = store.openSession()) { -+ Company company = new Company(); -+ company.setId("companies/1"); -+ company.setName("HR"); -+ -+ session.store(company); -+ -+ session.timeSeriesFor(company, "stockPrice") -+ .append(DateUtils.addDays(now, -7), 10); -+ session.timeSeriesFor(company, "stockPrice2") -+ .append(DateUtils.addDays(now, -5), 100); -+ -+ session.saveChanges(); -+ } -+ -+ assertThat(mre.tryAcquire(30, TimeUnit.SECONDS)) -+ .isTrue(); -+ } -+ } -+ } -+ -+ @Test -+ public void canCreateSubscriptionWithIncludeTimeSeries_All_LastRange() throws Exception { -+ Date now = RavenTestHelper.utcToday(); -+ -+ try (IDocumentStore store = getDocumentStore()) { -+ SubscriptionCreationOptions creationOptions = new SubscriptionCreationOptions(); -+ creationOptions.setIncludes(builder -> builder.includeAllTimeSeries(TimeSeriesRangeType.LAST, TimeValue.ofDays(7))); -+ -+ String name = store.subscriptions().create(Company.class, creationOptions); -+ -+ Semaphore mre = new Semaphore(0); -+ -+ try (SubscriptionWorker worker = store.subscriptions().getSubscriptionWorker(Company.class, name)) { -+ worker.run(batch -> { -+ try (IDocumentSession session = batch.openSession()) { -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ Company company = session.load(Company.class, "companies/1"); -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ ISessionDocumentTimeSeries timeSeries = session.timeSeriesFor(company, "stockPrice"); -+ TimeSeriesEntry[] timeSeriesEntries = timeSeries.get(DateUtils.addDays(now, -7), null); -+ -+ assertThat(timeSeriesEntries) -+ .hasSize(1); -+ assertThat(timeSeriesEntries[0].getTimestamp()) -+ .isEqualTo(DateUtils.addDays(now, -7)); -+ assertThat(timeSeriesEntries[0].getValue()) -+ .isEqualTo(10); -+ -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ -+ timeSeries = session.timeSeriesFor(company, "stockPrice2"); -+ timeSeriesEntries = timeSeries.get(DateUtils.addDays(now, -5), null); -+ -+ assertThat(timeSeriesEntries) -+ .hasSize(1); -+ assertThat(timeSeriesEntries[0].getTimestamp()) -+ .isEqualTo(DateUtils.addDays(now, -5)); -+ assertThat(timeSeriesEntries[0].getValue()) -+ .isEqualTo(100); -+ -+ assertThat(session.advanced().getNumberOfRequests()) -+ .isZero(); -+ } -+ -+ mre.release(); -+ }); -+ -+ try (IDocumentSession session = store.openSession()) { -+ Company company = new Company(); -+ company.setId("companies/1"); -+ company.setName("HR"); -+ -+ session.store(company); -+ -+ session.timeSeriesFor(company, "stockPrice") -+ .append(DateUtils.addDays(now, -7), 10); -+ session.timeSeriesFor(company, "stockPrice2") -+ .append(DateUtils.addDays(now, -5), 100); -+ -+ session.saveChanges(); -+ } -+ -+ assertThat(mre.tryAcquire(30, TimeUnit.SECONDS)) -+ .isTrue(); -+ } -+ } -+ } - */ // describe("Manual subscription tests", () => { diff --git a/test/TestDriver/index.ts b/test/TestDriver/index.ts index 594fb4f25..98eab4d53 100644 --- a/test/TestDriver/index.ts +++ b/test/TestDriver/index.ts @@ -46,6 +46,17 @@ export abstract class RavenTestDriver { public static debug: boolean; + public readonly samples: SamplesTestBase; + public readonly indexes: IndexesTestBase; + public readonly replication: ReplicationTestBase2; + + + public constructor() { + this.samples = new SamplesTestBase(this); + this.indexes = new IndexesTestBase(this); + this.replication = new ReplicationTestBase2(this); + } + public enableFiddler(): IDisposable { RequestExecutor.requestPostProcessor = (req) => { req.agent = new proxyAgent.HttpProxyAgent("http://127.0.0.1:8888") as unknown as http.Agent; @@ -198,24 +209,6 @@ export abstract class RavenTestDriver { return Promise.resolve(result); } - public async waitForIndexingErrors(store: IDocumentStore, timeoutInMs: number, ...indexNames: string[]) { - const sw = Stopwatch.createStarted(); - - while (sw.elapsed < timeoutInMs) { - const indexes = await store.maintenance.send(new GetIndexErrorsOperation(indexNames)); - - for (const index of indexes) { - if (index.errors && index.errors.length) { - return indexes; - } - } - - await delay(32); - } - - throwError("TimeoutException", "Got no index error from more than " + TimeUtil.millisToTimeSpan(timeoutInMs)); - } - public async waitForDocumentDeletion(store: IDocumentStore, id: string) { const sw = Stopwatch.createStarted(); @@ -319,6 +312,16 @@ export abstract class RavenTestDriver { return store.maintenance.send(operation); } + +} + +class SamplesTestBase { + private readonly _parent: RavenTestDriver; + + public constructor(parent) { + this._parent = parent; + } + public async createSimpleData(store: IDocumentStore) { { const session = store.openSession(); @@ -350,6 +353,7 @@ export abstract class RavenTestDriver { } } + public async createDogDataWithoutEdges(store: IDocumentStore) { { const session = store.openSession(); @@ -510,3 +514,76 @@ export abstract class RavenTestDriver { } } } + + +class ReplicationTestBase2 { + private readonly _parent: RavenTestDriver; + + constructor(parent: RavenTestDriver) { + this._parent = parent; + } + + async waitForConflict(docStore: IDocumentStore, id: string) { + const sw = Stopwatch.createStarted(); + while (sw.elapsed < 10000) { + try { + const session = docStore.openSession(); + await session.load(id); + + await BluebirdPromise.delay(10); + } catch (e) { + if (e.name === "DocumentConflictException") { + return; + } + + throw e; + } + } + + throwError("InvalidOperationException", + "Waited for conflict on '" + id + "' but it did not happen"); + } +} + +class IndexesTestBase { + private readonly _parent: RavenTestDriver; + + constructor(parent: RavenTestDriver) { + this._parent = parent; + } + + public waitForIndexing(store: IDocumentStore): Promise; + public waitForIndexing(store: IDocumentStore, database?: string): Promise; + public waitForIndexing(store: IDocumentStore, database?: string, timeout?: number): Promise; + public waitForIndexing( + store: IDocumentStore, database?: string, timeout?: number, throwOnIndexErrors?: boolean): Promise; + public waitForIndexing( + store: IDocumentStore, database?: string, timeout?: number, throwOnIndexErrors?: boolean, nodeTag?: string): Promise; + public waitForIndexing( + store: IDocumentStore, + database?: string, + timeout?: number, + throwOnIndexErrors: boolean = true, + nodeTag?: string): Promise { + return this._parent.waitForIndexing(store, database, timeout, throwOnIndexErrors, nodeTag); + } + + + public async waitForIndexingErrors(store: IDocumentStore, timeoutInMs: number, ...indexNames: string[]) { + const sw = Stopwatch.createStarted(); + + while (sw.elapsed < timeoutInMs) { + const indexes = await store.maintenance.send(new GetIndexErrorsOperation(indexNames)); + + for (const index of indexes) { + if (index.errors && index.errors.length) { + return indexes; + } + } + + await delay(32); + } + + throwError("TimeoutException", "Got no index error from more than " + TimeUtil.millisToTimeSpan(timeoutInMs)); + } +} diff --git a/test/Utils/TestUtil.ts b/test/Utils/TestUtil.ts index 85d6b329f..ee7163d49 100644 --- a/test/Utils/TestUtil.ts +++ b/test/Utils/TestUtil.ts @@ -106,7 +106,11 @@ class TestSecuredServiceLocator extends RavenServerLocator { } public getServerCertificatePath() { - return path.resolve(process.env[TestSecuredServiceLocator.ENV_SERVER_CERTIFICATE_PATH]); + const certPath = process.env[TestSecuredServiceLocator.ENV_SERVER_CERTIFICATE_PATH]; + if (!certPath) { + throwError("InvalidArgumentException", TestSecuredServiceLocator.ENV_SERVER_CERTIFICATE_PATH + " env variable is missing"); + } + return path.resolve(certPath); } public getClientAuthOptions(): IAuthOptions {