Skip to content

Commit

Permalink
Expose export functionality (#381)
Browse files Browse the repository at this point in the history
  • Loading branch information
macjuul authored Nov 22, 2024
1 parent 93872bc commit 3f12cf6
Show file tree
Hide file tree
Showing 12 changed files with 236 additions and 60 deletions.
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

0 comments on commit 3f12cf6

Please sign in to comment.