@k2b/rsql 0.4.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.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Valentin Kolb
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,81 @@
1
+ # @k2b/rsql
2
+
3
+ Type-safe, fetch-based client for the
4
+ [rsql](https://github.com/k2b-dev/rsql) HTTP API. It runs in Bun, Node, and
5
+ modern browsers without runtime dependencies.
6
+
7
+ ## Install
8
+
9
+ ```bash
10
+ bun add @k2b/rsql
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { createRsqlClient } from "@k2b/rsql";
17
+
18
+ const client = createRsqlClient({
19
+ url: "http://127.0.0.1:8080",
20
+ token: "secret",
21
+ });
22
+
23
+ const contacts = client.ns("crm").table<{
24
+ id: number;
25
+ name: string;
26
+ status: "active" | "inactive";
27
+ }>("contacts");
28
+
29
+ const result = await contacts.rows.list({
30
+ status: "eq.active",
31
+ order: "name.asc",
32
+ });
33
+
34
+ if (result.ok) {
35
+ console.log(result.data);
36
+ }
37
+ ```
38
+
39
+ Modules are hierarchical and stateless:
40
+
41
+ ```text
42
+ client.namespaces
43
+ client.ns(name).tables
44
+ client.ns(name).table(name).schema
45
+ client.ns(name).table(name).indexes
46
+ client.ns(name).table(name).rows
47
+ client.ns(name).query
48
+ client.ns(name).changelog
49
+ client.ns(name).overview
50
+ client.ns(name).events
51
+ ```
52
+
53
+ HTTP errors resolve to `RsqlResult<T>`. Network and runtime failures may reject
54
+ the promise. Export methods return the native streaming `Response`; SSE
55
+ subscriptions expose an async iterable and explicit `close` function.
56
+
57
+ ## Default Client
58
+
59
+ The `rsql` export reads `RSQL_URL` and `RSQL_API_TOKEN` lazily on first access:
60
+
61
+ ```ts
62
+ import { rsql } from "@k2b/rsql";
63
+
64
+ await rsql.namespaces.list();
65
+ ```
66
+
67
+ Imports remain browser-safe because environment variables are not read during
68
+ module evaluation. Browser applications should use an explicit client and must
69
+ not expose a server-wide administrative token.
70
+
71
+ ## Development
72
+
73
+ ```bash
74
+ bun install
75
+ bun run typecheck
76
+ bun test
77
+ bun run test:package
78
+ ```
79
+
80
+ Performance profiles are available through `bun run perf:fast` and
81
+ `bun run perf:deep`.
@@ -0,0 +1,2 @@
1
+ import type { CreateRsqlClientOptions, RsqlClient } from "./types.js";
2
+ export declare const createRsqlClient: (options: CreateRsqlClientOptions) => RsqlClient;
package/dist/client.js ADDED
@@ -0,0 +1,12 @@
1
+ import { createHttpClient } from "./http.js";
2
+ import { createNamespaceModule } from "./modules/namespace.js";
3
+ import { createNamespacesModule } from "./modules/namespaces.js";
4
+ export const createRsqlClient = (options) => {
5
+ const http = createHttpClient(options);
6
+ return {
7
+ namespaces: createNamespacesModule(http),
8
+ ns(name) {
9
+ return createNamespaceModule(http, name);
10
+ },
11
+ };
12
+ };
@@ -0,0 +1,2 @@
1
+ import type { RsqlClient } from "./types.js";
2
+ export declare const rsql: RsqlClient;
@@ -0,0 +1,28 @@
1
+ import { createRsqlClient } from "./client.js";
2
+ let defaultClient = null;
3
+ const readEnv = (key) => {
4
+ const maybeProcess = globalThis.process;
5
+ return maybeProcess?.env?.[key];
6
+ };
7
+ const createDefaultClient = () => {
8
+ const url = readEnv("RSQL_URL");
9
+ const token = readEnv("RSQL_API_TOKEN");
10
+ if (!url || !token) {
11
+ throw new Error("RSQL_URL and RSQL_API_TOKEN environment variables are required.\n" +
12
+ "You can also create an explicit client:\n" +
13
+ " createRsqlClient({ url: \"http://127.0.0.1:8080\", token: \"secret\" })");
14
+ }
15
+ return createRsqlClient({ url, token });
16
+ };
17
+ export const rsql = new Proxy({}, {
18
+ get(_target, prop, receiver) {
19
+ if (defaultClient === null) {
20
+ defaultClient = createDefaultClient();
21
+ }
22
+ const value = Reflect.get(defaultClient, prop, receiver);
23
+ if (typeof value === "function") {
24
+ return value.bind(defaultClient);
25
+ }
26
+ return value;
27
+ },
28
+ });
package/dist/http.d.ts ADDED
@@ -0,0 +1,8 @@
1
+ import type { CreateRsqlClientOptions, QueryInput, RsqlResult } from "./types.js";
2
+ export interface HttpClient {
3
+ json<T>(path: string, init?: RequestInit): Promise<RsqlResult<T>>;
4
+ empty(path: string, init?: RequestInit): Promise<RsqlResult<void>>;
5
+ raw(path: string, init?: RequestInit): Promise<RsqlResult<Response>>;
6
+ withQuery(path: string, query?: QueryInput): string;
7
+ }
8
+ export declare const createHttpClient: (options: CreateRsqlClientOptions) => HttpClient;
package/dist/http.js ADDED
@@ -0,0 +1,134 @@
1
+ const defaultHeaders = (token) => ({
2
+ Authorization: `Bearer ${token}`,
3
+ });
4
+ export const createHttpClient = (options) => {
5
+ const baseUrl = options.url.replace(/\/$/, "");
6
+ const doFetch = options.fetch ?? fetch;
7
+ const execute = async (path, init) => {
8
+ const headers = new Headers(defaultHeaders(options.token));
9
+ if (init?.headers) {
10
+ const incoming = new Headers(init.headers);
11
+ for (const [k, v] of incoming.entries()) {
12
+ headers.set(k, v);
13
+ }
14
+ }
15
+ return doFetch(`${baseUrl}${path}`, {
16
+ ...init,
17
+ headers,
18
+ });
19
+ };
20
+ return {
21
+ async json(path, init) {
22
+ const res = await execute(path, init);
23
+ if (!res.ok) {
24
+ return errorResult(res);
25
+ }
26
+ if (res.status === 204) {
27
+ return {
28
+ ok: true,
29
+ data: undefined,
30
+ status: res.status,
31
+ headers: res.headers,
32
+ };
33
+ }
34
+ const data = (await res.json());
35
+ return {
36
+ ok: true,
37
+ data,
38
+ status: res.status,
39
+ headers: res.headers,
40
+ };
41
+ },
42
+ async empty(path, init) {
43
+ const res = await execute(path, init);
44
+ if (!res.ok) {
45
+ return errorResult(res);
46
+ }
47
+ return {
48
+ ok: true,
49
+ data: undefined,
50
+ status: res.status,
51
+ headers: res.headers,
52
+ };
53
+ },
54
+ async raw(path, init) {
55
+ const res = await execute(path, init);
56
+ if (!res.ok) {
57
+ return errorResult(res);
58
+ }
59
+ return {
60
+ ok: true,
61
+ data: res,
62
+ status: res.status,
63
+ headers: res.headers,
64
+ };
65
+ },
66
+ withQuery(path, query) {
67
+ if (!query) {
68
+ return path;
69
+ }
70
+ const search = toSearchParams(query);
71
+ const qs = search.toString();
72
+ if (!qs) {
73
+ return path;
74
+ }
75
+ return `${path}?${qs}`;
76
+ },
77
+ };
78
+ };
79
+ const errorResult = async (res) => {
80
+ let body = {
81
+ error: "unknown_error",
82
+ message: res.statusText || "Unknown error",
83
+ };
84
+ try {
85
+ const text = await res.text();
86
+ if (text.trim() !== "") {
87
+ try {
88
+ const maybeJson = JSON.parse(text);
89
+ body = {
90
+ error: maybeJson.error ?? "unknown_error",
91
+ message: maybeJson.message ?? (res.statusText || "Unknown error"),
92
+ };
93
+ }
94
+ catch {
95
+ body.message = text;
96
+ }
97
+ }
98
+ }
99
+ catch {
100
+ // Keep the HTTP status fallback when the body cannot be read.
101
+ }
102
+ return {
103
+ ok: false,
104
+ error: body,
105
+ status: res.status,
106
+ headers: res.headers,
107
+ };
108
+ };
109
+ const toSearchParams = (input) => {
110
+ if (input instanceof URLSearchParams) {
111
+ return input;
112
+ }
113
+ const params = new URLSearchParams();
114
+ for (const [key, raw] of Object.entries(input)) {
115
+ if (Array.isArray(raw)) {
116
+ for (const v of raw) {
117
+ appendQueryValue(params, key, v);
118
+ }
119
+ continue;
120
+ }
121
+ appendQueryValue(params, key, raw);
122
+ }
123
+ return params;
124
+ };
125
+ const appendQueryValue = (params, key, value) => {
126
+ if (value === undefined) {
127
+ return;
128
+ }
129
+ if (value === null) {
130
+ params.append(key, "null");
131
+ return;
132
+ }
133
+ params.append(key, String(value));
134
+ };
@@ -0,0 +1,3 @@
1
+ export { createRsqlClient } from "./client.js";
2
+ export { rsql } from "./default.js";
3
+ export type { ApiErrorBody, ChangelogEntry, ColumnDefinition, CreateRsqlClientOptions, ImportFile, IndexCreateRequest, ListMeta, ListResponse, MutateOptions, NamespaceConfig, NamespaceDefinition, NamespaceModule, NamespaceOverview, NamespaceRecord, NamespacesModule, QueryInput, QueryRequest, QueryStatement, QueryValue, OverviewLatency, OverviewWindowName, RowsModule, RsqlClient, RsqlResult, SSEEvent, SSESubscribeOptions, SSESubscription, StorageSnapshot, TableCreateRequest, TableExportOptions, TableModule, TablesModule, TableUpdateRequest, } from "./types.js";
package/dist/index.js ADDED
@@ -0,0 +1,2 @@
1
+ export { createRsqlClient } from "./client.js";
2
+ export { rsql } from "./default.js";
@@ -0,0 +1,3 @@
1
+ import type { HttpClient } from "../http.js";
2
+ import type { NamespaceModule } from "../types.js";
3
+ export declare const createNamespaceModule: (http: HttpClient, namespace: string) => NamespaceModule;
@@ -0,0 +1,131 @@
1
+ import { createTableModule } from "./table.js";
2
+ import { createSSESubscription } from "./sse.js";
3
+ const version = "v1";
4
+ const jsonHeaders = {
5
+ "Content-Type": "application/json",
6
+ };
7
+ export const createNamespaceModule = (http, namespace) => {
8
+ const encoded = encodeURIComponent(namespace);
9
+ const base = `/${version}/${encoded}`;
10
+ return {
11
+ name: namespace,
12
+ tables: {
13
+ list() {
14
+ return http.json(`${base}/tables`);
15
+ },
16
+ create(request) {
17
+ return http.json(`${base}/tables`, {
18
+ method: "POST",
19
+ headers: jsonHeaders,
20
+ body: JSON.stringify(request),
21
+ });
22
+ },
23
+ get(name) {
24
+ return http.json(`${base}/tables/${encodeURIComponent(name)}`);
25
+ },
26
+ update(name, request) {
27
+ return http.json(`${base}/tables/${encodeURIComponent(name)}`, {
28
+ method: "PUT",
29
+ headers: jsonHeaders,
30
+ body: JSON.stringify(request),
31
+ });
32
+ },
33
+ delete(name, meta) {
34
+ if (!meta) {
35
+ return http.empty(`${base}/tables/${encodeURIComponent(name)}`, {
36
+ method: "DELETE",
37
+ });
38
+ }
39
+ return http.empty(`${base}/tables/${encodeURIComponent(name)}`, {
40
+ method: "DELETE",
41
+ headers: jsonHeaders,
42
+ body: JSON.stringify({ _meta: meta }),
43
+ });
44
+ },
45
+ },
46
+ table(name) {
47
+ return createTableModule(http, namespace, name);
48
+ },
49
+ query: {
50
+ run(request) {
51
+ return http.json(`${base}/query`, {
52
+ method: "POST",
53
+ headers: jsonHeaders,
54
+ body: JSON.stringify(request),
55
+ });
56
+ },
57
+ batch(statements) {
58
+ return http.json(`${base}/query`, {
59
+ method: "POST",
60
+ headers: jsonHeaders,
61
+ body: JSON.stringify({ statements }),
62
+ });
63
+ },
64
+ },
65
+ changelog: {
66
+ list(options) {
67
+ const path = http.withQuery(`${base}/changelog`, {
68
+ table: options?.table,
69
+ limit: options?.limit,
70
+ offset: options?.offset,
71
+ });
72
+ return http.json(path);
73
+ },
74
+ },
75
+ overview: {
76
+ get(options) {
77
+ return http.json(http.withQuery(`${base}/overview`, { window: options?.window }));
78
+ },
79
+ },
80
+ events: {
81
+ async subscribe(options) {
82
+ const controller = new AbortController();
83
+ const externalSignal = options?.signal;
84
+ if (externalSignal) {
85
+ if (externalSignal.aborted) {
86
+ controller.abort(externalSignal.reason);
87
+ }
88
+ else {
89
+ externalSignal.addEventListener("abort", () => {
90
+ controller.abort(externalSignal.reason);
91
+ }, { once: true });
92
+ }
93
+ }
94
+ const path = http.withQuery(`${base}/subscribe`, {
95
+ tables: options?.tables?.join(","),
96
+ });
97
+ const res = await http.raw(path, {
98
+ method: "GET",
99
+ signal: controller.signal,
100
+ headers: {
101
+ Accept: "text/event-stream",
102
+ },
103
+ });
104
+ if (!res.ok) {
105
+ return res;
106
+ }
107
+ try {
108
+ const subscription = createSSESubscription(res.data, controller);
109
+ return {
110
+ ok: true,
111
+ data: subscription,
112
+ status: res.status,
113
+ headers: res.headers,
114
+ };
115
+ }
116
+ catch (err) {
117
+ controller.abort();
118
+ return {
119
+ ok: false,
120
+ error: {
121
+ error: "invalid_sse_stream",
122
+ message: err instanceof Error ? err.message : "SSE stream is not available",
123
+ },
124
+ status: 500,
125
+ headers: res.headers,
126
+ };
127
+ }
128
+ },
129
+ },
130
+ };
131
+ };
@@ -0,0 +1,3 @@
1
+ import type { HttpClient } from "../http.js";
2
+ import type { NamespacesModule } from "../types.js";
3
+ export declare const createNamespacesModule: (http: HttpClient) => NamespacesModule;
@@ -0,0 +1,90 @@
1
+ const version = "v1";
2
+ const jsonHeaders = {
3
+ "Content-Type": "application/json",
4
+ };
5
+ export const createNamespacesModule = (http) => {
6
+ return {
7
+ create(request) {
8
+ const payload = {
9
+ ...request,
10
+ config: withDefaultNamespaceConfig(request.config),
11
+ };
12
+ return http.json(`/${version}/namespaces`, {
13
+ method: "POST",
14
+ headers: jsonHeaders,
15
+ body: JSON.stringify(payload),
16
+ });
17
+ },
18
+ list() {
19
+ return http.json(`/${version}/namespaces`);
20
+ },
21
+ get(name) {
22
+ return http.json(`/${version}/namespaces/${encodeURIComponent(name)}`);
23
+ },
24
+ update(name, config) {
25
+ return http.json(`/${version}/namespaces/${encodeURIComponent(name)}`, {
26
+ method: "PUT",
27
+ headers: jsonHeaders,
28
+ body: JSON.stringify({ config }),
29
+ });
30
+ },
31
+ delete(name) {
32
+ return http.empty(`/${version}/namespaces/${encodeURIComponent(name)}`, {
33
+ method: "DELETE",
34
+ });
35
+ },
36
+ duplicate(source, target) {
37
+ return http.json(`/${version}/namespaces/${encodeURIComponent(source)}/duplicate`, {
38
+ method: "POST",
39
+ headers: jsonHeaders,
40
+ body: JSON.stringify({ name: target }),
41
+ });
42
+ },
43
+ exportDb(name) {
44
+ return http.raw(`/${version}/namespaces/${encodeURIComponent(name)}/export`);
45
+ },
46
+ importDb(name, file) {
47
+ const formData = new FormData();
48
+ formData.set("file", toBlob(file.content, file.contentType), file.filename || "namespace.db");
49
+ return http.json(`/${version}/namespaces/${encodeURIComponent(name)}/import`, {
50
+ method: "POST",
51
+ body: formData,
52
+ });
53
+ },
54
+ importCsv(name, table, file) {
55
+ const formData = new FormData();
56
+ formData.set("file", toBlob(file.content, file.contentType ?? "text/csv"), file.filename || "rows.csv");
57
+ return http.json(`/${version}/namespaces/${encodeURIComponent(name)}/import?table=${encodeURIComponent(table)}`, {
58
+ method: "POST",
59
+ body: formData,
60
+ });
61
+ },
62
+ };
63
+ };
64
+ const withDefaultNamespaceConfig = (config) => {
65
+ const fallback = {
66
+ journal_mode: "wal",
67
+ busy_timeout: 5000,
68
+ query_timeout: 10000,
69
+ foreign_keys: true,
70
+ read_only: false,
71
+ };
72
+ return {
73
+ ...fallback,
74
+ ...(config ?? {}),
75
+ };
76
+ };
77
+ const toBlob = (content, contentType) => {
78
+ if (content instanceof Blob) {
79
+ return content;
80
+ }
81
+ if (typeof content === "string") {
82
+ return new Blob([content], { type: contentType });
83
+ }
84
+ if (content instanceof Uint8Array) {
85
+ const copy = new Uint8Array(content.byteLength);
86
+ copy.set(content);
87
+ return new Blob([copy.buffer], { type: contentType });
88
+ }
89
+ return new Blob([new Uint8Array(content)], { type: contentType });
90
+ };
@@ -0,0 +1,2 @@
1
+ import type { SSESubscription } from "../types.js";
2
+ export declare const createSSESubscription: (response: Response, controller: AbortController) => SSESubscription;
@@ -0,0 +1,68 @@
1
+ export const createSSESubscription = (response, controller) => {
2
+ if (!response.body) {
3
+ throw new Error("SSE response body is not readable");
4
+ }
5
+ return {
6
+ response,
7
+ close: () => {
8
+ controller.abort();
9
+ },
10
+ stream: parseEventStream(response.body, controller.signal),
11
+ };
12
+ };
13
+ async function* parseEventStream(stream, signal) {
14
+ const reader = stream.getReader();
15
+ const decoder = new TextDecoder();
16
+ let buffer = "";
17
+ let eventName = "message";
18
+ let dataLines = [];
19
+ try {
20
+ while (!signal.aborted) {
21
+ const { value, done } = await reader.read();
22
+ if (done) {
23
+ break;
24
+ }
25
+ buffer += decoder.decode(value, { stream: true });
26
+ while (true) {
27
+ const idx = buffer.indexOf("\n");
28
+ if (idx === -1) {
29
+ break;
30
+ }
31
+ const rawLine = buffer.slice(0, idx);
32
+ buffer = buffer.slice(idx + 1);
33
+ const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine;
34
+ if (line === "") {
35
+ if (dataLines.length > 0) {
36
+ const rawData = dataLines.join("\n");
37
+ dataLines = [];
38
+ try {
39
+ const parsed = JSON.parse(rawData);
40
+ if (!parsed.action && eventName !== "message") {
41
+ parsed.action = eventName;
42
+ }
43
+ yield parsed;
44
+ }
45
+ catch {
46
+ // Ignore malformed event payloads.
47
+ }
48
+ }
49
+ eventName = "message";
50
+ continue;
51
+ }
52
+ if (line.startsWith(":")) {
53
+ continue;
54
+ }
55
+ if (line.startsWith("event:")) {
56
+ eventName = line.slice("event:".length).trim();
57
+ continue;
58
+ }
59
+ if (line.startsWith("data:")) {
60
+ dataLines.push(line.slice("data:".length).trim());
61
+ }
62
+ }
63
+ }
64
+ }
65
+ finally {
66
+ reader.releaseLock();
67
+ }
68
+ }
@@ -0,0 +1,3 @@
1
+ import type { HttpClient } from "../http.js";
2
+ import type { TableModule } from "../types.js";
3
+ export declare const createTableModule: <Row extends Record<string, unknown>>(http: HttpClient, namespace: string, table: string) => TableModule<Row>;
@@ -0,0 +1,160 @@
1
+ export const createTableModule = (http, namespace, table) => {
2
+ const ctx = {
3
+ http,
4
+ namespace: encodeURIComponent(namespace),
5
+ table: encodeURIComponent(table),
6
+ };
7
+ return {
8
+ rows: createRowsModule(ctx),
9
+ indexes: {
10
+ create(request) {
11
+ return ctx.http.json(`/${version}/${ctx.namespace}/tables/${ctx.table}/indexes`, {
12
+ method: "POST",
13
+ headers: jsonHeaders,
14
+ body: JSON.stringify(request),
15
+ });
16
+ },
17
+ delete(indexName, meta) {
18
+ return deleteWithOptionalMeta(ctx.http, `/${version}/${ctx.namespace}/tables/${ctx.table}/indexes/${encodeURIComponent(indexName)}`, meta);
19
+ },
20
+ },
21
+ schema: {
22
+ get() {
23
+ return ctx.http.json(`/${version}/${ctx.namespace}/tables/${ctx.table}`);
24
+ },
25
+ update(request) {
26
+ return ctx.http.json(`/${version}/${ctx.namespace}/tables/${ctx.table}`, {
27
+ method: "PUT",
28
+ headers: jsonHeaders,
29
+ body: JSON.stringify(request),
30
+ });
31
+ },
32
+ delete(meta) {
33
+ return deleteWithOptionalMeta(ctx.http, `/${version}/${ctx.namespace}/tables/${ctx.table}`, meta);
34
+ },
35
+ },
36
+ export(options) {
37
+ // The endpoint accepts the same filter grammar as rows.list. We add
38
+ // the required `format` (and optional `bom`) on top of the caller's
39
+ // own filters.
40
+ const query = { format: options.format };
41
+ if (options.bom) {
42
+ query.bom = "true";
43
+ }
44
+ const merged = mergeQuery(options.query, query);
45
+ const path = ctx.http.withQuery(`/${version}/${ctx.namespace}/tables/${ctx.table}/export`, merged);
46
+ return ctx.http.raw(path, { method: "GET" });
47
+ },
48
+ };
49
+ };
50
+ const mergeQuery = (base, extras) => {
51
+ const params = new URLSearchParams();
52
+ if (base instanceof URLSearchParams) {
53
+ for (const [k, v] of base.entries())
54
+ params.append(k, v);
55
+ }
56
+ else if (base) {
57
+ for (const [k, raw] of Object.entries(base)) {
58
+ const values = Array.isArray(raw) ? raw : [raw];
59
+ for (const v of values) {
60
+ if (v === undefined)
61
+ continue;
62
+ params.append(k, v === null ? "null" : String(v));
63
+ }
64
+ }
65
+ }
66
+ for (const [k, v] of Object.entries(extras)) {
67
+ if (v === undefined || v === null)
68
+ continue;
69
+ params.set(k, String(v));
70
+ }
71
+ return params;
72
+ };
73
+ const createRowsModule = (ctx) => {
74
+ const basePath = `/${version}/${ctx.namespace}/tables/${ctx.table}/rows`;
75
+ return {
76
+ list(query) {
77
+ return ctx.http.json(ctx.http.withQuery(basePath, query));
78
+ },
79
+ insert(rows, options) {
80
+ const body = Array.isArray(rows) ? { rows } : { ...rows };
81
+ if (options?.meta) {
82
+ body._meta = options.meta;
83
+ }
84
+ return ctx.http.json(basePath, {
85
+ method: "POST",
86
+ headers: withPrefer(options?.prefer),
87
+ body: JSON.stringify(body),
88
+ });
89
+ },
90
+ get(id) {
91
+ return ctx.http.json(`${basePath}/${id}`);
92
+ },
93
+ update(id, payload, options) {
94
+ return ctx.http.json(`${basePath}/${id}`, {
95
+ method: "PUT",
96
+ headers: withPrefer(options?.prefer),
97
+ body: JSON.stringify(withMeta(payload, options?.meta)),
98
+ });
99
+ },
100
+ delete(id, options) {
101
+ return deleteWithPreferAndMeta(ctx.http, `${basePath}/${id}`, options);
102
+ },
103
+ bulkUpdate(query, payload, options) {
104
+ return ctx.http.json(ctx.http.withQuery(basePath, query), {
105
+ method: "PATCH",
106
+ headers: withPrefer(options?.prefer),
107
+ body: JSON.stringify(withMeta(payload, options?.meta)),
108
+ });
109
+ },
110
+ bulkDelete(query, options) {
111
+ return deleteWithPreferAndMeta(ctx.http, ctx.http.withQuery(basePath, query), options);
112
+ },
113
+ };
114
+ };
115
+ const deleteWithOptionalMeta = (http, path, meta) => {
116
+ if (!meta) {
117
+ return http.empty(path, { method: "DELETE" });
118
+ }
119
+ return http.empty(path, {
120
+ method: "DELETE",
121
+ headers: jsonHeaders,
122
+ body: JSON.stringify({ _meta: meta }),
123
+ });
124
+ };
125
+ const deleteWithPreferAndMeta = (http, path, options) => {
126
+ const headers = withPrefer(options?.prefer);
127
+ const body = options?.meta ? JSON.stringify({ _meta: options.meta }) : undefined;
128
+ if (options?.prefer === "return=representation") {
129
+ return http.json(path, {
130
+ method: "DELETE",
131
+ headers,
132
+ body,
133
+ });
134
+ }
135
+ return http.json(path, {
136
+ method: "DELETE",
137
+ headers,
138
+ body,
139
+ });
140
+ };
141
+ const withMeta = (payload, meta) => {
142
+ if (!meta) {
143
+ return payload;
144
+ }
145
+ return {
146
+ ...payload,
147
+ _meta: meta,
148
+ };
149
+ };
150
+ const withPrefer = (prefer) => {
151
+ const headers = new Headers(jsonHeaders);
152
+ if (prefer) {
153
+ headers.set("Prefer", prefer);
154
+ }
155
+ return headers;
156
+ };
157
+ const version = "v1";
158
+ const jsonHeaders = {
159
+ "Content-Type": "application/json",
160
+ };
@@ -0,0 +1,345 @@
1
+ export interface ApiErrorBody {
2
+ error: string;
3
+ message: string;
4
+ }
5
+ export type RsqlResult<T> = {
6
+ ok: true;
7
+ data: T;
8
+ status: number;
9
+ headers: Headers;
10
+ } | {
11
+ ok: false;
12
+ error: ApiErrorBody;
13
+ status: number;
14
+ headers: Headers;
15
+ };
16
+ export interface CreateRsqlClientOptions {
17
+ url: string;
18
+ token: string;
19
+ fetch?: typeof fetch;
20
+ }
21
+ export interface NamespaceConfig {
22
+ journal_mode: string;
23
+ busy_timeout: number;
24
+ max_db_size?: number;
25
+ query_timeout: number;
26
+ /** Optional. Omit to apply the server default (true). Send false to
27
+ * explicitly disable foreign-key enforcement for the namespace. */
28
+ foreign_keys?: boolean;
29
+ read_only?: boolean;
30
+ }
31
+ export interface NamespaceDefinition {
32
+ name: string;
33
+ config?: NamespaceConfig;
34
+ }
35
+ export interface ColumnDefinition {
36
+ name: string;
37
+ type: string;
38
+ not_null?: boolean;
39
+ unique?: boolean;
40
+ default?: unknown;
41
+ index?: boolean;
42
+ pattern?: string;
43
+ max_length?: number;
44
+ min?: number;
45
+ max?: number;
46
+ auto?: boolean;
47
+ options?: string[];
48
+ formula?: string;
49
+ metadata?: Record<string, unknown>;
50
+ primary_key?: boolean;
51
+ read_only?: boolean;
52
+ }
53
+ export interface TableCreateRequest {
54
+ type: "table" | "view";
55
+ name: string;
56
+ metadata?: Record<string, unknown>;
57
+ columns?: ColumnDefinition[];
58
+ sql?: string;
59
+ /** Audit-meta passthrough. Wire key is `_meta` so it cannot collide with
60
+ * a user-defined column named `meta`. */
61
+ _meta?: Record<string, unknown>;
62
+ }
63
+ export interface TableUpdateRequest {
64
+ rename?: string;
65
+ add_columns?: ColumnDefinition[];
66
+ drop_columns?: string[];
67
+ rename_columns?: Record<string, string>;
68
+ sql?: string;
69
+ metadata?: Record<string, unknown>;
70
+ _meta?: Record<string, unknown>;
71
+ }
72
+ export interface IndexCreateRequest {
73
+ type: "index" | "unique" | "fts";
74
+ name?: string;
75
+ columns: string[];
76
+ _meta?: Record<string, unknown>;
77
+ }
78
+ export interface ListMeta {
79
+ total_count: number;
80
+ filter_count: number;
81
+ limit: number;
82
+ offset: number;
83
+ }
84
+ export interface ListResponse<T> {
85
+ data: T[];
86
+ meta: ListMeta;
87
+ }
88
+ export interface QueryStatement {
89
+ sql: string;
90
+ params?: unknown[];
91
+ }
92
+ export interface QueryRequest {
93
+ sql?: string;
94
+ params?: unknown[];
95
+ statements?: QueryStatement[];
96
+ }
97
+ export interface ChangelogEntry {
98
+ id: number;
99
+ timestamp: string;
100
+ action: string;
101
+ table: string;
102
+ detail: Record<string, unknown>;
103
+ _meta?: Record<string, unknown>;
104
+ }
105
+ export interface SSEEvent {
106
+ namespace?: string;
107
+ table: string;
108
+ action: string;
109
+ source_table?: string;
110
+ source_action?: string;
111
+ row?: Record<string, unknown>;
112
+ row_count?: number;
113
+ row_ids?: Array<string | number>;
114
+ detail?: Record<string, unknown>;
115
+ _meta?: Record<string, unknown>;
116
+ timestamp: string;
117
+ }
118
+ export interface SSESubscribeOptions {
119
+ tables?: string[];
120
+ signal?: AbortSignal;
121
+ }
122
+ export interface SSESubscription {
123
+ stream: AsyncIterable<SSEEvent>;
124
+ response: Response;
125
+ close: () => void;
126
+ }
127
+ export type QueryInput = URLSearchParams | Record<string, QueryValue | QueryValue[]>;
128
+ export type QueryValue = string | number | boolean | null | undefined;
129
+ export interface MutateOptions {
130
+ prefer?: string;
131
+ meta?: Record<string, unknown>;
132
+ }
133
+ export interface ImportFile {
134
+ filename: string;
135
+ content: Blob | ArrayBuffer | Uint8Array | string;
136
+ contentType?: string;
137
+ }
138
+ export interface NamespaceRecord {
139
+ name: string;
140
+ created_at?: string;
141
+ db_path?: string;
142
+ config?: NamespaceConfig;
143
+ last_activity_at?: string;
144
+ storage?: StorageSnapshot;
145
+ }
146
+ export type OverviewWindowName = "1h" | "24h" | "7d" | "30d";
147
+ export interface StorageSnapshot {
148
+ used_bytes: number;
149
+ allocated_bytes: number;
150
+ wal_bytes: number;
151
+ disk_bytes: number;
152
+ }
153
+ export interface OverviewLatency {
154
+ count: number;
155
+ average_ms: number;
156
+ p50_ms: number;
157
+ p95_ms: number;
158
+ p99_ms: number;
159
+ }
160
+ export interface NamespaceOverview {
161
+ generated_at: string;
162
+ window: {
163
+ name: OverviewWindowName;
164
+ from: string;
165
+ to: string;
166
+ resolution: "5m" | "1h";
167
+ };
168
+ health: {
169
+ status: "healthy" | "degraded";
170
+ mode: "read_write" | "read_only";
171
+ issues: string[];
172
+ };
173
+ storage: StorageSnapshot & {
174
+ reclaimable_bytes: number;
175
+ quota_bytes: number;
176
+ remaining_bytes: number | null;
177
+ usage_ratio: number | null;
178
+ database_file_bytes: number;
179
+ shm_bytes: number;
180
+ };
181
+ activity: {
182
+ requests: number;
183
+ reads: number;
184
+ writes: number;
185
+ errors: number;
186
+ error_rate: number;
187
+ response_bytes: number;
188
+ last_activity_at: string | null;
189
+ series: Array<{
190
+ timestamp: string;
191
+ reads: number;
192
+ writes: number;
193
+ errors: number;
194
+ }>;
195
+ };
196
+ read_latency: OverviewLatency;
197
+ write_latency: OverviewLatency;
198
+ write_contention: {
199
+ wait: OverviewLatency;
200
+ busy_errors: number;
201
+ timeouts: number;
202
+ quota_rejections: number;
203
+ };
204
+ schema: {
205
+ tables: number;
206
+ views: number;
207
+ indexes: number;
208
+ fts_indexes: number;
209
+ };
210
+ realtime: {
211
+ subscribers: number;
212
+ };
213
+ top_operations: Array<{
214
+ name: string;
215
+ count: number;
216
+ errors: number;
217
+ error_rate: number;
218
+ latency: OverviewLatency;
219
+ }>;
220
+ growth?: {
221
+ bytes_per_day: number;
222
+ sample_count: number;
223
+ projected_full_at: string | null;
224
+ };
225
+ }
226
+ export interface NamespacesModule {
227
+ create(request: NamespaceDefinition): Promise<RsqlResult<NamespaceRecord>>;
228
+ list(): Promise<RsqlResult<NamespaceRecord[]>>;
229
+ get(name: string): Promise<RsqlResult<NamespaceRecord>>;
230
+ update(name: string, config: NamespaceConfig): Promise<RsqlResult<NamespaceRecord>>;
231
+ delete(name: string): Promise<RsqlResult<void>>;
232
+ duplicate(source: string, target: string): Promise<RsqlResult<{
233
+ source: string;
234
+ target: string;
235
+ }>>;
236
+ /** Stream a consistent SQLite snapshot. The caller consumes response.body. */
237
+ exportDb(name: string): Promise<RsqlResult<Response>>;
238
+ importDb(name: string, file: ImportFile): Promise<RsqlResult<{
239
+ imported: boolean;
240
+ type: string;
241
+ }>>;
242
+ importCsv(name: string, table: string, file: ImportFile): Promise<RsqlResult<{
243
+ inserted: number;
244
+ }>>;
245
+ }
246
+ export interface TablesModule {
247
+ list(): Promise<RsqlResult<Array<Record<string, unknown>>>>;
248
+ create(request: TableCreateRequest): Promise<RsqlResult<{
249
+ created: string;
250
+ type: string;
251
+ }>>;
252
+ get(name: string): Promise<RsqlResult<Record<string, unknown>>>;
253
+ update(name: string, request: TableUpdateRequest): Promise<RsqlResult<{
254
+ updated: boolean;
255
+ }>>;
256
+ delete(name: string, meta?: Record<string, unknown>): Promise<RsqlResult<void>>;
257
+ }
258
+ export interface RowsModule<Row extends Record<string, unknown>> {
259
+ list(query?: QueryInput): Promise<RsqlResult<ListResponse<Row> | {
260
+ data: Row[];
261
+ }>>;
262
+ insert(rows: Partial<Row> | Array<Partial<Row>>, options?: MutateOptions): Promise<RsqlResult<{
263
+ data: Row[];
264
+ } | {
265
+ inserted: number;
266
+ }>>;
267
+ get(id: number): Promise<RsqlResult<Row>>;
268
+ update(id: number, payload: Record<string, unknown>, options?: MutateOptions): Promise<RsqlResult<{
269
+ data: Row[];
270
+ } | {
271
+ updated: number;
272
+ }>>;
273
+ delete(id: number, options?: MutateOptions): Promise<RsqlResult<{
274
+ data: Row[];
275
+ } | {
276
+ deleted: number;
277
+ } | void>>;
278
+ bulkUpdate(query: QueryInput, payload: Record<string, unknown>, options?: MutateOptions): Promise<RsqlResult<{
279
+ data: Row[];
280
+ } | {
281
+ updated: number;
282
+ }>>;
283
+ bulkDelete(query: QueryInput, options?: MutateOptions): Promise<RsqlResult<{
284
+ data: Row[];
285
+ } | {
286
+ deleted: number;
287
+ } | void>>;
288
+ }
289
+ export interface TableExportOptions {
290
+ format: "csv";
291
+ /** Filter / select / order / search params, identical to rows.list. */
292
+ query?: QueryInput;
293
+ /** Prepend the UTF-8 BOM (0xEF 0xBB 0xBF) for Excel compatibility. */
294
+ bom?: boolean;
295
+ }
296
+ export interface TableModule<Row extends Record<string, unknown>> {
297
+ rows: RowsModule<Row>;
298
+ indexes: {
299
+ create(request: IndexCreateRequest): Promise<RsqlResult<{
300
+ created: boolean;
301
+ }>>;
302
+ delete(indexName: string, meta?: Record<string, unknown>): Promise<RsqlResult<void>>;
303
+ };
304
+ schema: {
305
+ get(): Promise<RsqlResult<Record<string, unknown>>>;
306
+ update(request: TableUpdateRequest): Promise<RsqlResult<{
307
+ updated: boolean;
308
+ }>>;
309
+ delete(meta?: Record<string, unknown>): Promise<RsqlResult<void>>;
310
+ };
311
+ /**
312
+ * Stream the table contents in the requested format. Returns the underlying
313
+ * Response so callers can decide how to consume the body (text(), blob(),
314
+ * getReader(), pipe to disk).
315
+ */
316
+ export(options: TableExportOptions): Promise<RsqlResult<Response>>;
317
+ }
318
+ export interface NamespaceModule {
319
+ readonly name: string;
320
+ tables: TablesModule;
321
+ table<Row extends Record<string, unknown> = Record<string, unknown>>(name: string): TableModule<Row>;
322
+ query: {
323
+ run(request: QueryRequest): Promise<RsqlResult<Record<string, unknown>>>;
324
+ batch(statements: QueryStatement[]): Promise<RsqlResult<Record<string, unknown>>>;
325
+ };
326
+ changelog: {
327
+ list(options?: {
328
+ table?: string;
329
+ limit?: number;
330
+ offset?: number;
331
+ }): Promise<RsqlResult<ChangelogEntry[]>>;
332
+ };
333
+ overview: {
334
+ get(options?: {
335
+ window?: OverviewWindowName;
336
+ }): Promise<RsqlResult<NamespaceOverview>>;
337
+ };
338
+ events: {
339
+ subscribe(options?: SSESubscribeOptions): Promise<RsqlResult<SSESubscription>>;
340
+ };
341
+ }
342
+ export interface RsqlClient {
343
+ namespaces: NamespacesModule;
344
+ ns(name: string): NamespaceModule;
345
+ }
package/dist/types.js ADDED
@@ -0,0 +1 @@
1
+ export {};
package/package.json ADDED
@@ -0,0 +1,54 @@
1
+ {
2
+ "name": "@k2b/rsql",
3
+ "version": "0.4.0",
4
+ "description": "Type-safe fetch client for rsql",
5
+ "type": "module",
6
+ "license": "MIT",
7
+ "sideEffects": false,
8
+ "main": "./dist/index.js",
9
+ "module": "./dist/index.js",
10
+ "types": "./dist/index.d.ts",
11
+ "files": [
12
+ "dist",
13
+ "LICENSE",
14
+ "README.md",
15
+ "package.json"
16
+ ],
17
+ "exports": {
18
+ ".": {
19
+ "types": "./dist/index.d.ts",
20
+ "import": "./dist/index.js",
21
+ "default": "./dist/index.js"
22
+ }
23
+ },
24
+ "repository": {
25
+ "type": "git",
26
+ "url": "git+https://github.com/k2b-dev/rsql.git",
27
+ "directory": "client"
28
+ },
29
+ "homepage": "https://github.com/k2b-dev/rsql#readme",
30
+ "bugs": {
31
+ "url": "https://github.com/k2b-dev/rsql/issues"
32
+ },
33
+ "publishConfig": {
34
+ "access": "public",
35
+ "registry": "https://registry.npmjs.org/"
36
+ },
37
+ "scripts": {
38
+ "build": "rm -rf dist && bunx tsc -p tsconfig.build.json",
39
+ "typecheck": "bunx tsc --noEmit",
40
+ "test": "bun test ./tests",
41
+ "test:package": "./scripts/test-package.sh",
42
+ "test:e2e": "bun test ./tests/e2e.test.ts",
43
+ "test:perf": "bun test ./tests/performance.test.ts",
44
+ "perf:fast": "bun run ./perf/cli.ts fast",
45
+ "perf:deep": "bun run ./perf/cli.ts deep",
46
+ "perf:report": "bun run ./perf/cli.ts report",
47
+ "prepack": "bun run build"
48
+ },
49
+ "devDependencies": {
50
+ "@types/bun": "1.3.14",
51
+ "typescript": "5.9.3"
52
+ },
53
+ "packageManager": "bun@1.3.14"
54
+ }