@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/dist/index.js ADDED
@@ -0,0 +1,473 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __commonJS = (cb, mod) => function __require() {
8
+ try {
9
+ return mod || (0, cb[__getOwnPropNames(cb)[0]])((mod = { exports: {} }).exports, mod), mod.exports;
10
+ } catch (e) {
11
+ throw mod = 0, e;
12
+ }
13
+ };
14
+ var __copyProps = (to, from, except, desc) => {
15
+ if (from && typeof from === "object" || typeof from === "function") {
16
+ for (let key of __getOwnPropNames(from))
17
+ if (!__hasOwnProp.call(to, key) && key !== except)
18
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
19
+ }
20
+ return to;
21
+ };
22
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
23
+ // If the importer is in node compatibility mode or this is not an ESM
24
+ // file that has been converted to a CommonJS file using a Babel-
25
+ // compatible transform (i.e. "__esModule" has not been set), then set
26
+ // "default" to the CommonJS "module.exports" for node compatibility.
27
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
28
+ mod
29
+ ));
30
+
31
+ // ../hosting-sdk/dist/index.cjs
32
+ var require_dist = __commonJS({
33
+ "../hosting-sdk/dist/index.cjs"(exports, module) {
34
+ "use strict";
35
+ var __defProp2 = Object.defineProperty;
36
+ var __getOwnPropDesc2 = Object.getOwnPropertyDescriptor;
37
+ var __getOwnPropNames2 = Object.getOwnPropertyNames;
38
+ var __hasOwnProp2 = Object.prototype.hasOwnProperty;
39
+ var __export = (target, all) => {
40
+ for (var name in all)
41
+ __defProp2(target, name, { get: all[name], enumerable: true });
42
+ };
43
+ var __copyProps2 = (to, from, except, desc) => {
44
+ if (from && typeof from === "object" || typeof from === "function") {
45
+ for (let key of __getOwnPropNames2(from))
46
+ if (!__hasOwnProp2.call(to, key) && key !== except)
47
+ __defProp2(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc2(from, key)) || desc.enumerable });
48
+ }
49
+ return to;
50
+ };
51
+ var __toCommonJS = (mod) => __copyProps2(__defProp2({}, "__esModule", { value: true }), mod);
52
+ var index_exports = {};
53
+ __export(index_exports, {
54
+ InfarisError: () => InfarisError3
55
+ });
56
+ module.exports = __toCommonJS(index_exports);
57
+ var errorBrand = /* @__PURE__ */ Symbol.for("@infaris/hosting-sdk/error");
58
+ var InfarisError3 = class _InfarisError extends Error {
59
+ // SDK bundles contain separate copies; catch the shared base across bundles and module formats.
60
+ static [Symbol.hasInstance](value) {
61
+ if (this !== _InfarisError) return Function.prototype[Symbol.hasInstance].call(this, value);
62
+ return value instanceof Error && errorBrand in value && value[errorBrand] === true;
63
+ }
64
+ code;
65
+ status;
66
+ constructor(message, code = "sdk_error", status = null) {
67
+ super(message);
68
+ Object.defineProperty(this, errorBrand, { value: true });
69
+ this.name = "InfarisError";
70
+ this.code = code;
71
+ this.status = status;
72
+ }
73
+ };
74
+ }
75
+ });
76
+
77
+ // src/graphql.ts
78
+ async function runGraphql(context, baseUrl, document2, variables, options = {}) {
79
+ const headers = await context.headers("POST", null);
80
+ headers["Content-Type"] = "application/json";
81
+ headers.Accept = "application/json";
82
+ if (options.schema) headers["Accept-Profile"] = options.schema;
83
+ const hash = typeof document2 === "string" ? void 0 : document2.__meta__?.hash;
84
+ const body = {};
85
+ if (options.persisted) {
86
+ if (!hash) throw new TypeError("persisted needs a document with __meta__.hash (graphql-codegen persistedDocuments: true)");
87
+ body.documentId = `sha256:${hash.replace(/^sha256:/, "")}`;
88
+ } else body.query = String(document2);
89
+ if (variables && Object.keys(variables).length) body.variables = variables;
90
+ if (options.operationName) body.operationName = options.operationName;
91
+ let response;
92
+ try {
93
+ response = await context.fetch(`${baseUrl}/graphql/v1`, { method: "POST", headers, body: JSON.stringify(body) });
94
+ } catch (cause) {
95
+ return { data: null, errors: [{ message: cause instanceof Error ? cause.message : String(cause), extensions: { code: "network_error" } }], status: 0 };
96
+ }
97
+ let parsed = {};
98
+ try {
99
+ parsed = await response.json();
100
+ } catch {
101
+ }
102
+ if (!parsed.errors && parsed.code && parsed.message) {
103
+ parsed.errors = [{ message: parsed.message, extensions: { code: parsed.code } }];
104
+ }
105
+ return { data: parsed.data ?? null, errors: parsed.errors?.length ? parsed.errors : null, status: response.status };
106
+ }
107
+
108
+ // src/types.ts
109
+ var import_hosting_sdk = __toESM(require_dist(), 1);
110
+ var DataApiError = class extends import_hosting_sdk.InfarisError {
111
+ code;
112
+ details;
113
+ hint;
114
+ status;
115
+ constructor(input) {
116
+ super(input.message, input.code, input.status);
117
+ this.name = "DataApiError";
118
+ this.code = input.code;
119
+ this.details = input.details ?? null;
120
+ this.hint = input.hint ?? null;
121
+ this.status = input.status ?? null;
122
+ }
123
+ };
124
+
125
+ // src/query.ts
126
+ function quoteValue(value) {
127
+ if (value === null) return "null";
128
+ const text = String(value);
129
+ return /[,()":\s\\{}]/.test(text) ? `"${text.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"` : text;
130
+ }
131
+ function containerValue(value) {
132
+ if (typeof value === "string") return value;
133
+ if (Array.isArray(value)) return `{${value.map(quoteValue).join(",")}}`;
134
+ return JSON.stringify(value);
135
+ }
136
+ var QueryBuilder = class {
137
+ constructor(ctx, path) {
138
+ this.ctx = ctx;
139
+ this.path = path;
140
+ }
141
+ ctx;
142
+ path;
143
+ method = "GET";
144
+ params = new URLSearchParams();
145
+ prefer = [];
146
+ body = void 0;
147
+ accept = null;
148
+ maybe = false;
149
+ signal;
150
+ retype() {
151
+ return this;
152
+ }
153
+ /** Overrides the result type when you know better (a view without types, a computed column). */
154
+ returns() {
155
+ return this.retype();
156
+ }
157
+ // --- operations -------------------------------------------------------------------------------------------------
158
+ /** Read rows (`GET`), or with a mutation before it, return the changed rows. `head: true` only counts. */
159
+ select(columns, options = {}) {
160
+ const cleaned = (columns ?? "*").replace(/\s+(?=([^"]*"[^"]*")*[^"]*$)/g, "");
161
+ this.params.set("select", cleaned);
162
+ if (this.method === "GET" || this.method === "HEAD") {
163
+ if (options.head) this.method = "HEAD";
164
+ } else {
165
+ this.prefer.push("return=representation");
166
+ }
167
+ if (options.count) this.prefer.push(`count=${options.count}`);
168
+ return this.retype();
169
+ }
170
+ insert(values, options = {}) {
171
+ this.method = "POST";
172
+ this.body = values;
173
+ if (Array.isArray(values)) this.columns(values);
174
+ if (options.defaultToNull === false) this.prefer.push("missing=default");
175
+ if (options.count) this.prefer.push(`count=${options.count}`);
176
+ return this.retype();
177
+ }
178
+ /** Insert or, on a conflict with `onConflict` (default the primary key), update (or skip with `ignoreDuplicates`). */
179
+ upsert(values, options = {}) {
180
+ this.method = "POST";
181
+ this.body = values;
182
+ this.prefer.push(`resolution=${options.ignoreDuplicates ? "ignore" : "merge"}-duplicates`);
183
+ if (options.onConflict) this.params.set("on_conflict", options.onConflict);
184
+ if (Array.isArray(values)) this.columns(values);
185
+ if (options.defaultToNull === false) this.prefer.push("missing=default");
186
+ if (options.count) this.prefer.push(`count=${options.count}`);
187
+ return this.retype();
188
+ }
189
+ update(values, options = {}) {
190
+ this.method = "PATCH";
191
+ this.body = values;
192
+ if (options.count) this.prefer.push(`count=${options.count}`);
193
+ return this.retype();
194
+ }
195
+ delete(options = {}) {
196
+ this.method = "DELETE";
197
+ if (options.count) this.prefer.push(`count=${options.count}`);
198
+ return this.retype();
199
+ }
200
+ /** `columns` for a bulk insert, so rows that leave a column out get its default instead of null. */
201
+ columns(rows) {
202
+ const keys = /* @__PURE__ */ new Set();
203
+ for (const row of rows) for (const key of Object.keys(row)) keys.add(key);
204
+ if (keys.size) this.params.set("columns", [...keys].map((k) => `"${k}"`).join(","));
205
+ }
206
+ // --- filters ----------------------------------------------------------------------------------------------------
207
+ /** Any PostgREST filter: `filter('age', 'gte', 18)`, `filter('tags', 'cs', '{a,b}')`. */
208
+ filter(column, operator, value) {
209
+ this.params.append(column, `${operator}.${value === null ? "null" : String(value)}`);
210
+ return this;
211
+ }
212
+ eq(column, value) {
213
+ return this.filter(column, "eq", value);
214
+ }
215
+ neq(column, value) {
216
+ return this.filter(column, "neq", value);
217
+ }
218
+ gt(column, value) {
219
+ return this.filter(column, "gt", value);
220
+ }
221
+ gte(column, value) {
222
+ return this.filter(column, "gte", value);
223
+ }
224
+ lt(column, value) {
225
+ return this.filter(column, "lt", value);
226
+ }
227
+ lte(column, value) {
228
+ return this.filter(column, "lte", value);
229
+ }
230
+ like(column, pattern) {
231
+ return this.filter(column, "like", pattern);
232
+ }
233
+ ilike(column, pattern) {
234
+ return this.filter(column, "ilike", pattern);
235
+ }
236
+ /** `IS NULL`, `IS TRUE`, `IS FALSE`. */
237
+ is(column, value) {
238
+ return this.filter(column, "is", value);
239
+ }
240
+ in(column, values) {
241
+ return this.filter(column, "in", `(${values.map(quoteValue).join(",")})`);
242
+ }
243
+ contains(column, value) {
244
+ return this.filter(column, "cs", containerValue(value));
245
+ }
246
+ containedBy(column, value) {
247
+ return this.filter(column, "cd", containerValue(value));
248
+ }
249
+ overlaps(column, value) {
250
+ return this.filter(column, "ov", containerValue(value));
251
+ }
252
+ /**
253
+ * Full-text search on a tsvector column (`infaris-guard hosting db search add` makes one):
254
+ * `textSearch('fts', 'fiets bel', { type: 'websearch', config: 'dutch' })`.
255
+ */
256
+ textSearch(column, query, options = {}) {
257
+ const op = options.type === "plain" ? "plfts" : options.type === "phrase" ? "phfts" : options.type === "websearch" ? "wfts" : "fts";
258
+ return this.filter(column, options.config ? `${op}(${options.config})` : op, query);
259
+ }
260
+ /** Several equalities at once. */
261
+ match(query) {
262
+ for (const [column, value] of Object.entries(query)) this.filter(column, "eq", value);
263
+ return this;
264
+ }
265
+ /** Negate a filter: `not('status', 'eq', 'done')`. */
266
+ not(column, operator, value) {
267
+ return this.filter(column, `not.${operator}`, value);
268
+ }
269
+ /** Raw PostgREST `or`: `or('status.eq.open,priority.gt.3')`; `referencedTable` filters an embedded table. */
270
+ or(filters, options = {}) {
271
+ this.params.append(options.referencedTable ? `${options.referencedTable}.or` : "or", `(${filters})`);
272
+ return this;
273
+ }
274
+ order(column, options = {}) {
275
+ const key = options.referencedTable ? `${options.referencedTable}.order` : "order";
276
+ const term = `${column}.${options.ascending === false ? "desc" : "asc"}${options.nullsFirst === void 0 ? "" : options.nullsFirst ? ".nullsfirst" : ".nullslast"}`;
277
+ const existing = this.params.get(key);
278
+ this.params.set(key, existing ? `${existing},${term}` : term);
279
+ return this;
280
+ }
281
+ limit(count, options = {}) {
282
+ this.params.set(options.referencedTable ? `${options.referencedTable}.limit` : "limit", String(count));
283
+ return this;
284
+ }
285
+ /** Rows `from` to `to`, both included, counting from 0. */
286
+ range(from, to, options = {}) {
287
+ const prefix = options.referencedTable ? `${options.referencedTable}.` : "";
288
+ this.params.set(`${prefix}offset`, String(from));
289
+ this.params.set(`${prefix}limit`, String(to - from + 1));
290
+ return this;
291
+ }
292
+ /** Exactly one row, else an error (PostgREST `PGRST116`). */
293
+ single() {
294
+ this.accept = "application/vnd.pgrst.object+json";
295
+ return this.retype();
296
+ }
297
+ /** One row or null; more than one is an error. */
298
+ maybeSingle() {
299
+ this.maybe = true;
300
+ return this.retype();
301
+ }
302
+ abortSignal(signal) {
303
+ this.signal = signal;
304
+ return this;
305
+ }
306
+ // --- running ----------------------------------------------------------------------------------------------------
307
+ /** The url this query will call (for logging and tests; holds filter values). */
308
+ toUrl() {
309
+ const query = this.params.toString();
310
+ return `${this.ctx.restUrl}/${this.path}${query ? `?${query}` : ""}`;
311
+ }
312
+ then(onfulfilled, onrejected) {
313
+ return this.execute().then(onfulfilled, onrejected);
314
+ }
315
+ /** Throws the error instead of returning it: `const rows = await db.from('todos').select().throwOnError()`. */
316
+ async throwOnError() {
317
+ const res = await this.execute();
318
+ if (res.error) throw res.error;
319
+ return res.data;
320
+ }
321
+ async execute() {
322
+ const headers = await this.ctx.headers(this.method, this.ctx.schema);
323
+ if (this.accept) headers.Accept = this.accept;
324
+ if (this.prefer.length) headers.Prefer = this.prefer.join(",");
325
+ if (this.body !== void 0) headers["Content-Type"] = "application/json";
326
+ let response;
327
+ try {
328
+ response = await this.ctx.fetch(this.toUrl(), {
329
+ method: this.method,
330
+ headers,
331
+ body: this.body === void 0 ? void 0 : JSON.stringify(this.body),
332
+ signal: this.signal
333
+ });
334
+ } catch (cause) {
335
+ const error = new DataApiError({ code: "network_error", message: cause instanceof Error ? cause.message : String(cause) });
336
+ return { data: null, error, count: null, status: 0, statusText: "" };
337
+ }
338
+ return parseResponse(response, this.maybe);
339
+ }
340
+ };
341
+ async function parseResponse(response, maybeSingle = false) {
342
+ const range = response.headers.get("content-range");
343
+ const total = range?.split("/")[1];
344
+ const count = total && total !== "*" ? Number(total) : null;
345
+ const text = response.status === 204 || response.status === 205 ? "" : await response.text();
346
+ const base = { count, status: response.status, statusText: response.statusText };
347
+ if (!response.ok) {
348
+ let body = {};
349
+ try {
350
+ body = text ? JSON.parse(text) : {};
351
+ } catch {
352
+ body = { message: text };
353
+ }
354
+ const nested = body.error ?? body;
355
+ const error = new DataApiError({
356
+ code: String(nested.code ?? response.status),
357
+ message: String(nested.message ?? response.statusText ?? "Request failed"),
358
+ details: nested.details ?? null,
359
+ hint: nested.hint ?? null,
360
+ status: response.status
361
+ });
362
+ return { data: null, error, ...base };
363
+ }
364
+ let data = null;
365
+ if (text) {
366
+ try {
367
+ data = JSON.parse(text);
368
+ } catch {
369
+ data = text;
370
+ }
371
+ }
372
+ if (maybeSingle && Array.isArray(data)) {
373
+ if (data.length > 1) {
374
+ const error = new DataApiError({ code: "PGRST116", message: `Expected at most one row, got ${data.length}`, status: 406 });
375
+ return { data: null, error, ...base, status: 406 };
376
+ }
377
+ data = data[0] ?? null;
378
+ }
379
+ return { data, error: null, ...base };
380
+ }
381
+
382
+ // src/client.ts
383
+ var KEY_PATTERN = /^ifr_(pk|sk)_ap_[a-z0-9]{20}_/;
384
+ var SchemaClient = class {
385
+ constructor(base, schemaName) {
386
+ this.base = base;
387
+ this.schemaName = schemaName;
388
+ }
389
+ base;
390
+ schemaName;
391
+ from(relation) {
392
+ if (!relation || relation.includes("/") || relation.includes("?")) throw new TypeError(`Invalid table name: ${relation}`);
393
+ return new QueryBuilder({ ...this.base, schema: this.schemaName }, encodeURIComponent(relation));
394
+ }
395
+ async rpc(fn, args = {}, options = {}) {
396
+ const method = options.head ? "HEAD" : options.get ? "GET" : "POST";
397
+ const headers = await this.base.headers(method, this.schemaName);
398
+ if (options.count) headers.Prefer = `count=${options.count}`;
399
+ let url = `${this.base.restUrl}/rpc/${encodeURIComponent(fn)}`;
400
+ let body;
401
+ if (method === "POST") {
402
+ headers["Content-Type"] = "application/json";
403
+ body = JSON.stringify(args);
404
+ } else {
405
+ const params = new URLSearchParams();
406
+ for (const [key, value] of Object.entries(args)) params.append(key, String(value));
407
+ const query = params.toString();
408
+ if (query) url += `?${query}`;
409
+ }
410
+ try {
411
+ return await parseResponse(await this.base.fetch(url, { method, headers, body }));
412
+ } catch (cause) {
413
+ const error = new DataApiError({ code: "network_error", message: cause instanceof Error ? cause.message : String(cause) });
414
+ return { data: null, error, count: null, status: 0, statusText: "" };
415
+ }
416
+ }
417
+ };
418
+ var DataClient = class extends SchemaClient {
419
+ url;
420
+ context;
421
+ constructor(url, key, options = {}) {
422
+ const base = url.replace(/\/+$/, "").replace(/\/rest\/v1$/, "");
423
+ if (!/^https?:\/\//.test(base)) throw new TypeError("The Data API url starts with https://");
424
+ if (!key) throw new TypeError("Pass the publishable key (or, on a server, the secret key) of the auth project");
425
+ if (KEY_PATTERN.test(key) && key.startsWith("ifr_sk_") && typeof window !== "undefined" && typeof document !== "undefined") {
426
+ throw new TypeError("A secret key (ifr_sk_) must never be used in a browser; use the publishable key");
427
+ }
428
+ const fetcher = options.fetch ?? ((input, init) => fetch(input, init));
429
+ const fixed = { ...options.headers ?? {} };
430
+ const context = {
431
+ restUrl: `${base}/rest/v1`,
432
+ fetch: fetcher,
433
+ headers: async (method, schema) => {
434
+ const token = options.accessToken ? await options.accessToken() : null;
435
+ const headers = { ...fixed, apikey: key, Authorization: `Bearer ${token ?? key}` };
436
+ if (schema) headers[method === "GET" || method === "HEAD" ? "Accept-Profile" : "Content-Profile"] = schema;
437
+ return headers;
438
+ }
439
+ };
440
+ super(context, options.schema ?? null);
441
+ this.url = base;
442
+ this.context = context;
443
+ }
444
+ /**
445
+ * A GraphQL query or mutation (`<url>/graphql/v1`), with the same key and token as REST. Pass a document of
446
+ * graphql-codegen for typed data and variables: `const { data } = await db.graphql(TodosDocument, { first: 10 })`.
447
+ */
448
+ graphql(document2, variables, options) {
449
+ return runGraphql(this.context, this.url, document2, variables, options);
450
+ }
451
+ /** Requests in another exposed schema. */
452
+ schema(name) {
453
+ return new SchemaClient(this.context, name);
454
+ }
455
+ };
456
+ function createClient(input, key, options = {}) {
457
+ if (typeof input !== "string") return new DataClient(input.url, input.key, input);
458
+ if (!key) throw new Error("A key is required");
459
+ return new DataClient(input, key, options);
460
+ }
461
+
462
+ // src/index.ts
463
+ var import_hosting_sdk2 = __toESM(require_dist(), 1);
464
+ var export_InfarisError = import_hosting_sdk2.InfarisError;
465
+ export {
466
+ DataApiError,
467
+ DataClient,
468
+ export_InfarisError as InfarisError,
469
+ QueryBuilder,
470
+ SchemaClient,
471
+ createClient,
472
+ quoteValue
473
+ };
package/package.json CHANGED
@@ -1,6 +1,67 @@
1
1
  {
2
2
  "name": "@infaris/data",
3
- "version": "0.0.0-stage",
4
- "stub": true,
5
- "description": "Temporary package placeholder for staged publishing"
6
- }
3
+ "version": "0.1.0",
4
+ "description": "JavaScript client for the Data API of Hosting by Infaris: REST over managed Postgres with Row Level Security and Infaris Auth, hosted in the EU. Bundles the shared Infaris error contract, PostgREST query syntax.",
5
+ "type": "module",
6
+ "main": "./dist/index.cjs",
7
+ "module": "./dist/index.js",
8
+ "types": "./dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "import": {
12
+ "types": "./dist/index.d.ts",
13
+ "default": "./dist/index.js"
14
+ },
15
+ "require": {
16
+ "types": "./dist/index.d.cts",
17
+ "default": "./dist/index.cjs"
18
+ }
19
+ }
20
+ },
21
+ "files": [
22
+ "dist",
23
+ "README.md",
24
+ "LICENSE"
25
+ ],
26
+ "sideEffects": false,
27
+ "scripts": {
28
+ "build": "tsup",
29
+ "prepublishOnly": "bun run build",
30
+ "typecheck": "tsgo --noEmit",
31
+ "typecheck:legacy": "tsc --noEmit",
32
+ "test": "bun test src",
33
+ "lint": "echo \"(no lint configured for data-js)\""
34
+ },
35
+ "devDependencies": {
36
+ "@graphql-codegen/cli": "7.4.6",
37
+ "@graphql-codegen/client-preset": "6.2.3",
38
+ "@graphql-typed-document-node/core": "3.2.0",
39
+ "@types/bun": "^1.3.14",
40
+ "graphql": "16.14.2",
41
+ "typescript": "5.9.3",
42
+ "tsup": "^8.5.1",
43
+ "@infaris/hosting-sdk": "0.1.0"
44
+ },
45
+ "keywords": [
46
+ "infaris",
47
+ "postgres",
48
+ "postgrest",
49
+ "rest",
50
+ "rls",
51
+ "database",
52
+ "eu"
53
+ ],
54
+ "license": "MIT",
55
+ "repository": {
56
+ "type": "git",
57
+ "url": "https://github.com/InfarisHQ/monorepo.git",
58
+ "directory": "packages/infaris-data-js"
59
+ },
60
+ "publishConfig": {
61
+ "access": "public",
62
+ "registry": "https://registry.npmjs.org"
63
+ },
64
+ "engines": {
65
+ "node": ">=22.12.0"
66
+ }
67
+ }