@ikeboy003/cloudrest-client 0.1.0 → 0.2.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/dist/client.d.ts +17 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +30 -0
- package/dist/client.js.map +1 -1
- package/dist/filter.d.ts +50 -10
- package/dist/filter.d.ts.map +1 -1
- package/dist/filter.js +70 -42
- package/dist/filter.js.map +1 -1
- package/dist/index.d.ts +33 -13
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +13 -5
- package/dist/index.js.map +1 -1
- package/dist/mutation.d.ts +77 -25
- package/dist/mutation.d.ts.map +1 -1
- package/dist/mutation.js +133 -54
- package/dist/mutation.js.map +1 -1
- package/dist/query.d.ts +31 -5
- package/dist/query.d.ts.map +1 -1
- package/dist/query.js +62 -24
- package/dist/query.js.map +1 -1
- package/dist/serialize.d.ts +3 -0
- package/dist/serialize.d.ts.map +1 -1
- package/dist/serialize.js +15 -1
- package/dist/serialize.js.map +1 -1
- package/package.json +5 -3
- package/src/client.ts +49 -4
- package/src/filter.ts +85 -42
- package/src/index.ts +23 -15
- package/src/mutation.ts +132 -62
- package/src/query.ts +69 -31
- package/src/rpc.ts +3 -3
- package/src/serialize.ts +17 -1
package/src/query.ts
CHANGED
|
@@ -4,13 +4,11 @@
|
|
|
4
4
|
//
|
|
5
5
|
// const rows = await db.from("x").select("a,b").eq("a", 1);
|
|
6
6
|
|
|
7
|
-
import type { AnyPaths, RowOf, TableName } from "
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "
|
|
12
|
-
import type { CloudRestError } from "./errors.js";
|
|
13
|
-
import { joinUrl } from "./serialize.js";
|
|
7
|
+
import type { AnyPaths, RowOf, TableName } from "@/types.js";
|
|
8
|
+
import * as F from "@/filter.js";
|
|
9
|
+
import type { FilterEntry, IsValue } from "@/filter.js";
|
|
10
|
+
import type { CloudRestError } from "@/errors.js";
|
|
11
|
+
import { joinUrl } from "@/serialize.js";
|
|
14
12
|
|
|
15
13
|
export interface ExecContext {
|
|
16
14
|
baseUrl: string;
|
|
@@ -24,6 +22,8 @@ export interface OrderOptions {
|
|
|
24
22
|
nullsFirst?: boolean;
|
|
25
23
|
}
|
|
26
24
|
|
|
25
|
+
export type CountStrategy = "exact" | "planned" | "estimated";
|
|
26
|
+
|
|
27
27
|
export class QueryBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowOf<P, T>>
|
|
28
28
|
implements PromiseLike<Row[]>
|
|
29
29
|
{
|
|
@@ -35,7 +35,8 @@ export class QueryBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowO
|
|
|
35
35
|
private rangeFrom?: number;
|
|
36
36
|
private rangeTo?: number;
|
|
37
37
|
private singleMode = false;
|
|
38
|
-
private
|
|
38
|
+
private countStrategy?: CountStrategy;
|
|
39
|
+
private preferExtra: string[] = [];
|
|
39
40
|
|
|
40
41
|
constructor(
|
|
41
42
|
private readonly ctx: ExecContext,
|
|
@@ -43,29 +44,51 @@ export class QueryBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowO
|
|
|
43
44
|
) {}
|
|
44
45
|
|
|
45
46
|
// ── projection ──────────────────────────────────────────────────────────
|
|
47
|
+
// Raw PostgREST select string — supports embedding (`*,author(*)`), aggregates
|
|
48
|
+
// (`count(),sum(x)`), JSON paths (`data->>k`), casts (`x::text`), spreads
|
|
49
|
+
// (`...t(*)`), and aliases (`alias:col`).
|
|
46
50
|
select(columns: string): this {
|
|
47
51
|
this.selectExpr = columns;
|
|
48
52
|
return this;
|
|
49
53
|
}
|
|
50
54
|
|
|
51
55
|
// ── filters ─────────────────────────────────────────────────────────────
|
|
52
|
-
eq(col: string, v: unknown): this { this.filters.push(fEq(col, v)); return this; }
|
|
53
|
-
neq(col: string, v: unknown): this { this.filters.push(fNeq(col, v)); return this; }
|
|
54
|
-
gt(col: string, v: unknown): this { this.filters.push(fGt(col, v)); return this; }
|
|
55
|
-
gte(col: string, v: unknown): this { this.filters.push(fGte(col, v)); return this; }
|
|
56
|
-
lt(col: string, v: unknown): this { this.filters.push(fLt(col, v)); return this; }
|
|
57
|
-
lte(col: string, v: unknown): this { this.filters.push(fLte(col, v)); return this; }
|
|
58
|
-
like(col: string, pat: string): this { this.filters.push(fLike(col, pat)); return this; }
|
|
59
|
-
ilike(col: string, pat: string): this { this.filters.push(fIlike(col, pat)); return this; }
|
|
60
|
-
|
|
61
|
-
|
|
56
|
+
eq(col: string, v: unknown): this { this.filters.push(F.fEq(col, v)); return this; }
|
|
57
|
+
neq(col: string, v: unknown): this { this.filters.push(F.fNeq(col, v)); return this; }
|
|
58
|
+
gt(col: string, v: unknown): this { this.filters.push(F.fGt(col, v)); return this; }
|
|
59
|
+
gte(col: string, v: unknown): this { this.filters.push(F.fGte(col, v)); return this; }
|
|
60
|
+
lt(col: string, v: unknown): this { this.filters.push(F.fLt(col, v)); return this; }
|
|
61
|
+
lte(col: string, v: unknown): this { this.filters.push(F.fLte(col, v)); return this; }
|
|
62
|
+
like(col: string, pat: string): this { this.filters.push(F.fLike(col, pat)); return this; }
|
|
63
|
+
ilike(col: string, pat: string): this { this.filters.push(F.fIlike(col, pat)); return this; }
|
|
64
|
+
match(col: string, pat: string): this { this.filters.push(F.fMatch(col, pat)); return this; }
|
|
65
|
+
imatch(col: string, pat: string): this { this.filters.push(F.fImatch(col, pat)); return this; }
|
|
66
|
+
in(col: string, vals: readonly unknown[]): this { this.filters.push(F.fIn(col, vals)); return this; }
|
|
67
|
+
is(col: string, v: IsValue): this { this.filters.push(F.fIs(col, v)); return this; }
|
|
68
|
+
isDistinct(col: string, v: unknown): this { this.filters.push(F.fIsDistinct(col, v)); return this; }
|
|
69
|
+
// full-text search (optional ts config, e.g. "english")
|
|
70
|
+
fts(col: string, q: string, config?: string): this { this.filters.push(F.fFts(col, q, config)); return this; }
|
|
71
|
+
plfts(col: string, q: string, config?: string): this { this.filters.push(F.fPlfts(col, q, config)); return this; }
|
|
72
|
+
phfts(col: string, q: string, config?: string): this { this.filters.push(F.fPhfts(col, q, config)); return this; }
|
|
73
|
+
wfts(col: string, q: string, config?: string): this { this.filters.push(F.fWfts(col, q, config)); return this; }
|
|
74
|
+
// array / range
|
|
75
|
+
cs(col: string, vals: readonly unknown[]): this { this.filters.push(F.fCs(col, vals)); return this; }
|
|
76
|
+
cd(col: string, vals: readonly unknown[]): this { this.filters.push(F.fCd(col, vals)); return this; }
|
|
77
|
+
ov(col: string, v: readonly unknown[] | string): this { this.filters.push(F.fOv(col, v)); return this; }
|
|
78
|
+
sl(col: string, range: unknown): this { this.filters.push(F.fSl(col, range)); return this; }
|
|
79
|
+
sr(col: string, range: unknown): this { this.filters.push(F.fSr(col, range)); return this; }
|
|
80
|
+
nxr(col: string, range: unknown): this { this.filters.push(F.fNxr(col, range)); return this; }
|
|
81
|
+
nxl(col: string, range: unknown): this { this.filters.push(F.fNxl(col, range)); return this; }
|
|
82
|
+
adj(col: string, range: unknown): this { this.filters.push(F.fAdj(col, range)); return this; }
|
|
62
83
|
// Negated `is` — e.g. `notIs("col", "null")` → `col=not.is.null`.
|
|
63
|
-
notIs(col: string, v: IsValue): this { this.filters.push(fNot(fIs(col, v))); return this; }
|
|
64
|
-
// Generic negation: pass
|
|
65
|
-
not(entry: FilterEntry): this { this.filters.push(fNot(entry)); return this; }
|
|
66
|
-
//
|
|
67
|
-
|
|
68
|
-
|
|
84
|
+
notIs(col: string, v: IsValue): this { this.filters.push(F.fNot(F.fIs(col, v))); return this; }
|
|
85
|
+
// Generic negation: pass a non-negated filter helper output.
|
|
86
|
+
not(entry: FilterEntry): this { this.filters.push(F.fNot(entry)); return this; }
|
|
87
|
+
// Generic escape hatch: any operator + already-serialized value.
|
|
88
|
+
filter(col: string, op: string, value: string): this { this.filters.push(F.fFilter(col, op, value)); return this; }
|
|
89
|
+
// Composite logic — pass bare `cond.*` helpers; nestable (`cond.and(cond.or(...))`).
|
|
90
|
+
and(...entries: FilterEntry[]): this { this.filters.push(F.fAnd(entries)); return this; }
|
|
91
|
+
or(...entries: FilterEntry[]): this { this.filters.push(F.fOr(entries)); return this; }
|
|
69
92
|
|
|
70
93
|
// ── ordering / paging ───────────────────────────────────────────────────
|
|
71
94
|
order(column: string, opts: OrderOptions = {}): this {
|
|
@@ -79,6 +102,14 @@ export class QueryBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowO
|
|
|
79
102
|
offset(n: number): this { this.offsetN = n; return this; }
|
|
80
103
|
range(from: number, to: number): this { this.rangeFrom = from; this.rangeTo = to; return this; }
|
|
81
104
|
|
|
105
|
+
// ── prefer tuning ─────────────────────────────────────────────────────────
|
|
106
|
+
/** Set the count strategy (`exact` | `planned` | `estimated`). withCount() implies `exact`. */
|
|
107
|
+
count(strategy: CountStrategy = "exact"): this { this.countStrategy = strategy; return this; }
|
|
108
|
+
/** `Prefer: timezone=<tz>` — render timestamptz output in this zone. */
|
|
109
|
+
timezone(tz: string): this { this.preferExtra.push(`timezone=${tz}`); return this; }
|
|
110
|
+
/** `Prefer: response-buffered` — server buffers + sets Content-Length (CF throttle fix). */
|
|
111
|
+
buffered(): this { this.preferExtra.push("response-buffered"); return this; }
|
|
112
|
+
|
|
82
113
|
// ── result shape ────────────────────────────────────────────────────────
|
|
83
114
|
/**
|
|
84
115
|
* Expect exactly one row. Resolves to `Row` (not `Row[]`).
|
|
@@ -90,12 +121,12 @@ export class QueryBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowO
|
|
|
90
121
|
}
|
|
91
122
|
|
|
92
123
|
/**
|
|
93
|
-
* Page-aware terminal: returns the rows AND the
|
|
94
|
-
* `
|
|
95
|
-
* `.
|
|
124
|
+
* Page-aware terminal: returns the rows AND the total row count via the
|
|
125
|
+
* `Content-Range` response header. Use with `.range(from,to)` (and optionally
|
|
126
|
+
* `.count("estimated")` for big tables) for paginated lists.
|
|
96
127
|
*/
|
|
97
128
|
withCount(): PagedQuery<Row> {
|
|
98
|
-
this.
|
|
129
|
+
if (!this.countStrategy) this.countStrategy = "exact";
|
|
99
130
|
return { then: (f, r) => this.execPaged().then(f as never, r as never) };
|
|
100
131
|
}
|
|
101
132
|
|
|
@@ -111,6 +142,12 @@ export class QueryBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowO
|
|
|
111
142
|
return `${joinUrl(this.ctx.baseUrl, String(this.table))}${qs ? `?${qs}` : ""}`;
|
|
112
143
|
}
|
|
113
144
|
|
|
145
|
+
private preferHeader(): string | undefined {
|
|
146
|
+
const tokens = [...this.preferExtra];
|
|
147
|
+
if (this.countStrategy) tokens.push(`count=${this.countStrategy}`);
|
|
148
|
+
return tokens.length ? tokens.join(",") : undefined;
|
|
149
|
+
}
|
|
150
|
+
|
|
114
151
|
// Shared request path for every terminal (then / single / withCount).
|
|
115
152
|
private async request(): Promise<{ res: Response; rows: Row[] }> {
|
|
116
153
|
const url = this.buildUrl();
|
|
@@ -122,14 +159,15 @@ export class QueryBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowO
|
|
|
122
159
|
headers["Range-Unit"] = "items";
|
|
123
160
|
headers["Range"] = `${this.rangeFrom}-${this.rangeTo}`;
|
|
124
161
|
}
|
|
125
|
-
|
|
162
|
+
const prefer = this.preferHeader();
|
|
163
|
+
if (prefer) headers["Prefer"] = prefer;
|
|
126
164
|
const token = await this.ctx.getToken?.();
|
|
127
165
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
128
166
|
|
|
129
167
|
const res = await this.ctx.fetch(url, { method: "GET", headers });
|
|
130
168
|
if (!res.ok) {
|
|
131
169
|
const body = await res.json().catch(() => null);
|
|
132
|
-
const { CloudRestError } = await import("
|
|
170
|
+
const { CloudRestError } = await import("@/errors.js");
|
|
133
171
|
throw new CloudRestError(res.status, res.statusText, body);
|
|
134
172
|
}
|
|
135
173
|
return { res, rows: (await res.json()) as Row[] };
|
|
@@ -149,7 +187,7 @@ export class QueryBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowO
|
|
|
149
187
|
const { rows } = await this.request();
|
|
150
188
|
if (this.singleMode) {
|
|
151
189
|
if (rows.length !== 1) {
|
|
152
|
-
const { CloudRestError } = await import("
|
|
190
|
+
const { CloudRestError } = await import("@/errors.js");
|
|
153
191
|
throw new CloudRestError(
|
|
154
192
|
rows.length === 0 ? 404 : 409,
|
|
155
193
|
rows.length === 0 ? "Not Found" : "Conflict",
|
package/src/rpc.ts
CHANGED
|
@@ -7,9 +7,9 @@
|
|
|
7
7
|
// - POST /rpc/<fn> body = { argName: value, ... }
|
|
8
8
|
// - Response is the function return — scalar, row, or set-of rows.
|
|
9
9
|
|
|
10
|
-
import type { ExecContext } from "
|
|
11
|
-
import { CloudRestError } from "
|
|
12
|
-
import { joinUrl } from "
|
|
10
|
+
import type { ExecContext } from "@/query.js";
|
|
11
|
+
import { CloudRestError } from "@/errors.js";
|
|
12
|
+
import { joinUrl } from "@/serialize.js";
|
|
13
13
|
|
|
14
14
|
export class RpcCaller<Result = unknown> implements PromiseLike<Result> {
|
|
15
15
|
constructor(
|
package/src/serialize.ts
CHANGED
|
@@ -30,7 +30,7 @@ export function serializeValue(v: unknown): string {
|
|
|
30
30
|
return escapeIfNeeded(String(v), SCALAR_SPECIAL);
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function serializeListItem(v: unknown): string {
|
|
33
|
+
export function serializeListItem(v: unknown): string {
|
|
34
34
|
if (v === null || v === undefined) return "null";
|
|
35
35
|
if (v instanceof Date) return v.toISOString();
|
|
36
36
|
if (typeof v === "boolean") return v ? "true" : "false";
|
|
@@ -38,6 +38,22 @@ function serializeListItem(v: unknown): string {
|
|
|
38
38
|
return escapeIfNeeded(String(v), LIST_SPECIAL);
|
|
39
39
|
}
|
|
40
40
|
|
|
41
|
+
// PostgreSQL array literal `{a,b,c}` — used by the array operators cs/cd/ov
|
|
42
|
+
// (`tags=cs.{a,b}`). Distinct from the `(a,b)` list form used by `in`.
|
|
43
|
+
export function serializeArray(v: readonly unknown[]): string {
|
|
44
|
+
return `{${v.map(serializeListItem).join(",")}}`;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Range/value argument for the range operators (sl/sr/nxr/nxl/adj). Accepts a
|
|
48
|
+
// ready-made literal (`"(1,10)"`, `"[2020-01-01,2020-12-31]"`) or a `[lo, hi]`
|
|
49
|
+
// tuple → `(lo,hi)`.
|
|
50
|
+
export function serializeRange(v: unknown): string {
|
|
51
|
+
if (Array.isArray(v)) {
|
|
52
|
+
return `(${serializeListItem(v[0])},${serializeListItem(v[1])})`;
|
|
53
|
+
}
|
|
54
|
+
return String(v);
|
|
55
|
+
}
|
|
56
|
+
|
|
41
57
|
function escapeIfNeeded(s: string, special: RegExp): string {
|
|
42
58
|
if (s === "") return '""';
|
|
43
59
|
if (!special.test(s)) return s;
|