Skip to content

Commit 6a6afb7

Browse files
authored
Merge pull request #169 from gregolsky/v4.0
RDBC-247: add singleOrNull() and firstOrNull(), verify single() and f…
2 parents e6d9d84 + e6a033c commit 6a6afb7

File tree

5 files changed

+210
-29
lines changed

5 files changed

+210
-29
lines changed

src/Documents/Session/AbstractDocumentQuery.ts

Lines changed: 39 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ import { SuggestToken } from "./Tokens/SuggestToken";
6464
import { SuggestionWithTerm } from "../Queries/Suggestions/SuggestionWithTerm";
6565
import { SuggestionWithTerms } from "../Queries/Suggestions/SuggestionWithTerms";
6666
import { QueryData } from "../Queries/QueryData";
67+
import { passResultToCallback } from "../../Utility/PromiseUtil";
6768

6869
/**
6970
* A query against a Raven index
@@ -1800,28 +1801,51 @@ export abstract class AbstractDocumentQuery<T extends object, TSelf extends Abst
18001801

18011802
public async first(callback?: AbstractCallback<T>): Promise<T> {
18021803
callback = callback || TypeUtil.NOOP;
1803-
const result = BluebirdPromise.resolve()
1804-
.then(() => this._executeQueryOperation(2))
1805-
.then(entries => entries[0] || null)
1806-
.tap(x => callback(null, x))
1807-
.tapCatch(err => callback(err));
1808-
return Promise.resolve(result);
1804+
const resultPromise = this._executeQueryOperation(2)
1805+
.then(entries => {
1806+
if (entries.length === 0) {
1807+
throwError("InvalidOperationException", "Expected at least one result.");
1808+
}
1809+
1810+
return entries[0];
1811+
});
1812+
1813+
passResultToCallback(resultPromise, callback);
1814+
return resultPromise;
1815+
}
1816+
1817+
public async firstOrNull(callback?: AbstractCallback<T>): Promise<T> {
1818+
callback = callback || TypeUtil.NOOP;
1819+
const resultPromise = this._executeQueryOperation(1)
1820+
.then(entries => entries[0] || null);
1821+
1822+
passResultToCallback(resultPromise, callback);
1823+
return resultPromise;
18091824
}
18101825

18111826
public async single(callback?: AbstractCallback<T>): Promise<T> {
18121827
callback = callback || TypeUtil.NOOP;
1813-
const result = BluebirdPromise.resolve()
1814-
.then(() => this._executeQueryOperation(2))
1828+
const resultPromise = this._executeQueryOperation(2)
18151829
.then(entries => {
1816-
if (entries.length > 1) {
1817-
throw getError("InvalidOperationException", "Expected single result, got: " + entries.length);
1830+
if (entries.length !== 1) {
1831+
throwError("InvalidOperationException",
1832+
`Expected single result, but got ${ entries.length ? "more than that" : 0 }.`);
18181833
}
1834+
1835+
return entries[0];
1836+
});
18191837

1820-
return entries[0] || null;
1821-
})
1822-
.tap(x => callback(null, x))
1823-
.tapCatch(err => callback(err));
1824-
return Promise.resolve(result);
1838+
passResultToCallback(resultPromise, callback);
1839+
return resultPromise;
1840+
}
1841+
1842+
public async singleOrNull(callback?: AbstractCallback<T>): Promise<T> {
1843+
callback = callback || TypeUtil.NOOP;
1844+
const resultPromise = this._executeQueryOperation(2)
1845+
.then(entries => entries.length === 1 ? entries[0] : null);
1846+
1847+
passResultToCallback(resultPromise, callback);
1848+
return resultPromise;
18251849
}
18261850

18271851
public async count(callback?: AbstractCallback<number>): Promise<number> {

src/Documents/Session/IDocumentQueryBaseSingle.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,20 @@ export interface IDocumentQueryBaseSingle<T extends object> {
1010
first(callback?: AbstractCallback<T>): Promise<T>;
1111

1212
/**
13-
* Returns first element or throws if sequence is empty or contains more than one element.
13+
* Returns first element if there's any or null otherwise.
14+
*/
15+
firstOrNull(callback?: AbstractCallback<T>): Promise<T>;
16+
17+
/**
18+
* Returns single element or throws if sequence is empty or contains more than one element.
1419
*/
1520
single(callback?: AbstractCallback<T>): Promise<T>;
1621

22+
/**
23+
* Returns single element if there's any or null otherwise.
24+
*/
25+
singleOrNull(callback?: AbstractCallback<T>): Promise<T>;
26+
1727
/**
1828
* Gets the total count of records for this query
1929
*/

test/Issues/RDBC_247.ts

