@irongraph/client 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,57 @@
1
+ export type TypedValue = {
2
+ type: 'null';
3
+ } | {
4
+ type: 'boolean';
5
+ value: boolean;
6
+ } | {
7
+ type: 'integer';
8
+ value: string;
9
+ } | {
10
+ type: 'float';
11
+ value: number;
12
+ } | {
13
+ type: 'string';
14
+ value: string;
15
+ } | {
16
+ type: string;
17
+ value?: unknown;
18
+ };
19
+ export interface QueryRequest {
20
+ cypher: string;
21
+ projectId?: string;
22
+ parameters?: Record<string, unknown>;
23
+ bookmark?: {
24
+ term: number;
25
+ index: number;
26
+ };
27
+ limits?: Partial<QueryLimits>;
28
+ signal?: AbortSignal;
29
+ }
30
+ export interface QueryLimits {
31
+ rows: number;
32
+ bytes: number;
33
+ nodes: number;
34
+ edges: number;
35
+ }
36
+ export interface QueryColumn {
37
+ name: string;
38
+ value_type: string;
39
+ nullable: boolean;
40
+ }
41
+ export interface QueryResult {
42
+ catalog?: Record<string, unknown>;
43
+ columns: QueryColumn[];
44
+ rows: TypedValue[][];
45
+ summary: Record<string, unknown>;
46
+ }
47
+ export declare class IronGraphError extends Error {
48
+ readonly code: string;
49
+ readonly retryable: boolean;
50
+ readonly retryAfterMs?: number | undefined;
51
+ constructor(message: string, code: string, retryable?: boolean, retryAfterMs?: number | undefined);
52
+ }
53
+ export declare class Client {
54
+ private readonly endpoint;
55
+ constructor(baseUrl: string | URL);
56
+ query(request: QueryRequest): Promise<QueryResult>;
57
+ }
package/dist/index.js ADDED
@@ -0,0 +1,105 @@
1
+ const MAXIMUM_EVENT_CHARACTERS = 64 * 1024 * 1024;
2
+ export class IronGraphError extends Error {
3
+ code;
4
+ retryable;
5
+ retryAfterMs;
6
+ constructor(message, code, retryable = false, retryAfterMs) {
7
+ super(message);
8
+ this.code = code;
9
+ this.retryable = retryable;
10
+ this.retryAfterMs = retryAfterMs;
11
+ this.name = 'IronGraphError';
12
+ }
13
+ }
14
+ export class Client {
15
+ endpoint;
16
+ constructor(baseUrl) {
17
+ this.endpoint = new URL('/api/query', baseUrl);
18
+ const local = this.endpoint.hostname === 'localhost'
19
+ || this.endpoint.hostname === '127.0.0.1'
20
+ || this.endpoint.hostname === '[::1]';
21
+ if (this.endpoint.protocol !== 'https:' && !(this.endpoint.protocol === 'http:' && local)) {
22
+ throw new IronGraphError('Plain remote connections are forbidden; use HTTPS with a browser-managed client certificate', 'ConfigurationError');
23
+ }
24
+ }
25
+ async query(request) {
26
+ if (!request.cypher.trim())
27
+ throw new IronGraphError('Cypher is empty', 'ConfigurationError');
28
+ const response = await fetch(this.endpoint, {
29
+ method: 'POST',
30
+ headers: { Accept: 'application/x-ndjson', 'Content-Type': 'application/json' },
31
+ body: JSON.stringify({
32
+ request_id: crypto.randomUUID(),
33
+ project_id: request.projectId ?? null,
34
+ query: request.cypher,
35
+ parameters: request.parameters ?? {},
36
+ bookmark: request.bookmark ?? null,
37
+ ...(request.limits ? { limits: request.limits } : {}),
38
+ }),
39
+ signal: request.signal,
40
+ });
41
+ if (!response.ok)
42
+ throw new IronGraphError(`HTTP ${response.status}`, 'HttpError');
43
+ if (!response.body)
44
+ throw new IronGraphError('Query response has no body', 'ProtocolError');
45
+ const result = { columns: [], rows: [], summary: {} };
46
+ for await (const event of decodeNdjson(response.body)) {
47
+ if (event.type === 'error') {
48
+ throw new IronGraphError(String(event.message ?? 'Query failed'), String(event.code ?? 'QueryError'), Boolean(event.retryable), typeof event.retry_after_ms === 'number' ? event.retry_after_ms : undefined);
49
+ }
50
+ if (event.type === 'catalog') {
51
+ const { type: _type, ...catalog } = event;
52
+ result.catalog = catalog;
53
+ }
54
+ if (event.type === 'schema')
55
+ result.columns = (event.columns ?? []);
56
+ if (event.type === 'batch')
57
+ appendBatch(result, event);
58
+ if (event.type === 'summary') {
59
+ const { type: _type, ...summary } = event;
60
+ result.summary = summary;
61
+ }
62
+ }
63
+ return result;
64
+ }
65
+ }
66
+ function appendBatch(result, event) {
67
+ const columns = Array.isArray(event.columns) ? event.columns : [];
68
+ const rowCount = Number(event.row_count ?? 0);
69
+ if (!Number.isSafeInteger(rowCount) || rowCount < 0) {
70
+ throw new IronGraphError('Invalid batch row count', 'ProtocolError');
71
+ }
72
+ if (columns.some((column) => !Array.isArray(column.values) || column.values.length !== rowCount)) {
73
+ throw new IronGraphError('Misaligned batch columns', 'ProtocolError');
74
+ }
75
+ for (let rowIndex = 0; rowIndex < rowCount; rowIndex += 1) {
76
+ result.rows.push(columns.map((column) => column.values[rowIndex]));
77
+ }
78
+ }
79
+ async function* decodeNdjson(stream) {
80
+ const reader = stream.getReader();
81
+ const decoder = new TextDecoder();
82
+ let buffered = '';
83
+ try {
84
+ while (true) {
85
+ const { value, done } = await reader.read();
86
+ buffered += decoder.decode(value, { stream: !done });
87
+ if (buffered.length > MAXIMUM_EVENT_CHARACTERS && !buffered.includes('\n')) {
88
+ throw new IronGraphError('One query event exceeds 64 MiB', 'ProtocolError');
89
+ }
90
+ const lines = buffered.split('\n');
91
+ buffered = lines.pop() ?? '';
92
+ for (const line of lines) {
93
+ if (line.trim())
94
+ yield JSON.parse(line);
95
+ }
96
+ if (done)
97
+ break;
98
+ }
99
+ if (buffered.trim())
100
+ yield JSON.parse(buffered);
101
+ }
102
+ finally {
103
+ reader.releaseLock();
104
+ }
105
+ }
@@ -0,0 +1,12 @@
1
+ import { type ReactNode } from 'react';
2
+ import { Client, type QueryRequest, type QueryResult } from './index.js';
3
+ export declare function IronGraphProvider({ baseUrl, children }: {
4
+ baseUrl: string | URL;
5
+ children: ReactNode;
6
+ }): import("react").FunctionComponentElement<import("react").ProviderProps<Client | null>>;
7
+ export declare function useIronGraphClient(): Client;
8
+ export declare function useIronGraphQuery(request: Omit<QueryRequest, 'signal'> | null): {
9
+ result: QueryResult | undefined;
10
+ error: unknown;
11
+ loading: boolean;
12
+ };
package/dist/react.js ADDED
@@ -0,0 +1,41 @@
1
+ import { createContext, createElement, useContext, useEffect, useMemo, useState } from 'react';
2
+ import { Client } from './index.js';
3
+ const IronGraphContext = createContext(null);
4
+ export function IronGraphProvider({ baseUrl, children }) {
5
+ const client = useMemo(() => new Client(baseUrl), [String(baseUrl)]);
6
+ return createElement(IronGraphContext.Provider, { value: client }, children);
7
+ }
8
+ export function useIronGraphClient() {
9
+ const client = useContext(IronGraphContext);
10
+ if (!client)
11
+ throw new Error('useIronGraphClient must be used within IronGraphProvider');
12
+ return client;
13
+ }
14
+ export function useIronGraphQuery(request) {
15
+ const client = useIronGraphClient();
16
+ const [result, setResult] = useState();
17
+ const [error, setError] = useState();
18
+ const [loading, setLoading] = useState(request !== null);
19
+ const signature = JSON.stringify(request);
20
+ useEffect(() => {
21
+ if (!request) {
22
+ setLoading(false);
23
+ return;
24
+ }
25
+ const controller = new AbortController();
26
+ setLoading(true);
27
+ setError(undefined);
28
+ client.query({ ...request, signal: controller.signal })
29
+ .then(setResult)
30
+ .catch((caught) => {
31
+ if (!controller.signal.aborted)
32
+ setError(caught);
33
+ })
34
+ .finally(() => {
35
+ if (!controller.signal.aborted)
36
+ setLoading(false);
37
+ });
38
+ return () => controller.abort();
39
+ }, [client, signature]);
40
+ return { result, error, loading };
41
+ }
package/package.json ADDED
@@ -0,0 +1,59 @@
1
+ {
2
+ "name": "@irongraph/client",
3
+ "version": "0.1.0",
4
+ "description": "JavaScript and React client for an embeddable graph database with automatic local embeddings and vector search.",
5
+ "license": "Apache-2.0",
6
+ "repository": {
7
+ "type": "git",
8
+ "url": "git+https://github.com/agentbusiness/IronGraph.git",
9
+ "directory": "bindings/javascript"
10
+ },
11
+ "homepage": "https://github.com/agentbusiness/IronGraph#readme",
12
+ "bugs": {
13
+ "url": "https://github.com/agentbusiness/IronGraph/issues"
14
+ },
15
+ "type": "module",
16
+ "main": "dist/index.js",
17
+ "types": "dist/index.d.ts",
18
+ "sideEffects": false,
19
+ "publishConfig": {
20
+ "access": "public"
21
+ },
22
+ "files": [
23
+ "dist",
24
+ "README.md",
25
+ "LICENSE.txt",
26
+ "THIRD_PARTY_NOTICES.txt"
27
+ ],
28
+ "exports": {
29
+ ".": {
30
+ "types": "./dist/index.d.ts",
31
+ "import": "./dist/index.js"
32
+ },
33
+ "./react": {
34
+ "types": "./dist/react.d.ts",
35
+ "import": "./dist/react.js"
36
+ }
37
+ },
38
+ "scripts": {
39
+ "build": "tsc -p tsconfig.json",
40
+ "test": "vitest run",
41
+ "pack:check": "npm pack --dry-run"
42
+ },
43
+ "peerDependencies": {
44
+ "react": ">=18"
45
+ },
46
+ "peerDependenciesMeta": {
47
+ "react": {
48
+ "optional": true
49
+ }
50
+ },
51
+ "devDependencies": {
52
+ "@types/react": "19.2.17",
53
+ "@types/react-dom": "19.2.3",
54
+ "react": "19.2.7",
55
+ "react-dom": "19.2.7",
56
+ "typescript": "6.0.3",
57
+ "vitest": "4.1.10"
58
+ }
59
+ }