Skip to content

Commit a33c91f

Browse files
committed
more input validation
1 parent dcbddb8 commit a33c91f

File tree

7 files changed

+236
-12
lines changed

7 files changed

+236
-12
lines changed

src/client/data-api-client.ts

Lines changed: 63 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,20 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { Db, mkDb } from '@/src/data-api/db';
16-
import { AstraAdmin, mkAdmin } from '@/src/devops/astra-admin';
17-
import { AdminSpawnOptions, DbSpawnOptions, InternalRootClientOpts, RootClientOptions } from '@/src/client/types';
15+
import { Db, mkDb, validateDbOpts } from '@/src/data-api/db';
16+
import { AstraAdmin, mkAdmin, validateAdminOpts } from '@/src/devops/astra-admin';
17+
import {
18+
AdminSpawnOptions,
19+
Caller,
20+
DbSpawnOptions,
21+
InternalRootClientOpts,
22+
RootClientOptions,
23+
} from '@/src/client/types';
1824
import TypedEmitter from 'typed-emitter';
1925
import EventEmitter from 'events';
2026
import { DataAPICommandEvents } from '@/src/data-api/events';
2127
import { AdminCommandEvents } from '@/src/devops';
28+
import { validateOption } from '@/src/data-api';
2229

2330
export type DataAPIClientEvents =
2431
& DataAPICommandEvents
@@ -59,13 +66,15 @@ export class DataAPIClient extends (EventEmitter as new () => TypedEmitter<DataA
5966
* @param token - The default token to use when spawning new instances of {@link Db} or {@link AstraAdmin}.
6067
* @param options - The default options to use when spawning new instances of {@link Db} or {@link AstraAdmin}.
6168
*/
62-
constructor(token: string, options?: RootClientOptions) {
69+
constructor(token: string, options?: RootClientOptions | null) {
6370
super();
6471

6572
if (!token || typeof token as any !== 'string') {
6673
throw new Error('A valid token is required to use the DataAPIClient');
6774
}
6875

76+
validateRootOpts(options);
77+
6978
this.#options = {
7079
...options,
7180
dbOptions: {
@@ -180,3 +189,53 @@ export class DataAPIClient extends (EventEmitter as new () => TypedEmitter<DataA
180189
return mkAdmin(this.#options, options);
181190
}
182191
}
192+
193+
function validateRootOpts(opts: RootClientOptions | undefined | null) {
194+
validateOption('root client options', opts, 'object');
195+
196+
if (!opts) {
197+
return;
198+
}
199+
200+
validateOption('caller', opts.caller, 'object', validateCaller);
201+
202+
validateDbOpts(opts.dbOptions);
203+
204+
validateAdminOpts(opts.adminOptions);
205+
}
206+
207+
function validateCaller(caller: Caller | Caller[]) {
208+
if (!Array.isArray(caller)) {
209+
throw new TypeError('Invalid caller; expected an array, or undefined/null');
210+
}
211+
212+
const isCallerArr = Array.isArray(caller[0]);
213+
214+
const callers = (
215+
(isCallerArr)
216+
? caller
217+
: [caller]
218+
) as Caller[];
219+
220+
callers.forEach((c, i) => {
221+
const idxMessage = (isCallerArr)
222+
? ` at index ${i}`
223+
: '';
224+
225+
if (!Array.isArray(c)) {
226+
throw new TypeError(`Invalid caller; expected [name, version?], or an array of such${idxMessage}`);
227+
}
228+
229+
if (c.length < 1 || 2 < c.length) {
230+
throw new Error(`Invalid caller; expected [name, version?], or an array of such${idxMessage}`);
231+
}
232+
233+
if (typeof c[0] !== 'string') {
234+
throw new Error(`Invalid caller; expected a string name${idxMessage}`);
235+
}
236+
237+
if (c.length === 2 && typeof c[1] !== 'string') {
238+
throw new Error(`Invalid caller; expected a string version${idxMessage}`);
239+
}
240+
});
241+
}

src/data-api/db.ts

Lines changed: 29 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
import { Collection, CollectionAlreadyExistsError, extractDbIdFromUrl, SomeDoc } from '@/src/data-api';
15+
import { Collection, CollectionAlreadyExistsError, extractDbIdFromUrl, SomeDoc, validateOption } from '@/src/data-api';
1616
import { DataAPIHttpClient, DEFAULT_DATA_API_PATH, DEFAULT_NAMESPACE, RawDataAPIResponse } from '@/src/api';
1717
import {
1818
CreateCollectionCommand,
@@ -115,10 +115,6 @@ export class Db implements Disposable {
115115

116116
this.#defaultOpts = options;
117117

118-
if (!this.namespace.match(/^[a-zA-Z0-9_]{1,222}$/)) {
119-
throw new Error('Invalid namespace format; either pass a valid namespace name, or don\'t pass one at all to use the default namespace');
120-
}
121-
122118
Object.defineProperty(this, '_httpClient', {
123119
value: new DataAPIHttpClient({
124120
baseUrl: endpoint,
@@ -523,9 +519,11 @@ export class Db implements Disposable {
523519
*/
524520
export function mkDb(rootOpts: InternalRootClientOpts, endpointOrId: string, regionOrOptions?: string | DbSpawnOptions, maybeOptions?: DbSpawnOptions) {
525521
const options = (typeof regionOrOptions === 'string')
526-
? maybeOptions!
522+
? maybeOptions
527523
: regionOrOptions;
528524

525+
validateDbOpts(options);
526+
529527
const endpoint = (typeof regionOrOptions === 'string')
530528
? 'https://' + endpointOrId + '-' + regionOrOptions + '.apps.astra.datastax.com'
531529
: endpointOrId;
@@ -538,3 +536,28 @@ export function mkDb(rootOpts: InternalRootClientOpts, endpointOrId: string, reg
538536
},
539537
});
540538
}
539+
540+
/**
541+
* @internal
542+
*/
543+
export function validateDbOpts(opts: DbSpawnOptions | undefined) {
544+
validateOption('db options', opts, 'object');
545+
546+
if (!opts) {
547+
return;
548+
}
549+
550+
validateOption<string>('namespace option', opts.namespace, 'string', (namespace) => {
551+
if (!namespace.match(/^\w{1,48}$/)) {
552+
throw new Error('Invalid namespace option; expected a string of 1-48 alphanumeric characters');
553+
}
554+
});
555+
556+
validateOption('monitorCommands option', opts.monitorCommands, 'boolean');
557+
558+
validateOption('token option', opts.token, 'string');
559+
560+
validateOption('dataApiPath option', opts.dataApiPath, 'string');
561+
562+
validateOption('useHttp2 option', opts.useHttp2, 'boolean');
563+
}

src/data-api/utils.ts

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,3 +62,25 @@ export function replaceAstraUrlIdAndRegion(uri: string, id: string, region: stri
6262
url.hostname = parts.join('.');
6363
return url.toString().slice(0, -1);
6464
}
65+
66+
/**
67+
* @internal
68+
*/
69+
export function validateOption<T = unknown>(name: string, obj: unknown, type: string, test?: (obj: T) => void): void {
70+
if (isUndefOrNull(obj)) {
71+
return;
72+
}
73+
74+
if (typeof obj !== type) {
75+
throw new TypeError(`Invalid ${name}; expected a ${type} value, or undefined/null`);
76+
}
77+
78+
test?.(obj as T);
79+
}
80+
81+
/**
82+
* @internal
83+
*/
84+
export function isUndefOrNull<T>(x: T | null | undefined): x is null | undefined {
85+
return x === null || x === undefined;
86+
}

src/devops/astra-admin.ts

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ import {
55
FullDatabaseInfo,
66
ListDatabasesOptions,
77
} from '@/src/devops/types';
8-
import { Db, mkDb } from '@/src/data-api';
8+
import { Db, mkDb, validateOption } from '@/src/data-api';
99
import { DEFAULT_DEVOPS_API_ENDPOINT, DEFAULT_NAMESPACE, DevOpsAPIHttpClient, HttpMethods } from '@/src/api';
1010
import { AstraDbAdmin } from '@/src/devops/astra-db-admin';
1111
import { AdminSpawnOptions, DbSpawnOptions, InternalRootClientOpts } from '@/src/client/types';
@@ -376,6 +376,8 @@ export class AstraAdmin {
376376
* @internal
377377
*/
378378
export function mkAdmin(rootOpts: InternalRootClientOpts, options?: AdminSpawnOptions): AstraAdmin {
379+
validateAdminOpts(options);
380+
379381
return new AstraAdmin({
380382
...rootOpts,
381383
adminOptions: {
@@ -384,3 +386,20 @@ export function mkAdmin(rootOpts: InternalRootClientOpts, options?: AdminSpawnOp
384386
},
385387
});
386388
}
389+
390+
/**
391+
* @internal
392+
*/
393+
export function validateAdminOpts(opts: AdminSpawnOptions | undefined) {
394+
validateOption('admin options', opts, 'object');
395+
396+
if (!opts) {
397+
return;
398+
}
399+
400+
validateOption('monitorCommands option', opts.monitorCommands, 'boolean');
401+
402+
validateOption('adminToken option', opts.adminToken, 'string');
403+
404+
validateOption('endpointUrl option', opts.endpointUrl, 'string');
405+
}

src/devops/astra-db-admin.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { Db } from '@/src/data-api';
44
import { AdminSpawnOptions, InternalRootClientOpts } from '@/src/client';
55
import { DbAdmin } from '@/src/devops/db-admin';
66
import { WithTimeout } from '@/src/common/types';
7+
import { validateAdminOpts } from '@/src/devops/astra-admin';
78

89
/**
910
* An administrative class for managing Astra databases, including creating, listing, and deleting databases.
@@ -259,6 +260,8 @@ export class AstraDbAdmin extends DbAdmin {
259260
* @internal
260261
*/
261262
export function mkDbAdmin(db: Db, httpClient: HttpClient, rootOpts: InternalRootClientOpts, options?: AdminSpawnOptions): AstraDbAdmin {
263+
validateAdminOpts(options);
264+
262265
return new AstraDbAdmin(db, httpClient, {
263266
...rootOpts.adminOptions,
264267
...options,

tests/unit/client/data-api-client.test.ts

Lines changed: 44 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
import { DataAPIClient } from '@/src/client';
1717
import * as process from 'process';
1818
import assert from 'assert';
19-
import { DEFAULT_DATA_API_PATH } from '@/src/api';
19+
import { DEFAULT_DATA_API_PATH, HTTP1Strategy } from '@/src/api';
2020

2121
describe('unit.client.data-api-client', () => {
2222
const endpoint = process.env.ASTRA_URI!;
@@ -42,6 +42,40 @@ describe('unit.client.data-api-client', () => {
4242
// @ts-expect-error - testing invalid input
4343
assert.throws(() => new DataAPIClient({ logLevel: 'warn' }));
4444
});
45+
46+
it('should accept null/undefined/{} for options', () => {
47+
assert.doesNotThrow(() => new DataAPIClient('dummy-token', null));
48+
assert.doesNotThrow(() => new DataAPIClient('dummy-token', undefined));
49+
assert.doesNotThrow(() => new DataAPIClient('dummy-token', {}));
50+
});
51+
52+
it('should accept valid callers', () => {
53+
// @ts-expect-error - null technically allowed
54+
assert.doesNotThrow(() => new DataAPIClient('dummy-token', { caller: null }));
55+
assert.doesNotThrow(() => new DataAPIClient('dummy-token', { caller: undefined }));
56+
assert.doesNotThrow(() => new DataAPIClient('dummy-token', { caller: ['a', 'b'] }));
57+
assert.doesNotThrow(() => new DataAPIClient('dummy-token', { caller: [['a', 'b'], ['c', 'd']] }));
58+
});
59+
60+
it('should throw on invalid caller', () => {
61+
assert.throws(() => new DataAPIClient('dummy-token', { caller: [] }));
62+
// @ts-expect-error - testing invalid input
63+
assert.throws(() => new DataAPIClient('dummy-token', { caller: 'invalid-type' }));
64+
// @ts-expect-error - testing invalid input
65+
assert.throws(() => new DataAPIClient('dummy-token', { caller: [1, 'b'] }));
66+
// @ts-expect-error - testing invalid input
67+
assert.throws(() => new DataAPIClient('dummy-token', { caller: ['a', 2] }));
68+
// @ts-expect-error - testing invalid input
69+
assert.throws(() => new DataAPIClient('dummy-token', { caller: [[1]] }));
70+
// @ts-expect-error - testing invalid input
71+
assert.throws(() => new DataAPIClient('dummy-token', { caller: [['a', 'b', 'c']] }));
72+
// @ts-expect-error - testing invalid input
73+
assert.throws(() => new DataAPIClient('dummy-token', { caller: [[]] }));
74+
// @ts-expect-error - testing invalid input
75+
assert.throws(() => new DataAPIClient('dummy-token', { caller: [{}] }));
76+
// @ts-expect-error - testing invalid input
77+
assert.throws(() => new DataAPIClient('dummy-token', { caller: { 0: ['name', 'version'] } }));
78+
});
4579
});
4680

4781
describe('db tests', () => {
@@ -66,4 +100,13 @@ describe('unit.client.data-api-client', () => {
66100
assert.notStrictEqual(db1['_httpClient'], db2['_httpClient']);
67101
});
68102
});
103+
104+
describe('admin tests', () => {
105+
it('should spawn an AstraAdmin instance', () => {
106+
const admin = new DataAPIClient('dummy-token').admin();
107+
assert.ok(admin);
108+
assert.strictEqual(admin['_httpClient'].unsafeGetToken(), 'dummy-token');
109+
assert.ok(admin['_httpClient'].requestStrategy instanceof HTTP1Strategy);
110+
});
111+
});
69112
});

tests/unit/data-api/db.test.ts

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@
1111
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
14+
// noinspection DuplicatedCode
1415

1516
import assert from 'assert';
1617
import { Db, mkDb } from '@/src/data-api';
@@ -127,6 +128,60 @@ describe('unit.data-api.db', () => {
127128
const db = mkDb(mkOptions(), process.env.ASTRA_URI!, { token: 'new' });
128129
assert.strictEqual(db['_httpClient'].unsafeGetToken(), 'new');
129130
});
131+
132+
it('should accept valid monitorCommands', () => {
133+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, {}));
134+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { monitorCommands: true }));
135+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { monitorCommands: false }));
136+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { monitorCommands: null! }));
137+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { monitorCommands: undefined }));
138+
});
139+
140+
it('should throw on invalid monitorCommands', () => {
141+
// @ts-expect-error - testing invalid input
142+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { monitorCommands: 'invalid' }));
143+
// @ts-expect-error - testing invalid input
144+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { monitorCommands: 1 }));
145+
// @ts-expect-error - testing invalid input
146+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { monitorCommands: [] }));
147+
// @ts-expect-error - testing invalid input
148+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { monitorCommands: {} }));
149+
});
150+
151+
it('should accept valid useHttp2', () => {
152+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, {}));
153+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { useHttp2: true }));
154+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { useHttp2: false }));
155+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { useHttp2: null! }));
156+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { useHttp2: undefined }));
157+
});
158+
159+
it('should throw on invalid useHttp2', () => {
160+
// @ts-expect-error - testing invalid input
161+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { useHttp2: 'invalid' }));
162+
// @ts-expect-error - testing invalid input
163+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { useHttp2: 1 }));
164+
// @ts-expect-error - testing invalid input
165+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { useHttp2: [] }));
166+
// @ts-expect-error - testing invalid input
167+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { useHttp2: {} }));
168+
});
169+
170+
it('should accept valid dataApiPath', () => {
171+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, {}));
172+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { dataApiPath: 'api/json/v2' }));
173+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { dataApiPath: null! }));
174+
assert.doesNotThrow(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { dataApiPath: undefined }));
175+
});
176+
177+
it('should throw on invalid dataApiPath', () => {
178+
// @ts-expect-error - testing invalid input
179+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { dataApiPath: 1 }));
180+
// @ts-expect-error - testing invalid input
181+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { dataApiPath: [] }));
182+
// @ts-expect-error - testing invalid input
183+
assert.throws(() => mkDb(mkOptions(), process.env.ASTRA_URI!, { dataApiPath: {} }));
184+
});
130185
});
131186

132187
describe('http-related tests', () => {

0 commit comments

Comments
 (0)