@infaris/data 0.0.0-stage → 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/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) Infaris
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,126 @@
1
- # Temporary Holding Version
1
+ # @infaris/data
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ JavaScript client for the **Data API** of Hosting by Infaris: a REST API over your managed Postgres database, with
4
+ Row Level Security and the sign-in of Infaris Auth. Hosted in the EU, next to your database. Bundled shared error contract, works
5
+ in the browser, Node, Bun, Deno and edge runtimes.
6
+
7
+ ```bash
8
+ npm install @infaris/data
9
+ ```
10
+
11
+ ```ts
12
+ import { createClient } from '@infaris/data';
13
+ import type { Database } from './database.types'; // infaris-guard hosting db types <id> --out src/database.types.ts
14
+
15
+ // Hosting puts NEXT_PUBLIC_INFARIS_DATA_URL (and INFARIS_DATA_URL for server code) on every site the database is
16
+ // attached to while the Data API is on; the key is the one of the auth project (`hosting auth attach`).
17
+ const db = createClient<Database>(process.env.NEXT_PUBLIC_INFARIS_DATA_URL!, process.env.NEXT_PUBLIC_INFARIS_AUTH_KEY!, {
18
+ // A signed-in user of Infaris Auth: their requests run as `authenticated`, and auth.uid() is their id.
19
+ accessToken: () => auth.getAccessToken(), // auth = createClient(...) of @infaris/auth-js
20
+ });
21
+
22
+ const { data, error } = await db
23
+ .from('todos')
24
+ .select('id, title, project:projects(name)')
25
+ .eq('done', false)
26
+ .order('created_at', { ascending: false })
27
+ .range(0, 19);
28
+
29
+ await db.from('todos').insert({ title: 'Ship it' }).select().single();
30
+ await db.from('todos').update({ done: true }).eq('id', 3);
31
+ await db.from('todos').delete().eq('id', 3);
32
+ await db.rpc('search_todos', { q: 'eu' });
33
+ await db.schema('api').from('stats').select('*');
34
+ ```
35
+
36
+ ## Keys
37
+
38
+ | Key | Role in the database | Where |
39
+ | --- | --- | --- |
40
+ | Publishable key `ifr_pk_…` | `anon` | Browser and apps; only from the allowed origins of the auth project |
41
+ | Publishable key plus a user's access token | `authenticated` | Browser and apps, after sign-in |
42
+ | Secret key `ifr_sk_…` | `service_role` (bypasses RLS) | Servers only; refused from a browser |
43
+
44
+ Nothing is reachable until you put it in the API: `infaris-guard hosting db expose <id> --schema public --table todos --yes`
45
+ (`--dry-run` shows the plan and what anyone could read). Keep columns such as `email` or `iban` out with
46
+ `--exclude-columns email,iban` (or name the allowed ones with `--columns`): then `select('*')` fails for those roles,
47
+ so name the columns, e.g. `.select('id, name')`. A table only goes in with Row Level Security on, and
48
+ `infaris-guard hosting db lint <id>` fails your CI when something is open that should not be.
49
+
50
+ ## Types
51
+
52
+ `infaris-guard hosting db types <id> --out src/database.types.ts` writes the `Database` type. With it, everything is
53
+ checked at compile time:
54
+
55
+ ```ts
56
+ const { data } = await db.from('posts').select('id, heading:title, author:users(name), comments(text)');
57
+ // data: { id: number; heading: string; author: { name: string } | null; comments: { text: string }[] }[] | null
58
+
59
+ db.from('posts').select().eq('price', 'ten'); // error: price is a number
60
+ db.from('posts').insert({ author_id: id }); // error: title is required
61
+ ```
62
+
63
+ The select string is parsed for columns, aliases (`heading:title`), casts (`price::text`), JSON paths (`meta->>theme`),
64
+ embedded relations (one object for a many-to-one, an array for a one-to-many; `!inner` drops the `null`), spreads
65
+ (`...users(name)`) and aggregates (`count()`, `price.sum()`). A misspelled column becomes a type that says so.
66
+ `.returns<T>()` overrides the result when you know better.
67
+
68
+ ## Search and aggregates
69
+
70
+ ```ts
71
+ // infaris-guard hosting db search add <id> --schema public --table posts --columns title,body --config dutch
72
+ await db.from('posts').select('id, title').textSearch('fts', 'fiets bel', { type: 'websearch', config: 'dutch' });
73
+
74
+ // infaris-guard hosting db api set <id> --enabled --aggregates
75
+ await db.from('orders').select('customer_id, total:amount.sum(), count()');
76
+ ```
77
+
78
+ ## GraphQL
79
+
80
+ The same Data API also speaks GraphQL at `<url>/graphql/v1` once the owner switches it on
81
+ (`infaris-guard hosting db graphql enable <id>`): the same key and user token, so the same role, grants and row level
82
+ security. Generate typed operations with graphql-codegen from the schema of your role and pass them in:
83
+
84
+ ```ts
85
+ // infaris-guard hosting db graphql schema <id> --role authenticated --out schema.graphql
86
+ // graphql-codegen with the client preset and documentMode: 'string'
87
+ import { TodosDocument } from './gql/graphql';
88
+
89
+ const { data, errors } = await db.graphql(TodosDocument, { first: 20 });
90
+ // Persisted documents (persistedDocuments: true, registered with `hosting db graphql persist`): only the hash is sent.
91
+ await db.graphql(TodosDocument, { first: 20 }, { persisted: true });
92
+ ```
93
+
94
+ `errors[].extensions.code` names a refusal: `GRAPHQL_VALIDATION_FAILED` (also for a table or column your role does
95
+ not have), `max_depth`, `max_complexity`, `page_too_large`, `introspection_disabled`, `rate_limited` or a Postgres code
96
+ such as `42501`.
97
+
98
+ ## Queries
99
+
100
+ `select`, `insert`, `upsert`, `update`, `delete`; filters `eq`, `neq`, `gt`, `gte`, `lt`, `lte`, `like`, `ilike`,
101
+ `is`, `in`, `contains`, `containedBy`, `overlaps`, `textSearch`, `match`, `not`, `or`, `filter`; modifiers `order`,
102
+ `limit`, `range`, `single`, `maybeSingle`, `abortSignal`; counts with `select('*', { count: 'exact' })`.
103
+ The syntax is PostgREST's, so its documentation applies (embedding with `table(columns)`, `!inner` joins, and so on).
104
+
105
+ Every call resolves to `{ data, error, count, status }` and never throws; `throwOnError()` throws instead. An error
106
+ is a `DataApiError` with `code` (PostgREST's `PGRST…` or Postgres `42501`, or the gateway's `invalid_key`,
107
+ `invalid_token`, `origin_not_allowed`, `rate_limited`, `secret_key_in_browser`), `message`, `details` and `hint`.
108
+
109
+ ## Runtime and modules
110
+
111
+ Node.js 22.12 or newer, Bun, modern browsers and edge runtimes with `fetch`. ES modules and CommonJS are both supported. Server credentials must stay on the server.
112
+
113
+ Licensed under MIT. See [LICENSE](./LICENSE).
114
+
115
+ All API errors extend `InfarisError` (also exported here), with `code`, `status` and `message`. Existing error classes remain supported.
116
+
117
+ ## Client factory
118
+
119
+ Every Hosting SDK exports `createClient(options)`. Existing named factories and positional Auth/Data arguments remain available.
120
+
121
+ ```ts
122
+ import { createClient } from '@infaris/data';
123
+ const client = createClient({ url: process.env.NEXT_PUBLIC_INFARIS_DATA_URL!, key: process.env.NEXT_PUBLIC_INFARIS_AUTH_KEY! });
124
+ ```
125
+
126
+ The common error implementation is bundled; installing this SDK does not require a private workspace package.
package/dist/index.cjs ADDED
@@ -0,0 +1,492 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __commonJS = (cb, mod) => function __require() {
9
+ try {
10
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
11
+ } catch (e) {
12
+ throw mod = 0, e;
13
+ }
14
+ };
15
+ var __export = (target, all) => {
16
+ for (var name in all)
17
+ __defProp(target, name, { get: all[name], enumerable: true });
18
+ };
19
+ var __copyProps = (to, from, except, desc) => {
20
+ if (from && typeof from === "object" || typeof from === "function") {
21
+ for (let key of __getOwnPropNames(from))
22
+ if (!__hasOwnProp.call(to, key) && key !== except)
23
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
24
+ }
25
+ return to;
26
+ };
27
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
28
+ // If the importer is in node compatibility mode or this is not an ESM
29
+ // file that has been converted to a CommonJS file using a Babel-
30
+ // compatible transform (i.e. "__esModule" has not been set), then set
31
+ // "default" to the CommonJS "module.exports" for node compatibility.
32
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
33
+ mod
34
+ ));
35
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
36
+
37
+ // ../hosting-sdk/dist/index.cjs
38
+ var require_dist = __commonJS({
39
+ "../hosting-sdk/dist/index.cjs"(exports2, module2) {
40
+ "use strict";
41
+ var __defProp2 = Object.defineProperty;
42
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
43
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
44
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
45
+ var __export2 = (target, all) => {
46
+ for (var name in all)
47
+ __defProp2(target, name, { get: all[name], enumerable: true });
48
+ };
49
+ var __copyProps2 = (to, from, except, desc) => {
50
+ if (from && typeof from === "object" || typeof from === "function") {
51
+ for (let key of __getOwnPropNames2(from))
52
+ if (!__hasOwnProp2.call(to, key) && key !== except)
53
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
54
+ }
55
+ return to;
56
+ };
57
+ var __toCommonJS2 = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
58
+ var index_exports2 = {};
59
+ __export2(index_exports2, {
60
+ InfarisError: () => InfarisError3
61
+ });
62
+ module2.exports = __toCommonJS2(index_exports2);
63
+ var errorBrand = /* @__PURE__ */ Symbol.for("@infaris/hosting-sdk/error");
64
+ var InfarisError3 = class _InfarisError extends Error {
65
+ // SDK bundles contain separate copies; catch the shared base across bundles and module formats.
66
+ static [Symbol.hasInstance](value) {
67
+ if (this !== _InfarisError) return Function.prototype[Symbol.hasInstance].call(this, value);
68
+ return value instanceof Error && errorBrand in value && value[errorBrand] === true;
69
+ }
70
+ code;
71
+ status;
72
+ constructor(message, code = "sdk_error", status = null) {
73
+ super(message);
74
+ Object.defineProperty(this, errorBrand, { value: true });
75
+ this.name = "InfarisError";
76
+ this.code = code;
77
+ this.status = status;
78
+ }
79
+ };
80
+ }
81
+ });
82
+
83
+ // src/index.ts
84
+ var index_exports = {};
85
+ __export(index_exports, {
86
+ DataApiError: () => DataApiError,
87
+ DataClient: () => DataClient,
88
+ InfarisError: () => import_hosting_sdk2.InfarisError,
89
+ QueryBuilder: () => QueryBuilder,
90
+ SchemaClient: () => SchemaClient,
91
+ createClient: () => createClient,
92
+ quoteValue: () => quoteValue
93
+ });
94
+ module.exports = __toCommonJS(index_exports);
95
+
96
+ // src/graphql.ts
97
+ async function runGraphql(context, baseUrl, document2, variables, options = {}) {
98
+ const headers = await context.headers("POST", null);
99
+ headers["Content-Type"] = "application/json";
100
+ headers.Accept = "application/json";
101
+ if (options.schema) headers["Accept-Profile"] = options.schema;
102
+ const hash = typeof document2 === "string" ? void 0 : document2.__meta__?.hash;
103
+ const body = {};
104
+ if (options.persisted) {
105
+ if (!hash) throw new TypeError("persisted needs a document with __meta__.hash (graphql-codegen persistedDocuments: true)");
106
+ body.documentId = `sha256:${hash.replace(/^sha256:/, "")}`;
107
+ } else body.query = String(document2);
108
+ if (variables && Object.keys(variables).length) body.variables = variables;
109
+ if (options.operationName) body.operationName = options.operationName;
110
+ let response;
111
+ try {
112
+ response = await context.fetch(`${baseUrl}/graphql/v1`, { method: "POST", headers, body: JSON.stringify(body) });
113
+ } catch (cause) {
114
+ return { data: null, errors: [{ message: cause instanceof Error ? cause.message : String(cause), extensions: { code: "network_error" } }], status: 0 };
115
+ }
116
+ let parsed = {};
117
+ try {
118
+ parsed = await response.json();
119
+ } catch {
120
+ }
121
+ if (!parsed.errors && parsed.code && parsed.message) {
122
+ parsed.errors = [{ message: parsed.message, extensions: { code: parsed.code } }];
123
+ }
124
+ return { data: parsed.data ?? null, errors: parsed.errors?.length ? parsed.errors : null, status: response.status };
125
+ }
126
+
127
+ // src/types.ts
128
+ var import_hosting_sdk = __toESM(require_dist(), 1);
129
+ var DataApiError = class extends import_hosting_sdk.InfarisError {
130
+ code;
131
+ details;
132
+ hint;
133
+ status;
134
+ constructor(input) {
135
+ super(input.message, input.code, input.status);
136
+ this.name = "DataApiError";
137
+ this.code = input.code;
138
+ this.details = input.details ?? null;
139
+ this.hint = input.hint ?? null;
140
+ this.status = input.status ?? null;
141
+ }
142
+ };
143
+
144
+ // src/query.ts
145
+ function quoteValue(value) {
146
+ if (value === null) return "null";
147
+ const text = String(value);
148
+ return /[,()":\s\\{}]/.test(text) ? `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : text;
149
+ }
150
+ function containerValue(value) {
151
+ if (typeof value === "string") return value;
152
+ if (Array.isArray(value)) return `{${value.map(quoteValue).join(",")}}`;
153
+ return JSON.stringify(value);
154
+ }
155
+ var QueryBuilder = class {
156
+ constructor(ctx, path) {
157
+ this.ctx = ctx;
158
+ this.path = path;
159
+ }
160
+ ctx;
161
+ path;
162
+ method = "GET";
163
+ params = new URLSearchParams();
164
+ prefer = [];
165
+ body = void 0;
166
+ accept = null;
167
+ maybe = false;
168
+ signal;
169
+ retype() {
170
+ return this;
171
+ }
172
+ /** Overrides the result type when you know better (a view without types, a computed column). */
173
+ returns() {
174
+ return this.retype();
175
+ }
176
+ // --- operations -------------------------------------------------------------------------------------------------
177
+ /** Read rows (`GET`), or with a mutation before it, return the changed rows. `head: true` only counts. */
178
+ select(columns, options = {}) {
179
+ const cleaned = (columns ?? "*").replace(/\s+(?=([^"]*"[^"]*")*[^"]*$)/g, "");
180
+ this.params.set("select", cleaned);
181
+ if (this.method === "GET" || this.method === "HEAD") {
182
+ if (options.head) this.method = "HEAD";
183
+ } else {
184
+ this.prefer.push("return=representation");
185
+ }
186
+ if (options.count) this.prefer.push(`count=${options.count}`);
187
+ return this.retype();
188
+ }
189
+ insert(values, options = {}) {
190
+ this.method = "POST";
191
+ this.body = values;
192
+ if (Array.isArray(values)) this.columns(values);
193
+ if (options.defaultToNull === false) this.prefer.push("missing=default");
194
+ if (options.count) this.prefer.push(`count=${options.count}`);
195
+ return this.retype();
196
+ }
197
+ /** Insert or, on a conflict with `onConflict` (default the primary key), update (or skip with `ignoreDuplicates`). */
198
+ upsert(values, options = {}) {
199
+ this.method = "POST";
200
+ this.body = values;
201
+ this.prefer.push(`resolution=${options.ignoreDuplicates ? "ignore" : "merge"}-duplicates`);
202
+ if (options.onConflict) this.params.set("on_conflict", options.onConflict);
203
+ if (Array.isArray(values)) this.columns(values);
204
+ if (options.defaultToNull === false) this.prefer.push("missing=default");
205
+ if (options.count) this.prefer.push(`count=${options.count}`);
206
+ return this.retype();
207
+ }
208
+ update(values, options = {}) {
209
+ this.method = "PATCH";
210
+ this.body = values;
211
+ if (options.count) this.prefer.push(`count=${options.count}`);
212
+ return this.retype();
213
+ }
214
+ delete(options = {}) {
215
+ this.method = "DELETE";
216
+ if (options.count) this.prefer.push(`count=${options.count}`);
217
+ return this.retype();
218
+ }
219
+ /** `columns` for a bulk insert, so rows that leave a column out get its default instead of null. */
220
+ columns(rows) {
221
+ const keys = /* @__PURE__ */ new Set();
222
+ for (const row of rows) for (const key of Object.keys(row)) keys.add(key);
223
+ if (keys.size) this.params.set("columns", [...keys].map((k) => `"${k}"`).join(","));
224
+ }
225
+ // --- filters ----------------------------------------------------------------------------------------------------
226
+ /** Any PostgREST filter: `filter('age', 'gte', 18)`, `filter('tags', 'cs', '{a,b}')`. */
227
+ filter(column, operator, value) {
228
+ this.params.append(column, `${operator}.${value === null ? "null" : String(value)}`);
229
+ return this;
230
+ }
231
+ eq(column, value) {
232
+ return this.filter(column, "eq", value);
233
+ }
234
+ neq(column, value) {
235
+ return this.filter(column, "neq", value);
236
+ }
237
+ gt(column, value) {
238
+ return this.filter(column, "gt", value);
239
+ }
240
+ gte(column, value) {
241
+ return this.filter(column, "gte", value);
242
+ }
243
+ lt(column, value) {
244
+ return this.filter(column, "lt", value);
245
+ }
246
+ lte(column, value) {
247
+ return this.filter(column, "lte", value);
248
+ }
249
+ like(column, pattern) {
250
+ return this.filter(column, "like", pattern);
251
+ }
252
+ ilike(column, pattern) {
253
+ return this.filter(column, "ilike", pattern);
254
+ }
255
+ /** `IS NULL`, `IS TRUE`, `IS FALSE`. */
256
+ is(column, value) {
257
+ return this.filter(column, "is", value);
258
+ }
259
+ in(column, values) {
260
+ return this.filter(column, "in", `(${values.map(quoteValue).join(",")})`);
261
+ }
262
+ contains(column, value) {
263
+ return this.filter(column, "cs", containerValue(value));
264
+ }
265
+ containedBy(column, value) {
266
+ return this.filter(column, "cd", containerValue(value));
267
+ }
268
+ overlaps(column, value) {
269
+ return this.filter(column, "ov", containerValue(value));
270
+ }
271
+ /**
272
+ * Full-text search on a tsvector column (`infaris-guard hosting db search add` makes one):
273
+ * `textSearch('fts', 'fiets bel', { type: 'websearch', config: 'dutch' })`.
274
+ */
275
+ textSearch(column, query, options = {}) {
276
+ const op = options.type === "plain" ? "plfts" : options.type === "phrase" ? "phfts" : options.type === "websearch" ? "wfts" : "fts";
277
+ return this.filter(column, options.config ? `${op}(${options.config})` : op, query);
278
+ }
279
+ /** Several equalities at once. */
280
+ match(query) {
281
+ for (const [column, value] of Object.entries(query)) this.filter(column, "eq", value);
282
+ return this;
283
+ }
284
+ /** Negate a filter: `not('status', 'eq', 'done')`. */
285
+ not(column, operator, value) {
286
+ return this.filter(column, `not.${operator}`, value);
287
+ }
288
+ /** Raw PostgREST `or`: `or('status.eq.open,priority.gt.3')`; `referencedTable` filters an embedded table. */
289
+ or(filters, options = {}) {
290
+ this.params.append(options.referencedTable ? `${options.referencedTable}.or` : "or", `(${filters})`);
291
+ return this;
292
+ }
293
+ order(column, options = {}) {
294
+ const key = options.referencedTable ? `${options.referencedTable}.order` : "order";
295
+ const term = `${column}.${options.ascending === false ? "desc" : "asc"}${options.nullsFirst === void 0 ? "" : options.nullsFirst ? ".nullsfirst" : ".nullslast"}`;
296
+ const existing = this.params.get(key);
297
+ this.params.set(key, existing ? `${existing},${term}` : term);
298
+ return this;
299
+ }
300
+ limit(count, options = {}) {
301
+ this.params.set(options.referencedTable ? `${options.referencedTable}.limit` : "limit", String(count));
302
+ return this;
303
+ }
304
+ /** Rows `from` to `to`, both included, counting from 0. */
305
+ range(from, to, options = {}) {
306
+ const prefix = options.referencedTable ? `${options.referencedTable}.` : "";
307
+ this.params.set(`${prefix}offset`, String(from));
308
+ this.params.set(`${prefix}limit`, String(to - from + 1));
309
+ return this;
310
+ }
311
+ /** Exactly one row, else an error (PostgREST `PGRST116`). */
312
+ single() {
313
+ this.accept = "application/vnd.pgrst.object+json";
314
+ return this.retype();
315
+ }
316
+ /** One row or null; more than one is an error. */
317
+ maybeSingle() {
318
+ this.maybe = true;
319
+ return this.retype();
320
+ }
321
+ abortSignal(signal) {
322
+ this.signal = signal;
323
+ return this;
324
+ }
325
+ // --- running ----------------------------------------------------------------------------------------------------
326
+ /** The url this query will call (for logging and tests; holds filter values). */
327
+ toUrl() {
328
+ const query = this.params.toString();
329
+ return `${this.ctx.restUrl}/${this.path}${query ? `?${query}` : ""}`;
330
+ }
331
+ then(onfulfilled, onrejected) {
332
+ return this.execute().then(onfulfilled, onrejected);
333
+ }
334
+ /** Throws the error instead of returning it: `const rows = await db.from('todos').select().throwOnError()`. */
335
+ async throwOnError() {
336
+ const res = await this.execute();
337
+ if (res.error) throw res.error;
338
+ return res.data;
339
+ }
340
+ async execute() {
341
+ const headers = await this.ctx.headers(this.method, this.ctx.schema);
342
+ if (this.accept) headers.Accept = this.accept;
343
+ if (this.prefer.length) headers.Prefer = this.prefer.join(",");
344
+ if (this.body !== void 0) headers["Content-Type"] = "application/json";
345
+ let response;
346
+ try {
347
+ response = await this.ctx.fetch(this.toUrl(), {
348
+ method: this.method,
349
+ headers,
350
+ body: this.body === void 0 ? void 0 : JSON.stringify(this.body),
351
+ signal: this.signal
352
+ });
353
+ } catch (cause) {
354
+ const error = new DataApiError({ code: "network_error", message: cause instanceof Error ? cause.message : String(cause) });
355
+ return { data: null, error, count: null, status: 0, statusText: "" };
356
+ }
357
+ return parseResponse(response, this.maybe);
358
+ }
359
+ };
360
+ async function parseResponse(response, maybeSingle = false) {
361
+ const range = response.headers.get("content-range");
362
+ const total = range?.split("/")[1];
363
+ const count = total && total !== "*" ? Number(total) : null;
364
+ const text = response.status === 204 || response.status === 205 ? "" : await response.text();
365
+ const base = { count, status: response.status, statusText: response.statusText };
366
+ if (!response.ok) {
367
+ let body = {};
368
+ try {
369
+ body = text ? JSON.parse(text) : {};
370
+ } catch {
371
+ body = { message: text };
372
+ }
373
+ const nested = body.error ?? body;
374
+ const error = new DataApiError({
375
+ code: String(nested.code ?? response.status),
376
+ message: String(nested.message ?? response.statusText ?? "Request failed"),
377
+ details: nested.details ?? null,
378
+ hint: nested.hint ?? null,
379
+ status: response.status
380
+ });
381
+ return { data: null, error, ...base };
382
+ }
383
+ let data = null;
384
+ if (text) {
385
+ try {
386
+ data = JSON.parse(text);
387
+ } catch {
388
+ data = text;
389
+ }
390
+ }
391
+ if (maybeSingle && Array.isArray(data)) {
392
+ if (data.length > 1) {
393
+ const error = new DataApiError({ code: "PGRST116", message: `Expected at most one row, got ${data.length}`, status: 406 });
394
+ return { data: null, error, ...base, status: 406 };
395
+ }
396
+ data = data[0] ?? null;
397
+ }
398
+ return { data, error: null, ...base };
399
+ }
400
+
401
+ // src/client.ts
402
+ var KEY_PATTERN = /^ifr_(pk|sk)_ap_[a-z0-9]{20}_/;
403
+ var SchemaClient = class {
404
+ constructor(base, schemaName) {
405
+ this.base = base;
406
+ this.schemaName = schemaName;
407
+ }
408
+ base;
409
+ schemaName;
410
+ from(relation) {
411
+ if (!relation || relation.includes("/") || relation.includes("?")) throw new TypeError(`Invalid table name: ${relation}`);
412
+ return new QueryBuilder({ ...this.base, schema: this.schemaName }, encodeURIComponent(relation));
413
+ }
414
+ async rpc(fn, args = {}, options = {}) {
415
+ const method = options.head ? "HEAD" : options.get ? "GET" : "POST";
416
+ const headers = await this.base.headers(method, this.schemaName);
417
+ if (options.count) headers.Prefer = `count=${options.count}`;
418
+ let url = `${this.base.restUrl}/rpc/${encodeURIComponent(fn)}`;
419
+ let body;
420
+ if (method === "POST") {
421
+ headers["Content-Type"] = "application/json";
422
+ body = JSON.stringify(args);
423
+ } else {
424
+ const params = new URLSearchParams();
425
+ for (const [key, value] of Object.entries(args)) params.append(key, String(value));
426
+ const query = params.toString();
427
+ if (query) url += `?${query}`;
428
+ }
429
+ try {
430
+ return await parseResponse(await this.base.fetch(url, { method, headers, body }));
431
+ } catch (cause) {
432
+ const error = new DataApiError({ code: "network_error", message: cause instanceof Error ? cause.message : String(cause) });
433
+ return { data: null, error, count: null, status: 0, statusText: "" };
434
+ }
435
+ }
436
+ };
437
+ var DataClient = class extends SchemaClient {
438
+ url;
439
+ context;
440
+ constructor(url, key, options = {}) {
441
+ const base = url.replace(/\/+$/, "").replace(/\/rest\/v1$/, "");
442
+ if (!/^https?:\/\//.test(base)) throw new TypeError("The Data API url starts with https://");
443
+ if (!key) throw new TypeError("Pass the publishable key (or, on a server, the secret key) of the auth project");
444
+ if (KEY_PATTERN.test(key) && key.startsWith("ifr_sk_") && typeof window !== "undefined" && typeof document !== "undefined") {
445
+ throw new TypeError("A secret key (ifr_sk_) must never be used in a browser; use the publishable key");
446
+ }
447
+ const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
448
+ const fixed = { ...options.headers ?? {} };
449
+ const context = {
450
+ restUrl: `${base}/rest/v1`,
451
+ fetch: fetcher,
452
+ headers: async (method, schema) => {
453
+ const token = options.accessToken ? await options.accessToken() : null;
454
+ const headers = { ...fixed, apikey: key, Authorization: `Bearer ${token ?? key}` };
455
+ if (schema) headers[method === "GET" || method === "HEAD" ? "Accept-Profile" : "Content-Profile"] = schema;
456
+ return headers;
457
+ }
458
+ };
459
+ super(context, options.schema ?? null);
460
+ this.url = base;
461
+ this.context = context;
462
+ }
463
+ /**
464
+ * A GraphQL query or mutation (`<url>/graphql/v1`), with the same key and token as REST. Pass a document of
465
+ * graphql-codegen for typed data and variables: `const { data } = await db.graphql(TodosDocument, { first: 10 })`.
466
+ */
467
+ graphql(document2, variables, options) {
468
+ return runGraphql(this.context, this.url, document2, variables, options);
469
+ }
470
+ /** Requests in another exposed schema. */
471
+ schema(name) {
472
+ return new SchemaClient(this.context, name);
473
+ }
474
+ };
475
+ function createClient(input, key, options = {}) {
476
+ if (typeof input !== "string") return new DataClient(input.url, input.key, input);
477
+ if (!key) throw new Error("A key is required");
478
+ return new DataClient(input, key, options);
479
+ }
480
+
481
+ // src/index.ts
482
+ var import_hosting_sdk2 = __toESM(require_dist(), 1);
483
+ // Annotate the CommonJS export names for ESM import in node:
484
+ 0 && (module.exports = {
485
+ DataApiError,
486
+ DataClient,
487
+ InfarisError,
488
+ QueryBuilder,
489
+ SchemaClient,
490
+ createClient,
491
+ quoteValue
492
+ });