ydb-qdrant 2.1.1 → 2.1.3

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/README.md CHANGED
@@ -1,8 +1,16 @@
1
1
  <img src="https://ydb-qdrant.tech/logo.svg" alt="YDB Qdrant logo" height="56">
2
2
 
3
+ [![CI](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-ydb-qdrant.yml/badge.svg)](https://github.com/astandrik/ydb-qdrant/actions/workflows/ci-ydb-qdrant.yml)
4
+ [![npm version](https://img.shields.io/npm/v/ydb-qdrant.svg)](https://www.npmjs.com/package/ydb-qdrant)
5
+ [![License: ISC](https://img.shields.io/badge/License-ISC-blue.svg)](https://opensource.org/licenses/ISC)
6
+
3
7
  # YDB Qdrant-compatible Service
4
8
 
5
- Qdrant-compatible Node.js/TypeScript service exposing a minimal REST API that stores and searches vectors in YDB using single‑phase top‑k with an automatic YDB vector index (`vector_kmeans_tree`) and table‑scan fallback. Topics: ydb, vector-search, qdrant-compatible, nodejs, typescript, express, yql, ann, semantic-search, rag.
9
+ Qdrant-compatible Node.js/TypeScript **service and npm library** that stores and searches vectors in YDB using single‑phase top‑k with an automatic YDB vector index (`vector_kmeans_tree`) and table‑scan fallback. Topics: ydb, vector-search, qdrant-compatible, nodejs, typescript, express, yql, ann, semantic-search, rag.
10
+
11
+ Modes:
12
+ - **HTTP server**: Qdrant-compatible REST API (`/collections`, `/points/*`) on top of YDB.
13
+ - **Node.js package**: programmatic client via `createYdbQdrantClient` for direct YDB-backed vector search without running a separate service.
6
14
 
7
15
  Promo site: [ydb-qdrant.tech](http://ydb-qdrant.tech)
8
16
  Architecture diagrams: [docs page](http://ydb-qdrant.tech/docs/)
package/dist/SmokeTest.js CHANGED
@@ -1,5 +1,5 @@
1
1
  import "dotenv/config";
2
- import { createYdbQdrantClient } from "./Api.js";
2
+ import { createYdbQdrantClient } from "./package/Api.js";
3
3
  async function main() {
4
4
  const tenant = process.env.SMOKE_TENANT ?? "smoke";
5
5
  const collection = process.env.SMOKE_COLLECTION ?? "demo";
@@ -30,11 +30,9 @@ async function main() {
30
30
  top: 2,
31
31
  with_payload: true,
32
32
  });
33
- // eslint-disable-next-line no-console
34
33
  console.log(JSON.stringify(result, null, 2));
35
34
  }
36
35
  void main().catch((err) => {
37
- // eslint-disable-next-line no-console
38
36
  console.error(err);
39
37
  process.exitCode = 1;
40
38
  });
package/dist/index.js CHANGED
@@ -4,7 +4,6 @@ import { PORT } from "./config/env.js";
4
4
  import { logger } from "./logging/logger.js";
5
5
  import { readyOrThrow } from "./ydb/client.js";
6
6
  import { ensureMetaTable } from "./ydb/schema.js";
7
- let server;
8
7
  async function start() {
9
8
  try {
10
9
  await readyOrThrow();
@@ -14,7 +13,7 @@ async function start() {
14
13
  logger.error({ err }, "YDB not ready; startup continues, requests may fail until configured.");
15
14
  }
16
15
  const app = buildServer();
17
- server = app.listen(PORT, () => {
16
+ app.listen(PORT, () => {
18
17
  logger.info({ port: PORT }, "ydb-qdrant proxy listening");
19
18
  });
20
19
  }
@@ -1,6 +1,7 @@
1
- import { createCollection as serviceCreateCollection, deleteCollection as serviceDeleteCollection, getCollection as serviceGetCollection, putCollectionIndex as servicePutCollectionIndex, upsertPoints as serviceUpsertPoints, searchPoints as serviceSearchPoints, deletePoints as serviceDeletePoints } from "./services/QdrantService.js";
2
- export { QdrantServiceError } from "./services/QdrantService.js";
3
- export { CreateCollectionReq, UpsertPointsReq, SearchReq, DeletePointsReq, } from "./types.js";
1
+ import type { IAuthService } from "ydb-sdk";
2
+ import { createCollection as serviceCreateCollection, deleteCollection as serviceDeleteCollection, getCollection as serviceGetCollection, putCollectionIndex as servicePutCollectionIndex, upsertPoints as serviceUpsertPoints, searchPoints as serviceSearchPoints, deletePoints as serviceDeletePoints } from "../services/QdrantService.js";
3
+ export { QdrantServiceError } from "../services/QdrantService.js";
4
+ export { CreateCollectionReq, UpsertPointsReq, SearchReq, DeletePointsReq, } from "../types.js";
4
5
  type CreateCollectionResult = Awaited<ReturnType<typeof serviceCreateCollection>>;
5
6
  type GetCollectionResult = Awaited<ReturnType<typeof serviceGetCollection>>;
6
7
  type DeleteCollectionResult = Awaited<ReturnType<typeof serviceDeleteCollection>>;
@@ -10,6 +11,10 @@ type SearchPointsResult = Awaited<ReturnType<typeof serviceSearchPoints>>;
10
11
  type DeletePointsResult = Awaited<ReturnType<typeof serviceDeletePoints>>;
11
12
  export interface YdbQdrantClientOptions {
12
13
  defaultTenant?: string;
14
+ endpoint?: string;
15
+ database?: string;
16
+ connectionString?: string;
17
+ authService?: IAuthService;
13
18
  }
14
19
  export interface YdbQdrantTenantClient {
15
20
  createCollection(collection: string, body: unknown): Promise<CreateCollectionResult>;
@@ -1,9 +1,20 @@
1
- import { readyOrThrow } from "./ydb/client.js";
2
- import { ensureMetaTable } from "./ydb/schema.js";
3
- import { createCollection as serviceCreateCollection, deleteCollection as serviceDeleteCollection, getCollection as serviceGetCollection, putCollectionIndex as servicePutCollectionIndex, upsertPoints as serviceUpsertPoints, searchPoints as serviceSearchPoints, deletePoints as serviceDeletePoints, } from "./services/QdrantService.js";
4
- export { QdrantServiceError } from "./services/QdrantService.js";
5
- export { CreateCollectionReq, UpsertPointsReq, SearchReq, DeletePointsReq, } from "./types.js";
1
+ import { readyOrThrow, configureDriver } from "../ydb/client.js";
2
+ import { ensureMetaTable } from "../ydb/schema.js";
3
+ import { createCollection as serviceCreateCollection, deleteCollection as serviceDeleteCollection, getCollection as serviceGetCollection, putCollectionIndex as servicePutCollectionIndex, upsertPoints as serviceUpsertPoints, searchPoints as serviceSearchPoints, deletePoints as serviceDeletePoints, } from "../services/QdrantService.js";
4
+ export { QdrantServiceError } from "../services/QdrantService.js";
5
+ export { CreateCollectionReq, UpsertPointsReq, SearchReq, DeletePointsReq, } from "../types.js";
6
6
  export async function createYdbQdrantClient(options = {}) {
7
+ if (options.endpoint !== undefined ||
8
+ options.database !== undefined ||
9
+ options.connectionString !== undefined ||
10
+ options.authService !== undefined) {
11
+ configureDriver({
12
+ endpoint: options.endpoint,
13
+ database: options.database,
14
+ connectionString: options.connectionString,
15
+ authService: options.authService,
16
+ });
17
+ }
7
18
  await readyOrThrow();
8
19
  await ensureMetaTable();
9
20
  const defaultTenant = options.defaultTenant ?? "default";
@@ -38,7 +49,7 @@ export async function createYdbQdrantClient(options = {}) {
38
49
  return await serviceDeletePoints({ tenant, collection }, body);
39
50
  },
40
51
  forTenant(tenantId) {
41
- const tenant = resolveTenant(tenantId);
52
+ const tenant = tenantId;
42
53
  return {
43
54
  createCollection(collection, body) {
44
55
  return serviceCreateCollection({ tenant, collection }, body);
@@ -72,12 +72,13 @@ export async function buildVectorIndex(tableName, dimension, distance, vectorTyp
72
72
  await withSession(async (s) => {
73
73
  // Drop existing index if present
74
74
  const dropDdl = `ALTER TABLE ${tableName} DROP INDEX emb_idx;`;
75
+ const rawSession = s;
75
76
  try {
76
- const dropReq = { sessionId: s.sessionId, yqlText: dropDdl };
77
- await s.api.executeSchemeQuery(dropReq);
77
+ const dropReq = { sessionId: rawSession.sessionId, yqlText: dropDdl };
78
+ await rawSession.api.executeSchemeQuery(dropReq);
78
79
  }
79
80
  catch (e) {
80
- const msg = String(e?.message ?? e);
81
+ const msg = e instanceof Error ? e.message : String(e);
81
82
  // ignore if index doesn't exist
82
83
  if (!/not found|does not exist|no such index/i.test(msg)) {
83
84
  throw e;
@@ -98,8 +99,8 @@ export async function buildVectorIndex(tableName, dimension, distance, vectorTyp
98
99
  levels=${levels}
99
100
  );
100
101
  `;
101
- const createReq = { sessionId: s.sessionId, yqlText: createDdl };
102
- await s.api.executeSchemeQuery(createReq);
102
+ const createReq = { sessionId: rawSession.sessionId, yqlText: createDdl };
103
+ await rawSession.api.executeSchemeQuery(createReq);
103
104
  });
104
105
  }
105
106
  function mapDistanceToIndexParam(distance) {
@@ -1,7 +1,6 @@
1
1
  import { TypedValues, withSession } from "../ydb/client.js";
2
2
  import { buildJsonOrEmpty, buildVectorParam } from "../ydb/helpers.js";
3
3
  import { logger } from "../logging/logger.js";
4
- import { APPROX_PRESELECT } from "../config/env.js";
5
4
  import { notifyUpsert } from "../indexing/IndexScheduler.js";
6
5
  export async function upsertPoints(tableName, points, vectorType, dimension) {
7
6
  let upserted = 0;
@@ -30,14 +29,13 @@ export async function upsertPoints(tableName, points, vectorType, dimension) {
30
29
  // Retry on transient schema/metadata mismatches during index rebuild
31
30
  const maxRetries = 6; // ~ up to ~ (0.25 + jitter) * 2^5 ≈ few seconds
32
31
  let attempt = 0;
33
- // eslint-disable-next-line no-constant-condition
34
32
  while (true) {
35
33
  try {
36
34
  await s.executeQuery(ddl, params);
37
35
  break;
38
36
  }
39
37
  catch (e) {
40
- const msg = String(e?.message ?? e);
38
+ const msg = e instanceof Error ? e.message : String(e);
41
39
  const isTransient = /Aborted|schema version mismatch|Table metadata loading|Failed to load metadata/i.test(msg);
42
40
  if (!isTransient || attempt >= maxRetries) {
43
41
  throw e;
@@ -63,7 +61,6 @@ export async function searchPoints(tableName, queryVector, top, withPayload, dis
63
61
  }
64
62
  const { fn, order } = mapDistanceToKnnFn(distance);
65
63
  // Single-phase search over embedding using vector index if present
66
- const preselect = Math.min(APPROX_PRESELECT, Math.max(top * 10, top));
67
64
  const qf = buildVectorParam(queryVector, vectorType);
68
65
  const params = {
69
66
  $qf: qf,
@@ -87,7 +84,7 @@ export async function searchPoints(tableName, queryVector, top, withPayload, dis
87
84
  logger.info({ tableName }, "vector index found; using index for search");
88
85
  }
89
86
  catch (e) {
90
- const msg = String(e?.message ?? e);
87
+ const msg = e instanceof Error ? e.message : String(e);
91
88
  // Fallback to table scan if index not found or not ready
92
89
  if (/not found|does not exist|no such index|no global index|is not ready to use/i.test(msg)) {
93
90
  logger.info({ tableName }, "vector index not available (missing or building); falling back to table scan");
@@ -103,6 +100,9 @@ export async function searchPoints(tableName, queryVector, top, withPayload, dis
103
100
  const rows = (rowset?.rows ?? []);
104
101
  return rows.map((row) => {
105
102
  const id = row.items?.[0]?.textValue;
103
+ if (typeof id !== "string") {
104
+ throw new Error("point_id is missing in YDB search result");
105
+ }
106
106
  let payload;
107
107
  let scoreIdx = 1;
108
108
  if (withPayload) {
@@ -129,7 +129,10 @@ export async function deletePoints(tableName, ids) {
129
129
  DECLARE $id AS Utf8;
130
130
  DELETE FROM ${tableName} WHERE point_id = $id;
131
131
  `;
132
- await s.executeQuery(yql, { $id: TypedValues.utf8(String(id)) });
132
+ const params = {
133
+ $id: TypedValues.utf8(String(id)),
134
+ };
135
+ await s.executeQuery(yql, params);
133
136
  deleted += 1;
134
137
  }
135
138
  });
@@ -16,9 +16,8 @@ collectionsRouter.put("/:collection/index", async (req, res) => {
16
16
  return res.status(err.statusCode).json(err.payload);
17
17
  }
18
18
  logger.error({ err }, "build index failed");
19
- res
20
- .status(500)
21
- .json({ status: "error", error: String(err?.message ?? err) });
19
+ const errorMessage = err instanceof Error ? err.message : String(err);
20
+ res.status(500).json({ status: "error", error: errorMessage });
22
21
  }
23
22
  });
24
23
  collectionsRouter.put("/:collection", async (req, res) => {
@@ -33,9 +32,8 @@ collectionsRouter.put("/:collection", async (req, res) => {
33
32
  return res.status(err.statusCode).json(err.payload);
34
33
  }
35
34
  logger.error({ err }, "create collection failed");
36
- res
37
- .status(500)
38
- .json({ status: "error", error: String(err?.message ?? err) });
35
+ const errorMessage = err instanceof Error ? err.message : String(err);
36
+ res.status(500).json({ status: "error", error: errorMessage });
39
37
  }
40
38
  });
41
39
  collectionsRouter.get("/:collection", async (req, res) => {
@@ -50,9 +48,8 @@ collectionsRouter.get("/:collection", async (req, res) => {
50
48
  return res.status(err.statusCode).json(err.payload);
51
49
  }
52
50
  logger.error({ err }, "get collection failed");
53
- res
54
- .status(500)
55
- .json({ status: "error", error: String(err?.message ?? err) });
51
+ const errorMessage = err instanceof Error ? err.message : String(err);
52
+ res.status(500).json({ status: "error", error: errorMessage });
56
53
  }
57
54
  });
58
55
  collectionsRouter.delete("/:collection", async (req, res) => {
@@ -67,8 +64,7 @@ collectionsRouter.delete("/:collection", async (req, res) => {
67
64
  return res.status(err.statusCode).json(err.payload);
68
65
  }
69
66
  logger.error({ err }, "delete collection failed");
70
- res
71
- .status(500)
72
- .json({ status: "error", error: String(err?.message ?? err) });
67
+ const errorMessage = err instanceof Error ? err.message : String(err);
68
+ res.status(500).json({ status: "error", error: errorMessage });
73
69
  }
74
70
  });
@@ -16,9 +16,8 @@ pointsRouter.put("/:collection/points", async (req, res) => {
16
16
  return res.status(err.statusCode).json(err.payload);
17
17
  }
18
18
  logger.error({ err }, "upsert points (PUT) failed");
19
- res
20
- .status(500)
21
- .json({ status: "error", error: String(err?.message ?? err) });
19
+ const errorMessage = err instanceof Error ? err.message : String(err);
20
+ res.status(500).json({ status: "error", error: errorMessage });
22
21
  }
23
22
  });
24
23
  pointsRouter.post("/:collection/points/upsert", async (req, res) => {
@@ -34,9 +33,8 @@ pointsRouter.post("/:collection/points/upsert", async (req, res) => {
34
33
  return res.status(err.statusCode).json(err.payload);
35
34
  }
36
35
  logger.error({ err }, "upsert points failed");
37
- res
38
- .status(500)
39
- .json({ status: "error", error: String(err?.message ?? err) });
36
+ const errorMessage = err instanceof Error ? err.message : String(err);
37
+ res.status(500).json({ status: "error", error: errorMessage });
40
38
  }
41
39
  });
42
40
  pointsRouter.post("/:collection/points/search", async (req, res) => {
@@ -52,9 +50,8 @@ pointsRouter.post("/:collection/points/search", async (req, res) => {
52
50
  return res.status(err.statusCode).json(err.payload);
53
51
  }
54
52
  logger.error({ err }, "search points failed");
55
- res
56
- .status(500)
57
- .json({ status: "error", error: String(err?.message ?? err) });
53
+ const errorMessage = err instanceof Error ? err.message : String(err);
54
+ res.status(500).json({ status: "error", error: errorMessage });
58
55
  }
59
56
  });
60
57
  // Compatibility: some clients call POST /collections/:collection/points/query
@@ -71,9 +68,8 @@ pointsRouter.post("/:collection/points/query", async (req, res) => {
71
68
  return res.status(err.statusCode).json(err.payload);
72
69
  }
73
70
  logger.error({ err }, "search points (query) failed");
74
- res
75
- .status(500)
76
- .json({ status: "error", error: String(err?.message ?? err) });
71
+ const errorMessage = err instanceof Error ? err.message : String(err);
72
+ res.status(500).json({ status: "error", error: errorMessage });
77
73
  }
78
74
  });
79
75
  pointsRouter.post("/:collection/points/delete", async (req, res) => {
@@ -89,8 +85,7 @@ pointsRouter.post("/:collection/points/delete", async (req, res) => {
89
85
  return res.status(err.statusCode).json(err.payload);
90
86
  }
91
87
  logger.error({ err }, "delete points failed");
92
- res
93
- .status(500)
94
- .json({ status: "error", error: String(err?.message ?? err) });
88
+ const errorMessage = err instanceof Error ? err.message : String(err);
89
+ res.status(500).json({ status: "error", error: errorMessage });
95
90
  }
96
91
  });
@@ -30,8 +30,7 @@ export declare function getCollection(ctx: CollectionContextInput): Promise<{
30
30
  export declare function deleteCollection(ctx: CollectionContextInput): Promise<{
31
31
  acknowledged: boolean;
32
32
  }>;
33
- interface PointsContextInput extends CollectionContextInput {
34
- }
33
+ type PointsContextInput = CollectionContextInput;
35
34
  export declare function upsertPoints(ctx: PointsContextInput, body: unknown): Promise<{
36
35
  upserted: number;
37
36
  }>;
@@ -101,10 +101,10 @@ function extractVectorLoose(body, depth = 0) {
101
101
  return obj.embedding;
102
102
  const query = obj.query;
103
103
  if (query) {
104
- const queryVector = query.vector;
104
+ const queryVector = query["vector"];
105
105
  if (isNumberArray(queryVector))
106
106
  return queryVector;
107
- const nearest = query.nearest;
107
+ const nearest = query["nearest"];
108
108
  if (nearest && isNumberArray(nearest.vector)) {
109
109
  return nearest.vector;
110
110
  }
@@ -131,39 +131,68 @@ function extractVectorLoose(body, depth = 0) {
131
131
  return undefined;
132
132
  }
133
133
  function normalizeSearchBodyForSearch(body) {
134
+ if (!body || typeof body !== "object") {
135
+ return {
136
+ vector: undefined,
137
+ top: undefined,
138
+ withPayload: undefined,
139
+ scoreThreshold: undefined,
140
+ };
141
+ }
134
142
  const b = body;
135
- const vector = Array.isArray(b?.vector) ? b.vector : undefined;
136
- const topFromTop = typeof b?.top === "number" ? b.top : undefined;
137
- const topFromLimit = typeof b?.limit === "number" ? b.limit : undefined;
143
+ const rawVector = b["vector"];
144
+ const vector = isNumberArray(rawVector) ? rawVector : undefined;
145
+ const rawTop = b["top"];
146
+ const rawLimit = b["limit"];
147
+ const topFromTop = typeof rawTop === "number" ? rawTop : undefined;
148
+ const topFromLimit = typeof rawLimit === "number" ? rawLimit : undefined;
138
149
  const top = topFromTop ?? topFromLimit;
139
150
  let withPayload;
140
- const rawWithPayload = b?.with_payload;
151
+ const rawWithPayload = b["with_payload"];
141
152
  if (typeof rawWithPayload === "boolean") {
142
153
  withPayload = rawWithPayload;
143
154
  }
144
- else if (Array.isArray(rawWithPayload) || typeof rawWithPayload === "object") {
155
+ else if (Array.isArray(rawWithPayload) ||
156
+ typeof rawWithPayload === "object") {
145
157
  withPayload = true;
146
158
  }
147
- const thresholdValue = Number(b?.score_threshold);
148
- const scoreThreshold = Number.isFinite(thresholdValue) ? thresholdValue : undefined;
159
+ const thresholdRaw = b["score_threshold"];
160
+ const thresholdValue = typeof thresholdRaw === "number" ? thresholdRaw : Number(thresholdRaw);
161
+ const scoreThreshold = Number.isFinite(thresholdValue)
162
+ ? thresholdValue
163
+ : undefined;
149
164
  return { vector, top, withPayload, scoreThreshold };
150
165
  }
151
166
  function normalizeSearchBodyForQuery(body) {
167
+ if (!body || typeof body !== "object") {
168
+ return {
169
+ vector: undefined,
170
+ top: undefined,
171
+ withPayload: undefined,
172
+ scoreThreshold: undefined,
173
+ };
174
+ }
152
175
  const b = body;
153
176
  const vector = extractVectorLoose(b);
154
- const topFromTop = typeof b?.top === "number" ? b.top : undefined;
155
- const topFromLimit = typeof b?.limit === "number" ? b.limit : undefined;
177
+ const rawTop = b["top"];
178
+ const rawLimit = b["limit"];
179
+ const topFromTop = typeof rawTop === "number" ? rawTop : undefined;
180
+ const topFromLimit = typeof rawLimit === "number" ? rawLimit : undefined;
156
181
  const top = topFromTop ?? topFromLimit;
157
182
  let withPayload;
158
- const rawWithPayload = b?.with_payload;
183
+ const rawWithPayload = b["with_payload"];
159
184
  if (typeof rawWithPayload === "boolean") {
160
185
  withPayload = rawWithPayload;
161
186
  }
162
- else if (Array.isArray(rawWithPayload) || typeof rawWithPayload === "object") {
187
+ else if (Array.isArray(rawWithPayload) ||
188
+ typeof rawWithPayload === "object") {
163
189
  withPayload = true;
164
190
  }
165
- const thresholdValue = Number(b?.score_threshold);
166
- const scoreThreshold = Number.isFinite(thresholdValue) ? thresholdValue : undefined;
191
+ const thresholdRaw = b["score_threshold"];
192
+ const thresholdValue = typeof thresholdRaw === "number" ? thresholdRaw : Number(thresholdRaw);
193
+ const scoreThreshold = Number.isFinite(thresholdValue)
194
+ ? thresholdValue
195
+ : undefined;
167
196
  return { vector, top, withPayload, scoreThreshold };
168
197
  }
169
198
  export async function upsertPoints(ctx, body) {
@@ -193,7 +222,11 @@ async function executeSearch(ctx, normalizedSearch, source) {
193
222
  logger.info({ tenant: normalized.tenant, collection: normalized.collection }, `${source}: resolve collection meta`);
194
223
  const meta = await getCollectionMeta(normalized.metaKey);
195
224
  if (!meta) {
196
- logger.warn({ tenant: normalized.tenant, collection: normalized.collection, metaKey: normalized.metaKey }, `${source}: collection not found`);
225
+ logger.warn({
226
+ tenant: normalized.tenant,
227
+ collection: normalized.collection,
228
+ metaKey: normalized.metaKey,
229
+ }, `${source}: collection not found`);
197
230
  throw new QdrantServiceError(404, {
198
231
  status: "error",
199
232
  error: "collection not found",
@@ -235,7 +268,11 @@ async function executeSearch(ctx, normalizedSearch, source) {
235
268
  }
236
269
  return hit.score <= threshold;
237
270
  });
238
- logger.info({ tenant: normalized.tenant, collection: normalized.collection, hits: hits.length }, `${source}: completed`);
271
+ logger.info({
272
+ tenant: normalized.tenant,
273
+ collection: normalized.collection,
274
+ hits: hits.length,
275
+ }, `${source}: completed`);
239
276
  return { points: filtered };
240
277
  }
241
278
  export async function searchPoints(ctx, body) {
@@ -1,6 +1,12 @@
1
- import type { Session } from "ydb-sdk";
2
- declare const Types: any, TypedValues: any, TableDescription: any, Column: any;
1
+ import type { Session, IAuthService } from "ydb-sdk";
2
+ declare const Types: typeof import("ydb-sdk").Types, TypedValues: typeof import("ydb-sdk").TypedValues, TableDescription: typeof import("ydb-sdk").TableDescription, Column: typeof import("ydb-sdk").Column;
3
3
  export { Types, TypedValues, TableDescription, Column };
4
- export declare const driver: any;
4
+ type DriverConfig = {
5
+ endpoint?: string;
6
+ database?: string;
7
+ connectionString?: string;
8
+ authService?: IAuthService;
9
+ };
10
+ export declare function configureDriver(config: DriverConfig): void;
5
11
  export declare function readyOrThrow(): Promise<void>;
6
12
  export declare function withSession<T>(fn: (s: Session) => Promise<T>): Promise<T>;
@@ -1,20 +1,41 @@
1
1
  import { createRequire } from "module";
2
2
  import { YDB_DATABASE, YDB_ENDPOINT } from "../config/env.js";
3
3
  const require = createRequire(import.meta.url);
4
- // eslint-disable-next-line @typescript-eslint/no-var-requires
5
4
  const { Driver, getCredentialsFromEnv, Types, TypedValues, TableDescription, Column, } = require("ydb-sdk");
6
5
  export { Types, TypedValues, TableDescription, Column };
7
- export const driver = new Driver({
8
- endpoint: YDB_ENDPOINT,
9
- database: YDB_DATABASE,
10
- authService: getCredentialsFromEnv(),
11
- });
6
+ let overrideConfig;
7
+ let driver;
8
+ export function configureDriver(config) {
9
+ if (driver) {
10
+ // Driver already created; keep existing connection settings.
11
+ return;
12
+ }
13
+ overrideConfig = config;
14
+ }
15
+ function getOrCreateDriver() {
16
+ if (driver) {
17
+ return driver;
18
+ }
19
+ const base = overrideConfig?.connectionString != null
20
+ ? { connectionString: overrideConfig.connectionString }
21
+ : {
22
+ endpoint: overrideConfig?.endpoint ?? YDB_ENDPOINT,
23
+ database: overrideConfig?.database ?? YDB_DATABASE,
24
+ };
25
+ driver = new Driver({
26
+ ...base,
27
+ authService: overrideConfig?.authService ?? getCredentialsFromEnv(),
28
+ });
29
+ return driver;
30
+ }
12
31
  export async function readyOrThrow() {
13
- const ok = await driver.ready(10000);
32
+ const d = getOrCreateDriver();
33
+ const ok = await d.ready(10000);
14
34
  if (!ok) {
15
35
  throw new Error("YDB driver is not ready in 10s. Check connectivity and credentials.");
16
36
  }
17
37
  }
18
38
  export async function withSession(fn) {
19
- return await driver.tableClient.withSession(fn, 15000);
39
+ const d = getOrCreateDriver();
40
+ return await d.tableClient.withSession(fn, 15000);
20
41
  }
@@ -1,2 +1,2 @@
1
- export declare function buildVectorParam(vector: number[], vectorType: "float" | "uint8"): any;
2
- export declare function buildJsonOrEmpty(payload?: Record<string, unknown>): any;
1
+ export declare function buildVectorParam(vector: number[], vectorType: "float" | "uint8"): import("ydb-sdk-proto").Ydb.ITypedValue;
2
+ export declare function buildJsonOrEmpty(payload?: Record<string, unknown>): import("ydb-sdk-proto").Ydb.ITypedValue;
package/package.json CHANGED
@@ -1,10 +1,10 @@
1
1
  {
2
2
  "name": "ydb-qdrant",
3
- "version": "2.1.1",
4
- "main": "dist/Api.js",
5
- "types": "dist/Api.d.ts",
3
+ "version": "2.1.3",
4
+ "main": "dist/package/Api.js",
5
+ "types": "dist/package/Api.d.ts",
6
6
  "exports": {
7
- ".": "./dist/Api.js",
7
+ ".": "./dist/package/Api.js",
8
8
  "./server": "./dist/server.js"
9
9
  },
10
10
  "files": [
@@ -16,10 +16,12 @@
16
16
  "scripts": {
17
17
  "test": "vitest run",
18
18
  "build": "tsc -p tsconfig.json",
19
+ "typecheck": "tsc -p tsconfig.json --noEmit",
19
20
  "dev": "tsx watch src/index.ts",
20
21
  "start": "node --experimental-specifier-resolution=node --enable-source-maps dist/index.js",
21
22
  "smoke": "npm run build && node --experimental-specifier-resolution=node --enable-source-maps dist/SmokeTest.js",
22
- "lint": "biome check .",
23
+ "lint": "eslint \"src/**/*.ts\" \"test/**/*.ts\"",
24
+ "prepare": "husky",
23
25
  "prepublishOnly": "npm test && npm run build"
24
26
  },
25
27
  "keywords": [
@@ -62,13 +64,16 @@
62
64
  "zod": "^4.1.12"
63
65
  },
64
66
  "devDependencies": {
65
- "@biomejs/biome": "^2.2.7",
67
+ "@eslint/js": "^9.39.1",
66
68
  "@types/express": "^5.0.3",
67
69
  "@types/node": "^24.9.1",
68
70
  "docsify-cli": "^4.4.4",
71
+ "eslint": "^9.39.1",
72
+ "husky": "^9.1.7",
69
73
  "node-plantuml-latest": "^2.4.0",
70
74
  "tsx": "^4.20.6",
71
75
  "typescript": "^5.9.3",
76
+ "typescript-eslint": "^8.47.0",
72
77
  "vitest": "^4.0.12"
73
78
  }
74
79
  }