@vantis/data 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +201 -0
- package/README.md +128 -0
- package/dist/browser.cjs +101 -0
- package/dist/browser.cjs.map +1 -0
- package/dist/browser.d.cts +51 -0
- package/dist/browser.d.ts +51 -0
- package/dist/browser.mjs +64 -0
- package/dist/browser.mjs.map +1 -0
- package/dist/index.cjs +997 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +879 -0
- package/dist/index.d.ts +879 -0
- package/dist/index.mjs +958 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +57 -0
- package/vantis.schema.schema.json +494 -0
package/dist/index.mjs
ADDED
|
@@ -0,0 +1,958 @@
|
|
|
1
|
+
// src/verbs.ts
|
|
2
|
+
var VERB_BRAND = /* @__PURE__ */ Symbol.for("@vantis/data:verb");
|
|
3
|
+
function isVerbEnvelope(v) {
|
|
4
|
+
return v !== null && typeof v === "object" && v[VERB_BRAND] === true;
|
|
5
|
+
}
|
|
6
|
+
function makeVerb(op, props) {
|
|
7
|
+
const env = /* @__PURE__ */ Object.create(null);
|
|
8
|
+
Object.defineProperty(env, VERB_BRAND, {
|
|
9
|
+
value: true,
|
|
10
|
+
enumerable: false,
|
|
11
|
+
configurable: false,
|
|
12
|
+
writable: false
|
|
13
|
+
});
|
|
14
|
+
env["op"] = op;
|
|
15
|
+
if (props) {
|
|
16
|
+
for (const [k, vv] of Object.entries(props)) {
|
|
17
|
+
env[k] = vv;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
return Object.freeze(env);
|
|
21
|
+
}
|
|
22
|
+
function numOp(n) {
|
|
23
|
+
return typeof n === "bigint" ? String(n) : n;
|
|
24
|
+
}
|
|
25
|
+
function increment(n) {
|
|
26
|
+
return makeVerb("increment", { v: numOp(n) });
|
|
27
|
+
}
|
|
28
|
+
function decrement(n) {
|
|
29
|
+
return makeVerb("decrement", { v: numOp(n) });
|
|
30
|
+
}
|
|
31
|
+
function multiply(n) {
|
|
32
|
+
return makeVerb("multiply", { v: numOp(n) });
|
|
33
|
+
}
|
|
34
|
+
function divide(n) {
|
|
35
|
+
return makeVerb("divide", { v: numOp(n) });
|
|
36
|
+
}
|
|
37
|
+
function min(v) {
|
|
38
|
+
return makeVerb("min", { v: numOp(v) });
|
|
39
|
+
}
|
|
40
|
+
function max(v) {
|
|
41
|
+
return makeVerb("max", { v: numOp(v) });
|
|
42
|
+
}
|
|
43
|
+
function toggle() {
|
|
44
|
+
return makeVerb("toggle");
|
|
45
|
+
}
|
|
46
|
+
function serverTimestamp() {
|
|
47
|
+
return makeVerb("serverTimestamp");
|
|
48
|
+
}
|
|
49
|
+
function concat(s) {
|
|
50
|
+
return makeVerb("concat", { v: s });
|
|
51
|
+
}
|
|
52
|
+
function merge(obj) {
|
|
53
|
+
return makeVerb("merge", { v: obj });
|
|
54
|
+
}
|
|
55
|
+
function jsonSet(path, v) {
|
|
56
|
+
return makeVerb("jsonSet", { path, v });
|
|
57
|
+
}
|
|
58
|
+
function setOnInsert(v) {
|
|
59
|
+
return makeVerb("setOnInsert", { v });
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// src/errors.ts
|
|
63
|
+
function makeConfigError(code, message) {
|
|
64
|
+
return { kind: "config", code, message, details: null, hint: null };
|
|
65
|
+
}
|
|
66
|
+
async function parsePostgRESTError(res) {
|
|
67
|
+
let code = "PGRST_UNKNOWN";
|
|
68
|
+
let message = `HTTP ${res.status} ${res.statusText}`;
|
|
69
|
+
let details = null;
|
|
70
|
+
let hint = null;
|
|
71
|
+
try {
|
|
72
|
+
const body = await res.json();
|
|
73
|
+
if (body !== null && typeof body === "object") {
|
|
74
|
+
const b = body;
|
|
75
|
+
if (typeof b["code"] === "string") code = b["code"];
|
|
76
|
+
if (typeof b["message"] === "string") message = b["message"];
|
|
77
|
+
if (typeof b["details"] === "string") details = b["details"];
|
|
78
|
+
else if (b["details"] == null) details = null;
|
|
79
|
+
if (typeof b["hint"] === "string") hint = b["hint"];
|
|
80
|
+
else if (b["hint"] == null) hint = null;
|
|
81
|
+
}
|
|
82
|
+
} catch {
|
|
83
|
+
}
|
|
84
|
+
return { kind: "postgrest", code, message, details, hint };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
// src/client.ts
|
|
88
|
+
var DEFAULT_TIMEOUT_MS = 3e4;
|
|
89
|
+
var DEFAULT_RPC_TIMEOUT_MS = 65e3;
|
|
90
|
+
function bigintReplacer(_key, value) {
|
|
91
|
+
return typeof value === "bigint" ? String(value) : value;
|
|
92
|
+
}
|
|
93
|
+
function resolveDataApiEnv() {
|
|
94
|
+
const _g = globalThis;
|
|
95
|
+
if (_g["window"] !== void 0 || _g["document"] !== void 0) {
|
|
96
|
+
return {
|
|
97
|
+
configError: makeConfigError(
|
|
98
|
+
"BROWSER_ENV",
|
|
99
|
+
"@vantis/data is server-only. DATA_API_TOKEN must never run in a browser. The client-side SDK is tracked as VAN-846."
|
|
100
|
+
)
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
const apiUrl = (typeof process !== "undefined" ? process.env["DATA_API_URL"] : void 0) ?? "";
|
|
104
|
+
const apiToken = (typeof process !== "undefined" ? process.env["DATA_API_TOKEN"] : void 0) ?? "";
|
|
105
|
+
if (!apiUrl) {
|
|
106
|
+
return {
|
|
107
|
+
configError: makeConfigError(
|
|
108
|
+
"MISSING_DATA_API_URL",
|
|
109
|
+
"DATA_API_URL is not set. Ensure this service has declared a --uses edge to a database block so Vantis injects the runtime env vars."
|
|
110
|
+
)
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
if (!apiToken) {
|
|
114
|
+
return {
|
|
115
|
+
configError: makeConfigError(
|
|
116
|
+
"MISSING_DATA_API_TOKEN",
|
|
117
|
+
"DATA_API_TOKEN is not set. Ensure this service has declared a --uses edge to a database block so Vantis injects the runtime env vars."
|
|
118
|
+
)
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
return { apiUrl, apiToken };
|
|
122
|
+
}
|
|
123
|
+
function translateWildcard(pattern) {
|
|
124
|
+
return pattern.replace(/%/g, "*");
|
|
125
|
+
}
|
|
126
|
+
function quoteInElement(v) {
|
|
127
|
+
const s = String(v);
|
|
128
|
+
if (/[,()"\\]/.test(s)) {
|
|
129
|
+
return `"${s.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
|
|
130
|
+
}
|
|
131
|
+
return s;
|
|
132
|
+
}
|
|
133
|
+
function encodeIn(values) {
|
|
134
|
+
return `(${values.map(quoteInElement).join(",")})`;
|
|
135
|
+
}
|
|
136
|
+
function defaultState(tableName) {
|
|
137
|
+
return {
|
|
138
|
+
table: tableName,
|
|
139
|
+
filters: [],
|
|
140
|
+
selectCols: "*",
|
|
141
|
+
orderParts: [],
|
|
142
|
+
limitVal: null,
|
|
143
|
+
limitReferencedTable: null,
|
|
144
|
+
offsetVal: null,
|
|
145
|
+
countMode: null,
|
|
146
|
+
headMode: false,
|
|
147
|
+
singleMode: false,
|
|
148
|
+
maybeSingleMode: false,
|
|
149
|
+
writeOp: null,
|
|
150
|
+
writeBody: null,
|
|
151
|
+
writeSelect: false,
|
|
152
|
+
writeSelectCols: "*",
|
|
153
|
+
onConflict: null,
|
|
154
|
+
ignoreDuplicates: false,
|
|
155
|
+
embeds: [],
|
|
156
|
+
searchQuery: null,
|
|
157
|
+
searchLimit: null
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
var QueryBuilder = class {
|
|
161
|
+
_state;
|
|
162
|
+
constructor(tableName, state) {
|
|
163
|
+
this._state = state ?? defaultState(tableName);
|
|
164
|
+
}
|
|
165
|
+
// ─── Filters ────────────────────────────────────────────────────────────
|
|
166
|
+
/**
|
|
167
|
+
* Equality filter: column = value.
|
|
168
|
+
*
|
|
169
|
+
* The value is emitted raw (not quoted). URLSearchParams URL-encodes it, which
|
|
170
|
+
* is correct and sufficient for scalar operators — PostgREST takes the whole
|
|
171
|
+
* value after `eq.` and does NOT strip double-quotes for scalar ops (quoting
|
|
172
|
+
* would cause a literal string mismatch, e.g. `email=eq."a@b.com"` → 0 rows).
|
|
173
|
+
*/
|
|
174
|
+
eq(col, val) {
|
|
175
|
+
this._assertNotSearch("eq");
|
|
176
|
+
this._state.filters.push(`${col}=eq.${String(val)}`);
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
/**
|
|
180
|
+
* Inequality filter: column != value.
|
|
181
|
+
*
|
|
182
|
+
* Value emitted raw — see eq() for the scalar-quoting rationale.
|
|
183
|
+
*/
|
|
184
|
+
neq(col, val) {
|
|
185
|
+
this._assertNotSearch("neq");
|
|
186
|
+
this._state.filters.push(`${col}=neq.${String(val)}`);
|
|
187
|
+
return this;
|
|
188
|
+
}
|
|
189
|
+
/**
|
|
190
|
+
* Greater-than filter: column > value.
|
|
191
|
+
*
|
|
192
|
+
* Value emitted raw — see eq() for the scalar-quoting rationale.
|
|
193
|
+
*/
|
|
194
|
+
gt(col, val) {
|
|
195
|
+
this._assertNotSearch("gt");
|
|
196
|
+
this._state.filters.push(`${col}=gt.${String(val)}`);
|
|
197
|
+
return this;
|
|
198
|
+
}
|
|
199
|
+
/**
|
|
200
|
+
* Greater-than-or-equal filter: column >= value.
|
|
201
|
+
*
|
|
202
|
+
* Value emitted raw — see eq() for the scalar-quoting rationale.
|
|
203
|
+
*/
|
|
204
|
+
gte(col, val) {
|
|
205
|
+
this._assertNotSearch("gte");
|
|
206
|
+
this._state.filters.push(`${col}=gte.${String(val)}`);
|
|
207
|
+
return this;
|
|
208
|
+
}
|
|
209
|
+
/**
|
|
210
|
+
* Less-than filter: column < value.
|
|
211
|
+
*
|
|
212
|
+
* Value emitted raw — see eq() for the scalar-quoting rationale.
|
|
213
|
+
*/
|
|
214
|
+
lt(col, val) {
|
|
215
|
+
this._assertNotSearch("lt");
|
|
216
|
+
this._state.filters.push(`${col}=lt.${String(val)}`);
|
|
217
|
+
return this;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Less-than-or-equal filter: column <= value.
|
|
221
|
+
*
|
|
222
|
+
* Value emitted raw — see eq() for the scalar-quoting rationale.
|
|
223
|
+
*/
|
|
224
|
+
lte(col, val) {
|
|
225
|
+
this._assertNotSearch("lte");
|
|
226
|
+
this._state.filters.push(`${col}=lte.${String(val)}`);
|
|
227
|
+
return this;
|
|
228
|
+
}
|
|
229
|
+
/**
|
|
230
|
+
* LIKE filter. Pattern uses `%` (translated to PostgREST `*` on the wire)
|
|
231
|
+
* or `*` directly. Example: `.like('email', '%@example.com')`.
|
|
232
|
+
*
|
|
233
|
+
* The wildcard-translated pattern is emitted raw. URLSearchParams URL-encodes
|
|
234
|
+
* it, which is correct and sufficient — PostgREST takes the whole value after
|
|
235
|
+
* `like.` as the pattern. Do NOT quote like patterns: PostgREST does NOT strip
|
|
236
|
+
* double-quotes for scalar operators, so `like."a.b*"` would match a literal
|
|
237
|
+
* value containing `"` rather than performing `LIKE 'a.b%'`.
|
|
238
|
+
*
|
|
239
|
+
* NOTE: a LITERAL `*` in a user-supplied substring cannot be escaped —
|
|
240
|
+
* PostgREST has no like-escape mechanism and `*` is always the wildcard.
|
|
241
|
+
* Strip or reject literal `*` from user input at the call site if needed.
|
|
242
|
+
*/
|
|
243
|
+
like(col, pattern) {
|
|
244
|
+
this._assertNotSearch("like");
|
|
245
|
+
this._state.filters.push(`${col}=like.${translateWildcard(pattern)}`);
|
|
246
|
+
return this;
|
|
247
|
+
}
|
|
248
|
+
/**
|
|
249
|
+
* Case-insensitive LIKE filter. Pattern uses `%` or `*`.
|
|
250
|
+
*
|
|
251
|
+
* Pattern emitted raw after wildcard translation — see like() for the
|
|
252
|
+
* scalar-quoting rationale.
|
|
253
|
+
*/
|
|
254
|
+
ilike(col, pattern) {
|
|
255
|
+
this._assertNotSearch("ilike");
|
|
256
|
+
this._state.filters.push(`${col}=ilike.${translateWildcard(pattern)}`);
|
|
257
|
+
return this;
|
|
258
|
+
}
|
|
259
|
+
/**
|
|
260
|
+
* IN filter: column IN (values).
|
|
261
|
+
* Each element is quoted per the PostgREST URL grammar.
|
|
262
|
+
*/
|
|
263
|
+
in(col, values) {
|
|
264
|
+
this._assertNotSearch("in");
|
|
265
|
+
this._state.filters.push(`${col}=in.${encodeIn(values)}`);
|
|
266
|
+
return this;
|
|
267
|
+
}
|
|
268
|
+
/**
|
|
269
|
+
* IS filter: column IS true/false/null.
|
|
270
|
+
* PostgREST wire: `?col=is.true` / `?col=is.false` / `?col=is.null`.
|
|
271
|
+
* Values are fixed keywords — NOT subject to quoteFilterValue.
|
|
272
|
+
*/
|
|
273
|
+
is(col, val) {
|
|
274
|
+
this._assertNotSearch("is");
|
|
275
|
+
const wire = val === null ? "null" : val ? "true" : "false";
|
|
276
|
+
this._state.filters.push(`${col}=is.${wire}`);
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
select(columns, options) {
|
|
280
|
+
this._assertNotSearch("select");
|
|
281
|
+
if (this._state.writeOp !== null) {
|
|
282
|
+
this._state.writeSelect = true;
|
|
283
|
+
this._state.writeSelectCols = columns ?? "*";
|
|
284
|
+
} else {
|
|
285
|
+
this._state.selectCols = columns ?? "*";
|
|
286
|
+
}
|
|
287
|
+
if (options?.head) this._state.headMode = true;
|
|
288
|
+
if (options?.count) this._state.countMode = options.count;
|
|
289
|
+
return this;
|
|
290
|
+
}
|
|
291
|
+
// ─── FK embedding ────────────────────────────────────────────────────────
|
|
292
|
+
/**
|
|
293
|
+
* Embed a related table by FK.
|
|
294
|
+
*
|
|
295
|
+
* Adds the related table to the PostgREST select: `?select=*,<relatedTable>(*)`.
|
|
296
|
+
* PostgREST resolves the FK automatically (requires a schema-cache reload after
|
|
297
|
+
* FK changes — https://postgrest.org/en/v14/references/api/resource_embedding.html).
|
|
298
|
+
*
|
|
299
|
+
* The result row type gains `{ <relatedTable>: EmbedResult<TTableName, TEmbedTable> }`.
|
|
300
|
+
* The cardinality is resolved from the `Embeds` registry populated by `vantis data build`:
|
|
301
|
+
* - `'one'` (FK column is the referencing table's PK) → single object `Schema[TEmbedTable]`.
|
|
302
|
+
* - `'many'` (all other FKs — one-to-many inverse) → array `Schema[TEmbedTable][]`.
|
|
303
|
+
* - Unregistered relationships default to the array form.
|
|
304
|
+
*
|
|
305
|
+
* @example
|
|
306
|
+
* ```ts
|
|
307
|
+
* // posts.user_id FK → users (many-to-one; Embeds says 'one')
|
|
308
|
+
* const result = await data.from('posts').embed('users');
|
|
309
|
+
* // result.data[0].users — Schema['users'] (object, not array)
|
|
310
|
+
*
|
|
311
|
+
* // users.id → posts (one-to-many; Embeds says 'many')
|
|
312
|
+
* const result2 = await data.from('users').embed('posts');
|
|
313
|
+
* // result2.data[0].posts — Schema['posts'][] (array)
|
|
314
|
+
* ```
|
|
315
|
+
*/
|
|
316
|
+
embed(relatedTable) {
|
|
317
|
+
this._assertNotSearch("embed");
|
|
318
|
+
const name = relatedTable;
|
|
319
|
+
if (!this._state.embeds.includes(name)) {
|
|
320
|
+
this._state.embeds.push(name);
|
|
321
|
+
}
|
|
322
|
+
return this;
|
|
323
|
+
}
|
|
324
|
+
// ─── Modifiers ───────────────────────────────────────────────────────────
|
|
325
|
+
/**
|
|
326
|
+
* Order results by a column.
|
|
327
|
+
* @param col Column name.
|
|
328
|
+
* @param options ascending (default true), nullsFirst (default undefined), referencedTable.
|
|
329
|
+
*/
|
|
330
|
+
order(col, options) {
|
|
331
|
+
this._assertNotSearch("order");
|
|
332
|
+
const asc = options?.ascending !== false;
|
|
333
|
+
const dir = asc ? "asc" : "desc";
|
|
334
|
+
const nulls = options?.nullsFirst === true ? ".nullsfirst" : options?.nullsFirst === false ? ".nullslast" : "";
|
|
335
|
+
const prefix = options?.referencedTable ? `${options.referencedTable}.` : "";
|
|
336
|
+
this._state.orderParts.push(`${prefix}${col}.${dir}${nulls}`);
|
|
337
|
+
return this;
|
|
338
|
+
}
|
|
339
|
+
/**
|
|
340
|
+
* Limit the number of rows returned.
|
|
341
|
+
*
|
|
342
|
+
* When `referencedTable` is set, emits `?<referencedTable>.limit=n` to limit
|
|
343
|
+
* an embedded resource (PostgREST embedded-resource limiting). Without
|
|
344
|
+
* `referencedTable`, emits the top-level `?limit=n`.
|
|
345
|
+
*/
|
|
346
|
+
limit(n, options) {
|
|
347
|
+
this._assertNotSearch("limit");
|
|
348
|
+
this._state.limitVal = n;
|
|
349
|
+
if (options?.referencedTable) {
|
|
350
|
+
this._state.limitReferencedTable = options.referencedTable;
|
|
351
|
+
}
|
|
352
|
+
return this;
|
|
353
|
+
}
|
|
354
|
+
/**
|
|
355
|
+
* Return a range of rows by offset.
|
|
356
|
+
* @param from Start offset (0-indexed).
|
|
357
|
+
* @param to End offset (inclusive).
|
|
358
|
+
*/
|
|
359
|
+
range(from, to) {
|
|
360
|
+
this._assertNotSearch("range");
|
|
361
|
+
this._state.offsetVal = from;
|
|
362
|
+
this._state.limitVal = to - from + 1;
|
|
363
|
+
return this;
|
|
364
|
+
}
|
|
365
|
+
// ─── Writes ──────────────────────────────────────────────────────────────
|
|
366
|
+
/**
|
|
367
|
+
* Insert one or more rows.
|
|
368
|
+
*
|
|
369
|
+
* By default returns `data: null` (PostgREST `return=minimal`).
|
|
370
|
+
* Chain `.select()` to get the inserted rows back (`return=representation`).
|
|
371
|
+
*
|
|
372
|
+
* When the Go emitter (`vantis data build`) has registered the table in
|
|
373
|
+
* `SchemaInsert`, the payload enforces required NOT-NULL-no-default columns
|
|
374
|
+
* at compile time. Tables not yet registered fall back to `Partial<Row>`.
|
|
375
|
+
*/
|
|
376
|
+
insert(values) {
|
|
377
|
+
this._assertNotSearch("insert");
|
|
378
|
+
this._state.writeOp = "insert";
|
|
379
|
+
this._state.writeBody = values;
|
|
380
|
+
return this;
|
|
381
|
+
}
|
|
382
|
+
/**
|
|
383
|
+
* Update rows matching the current filters.
|
|
384
|
+
*
|
|
385
|
+
* By default returns `data: null`. Chain `.select()` for the updated rows.
|
|
386
|
+
*
|
|
387
|
+
* `values` is always `Partial<Row>` — you send only the columns to change.
|
|
388
|
+
* For insert/upsert, see `.insert()` and `.upsert()` (strict SchemaInsert typing).
|
|
389
|
+
*/
|
|
390
|
+
update(values) {
|
|
391
|
+
this._assertNotSearch("update");
|
|
392
|
+
this._state.writeOp = "update";
|
|
393
|
+
this._state.writeBody = values;
|
|
394
|
+
return this;
|
|
395
|
+
}
|
|
396
|
+
/**
|
|
397
|
+
* Delete rows matching the current filters.
|
|
398
|
+
*
|
|
399
|
+
* By default returns `data: null`. Chain `.select()` for the deleted rows.
|
|
400
|
+
*/
|
|
401
|
+
delete() {
|
|
402
|
+
this._assertNotSearch("delete");
|
|
403
|
+
this._state.writeOp = "delete";
|
|
404
|
+
this._state.writeBody = null;
|
|
405
|
+
return this;
|
|
406
|
+
}
|
|
407
|
+
/**
|
|
408
|
+
* Upsert one or more rows.
|
|
409
|
+
*
|
|
410
|
+
* Uses `POST + Prefer: resolution=merge-duplicates` (or `ignore-duplicates`).
|
|
411
|
+
* By default returns `data: null`. Chain `.select()` for the upserted rows.
|
|
412
|
+
*/
|
|
413
|
+
upsert(values, options) {
|
|
414
|
+
this._assertNotSearch("upsert");
|
|
415
|
+
this._state.writeOp = "upsert";
|
|
416
|
+
this._state.writeBody = values;
|
|
417
|
+
if (options?.onConflict !== void 0) this._state.onConflict = options.onConflict;
|
|
418
|
+
if (options?.ignoreDuplicates === true) this._state.ignoreDuplicates = true;
|
|
419
|
+
return this;
|
|
420
|
+
}
|
|
421
|
+
// ─── Result narrowers ────────────────────────────────────────────────────
|
|
422
|
+
/**
|
|
423
|
+
* Expect exactly one row. Returns `data: TRow` (not an array).
|
|
424
|
+
* If zero or more than one row is returned, PostgREST returns a 406 and this
|
|
425
|
+
* resolves to an error.
|
|
426
|
+
*
|
|
427
|
+
* Sets `Accept: application/vnd.pgrst.object+json`.
|
|
428
|
+
*/
|
|
429
|
+
single() {
|
|
430
|
+
this._assertNotSearch("single");
|
|
431
|
+
this._state.singleMode = true;
|
|
432
|
+
return this;
|
|
433
|
+
}
|
|
434
|
+
/**
|
|
435
|
+
* Expect zero or one row. Returns `data: TRow | null`.
|
|
436
|
+
*
|
|
437
|
+
* Client-side normalization (mirrors postgrest-js):
|
|
438
|
+
* - 0 rows → `{ data: null, error: null }` (no error)
|
|
439
|
+
* - 1 row → `{ data: <row>, error: null }`
|
|
440
|
+
* - >1 rows → `{ data: null, error: { code: 'PGRST116', ... } }`
|
|
441
|
+
*
|
|
442
|
+
* Uses the normal array `Accept: application/json` — PostgREST returns the
|
|
443
|
+
* array, and normalization happens client-side. This avoids the 406 that
|
|
444
|
+
* `vnd.pgrst.object+json` would return for 0 rows.
|
|
445
|
+
*/
|
|
446
|
+
maybeSingle() {
|
|
447
|
+
this._assertNotSearch("maybeSingle");
|
|
448
|
+
this._state.maybeSingleMode = true;
|
|
449
|
+
return this;
|
|
450
|
+
}
|
|
451
|
+
// ─── Full-text search ────────────────────────────────────────────────────
|
|
452
|
+
/**
|
|
453
|
+
* Full-text search via the engine-provisioned rank RPC.
|
|
454
|
+
*
|
|
455
|
+
* Routes to `GET /rpc/search_<table>?query=<text>` (the PostgREST `/rpc`
|
|
456
|
+
* path, not the table path). Results are pre-sorted most-relevant-first by
|
|
457
|
+
* the server (`ts_rank_cd` ordering inside the function). Each result row
|
|
458
|
+
* carries a `rank` field — an OPAQUE ordering-only relevance score.
|
|
459
|
+
*
|
|
460
|
+
* The four swap invariants (VAN-830 / ADR-0044):
|
|
461
|
+
* (i) One entry point — `.search(query, opts)`. Engine specifics never exposed.
|
|
462
|
+
* (ii) Text query in — engine-specific parsing is internal.
|
|
463
|
+
* (iii) Opaque `rank` — ordering-only, never normalized. A future BM25/rerank
|
|
464
|
+
* swap changes the scale, not the contract.
|
|
465
|
+
* (iv) Pluggable transport — currently PostgREST `/rpc`; semantic search goes
|
|
466
|
+
* to the AI gateway (opt-in, reserved).
|
|
467
|
+
*
|
|
468
|
+
* Security (INV-01/INV-02):
|
|
469
|
+
* - The query text is passed via URLSearchParams (URL-encoded by the browser
|
|
470
|
+
* URLSearchParams API) — NOT double-quoted (no PostgREST scalar-op quoting
|
|
471
|
+
* applies to RPC function arguments).
|
|
472
|
+
* - The DATA_API_TOKEN is in the Authorization header ONLY, never in the URL.
|
|
473
|
+
* - Filters/select/order from any previously chained methods are NOT emitted
|
|
474
|
+
* in search mode — the RPC returns the full row shape + rank, ordered by
|
|
475
|
+
* the server.
|
|
476
|
+
*
|
|
477
|
+
* Requires the table to have at least one `searchable: true` column in
|
|
478
|
+
* `vantis.schema.json` and a `vantis data apply` that provisions the search function.
|
|
479
|
+
*
|
|
480
|
+
* @example
|
|
481
|
+
* ```ts
|
|
482
|
+
* const result = await data.from('articles').search('hello world');
|
|
483
|
+
* if (result.error) { ... }
|
|
484
|
+
* for (const row of result.data) {
|
|
485
|
+
* console.log(row.title, row.rank); // rank: opaque ordering-only score
|
|
486
|
+
* }
|
|
487
|
+
* ```
|
|
488
|
+
*
|
|
489
|
+
* With a limit:
|
|
490
|
+
* ```ts
|
|
491
|
+
* const result = await data.from('articles').search('typescript', { limit: 10 });
|
|
492
|
+
* ```
|
|
493
|
+
*/
|
|
494
|
+
search(query, opts) {
|
|
495
|
+
const _s = this._state;
|
|
496
|
+
if (_s.filters.length > 0 || _s.embeds.length > 0 || _s.orderParts.length > 0 || _s.limitVal !== null || _s.offsetVal !== null || _s.selectCols !== "*" || _s.writeOp !== null || _s.singleMode || _s.headMode || _s.countMode !== null || _s.maybeSingleMode) {
|
|
497
|
+
throw new Error(
|
|
498
|
+
`@vantis/data: .search() must be called directly on data.from('${_s.table}') in v0 \u2014 it cannot be combined with .eq()/.select()/.order()/.embed()/.limit() or a write. Apply those before search is not yet supported; remove them or filter the returned results in code.`
|
|
499
|
+
);
|
|
500
|
+
}
|
|
501
|
+
this._state.searchQuery = query;
|
|
502
|
+
this._state.searchLimit = opts?.limit ?? null;
|
|
503
|
+
return this;
|
|
504
|
+
}
|
|
505
|
+
// ─── Atomic verb modifier ─────────────────────────────────────────────────
|
|
506
|
+
/**
|
|
507
|
+
* Atomically modify a single row via the server-side verb RPC
|
|
508
|
+
* (`__vantis_modify_<table>`). Each value in `ops` must be a VerbEnvelope
|
|
509
|
+
* returned by one of the verb helpers (increment, decrement, multiply, divide,
|
|
510
|
+
* min, max, toggle, serverTimestamp, concat, merge, jsonSet, setOnInsert).
|
|
511
|
+
* Bare objects or plain values are rejected synchronously.
|
|
512
|
+
*
|
|
513
|
+
* Wire: `POST /rpc/__vantis_modify_<table>` `Content-Profile: api`
|
|
514
|
+
* ```json
|
|
515
|
+
* { "__vantis_match": {...}, "__vantis_ops": {...},
|
|
516
|
+
* "__vantis_row"?: {...}, "__vantis_guard"?: {...} }
|
|
517
|
+
* ```
|
|
518
|
+
*
|
|
519
|
+
* Returns:
|
|
520
|
+
* - `{ data: [row], error: null }` — applied (UPDATE path).
|
|
521
|
+
* - `{ data: [], error: null }` — guard miss or row absent (not-applied).
|
|
522
|
+
* Check `result.data?.length === 0` in your application logic.
|
|
523
|
+
* - `{ data: null, error }` — a DataError from PostgREST or the engine.
|
|
524
|
+
*
|
|
525
|
+
* Security: token in Authorization header only (INV-01/INV-02). Lazy env
|
|
526
|
+
* read (INV-11). Browser guard (secondary; browser.ts export condition is
|
|
527
|
+
* primary).
|
|
528
|
+
*
|
|
529
|
+
* @example
|
|
530
|
+
* ```ts
|
|
531
|
+
* const result = await data.from('orders').modify(
|
|
532
|
+
* { total: increment(10) },
|
|
533
|
+
* { id: orderId },
|
|
534
|
+
* { guard: { total: { gte: 0 } } },
|
|
535
|
+
* );
|
|
536
|
+
* if (!result.error && result.data?.length === 0) {
|
|
537
|
+
* // guard miss — row unchanged
|
|
538
|
+
* }
|
|
539
|
+
* ```
|
|
540
|
+
*/
|
|
541
|
+
async modify(ops, match, opts) {
|
|
542
|
+
const _s = this._state;
|
|
543
|
+
if (_s.filters.length > 0 || _s.embeds.length > 0 || _s.orderParts.length > 0 || _s.limitVal !== null || _s.offsetVal !== null || _s.selectCols !== "*" || _s.writeOp !== null || _s.singleMode || _s.headMode || _s.countMode !== null || _s.maybeSingleMode || _s.searchQuery !== null) {
|
|
544
|
+
throw new Error(
|
|
545
|
+
`@vantis/data: .modify() must be called directly on data.from('${_s.table}') \u2014 it cannot be combined with filters, .select(), .order(), .embed(), .limit(), writes, or .search().`
|
|
546
|
+
);
|
|
547
|
+
}
|
|
548
|
+
const serializedOps = /* @__PURE__ */ Object.create(null);
|
|
549
|
+
for (const [col, envelope] of Object.entries(
|
|
550
|
+
ops
|
|
551
|
+
)) {
|
|
552
|
+
if (!isVerbEnvelope(envelope)) {
|
|
553
|
+
throw new Error(
|
|
554
|
+
`@vantis/data: .modify() ops['${col}'] is not a VerbEnvelope. Use a verb helper: increment(), decrement(), toggle(), serverTimestamp(), etc.`
|
|
555
|
+
);
|
|
556
|
+
}
|
|
557
|
+
const entry = { op: envelope.op };
|
|
558
|
+
if (envelope.v !== void 0) entry.v = envelope.v;
|
|
559
|
+
if (envelope.path !== void 0) entry.path = envelope.path;
|
|
560
|
+
serializedOps[col] = entry;
|
|
561
|
+
}
|
|
562
|
+
const body = /* @__PURE__ */ Object.create(null);
|
|
563
|
+
body["__vantis_match"] = match;
|
|
564
|
+
body["__vantis_ops"] = serializedOps;
|
|
565
|
+
if (opts?.upsert !== void 0) body["__vantis_row"] = opts.upsert;
|
|
566
|
+
if (opts?.guard !== void 0) body["__vantis_guard"] = opts.guard;
|
|
567
|
+
const rpcName = `__vantis_modify_${_s.table}`;
|
|
568
|
+
const rpcResult = await executeRpc(rpcName, body);
|
|
569
|
+
if (rpcResult.error) {
|
|
570
|
+
return {
|
|
571
|
+
data: null,
|
|
572
|
+
error: rpcResult.error,
|
|
573
|
+
count: null,
|
|
574
|
+
status: rpcResult.status,
|
|
575
|
+
statusText: rpcResult.statusText
|
|
576
|
+
};
|
|
577
|
+
}
|
|
578
|
+
if (rpcResult.data !== null) {
|
|
579
|
+
if (!Array.isArray(rpcResult.data)) {
|
|
580
|
+
return {
|
|
581
|
+
data: null,
|
|
582
|
+
error: makeConfigError(
|
|
583
|
+
"INVALID_RESPONSE",
|
|
584
|
+
`__vantis_modify_${_s.table} returned a non-array response body. Expected a JSON array (SETOF api.${_s.table}).`
|
|
585
|
+
),
|
|
586
|
+
count: null,
|
|
587
|
+
status: rpcResult.status,
|
|
588
|
+
statusText: rpcResult.statusText
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
return {
|
|
592
|
+
data: rpcResult.data,
|
|
593
|
+
error: null,
|
|
594
|
+
count: null,
|
|
595
|
+
status: rpcResult.status,
|
|
596
|
+
statusText: rpcResult.statusText
|
|
597
|
+
};
|
|
598
|
+
}
|
|
599
|
+
return {
|
|
600
|
+
data: null,
|
|
601
|
+
error: makeConfigError(
|
|
602
|
+
"INVALID_RESPONSE",
|
|
603
|
+
`__vantis_modify_${_s.table} returned an unreadable response. Expected a JSON array (SETOF api.${_s.table}).`
|
|
604
|
+
),
|
|
605
|
+
count: null,
|
|
606
|
+
status: rpcResult.status,
|
|
607
|
+
statusText: rpcResult.statusText
|
|
608
|
+
};
|
|
609
|
+
}
|
|
610
|
+
// ─── Private helpers ─────────────────────────────────────────────────────
|
|
611
|
+
/**
|
|
612
|
+
* Guard: throws synchronously if `.search()` has already been called on this
|
|
613
|
+
* builder. `.search()` is terminal in v0 — no mutator may be chained after it.
|
|
614
|
+
*
|
|
615
|
+
* Without this guard, a trailing mutator (e.g. `.eq()`) would modify `_state`
|
|
616
|
+
* fields that `_buildRequest`'s search branch silently ignores, producing an
|
|
617
|
+
* unscoped query that returns rows the caller did not intend to receive.
|
|
618
|
+
*
|
|
619
|
+
* Called at the top of every state-mutating method.
|
|
620
|
+
*/
|
|
621
|
+
_assertNotSearch(method) {
|
|
622
|
+
if (this._state.searchQuery !== null) {
|
|
623
|
+
throw new Error(
|
|
624
|
+
`@vantis/data: .search() is terminal \u2014 ${method}() cannot be chained after .search() on '${this._state.table}'. Build filters/projection before .search() is not supported in v0.`
|
|
625
|
+
);
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
// ─── Thenable ────────────────────────────────────────────────────────────
|
|
629
|
+
then(onfulfilled, onrejected) {
|
|
630
|
+
return this._execute().then(onfulfilled, onrejected);
|
|
631
|
+
}
|
|
632
|
+
// ─── Execution ───────────────────────────────────────────────────────────
|
|
633
|
+
async _execute() {
|
|
634
|
+
const env = resolveDataApiEnv();
|
|
635
|
+
if ("configError" in env) {
|
|
636
|
+
return { data: null, error: env.configError, count: null, status: 0, statusText: "" };
|
|
637
|
+
}
|
|
638
|
+
const apiUrl = env.apiUrl;
|
|
639
|
+
const { url, method, headers, body } = this._buildRequest(apiUrl, env.apiToken);
|
|
640
|
+
const controller = new AbortController();
|
|
641
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_TIMEOUT_MS);
|
|
642
|
+
try {
|
|
643
|
+
const res = await fetch(url, {
|
|
644
|
+
method,
|
|
645
|
+
headers,
|
|
646
|
+
body: body !== void 0 ? JSON.stringify(body, bigintReplacer) : void 0,
|
|
647
|
+
signal: controller.signal
|
|
648
|
+
});
|
|
649
|
+
return await this._parseResponse(res);
|
|
650
|
+
} catch (err) {
|
|
651
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
652
|
+
const dataErr2 = makeConfigError(
|
|
653
|
+
"TIMEOUT",
|
|
654
|
+
`Data API request timed out after ${DEFAULT_TIMEOUT_MS}ms.`
|
|
655
|
+
);
|
|
656
|
+
return { data: null, error: dataErr2, count: null, status: 0, statusText: "Timeout" };
|
|
657
|
+
}
|
|
658
|
+
const errName = err instanceof Error ? err.name : "UnknownError";
|
|
659
|
+
const dataErr = makeConfigError(
|
|
660
|
+
"NETWORK_ERROR",
|
|
661
|
+
`Data API request failed (${errName}). Check network connectivity and DATA_API_URL.`
|
|
662
|
+
);
|
|
663
|
+
return { data: null, error: dataErr, count: null, status: 0, statusText: "Network Error" };
|
|
664
|
+
} finally {
|
|
665
|
+
clearTimeout(timer);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
_buildRequest(baseUrl, apiToken) {
|
|
669
|
+
const s = this._state;
|
|
670
|
+
const isWrite = s.writeOp !== null;
|
|
671
|
+
const isDelete = s.writeOp === "delete";
|
|
672
|
+
const isUpsert = s.writeOp === "upsert";
|
|
673
|
+
let method;
|
|
674
|
+
if (!isWrite) {
|
|
675
|
+
method = s.headMode ? "HEAD" : "GET";
|
|
676
|
+
} else {
|
|
677
|
+
switch (s.writeOp) {
|
|
678
|
+
case "insert":
|
|
679
|
+
method = "POST";
|
|
680
|
+
break;
|
|
681
|
+
case "upsert":
|
|
682
|
+
method = "POST";
|
|
683
|
+
break;
|
|
684
|
+
case "update":
|
|
685
|
+
method = "PATCH";
|
|
686
|
+
break;
|
|
687
|
+
case "delete":
|
|
688
|
+
method = "DELETE";
|
|
689
|
+
break;
|
|
690
|
+
default:
|
|
691
|
+
method = "GET";
|
|
692
|
+
}
|
|
693
|
+
}
|
|
694
|
+
const headers = {
|
|
695
|
+
// token is in Authorization header ONLY — never in URL, never in errors.
|
|
696
|
+
"Authorization": `Bearer ${apiToken}`
|
|
697
|
+
};
|
|
698
|
+
if (!isWrite) {
|
|
699
|
+
headers["Accept-Profile"] = "api";
|
|
700
|
+
headers["Accept"] = "application/json";
|
|
701
|
+
} else {
|
|
702
|
+
headers["Content-Profile"] = "api";
|
|
703
|
+
if (!isDelete) {
|
|
704
|
+
headers["Content-Type"] = "application/json";
|
|
705
|
+
}
|
|
706
|
+
}
|
|
707
|
+
const preferParts = [];
|
|
708
|
+
if (isWrite) {
|
|
709
|
+
if (s.writeSelect) {
|
|
710
|
+
preferParts.push("return=representation");
|
|
711
|
+
} else {
|
|
712
|
+
preferParts.push("return=minimal");
|
|
713
|
+
}
|
|
714
|
+
}
|
|
715
|
+
if (isUpsert) {
|
|
716
|
+
if (s.ignoreDuplicates) {
|
|
717
|
+
preferParts.push("resolution=ignore-duplicates");
|
|
718
|
+
} else {
|
|
719
|
+
preferParts.push("resolution=merge-duplicates");
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
if (!isWrite && s.countMode !== null) {
|
|
723
|
+
preferParts.push(`count=${s.countMode}`);
|
|
724
|
+
}
|
|
725
|
+
if (preferParts.length > 0) {
|
|
726
|
+
headers["Prefer"] = preferParts.join(",");
|
|
727
|
+
}
|
|
728
|
+
if (s.singleMode) {
|
|
729
|
+
headers["Accept"] = "application/vnd.pgrst.object+json";
|
|
730
|
+
}
|
|
731
|
+
if (s.searchQuery !== null) {
|
|
732
|
+
const rpcUrl = `${baseUrl.replace(/\/$/, "")}/rpc/search_${encodeURIComponent(s.table)}`;
|
|
733
|
+
const rpcParams = new URLSearchParams();
|
|
734
|
+
rpcParams.set("query", s.searchQuery);
|
|
735
|
+
if (s.searchLimit !== null) {
|
|
736
|
+
rpcParams.set("limit", String(s.searchLimit));
|
|
737
|
+
}
|
|
738
|
+
const rpcQs = rpcParams.toString();
|
|
739
|
+
return {
|
|
740
|
+
url: rpcQs ? `${rpcUrl}?${rpcQs}` : rpcUrl,
|
|
741
|
+
method: "GET",
|
|
742
|
+
// invariant: .search() guard ensures headMode=false + writeOp=null
|
|
743
|
+
headers,
|
|
744
|
+
// invariant: read-only headers only (Accept-Profile:api, Authorization)
|
|
745
|
+
body: void 0
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
const tableUrl = `${baseUrl.replace(/\/$/, "")}/${encodeURIComponent(s.table)}`;
|
|
749
|
+
const params = new URLSearchParams();
|
|
750
|
+
for (const f of s.filters) {
|
|
751
|
+
const eqIdx = f.indexOf("=");
|
|
752
|
+
if (eqIdx !== -1) {
|
|
753
|
+
params.append(f.slice(0, eqIdx), f.slice(eqIdx + 1));
|
|
754
|
+
}
|
|
755
|
+
}
|
|
756
|
+
let selectStr;
|
|
757
|
+
if (isWrite && s.writeSelect) {
|
|
758
|
+
selectStr = s.writeSelectCols;
|
|
759
|
+
} else {
|
|
760
|
+
selectStr = s.selectCols;
|
|
761
|
+
}
|
|
762
|
+
if (s.embeds.length > 0) {
|
|
763
|
+
const embedParts = s.embeds.map((e) => `${e}(*)`).join(",");
|
|
764
|
+
selectStr = `${selectStr},${embedParts}`;
|
|
765
|
+
}
|
|
766
|
+
if (selectStr !== "*" || s.embeds.length > 0) {
|
|
767
|
+
params.set("select", selectStr);
|
|
768
|
+
}
|
|
769
|
+
if (s.orderParts.length > 0) {
|
|
770
|
+
params.set("order", s.orderParts.join(","));
|
|
771
|
+
}
|
|
772
|
+
if (s.limitVal !== null) {
|
|
773
|
+
const limitKey = s.limitReferencedTable ? `${s.limitReferencedTable}.limit` : "limit";
|
|
774
|
+
params.set(limitKey, String(s.limitVal));
|
|
775
|
+
}
|
|
776
|
+
if (s.offsetVal !== null) params.set("offset", String(s.offsetVal));
|
|
777
|
+
if (isUpsert && s.onConflict !== null) {
|
|
778
|
+
params.set("on_conflict", s.onConflict);
|
|
779
|
+
}
|
|
780
|
+
const qs = params.toString();
|
|
781
|
+
const url = qs ? `${tableUrl}?${qs}` : tableUrl;
|
|
782
|
+
const body = !isWrite || isDelete ? void 0 : s.writeBody;
|
|
783
|
+
return { url, method, headers, body };
|
|
784
|
+
}
|
|
785
|
+
async _parseResponse(res) {
|
|
786
|
+
const s = this._state;
|
|
787
|
+
if (!res.ok) {
|
|
788
|
+
const error = await parsePostgRESTError(res);
|
|
789
|
+
return { data: null, error, count: null, status: res.status, statusText: res.statusText };
|
|
790
|
+
}
|
|
791
|
+
let count = null;
|
|
792
|
+
if (s.countMode !== null) {
|
|
793
|
+
const contentRange = res.headers.get("Content-Range");
|
|
794
|
+
if (contentRange) {
|
|
795
|
+
const slash = contentRange.indexOf("/");
|
|
796
|
+
if (slash !== -1) {
|
|
797
|
+
const totalStr = contentRange.slice(slash + 1);
|
|
798
|
+
if (totalStr !== "*") {
|
|
799
|
+
const parsed = parseInt(totalStr, 10);
|
|
800
|
+
if (!isNaN(parsed)) count = parsed;
|
|
801
|
+
}
|
|
802
|
+
}
|
|
803
|
+
}
|
|
804
|
+
}
|
|
805
|
+
let data2 = null;
|
|
806
|
+
const isMinimalWrite = s.writeOp !== null && !s.writeSelect;
|
|
807
|
+
if (!s.headMode && !isMinimalWrite) {
|
|
808
|
+
const text = await res.text();
|
|
809
|
+
if (text) {
|
|
810
|
+
try {
|
|
811
|
+
data2 = JSON.parse(text);
|
|
812
|
+
} catch {
|
|
813
|
+
}
|
|
814
|
+
}
|
|
815
|
+
}
|
|
816
|
+
if (isMinimalWrite) {
|
|
817
|
+
return { data: null, error: null, count, status: res.status, statusText: res.statusText };
|
|
818
|
+
}
|
|
819
|
+
if (s.headMode) {
|
|
820
|
+
return { data: null, error: null, count, status: res.status, statusText: res.statusText };
|
|
821
|
+
}
|
|
822
|
+
if (s.maybeSingleMode) {
|
|
823
|
+
if (!Array.isArray(data2)) {
|
|
824
|
+
return { data: null, error: null, count, status: res.status, statusText: res.statusText };
|
|
825
|
+
}
|
|
826
|
+
if (data2.length === 0) {
|
|
827
|
+
return { data: null, error: null, count, status: res.status, statusText: res.statusText };
|
|
828
|
+
}
|
|
829
|
+
if (data2.length > 1) {
|
|
830
|
+
const err = {
|
|
831
|
+
kind: "postgrest",
|
|
832
|
+
code: "PGRST116",
|
|
833
|
+
message: "The result contains 0 or more than 1 rows.",
|
|
834
|
+
details: null,
|
|
835
|
+
hint: null
|
|
836
|
+
};
|
|
837
|
+
return { data: null, error: err, count: null, status: res.status, statusText: res.statusText };
|
|
838
|
+
}
|
|
839
|
+
return { data: data2[0], error: null, count, status: res.status, statusText: res.statusText };
|
|
840
|
+
}
|
|
841
|
+
if (s.singleMode) {
|
|
842
|
+
return { data: data2, error: null, count, status: res.status, statusText: res.statusText };
|
|
843
|
+
}
|
|
844
|
+
return { data: data2, error: null, count, status: res.status, statusText: res.statusText };
|
|
845
|
+
}
|
|
846
|
+
};
|
|
847
|
+
async function executeRpc(name, args) {
|
|
848
|
+
const env = resolveDataApiEnv();
|
|
849
|
+
if ("configError" in env) {
|
|
850
|
+
return { data: null, error: env.configError, count: null, status: 0, statusText: "" };
|
|
851
|
+
}
|
|
852
|
+
const apiUrl = env.apiUrl;
|
|
853
|
+
const apiToken = env.apiToken;
|
|
854
|
+
const url = `${apiUrl.replace(/\/$/, "")}/rpc/${encodeURIComponent(name)}`;
|
|
855
|
+
const headers = {
|
|
856
|
+
"Authorization": `Bearer ${apiToken}`,
|
|
857
|
+
"Content-Profile": "api",
|
|
858
|
+
"Content-Type": "application/json",
|
|
859
|
+
"Accept": "application/json"
|
|
860
|
+
};
|
|
861
|
+
const controller = new AbortController();
|
|
862
|
+
const timer = setTimeout(() => controller.abort(), DEFAULT_RPC_TIMEOUT_MS);
|
|
863
|
+
try {
|
|
864
|
+
const res = await fetch(url, {
|
|
865
|
+
method: "POST",
|
|
866
|
+
headers,
|
|
867
|
+
body: JSON.stringify(args ?? {}, bigintReplacer),
|
|
868
|
+
signal: controller.signal
|
|
869
|
+
});
|
|
870
|
+
if (!res.ok) {
|
|
871
|
+
const error = await parsePostgRESTError(res);
|
|
872
|
+
return { data: null, error, count: null, status: res.status, statusText: res.statusText };
|
|
873
|
+
}
|
|
874
|
+
let data2 = null;
|
|
875
|
+
if (res.status !== 204) {
|
|
876
|
+
const text = await res.text();
|
|
877
|
+
if (text) {
|
|
878
|
+
try {
|
|
879
|
+
data2 = JSON.parse(text);
|
|
880
|
+
} catch {
|
|
881
|
+
data2 = null;
|
|
882
|
+
}
|
|
883
|
+
}
|
|
884
|
+
}
|
|
885
|
+
return { data: data2, error: null, count: null, status: res.status, statusText: res.statusText };
|
|
886
|
+
} catch (err) {
|
|
887
|
+
if (err instanceof Error && err.name === "AbortError") {
|
|
888
|
+
return {
|
|
889
|
+
data: null,
|
|
890
|
+
error: makeConfigError("TIMEOUT", `Data API request timed out after ${DEFAULT_RPC_TIMEOUT_MS}ms.`),
|
|
891
|
+
count: null,
|
|
892
|
+
status: 0,
|
|
893
|
+
statusText: "Timeout"
|
|
894
|
+
};
|
|
895
|
+
}
|
|
896
|
+
const errName = err instanceof Error ? err.name : "UnknownError";
|
|
897
|
+
return {
|
|
898
|
+
data: null,
|
|
899
|
+
error: makeConfigError(
|
|
900
|
+
"NETWORK_ERROR",
|
|
901
|
+
`Data API request failed (${errName}). Check network connectivity and DATA_API_URL.`
|
|
902
|
+
),
|
|
903
|
+
count: null,
|
|
904
|
+
status: 0,
|
|
905
|
+
statusText: "Network Error"
|
|
906
|
+
};
|
|
907
|
+
} finally {
|
|
908
|
+
clearTimeout(timer);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
var data = {
|
|
912
|
+
/**
|
|
913
|
+
* Start a query against the given table.
|
|
914
|
+
*
|
|
915
|
+
* `tableName` is typed as `keyof Schema` — the augmented Schema interface
|
|
916
|
+
* populated by `vantis data build`.
|
|
917
|
+
*
|
|
918
|
+
* Returns a `QueryBuilder` that is also PromiseLike — `await` it directly.
|
|
919
|
+
*/
|
|
920
|
+
from(tableName) {
|
|
921
|
+
return new QueryBuilder(tableName);
|
|
922
|
+
},
|
|
923
|
+
/**
|
|
924
|
+
* Call a typed RPC (VAN-832). `data.run.<name>(args)` POSTs to /rpc/<name>
|
|
925
|
+
* and returns the typed result. Names + arg/return types come from the
|
|
926
|
+
* generated Rpc registry (`vantis data build`).
|
|
927
|
+
*
|
|
928
|
+
* @example
|
|
929
|
+
* ```ts
|
|
930
|
+
* const r = await data.run.place_order({ product_id: id, qty: 2 });
|
|
931
|
+
* if (r.error) { ... }
|
|
932
|
+
* const orderId = r.data; // typed from the fn() declaration
|
|
933
|
+
* ```
|
|
934
|
+
*/
|
|
935
|
+
run: new Proxy({}, {
|
|
936
|
+
get(_target, prop) {
|
|
937
|
+
if (typeof prop !== "string") return void 0;
|
|
938
|
+
if (prop === "then" || prop === "catch" || prop === "finally") return void 0;
|
|
939
|
+
return (args) => executeRpc(prop, args);
|
|
940
|
+
}
|
|
941
|
+
})
|
|
942
|
+
};
|
|
943
|
+
export {
|
|
944
|
+
concat,
|
|
945
|
+
data,
|
|
946
|
+
decrement,
|
|
947
|
+
divide,
|
|
948
|
+
increment,
|
|
949
|
+
jsonSet,
|
|
950
|
+
max,
|
|
951
|
+
merge,
|
|
952
|
+
min,
|
|
953
|
+
multiply,
|
|
954
|
+
serverTimestamp,
|
|
955
|
+
setOnInsert,
|
|
956
|
+
toggle
|
|
957
|
+
};
|
|
958
|
+
//# sourceMappingURL=index.mjs.map
|