Lines changed: 148 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,148 @@
1+
import * as mocha from "mocha";
2+
import * as assert from "assert";
3+
import { User, Company, Order } from "../Assets/Entities";
4+
import { assertThrows } from "../Utils/AssertExtensions";
5+
import { testContext, disposeTestDocumentStore } from "../Utils/TestUtil";
6+
7+
import {
8+
RavenErrorType,
9+
IDocumentStore,
10+
IDocumentSession,
11+
} from "../../src";
12+
13+
describe("RDBC-247", function () {
14+
15+
let store: IDocumentStore;
16+
17+
beforeEach(async function () {
18+
store = await testContext.getDocumentStore();
19+
});
20+
21+
afterEach(async () =>
22+
await disposeTestDocumentStore(store));
23+
24+
beforeEach(async () => {
25+
const session = store.openSession();
26+
await session.store(Object.assign(new User(), { name: "John" }));
27+
await session.store(Object.assign(new User(), { name: "John2" }));
28+
await session.store(Object.assign(new User(), { name: "John3" }));
29+
await session.store(Object.assign(new User(), { name: "John4" }));
30+
await session.saveChanges();
31+
});
32+
33+
let session: IDocumentSession;
34+
35+
beforeEach(() => session = store.openSession());
36+
37+
describe("single()", function () {
38+
39+
it("should throw if 0 results", async function () {
40+
await assertThrows(
41+
async () => await session.query({ collection: "users" })
42+
.whereEquals("name", "Merry")
43+
.single(),
44+
err => {
45+
assert.strictEqual(err.name, "InvalidOperationException");
46+
assert.strictEqual(err.message, "Expected single result, but got 0.");
47+
});
48+
});
49+
50+
it("should throw if more than 1 result", async function() {
51+
await assertThrows(
52+
async () => await session.query({ collection: "users" }).single(),
53+
err => {
54+
assert.strictEqual(err.name, "InvalidOperationException");
55+
assert.strictEqual(err.message, "Expected single result, but got more than that.");
56+
});
57+
58+
});
59+
60+
it("should return exactly 1 result", async function() {
61+
const result = await session.query<User>({ collection: "users" })
62+
.whereEquals("name", "John")
63+
.single();
64+
65+
assert.ok(result);
66+
assert.ok(result instanceof User);
67+
assert.strictEqual(result.name, "John");
68+
});
69+
70+
});
71+
72+
describe("singleOrNull()", function () {
73+
74+
it("should return null if 0 results", async function () {
75+
const result = await session.query<User>({ collection: "users" })
76+
.whereEquals("name", "Merry")
77+
.singleOrNull();
78+
assert.strictEqual(result, null);
79+
});
80+
81+
it("should return null if more than 1 result", async function() {
82+
const result = await session.query<User>({ collection: "users" })
83+
.singleOrNull();
84+
assert.strictEqual(result, null);
85+
});
86+
87+
it("should return exactly 1 result", async function() {
88+
const result = await session.query<User>({ collection: "users" })
89+
.whereEquals("name", "John")
90+
.singleOrNull();
91+
92+
assert.ok(result);
93+
assert.ok(result instanceof User);
94+
assert.strictEqual(result.name, "John");
95+
});
96+
97+
});
98+
99+
describe("first()", function () {
100+
101+
it("should return first result", async function() {
102+
const result = await session.query<User>({ collection: "users" })
103+
.whereStartsWith("name", "John")
104+
.orderBy("name")
105+
.first();
106+
107+
assert.ok(result);
108+
assert.ok(result instanceof User);
109+
assert.strictEqual(result.name, "John");
110+
});
111+
112+
it("should throw for no results", async function() {
113+
await assertThrows(
114+
async () => await session.query({ collection: "users" })
115+
.whereEquals("name", "Merry")
116+
.first(),
117+
err => {
118+
assert.strictEqual(err.name, "InvalidOperationException");
119+
assert.strictEqual(err.message, "Expected at least one result.");
120+
});
121+
});
122+
123+
});
124+
125+
describe("firstOrNull()", function () {
126+
127+
it("should return first result", async function() {
128+
const result = await session.query<User>({ collection: "users" })
129+
.whereStartsWith("name", "John")
130+
.orderBy("name")
131+
.firstOrNull();
132+
133+
assert.ok(result);
134+
assert.ok(result instanceof User);
135+
assert.strictEqual(result.name, "John");
136+
});
137+
138+
it("should return null for no results", async function() {
139+
const result = await session.query<User>({ collection: "users" })
140+
.whereStartsWith("name", "Merry")
141+
.firstOrNull();
142+
143+
assert.strictEqual(result, null);
144+
});
145+
146+
});
147+
148+
});

test/Utils/AssertExtensions.ts

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
11
import * as assert from "assert";
22

3-
export async function throwsAsync(fn, regExp) {
4-
// tslint:disable-next-line:no-empty
5-
let f = () => {
6-
};
3+
export async function assertThrows(func: Function, errAssert?: (err: Error) => void) {
74
try {
8-
await fn();
9-
} catch (e) {
10-
f = () => {
11-
throw e;
12-
};
13-
} finally {
14-
assert.throws(f, regExp);
5+
await func();
6+
assert.fail(`Function '${func.name || func.toString()}' should have thrown.`);
7+
} catch (err) {
8+
if (errAssert) {
9+
errAssert(err);
10+
}
11+
12+
return err;
1513
}
1614
}

tslint.json

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,10 +32,11 @@
3232
"adjacent-overload-signatures": false,
3333
"ban-types": false,
3434
"no-unused-variable": false,
35-
"space-before-function-paren": ["warn", "always"],
35+
"space-before-function-paren": [ "warn", "always"],
3636
"object-curly-spacing": [true, "always"],
3737
"comment-format": false,
38-
"no-shadowed-variable": false
38+
"no-shadowed-variable": false,
39+
"mocha-avoid-only": true
3940
},
4041
"rulesDirectory": [
4142
"node_modules/tslint-microsoft-contrib"

0 commit comments

Comments
 (0)