falkordb 6.8.0 → 6.9.1
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/dist/index.d.ts +5 -4
- package/dist/index.js +9 -5
- package/dist/src/clients/client.d.ts +5 -13
- package/dist/src/clients/cluster.d.ts +7 -12
- package/dist/src/clients/cluster.js +38 -5
- package/dist/src/clients/nullClient.d.ts +3 -11
- package/dist/src/clients/sentinel.d.ts +31 -0
- package/dist/src/clients/sentinel.js +179 -24
- package/dist/src/clients/single.d.ts +16 -12
- package/dist/src/clients/single.js +29 -21
- package/dist/src/commands/CONFIG_GET.d.ts +1 -5
- package/dist/src/commands/CONSTRAINT_CREATE.d.ts +2 -8
- package/dist/src/commands/CONSTRAINT_CREATE.js +3 -10
- package/dist/src/commands/CONSTRAINT_DROP.d.ts +1 -1
- package/dist/src/commands/MEMORY_USAGE.d.ts +2 -4
- package/dist/src/commands/QUERY.d.ts +4 -23
- package/dist/src/commands/QUERY.js +14 -4
- package/dist/src/commands/RO_QUERY.d.ts +2 -2
- package/dist/src/commands/RO_QUERY.js +4 -4
- package/dist/src/commands/SLOWLOG.d.ts +1 -13
- package/dist/src/commands/UDF_LIST.d.ts +2 -2
- package/dist/src/commands/index.d.ts +4 -11
- package/dist/src/commands/index.js +58 -22
- package/dist/src/enums/constraint-type.d.ts +4 -0
- package/dist/src/enums/constraint-type.js +8 -0
- package/dist/src/enums/entity-type.d.ts +4 -0
- package/dist/src/enums/entity-type.js +8 -0
- package/dist/src/enums/graph-value-types.d.ts +19 -0
- package/dist/src/enums/graph-value-types.js +24 -0
- package/dist/src/enums/index.d.ts +3 -0
- package/dist/src/enums/index.js +19 -0
- package/dist/src/errors.d.ts +10 -0
- package/dist/src/errors.js +18 -0
- package/dist/src/falkordb.d.ts +14 -88
- package/dist/src/falkordb.js +133 -8
- package/dist/src/graph.d.ts +28 -15
- package/dist/src/graph.js +92 -53
- package/dist/src/types/cluster-graph-connection.d.ts +5 -0
- package/dist/src/types/cluster-graph-connection.js +2 -0
- package/dist/src/types/config-item.d.ts +4 -0
- package/dist/src/types/config-item.js +2 -0
- package/dist/src/types/falkordb-options.d.ts +65 -0
- package/dist/src/types/falkordb-options.js +2 -0
- package/dist/src/types/graph-metadata.d.ts +5 -0
- package/dist/src/types/graph-metadata.js +2 -0
- package/dist/src/types/graph-raw-value.d.ts +29 -0
- package/dist/src/types/graph-raw-value.js +2 -0
- package/dist/src/types/graph-value.d.ts +29 -0
- package/dist/src/types/graph-value.js +2 -0
- package/dist/src/types/index.d.ts +16 -0
- package/dist/src/types/index.js +32 -0
- package/dist/src/types/memory-usage.d.ts +4 -0
- package/dist/src/types/memory-usage.js +2 -0
- package/dist/src/types/net-socket-options.d.ts +4 -0
- package/dist/src/types/net-socket-options.js +2 -0
- package/dist/src/types/query-options.d.ts +9 -0
- package/dist/src/types/query-options.js +2 -0
- package/dist/src/types/query-reply.d.ts +20 -0
- package/dist/src/types/query-reply.js +2 -0
- package/dist/src/types/single-graph-connection.d.ts +5 -0
- package/dist/src/types/single-graph-connection.js +2 -0
- package/dist/src/types/slow-log.d.ts +12 -0
- package/dist/src/types/slow-log.js +2 -0
- package/dist/src/types/socket-options.d.ts +40 -0
- package/dist/src/types/socket-options.js +2 -0
- package/dist/src/types/tls-socket-options.d.ts +4 -0
- package/dist/src/types/tls-socket-options.js +2 -0
- package/dist/src/types/typed-redis-client-options.d.ts +9 -0
- package/dist/src/types/typed-redis-client-options.js +2 -0
- package/dist/src/types/udf-list.d.ts +2 -0
- package/dist/src/types/udf-list.js +2 -0
- package/package.json +7 -6
package/dist/src/falkordb.js
CHANGED
|
@@ -1,27 +1,71 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
2
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.FalkorDB = void 0;
|
|
3
7
|
const events_1 = require("events");
|
|
4
8
|
const redis_1 = require("redis");
|
|
5
|
-
const graph_1 = require("./graph");
|
|
6
|
-
const commands_1 = require("./commands");
|
|
9
|
+
const graph_1 = __importDefault(require("./graph"));
|
|
10
|
+
const commands_1 = __importDefault(require("./commands"));
|
|
11
|
+
const client_1 = require("@redis/client");
|
|
7
12
|
const single_1 = require("./clients/single");
|
|
8
13
|
const sentinel_1 = require("./clients/sentinel");
|
|
9
14
|
const cluster_1 = require("./clients/cluster");
|
|
10
15
|
const nullClient_1 = require("./clients/nullClient");
|
|
11
|
-
|
|
16
|
+
/**
|
|
17
|
+
* Retries the initial connection gets before `FalkorDB.connect()` rejects.
|
|
18
|
+
*/
|
|
19
|
+
const INITIAL_CONNECT_RETRIES = 3;
|
|
20
|
+
/**
|
|
21
|
+
* node-redis' default back-off: exponential up to 2s plus jitter, and no
|
|
22
|
+
* reconnect after a socket timeout.
|
|
23
|
+
*/
|
|
24
|
+
function defaultReconnectStrategy(retries, cause) {
|
|
25
|
+
if (cause instanceof client_1.SocketTimeoutError) {
|
|
26
|
+
return false;
|
|
27
|
+
}
|
|
28
|
+
return Math.min(Math.pow(2, retries) * 50, 2000) + Math.floor(Math.random() * 200);
|
|
29
|
+
}
|
|
30
|
+
function initialReconnectStrategy(retries, cause, tlsEnabled) {
|
|
31
|
+
if (retries < INITIAL_CONNECT_RETRIES) {
|
|
32
|
+
return defaultReconnectStrategy(retries, cause);
|
|
33
|
+
}
|
|
34
|
+
// A TLS server drops a plain-text client mid-handshake, which node-redis
|
|
35
|
+
// reports as the socket closing unexpectedly.
|
|
36
|
+
const hint = !tlsEnabled && cause instanceof client_1.SocketClosedUnexpectedlyError
|
|
37
|
+
? ' The server closed the connection during the handshake; if it requires TLS, set `socket.tls: true` or use a `rediss://` URL.'
|
|
38
|
+
: '';
|
|
39
|
+
return new Error(`Failed to connect to FalkorDB: ${cause.message}.${hint}`, { cause });
|
|
40
|
+
}
|
|
41
|
+
async function clientFactory(client, sentinelOptions) {
|
|
12
42
|
const info = await client.info("server");
|
|
13
43
|
if (info.includes("redis_mode:sentinel")) {
|
|
14
|
-
return new sentinel_1.Sentinel(client);
|
|
44
|
+
return new sentinel_1.Sentinel(client, sentinelOptions);
|
|
15
45
|
}
|
|
16
46
|
else if (info.includes("redis_mode:cluster")) {
|
|
17
47
|
return new cluster_1.Cluster(client);
|
|
18
48
|
}
|
|
19
49
|
return new single_1.Single(client);
|
|
20
50
|
}
|
|
51
|
+
// Upper bound on the `Graph` instances `selectGraph` keeps. Past it the least
|
|
52
|
+
// recently selected one is dropped; that only costs its metadata cache, which
|
|
53
|
+
// the next `selectGraph` for that id rebuilds.
|
|
54
|
+
const MAX_CACHED_GRAPHS = 1000;
|
|
21
55
|
class FalkorDB extends events_1.EventEmitter {
|
|
22
56
|
#client = new nullClient_1.NullClient();
|
|
57
|
+
#graphs = new Map();
|
|
58
|
+
// Shared by every instance for a graph id, including ones evicted from
|
|
59
|
+
// #graphs that callers still hold. Weakly held, so an entry lives only as
|
|
60
|
+
// long as some instance using it does.
|
|
61
|
+
#schemas = new Map();
|
|
62
|
+
#schemasCleanup = new FinalizationRegistry((graphId) => {
|
|
63
|
+
if (!this.#schemas.get(graphId)?.deref()) {
|
|
64
|
+
this.#schemas.delete(graphId);
|
|
65
|
+
}
|
|
66
|
+
});
|
|
23
67
|
static async connect(options) {
|
|
24
|
-
const { host, port, ...rest } = options ?? {};
|
|
68
|
+
const { host, port, retryWritesOnConnectionLoss, ...rest } = options ?? {};
|
|
25
69
|
const redisOption = rest;
|
|
26
70
|
// Support the top-level `host`/`port` shorthand used by the other FalkorDB
|
|
27
71
|
// clients by folding them into the socket options node-redis expects.
|
|
@@ -41,22 +85,100 @@ class FalkorDB extends events_1.EventEmitter {
|
|
|
41
85
|
redisOption.modules = {
|
|
42
86
|
falkordb: commands_1.default
|
|
43
87
|
};
|
|
88
|
+
// node-redis v6 defaults to RESP3, which auto-converts module map replies
|
|
89
|
+
// into nested objects: `GRAPH.MEMORY USAGE` comes back as
|
|
90
|
+
// `{ total_graph_sz_mb: 0, ... }` instead of the flat
|
|
91
|
+
// `['total_graph_sz_mb', 0, ...]` that the reply parsers - and the
|
|
92
|
+
// `MemoryUsageReply` array type - are written against. So RESP 2 is a
|
|
93
|
+
// requirement here, not a default: honouring an explicit RESP 3 would
|
|
94
|
+
// hand the caller a reply shape the published types say is impossible.
|
|
95
|
+
// The sentinel and cluster clients reuse this options object, so they
|
|
96
|
+
// inherit the pin too.
|
|
97
|
+
const requestedRESP = redisOption.RESP;
|
|
98
|
+
if (requestedRESP !== undefined && requestedRESP !== 2) {
|
|
99
|
+
throw new Error(`FalkorDB requires RESP 2, but RESP ${requestedRESP} was requested. ` +
|
|
100
|
+
'Under RESP 3 node-redis rewrites module map replies into objects, which does ' +
|
|
101
|
+
'not match the reply shapes this client parses and publishes types for.');
|
|
102
|
+
}
|
|
103
|
+
redisOption.RESP = 2;
|
|
104
|
+
// node-redis retries the first connection forever by default, so a server
|
|
105
|
+
// that keeps closing the socket - e.g. a TLS server reached without TLS -
|
|
106
|
+
// leaves `connect()` pending indefinitely. Unless the caller chose a
|
|
107
|
+
// strategy, give up after a few attempts while connecting and keep the
|
|
108
|
+
// usual unbounded back-off once connected. The sentinel and cluster
|
|
109
|
+
// clients reuse these socket options, so `connected` is only set once
|
|
110
|
+
// they are up too: a node discovered during `init()` that cannot be
|
|
111
|
+
// reached must fail `connect()` as well.
|
|
112
|
+
let connected = false;
|
|
113
|
+
if (redisOption.socket?.reconnectStrategy === undefined) {
|
|
114
|
+
const tlsEnabled = redisOption.socket?.tls === true || !!redisOption.url?.startsWith('rediss:');
|
|
115
|
+
redisOption.socket = {
|
|
116
|
+
...redisOption.socket,
|
|
117
|
+
reconnectStrategy: (retries, cause) => connected
|
|
118
|
+
? defaultReconnectStrategy(retries, cause)
|
|
119
|
+
: initialReconnectStrategy(retries, cause, tlsEnabled),
|
|
120
|
+
};
|
|
121
|
+
}
|
|
44
122
|
// Create an empty FalkorDB instance for the redisClient on error event to work
|
|
45
123
|
const falkordb = new FalkorDB();
|
|
46
124
|
// Create initial redis single client
|
|
47
125
|
const redisClient = (0, redis_1.createClient)(redisOption);
|
|
48
126
|
await redisClient
|
|
49
|
-
.
|
|
127
|
+
// Forward errors. Nobody can listen on `falkordb` before it is returned,
|
|
128
|
+
// so emitting while connecting would only throw an unhandled 'error';
|
|
129
|
+
// those failures reach the caller as the rejection of `connect()`.
|
|
130
|
+
.on('error', err => { if (connected)
|
|
131
|
+
falkordb.emit('error', err); })
|
|
50
132
|
.connect();
|
|
51
133
|
// Create FalkorDB client wrapper
|
|
52
|
-
const client = await clientFactory(redisClient);
|
|
134
|
+
const client = await clientFactory(redisClient, { retryWritesOnConnectionLoss });
|
|
53
135
|
await client.init(falkordb);
|
|
136
|
+
connected = true;
|
|
54
137
|
// Set the client to the FalkorDB instance after it was initialized
|
|
55
138
|
falkordb.#client = client;
|
|
56
139
|
return falkordb;
|
|
57
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Returns the `Graph` for `graphId`. Repeated calls with the same id return
|
|
143
|
+
* the same instance, so its cached metadata (labels, relationship types and
|
|
144
|
+
* property keys) is shared instead of being refetched by every new handle.
|
|
145
|
+
* The instance is released when the graph is deleted through it, or when
|
|
146
|
+
* more than `MAX_CACHED_GRAPHS` graphs have been selected since its last use.
|
|
147
|
+
* Each query carries the schema version the cached metadata matches, so a
|
|
148
|
+
* schema changed by another client (e.g. the graph deleted and recreated) is
|
|
149
|
+
* rejected by the server and the metadata refreshed before the query is retried.
|
|
150
|
+
*/
|
|
58
151
|
selectGraph(graphId) {
|
|
59
|
-
|
|
152
|
+
let graph = this.#graphs.get(graphId);
|
|
153
|
+
if (graph) {
|
|
154
|
+
// Re-insert so the Map's insertion order tracks recency of use
|
|
155
|
+
this.#graphs.delete(graphId);
|
|
156
|
+
}
|
|
157
|
+
else {
|
|
158
|
+
graph = new graph_1.default(this.#client, graphId, {
|
|
159
|
+
schema: this.#schema(graphId),
|
|
160
|
+
onDelete: (deleted) => {
|
|
161
|
+
// An instance already evicted must not remove its replacement
|
|
162
|
+
if (this.#graphs.get(graphId) === deleted) {
|
|
163
|
+
this.#graphs.delete(graphId);
|
|
164
|
+
}
|
|
165
|
+
},
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
this.#graphs.set(graphId, graph);
|
|
169
|
+
if (this.#graphs.size > MAX_CACHED_GRAPHS) {
|
|
170
|
+
this.#graphs.delete(this.#graphs.keys().next().value);
|
|
171
|
+
}
|
|
172
|
+
return graph;
|
|
173
|
+
}
|
|
174
|
+
#schema(graphId) {
|
|
175
|
+
let schema = this.#schemas.get(graphId)?.deref();
|
|
176
|
+
if (!schema) {
|
|
177
|
+
schema = { generation: 0, version: 0 };
|
|
178
|
+
this.#schemas.set(graphId, new WeakRef(schema));
|
|
179
|
+
this.#schemasCleanup.register(schema, graphId);
|
|
180
|
+
}
|
|
181
|
+
return schema;
|
|
60
182
|
}
|
|
61
183
|
get connection() {
|
|
62
184
|
return this.#client.getConnection();
|
|
@@ -100,7 +222,10 @@ class FalkorDB extends events_1.EventEmitter {
|
|
|
100
222
|
* Closes the client.
|
|
101
223
|
*/
|
|
102
224
|
async close() {
|
|
225
|
+
this.#graphs.clear();
|
|
226
|
+
this.#schemas.clear();
|
|
103
227
|
return this.#client.disconnect();
|
|
104
228
|
}
|
|
105
229
|
}
|
|
106
230
|
exports.default = FalkorDB;
|
|
231
|
+
exports.FalkorDB = FalkorDB;
|
package/dist/src/graph.d.ts
CHANGED
|
@@ -1,28 +1,40 @@
|
|
|
1
1
|
import { RedisArgument } from "@redis/client";
|
|
2
|
-
import { QueryOptions } from "./commands";
|
|
3
|
-
import { QueryReply } from "./commands/QUERY";
|
|
4
|
-
import { ConstraintType, EntityType } from "./commands/CONSTRAINT_CREATE";
|
|
5
2
|
import { Client } from "./clients/client";
|
|
6
|
-
import {
|
|
3
|
+
import { ConstraintType, EntityType } from "./enums";
|
|
4
|
+
import type { GraphReply, MemoryUsageOptions, QueryOptions, QueryReply } from "./types";
|
|
7
5
|
export { ConstraintType, EntityType };
|
|
8
|
-
export type GraphReply
|
|
9
|
-
|
|
10
|
-
|
|
6
|
+
export type { GraphReply };
|
|
7
|
+
/**
|
|
8
|
+
* What the client knows about a graph's schema. Every `Graph` instance for the
|
|
9
|
+
* same graph shares one, so a change seen through any of them invalidates the
|
|
10
|
+
* metadata cached by all of them.
|
|
11
|
+
*/
|
|
12
|
+
export interface GraphSchemaState {
|
|
13
|
+
generation: number;
|
|
14
|
+
version: number;
|
|
15
|
+
}
|
|
16
|
+
export interface GraphCacheOptions {
|
|
17
|
+
schema?: GraphSchemaState;
|
|
18
|
+
onDelete?: (graph: Graph) => void;
|
|
19
|
+
}
|
|
11
20
|
export default class Graph {
|
|
12
21
|
#private;
|
|
13
|
-
constructor(client: Client, name: string);
|
|
22
|
+
constructor(client: Client, name: string, cache?: GraphCacheOptions);
|
|
14
23
|
query<T>(query: RedisArgument, options?: QueryOptions): Promise<GraphReply<T>>;
|
|
15
24
|
roQuery<T>(query: RedisArgument, options?: QueryOptions): Promise<GraphReply<T>>;
|
|
16
25
|
delete(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Drops the cached labels, relationship types and property keys, here and in
|
|
28
|
+
* every other instance `selectGraph` returned for this graph, so they are
|
|
29
|
+
* refetched the next time a reply needs them. A fetch already in flight does
|
|
30
|
+
* not repopulate the cache. Schema changes made by other clients are detected
|
|
31
|
+
* on the next query, so this is rarely needed.
|
|
32
|
+
*/
|
|
33
|
+
clearMetadataCache(): void;
|
|
17
34
|
explain(query: string): Promise<any>;
|
|
18
35
|
profile(query: string): Promise<any>;
|
|
19
|
-
memoryUsage(options?: MemoryUsageOptions): Promise<import("./
|
|
20
|
-
slowLog(): Promise<
|
|
21
|
-
timestamp: Date;
|
|
22
|
-
command: string;
|
|
23
|
-
query: string;
|
|
24
|
-
took: number;
|
|
25
|
-
}[]>;
|
|
36
|
+
memoryUsage(options?: MemoryUsageOptions): Promise<import("./types").MemoryUsageReply>;
|
|
37
|
+
slowLog(): Promise<import("./types").SlowLogReply>;
|
|
26
38
|
constraintCreate(constraintType: ConstraintType, entityType: EntityType, label: string, ...properties: string[]): Promise<void>;
|
|
27
39
|
constraintDrop(constraintType: ConstraintType, entityType: EntityType, label: string, ...properties: string[]): Promise<void>;
|
|
28
40
|
copy(destGraph: string): Promise<any>;
|
|
@@ -41,3 +53,4 @@ export default class Graph {
|
|
|
41
53
|
dropEdgeFulltextIndex(label: string, attribute: string): Promise<QueryReply>;
|
|
42
54
|
dropEdgeVectorIndex(label: string, attribute: string): Promise<QueryReply>;
|
|
43
55
|
}
|
|
56
|
+
export { Graph };
|
package/dist/src/graph.js
CHANGED
|
@@ -1,50 +1,68 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.EntityType = exports.ConstraintType = void 0;
|
|
4
|
-
const CONSTRAINT_CREATE_1 = require("./commands/CONSTRAINT_CREATE");
|
|
5
|
-
Object.defineProperty(exports, "ConstraintType", { enumerable: true, get: function () { return CONSTRAINT_CREATE_1.ConstraintType; } });
|
|
6
|
-
Object.defineProperty(exports, "EntityType", { enumerable: true, get: function () { return CONSTRAINT_CREATE_1.EntityType; } });
|
|
3
|
+
exports.Graph = exports.EntityType = exports.ConstraintType = void 0;
|
|
7
4
|
const polyfill_1 = require("@js-temporal/polyfill");
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
(function (
|
|
11
|
-
|
|
12
|
-
GraphValueTypes[GraphValueTypes["NULL"] = 1] = "NULL";
|
|
13
|
-
GraphValueTypes[GraphValueTypes["STRING"] = 2] = "STRING";
|
|
14
|
-
GraphValueTypes[GraphValueTypes["INTEGER"] = 3] = "INTEGER";
|
|
15
|
-
GraphValueTypes[GraphValueTypes["BOOLEAN"] = 4] = "BOOLEAN";
|
|
16
|
-
GraphValueTypes[GraphValueTypes["DOUBLE"] = 5] = "DOUBLE";
|
|
17
|
-
GraphValueTypes[GraphValueTypes["ARRAY"] = 6] = "ARRAY";
|
|
18
|
-
GraphValueTypes[GraphValueTypes["EDGE"] = 7] = "EDGE";
|
|
19
|
-
GraphValueTypes[GraphValueTypes["NODE"] = 8] = "NODE";
|
|
20
|
-
GraphValueTypes[GraphValueTypes["PATH"] = 9] = "PATH";
|
|
21
|
-
GraphValueTypes[GraphValueTypes["MAP"] = 10] = "MAP";
|
|
22
|
-
GraphValueTypes[GraphValueTypes["POINT"] = 11] = "POINT";
|
|
23
|
-
GraphValueTypes[GraphValueTypes["VECTORF32"] = 12] = "VECTORF32";
|
|
24
|
-
GraphValueTypes[GraphValueTypes["DATETIME"] = 13] = "DATETIME";
|
|
25
|
-
GraphValueTypes[GraphValueTypes["DATE"] = 14] = "DATE";
|
|
26
|
-
GraphValueTypes[GraphValueTypes["TIME"] = 15] = "TIME";
|
|
27
|
-
GraphValueTypes[GraphValueTypes["DURATION"] = 16] = "DURATION";
|
|
28
|
-
})(GraphValueTypes || (GraphValueTypes = {}));
|
|
29
|
-
// export type GraphConnection = SingleGraphConnection | ClusterGraphConnection;
|
|
5
|
+
const enums_1 = require("./enums");
|
|
6
|
+
Object.defineProperty(exports, "ConstraintType", { enumerable: true, get: function () { return enums_1.ConstraintType; } });
|
|
7
|
+
Object.defineProperty(exports, "EntityType", { enumerable: true, get: function () { return enums_1.EntityType; } });
|
|
8
|
+
const errors_1 = require("./errors");
|
|
30
9
|
class Graph {
|
|
31
10
|
#client;
|
|
32
11
|
#name;
|
|
33
12
|
#metadata;
|
|
34
|
-
|
|
13
|
+
#schema;
|
|
14
|
+
#onDelete;
|
|
15
|
+
constructor(client, name, cache) {
|
|
35
16
|
this.#client = client;
|
|
36
17
|
this.#name = name;
|
|
18
|
+
this.#schema = cache?.schema ?? { generation: 0, version: 0 };
|
|
19
|
+
this.#onDelete = cache?.onDelete;
|
|
37
20
|
}
|
|
38
21
|
async query(query, options) {
|
|
39
|
-
const reply = await this.#client.query(this.#name, query, options);
|
|
22
|
+
const reply = await this.#withSchemaVersion((version) => this.#client.query(this.#name, query, options, true, version));
|
|
40
23
|
return this.#parseReply(reply);
|
|
41
24
|
}
|
|
42
25
|
async roQuery(query, options) {
|
|
43
|
-
const reply = await this.#client.roQuery(this.#name, query, options);
|
|
26
|
+
const reply = await this.#withSchemaVersion((version) => this.#client.roQuery(this.#name, query, options, true, version));
|
|
44
27
|
return this.#parseReply(reply);
|
|
45
28
|
}
|
|
29
|
+
async #withSchemaVersion(run) {
|
|
30
|
+
try {
|
|
31
|
+
return await run(this.#schema.version);
|
|
32
|
+
}
|
|
33
|
+
catch (err) {
|
|
34
|
+
if (!(err instanceof errors_1.SchemaVersionMismatchError))
|
|
35
|
+
throw err;
|
|
36
|
+
// The server rejected the query without running it, so retrying is
|
|
37
|
+
// safe. Metadata fetched from here on is at least as new as
|
|
38
|
+
// `err.version`, so pairing the two is sound. The retry is sent without
|
|
39
|
+
// a version so that further concurrent schema changes cannot make it
|
|
40
|
+
// loop; names they add get new ids and are fetched when needed.
|
|
41
|
+
this.clearMetadataCache();
|
|
42
|
+
this.#schema.version = err.version;
|
|
43
|
+
return run();
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
46
|
async delete() {
|
|
47
|
-
|
|
47
|
+
const reply = await this.#client.delete(this.#name);
|
|
48
|
+
// The instance is reused by `selectGraph`, and a graph recreated under the
|
|
49
|
+
// same name assigns new ids, so the cached metadata must not outlive it.
|
|
50
|
+
this.clearMetadataCache();
|
|
51
|
+
// A recreated graph starts from an empty schema, whose version is 0
|
|
52
|
+
this.#schema.version = 0;
|
|
53
|
+
this.#onDelete?.(this);
|
|
54
|
+
return reply;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* Drops the cached labels, relationship types and property keys, here and in
|
|
58
|
+
* every other instance `selectGraph` returned for this graph, so they are
|
|
59
|
+
* refetched the next time a reply needs them. A fetch already in flight does
|
|
60
|
+
* not repopulate the cache. Schema changes made by other clients are detected
|
|
61
|
+
* on the next query, so this is rarely needed.
|
|
62
|
+
*/
|
|
63
|
+
clearMetadataCache() {
|
|
64
|
+
this.#schema.generation++;
|
|
65
|
+
this.#metadata = undefined;
|
|
48
66
|
}
|
|
49
67
|
async explain(query) {
|
|
50
68
|
return this.#client.explain(this.#name, query);
|
|
@@ -67,30 +85,50 @@ class Graph {
|
|
|
67
85
|
async copy(destGraph) {
|
|
68
86
|
return this.#client.copy(this.#name, destGraph);
|
|
69
87
|
}
|
|
70
|
-
#
|
|
88
|
+
#pendingMetadata;
|
|
71
89
|
#updateMetadata() {
|
|
72
|
-
|
|
73
|
-
|
|
90
|
+
const generation = this.#schema.generation;
|
|
91
|
+
// A fetch started before the metadata was invalidated is not joined
|
|
92
|
+
if (this.#pendingMetadata?.generation !== generation) {
|
|
93
|
+
const pending = {
|
|
94
|
+
generation,
|
|
95
|
+
promise: this.#setMetadata(generation).finally(() => {
|
|
96
|
+
if (this.#pendingMetadata === pending) {
|
|
97
|
+
this.#pendingMetadata = undefined;
|
|
98
|
+
}
|
|
99
|
+
}),
|
|
100
|
+
};
|
|
101
|
+
this.#pendingMetadata = pending;
|
|
102
|
+
}
|
|
103
|
+
return this.#pendingMetadata.promise;
|
|
74
104
|
}
|
|
75
105
|
// DO NOT use directly, use #updateMetadata instead
|
|
76
|
-
async #setMetadata() {
|
|
106
|
+
async #setMetadata(generation) {
|
|
77
107
|
const [labels, relationshipTypes, propertyKeys] = await Promise.all([
|
|
78
108
|
this.#client.roQuery(this.#name, "CALL db.labels()", undefined, false),
|
|
79
109
|
this.#client.roQuery(this.#name, "CALL db.relationshipTypes()", undefined, false),
|
|
80
110
|
this.#client.roQuery(this.#name, "CALL db.propertyKeys()", undefined, false),
|
|
81
111
|
]);
|
|
82
|
-
|
|
112
|
+
const metadata = {
|
|
83
113
|
labels: this.#cleanMetadataArray(labels.data),
|
|
84
114
|
relationshipTypes: this.#cleanMetadataArray(relationshipTypes.data),
|
|
85
115
|
propertyKeys: this.#cleanMetadataArray(propertyKeys.data),
|
|
86
116
|
};
|
|
87
|
-
|
|
117
|
+
// The caller's reply predates any reset, so it still gets these names,
|
|
118
|
+
// but they are only cached if the graph was not deleted meanwhile
|
|
119
|
+
if (generation === this.#schema.generation) {
|
|
120
|
+
this.#metadata = { generation, names: metadata };
|
|
121
|
+
}
|
|
122
|
+
return metadata;
|
|
88
123
|
}
|
|
89
124
|
#cleanMetadataArray(arr) {
|
|
90
125
|
return arr.map(([value]) => value);
|
|
91
126
|
}
|
|
92
127
|
#getMetadata(key, id) {
|
|
93
|
-
|
|
128
|
+
const metadata = this.#metadata?.generation === this.#schema.generation
|
|
129
|
+
? this.#metadata.names
|
|
130
|
+
: undefined;
|
|
131
|
+
return metadata?.[key][id] ?? this.#getMetadataAsync(key, id);
|
|
94
132
|
}
|
|
95
133
|
// DO NOT use directly, use #getMetadata instead
|
|
96
134
|
async #getMetadataAsync(key, id) {
|
|
@@ -121,53 +159,53 @@ class Graph {
|
|
|
121
159
|
}
|
|
122
160
|
#parseValue([valueType, value], promises) {
|
|
123
161
|
switch (valueType) {
|
|
124
|
-
case GraphValueTypes.NULL:
|
|
162
|
+
case enums_1.GraphValueTypes.NULL:
|
|
125
163
|
return null;
|
|
126
|
-
case GraphValueTypes.STRING:
|
|
127
|
-
case GraphValueTypes.INTEGER:
|
|
164
|
+
case enums_1.GraphValueTypes.STRING:
|
|
165
|
+
case enums_1.GraphValueTypes.INTEGER:
|
|
128
166
|
return value;
|
|
129
|
-
case GraphValueTypes.BOOLEAN:
|
|
167
|
+
case enums_1.GraphValueTypes.BOOLEAN:
|
|
130
168
|
return value === "true";
|
|
131
|
-
case GraphValueTypes.DOUBLE:
|
|
169
|
+
case enums_1.GraphValueTypes.DOUBLE:
|
|
132
170
|
return parseFloat(value);
|
|
133
|
-
case GraphValueTypes.ARRAY:
|
|
171
|
+
case enums_1.GraphValueTypes.ARRAY:
|
|
134
172
|
return value.map((x) => this.#parseValue(x, promises));
|
|
135
|
-
case GraphValueTypes.EDGE:
|
|
173
|
+
case enums_1.GraphValueTypes.EDGE:
|
|
136
174
|
return this.#parseEdge(value, promises);
|
|
137
|
-
case GraphValueTypes.NODE:
|
|
175
|
+
case enums_1.GraphValueTypes.NODE:
|
|
138
176
|
return this.#parseNode(value, promises);
|
|
139
|
-
case GraphValueTypes.PATH:
|
|
177
|
+
case enums_1.GraphValueTypes.PATH:
|
|
140
178
|
return {
|
|
141
179
|
nodes: value[0][1].map(([, node]) => this.#parseNode(node, promises)),
|
|
142
180
|
edges: value[1][1].map(([, edge]) => this.#parseEdge(edge, promises)),
|
|
143
181
|
};
|
|
144
|
-
case GraphValueTypes.MAP: {
|
|
182
|
+
case enums_1.GraphValueTypes.MAP: {
|
|
145
183
|
const map = {};
|
|
146
184
|
for (let i = 0; i < value.length; i++) {
|
|
147
185
|
map[value[i++]] = this.#parseValue(value[i], promises);
|
|
148
186
|
}
|
|
149
187
|
return map;
|
|
150
188
|
}
|
|
151
|
-
case GraphValueTypes.POINT:
|
|
189
|
+
case enums_1.GraphValueTypes.POINT:
|
|
152
190
|
return {
|
|
153
191
|
latitude: parseFloat(value[0]),
|
|
154
192
|
longitude: parseFloat(value[1]),
|
|
155
193
|
};
|
|
156
|
-
case GraphValueTypes.VECTORF32:
|
|
194
|
+
case enums_1.GraphValueTypes.VECTORF32:
|
|
157
195
|
return value.map((x) => Number(x));
|
|
158
|
-
case GraphValueTypes.DATETIME:
|
|
196
|
+
case enums_1.GraphValueTypes.DATETIME:
|
|
159
197
|
return polyfill_1.Temporal.Instant.fromEpochMilliseconds(value * 1000)
|
|
160
198
|
.toZonedDateTimeISO("UTC")
|
|
161
199
|
.toPlainDateTime();
|
|
162
|
-
case GraphValueTypes.DATE:
|
|
200
|
+
case enums_1.GraphValueTypes.DATE:
|
|
163
201
|
return polyfill_1.Temporal.Instant.fromEpochMilliseconds(value * 1000)
|
|
164
202
|
.toZonedDateTimeISO("UTC")
|
|
165
203
|
.toPlainDate();
|
|
166
|
-
case GraphValueTypes.TIME:
|
|
204
|
+
case enums_1.GraphValueTypes.TIME:
|
|
167
205
|
return polyfill_1.Temporal.Instant.fromEpochMilliseconds(value * 1000)
|
|
168
206
|
.toZonedDateTimeISO("UTC")
|
|
169
207
|
.toPlainTime();
|
|
170
|
-
case GraphValueTypes.DURATION:
|
|
208
|
+
case enums_1.GraphValueTypes.DURATION:
|
|
171
209
|
const time = polyfill_1.Temporal.Instant.fromEpochMilliseconds(value * 1000);
|
|
172
210
|
const epoch = polyfill_1.Temporal.Instant.fromEpochMilliseconds(0);
|
|
173
211
|
return epoch
|
|
@@ -295,3 +333,4 @@ class Graph {
|
|
|
295
333
|
}
|
|
296
334
|
}
|
|
297
335
|
exports.default = Graph;
|
|
336
|
+
exports.Graph = Graph;
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import type { SocketOptions } from './socket-options';
|
|
2
|
+
export interface FalkorDBOptions {
|
|
3
|
+
/**
|
|
4
|
+
* `falkor[s]://[[username][:password]@][host][:port][/db-number]`
|
|
5
|
+
*/
|
|
6
|
+
url?: string;
|
|
7
|
+
/**
|
|
8
|
+
* Socket connection properties
|
|
9
|
+
*/
|
|
10
|
+
socket?: SocketOptions;
|
|
11
|
+
/**
|
|
12
|
+
* Server hostname, shorthand for `socket.host`.
|
|
13
|
+
* Ignored when `url` or `socket.host` is provided.
|
|
14
|
+
*/
|
|
15
|
+
host?: string;
|
|
16
|
+
/**
|
|
17
|
+
* Server port, shorthand for `socket.port`.
|
|
18
|
+
* Ignored when `url` or `socket.port` is provided.
|
|
19
|
+
*/
|
|
20
|
+
port?: number;
|
|
21
|
+
/**
|
|
22
|
+
* Sentinel only. Resend write commands to the new master when the
|
|
23
|
+
* connection is lost while waiting for their reply, e.g. during a master
|
|
24
|
+
* hand-over. The old master may have already run them, so a write can be
|
|
25
|
+
* applied twice. Read-only commands, and commands the server certainly did
|
|
26
|
+
* not run (such as ones rejected with `READONLY`), are always resent.
|
|
27
|
+
* @default false
|
|
28
|
+
*/
|
|
29
|
+
retryWritesOnConnectionLoss?: boolean;
|
|
30
|
+
/**
|
|
31
|
+
* ACL username ([see ACL guide](https://redis.io/topics/acl))
|
|
32
|
+
*/
|
|
33
|
+
username?: string;
|
|
34
|
+
/**
|
|
35
|
+
* ACL password or the old "--requirepass" password
|
|
36
|
+
*/
|
|
37
|
+
password?: string;
|
|
38
|
+
/**
|
|
39
|
+
* Client name ([see `CLIENT SETNAME`](https://redis.io/commands/client-setname))
|
|
40
|
+
*/
|
|
41
|
+
name?: string;
|
|
42
|
+
/**
|
|
43
|
+
* Connect in [`READONLY`](https://redis.io/commands/readonly) mode
|
|
44
|
+
*/
|
|
45
|
+
readonly?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Send `PING` command at interval (in ms).
|
|
48
|
+
* Useful with Redis deployments that do not use TCP Keep-Alive.
|
|
49
|
+
*/
|
|
50
|
+
pingInterval?: number;
|
|
51
|
+
/**
|
|
52
|
+
* If set to true, disables sending client identifier (user-agent like message) to the redis server
|
|
53
|
+
*/
|
|
54
|
+
disableClientInfo?: boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Tag to append to library name that is sent to the Redis server
|
|
57
|
+
*/
|
|
58
|
+
clientInfoTag?: string;
|
|
59
|
+
/**
|
|
60
|
+
* RESP protocol version. Only RESP 2 is supported: under RESP 3 node-redis rewrites
|
|
61
|
+
* module map replies into objects, which does not match the reply shapes this client
|
|
62
|
+
* parses and publishes types for. Passing anything else throws.
|
|
63
|
+
*/
|
|
64
|
+
RESP?: 2;
|
|
65
|
+
}
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { GraphValueTypes } from '../enums';
|
|
2
|
+
export type GraphEntityRawProperties = Array<[id: number, ...value: GraphRawValue]>;
|
|
3
|
+
export type GraphEdgeRawValue = [
|
|
4
|
+
GraphValueTypes.EDGE,
|
|
5
|
+
[
|
|
6
|
+
id: number,
|
|
7
|
+
relationshipTypeId: number,
|
|
8
|
+
sourceId: number,
|
|
9
|
+
destinationId: number,
|
|
10
|
+
properties: GraphEntityRawProperties
|
|
11
|
+
]
|
|
12
|
+
];
|
|
13
|
+
export type GraphNodeRawValue = [
|
|
14
|
+
GraphValueTypes.NODE,
|
|
15
|
+
[
|
|
16
|
+
id: number,
|
|
17
|
+
labelIds: Array<number>,
|
|
18
|
+
properties: GraphEntityRawProperties
|
|
19
|
+
]
|
|
20
|
+
];
|
|
21
|
+
export type GraphPathRawValue = [
|
|
22
|
+
GraphValueTypes.PATH,
|
|
23
|
+
[
|
|
24
|
+
nodes: [GraphValueTypes.ARRAY, Array<GraphNodeRawValue>],
|
|
25
|
+
edges: [GraphValueTypes.ARRAY, Array<GraphEdgeRawValue>]
|
|
26
|
+
]
|
|
27
|
+
];
|
|
28
|
+
export type GraphMapRawValue = [GraphValueTypes.MAP, Array<string | GraphRawValue>];
|
|
29
|
+
export type GraphRawValue = [GraphValueTypes.NULL, null] | [GraphValueTypes.STRING, string] | [GraphValueTypes.INTEGER, number] | [GraphValueTypes.BOOLEAN, string] | [GraphValueTypes.DOUBLE, string] | [GraphValueTypes.ARRAY, Array<GraphRawValue>] | GraphEdgeRawValue | GraphNodeRawValue | GraphPathRawValue | GraphMapRawValue | [GraphValueTypes.POINT, [latitude: string, longitude: string]] | [GraphValueTypes.VECTORF32, number[]] | [GraphValueTypes.DATETIME, number] | [GraphValueTypes.DATE, number] | [GraphValueTypes.TIME, number] | [GraphValueTypes.DURATION, number];
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import type { Temporal } from '@js-temporal/polyfill';
|
|
2
|
+
import type { QueryReply } from './query-reply';
|
|
3
|
+
export type GraphEntityProperties = Record<string, GraphValue>;
|
|
4
|
+
export interface GraphEdge {
|
|
5
|
+
id: number;
|
|
6
|
+
relationshipType: string;
|
|
7
|
+
sourceId: number;
|
|
8
|
+
destinationId: number;
|
|
9
|
+
properties: GraphEntityProperties;
|
|
10
|
+
}
|
|
11
|
+
export interface GraphNode {
|
|
12
|
+
id: number;
|
|
13
|
+
labels: Array<string>;
|
|
14
|
+
properties: GraphEntityProperties;
|
|
15
|
+
}
|
|
16
|
+
export interface GraphPath {
|
|
17
|
+
nodes: Array<GraphNode>;
|
|
18
|
+
edges: Array<GraphEdge>;
|
|
19
|
+
}
|
|
20
|
+
export type GraphMap = {
|
|
21
|
+
[key: string]: GraphValue;
|
|
22
|
+
};
|
|
23
|
+
export type GraphValue = null | string | number | boolean | Array<GraphValue> | GraphEdge | GraphNode | GraphPath | GraphMap | {
|
|
24
|
+
latitude: string;
|
|
25
|
+
longitude: string;
|
|
26
|
+
} | number[] | Temporal.PlainDateTime | Temporal.PlainDate | Temporal.PlainTime | Temporal.Duration;
|
|
27
|
+
export type GraphReply<T> = Omit<QueryReply, "headers" | "data"> & {
|
|
28
|
+
data?: Array<T>;
|
|
29
|
+
};
|