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 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
@@ -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
+ }