UNPKG

@ultipa-graph/ultipa-driver

Version:

NodeJS SDK for Ultipa GQL

1,124 lines 241 kB
"use strict"; /** * Main client for GQLDB Node.js driver. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.GqldbClient = void 0; const grpc = __importStar(require("@grpc/grpc-js")); const config_1 = require("./config"); const response_1 = require("./response"); const session_1 = require("./session"); const transaction_1 = require("./transaction"); const types_1 = require("./types"); const services_1 = require("./services"); const index_1 = require("./services/index"); /** * Map an `InsertType` value to the GQL keyword(s) selecting the * matching server-side semantics. * * - `InsertType.Normal` -> `"INSERT"` (error on duplicate `_id`) * - `InsertType.Overwrite` -> `"INSERT OVERWRITE"` (replace on duplicate) * - `InsertType.Upsert` -> `"UPSERT"` (merge on duplicate) * * `Overwrite` and `Upsert` are different semantics on existing rows; * they are NOT interchangeable. See {@link InsertType} docs. */ function insertKeywordForType(type) { switch (type) { case types_1.InsertType.Overwrite: return 'INSERT OVERWRITE'; case types_1.InsertType.Upsert: return 'UPSERT'; default: return 'INSERT'; } } /** Main client for interacting with GQLDB */ class GqldbClient { config; clients; sessions; txManager; closed = false; // Stored credentials for auto-reconnect storedUsername = ''; storedPassword = ''; storedGraph = ''; // Cached gRPC plumbing so we can rebuild the channel on UNAVAILABLE. grpcHost = ''; grpcCredentials; grpcChannelOptions; ctx; // Service instances sessionService; queryService; graphService; transactionService; dataService; healthService; adminService; bulkImportService; /** Get session metadata for authenticated requests */ getSessionMetadata() { const metadata = new grpc.Metadata(); const session = this.sessions.getSession(); metadata.add('session-id', session?.id?.toString() || '0'); if (this.clientSessionId) { metadata.add('x-ultipa-session-id', this.clientSessionId); } return metadata; } /** * Stable per-client logical session id surfaced under the * transaction-branch model. See TRANSACTIONS_DRIVER_GUIDE.md §2.0–2.1. * Falls back to a UUID v4 hex generated at construction when * `config.sessionId` is not set. */ clientSessionId; constructor(config) { (0, config_1.validateConfig)(config); this.config = config; this.sessions = new session_1.SessionManager(); this.txManager = new transaction_1.TransactionManager(); // Bootstrap stable per-client session id (used as `x-ultipa-session-id` // metadata when §2.1 opt-in is enabled, and surfaced on every Transaction). const cfgSid = config.sessionId; this.clientSessionId = cfgSid && cfgSid.length > 0 ? cfgSid : require('crypto').randomBytes(16).toString('hex'); // Create gRPC credentials let credentials; if (config.tlsOptions) { credentials = grpc.credentials.createSsl(); } else { credentials = grpc.credentials.createInsecure(); } // Channel options const options = { 'grpc.max_receive_message_length': config.maxRecvSize ?? 64 * 1024 * 1024, 'grpc.max_send_message_length': config.maxRecvSize ?? 64 * 1024 * 1024, 'grpc.keepalive_time_ms': 30000, 'grpc.keepalive_timeout_ms': 10000, 'grpc.keepalive_permit_without_calls': 1, }; // Create service clients for the first host const host = config.hosts[0]; this.grpcHost = host; this.grpcCredentials = credentials; this.grpcChannelOptions = options; this.clients = (0, services_1.createServiceClients)(host, credentials, options); // Initialize service context const ctx = new index_1.ServiceContext(this.config, this.sessions, this.txManager, this.clients, this.clientSessionId); this.ctx = ctx; // Initialize service instances this.sessionService = new index_1.SessionService(ctx); this.queryService = new index_1.QueryService(ctx); this.graphService = new index_1.GraphService(ctx); this.transactionService = new index_1.TransactionService(ctx); this.dataService = new index_1.DataService(ctx); this.healthService = new index_1.HealthService(ctx); this.adminService = new index_1.AdminService(ctx); this.bulkImportService = new index_1.BulkImportService(ctx); } /** Close the client and all connections */ async close() { if (this.closed) return; this.closed = true; if (this.sessions.isLoggedIn()) { try { await this.logout(); } catch (e) { // Ignore logout errors during close } } // Close all service clients try { this.clients.sessionService.close?.(); this.clients.queryService.close?.(); this.clients.graphService.close?.(); this.clients.transactionService.close?.(); this.clients.dataService.close?.(); this.clients.healthService.close?.(); this.clients.adminService.close?.(); this.clients.bulkImportService.close?.(); } catch (e) { // Ignore close errors } } // =========================================================================== // Session Service // =========================================================================== /** Authenticate the user and create a session */ async login(username, password) { const session = await this.sessionService.login(username, password, this.config.defaultGraph || ''); // Store credentials for auto-reconnect this.storedUsername = username; this.storedPassword = password; return session; } /** * Rebuild every gRPC client used by this connection so the next call * gets a fresh channel. Called by withAutoReconnect when a call * fails with UNAVAILABLE / connection reset. Does NOT close old * clients synchronously — in-flight calls hold them alive and gRPC * closes them once those calls complete. */ forceReconnectAll() { const fresh = (0, services_1.createServiceClients)(this.grpcHost, this.grpcCredentials, this.grpcChannelOptions); // Mutate in place so existing service instances pick up the new // clients (they hold a reference to this.ctx.clients). Object.assign(this.clients, fresh); Object.assign(this.ctx.clients, fresh); } /** * Execute fn with two recovery behaviours: * - UNAUTHENTICATED — if stored credentials are available, re-login * and retry fn once. * - UNAVAILABLE / connection reset — rebuild every gRPC client so * the NEXT call gets a fresh channel. The current call is NOT * retried; caller decides (write-side calls are often not * idempotent). */ async withAutoReconnect(fn) { try { return await fn(); } catch (e) { const msgLower = e?.message ? String(e.message).toLowerCase() : ''; const isUnauthenticated = (e?.code === grpc.status.UNAUTHENTICATED) || msgLower.includes('unauthenticated') || msgLower.includes('session not found') || msgLower.includes('session expired'); if (isUnauthenticated && this.storedUsername) { // Save current graph context before re-login const savedGraph = this.storedGraph; try { await this.login(this.storedUsername, this.storedPassword); } catch { throw e; // Return original error if re-login fails } // Restore graph context after re-login if (savedGraph && savedGraph !== '__system__') { try { await this.useGraph(savedGraph); } catch { /* best effort */ } } return fn(); // Retry once } const isUnavailable = (e?.code === grpc.status.UNAVAILABLE) || msgLower.includes('connection reset by peer') || msgLower.includes('socket closed') || msgLower.includes('transport is closing'); if (isUnavailable) { try { this.forceReconnectAll(); } catch { /* best effort */ } } throw e; } } /** Terminate the current session */ async logout() { return this.sessionService.logout(); } /** Check the connection and return the latency in nanoseconds */ async ping() { return this.sessionService.ping(); } // =========================================================================== // Query Service // =========================================================================== /** Execute a GQL query and return the result */ async gql(query, config) { return this.withAutoReconnect(() => this.queryService.gql(query, config)); } /** Execute a GQL query and stream the results */ async gqlStream(query, config, callback) { return this.withAutoReconnect(() => this.queryService.gqlStream(query, config, callback)); } /** Return the execution plan for a query */ async explain(query, config) { return this.withAutoReconnect(() => this.queryService.explain(query, config)); } /** Execute a query with profiling and return statistics */ async profile(query, config) { return this.withAutoReconnect(() => this.queryService.profile(query, config)); } // =========================================================================== // Graph Service // =========================================================================== /** * Create a new graph. * * When `edgeId` is provided, the driver issues a follow-up * `ALTER GRAPH <name> SET EDGE_ID ENABLED|DISABLED` via GQL after the * gRPC create call. When `edgeId` is undefined, behavior is unchanged * and no ALTER statement is sent. */ async createGraph(name, graphType = types_1.GraphType.OPEN, description = '', edgeId) { await this.graphService.createGraph(name, graphType, description); if (edgeId !== undefined) { const state = edgeId === types_1.EdgeIdMode.ENABLED ? 'ENABLED' : 'DISABLED'; await this.gql(`ALTER GRAPH ${name} SET EDGE_ID ${state}`); } } /** Delete a graph */ async dropGraph(name, ifExists = false) { return this.graphService.dropGraph(name, ifExists); } /** Set the current graph for the session */ async useGraph(name) { await this.graphService.useGraph(name); this.storedGraph = name; } /** Return all available graphs */ async listGraphs() { return this.graphService.listGraphs(); } /** Return information about a specific graph */ async getGraphInfo(name) { return this.graphService.getGraphInfo(name); } // =========================================================================== // Transaction Service // =========================================================================== /** Start a new transaction */ async beginTransaction(graphName, readOnly = false, timeout = 0) { const tx = await this.transactionService.beginTransaction(graphName, readOnly, timeout); // Surface the per-client logical session id on the returned tx // (transaction-branch ergonomic, see TRANSACTIONS_DRIVER_GUIDE.md). tx.clientSessionId = this.clientSessionId; return tx; } /** Commit a transaction */ async commit(transactionId) { return this.transactionService.commit(transactionId); } /** Rollback a transaction */ async rollback(transactionId) { return this.transactionService.rollback(transactionId); } /** Return active transactions */ async listTransactions() { return this.transactionService.listTransactions(); } /** Execute a function within a transaction */ async withTransaction(graphName, fn, readOnly = false) { return this.transactionService.withTransaction(graphName, fn, readOnly); } // ---- Admin DDL surface (transaction-branch) ------------------------------- /** * Return active transactions via the GQL admin DDL `SHOW TRANSACTIONS`. * Each row mirrors the 5-column server-side schema: * `transactionId / status / readOnly / startTime / sessionId`. * `sessionId` is `''` unless the server has auto-derived one from peer * info or the driver explicitly surfaces `x-ultipa-session-id` metadata. * * Distinct from `listTransactions()` which uses the legacy gRPC. */ async showTransactions() { const resp = await this.gql('SHOW TRANSACTIONS'); const idx = {}; resp.columns.forEach((c, i) => { idx[c] = i; }); const rows = []; for (const row of resp.rows) { const get = (n, fallback = '') => { const i = idx[n]; return i === undefined ? fallback : row.values[i]?.value ?? row.values[i]; }; rows.push({ transactionId: String(get('transaction_id', '') ?? ''), status: String(get('status', '') ?? ''), readOnly: Boolean(get('read_only', false)), startTime: String(get('start_time', '') ?? ''), sessionId: String(get('session_id', '') ?? ''), }); } return rows; } /** * Roll back a single transaction by id via `KILL TRANSACTION '<id>'`. * `transactionId` here is the **string** id surfaced by * `showTransactions()` (e.g. `'tx_a396c531-...'`), distinct from the * `number` id returned by `begin()`. */ async killTransaction(transactionId) { const escaped = String(transactionId).replace(/'/g, "''"); return this.gql(`KILL TRANSACTION '${escaped}'`); } /** * Roll back every active transaction via `RESET TRANSACTIONS`. Admin-only. * Intended as an escape hatch when an orphan tx blocks new BEGINs. */ async resetTransactions() { return this.gql('RESET TRANSACTIONS'); } // =========================================================================== // Data Service // =========================================================================== /** Insert multiple nodes into a graph using gRPC bulk insert */ async insertNodesBatchAuto(graphName, nodes, config) { return this.dataService.insertNodes(graphName, nodes, config); } /** Insert multiple edges into a graph using gRPC bulk insert */ async insertEdgesBatchAuto(graphName, edges, config) { return this.dataService.insertEdges(graphName, edges, config); } /** * Delete nodes by id list. Emits * `MATCH (n) WHERE id(n) IN [...] DETACH DELETE n RETURN n`. * * Empty/null `nodeIds` short-circuits without contacting the server. * * @returns Response with one row per actually-deleted node, column * "n" holding the GqldbNode. Use `response.alias("n").asNodes()`. * With `returnDeleted=false` the response carries only * `rowsAffected`; rows is empty. */ async deleteNodesByIds(nodeIds, config) { const cfg = { returnDeleted: true, allowDeleteAll: false, ...config }; if (!nodeIds || nodeIds.length === 0) { return new response_1.Response([], [], 0, false, [], 0); } let gql = `MATCH (n) WHERE id(n) IN [${formatStringList(nodeIds)}] DETACH DELETE n`; if (cfg.returnDeleted !== false) gql += ' RETURN n'; return this.gql(gql, cfg); } /** * Delete nodes matching labels and/or where clause. Emits * `MATCH (n:L1|L2) WHERE <where> DETACH DELETE n RETURN n`. * * Throws if both labels and where are empty unless * `config.allowDeleteAll = true`. */ async deleteNodesByCondition(labels, where, limit, config) { const cfg = { returnDeleted: true, allowDeleteAll: false, ...config }; const noLabels = !labels || labels.length === 0; const noWhere = !where || !where.trim(); if (noLabels && noWhere && !cfg.allowDeleteAll) { throw new Error('deleteNodesByCondition with no labels and no where would delete every ' + 'node in the graph. If this is intentional, set DeleteConfig.allowDeleteAll = true.'); } let gql = 'MATCH (n'; if (!noLabels) gql += ':' + labels.map(l => `\`${l}\``).join('|'); gql += ')'; if (!noWhere) gql += ' WHERE ' + where; if (limit !== undefined && limit > 0) gql += ` LIMIT ${limit}`; gql += ' DETACH DELETE n'; if (cfg.returnDeleted !== false) gql += ' RETURN n'; return this.gql(gql, cfg); } /** * Delete edges by id list. Emits 5-column GQL with `id(e)` so the * same emit works on graphs with or without EDGE_ID enabled, then * reshapes the response into a single "e" column holding GqldbEdge. */ async deleteEdgesByIds(edgeIds, config) { const cfg = { returnDeleted: true, allowDeleteAll: false, ...config }; if (!edgeIds || edgeIds.length === 0) { return new response_1.Response([], [], 0, false, [], 0); } let gql = `MATCH ()-[e]->() WHERE id(e) IN [${formatStringList(edgeIds)}] DELETE e`; if (cfg.returnDeleted !== false) { gql += ' RETURN id(e), e._from, e._to, labels(e)[0], properties(e)'; } const raw = await this.gql(gql, cfg); return cfg.returnDeleted === false ? raw : reshapeEdgeDelete(raw); } /** * Delete edges matching label and/or where. Emits 5-column GQL, * then reshapes into a single "e" column. * * Throws if both label and where are empty unless * `config.allowDeleteAll = true`. */ async deleteEdgesByCondition(label, where, limit, config) { const cfg = { returnDeleted: true, allowDeleteAll: false, ...config }; const noLabel = !label; const noWhere = !where || !where.trim(); if (noLabel && noWhere && !cfg.allowDeleteAll) { throw new Error('deleteEdgesByCondition with no label and no where would delete every ' + 'edge in the graph. If this is intentional, set DeleteConfig.allowDeleteAll = true.'); } let gql = 'MATCH ()-[e'; if (!noLabel) gql += `:\`${label}\``; gql += ']->()'; if (!noWhere) gql += ' WHERE ' + where; if (limit !== undefined && limit > 0) gql += ` LIMIT ${limit}`; gql += ' DELETE e'; if (cfg.returnDeleted !== false) { gql += ' RETURN id(e), e._from, e._to, labels(e)[0], properties(e)'; } const raw = await this.gql(gql, cfg); return cfg.returnDeleted === false ? raw : reshapeEdgeDelete(raw); } /** * Export graph data in JSON Lines format (streaming). * @param config Export configuration * @param callback Callback for each exported chunk */ async export(config, callback) { return this.dataService.export(config, callback); } /** * Stream nodes from a graph * @deprecated Use export() with ExportConfig instead */ async exportNodes(graphName, labels, limit = 0, callback) { return this.dataService.exportNodes(graphName, labels, limit, callback); } /** * Stream edges from a graph * @deprecated Use export() with ExportConfig instead */ async exportEdges(graphName, labels, limit = 0, callback) { return this.dataService.exportEdges(graphName, labels, limit, callback); } // =========================================================================== // Health Service // =========================================================================== /** Check the health of a service */ async healthCheck(service = '') { return this.healthService.healthCheck(service); } /** * Watch health status changes via server-side streaming. * Returns a HealthWatcher that emits 'status' events with HealthStatus values. * Call stop() to cancel the stream. * * @param service - Optional service name to watch * @returns HealthWatcher - EventEmitter with stop() method * * @example * ```typescript * const watcher = client.watch(); * watcher.on('status', (status: HealthStatus) => { * console.log('Health status:', status); * }); * watcher.on('error', (err) => { * console.error('Watch error:', err); * }); * watcher.on('end', () => { * console.log('Watch stream ended'); * }); * // Later, to stop watching: * watcher.stop(); * ``` */ watch(service = '') { return this.healthService.watch(service); } // =========================================================================== // Admin Service // =========================================================================== /** Pre-allocate parser instances */ async warmupParser(count) { return this.adminService.warmupParser(count); } /** Return cache statistics */ async getCacheStats(cacheType = types_1.CacheType.ALL) { return this.adminService.getCacheStats(cacheType); } /** Clear the specified cache */ async clearCache(cacheType = types_1.CacheType.ALL) { return this.adminService.clearCache(cacheType); } /** Return database statistics */ async getStatistics(graphName = '') { return this.adminService.getStatistics(graphName); } /** Invalidate the RBAC permission cache */ async invalidatePermissionCache(username = '') { return this.adminService.invalidatePermissionCache(username); } /** Return system-level metrics (CPU, memory, disk I/O, storage, network) */ async getSystemMetrics() { return this.adminService.getSystemMetrics(); } /** Trigger manual compaction of the database storage */ async compact() { return this.adminService.compact(); } /** Wait for the computing engine topology to be ready */ async waitForComputeTopology(graphName, timeoutMs = 0) { return this.adminService.waitForComputeTopology(graphName, timeoutMs); } // =========================================================================== // Bulk Import Service // =========================================================================== /** * Start a bulk import session for optimized high-throughput inserts. * @param graphName Target graph name * @param options Optional bulk import configuration */ async startBulkImport(graphName, options) { return this.bulkImportService.startBulkImport(graphName, options); } /** * @deprecated Checkpoint is no longer needed. Use endBulkImport() which performs a final flush. */ async checkpoint(sessionId) { return { success: true, recordCount: 0, lastCheckpointCount: 0, message: 'Checkpoint has been removed; use endBulkImport which performs a final flush', }; } /** * End the bulk import session with a final checkpoint. * @param sessionId Bulk import session ID */ async endBulkImport(sessionId) { return this.bulkImportService.endBulkImport(sessionId); } /** * Cancel the bulk import session without final sync. * @param sessionId Bulk import session ID */ async abortBulkImport(sessionId) { return this.bulkImportService.abortBulkImport(sessionId); } /** * Return the current status of a bulk import session. * @param sessionId Bulk import session ID */ async getBulkImportStatus(sessionId) { return this.bulkImportService.getBulkImportStatus(sessionId); } // =========================================================================== // Convenience Methods — Getters // =========================================================================== /** Get the current session */ getSession() { return this.sessions.getSession(); } /** Check if there is an active session */ isLoggedIn() { return this.sessions.isLoggedIn(); } /** Get the client configuration */ getConfig() { return this.config; } /** Whether this client is connected to a cluster deployment */ get isCluster() { return this.sessions.getSession()?.isCluster || false; } /** Get the cluster ID (empty string if not a cluster) */ get clusterId() { return this.sessions.getSession()?.clusterId || ''; } /** Get the partition count (0 if not a cluster) */ get partitionCount() { return this.sessions.getSession()?.partitionCount || 0; } // =========================================================================== // Internal Helpers — Name Quoting // =========================================================================== /** Wrap a name in backticks for safe use in GQL statements. * Label names and property names should be quoted; graph names, index names, * and fulltext names should NOT be wrapped in backticks. */ static quoteLabel(name) { return '`' + name + '`'; } /** Wrap each label name in backticks and join with ", " */ static quoteLabels(...names) { return names.map(n => '`' + n + '`').join(', '); } /** Join names with ", " without backtick wrapping */ static joinNames(...names) { return names.join(', '); } // =========================================================================== // Convenience API — Graph Operations (6 methods) // =========================================================================== /** Create a new open (schema-less) graph * * v6 GQL: `CREATE GRAPH g` -> OPEN; `CREATE GRAPH g {}` -> CLOSED. The * brace-less form is required to actually get an OPEN graph. */ async createOpenGraph(name, config) { return this.gql(`CREATE GRAPH ${name}`, config); } /** Create a new closed (schema-enforced) graph with node and edge label definitions */ async createClosedGraph(name, config) { return this.gql(`CREATE GRAPH ${name} {}`, config); } /** * Create a graph if it does not already exist. * Returns true if the graph was created, false if it already existed. */ async createGraphIfNotExist(name, graphType = types_1.GraphType.OPEN, description = '', edgeId) { try { await this.createGraph(name, graphType, description, edgeId); return true; } catch (e) { const msg = (e.message || '').toLowerCase(); if (msg.includes('already exist') || msg.includes('duplicate')) { return false; } throw e; } } /** Check whether a graph with the given name exists */ async hasGraph(name) { const graphs = await this.listGraphs(); return graphs.some(g => g.name === name); } /** Rename a graph */ async alterGraph(graphName, newName, config) { return this.gql(`ALTER GRAPH ${graphName} RENAME TO ${newName}`, config); } /** Remove all data from a graph while keeping its structure */ async truncate(graphName, config) { return this.gql(`TRUNCATE GRAPH ${graphName}`, config); } // =========================================================================== // Convenience API — Label Operations (16 methods) // =========================================================================== /** Return all labels (node and edge) in the current graph */ async showLabels(config) { const resp = await this.gql('SHOW LABELS', config); return parseLabelInfoRows(resp); } /** Return all node labels in the current graph */ async showNodeLabels(config) { const resp = await this.gql('SHOW NODE LABELS', config); return parseLabelInfoRows(resp, 'NODE'); } /** Return all edge labels in the current graph */ async showEdgeLabels(config) { const resp = await this.gql('SHOW EDGE LABELS', config); return parseLabelInfoRows(resp, 'EDGE'); } /** Return all node types with their properties */ async showNodeTypes(config) { const resp = await this.gql('SHOW NODE TYPES', config); return parseNodeTypeInfoRows(resp); } /** Return all edge types with their properties */ async showEdgeTypes(config) { const resp = await this.gql('SHOW EDGE TYPES', config); return parseEdgeTypeInfoRows(resp); } /** * Return a specific label by name from the current graph, or null if not found. * Matches entries where `name` is one of the .labels (supports multi-label groups). */ async getLabel(name, config) { const labels = await this.showLabels(config); return labels.find(l => l.labels.includes(name)) || null; } /** Return a specific node type by name, or null if not found */ async getNodeLabel(name, config) { const types = await this.showNodeTypes(config); return types.find(t => t.name === name) || null; } /** Return a specific edge type by name, or null if not found */ async getEdgeLabel(name, config) { const types = await this.showEdgeTypes(config); return types.find(t => t.name === name) || null; } /** Create a node label in the current graph (closed graph) */ async createNodeLabel(name, props = [], config) { const targetGraph = config?.graphName || this.sessions.getDefaultGraph(); const propStr = (0, types_1.buildPropertyDefString)(props); return this.gql(`ALTER GRAPH ${targetGraph} ADD NODE { ${GqldbClient.quoteLabel(name)} (${propStr}) }`, config); } /** Create an edge label in the current graph (closed graph) */ async createEdgeLabel(name, props = [], config) { const targetGraph = config?.graphName || this.sessions.getDefaultGraph(); const propStr = (0, types_1.buildPropertyDefString)(props); return this.gql(`ALTER GRAPH ${targetGraph} ADD EDGE { ${GqldbClient.quoteLabel(name)} ()-[${propStr}]->() }`, config); } /** Drop a node label from the current graph (closed graph) */ async dropNodeLabel(name, config) { const targetGraph = config?.graphName || this.sessions.getDefaultGraph(); return this.gql(`ALTER GRAPH ${targetGraph} DROP NODE ${GqldbClient.quoteLabel(name)}`, config); } /** Drop one or more edge labels from the current graph (closed graph) */ async dropEdgeLabel(...namesOrConfig) { // Last arg may be a QueryConfig (or undefined); treat non-string as config. let config; let names; if (namesOrConfig.length > 0 && typeof namesOrConfig[namesOrConfig.length - 1] !== 'string') { config = namesOrConfig[namesOrConfig.length - 1]; names = namesOrConfig.slice(0, -1); } else { names = namesOrConfig; } const targetGraph = config?.graphName || this.sessions.getDefaultGraph(); return this.gql(`ALTER GRAPH ${targetGraph} DROP EDGE ${GqldbClient.quoteLabels(...names)}`, config); } /** * Create a label if it does not already exist. * Returns true if created, false if it already existed. */ async createLabelIfNotExist(nodeOrEdge, name, props = [], config) { const labels = await this.showLabels(config); const targetType = nodeOrEdge === types_1.DBType.EDGE ? 'EDGE' : 'NODE'; if (labels.some(l => l.labels.includes(name) && l.type === targetType)) { return false; } if (nodeOrEdge === types_1.DBType.NODE) { await this.createNodeLabel(name, props, config); } else { await this.createEdgeLabel(name, props, config); } return true; } /** Rename a node label */ async alterNodeLabel(oldName, newName, config) { return this.gql(`ALTER NODE ${GqldbClient.quoteLabel(oldName)} RENAME TO ${GqldbClient.quoteLabel(newName)}`, config); } /** Rename an edge label */ async alterEdgeLabel(oldName, newName, config) { return this.gql(`ALTER EDGE ${GqldbClient.quoteLabel(oldName)} RENAME TO ${GqldbClient.quoteLabel(newName)}`, config); } /** Return all available algorithms */ async showAlgos(config) { const resp = await this.gql('SHOW ALGOS', config); return parseAlgoInfoRows(resp); } // =========================================================================== // Convenience API — Property Operations (13 methods) // =========================================================================== /** Return properties for a label (node or edge) in the current graph */ async showProperty(nodeOrEdge, labelName, config) { if (nodeOrEdge === types_1.DBType.NODE) { return this.showNodeProperty(labelName, config); } return this.showEdgeProperty(labelName, config); } /** Return properties for a node label */ async showNodeProperty(labelName, config) { const nt = await this.getNodeLabel(labelName, config); if (!nt) throw new Error(`Node label "${labelName}" not found`); return nt.properties; } /** Return properties for an edge label */ async showEdgeProperty(labelName, config) { const et = await this.getEdgeLabel(labelName, config); if (!et) throw new Error(`Edge label "${labelName}" not found`); return et.properties; } /** Return a specific property definition for a label, or null if not found */ async getProperty(nodeOrEdge, labelName, propName, config) { const props = await this.showProperty(nodeOrEdge, labelName, config); return props.find(p => p.name === propName) || null; } /** Return a specific property definition for a node label, or null if not found */ async getNodeProperty(labelName, propName, config) { return this.getProperty(types_1.DBType.NODE, labelName, propName, config); } /** Return a specific property definition for an edge label, or null if not found */ async getEdgeProperty(labelName, propName, config) { return this.getProperty(types_1.DBType.EDGE, labelName, propName, config); } /** Add properties to a label (node or edge) */ async createProperty(nodeOrEdge, labelName, props, config) { if (nodeOrEdge === types_1.DBType.NODE) { return this.createNodeProperty(labelName, props, config); } return this.createEdgeProperty(labelName, props, config); } /** Add properties to a node label */ async createNodeProperty(labelName, props, config) { const propStr = (0, types_1.buildPropertyDefString)(props); return this.gql(`ALTER NODE ${GqldbClient.quoteLabel(labelName)} ADD PROPERTY ${propStr}`, config); } /** Add properties to an edge label */ async createEdgeProperty(labelName, props, config) { const propStr = (0, types_1.buildPropertyDefString)(props); return this.gql(`ALTER EDGE ${GqldbClient.quoteLabel(labelName)} ADD PROPERTY ${propStr}`, config); } /** Drop properties from a label (node or edge) */ async dropProperty(nodeOrEdge, labelName, ...propNamesOrConfig) { const { names, config } = splitTrailingConfig(propNamesOrConfig); if (nodeOrEdge === types_1.DBType.NODE) { return this.dropNodeProperty(labelName, ...names, config); } return this.dropEdgeProperty(labelName, ...names, config); } /** Drop properties from a node label */ async dropNodeProperty(labelName, ...propNamesOrConfig) { const { names, config } = splitTrailingConfig(propNamesOrConfig); return this.gql(`ALTER NODE ${GqldbClient.quoteLabel(labelName)} DROP PROPERTY ${GqldbClient.quoteLabels(...names)}`, config); } /** Drop properties from an edge label */ async dropEdgeProperty(labelName, ...propNamesOrConfig) { const { names, config } = splitTrailingConfig(propNamesOrConfig); return this.gql(`ALTER EDGE ${GqldbClient.quoteLabel(labelName)} DROP PROPERTY ${GqldbClient.quoteLabels(...names)}`, config); } /** * Create properties on a label if they do not already exist. * Returns true if any properties were created, false if all already existed. */ async createPropertyIfNotExist(nodeOrEdge, labelName, props, config) { const existing = await this.showProperty(nodeOrEdge, labelName, config); const existingSet = new Set(existing.map(p => p.name)); const newProps = props.filter(p => !existingSet.has(p.name)); if (newProps.length === 0) return false; await this.createProperty(nodeOrEdge, labelName, newProps, config); return true; } // =========================================================================== // Convenience API — Constraint Operations (4 methods) // =========================================================================== // Server replaced `ALTER ... ADD/DROP CONSTRAINT` with the // `CREATE CONSTRAINT <name> FOR ... REQUIRE ... IS NOT NULL/UNIQUE` // form. Constraint names are derived from {kind}_{node|edge}_{label}_{prop} // so create/drop produce matching identifiers without exposing naming. static constraintName(kind, nodeOrEdge, labelName, propNames) { const ent = nodeOrEdge === types_1.DBType.NODE ? 'node' : 'edge'; const props = propNames.map(p => p.toLowerCase()).join('_'); return `${kind}_${ent}_${labelName.toLowerCase()}_${props}`; } static constraintTarget(nodeOrEdge, labelName) { const ql = GqldbClient.quoteLabel(labelName); return nodeOrEdge === types_1.DBType.NODE ? { pattern: `(n:${ql})`, variable: 'n' } : { pattern: `()-[e:${ql}]->()`, variable: 'e' }; } /** Create a NOT NULL constraint on a property */ async createNotNullConstraint(nodeOrEdge, labelName, propName, config) { const name = GqldbClient.constraintName('nn', nodeOrEdge, labelName, [propName]); const { pattern, variable } = GqldbClient.constraintTarget(nodeOrEdge, labelName); return this.gql(`CREATE CONSTRAINT ${name} FOR ${pattern} REQUIRE ${variable}.${GqldbClient.quoteLabel(propName)} IS NOT NULL`, config); } /** Create a UNIQUE constraint on one or more properties */ async createUniqueConstraint(nodeOrEdge, labelName, ...propNamesOrConfig) { const { names, config } = splitTrailingConfig(propNamesOrConfig); const cname = GqldbClient.constraintName('uq', nodeOrEdge, labelName, names); const { pattern, variable } = GqldbClient.constraintTarget(nodeOrEdge, labelName); const propsList = names.map(n => `${variable}.${GqldbClient.quoteLabel(n)}`).join(', '); const requireExpr = names.length > 1 ? `(${propsList})` : propsList; return this.gql(`CREATE CONSTRAINT ${cname} FOR ${pattern} REQUIRE ${requireExpr} IS UNIQUE`, config); } /** Remove a NOT NULL constraint from a property */ async dropNotNullConstraint(nodeOrEdge, labelName, propName, config) { const name = GqldbClient.constraintName('nn', nodeOrEdge, labelName, [propName]); return this.gql(`DROP CONSTRAINT ${name}`, config); } /** Remove a UNIQUE constraint from one or more properties */ async dropUniqueConstraint(nodeOrEdge, labelName, ...propNamesOrConfig) { const { names, config } = splitTrailingConfig(propNamesOrConfig); const cname = GqldbClient.constraintName('uq', nodeOrEdge, labelName, names); return this.gql(`DROP CONSTRAINT ${cname}`, config); } // =========================================================================== // Convenience API — Index Operations (7 methods) // =========================================================================== /** Return all indexes in the current graph */ async showIndex(config) { const resp = await this.gql('SHOW INDEX', config); return parseIndexInfoRows(resp); } /** Return all node indexes in the current graph */ async showNodeIndex(config) { const resp = await this.gql('SHOW NODE INDEX', config); return parseIndexInfoRows(resp); } /** Return all edge indexes in the current graph */ async showEdgeIndex(config) { const resp = await this.gql('SHOW EDGE INDEX', config); return parseIndexInfoRows(resp); } /** Create an index on a node label */ async createNodeIndex(indexName, labelName, props, config) { const propStr = (0, types_1.buildIndexPropertyString)(props); return this.gql(`CREATE INDEX ${indexName} ON NODE ${GqldbClient.quoteLabel(labelName)} (${propStr})`, config); } /** Create an index on an edge label */ async createEdgeIndex(indexName, labelName, props, config) { const propStr = (0, types_1.buildIndexPropertyString)(props); return this.gql(`CREATE INDEX ${indexName} ON EDGE ${GqldbClient.quoteLabel(labelName)} (${propStr})`, config); } /** Drop a node index by name */ async dropNodeIndex(indexName, config) { return this.gql(`DROP NODE INDEX ${indexName}`, config); } /** Drop an edge index by name */ async dropEdgeIndex(indexName, config) { return this.gql(`DROP EDGE INDEX ${indexName}`, config); } // =========================================================================== // Convenience API — Fulltext Operations (7 methods) // =========================================================================== /** Return all fulltext indexes in the current graph */ async showFulltext(config) { const resp = await this.gql('SHOW FULLTEXT', config); return parseFulltextInfoRows(resp); } /** Return all node fulltext indexes */ async showNodeFulltext(config) { const resp = await this.gql('SHOW NODE FULLTEXT', config); return parseFulltextInfoRows(resp); } /** Return all edge fulltext indexes */ async showEdgeFulltext(config) { const resp = await this.gql('SHOW EDGE FULLTEXT', config); return parseFulltextInfoRows(resp); } /** Create a fulltext index on a node label */ async createNodeFulltext(indexName, labelName, props, config) { return this.gql(`CREATE FULLTEXT ${indexName} ON NODE ${GqldbClient.quoteLabel(labelName)} (${GqldbClient.quoteLabels(...props)})`, config); } /** Create a fulltext index on an edge label */ async createEdgeFulltext(indexName, labelName, props, config) { return this.gql(`CREATE FULLTEXT ${indexName} ON EDGE ${GqldbClient.quoteLabel(labelName)} (${GqldbClient.quoteLabels(...props)})`, config); } /** Drop a node fulltext index by name */ async dropNodeFulltext(indexName, config) { return this.gql(`DROP NODE FULLTEXT ${indexName}`, config); } /** Drop an edge fulltext index by name */ async dropEdgeFulltext(indexName, config) { return this.gql(`DROP EDGE FULLTEXT ${indexName}`, config); } // =========================================================================== // Convenience API — Task Operations (3 methods) // =========================================================================== /** Return all tasks in the current graph */ async showTasks(config) { const resp = await this.gql('SHOW TASKS', config); return parseTaskInfoRows(resp); } /** Delete a task by ID */ async deleteTask(taskId, config) { return this.gql(`DELETE TASK ${taskId}`, config); } /** Stop a running task by ID */ async stopTask(taskId, config) { return this.gql(`STOP TASK ${taskId}`, config); } async insertNodes(arg1, arg2, arg3) { // Path 1: legacy bulk-gRPC signature (graphName, nodes, config?) if (typeof arg1 === 'string') { return this.dataService.insertNodes(arg1, arg2, arg3); } // Path 2: convenience GQL-emitter signature (nodes, config?) const nodes = arg1; const config = arg2; if (nodes.length === 0) return new response_1.Response([], [], 0, false, [], 0); nodes.forEach((node, i) => { if (!node.labels || node.labels.length === 0) { throw new Error(`Node at index ${i} has no labels. At least one label is required.`); } }); const varNames = []; const parts = nodes.map((node, i) => { const varName = `n${i}`; varNames.push(varName); // GQL INSERT only supports single label per node const label = GqldbClient.quoteLabel(node.labels[0]); // Build properties including _id if set let props = node.properties; if (node.id) { props = { _id: node.id, ...props }; } const propStr = (0, types_1.buildPropertiesValueString)(props); return propStr ? `(${varName}:${label} ${propStr})` : `(${varName}:${label})`; }); const insertKeyword = insertKeywordForType(config?.insertType); const gqlStr = insertKeyword + ' ' + parts.join(', ') + ' RETURN ' + varNames.join(', '); return this.gql(gqlStr, config); } async insertEdges(arg1, arg2, arg3) { // Path 1: legacy bulk-gRPC signature if (typeof arg1 === 'string') { return this.dataService.insertEdges(arg1, arg2, arg3); } // Path 2: convenience GQL-emitter signature const edges = arg1; const config = arg2; if (edges.length === 0) return new response_1.Response([], [], 0, false, [], 0); const insertKeyword = insertKeywordForType(config?.insertType); const allColumns = []; const allValues = []; let totalAffected = 0; for (let i = 0; i < edges.length; i++) { const edge = edges[i]; const varName = `e${i}`; const labelPart = edge.label ? ':' + GqldbClient.quoteLabel(edge.label) : ''; // Build properties including _id if set — mirrors the node // emitter. The server's GQL parser accepts // `[e0:Knows {_id:'...', ...}]` and stores the edge id verbatim // when EDGE_ID is enabled on the graph. let props = edge.properties; if (edge.id && edge.id.length > 0) { props = { _id: edge.id, ...(edge.properties ?? {}) }; } const propStr = (0, types_1.buildPropertiesValueString)(props); const edgePart = propStr ? `[${varName}${labelPart} ${propStr}]` : `[${varName}${labelPart}]`; const gqlStr = `MATCH (src WHERE id(src) = '${edge.fromNodeId}'), ` + `(dst WHERE id(dst) = '${edge.toNodeId}') ` + `${insertKeyword} (src)-${edgePart}->(dst) RETURN ${varName}`; const resp = await this.gql(gqlStr, config); totalAffected += resp.rowsAffected; allColumns.push(varName); if (resp.rows.length > 0 && resp.rows[0].values.length > 0) { allValues.push(resp.rows[0].values[0]); } } const mergedRow = new response_1.Row(allValues); return new response_1.Response(allColumns, [mergedRow], 1, false, [], totalAffected); } // ==========================================================================