aroraql-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.
- package/README.md +189 -0
- package/dist/index.d.ts +97 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +150 -0
- package/dist/index.js.map +1 -0
- package/package.json +36 -0
- package/src/index.ts +209 -0
package/README.md
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
1
|
+
# aroraql-client
|
|
2
|
+
|
|
3
|
+
Supabase-inspired, **type-safe fluent query builder** for the frontend. Chain calls, get a JSON payload, and let the client POST it to your AroraQL endpoint — results come back mapped to _your_ types.
|
|
4
|
+
|
|
5
|
+
```ts
|
|
6
|
+
const employees = await arora
|
|
7
|
+
.from<Employee>("Employees")
|
|
8
|
+
.select("Id", "Name", "Department.Name")
|
|
9
|
+
.where("Age")
|
|
10
|
+
.gt(18)
|
|
11
|
+
.where("Country")
|
|
12
|
+
.eq("Egypt")
|
|
13
|
+
.orderBy("Name")
|
|
14
|
+
.asc()
|
|
15
|
+
.take(50)
|
|
16
|
+
.many(); // Employee[]
|
|
17
|
+
```
|
|
18
|
+
|
|
19
|
+
- **Zero dependencies** — just `fetch`
|
|
20
|
+
- **Fully generic** — field names and value types checked against your row type
|
|
21
|
+
- **Runs anywhere** — Bun, Node 18+, and browsers
|
|
22
|
+
|
|
23
|
+
## Install
|
|
24
|
+
|
|
25
|
+
```sh
|
|
26
|
+
bun add aroraql-client
|
|
27
|
+
# or
|
|
28
|
+
npm install aroraql-client
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
## Setup
|
|
32
|
+
|
|
33
|
+
The client auto-detects the endpoint from the `AroraQL_Api` environment variable:
|
|
34
|
+
|
|
35
|
+
```sh
|
|
36
|
+
# .env
|
|
37
|
+
AroraQL_Api=https://api.example.com/query
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
```ts
|
|
41
|
+
import { createClient } from "aroraql-client";
|
|
42
|
+
|
|
43
|
+
const arora = createClient();
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or configure it explicitly (required in browsers, where env variables don't exist):
|
|
47
|
+
|
|
48
|
+
```ts
|
|
49
|
+
const arora = createClient({
|
|
50
|
+
url: "https://api.example.com/query",
|
|
51
|
+
headers: { authorization: `Bearer ${token}` }, // optional, sent on every request
|
|
52
|
+
});
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
## Usage
|
|
56
|
+
|
|
57
|
+
Define your row type once and every part of the query is checked against it:
|
|
58
|
+
|
|
59
|
+
```ts
|
|
60
|
+
type Employee = {
|
|
61
|
+
Id: number;
|
|
62
|
+
Name: string;
|
|
63
|
+
Age: number;
|
|
64
|
+
Country: string;
|
|
65
|
+
Department: { Name: string };
|
|
66
|
+
};
|
|
67
|
+
```
|
|
68
|
+
|
|
69
|
+
### Fetch many rows
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const rows = await arora
|
|
73
|
+
.from<Employee>("Employees")
|
|
74
|
+
.where("Age")
|
|
75
|
+
.gte(18)
|
|
76
|
+
.orderBy("Name")
|
|
77
|
+
.asc()
|
|
78
|
+
.many(); // Employee[]
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
### Fetch a single row
|
|
82
|
+
|
|
83
|
+
```ts
|
|
84
|
+
// First match or null — never throws on "not found"
|
|
85
|
+
const employee = await arora
|
|
86
|
+
.from<Employee>("Employees")
|
|
87
|
+
.where("Id")
|
|
88
|
+
.eq(42)
|
|
89
|
+
.maybeSingle(); // Employee | null
|
|
90
|
+
|
|
91
|
+
// Exactly one row — throws unless precisely 1 match
|
|
92
|
+
const employee = await arora
|
|
93
|
+
.from<Employee>("Employees")
|
|
94
|
+
.where("Id")
|
|
95
|
+
.eq(42)
|
|
96
|
+
.single(); // Employee
|
|
97
|
+
```
|
|
98
|
+
|
|
99
|
+
### Build without fetching
|
|
100
|
+
|
|
101
|
+
```ts
|
|
102
|
+
const payload = arora
|
|
103
|
+
.from<Employee>("Employees")
|
|
104
|
+
.select("Id", "Name")
|
|
105
|
+
.where("Country")
|
|
106
|
+
.eq("Egypt")
|
|
107
|
+
.build();
|
|
108
|
+
// { from: "Employees", select: ["Id", "Name"],
|
|
109
|
+
// where: [{ field: "Country", op: "=", value: "Egypt" }], orderBy: [] }
|
|
110
|
+
```
|
|
111
|
+
|
|
112
|
+
## API
|
|
113
|
+
|
|
114
|
+
### Query builder
|
|
115
|
+
|
|
116
|
+
| Method | Description |
|
|
117
|
+
| ----------------------------------- | -------------------------------------------------------------------------------- |
|
|
118
|
+
| `.from<T>(table)` | Start a query. `T` types everything downstream. |
|
|
119
|
+
| `.select(...fields)` | Project fields. Supports dotted paths (`"Department.Name"`). Empty = all fields. |
|
|
120
|
+
| `.where(field)` | Start a condition — follow with an operator (below). |
|
|
121
|
+
| `.orderBy(field).asc()` / `.desc()` | Sort. Chain multiple for multi-field ordering. |
|
|
122
|
+
| `.take(n)` | Limit the number of rows. |
|
|
123
|
+
|
|
124
|
+
### Where operators
|
|
125
|
+
|
|
126
|
+
| Method | SQL equivalent |
|
|
127
|
+
| ---------------------------- | ------------------------------------ |
|
|
128
|
+
| `.eq(value)` | `=` |
|
|
129
|
+
| `.ne(value)` | `!=` |
|
|
130
|
+
| `.gt(value)` / `.gte(value)` | `>` / `>=` |
|
|
131
|
+
| `.lt(value)` / `.lte(value)` | `<` / `<=` |
|
|
132
|
+
| `.like(pattern)` | `LIKE` (`%` any chars, `_` one char) |
|
|
133
|
+
|
|
134
|
+
Operator values are typed as `T[K]` — `where("Age").eq("old")` is a compile error.
|
|
135
|
+
|
|
136
|
+
### Executors
|
|
137
|
+
|
|
138
|
+
| Method | Returns | Behavior |
|
|
139
|
+
| ---------------- | -------------------- | --------------------------------------------------- |
|
|
140
|
+
| `.many()` | `Promise<T[]>` | All matching rows. |
|
|
141
|
+
| `.maybeSingle()` | `Promise<T \| null>` | First row or `null`. |
|
|
142
|
+
| `.single()` | `Promise<T>` | Exactly one row — throws `AroraQLError` on 0 or 2+. |
|
|
143
|
+
| `.build()` | `QueryPayload` | The raw JSON payload, no request made. |
|
|
144
|
+
|
|
145
|
+
## Backend contract
|
|
146
|
+
|
|
147
|
+
The client sends a single `POST` with a JSON body:
|
|
148
|
+
|
|
149
|
+
```json
|
|
150
|
+
{
|
|
151
|
+
"from": "Employees",
|
|
152
|
+
"select": ["Id", "Name", "Department.Name"],
|
|
153
|
+
"where": [{ "field": "Age", "op": ">", "value": 18 }],
|
|
154
|
+
"orderBy": [{ "field": "Name", "dir": "asc" }],
|
|
155
|
+
"take": 50
|
|
156
|
+
}
|
|
157
|
+
```
|
|
158
|
+
|
|
159
|
+
Your endpoint maps this onto any data source and responds with:
|
|
160
|
+
|
|
161
|
+
```json
|
|
162
|
+
{ "data": [ ... ] }
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
or, on failure (any status):
|
|
166
|
+
|
|
167
|
+
```json
|
|
168
|
+
{ "error": "Unknown table: Employes" }
|
|
169
|
+
```
|
|
170
|
+
|
|
171
|
+
Errors are surfaced as `AroraQLError` (with `.status`). A minimal Bun endpoint:
|
|
172
|
+
|
|
173
|
+
```ts
|
|
174
|
+
import type { QueryPayload } from "aroraql-client";
|
|
175
|
+
|
|
176
|
+
Bun.serve({
|
|
177
|
+
port: 3123,
|
|
178
|
+
async fetch(req) {
|
|
179
|
+
const payload = (await req.json()) as QueryPayload;
|
|
180
|
+
return Response.json({ data: runQuery(payload) }); // map payload -> your DB
|
|
181
|
+
},
|
|
182
|
+
});
|
|
183
|
+
```
|
|
184
|
+
|
|
185
|
+
All payload types (`QueryPayload`, `WhereCondition`, `OrderByClause`, `Operator`) are exported so your backend can share them.
|
|
186
|
+
|
|
187
|
+
## License
|
|
188
|
+
|
|
189
|
+
MIT
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
export type Operator = "=" | "!=" | ">" | ">=" | "<" | "<=" | "like";
|
|
2
|
+
export type SortDir = "asc" | "desc";
|
|
3
|
+
export interface WhereCondition {
|
|
4
|
+
field: string;
|
|
5
|
+
op: Operator;
|
|
6
|
+
value: unknown;
|
|
7
|
+
}
|
|
8
|
+
export interface OrderByClause {
|
|
9
|
+
field: string;
|
|
10
|
+
dir: SortDir;
|
|
11
|
+
}
|
|
12
|
+
/** The JSON shape sent to the backend. */
|
|
13
|
+
export interface QueryPayload {
|
|
14
|
+
from: string;
|
|
15
|
+
select: string[];
|
|
16
|
+
where: WhereCondition[];
|
|
17
|
+
orderBy: OrderByClause[];
|
|
18
|
+
take?: number;
|
|
19
|
+
}
|
|
20
|
+
export interface AroraQLConfig {
|
|
21
|
+
/** Endpoint url. Falls back to the `AroraQL_Api` env variable. */
|
|
22
|
+
url?: string;
|
|
23
|
+
/** Extra headers (e.g. auth) sent with every request. */
|
|
24
|
+
headers?: Record<string, string>;
|
|
25
|
+
/** Custom fetch implementation (useful for tests). */
|
|
26
|
+
fetch?: typeof globalThis.fetch;
|
|
27
|
+
}
|
|
28
|
+
export declare class AroraQLError extends Error {
|
|
29
|
+
readonly status?: number | undefined;
|
|
30
|
+
constructor(message: string, status?: number | undefined);
|
|
31
|
+
}
|
|
32
|
+
type Row = Record<string, unknown>;
|
|
33
|
+
/** keyof T with autocomplete, but still allows dotted paths like "Department.Name". */
|
|
34
|
+
type Field<T> = Extract<keyof T, string> | (string & {});
|
|
35
|
+
/** Value type for a field: exact type when K is a key of T, unknown for dotted paths. */
|
|
36
|
+
type FieldValue<T, K> = K extends keyof T ? T[K] : unknown;
|
|
37
|
+
/**
|
|
38
|
+
* Create a client. Reads the endpoint from `AroraQL_Api` when no url is given.
|
|
39
|
+
*
|
|
40
|
+
* ```ts
|
|
41
|
+
* const arora = createClient(); // uses process.env.AroraQL_Api
|
|
42
|
+
* const rows = await arora.from<Employee>("Employees").where("Age").gt(18).many();
|
|
43
|
+
* ```
|
|
44
|
+
*/
|
|
45
|
+
export declare function createClient(config?: AroraQLConfig): AroraQLClient;
|
|
46
|
+
export declare class AroraQLClient {
|
|
47
|
+
readonly url: string;
|
|
48
|
+
private readonly config;
|
|
49
|
+
constructor(url: string, config?: AroraQLConfig);
|
|
50
|
+
/** Start a query against a table. Pass your row type for full type safety. */
|
|
51
|
+
from<T extends Row = Row>(table: string): QueryBuilder<T>;
|
|
52
|
+
/** @internal Sends the payload and unwraps `{ data, error }`. */
|
|
53
|
+
execute<T>(payload: QueryPayload): Promise<T[]>;
|
|
54
|
+
}
|
|
55
|
+
export declare class QueryBuilder<T extends Row> {
|
|
56
|
+
#private;
|
|
57
|
+
private readonly client;
|
|
58
|
+
constructor(client: AroraQLClient, table: string);
|
|
59
|
+
select(...fields: Field<T>[]): this;
|
|
60
|
+
where<K extends Field<T>>(field: K): WhereBuilder<T, K>;
|
|
61
|
+
orderBy(field: Field<T>): OrderByBuilder<T>;
|
|
62
|
+
take(count: number): this;
|
|
63
|
+
/** @internal */
|
|
64
|
+
_addWhere(condition: WhereCondition): this;
|
|
65
|
+
/** @internal */
|
|
66
|
+
_addOrderBy(order: OrderByClause): this;
|
|
67
|
+
/** The raw JSON payload (no request is made). */
|
|
68
|
+
build(): QueryPayload;
|
|
69
|
+
/** Execute and return all matching rows. */
|
|
70
|
+
many(): Promise<T[]>;
|
|
71
|
+
/** Execute and return the first row, or null when nothing matches. */
|
|
72
|
+
maybeSingle(): Promise<T | null>;
|
|
73
|
+
/** Execute and return exactly one row — throws on 0 or 2+ matches. */
|
|
74
|
+
single(): Promise<T>;
|
|
75
|
+
}
|
|
76
|
+
export declare class WhereBuilder<T extends Row, K extends Field<T>> {
|
|
77
|
+
#private;
|
|
78
|
+
private readonly query;
|
|
79
|
+
private readonly field;
|
|
80
|
+
constructor(query: QueryBuilder<T>, field: K);
|
|
81
|
+
eq(value: FieldValue<T, K>): QueryBuilder<T>;
|
|
82
|
+
ne(value: FieldValue<T, K>): QueryBuilder<T>;
|
|
83
|
+
gt(value: FieldValue<T, K>): QueryBuilder<T>;
|
|
84
|
+
gte(value: FieldValue<T, K>): QueryBuilder<T>;
|
|
85
|
+
lt(value: FieldValue<T, K>): QueryBuilder<T>;
|
|
86
|
+
lte(value: FieldValue<T, K>): QueryBuilder<T>;
|
|
87
|
+
like(value: string): QueryBuilder<T>;
|
|
88
|
+
}
|
|
89
|
+
export declare class OrderByBuilder<T extends Row> {
|
|
90
|
+
private readonly query;
|
|
91
|
+
private readonly field;
|
|
92
|
+
constructor(query: QueryBuilder<T>, field: Field<T>);
|
|
93
|
+
asc(): QueryBuilder<T>;
|
|
94
|
+
desc(): QueryBuilder<T>;
|
|
95
|
+
}
|
|
96
|
+
export {};
|
|
97
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
// AroraQL Client — a tiny, supabase-inspired fluent query client.
|
|
2
|
+
// Builds a JSON query payload and POSTs it to your AroraQL endpoint.
|
|
3
|
+
export class AroraQLError extends Error {
|
|
4
|
+
status;
|
|
5
|
+
constructor(message, status) {
|
|
6
|
+
super(message);
|
|
7
|
+
this.status = status;
|
|
8
|
+
this.name = "AroraQLError";
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
/** Safe env lookup — works in Bun/Node and is a no-op in browsers (pass `url` there). */
|
|
12
|
+
function envVar(name) {
|
|
13
|
+
const env = globalThis
|
|
14
|
+
.process?.env;
|
|
15
|
+
return env?.[name];
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Create a client. Reads the endpoint from `AroraQL_Api` when no url is given.
|
|
19
|
+
*
|
|
20
|
+
* ```ts
|
|
21
|
+
* const arora = createClient(); // uses process.env.AroraQL_Api
|
|
22
|
+
* const rows = await arora.from<Employee>("Employees").where("Age").gt(18).many();
|
|
23
|
+
* ```
|
|
24
|
+
*/
|
|
25
|
+
export function createClient(config = {}) {
|
|
26
|
+
const url = config.url ??
|
|
27
|
+
envVar("AroraQL_Api") ??
|
|
28
|
+
envVar("ARORAQL_API");
|
|
29
|
+
if (!url) {
|
|
30
|
+
throw new AroraQLError("AroraQL: missing API url. Set the `AroraQL_Api` env variable or pass `createClient({ url })`.");
|
|
31
|
+
}
|
|
32
|
+
return new AroraQLClient(url, config);
|
|
33
|
+
}
|
|
34
|
+
export class AroraQLClient {
|
|
35
|
+
url;
|
|
36
|
+
config;
|
|
37
|
+
constructor(url, config = {}) {
|
|
38
|
+
this.url = url;
|
|
39
|
+
this.config = config;
|
|
40
|
+
}
|
|
41
|
+
/** Start a query against a table. Pass your row type for full type safety. */
|
|
42
|
+
from(table) {
|
|
43
|
+
return new QueryBuilder(this, table);
|
|
44
|
+
}
|
|
45
|
+
/** @internal Sends the payload and unwraps `{ data, error }`. */
|
|
46
|
+
async execute(payload) {
|
|
47
|
+
const doFetch = this.config.fetch ?? globalThis.fetch;
|
|
48
|
+
const res = await doFetch(this.url, {
|
|
49
|
+
method: "POST",
|
|
50
|
+
headers: { "content-type": "application/json", ...this.config.headers },
|
|
51
|
+
body: JSON.stringify(payload),
|
|
52
|
+
});
|
|
53
|
+
const body = (await res.json().catch(() => null));
|
|
54
|
+
if (!res.ok || !body || body.error) {
|
|
55
|
+
throw new AroraQLError(body?.error ?? `AroraQL: request failed with status ${res.status}`, res.status);
|
|
56
|
+
}
|
|
57
|
+
return body.data ?? [];
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
export class QueryBuilder {
|
|
61
|
+
client;
|
|
62
|
+
#query;
|
|
63
|
+
constructor(client, table) {
|
|
64
|
+
this.client = client;
|
|
65
|
+
this.#query = { from: table, select: [], where: [], orderBy: [] };
|
|
66
|
+
}
|
|
67
|
+
select(...fields) {
|
|
68
|
+
this.#query.select.push(...fields);
|
|
69
|
+
return this;
|
|
70
|
+
}
|
|
71
|
+
where(field) {
|
|
72
|
+
return new WhereBuilder(this, field);
|
|
73
|
+
}
|
|
74
|
+
orderBy(field) {
|
|
75
|
+
return new OrderByBuilder(this, field);
|
|
76
|
+
}
|
|
77
|
+
take(count) {
|
|
78
|
+
this.#query.take = count;
|
|
79
|
+
return this;
|
|
80
|
+
}
|
|
81
|
+
/** @internal */
|
|
82
|
+
_addWhere(condition) {
|
|
83
|
+
this.#query.where.push(condition);
|
|
84
|
+
return this;
|
|
85
|
+
}
|
|
86
|
+
/** @internal */
|
|
87
|
+
_addOrderBy(order) {
|
|
88
|
+
this.#query.orderBy.push(order);
|
|
89
|
+
return this;
|
|
90
|
+
}
|
|
91
|
+
/** The raw JSON payload (no request is made). */
|
|
92
|
+
build() {
|
|
93
|
+
return structuredClone(this.#query);
|
|
94
|
+
}
|
|
95
|
+
/** Execute and return all matching rows. */
|
|
96
|
+
async many() {
|
|
97
|
+
return this.client.execute(this.build());
|
|
98
|
+
}
|
|
99
|
+
/** Execute and return the first row, or null when nothing matches. */
|
|
100
|
+
async maybeSingle() {
|
|
101
|
+
const payload = this.build();
|
|
102
|
+
payload.take = 1;
|
|
103
|
+
const rows = await this.client.execute(payload);
|
|
104
|
+
return rows[0] ?? null;
|
|
105
|
+
}
|
|
106
|
+
/** Execute and return exactly one row — throws on 0 or 2+ matches. */
|
|
107
|
+
async single() {
|
|
108
|
+
const payload = this.build();
|
|
109
|
+
payload.take = 2;
|
|
110
|
+
const rows = await this.client.execute(payload);
|
|
111
|
+
if (rows.length === 0)
|
|
112
|
+
throw new AroraQLError("AroraQL: single() found no rows");
|
|
113
|
+
if (rows.length > 1)
|
|
114
|
+
throw new AroraQLError("AroraQL: single() found more than one row");
|
|
115
|
+
return rows[0];
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
export class WhereBuilder {
|
|
119
|
+
query;
|
|
120
|
+
field;
|
|
121
|
+
constructor(query, field) {
|
|
122
|
+
this.query = query;
|
|
123
|
+
this.field = field;
|
|
124
|
+
}
|
|
125
|
+
#add(op, value) {
|
|
126
|
+
return this.query._addWhere({ field: this.field, op, value });
|
|
127
|
+
}
|
|
128
|
+
eq(value) { return this.#add("=", value); }
|
|
129
|
+
ne(value) { return this.#add("!=", value); }
|
|
130
|
+
gt(value) { return this.#add(">", value); }
|
|
131
|
+
gte(value) { return this.#add(">=", value); }
|
|
132
|
+
lt(value) { return this.#add("<", value); }
|
|
133
|
+
lte(value) { return this.#add("<=", value); }
|
|
134
|
+
like(value) { return this.#add("like", value); }
|
|
135
|
+
}
|
|
136
|
+
export class OrderByBuilder {
|
|
137
|
+
query;
|
|
138
|
+
field;
|
|
139
|
+
constructor(query, field) {
|
|
140
|
+
this.query = query;
|
|
141
|
+
this.field = field;
|
|
142
|
+
}
|
|
143
|
+
asc() {
|
|
144
|
+
return this.query._addOrderBy({ field: this.field, dir: "asc" });
|
|
145
|
+
}
|
|
146
|
+
desc() {
|
|
147
|
+
return this.query._addOrderBy({ field: this.field, dir: "desc" });
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"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"}
|
package/package.json
ADDED
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "aroraql-client",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Supabase-inspired, type-safe fluent query builder that turns chained calls into a JSON payload and POSTs it to your AroraQL endpoint",
|
|
5
|
+
"type": "module",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"module": "./dist/index.js",
|
|
8
|
+
"types": "./dist/index.d.ts",
|
|
9
|
+
"exports": {
|
|
10
|
+
".": {
|
|
11
|
+
"types": "./dist/index.d.ts",
|
|
12
|
+
"bun": "./src/index.ts",
|
|
13
|
+
"import": "./dist/index.js"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"files": ["dist", "src"],
|
|
17
|
+
"sideEffects": false,
|
|
18
|
+
"scripts": {
|
|
19
|
+
"build": "tsc -p tsconfig.json",
|
|
20
|
+
"prepublishOnly": "npm run build"
|
|
21
|
+
},
|
|
22
|
+
"keywords": [
|
|
23
|
+
"fluent",
|
|
24
|
+
"query-builder",
|
|
25
|
+
"json",
|
|
26
|
+
"typescript",
|
|
27
|
+
"type-safe",
|
|
28
|
+
"supabase",
|
|
29
|
+
"rest",
|
|
30
|
+
"aroraql"
|
|
31
|
+
],
|
|
32
|
+
"engines": {
|
|
33
|
+
"node": ">=18"
|
|
34
|
+
},
|
|
35
|
+
"license": "MIT"
|
|
36
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,209 @@
|
|
|
1
|
+
// AroraQL Client — a tiny, supabase-inspired fluent query client.
|
|
2
|
+
// Builds a JSON query payload and POSTs it to your AroraQL endpoint.
|
|
3
|
+
|
|
4
|
+
export type Operator = "=" | "!=" | ">" | ">=" | "<" | "<=" | "like";
|
|
5
|
+
export type SortDir = "asc" | "desc";
|
|
6
|
+
|
|
7
|
+
export interface WhereCondition {
|
|
8
|
+
field: string;
|
|
9
|
+
op: Operator;
|
|
10
|
+
value: unknown;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export interface OrderByClause {
|
|
14
|
+
field: string;
|
|
15
|
+
dir: SortDir;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
/** The JSON shape sent to the backend. */
|
|
19
|
+
export interface QueryPayload {
|
|
20
|
+
from: string;
|
|
21
|
+
select: string[];
|
|
22
|
+
where: WhereCondition[];
|
|
23
|
+
orderBy: OrderByClause[];
|
|
24
|
+
take?: number;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface AroraQLConfig {
|
|
28
|
+
/** Endpoint url. Falls back to the `AroraQL_Api` env variable. */
|
|
29
|
+
url?: string;
|
|
30
|
+
/** Extra headers (e.g. auth) sent with every request. */
|
|
31
|
+
headers?: Record<string, string>;
|
|
32
|
+
/** Custom fetch implementation (useful for tests). */
|
|
33
|
+
fetch?: typeof globalThis.fetch;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export class AroraQLError extends Error {
|
|
37
|
+
constructor(message: string, readonly status?: number) {
|
|
38
|
+
super(message);
|
|
39
|
+
this.name = "AroraQLError";
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
type Row = Record<string, unknown>;
|
|
44
|
+
/** keyof T with autocomplete, but still allows dotted paths like "Department.Name". */
|
|
45
|
+
type Field<T> = Extract<keyof T, string> | (string & {});
|
|
46
|
+
/** Value type for a field: exact type when K is a key of T, unknown for dotted paths. */
|
|
47
|
+
type FieldValue<T, K> = K extends keyof T ? T[K] : unknown;
|
|
48
|
+
|
|
49
|
+
/** Safe env lookup — works in Bun/Node and is a no-op in browsers (pass `url` there). */
|
|
50
|
+
function envVar(name: string): string | undefined {
|
|
51
|
+
const env = (globalThis as { process?: { env?: Record<string, string | undefined> } })
|
|
52
|
+
.process?.env;
|
|
53
|
+
return env?.[name];
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* Create a client. Reads the endpoint from `AroraQL_Api` when no url is given.
|
|
58
|
+
*
|
|
59
|
+
* ```ts
|
|
60
|
+
* const arora = createClient(); // uses process.env.AroraQL_Api
|
|
61
|
+
* const rows = await arora.from<Employee>("Employees").where("Age").gt(18).many();
|
|
62
|
+
* ```
|
|
63
|
+
*/
|
|
64
|
+
export function createClient(config: AroraQLConfig = {}): AroraQLClient {
|
|
65
|
+
const url =
|
|
66
|
+
config.url ??
|
|
67
|
+
envVar("AroraQL_Api") ??
|
|
68
|
+
envVar("ARORAQL_API");
|
|
69
|
+
if (!url) {
|
|
70
|
+
throw new AroraQLError(
|
|
71
|
+
"AroraQL: missing API url. Set the `AroraQL_Api` env variable or pass `createClient({ url })`."
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
return new AroraQLClient(url, config);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export class AroraQLClient {
|
|
78
|
+
constructor(
|
|
79
|
+
readonly url: string,
|
|
80
|
+
private readonly config: AroraQLConfig = {}
|
|
81
|
+
) {}
|
|
82
|
+
|
|
83
|
+
/** Start a query against a table. Pass your row type for full type safety. */
|
|
84
|
+
from<T extends Row = Row>(table: string): QueryBuilder<T> {
|
|
85
|
+
return new QueryBuilder<T>(this, table);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** @internal Sends the payload and unwraps `{ data, error }`. */
|
|
89
|
+
async execute<T>(payload: QueryPayload): Promise<T[]> {
|
|
90
|
+
const doFetch = this.config.fetch ?? globalThis.fetch;
|
|
91
|
+
const res = await doFetch(this.url, {
|
|
92
|
+
method: "POST",
|
|
93
|
+
headers: { "content-type": "application/json", ...this.config.headers },
|
|
94
|
+
body: JSON.stringify(payload),
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const body = (await res.json().catch(() => null)) as
|
|
98
|
+
| { data?: T[]; error?: string }
|
|
99
|
+
| null;
|
|
100
|
+
|
|
101
|
+
if (!res.ok || !body || body.error) {
|
|
102
|
+
throw new AroraQLError(
|
|
103
|
+
body?.error ?? `AroraQL: request failed with status ${res.status}`,
|
|
104
|
+
res.status
|
|
105
|
+
);
|
|
106
|
+
}
|
|
107
|
+
return body.data ?? [];
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export class QueryBuilder<T extends Row> {
|
|
112
|
+
#query: QueryPayload;
|
|
113
|
+
|
|
114
|
+
constructor(private readonly client: AroraQLClient, table: string) {
|
|
115
|
+
this.#query = { from: table, select: [], where: [], orderBy: [] };
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
select(...fields: Field<T>[]): this {
|
|
119
|
+
this.#query.select.push(...fields);
|
|
120
|
+
return this;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
where<K extends Field<T>>(field: K): WhereBuilder<T, K> {
|
|
124
|
+
return new WhereBuilder(this, field);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
orderBy(field: Field<T>): OrderByBuilder<T> {
|
|
128
|
+
return new OrderByBuilder(this, field);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
take(count: number): this {
|
|
132
|
+
this.#query.take = count;
|
|
133
|
+
return this;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/** @internal */
|
|
137
|
+
_addWhere(condition: WhereCondition): this {
|
|
138
|
+
this.#query.where.push(condition);
|
|
139
|
+
return this;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** @internal */
|
|
143
|
+
_addOrderBy(order: OrderByClause): this {
|
|
144
|
+
this.#query.orderBy.push(order);
|
|
145
|
+
return this;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
/** The raw JSON payload (no request is made). */
|
|
149
|
+
build(): QueryPayload {
|
|
150
|
+
return structuredClone(this.#query);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Execute and return all matching rows. */
|
|
154
|
+
async many(): Promise<T[]> {
|
|
155
|
+
return this.client.execute<T>(this.build());
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/** Execute and return the first row, or null when nothing matches. */
|
|
159
|
+
async maybeSingle(): Promise<T | null> {
|
|
160
|
+
const payload = this.build();
|
|
161
|
+
payload.take = 1;
|
|
162
|
+
const rows = await this.client.execute<T>(payload);
|
|
163
|
+
return rows[0] ?? null;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** Execute and return exactly one row — throws on 0 or 2+ matches. */
|
|
167
|
+
async single(): Promise<T> {
|
|
168
|
+
const payload = this.build();
|
|
169
|
+
payload.take = 2;
|
|
170
|
+
const rows = await this.client.execute<T>(payload);
|
|
171
|
+
if (rows.length === 0) throw new AroraQLError("AroraQL: single() found no rows");
|
|
172
|
+
if (rows.length > 1) throw new AroraQLError("AroraQL: single() found more than one row");
|
|
173
|
+
return rows[0]!;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export class WhereBuilder<T extends Row, K extends Field<T>> {
|
|
178
|
+
constructor(
|
|
179
|
+
private readonly query: QueryBuilder<T>,
|
|
180
|
+
private readonly field: K
|
|
181
|
+
) {}
|
|
182
|
+
|
|
183
|
+
#add(op: Operator, value: unknown): QueryBuilder<T> {
|
|
184
|
+
return this.query._addWhere({ field: this.field, op, value });
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
eq(value: FieldValue<T, K>) { return this.#add("=", value); }
|
|
188
|
+
ne(value: FieldValue<T, K>) { return this.#add("!=", value); }
|
|
189
|
+
gt(value: FieldValue<T, K>) { return this.#add(">", value); }
|
|
190
|
+
gte(value: FieldValue<T, K>) { return this.#add(">=", value); }
|
|
191
|
+
lt(value: FieldValue<T, K>) { return this.#add("<", value); }
|
|
192
|
+
lte(value: FieldValue<T, K>) { return this.#add("<=", value); }
|
|
193
|
+
like(value: string) { return this.#add("like", value); }
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export class OrderByBuilder<T extends Row> {
|
|
197
|
+
constructor(
|
|
198
|
+
private readonly query: QueryBuilder<T>,
|
|
199
|
+
private readonly field: Field<T>
|
|
200
|
+
) {}
|
|
201
|
+
|
|
202
|
+
asc(): QueryBuilder<T> {
|
|
203
|
+
return this.query._addOrderBy({ field: this.field, dir: "asc" });
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
desc(): QueryBuilder<T> {
|
|
207
|
+
return this.query._addOrderBy({ field: this.field, dir: "desc" });
|
|
208
|
+
}
|
|
209
|
+
}
|