@ultipa-graph/ultipa-driver
Version:
NodeJS SDK for Ultipa GQL
1,124 lines • 241 kB
JavaScript
"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);
}
// ==========================================================================