@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/dist/serialize.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../src/serialize.ts"],"names":[],"mappings":"AAAA,qEAAqE;AACrE,+DAA+D;AAC/D,EAAE;AACF,SAAS;AACT,0DAA0D;AAC1D,uBAAuB;AACvB,4EAA4E;AAC5E,0EAA0E;AAE1E,uEAAuE;AACvE,sEAAsE;AACtE,+CAA+C;AAC/C,MAAM,cAAc,GAAG,MAAM,CAAC;AAC9B,0EAA0E;AAC1E,MAAM,YAAY,GAAG,SAAS,CAAC;AAE/B,2EAA2E;AAC3E,kFAAkF;AAClF,kEAAkE;AAClE,MAAM,UAAU,OAAO,CAAC,IAAY,EAAE,IAAY;IAChD,OAAO,GAAG,IAAI,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,IAAI,MAAM,CAAC,IAAI,CAAC,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,EAAE,CAAC;AAC3E,CAAC;AAED,MAAM,UAAU,cAAc,CAAC,CAAU;IACvC,IAAI,CAAC,KAAK,IAAI,IAAI,CAAC,KAAK,SAAS;QAAE,OAAO,MAAM,CAAC;IACjD,IAAI,CAAC,YAAY,IAAI;QAAE,OAAO,CAAC,CAAC,WAAW,EAAE,CAAC;IAC9C,IAAI,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;QAAE,OAAO,IAAI,CAAC,CAAC,GAAG,CAAC,iBAAiB,CAAC,CAAC,IAAI,CAAC,GAAG,CAAC,GAAG,CAAC;IACvE,IAAI,OAAO,CAAC,KAAK,SAAS;QAAE,OAAO,CAAC,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,OAAO,CAAC;IACxD,IAAI,OAAO,CAAC,KAAK,QAAQ,IAAI,OAAO,CAAC,KAAK,QAAQ;QAAE,OAAO,MAAM,CAAC,CAAC,CAAC,CAAC;IACrE,OAAO,cAAc,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,cAAc,CAAC,CAAC;AACnD,CAAC;AAED,
|
|
1
|
+
{"version":3,"file":"serialize.js","sourceRoot":"","sources":["../src/serialize.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ikeboy003/cloudrest-client",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Typed query builder for cloudrest. PostgREST-grammar compatible.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "./dist/index.js",
|
|
@@ -17,13 +17,15 @@
|
|
|
17
17
|
"README.md"
|
|
18
18
|
],
|
|
19
19
|
"scripts": {
|
|
20
|
-
"build": "tsc -p tsconfig.json",
|
|
21
|
-
"dev": "tsc -p tsconfig.json --watch"
|
|
20
|
+
"build": "tsc -p tsconfig.json && tsc-alias -p tsconfig.json",
|
|
21
|
+
"dev": "tsc -p tsconfig.json --watch",
|
|
22
|
+
"test": "node --test test/*.test.mjs"
|
|
22
23
|
},
|
|
23
24
|
"publishConfig": {
|
|
24
25
|
"access": "public"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
28
|
+
"tsc-alias": "^1.8.17",
|
|
27
29
|
"typescript": "^5.5.0"
|
|
28
30
|
}
|
|
29
31
|
}
|
package/src/client.ts
CHANGED
|
@@ -1,7 +1,16 @@
|
|
|
1
|
-
import type { AnyPaths, ClientOptions, RowOf, TableName } from "
|
|
2
|
-
import { QueryBuilder, type ExecContext } from "
|
|
3
|
-
import { InsertBuilder, UpdateBuilder, DeleteBuilder } from "
|
|
4
|
-
import { RpcCaller } from "
|
|
1
|
+
import type { AnyPaths, ClientOptions, RowOf, TableName } from "@/types.js";
|
|
2
|
+
import { QueryBuilder, type ExecContext } from "@/query.js";
|
|
3
|
+
import { InsertBuilder, UpdateBuilder, DeleteBuilder } from "@/mutation.js";
|
|
4
|
+
import { RpcCaller } from "@/rpc.js";
|
|
5
|
+
|
|
6
|
+
export interface CopyOptions {
|
|
7
|
+
/** Target columns, in body order. Required (maps to `?columns=`). */
|
|
8
|
+
columns: string[];
|
|
9
|
+
/** Schema the table lives in (default "public"). */
|
|
10
|
+
schema?: string;
|
|
11
|
+
/** Wire format of `body` (default "csv"). NDJSON is transcoded server-side. */
|
|
12
|
+
format?: "csv" | "ndjson";
|
|
13
|
+
}
|
|
5
14
|
|
|
6
15
|
export class CloudRestClient<P extends AnyPaths> {
|
|
7
16
|
private readonly ctx: ExecContext;
|
|
@@ -50,6 +59,42 @@ export class CloudRestClient<P extends AnyPaths> {
|
|
|
50
59
|
): RpcCaller<Result> {
|
|
51
60
|
return new RpcCaller<Result>(this.ctx, fn, args);
|
|
52
61
|
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Bulk-ingest via the server's COPY endpoint
|
|
65
|
+
* (`POST /_copy/{schema}/{table}?columns=`). `body` is CSV (with a header row)
|
|
66
|
+
* or NDJSON. Streams straight into `COPY` — far faster than row-by-row
|
|
67
|
+
* inserts for large loads. Resolves to `{ inserted: N }`.
|
|
68
|
+
*/
|
|
69
|
+
async copy(
|
|
70
|
+
table: TableName<P>,
|
|
71
|
+
body: string | Uint8Array,
|
|
72
|
+
opts: CopyOptions,
|
|
73
|
+
): Promise<{ inserted: number }> {
|
|
74
|
+
const schema = opts.schema ?? "public";
|
|
75
|
+
const cols = opts.columns.map(encodeURIComponent).join(",");
|
|
76
|
+
const url = `${this.ctx.baseUrl}/_copy/${schema}/${String(table)}?columns=${cols}`;
|
|
77
|
+
const headers: Record<string, string> = {
|
|
78
|
+
Accept: "application/json",
|
|
79
|
+
"Content-Type":
|
|
80
|
+
opts.format === "ndjson" ? "application/x-ndjson" : "text/csv",
|
|
81
|
+
...this.ctx.headers,
|
|
82
|
+
};
|
|
83
|
+
const token = await this.ctx.getToken?.();
|
|
84
|
+
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
85
|
+
|
|
86
|
+
const res = await this.ctx.fetch(url, {
|
|
87
|
+
method: "POST",
|
|
88
|
+
headers,
|
|
89
|
+
body: body as BodyInit,
|
|
90
|
+
});
|
|
91
|
+
if (!res.ok) {
|
|
92
|
+
const errBody = await res.json().catch(() => null);
|
|
93
|
+
const { CloudRestError } = await import("@/errors.js");
|
|
94
|
+
throw new CloudRestError(res.status, res.statusText, errBody);
|
|
95
|
+
}
|
|
96
|
+
return (await res.json()) as { inserted: number };
|
|
97
|
+
}
|
|
53
98
|
}
|
|
54
99
|
|
|
55
100
|
export function createClient<P extends AnyPaths>(opts: ClientOptions): CloudRestClient<P> {
|
package/src/filter.ts
CHANGED
|
@@ -1,57 +1,100 @@
|
|
|
1
|
-
// Filter ops —
|
|
2
|
-
// plus the `or` composite. (`and`, `fts`, `jsonpath` still to come.)
|
|
1
|
+
// Filter ops — the full PostgREST operator set + nestable and/or/not logic.
|
|
3
2
|
//
|
|
4
|
-
// Each filter contributes one entry
|
|
3
|
+
// Each filter contributes one entry. A leaf renders as `column=op.value`; a
|
|
4
|
+
// logic group renders as `and=(child,child)` / `or=(...)`. As a CHILD inside a
|
|
5
|
+
// group, a leaf re-spells to `column.op.value` and a group to `and(...)`/`or(...)`,
|
|
6
|
+
// which is how PostgREST nests them.
|
|
5
7
|
|
|
6
|
-
import { serializeValue } from "
|
|
8
|
+
import { serializeValue, serializeArray, serializeRange } from "@/serialize.js";
|
|
7
9
|
|
|
8
10
|
export type IsValue = "null" | "true" | "false" | "unknown";
|
|
9
11
|
|
|
10
12
|
export interface FilterEntry {
|
|
13
|
+
/** Leaf: the column. Group: "and" | "or". */
|
|
11
14
|
column: string;
|
|
15
|
+
/** Leaf: "op.value". Group: "(child,child,...)". */
|
|
12
16
|
expr: string;
|
|
17
|
+
/** True for a logic group (and/or), so child-rendering omits the dot. */
|
|
18
|
+
group?: boolean;
|
|
13
19
|
}
|
|
14
20
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
export
|
|
19
|
-
|
|
20
|
-
}
|
|
21
|
-
export
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
}
|
|
27
|
-
export
|
|
28
|
-
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
export
|
|
37
|
-
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
21
|
+
const leaf = (column: string, expr: string): FilterEntry => ({ column, expr });
|
|
22
|
+
|
|
23
|
+
// ── comparison / equality ───────────────────────────────────────────────────
|
|
24
|
+
export const fEq = (c: string, v: unknown): FilterEntry => leaf(c, `eq.${serializeValue(v)}`);
|
|
25
|
+
export const fNeq = (c: string, v: unknown): FilterEntry => leaf(c, `neq.${serializeValue(v)}`);
|
|
26
|
+
export const fGt = (c: string, v: unknown): FilterEntry => leaf(c, `gt.${serializeValue(v)}`);
|
|
27
|
+
export const fGte = (c: string, v: unknown): FilterEntry => leaf(c, `gte.${serializeValue(v)}`);
|
|
28
|
+
export const fLt = (c: string, v: unknown): FilterEntry => leaf(c, `lt.${serializeValue(v)}`);
|
|
29
|
+
export const fLte = (c: string, v: unknown): FilterEntry => leaf(c, `lte.${serializeValue(v)}`);
|
|
30
|
+
|
|
31
|
+
// ── pattern match ───────────────────────────────────────────────────────────
|
|
32
|
+
export const fLike = (c: string, p: string): FilterEntry => leaf(c, `like.${serializeValue(p)}`);
|
|
33
|
+
export const fIlike = (c: string, p: string): FilterEntry => leaf(c, `ilike.${serializeValue(p)}`);
|
|
34
|
+
/** POSIX regex (`~`). */
|
|
35
|
+
export const fMatch = (c: string, p: string): FilterEntry => leaf(c, `match.${serializeValue(p)}`);
|
|
36
|
+
/** Case-insensitive POSIX regex (`~*`). */
|
|
37
|
+
export const fImatch = (c: string, p: string): FilterEntry => leaf(c, `imatch.${serializeValue(p)}`);
|
|
38
|
+
|
|
39
|
+
// ── set membership / null / distinct ────────────────────────────────────────
|
|
40
|
+
export const fIn = (c: string, vals: readonly unknown[]): FilterEntry => leaf(c, `in.${serializeValue(vals)}`);
|
|
41
|
+
// `is` is the correct NULL / boolean check — `eq.null` is a SQL no-op.
|
|
42
|
+
export const fIs = (c: string, v: IsValue): FilterEntry => leaf(c, `is.${v}`);
|
|
43
|
+
/** `IS DISTINCT FROM` — null-safe inequality. */
|
|
44
|
+
export const fIsDistinct = (c: string, v: unknown): FilterEntry => leaf(c, `isdistinct.${serializeValue(v)}`);
|
|
45
|
+
|
|
46
|
+
// ── full-text search ────────────────────────────────────────────────────────
|
|
47
|
+
// Optional text-search config (e.g. "english") rides in parens: `fts(english).query`.
|
|
48
|
+
const ftsOp = (op: string) => (c: string, query: string, config?: string): FilterEntry =>
|
|
49
|
+
leaf(c, config ? `${op}(${config}).${serializeValue(query)}` : `${op}.${serializeValue(query)}`);
|
|
50
|
+
/** `to_tsquery` full-text search. */
|
|
51
|
+
export const fFts = ftsOp("fts");
|
|
52
|
+
/** `plainto_tsquery`. */
|
|
53
|
+
export const fPlfts = ftsOp("plfts");
|
|
54
|
+
/** `phraseto_tsquery`. */
|
|
55
|
+
export const fPhfts = ftsOp("phfts");
|
|
56
|
+
/** `websearch_to_tsquery`. */
|
|
57
|
+
export const fWfts = ftsOp("wfts");
|
|
58
|
+
|
|
59
|
+
// ── array / range containment + adjacency ───────────────────────────────────
|
|
60
|
+
/** contains `@>` (array/range/jsonb). */
|
|
61
|
+
export const fCs = (c: string, v: readonly unknown[]): FilterEntry => leaf(c, `cs.${serializeArray(v)}`);
|
|
62
|
+
/** contained-in `<@`. */
|
|
63
|
+
export const fCd = (c: string, v: readonly unknown[]): FilterEntry => leaf(c, `cd.${serializeArray(v)}`);
|
|
64
|
+
/** overlap `&&` (array or range). */
|
|
65
|
+
export const fOv = (c: string, v: readonly unknown[] | string): FilterEntry =>
|
|
66
|
+
leaf(c, `ov.${Array.isArray(v) ? serializeArray(v) : serializeRange(v)}`);
|
|
67
|
+
/** strictly-left-of `<<`. */
|
|
68
|
+
export const fSl = (c: string, range: unknown): FilterEntry => leaf(c, `sl.${serializeRange(range)}`);
|
|
69
|
+
/** strictly-right-of `>>`. */
|
|
70
|
+
export const fSr = (c: string, range: unknown): FilterEntry => leaf(c, `sr.${serializeRange(range)}`);
|
|
71
|
+
/** does-not-extend-to-the-right-of `&<`. */
|
|
72
|
+
export const fNxr = (c: string, range: unknown): FilterEntry => leaf(c, `nxr.${serializeRange(range)}`);
|
|
73
|
+
/** does-not-extend-to-the-left-of `&>`. */
|
|
74
|
+
export const fNxl = (c: string, range: unknown): FilterEntry => leaf(c, `nxl.${serializeRange(range)}`);
|
|
75
|
+
/** adjacent `-|-`. */
|
|
76
|
+
export const fAdj = (c: string, range: unknown): FilterEntry => leaf(c, `adj.${serializeRange(range)}`);
|
|
77
|
+
|
|
78
|
+
// ── generic escape hatch ────────────────────────────────────────────────────
|
|
79
|
+
/** Any operator + already-serialized value, e.g. `fFilter("data->>k", "eq", "x")`. */
|
|
80
|
+
export const fFilter = (c: string, op: string, value: string): FilterEntry => leaf(c, `${op}.${value}`);
|
|
81
|
+
|
|
82
|
+
// ── negation + logic groups (nestable) ──────────────────────────────────────
|
|
83
|
+
/** Negate any single-op filter: `column=not.is.null`, `column=not.eq.5`. */
|
|
48
84
|
export function fNot(entry: FilterEntry): FilterEntry {
|
|
49
85
|
return { column: entry.column, expr: `not.${entry.expr}` };
|
|
50
86
|
}
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
87
|
+
|
|
88
|
+
/** Render an entry as a CHILD inside a logic group. */
|
|
89
|
+
function renderChild(e: FilterEntry): string {
|
|
90
|
+
return e.group ? `${e.column}${e.expr}` : `${e.column}.${e.expr}`;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** Composite AND — `and=(a.eq.1,b.gt.2)`. Children may be leaves or groups (nests). */
|
|
94
|
+
export function fAnd(entries: readonly FilterEntry[]): FilterEntry {
|
|
95
|
+
return { column: "and", expr: `(${entries.map(renderChild).join(",")})`, group: true };
|
|
96
|
+
}
|
|
97
|
+
/** Composite OR — `or=(a.eq.1,b.gt.2)`. Children may be leaves or groups (nests). */
|
|
54
98
|
export function fOr(entries: readonly FilterEntry[]): FilterEntry {
|
|
55
|
-
|
|
56
|
-
return { column: "or", expr: `(${inner})` };
|
|
99
|
+
return { column: "or", expr: `(${entries.map(renderChild).join(",")})`, group: true };
|
|
57
100
|
}
|
package/src/index.ts
CHANGED
|
@@ -1,24 +1,32 @@
|
|
|
1
|
-
export { createClient, CloudRestClient } from "
|
|
2
|
-
export {
|
|
3
|
-
export {
|
|
4
|
-
export {
|
|
5
|
-
export {
|
|
1
|
+
export { createClient, CloudRestClient } from "@/client.js";
|
|
2
|
+
export type { CopyOptions } from "@/client.js";
|
|
3
|
+
export { CloudRestError } from "@/errors.js";
|
|
4
|
+
export { QueryBuilder } from "@/query.js";
|
|
5
|
+
export type { OrderOptions, CountStrategy, SingleQuery, PagedQuery } from "@/query.js";
|
|
6
|
+
export { InsertBuilder, UpdateBuilder, DeleteBuilder } from "@/mutation.js";
|
|
7
|
+
export { RpcCaller } from "@/rpc.js";
|
|
6
8
|
export type {
|
|
7
9
|
AnyPaths,
|
|
8
10
|
ClientOptions,
|
|
9
11
|
RowOf,
|
|
10
12
|
TableName,
|
|
11
13
|
TokenGetter,
|
|
12
|
-
} from "
|
|
13
|
-
export type {
|
|
14
|
-
export type { IsValue, FilterEntry } from "./filter.js";
|
|
14
|
+
} from "@/types.js";
|
|
15
|
+
export type { IsValue, FilterEntry } from "@/filter.js";
|
|
15
16
|
|
|
16
|
-
// Bare condition builders for composing `.
|
|
17
|
-
// db.from("
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
} from "
|
|
17
|
+
// Bare condition builders for composing `.and(...)` / `.or(...)` (nestable).
|
|
18
|
+
// db.from("books").or(cond.ilike("title","*x*"), cond.ilike("author","*x*"))
|
|
19
|
+
// db.from("books").and(cond.gte("year",2000), cond.or(cond.eq("a",1), cond.eq("b",2)))
|
|
20
|
+
import * as F from "@/filter.js";
|
|
21
|
+
import type { FilterEntry } from "@/filter.js";
|
|
21
22
|
export const cond = {
|
|
22
|
-
eq: fEq, neq: fNeq, gt: fGt, gte: fGte, lt: fLt, lte: fLte,
|
|
23
|
-
like: fLike, ilike: fIlike,
|
|
23
|
+
eq: F.fEq, neq: F.fNeq, gt: F.fGt, gte: F.fGte, lt: F.fLt, lte: F.fLte,
|
|
24
|
+
like: F.fLike, ilike: F.fIlike, match: F.fMatch, imatch: F.fImatch,
|
|
25
|
+
in: F.fIn, is: F.fIs, isDistinct: F.fIsDistinct,
|
|
26
|
+
fts: F.fFts, plfts: F.fPlfts, phfts: F.fPhfts, wfts: F.fWfts,
|
|
27
|
+
cs: F.fCs, cd: F.fCd, ov: F.fOv, sl: F.fSl, sr: F.fSr, nxr: F.fNxr, nxl: F.fNxl, adj: F.fAdj,
|
|
28
|
+
filter: F.fFilter, not: F.fNot,
|
|
29
|
+
// Variadic so they compose ergonomically + nest: cond.and(cond.eq(..), cond.or(..)).
|
|
30
|
+
and: (...e: FilterEntry[]): FilterEntry => F.fAnd(e),
|
|
31
|
+
or: (...e: FilterEntry[]): FilterEntry => F.fOr(e),
|
|
24
32
|
} as const;
|
package/src/mutation.ts
CHANGED
|
@@ -1,29 +1,96 @@
|
|
|
1
|
-
// Mutations: insert / update / delete
|
|
2
|
-
//
|
|
1
|
+
// Mutations: insert / update / delete (+ upsert, bulk media, write tuning).
|
|
2
|
+
// PostgREST conventions:
|
|
3
|
+
// - POST /table array/object body → insert (`?on_conflict=` → upsert)
|
|
3
4
|
// - PATCH /table?filters → update
|
|
4
5
|
// - DELETE /table?filters → delete
|
|
5
|
-
// - Prefer: return=representation
|
|
6
|
+
// - Prefer: return=representation|minimal, resolution=*, missing=*, tx=rollback,
|
|
7
|
+
// max-affected/min-affected, timezone, response-buffered
|
|
6
8
|
|
|
7
|
-
import type { AnyPaths, RowOf, TableName } from "
|
|
8
|
-
import
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
} from "
|
|
12
|
-
import type { ExecContext } from "./query.js";
|
|
13
|
-
import { joinUrl } from "./serialize.js";
|
|
9
|
+
import type { AnyPaths, RowOf, TableName } from "@/types.js";
|
|
10
|
+
import * as F from "@/filter.js";
|
|
11
|
+
import type { FilterEntry, IsValue } from "@/filter.js";
|
|
12
|
+
import type { ExecContext } from "@/query.js";
|
|
13
|
+
import { joinUrl } from "@/serialize.js";
|
|
14
14
|
|
|
15
|
-
|
|
15
|
+
// Shared write-tuning state + Prefer assembly (insert/update/delete).
|
|
16
|
+
abstract class WriteTuning<Row> {
|
|
17
|
+
protected returnMode?: "representation" | "minimal";
|
|
18
|
+
protected txMode?: "rollback" | "commit";
|
|
19
|
+
protected maxAff?: number;
|
|
20
|
+
protected minAff?: number;
|
|
21
|
+
protected tz?: string;
|
|
22
|
+
protected bufferedFlag = false;
|
|
23
|
+
protected missingMode?: "default" | "null";
|
|
24
|
+
protected resolution?: "merge-duplicates" | "ignore-duplicates";
|
|
25
|
+
|
|
26
|
+
/** Return affected rows (adds Prefer: return=representation). */
|
|
27
|
+
returning(): this { this.returnMode = "representation"; return this; }
|
|
28
|
+
/** No body back (Prefer: return=minimal) — the default if unset. */
|
|
29
|
+
minimal(): this { this.returnMode = "minimal"; return this; }
|
|
30
|
+
/** Dry-run: run + roll back (Prefer: tx=rollback). */
|
|
31
|
+
dryRun(): this { this.txMode = "rollback"; return this; }
|
|
32
|
+
/** Fail (PGRST) if more than N rows would be affected. */
|
|
33
|
+
maxAffected(n: number): this { this.maxAff = n; return this; }
|
|
34
|
+
/** Fail if fewer than N rows are affected. */
|
|
35
|
+
minAffected(n: number): this { this.minAff = n; return this; }
|
|
36
|
+
/** Render timestamptz output in this zone (Prefer: timezone=<tz>). */
|
|
37
|
+
timezone(tz: string): this { this.tz = tz; return this; }
|
|
38
|
+
/** Buffer the response + set Content-Length (Prefer: response-buffered). */
|
|
39
|
+
buffered(): this { this.bufferedFlag = true; return this; }
|
|
40
|
+
|
|
41
|
+
protected prefer(): string[] {
|
|
42
|
+
const p: string[] = [];
|
|
43
|
+
if (this.returnMode) p.push(`return=${this.returnMode}`);
|
|
44
|
+
if (this.resolution) p.push(`resolution=${this.resolution}`);
|
|
45
|
+
if (this.missingMode) p.push(`missing=${this.missingMode}`);
|
|
46
|
+
if (this.txMode) p.push(`tx=${this.txMode}`);
|
|
47
|
+
if (this.maxAff !== undefined) p.push(`max-affected=${this.maxAff}`);
|
|
48
|
+
if (this.minAff !== undefined) p.push(`min-affected=${this.minAff}`);
|
|
49
|
+
if (this.tz) p.push(`timezone=${this.tz}`);
|
|
50
|
+
if (this.bufferedFlag) p.push("response-buffered");
|
|
51
|
+
return p;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Filter methods shared by update/delete (mirrors QueryBuilder's set).
|
|
56
|
+
abstract class FilterableMutation<Row> extends WriteTuning<Row> {
|
|
16
57
|
protected filters: FilterEntry[] = [];
|
|
17
|
-
eq(
|
|
18
|
-
neq(
|
|
19
|
-
gt(
|
|
20
|
-
gte(
|
|
21
|
-
lt(
|
|
22
|
-
lte(
|
|
23
|
-
like(
|
|
24
|
-
ilike(
|
|
25
|
-
|
|
26
|
-
|
|
58
|
+
eq(c: string, v: unknown): this { this.filters.push(F.fEq(c, v)); return this; }
|
|
59
|
+
neq(c: string, v: unknown): this { this.filters.push(F.fNeq(c, v)); return this; }
|
|
60
|
+
gt(c: string, v: unknown): this { this.filters.push(F.fGt(c, v)); return this; }
|
|
61
|
+
gte(c: string, v: unknown): this { this.filters.push(F.fGte(c, v)); return this; }
|
|
62
|
+
lt(c: string, v: unknown): this { this.filters.push(F.fLt(c, v)); return this; }
|
|
63
|
+
lte(c: string, v: unknown): this { this.filters.push(F.fLte(c, v)); return this; }
|
|
64
|
+
like(c: string, p: string): this { this.filters.push(F.fLike(c, p)); return this; }
|
|
65
|
+
ilike(c: string, p: string): this { this.filters.push(F.fIlike(c, p)); return this; }
|
|
66
|
+
match(c: string, p: string): this { this.filters.push(F.fMatch(c, p)); return this; }
|
|
67
|
+
imatch(c: string, p: string): this { this.filters.push(F.fImatch(c, p)); return this; }
|
|
68
|
+
in(c: string, vals: readonly unknown[]): this { this.filters.push(F.fIn(c, vals)); return this; }
|
|
69
|
+
is(c: string, v: IsValue): this { this.filters.push(F.fIs(c, v)); return this; }
|
|
70
|
+
isDistinct(c: string, v: unknown): this { this.filters.push(F.fIsDistinct(c, v)); return this; }
|
|
71
|
+
fts(c: string, q: string, cfg?: string): this { this.filters.push(F.fFts(c, q, cfg)); return this; }
|
|
72
|
+
plfts(c: string, q: string, cfg?: string): this { this.filters.push(F.fPlfts(c, q, cfg)); return this; }
|
|
73
|
+
phfts(c: string, q: string, cfg?: string): this { this.filters.push(F.fPhfts(c, q, cfg)); return this; }
|
|
74
|
+
wfts(c: string, q: string, cfg?: string): this { this.filters.push(F.fWfts(c, q, cfg)); return this; }
|
|
75
|
+
cs(c: string, vals: readonly unknown[]): this { this.filters.push(F.fCs(c, vals)); return this; }
|
|
76
|
+
cd(c: string, vals: readonly unknown[]): this { this.filters.push(F.fCd(c, vals)); return this; }
|
|
77
|
+
ov(c: string, v: readonly unknown[] | string): this { this.filters.push(F.fOv(c, v)); return this; }
|
|
78
|
+
sl(c: string, r: unknown): this { this.filters.push(F.fSl(c, r)); return this; }
|
|
79
|
+
sr(c: string, r: unknown): this { this.filters.push(F.fSr(c, r)); return this; }
|
|
80
|
+
nxr(c: string, r: unknown): this { this.filters.push(F.fNxr(c, r)); return this; }
|
|
81
|
+
nxl(c: string, r: unknown): this { this.filters.push(F.fNxl(c, r)); return this; }
|
|
82
|
+
adj(c: string, r: unknown): this { this.filters.push(F.fAdj(c, r)); return this; }
|
|
83
|
+
not(entry: FilterEntry): this { this.filters.push(F.fNot(entry)); return this; }
|
|
84
|
+
notIs(c: string, v: IsValue): this { this.filters.push(F.fNot(F.fIs(c, v))); return this; }
|
|
85
|
+
filter(c: string, op: string, value: string): this { this.filters.push(F.fFilter(c, op, value)); return this; }
|
|
86
|
+
and(...entries: FilterEntry[]): this { this.filters.push(F.fAnd(entries)); return this; }
|
|
87
|
+
or(...entries: FilterEntry[]): this { this.filters.push(F.fOr(entries)); return this; }
|
|
88
|
+
|
|
89
|
+
protected filterQs(): string {
|
|
90
|
+
const params = new URLSearchParams();
|
|
91
|
+
for (const f of this.filters) params.append(f.column, f.expr);
|
|
92
|
+
return params.toString();
|
|
93
|
+
}
|
|
27
94
|
}
|
|
28
95
|
|
|
29
96
|
async function send(
|
|
@@ -31,7 +98,7 @@ async function send(
|
|
|
31
98
|
method: "POST" | "PATCH" | "DELETE",
|
|
32
99
|
url: string,
|
|
33
100
|
body: unknown | undefined,
|
|
34
|
-
|
|
101
|
+
prefer: string[],
|
|
35
102
|
): Promise<unknown> {
|
|
36
103
|
// A FormData body is a multipart media write — let the runtime set the
|
|
37
104
|
// multipart Content-Type (with boundary); don't JSON-encode or force a type.
|
|
@@ -41,7 +108,7 @@ async function send(
|
|
|
41
108
|
...(isForm ? {} : { "Content-Type": "application/json" }),
|
|
42
109
|
...ctx.headers,
|
|
43
110
|
};
|
|
44
|
-
if (
|
|
111
|
+
if (prefer.length) headers["Prefer"] = prefer.join(",");
|
|
45
112
|
const token = await ctx.getToken?.();
|
|
46
113
|
if (token) headers["Authorization"] = `Bearer ${token}`;
|
|
47
114
|
|
|
@@ -54,7 +121,7 @@ async function send(
|
|
|
54
121
|
});
|
|
55
122
|
if (!res.ok) {
|
|
56
123
|
const errBody = await res.json().catch(() => null);
|
|
57
|
-
const { CloudRestError } = await import("
|
|
124
|
+
const { CloudRestError } = await import("@/errors.js");
|
|
58
125
|
throw new CloudRestError(res.status, res.statusText, errBody);
|
|
59
126
|
}
|
|
60
127
|
if (res.status === 204) return [];
|
|
@@ -62,39 +129,62 @@ async function send(
|
|
|
62
129
|
}
|
|
63
130
|
|
|
64
131
|
export class InsertBuilder<P extends AnyPaths, T extends TableName<P>, Row = RowOf<P, T>>
|
|
132
|
+
extends WriteTuning<Row>
|
|
65
133
|
implements PromiseLike<Row[]>
|
|
66
134
|
{
|
|
67
135
|
private rows: Partial<Row>[];
|
|
68
|
-
private returnRows = false;
|
|
69
136
|
private files: Record<string, Blob> = {};
|
|
137
|
+
private onConflictCols?: string;
|
|
138
|
+
private columnsList?: string;
|
|
70
139
|
|
|
71
140
|
constructor(
|
|
72
141
|
private readonly ctx: ExecContext,
|
|
73
142
|
private readonly table: T,
|
|
74
143
|
rows: Partial<Row> | Partial<Row>[],
|
|
75
144
|
) {
|
|
145
|
+
super();
|
|
76
146
|
this.rows = Array.isArray(rows) ? rows : [rows];
|
|
77
147
|
}
|
|
78
148
|
|
|
79
|
-
/**
|
|
80
|
-
|
|
81
|
-
|
|
149
|
+
/**
|
|
150
|
+
* Upsert on conflict of `columns` (a PK/unique set). Default resolution
|
|
151
|
+
* merges (UPDATE); pass `{ ignoreDuplicates: true }` for DO NOTHING.
|
|
152
|
+
*/
|
|
153
|
+
onConflict(columns: string | string[], opts: { ignoreDuplicates?: boolean } = {}): this {
|
|
154
|
+
this.onConflictCols = Array.isArray(columns) ? columns.join(",") : columns;
|
|
155
|
+
this.resolution = opts.ignoreDuplicates ? "ignore-duplicates" : "merge-duplicates";
|
|
156
|
+
return this;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Restrict the insert to these columns (`?columns=`) — consistent bulk keys. */
|
|
160
|
+
columns(cols: string[]): this {
|
|
161
|
+
this.columnsList = cols.join(",");
|
|
162
|
+
return this;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
/** Treat keys absent from a row as `NULL` rather than the column default. */
|
|
166
|
+
missing(mode: "default" | "null"): this {
|
|
167
|
+
this.missingMode = mode;
|
|
82
168
|
return this;
|
|
83
169
|
}
|
|
84
170
|
|
|
85
171
|
/**
|
|
86
|
-
* Attach a file for a media column
|
|
87
|
-
*
|
|
88
|
-
*
|
|
89
|
-
*
|
|
172
|
+
* Attach a file for a media column — switches to a multipart write (scalar
|
|
173
|
+
* columns as a JSON `payload` part, each file as a part named after its
|
|
174
|
+
* column). The server's media stage stores bytes → reference into the column.
|
|
175
|
+
* Single-row inserts only.
|
|
90
176
|
*/
|
|
91
177
|
attach(column: string, file: Blob): this {
|
|
92
178
|
this.files[column] = file;
|
|
93
179
|
return this;
|
|
94
180
|
}
|
|
95
181
|
|
|
96
|
-
private
|
|
97
|
-
|
|
182
|
+
private buildUrl(): string {
|
|
183
|
+
const params = new URLSearchParams();
|
|
184
|
+
if (this.onConflictCols) params.set("on_conflict", this.onConflictCols);
|
|
185
|
+
if (this.columnsList) params.set("columns", this.columnsList);
|
|
186
|
+
const qs = params.toString();
|
|
187
|
+
return `${joinUrl(this.ctx.baseUrl, String(this.table))}${qs ? `?${qs}` : ""}`;
|
|
98
188
|
}
|
|
99
189
|
|
|
100
190
|
private formBody(): FormData {
|
|
@@ -106,9 +196,7 @@ export class InsertBuilder<P extends AnyPaths, T extends TableName<P>, Row = Row
|
|
|
106
196
|
"payload",
|
|
107
197
|
new Blob([JSON.stringify(this.rows[0])], { type: "application/json" }),
|
|
108
198
|
);
|
|
109
|
-
for (const [column, file] of Object.entries(this.files))
|
|
110
|
-
fd.append(column, file);
|
|
111
|
-
}
|
|
199
|
+
for (const [column, file] of Object.entries(this.files)) fd.append(column, file);
|
|
112
200
|
return fd;
|
|
113
201
|
}
|
|
114
202
|
|
|
@@ -116,9 +204,9 @@ export class InsertBuilder<P extends AnyPaths, T extends TableName<P>, Row = Row
|
|
|
116
204
|
onfulfilled?: ((value: Row[]) => R1 | PromiseLike<R1>) | null,
|
|
117
205
|
onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null,
|
|
118
206
|
): PromiseLike<R1 | R2> {
|
|
119
|
-
const
|
|
120
|
-
const body =
|
|
121
|
-
return send(this.ctx, "POST",
|
|
207
|
+
const hasFiles = Object.keys(this.files).length > 0;
|
|
208
|
+
const body = hasFiles ? this.formBody() : this.rows;
|
|
209
|
+
return send(this.ctx, "POST", this.buildUrl(), body, this.prefer())
|
|
122
210
|
.then((v) => v as Row[])
|
|
123
211
|
.then(onfulfilled, onrejected as never);
|
|
124
212
|
}
|
|
@@ -128,8 +216,6 @@ export class UpdateBuilder<P extends AnyPaths, T extends TableName<P>, Row = Row
|
|
|
128
216
|
extends FilterableMutation<Row>
|
|
129
217
|
implements PromiseLike<Row[]>
|
|
130
218
|
{
|
|
131
|
-
private returnRows = false;
|
|
132
|
-
|
|
133
219
|
constructor(
|
|
134
220
|
private readonly ctx: ExecContext,
|
|
135
221
|
private readonly table: T,
|
|
@@ -138,15 +224,8 @@ export class UpdateBuilder<P extends AnyPaths, T extends TableName<P>, Row = Row
|
|
|
138
224
|
super();
|
|
139
225
|
}
|
|
140
226
|
|
|
141
|
-
returning(): this {
|
|
142
|
-
this.returnRows = true;
|
|
143
|
-
return this;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
227
|
private url(): string {
|
|
147
|
-
const
|
|
148
|
-
for (const f of this.filters) params.append(f.column, f.expr);
|
|
149
|
-
const qs = params.toString();
|
|
228
|
+
const qs = this.filterQs();
|
|
150
229
|
return `${joinUrl(this.ctx.baseUrl, String(this.table))}${qs ? `?${qs}` : ""}`;
|
|
151
230
|
}
|
|
152
231
|
|
|
@@ -154,7 +233,7 @@ export class UpdateBuilder<P extends AnyPaths, T extends TableName<P>, Row = Row
|
|
|
154
233
|
onfulfilled?: ((value: Row[]) => R1 | PromiseLike<R1>) | null,
|
|
155
234
|
onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null,
|
|
156
235
|
): PromiseLike<R1 | R2> {
|
|
157
|
-
return send(this.ctx, "PATCH", this.url(), this.patch, this.
|
|
236
|
+
return send(this.ctx, "PATCH", this.url(), this.patch, this.prefer())
|
|
158
237
|
.then((v) => v as Row[])
|
|
159
238
|
.then(onfulfilled, onrejected as never);
|
|
160
239
|
}
|
|
@@ -164,8 +243,6 @@ export class DeleteBuilder<P extends AnyPaths, T extends TableName<P>, Row = Row
|
|
|
164
243
|
extends FilterableMutation<Row>
|
|
165
244
|
implements PromiseLike<Row[]>
|
|
166
245
|
{
|
|
167
|
-
private returnRows = false;
|
|
168
|
-
|
|
169
246
|
constructor(
|
|
170
247
|
private readonly ctx: ExecContext,
|
|
171
248
|
private readonly table: T,
|
|
@@ -173,15 +250,8 @@ export class DeleteBuilder<P extends AnyPaths, T extends TableName<P>, Row = Row
|
|
|
173
250
|
super();
|
|
174
251
|
}
|
|
175
252
|
|
|
176
|
-
returning(): this {
|
|
177
|
-
this.returnRows = true;
|
|
178
|
-
return this;
|
|
179
|
-
}
|
|
180
|
-
|
|
181
253
|
private url(): string {
|
|
182
|
-
const
|
|
183
|
-
for (const f of this.filters) params.append(f.column, f.expr);
|
|
184
|
-
const qs = params.toString();
|
|
254
|
+
const qs = this.filterQs();
|
|
185
255
|
return `${joinUrl(this.ctx.baseUrl, String(this.table))}${qs ? `?${qs}` : ""}`;
|
|
186
256
|
}
|
|
187
257
|
|
|
@@ -189,7 +259,7 @@ export class DeleteBuilder<P extends AnyPaths, T extends TableName<P>, Row = Row
|
|
|
189
259
|
onfulfilled?: ((value: Row[]) => R1 | PromiseLike<R1>) | null,
|
|
190
260
|
onrejected?: ((reason: unknown) => R2 | PromiseLike<R2>) | null,
|
|
191
261
|
): PromiseLike<R1 | R2> {
|
|
192
|
-
return send(this.ctx, "DELETE", this.url(), undefined, this.
|
|
262
|
+
return send(this.ctx, "DELETE", this.url(), undefined, this.prefer())
|
|
193
263
|
.then((v) => v as Row[])
|
|
194
264
|
.then(onfulfilled, onrejected as never);
|
|
195
265
|
}
|