Skip to content

Commit 2a9efce

Browse files
authored
Mutable DB namespaces (#60)
* mutable namespaces (no docs + testing) * documentation for mutable namespaces * tests for mutable namespace
1 parent c0faf53 commit 2a9efce

File tree

19 files changed

+330
-122
lines changed

19 files changed

+330
-122
lines changed

src/api/clients/data-api-http-client.ts

Lines changed: 8 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -15,12 +15,13 @@
1515

1616
import {
1717
DEFAULT_DATA_API_AUTH_HEADER,
18-
DEFAULT_NAMESPACE,
19-
DEFAULT_TIMEOUT, HeaderProvider,
18+
DEFAULT_TIMEOUT,
19+
HeaderProvider,
2020
hrTimeMs,
2121
HttpClient,
2222
HTTPClientOptions,
2323
HttpMethods,
24+
NamespaceRef,
2425
RawDataAPIResponse,
2526
} from '@/src/api';
2627
import { DataAPIResponseError, DataAPITimeoutError, ObjectId, UUID } from '@/src/data-api';
@@ -94,7 +95,7 @@ const adaptInfo4Devops = (info: DataAPIRequestInfo) => (<const>{
9495
});
9596

9697
interface DataAPIHttpClientOpts extends HTTPClientOptions {
97-
namespace: string | undefined,
98+
namespace: NamespaceRef,
9899
emissionStrategy: EmissionStrategy,
99100
embeddingHeaders: EmbeddingHeadersProvider,
100101
tokenProvider: TokenProvider,
@@ -105,14 +106,14 @@ interface DataAPIHttpClientOpts extends HTTPClientOptions {
105106
*/
106107
export class DataAPIHttpClient extends HttpClient {
107108
public collection?: string;
108-
public namespace?: string;
109+
public namespace?: NamespaceRef;
109110
public maxTimeMS: number;
110111
public emissionStrategy: ReturnType<EmissionStrategy>
111112
readonly #props: DataAPIHttpClientOpts;
112113

113114
constructor(props: DataAPIHttpClientOpts) {
114115
super(props, [mkAuthHeaderProvider(props.tokenProvider), props.embeddingHeaders.getHeaders.bind(props.embeddingHeaders)]);
115-
this.namespace = 'namespace' in props ? props.namespace : DEFAULT_NAMESPACE;
116+
this.namespace = props.namespace;
116117
this.#props = props;
117118
this.maxTimeMS = this.fetchCtx.maxTimeMS ?? DEFAULT_TIMEOUT;
118119
this.emissionStrategy = props.emissionStrategy(props.emitter);
@@ -122,7 +123,7 @@ export class DataAPIHttpClient extends HttpClient {
122123
const clone = new DataAPIHttpClient({
123124
...this.#props,
124125
embeddingHeaders: EmbeddingHeadersProvider.parseHeaders(opts?.embeddingApiKey),
125-
namespace: namespace,
126+
namespace: { ref: namespace },
126127
});
127128

128129
clone.collection = collection;
@@ -171,7 +172,7 @@ export class DataAPIHttpClient extends HttpClient {
171172
info.collection ||= this.collection;
172173

173174
if (info.namespace !== null) {
174-
info.namespace ||= this.namespace;
175+
info.namespace ||= this.namespace?.ref;
175176
}
176177

177178
const keyspacePath = info.namespace ? `/${info.namespace}` : '';

src/api/clients/types.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import type TypedEmitter from 'typed-emitter';
1616
import type { DataAPICommandEvents } from '@/src/data-api';
1717
import type { FetchCtx, HttpMethods } from '@/src/api';
1818
import type { TimeoutManager } from '@/src/api/timeout-managers';
19+
import { Ref } from '@/src/common';
1920

2021
/**
2122
* @internal
@@ -39,6 +40,11 @@ export type HeaderProvider = (() => Promise<Record<string, string>> | Record<str
3940
*/
4041
export type HttpMethodStrings = typeof HttpMethods[keyof typeof HttpMethods];
4142

43+
/**
44+
* @internal
45+
*/
46+
export type NamespaceRef = Ref<string | undefined>;
47+
4248
/**
4349
* @internal
4450
*/

src/api/fetch/types.ts

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@
1313
// limitations under the License.
1414
// noinspection ExceptionCaughtLocallyJS
1515

16+
import { Ref } from '@/src/common';
17+
1618
/**
1719
* A simple adapter interface that allows you to define a custom http client that `astra-db-ts` may use to make requests.
1820
*
@@ -119,6 +121,6 @@ export interface FetcherResponseInfo {
119121
*/
120122
export interface FetchCtx {
121123
ctx: Fetcher,
122-
closed: { ref: boolean },
124+
closed: Ref<boolean>,
123125
maxTimeMS: number | undefined,
124126
}

src/common/types.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,3 +46,8 @@ export type nullish = null | undefined;
4646
* @public
4747
*/
4848
export type DataAPIEnvironment = typeof DataAPIEnvironments[number];
49+
50+
/**
51+
* @internal
52+
*/
53+
export type Ref<T> = { ref: T }

src/data-api/db.ts

Lines changed: 91 additions & 46 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ import {
2323
DataAPIHttpClient,
2424
DEFAULT_DATA_API_PATHS,
2525
DEFAULT_NAMESPACE,
26-
EmissionStrategy,
26+
EmissionStrategy, NamespaceRef,
2727
RawDataAPIResponse,
2828
} from '@/src/api';
2929
import {
@@ -84,21 +84,58 @@ export class Db {
8484
readonly #defaultOpts!: InternalRootClientOpts;
8585
readonly #token!: TokenProvider;
8686

87-
private readonly _httpClient!: DataAPIHttpClient;
87+
private readonly _httpClient: DataAPIHttpClient;
88+
private readonly _namespace: NamespaceRef;
8889
private readonly _id?: string;
8990

91+
/**
92+
* Use {@link DataAPIClient.db} to obtain an instance of this class.
93+
*
94+
* @internal
95+
*/
96+
constructor(endpoint: string, rootOpts: InternalRootClientOpts, dbOpts: DbSpawnOptions | nullish) {
97+
this.#defaultOpts = rootOpts;
98+
99+
this.#token = TokenProvider.parseToken(dbOpts?.token ?? rootOpts.dbOptions.token);
100+
101+
const combinedDbOpts = {
102+
...rootOpts.dbOptions,
103+
...dbOpts,
104+
}
105+
106+
this._namespace = {
107+
ref: (rootOpts.environment === 'astra')
108+
? combinedDbOpts.namespace ?? DEFAULT_NAMESPACE
109+
: combinedDbOpts.namespace
110+
};
111+
112+
this._httpClient = new DataAPIHttpClient({
113+
baseUrl: endpoint,
114+
tokenProvider: this.#token,
115+
embeddingHeaders: EmbeddingHeadersProvider.parseHeaders(null),
116+
baseApiPath: combinedDbOpts.dataApiPath || DEFAULT_DATA_API_PATHS[rootOpts.environment],
117+
emitter: rootOpts.emitter,
118+
monitorCommands: combinedDbOpts.monitorCommands,
119+
fetchCtx: rootOpts.fetchCtx,
120+
namespace: this._namespace,
121+
userAgent: rootOpts.userAgent,
122+
emissionStrategy: EmissionStrategy.Normal,
123+
});
124+
125+
this._id = extractDbIdFromUrl(endpoint);
126+
}
127+
90128
/**
91129
* The default namespace to use for all operations in this database, unless overridden in a method call.
92130
*
93131
* @example
94132
* ```typescript
95-
*
96133
* // Uses 'default_keyspace' as the default namespace for all future db spawns
97134
* const client1 = new DataAPIClient('*TOKEN*');
98135
*
99136
* // Overrides the default namespace for all future db spawns
100137
* const client2 = new DataAPIClient('*TOKEN*', {
101-
*   dbOptions: { namespace: 'my_namespace' }
138+
*   dbOptions: { namespace: 'my_namespace' },
102139
* });
103140
*
104141
* // Created with 'default_keyspace' as the default namespace
@@ -118,62 +155,70 @@ export class Db {
118155
* });
119156
* ```
120157
*/
121-
public readonly namespace!: string;
122-
123-
/**
124-
* Use {@link DataAPIClient.db} to obtain an instance of this class.
125-
*
126-
* @internal
127-
*/
128-
constructor(endpoint: string, rootOpts: InternalRootClientOpts, dbOpts: DbSpawnOptions | nullish) {
129-
this.#defaultOpts = rootOpts;
130-
131-
this.#token = TokenProvider.parseToken(dbOpts?.token ?? rootOpts.dbOptions.token);
132-
133-
const combinedDbOpts = {
134-
...rootOpts.dbOptions,
135-
...dbOpts,
158+
public get namespace(): string {
159+
if (!this._namespace.ref) {
160+
throw new Error('No namespace set for DB (can\'t do db.namespace)');
136161
}
137-
138-
Object.defineProperty(this, 'namespace', {
139-
value: combinedDbOpts.namespace ?? DEFAULT_NAMESPACE,
140-
writable: false,
141-
});
142-
143-
Object.defineProperty(this, '_httpClient', {
144-
value: new DataAPIHttpClient({
145-
baseUrl: endpoint,
146-
tokenProvider: this.#token,
147-
embeddingHeaders: EmbeddingHeadersProvider.parseHeaders(null),
148-
baseApiPath: combinedDbOpts.dataApiPath || DEFAULT_DATA_API_PATHS[rootOpts.environment],
149-
emitter: rootOpts.emitter,
150-
monitorCommands: combinedDbOpts.monitorCommands,
151-
fetchCtx: rootOpts.fetchCtx,
152-
namespace: this.namespace,
153-
userAgent: rootOpts.userAgent,
154-
emissionStrategy: EmissionStrategy.Normal,
155-
}),
156-
enumerable: false,
157-
});
158-
159-
Object.defineProperty(this, '_id', {
160-
value: extractDbIdFromUrl(endpoint),
161-
enumerable: false,
162-
});
162+
return this._namespace.ref;
163163
}
164164

165165
/**
166166
* The ID of the database, if it's an Astra database. If it's not an Astra database, this will throw an error.
167167
*
168168
* @throws Error - if the database is not an Astra database.
169169
*/
170-
get id(): string {
170+
public get id(): string {
171171
if (!this._id) {
172172
throw new Error('Non-Astra databases do not have an appropriate ID');
173173
}
174174
return this._id;
175175
}
176176

177+
/**
178+
* Sets the default working namespace of the `Db` instance. Does not retroactively update any previous collections
179+
* spawned from this `Db` to use the new namespace.
180+
*
181+
* @example
182+
* ```typescript
183+
* // Spawns a `Db` with default working namespace `my_namespace`
184+
* const db = client.db('<endpoint>', { namespace: 'my_namespace' });
185+
*
186+
* // Gets a collection from namespace `my_namespace`
187+
* const coll1 = db.collection('my_coll');
188+
*
189+
* // `db` now uses `my_other_namespace` as the default namespace for all operations
190+
* db.useNamespace('my_other_namespace');
191+
*
192+
* // Gets a collection from namespace `my_other_namespace`
193+
* // `coll1` still uses namespace `my_namespace`
194+
* const coll2 = db.collection('my_other_coll');
195+
*
196+
* // Gets `my_coll` from namespace `my_namespace` again
197+
* // (The default namespace is still `my_other_namespace`)
198+
* const coll3 = db.collection('my_coll', { namespace: 'my_namespace' });
199+
* ```
200+
*
201+
* @example
202+
* ```typescript
203+
* // If using non-astra, this may be a common idiom:
204+
* const client = new DataAPIClient({ environment: 'dse' });
205+
* const db = client.db('<endpoint>', { token: '<token>' });
206+
*
207+
* // Will internally call `db.useNamespace('new_namespace')`
208+
* await db.admin().createNamespace('new_namespace', {
209+
*   updateDbNamespace: true,
210+
* });
211+
*
212+
* // Creates collection in namespace `new_namespace` by default now
213+
* const coll = db.createCollection('my_coll');
214+
* ```
215+
*
216+
* @param namespace - The namespace to use
217+
*/
218+
public useNamespace(namespace: string) {
219+
this._namespace.ref = namespace;
220+
}
221+
177222
/**
178223
* Spawns a new {@link AstraDbAdmin} instance for this database, used for performing administrative operations
179224
* on the database, such as managing namespaces, or getting database information.

src/data-api/types/misc/spawn-db.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -25,8 +25,13 @@ export interface DbSpawnOptions {
2525
/**
2626
* The namespace (aka keyspace) to use for the database.
2727
*
28-
* Defaults to `'default_keyspace'`. if never provided. However, if it was provided when creating the
29-
* {@link DataAPIClient}, it will default to that value instead.
28+
* There are a few rules for what the default namespace will be:
29+
* 1. If a namespace was provided when creating the {@link DataAPIClient}, it will default to that value.
30+
* 2. If using an `astra` database, it'll default to "default_namespace".
31+
* 3. Otherwise, no default will be set, and it'll be on the user to provide one when necessary.
32+
*
33+
* The client itself will not throw an error if an invalid namespace (or even no namespace at all) is provided—it'll
34+
* let the Data API propagate the error itself.
3035
*
3136
* Every db method will use this namespace as the default namespace, but they all allow you to override it
3237
* in their options.

src/devops/astra-db-admin.ts

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414
// noinspection ExceptionCaughtLocallyJS
1515

16-
import { AdminBlockingOptions, AdminSpawnOptions, FullDatabaseInfo } from '@/src/devops/types';
16+
import { AdminBlockingOptions, AdminSpawnOptions, CreateNamespaceOptions, FullDatabaseInfo } from '@/src/devops/types';
1717
import { DEFAULT_DEVOPS_API_ENDPOINT, DEFAULT_NAMESPACE, DevOpsAPIHttpClient, HttpMethods } from '@/src/api';
1818
import { Db } from '@/src/data-api';
1919
import { DbAdmin } from '@/src/devops/db-admin';
@@ -202,7 +202,7 @@ export class AstraDbAdmin extends DbAdmin {
202202
*
203203
* @returns A promise that resolves when the operation completes.
204204
*/
205-
public override async createNamespace(namespace: string, options?: AdminBlockingOptions): Promise<void> {
205+
public override async createNamespace(namespace: string, options?: CreateNamespaceOptions): Promise<void> {
206206
await this._httpClient.requestLongRunning({
207207
method: HttpMethods.Post,
208208
path: `/databases/${this._db.id}/keyspaces/${namespace}`,
@@ -213,6 +213,10 @@ export class AstraDbAdmin extends DbAdmin {
213213
defaultPollInterval: 1000,
214214
options,
215215
});
216+
217+
if (options?.updateDbNamespace) {
218+
this._db.useNamespace(namespace);
219+
}
216220
}
217221

218222
/**

src/devops/data-api-db-admin.ts

Lines changed: 9 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -19,7 +19,7 @@ import { Db } from '@/src/data-api';
1919
import { DbAdmin } from '@/src/devops/db-admin';
2020
import { WithTimeout } from '@/src/common/types';
2121
import { validateAdminOpts } from '@/src/devops/utils';
22-
import { CreateNamespaceOptions } from '@/src/devops/types/db-admin/create-namespace';
22+
import { LocalCreateNamespaceOptions } from '@/src/devops/types/db-admin/local-create-namespace';
2323

2424
/**
2525
* An administrative class for managing non-Astra databases, including creating, listing, and deleting namespaces.
@@ -130,14 +130,14 @@ export class DataAPIDbAdmin extends DbAdmin {
130130
* ```typescript
131131
* await dbAdmin.createNamespace('my_namespace');
132132
*
133-
* await dbAdmin.createNamespace('my_namespace' {
133+
* await dbAdmin.createNamespace('my_namespace', {
134134
*   replication: {
135135
*   class: 'SimpleStrategy',
136136
*   replicatonFactor: 3,
137137
*   },
138138
* });
139139
*
140-
* await dbAdmin.createNamespace('my_namespace' {
140+
* await dbAdmin.createNamespace('my_namespace', {
141141
*   replication: {
142142
*   class: 'NetworkTopologyStrategy',
143143
*   datacenter1: 3,
@@ -151,12 +151,17 @@ export class DataAPIDbAdmin extends DbAdmin {
151151
*
152152
* @returns A promise that resolves when the operation completes.
153153
*/
154-
public override async createNamespace(namespace: string, options?: CreateNamespaceOptions): Promise<void> {
154+
public override async createNamespace(namespace: string, options?: LocalCreateNamespaceOptions): Promise<void> {
155155
const replication = options?.replication ?? {
156156
class: 'SimpleStrategy',
157157
replicationFactor: 1,
158158
};
159+
159160
await this._httpClient.executeCommand({ createNamespace: { name: namespace, options: { replication } } }, { maxTimeMS: options?.maxTimeMS });
161+
162+
if (options?.updateDbNamespace) {
163+
this._db.useNamespace(namespace);
164+
}
160165
}
161166

162167
/**

src/devops/db-admin.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@
1313
// limitations under the License.
1414
// noinspection ExceptionCaughtLocallyJS
1515

16-
import { AdminBlockingOptions } from '@/src/devops/types';
16+
import { AdminBlockingOptions, CreateNamespaceOptions } from '@/src/devops/types';
1717
import { Db } from '@/src/data-api';
1818

1919
/**
@@ -92,7 +92,7 @@ export abstract class DbAdmin {
9292
*
9393
* @returns A promise that resolves when the operation completes.
9494
*/
95-
abstract createNamespace(namespace: string, options?: AdminBlockingOptions): Promise<void>;
95+
abstract createNamespace(namespace: string, options?: CreateNamespaceOptions): Promise<void>;
9696
/**
9797
* Drops a namespace (aka keyspace) from this database.
9898
*

0 commit comments

Comments
 (0)