-
Notifications
You must be signed in to change notification settings - Fork 2k
/
cassandra.ts
248 lines (219 loc) Β· 8.08 KB
/
cassandra.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
import { Client, DseClientOptions } from "cassandra-driver";
import fs from "node:fs/promises";
import * as path from "node:path";
import * as os from "node:os";
export interface AstraServiceProviderArgs {
datacenterID?: string;
endpoint?: string | URL;
token: string;
regionName?: string;
}
export interface CassandraServiceProviderArgs {
astra?: AstraServiceProviderArgs;
}
export interface CassandraClientArgs extends DseClientOptions {
serviceProviderArgs?: CassandraServiceProviderArgs;
}
/**
* A factory class for creating Cassandra clients.
*/
export class CassandraClientFactory {
/**
* Get a Cassandra client with the provided arguments.
* @param args The arguments for creating the Cassandra client.
* @returns A Client object from the cassandra-driver.
*/
public static async getClient(args: CassandraClientArgs): Promise<Client> {
const modifiedArgs = await this.processArgs(args);
return new Client(modifiedArgs);
}
/**
* Process the arguments for creating a Cassandra client.
* @param args The arguments for creating the Cassandra client.
* @returns The processed arguments.
* @throws Error if the configuration is unsupported (e.g. unknown service provider).
*/
private static processArgs(
args: CassandraClientArgs
): Promise<CassandraClientArgs> {
if (!args.serviceProviderArgs) {
return Promise.resolve(args);
}
if (args.serviceProviderArgs && args.serviceProviderArgs.astra) {
return CassandraClientFactory.processAstraArgs(args);
}
throw new Error("Unsupported configuration for Cassandra client.");
}
/**
* Process the arguments for creating a Cassandra client to Astra.
* @param args The arguments for creating the Cassandra client.
* @returns The processed arguments for connecting to Astra.
* @throws Error if the Astra configuration is not provided.
*/
private static async processAstraArgs(
args: CassandraClientArgs
): Promise<CassandraClientArgs> {
const astraArgs = args.serviceProviderArgs?.astra;
if (!astraArgs) {
throw new Error("Astra configuration is not provided in args.");
}
if (!astraArgs.endpoint && !astraArgs.datacenterID) {
throw new Error(
"Astra endpoint or datacenterID must be provided in args."
);
}
// Extract datacenterID and regionName from endpoint if provided
if (astraArgs.endpoint) {
const endpoint = new URL(astraArgs.endpoint.toString());
const hostnameParts = endpoint.hostname.split("-");
const domainSuffix = ".apps.astra.datastax.com";
if (hostnameParts[hostnameParts.length - 1].endsWith(domainSuffix)) {
astraArgs.datacenterID =
astraArgs.datacenterID || hostnameParts.slice(0, 5).join("-");
// Extract regionName by joining elements from index 5 to the end, and then remove the domain suffix
const fullRegionName = hostnameParts.slice(5).join("-");
astraArgs.regionName =
astraArgs.regionName || fullRegionName.replace(domainSuffix, "");
}
}
// Initialize cloud configuration if not already defined
const modifiedArgs = {
...args,
cloud: args.cloud || { secureConnectBundle: "" },
};
// Set default bundle location if it is not set
if (!modifiedArgs.cloud.secureConnectBundle) {
modifiedArgs.cloud.secureConnectBundle =
await CassandraClientFactory.getAstraDefaultBundleLocation(astraArgs);
}
// Ensure secure connect bundle exists
await CassandraClientFactory.setAstraBundle(
astraArgs,
modifiedArgs.cloud.secureConnectBundle
);
// Ensure credentials are set
modifiedArgs.credentials = modifiedArgs.credentials || {
username: "token",
password: astraArgs.token,
};
return modifiedArgs;
}
/**
* Get the default bundle location for Astra.
* @param astraArgs The Astra service provider arguments.
* @returns The default bundle file path.
*/
private static async getAstraDefaultBundleLocation(
astraArgs: AstraServiceProviderArgs
): Promise<string> {
const dir = path.join(os.tmpdir(), "cassandra-astra");
await fs.mkdir(dir, { recursive: true });
let scbFileName = `astra-secure-connect-${astraArgs.datacenterID}`;
if (astraArgs.regionName) {
scbFileName += `-${astraArgs.regionName}`;
}
scbFileName += ".zip";
const scbPath = path.join(dir, scbFileName);
return scbPath;
}
/**
* Set the Astra bundle for the Cassandra client.
* @param astraArgs The Astra service provider arguments.
* @param scbPath The path to the secure connect bundle.
* @returns Promise that resolves when the bundle is set.
* @throws Error if the bundle URLs cannot be retrieved or the file cannot be downloaded.
*/
private static async setAstraBundle(
astraArgs: AstraServiceProviderArgs,
scbPath: string | URL
): Promise<void> {
// If scbPath is a URL, we assume the URL is correct and do nothing further.
// But if it is a string, we need to check if the file exists and download it if necessary.
if (typeof scbPath === "string") {
try {
// Check if the file exists
const stats = await fs.stat(scbPath);
// Calculate the age of the file in days
const fileAgeInDays =
(Date.now() - stats.mtime.getTime()) / (1000 * 60 * 60 * 24);
// File is more than 360 days old, download a fresh copy
if (fileAgeInDays > 360) {
await CassandraClientFactory.downloadAstraSecureConnectBundle(
astraArgs,
scbPath
);
}
} catch (error: unknown) {
if (
typeof error === "object" &&
error !== null &&
"code" in error &&
error.code === "ENOENT"
) {
// Handle file not found error (ENOENT)
await CassandraClientFactory.downloadAstraSecureConnectBundle(
astraArgs,
scbPath
);
} else {
throw error;
}
}
}
}
/**
* Downloads the Astra secure connect bundle for the given Astra service provider arguments
* and saves it to the specified file path.
*
* @param astraArgs - The Astra service provider arguments.
* @param scbPath - The file path to save the secure connect bundle.
* @returns A promise that resolves when the secure connect bundle is downloaded and saved.
* @throws An error if there is an HTTP error or if the secure bundle URLs cannot be retrieved.
*/
private static async downloadAstraSecureConnectBundle(
astraArgs: AstraServiceProviderArgs,
scbPath: string
): Promise<void> {
if (!astraArgs.datacenterID) {
throw new Error("Astra datacenterID is not provided in args.");
}
// First POST request gets all bundle locations for the database_id
const bundleURLTemplate =
"https://api.astra.datastax.com/v2/databases/{database_id}/secureBundleURL?all=true";
const url = bundleURLTemplate.replace(
"{database_id}",
astraArgs.datacenterID
);
const postResponse = await fetch(url, {
method: "POST",
headers: {
Authorization: `Bearer ${astraArgs.token}`,
"Content-Type": "application/json",
},
});
if (!postResponse.ok) {
throw new Error(`HTTP error! Status: ${postResponse.status}`);
}
const postData = await postResponse.json();
if (!postData || !Array.isArray(postData) || postData.length === 0) {
throw new Error("Failed to get secure bundle URLs.");
}
// Find the download URL for the region, if specified
let { downloadURL } = postData[0];
if (astraArgs.regionName) {
const regionalBundle = postData.find(
(bundle) => bundle.region === astraArgs.regionName
);
if (regionalBundle) {
downloadURL = regionalBundle.downloadURL;
}
}
// GET request to download the file itself, and write to disk
const getResponse = await fetch(downloadURL);
if (!getResponse.ok) {
throw new Error(`HTTP error! Status: ${getResponse.status}`);
}
const bundleData = await getResponse.arrayBuffer();
await fs.writeFile(scbPath, Buffer.from(bundleData));
}
}