pacificdb-client 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +16 -0
- package/LICENSE +21 -0
- package/README.md +100 -0
- package/dist/index.cjs +377 -0
- package/dist/index.d.cts +142 -0
- package/dist/index.d.ts +142 -0
- package/dist/index.js +340 -0
- package/dist/index.mjs +340 -0
- package/package.json +47 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,340 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var PacificDBError = class extends Error {
|
|
3
|
+
code;
|
|
4
|
+
requestId;
|
|
5
|
+
constructor(message, code = "PACIFICDB_ERROR", requestId) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.name = new.target.name;
|
|
8
|
+
this.code = code;
|
|
9
|
+
this.requestId = requestId;
|
|
10
|
+
}
|
|
11
|
+
};
|
|
12
|
+
var PacificDBConfigError = class extends PacificDBError {
|
|
13
|
+
constructor(message) {
|
|
14
|
+
super(message, "CONFIG_ERROR");
|
|
15
|
+
}
|
|
16
|
+
};
|
|
17
|
+
var PacificDBAuthError = class extends PacificDBError {
|
|
18
|
+
constructor(message, requestId) {
|
|
19
|
+
super(message, "AUTH_ERROR", requestId);
|
|
20
|
+
}
|
|
21
|
+
};
|
|
22
|
+
var PacificDBClientError = class extends PacificDBError {
|
|
23
|
+
status;
|
|
24
|
+
constructor(message, code, status, requestId) {
|
|
25
|
+
super(message, code, requestId);
|
|
26
|
+
this.status = status;
|
|
27
|
+
}
|
|
28
|
+
};
|
|
29
|
+
var PacificDBServerError = class extends PacificDBError {
|
|
30
|
+
status;
|
|
31
|
+
constructor(message, code, status, requestId) {
|
|
32
|
+
super(message, code, requestId);
|
|
33
|
+
this.status = status;
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
var PacificDBTimeoutError = class extends PacificDBError {
|
|
37
|
+
constructor(timeoutMs, requestId) {
|
|
38
|
+
super(`PacificDB request timed out after ${timeoutMs}ms`, "TIMEOUT", requestId);
|
|
39
|
+
}
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
// src/connectionString.ts
|
|
43
|
+
var SECRET_KEYS = /* @__PURE__ */ new Set(["password", "token", "secret", "key", "sessiontoken", "connectionstring"]);
|
|
44
|
+
function parseConnectionString(uri) {
|
|
45
|
+
let url;
|
|
46
|
+
try {
|
|
47
|
+
url = new URL(uri);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
throw new PacificDBConfigError(`Invalid PacificDB connection string: ${error.message}`);
|
|
50
|
+
}
|
|
51
|
+
const protocol = url.protocol.replace(":", "");
|
|
52
|
+
if (protocol !== "pacificdb") {
|
|
53
|
+
throw new PacificDBConfigError("PacificDB connection strings must use the pacificdb:// protocol.");
|
|
54
|
+
}
|
|
55
|
+
if (!url.username || !url.password) {
|
|
56
|
+
throw new PacificDBConfigError("PacificDB connection string requires a database username and password.");
|
|
57
|
+
}
|
|
58
|
+
const databaseName = decodeURIComponent(url.pathname.replace(/^\/+/, ""));
|
|
59
|
+
if (!databaseName) {
|
|
60
|
+
throw new PacificDBConfigError("PacificDB connection string requires a database name path.");
|
|
61
|
+
}
|
|
62
|
+
const orgId = url.searchParams.get("orgId") || url.searchParams.get("organizationId") || "";
|
|
63
|
+
const projectId = url.searchParams.get("projectId") || "";
|
|
64
|
+
const clusterId = url.searchParams.get("clusterId") || "";
|
|
65
|
+
const databaseId = url.searchParams.get("databaseId") || "";
|
|
66
|
+
if (!orgId || !projectId || !clusterId || !databaseId) {
|
|
67
|
+
throw new PacificDBConfigError("PacificDB connection string requires orgId, projectId, clusterId, and databaseId.");
|
|
68
|
+
}
|
|
69
|
+
const tlsRaw = url.searchParams.get("tls");
|
|
70
|
+
return {
|
|
71
|
+
protocol: "pacificdb",
|
|
72
|
+
username: decodeURIComponent(url.username),
|
|
73
|
+
password: decodeURIComponent(url.password),
|
|
74
|
+
clusterHost: url.host,
|
|
75
|
+
databaseName,
|
|
76
|
+
orgId,
|
|
77
|
+
projectId,
|
|
78
|
+
clusterId,
|
|
79
|
+
databaseId,
|
|
80
|
+
tls: tlsRaw === null ? true : tlsRaw !== "false",
|
|
81
|
+
authSource: url.searchParams.get("authSource") || "admin",
|
|
82
|
+
appName: url.searchParams.get("appName") || void 0
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
function buildConnectionString(input) {
|
|
86
|
+
const url = new URL(`pacificdb://${input.clusterHost}/${encodeURIComponent(input.databaseName)}`);
|
|
87
|
+
url.username = encodeURIComponent(input.username);
|
|
88
|
+
url.password = encodeURIComponent(input.password);
|
|
89
|
+
if (input.orgId) url.searchParams.set("orgId", input.orgId);
|
|
90
|
+
url.searchParams.set("projectId", input.projectId);
|
|
91
|
+
url.searchParams.set("clusterId", input.clusterId);
|
|
92
|
+
url.searchParams.set("databaseId", input.databaseId);
|
|
93
|
+
url.searchParams.set("tls", String(input.tls !== false));
|
|
94
|
+
url.searchParams.set("authSource", input.authSource || "admin");
|
|
95
|
+
if (input.appName) url.searchParams.set("appName", input.appName);
|
|
96
|
+
return url.toString();
|
|
97
|
+
}
|
|
98
|
+
function redactConnectionString(uri) {
|
|
99
|
+
try {
|
|
100
|
+
const url = new URL(uri);
|
|
101
|
+
if (url.password) url.password = "REDACTED";
|
|
102
|
+
return url.toString();
|
|
103
|
+
} catch {
|
|
104
|
+
return uri.replace(/(pacificdb:\/\/[^:]+:)[^@]+@/i, "$1REDACTED@");
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
function redactSecrets(value) {
|
|
108
|
+
if (!value || typeof value !== "object") return value;
|
|
109
|
+
if (Array.isArray(value)) return value.map(redactSecrets);
|
|
110
|
+
const out = {};
|
|
111
|
+
for (const [key, item] of Object.entries(value)) {
|
|
112
|
+
out[key] = SECRET_KEYS.has(key.toLowerCase()) ? "REDACTED" : redactSecrets(item);
|
|
113
|
+
}
|
|
114
|
+
return out;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// src/client.ts
|
|
118
|
+
var RETRIABLE_CODES = /* @__PURE__ */ new Set(["server_busy", "not_leader", "TIMEOUT", "RATE_LIMITED", "HTTP_503", "HTTP_429", "ENGINE_UNAVAILABLE"]);
|
|
119
|
+
function randomRequestId() {
|
|
120
|
+
return `req_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 10)}`;
|
|
121
|
+
}
|
|
122
|
+
var PacificDBClient = class {
|
|
123
|
+
connection;
|
|
124
|
+
fetchImpl;
|
|
125
|
+
apiUrl;
|
|
126
|
+
readTimeoutMs;
|
|
127
|
+
writeTimeoutMs;
|
|
128
|
+
retryLimit;
|
|
129
|
+
retryBackoffMs;
|
|
130
|
+
logger;
|
|
131
|
+
sessionToken = "";
|
|
132
|
+
connected = false;
|
|
133
|
+
constructor(uri, options = {}) {
|
|
134
|
+
this.connection = parseConnectionString(uri);
|
|
135
|
+
this.apiUrl = (options.apiUrl || `https://${this.connection.clusterHost}`).replace(/\/+$/, "");
|
|
136
|
+
if (this.apiUrl.startsWith("http://") && !options.allowInsecure) {
|
|
137
|
+
throw new PacificDBConfigError(
|
|
138
|
+
"Refusing to send database credentials over http://. Use https, or pass { allowInsecure: true } for local development."
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
const fetchImpl = options.fetch || (typeof fetch !== "undefined" ? fetch : void 0);
|
|
142
|
+
if (!fetchImpl) {
|
|
143
|
+
throw new PacificDBConfigError("No fetch implementation available. Use Node.js >= 18 or pass options.fetch.");
|
|
144
|
+
}
|
|
145
|
+
this.fetchImpl = fetchImpl;
|
|
146
|
+
this.readTimeoutMs = options.readTimeoutMs ?? 3e4;
|
|
147
|
+
this.writeTimeoutMs = options.writeTimeoutMs ?? 3e4;
|
|
148
|
+
this.retryLimit = options.retryLimit ?? 3;
|
|
149
|
+
this.retryBackoffMs = options.retryBackoffMs ?? 250;
|
|
150
|
+
this.logger = options.logger;
|
|
151
|
+
}
|
|
152
|
+
/** The redacted URI (safe for logs/errors). */
|
|
153
|
+
get redactedUri() {
|
|
154
|
+
return redactConnectionString(
|
|
155
|
+
`pacificdb://${encodeURIComponent(this.connection.username)}:x@${this.connection.clusterHost}/${this.connection.databaseName}`
|
|
156
|
+
);
|
|
157
|
+
}
|
|
158
|
+
async connect() {
|
|
159
|
+
await this.authenticate();
|
|
160
|
+
this.connected = true;
|
|
161
|
+
}
|
|
162
|
+
async authenticate() {
|
|
163
|
+
const result = await this.request("POST", "/api/v1/db/sessions", {
|
|
164
|
+
username: this.connection.username,
|
|
165
|
+
password: this.connection.password,
|
|
166
|
+
orgId: this.connection.orgId,
|
|
167
|
+
projectId: this.connection.projectId,
|
|
168
|
+
clusterId: this.connection.clusterId,
|
|
169
|
+
databaseId: this.connection.databaseId,
|
|
170
|
+
databaseName: this.connection.databaseName,
|
|
171
|
+
authSource: this.connection.authSource
|
|
172
|
+
}, { requireSession: false, isWrite: true });
|
|
173
|
+
if (!result?.sessionToken) {
|
|
174
|
+
throw new PacificDBAuthError("Database authentication did not return a session token.");
|
|
175
|
+
}
|
|
176
|
+
this.sessionToken = result.sessionToken;
|
|
177
|
+
this.logger?.debug?.("pacificdb.session.created", { database: this.connection.databaseName });
|
|
178
|
+
}
|
|
179
|
+
async close() {
|
|
180
|
+
if (!this.sessionToken) return;
|
|
181
|
+
try {
|
|
182
|
+
await this.request("DELETE", "/api/v1/db/sessions/current", void 0, { isWrite: true });
|
|
183
|
+
} catch {
|
|
184
|
+
} finally {
|
|
185
|
+
this.sessionToken = "";
|
|
186
|
+
this.connected = false;
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
collection(name) {
|
|
190
|
+
const encoded = encodeURIComponent(name);
|
|
191
|
+
return {
|
|
192
|
+
insertOne: async (document) => {
|
|
193
|
+
const r = await this.request(
|
|
194
|
+
"POST",
|
|
195
|
+
`/api/v1/db/collections/${encoded}/documents`,
|
|
196
|
+
{ document },
|
|
197
|
+
{ isWrite: true }
|
|
198
|
+
);
|
|
199
|
+
return { inserted: r?.inserted !== false, id: r?.id ?? null };
|
|
200
|
+
},
|
|
201
|
+
findOne: async (filter) => {
|
|
202
|
+
const r = await this.request(
|
|
203
|
+
"POST",
|
|
204
|
+
`/api/v1/db/collections/${encoded}/find`,
|
|
205
|
+
{ query: filter, limit: 1 },
|
|
206
|
+
{ isWrite: false }
|
|
207
|
+
);
|
|
208
|
+
return r?.documents?.[0] ?? null;
|
|
209
|
+
},
|
|
210
|
+
find: async (filter, options = {}) => {
|
|
211
|
+
const r = await this.request(
|
|
212
|
+
"POST",
|
|
213
|
+
`/api/v1/db/collections/${encoded}/find`,
|
|
214
|
+
{ query: filter, ...options },
|
|
215
|
+
{ isWrite: false }
|
|
216
|
+
);
|
|
217
|
+
return r?.documents ?? [];
|
|
218
|
+
},
|
|
219
|
+
updateOne: (filter, update) => this.request("PATCH", `/api/v1/db/collections/${encoded}/documents`, { filter, update }, { isWrite: true }),
|
|
220
|
+
deleteOne: (filter) => this.request("DELETE", `/api/v1/db/collections/${encoded}/documents`, { filter }, { isWrite: true })
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
model(name, schema) {
|
|
224
|
+
const collection = this.collection(name);
|
|
225
|
+
const validate = (document) => {
|
|
226
|
+
for (const [field, type] of Object.entries(schema)) {
|
|
227
|
+
const value = document[field];
|
|
228
|
+
if (value === void 0 || value === null) continue;
|
|
229
|
+
const expected = typeof type === "function" ? type.name?.toLowerCase() : void 0;
|
|
230
|
+
if (expected && expected !== "object" && typeof value !== expected) {
|
|
231
|
+
throw new PacificDBConfigError(`Field "${field}" expected ${expected}, got ${typeof value}.`);
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
};
|
|
235
|
+
return {
|
|
236
|
+
create: async (document) => {
|
|
237
|
+
validate(document);
|
|
238
|
+
return collection.insertOne(document);
|
|
239
|
+
},
|
|
240
|
+
findOne: (filter) => collection.findOne(filter),
|
|
241
|
+
find: (filter, options) => collection.find(filter, options),
|
|
242
|
+
updateOne: (filter, update) => collection.updateOne(filter, update),
|
|
243
|
+
deleteOne: (filter) => collection.deleteOne(filter)
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
async request(method, path, body, opts = {}) {
|
|
247
|
+
const { requireSession = true, isWrite = false } = opts;
|
|
248
|
+
if (requireSession && !this.sessionToken) {
|
|
249
|
+
throw new PacificDBAuthError("Call connect() before issuing database operations.");
|
|
250
|
+
}
|
|
251
|
+
const timeoutMs = isWrite ? this.writeTimeoutMs : this.readTimeoutMs;
|
|
252
|
+
const url = new URL(path, `${this.apiUrl}/`);
|
|
253
|
+
url.searchParams.set("projectId", this.connection.projectId);
|
|
254
|
+
url.searchParams.set("clusterId", this.connection.clusterId);
|
|
255
|
+
url.searchParams.set("databaseId", this.connection.databaseId);
|
|
256
|
+
let refreshed = false;
|
|
257
|
+
let lastError = null;
|
|
258
|
+
for (let attempt = 0; attempt <= this.retryLimit; attempt++) {
|
|
259
|
+
const requestId = randomRequestId();
|
|
260
|
+
const controller = new AbortController();
|
|
261
|
+
const timer = setTimeout(() => controller.abort(), timeoutMs);
|
|
262
|
+
try {
|
|
263
|
+
const response = await this.fetchImpl(url, {
|
|
264
|
+
method,
|
|
265
|
+
signal: controller.signal,
|
|
266
|
+
headers: {
|
|
267
|
+
"content-type": "application/json",
|
|
268
|
+
"x-request-id": requestId,
|
|
269
|
+
...this.sessionToken ? { authorization: `Bearer ${this.sessionToken}` } : {}
|
|
270
|
+
},
|
|
271
|
+
body: body === void 0 ? void 0 : JSON.stringify(body)
|
|
272
|
+
});
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
const json = await response.json().catch(() => ({}));
|
|
275
|
+
if (response.ok && json.success !== false) {
|
|
276
|
+
return json.data ?? json;
|
|
277
|
+
}
|
|
278
|
+
const errObj = typeof json.error === "object" ? json.error : void 0;
|
|
279
|
+
const code = errObj?.code || (typeof json.error === "string" ? json.error : "") || `HTTP_${response.status}`;
|
|
280
|
+
const message = errObj?.message || `PacificDB request failed (${code})`;
|
|
281
|
+
if (response.status === 401 && this.connected && !refreshed && requireSession) {
|
|
282
|
+
refreshed = true;
|
|
283
|
+
this.logger?.debug?.("pacificdb.session.refresh", { requestId });
|
|
284
|
+
await this.authenticate();
|
|
285
|
+
attempt--;
|
|
286
|
+
continue;
|
|
287
|
+
}
|
|
288
|
+
if (response.status === 401) {
|
|
289
|
+
throw new PacificDBAuthError(message, requestId);
|
|
290
|
+
}
|
|
291
|
+
const retriable = RETRIABLE_CODES.has(code) || response.status === 429 || response.status === 503;
|
|
292
|
+
if (!retriable || attempt === this.retryLimit) {
|
|
293
|
+
if (response.status >= 500) throw new PacificDBServerError(message, code, response.status, requestId);
|
|
294
|
+
throw new PacificDBClientError(message, code, response.status, requestId);
|
|
295
|
+
}
|
|
296
|
+
const retryAfter = Number(response.headers.get("retry-after-ms") || json.retry_after_ms || 0);
|
|
297
|
+
lastError = new PacificDBServerError(message, code, response.status, requestId);
|
|
298
|
+
await this.backoff(attempt, retryAfter);
|
|
299
|
+
} catch (error) {
|
|
300
|
+
clearTimeout(timer);
|
|
301
|
+
if (error instanceof PacificDBClientError || error instanceof PacificDBAuthError || error instanceof PacificDBConfigError || error instanceof PacificDBServerError) {
|
|
302
|
+
if (error instanceof PacificDBServerError && attempt < this.retryLimit) {
|
|
303
|
+
lastError = error;
|
|
304
|
+
await this.backoff(attempt, 0);
|
|
305
|
+
continue;
|
|
306
|
+
}
|
|
307
|
+
throw error;
|
|
308
|
+
}
|
|
309
|
+
const isAbort = error.name === "AbortError";
|
|
310
|
+
lastError = isAbort ? new PacificDBTimeoutError(timeoutMs) : error;
|
|
311
|
+
this.logger?.warn?.("pacificdb.request.retry", {
|
|
312
|
+
attempt,
|
|
313
|
+
code: isAbort ? "TIMEOUT" : "NETWORK_ERROR",
|
|
314
|
+
target: this.redactedUri
|
|
315
|
+
});
|
|
316
|
+
if (attempt === this.retryLimit) break;
|
|
317
|
+
await this.backoff(attempt, 0);
|
|
318
|
+
}
|
|
319
|
+
}
|
|
320
|
+
throw lastError || new PacificDBServerError("PacificDB request failed.", "UNKNOWN", 0);
|
|
321
|
+
}
|
|
322
|
+
backoff(attempt, retryAfterMs) {
|
|
323
|
+
const base = retryAfterMs || this.retryBackoffMs * Math.pow(2, attempt);
|
|
324
|
+
const jitter = Math.floor(Math.random() * this.retryBackoffMs);
|
|
325
|
+
return new Promise((resolve) => setTimeout(resolve, base + jitter));
|
|
326
|
+
}
|
|
327
|
+
};
|
|
328
|
+
export {
|
|
329
|
+
PacificDBAuthError,
|
|
330
|
+
PacificDBClient,
|
|
331
|
+
PacificDBClientError,
|
|
332
|
+
PacificDBConfigError,
|
|
333
|
+
PacificDBError,
|
|
334
|
+
PacificDBServerError,
|
|
335
|
+
PacificDBTimeoutError,
|
|
336
|
+
buildConnectionString,
|
|
337
|
+
parseConnectionString,
|
|
338
|
+
redactConnectionString,
|
|
339
|
+
redactSecrets
|
|
340
|
+
};
|
package/package.json
ADDED
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "pacificdb-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Official PacificDB TypeScript/JavaScript client",
|
|
5
|
+
"keywords": ["pacificdb", "database", "nosql", "client", "sdk"],
|
|
6
|
+
"license": "MIT",
|
|
7
|
+
"author": "PacificDB",
|
|
8
|
+
"repository": {
|
|
9
|
+
"type": "git",
|
|
10
|
+
"url": "https://github.com/hitesh-reddy-k/database-engine-linux.git",
|
|
11
|
+
"directory": "packages/pacificdb-client"
|
|
12
|
+
},
|
|
13
|
+
"type": "module",
|
|
14
|
+
"main": "dist/index.cjs",
|
|
15
|
+
"module": "dist/index.js",
|
|
16
|
+
"types": "dist/index.d.ts",
|
|
17
|
+
"exports": {
|
|
18
|
+
".": {
|
|
19
|
+
"types": "./dist/index.d.ts",
|
|
20
|
+
"import": "./dist/index.js",
|
|
21
|
+
"require": "./dist/index.cjs"
|
|
22
|
+
}
|
|
23
|
+
},
|
|
24
|
+
"files": [
|
|
25
|
+
"dist",
|
|
26
|
+
"README.md",
|
|
27
|
+
"CHANGELOG.md",
|
|
28
|
+
"LICENSE"
|
|
29
|
+
],
|
|
30
|
+
"sideEffects": false,
|
|
31
|
+
"engines": {
|
|
32
|
+
"node": ">=18"
|
|
33
|
+
},
|
|
34
|
+
"devDependencies": {
|
|
35
|
+
"tsup": "^8.3.0",
|
|
36
|
+
"typescript": "^5.5.0"
|
|
37
|
+
},
|
|
38
|
+
"scripts": {
|
|
39
|
+
"build": "tsup src/index.ts --format cjs,esm --dts --out-dir dist",
|
|
40
|
+
"typecheck": "tsc --noEmit",
|
|
41
|
+
"test": "node --test test/*.test.mjs",
|
|
42
|
+
"prepublishOnly": "npm run build && npm test"
|
|
43
|
+
},
|
|
44
|
+
"publishConfig": {
|
|
45
|
+
"access": "public"
|
|
46
|
+
}
|
|
47
|
+
}
|