Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Expose export functionality #381

Merged
merged 13 commits into from
Nov 22, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
surrealdb: ["v1.4.2", "v1.5.3", "v2.0.2"]
surrealdb: ["v1.4.2", "v1.5.3", "v2.0.4", "v2.1.0"]
engine: ["ws", "http"]
steps:
- name: Install SurrealDB ${{ matrix.surrealdb }} over ${{ matrix.engine }} engine
Expand Down
Binary file modified bun.lockb
Binary file not shown.
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
"devDependencies": {
"@biomejs/biome": "1.8.3",
"@types/bun": "latest",
"compare-versions": "^6.1.1",
"dts-bundle-generator": "^9.5.1",
"esbuild": "^0.21.5",
"esbuild-plugin-tsc": "^0.4.0",
Expand Down
48 changes: 46 additions & 2 deletions src/engines/abstract.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import type { EngineDisconnected } from "../errors";
import { type EngineDisconnected, HttpConnectionError } from "../errors";
import type {
ExportOptions,
LiveHandlerArguments,
Expand Down Expand Up @@ -90,5 +90,49 @@ export abstract class AbstractEngine {
>(request: RpcRequest<Method, Params>): Promise<RpcResponse<Result>>;

abstract version(url: URL, timeout?: number): Promise<string>;
abstract export(options?: ExportOptions): Promise<string>;
abstract export(options?: Partial<ExportOptions>): Promise<string>;

protected async req_post(
body: unknown,
url?: URL,
headers_?: Record<string, string>,
): Promise<ArrayBuffer> {
const headers: Record<string, string> = {
"Content-Type": "application/cbor",
Accept: "application/cbor",
...headers_,
};

if (this.connection.namespace) {
headers["Surreal-NS"] = this.connection.namespace;
}

if (this.connection.database) {
headers["Surreal-DB"] = this.connection.database;
}

if (this.connection.token) {
headers.Authorization = `Bearer ${this.connection.token}`;
}

const raw = await fetch(`${url ?? this.connection.url}`, {
method: "POST",
headers,
body: this.encodeCbor(body),
});

const buffer = await raw.arrayBuffer();

if (raw.status === 200) {
return buffer;
}

const dec = new TextDecoder("utf-8");
throw new HttpConnectionError(
dec.decode(buffer),
raw.status,
raw.statusText,
buffer,
);
}
}
54 changes: 3 additions & 51 deletions src/engines/http.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,4 @@
import {
ConnectionUnavailable,
HttpConnectionError,
MissingNamespaceDatabase,
} from "../errors";
import { ConnectionUnavailable, MissingNamespaceDatabase } from "../errors";
import type { ExportOptions, RpcRequest, RpcResponse } from "../types";
import { getIncrementalID } from "../util/get-incremental-id";
import { retrieveRemoteVersion } from "../util/version-check";
Expand Down Expand Up @@ -165,63 +161,19 @@ export class HttpEngine extends AbstractEngine {
return !!this.connection.url;
}

async export(options?: ExportOptions): Promise<string> {
async export(options?: Partial<ExportOptions>): Promise<string> {
if (!this.connection.url) {
throw new ConnectionUnavailable();
}
const url = new URL(this.connection.url);
const basepath = url.pathname.slice(0, -4);
url.pathname = `${basepath}/export`;

const buffer = await this.req_post(options, url, {
const buffer = await this.req_post(options ?? {}, url, {
Accept: "plain/text",
});

const dec = new TextDecoder("utf-8");
return dec.decode(buffer);
}

private async req_post(
body: unknown,
url?: URL,
headers_?: Record<string, string>,
): Promise<ArrayBuffer> {
const headers: Record<string, string> = {
"Content-Type": "application/cbor",
Accept: "application/cbor",
...headers_,
};

if (this.connection.namespace) {
headers["Surreal-NS"] = this.connection.namespace;
}

if (this.connection.database) {
headers["Surreal-DB"] = this.connection.database;
}

if (this.connection.token) {
headers.Authorization = `Bearer ${this.connection.token}`;
}

const raw = await fetch(`${url ?? this.connection.url}`, {
method: "POST",
headers,
body: this.encodeCbor(body),
});

const buffer = await raw.arrayBuffer();

if (raw.status === 200) {
return buffer;
}

const dec = new TextDecoder("utf-8");
throw new HttpConnectionError(
dec.decode(buffer),
raw.status,
raw.statusText,
buffer,
);
}
}
18 changes: 15 additions & 3 deletions src/engines/ws.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ import { WebSocket } from "isows";
import {
ConnectionUnavailable,
EngineDisconnected,
FeatureUnavailableForEngine,
ResponseError,
UnexpectedConnectionError,
UnexpectedServerResponse,
Expand Down Expand Up @@ -214,8 +213,21 @@ export class WebsocketEngine extends AbstractEngine {
return !!this.socket;
}

async export(options?: ExportOptions): Promise<string> {
throw new FeatureUnavailableForEngine();
async export(options?: Partial<ExportOptions>): Promise<string> {
if (!this.connection.url) {
throw new ConnectionUnavailable();
}
const url = new URL(this.connection.url);
const basepath = url.pathname.slice(0, -4);
url.protocol = url.protocol.replace("ws", "http");
url.pathname = `${basepath}/export`;

const buffer = await this.req_post(options ?? {}, url, {
Accept: "plain/text",
});

const dec = new TextDecoder("utf-8");
return dec.decode(buffer);
}
}

Expand Down
11 changes: 11 additions & 0 deletions src/surreal.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import {
type AccessRecordAuth,
type ActionResult,
type AnyAuth,
type ExportOptions,
type LiveHandler,
type MapQueryResult,
type Patch,
Expand Down Expand Up @@ -741,6 +742,16 @@ export class Surreal {
params,
});
}

/**
* Export the database and return the result as a string
* @param options - Export configuration options
*/
public async export(options?: Partial<ExportOptions>): Promise<string> {
await this.ready;
if (!this.connection) throw new NoActiveSocket();
return this.connection.export(options);
}
}

type Output<T, S> = S extends RecordId ? T : T[];
Expand Down
5 changes: 5 additions & 0 deletions tests/integration/helpers.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import type Surreal from "../../src";

export async function fetchVersion(surreal: Surreal): Promise<string> {
return (await surreal.version()).replace(/^surrealdb-/, "");
}
94 changes: 94 additions & 0 deletions tests/integration/tests/__snapshots__/export.test.ts.snap
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
// Bun Snapshot v1, https://goo.gl/fbAQLP

exports[`export basic 1`] = `
"-- ------------------------------
-- OPTION
-- ------------------------------

OPTION IMPORT;

-- ------------------------------
-- FUNCTIONS
-- ------------------------------

DEFINE FUNCTION fn::foo() { RETURN 'bar'; } PERMISSIONS FULL;

-- ------------------------------
-- TABLE: bar
-- ------------------------------

DEFINE TABLE bar TYPE ANY SCHEMALESS PERMISSIONS NONE;




-- ------------------------------
-- TABLE DATA: bar
-- ------------------------------

INSERT [ { hello: 'world', id: bar:1 } ];

-- ------------------------------
-- TABLE: foo
-- ------------------------------

DEFINE TABLE foo TYPE ANY SCHEMALESS PERMISSIONS NONE;




-- ------------------------------
-- TABLE DATA: foo
-- ------------------------------

INSERT [ { hello: 'world', id: foo:1 } ];

"
`;

exports[`export filter tables 1`] = `
"-- ------------------------------
-- OPTION
-- ------------------------------

OPTION IMPORT;

-- ------------------------------
-- FUNCTIONS
-- ------------------------------

DEFINE FUNCTION fn::foo() { RETURN 'bar'; } PERMISSIONS FULL;

-- ------------------------------
-- TABLE: foo
-- ------------------------------

DEFINE TABLE foo TYPE ANY SCHEMALESS PERMISSIONS NONE;




-- ------------------------------
-- TABLE DATA: foo
-- ------------------------------

INSERT [ { hello: 'world', id: foo:1 } ];

"
`;

exports[`export filter functions 1`] = `
"-- ------------------------------
-- OPTION
-- ------------------------------

OPTION IMPORT;

-- ------------------------------
-- FUNCTIONS
-- ------------------------------

DEFINE FUNCTION fn::foo() { RETURN 'bar'; } PERMISSIONS FULL;

"
`;
46 changes: 46 additions & 0 deletions tests/integration/tests/export.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
import { beforeAll, describe, expect, test } from "bun:test";
import { compareVersions } from "compare-versions";
import { surql } from "../../../src";
import { fetchVersion } from "../helpers.ts";
import { setupServer } from "../surreal.ts";

const { createSurreal } = await setupServer();

beforeAll(async () => {
const surreal = await createSurreal();

await surreal.query(surql`
CREATE foo:1 CONTENT { hello: "world" };
CREATE bar:1 CONTENT { hello: "world" };
DEFINE FUNCTION fn::foo() { RETURN "bar"; };
`);
});

describe("export", async () => {
const surreal = await createSurreal();
const version = await fetchVersion(surreal);
const hasPostExport = compareVersions(version, "2.1.0") >= 0;

test.if(hasPostExport)("basic", async () => {
const res = await surreal.export();

expect(res).toMatchSnapshot();
});

test.if(hasPostExport)("filter tables", async () => {
const res = await surreal.export({
tables: ["foo"],
});

expect(res).toMatchSnapshot();
});

test.if(hasPostExport)("filter functions", async () => {
const res = await surreal.export({
functions: true,
tables: false,
});

expect(res).toMatchSnapshot();
});
});
6 changes: 6 additions & 0 deletions tests/integration/tests/live.test.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import { describe, expect, test } from "bun:test";
import { compareVersions } from "compare-versions";
import {
type LiveHandlerArguments,
RecordId,
ResponseError,
type Surreal,
Uuid,
} from "../../../src";
import { fetchVersion } from "../helpers.ts";
import { setupServer } from "../surreal.ts";

const { createSurreal } = await setupServer();
Expand All @@ -24,8 +26,12 @@ describe("Live Queries HTTP", async () => {

describe("Live Queries WS", async () => {
const surreal = await createSurreal();
const version = await fetchVersion(surreal);
if (isHttp(surreal)) return;

// temp - subscribe is broken is 2.1.0
if (compareVersions(version, "2.1.0") >= 0) return;

test("live", async () => {
const events = new CollectablePromise<{
action: LiveHandlerArguments[0];
Expand Down
Loading